ANR 实践集锦


前言

        本文不会讲述ANR 类型、如何分析 ANR trace文件,ANR 发生原理等,因为这些网上已有很多了,本文重点讲述的是亲身经历过的一些经验,意在记录个人在学习和项目过程中遇到的 ANR 问题以及如何解决这些 ANR 问题的个人心得,希望和各位看官一起探讨~

        应用的卡顿、ANR 性能问题除了和我们编码息息相关,设备等级、系统环境因素也占据了半壁江山。对于“系统问题”,我们是否无作为就好了? 其实我们可以在应用层面做好最佳编码姿势~


一、什么是 ANR

ANR 指的是应用进程无响应,ANR 发生率和崩溃率一样是衡量应用质量的核心指标。

二、ANR 实践

1.SharePreference ANR:java.io.FileDescriptor.sync (FileDescriptor.java)

问题堆栈:

java.io.FileDescriptor.sync (FileDescriptor.java)
android.os.FileUtils.sync (FileUtils.java:256)
.SharedPreferencesImpl.writeToFile (SharedPreferencesImpl.java:807)
.SharedPreferencesImpl.access$900 (SharedPreferencesImpl.java:59)
.SharedPreferencesImpl$2.run (SharedPreferencesImpl.java:672)
.QueuedWork.processPendingWork (QueuedWork.java:265)
.QueuedWork.waitToFinish (QueuedWork.java:178)
.ActivityThread.handleServiceArgs (ActivityThread.java:4977)
.ActivityThread.access$2300 (ActivityThread.java:284)
.ActivityThread$H.handleMessage (ActivityThread.java:2322)
android.os.Handler.dispatchMessage (Handler.java:106)
android.os.Looper.loopOnce (Looper.java:233)
android.os.Looper.loop (Looper.java:334)
.ActivityThread.main (ActivityThread.java:8396)
java.lang.reflect.Method.invoke (Method.java)
com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:582)
com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1068)

该类问题是使用原生 SharePreference 固有的缺陷导致的,

  • SharePreference 的 commit 方式会阻塞调用的线程引发 ANR。
  • SharePreference 的 apply 方法是异步添加任务,虽然不会阻塞调用的线程,但是如果写入任务比较耗时或者提交的任务较多,ActivityThread 执行handleStopActivity 时会通过 waitToFinish 等待这些异步任务完成——容易造成 onStop 执行时间较长,触发 ANR。

最佳实践:

使用 MMKV 代替安卓原生SharePreference

2. 静态广播 ANR 

问题堆栈:

Native method - android.os.MessageQueue.nativePollOnce
Broadcast of Intent { act=android.intent.action.MEDIA_SCANNER_SCAN_FILE}

项目应用中有媒体扫描文件的需求,因此静态注册了一个广播, 结果上线后该静态广播引发的 ANR 牢牢占据 ANR 问题列表 Top 5

<receiver
      android:name=".receiver.CustomMediaScannerReceiver"
      android:enabled="true"
      android:exported="true">
      <intent-filter>
        <action android:name="android.intent.action.MEDIA_SCANNER_STARTED"/>
        <data android:scheme="file"/>
      </intent-filter>
      <intent-filter>
        <action android:name="android.intent.action.MEDIA_SCANNER_FINISHED"/>
        <data android:scheme="file"/>
      </intent-filter>
      <intent-filter>
        <action android:name="android.intent.action.MEDIA_SCANNER_SCAN_FILE"/>
        <data android:scheme="file"/>
      </intent-filter>
    </receiver>

 对于这个媒体扫描广播,我们是否可以通过动态注册来解决?——通过动态注册需要等应用启动后进行,这个时候再扫描,时机变慢,影响用户体验。我们更多希望用户启动应用后,媒体扫描已经完成。

