作者
大家好,我叫小嘉; 本人20年本科毕业于广东工业大学,于2020年6月加入37手游安卓团队;目前工作是国内游戏发行安卓相关开发。
背景
游戏发行领域,我们做的渠道切包是基于下面的原理:
在进行切包的流程中有一个步骤是需要进行smali代码合并,目前我们所做的只是针对一个 smali 文件夹的情况,这样会导致渠道 sdk 在覆盖母包的时候,有些会覆盖不到。比如:
这样会使得同一个smali文件存在于多个smali文件夹中,导致在加载dex的时候,不同dex文件都有同一个类,那么假如这个类在不同版本里面类实现存在差异,就有可能会导致执行调用的时候出错。
于是便修改了整个打包的流程:
(自动打包博客指引:juejin.cn/post/690752…
其中优化了自动打包的流程:
1.自动分task 作为必执行阶段后,会先对当前 smali 方法数进行判断,假如不会超过,就不会执行分包操作,只是多了计算的时间
2.自动分包task 执行后,会再次执行 smali 方法数目判断,如果会超出(因为目前的分包逻辑是根据获取 Application 以及清单文件中的四大组件的索引所有类来作为第一个smali 的内容,不是直接根据方法数来做判断,所以有可能会超出),就进行以方法数目为根据的分包。
这样执行下来打包的时候就不会再有 65535 问题。
整个修改流程中一个比较重要的是判断方法数: 原先的做法是:扫描 smali文件中的内容: 通过 .end method 字段来计数,本能的理解为这就是方法数的计数。 但是尝试过几次后发现不准,严格来说,是小了很多。
计数方案
关于官方给出的就是一个简单的说法,就是引用的函数个数(不重复的)。 针对于该说法,进行了一些实现。
- 最终得出结论: 有定义并且未被调用的方法 + 被调用的方法,并且都只保留一次
怎么判定写的方法计数和 最终生成dex后引用的个数是不是一样呢? AndroidStudio 的 APK Analyzer 的在分析一个dex的时候,有一个reference method count,该数字就是最后的计算结果:
做法:得出 apk 中的 dex 的引用方法后,转化为 smali 文件,用自己写的算法结果来比对得出算法是否正确。
算法思路:
目标:实现有定义并且未被调用的方法 + 被调用的方法,并且都只保留一次,计算总共的次数
因为都只保留一次,所以适合采用set数据接口,对于重复只算一次。
在smali语法里面:
方法调用是用 .invoke-xxxx 来表示:
比如:
invoke-static {v2, v3}, Lcom/netease/ntunisdk/base/UniSdkUtils;->e(Ljava/lang/String;Ljava/lang/String;)V
此句表达的是 调用 UniSdkUtils 类的 e(String,String)静态方法,返回值为空
方法定义是 .method xxxx 来表示:
比如:
.method public static obj2Json(Lcom/netease/ntunisdk/base/AccountInfo;)Lorg/json/JSONObject;
此句表达的是定义了一个 obj2Json(AccountInfo) 返回值为 JSONObject 的public方法
由于不同类的方法签名有可能一样,所以需要类+方法的形式来做区分。
- 转化为算法就是:
找到smali文件后读取内容:
-
新建一个 set 数据结构,保证有定义的方法并且和被调用的方法是同一个的情况下,只计算一次。
-
找到以.class 开头的内容,截取当前类名: Lcom/test/test_class;
-
找到以.method 开头的内容,转译为类名+“->”+函数名+字段类型+返回格式
.method public constructor ()V
转译为: Lcom/test/test_class;->()V
将该字符串加入 set 数据结构 -
找到以.invoke- 开头的内容,直接保留
Lcom/test/test_class;->()V 将该字符串加入 set 数据结构
代码实现
/*
* dfs 遍历,当前smali的引用方法数有多少,超过sum数目的话,返回需要移动的文件
public static boolean dfsByStack(String path,int sum,List<String> list){
HashSet<String> hashSet = new HashSet<String>(70000);
int current = 0;
File file = new File(path);
if (!file.exists()) return false;
Stack<String> stack = new Stack<>();
stack.push(path);
while (!stack.isEmpty()){
String path1 = stack.pop();
File file1 = new File(path1);
if (file1.exists()){
if (file1.isFile()){
String className = "";
List<String> lines = FileUtil.readAllLines(file1.getAbsolutePath());
if (lines == null) continue;
for (String line : lines){
line = trimStart(line);
//1 找到类名
if (line.startsWith(".class")){
String[] content = line.split(" ");
className = content[content.length-1];
}
//2 找到方法名
if (line.startsWith(".method")){
String[] content = line.split(" ");
String method = content[content.length-1];
hashSet.add(className + "->" + method);
}
//3 找到调用方法
if (line.startsWith("invoke-")){
String[] content = line.split(" ");
String method = content[content.length-1];
hashSet.add(method);
}
}
if (hashSet.size() > sum){
System.out.println("hashSet: " + hashSet.size());
System.out.println("current: " + current);
return true;
}
current = hashSet.size();
if (list != null) {
if (current <= sum /2) {
list.add(file1.getAbsolutePath());
}
}
}
if (file1.isDirectory()){
for (File path2: file1.listFiles()){
stack.push(path2.getAbsolutePath());
}
}
}
}
System.out.println("current: " + current);
return false;
}
小结
本次博客主要讲解之前打包机实现上的一些问题,提出了改进方法后,对自动分包进行优化,讲解了 dex 文件中方法计数的实现,并在 smali 语境下给出了代码实现。