🤡 公司Android老项目升级踩坑小记

1. 引言

😶 最近组里准备用Flutter重构的模块中包含了 "视频播放 ",寻思着用 官方video_player 2.7.2 插件 (🐶鸿蒙也有适配),添加完依赖,Gradle构建直接GG,不过也正常,9年多的项目,构建工具都比较老。

😐 之前用 mmkv 的时候就报过错了,但是当时比较急,只能把库拉下来,删掉android部分,然后封装一层,判断平台,iOS走 mmkv 存储 键值对AndroidMethodChannel,原生端实现sp读写相关的方法。

🤷‍♀️ 但这次好像逃不过了,自己整起来肯定很么烦,长痛不如短痛,唉,升级一波吧😑,折腾了 快四天,项目终于能跑起来,基本功能验证没问题。本文记录下踩坑过程,希望对同样有升级需求的童鞋有帮助,下面是旧项目构建相关的版本信息:

bash 复制代码
com.android.tools.build:gradle:4.2.2
org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.21
buildToolsVersion = "30.0.2"
compileSdkVersion = 31
minSdkVersion = 22
targetSdkVersion = 28
gradle-7.5
java 11

2. minCompileSdk(34) specified xxx

用到的 androidx.media3:media3-extractor:1.4.0 库要求 最低CompileSdk 为 34,旧项目为31,两种解法:

  • 将 compileSdk 提升到 34 ,需要更新 AGPGradle 等的版本。
  • 将 Media3 固定到较低版本,强行降级可能存在API不匹配,导致编译问题或运行时错误

🤔 从 长期 考虑 (新版本修复与特性,其它库也需要升级),选了前者~

3. AAPT2 process xxx Unknow chunk type '200'

搜了下错误,《安卓14适配编译问题和坑总结》提到需要升级 Gradle 版本,最低7.4.2了,而我原本已经7.5了,问了下 Cursor,建议我走 GP 8.1+ + Gradle 8.0+ + JDK 17 的组合,更新下相关版本:

bash 复制代码
com.android.tools.build:gradle:8.1.4
org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.22
targetSdkVersion = 34
gradle-8.2.1
java 17

4. Gradle JVM version incompatible

修改 Gradle JDK 路径,选个17的:

5. com.huawei.agconnect 版本过低 (1.6.0.300)

原因:AGP 8 上调用了已移除的 Transform API (android.registerTransform)。

解法 :升级到支持 AGP 8 的新版本,这里改为 1.9.1.300

6. Multiple build operations failed

this and base files have different roots

原因 :Windows盘符不一致导致的路径相对化报错,Flutter把插件复制到E盘路径,但同时又从C盘读取源码,AGP 在生成 generateDebugUnitTestConfig 时对路径做相对化,因根不同直接抛错。

解法 :要么迁移项目路径,要么修改pub的默认缓存目录 ,这里选后者,设置下 PUB_CACHE 环境变量,位置随意,只要跟 Flutter 项目同一个磁盘就行:

