作者简介: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