幸好该媒体扫描广播的功能相对独立,我们比较容易地把这个媒体扫描静态广播相关的逻辑拆分出来,然后开启一个子进程,在子进程进行处理。既分摊了主进程的任务,也降低了发生 ANR 的概率。

最佳实践:

少用或者不用静态广播。对于一定要静态注册的广播,笔者的思路是拆分进程,把这些静态广播注册到一个独立进程,解放原来的主进程。这一措施验证可行,大大降低项目中静态广播 ANR 的发生率。

3. 动态注册SCREEN_ON 和 SCREEN_OFF 广播 ANR

问题堆栈:

Native method - android.os.MessageQueue.nativePollOnce
Broadcast of Intent { act=android.intent.action.SCREEN_OFF }

        该类问题一度成为我们项目的 Top 1 ANR 问题。SCREENON_ON 和 SCEREEN_OFF 是通过动态注册的,但是它们是有序广播,因此有机会发生 ANR。

最佳实践:

有序广播有机会发生ANR,动态注册的非有序广播不会ANR。因此使用PowerManager 代替 动态注册 SCREEN_ON 和  SCREEN_OFF  来监听屏幕亮屏。

PowerManager pm = (PowerManager) getContext().getApplicationContext().getSystemService(Context.POWER_SERVICE);
final boolean isScreenOn = pm.isScreenOn();

按上述方案在自己业务使用了 PowerManager 代替 SCREEN_ON 和 SCREEN_OFF 广播来监听屏幕亮屏后,本以为会消除 Top 1 ANR 问题,带来巨大收益。谁知道在 Google Play 性能监控平台上这类问题还是居高不下。

自己业务已经没有注册 SCREENON_ON 和 SCEREEN_OFF 广播,为什么还会上报这类问题?会不会是三方 SDK 带入的?我们发现项目中依赖了 google ad 做商业化,是 google ad 里注册了 SCREENON_ON 和 SCEREEN_OFF 广播。

一开始我们觉得三方 SDK 我们改不动了,但是想了下,google ad 监听屏幕亮屏的目的是什么?广告频控?埋点上报?至少对业务来说去掉了也不会影响功能,于是我们做了一个尝试。重写Application 的 registerReceiver 方法,把 SCREENON_ON 和 SCEREEN_OFF 剔除。

@Override
    public Intent registerReceiver(@Nullable BroadcastReceiver receiver, IntentFilter filter) {
        IntentFilter replaceFilter = new IntentFilter();
        int countActions = filter.countActions();
        for(int i= 0; i< countActions; i++) {
            String action = filter.getAction(i);
            if(Intent.ACTION_SCREEN_ON.equals(action) || Intent.ACTION_SCREEN_OFF.equals(action)) {
                continue;
            }
            replaceFilter.addAction(action);
        }
        return super.registerReceiver(receiver, replaceFilter);
    }

类似上述代码,在基类 Application,基类 Activity 重写所有的 registerReceiver 方法,剔除掉 SCREENON_ON 和 SCEREEN_OFF。

该方案剑走偏锋,是把双刃剑,在采取这种骚操作之前一定要充分评估对自己项目功能、营收和数据埋点等是否有影响,三思而后行,不然可能埋坑~

对我们项目来说,功能、营收和数据埋点影响不大,具有可行性,并且也把 Top 1 ANR 问题消除了。

4. com.tencent.mmkv.MMKV.getMMKVWithID ANR

问题堆栈:

android 监听系统字体变化 安卓监听屏幕变化_java

使用了MMKV 代替了 原生 SP ,java.io.FileDescriptor.sync (FileDescriptor.java) 的ANR 问题消失了,但是又有了新的堆栈?真头疼。并且这个问题有人在MMKV github 上提过issue:ANR in MMKV · Issue #454 · Tencent/MMKV · GitHub,不过没有好的解决方案。

我们看一下 MMKV.mmkvWithID 内部到底做了啥:

android 监听系统字体变化 安卓监听屏幕变化_ide_02

