源码茶舍之由一次简单的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中简便地进行异步操作
  • …想到再说,也欢迎大家补充。
发布了91 篇原创文章 · 获赞 204 · 访问量 54万+
展开阅读全文

安卓7.1的原生Launcher2无法运行

07-12

我最近用安卓7.1代码里的Launcher2取代原来的Launcher后,开机报错,launcher强制停止, 大神们帮忙看看是什么问题,感激不尽.. 01-01 00:02:31.297 E/AndroidRuntime( 7262): FATAL EXCEPTION: setWallpaperDimension 01-01 00:02:31.297 E/AndroidRuntime( 7262): Process: com.android.launcher, PID: 7262 01-01 00:02:31.297 E/AndroidRuntime( 7262): DeadSystemException: The system died; earlier logs will point to the root cause 01-01 00:02:31.299 W/ActivityManager( 4362): Process com.android.launcher has crashed too many times: killing! 01-01 00:02:31.299 W/ActivityManager( 4362): Force finishing activity com.android.launcher/com.android.launcher2.Launcher 01-01 00:02:31.304 I/ActivityManager( 4362): START u0 {act=android.intent.action.MAIN cat=[android.intent.category.HOME] flg=0x10000100 cmp=com.android.launcher/com.android.launcher2.Launcher} from uid 0 on display 0 01-01 00:02:31.306 W/WindowManager( 4362): Failed looking up window 01-01 00:02:31.306 W/WindowManager( 4362): java.lang.IllegalArgumentException: Requested window android.view.ViewRootImpl$W@8d24662 does not exist 01-01 00:02:31.306 W/WindowManager( 4362): at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:9415) 01-01 00:02:31.306 W/WindowManager( 4362): at com.android.server.wm.WindowManagerService.windowForClientLocked(WindowManagerService.java:9406) 01-01 00:02:31.306 W/WindowManager( 4362): at com.android.server.wm.WindowManagerService.removeWindow(WindowManagerService.java:2387) 01-01 00:02:31.306 W/WindowManager( 4362): at com.android.server.wm.Session.remove(Session.java:193) 01-01 00:02:31.306 W/WindowManager( 4362): at android.view.ViewRootImpl.dispatchDetachedFromWindow(ViewRootImpl.java:3290) 01-01 00:02:31.306 W/WindowManager( 4362): at android.view.ViewRootImpl.doDie(ViewRootImpl.java:5917) 01-01 00:02:31.306 W/WindowManager( 4362): at android.view.ViewRootImpl$ViewRootHandler.handleMessage(ViewRootImpl.java:3626) 01-01 00:02:31.306 W/WindowManager( 4362): at android.os.Handler.dispatchMessage(Handler.java:102) 01-01 00:02:31.306 W/WindowManager( 4362): at android.os.Looper.loop(Looper.java:154) 01-01 00:02:31.306 W/WindowManager( 4362): at android.os.HandlerThread.run(HandlerThread.java:61) 01-01 00:02:31.306 W/WindowManager( 4362): at com.android.server.ServiceThread.run(ServiceThread.java:46) 01-01 00:02:31.309 I/ActivityManager( 4362): Killing 7262:com.android.launcher/1000 (adj 0): crash 01-01 00:02:31.309 D/ActivityManager( 4362): cleanUpApplicationRecord -- 7262 01-01 00:02:31.310 I/libprocessgroup( 4362): Killing pid 7281 in uid 1000 as part of process group 7262 01-01 00:02:31.325 I/ActivityManager( 4362): Start proc 7284:com.android.launcher/1000 for activity com.android.launcher/com.android.launcher2.Launcher 01-01 00:02:31.353 W/System ( 7284): ClassLoader referenced unknown path: /system/priv-app/Launcher2/lib/arm64 01-01 00:02:31.357 W/art ( 7284): JIT profile information will not be recorded: profile file does not exits. 01-01 00:02:31.366 W/ContextImpl( 7284): Failed to ensure /data/user_de/0/com.android.launcher/code_cache: mkdir failed: EACCES (Permission denied) 01-01 00:02:31.386 D/FrameBuffer( 4029): GLES, req offset: 1080 01-01 00:02:31.539 W/WallpaperManager( 7284): WallpaperService not running 01-01 00:02:31.539 E/AndroidRuntime( 7284): FATAL EXCEPTION: setWallpaperDimension 01-01 00:02:31.539 E/AndroidRuntime( 7284): Process: com.android.launcher, PID: 7284 01-01 00:02:31.539 E/AndroidRuntime( 7284): DeadSystemException: The system died; earlier logs will point to the root cause 01-01 00:02:31.541 W/ActivityManager( 4362): Force finishing activity com.android.launcher/com.android.launcher2.Launcher 问答

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览