業(yè)界方案
在網(wǎng)上隨便搜索一下就能發(fā)現(xiàn)瘦身有好多方案,但是實(shí)踐一下就能發(fā)現(xiàn)好多都不靠譜
方案 | 作用 | 瘦身效果 |
---|---|---|
proguard | 代碼混淆 | 效果明顯 |
abiFilter "armeabi" | 去除其他平臺so | 效果明顯 |
resConfigs "zh" | 語言文件去除 | 0.1M |
shrinkResources | 無用資源去除需維護(hù)keep文件 | 1M |
TinyPng | 圖片壓縮呐矾,賬號收費(fèi) | 3M |
ThinR | 移除R文件 | 0.3M |
AndResGuard | 資源混淆白名單維護(hù)難 | 資源混淆0.3M,7zip壓縮2M |
webp | android兼容性差 | 不推薦 |
Lint 無用資源去除 | 有可能刪除getIdentifier調(diào)用的資源 | 不推薦 |
redex | 安全風(fēng)險(xiǎn)高疼约,對于加固躬窜、熱修復(fù)等功能有影響 | 未實(shí)踐 |
so動態(tài)加載 | 風(fēng)險(xiǎn)高捞高,大部分so都需要實(shí)時(shí)加載 | 未實(shí)踐 |
加固 | 隱藏dex | 1M |
重復(fù)資源優(yōu)化 | 對比資源文件 md5,刪除重復(fù)文件和resources.arsc 中的定義 |
0.2M |
移除TINY_PNG文件 | 通過android-chunk-utils把resources.arsc 中對應(yīng)的定義和文件移除,風(fēng)險(xiǎn)高 |
美團(tuán)文章一帶而過甸私,我實(shí)踐一下猪勇,實(shí)際代碼特別復(fù)雜,arsc 文件索引value要重新計(jì)算颠蕴,減小0.1M都不到 |
方案實(shí)踐
Smallapk Gradle插件減小APK體積25%
apply plugin: 'smallapk'
動態(tài)資源查找
其他方案網(wǎng)上都有泣刹,我重點(diǎn)講講SmallApk插件怎么解決getIdentifier方法帶來的動態(tài)資源問題。
- ShrinkResources只能去除小部分無用資源的問題
- 解決AndResGuard需要配置白名單的問題
首先需要了解ShrinkResources的原理:
通過ResourceUseModel建立一個(gè)資源引用樹犀被,找到有可能是resource.getIdentifier調(diào)用的資源標(biāo)記為reachable椅您,找到無用資源并替換成tiny的小文件
用這種方式查找到的動態(tài)資源會特別多,因?yàn)橛谜齽t表達(dá)式匹配了所有的字符串寡键,那么如何精確找到動態(tài)資源呢掀泳,你會發(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插件找到的動態(tài)資源
這個(gè)是找到的動態(tài)資源調(diào)用關(guān)系圖
那么SmallApk是怎么做的呢
思路和android源碼ResourceUsageAnalyzer是一樣的藕畔,都是匹配字符串常量马僻,唯一的區(qū)別就是加入了方法有向圖搜索節(jié)點(diǎn)未辆,排除大部分無用字符串毡琉。
首先形成調(diào)用有向圖
/**
* KeepResUsageVisitor會把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);
}
}
}
}
}
最后匹配字符串常量找到動態(tài)資源
// getResources().getIdentifier("ic_video_codec_" + codecName, "drawable", ...)
for (Resource resource : mModel.getResources()) {
if (resource.name.startsWith(name)) {
mDynamicUsed.add(resource);
}
}
找到動態(tài)資源以后就能去解決AndResGuard和ShrinkResources的問題了
解決ShrinkResources只能去除小部分無用資源的問題沙廉,只要把找到的動態(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需要配置白名單的問題憨颠,只要把動態(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會混淆資源文件名氮趋,xml資源文件里面也使用了文件名的字符串辜御,那為什么apk沒有崩潰鸭你?
因?yàn)榫幾g完以后布局xml文件里變成了int常量,AndResGuard修改的是字符串,int索引沒變
-
proguard也會去除R文件袱巨,那為什么用ThinR還會減小包體積袜茧?
因?yàn)閍ar包里不存在R.class的,app打包的時(shí)候會重新生成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再壓縮會去俺夕,你會發(fā)現(xiàn)這個(gè)apk已經(jīng)沒法安裝了,為什么贱鄙,照理說不做任何操作應(yīng)該不影響apk簽名呀劝贸?
因?yàn)镸AC解壓縮的時(shí)候會存在.DS_Store文件,直接壓縮會把外面的文件夾目錄也壓縮進(jìn)去
-
重新壓縮apk以后體積會小逗宁,為什么apk自己不是壓縮過了嗎映九?
因?yàn)槟J(rèn)圖片是不壓縮的
shrinkResources不是刪除了無用資源嗎,那為什么我用Lint去刪除無用資源瞎颗,包體積還是會變屑?
一個(gè)是資源問題哼拔,一個(gè)是代碼問題引有。
資源問題:shrinkResources匹配字符串常量得到的無用資源會比較少,而lint掃描會只掃描硬靜態(tài)引用資源倦逐,這樣掃描的資源文件會比較多
代碼問題:lint還會刪掉java文件譬正,而shrinkResources只會去除無用資源,雖然android源碼里面二次打包TWO_PASS_AAPT
檬姥,但是默認(rèn)沒開啟android gradle插件默認(rèn)是開啟v2簽名的曾我,為什么在我們的app里面用修改meta-inf文件的方式加入渠道號還可以運(yùn)行?
因?yàn)槲覀兿燃庸蹋缓笾匦聉1簽名健民,再打渠道包抒巢,運(yùn)氣好,剛好繞過了v2簽名的坑秉犹,哈哈zipalign會影響v1簽名和v2簽名嗎虐秦?
請?jiān)趘1簽名后使用zipalign,v2簽名前使用zipalign凤优,v1簽名和v2簽名可以同時(shí)存在悦陋,不能只用v2簽名,因?yàn)樵?.0手機(jī)只會校驗(yàn)v1簽名