ANR是Android的老大难了,关于这方面的基础知识和深刻好文都很是多,你们不妨谷歌一下。 最近搭载骁龙855的小米9也发布了,移动平台的设备性能愈来愈强,许多App大多时候其实都吃不完那么多计算资源。 说得可能很差听一点,不少烂代码要是在不少年前的手机上,本该致使卡顿(甚至是ANR)的,但因为现在强大的计算性能,卡顿概率大大减少了。从某方面来讲增大了程序的容错,同时也掩盖了程序自己的缺陷。java
今天的题目关键词是“简单分析”和“深刻了解”,哈哈,可能对于大佬们来讲这些内容并不深刻,因此我措辞为“了解”,望轻喷。android
前段时间,业务质量平台报上来不少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就这么爆出来了。bash
Context是一个抽象类,ContextImpl是Context的实现类(具体一些继承关系可参考Context都没弄明白,还怎么作Android开发?,某大佬写的,比较全面)。 那么,上面的ANR咱们重点关注的对象0x0cfeaaf2究竟是谁呢?根据这一行: at android.app.ContextImpl.getPreferencesDir(ContextImpl.java:483) 咱们直接Read the fucking code,看看ContextImpl中这个方法在干啥:app
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中:ide
@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对象。