可以看到 MMKV.mmkvWithID 是有文件操作的,很多时候,我们在业务模块初始化的时候先调用 getSharedPreferences初始化好一个Sp,而业务模块基本在 Application 初始化的时候调用,团队庞大的时候,业务模块会越来越多,Application 初始化的时候在主线程做的事情太多了,有可能 MMKV.mmkvWithID 文件操作时被卡住,进而引发了 ANR。

最佳实践:

搞一个后台任务去调用 getSharedPreferences  即可提前建立缓存——子线程操作,不影响主线程。

实践证明,如果提前建立了缓存,后续即使在主线程调用 getSharedPreferences,也不会发生 ANR。

5. 启动服务startForegroundService ANR

问题堆栈:

Native method - android.os.MessageQueue.nativePollOnce
Context.startForegroundService() did not then call Service.startForeground()

 Android 8.0 后 谷歌做了限制:后台应用启动服务需要通过  Context.startForegroundService() ,

并且调用 Context.startForegroundService() 创建服务后需要在5秒内调用 startForeground() 发一个通知栏,超过这个限定的时间未调用,系统就会抛ANR。

最佳实践:

在后台启动服务的场景下,服务被创建onCreate时, startForeground() 发一个通知栏。

但是,我们的性能监控平台还是捕获到启动服务 ANR 

问题堆栈:

android 监听系统字体变化 安卓监听屏幕变化_ide_03

问题发生在 Binder 调用,系统服务超时了。

遇到系统繁忙导致的 ANR ,有办法根治? 笔者认为这种类型的 ANR 问题没有固定的复现路径,而且是综合性原因导致的,在后面的阐述中有一个小节介绍综合性方案——降低CPU、运行时内存、分多进程等。

最佳实践:

1)减少 StartService 的次数,公司项目是一个关于音乐的App,在切换歌曲,暂停播放的时候就重复 StartService。笔者较早前分析过 重复启动同一个服务 会不会有多次的 binder 调用Android 短时间内多次启动同一个Service会不会有多次的binder调用_android service多次启动_我不勤奋v的博客-

2)业务允许的情况下,使用 BinderService 代替 StartService

6. androidx.core.content.FileProvider.getPathStrategy ANR

问题堆栈:

android 监听系统字体变化 安卓监听屏幕变化_性能优化_04


 这种类型的 ANR 发生在系统 11 以上的手机,相信有不少人遇过。从堆栈可以看到,应用启动时,Install Contentproviders 时发生。那Install ContentProviders 做了什么呢?  上面的堆栈其实可以看出了个大概:

FilePrivider.getPathStrategy -> ContentCompat.getExternalCacheDirs -> File.exist

看到了吧,又是 IO 操作,那么是不是 IO 操作就一定 ANR ?  不是的,在好的设备,系统空闲场景下主线程 IO 也不一定发生卡顿。归根到底,还是综合性因素有关——如果用户手机设备内存很低又或者用户手机设备还开启了其他应用,想想这个时候 IO 操作不会有概率被卡住么?

那是否这种问题没有办法解决了? 笔者这里介绍一种实践过并且有效的:延后调用getPathStrategy, 在ContentProvider 调用 query 时再调用。

通过查看 FileProvider 源码发现,只要 ProviderInfo的grantUriPermissions为 fasle, 就可以拦截getPathStrategy 的调用。然后后续调用FileProvider的query、openFile、delete等接口前再反射调用 getPathStrategy 。

android 监听系统字体变化 安卓监听屏幕变化_性能优化_05

 最佳实践:

