今天看啥  ›  专栏  ›  针叶

源码茶舍之由一次简单的ANR分析深入了解Context

针叶  · 掘金  ·  · 2020-01-22 06:32

文章预览

阅读 34

源码茶舍之由一次简单的ANR分析深入了解Context

ANR是Android的老大难了,关于这方面的基础知识和深入好文都非常多,大家不妨谷歌一下。 最近搭载骁龙855的小米9也发布了,移动平台的设备性能越来越强,许多App大多时候其实都吃不完那么多计算资源。 说得可能不好听一点,很多烂代码要是在很多年前的手机上,本该导致卡顿(甚至是ANR)的,但由于如今强大的计算性能,卡顿几率大大减小了。从某方面来说增大了程序的容错,同时也掩盖了程序本身的缺陷。

今天的题目关键词是“简单分析”和“深入了解”,哈哈,可能对于大佬们来说这些内容并不深入,所以我措辞为“了解”,望轻喷。

分析traces文件

前段时间,业务质量平台报上来很多ANR,我是一看就头疼呀!每次心里都犯嘀咕,我怎么就从来没遇到ANR呢?你们到底是怎么使用的。 吐槽归吐槽,问题还是要解决的,Android的系统日志打包上来一般都会有traces.txt文件(还有event log等等,这里给大家硬广一下我另一篇使用可视化的ChkBugreport分析log文件),也是我们分析这类问题的入口,里面记录了各个应用进程和系统进程的函数堆栈信息。于是乎,抓一份来瞧瞧:

"main" prio=5 tid=1 Blocked
group="main" sCount=1 dsCount=0 obj=0x75afba88 self=0x7fb0e96a00
...
at android.app.ContextImpl.getPreferencesDir(ContextImpl.java:483)
- waiting to lock <0x0cfeaaf2> (a java.lang.Object) held by thread 24
at android.app.ContextImpl.getSharedPreferencesPath(ContextImpl.java:665)
at android.app.ContextImpl.getSharedPreferences(ContextImpl.java:364)
- locked <0x09b0b543> (a java.lang.Class<android.app.ContextImpl>)
at android.content.ContextWrapper.getSharedPreferences(ContextWrapper.java:174)
at android.content.ContextWrapper.getSharedPreferences(ContextWrapper.java:174)
...
at com.xxx.receiver.xxx.onReceive(xxx.java:36)
...
复制代码

这里简单解释一下,ANR无非就是UI线程Block了,所以我们找到形如 "main" prio=5 tid=1 Blocked 这样的片段,main表示主线程,prio即priority,线程优先级(这里不是重点),tid就是thread的id,即线程id,最后标记了Blocked,表示线程阻塞了。 接着的信息就是告诉你线程被哪个鬼lock了,关注这行: waiting to lock <0x0cfeaaf2> (a java.lang.Object) held by thread 24 说明主线程的getPreferencesDir方法等着要去锁一个id为0x0cfeaaf2的Object类型的对象,但是被该死的tid=24的线程抢占了!让我来看看是谁,于是我们可以直接在traces文件里全局搜索0x0cfeaaf2或者tid=24这些字符串,锁定到如下日志:

"PackageProcessor" daemon prio=5 tid=24 Native
group="main" sCount=1 dsCount=0 obj=0x32c06af0 self=0x7fb0f36400
...
native: #06 pc 0000000000862c18 /system/framework/arm64/boot-framework.oat (Java_android_os_BinderProxy_transactNative__ILandroid_os_Parcel_2Landroid_os_Parcel_2I+196)
at android.os.BinderProxy.transactNative(Native method)
at android.os.BinderProxy.transact(Binder.java:620)
at android.os.storage.IMountService$Stub$Proxy.mkdirs(IMountService.java:870)
at android.app.ContextImpl.ensureExternalDirsExistOrFilter(ContextImpl.java:2228)
at android.app.ContextImpl.getExternalFilesDirs(ContextImpl.java:586)
- locked <0x0cfeaaf2> (a java.lang.Object)
at android.app.ContextImpl.getExternalFilesDir(ContextImpl.java:569)
at android.content.ContextWrapper.getExternalFilesDir(ContextWrapper.java:243)
at com.xxx.push.log.xxx.writeLog2File(xxx.java:100)
...
复制代码

这里很明显就看到了 locked <0x0cfeaaf2> (a java.lang.Object) ,某个和推送服务相关的writeLog2File方法调用了getExternalFilesDirs,然后此方法进一步锁住了 0x0cfeaaf2 对象,没错,这个对象和刚才主线程等待要锁的对象是同一个。 所以主线程被tid=24的线程阻塞了,因为两个线程需要同一把对象锁,tid=24线程一直占着茅坑,导致死锁,ANR就这么爆出来了。

了解Context