设置完依次执行:flutter cleanflutter pub get,看到设置的目录有缓存文件生成就可以了`

7. Build Type 'debug' contains custom BuildConfig fields

原因 :在 debug 构建类型里用了 buildConfigField ,但 buildConfig 生成功能被关闭,导致不能生成 BuildConfig 类而直接报错。从 AGP 8 起 (尤其是库模块 ),buildConfig 默认可能是关闭的,一旦你声明了 buildConfigField ,就必须显式开启 android.buildFeatures.buildConfig true

解法 :在对应模块的 android { buildFeatures { buildConfig true } } 打开它:

8. kaptGenerateStubsDebugKotlin jvm target compatibility

原因 :KAPT/JVM 版本不一致,在 AGP 8 + Gradle 8 + JDK 17 环境下,Kotlin 的 KAPT 任务默认跑在 JDK 17 上,导致 kaptGenerateStubsDebugKotlin 的 jvmTarget=17,而 Java 编译任务仍是 1.8,于是出现 "两个任务的 JVM 目标不一致" 的报错。

解法 :Java 和 Kotlin 的 JVM 目标统一到同一个版本,官方更推荐用 JVM Toolchain

也可以这样写:

bash 复制代码
kotlin {
	jvmToolchain(17)
}

🤔 一个个module改太麻烦了,直接项目根目录的 build.gradle 中写代码统一项目中所有 Android/Kotlin 子模块的 Java 和 Kotlin 编译目标版本:

bash 复制代码
// 统一所有 Android/Kotlin 子模块的 Java/Kotlin 目标版本,避免 kapt 与 javac 不一致
subprojects { sub ->
    // 仅对白名单中的本仓库模块生效,避免干扰 Flutter/第三方插件模块(如 :flutter、:shared_preferences_android 等)
    def targetModules = [
            'app', 'module_xxx', 'module_yyy', 'module_zzz', 'xxx_yyy',
    ]
    if (!targetModules.contains(sub.name)) {
        return
    }
    // 统一 Kotlin Toolchain 与 jvmTarget 为 17
    sub.plugins.withId('org.jetbrains.kotlin.android') {
        sub.extensions.findByName('kotlin')?.jvmToolchain(17)
        sub.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { task ->
            task.kotlinOptions { jvmTarget = "17" }
        }
    }
    // 兼容旧插件ID:kotlin-android
    sub.plugins.withId('kotlin-android') {
        sub.extensions.findByName('kotlin')?.jvmToolchain(17)
        sub.tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).configureEach { task ->
            task.kotlinOptions { jvmTarget = "17" }
        }
    }

    // 统一 Java 源/目标兼容性为 17
    sub.plugins.withId('com.android.application') {
        sub.android {
            compileOptions {
                sourceCompatibility JavaVersion.VERSION_17
                targetCompatibility JavaVersion.VERSION_17
            }
        }
    }
    sub.plugins.withId('com.android.library') {
        sub.android {
            compileOptions {
                sourceCompatibility JavaVersion.VERSION_17
                targetCompatibility JavaVersion.VERSION_17
            }
        }
    }

    // 兜底:即使模块没暴露 android{} 或被其他脚本覆盖,强制所有 JavaCompile 使用 17
    sub.tasks.withType(JavaCompile).configureEach { jc ->
        jc.sourceCompatibility = JavaVersion.VERSION_17
        jc.targetCompatibility = JavaVersion.VERSION_17
        // 不再强制设置 --release,避免 AGP 在 Java9+ 下的告警与潜在兼容性问题
    }
}

9. Manifest 合并失败

原因:Android 12 开始,凡是 activity/service/receiver 里声明了 intent-filter 的组件,必须显式标注 android:exported。

解法 :报错模块的 AndroidManifest.xml 的四大组件添加 android:exported="true"或"false"。

10. SupportLibraryBlurImpl 爆红

原因 :AGP 7+ 开始弃用 RenderScript 、AGP 8 后不再支持 RenderScript Support Mode,androidx.renderscript 工件也已下线,导致 import androidx.renderscript.* 直接编译报"找不到类"。

解法 :SupportLibraryBlurImpl.java 的 androidx.renderscript. * 改为 android.renderscript.* ,与系统自带 RS API 对齐。移除 renderscriptTargetApi 与 renderscriptSupportModeEnabled。

11. Unresolved reference: synthetic

原因 :项目中用了大量旧的 Kotlin Android Extensions (kotlinx.android.synthetic.*) 来直接访问布局控件。该特性已在 Kotlin 1.4.20 起废弃,并在 Kotlin 1.8 起彻底移除。上面更新了Kotlin版本,所以出现这个错误。

解法 :用 ViewBinding/DataBinding 替代 synthetic ,或者手写 findViewById() 。这里直接后者,ViewBinding 弄起来太麻烦了,每次改完还得重新构建,看布局生成对应的 Binding 导入有没有爆红,远没findViewById() 验证得快。

🤮 要改动的地方太多了 (上百个文件...),让AI代劳,先写个py脚本,把要修改的文件都筛出来(做下去重):

生成的脚本:

python 复制代码
import re

def extract_file_paths(text):
    """
    提取文本中的文件路径
    """
    # 匹配 file:///盘符:/路径 格式的文件路径
    pattern = r'file:///([A-Za-z]:/[^:\s]+.(kt|java|xml|py|js|ts|cpp|h|c))'
    matches = re.findall(pattern, text)
    # 只返回文件路径部分
    file_paths = [match[0] for match in matches]
    return file_paths


def save_paths_to_file(file_paths, output_file):
    """
    将文件路径保存到txt文件
    """
    # 去重并排序
    unique_paths = sorted(set(file_paths))

    with open(output_file, 'w', encoding='utf-8') as f:
        for path in unique_paths:
            f.write(path + '\n')

    print(f"已提取 {len(unique_paths)} 个唯一文件路径,保存到 {output_file}")


def main():
    print("请粘贴包含文件路径的文本(两个连续回车结束):")
    lines = []
    empty_line_count = 0

    while True:
        line = input()
        if line.strip() == '':
            empty_line_count += 1
            if empty_line_count >= 2:
                break
        else:
            empty_line_count = 0
            lines.append(line)

    if not lines:
        print("未输入任何文本")
        return

    text = '\n'.join(lines)

    # 提取文件路径
    file_paths = extract_file_paths(text)

    if not file_paths:
        print("未找到任何文件路径")
        return

    unique_paths = sorted(set(file_paths))
    for path in unique_paths:
        print(path)


if __name__ == "__main__":
    main()

运行下,可以看到成功把路径提取出来了 (还做了去重):

接着写Prompt,让 Cursor 来改:

markdown 复制代码
## 背景

我之前使用了kotlinx.android.synthetic.*来直接访问布局控件,升级了kotlin版本后,构建报错Unresolved reference: synthetic。

## 指令

1.使用 **findViewById** 来替换,我会给出一个待修改的文件列表。
2.需要你删掉文件的import kotlinx.android.synthetic,改为findViewById()的方式访问组件。
3.需要同步组件调用处变量名的修改,组件的类型需要跟对应xml里的组件id对得上,并添加improt导包 (注意避免重复导入)。

## 约束

1.不要进行其它额外的操作,如生成文档,python脚本。
2.老老实实给我一个个文件修改

这些是需要修改的文件列表:[]

😳 等等,为啥我要编译失败,copy?编译失败,再copy,我直接遍历包含 import kotlinx.android.synthetic 的文件不就好了... 直接让 Cursor 代劳:

🤡 经过AI漫长的改动 (总共改了90+个kt文件),总算解决,继续`

