diff --git a/Android4.2兼容性改动文档.md b/Android4.2兼容性改动文档.md new file mode 100644 index 0000000..6742d21 --- /dev/null +++ b/Android4.2兼容性改动文档.md @@ -0,0 +1,156 @@ +# Android 4.2 兼容性改动文档 + +## 背景 + +项目 original 目标为 Android 5.0+,需降级兼容 Android 4.2(API 17)。Android 4.2 使用 Dalvik 虚拟机(非 ART),对 multidex 和高版本 API 引用有严格限制,导致一系列兼容性问题。 + +## 问题分类 + +兼容性问题分为两大类: + +1. **Dalvik multidex 类加载失败**:Dalvik 在框架级反射和内部类加载场景下,无法从 secondary dex 加载类,导致 `ClassNotFoundException` / `NoClassDefFoundError` / `VerifyError` +2. **高版本 API 引用**:第三方库的某些类引用了 Android 10+ 或 Java 9+ 的 API,Dalvik 验证器拒绝加载,导致 `VerifyError` + +--- + +## 一、Multidex 配置改动 + +### 1.1 multidex-config.pro(主 dex 保留类) + +**文件**:`app/multidex-config.pro` + +**原因**:Dalvik multidex 环境下,以下场景无法从 secondary dex 加载类: +- `LayoutInflater.createViewFromTag`(框架级反射创建 View) +- `AppCompatDelegateImpl.createSubDecor`(inflate 内部装饰布局) +- 协程内部机制加载 `CoroutineExceptionHandlerImplKt` +- Retrofit `KotlinExtensions.await()` 被 `HttpServiceMethod` 运行时调用 + +**保留到主 dex 的包**: + +``` +- com.pukka.mediaapp.** # 项目自身所有类 +- androidx.appcompat.app.** # AppCompatDelegateImpl、AppCompatViewInflater +- androidx.appcompat.widget.** # FitWindowsLinearLayout、AppCompatButton 等 +- kotlinx.coroutines.** # 协程核心类 +- retrofit2.** # KotlinExtensions、HttpServiceMethod 等 +``` + +### 1.2 AndroidManifest.xml + +**文件**:`app/src/main/AndroidManifest.xml` + +**改动**: +- 移除 `com.google.android.exoplayer2` 的 `overrideLibrary`(已移除 ExoPlayer 依赖) +- 保留其他 AndroidX 库的 `overrideLibrary`(core-ktx 1.12.0、appcompat 1.6.1 等的 minSdk 为 19-21,通过 override 覆盖为 17) +- 移除 `androidx.startup.InitializationProvider`(Android 4.x Dalvik multidex 下 Provider 启动时无法加载 secondary dex 中的初始化类,App 手动初始化所有组件) + +--- + +## 二、第三方库版本降级 + +### 2.1 OkHttp:3.14.9 → 3.12.0 + +**问题**:OkHttp 3.12.1+ 包含 `Android10Platform` 类,引用 Android 10+ 的 SSL API(`SSLParameters.setApplicationProtocols`)。`Platform.findAndroidPlatform()` 在静态初始化时尝试加载该类,Dalvik 验证器拒绝 → `VerifyError` + +**修复**: +```groovy +implementation 'com.squareup.okhttp3:okhttp:3.12.0' +implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0' +// 防止其他依赖传递引入 3.12.1+ +configurations.all { + resolutionStrategy.force 'com.squareup.okhttp3:okhttp:3.12.0' +} +``` + +### 2.2 Gson:2.10.1 → 2.8.6 + +**问题**:Gson 2.9+ 引入 `ReflectionHelper` 类,引用 Java 9+ 的反射 API(`java.lang.invoke.MethodHandles`)。`ReflectiveTypeAdapterFactory.create()` 调用该类,Dalvik 验证器拒绝 → `VerifyError` + +**修复**: +```groovy +implementation 'com.google.code.gson:gson:2.8.6' +``` + +### 2.3 Retrofit:2.9.0 → 2.6.0 + +**问题**:Retrofit 2.9.0 传递依赖 OkHttp 3.14.9(含 `Android10Platform`)。与客户环境保持一致使用 2.6.0。 + +**修复**: +```groovy +implementation 'com.squareup.retrofit2:retrofit:2.6.0' +implementation 'com.squareup.retrofit2:converter-gson:2.6.0' +``` + +### 2.4 Kotlin Coroutines:1.7.3 → 1.6.4 + +**问题**:kotlinx-coroutines 1.7+ 要求 minSdk 21(Android 5.0),1.6.4 兼容 API 16+。 + +**修复**: +```groovy +implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' +``` + +--- + +## 三、ExoPlayer 移除,改用系统原生 MediaPlayer + +**问题**:ExoPlayer 2.14+ 要求 minSdk 21,2.19.1 的类大量引用 Android 5+ API,在 Android 4.2 上会 `VerifyError`。即使降到 2.13.0,API 差异大且仍有兼容性风险。 + +**修复**:完全移除 ExoPlayer,改用 Android 系统原生 `MediaPlayer` + `SurfaceView`(API 1+,零兼容性问题)。 + +**涉及文件**: + +| 文件 | 改动 | +|------|------| +| `app/build.gradle` | 移除 `exoplayer:2.19.1` 依赖 | +| `app/src/main/AndroidManifest.xml` | 移除 `com.google.android.exoplayer2` overrideLibrary | +| `app/src/main/res/layout/activity_player.xml` | `PlayerView` → `SurfaceView` | +| `app/src/main/java/.../ui/VideoPlayerActivity.kt` | 重写为 `MediaPlayer` + `SurfaceHolder.Callback` | + +--- + +## 四、主题改动 + +**文件**:`app/src/main/res/values/themes.xml` + +**改动**:`Theme.MaterialComponents.NoActionBar` → `Theme.AppCompat.NoActionBar` + +**原因**: +- `MaterialComponentsViewInflater` 继承 `AppCompatViewInflater`,被框架反射加载时需在主 dex +- 保留整个 `com.google.android.material.**` 到主 dex 会导致方法数超限(80949 > 65536) +- 布局中只用了标准控件(TextView/EditText/Button/ProgressBar),无需 Material 主题 +- 改用 AppCompat 主题后,使用 `AppCompatViewInflater`,避免 Material 的 multidex 问题 + +--- + +## 五、build.gradle 完整依赖对照 + +| 依赖 | 修复前 | 修复后 | 原因 | +|------|--------|--------|------| +| retrofit | 2.9.0 | **2.6.0** | 传递依赖 OkHttp 3.14.9 不兼容 | +| converter-gson | 2.9.0 | **2.6.0** | 同上 | +| okhttp | 3.14.9(传递) | **3.12.0**(force) | Android10Platform VerifyError | +| logging-interceptor | 3.12.12 | **3.12.0** | 同上 | +| gson | 2.10.1 | **2.8.6** | ReflectionHelper VerifyError | +| coroutines | 1.7.3 | **1.6.4** | 1.7+ 要求 minSdk 21 | +| exoplayer | 2.19.1 | **移除** | minSdk 21,改用 MediaPlayer | +| appcompat | 1.6.1 | 1.6.1(不变) | overrideLibrary 覆盖 | +| core-ktx | 1.12.0 | 1.12.0(不变) | overrideLibrary 覆盖 | +| material | 1.11.0 | 1.11.0(不变) | 改用 AppCompat 主题 | +| lifecycle | 2.3.1 | 2.3.1(不变) | 兼容 API 16+ | +| activity | 1.3.1 | 1.3.1(不变) | 兼容 API 16+ | +| multidex | 2.0.1 | 2.0.1(不变) | 兼容 API 16+ | + +--- + +## 六、问题排查记录(按发现顺序) + +| 序号 | 错误 | 根因 | 修复 | +|------|------|------|------| +| 1 | `VerifyError: AppCompatViewInflater` | Dalvik multidex 无法加载 secondary dex 的 AppCompat 类 | multidex-config.pro 保留 `androidx.appcompat.**` | +| 2 | `ClassNotFoundException: FitWindowsLinearLayout` | AppCompat 内部 widget 不以 `AppCompat` 开头,通配符未覆盖 | 改用包级别 `androidx.appcompat.widget.**` | +| 3 | `NoClassDefFoundError: CoroutineExceptionHandlerImplKt` | 协程内部类加载失败 | multidex-config.pro 保留 `kotlinx.coroutines.**` | +| 4 | `VerifyError: Android10Platform` | OkHttp 3.12.1+ 引用 Android 10+ API | OkHttp 降级 3.12.0 + force 锁定 | +| 5 | `VerifyError: ReflectionHelper` | Gson 2.9+ 引用 Java 9+ API | Gson 降级 2.8.6 | +| 6 | `NoClassDefFoundError: retrofit2.KotlinExtensions` | Retrofit suspend 支持类在 secondary dex | multidex-config.pro 保留 `retrofit2.**` | +| 7 | 主 dex 方法数超限 80949 > 65536 | 保留 material.** 到主 dex | 改用 AppCompat 主题,移除 material 保留 | diff --git a/app/build.gradle b/app/build.gradle index 02d3697..49446f3 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -72,23 +72,25 @@ dependencies { // multidex implementation 'androidx.multidex:multidex:2.0.1' - // Retrofit + OkHttp(3.12.x 兼容 API 16+) - implementation 'com.squareup.retrofit2:retrofit:2.9.0' - implementation 'com.squareup.retrofit2:converter-gson:2.9.0' - implementation 'com.squareup.okhttp3:logging-interceptor:3.12.12' + // Retrofit 2.6.0 + OkHttp 3.12.0(与客户环境一致,兼容 Android 4.x) + // Retrofit 2.6.0 自然依赖 OkHttp 3.12.0(无 Android10Platform,不会 VerifyError) + // OkHttp 3.12.1+ 包含 Android10Platform,引用 Android 10+ API,在 Android 4.x 上 VerifyError + implementation 'com.squareup.retrofit2:retrofit:2.6.0' + implementation 'com.squareup.retrofit2:converter-gson:2.6.0' + implementation 'com.squareup.okhttp3:okhttp:3.12.0' + implementation 'com.squareup.okhttp3:logging-interceptor:3.12.0' - // Gson - implementation 'com.google.code.gson:gson:2.10.1' + // Gson 2.8.6(2.9+ 引入 ReflectionHelper,引用 Java 9+ API,在 Android 4.x 上 VerifyError) + implementation 'com.google.code.gson:gson:2.8.6' - // Coroutines - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3' + // Coroutines 1.6.4(1.7+ 要求 minSdk 21,1.6.4 兼容 API 16+) + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4' // JetsenxSdk + NewTVSDK implementation files('libs/JetsenxSdk-api-future-release-v1.23.aar') implementation files('libs/NewTVSDK-v4.0.3-app-lite.jar') - // ExoPlayer - implementation 'com.google.android.exoplayer:exoplayer:2.19.1' + // 视频播放使用系统原生 MediaPlayer + SurfaceView,无需 ExoPlayer // commons-codec(SDK 依赖) implementation 'commons-codec:commons-codec:1.15' @@ -103,3 +105,9 @@ dependencies { implementation 'net.jpountz.lz4:lz4:1.3.0' implementation 'org.slf4j:slf4j-android:1.7.36' } + +// 强制 OkHttp 3.12.0(3.12.1+ 包含 Android10Platform,引用 Android 10+ API,在 Android 4.x 上 VerifyError) +// 其他依赖传递引入 3.12.13,需 force 锁定 3.12.0 +configurations.all { + resolutionStrategy.force 'com.squareup.okhttp3:okhttp:3.12.0' +} diff --git a/app/multidex-config.pro b/app/multidex-config.pro index 9c64ac3..e87f17c 100644 --- a/app/multidex-config.pro +++ b/app/multidex-config.pro @@ -1,6 +1,28 @@ # Android 4.x Dalvik multidex 兼容配置 # InitializationProvider 已从 manifest 移除,仅需保留 App 自身启动类 +# +# 核心原则:Dalvik multidex 下,只有以下两种场景无法加载 secondary dex 中的类 +# 1. Android 框架级反射:LayoutInflater.createView 使用框架 ClassLoader +# 创建 View 时找不到 secondary dex 中的 AppCompat 控件类 +# 2. 协程内部类加载:kotlinx.coroutines 内部加载 CoroutineExceptionHandlerImplKt +# 等核心类时,在 Dalvik multidex 下失败 +# 注意:已将主题从 MaterialComponents 改为 AppCompat,避免 MaterialComponentsViewInflater +# 的 multidex 类加载问题,同时减少主 dex 方法数(material 包约 20000+ 方法) -# App 自身核心类 --keep class com.pukka.mediaapp.PartnerApp { *; } --keep class com.pukka.mediaapp.ui.PartnerLaunchActivity { *; } +# === 项目自身所有类 === +-keep class com.pukka.mediaapp.** { *; } + +# === AppCompat 完整包(app + widget)=== +# LayoutInflater.createViewFromTag 反射创建:FitWindowsLinearLayout、AppCompatButton 等 +# AppCompatDelegateImpl.createSubDecor inflate 内部装饰布局 +-keep class androidx.appcompat.app.** { *; } +-keep class androidx.appcompat.widget.** { *; } + +# === Kotlin Coroutines === +# 协程启动时内部加载 CoroutineExceptionHandlerImplKt(无法从 secondary dex 加载) +# 覆盖 flow(StateFlow)、android(Dispatchers)、internal 等 +-keep class kotlinx.coroutines.** { *; } + +# === Retrofit === +# KotlinExtensions.await() 被 HttpServiceMethod 在运行时调用,suspend 函数支持 +-keep class retrofit2.** { *; } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f683ba2..de945d0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -9,7 +9,6 @@ showLoading(false) - Player.STATE_BUFFERING -> showLoading(true) - Player.STATE_ENDED -> showLoading(false) - Player.STATE_IDLE -> showLoading(false) - } + val surfaceView = binding.surfaceView + surfaceView.holder.addCallback(object : SurfaceHolder.Callback { + override fun surfaceCreated(holder: SurfaceHolder) { + Log.d(TAG, "surfaceCreated") + surfaceReady = true + startPlayback() } - override fun onPlayerError(error: com.google.android.exoplayer2.PlaybackException) { - Log.e(TAG, "onPlayerError", error) - showLoading(false) - showError("播放错误: ${error.message}") + override fun surfaceChanged(holder: SurfaceHolder, format: Int, width: Int, height: Int) {} + + override fun surfaceDestroyed(holder: SurfaceHolder) { + Log.d(TAG, "surfaceDestroyed") + surfaceReady = false + mediaPlayer?.release() + mediaPlayer = null } }) } + private fun startPlayback() { + if (!surfaceReady) return + + try { + mediaPlayer = MediaPlayer().apply { + setDataSource(this@VideoPlayerActivity, Uri.parse(videoUrl)) + setDisplay(binding.surfaceView.holder) + + setOnPreparedListener { mp -> + Log.d(TAG, "onPrepared, duration=${mp.duration}") + showLoading(false) + mp.start() + mp.setVolume(1f, 1f) + } + + setOnCompletionListener { + Log.d(TAG, "onCompletion") + showLoading(false) + } + + setOnErrorListener { mp, what, extra -> + Log.e(TAG, "onError what=$what extra=$extra") + showLoading(false) + showError("播放错误: $what/$extra") + true + } + + setOnBufferingUpdateListener { _, percent -> + Log.d(TAG, "onBufferingUpdate percent=$percent") + } + + prepareAsync() + } + } catch (e: Exception) { + Log.e(TAG, "startPlayback failed", e) + showLoading(false) + showError("播放失败: ${e.message}") + } + } + private fun showLoading(show: Boolean) { binding.progressBar.visibility = if (show) View.VISIBLE else View.GONE } @@ -108,28 +123,30 @@ class VideoPlayerActivity : AppCompatActivity() { showLoading(false) binding.tvError.text = message binding.tvError.visibility = View.VISIBLE - binding.playerView.visibility = View.GONE + binding.surfaceView.visibility = View.GONE } override fun onResume() { super.onResume() - if (!hasError) player?.playWhenReady = true + if (surfaceReady && mediaPlayer != null && !hasError) { + mediaPlayer?.start() + } } override fun onPause() { super.onPause() - player?.playWhenReady = false + mediaPlayer?.pause() } override fun onStop() { super.onStop() - player?.stop() + mediaPlayer?.stop() } override fun onDestroy() { super.onDestroy() - player?.release() - player = null + mediaPlayer?.release() + mediaPlayer = null } override fun onWindowFocusChanged(hasFocus: Boolean) { diff --git a/app/src/main/res/layout/activity_player.xml b/app/src/main/res/layout/activity_player.xml index 23aca54..440a6e5 100644 --- a/app/src/main/res/layout/activity_player.xml +++ b/app/src/main/res/layout/activity_player.xml @@ -1,15 +1,13 @@ - + android:layout_height="match_parent" /> -