Android性能优化(五)--更快--稳定--响应速度ANR
1 ANR简介
2 为什么会有ANR ANR的产生需要同时满足三个条件:
- ANR,是“Application Not Responding”的缩写,即“应用程序无响应”。
- 在Android中,ActivityManagerService(简称AMS)和WindowManagerService(简称WMS)会监测应用程序的响应时间,如果应用程序主线程(即UI线程)在超时时间内对输入事件没有处理完毕(或没有处理),或者对特定操作没有执行完毕,就会出现ANR。
- 对于输入事件没有处理完毕产生的ANR,Android会显示一个对话框,提示用户当前应用程序没有响应,用户可以选择继续等待或者关闭这个应用程序(也就是杀掉这个应用程序的进程)。
3 三种情况会触发ANR 3.1 主线程对输入事件在5秒内没有处理完毕
- 主线程:只有应用程序进程的主线程响应超时才会产生ANR;
- 超时时间:产生ANR的上下文不同,超时时间也会不同,但只要在这个时间上限内没有响应就会ANR;
- 输入事件/特定操作:输入事件是指按键、触屏等设备输入事件,特定操作是指BroadcastReceiver和Service的生命周期中的各个函数,产生ANR的上下文不同,导致ANR的原因也会不同;
- Android的事件系统从2.3开始做了完全不同的实现,原先2.2中是在Java层实现的,但在2.3中整体转移到了C++层,本书基于2.3以后的版本进行说明。我们先简单了解一下产生这种ANR的整个流程。
- 当应用程序的Window处于Active状态并且能够接收输入事件(例如按键事件、触摸事件等)时,系统底层上报的事件就会被InputDispatcher分发给这个应用程序,应用程序的主线程通过InputChannel读取输入事件并交给界面视图处理,界面视图是一个树状结构,DecorView是视图树的根,事件从树根开始一层一层向端点(例如一个Button)传递。
- 我们通常会注册一个监听器来接收并处理事件,或者创建自定义的视图控件来处理事件。
- InputDispatcher运行在系统进程(进程名为system_server)的一个单独的线程中,应用程序的主线程在处理事件的过程中,InputDispatcher会不断的检测处理过程是否超时,一旦超时,会通过一系列的回调通知WMS的notifyANR函数,最终会调用到AMS中mHandler对象里的SHOW_NOT_RESPONDING_MSG这个case,此时界面上就显示系统提示对话框了,同时使用logcat命令查看log(日志信息)也可以看到关于ANR的信息。
- Window:具体指的是PhoneWindow对象,表示一个能够显示的窗口,它能够接收系统分发的各种输入事件;
- InputDispatcher:将系统上报的输入事件分发给当前活动的窗口;
- InputChannel:InputDispatcher和应用程序分别运行在两个不同的进程中,InputDispatcher就是通过InputChannel将事件对象传递给应用进程的。
3.2 主线程在执行BroadcastReceiver的onReceive函数时10秒内没有执行完毕
- 注意:产生这种ANR的前提是要有输入事件,如果用户没有触发任何输入事件,即便是主线程阻塞了,也不会产生ANR,因为InputDispatcher没有分发事件给应用程序,当然也不会检测处理超时和报告ANR了。
3.3 主线程在执行Service的各个生命周期函数时20秒内没有执行完毕
- BroadcastReceiver(简称BR)的onReceive函数运行在主线程中,当这个函数超过10秒钟没有返回就会触发ANR。不过对这种情况的ANR系统不会显示对话框提示,仅是输出log而已。
3.4 小结
- Service的各个生命周期函数也运行在主线程中,当这些函数超过20秒钟没有返回就会触发ANR。同样对对这种情况的ANR系统不会显示对话框提示,仅是输出log而已。
- 1.三种ANR中只有第1种会显示系统提示对话框,因为用户正在做界面交互操作,如果长时间没有任何响应,会让用户怀疑设备死机了,大多数人此时会开始乱按,甚至拔出电池重启,给用户的体验肯定是非常糟糕的。
- 2.三种ANR发生时都会在log中输出错误信息,你会发现各个应用进程和系统进程的函数堆栈信息都输出到了一个/data/anr/traces.txt的文件中,这个文件是分析ANR原因的关键文件,同时在日志中还会看到当时的CPU使用率,这也是重要信息,可以利用它们分析ANR问题。
4 traces.txt--ANR分析 4.1 获取ANR产生的trace文件
- 3.这三种ANR不是孤立的,有可能会相互影响。例如一个应用程序进程中同时有一个正在显示的Activity和一个正在处理消息的BroadcastReceiver,它们都运行在这个进程的主线程中。如果BR的onReceive函数没有返回,此时用户点击屏幕,而onReceive超过5秒仍然没有返回,主线程无法处理用户输入事件,就会引起第1种ANR。如果继续超过10秒没有返回,又会引起第2种ANR。
ANR产生时, 系统会生成一个traces.txt的文件放在/data/anr/下. 可以通过adb命令将其导出到本地:
$adb pull data/anr/traces.txt .
4.2 分析traces.txt
4.2.1 普通阻塞导致的ANR 强行sleep thread产生的一个ANR
获取到的tracs.txt文件一般如下:
----- pid 2976 at 2016-09-08 23:02:47 -----
Cmd line: com.anly.githubapp// 最新的ANR发生的进程(包名)...DALVIK THREADS (41):
"main" prio=5 tid=1 Sleeping
| group="main" sCount=1 dsCount=0 obj=0x73467fa8 self=0x7fbf66c95000
| sysTid=2976 nice=0 cgrp=default sched=0/0 handle=0x7fbf6a8953e0
| state=S schedstat=( 0 0 0 ) utm=60 stm=37 core=1 HZ=100
| stack=0x7ffff4ffd000-0x7ffff4fff000 stackSize=8MB
| held mutexes=
at java.lang.Thread.sleep!(Native method)
- sleeping on <0x35fc9e33> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:1031)
- locked <0x35fc9e33> (a java.lang.Object)
at java.lang.Thread.sleep(Thread.java:985) // 主线程中sleep过长时间, 阻塞导致无响应.
at com.tencent.bugly.crashreport.crash.c.l(BUGLY:258)
- locked <@addr=0x12dadc70> (a com.tencent.bugly.crashreport.crash.c)
at com.tencent.bugly.crashreport.CrashReport.testANRCrash(BUGLY:166)// 产生ANR的那个函数调用
- locked <@addr=0x12d1e840> (a java.lang.Class)
at com.anly.githubapp.common.wrapper.CrashHelper.testAnr(CrashHelper.java:23)
at com.anly.githubapp.ui.module.main.MineFragment.onClick(MineFragment.java:80) // ANR的起点
at com.anly.githubapp.ui.module.main.MineFragment_ViewBinding$2.doClick(MineFragment_ViewBinding.java:47)
at butterknife.internal.DebouncingOnClickListener.onClick(DebouncingOnClickListener.java:22)
at android.view.View.performClick(View.java:4780)
at android.view.View$PerformClick.run(View.java:19866)
at android.os.Handler.handleCallback(Handler.java:739)
at android.os.Handler.dispatchMessage(Handler.java:95)
at android.os.Looper.loop(Looper.java:135)
at android.app.ActivityThread.main(ActivityThread.java:5254)
at java.lang.reflect.Method.invoke!(Native method)
at java.lang.reflect.Method.invoke(Method.java:372)
at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:903)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:698)
如上trace信息中的添加的中文注释已基本说明了trace文件该怎么分析:
4.2.2 CPU满负荷 race信息可能会包含这样的信息:
- 1.文件最上的即为最新产生的ANR的trace信息.
- 2.前面两行表明ANR发生的进程pid, 时间, 以及进程名字(包名).
- 3.寻找我们的代码点, 然后往前推, 看方法调用栈, 追溯到问题产生的根源.
Process:com.anly.githubapp
...
CPU usage from 3330ms to 814ms ago:
6% 178/system_server: 3.5% user + 1.4% kernel / faults: 86 minor 20 major
4.6% 2976/com.anly.githubapp: 0.7% user + 3.7% kernel /faults: 52 minor 19 major
0.9% 252/com.android.systemui: 0.9% user + 0% kernel
...100%TOTAL: 5.9% user + 4.1% kernel + 89% iowait
4.2.3 内存原因
- 最后一句表明了:
- 1.当是CPU占用100%, 满负荷了.
- 2.其中绝大数是被iowait即I/O操作占用了.
- 其实内存原因有可能会导致ANR,
例如如果由于内存泄露, App可使用内存所剩无几, 我们点击按钮启动一个大图片作为背景的activity, 就可能会产生ANR, 这时trace信息可能是这样的:
// 以下trace信息来自网络, 用来做个示例
Cmdline: android.process.acoreDALVIK THREADS:
"main"prio=5 tid=3 VMWAIT
|group="main" sCount=1 dsCount=0 s=N obj=0x40026240self=0xbda8
| sysTid=1815 nice=0 sched=0/0 cgrp=unknownhandle=-1344001376
atdalvik.system.VMRuntime.trackExternalAllocation(NativeMethod)
atandroid.graphics.Bitmap.nativeCreate(Native Method)
atandroid.graphics.Bitmap.createBitmap(Bitmap.java:468)
atandroid.view.View.buildDrawingCache(View.java:6324)
atandroid.view.View.getDrawingCache(View.java:6178)...MEMINFO in pid 1360 [android.process.acore] **
native dalvik other total
size: 17036 23111 N/A 40147
allocated: 16484 20675 N/A 37159
free: 296 2436 N/A 2732
可以看到free的内存已所剩无几
当然这种情况可能更多的是会产生OOM的异常...
5 ANR的避免和检测 5.1 切记不要在主线程中作耗时操作
针对三种不同的情况, 一般的处理情况如下
- 1.主线程阻塞的
开辟单独的子线程来处理耗时阻塞事务.
- 2.CPU满负荷, I/O阻塞的
I/O阻塞一般来说就是文件读写或数据库操作执行在主线程了, 也可以通过开辟子线程的方式异步执行.
5.2 借助于一些工具来进行检测,从而更有效的避免ANR的引入。
- 3.内存不够用的
增大VM内存, 使用largeHeap属性, 排查内存泄露。
5.2.1 StrictMode 严格模式StrictMode是Android SDK提供的一个用来检测代码中是否存在违规操作的工具类,StrictMode主要检测两大类问题。
- 线程策略 ThreadPolicy
1.detectCustomSlowCalls:检测自定义耗时操作
2.detectDiskReads:检测是否存在磁盘读取操作
3.detectDiskWrites:检测是否存在磁盘写入操作
4.detectNetWork:检测是否存在网络操作
- 虚拟机策略VmPolicy
1.detectActivityLeaks:检测是否存在Activity泄露
2.detectLeakedClosableObjects:检测是否存在未关闭的Closeable对象泄露
3.detectLeakedSqlLiteObjects:检测是否存在Sqlite对象泄露
4.setClassInstanceLimit:检测类实例个数是否超过限制
Debug版本中使用
ThreadPolicy可以用来检测可能催在的主线程耗时操作,需要注意的是我们只能在Debug版本中使用它,发布到市场上的版本要关闭掉。StrictMode的使用很简单,我们只需要在应用初始化的地方例如Application或者MainActivity类的onCreate方法中执行如下代码:
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
// 开启线程模式
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectAll()//调用detectAll方法表示启动所有的检测策略
.penaltyLog()
.penaltyDialog() ////打印logcat,当然也可以定位到dropbox,通过文件保存相应的log
.build());
// 开启虚拟机模式
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectAll()//调用detectAll方法表示启动所有的检测策略
.penaltyLog()//调用penaltyLog表示在Logcat中打印日志
.build());
}
也可以根据应用的具体要求只开启某些策略
StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
.detectDiskReads()
.detectDiskWrites()
.detectNetwork()
.penaltyLog()
.build());
StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
.detectLeakedSqlLiteObjects()
.detectLeakedClosableObjects()
.detectActivityLeaks()
.penaltyLog()
.build());
5.2.2 BlockCanary监控应用主线程的卡顿
- BlockCanary是一个非侵入式式的性能监控函数库,它的用法和leakCanary类似,只不过后者监控应用的内存泄露,而BlockCanary主要用来监控应用主线程的卡顿。
- 它的基本原理是利用主线程的消息队列处理机制,通过对比消息分发开始和结束的时间点来判断是否超过设定的时间,如果是,则判断为主线程卡顿。
- 它的集成很简单,首先在build.gradle中添加依赖一般选取以下其中一个 case 引入即可
dependencies {
compile 'com.github.markzhai:blockcanary-android:1.5.0'// 仅在debug包启用BlockCanary进行卡顿监控和提示的话,可以这么用
debugCompile 'com.github.markzhai:blockcanary-android:1.5.0'
releaseCompile 'com.github.markzhai:blockcanary-no-op:1.5.0'
}
然后在Application类中进行配置和初始化即可
public class AnrDemoApplication extends Application {@Override
public void onCreate() {
super.onCreate();
// 在主进程初始化调用哈
BlockCanary.install(this, new AppBlockCanaryContext()).start();
}
}
实现自己监控的上下文
public class AppBlockCanaryContext extends BlockCanaryContext {
// 实现各种上下文,包括应用标示符,用户uid,网络类型,卡慢判断阙值,Log保存位置等/**
* Implement in your project.
*
* @return Qualifier which can specify this installation, like version + flavor.
*/
public String provideQualifier() {
return "unknown";
}/**
* Implement in your project.
*
* @return user id
*/
public String provideUid() {
return "uid";
}/**
* Network type
*网络类型
* @return {@link String} like 2G, 3G, 4G, wifi, etc.
*/
public String provideNetworkType() {
return "unknown";
}/**
* Config monitor duration, after this time BlockCanary will stop, use
* with {@code BlockCanary}'s isMonitorDurationEnd
* 配置监视器持续时间,此时BlockCanary将停止,并使用{@Code BlockCanary}的isMonitorDurationEnd
*
* @return monitor last duration (in hour)
*/
public int provideMonitorDuration() {
return -1;
}/**
* Config block threshold (in millis), dispatch over this duration is regarded as a BLOCK. You may set it
* from performance of device.
* 配置块阈值(以毫为单位),在此持续时间内的调度被视为BLOCK。 您可以根据设备的性能进行设置。
* @return threshold in mills
*/
public int provideBlockThreshold() {
return 1000;
}/**
* Thread stack dump interval, use when block happens, BlockCanary will dump on main thread
* stack according to current sample cycle.
* * Because the implementation mechanism of Looper, real dump interval would be longer than
* the period specified here (especially when cpu is busier).
*
*
* @return dump interval (in millis)
*/
public int provideDumpInterval() {
return provideBlockThreshold();
}/**
* Path to save log, like "/blockcanary/", will save to sdcard if can.
*
* @return path of log files
* 日志文件的路径
*/
public String providePath() {
return "/blockcanary/";
}/**
* If need notification to notice block.
* 是否需要通知
* @return true if need, else if not need.
*/
public boolean displayNotification() {
return true;
}/**
* Implement in your project, bundle files into a zip file.
* 将文件捆绑到一个zip文件中。
* @param srcfiles before compress
* @param dest files compressed
* @return true if compression is successful
*/
public boolean zip(File[] src, File dest) {
return false;
}/**
* Implement in your project, bundled log files.
*
* @param zippedFile zipped file
*/
public void upload(File zippedFile) {
throw new UnsupportedOperationException();
}/**
* Packages that developer concern, by default it uses process name,
* put high priority one in pre-order.
* 开发人员关注的软件包,默认情况下它使用进程名称,并在预订中放置高优先级的软件包。
* @return null if simply concern only package with process name.
* 如果仅仅关注包含进程名称的包,则返回null。
*/
public List concernPackages() {
return null;
}/**
* Filter stack without any in concern package, used with @{code concernPackages}.
* 无需关注包的过滤器堆栈,与@ {code concernPackages}一起使用。
* @return true if filter, false it not.
*/
public boolean filterNonConcernStack() {
return false;
}/**
* Provide white list, entry in white list will not be shown in ui list.
* 提供白名单,白名单中的条目不会显示在用户名单中。
* @return return null if you don't need white-list filter.
*/
public List provideWhiteList() {
LinkedList whiteList = new LinkedList<>();
whiteList.add("org.chromium");
return whiteList;
}/**
* Whether to delete files whose stack is in white list, used with white-list.
* 是否删除堆栈位于白名单中的文件,与白名单一起使用。
* @return true if delete, false it not.
*/
public boolean deleteFilesInWhiteList() {
return true;
}/**
* Block interceptor, developer may provide their own actions.
* Block拦截器,开发者可以提供自己的操作。
*/
public void onBlock(Context context, BlockInfo blockInfo) {}
}
6 使用子线程的方式 6.1 启Thread方式
这个其实也是Java实现多线程的方式. 有两种实现方法, 继承Thread 或 实现Runnable接口:
继承Thread
class PrimeThread extends Thread {
long minPrime;
PrimeThread(long minPrime) {
this.minPrime = minPrime;
}public void run() {
// compute primes larger than minPrime
. . .
}
}PrimeThread p = new PrimeThread(143);
p.start();
实现Runnable接口
class PrimeRun implements Runnable {
long minPrime;
PrimeRun(long minPrime) {
this.minPrime = minPrime;
}public void run() {
// compute primes larger than minPrime
. . .
}
}PrimeRun p = new PrimeRun(143);
new Thread(p).start();
6.2 使用AsyncTask
这个是Android特有的方式, AsyncTask顾名思义, 就是异步任务的意思.
private class DownloadFilesTask extends AsyncTask {
// Do the long-running work in here
// 执行在子线程
protected Long doInBackground(URL... urls) {
int count = urls.length;
long totalSize = 0;
for (int i = 0;
i < count;
i++) {
totalSize += Downloader.downloadFile(urls[i]);
publishProgress((int) ((i / (float) count) * 100));
// Escape early if cancel() is called
if (isCancelled()) break;
}
return totalSize;
}// This is called each time you call publishProgress()
// 执行在主线程
protected void onProgressUpdate(Integer... progress) {
setProgressPercent(progress[0]);
}// This is called when doInBackground() is finished
// 执行在主线程
protected void onPostExecute(Long result) {
showNotification("Downloaded " + result + " bytes");
}
}// 启动方式
new DownloadFilesTask().execute(url1, url2, url3);
6.3 HandlerThread
Android中结合Handler和Thread的一种方式. 前面有云, 默认情况下Handler的handleMessage是执行在主线程的, 但是如果我给这个Handler传入了子线程的looper, handleMessage就会执行在这个子线程中的. HandlerThread正是这样的一个结合体:
// 启动一个名为new_thread的子线程
HandlerThread thread = new HandlerThread("new_thread");
thread.start();
// 取new_thread赋值给ServiceHandler
private ServiceHandler mServiceHandler;
mServiceLooper = thread.getLooper();
mServiceHandler = new ServiceHandler(mServiceLooper);
private final class ServiceHandler extends Handler {
public ServiceHandler(Looper looper) {
super(looper);
}@Override
public void handleMessage(Message msg) {
// 此时handleMessage是运行在new_thread这个子线程中了.
}
}
6.4 IntentService
【Android性能优化(五)--更快--稳定--响应速度ANR】Service是运行在主线程的, 然而IntentService是运行在子线程的.
实际上IntentService就是实现了一个HandlerThread + ServiceHandler的模式.
6.5 Loader
Android 3.0引入的数据加载器, 可以在Activity/Fragment中使用. 支持异步加载数据, 并可监控数据源在数据发生变化时传递新结果. 常用的有CursorLoader, 用来加载数据库数据.
// 创建一个Loader
public Loader onCreateLoader(int id, Bundle args) {
// This is called when a new Loader needs to be created.This
// sample only has one Loader, so we don't care about the ID.
// First, pick the base URI to use depending on whether we are
// currently filtering.
Uri baseUri;
if (mCurFilter != null) {
baseUri = Uri.withAppendedPath(Contacts.CONTENT_FILTER_URI,
Uri.encode(mCurFilter));
} else {
baseUri = Contacts.CONTENT_URI;
}// Now create and return a CursorLoader that will take care of
// creating a Cursor for the data being displayed.
String select = "((" + Contacts.DISPLAY_NAME + " NOTNULL) AND ("
+ Contacts.HAS_PHONE_NUMBER + "=1) AND ("
+ Contacts.DISPLAY_NAME + " != '' ))";
return new CursorLoader(getActivity(), baseUri,
CONTACTS_SUMMARY_PROJECTION, select, null,
Contacts.DISPLAY_NAME + " COLLATE LOCALIZED ASC");
}// 加载完成
public void onLoadFinished(Loader loader, Cursor data) {
// Swap the new cursor in.(The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
}
6.6 特别注意
使用Thread和HandlerThread时, 为了使效果更好, 建议设置Thread的优先级偏低一点:
Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
因为如果没有做任何优先级设置的话, 你创建的Thread默认和UI Thread是具有同样的优先级的, 你懂的. 同样的优先级的Thread, CPU调度上还是可能会阻塞掉你的UI Thread, 导致ANR的.
参考 https://www.jianshu.com/p/c8c15608c68e
https://www.jianshu.com/p/6d855e984b99
https://blog.csdn.net/sinat_36668731/article/details/74924432
推荐阅读
- Android|Android Studio 3.0中mipmap-anydpi-v26是什么()
- Android中的Drawable(一)
- Android|Android 监听键盘状态变化,并获取键盘高度
- 跨平台Android和IOS百度语音在线识别原生插件
- Go十大常见错误第3篇(Go指针的性能问题和内存逃逸)
- MATLAB|基于PSO优化VIC算法的WORM蠕虫检测matlab仿真
- Android|Android 11源码——预置无源码apk到Android系统(带so文件)
- Android|2022年中高级 Android 大厂面试秘籍,为你保驾护航金九银十,直通大厂
- iQOO|iQOO 3 评测(“性能旗舰”的新解读)
- Android|Android Studio内存性能分析器