前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深入Android Runtime: inline优化与字符串

深入Android Runtime: inline优化与字符串

作者头像
天天P图攻城狮
发布2018-10-25 21:54:16
1.8K3
发布2018-10-25 21:54:16
举报
文章被收录于专栏:天天P图攻城狮天天P图攻城狮

作者简介:dc, 天天P图AND工程师


奇怪的现象?

先看下面一段apk的代码:

代码语言:javascript
复制
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类代码为:

代码语言:javascript
复制
public class Test {    
    public static String test1(){
        String s = "test1";        
        return s;
    }

}

如果我们运行一下,文本框会显示以下结果:test1

如果我们给apk的PathClassLoader的classpath最开始注入一个dex文件,这个dex代码如下:

代码语言:javascript
复制
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

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2018-10-25,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 天天P图攻城狮 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 奇怪的现象?
  • 现象解释
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档