12. BuildConfig.DEBUG not found

AGP 8.0 及以后的版本中,默认禁用了 buildConfig ,在以前的版本中,这个功能默认是启用的,允许你在代码里通过 BuildConfig 类访问一些自动生成的构建配置常量。在 AGP 8.0+ 版本中想继续使用,需要在模块级别的 build.gradle 中明确启用:

groovy 复制代码
android {
    // 加上这个
    buildFeatures {
        buildConfig = true
    }
}

13. Constant expression required

使用较新版本的Android Gradle插件或者在库模块中,R.id.xxx 资源ID 不再是 编译时常量 ,无法在 switch 语句的case中使用,改为 if-else 即可解决。

14. okhttp3 Expected Android API level 21+ but was 32

😳 兼容性冲突,项目用到的 OkHttp 3.6.0 版本过旧 (2017年发布),不支持 targetSdkVersion 32,更新下相关依赖:

groovy 复制代码
// 旧版本 (有问题)
api 'com.squareup.retrofit2:retrofit:2.0.0'
api 'com.squareup.retrofit2:converter-gson:2.0.0'  
api 'com.squareup.retrofit2:adapter-rxjava:2.0.0'
api 'com.squareup.okhttp3:logging-interceptor:3.6.0'

// 新版本 (已修复)
api 'com.squareup.retrofit2:retrofit:2.9.0'
api 'com.squareup.retrofit2:converter-gson:2.9.0'
api 'com.squareup.retrofit2:adapter-rxjava:2.9.0'  
api 'com.squareup.okhttp3:logging-interceptor:4.12.0'
api 'com.squareup.okhttp3:okhttp:4.12.0'

OkHttp 4.x API 变更导致报错:

  • 属性化:方法调用改为属性访问 (url() → url)
  • 扩展函数:静态方法改为扩展函数 (parse() → toMediaType())
  • SSL 参数:sslSocketFactory 需要额外的 X509TrustManager 参数

部分修改代码:

kotlin 复制代码
// 属性化
val originUrl = request.url
val originUrlStr = originUrl.toUrl().toString()
val hostUrl = originUrl.host
return chain.proceed(request.newBuilder().url(hookUrlStr.toHttpUrl()).build())

// SSL
// 创建信任管理器
private fun createTrustManager() = object : X509TrustManager {
    override fun checkClientTrusted(chain: Array<out X509Certificate>?, authType: String?) { }

    override fun checkServerTrusted(chain: Array<out X509Certificate>?, authType: String?) { }

    override fun getAcceptedIssuers(): Array<X509Certificate?>? = arrayOfNulls(0)
}

// 设置信任全部证书
private fun createSSLSocketFactory() = try {
    val mMyTrustManager = createTrustManager()
    val sc: SSLContext = SSLContext.getInstance("TLS")
    sc.init(null, arrayOf(mMyTrustManager), SecureRandom())
    sc.socketFactory
} catch (ignored: Exception) {
    ignored.printStackTrace()
    null
}

// MediaType
val requestFile = RequestBody.create("multipart/form-data".toMediaType(), file)
val requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), compressVideoFile!!)

15. SecurityException: getDeviceId: not meet xxx