Context是一个抽象类,ContextImpl是Context的实现类(具体一些继承关系可参考Context都没弄明白,还怎么做Android开发?,某大佬写的,比较全面)。 那么,上面的ANR我们重点关注的对象0x0cfeaaf2到底是谁呢?根据这一行: at android.app.ContextImpl.getPreferencesDir(ContextImpl.java:483) 我们直接Read the fucking code,看看ContextImpl中这个方法在干啥:

    private File getPreferencesDir() {
        synchronized (mSync) {
            if (mPreferencesDir == null) {
                mPreferencesDir = new File(getDataDir(), "shared_prefs");
            }
            return ensurePrivateDirExists(mPreferencesDir);
        }
    }
复制代码

可见,这里涉及到shared_prefs文件的IO操作,系统考虑到线程安全,搞了个同步锁,mSync对象被锁住。这个mSync就是我们刚才反复提到的id为0x0cfeaaf2的Object对象,去看看它的实例化就知晓了:

    private final Object mSync = new Object();
复制代码

private final,两个关键字合体了,说明这个成员是不可变的,而且是私有的,不准继承,即在Context的生命周期内全局只实例化一次,这样才能在加锁的时候保证唯一性。 接下来又看刚才tid=24给对象加锁的方法,源码自然也在ContextImpl中:

    @Override
    public File[] getExternalFilesDirs(String type) {
        synchronized (mSync) {
            File[] dirs = Environment.buildExternalStorageAppFilesDirs(getPackageName());
            if (type != null) {
                dirs = Environment.buildPaths(dirs, type);
            }
            return ensureExternalDirsExistOrFilter(dirs);
        }
    }
复制代码

OK,它也有给mSync加锁的操作, 所以tid=24线程的getExternalFilesDirs方法先加锁,造成主线程的getPreferencesDir方法抢不到这把锁,这真是喧宾夺主啊! 你区区一个子线程和主线程作对,分析到此我们基本清楚了这次ANR是怎么来的了。 这里我们进一步看看上面return的ensureExternalDirsExistOrFilter方法:

    /**
     * Ensure that given directories exist, trying to create them if missing. If
     * unable to create, they are filtered by replacing with {@code null}.
     */
    private File[] ensureExternalDirsExistOrFilter(File[] dirs) {
        final StorageManager sm = getSystemService(StorageManager.class);
        final File[] result = new File[dirs.length];
        for (int i = 0; i < dirs.length; i++) {
            File dir = dirs[i];
            if (!dir.exists()) {
                if (!dir.mkdirs()) {
                    // recheck existence in case of cross-process race
                    if (!dir.exists()) {
                        // Failing to mkdir() may be okay, since we might not have
                        // enough permissions; ask vold to create on our behalf.
                        try {
                            sm.mkdirs(dir);
                        } catch (Exception e) {
                            Log.w(TAG, "Failed to ensure " + dir + ": " + e);
                            dir = null;
                        }
                    }
                }
            }
            result[i] = dir;
        }
        return result;
    }
复制代码

我的天鸭,你看看,这操作多重啊,又是循环又是创建文件的,还有getSystemService这些系统服务对端调用,加在一起就是灰常耗时的操作,尤其是在文件目录极其散乱繁杂而且磁盘读写性能还不好的时候,此方法将进一步延长阻塞时间。

我又一想,什么SP啊,DB啊,外部存储啊这些我们平时经常访问啊,也并不是那么容易就ANR的。也就是说虽然上面的系统方法操作很繁杂,但应该不是导致最终问题的核心因素。

经过我反复分析traces文件,发现除了main线程在wait to lock这把锁,还有几个其它的子线程也在等待锁(有一些是访问App本地数据库的,最终调用也在ContextImpl中,和上面分析的两个方法类似)。说明当前这短暂的时间内,需要通过某个Context进行的IO操作太多了,各个线程都排着队要锁mSync,所以耗时操作不可怕,可怕的是一窝蜂全上来。自然就增大了ANR的风险。如果你反复遇到这种ANR,就应该考虑优化了。

最终,追溯到方法调用的源头,是在Application初始化时,各种SDK加载,以及一些业务逻辑触发。很显然,它们都是通过getApplicationContext来拿到的同一个Context引用,请求锁的也是同一个mSync对象。

结论与建议

  • 调用Context相关的IO操作,不是启个子线程就高枕无忧了,由上面分析,mSync对象锁就这么一把,该阻塞还是阻塞,和是不是主线程无关。
  • 尽量不要在Application的初始化时刻进行太多的方法调用,尤其是针对ApplicationContext的IO操作。
  • 在主Activity中延后初始化,用IntentService进行异步操作(因为实例化一个Service就是另一个Context对象了)等都是比较好的优化方案。
  • 所以为什么有大佬说不要滥用SharedPreference,它的性能并不是很好,从本文分析也可知它直接可能阻塞UI线程,试图寻找其它替代品吧。
  • 广播接收onReceive里面可以用goAsync异步处理,见:goAsync帮你在onReceive中简便地进行异步操作
  • ...想到再说,也欢迎大家补充。
………………………………

原文地址:访问原文地址
快照地址: 访问文章快照
总结与预览地址:访问总结与预览