Android异常崩溃收集大全


原理:Android崩溃机制

常见的Android崩溃有两类,一类是Java Exception异常,一类是Native Signal异常。我们将围绕这两类异常进行。对于很多基于Unity、Cocos平台的游戏,还会有C#、JavaScript、Lua等的异常,这里不做讨论。Android应用程序的开发是基于Java语言的,所以主要分析第一类Android崩溃Java Exception。

Java的异常可以分为两类:Checked Exception和UnChecked Exception。所有RuntimeException类及其子类的实例被称为Runt ime异常,即UnChecked Exception,不是RuntimeException类及其子类的异常实例则被称为Checked Exception。

Checked异常又称为编译时异常

即在编译阶段被处理的异常。编译器会强制程序处理所有的Checked异常,也就是用try…catch显式的捕获并处理,因为Java认为这类异常都是可以被处理(修复)的。在Java API文档中,方法说明时,都会添加是否throw某个exception,这个exception就是Checked异常。如果没有try…catch这个异常,则编译出错,错误提示类似于“Unhandled exception type xxxxx”。

该类异常捕获的流程是:

  • 执行try块中的代码出现异常,系统会自动生成一个异常对象,并将该异常对象提交给Java运行环境,这个就是异常抛出(throw)阶段;
  • 当Java运行环境收到异常对象时,会寻找最近的能够处理该异常对象的catch块,找到之后把该异常对象交给catch块处理,这个就是异常捕获(catch)阶段。

Checked异常一般是不引起Android App Crash的,注意是“一般”,这里之所以介绍Checked异常,有两个原因:

  • 形成系统的了解,更好地对比理解UnCheckedException;
  • 对于一些Checked Exception,虽然我们在程序里面已经捕获并处理了,但是如果能同时将该异常收集并发送到后台,将有助于提升App的健壮性。比如修改代码逻辑回避该异常,或者捕获后采用更好的方法去处理该异常。至于应该收集哪些Checked Exception,则取决于App的业务逻辑和开发者的经验了。

UnChecked异常又称为运行时异常,即Runtime-Exception

最常见的莫过于NullPointerException。UnChecked异常发生时,由于没有相应的try…catch处理该异常对象,所以Java运行环境将会终止,程序将退出,也就是我们所说的Crash。当然,你可能会说,那我们把这些异常也try…catch住不就行了。理论上确实是可以的,但有两点会导致这种方案不可行:

  • 无法将所有的代码都加上try…catch,这样对代码的效率和可读性将是毁灭性的;
  • UnChecked异常通常都是较为严重的异常,或者说已经破坏了运行环境的。比如内存地址,即使我们try…catch住了,也不能明确知道如何处理该异常,才能保证程序接下来的运行是正确的。

没有try…catch住的异常,即Uncaught异常,都会导致应用程序崩溃。那么面对崩溃,我们是否可以做些什么呢?比如程序退出前,弹出个性化对话框,而不是默认的强制关闭对话框,或者弹出一个提示框安慰一下用户,甚至重启应用程序等。

其实Java提供了一个接口给我们,可以完成这些,这就是UncaughtExceptionHandler,该接口含有一个纯虚函数:public abstract void uncaughtException (Thread thread, Throwableex)。

Uncaught异常发生时会终止线程,此时,系统便会通知UncaughtExceptionHandler,告诉它被终止的线程以及对应的异常,然后便会调用uncaughtException函数。如果该handler没有被显式设置,则会调用对应线程组的默认handler。如果我们要捕获该异常,必须实现我们自己的handler,并通过以下函数进行设置:
public static void setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler handler)
实现自定义的handler,只需要继承UncaughtExceptionHandler该接口,并实现uncaughtException方法即可。

