青春须早为,岂能长少年。这篇文章主要讲述Android ANR优化 1相关的知识,希望能为你提供帮助。
1, 你碰到ANR了吗在App使用过程中, 你可能遇到过这样的情况:
文章图片
ANR恭喜你, 这就是传说中的ANR.
1.1 何为ANR
ANR全名Application Not Responding, 也就是"应用无响应". 当操作在一段时间内系统无法处理时, 系统层面会弹出上图那样的ANR对话框.
1.2 为什么会产生ANR
在android里, App的响应能力是由Activity Manager和Window Manager系统服务来监控的. 通常在如下两种情况下会弹出ANR对话框:
- 5s内无法响应用户输入事件(例如键盘输入, 触摸屏幕等).
- BroadcastReceiver在10s内无法结束.
1.3 如何避免ANR
知道了ANR产生的原因, 那么想要避免ANR, 也就很简单了, 就一条规则:
不要在主线程(UI线程)里面做繁重的操作.这里面实际上涉及到两个问题:
- 哪些地方是运行在主线程的?
- 不在主线程做, 在哪儿做?
2, ANR分析 2.1 获取ANR产生的trace文件
ANR产生时, 系统会生成一个traces.txt的文件放在/data/anr/下. 可以通过adb命令将其导出到本地:
$adb pull data/anr/traces.txt .
2.2 分析traces.txt
2.2.1 普通阻塞导致的ANR获取到的tracs.txt文件一般如下:
如下以GithubApp代码为例, 强行sleep thread产生的一个ANR.
----- 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<
com.tencent.bugly.crashreport.CrashReport>
)
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信息中的添加的中文注释已基本说明了trace文件该怎么分析:
- 文件最上的即为最新产生的ANR的trace信息.
- 前面两行表明ANR发生的进程pid, 时间, 以及进程名字(包名).
- 寻找我们的代码点, 然后往前推, 看方法调用栈, 追溯到问题产生的根源.
以上的ANR trace是属于相对简单, 还有可能你并没有在主线程中做过于耗时的操作, 然而还是ANR了. 这就有可能是如下两种情况了:2.2.2 CPU满负荷这个时候你看到的trace信息可能会包含这样的信息:
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
最后一句表明了:
- 当是CPU占用100%, 满负荷了.
- 其中绝大数是被iowait即I/O操作占用了.
2.2.3 内存原因其实内存原因有可能会导致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的异常...2.2 ANR的处理
针对三种不同的情况, 一般的处理情况如下
- 主线程阻塞的
开辟单独的子线程来处理耗时阻塞事务.
- CPU满负荷, I/O阻塞的
I/O阻塞一般来说就是文件读写或数据库操作执行在主线程了, 也可以通过开辟子线程的方式异步执行.
- 内存不够用的
增大VM内存, 使用largeHeap属性, 排查内存泄露(这个在内存优化那篇细说吧)等.
高手和新手的区别是, 高手知道怎么在一开始就避免问题的发生. 那么针对ANR这个问题, 我们需要做哪些层次的工作来避免其发生呢?
3.1 哪些地方是执行在主线程的
- Activity的所有生命周期回调都是执行在主线程的.
- Service默认是执行在主线程的.
- BroadcastReceiver的onReceive回调是执行在主线程的.
- 没有使用子线程的looper的Handler的handleMessage, post(Runnable)是执行在主线程的.
- AsyncTask的回调中除了doInBackground, 其他都是执行在主线程的.
- View的post(Runnable)是执行在主线程的.
上面我们几乎一直在说, 避免ANR的方法就是在子线程中执行耗时阻塞操作. 那么在Android中有哪些方式可以让我们实现这一点呢.
3.2.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();
3.2.2 使用AsyncTask这个是Android特有的方式, AsyncTask顾名思义, 就是异步任务的意思.
private class DownloadFilesTask extends AsyncTask<
URL, Integer, Long>
{
// 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);
3.2.3 HandlerThreadAndroid中结合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这个子线程中了.
}
}
3.2.4 IntentServiceService是运行在主线程的, 然而IntentService是运行在子线程的.
实际上IntentService就是实现了一个HandlerThread + ServiceHandler的模式.
以上HandlerThread的使用代码示例也就来自于IntentService源码.
3.2.5 LoaderAndroid 3.0引入的数据加载器, 可以在Activity/Fragment中使用. 支持异步加载数据, 并可监控数据源在数据发生变化时传递新结果. 常用的有CursorLoader, 用来加载数据库数据.
// Prepare the loader.Either re-connect with an existing one,
// or start a new one.
// 使用LoaderManager来初始化Loader
getLoaderManager().initLoader(0, null, this);
//如果 ID 指定的加载器已存在,则将重复使用上次创建的加载器。
//如果 ID 指定的加载器不存在,则 initLoader() 将触发 LoaderManager.LoaderCallbacks 方法 //onCreateLoader()。在此方法中,您可以实现代码以实例化并返回新加载器// 创建一个Loader
public Loader<
Cursor>
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<
Cursor>
loader, Cursor data) {
// Swap the new cursor in.(The framework will take care of closing the
// old cursor once we return.)
mAdapter.swapCursor(data);
}
具体请参看官网Loader介绍.
3.2.6 特别注意使用Thread和HandlerThread时, 为了使效果更好, 建议设置Thread的优先级偏低一点:
Process.setThreadPriority(THREAD_PRIORITY_BACKGROUND);
因为如果没有做任何优先级设置的话, 你创建的Thread默认和UI Thread是具有同样的优先级的, 你懂的. 同样的优先级的Thread, CPU调度上还是可能会阻塞掉你的UI Thread, 导致ANR的.
结语【Android ANR优化 1】对于ANR问题, 个人认为还是预防为主, 认清代码中的阻塞点, 善用线程. 同时形成良好的编程习惯, 要有MainThread和Worker Thread的概念的...(实际上人的工作状态也是这样的~~哈哈)
推荐阅读
- zabbix自发现监控应用app里的url例子
- class file for org.springframework.context.ConfigurableApplicationContext not found 解决方法
- android studio 3.0 hello world 之 修改系统状态栏颜色
- Android ANR优化 2
- Android开发笔记——圆角和边框们
- Android内存回收机制
- Android查缺补漏(IPC篇)-- 进程间通讯基础知识热身
- 赚享客商城分销模式系统app技术开发
- create-react-app脚手架中配置webpack的方法