从Android 10 (API 29)开始,Google严格限制了对设备唯一标识符的访问,普通应用无法再获取设备的IMEI/MEID等敏感信息,TelephonyManager.getDeviceId()在Android 10+上即使有权限也无法正常调用。

kotlin 复制代码
    public String getMyUUID() {
        Context mContext = HttpConfig.getInstance().getAppContext();
        final String tmDevice, tmSerial, androidId;
        
        // 获取Android ID(所有版本都可用)
        androidId = android.provider.Settings.Secure.getString(
            HttpConfig.getInstance().getAppContext().getContentResolver(), 
            android.provider.Settings.Secure.ANDROID_ID
        );
        
        // 尝试获取设备ID和SIM序列号,如果失败则使用备用方案
        String deviceId = "";
        String simSerial = "";
        
        try {
            // 检查权限
            if (ContextCompat.checkSelfPermission(mContext, Manifest.permission.READ_PHONE_STATE) == 
                    PackageManager.PERMISSION_GRANTED) {
                
                final TelephonyManager tm = (TelephonyManager) mContext.getSystemService(Context.TELEPHONY_SERVICE);
                
                // Android 10+需要特殊处理
                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
                    // Android 10+ 无法获取设备ID,使用备用方案
                    deviceId = getAlternativeDeviceId(mContext);
                    try {
                        simSerial = tm.getSimSerialNumber();
                    } catch (SecurityException e) {
                        simSerial = "unavailable_q";
                        Log.w("AndroidUtils", "无法获取SIM序列号: " + e.getMessage());
                    }
                } else {
                    // Android 9及以下版本
                    try {
                        deviceId = tm.getDeviceId();
                        simSerial = tm.getSimSerialNumber();
                    } catch (SecurityException e) {
                        Log.w("AndroidUtils", "获取设备ID失败: " + e.getMessage());
                        deviceId = getAlternativeDeviceId(mContext);
                        simSerial = "unavailable";
                    }
                }
            } else {
                // 没有权限时使用备用方案
                deviceId = getAlternativeDeviceId(mContext);
                simSerial = "no_permission";
            }
        } catch (Exception e) {
            Log.e("AndroidUtils", "获取设备信息异常: " + e.getMessage());
            deviceId = getAlternativeDeviceId(mContext);
            simSerial = "exception";
        }
        
        tmDevice = deviceId;
        tmSerial = simSerial;
        
        // 生成UUID
        UUID deviceUuid = new UUID(
            androidId.hashCode(), 
            ((long) tmDevice.hashCode() << 32) | tmSerial.hashCode()
        );
        String uniqueId = deviceUuid.toString();

        Log.d("debug", "uuid=" + uniqueId);
        return uniqueId;
    }

/**
 * 获取备用设备标识符
 * 当无法获取设备ID时使用的备用方案:使用Android ID + 制造商 + 型号 + 其他信息
 */
private String getAlternativeDeviceId(Context context) {
    try {
        // 组合多个可获取的标识符
        String androidId = android.provider.Settings.Secure.getString(
            context.getContentResolver(), 
            android.provider.Settings.Secure.ANDROID_ID
        );
        String manufacturer = Build.MANUFACTURER;
        String model = Build.MODEL;
        String serial = Build.VERSION.SDK_INT >= Build.VERSION_CODES.O ? 
            Build.getRadioVersion() : "unknown";
        
        return androidId + "_" + manufacturer + "_" + model + "_" + serial;
    } catch (Exception e) {
        Log.e("AndroidUtils", "获取备用设备ID失败: " + e.getMessage());
        return "fallback_" + System.currentTimeMillis();
    }
}

16. Targeting S+ (version 31 and above) requires xxx

完整错误:

argeting S+ (version 31 and above) requires that one of FLAG_IMMUTABLE or FLAG_MUTABLE be specified when creating a PendingIntent. Strongly consider using FLAG_IMMUTABLE, only use FLAG_MUTABLE if some functionality depends on the PendingIntent being mutable, e.g. if it needs to be used with inline replies or bubbles.

错误原因:

Android 12 开始,创建 PendingIntent 时必须明确指定 FLAG_IMMUTABLE (更安全) 或 FLAG_MUTABLE 标志位,以提高安全性。

解决方法:

用到 PendingIntent 的地方添加下 FLAG_IMMUTABLE 标识。

17. insertFileIfNecessary failed

完整错误:

Primary directory com.xxx.xxx.debug not allowed for content://media/external_primary/file; allowed directories are [Download, Documents]

错误原因