异常大全

  • android.database.sqlite.SQLiteCantOpenDatabaseException
    该异常表示发生了未知错误,导致不能打开数据库,可能是因为不能正常访问SD卡导致的
  • java.lang.NoSuchMethodException
    方法不存在异常。当访问某个类的不存在的方法时抛出该异常
  • java.lang.NullPointerException
    这种异常通常是调用一个对象的接口方法抛出的,凡是调用一个对象的接口方法之前,一定要进行判空或者进行try-catch,这样基本可以规避大部分空指针异常。null.方法或变量
  • java.lang.IllegalArgumentException
    参数不匹配异常,通常由于传递了不正确的参数导致。
    常见于:
    1. Activity、Service状态异常;
    2. 非法URL;
    3. UI线程操作。
    4.Fragment中嵌套了子Fragment,Fragment被销毁,而内部Fragment未被销毁,所以导致再次加载时重复,在onDestroyView() 中将内部Fragment销毁即可
    5.在请求网络的回调中使用了glide.into(view),view已经被销毁会导致该错误
  • java.util.ConcurrentModificationException
    该异常表示迭代器迭代过程中,迭代的对象发生了改变,如数据项增加或删除。
    [解决方案]:由于迭代对象不是线程安全,在迭代的过程中,会检查modCount是否和初始modCount即expectedModCount一致,如果不一致,则认为数据有变化,迭代终止并抛出异常。常出现的场景是,两个线程同时对集合进行操作,线程1对集合进行遍历,而线程2对集合进行增加、删除操作,此时将会发生ConcurrentModificationException异常。
    具体方法:多线程访问时要增加同步锁,或者建议使用线程安全的集合:
    1. 使用ConcurrentHashMap替换HashMap,CopyOnWriteArrayList替换ArrayList;
    2. 或者使用使用Vector替换ArrayList,Vector是线程安全的。Vector的缺点:大量数据操作时,由于线程安全,性能比ArrayList低.
  • java.lang.OutOfMemoryError
    该异常表示未能成功分配字节内存,通常是因为内存不足导致的内存溢出。
    [解决方案]:OOM就是内存溢出,即Out of Memory。也就是说内存占有量超过了VM所分配的最大。怎么解决OOM,通常OOM都发生在需要用到大量内存的情况下(创建或解析Bitmap,分配特大的数组等),这里列举常见避免OOM的几个注意点:
    1.适当调整图像大小。
    2.采用合适的缓存策略。
    3.采用低内存占用量的编码方式,比如Bitmap.Config.ARGB_4444比Bitmap.Config.ARGB_8888更省内存。
    4.及时回收Bitmap。
    5.不要在循环中创建过多的本地变量。
    6.自定义对内存分配大小。
    7.特殊情况可在mainfests的Application中增加 android:largeHeap=”true”属性,比如临时创建多个小图片(地图marker)
  • OOM是常见的java错误,OOM主要有:
    1.OOM fo heapjava.lang:OutOfMemoryError: Java heap space,此OOM是由于JVM中heap的最大值不满足需要,将设置heap的最大值调高即可。
    2.OOM for Perm:java.lang:OutOfMemoryError: Java perm space,此OOM是由于JVM中perm的最大值不满足需要,将设置perm的最大值调高即可,参数样例为:-XX:MaxPermSize=512M
    3.OOM for GC=>例如:java.lang:OutOfMemoryError: GC overhead limit exceeded,此OOM是由于JVM在GC时,对象过多,导致内存溢出,建议调整GC的策略
    4.OOM for native thread created:java.lang.OutOfMemoryError: unable to create new native thread,此OOM是由于进程剩余的空间不足,导致创建进程失败
    5.OOM for allocate huge array:Exception in thread “main”: java.lang.OutOfMemoryError: Requested array size exceeds VM limit,此类信息表明应用程序(或者被应用程序调用的APIs)试图分配一个大于堆大小的数组
    6.OOM for small swap:Exception in thread “main”: java.lang.OutOfMemoryError: request <size> bytes for <reason>. Out of swap space?,抛出这类错误,是由于从native堆中分配内存失败,并且堆内存可能接近耗尽
    7.OutOfMemoryError thrown while trying to throw OutOfMemoryError; no stack trace available,抛出这类错误,一般是由于方法重复调用、死循环引起,直至内存耗尽
  • java.lang.InternalError
    该异常表示发生了内部错误,而这个错误是线程在runtime宕机时启动造成的。
    [解决方案]:这类问题一般是是线程开启的太晚了导致的。我们启动一个线程会调用Thread的start方法,start方法会调用名为nativeCreate的本地方法,具体位置在/android/art/runtime/native/java_lang_Thread.cc,名字换成了CreateNativeThread:我们可以看看它的源码:
    void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
    CHECK(java_peer != nullptr);//即为java层的thread实例,包裹着run方法的具体实现
    Thread* self = static_cast<JNIEnvExt*>(env)->self;
    Runtime* runtime = Runtime::Current();
  • // Atomically start the birth of the thread ensuring the runtime isn’t shutting down.
    bool thread_start_during_shutdown = false;//这段代码用来检测thread是否在runtime宕机时start的
    {
    MutexLock mu(self, *Locks::runtime_shutdown_lock_);
    if (runtime->IsShuttingDownLocked()) {
    thread_start_during_shutdown = true;
    } else {
    runtime->StartThreadBirth();
    }
    }
    if (thread_start_during_shutdown) {//若runtime宕机了就抛出异常
    ScopedLocalRef<jclass> error_class(env, env->FindClass(“java/lang/InternalError”));
    env->ThrowNew(error_class.get(), “Thread starting during runtime shutdown”);
    return;
    }
    ….. 省略代码

    从代码我们可以知道,如果runtime宕机了就会抛出InternalError的异常。我们在启动线程的时候,需要确认一下是否有线程嵌套的情况,不要在线程中再去启动一个线程
  • android.view.WindowManager$BadTokenException
    该异常表示不能添加窗口,通常是所要依附的view已经不存在导致的。
    [解决方案]:Dialog&AlertDialog,WindowManager不能正确使用时,经常会报出该异常,原因比较多,几个常见的场景如下:
    1.上一个页面没有destroy的时候,之前的Activity已经接收到了广播。如果此时之前的Activity进行UI层面的操作处理,就会造成crash。UI层面的刷新,一定要注意时机,建议使用set_result来代替广播的形式进行刷新操作,避免使用广播的方式,代码不直观且容易出错。
    2.Dialog在Actitivty退出后弹出。在Dialog调用show方法进行显示时,必须要有一个Activity作为窗口的载体,如果Activity被销毁,那么导致Dialog的窗口载体找不到。建议在Dialog调用show方法之前先判断Activity是否已经被销毁。
    3.Service&Application弹出对话框或WindowManager添加view时,没有设置window type为TYPE_SYSTEM_ALERT。需要在调用dialog.show()方法前添加dialog.getWindow().SetType(WindowManager.LayoutParams.TYPE_SYSTEM_ALERT)。
    4.6.0的系统上, (非定制 rom 行为)若没有给予悬浮窗权限, 会弹出该问题, 可以通过Settings.canDrawOverlays来判断是否有该权限.
    5.某些不稳定的MIUI系统bug引起的权限问题,系统把Toast也当成了系统级弹窗,android6.0的系统Dialog弹窗需要用户手动授权,若果app没有加入SYSTEM_ALERT_WINDOW权限就会报这个错。需要加入给app加系统Dialog弹窗权限,并动态申请权限,不满足第一条会出现没权限闪退,不满足第二条会出现没有Toast的情况。
  • java.lang.IllegalStateException
    状态异常
    java.lang.IllegalStateException异常产生的原因及解决办法
    错误类型大致为以下几种:
    java.lang.IllegalStateException:Cannot forward a response that is already committed
    IllegalStateException:response already commited
    IllegalStateException:getOutputStream() has already been called for this request
    IllegalStateException: Can not perform this action after onSaveInstanceState:
    #解决办法:onSaveInstanceState方法是在该Activity即将被销毁前调用,来保存Activity数据的,如果在保存玩状态后再给它添加Fragment就会出错。解决办法就是把commit()方法替换成 commitAllowingStateLoss()
    错误原因:该异常表示,当前对客户端的响应已经结束,不能在响应已经结束(或说消亡)后再向客户端(实际上是缓冲区)输出任何内容。
    Object is no longer valid to operate on. Was it deleted by another thread?
    该异常表示,realmObject对象在其他线程已被删除,在这个线程中使用的时候抛出的异常。
    具体分析:
    首先解释下flush(),我们知道在使用读写流的时候数据先被读入内存这个缓冲区中, 然后再写入文件,但是当数据读完时不代表数据已经写入文件完毕,因为可能还有一部分仍未写入文件而留在内存中,这时调用flush()方法就会把缓冲区的数据强行清空输出,因此flush()的作用就是保证缓存清空输出。response是服务端对客户端请求的一个响应,其中封装了响应头、状态码、内容等,服务端在把response提交到客户端之前,会向缓冲区内写入响应头和状态码,然后将所有内容flush。这就标志着该次响应已经committed(提交)。对于当前页面中已经committed(提交)的response,就不能再使用这个response向缓冲区写任何东西(注:同一个页面中的response.XXX()是同一个response的不同方法,只要其中一个已经导致了committed,那么其它类似方式的调用都会导致 IllegalStateException异常)。
    参考:http://my.oschina.net/guhai2004/blog/187041,https://github.com/realm/realm-java/issues/1206
  • 补充另一种异常情况:
    我这里的异常是:
    java.lang.IllegalStateException
    Can’t change tag of fragment d{e183845 #0 d{e183845}}: was d{e183845} now d{e183845 #0 d{e183845}}
    经查,我在显示fragment的代码中使用了:
    fragment.show(getSupportFragmentManager, fragment.toString());
    而这里是因为两次toString()结果不同,导致不同的tag指向的是同一个fragment。
    获取fragment的tag的正确方法应该是使用其提供的fragment.getTag()方法。
    补充异常:
    java.lang.IllegalStateException
    Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 37 path $.data
    错误原因:该异常是由于服务器错误返回的JSON字符串和服务器正常下时返回的JSON字符串结构不同,导致利用Gson解析的时候报了一个异常:本该去解析集合却强制去解析对象所致.
    解决办法:在使用Gson解析JSON时try cash一下,不报错按照正常逻辑继续解析,报异常则处理为请求失败逻辑即可.
  • android.os.NetworkOnMainThreadException
    不能在在主程序中进行访问网络操作
  • java.net.URISyntaxException
    URL语法异常。检测你的URL中是否引用了特殊字符或者非法字符
  • java.lang.ArrayIndexOutOfBoundsException
    概述:该异常表示数组越界。
    [解决方案]:这种情况一般要在数组循环前做好length判断,index超出length上限和下限时都会报错。举例如下:一个数组int test[N],一共有N个元素分别是test[0]~test[N-1],如果调用test[N],将会报错。建议读取时,不要超过数组的长度(array.length)。
    Android中一种常见情形就是上拉刷新中header也会作为listview的第0个位置,如果判断失误很容易造成越界。
    异常情况1:
    TextView 中 ellipsize 使用引发 Crash,该问题为 Android 系统 bug,存在于 Android 5.0 及以下设备,问题描述参考:https://code.google.com/p/android/issues/detail?id=33868
    [解决方案]:使用 android:singleLine=”true” 代替 android:lines=”1″ 和 android:maxLines=”1″
    异常情况2:
    PNG转SVG的时候(使用这个网站: http://inloop.github.io/svg2android/.),生成的XML里出现了“e-“这类的pathData值时,Android系统无法计算该数值的bug,存在于 Android 5.0 及以下设备,问题描述详细参考:http://stackoverflow.com/questions/33737192/android-vector-drawable-crash
    [解决方案]:检查你的XML不要出现e-,出现了根据具体数值更改成不带e-的接近数值

标签

发表评论

评论列表(1)