🤡 公司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页面,好了 !!!卧槽,有毒...

相关推荐
娅娅梨5 小时前
Android- Surface, SurfaceView, TextureView, SurfaceTexture 原理图解
android·surface
2501_915918417 小时前
HTTPS 端口号详解 443 端口作用、iOS 抓包方法、常见 HTTPS 抓包工具与网络调试实践
android·网络·ios·小程序·https·uni-app·iphone
程序员码歌7 小时前
明年35岁了,如何破局?说说心里话
android·前端·后端
非门由也9 小时前
Android studio安装教程——超详细(含安装包安装教程)
android·ide·android studio
平淡风云9 小时前
Android应用添加日历提醒功能
android·日历
骐骥110 小时前
2025-09-08升级问题记录:app提示“此应用专为旧版Android打造..”或“此应用与最新版 Android 不兼容”
android·升级·不兼容·target sdk·专为旧版 android 系统
Zender Han10 小时前
Flutter 视频播放器——flick_video_player 介绍与使用
android·flutter·ios·音视频
尚久龙12 小时前
安卓学习 之 用户登录界面的简单实现
android·运维·服务器·学习·手机·android studio·安卓
Modu_MrLiu12 小时前
Android实战进阶 - 启动页
android·实战进阶·启动页·倒计时场景
出门吃三碗饭12 小时前
编译器构造:从零手写汇编与反汇编程序(一)
android·汇编