業(yè)界方案
在網(wǎng)上隨便搜索一下就能發(fā)現(xiàn)瘦身有好多方案,但是實(shí)踐一下就能發(fā)現(xiàn)好多都不靠譜
| 方案 | 作用 | 瘦身效果 |
|---|---|---|
| proguard | 代碼混淆 | 效果明顯 |
| abiFilter "armeabi" | 去除其他平臺(tái)so | 效果明顯 |
| resConfigs "zh" | 語言文件去除 | 0.1M |
| shrinkResources | 無用資源去除需維護(hù)keep文件 | 1M |
| TinyPng | 圖片壓縮,賬號(hào)收費(fèi) | 3M |
| ThinR | 移除R文件 | 0.3M |
| AndResGuard | 資源混淆白名單維護(hù)難 | 資源混淆0.3M,7zip壓縮2M |
| webp | android兼容性差 | 不推薦 |
| Lint 無用資源去除 | 有可能刪除getIdentifier調(diào)用的資源 | 不推薦 |
| redex | 安全風(fēng)險(xiǎn)高,對(duì)于加固、熱修復(fù)等功能有影響 | 未實(shí)踐 |
| so動(dòng)態(tài)加載 | 風(fēng)險(xiǎn)高,大部分so都需要實(shí)時(shí)加載 | 未實(shí)踐 |
| 加固 | 隱藏dex | 1M |
| 重復(fù)資源優(yōu)化 | 對(duì)比資源文件 md5,刪除重復(fù)文件和resources.arsc中的定義 |
0.2M |
| 移除TINY_PNG文件 | 通過android-chunk-utils把resources.arsc中對(duì)應(yīng)的定義和文件移除,風(fēng)險(xiǎn)高 |
美團(tuán)文章一帶而過,我實(shí)踐一下,實(shí)際代碼特別復(fù)雜,arsc文件索引value要重新計(jì)算,減小0.1M都不到 |
方案實(shí)踐
Smallapk Gradle插件減小APK體積25%
apply plugin: 'smallapk'


動(dòng)態(tài)資源查找
其他方案網(wǎng)上都有,我重點(diǎn)講講SmallApk插件怎么解決getIdentifier方法帶來的動(dòng)態(tài)資源問題。
- ShrinkResources只能去除小部分無用資源的問題
- 解決AndResGuard需要配置白名單的問題
首先需要了解ShrinkResources的原理:
通過ResourceUseModel建立一個(gè)資源引用樹,找到有可能是resource.getIdentifier調(diào)用的資源標(biāo)記為reachable,找到無用資源并替換成tiny的小文件

用這種方式查找到的動(dòng)態(tài)資源會(huì)特別多,因?yàn)橛谜齽t表達(dá)式匹配了所有的字符串,那么如何精確找到動(dòng)態(tài)資源呢,你會(huì)發(fā)現(xiàn)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!
}
}
那就只能自己想個(gè)方案找到getIdentifier引用的所有資源了。
先來看看效果,這個(gè)是getIdentifier的多種調(diào)用方式

這個(gè)是用SmallApk插件找到的動(dòng)態(tài)資源

這個(gè)是找到的動(dòng)態(tài)資源調(diào)用關(guān)系圖

那么SmallApk是怎么做的呢

思路和android源碼ResourceUsageAnalyzer是一樣的,都是匹配字符串常量,唯一的區(qū)別就是加入了方法有向圖搜索節(jié)點(diǎn),排除大部分無用字符串。
首先形成調(diào)用有向圖
/**
* KeepResUsageVisitor會(huì)把methodNode、constantNode、fieldNode、classNode調(diào)用關(guān)系轉(zhuǎn)換成有向圖
*/
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) {//常量節(jié)點(diǎn)
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);//變量節(jié)點(diǎn)
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) {//方法節(jié)點(diǎn)
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) {//變量節(jié)點(diǎn)
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的方法節(jié)點(diǎn)
@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);
}
}
}
然后找到所有調(diào)用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);
}
}
}
}
}
最后匹配字符串常量找到動(dòng)態(tài)資源
// getResources().getIdentifier("ic_video_codec_" + codecName, "drawable", ...)
for (Resource resource : mModel.getResources()) {
if (resource.name.startsWith(name)) {
mDynamicUsed.add(resource);
}
}
找到動(dòng)態(tài)資源以后就能去解決AndResGuard和ShrinkResources的問題了
解決ShrinkResources只能去除小部分無用資源的問題,只要把找到的動(dòng)態(tài)資源文件寫入到/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需要配置白名單的問題,只要把動(dòng)態(tài)資源加入到白名單就可以
Set<String> keepResSet = new HashSet<>();
if (mDynamicUsed != null){
for (Resource resource : mDynamicUsed) {
keepResSet.add("R."+resource.type.getName()+"."+resource.name);
}
}
resproguardTask.setWhiteList(keepResSet)
你問我答
-
AndResGuard會(huì)混淆資源文件名,xml資源文件里面也使用了文件名的字符串,那為什么apk沒有崩潰?
因?yàn)榫幾g完以后布局xml文件里變成了int常量,AndResGuard修改的是字符串,int索引沒變
-
proguard也會(huì)去除R文件,那為什么用ThinR還會(huì)減小包體積?
因?yàn)閍ar包里不存在R.class的,app打包的時(shí)候會(huì)重新生成lib庫的R文件,但是因?yàn)樯蒷ib庫的class文件時(shí)R文件的變量不是final,所以aar里面是直接引用引用了lib.R.id,
然后proguard判斷l(xiāng)ib庫R文件是有引用關(guān)系的不能去除,ThinR相當(dāng)于接著把lib庫里面的R文件刪除
-
在mac上解壓縮apk再壓縮會(huì)去,你會(huì)發(fā)現(xiàn)這個(gè)apk已經(jīng)沒法安裝了,為什么,照理說不做任何操作應(yīng)該不影響apk簽名呀?
因?yàn)镸AC解壓縮的時(shí)候會(huì)存在.DS_Store文件,直接壓縮會(huì)把外面的文件夾目錄也壓縮進(jìn)去
-
重新壓縮apk以后體積會(huì)小,為什么apk自己不是壓縮過了嗎?
因?yàn)槟J(rèn)圖片是不壓縮的
shrinkResources不是刪除了無用資源嗎,那為什么我用Lint去刪除無用資源,包體積還是會(huì)變?。?br> 一個(gè)是資源問題,一個(gè)是代碼問題。
資源問題:shrinkResources匹配字符串常量得到的無用資源會(huì)比較少,而lint掃描會(huì)只掃描硬靜態(tài)引用資源,這樣掃描的資源文件會(huì)比較多
代碼問題:lint還會(huì)刪掉java文件,而shrinkResources只會(huì)去除無用資源,雖然android源碼里面二次打包TWO_PASS_AAPT,但是默認(rèn)沒開啟android gradle插件默認(rèn)是開啟v2簽名的,為什么在我們的app里面用修改meta-inf文件的方式加入渠道號(hào)還可以運(yùn)行?
因?yàn)槲覀兿燃庸?,然后重新v1簽名,再打渠道包,運(yùn)氣好,剛好繞過了v2簽名的坑,哈哈zipalign會(huì)影響v1簽名和v2簽名嗎?
請(qǐng)?jiān)趘1簽名后使用zipalign,v2簽名前使用zipalign,v1簽名和v2簽名可以同時(shí)存在,不能只用v2簽名,因?yàn)樵?.0手機(jī)只會(huì)校驗(yàn)v1簽名




