Anddroid 性能优化——ANR 实践集锦

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 正是无时无刻不在执行的。

最佳实践:

见综合治理手段

综合治理手段

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

总结

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

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

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

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

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

猜你喜欢

转载自blog.csdn.net/xiaobaaidaba123/article/details/127834494