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)
android.app.SharedPreferencesImpl.writeToFile (SharedPreferencesImpl.java:807)
android.app.SharedPreferencesImpl.access$900 (SharedPreferencesImpl.java:59)
android.app.SharedPreferencesImpl$2.run (SharedPreferencesImpl.java:672)
android.app.QueuedWork.processPendingWork (QueuedWork.java:265)
android.app.QueuedWork.waitToFinish (QueuedWork.java:178)
android.app.ActivityThread.handleServiceArgs (ActivityThread.java:4977)
android.app.ActivityThread.access$2300 (ActivityThread.java:284)
android.app.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)
android.app.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, 少用或者不用静态广播。对于一定要静态注册的广播,笔者的思路是拆分进程,把这些静态广播注册到一个独立进程,解放原来的主进程。这一措施验证可行,大大降低项目中静态广播 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

问题堆栈:

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

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

可以看到 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 

问题堆栈:

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

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

最佳实践:

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

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

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

问题堆栈:


 这种类型的 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 。

 最佳实践:

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. android.app.ContextImpl.getExternalCacheDirs  ANR

问题堆栈:

此类问题发生在获取文件路径时,对于这类问题,我们一般的思路就是做缓存,因为像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

问题堆栈:

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

最佳实践:

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

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

 问题堆栈:

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

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

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

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

最佳实践:

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

10. NativePollOnce ANR

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

问题堆栈:

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

今日头条 ANR 优化实践系列 - 设计原理及影响因素_ 字节跳动技术团队的博客-CSDN博客

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

系统服务创建服务,广播,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

问题堆栈:

这个问题一开始也让笔者困惑了,一个动画启动为啥会报 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

问题堆栈:

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

最佳实践:

切子线程

13.androidx.lifecycle.LifecycleRegistry.addObserver anr

问题堆栈:

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

最佳实践:

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

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

14. FirebasePerfProvider.attachInfo anr

问题堆栈:

项目中接入了 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 问题,欢迎留言一起交流探讨~

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