public class MyFileProvider extends FileProvider {
    private boolean mIsPathStrategyInit = false;
    private ProviderInfo mProviderInfo;
    @Override
    public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
        hookAttachInfo(context, info);
    }
    
    private void hookAttachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
        mProviderInfo = info;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            boolean grantUriPermissions = info.grantUriPermissions;
            info.grantUriPermissions = false;
            try {
                super.attachInfo(context, info);
            } catch (Throwable e) {
                e.printStackTrace();
            }
            info.grantUriPermissions = grantUriPermissions;
        } else {
            super.attachInfo(context, info);
        }
    }

    private synchronized void callGetPathStrategy() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
            if (!mIsPathStrategyInit) {
                Class<?> classType = FileProvider.class;
                try {
                    Method method = classType.getDeclaredMethod("getPathStrategy", new Class[]{Context.class, String.class});
                    method.setAccessible(true);
                    Object mStrategy = method.invoke(this, new Object[]{getContext(), mProviderInfo.authority.split(";")[0]});
                    Field strategy = classType.getDeclaredField("mStrategy");
                    strategy.setAccessible(true);
                    strategy.set(this, mStrategy);
                    mIsPathStrategyInit = true;

                } catch (Throwable e) {
                    e.printStackTrace();
                }
            }
        }
    }

    @Override
    public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection,
                        @Nullable String[] selectionArgs,
                        @Nullable String sortOrder) {
        callGetPathStrategy();
        return super.query(uri, projection, selection,
                selectionArgs,
                sortOrder);
    }


    @Override
    public int delete(@NonNull Uri uri, @Nullable String selection,
                      @Nullable String[] selectionArgs) {
        callGetPathStrategy();
        return super.delete(uri, selection, selectionArgs);
    }

    public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
        callGetPathStrategy();
        return super.openFile(uri, mode);
    }

    @Override
    public String getType(@NonNull Uri uri) {
        callGetPathStrategy();
        return super.getType(uri);
    }
}

 7. .ContextImpl.getExternalCacheDirs  ANR

问题堆栈:

android 监听系统字体变化 安卓监听屏幕变化_ide_06

此类问题发生在获取文件路径时,对于这类问题,我们一般的思路就是做缓存,因为像Context.getFilesDir、Context.getCacheDir 获取的路径都是固定的,可以建立好缓存,避免后续每次调用发生 binder 调用。

减少 Binder 调用,或者业务允许的情况下在子线程进行 binder 调用是解决 ANR 的有效手段之一。 

最佳实践:建立文件路径缓存

private static String mAppFilesPath;
  public static String getAppFilesPath(Context context) {
    if(TextUtils.isEmpty(mAppFilesPath)) {
      mAppFilesPath = context.getFilesDir().getPath();
    }
    return mAppFilesPath;
  }

是不是这种文件路径不会改变呢? —— 插拔SD card 的时候有可能发生变化,因此我们可以监听 SDCard 插拔进行重置。目前国内手机设备基本都没有外插 SDCard,海外手机设备可能多些,不过插拔 SDCard 操作概率较低,在应用运行过程中进行 SDCard 插拔概率更低,所以此方案是比较安全的并且实践证明对降低此类 ANR 问题有效。

8.android.net.IConnectivityManager$Stub$Proxy.getActiveNetworkInfo

问题堆栈:

android 监听系统字体变化 安卓监听屏幕变化_java_07

  这类问题和 7一样,都是binder 调用,我们采用的措施是建立缓存,减少调用次数。网络状态我们可以在应用层面缓存和监听更新网络变化的,不需要每次使用时候 binder 调用查询系统服务。一些业务逻辑如果考虑不周会频繁调用查询网络状态。

最佳实践:

        建立网络状态缓存 + 监听网络变化

9. java.lang.Thread.nativeCreate (Thread.java)

 问题堆栈:

android 监听系统字体变化 安卓监听屏幕变化_性能优化_08

笔者第一次看到这种堆栈也懵逼,什么? 创建线程也会发生ANR?

不过想到我们用户群体的设备中低端,也见怪不怪了,其实这类问题在高端设备也有可能发生。我们的措施是统一业务线程池,做好线程复用。想想看,每个业务都有自己的线程池轮子,那无疑增加了创建线程的风险。

