zhengwenhui / blogs Goto Github PK
View Code? Open in Web Editor NEWblogs
blogs
https://developer.android.com/topic/performance/vitals/crash
每当出现由未处理的异常或信号引起的意外退出时应用就会崩溃。使用Java编写的应用程序如果抛出未处理的异常(由Throwable类表示)则会崩溃。如果在执行期间存在未处理的信号(例如SIGSEGV),则使用本机代码语言编写的应用程序会崩溃。
当应用崩溃,Android终止app的进程,并显示一个对话框提醒用户app停止了
应用程序无需在前台运行即可崩溃。任何应用程序组件,甚至是在后台运行的广播接收器或内容提供商等组件都可能导致应用程序崩溃。这些崩溃通常会让用户感到困惑,因为他们没有积极参与您的应用。
如果您的应用程序遇到崩溃,您可以使用此页面中的指导来诊断和解决问题。有关如何诊断使用本机代码语言构建的应用程序中的崩溃的指导,请参阅诊断本机崩溃。
检测问题
您可能并不总是知道您的用户在使用您的应用时遇到过多的崩溃。如果您已经发布了应用程序,Android vitals可以帮助您了解问题。
Android vitals
当你的应用程序出现过多崩溃时,Android vitals可以通过Play控制台提醒您,以帮助提高应用程序的性能。 Android vitals认为应用崩溃过多的情况:
至少1.09%的日活出现至少1次崩溃
至少0.18%的日活出现两次或更多地崩溃
日活是指您的应用每天使用的人数。有关Google Play如何收集Android重要数据的信息,请参阅Play控制台文档。
在您了解到您的应用程序遭遇过多崩溃后,下一步就是诊断它们。
诊断崩溃
要解决崩溃可能很难。但是如果您可以确定导致崩溃的原因,那么更容易找到解决方案。
有许多情况可能会导致应用程序崩溃。一些原因是显而易见的,比如检查空值或空字符串,还有一些其他原因更为微妙,比如将无效参数传递给API甚至是复杂的多线程交互。
读取堆栈跟踪
修复崩溃的第一步是确定崩溃发生的位置。如果使用Play Console或logcat工具的输出,则可以使用报告详细信息中提供的堆栈跟踪。如果您没有可用的堆栈跟踪,则应通过手动测试应用程序或联系受影响的用户来本地重现崩溃,并在使用logcat时重现它。
以下跟踪信息显示了一个例子应用的崩溃示例
堆栈跟踪显示两条对调试崩溃至关重要的信息:
抛出的异常的类型
引发异常的代码部分
抛出的异常类型通常是一个非常强烈的暗示,即出现了什么问题。查看它是否是IOException,OutOfMemoryError或其他内容,并查找有关异常类的文档。
在堆栈跟踪的第二行显示类,方法,文件引发异常的源文件的行号。对于每个被调用的函数,另一行显示前面的调用站点(称为堆栈帧),通过向上移动堆栈并检查代码,您可能会发现传递错误值的位置。如果您的代码没有出现在堆栈跟踪中,很可能在某处,您将无效参数传递给异步操作。您通常可以通过检查堆栈跟踪的每一行,找到您使用的任何API类,并确认您传递的参数是正确的,以及您从允许的位置调用它来弄清楚发生了什么。
有关本机应用程序崩溃的详细信息,请参阅诊断本机崩溃。
重现崩溃的提示
仅通过启动模拟器或将设备连接到计算机,您可能无法完全解决问题。开发环境往往拥有更多资源,例如带宽,内存和存储。使用异常类型来确定可能缺少的资源,或查找Android版本,设备类型或应用程序版本之间的相关性。
内存错误
如果OutOfMemoryError,那么您可以创建一个内存容量较低的模拟器。图2显示了AVD管理器设置,您可以在其中控制设备上的内存量。
网络异常
由于用户经常移入和移出移动或WiFi网络覆盖范围,因此在应用程序网络中,异常通常不应被视为错误,而是作为意外发生的正常操作条件。如果您需要重现网络异常,例如UnknownHostException,请尝试在应用程序尝试使用网络时启用飞行模式。
另一种选择是通过选择网络速度仿真和/或网络延迟来降低仿真器中网络的质量。您可以使用AVD管理器上的“速度”和“延迟”设置,也可以使用-netdelay和-netspeed标志启动模拟器,如以下命令行示例所示:
emulator -avd [your-avd-image] -netdelay 20000 -netspeed gsm
此示例在所有网络请求上设置20秒的延迟,上载和下载速度为14.4 Kbps。有关模拟器的命令行选项的更多信息,请参阅从命令行启动模拟器。
用logcat阅读
一旦您能够完成重现崩溃的步骤,您就可以使用logcat之类的工具来获取更多信息。
logcat输出将显示您打印的其他日志消息以及系统中的其他消息。不要忘记关闭已添加的任何额外日志语句,因为打印它们会在应用程序运行时浪费CPU和电池。
第8章 虚拟机字节码执行引擎
8.1 概述
执行引擎是java虚拟机最核心的组成部分之一。虚拟机是一个相对于物理机的概念。在java虚拟机规范中指定了虚拟机字节码执行引擎的概念模型,执行引擎在执行java代码的时候有解释执行(通过解释器执行)和编译执行(通过即时编译器产生本地代码执行)两种选择。Java虚拟机执行引擎外观看起来是一致的:输入的是字节码文件,处理过程是字节码解析的等效过程,输出的是执行结果。
https://developer.android.com/studio/debug/bug-report
抓取和分析bug报告
bug报告包含了设备日志、堆栈跟踪信息、以及其他诊断信息,可以帮助你查找并解决应用的bug。您可以使用设备上的Take bug report开发者选项,Android Emulator菜单或开发机器上的的adb bugreport命令从设备抓取错误报告。
要获取错误报告,您必须在设备上启用“开发者”选项,以便访问“获取错误报告”选项。
从设备捕获错误报告
要从设备直接获得错误报告,按以下步骤操作:
从Android模拟器获取错误报告
使用adb抓取错误报告
如果你只连接了一个设备,你可以如下使用adb获取错误报告
$ adb bugreport E:\Reports\MyBugReports
如果未指定bug报告的路径,则会将其保存到本地目录。
如果连接了多个设备,则必须使用-s选项指定设备。运行以下adb命令以获取设备序列号并生成错误报告。
$ adb devices
List of devices attached
emulator-5554 device
8XV7N15C31003476 device
$ adb -s 8XV7N15C31003476 bugreport
检查错误报告ZIP文件
默认情况下,ZIP文件名为bugreport-BUILD_ID-DATE.zip,它可能包含多个文件,其中最重要的文件是bugreport-BUILD_ID-DATE.txt。这个文件就是错误报告,它包含系统服务(dumpsys),错误日志(dumpstate)和系统消息日志(logcat)的诊断输出。系统消息包括设备出现错误时的堆栈跟踪,以及应用中使用Log类打印的信息。
ZIP文件包含一个包含Android发布版本的version.txt元数据文件,启用systrace后,ZIP文件还包含systrace.txt文件。Systrace工具通过捕获和显示应用程序进程和其他Android系统进程的执行时间来帮助分析应用程序的性能。
dumpstate工具将文件从设备的文件系统复制到FS文件夹下的ZIP文件中,以便您可以引用它们。例如,设备中的/dirA / dirB / fileC文件将在ZIP文件中生成FS / dirA / dirB / fileC条目。
更多信息,请见Reading bug reports
从您的用户处获取报告
如上所述的抓取错误报告在你自己调试app时非常有用,但app的最终用户无法方便的与您共享这些类型的错误报告。要获得来自真实用户的堆栈跟踪信息的崩溃报告,您应该利用Google Play和Firebase的崩溃报告功能。
Google Play控制台
您可以从Google Play控制台获取报告,以查看从Google
Play安装应用的用户发生崩溃和应用无响应(ANR)错误的数据。数据保存六个月。
有关更多信息,请参阅Play Console帮助中的查看崩溃和应用程序无响应(ANR)错误。
Firebase崩溃报告
Firebase崩溃报告会创建应用中错误的详细报告。错误按照具有相似堆栈跟踪的问题进行分组,并根据对用户的影响严重程度进行分类。除自动报告外,您还可以记录自定义事件以帮助捕获导致崩溃的步骤。
您只需将Firebase依赖项添加到build.gradle文件即可开始接收来自任何用户的崩溃报告。有关更多信息,请参阅Firebase崩溃报告。
https://developer.android.com/studio/debug/stacktraces
调试app经常涉及到堆栈跟踪信息,当app由于错误或者异常崩溃的时候就会产生堆栈跟踪信息。你也可以在app代码的任何位置调用方法例如Thread.dumpStack()打印堆栈跟踪信息
当你的app以调试模式运行在连接设备上,Android
Studio会在logcat视窗打印并高亮显示堆栈调用信息,如图1所示。
堆栈跟踪显示导致抛出异常的方法调用列表,以及发生调用的文件名和行号。您可以单击高亮显示的文件名以打开文件并检查方法调用的代码。单击堆栈跟踪和向下堆栈跟踪以快速在logcat窗口中显示的堆栈跟踪线之间移动。点击向上堆栈跟踪和向下堆栈跟踪以快速在logcat窗口中显示的堆栈跟踪信息行之间移动。
从外部源打开堆栈跟踪信息
有时可能需要分析在错误报告中与您共享的堆栈跟踪信息,而不是在调试时发现的堆栈跟踪信息。例如,您可能正在从Google
Play控制台或其他工具(如Firebase崩溃报告)收集在用户设备上生成的堆栈跟踪信息。
要让错误报告中获取的外部堆栈跟踪信息同样的高亮显示和可单击,请按照下列步骤操作:
监视剪贴板以查找新堆栈跟踪信息
如果您经常使用外部堆栈跟踪,则可以通过允许Android
Studio持续监视系统剪贴板以获取新的堆栈跟踪信息来提高您的工作效率:
SystemUI是为用户提供系统级别的信息显示与交互的一套UI组件
包括状态栏、导航栏、图片壁纸、RecentPanel、截屏、PowerUI(监控剩余电量、低电警告)、RingtonePlayer(播放铃声)
当核心系统服务启动完成后
在ActivityManagerService.systemReady()中回调
SystemServer.java传入的Runnable实例的run方法,
在此方法中
startSystemUi(context, windowManagerF);
启动了SystemUIService服务
SystemUIService的onCreate方法中调用了
SystemUIApplication.startServicesIfNeeded()
实例化了字符串数组资源中的服务,这些服务继承自SystemUI
<!-- SystemUI Services: The classes of the stuff to start. -->
<string-array name="config_systemUIServiceComponents" translatable="false">
<item>com.android.systemui.Dependency</item>
<item>com.android.systemui.util.NotificationChannels</item>
<item>com.android.systemui.statusbar.CommandQueue$CommandQueueStart</item>
<item>com.android.systemui.keyguard.KeyguardViewMediator</item>
<item>com.android.systemui.recents.Recents</item>
<item>com.android.systemui.volume.VolumeUI</item>
<item>com.android.systemui.stackdivider.Divider</item>
<item>com.android.systemui.SystemBars</item>
<item>com.android.systemui.usb.StorageNotification</item>
<item>com.android.systemui.power.PowerUI</item>
<item>com.android.systemui.media.RingtonePlayer</item>
<item>com.android.systemui.keyboard.KeyboardUI</item>
<item>com.android.systemui.pip.PipUI</item>
<item>com.android.systemui.shortcut.ShortcutKeyDispatcher</item>
<item>@string/config_systemUIVendorServiceComponent</item>
<item>com.android.systemui.util.leak.GarbageMonitor$Service</item>
<item>com.android.systemui.LatencyTester</item>
<item>com.android.systemui.globalactions.GlobalActionsComponent</item>
<item>com.android.systemui.ScreenDecorations</item>
<item>com.android.systemui.fingerprint.FingerprintDialogImpl</item>
<item>com.android.systemui.SliceBroadcastRelayHandler</item>
</string-array>
其中SystemBars中根据config_statusBarComponent配置实例化了StatusBar。
StatusBar.java onStart()
mBarService = IStatusBarService.Stub.asInterface(
ServiceManager.getService(Context.STATUS_BAR_SERVICE));
IStatusBarService 是一个系统服务,有ServerThread启动并常驻system_server进程中。
IStatusBarService为那些对状态栏感兴趣的其他系统服务定义了一系列的API,然而对SystemUI而言,
它更像是一个客户端,因为IStatusBarService会将操作状态栏的请求发送给SystemUI,并由或者完成请求
随后BaseStatusBar将自己注册到IStatusBarService中,声明本实例才是状态栏的真正实现者,IStatusBarService 会将
其所接受的请求转发给本实例。
SystemUI可能因为某些原因意外终止。而状态栏所显示的信息并不属于状态栏自己,而是属于其他的应用程序或其他的系统服务,
因此当SystemUI重新启动时,便需要回复其终止前所显示的信息以避免信息的丢失。为此IStatusBarService 中保存了所有需要状态栏
进行显示的信息的副本,并在新的状态栏实例启动后,这些副本将会伴随着注册的过程传递给状态栏进行显示,从而避免信息丢失
从代码分析的角度来看,这一从IStatusBarService 取回的信息副本的过程正好完成的体现了状态栏所有显示的信息的类型
ArrayList<String> iconSlots = new ArrayList<>();
ArrayList<StatusBarIcon> icons = new ArrayList<>();
//用于显示在状态栏的系统状态栏区中的状态图标列表,状态的名称,需要显示的图标资源
protected CommandQueue mCommandQueue;
CommandQueue 继承自IStatusBar.Stub 因此它是IStatusBar的Bn端。在完成注册后,这一Binder对象的Bp端将保存在IStatusBar.Stub
之中,因此它是IStatusBarService与BaseStatusBar的通信桥梁
int[] switches = new int[9];
存储了一些杂项
ArrayList<IBinder> binders = new ArrayList<>();
Rect fullscreenStackBounds = new Rect();
Rect dockedStackBounds = new Rect();
try {
mBarService.registerStatusBar(mCommandQueue, iconSlots, icons, switches, binders,
fullscreenStackBounds, dockedStackBounds);
} catch (RemoteException ex) {
// If the system process isn't there we're doomed anyway.
}
createAndAddWindows();
创建状态栏和导航栏的窗口
应用来自IStatusBarService中所获取的信息mCommandQueue已经注册到IStatusBarService中,状态栏和导航栏的窗口与控件树也都创建完毕,接下来的任务就是应用从
IStatusBarService中所获取的信息
// Make sure we always have the most current wallpaper info.
IntentFilter wallpaperChangedFilter = new IntentFilter(Intent.ACTION_WALLPAPER_CHANGED);
mContext.registerReceiver(mWallpaperChangedReceiver, wallpaperChangedFilter);
mWallpaperChangedReceiver.onReceive(mContext, null);
//壁纸改变事件
mLockscreenUserManager.setUpWithPresenter(this, mEntryManager);
mCommandQueue.disable(switches[0], switches[6], false /* animate */); //禁用某些功能
setSystemUiVisibility(switches[1], switches[7], switches[8], 0xffffffff,
fullscreenStackBounds, dockedStackBounds); //设置SystemUIVisibility
topAppWindowChanged(switches[2] != 0); //设置菜单栏的可见性
// StatusBarManagerService has a back up of IME token and it's restored here.
setImeWindowStatus(binders.get(0), switches[3], switches[4], switches[5] != 0);
// 设置硬件键盘信息
// Set up the initial icon state
int N = iconSlots.size();
for (int i=0; i < N; i++) {
mCommandQueue.setIcon(iconSlots.get(i), icons.get(i));
}
//依次向系统状态栏添加状态图标
// Set up the initial notification state.
mNotificationListener.setUpWithPresenter(this, mEntryManager);
状态栏和导航栏的启动分为如下几个过程
获取IStatusBarService
运行与system_server的一个系统服务,它接受操作状态栏/导航栏的请求并将其转发给BaseStatusBar,为了保证SystemUI意外退出
后不会发生信息的丢失,IStatusBarService保存了所有需要状态栏与导航栏进行显示或处理的信息副本
将继承自IStatusBar.Stub的CommandQueue的实例注册到IStatusBarService以建立通信,并将信息副本取回
通过调用createAndAddWindows()方法完成状态栏与导航栏的控件树及窗口的创建于显示
使用从IStatusBarService取回的信息副本
实现者是StatusBarManagerService。系统服务在SystemServer中创建
SystemServer.java startOtherServices()
try {
statusBar = new StatusBarManagerService(context, wm);
ServiceManager.addService(Context.STATUS_BAR_SERVICE, statusBar);
} catch (Throwable e) {
reportWtf("starting StatusBarManagerService", e);
}
/**
* Construct the service, add the status bar view to the window manager
*/
public StatusBarManagerService(Context context, WindowManagerService windowManager) {
mContext = context;
mWindowManager = windowManager;
LocalServices.addService(StatusBarManagerInternal.class, mInternalService);
LocalServices.addService(GlobalActionsProvider.class, mGlobalActionsProvider);
}
由StatusBar调用,建立其与StatusBarManagerService的通信连接,并取回保存在其中的信息副本
public void registerStatusBar(IStatusBar bar, List<String> iconSlots,
List<StatusBarIcon> iconList, int switches[], List<IBinder> binders,
Rect fullscreenStackBounds, Rect dockedStackBounds) {
enforceStatusBarService(); //权限检查
Slog.i(TAG, "registerStatusBar bar=" + bar);
mBar = bar; //就是StatusBar中的CommandQueue的Bp端,将通过bar与StatusBar进行通信,可以理解bar就是SystemUI中的状态栏和导航栏
try {
mBar.asBinder().linkToDeath(new DeathRecipient() {
@Override
public void binderDied() {
mBar = null;
notifyBarAttachChanged();
}
}, 0);
} catch (RemoteException e) {
}
notifyBarAttachChanged();
//系统状态栏图标列表
synchronized (mIcons) {
for (String slot : mIcons.keySet()) {
iconSlots.add(slot);
iconList.add(mIcons.get(slot));
}
}
//switches中的杂项
synchronized (mLock) {
switches[0] = gatherDisableActionsLocked(mCurrentUserId, 1);
switches[1] = mSystemUiVisibility;
switches[2] = mMenuVisible ? 1 : 0;
switches[3] = mImeWindowVis;
switches[4] = mImeBackDisposition;
switches[5] = mShowImeSwitcher ? 1 : 0;
switches[6] = gatherDisableActionsLocked(mCurrentUserId, 2);
switches[7] = mFullscreenStackSysUiVisibility;
switches[8] = mDockedStackSysUiVisibility;
binders.add(mImeToken);
fullscreenStackBounds.set(mFullscreenStackBounds);
dockedStackBounds.set(mDockedStackBounds);
}
}
//设置系统状态栏图标操作
public void setIcon(String slot, String iconPackage, int iconId, int iconLevel,
String contentDescription) {
enforceStatusBar(); //检查权限
synchronized (mIcons) {
StatusBarIcon icon = new StatusBarIcon(iconPackage, UserHandle.SYSTEM, iconId,
iconLevel, 0, contentDescription);
//Slog.d(TAG, "setIcon slot=" + slot + " index=" + index + " icon=" + icon);
mIcons.put(slot, icon); //保存
if (mBar != null) {
try {
mBar.setIcon(slot, icon); //设置请求发送给StatusBar
} catch (RemoteException ex) {
}
}
}
}
StatusBarManagerService是SystemUI中的状态栏与导航栏在system_server中的代理,所有对状态栏或导航栏的有需求的对象
都可以通过获取StatusBarManagerService的实例或Bp端达到其目的。只不过使用者必须拥有能够完成操作的权限
保存了状态栏 导航栏所需的信息副本,用于在SystemUI意外退出之后的恢复
SystemUIService Android服务,以一个容器的角色运行于SystemUI的进程中,在它内部运行着多个子服务,其中之一便是状态栏和导航栏的实现者StatusBar
IStatusBarService 系统服务StatusBarManagerService,是状态栏导航栏向外界提供服务的前端接口,运行于system_server进程中
StatusBar 状态栏与导航栏的实现者,运行于SystemUIService中
IStatusBar 即SystemUI中的CommandQueue是联系StatusBarManagerService与StatusBar的桥梁
https://developer.android.com/training/articles/perf-anr
让应用保持响应
存在这样的代码,虽然通过了所有的性能测试,但在关键时候依然会出现延迟、挂起或者冻结,或者输入信息处理耗时太长。应用的响应最糟糕的是弹出ANR对话框。
在Android中,系统通过显示一个如图1所示的应用停止响应的对话框来防范一段时间响应不及时的应用。此时,您的应用已经在相当长的一段时间内没有响应,因此系统为用户提供了一个退出应用的选项。提高应用的响应性确保系统永远不会向用户显示ANR对话框至关重要。
本文档描述了Android系统如何确定应用程序是否没有响应,并提供了确保应用程序保持响应的指南。
什么触发ANR
一般的,如果应用无法响应用户输入,系统就会显示ANR。例如,如果app在UI线程上的一些I/O操作发生阻塞(通常是网络访问),导致系统无法处理用户输入事件。或者应用程序在UI线程中耗费太多时间构建精心设计的内存结构或计算游戏中的下一步行动。确保这些计算高效是非常重要的,但是即使是最高效的代码运行也是需要时间的。
如果应用中可能执行耗时的操作,在任何情况下,都应该创建一个工作线程,在工作线程中执行这些操作,而不是在UI线程中执行这些操作。这使得UI线程(驱动UI事件循环)保持响应,以防止系统断定您的代码已冻结。因为这种线程通常是在类级别完成的,所以您可以将响应性视为类问题。
(将其与基本代码性能进行比较,这是方法级别的问题。)
在Android中,应用程序响应性由活动管理器和Window Manager系统服务监视。当Android检测到以下某种情况时,它将显示特定应用程序的ANR对话框:
输入事件(如按键或屏幕触摸事件)5秒内没有响应。
BroadcastReceiver未在10秒内完成执行。
如何避免ANRs
Android应用程序通常完全在单个线程上运行,默认为“UI线程”或“主线程”)。这意味在应用的UI线程中执行任何需要耗时很长的操作都可能触发ANR对话框,因为如果在运行耗时的操作就没有机会处理输入事件或者广播消息。
因此,在UI线程中运行的任何方法都应该在该线程上尽可能少地工作。特别是,Activitys应该尽可能少地在关键的生命周期方法中处理操作,例如onCreate()和onResume()。潜在的长时间运行操作(如网络或数据库操作)或计算成本高昂的计算(如调整位图大小)应在工作线程中完成(或者在数据库操作的情况下,通过异步请求)。
为更长时间的操作创建工作线程的最有效方法是使用AsyncTask类。只需继承AsyncTask并实现doInBackground()方法即可完成工作。要将进度更改发布给用户,可以调用publishProgress(),它调用onProgressUpdate()回调方法。从onProgressUpdate()(在UI线程上运行)的实现,您可以通知用户。例如:
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");
}
}
要执行此工作线程,只需创建一个实例并调用execute():
new DownloadFilesTask().execute(url1, url2, url3);
虽然它比AsyncTask更复杂,但您可能希望创建自己的Thread或HandlerThread类。如果这样做,您应该通过调用Process.setThreadPriority()并传递THREAD_PRIORITY_BACKGROUND将线程优先级设置为“后台”优先级。如果您没有以这种方式将线程设置为较低的优先级,那么线程仍然会使您的应用程序变慢,因为默认情况下它的操作优先级与UI线程相同。
如果实现Thread或HandlerThread,请确保在等待工作线程完成时UI线程不会阻塞 -
不要调用Thread.wait()或Thread.sleep()。在等待工作线程完成时,主线程应该为其他线程提供一个Handler,以便在完成时回调。以这种方式设计应用程序将允许应用程序的UI线程保持对输入的响应,从而避免由5秒输入事件超时引起的ANR对话框。
BroadcastReceiver执行时间的具体约束强调了广播接收器的意图:在后台进行小量,离散的工作,例如保存设置或注册通知。因此,与UI线程中调用的其他方法一样,应用程序应避免在广播接收器中进行长时间运行或计算。但是,如果需要采取可能长时间运行的操作来响应广播消息,则应用程序应该启动IntentService,而不是通过工作线程执行密集型任务。
当BroadcastReceiver对象执行得太频繁时,会出现另一个常见问题。频繁的后台执行可以减少其他应用程序可用的内存量。有关如何有效启用和禁用BroadcastReceiver对象的更多信息,请参阅按需操作广播接收器。
加强响应能力
通常,100到200ms是用户将感知应用程序缓慢的阈值。因此,除了应该采取的措施之外,还有一些其他提示可以避免ANR,并使您的应用程序看起来对用户响应:
如果您的应用程序在后台运行以响应用户输入,则显示正在进行的进度(例如在UI中使用ProgressBar)。
特别是对于游戏,在工作线程中做移动的计算。
如果您的应用程序具有耗时的初始设置阶段,请考虑尽快显示启动画面或渲染主视图,指示正在加载并异步填充信息。在任何一种情况下,您都应该以某种方式表明正在取得进展,以免用户认为应用程序被冻结。
使用Systrace和Traceview等性能工具来确定应用程序响应能力的瓶颈。
https://developer.android.com/topic/performance/vitals/anr#java
当一个app的UI线程阻塞太长时间,就会触发一个应用没有响应的错误。如果app在前台,系统就会显示一个对话框,如图一所示,ANR对话框可以让用户选择是否强行退出应用程序。
ANRs是由于负责更新UI的主线程,无法处理用户输入事件或者绘制,导致用户体验差,更多关于app主线程的信息,请查看进程和线程。
当以下的情况之一出现就会触发应用ANR
当activity在前台,应用在5秒内没有响应一个输入事件或者广播接收器(如按键或者屏幕触摸事件)
如果没有activity在前台,广播接收器在相当长的时间内还没有执行完
如果你的app出现了ANR,你可以按照这篇文档的指导分析和处理问题
检测和分析问题
Android提供了好几个方式,让你查找你的app中的问题,并帮助你分析问题。如果你已经发布了应用,Android
vitals会提醒你如果出现了问题的话,后面还会介绍几个分析工具帮助你查找问题。
Android vitals
Android vitals 可以借助Play Console当你的app
ANR过多的时候发出警报来助你提升app的性能。当一个app符合以下情况时Android
vitals判断为ANR超标:
至少0.47%的日活出现至少一次ANR
至少0.24%的日活出现两次以上的ANR
日活是指app每天被使用的次数
关于Google Play如果收集Android vitals数据的更多信息,请阅读Play Console文档
分析 ANRs
分析ANRs时有一些常见的情形去查找
主线程与另一个线程处于死锁状态,无论是在您的进程中还是通过Binder调用。主线程不仅仅是等待一个长时间的操作才能完成,而是进入死锁状态。有关更多信息,请参阅Wikipedia上的死锁。
下面这些技术可以帮助你找出以上哪个是导致出现你的app出现ANR的原因。
Strict mode 严格模式
使用StrictMode可以帮助你在开发应用时发现主线程上无意的I/O操作。你可以在application层或者activity层使用StrictMode
使能后台ANR对话框
Android只有在设备开发选项中使能 Show background
ANRs时,当app处理广播信息超时才会显示ANR对话框。由于这个原因,后台ANR对话框并不总是显示给用户,但是app仍面临性能问题。
TraceView
你可以借助TraceView获取调用跟踪信息,你运行的app
当过用例时,确定主线程忙碌的位置。关于如何使用Traceview的详细信息参见https://developer.android.com/studio/profile/traceview
拉取一个调用跟踪文件
Android在出现ANR时会存储堆栈信息。在旧的OS版本上,设备上会有一个/data/anr/traces.txt文件,在最近的OS版本上,会有多个/data/anr/anr_*文件。可以在root权限下通过adb访问这些ANR堆栈信息
adb root
adb shell ls /data/anr
adb pull /data/anr/
你即可以通过设备上的获取bug报告开发者选项也可以通过在开发机器上adb
bug报告命令从物理设备上获取bug报告。
解决问题
在定位到问题后,你可以根据本节的提示解决常见的问题
主线程上的慢代码
在你的代码中找出app的主线程忙碌超过5s的位置,查找可疑的用例并尝试复现ANR
例如 图2展示了一个Traceview时间线,这里的主线程忙碌超过5s
图2向我们展示了大多数违规的代码发生在onClick handler中,如下代码例子所示
在这个例子中,你可以将运行在主线程中的工作移到工作线程中。Android框架提供了帮助将任务移动到工作线程的类,更多信息请看Helper
classes for
threading。下面的代码展示了如何使用AsyncTask帮助类在工作线程中运行任务:
Traceview
显示大多数代码运行在工作线程中,如图3示。然后主线程就可以响应用户事件了。
主线程上的IO
在主线程上执行IO操作是一个常见的拖慢主线程操作的原因,这会导致ANRs。正如前面章节所讨论的,建议将所有的IO操作移到工作线程中去。
IO操作包括网络和存储操作,更多信息见Performing network operations 和 Saving data
锁争用
在一些场景中,导致ANR的工作并不是直接运行在app的主线程中的。如果一个工作线程持有主线程所需要去完成工作的资源的锁一个资源的锁,一个ANR就可能出现。
例如 图4显示了一个Traceview时间线,几乎所有的工作都在工作线程执行。
但是如果用户仍然出现了ANRs,你就应该在Android Device
Monitor中查看主线程的状态。通常,如果主线程是正在准备更新UI并且是可以响应的,就会处在RUNNABLE状态。
但是如果主线程无法恢复执行,那么就处在BLOCKED状态,不能响应事件。Android
Device Monitor中显示的状态为监视或等待,如图5所示。
下面的堆栈信息显示了应用的主线程阻塞等待资源
查看堆栈信息可以帮助定位阻塞主线程的代码,下面就是持有锁导致主线程阻塞产生前文堆栈信息的代码
另一个例子是主线程等待工作线程的结果,如下面代码所示,注意使用wait()和notify()在kotlin中并不推荐,kotlin有自己的机制处理并发。当使用kotlin时,应当是有kotlin特有的机制如果可能的话。
还有一些其他的情况可能阻塞主线程,包括线程使用Lock、Semaphore,以及资源池(类似与数据库连接池)或者其他互斥机制
一般的你需要评估你的应用持有的资源的锁,但是如果你想要避免ANRs,那么你应当查看一下那些主线程需要的资源的锁。
确保锁持有最少的时间,或者更好,首先评估app是否需要持有锁。如果你使用锁来确定根据工作线程的处理情况何时更新UI,使用像onProgressUpdate()和onPostExecute()的机制来在工作和主线程间通信。
死锁
当A线程由于请求被B线程持有的资源而进入等待状态,同时B线程也在等待被A线程持有的一个资源时,死锁发生。如果主线程在这样的状况下,ANR可能会发生。
死锁是计算机科学中一个经过充分研究的现象,并且可以使用死锁预防算法来避免死锁。
更过信息请在Wikipedia上看Deadlock和Deadlock prevention algorithms
慢广播接收器
应用可以通过广播接收器响应广播消息,例如启用或禁用飞行模式或改变连接状态。当应用程序花费太长时间来处理广播消息时会发生ANR。
ANR在以下情况下发生
广播接收器尚未在相当长的时间内完成其onReceive()方法
广播接收器调用goAsync()并且在PendingResult对象上调用finish()失败
应用只应在BroadcastReceiver的onReceive()方法中执行简短操作。但是如果应用程序因广播消息而需要更复杂的处理,则应将任务推迟到IntentService。
你可以使用Traceview等工具来识别广播接收器是否在应用程序的主线程上执行长耗时的操作。例如图6显示了一个在主线程中耗时约100s处理消息的广播接收器的时间线。
在BroadcastReceiver的onReceive()方法中执行长耗时操作可能会导致此现象,如以下示例所示:
像这种情况下,建议将长耗时的操作移动到IntentService,因为它使用工作线程来执行其工作。以下代码显示如何使用IntentService处理长耗时的操作:
使用IntentService后,将在工作线程而不是主线程上执行长耗时的操作。图7显示了工作延迟到工作线程的Traceview时间轴。
广播接收器可以使用goAsync()向系统发出信号,表明它需要更多时间来处理消息。但是,您应该在PendingResult对象上调用finish()。以下示例显示如何调用finish()以使系统回收广播接收器并避免ANR:
但是,将代码从一个慢的广播接收器移动到另一个线程并使用goAsync(),如果广播是在后台,并不能解决ANR。ANR仍会出现。
关于ANRs更多信息见Keeping your app responsive。更多关于线程的信息见Threading
performance。
Lambda表达式是Java SE 8中新增的一个非常重要的功能,提供了一种
匿名内部类
在Java中,对于那些在应用中只使用一次的类,就可以用匿名内部类的方式去实现。例如在Swing或者JavaFX应用中需要大量的处理键盘和鼠标的事件处理类,与其为每个事件定义一个单独的事件处理类,你可以使用匿名内部类,如下:
既然每个事件都需要一个单独的实现了ActionListener的类,使用匿名内部类优于创建一个新类,易读性也好。
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.