业界方案
在网上随便搜索一下就能发现瘦身有好多方案, 但是实践一下就能发现好多都不靠谱
方案 | 作用 | 瘦身效果 |
---|---|---|
proguard | 代码混淆 | 效果明显 |
abiFilter "armeabi" | 去除其他平台 so | 效果明显 |
resConfigs "zh" | 语言文件去除 | 0.1M |
shrinkResources | 无用资源去除需维护 keep 文件 | 1M |
TinyPng | 图片压缩,账号收费 | 3M |
ThinR | 移除 R 文件 | 0.3M |
AndResGuard | 资源混淆白名单维护难 | 资源混淆 0.3M,7zip 压缩 2M |
webp | android 兼容性差 | 不推荐 |
Lint 无用资源去除 | 有可能删除 getIdentifier 调用的资源 | 不推荐 |
redex | 安全风险高,对于加固、热修复等功能有影响 | 未实践 |
so 动态加载 | 风险高,大部分 so 都需要实时加载 | 未实践 |
加固 | 隐藏 dex | 1M |
重复资源优化 | 对比资源文件 md5,删除重复文件和 resources.arsc 中的定义 | 0.2M |
移除 TINY_PNG 文件 | 通过 android-chunk-utils 把 resources.arsc 中对应的定义和文件移除, 风险高 | 美团文章一带而过,我实践一下,实际代码特别复杂,arsc 文件索引 value 要重新计算,减小 0.1M 都不到 |
方案实践
Smallapk Gradle 插件减小 APK 体积 25%
apply plugin: 'smallapk'
动态资源查找
其他方案网上都有, 我重点讲讲 SmallApk 插件怎么解决 getIdentifier 方法带来的动态资源问题
ShrinkResources 只能去除小部分无用资源的问题
解决 AndResGuard 需要配置白名单的问题
首先需要了解 ShrinkResources 的原理:
通过 ResourceUseModel 建立一个资源引用树, 找到有可能是 resource.getIdentifier 调用的资源标记为 reachable, 找到无用资源并替换成 tiny 的小文件
用这种方式查找到的动态资源会特别多, 因为用正则表达式匹配了所有的字符串, 那么如何精确找到动态资源呢, 你会发现 android 源码里面写着 Todo, 哈哈
- @Override
- public void visitMethodInsn(int opcode, String owner, String name,
- String desc, boolean itf) {
- super.visitMethodInsn(opcode, owner, name, desc, itf);
- if (owner.equals("android/content/res/Resources")
- && name.equals("getIdentifier")
- && desc.equals(
- "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)I")) {
- mFoundGetIdentifier = true;
- // TODO: Check previous instruction and see if we can find a literal
- // String; if so, we can more accurately dispatch the resource here
- // rather than having to check the whole string pool!
- }
- }
那就只能自己想个方案找到 getIdentifier 引用的所有资源了
先来看看效果, 这个是 getIdentifier 的多种调用方式
这个是用 SmallApk 插件找到的动态资源
这个是找到的动态资源调用关系图
那么 SmallApk 是怎么做的呢
思路和 android 源码 ResourceUsageAnalyzer 是一样的, 都是匹配字符串常量, 唯一的区别就是加入了方法有向图搜索节点, 排除大部分无用字符串
首先形成调用有向图
- /**
- * KeepResUsageVisitor 会把 methodNodeconstantNodefieldNodeclassNode 调用关系转换成有向图
- */
- class KeepResUsageVisitor extends ClassVisitor {
- private String className;
- public KeepResUsageVisitor() {
- super(Opcodes.ASM5);
- }
- @Override
- public void visit(int version, int access, String name, String signature,
- String superName, String[] interfaces) {
- super.visit(version, access, name, signature, superName, interfaces);
- className = name;
- }
- @Override
- public MethodVisitor visitMethod(int access, final String name,
- String desc, String signature, String[] exceptions) {
- String methodName = name;
- return new MethodVisitor(Opcodes.ASM5) {
- @Override
- public void visitLdcInsn(Object cst) {
- super.visitLdcInsn(cst);
- if (cst instanceof String) {// 常量节点
- String constant = (String) cst;
- GraphNode caller = new GraphNode();
- caller.putClass(className);
- caller.putMethod(methodName);
- caller.putConstant(constant);
- GraphNode called = new GraphNode();
- called.putClass(className);
- called.putMethod(methodName);
- GraphHolder.addNode(caller, called);
- }
- }
- @Override
- public void visitFieldInsn(int opcode, String owner, String name, String desc) {
- super.visitFieldInsn(opcode, owner, name, desc);// 变量节点
- GraphNode caller = new GraphNode();
- caller.putClass(owner);
- caller.putField(name);
- GraphNode called = new GraphNode();
- called.putClass(className);
- called.putMethod(methodName);
- GraphHolder.addNode(caller, called);
- }
- @Override
- public void visitMethodInsn(int opcode, String owner, String name,
- String desc, boolean itf) {// 方法节点
- super.visitMethodInsn(opcode, owner, name, desc, itf);
- GraphNode caller = new GraphNode();
- caller.putClass(className);
- caller.putMethod(methodName);
- GraphNode called = new GraphNode();
- called.putClass(owner);
- called.putMethod(name);
- GraphHolder.addNode(caller, called);
- }
- };
- }
- @Override
- public FieldVisitor visitField(int access, String name, String desc, String signature,
- Object value) {
- final String field = name;
- if (value instanceof String) {// 变量节点
- String constant = (String) value;
- GraphNode caller = new GraphNode();
- caller.putClass(className);
- caller.putField(field);
- caller.putConstant(constant);
- GraphNode called = new GraphNode();
- called.putClass(className);
- called.putField(field);
- GraphHolder.addNode(caller, called);
- }
- return new FieldVisitor(Opcodes.ASM5) ;
- }
- }
接着找到 getIdentifier 的方法节点
- @Override
- public void call(GraphNode caller, GraphNode called) {
- if (called.getClassName().equals("android/content/res/Resources")
- && called.getMethod().equals("getIdentifier")) {
- if (!caller.getClassName().startsWith("android/support/v7")) {
- dynamicCallGraph.add(caller);
- }
- }
- }
然后找到所有调用 getIdentifier 的字符串常量
- private void addCodeStrings() {
- mLogPrinter.println("Dynamic String---->CodeString:");
- List<GraphNode> list = new ArrayList<>();
- Set<String> codeStrings = new HashSet<>();
- for (GraphNode callGraph : dynamicCallGraph) {
- Collection<GraphCall> set = GraphHolder.findParentNode(callGraph);
- if (set != null) {
- for (GraphCall call : set) {
- GraphNode caller = call.getCaller();
- String value = caller.getConstant();
- if (value != null) {
- list.add(caller);
- codeStrings.add(value);
- }
- }
- }
- }
- }
最后匹配字符串常量找到动态资源
- // getResources().getIdentifier("ic_video_codec_" + codecName, "drawable", ...)
- for (Resource resource: mModel.getResources()) {
- if (resource.name.startsWith(name)) { mDynamicUsed.add(resource);
- }
- }
找到动态资源以后就能去解决 AndResGuard 和 ShrinkResources 的问题了
解决 ShrinkResources 只能去除小部分无用资源的问题, 只要把找到的动态资源文件写入到
/build/intermediates / res / merged / release / raw / keep.xml
中
- static void writeKeepXml(Set<ResourceUsageModel.Resource> list, File keepFile) {
- if (list == null || list.size() == 0) {
- return
- }
- StringBuffer buffer = new StringBuffer()
- list.each { value ->
- buffer.append(@ + value.type.getName() + / + value.name)
- buffer.append(,)
- }
- buffer.deleteCharAt(buffer.length() - 1)
- def builder = new groovy.xml.StreamingMarkupBuilder()
- builder.encoding = UTF-8
- def result = builder.bind {
- mkp.xmlDeclaration()
- mkp.declareNamespace(tools: http://schemas.android.com/tools)
- resources(tools:shrinkMode: strict, tools:keep: buffer)
- }
- def writer = new FileWriter(keepFile)
- writer << result
- }
解决 AndResGuard 需要配置白名单的问题, 只要把动态资源加入到白名单就可以
- Set<String> keepResSet = new HashSet<>();
- if (mDynamicUsed != null){
- for (Resource resource : mDynamicUsed) {
- keepResSet.add("R."+resource.type.getName()+"."+resource.name);
- }
- }
- resproguardTask.setWhiteList(keepResSet)
你问我答
AndResGuard 会混淆资源文件名, xml 资源文件里面也使用了文件名的字符串, 那为什么 apk 没有崩溃?
因为编译完以后布局 xml 文件里变成了 int 常量, AndResGuard 修改的是字符串, int 索引没变
proguard 也会去除 R 文件, 那为什么用 ThinR 还会减小包体积?
因为 aar 包里不存在 R.class 的, app 打包的时候会重新生成 lib 库的 R 文件, 但是因为生成 lib 库的 class 文件时 R 文件的变量不是 final, 所以 aar 里面是直接引用引用了 lib.R.id,
然后 proguard 判断 lib 库 R 文件是有引用关系的不能去除, ThinR 相当于接着把 lib 库里面的 R 文件删除
在 mac 上解压缩 apk 再压缩会去, 你会发现这个 apk 已经没法安装了, 为什么, 照理说不做任何操作应该不影响 apk 签名呀?
因为 MAC 解压缩的时候会存在. DS_Store 文件, 直接压缩会把外面的文件夹目录也压缩进去
重新压缩 apk 以后体积会小, 为什么 apk 自己不是压缩过了吗?
因为默认图片是不压缩的
shrinkResources 不是删除了无用资源吗, 那为什么我用 Lint 去删除无用资源, 包体积还是会变小?
一个是资源问题, 一个是代码问题
资源问题: shrinkResources 匹配字符串常量得到的无用资源会比较少, 而 lint 扫描会只扫描硬静态引用资源, 这样扫描的资源文件会比较多
代码问题: lint 还会删掉 java 文件, 而 shrinkResources 只会去除无用资源, 虽然 android 源码里面二次打包 TWO_PASS_AAPT, 但是默认没开启
android gradle 插件默认是开启 v2 签名的, 为什么在我们的 app 里面用修改 meta-inf 文件的方式加入渠道号还可以运行?
因为我们先加固, 然后重新 v1 签名, 再打渠道包, 运气好, 刚好绕过了 v2 签名的坑, 哈哈
zipalign 会影响 v1 签名和 v2 签名吗?
请在 v1 签名后使用 zipalign,v2 签名前使用 zipalign,v1 签名和 v2 签名可以同时存在, 不能只用 v2 签名, 因为在 7.0 手机只会校验 v1 签名
来源: http://mp.weixin.qq.com/s/zgRjVGkXgYW1GEo_c_Dspg