创建线程是需要跟系统申请资源的,一般正常情况下,创建线程不会轻易发生 ANR。但是在设备系统资源紧缺的时候,设备系统繁忙的时候,你来创建线程,你不得等待下嘛,等着等着就发生卡顿、ANR 了......

由此可见,设备因素,系统环境因素是性能好坏的关键因素,那是不是意味着我们应用层面不用做什么事情了,反正通通归类为“系统问题”。  我们应用层面能做的就是做好最佳编码姿势~

最佳实践:

        统一业务线程池,复用线程。

10. NativePollOnce ANR

nativePollOnce ANR 相信是大多数 App 的Top 1 问题了,该类问题难在没有任何的业务堆栈,无从入手

问题堆栈:

android 监听系统字体变化 安卓监听屏幕变化_性能优化_09

该类问题,字节跳动技术团队做了分析,笔者不班门弄斧了,详细见

这里按自己的理解概括下:

系统服务创建服务,广播,Input 事件等会发送消息到目标进程,同时会启动一个超时机制来异步监控这个消息是否被响应(“埋炸弹”),该消息在限定的时间内被目标进程处理完并且通知系统进程,则炸弹拆除(“拆炸弹”),否则“炸弹爆炸”,抛出 ANR 。

目标进程的 Binder 线程接收到这个消息后会按时间顺序插入到消息队列,但是这个消息队列有可能有以下几种情况:已经有大量的消息等待调度;可能存在少量消息,但是有个别消息耗时很长;其他进程或者整个系统负载很高等。都可能导致系统服务发送的这个消息没有来得及处理,从而触发了“炸弹爆炸”。

“炸弹爆炸”后,目标进程收到系统进程的 SIGNAL QUIT 信号 开始 Dump 业务堆栈,这个时候目标进程的主线程不耗时间了,主线程 Dump 的是当前某个消息执行过程的业务堆栈。

而这主线程的消息队列 取消息 MessgaQueue.next——> MessageQueue.nativePollonce 正是无时无刻不在执行的。

最佳实践:

面对Native Poll Once 的问题,不要再盯着 Natvie Poll Once的堆栈看了,因为你无法从中获取有效信息来解决问题。

性能监控 SDK 抓取到 Natvie Poll Once,相当于堆栈偏移,大概率是 MessageQueue 消息队列里有很多长耗时的消息。

这里分享笔者亲身的经验:解决卡顿问题以及内存问题(包括内存抖动、内存泄露、OOM 等问题)。为什么内存问题和卡顿、ANR 有关?因为内存泄露、内存抖动等问题触发虚拟机 GC, 造成系统资源紧缺,一些Binder 调用,页面渲染等系统调用有可能变慢。你是否见过一些普通的系统调用方法报 ANR? 原因和这相关。

卡顿问题和内存问题可以接入火山引擎或者 Bugly 专业版本进行排查,如果没有这种条件,线下接入 Matrix 的卡顿监控以及 LeakCanary 进行排查。

此外,还有些综合治理手段

11 . 动画 objectAnimator.start 导致 ANR 

ANR 堆栈最后报在了 ObjectAnimator.start内部的 ArrayList.get

问题堆栈:

android 监听系统字体变化 安卓监听屏幕变化_性能优化_10

这个问题一开始也让笔者困惑了,一个动画启动为啥会报 ANR ?  

切子线程一把梭行不行? 幸亏没这么搞,不然就把问题本质掩盖了。

业务代码是这样的:

private void fun() {
      ObjectAnimator objectAnimator = ObjectAnimator
                    .ofFloat(bottomDescView, "scaleX", 0, 1)
                    .setDuration(400);
            objectAnimator.setStartDelay(500);
            objectAnimator.addListener(new SimpleAnimatorListener(){
                @Override
                public void onAnimationStart(@Nullable Animator animation) {
                  //...
                }
            });
            objectAnimator.start();
}

