前言
合并方法指的是把多个方法合并成一个方法,从而达到减少方法数量,增加分析难度的目的。
前面提到过,Android中Dex有65536个方法数量的限制,因而进行方法合并有利于缓解此限制。另外上一章提到的重构方法方案会大量的增加方法,如果把增加的方法进行合并,那么可缓解增加方法的数量。
一些问题
合并方法时,需要修改合并后的方法签名,而有些方法是对外提供的接口,是不能合并的。如Android中的Activity等四大组件中继承的一些特定方法,插件化框架中对外提供的接口等等。
被合并方法选择
1、只要被Proguard混淆过的方法,大部分都可以进行合并,而未混淆的代码,则由于可能有各种各样的潜规则(如JNI或其它地方反射调用该方法、作为插件化框架所提供的接口等),而需要通过配置Keep住。
2、没继承关系的方法,皆可进行处理。
3、有继承关系的方法,虽有部分能处理,但经测试量较少,且处理成本非常大,因而不建议处理。
4、重构产生的方法,均可以进行合并处理
合并原理(基于bytecode)
1、实例方法–>静态方法:
前:
1 | class Book { |
1 | class com/tencent/Book { |
后:
1 | class Book{ |
1 | class com/tencent/Book { |
我们看到,实例方法转静态方法基本没什么要改的,只需要把方法的权限改为静态的,然后在方法参数最前面增加一个当前类的对象参数(即this指针)。
2、多个方法–>一个方法(通用方案):
合并前:
1 | static long add(int a, long b) { |
1 | static add(IL)L |
1 | static long print(String str, long k) { |
1 | static print(Ljava/lang/String;L)L |
合并后:
1 | static long merge(int which, int a, long b, String str) { |
1 | static merge(IIJLjava/lang/String;)J |
这个方案支持任意两个方法进行合并,原理是把第一个参数作为识别当前调用的是哪个方法,接着通过switch语句来跳转到对应的代码,然后把新方法的参数加载到对应的栈中,并重新保存成原方法需要的参数位置,这样原方法的代码就全部都不用改了。
注意下,这里用于识别旧方法的参数也可以放在最后面,如果放到最后面,且参数位置安排得当的话,可以减少一些修复指令。
上面限定了返回值一致的方法才能合并,其实返回值不一致的也能合并,不过还需要增加不少额外的处理。这里建议返回值为基本数据类型时,需要一致才进行合并,如果返回值为对象类型时,就返回Object就行,这样只需在修复调用该方法的返回值时增加一个强制类型转换。
同理,如果参数为对象类型,那么都可以当作Object类型,然后再在修复时转换为相应的类型。
3、多个方法–>一个方法(start with方案):
合并前:
1 | static long add(int a, long b) { |
1 | static add(IL)L |
1 | static long print(int m, long k, String str) { |
1 | static print(Ljava/lang/String;L)L |
合并后:
1 | static long merge(int a, long b, String str, int which) { |
1 | static merge(IJLjava/lang/String;I)J |
这个方案要求待合并的方法的参数必须是start with类型(以最长的方法参数为基准,其它方法的参数必须与其前面的参数一一对应)。假设一个最长的方法的参数为IJI,那么参数为IJI、IJ、I的方法才能和它进行合并。
显然,这种方案少了很多修复指令,不过会有一定限制,但经测试发现,使用这种方案进行合并后方法数比通用方案只差一点点,但增加的体积相对通用方案明显少了很多,因此建议优先使用。
4、可内联方法插入到方法调用的位置:
内联方法:
1 | static long add(int a, long b) { |
1 | static add(II)I |
调用方法:
1 | static int count(int a, int b, int c) { |
1 | static count(III)I |
内联方法指令合并到调用方法后:
1 | static int count(int a, int b, int c) { |
1 | static count(III)I |
内联方法是指指令比较少的方法,上例中通过增加本地变量来模拟内联方法的参数,当然,使用到参数的指令都需要一一进行修改,如果内联方法有临时变量,也需要把临时变量的位置修改一下。
合并方案说明
1、通用方案会增加非常多的指令,以致于dex体积增加,而除了65536方法数限制外,还有个5M线性内存的限制,因此通用方案可能会导致提前达到5M的限制,不建议使用。
2、内联方法方案在处理插件化框架中,修复成本非常高,且经测试,可内联的方法比例非常低。
3、上面方案中,都是针对没继承关系的来处理,并没列出有继承关系的处理方案,当然有兴趣的可找我拿或自行研究下。这里不列出是因为此方案能处理的方法比例非常不高,且处理成本非常大,因而不建议使用。