从 Android 10 (API 29) 开始,Google 引入了 Scoped Storage(分区存储) 机制,限制了应用对外部存储的访问范围。应用不能再随意在外部存储的根目录或任意位置创建文件夹。我的代码中使用了 MediaStore.Images.Media.insertImage() ,试图将图片保存到系统相册。

解决方法:

Android 10+ 使用新的 ContentValues + MediaStore.Images.Media.EXTERNAL_CONTENT_URI 方式

kotlin 复制代码
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
    // Android 10+ 使用 MediaStore API
    val values = ContentValues().apply {
        put(MediaStore.Images.Media.DISPLAY_NAME, fileName)
        put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg")
        put(MediaStore.Images.Media.RELATIVE_PATH, Environment.DIRECTORY_PICTURES)
    }
    
    val uri = context.contentResolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values)
    uri?.let {
        context.contentResolver.openOutputStream(it)?.use { outputStream ->
            bmp.compress(Bitmap.CompressFormat.JPEG, quality, outputStream)
            success = true
        }
    }

18. Toast不显示

用的 com.blankj:utilcodex 库来弹 Toast ,升级完发现调原来的 ToastUtils.showShort() 不显示了,搜了下issues,找到这个《Toastutils - Android target 31 不允许通过getView 自定义 Toast》,把版本从原来的 1.29.0 改为 1.30.0 解决。然后一些 静态 设置Toast样式的方法,如:setMsgColor()、setBgColor() 等都被干掉了,需要通过 链式调用 的方式来设置属性:

kotlin 复制代码
ToastUtils.make()
    .setTextColor(ContextCompat.getColor(context, R.color.white))
    .setBgColor(ContextCompat.getColor(context, R.color.transparent_30))
    .show("你的消息内容");

// 可以包一层,定义一个工具方法便于调用
public static void showStyledToast(String message) {
    ToastUtils.make()
        .setTextColor(ContextCompat.getColor(context, R.color.white))
        .setBgColor(ContextCompat.getColor(context, R.color.transparent_30))
        .show(message);
}

19. flutter页面白屏

🤡 这个bug搞了我快两天,莫名其妙就 FlutterFragment-白屏、FlutterActivity-黑屏,啥报错没有,就一句:

bash 复制代码
Tried to send viewport metrics from Android to Flutter 
but this FlutterView was not attached to a FlutterEngine.

// 翻译下就是:
试图在 FlutterView 还没有正确附加到 FlutterEngine 时发送视口度量信息

// 报错的可能原因
1. FlutterView 和 FlutterEngine 的生命周期没有正确同步
2. 可能过早地调用了需要 FlutterEngine 的操作
3. FlutterEngine 可能已经被销毁,但 FlutterView 还在尝试使用它

问题是我在 Application 类中已经做了 Flutter引擎预热 ,添加发现有走,但是 enginenull ...

😐 感觉是 flutter_boost 的问题,issues 搜了一圈没找到解决方案,让 Cursor 给我在 FlutterFragment 和 FlutterActivity 相关回调打日志,帮助我排查,甚至做了引擎判断,为null自己创建一个,白屏依旧 ... 不知道尝试了多少次 gradle cleaninvalidate Caches.. ,一样白屏,然后代码推到打包机上打包又正常,🙃 人麻了,就差没重装电脑了... 后面又再仔细看了下issues,在《[Bug]: flutterboost4.4.0版本:原生页面打开Flutter页面时空白页》看到了这个:

😳 em... 跟我之前搞 "主项目打Debug,flutter子项目打Release,Flutter引擎二进制文件不匹配导致的白屏" 有关吗?flutter子项目没变化,so文件没重新生成?执行下述命令重新构建子项目:

bash 复制代码
flutter clean
flutter pub get
flutter pub run build_runner build --delete-conflicting-outputs

运行无误后,运行主项目,再打开flutter页面,好了 !!!卧槽,有毒...

相关推荐
死就死在补习班4 小时前
Android系统源码分析Input - InputReader读取事件
android
死就死在补习班4 小时前
Android系统源码分析Input - InputChannel通信
android
死就死在补习班4 小时前
Android系统源码分析Input - 设备添加流程
android
死就死在补习班4 小时前
Android系统源码分析Input - 启动流程
android
tom4i5 小时前
Launcher3 to Launchpad 01 布局修改
android
雨白5 小时前
OkHttpClient 核心配置详解
android·okhttp
淡淡的香烟5 小时前
Android auncher3实现简单的负一屏功能
android
RabbitYao5 小时前
Android 项目 通过 AndroidStringsTool 更新多语言词条
android·python
RabbitYao5 小时前
使用 Gemini 及 Python 更新 Android 多语言 Excel 文件
android·python