简介:这份资源面向Android开发初学者与需要维护旧项目的工程师,提供android-support-v4.jar与android-support-v7-appcompat.jar两个经典兼容库,用于解决低版本系统无法使用Fragment、ActionBar及Material Design组件的问题。压缩包共447个文件,以223个xml布局与资源定义、213个png图片素材为主,另含3个java源码、2个jar库文件及readme、properties等配置说明,整体约2.05MB,结构接近完整示例工程,便于直接导入参考。已有237人学习下载。通过其中的jar包与配套资源,读者可理解v4库对API 4及以上Fragment、Loader等组件的支持方式,以及v7 appcompat库如何借助AppCompatActivity在API 7及以上实现ActionBar与主题样式兼容,从而掌握Android向后兼容策略,并为迁移至AndroidX打下基础。
1. 两个 support 包到底该留谁:一次依赖清理引发的连环崩溃
如果你手里有一个 2015 年前后起盘、至今还在维护的 Android 工程,android-support-v4.jar和android-support-v7-appcompat.jar这两个名字大概率同时躺在libs/目录里。它们不是两个可以随便删一个的重复文件,而是新旧两代兼容库的交叉点:v4 是早期把新 API 向下兼容到旧系统的那一层,v7-appcompat 则是在 v4 基础上补上了 ActionBar、主题和资源兼容。很多工程当年为了图省事,两个都塞进libs/,结果就是编译能过、运行偶发NoClassDefFoundError、ClassNotFoundException,甚至资源找不到。这篇笔记不讲空泛的兼容库历史,只讲一件事:面对这两个 jar,怎么判断该保留哪个、怎么迁移、迁移过程中哪些参数和目录结构会翻车。适合正在维护老工程、准备做依赖收敛的 Android 开发者。
2. 先分清 v4 和 v7-appcompat 的职责边界
2.1 为什么两个 jar 会同时出现
早期 Android 工程没有成熟的远程依赖体系,很多团队直接把 support 库的 jar 包复制到libs/下。android-support-v4.jar提供的是Fragment、ViewPager、Loader、NotificationCompat这类基础兼容类;android-support-v7-appcompat.jar提供的是ActionBarActivity、AppCompatActivity、Toolbar以及配套的res资源。问题在于,v7-appcompat 本身依赖 v4,但很多工程在复制 jar 时只复制了 v7 的 jar,没有把 v7 的res目录一起导入,于是又单独放了一份 v4 jar 来补类。时间一长,两个 jar 的版本号不一致,方法签名对不上,运行时就会在FragmentActivity或AppCompatDelegate上炸掉。
判断当前工程到底依赖了哪些类,最直接的办法是反编译或解压 jar 看包结构。下面这段命令用来列出两个 jar 里的类清单,对比重复项:
# 解压两个 jar 到临时目录,列出所有 class 文件 mkdir -p /tmp/v4_classes /tmp/v7_classes unzip -q libs/android-support-v4.jar -d /tmp/v4_classes unzip -q libs/android-support-v7-appcompat.jar -d /tmp/v7_classes # 统计各自包路径下的类数量 find /tmp/v4_classes -name "*.class" | wc -l find /tmp/v7_classes -name "*.class" | wc -l # 找出两个 jar 中都存在的同名类(重复定义的高风险区) comm -12 \ <(find /tmp/v4_classes -name "*.class" | sed 's|/tmp/v4_classes/||' | sort) \ <(find /tmp/v7_classes -name "*.class" | sed 's|/tmp/v7_classes/||' | sort)逻辑说明:unzip -q静默解压,避免刷屏;comm -12取两个排序列表的交集。如果交集里出现android/support/v4/app/Fragment.class这类条目,说明两个 jar 都打包了同一个类,运行时加载顺序不确定,属于必须清理的重复依赖。参数上,-d指定解压目录,路径不要带空格;sed用来去掉前缀,保证两边路径格式一致。
2.2 v7-appcompat 不是 v4 的替代品
一个常见误解是“用了 v7 就不需要 v4”。实际上 v7-appcompat 的AppCompatActivity继承链里仍然引用 v4 的FragmentActivity,只是它把 v4 作为传递依赖带进来。如果你用 Gradle 远程依赖,写com.android.support:appcompat-v7:xx.x.x会自动拉取support-v4,不需要手动放 jar。但老工程如果是本地 jar 模式,就必须同时保证 v4 和 v7 的版本号一致,否则会出现NoSuchMethodError。判断版本是否一致,可以看 jar 内META-INF/MANIFEST.MF里的Implementation-Version,或者直接看文件名里的版本后缀。
我一般会先确认工程是哪种依赖模式:打开build.gradle,如果dependencies里是compile fileTree(dir: 'libs', include: ['*.jar']),那就是本地 jar 模式;如果是compile 'com.android.support:appcompat-v7:23.4.0',那就是远程依赖模式。两种模式的清理路径完全不同,混着改最容易翻车。
2.3 用 Gradle 依赖树定位真实来源
远程依赖模式下,重复类往往来自传递依赖。用下面命令打印依赖树,看support-v4被谁拉进来:
# 在工程根目录执行,查看 app 模块的依赖树 ./gradlew :app:dependencies --configuration debugCompileClasspath # 只看 support 相关条目,过滤输出 ./gradlew :app:dependencies --configuration debugCompileClasspath | grep -i "support"逻辑说明:--configuration debugCompileClasspath指定查看 debug 变体的编译类路径,比默认的compile更贴近实际打包结果。grep -i "support"过滤出兼容库条目。如果看到com.android.support:appcompat-v7:23.4.0 -> 23.4.0和com.android.support:support-v4:23.4.0同时出现,且版本号一致,说明远程依赖已经处理好传递关系,此时libs/下如果还有同名 jar,就是多余且危险的。参数上,debugCompileClasspath可以换成releaseCompileClasspath检查发布变体,两者不一致时以 release 为准。
3. 从本地 jar 迁移到远程依赖的完整步骤
3.1 备份与清理 libs 目录
迁移第一步不是改代码,而是把libs/下的 support jar 移走。直接删除风险太大,先移到工程外的备份目录:
# 在工程根目录执行,创建备份目录并移动 support 相关 jar mkdir -p ../backup_support_jars mv libs/android-support-v4.jar ../backup_support_jars/ mv libs/android-support-v7-appcompat.jar ../backup_support_jars/ # 确认 libs 下不再有 support 开头的 jar ls libs/ | grep -i support逻辑说明:mv而不是rm,保留后悔药。ls | grep用来确认清理干净,如果还有输出,说明有遗漏的 support jar 或 aar。注意,有些工程会把 v7-appcompat 以 library module 形式导入,而不是 jar,这种情况下要检查settings.gradle里是否 include 了:appcompat之类的模块,迁移时需要一并移除。
3.2 改写 build.gradle 依赖声明
清理完本地 jar 后,在app/build.gradle里加入远程依赖。版本号要和compileSdkVersion匹配,不能随便写:
android { compileSdkVersion 23 buildToolsVersion "23.0.3" } dependencies { // 使用 appcompat-v7,它会自动传递 support-v4 compile 'com.android.support:appcompat-v7:23.4.0' // 如果代码里直接用了 v4 的类,显式声明 support-v4 更稳妥 compile 'com.android.support:support-v4:23.4.0' }逻辑说明:compileSdkVersion 23对应 support 库 23.x.x 系列,版本号不能超过 compileSdkVersion 对应的 API 级别,否则会报Failed to resolve。compile在旧版 Gradle 插件里是标准写法,新版插件用implementation,但老工程通常还在用com.android.tools.build:gradle:2.x,保持compile即可。两个依赖都写是为了显式锁定版本,避免传递依赖被其他库拉高或拉低。参数上,23.4.0是 23 系列最后一个稳定版,如果工程 compileSdkVersion 是 22,就改成22.2.1。
3.3 处理资源引用与主题继承
迁移后最常见的编译错误是resource not found,因为 v7-appcompat 的res资源不再以本地目录形式存在,而是通过 AAR 提供。检查AndroidManifest.xml和styles.xml:
<!-- AndroidManifest.xml 中 application 或 activity 的 theme 引用 --> <application android:theme="@style/Theme.AppCompat.Light.NoActionBar"> ... </application><!-- res/values/styles.xml 中自定义主题必须继承 AppCompat --> <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar"> <item name="colorPrimary">@color/primary</item> </style>逻辑说明:Theme.AppCompat系列由 appcompat-v7 提供,迁移后必须确保所有 Activity 继承AppCompatActivity,否则主题里的colorPrimary等属性不会生效。如果工程里还有@style/Theme.AppCompat找不到的报错,检查build.gradle里是否漏了compile 'com.android.support:appcompat-v7:xx.x.x'。参数上,NoActionBar和DarkActionBar按原有 UI 选择,不要随意改,否则状态栏和标题栏样式会变。
3.4 同步后验证类加载
改完依赖后执行一次 clean build,并检查 APK 里的类是否唯一:
# 清理并重新编译 debug 包 ./gradlew clean :app:assembleDebug # 用 dexdump 或 unzip 查看 APK 中的 classes.dex 是否包含重复类 unzip -l app/build/outputs/apk/app-debug.apk | grep classes逻辑说明:clean清除旧增量编译缓存,避免残留的本地 jar 类混入。assembleDebug生成调试包。unzip -l列出 APK 内容,确认classes.dex存在且没有多个 dex 冲突。如果编译报Duplicate class,说明还有地方引用了本地 jar,回到 3.1 检查libs/和settings.gradle。参数上,如果工程方法数超过 65536,需要开启multiDexEnabled true,但这属于另一个话题,迁移 support 库时先保证单 dex 能过。
4. 避坑:迁移 support 库时最容易翻车的五个点
4.1 现象:编译通过但运行崩溃NoClassDefFoundError
原因:本地 jar 移走了,但某个模块的build.gradle还在用fileTree引用libs/,或者某个 library module 没有同步加远程依赖。解决:全局搜索fileTree和android-support,确保所有 module 的依赖声明一致。用./gradlew :moduleName:dependencies逐个模块检查。
4.2 现象:AppCompatActivity报红,提示找不到符号
原因:compileSdkVersion低于 appcompat-v7 要求的版本,或者 IDE 缓存没刷新。解决:把compileSdkVersion提到 23 以上,执行File > Invalidate Caches / Restart,再./gradlew clean。如果还不行,检查buildToolsVersion是否匹配。
4.3 现象:主题里的colorPrimary不生效,界面变灰
原因:Activity 继承的是Activity而不是AppCompatActivity,或者AndroidManifest.xml里 theme 写的是@android:style/Theme.Material。解决:把所有 Activity 的继承改为AppCompatActivity,theme 改为Theme.AppCompat系列。注意,AppCompatActivity需要导入android.support.v7.app.AppCompatActivity,不是android.app.Activity。
4.4 现象:Fragment相关代码报ClassCastException
原因:v4 的Fragment和系统android.app.Fragment混用。迁移后如果代码里同时存在两种Fragment导入,运行时会类型不匹配。解决:统一使用android.support.v4.app.Fragment和FragmentManager,检查import语句,把android.app.Fragment全部替换掉。FragmentPagerAdapter也要用 v4 版本。
4.5 现象:Gradle sync 报Failed to resolve: com.android.support:appcompat-v7:xx.x.x
原因:本地没有对应版本的 support 仓库缓存,或者repositories里缺少maven { url 'https://maven.google.com' }(旧版 Gradle 需要)。解决:在工程根目录build.gradle的allprojects.repositories里加入 Google Maven 仓库,或者把版本号降到本地 SDK 已缓存的版本。注意,不要随意写一个不存在的版本号,版本号必须和 compileSdkVersion 对应。
5. 进阶:用support-v4和appcompat-v7的版本对齐技巧收尾
迁移完成后,真正要养成的习惯是版本对齐。support 库系列要求所有组件版本号完全一致,support-v4、appcompat-v7、recyclerview-v7、design如果版本号有高有低,运行时会出现NoSuchMethodError或资源冲突。我一般会在build.gradle里定义一个变量统一管理:
ext { supportLibVersion = '23.4.0' } dependencies { compile "com.android.support:support-v4:${supportLibVersion}" compile "com.android.support:appcompat-v7:${supportLibVersion}" compile "com.android.support:recyclerview-v7:${supportLibVersion}" compile "com.android.support:design:${supportLibVersion}" }逻辑说明:ext块定义扩展属性,${supportLibVersion}在字符串里插值。这样改版本号只需要改一处,避免漏改。参数上,supportLibVersion的值必须和compileSdkVersion对应,23 对应 23.4.0,24 对应 24.2.1,25 对应 25.4.0,以此类推。如果工程里还有第三方库依赖了不同版本的 support 库,用exclude排除传递依赖:
compile('com.some.library:1.0.0') { exclude group: 'com.android.support', module: 'support-v4' }逻辑说明:exclude阻止第三方库拉入自己的 support-v4 版本,统一由工程根依赖决定。参数上,group和module必须写全,不能只写module。改完后执行./gradlew :app:dependencies确认没有版本冲突。
验证版本是否真正对齐,可以写一个简单的运行时检查,在Application.onCreate里打印 support 库版本:
@Override public void onCreate() { super.onCreate(); // 打印 support-v4 和 appcompat-v7 的版本,确认一致 Log.d("SupportCheck", "v4: " + android.support.v4.app.FragmentActivity.class.getPackage().getImplementationVersion()); Log.d("SupportCheck", "v7: " + android.support.v7.app.AppCompatActivity.class.getPackage().getImplementationVersion()); }逻辑说明:getPackage().getImplementationVersion()读取 jar 或 aar 的清单版本,两个日志输出一致才算对齐。如果输出null,说明打包时清单信息被剥离,可以改用BuildConfig或直接看依赖树。参数上,日志 tag 用SupportCheck方便过滤,发布前记得移除或降级为verbose。
最后说一个我踩过的坑:曾经在一个老工程里只删了android-support-v4.jar,保留了 v7-appcompat 的本地 jar,结果编译通过,但运行时ViewPager直接崩,因为 v7 的本地 jar 里没有打包 v4 的ViewPager类。后来老老实实全部迁到远程依赖,版本锁死,才彻底消停。维护老工程,依赖收敛这件事没有捷径,先看清 jar 里有什么,再决定留谁。希望帮到你。
本文还有配套的精品资源,点击获取