不知道给位看官有没看出什么端倪?

业务里动画 ObjectAnimator 是一个局部变量, 恰好是这个局部变量碍事。

每一个Animator实现类的实例都是通过向 AnimationHandler.getInstance() 注册垂直信号帧刷新回调,从而实现动画帧更新的。AnimationHandler.getInstance() 是一个单例类,所有的Animator动画实例的更新回调都是借助于同一个单例实例。

这里采用了ObjectAnimator 局部变量,被AnimationHandler.callback持有,AnimationHandler是一个单例,然后ObjectAnimator.addListener 是通过匿名内部类的方式,匿名内部类隐式持有外部类引用,也就是持有外部的Fragment。

引用链:AnimationHandler.callback->ObjectAnimator->Fragment

此外,通过这种局部ObjectAnimator,动画资源也没有释放。这就解析了为啥最后报在了ArrayList.get。

综上,这里有严重的内存泄露,当内存泄露到一定程度,引发GC 或者 OOM, 频繁GC 带来卡顿,严重会引发 ANR !!!

最佳实践:

ObjectAnimator 局部变量改私有变量,在 ObjectAnimator使用之前如果不为空,释放动画资源。并且在 Fragment ondestroy的时候释放资源并且置空。

private ObjectAnimator objectAnimator;

private void fun() {
    //1. 使用之前先释放一下!!!
    if(objectAnimator != null){
       objectAnimator.cancel();
    }
     objectAnimator = ObjectAnimator
                    .ofFloat(bottomDescView, "scaleX", 0, 1)
                    .setDuration(400);
            objectAnimator.setStartDelay(500);
            objectAnimator.addListener(new SimpleAnimatorListener(){
                @Override
                public void onAnimationStart(@Nullable Animator animation) {
                  //...
                }
            });
            objectAnimator.start();
}

//...省略代码
    @Override
    public void onDestroyView() {
     //2. Fragment 和 Activity 生命周期销毁的时候释放动画!!!
        if(objectAnimator!=null){
            objectAnimator.cancel();
            objectAnimator = null;
        }
        super.onDestroyView();
    }

线上验证,完美解决。这也再一次印证笔者在 NativePollOnce 问题中说的观点:

内存泄露、内存抖动等问题触发虚拟机 GC, 造成系统资源紧缺,一些Binder 调用,页面渲染等系统调用有可能变慢,引发卡顿、ANR。

12.android.net.ConnectivityManager.registerNetworkCallback

问题堆栈:

android 监听系统字体变化 安卓监听屏幕变化_性能优化_11

这类问题发生在系统方法调用,笔者一般做法直接切子线程,因为切子线程后对原本功能影响不大

最佳实践:

切子线程

13.androidx.lifecycle.LifecycleRegistry.addObserver anr

问题堆栈:

android 监听系统字体变化 安卓监听屏幕变化_ide_12

一开始,这种问题有点令人懵逼,LifecycleRegistry.addObserver 也能发生 ANR , 详细看到堆栈后面会调用 Class.isAnonymousClass, 这个调用其实是个耗时方法。像平时我们使用 XXX.class.getName 最后也会调用到 Class.isAnonymousClass。

最佳实践:

这种不能无脑切子线程了,因为它是一个注册方法,切子线程后,可能会丢失一些监听。

笔者更换了另外一种方案 application.registerActivityLifecycleCallbacks 通过监听Activity 的 onStart和 onStop 也能计算出是前台还是后台。

14. FirebasePerfProvider.attachInfo anr

问题堆栈:

android 监听系统字体变化 安卓监听屏幕变化_java_13

项目中接入了 Firebase 的性能监控,在Application 启动的时候 InstallProvider 走到了FirebasePrefProvider.attachIno,attachInfo里有耗费时间的操作。

最佳实践:

