作者简介:dc, 天天P图AND工程师
先看下面一段apk的代码:
public class MainActivity extends AppCompatActivity {
? ?Button button;
? ?TextView textView; ? ?
? ?@Override
? ?protected void onCreate(Bundle savedInstanceState) { ? ? ? ?super.onCreate(savedInstanceState);
? ? ? ?setContentView(R.layout.activity_main);
? ? ? ?initView();
? ?} ? ?
? ?void initView() {
? ? ? ?textView = findViewById(R.id.text);
? ? ? ?button = findViewById(R.id.button);
? ? ? ?button.setOnClickListener(new View.OnClickListener() { ? ? ? ? ? ?@Override
? ? ? ? ? ?public void onClick(View v) {
? ? ? ? ? ? ? ?String s = Test.test1();
? ? ? ? ? ? ? ?textView.setText(s);
? ? ? ? ? ?}
? ? ? ?});
? ?}
}
其中Test类代码为:
public class Test { ? ?
? ?public static String test1(){
? ? ? ?String s = "test1"; ? ? ? ?
? ? ? ?return s;
? ?}
}
如果我们运行一下,文本框会显示以下结果:test1
如果我们给apk的PathClassLoader的classpath最开始注入一个dex文件,这个dex代码如下:
public class Test {
? ?String a = "string a";
? ?String b = "string b";
? ?String c = "string c";
? ?String d = "string d";
? ?String e = "string e";
? ?String f = "string f";
? ?String g = "string g";
? ?String h = "string h";
? ?String i = "string i";
? ?String j = "string j";
? ?String k = "string k";
? ?String l = "string l";
? ?String m = "string m";
? ?String n = "string n";
? ?String o = "string o";
? ?String p = "string p";
? ?String q = "string q";
? ?String r = "string r";
? ?String s = "string s";
? ?String t = "string t";
? ?String u = "string u";
? ?String v = "string v";
? ?String w = "string w";
? ?String x = "string x";
? ?String y = "string y";
? ?String z = "string z"; ? ?
? ?public static String test1(){
? ? ? ?String s = "test1"; ? ? ? ?return s;
? ?}
}
此时再次执行apk,是否会和之前的结果一样呢?
实际上,刚开始执行的时,结果还是一样的,如果你的apk运行的次数足够多,几天之后,你就会发现,程序再也不能正常运行了,会直接crash掉,日志如下:
这有点超出正常的认知,明明定义了字符串test1,并且只有简单的2行代码,为什么会crash呢?
要解释这种现象,需要了解Android虚拟机字符串处理机制。
在Android中,字符串是存在dex文件中的,以String表进行存储,通过StringID可以查找到对应的String。这些String除了包含我们定义的字符串常量,还包括变量名、方法名、类名等等。
正常情况下,之前的test1方法对应指令如下:
而我们调用的代码如下:
通过方法索引16903,虚拟机可以找到test1方法,然后通过test1找到字符串索引20194,再找到正确的字符串,这样运行的结果就是正确的。
如果我们注入了另外一个包含相同类的dex文件,那么如果是在解释模式下执行,调用test1时,就会在新的dex中找到test1方法,而这个test1方法中的字符串索引是相对于这个dex而言的,而不是apk中字符串表里的索引,此时能正确找到字符串并得到正确的运行结果。
但是尽管apk安装时会以interpret-only方式进行了优化(见前一篇文章),仍然是以解释模式运行,那么不可避免method调用次数达到一定阈值时触发JIT编译。或者后台根据method执行的profile信息进行speed-profile编译,那么,就会导致按钮点击com.tencent.mytest.MainActivity$1.onClick
这个方法以极大概率编译成机器码,也就是如下的指令:
编译成机器码一般情况下不会有什么问题,但是由于其调用的test1方法过于短小,字节码指令数目有限,会被编译器进行inline优化。也就是红框中的0x4ee2(对应test1中的字符串索引20194)被写入了机器码。
这样我们编译时产生的机器码实际上依赖的是早先apk自身的Test类的代码,而运行的时候是执行的注入dex中的代码,虚拟机在解析这个0x4ee2字符串索引时候,会从注入的dex的字符串常量池中查找,实际上这个dex的字符串数目是非常少的,尽管我们在代码里面添加了26个新的字符串。
由于无法通过索引0x4ee2找到字符串,虚拟机会在产生一个无效的地址,这个地址指向的也许是另外一个字符串,也许指向的是一块非法的内存,那么我们再将这个字符串读出来写到文本框时,就会引发不可预知的异常(代码里的String s很可能不是字符串的内存了)。
我们使用不同jar/dex中新的class覆盖旧的class时,需要注意,在inline场景下,编译器会将一些索引硬编码到机器码中,导致与运行时的数据不一致。归根结底,是编译时依赖与运行时依赖jar/dex不一致引发的问题,需要格外注意。
另外,Android P上Google已经对跨dex的inline进行了限制,会直接abort,因此热修复相关技术可能会出现crash,具体见《通告 | Android P新增检测项 应用热修复受重大影响》
文章后记: 天天P图是由腾讯公司开发的业内领先的图像处理,相机美拍的APP。欢迎扫码或搜索关注我们的微信公众号:“天天P图攻城狮”,那上面将陆续公开分享我们的技术实践,期待一起交流学习!
加入我们: 天天P图技术团队长期招聘: (1) AND / iOS 开发工程师?(2) 图像处理算法工程师? 期待对我们感兴趣或者有推荐的技术牛人加入我们(base 上海)!联系方式:ttpic_dev@qq.com