目录
AMS.handleApplicationCrashInner()
AppErrors.makeAppCrashingLocked()
ProcessRecord.startAppProblemLocked()
ProcessRecord.stopFreezingActivities()
AppErrors.handleAppCrashLocked()
WMS.stopFreezingScreenLocked()
-
什么是Crash?
App Crash全称Application crash, 对于Crash可分为Java Crash和Native Crash。
在应用启动及运行中,出现闪退(崩溃),屏幕提示当前程序停止运行的弹窗,类似于windows的应用程序崩溃。
小结:Crash是由于代码异常而导致App非正常退出现象,也就是我们常说的崩溃
为什么会出现崩溃?
概括来讲,就是程序运行中有未捕获的异常,未被 try-catch,导致进程被杀
-
Java Crash
java代码导致jvm退出,弹出“程序已经崩溃”的对话框,最终用户点击关闭后进程退出。Logcat会在“AndroidRuntime”tag下输出Java的调用栈。
-
Native Crash
No.&Name | Reason | Resolution | Comment |
空指针 | 试图对空指针进行操作时(如读取空指针指向的内存),处理器就会产生一个异常 | 在使用指针前加以判断,如果为空,则是不可访问的。 | 空指针目前是糯米app最多的一种引起crash的原因,但是它也很容易被发现和修复。 |
野指针 | 指向的是一个无效的地址,该地址如果是不可读不可写的,那么会马上Crash;如果访问的地址为可写,而且通过野指针修改了该处的内存,那么很有可能会等一段时间(其它的代码使用了该处的内存后)才发生Crash。 | 在指针变量定义时,一定要初始化,特别是在结构体或类中的成员指针变量。 在释放了指针指向的内存后,如该指针不再用应置为NULL 看代码很难查找,通过代码分析工具也很难找出,只有通过专业的内存检测工具,才能发现这类bug。 | |
数组越界 | 访问无效的地址。如果该地址不可读写,则会马上Crash;如果修改了该处的内存,造成内存破坏,那么有可能会等一段时间才在别处发生Crash。 | 所有数组遍历的循环,都要加上越界判断。 用下标访问数组时,要判断是否越界。 通过代码分析工具可以发现绝大部分的数组越界问题。 | 破坏内存的bug,很难查找 |
整数除以零 | 整数除以零默认的处理方式是终止进程 | 在做整数除法时,要判断被除数是否为0的情况。 | 改情况在开发环境下很难出现,但庞大的用户量和复杂的用户输入,就很容易导致被除数为0的情况出现。 |
格式化输出参数错误 | 与野指针类似,但是只会读取无效地址的内存,而不会造成内存破坏。其结果是要么打印出错乱的数据,要么访问了无读写权限的内存而立即宕机。 | 在书写输出格式和参数时,要做到参数个数和类型都要与输出格式一致 | |
缓冲区溢出 | 通过往程序的缓冲区写超出其长度的内容,造成缓冲区的溢出,从而破坏函数调用的堆栈,修改函数调用的返回地址。如果不是黑客故意攻击,那么最终函数调用很可能会跳转到无法读写的内存区域,造成程序崩溃。 | 检查所有容易产生漏洞的库调用,比如sprintf,strcpy等,它们都没有检查输入参数的长度。 使用带有长度检查的库调用,如用snprintf来代替sprintf,或者自己在sprintf上封装一个带长度检查的函数。 | |
内存管理错误 | 可用内存过低,app所需的内存超过设备的限制,app跑不起来导致App crash。 内存泄露,程序运行的时间越长,所占用的内存越大,最终用尽全部内存,导致整个系统崩溃。 | imageview,图片占据太多内存,糯米app狂刷列表。 |
-
系统的Crash 处理过程
-
Java Crash 产生过程
上图是Android App启动时序图,我们想要探知的Crash秘密就存在于ZygoteInit.zygoteInit()函数中。
它的内部实现:
public static final Runnable zygoteInit(int targetSdkVersion, long[] disabledCompatChanges,
String[] argv, ClassLoader classLoader) {
//重定向的System.out和System.err到Android日志
RuntimeInit.redirectLogStreams();
// **重点关注** 用于初始化一些App运行期间需要用到的配置
RuntimeInit.commonInit();
// 调用Native方法进行初始化
ZygoteInit.nativeZygoteInit();
...
}
可以看到zygoteInit
是一个组装函数,下面让我们一探隐藏在内部的RuntimeInit.commonInit()
函数。
protected static final void commonInit() {
// LoggingHandler 用于组装异常信息并打印
LoggingHandler loggingHandler = new LoggingHandler();
// API 30 开始加入RuntimeHooks。RuntimeHooks.setUncaughtExceptionPreHandler()内部调用了Thread.setUncaughtExceptionPreHandler()
RuntimeHooks.setUncaughtExceptionPreHandler(loggingHandler);
// KillApplicationHandler 用于弹出Dialog并杀死进程
Thread.setDefaultUncaughtExceptionHandler(new KillApplicationHandler(loggingHandler));
...
}
所以当Android App发生未被捕获的异常发生时会终止线程,此时系统便会调用UncaughtExceptionHandler,告诉它被出错的线程以及对应的异常,然后便会调用uncaughtException函数.如果UncaughtExceptionHandler没有被设置,则会调用对应线程组的DefaultUncaughtExceptionHandler。
- Thread.setUncaughtExceptionPreHandler覆盖所有线程,会在DefaultUncaughtExceptionHandler之前调用,只能在Android Framework内部调用该方法.
- Thread.setDefaultUncaughtExceptionHandler在任意线程中设置即可作为所有线程的默认异常处理,可以在应用层调用,每次调用传入的Thread.UncaughtExceptionHandler都会替换上一次的.
- new Thread().setUncaughtExceptionHandler()只可以处理当前线程的异常,如果有Thread设置了UncaughtExceptionHandler,则在当前线程不会再使用全局的DefaultUncaughtExceptionHandler.
LoggingHandler.Java
private static class LoggingHandler implements Thread.UncaughtExceptionHandler {
// 用于标识是否已触发处理逻辑
public volatile boolean mTriggered = false;
@Override
public void uncaughtException(Thread t, Throwable e) {
mTriggered = true;
// 如果 KillApplicationHandler.uncaughtException()已经被触发了则不再继续执行
if (mCrashing) return;
// 判断是否为系统进程,mApplicationObject == null,一定不是普通的app进程. 但是除了system进程, 也有可能是shell进程, 即通过app_process + 命令参数 的方式创建的进程
if (mApplicationObject == null && (Process.SYSTEM_UID == Process.myUid())) {
Clog_e(TAG, "*** FATAL EXCEPTION IN SYSTEM PROCESS: " + t.getName(), e);
} else {
StringBuilder message = new StringBuilder();
message.append("FATAL EXCEPTION: ").append(t.getName()).append("\n");
final String processName = ActivityThread.currentProcessName();
if (processName != null) {
message.append("Process: ").append(processName).append(", ");
}
message.append("PID: ").append(Process.myPid());
Clog_e(TAG, message.toString(), e);
}
}
}
-
当system进程Crash的信息:
以*** FATAL EXCEPTION IN SYSTEM PROCESS [线程名]
为开头; 在第二行开始输出发生Crash时的调用栈信息;
-
其他进程Crash时的信息:
以FATAL EXCEPTION: [线程名]
为开头 在第二行输出 Process: [进程名], PID: [进程id]
; 第三行开始输出发生Crash时的调用栈信息;
KillApplicationHandler.Java
private static class KillApplicationHandler implements Thread.UncaughtExceptionHandler {
// 内部持有LoggingHandler用于输出异常信息。
private final LoggingHandler mLoggingHandler;
public KillApplicationHandler(LoggingHandler loggingHandler) {
this.mLoggingHandler = Objects.requireNonNull(loggingHandler);
}
@Override
public void uncaughtException(Thread t, Throwable e) {
try {
// 触发异常信息记录
ensureLogging(t, e);
//避免在处理崩溃信息时发生异常导致无限重入
if (mCrashing) return;
mCrashing = true;
//进行意外停止分析
if (ActivityThread.currentActivityThread() != null) {
ActivityThread.currentActivityThread().stopProfiling();
}
// 在此处弹出弹窗并杀死所在进程 **核心 见小节1.2**
ActivityManager.getService().handleApplicationCrash(
mApplicationObject, new ApplicationErrorReport.ParcelableCrashInfo(e));
} catch (Throwable t2) {
...
} finally {
// 通过finally语句块保证能执行并彻底杀掉Crash进程。当Crash进程被杀后,并没有完全结束,还有Binder死亡通知的流程还没有处理完成。
Process.killProcess(Process.myPid());
System.exit(10);
}
}
private void ensureLogging(Thread t, Throwable e) {
// 调用mLoggingHandler组装并打印异常信息
}
}
Android Crash的特点:
-
一般情况下程序出错时会弹出提示框;
-
程序所在进程被杀死,JVM虚拟机退出;
-
系统提供了捕获Crash的接口;
-
由Java 层代码引发的Java Crash 较容易捕获分析;
-
有C++ 层代码引发的Native Crash 一般的工具不能将其捕获;
-
发生Crash 时系统都做了什么?
在面,其中在KillApplicationHandler .uncaughtException()函数中我们看到了ActivityManager.getService().handleApplicationCrash()这样一个函数,其实这才是Android系统在发生Crash时所做事情的核心入口。
处理Crash的流程图如下:
ActivityManager.getService()返回的是ActivityManagerProxy实例(简称AMP),AMP经过binder调用最终交给ActivityManagerService(简称AMS)中相应的方法去处理,故接下来调用的是AMS.handleApplicationCrash()。
-
AMS.handleApplicationCrash()
public void handleApplicationCrash(IBinder app, ApplicationErrorReport.ParcelableCrashInfo crashInfo) { // 获取进程对象,见1.2.2小节 ProcessRecord r = findAppProcess(app, "Crash"); final String processName = app == null ? "system_server" : (r == null ? "unknown" : r.processName); // 见1.2.3小节 handleApplicationCrashInner("crash", r, processName, crashInfo); }
关于进程名(processName):
-
当远程IBinder对象为空时,则进程名为system_server;
-
当远程IBinder对象不为空,且ProcessRecord为空时,则进程名为unknown;
-
当远程IBinder对象不为空,且ProcessRecord不为空时,则进程名为ProcessRecord对象中相应进程名。
-
AMS.findAppProcess()
private ProcessRecord findAppProcess(IBinder app, String reason) { if (app == null) { return null; } synchronized (this) { return 找到app对应的进程信息,如果没找到会返回Null; } }
-
AMS.handleApplicationCrashInner()
void handleApplicationCrashInner(String eventType, ProcessRecord r, String processName, ApplicationErrorReport.CrashInfo crashInfo) { //将Crash信息写入到 Event log EventLog.writeEvent(EventLogTags.AM_CRASH,...); //将错误信息添加到 DropBox addErrorToDropBox(eventType, r, processName, null, null, null, null, null, crashInfo); // 见1.2.4 mAppErrors.crashApplication(r, crashInfo); }
addErrorToDropBox
是将Crash的信息输出到目录/data/system/dropbox
。例如system_server的dropbox文件名为system_server_crash@时间戳.txt
-
AppErrors.crashApplication()
void crashApplication(ProcessRecord r, ApplicationErrorReport.CrashInfo crashInfo) { //清除远程调用者uid和pid信息,并保存到origId final int callingPid = Binder.getCallingPid(); final int callingUid = Binder.getCallingUid(); final long origId = Binder.clearCallingIdentity(); try { if(存在ActivityController,比如monkey){ // 调用monkey的appCrashed retutn; } // 见1.2.5 if (!makeAppCrashingLocked()) { Binder.restoreCallingIdentity(origId); return; } ... //发送消息SHOW_ERROR_MSG,弹出提示crash的对话框,等待用户选择【见小节10】 mUiHandler.sendMessage(msg); //进入阻塞等待,直到用户选择crash对话框 int res = result.get(); } finally { //恢复远程调用者uid和pid Binder.restoreCallingIdentity(origId); } }
此方法主要做的两件事:
-
调用
makeAppCrashingLocked
,继续处理Crash流程; -
发送消息,弹出提示Crash的对话框,等待用户选择;
-
-
AppErrors.makeAppCrashingLocked()
private boolean makeAppCrashingLocked(ProcessRecord app, String shortMsg, String longMsg, String stackTrace, AppErrorDialog.Data data) { app.setCrashing(true); //封装crash信息到crashingReport对象 app.crashingReport = generateProcessError(app, ActivityManager.ProcessErrorStateInfo.CRASHED, null, shortMsg, longMsg, stackTrace); // 见1.2.6 app.startAppProblemLocked(); // 停止屏幕冻结 见1.2.7 app.getWindowProcessController().stopFreezingActivities(); // 见1.2.8 return handleAppCrashLocked(app, "force-crash" /*reason*/, shortMsg, longMsg, stackTrace, data); }
-
ProcessRecord.startAppProblemLocked()
void startAppProblemLocked() { ComponentName errorReportReceiver = null; for (int userId : mService.mUserController.getCurrentProfileIds()) { if (this.userId == userId) { // 获取获取当前用户下的Crash应用的 ErrorReceiver(需要在系统的设置中的错误报告功能开启时才会有值) errorReportReceiver = ApplicationErrorReport.getErrorReportReceiver( mService.mContext, info.packageName, info.flags); } } //调用AMS忽略当前app的广播接收 mService.skipCurrentReceiverLocked(this); }
此方法主要做的两件事:
-
如果开启了错误报告功能,获取广播接收器的组件名称;
-
忽略发生Crash应用的所有广播接收;
-
-
ProcessRecord.stopFreezingActivities()
public void stopFreezingActivities(boolean force) {
...
//其中activities类型为ArrayList<ActivityRecord>,停止进程里所有的Activity
int i = activities.size();
while (i > 0) {
i--;
activities.get(i).stopFreezingScreenLocked(true);
}
}
public void stopFreezingScreenLocked(boolean force) {
// appToken是WindowManager的token。 见1.7.9
WMS.stopAppFreezingScreen(appToken, force);
}
-
AppErrors.handleAppCrashLocked()
boolean handleAppCrashLocked(ProcessRecord app, String reason, String shortMsg, String longMsg, String stackTrace) {
if(同一进程在1分钟内连续两次crash){
if(不是persistent进程){
// ActivityStackSupervisor 简称ASS
ASS.handleAppCrashLocked, 直接结束该应用所有activity
AMS.removeProcessLocked,杀死该进程以及同一个进程组下的所有进程
}
ASS.resumeTopActivitiesLocked,恢复栈顶第一个非finishing状态的activity
}else{
ASS.finishTopRunningActivityLocked,执行结束栈顶正在运行activity
}
}
-
WMS.stopFreezingScreenLocked()
public void stopFreezingScreen() { if (权限检查) { throw new SecurityException("Requires FREEZE_SCREEN permission"); } synchronized(mWindowMap) { 1. 处理屏幕旋转相关逻辑; 2. 移除冻屏的超时消息; 3. 屏幕旋转动画的相关操作; 4. 使能输入事件分发功能; 5. display冻结时,执行gc操作; 6. 更新当前的屏幕方向; 7. 发送configuraion改变的消息。 } }
-
AMS.UiHandler
final class UiHandler extends Handler { public void handleMessage(Message msg) { switch (msg.what) { case SHOW_ERROR_MSG: 1. 创建提示crash对话框,等待用户选择,5分钟操作等待。 2. 阻塞等待用户选择,当用户不做任何选择5分钟超时后,默认选择“确定”,当手机休眠时也默认选择“确定” break; ... } }
-
总结
-
首先发生Crash所在进程,在创建之初便准备好了DefaultUncaughtHandler,用来来处理Uncaught Exception,并输出当前Crash基本信息;
-
调用当前进程中的AMP.handleApplicationCrash;经过binder ipc机制,传递到system_server进程;
-
接下来,进入system_server进程,调用binder服务端执行AMS.handleApplicationCrash;
-
从mProcessNames查找到目标进程的ProcessRecord对象;并将进程crash信息输出到目录/data/system/dropbox;
-
执行makeAppCrashingLocked
-
执行handleAppCrashLocked方法,
-
通过UiHandler发送消息SHOW_ERROR_MSG,弹出Crash对话框;
-
system_server进程执行完成。回到Crash进程开始执行杀掉当前进程的操作;
-
当Crash进程被杀,通过binder死亡通知,告知system_server进程来执行appDiedLocked();
-
最后,执行清理应用相关的activity/service/ContentProvider/receiver组件信息。
-
Crash 的基本原理
-
Android 应用运行在 Dalvik/ART 虚拟机中,当应用中出现未处理的异常时,虚拟机会终止应用的进程,导致应用崩溃。通常, Android Crash 可以分为以下几种类型:
-
空指针异常(NullPointerException) :当代码尝试访问一个空对象引用时,会抛出该异常。
-
数组越界异常(ArrayIndexOutOfBoundsException) :当代码尝试访问超过数组边界范围的元素时,会抛出该异常。
-
类转换异常(ClassCastException) :当代码尝试将一个对象转换成不兼容的类型时,会抛出该异常。
-
内存泄漏(Memory Leaks) :当应用程序持有对某个对象的引用,但该对象已经不再需要并且无法被释放回收时,就发生了内存泄漏。
-
资源未释放(Resource Not Released) :在使用一些需要手动管理资源(如数据库连接、文件流等)的情况下,如果没有正确释放这些资源,在长时间运行后可能导致系统资源耗尽并触发崩溃
-
Crash 分析思路
-
收集 Crash 信息
要分析 Crash,首先需要收集相关的信息。我们可以通过以下途径获取 Crash 信息:
• Android Studio 的 Logcat
• 第三方 Crash 收集工具(如: Firebase Crashlytics, Bugly 等)
• 操作系统提供的日志工具(如: adb logcat)
2.分析 Crash 日志
分析 Crash 日志是定位问题的关键。我们需要关注以下几个方面的信息:
• 异常类型: Java 层异常、 Native 层异常或 ANR
• 异常原因:例如空指针异常、数组越界等
• 堆栈信息:从堆栈信息中,我们可以定位到具体的代码行数
3.重现崩溃
为了更好地理解问题,我们需要尝试重现崩溃。这可以帮助我们确定崩溃发生的条件和场
景。重现崩溃还可以帮助我们验证解决方案是否有效。
4.调试和测试
在确定崩溃原因后,我们需要进行调试和测试。这可以帮助我们验证解决方案是否有效,
并确保它不会引入新的问题。一般 crash 的问题比较明显,容易找到问题。
-
Java Crash监控
-
如何收集 Java_Crash 日志
自定义java异常处理,记录当前设备的基础信息,及线程错误信息存储到本地文件中。在需要的时候上传到服务器
CrashHandler
internal object CrashHandler {
var CRASH_DIR = "crash_dir"
fun init(crashDir: String) {
Thread.setDefaultUncaughtExceptionHandler(CaughtExceptionHandler())
this.CRASH_DIR = crashDir
}
private class CaughtExceptionHandler : Thread.UncaughtExceptionHandler {
private val context = AppGlobals.get()!!
private val formatter = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss", Locale.CHINA)
private val LAUNCH_TIME = formatter.format(Date())
private val defaultExceptionHandler = Thread.getDefaultUncaughtExceptionHandler();
override fun uncaughtException(t: Thread, e: Throwable) {
if (!handleException(e) && defaultExceptionHandler != null) {
defaultExceptionHandler.uncaughtException(t, e)
}
restartApp()
}
private fun restartApp() {
val intent: Intent? =
context.packageManager?.getLaunchIntentForPackage(context.packageName)
intent?.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
context.startActivity(intent)
Process.killProcess(Process.myPid())
exitProcess(10)
}
private fun handleException(e: Throwable?): Boolean {
if (e == null) return false
val log = collectDeviceInfo(e)
if (BuildConfig.DEBUG) {
HiLog.e(log)
}
saveCrashInfo2File(log)
return true
}
private fun saveCrashInfo2File(log: String) {
val crashDir = File(CRASH_DIR)
if (!crashDir.exists()) {
crashDir.mkdirs()
}
val crashFile = File(crashDir, formatter.format(Date()) + "-crash.txt")
crashFile.createNewFile()
val fos = FileOutputStream(crashFile)
try {
fos.write(log.toByteArray())
fos.flush()
} catch (ex: Exception) {
ex.printStackTrace()
} finally {
fos.close()
}
}
/**
* 设备类型、OS本版、线程名、前后台、使用时长、App版本、升级渠道
CPU架构、内存信息、存储信息、permission权限
*/
private fun collectDeviceInfo(e: Throwable): String {
val sb = StringBuilder()
sb.append("brand=${Build.BRAND}\n")// huawei,xiaomi
sb.append("rom=${Build.MODEL}\n") //sm-G9550
sb.append("os=${Build.VERSION.RELEASE}\n")//9.0
sb.append("sdk=${Build.VERSION.SDK_INT}\n")//28
sb.append("launch_time=${LAUNCH_TIME}\n")//启动APP的时间
sb.append("crash_time=${formatter.format(Date())}\n")//crash发生的时间
sb.append("forground=${ActivityManager.instance.front}\n")//应用处于前后台
sb.append("thread=${Thread.currentThread().name}\n")//异常线程名
sb.append("cpu_arch=${Build.CPU_ABI}\n")//armv7 armv8
//app 信息
val packageInfo = context.packageManager.getPackageInfo(context.packageName, 0)
sb.append("version_code=${packageInfo.versionCode}\n")
sb.append("version_name=${packageInfo.versionName}\n")
sb.append("package_name=${packageInfo.packageName}\n")
sb.append("requested_permission=${Arrays.toString(packageInfo.requestedPermissions)}\n")//已申请到那些权限
//统计一波 存储空间的信息,
val memInfo = android.app.ActivityManager.MemoryInfo()
val ams =
context.getSystemService(Context.ACTIVITY_SERVICE) as android.app.ActivityManager
ams.getMemoryInfo(memInfo)
sb.append("availMem=${Formatter.formatFileSize(context, memInfo.availMem)}\n")//可用内存
sb.append("totalMem=${Formatter.formatFileSize(context, memInfo.totalMem)}\n")//设备总内存
val file = Environment.getExternalStorageDirectory()
val statFs = StatFs(file.path)
val availableSize = statFs.availableBlocks * statFs.blockSize
sb.append(
"availStorage=${Formatter.formatFileSize(
context,
availableSize.toLong()
)}\n"
)//存储空间
val write: Writer = StringWriter()
val printWriter = PrintWriter(write)
e.printStackTrace(printWriter)
var cause = e.cause
while (cause != null) {
cause.printStackTrace(printWriter)
cause = cause.cause
}
printWriter.close()
sb.append(write.toString())
return sb.toString()
}
}
fun crashFiles(): Array<File> {
return File(
AppGlobals.get()?.cacheDir,
CRASH_DIR
).listFiles()
}
}
-
重启应用
注意事项:如果进入应用就崩溃,就会进入重复重启的死循环。所以在做重启时候,需要做一些判断:例如1分钟内系统出现两次 crash 就不再重启了,否则这个应用被卸载无疑
-
Native crash异常处理
native_crash,就是 native 曾发生了 crash。内部是通过一个 NativeCrashListener 线程去监控的。
SystemServer --> ActivityManagerService --> startObservingNativeCrashes();
直接引入第三方库来处理 native 异常
class ActivityManagerService{
public void startObservingNativeCrashes() {
final NativeCrashListener ncl = new NativeCrashListener(this);
ncl.start();
}
}
-
案例分析
空指针异常
09-22 07:16:41.848 +0000 17248 17248 D AndroidRuntime: Shutting down VM
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: *** FATAL EXCEPTION INSYSTEM PROCESS: main
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: java.lang.NullPointerException: Attempt to read from field 'java.lang.String com.android.server.pm.Settings$VersionInfo.fingerprint' on a null object reference
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.server.pm.PackageManagerService.<init>(PackageManagerService.java:7613)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.server.pm.PackageManagerService.main(PackageManagerService.java:7121)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.server.SystemServer.startBootstrapServices(SystemServer.java:1166)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.server.SystemServer.run(SystemServer.java:880)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.server.SystemServer.main(SystemServer.java:613)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at java.lang.reflect.Method.invoke(Native Method)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:548)
09-22 07:16:41.848 +0000 17248 17248 E AndroidRuntime: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:987)
09-22 07:16:41.850 +0000 17248 17248 E AndroidRuntime: Error reporting crash
09-22 07:16:41.850 +0000 17248 17248 E AndroidRuntime: java.lang.NullPointerException: Attempt to invoke interface method 'void android.app.IActivityManager.handleApplicationCrash(android.os.IBinder,android.app.ApplicationErrorReport$ParcelableCrashInfo)' on a null object reference
09-22 07:16:41.850 +0000 17248 17248 E AndroidRuntime: at com.android.internal.os.RuntimeInit$KillApplicationHandler.uncaughtException(RuntimeInit.java:156)
09-22 07:16:41.850 +0000 17248 17248 E AndroidRuntime: at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1073)
09-22 07:16:41.850 +0000 17248 17248 E AndroidRuntime: at java.lang.ThreadGroup.uncaughtException(ThreadGroup.java:1068)
09-22 07:16:41.850 +0000 17248 17248 E AndroidRuntime: at java.lang.Thread.dispatchUncaughtException(Thread.java:2200)
分析过程:
(1) Settings$VersionInfo.fingerprint 为空,找到出现问题的代码:
(2)自己对源码进行分析,发现一个方法,该方法能创建或者找到版本的信息,如果为空则添加到集合中。而报错的地方,是调用 getInternalVersion(),没有做判空的处理。
(3)继续跟踪代码, mVersion 是一个集合,通过键值对来获取那个值
(4)UUID_PRIVATE_INTERNAL 是 null
(5)猜想空指针异常可能是没有调用 findOrCreateVersion(),没有创建对应的值并且没有put 到集合中。并且打印了正常能开机的 mVersion 的大小,都是 2。
修改方案如下:
-
预防措施
为了减少应用程序崩溃的风险,可以采取以下预防措施:
-
良好的异常处理:在关键代码块中使用 try-catch 语句来捕获和处理可能抛出的异常。这样可以避免应用程序因为未处理的异常而崩溃,并提供更友好的用户体验。
-
合理地使用空值检查:确保在访问对象之前进行适当的空值检查,以避免触发空指针异常。
-
正确释放资源:对于需要手动管理资源(如数据库连接、文件流等),始终确保及时释放这些资源,以避免资源泄漏和系统负载过高导致崩溃。
-
谨慎使用第三方库:选择可靠且经过广泛测试的第三方库,并仔细阅读其文档和源代码。不推荐使用已知存在问题或长期无更新支持的库。
-
定期进行性能优化和内存管理:通过减少内存占用、优化算法、限制并发线程数等方法,改善应用程序性能并减少崩溃风险。
-
多渠道测试与发布前验证:在发布前进行全面测试,并覆盖各种设备、操作系统版本和网络条件下运行应用程序的场景。这有助于发现并修复潜在的崩溃问题。
-
常见Android Native 崩溃及错误原因
-
什么是错误信号:
Android本质就是一个Linux,信号跟Linux信号是同一个东西,信号本身是用于进程间通信的没有正确错误之分,但是Android官方给信号赋予了特定的含义及处理动作
信号通常的来源:
1. 硬件发生异常,及硬件(通常示CPU)检测到一个错误条件并通知Linux内核,内核处理该异常,给相应的进程发送信号。硬件异常的情况包括执行一条异常的机器语言指令。比如被0除,或者引用了无法访问的内存区域。大部分信号如果没有被进程处理,默认的操作就是杀死进程。其中SIGSEGV(段错误)、SIGBUS(内存访问错误)、SIGFPE(算数错误)就属于这种信号(错误信号)
2. 进程调用的库发现错误,会给自己发送中止信号,默认情况下,该信号会终止进程。其中,SIGABRT(中止进程)属于这种信号
3. 用户或第三方恶意通过kill-信号 pid的方式给错误进程发送,这时signal中的si_code会小于0.
-
常见的崩溃:
-
空指针
代码实例: int * p = 0; //空指针 *p = 1; //写空指针指向的内存,产生SIGSEGV信号,造成Crash
原因分析: 在进程的地址空间中,从0开始的第一个页面的权限被设置为不可读也不可写,当进程的指令试图访问该页面中的地址时(如读取空指针指向的内存),处 理器就会产生一个异常,然后Linux内核会给进程发送一个段错误信号(SIGSEGV),默认的操作就是杀死进程,并产生core文件。
解决方法: 在使用指针前加以判断,如果为空,则是不可访问的。
Bug评述: 空指针时很容易出现的一种bug,在代码量大,快速迭代发布的今天,是很容易出现的错误。但是它也很容易被发现和修复。
-
野指针
代码实例: int* p; //野指针,未初始化,其指向的地址通常是随机的 *p = 1; //写野指针指向的内存,有可能不会马上Crash,而是破坏了别处的内存
原因分析: 野指针指向的是一个无效的地址,该地址如果是不可读不可写的,那么会马上Crash(Linux内核给进程发送段错误信号SIGSEGV),这是bug会很容易被发现。如果访问的地址为可写,而且通过野指针修改了该处的内存,那么很可能会等一段时间(其他的代码使用了该处的内存后)才会发生Crash。。这是查看Crash时显示的调用栈,和野指针所在的代码部分,有可能基本上没有任何关联。
解决方法: 1. 在指针变量定义时,一定要初始化,特别是在结构体或类中的成员指针变量
2. 在释放了该指针的内存后,要把该指针设置为NULL(但是如果在别的地方也有指针指向该内存,那这种方式就不好解决了)
3. 野指针造成的内存破坏的问题,有时候光看代码是很难查找的,通过代码分析工具也很难查找,只有通过专业的内存检测工具,才能发现这类bug
Bug评述: 野指针的bug,特别是内存被破坏的问题,有时候查起来毫无头绪,没有一点线索,让开发者感觉很迷茫(日志上报的信息查不出任何问题)。可以说内存破坏这类的bug是服务器稳定的最大杀手,也是开发者最应该注意的问题(c/c++开发尤其注意)
-
数组越界
代码实例: int arr[10] arr[10] = 1; //数组越界,超过了数组长度的限制,有可能会马上Crash,也有可能会破坏别处的内存
原因分析: 数组越界和野指针类似,访问了无效的地址,如果该地址不可读写,则会马上Crash(Linux内核会给进程发送段错误信号SIGSEGV),如果修改了该处的内存,造成内存破坏,那么有可能要等一段时间才在别处发生Crash。
解决方法:1. 所有数组遍历的循环,都要加上越界判断
2. 用下标访问数组时,要判断是否越界(是否超出了数组的长度)
3. 通过代码分析工具可以发现绝大部分的数组越界问题
Bug评述: 数组越界也是一种内存破坏的bug,有时候与野指针一样也是很难查找的
-
整数除以0
代码实例: int a = 1; int b = a/0; //整数除以0,产生SIGFPE信号,导致Crash
原因分析: 整数除以0总是产生SIGFPE(浮点异常,产生SIGFPE信号时并非一定要涉及浮点算数,整数运算异常也用浮点异常信号是为了保持向下兼容性)信号,默认的处理方式是终止进程,并生成core文件。
解决方法: 在做整数除法时,要判断被除数是否为0的情况。
Bug评述: 整数被0除的bug很容易被开发者忽视,因为被除数为0的情况在开发及测试环境下很难出现,但是到了生产应用环节,庞大的用户量和复杂的用户输入,就很容易导致被除数为0的情况出现了。
-
格式化输出参数错误
代码实例: char text[200]; //格式化参数错误,可能会导致非法的内存访问,从而造成宕机 snprintf(text,200,"Valid %u, Invalid %u %s", 1); //format格式不匹配
原因分析: 格式化参数和野指针也很类似,但是它只会读取无效地址的内存,而不会造成内存破坏。其结果是要么打印出错乱的数据,要么访问了无读写权限的内存(收到段错误信号SIGSEGV)而立即宕机。
解决方法:在书写输出格式和参数时,要做到参数个数和类型都要与输出格式一致。并且最好在GCC的编译选项中加入-wformat,让GCC在编译时检测此类错误。
-
缓冲区溢出
代码实例: char szBuffer[10]; sprintf(szBuffer,"Stack Buffer Overrun!888888888888888888888" "88888888888888888888888888888888")
-
原因分析: 通过往程序的缓冲区写超出其长度的内容,造成缓冲区溢出,函数的堆栈被破坏,修改函数调用的返回地址,在函数返回时会跳转到未知的地址上。如果不是黑客故意攻击,那么最终函数调用很可能会跳转到无法读写的内存区域,产生段错误信号SIGSEGV或SIGABRT,造成程序崩溃。
解决方法: 1. 检查所有容易产生漏洞的库调用,比如sprintf,strcpy等,它们都没有检查输入参数的长度。
2. 使用带有长度检查的库调用,如用snprintf来代替sprintf,或者自己在sprintf上封装一个带长度检查的函数。
3. 在GCC编译时,在-O1以上的优化行为下,使用-D_FORTIFY_SOURCE=level进行编译(level=1或2,代表的是检测级别不同,数值越大越严格),这样GCC会在编译时报告缓冲区溢出的错误。
4. 在GCC编译时加上-fstack-protector或-fstack-protector-all选项,使得堆栈保护功能生效。该功能会在编译后的汇编代码中插入堆栈检测的代码,并在运行时能够检测到栈破坏并输出报告。
Bug评述: 缓冲区溢出是一种非常普遍、非常危险的漏洞,在各种操作系统、应用软件中广泛存在。黑客在进行攻击时,输入的字符串一般不会让程序崩溃,而是修改函数的返回地址,使程序跳转到别的地方,转而执行黑客安排好的指令,以此达到攻击的目的。缓冲区溢出后,调试生成的core,可以看见调用栈是混乱的,因为函数的返回地址已经被修改到随机的地址上去了。服务器宕机后,如果core文件和可执行文件是匹配的,但是调用栈时错乱的,那么很大的可能性是发生了缓冲区溢出。