对Provider的问题,能不在 attachInfo 以及 onCreate 里做事情就不要做,实在要做切不能做耗时的任务。因为它不仅可能引发ANR, 还影响了启动速度。

此外,项目中应该尽可能把多个 Provider 合并。

对这个例子而言,因为是三方 SDK 的 Provider,我们好像没有什么办法改变它。但是想到它是一个性能监控用到的,那么是否可以减轻它的影响?

于是我们做了一个策略:项目中自定义一个 CustomFirebasePerfProvider 继承 FirebasePerfProvider ,并且重写attachInfo 方法:通过远程配置开关决定是否执行super.attachInfo,这样降低了走 attachInfo 耗时引发的ANR

public class CustomFirebasePerfProvider extends com.google.firebase.perf.provider.FirebasePerfProvider {

  @Override
  public void attachInfo(Context context, ProviderInfo info) {
    //.. enable 读取远程配置开关,只有部分用户才执行attachInfo
    if (enable) {
      super.attachInfo(context, info);
    } else {
      ConfigResolver configResolver = ConfigResolver.getInstance();
      configResolver.setContentProviderContext(context);
    }
  }
}

同时 AndroidManifest.xml 把三方SDK 的 FirebasePerfProvider 移除

<provider
            android:name=".CustomFirebasePerfProvider"
            android:authorities="${applicationId}.firebaseinitprovider"
            android:exported="false"
            android:initOrder="101"/>

        <provider
            android:name="com.google.firebase.perf.provider.FirebasePerfProvider"
            android:authorities="${applicationId}.firebaseperfprovider"
            android:exported="false"
            android:initOrder="101"
            tools:node="remove"/>

综合治理手段

  • 凡涉及 binder 调用、IO 操作以及耗费时间的逻辑,业务允许的情况下,使用子线程。
  • 降低应用运行时内存
  • 降低应用 CPU 占用
  • 业务庞大,拆分子进程——分拆进程,减轻单个进程里的主线程负担。业界一流 App 并不一味单进程的,像 QQ 有主进程,后台进程,工具进程,小游戏进程等。因为 QQ 的业务实在太多了,如果都杂糅在一个进程,性能问题不堪设想。此外,比如音乐类 App 可以拆分主进程和播放进程;视频类 App 可以拆分主进程和视频进程;下载工具类的 App 可以拆分主进程和下载进程等。但是拆分进程需要区分机型:低端机(内存只有2G及以下的设备)不建议多开进程,笔者实践经验效果负向。
  • 缓存思想:获取目标变量耗费时间考虑下缓存
  • 尽量使用动态非有序广播代替:静态广播、有序的动态广播等
  • 跨进程需求可以优先考虑 Content Provider 作为 IPC, 笔者实践经验:使用Content Provider 作为 IPC 更加轻量方便,并且不容易发生 ANR。(应用启动的 Install Provider 的 ANR 上述已优化)
  • 业务逻辑按需执行,避免主线程的消息队列经常处于大量消息等待调度

总结

        应用性能好坏影响因素其实多方面的:

        和我们设备有关,低端机设备内存低,更加容易发生卡顿,笔者公司的项目用户群体大部分都是中低端设备。 一些在高端机不会出现的问题,在低端机比较容易出现。       

        和我们平时编码习惯息息相关,为了方便或者一时没有意识直接在主线程做 IO 操作,上述的获取文件路径,获取网络状态不做缓存直接调用,我们也往往不重视...... 当系统资源越来越紧缺,这些细节也能成为卡顿的“元凶”!

        和我们 App 的业务复杂度有关:一些好几个小组在同一个 App 上开发的项目,不同的业务都往 App 上加逻辑,每个业务都搞自己轮子。例如:没有统一的线程池做好复用,也容易造成无必要的 ANR 。因此基础建设也非常重要。

以上是笔者亲身经历收录的部分 ANR 问题实例,各位看官如果有遇到不同类型的 ANR 问题,欢迎留言一起交流探讨~