阅读 5862

Android实际开发bug大总结

目录介绍

  • 1.1 java.lang.UnsatisfiedLinkError找不到so库异常
  • 1.2 java.lang.IllegalStateException非法状态异常
  • 1.3 android.content.res.Resources$NotFoundException
  • 1.4 java.lang.IllegalArgumentException参数不匹配异常
  • 1.5 IllegalStateException:Can't compress a recycled bitmap
  • 1.6 java.lang.NullPointerException空指针异常
  • 1.7 android.view.WindowManager$BadTokenException异常
  • 1.8 java.lang.ClassCastException类转化异常
  • 1.9 Toast运行在子线程问题,handler问题
  • 2.1 java.lang.ClassNotFoundException类找不到异常
  • 2.2 java.util.concurrent.TimeoutException连接超时崩溃
  • 2.3 java.lang.NumberFormatException格式转化错误
  • 2.4 java.lang.IllegalStateException: Fragment not attached to Activity
  • 2.5 ArrayIndexOutOfBoundsException 角标越界异常
  • 2.6 IllegalAccessException 方法中构造方法权限异常
  • 2.7 android.view.WindowManager$BadTokenException,dialog弹窗异常
  • 2.8 java.lang.NoClassDefFoundError 找不到类异常
  • 2.9 Android出现:Your project path contains non-ASCII characters.
  • 3.1 OnErrorNotImplementedException【 Can't create handler inside thread that has not called Looper.prepare()】
  • 3.2 adb.exe,start-server' failed -- run manually if necessary
  • 3.3 java.lang.IllegalStateException: ExpectedBEGIN_OBJECT but was STRING at line 1 column 1 path $
  • 3.4 android.content.ActivityNotFoundException: No Activity found to handle Intent
  • 3.5 Package manager has died导致崩溃
  • 3.6 IllegalArgumentException View添加窗口错误
  • 3.7 IllegalStateException: Not allowed to start service Intent异常崩溃
  • 3.8 java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState
  • 3.9 在Fragment中通过getActivity找不到上下文,报null导致空指针异常
  • 4.1 IllegalArgumentException导致崩溃【url地址传入非法参数,转义字符】
  • 4.2 ClassNotFoundException: Didn't find class "*****" on path: /data/app/**错误
  • 4.3 NoClassDefFoundError异常【该异常表示找不到类定义】
  • 4.4 公司之前项目使用客服udesk,sdk更新后初始化导致崩溃问题
  • 4.5 java.util.concurrent.ExecutionException: com.android.tools.aapt2.Aapt2Exception
  • 4.6 java.util.concurrent.ExecutionException: com.android.ide.common.process.ProcessException
  • 4.7 00768556 /vendor/lib/libllvm-glnext.so [armeabi-v8]无法加载so库导致崩溃
  • 4.8 Only the original thread that created a view hierarchy can touch its views
  • 4.9 NoSuchMethodException android.support.v4.app.Fragment$InstantiationException

吕诗禹想换个工作,渴望同行内推

  • 个人信息
    • 姓名:吕诗禹
    • 邮箱:17801164348@163.com
    • 微信:13940574490
    • 目前工作情况:在职状态
    • 工作年限:4年
    • 工作地点:北京
    • 擅长Android性能优化,Java,kotlin,flutter
  • 感谢同行朋友,如果可以,可以直接电话联系或者微信联系!

好消息

  • 博客笔记大汇总【16年3月到至今】,包括Java基础及深入知识点,Android技术博客,Python学习笔记等等,还包括平时开发中遇到的bug汇总,当然也在工作之余收集了大量的面试题,长期更新维护并且修正,持续完善……开源的文件是markdown格式的!同时也开源了生活博客,从12年起,积累共计47篇[近20万字],转载请注明出处,谢谢!
  • 链接地址:github.com/yangchong21…
  • 如果觉得好,可以star一下,谢谢!当然也欢迎提出建议,万事起于忽微,量变引起质变!

1.1 java.lang.UnsatisfiedLinkError

  • A.详细崩溃日志信息
    # main(1)
    java.lang.UnsatisfiedLinkError
    dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.paidian.hwmc-1/base.apk", dex file "/data/app/com.paidian.hwmc-1/base.apk"],nativeLibraryDirectories=[/data/app/com.paidian.hwmc-1/lib/arm64, /data/app/com.paidian.hwmc-1/base.apk!/lib/arm64-v8a, /vendor/lib64, /system/lib64]]] couldn't find "libijkffmpeg.so"
    复制代码
  • B.查看崩溃类信息
    • 这个异常类的大意是:如果Java虚拟机找不到声明为本机的方法的适当本机语言定义,则引发。
    public class UnsatisfiedLinkError extends LinkageError {
        private static final long serialVersionUID = -4019343241616879428L;
    
        public UnsatisfiedLinkError() {
            super();
        }
    
        public UnsatisfiedLinkError(String s) {
            super(s);
        }
    }
    复制代码
  • C.项目中异常分析
    • 根据实际项目可知,当准备播放视频时,找不到libijkffmpeg.so这个库,导致直接崩溃。
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 报这个错误通常是so库加载失败,或者找不到准备执行的JNI方法:
      • 1.建议检查so在安装的过程中是否丢失,没有放入指定的目录下;
      • 2.调用loadLibrary时检查是否调用了正确的so文件名,并对其进行捕获,进行相应的处理,防止程序发生崩溃;
      • 3.检查下so的架构是否跟设备架构一至(如在64-bit架构下调用32-bit的so)。
    • 代码展示
    ndk {
        //根据需要 自行选择添加的对应cpu类型的.so库。
        //abiFilters 'armeabi', 'armeabi-v7a', 'arm64-v8a', 'x86', 'mips'
        abiFilters 'armeabi-v7a'
    }
    
    dependencies {
        compile fileTree(dir: 'libs', include: ['*.jar'])
        //这两个是必须要加的,其它的可供选择
        compile 'tv.danmaku.ijk.media:ijkplayer-java:0.8.4'
        compile 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.4'
        //其他库文件
        //compile 'tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8'
        //compile 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
        //compile 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8'
        //compile 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8'
    }
    复制代码
  • G.知识延申
    • Android 应用开发者应该对 UnsatisfiedLinkError 这种类型的错误比较熟悉了,这个问题一直困扰着广大的开发者,那么有没有想过有可能你什么都没做错,也会出现这个问题呢?
    • 我们在 Android 应用开发测试过程中曾经碰到过这样的案例,apk 在某机型上安装完成之后运行即崩溃,报错 UnsatisfiedLinkError。
    • java.lang.UnsatisfiedLinkError: Couldn’t load mobsec from loader dalvik.system.PathClassLoader.....findLibrary returned null
    • 首先怀疑是在 apk 中相应的 libs\abi 目录下没有放置 libmobsec.so,然而检查发现这个 so 在所有的 libs\abi 下都有放置过,继续排查;
    • 然后的想法是放置的 so 不是对应 abi 的,比如由于粗心在 armeabi 目录下放置了 x86 指令集的 so,导致在 armeabi 指令集手机上加载出错,这个也被排除掉;
    • 就在没有头绪的时候,想到 System.loadLibrary 函数加载 so 时,系统是从指定的路径下加载的,那么这个路径下 so 是否存在呢?
    • 我们知道应用的私有 Native library 目录 /data/data/packagename/lib 是一个符号链接,链接到 /data/app-lib/ 目录,System.loadLibrary 是到这个目录去尝试加载 so 的。
    • adb shell 到这个路径下,使用命令 ls 查看,果然这个 libmobsec.so 是不存在的。那么是什么原因导致的呢?
    • 分析 Android 系统源码的实现,发现 /data/app-lib/ 这个目录下的 so ,是在系统安装 apk 时从 apk 的 lib 目录下去抽取的。

1.2 java.lang.IllegalStateException非法状态异常

  • A.详细崩溃日志信息
    • onSaveInstanceState方法是在该Activity即将被销毁前调用,来保存Activity数据的,如果在保存玩状态后 再给它添加Fragment就会出错。
    IllegalStateException: Can not perform this action after onSaveInstanceState:
    复制代码
  • B.查看崩溃类信息
    • 在非法或不适当的时间调用方法的信号。换句话说,Java环境或Java应用程序没有处于请求操作的适当状态。
    public class IllegalStateException extends RuntimeException {
        public IllegalStateException() {
            super();
        }
    
        public IllegalStateException(String s) {
            super(s);
        }
    
        public IllegalStateException(String message, Throwable cause) {
            super(message, cause);
        }
    
        public IllegalStateException(Throwable cause) {
            super(cause);
        }
    
        static final long serialVersionUID = -1848914673093119416L;
    }
    复制代码
  • C.项目中异常分析
    • 分析
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 解决办法就是把commit()方法替换成 commitAllowingStateLoss()
  • G.其他延申
    • 错误类型大致为以下几种:
    java.lang.IllegalStateException:Can't change tag of fragment d{e183845 #0 d{e183845}}: was d{e183845} now d{e183845 #0 d{e183845}}
    java.lang.IllegalStateException:Expected BEGIN_ARRAY but was BEGIN_OBJECT at line 1 column 37 path $.data
    复制代码
    • 第一种:我在显示fragment的代码中使用了:fragment.show(getSupportFragmentManager, fragment.toString());而这里是因为两次toString()结果不同,导致不同的tag指向的是同一个fragment。获取fragment的tag的正确方法应该是使用其提供的fragment.getTag()方法。
    • 第二种:该异常是由于服务器错误返回的JSON字符串和服务器正常下时返回的JSON字符串结构不同,导致利用Gson解析的时候报了一个异常:本该去解析集合却强制去解析对象所致.解决办法:在使用Gson解析JSON时try cash一下,不报错按照正常逻辑继续解析,报异常则处理为请求失败逻辑即可.

1.3 android.content.res.Resources$NotFoundException

  • A.详细崩溃日志信息
    • Android资源不是可绘制的(颜色或路径)
    Resource is not a Drawable (color or path): TypedValue{t=0x2/d=0x7f040151 a=2}
    android.view.LayoutInflater.createView(LayoutInflater.java:620)
    复制代码
  • B.查看崩溃类信息
    • 当找不到请求的资源时,资源API将引发此异常。
    public static class NotFoundException extends RuntimeException {
        public NotFoundException() {
        }
    
        public NotFoundException(String name) {
            super(name);
        }
    
        public NotFoundException(String name, Exception cause) {
            super(name, cause);
        }
    }
    复制代码
  • C.项目中异常分析
    • 由于将图片资源拷贝到了drawable-land-xhdpi目录下,本来应该拷贝到drawable-xhdpi目录下。
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 1.引用的资源ID 是否能匹配到R.java文件中定义的资源;
    • 2.是否因为缓存等原因导致编译APK时未把资源文件打包进去,可以把APK反编译检查下;
    • 3.是否使用了一个错误的类型来引用了某个资源或者配置资源时存在错误;
    • 4.是否将Int等整型变量作为了参数传给了View.setText调用,这种情况下该整型变量将被认为是一个资源ID号去资源列表中查找对应的资源,导致找不到对应资源错误;解决方法是做类型转换View.setText(String.valueOf(Int id))。

1.4 java.lang.IllegalArgumentException参数不匹配异常

  • A.详细崩溃日志信息
  • B.查看崩溃类信息
    • 参数不匹配异常,通常由于传递了不正确的参数导致。
    public class IllegalArgumentException extends RuntimeException {
        public IllegalArgumentException() {
            super();
        }
    
        public IllegalArgumentException(String s) {
            super(s);
        }
    
        public IllegalArgumentException(String message, Throwable cause) {
            super(message, cause);
        }
    
    
        public IllegalArgumentException(Throwable cause) {
            super(cause);
        }
    
        private static final long serialVersionUID = -5365630128856068164L;
    }
    复制代码
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
  • F.解决办法
  • G.常见的出现场景
    • Activity、Service状态异常;
    • 非法URL;
    • UI线程操作。
    • Fragment中嵌套了子Fragment,Fragment被销毁,而内部Fragment未被销毁,所以导致再次加载时重复,在onDestroyView() 中将内部Fragment销毁即可
    • 在请求网络的回调中使用了glide.into(view),view已经被销毁会导致该错误

1.5 IllegalStateException:Can't compress a recycled bitmap

  • A.详细崩溃日志信息
    • 无法压缩回收位图
    Can't compress a recycled bitmap
    com.paidian.hwmc.utils.i.a(FileUtils.java:75)
    复制代码
  • B.查看崩溃类信息
    • 如果位图已被回收,则希望抛出异常的方法将调用此值。知道了崩溃的具体位置,就该分析具体的原因呢!
    public boolean compress(CompressFormat format, int quality, OutputStream stream) {
        checkRecycled("Can't compress a recycled bitmap");
        //省略代码
        return result;
    }
    
    //如果位图已被回收,则希望抛出异常的方法将调用此值。
    private void checkRecycled(String errorMessage) {
        if (mRecycled) {
            throw new IllegalStateException(errorMessage);
        }
    }
    复制代码
  • C.项目中异常分析
    • 使用了已经被释放过内存的对象。对于Bitmap:Bitmap bitmap=一个bitmap对象。使用过程中调用bitmap.recycle(),之后再使用bitmap就会报错。
  • D.引发崩溃日志的流程分析
    • bitmap.recycle()解释如下所示,释放与此位图关联的本机对象,并清除对像素数据的引用。这将不会同步释放像素数据;它只允许在没有其他引用的情况下对其进行垃圾收集。位图被标记为“死”,这意味着如果调用getPixels()或setPixels(),它将抛出异常,而不会绘制任何内容。此操作不能反转,因此只有在确定没有进一步使用位图的情况下才应调用该操作。这是一个高级调用,通常不需要调用,因为当没有对此位图的引用时,普通GC进程将释放此内存。
    Free the native object associated with this bitmap, and clear the reference to the pixel data
    复制代码
  • F.解决办法
    • 第一种:在使用bitmap前增加判断,if (mBitmap.isRecycled()) return null;

1.6 java.lang.NullPointerException空指针异常

  • A.详细崩溃日志信息
    Please call the AutoSizeConfig#init() first
    com.paidian.hwmc.base.BaseApplication.initAutoSizeConfig(BaseApplication.java:386)
    复制代码
  • B.查看崩溃类信息
    • 空指针异常,也是十分常见的一个异常
    public class NullPointerException extends RuntimeException {
        private static final long serialVersionUID = 5162710183389028792L;
        public NullPointerException() {
            super();
        }
        public NullPointerException(String s) {
            super(s);
        }
    }
    复制代码
  • C.项目中异常分析
    • 空指针发生场景较多,是指某一个对象报null,这个使用去使用它的话就i会报该异常。
  • D.引发崩溃日志的流程分析
    • 导致出现空指针的原因: 必须满足两个条件才会发生空指针:引用变量指向了空,并且调用了这个引用的方法
    • 空指针问题解决思路:
      • 查看Log信息看第一行导致空指针发生的代码,直接双击打开报空指针的类
      • 查看该行代码中有几处调用了方法,则有几个对象可能是空的,找出哪个对象是空的
      • 查看这些对方在哪里赋值了
      • 如果没赋值,则给她赋值,问题解决
      • 如果有地方赋值了,则看这个方法有没有被调用(Ctrl + Shift + G)
      • 如果没有调用(可能没调用或可能调用时机太晚),在使用她前先调用赋值,问题解决
      • 如果有调用,则看是不是有其它地方又给她赋值为null了,如果没有设置为null,则要看赋值的变量和我们使用时的变量是否是同一个变量。
  • F.解决办法
    • 空指针最为常见,也最容易规避,使用的时候一定要进行null check,采取不信任原则:
      • 1.方法形参要判空后才使用;
      • 2.全局变量容易被系统回收或者更改,使用全局变量前建议判空;
      • 3.第三方接口的调用,对返回值进行判空。
      • 4.请注意线程安全

1.7 android.view.WindowManager$BadTokenException异常,Toast报错Unable to add window

  • A.详细崩溃日志信息
    android.view.WindowManager$BadTokenException
        Unable to add window -- token android.os.BinderProxy@7f652b2 is not valid; is your activity running?
    复制代码
  • B.查看崩溃类信息
    • 查询报错日志是从哪里来的
    • image
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
    • 这个异常发生在Toast显示的时候,原因是因为token失效。通常情况下,一般是不会出现这种异常。但是由于在某些情况下, Android进程某个UI线程的某个消息阻塞。导致 TN 的 show 方法 post 出来 0 (显示) 消息位于该消息之后,迟迟没有执行。这时候,NotificationManager 的超时检测结束,删除了 WMS 服务中的 token 记录。删除 token 发生在 Android 进程 show 方法之前。这就导致了上面的异常。
    • 测试代码。模拟一下异常的发生场景,其实很容易,只需要这样做就可以出现上面这个问题
     Toast.makeText(this,"潇湘剑雨-yc",Toast.LENGTH_SHORT).show();
        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    复制代码
  • F.解决办法
    • 目前见过好几种,思考一下那种比较好……
    • 第一种,既然是报is your activity running,那可以不可以在吐司之前先判断一下activity是否running呢?
    • 第二种,抛出异常增加try-catch,代码如下所示,最后仍然无法解决问题
      • 按照源码分析,异常是发生在下一个UI线程消息中,因此在上一个ui线程消息中加入try-catch是没有意义的。而且用到吐司地方这么多,这样做也不方便啦!
    • 第三种,那就是自定义类似吐司Toast的view控件。个人建议除非要求非常高,不然不要这样做。毕竟发生这种异常还是比较少见的
  • G.哪些情况会发生该问题?
    • UI 线程执行了一条非常耗时的操作,比如加载图片等等,就类似上面用 sleep 模拟情况
    • 进程退后台或者息屏了,系统为了减少电量或者某种原因,分配给进程的cpu时间减少,导致进程内的指令并不能被及时执行,这样一样会导致进程看起来”卡顿”的现象
    • 当TN抛出消息的时候,前面有大量的 UI 线程消息等待执行,而每个 UI 线程消息虽然并不卡顿,但是总和如果超过了 NotificationManager 的超时时间,还是会出现问题

1.8 java.lang.ClassCastException类转化异常

  • A.详细崩溃日志信息
    android.widget.FrameLayout cannot be cast to android.widget.RelativeLayout
    com.paidian.hwmc.goods.activity.GoodsDetailsActivity.initView(GoodsDetailsActivity.java:712)
    复制代码
  • B.查看崩溃类信息
    • 抛出以指示代码试图将对象强制转换为它不是实例的子类。
    public class ClassCastException extends RuntimeException {
        private static final long serialVersionUID = -9223365651070458532L;
    
    
        public ClassCastException() {
            super();
        }
    
        public ClassCastException(String s) {
            super(s);
        }
    }
    复制代码
  • C.项目中异常分析
    • 该异常表示类型转换异常,通常是因为一个类对象转换为其他不兼容类对象抛出的异常,检查你要转换的类对象类型。
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 一般在强制类型转换时出现,例如如果A向B转换,而A不是B的父类时,将产生java.lang.ClassCastException异常。一般建议做这时要使用instanceof做一下类型判断,再做转换。
    • 该案例中,需要把FrameLayout更改成RelativeLayout就可以呢

1.9 Toast运行在子线程问题,handler问题

  • A.详细崩溃日志信息
    • 先来看看问题代码,会出现什么问题呢?
    new Thread(new Runnable() {
        @Override
        public void run() {
            ToastUtils.showRoundRectToast("潇湘剑雨-杨充");
        }
    }).start();
    复制代码
    • 报错日志如下所示:
    • image
  • 然后找找报错日志从哪里来的
    • image
  • 子线程中吐司的正确做法,代码如下所示
    new Thread(new Runnable() {
        @Override
        public void run() {
            Looper.prepare();
            ToastUtils.showRoundRectToast("潇湘剑雨-杨充");
            Looper.loop();
        }
    }).start();
    复制代码
  • 得出的结论
    • Toast也可以在子线程执行,不过需要手动提供Looper环境的。
    • Toast在调用show方法显示的时候,内部实现是通过Handler执行的,因此自然是不阻塞Binder线程,另外,如果addView的线程不是Loop线程,执行完就结束了,当然就没机会执行后续的请求,这个是由Hanlder的构造函数保证的。可以看看handler的构造函数,如果Looper==null就会报错,而Toast对象在实例化的时候,也会为自己实例化一个Hanlder,这就是为什么说“一定要在主线程”,其实准确的说应该是 “一定要在Looper非空的线程”。
    • Handler的构造函数如下所示:
    • image
    • image

2.1 java.lang.ClassNotFoundException类找不到异常

  • A.详细崩溃日志信息
    Didn't find class "om.scwang.smartrefresh.layout.SmartRefreshLayout" on path: DexPathList[[zip file "/data/app/com.paidian.hwmc-EsIbVq6e0mFwE0-rPanqdg==/base.apk", zip file "/data/app/com.paidian.hwmc-EsIbVq6e0mFwE0-rPanqdg==/split_lib_dependencies_apk.apk", zip file "/data/app/com.paidian.hwmc-EsIbVq6e0mFwE0-rPanqdg==/split_lib_slice_0_apk.apk", zip file "/data/app/com.paidian.hwmc-EsIbVq6e0mFwE0-rPanqdg==/split_lib_slice_1_apk.apk", zip file "/data/app/com.paidian.hwmc-EsIbVq6e0mFwE0-rPanqdg==/split_lib_s
    com.paidian.hwmc.goods.activity.GoodsDetailsActivity.onCreate(GoodsDetailsActivity.java:209)
    复制代码
  • B.查看崩溃类信息
    • 当应用程序尝试使用字符串名称加载类时引发:但无法找到具有指定名称的类的定义。从1.4版开始,已对此异常进行了修改,以符合通用的异常链接机制。在构建时提供并通过{@link#getException()}方法访问的“在加载类时引发的可选异常”现在称为原因,并且可以通过{@link Throwable#getCace()}方法以及前面提到的“遗留方法”进行访问。
    public class ClassNotFoundException extends ReflectiveOperationException {
        private static final long serialVersionUID = 9176873029745254542L;
        private Throwable ex;
        public ClassNotFoundException() {
            super((Throwable)null);  // Disallow initCause
        }
        public ClassNotFoundException(String s) {
            super(s, null);  //  Disallow initCause
        }
        public ClassNotFoundException(String s, Throwable ex) {
            super(s, null);  //  Disallow initCause
            this.ex = ex;
        }
        public Throwable getException() {
            return ex;
        }
        public Throwable getCause() {
            return ex;
        }
    }
    复制代码
  • C.项目中异常分析
    • 该异常表示在路径下,找不到指定类,通常是因为构建路径问题导致的。
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 类名是以字符串形式标识的,可信度比较低,在调用Class.forName(""),Class.findSystemClass(""),Class.loadClass("")等方法时,找不到类名时将会报错。如果找不到的Class是系统Class,那么可能是系统版本兼容,厂家Rom兼容的问题,找到对应的设备尝试重现,解决方法可以考虑更换Api,或用自己实现的Class替代。
    • 如果找不到的Class是应用自由Class(含第三方SDK的Class),可以通过反编译工具查看对应apk中是否真的缺少该Class,再进行定位,这种往往发生在:
      • 1.要找的Class被混淆了,存在但名字变了;
      • 2.要找的Class未被打入Dex,确实不存在,可能是因为自己的疏忽,或编译环境的冲突;
      • 3.要找的Class确实存在,但你的Classlorder找不到这个Class,往往因为这个Classloder是你自实现的(插件化应用中常见)。
  • G.其他延申

2.2 java.util.concurrent.TimeoutException连接超时崩溃

  • A.详细崩溃日志信息
    java.util.concurrent.TimeoutException: android.view.ThreadedRenderer.finalize() timed out after 10 seconds
    at android.view.ThreadedRenderer.nDeleteProxy(Native Method)
    at android.view.ThreadedRenderer.finalize(ThreadedRenderer.java:423) 
    复制代码
  • B.查看崩溃类信息
    • 当阻塞操作超时引发的异常。指定超时的阻塞操作需要一种方法来指示已发生超时。对于许多此类操作,可以返回指示超时的值;如果不可能或不需要,则应声明并抛出{@code TimeoutException}。
    public class TimeoutException extends Exception {
        private static final long serialVersionUID = 1900926677490660714L;
        public TimeoutException() {}
        public TimeoutException(String message) {
            super(message);
        }
    }
    复制代码
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 一般是系统在gc时,调用对象的finalize超时导致,解决办法:
    • 1.检查分析finalize的实现为什么耗时较高,修复它;
    • 2.检查日志查看GC是否过于频繁,导致超时,减少内容开销,防止内存泄露。
  • G.其他延申

2.3 java.lang.NumberFormatException格式转化错误

  • A.详细崩溃日志信息
    Exception in thread "main" java.lang.NumberFormatException: For input string: "100 "
        at java.lang.NumberFormatException.forInputString(NumberFormatException.java:48)
        at java.lang.Integer.parseInt(Integer.java:458)
        at java.lang.Integer.parseInt(Integer.java:499)
    复制代码
  • B.查看崩溃类信息
    • 引发,以指示应用程序试图将字符串转换为数字类型之一,但该字符串没有适当的格式。
    public class NumberFormatException extends IllegalArgumentException {
        static final long serialVersionUID = -2848938806368998894L;
    
        public NumberFormatException () {
            super();
        }
    
        public NumberFormatException (String s) {
            super (s);
        }
    
        static NumberFormatException forInputString(String s) {
            return new NumberFormatException("For input string: \"" + s + "\"");
        }
    }
    复制代码
  • C.项目中异常分析
    • 错误关键字 java.lang.NumberFormatException 这句话明确告诉了我们是数字格式异常,接着后面有 For input string: "100 " 提示,这就告诉我们,当前想把 "100 " 转换成数字类型时出错了,这样就很确切了。
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 解决办法很简单,改成 Integer.parseInt(str.trim()),注意将字符串转化成整数数据类型时,注意需要trim一下。
  • G.其他延申

2.4 java.lang.IllegalStateException: Fragment not attached to Activity

  • A.详细崩溃日志信息
    java.lang.IllegalStateException: Fragment not attached to Activity
    复制代码
  • B.查看崩溃类信息
  • C.项目中异常分析
    • 出现该异常,是因为Fragment的还没有Attach到Activity时,调用了如getResource()等,需要上下文Content的函数。
    • 出现该异常,是因为Fragment还没有Attach到Activity时,调用了如getResource()等,需要上下文Context的函数。解决方法,就是等将调用的代码写在OnStart()中。
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 将调用的代码运行在Fragment Attached的生命周期内。
    • 第一种:在调用需要Context的函数之前,增加一个判断isAdded()
    if(isAdded()){//isAdded方法是Android系统提供的,只有在Fragment被添加到所属的Activity后才返回true
        activity.getResourses().getString(...);
    }
    复制代码
    • 第二种:如下所示
    @Override
    public void onAttach(Context context) {
        super.onAttach(context);
        activity = (MainActivity) context;
    }
    
    @Override
    public void onDetach() {
        super.onDetach();
        if (activity != null) {
            activity = null;
        }
    }
    复制代码
  • G.其他延申
    • 发生场景:该错误经常发生在fragment的线程中执行了一个耗时操作,线程在执行完毕后会调用getResources来更新ui。如果在线程操作没有完成,就调用getActivity().recreate()重新加载activity或屏幕旋转,这时就会出现Fragment not attached to Activity的错误

2.5 ArrayIndexOutOfBoundsException 角标越界异常

  • A.详细崩溃日志信息
    • 该异常表示数组越界
    java.lang.ArrayIndexOutOfBoundsException: 0
    	at com.example.mytest.CityAdapter.setDataNotify(CityAdapter.java:183)
    	at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    	at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    复制代码
  • B.查看崩溃类信息
    • 引发,以指示已使用非法索引访问数组。索引不是负的,就是大于或等于数组的大小。
    public class ArrayIndexOutOfBoundsException extends IndexOutOfBoundsException {
        private static final long serialVersionUID = -5116101128118950844L;
        public ArrayIndexOutOfBoundsException() {
            super();
        }
        public ArrayIndexOutOfBoundsException(int index) {
            super("Array index out of range: " + index);
        }
        public ArrayIndexOutOfBoundsException(String s) {
            super(s);
        }
        public ArrayIndexOutOfBoundsException(int sourceLength, int index) {
            super("length=" + sourceLength + "; index=" + index);
        }
        public ArrayIndexOutOfBoundsException(int sourceLength, int offset,
                int count) {
            super("length=" + sourceLength + "; regionStart=" + offset
                    + "; regionLength=" + count);
        }
    }
    复制代码
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 这种情况一般要在数组循环前做好length判断,index超出length上限和下限时都会报错。举例如下:一个数组int test[N],一共有N个元素分别是test[0]~test[N-1],如果调用test[N],将会报错。建议读取时,不要超过数组的长度(array.length)。
    • Android中一种常见情形就是上拉刷新中header也会作为listview的第0个位置,如果判断失误很容易造成越界。
  • G.其他延申

2.6 IllegalAccessException 方法中构造方法权限异常

  • A.详细崩溃日志信息
    Unable to instantiate application com.pedaily.yc.meblurry.App: java.lang.IllegalAccessException
    复制代码
  • B.查看崩溃类信息
    • 当应用程序试图反射地创建实例(数组除外)、设置或获取字段或调用方法时,将引发IllegalAccessException,但当前执行的方法无法访问指定的类、字段、方法或构造函数的定义。
    public class IllegalAccessException extends ReflectiveOperationException {
        private static final long serialVersionUID = 6616958222490762034L;
        public IllegalAccessException() {
            super();
        }
        public IllegalAccessException(String s) {
            super(s);
        }
    }
    复制代码
  • C.项目中异常分析
    • 错误提示是,构造方法的权限不对
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 检查了整个Application,才发现,原来有一个无参数的构造方法,被设计成private。修改其为public即可。
  • G.其他延申
    • android BroadcastReceiver遇到java.lang.IllegalAccessException解决方法,错误原因主要是app中其他地方调用了默认的构造函数,必须增加默认构造函数且访问权限为public

2.7 android.view.WindowManager$BadTokenException,dialog弹窗异常

  • A.详细崩溃日志信息
    Unable to add window -- token android.os.BinderProxy@9a57804 is not valid; is your activity running?
    android.view.ViewRootImpl.setView(ViewRootImpl.java:907)
    复制代码
  • B.查看崩溃类信息
    • 在WindowManager中可以找到这个异常类,主要发生在尝试添加视图时引发的
    public static class BadTokenException extends RuntimeException {
        public BadTokenException() {
        }
    
        public BadTokenException(String name) {
            super(name);
        }
    }
    复制代码
  • C.项目中异常分析
    • 该异常表示不能添加窗口,通常是所要依附的view已经不存在导致的。
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 之前项目中有一个自定义弹窗,偶尔会报这个错。解决办法如下代码所示
    • 主要逻辑是在弹窗show或者dismiss的时候,都增加了逻辑判断,判断宿主activity存在。
    /**
     * 展示加载窗
     * @param context               上下文
     * @param isCancel              是否可以取消
     */
    public static void show(Context context,  boolean isCancel) {
        if(context == null){
            return;
        }
        if (context instanceof Activity) {
            if (((Activity) context).isFinishing()) {
                return;
            }
        }
        if (loadDialog != null && loadDialog.isShowing()) {
            return;
        }
        loadDialog = new LoadLayoutDialog(context, isCancel);
        loadDialog.show();
    }
    
    /**
     * 销毁加载窗
     * @param context               上下文
     */
    public static void dismiss(Context context) {
        if(context == null){
            return;
        }
        try {
            if (context instanceof Activity) {
                if (((Activity) context).isFinishing()) {
                    loadDialog = null;
                    return;
                }
            }
            if (loadDialog != null && loadDialog.isShowing()) {
                Context loadContext = loadDialog.getContext();
                if (loadContext instanceof Activity) {
                    if (((Activity) loadContext).isFinishing()) {
                        loadDialog = null;
                        return;
                    }
                }
                loadDialog.dismiss();
                loadDialog = null;
            }
        } catch (Exception e) {
            e.printStackTrace();
            loadDialog = null;
        }
    }
    复制代码
  • G.其他延申
    • Dialog&AlertDialog,Toast,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的情况。
  • H.其他建议
    • 1.不要在非UI线程中使用对话框创建,显示和取消对话框;
    • 2.尽量少用单独线程,出发是真正的耗时操作采用线程,线程也不要直接用Java式的匿名线程,除非是那种单纯的操作,操作完成不需要做其他事情的。
    • 3.如果是在fragment中发起异步网络的回调中进行dialog的操作,那么在操作之前,需要判断 isAdd( ),避免fragment被回收了但是还要求dialog去dismiss
    • 4.在Activity onDestroy中对Dialog提前进行关闭

2.8 java.lang.NoClassDefFoundError 找不到类异常

  • A.详细崩溃日志信息
  • B.查看崩溃类信息
    • 如果Java虚拟机或ClassLoader实例试图加载类的定义(作为普通方法调用的一部分或使用新的表达式创建新实例的一部分),则抛出该类的定义。编译当前执行的类时存在搜索类定义,但无法再找到该定义。
    public class NoClassDefFoundError extends LinkageError {
        private static final long serialVersionUID = 9095859863287012458L;
        public NoClassDefFoundError() {
            super();
        }
        public NoClassDefFoundError(String s) {
            super(s);
        }
        private NoClassDefFoundError(String detailMessage, Throwable throwable) {
            super(detailMessage, throwable);
        }
    }
    复制代码
  • C.项目中异常分析
    • 问题的主要原因:方法数超65536限制。由于实际开发当中的需求不断变更,开源框架越来越多,大多都用第三方SDK,导致方法数很容易超出65536限制。出现错误Java.lang.NoClassDefFoundError
  • D.引发崩溃日志的流程分析
    • 这个错误是Android应用的方法总数限制造成的。android平台的Java虚拟机Dalvik在执行DEX格式的Java应用程序时,使用原生类型short来索引DEX文件中的方法。这意味着单个DEX文件可被引用的方法总数被限制为65536。通常APK包含一个classes.dex文件,因此Android应用的方法总数不能超过这个数量,这包括Android框架、类库和你自己开发的代码。而Android 5.0和更高版本使用名为ART的运行时,它原生支持从APK文件加载多个DEX文件。在应用安装时,它会执行预编译,扫描classes(..N).dex文件然后将其编译成单个.oat文件用于执行. 通熟的讲,就是分包。
  • F.解决办法
    • 64k解决办法
  • G.其他延申
    • 该异常表示找不到类定义,当JVM或者ClassLoader实例尝试装载该类的定义(这通常是一个方法调用或者new表达式创建一个实例过程的一部分)而这个类定义并没有找时所抛出的错误。
    • [解决方案]:NoClassDefFoundError异常一般出现在编译环境和运行环境不一致的情况下,就是说有可能在编译过后更改了Classpath或者jar包所以导致在运行的过程中JVM或者ClassLoader无法找到这个类的定义。
      • 1.分dex包编程,如果依赖的dex包删除了指定的类,执行初始化方法时将会报错;
      • 2.使用第三方SDK或插件化编程时,动态加载或实例化类失败将会报错;
      • 3.系统资源紧张时,当大量class需要加载到内存的时候,处于竞争关系,部分calss竞争失败,导致加载不成功;
      • 4.装载并初始化一个类时失败(比如静态块抛 java.lang.ExceptionInInitializerError 异常),然后再次引用此类也会提示NoClassDefFoundErr 错误;
      • 5.手机系统版本或硬件设备不匹配(如ble设备只支持18以上SDK),程序引用的class在低版本中不存在,导致NoClassDefFoundErr 错误。
      • 6.so文件找不到,设备平台armeabi-v7a,但是我的so库是放在armeabi中的,解决方法新建一个armeabi-v7a包,并且把armeabi的文件拷贝过来.

2.9 Android出现:Your project path contains non-ASCII characters.

  • A.详细崩溃日志信息
  • B.查看崩溃类信息
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 很好解决啦,就是你的工程项目路径或者项目名称包含了中文,修改相关的名称就好
  • G.其他延申

3.1 OnErrorNotImplementedException【 Can't create handler inside thread that has not called Looper.prepare()】

  • A.详细崩溃日志信息
    Can't create handler inside thread that has not called Looper.prepare()
    复制代码
  • B.查看崩溃类信息
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
    • 这是因为Handler对象与其调用者在同一线程中,如果在Handler中设置了延时操作,则调用线程也会堵塞。每个Handler对象都会绑定一个Looper对象,每个Looper对象对应一个消息队列(MessageQueue)。如果在创建Handler时不指定与其绑定的Looper对象,系统默认会将当前线程的Looper绑定到该Handler上。
    • 在主线程中,可以直接使用new Handler()创建Handler对象,其将自动与主线程的Looper对象绑定;在非主线程中直接这样创建Handler则会报错,因为Android系统默认情况下非主线程中没有开启Looper,而Handler对象必须绑定Looper对象。
    • 如果在主线程中创建handler时,系统会自动创建Looper,但是在子线程中创建handler时,是不会自动创建Looper的,此时如果不手动创建Looper,系统就会崩溃
  • F.解决办法
    • 不要在子线程中做UI操作,比如更改界面,吐司等等……
    • 方法1:需先在该线程中手动开启Looper(Looper.prepare()-->Looper.loop()),然后将其绑定到Handler对象上;
    final Runnable runnable = new Runnable() {
      @Override
      public void run() {
        //执行耗时操作
        try {
    
          Log.e("bm", "runnable线程: " + Thread.currentThread().getId()+ " name:" + Thread.currentThread().getName());
    
          Thread.sleep(2000);
          Log.e("bm", "执行完耗时操作了~");
        } catch (InterruptedException e) {
        e.printStackTrace();
        }
      }
    };
    new Thread() {
      public void run() {
        Looper.prepare();
        new Handler().post(runnable);//在子线程中直接去new 一个handler
        Looper.loop();    //这种情况下,Runnable对象是运行在子线程中的,可以进行联网操作,但是不能更新UI
      }
    }.start();
    复制代码
    • 方法2:通过Looper.getMainLooper(),获得主线程的Looper,将其绑定到此Handler对象上。
    final Runnable runnable = new Runnable() {
      @Override
      public void run() {
        //执行耗时操作
        try {
          Log.e("bm", "runnable线程: " + Thread.currentThread().getId()+ " name:" + Thread.currentThread().getName());
          Thread.sleep(2000);
          Log.e("bm", "执行完耗时操作了~");
        } catch (InterruptedException e) {
        e.printStackTrace();
        }
      }
    };
    new Thread() {
      public void run() {
          //在子线程中直接去new 一个handler
        new Handler(Looper.getMainLooper()).post(runnable);
        //这种情况下,Runnable对象是运行在主线程中的,不可以进行联网操作,但是可以更新UI
      }
    }.start();
    复制代码
  • G.其他延申

3.2 platform-tools\adb.exe,start-server' failed -- run manually if necessary

  • A.详细崩溃日志信息
  • B.查看崩溃类信息
  • C.项目中异常分析
    • adb启动失败,端口被占用
  • D.引发崩溃日志的流程分析
  • F.解决办法
    百度google大家多说的是任务管理器 kill掉adb 或者重启adb server,但我任务管理器就没有adb ,猜测是某个程序占用了adb端口。于是按此思路查找。
    5037为adb默认端口 查看该端口情况如下:
    netstat -aon|findstr "5037"
    TCP 127.0.0.1:5037 0.0.0.0:0 LISTENING 6540
    发现6540占用了 5037端口,继续查看6540的task,发现是wandoujia .如下所示
    tasklist|findstr "6540"
    wandoujia_daemon.exe 6540 Console 1 4,276 K
    
    接下来问题就好解决了,在任务管理器kill掉wandoujia_daemon.exe ,运行android程序,ok .
    
    1.关闭xx荚进程
    2.adb kill-server
    3.adb start-server
    复制代码
  • G.其他延申

3.3 java.lang.IllegalStateException: ExpectedBEGIN_OBJECT but was STRING at line 1 column 1 path $

  • A.详细崩溃日志信息
    • 非法参数,开始读取时应该是{}括号,所以需要处理String字符串,它有可能不是标准的json数据
    java.lang.IllegalStateException: ExpectedBEGIN_OBJECT but was STRING at line 1 column 1 path $
    复制代码
  • B.查看崩溃类信息
  • C.项目中异常分析
    • Gson解析数据出现问题,原因服务器返回数据不严谨
  • D.引发崩溃日志的流程分析
    • 可能的错误:
      • bean类字段类型和字段名称不一致。
      • 服务器访问得到的字符串不是纯json前面有空格和回车等字符(难发现)。
      • 如果访问的json字符串不是utf-8编码时,用Gson解析会出这种问题,在日志中打印会发现json的{}前面有乱码字符,也需要注意一下。这是因为不同的编码的原因导致的,因此必须访问utf-8的json字符串,才会减少这种问题。
    • 问题可能是:字符串并不是纯json字符串,开头可能会带有空字符或者回车符,这属于服务器问题,但我们也可以解决。
    • 最重要原因的我们网络请求后结果是字符串,而不是json,因此需要处理。
  • F.解决办法
    /**
    * 判断是否是json结构。这种判断不是很严谨
    */
    public static boolean isJson(String value) {
        try {
            new JSONObject(value);
        } catch (JSONException e) {
            return false;
        }
        return true;
    }
    
    /**
    * 判断是否是json结构
    */
    public static boolean isGoodJson(String json) {
        try {
            new JsonParser().parse(json);
            return true;
        } catch (JsonParseException e) {
            System.out.println("bad json: " + json);
            return false;
        }
    }
    复制代码
  • G.其他延申,补充说明
    • 解决办法:后台输出稳定的Gson格式。此方法工程量太大
    • 真正的问题是我的数据结构有问题
    • 例如下面Json字符串:
    • {"code":1,"info":"success","results":{"id":"1","name":"hehe"}}
    • results对应的应该是一个实体类,如果这个时候想把他解析为String或者List就会出现异常
    • 如果参考使用GsonForm处理后的数据模型,几乎不会出现问题;加入result后面的内容可能在请求时会因为某些原因会存在格式上的变化,这个时候就有出现该异常的风险。Gson中,关键字后面出现""引起来的内容将会被只认为是STRING,“{}”只被认为是类,“[]”只被认为是List,这个几乎是强制性的。
    • 就是说如果你的实体预计是获取String的变量,但是关键字后面对应的却出现了“{”或“[”,那么这个转换将被认为是错误的,抛出异常。

3.4 android.content.ActivityNotFoundException: No Activity found to handle Intent

  • A.详细崩溃日志信息
    android.content.ActivityNotFoundException: No Activity found to handle Intent
    复制代码
  • B.查看崩溃类信息
    • 当调用{@link Context#startActivity}或其变体之一失败时,会引发此异常,因为无法找到执行给定意图的活动。
    public class ActivityNotFoundException extends RuntimeException
    {
        public ActivityNotFoundException()
        {
        }
    
        public ActivityNotFoundException(String name)
        {
            super(name);
        }
    };
    复制代码
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 第一种办法:做一个try catch
    Intent intent = new Intent(Intent.ACTION_SENDTO,url);
    try {
        context.startActivity(intent);
    } catch(ActivityNotFoundException exception) {
        Toast.makeText(this, "no activity", Toast.LENGTH_SHORT).show();
    }
    复制代码
    • 第二种办法:判断是否有应用宝客户端
    //避免安装了应用宝的用户点击其他外部链接走此方法导致崩溃
    //判断是否用应用宝客户端
    if(AppUtils.isPkgInstalled(AdDetailActivity.this,"com.tencent.android.qqdownloader")){
        Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(url));
        startActivity( intent);
    }
    复制代码

3.5 Package manager has died导致崩溃

  • A.详细崩溃日志信息
    出错代码位置
    public static String softVersionName(Context context) {
        PackageInfo info = null;
        try {
            info = context.getPackageManager().getPackageInfo( context.getPackageName(), 0);     //在这里
        } catch (NameNotFoundException e) {
            e.printStackTrace();
        }
        return info.versionName;
    }
    复制代码
  • B.查看崩溃类信息
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
    • 原因分析(Binder造成)
    • 如果一个进程中使用的Binder内容超过了1M,就会crash.
    • 如果Binder的使用超出了一个进程的限制就会抛出TransactionTooLargeException这个异常。
    • 如果是其他原因造成Binder crash的话就会抛出RuntimeException。
  • F.解决办法
    public static String softVersionName(Context context) {
        PackageInfo info = null;
        try {//增加同步块
            synchronized (context) {
                info =context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            }
            return info.versionName;
        } catch (Exception e) {
            e.printStackTrace();
            return "";
        }
    }
    复制代码
  • G.其他延申
    private void test() {
            //这个Demo就是同时创建两个线程来进行Binder调用.
            for (int i = 0; i < 2; i++) {
                new Thread() {
                    @Override
                    public void run() {
                        int count = 0;
                        List<PackageInfo> list = getPackageManager().getInstalledPackages(0);
                        for (PackageInfo info : list) {
                            if(count >=1000){
                                break;
                            }
                            try {
                                PackageInfo pi = getPackageManager().getPackageInfo(info.packageName, PackageManager.GET_ACTIVITIES);
                            } catch (PackageManager.NameNotFoundException e) {
    
                            }
                        }
                    }
                }.start();
            }
        }
    }
    复制代码
    • 错误打印日志
    • image
    • 解决方式:其实只要避免多个线程同时来调用Binder就可以了,毕竟一个线程用了会释放,所以理论上是很难发生的。
    synchronized(MainActivity.class){ 
        PackageInfo pi = getPackageManager() .getPackageInfo(info.packageName, PackageManager.GET_ACTIVITIES); 
    } 
    复制代码

3.6 IllegalArgumentException View添加窗口错误

  • A.详细崩溃日志信息
    View=com.android.internal.policy.impl.PhoneWindow$DecorView{22a4fb16 V.E..... R.....ID 0,0-1080,1020} not attached to window manager
    com.flyco.dialog.widget.base.BaseDialog.superDismiss(BaseDialog.java)
    复制代码
  • B.查看崩溃类信息
  • C.项目中异常分析
    • 该异常表示view没有添加到窗口管理器,通常是我们dismiss对话框的时候,activity已经不存在了,建议不要在非UI线程操作对话框。
  • D.引发崩溃日志的流程分析
    • 常发生这类Exception的情形都是,有一个费时的线程操作,需要显示一个Dialog,在任务开始的时候显示一个对话框,然后当任务完成了在Dismiss对话框,如果在此期间如果Activity因为某种原因被杀掉且又重新启动了,那么当dialog调用dismiss的时候WindowManager检查发现Dialog所属的Activity已经不存在,所以会报错。要避免此类Exception,就要正确的使用对话框,也要正确的使用线程
  • F.解决办法
    • 可以参考崩溃bug日志总结1中的1.7
  • G.其他延申,建议
    • 不要在非UI线程中使用对话框创建,显示和取消对话框;
    • 尽量少用单独线程,出发是真正的耗时操作采用线程,线程也不要直接用Java式的匿名线程,除非是那种单纯的操作,操作完成不需要做其他事情的。
    • 如果是在fragment中发起异步网络的回调中进行dialog的操作,那么在操作之前,需要判断 isAdd( ),避免fragment被回收了但是还要求dialog去dismiss
    • 在Activity onDestroy中对Dialog提前进行关闭

3.7 IllegalStateException: Not allowed to start service Intent异常崩溃

  • A.详细崩溃日志信息
     Caused by: java.lang.IllegalStateException: Not allowed to start service Intent { act=initApplication cmp=com.paidian.hwmc/.service.InitializeService }: app is in background uid UidRecord{a37d28d u0a386 TRNB bg:+5m30s482ms idle procs:3 seq(0,0,0)}
    复制代码
  • B.查看崩溃类信息
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
  • F.解决办法
  • G.其他延申

3.8 java.lang.IllegalStateException:Can not perform this action after onSaveInstanceState

  • A.详细崩溃日志信息
    • image
  • B.查看崩溃类信息
  • C.项目中异常分析
    • 通过下面的源码分析,我们可以知道,出现以上崩溃日志的原因,是因为我们在按下页面返回键的时候,当前Activity以及在执行销毁操作(也就是说我们以前在其他地方调用了finish方法)。
  • D.引发崩溃日志的流程分析
    • 问题所在是Activity#onBackPressed()方法。查看源代码:点击onBackPressed方法中的super
    • 在FragmentActivity中
    @Override
    public void onBackPressed() {
        if (!mFragments.getSupportFragmentManager().popBackStackImmediate()) {
            super.onBackPressed();
        }
    }
    复制代码
    • 接着再次点击super,在Activity中
    public void onBackPressed() {
        if (mActionBar != null && mActionBar.collapseActionView()) {
            return;
        }
    
        if (!mFragments.getFragmentManager().popBackStackImmediate()) {
            finishAfterTransition();
        }
    }
    public void finishAfterTransition() {
        if (!mActivityTransitionState.startExitBackTransition(this)) {
            finish();
        }
    }
    复制代码
    • 我们看到onBackPressed()方法执行了两个操作,第一个是获取当前的FragmentManager,并且执行退栈操作,第二个是在退栈完成之后,执行finish方法。继续查看源码,关键是FragmentManager实现类的popBackStackImmediate方法
    @Override
    public boolean popBackStackImmediate() {
        checkStateLoss();
        executePendingTransactions();
        return popBackStackState(mHost.getHandler(), null, -1, 0);
    }
    复制代码
    • 我们看到,在执行退栈动作之前,这里还有一步检查操作
    private void checkStateLoss() {
        if (mStateSaved) {
            throw new IllegalStateException(
                    "Can not perform this action after onSaveInstanceState");
        }
        if (mNoTransactionsBecause != null) {
            throw new IllegalStateException(
                    "Can not perform this action inside of " + mNoTransactionsBecause);
        }
    }
    复制代码
    • 从这里,我们终于找到了崩溃日志上的异常文案:Can not perform this action after onSaveInstanceState
  • F.解决办法
    • 方案1,在调用super.onBackPressed的时候,我们需要判断当前Activity是否正在执行销毁操作。
    if (!isFinishing()) {
        super.onBackPressed();
    }
    复制代码
    • 方案2,通过上面的源码分析,我们也知道了,super.onBackPressed最后也是调用finish()方法,因此我们可以重写onBackPressed,直接调用finish方法。
  • G.其他延申

3.9 在Fragment中通过getActivity找不到上下文,报null导致空指针异常

  • A.详细崩溃日志信息
  • B.查看崩溃类信息
  • C.项目中异常分析
    • 使用ViewPager+Fragment进行视图滑动,在某些部分逻辑也许我们需要利用上下文Context(例如基本的Toast),但是由于Fragment只是衣服在Activity容器的一个试图,如果需要拿到当前的Activity的上下文Context就必须通过getActivity()获取。
    • 遇过出现getActivity()出现null的时候导致程序报出空指针异常。其实原因可以归结于因为我们在
      • 切换fragment的时候,会频繁被crash
      • 系统内存不足
      • 横竖屏幕切换的时候
      • 以上情况都会导致Activity被系统回收,但是由于fragment的生命周期不会随着Actiivty被回收而被回收,因此才会导致getActivity()出现null的问题。
    • 很多人都曾被这个问题所困扰,如果app长时间在后台运行,再次进入app的时候可能会出现crash,而且fragment会有重叠现象。如果系统内存不足、切换横竖屏、app长时间在后台运行,Activity都可能会被系统回收然后重建,但Fragment并不会随着Activity的回收而被回收,创建的所有Fragment会被保存到Bundle里面,从而导致Fragment丢失对应的Activity。
  • D.引发崩溃日志的流程分析
    • 当遇到getActivity()为null,或getContext()时,先冷静想想以下3点:
      • 1.是不是放在了第三方的回调中
      • 2.是不是在其他进程中调用了(其实第一点就是在其他进程中调用了)
      • 3.是不是调用时不在指定生命周期范围内(onAttach与onDetach之间)
  • F.解决办法
    在Fragment中直接调用
    private MActivity mActivity; 
    @Override 
    public void onAttach(Activity activity) { 
        super.onAttach(activity); 
        mActivity = (MActivity) activity; 
    }
    @Override
    public void onDetach() {
        super.onDetach();
        mActivity = null;
    }
    复制代码
  • G.其他延申
    • 源码解读:在FragmentActivity中
    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        Parcelable p = mFragments.saveAllState();
        if (p != null) {
            outState.putParcelable(FRAGMENTS_TAG, p);
        }
        ……
    }
    复制代码
    • 如果从最近使用的应用里面点击我们的应用,系统会恢复之前被回收的Activity,这个时候FragmentActivity在oncreate里面也会做Fragment的恢复
    @SuppressWarnings("deprecation")
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        mFragments.attachHost(null /*parent*/);
        super.onCreate(savedInstanceState);
        NonConfigurationInstances nc = (NonConfigurationInstances) getLastNonConfigurationInstance();
        if (nc != null) {
            mFragments.restoreLoaderNonConfig(nc.loaders);
        }
        if (savedInstanceState != null) {
            Parcelable p = savedInstanceState.getParcelable(FRAGMENTS_TAG);
            mFragments.restoreAllState(p, nc != null ? nc.fragments : null);
      ……
        }
        if (mPendingFragmentActivityResults == null) {
            mPendingFragmentActivityResults = new SparseArrayCompat<>();
            mNextCandidateRequestIndex = 0;
        }
        mFragments.dispatchCreate();
    }
    复制代码
    • 假设我们的页面叫MyActivity(继承自FragmentActivity),其中用到的Fragment叫MyFragment。出现上面这种情况时,app发生的变化如下:
      • 1、在前面提到的几种情况下系统回收了MyActivity
      • 2、通过onSaveInstanceState保存MyFragment的状态
      • 3、用户再次点击进入app
      • 4、由于MyActivity被回收,系统会重启MyActivity,根据之前保存的MyFragment的状态恢复fragment
      • 5、MyActivity的代码逻辑中,会再次创建新的MyFragment
      • 6、页面出现混乱,覆盖了两层的fragment。假如恢复的MyFragment使用到了getActivity()方法,会报空指针异常
    • 对于上面的问题,可以考虑下面这两种解决办法:
      • 1、不保存fragment的状态:在MyActivity中重写onSaveInstanceState方法,将super.onSaveInstanceState(outState);注释掉,让其不再保存Fragment的状态,达到fragment随MyActivity一起销毁的目的。
      • 2、重建时清除已经保存的fragment的状态:在恢复Fragment之前把Bundle里面的fragment状态数据给清除。方法如下:
      if(savedInstanceState!= null){
          String FRAGMENTS_TAG =  "Android:support:fragments";
          savedInstanceState.remove(FRAGMENTS_TAG);
      }
      复制代码

4.1 IllegalArgumentException导致崩溃【url地址传入非法参数,转义字符】

  • A.详细崩溃日志信息
    • image
  • B.查看崩溃类信息
  • C.项目中异常分析
    • 只有很少一部分传入非法参数导致崩溃,不能直接用常规方法。需要过滤
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • Java调用 URLDecoder.decode(str,"UTF-8");抛出以上的异常,其主要原因是%在URL中是特殊字符,需要特殊转义一下
    public static String replacer(String data) {
        try {
            //使用%25替换字符串中的%号
            data = data.replaceAll("%(?![0-9a-fA-F]{2})", "%25");      
            data = URLDecoder.decode(data, "utf-8");
        } catch (Exception e) {
            e.printStackTrace();
        }
        return data;
    }
    复制代码

4.2 ClassNotFoundException: Didn't find class "*****" on path: /data/app/**错误

  • A.详细崩溃日志信息
    java.lang.RuntimeException: Unable to instantiate activity ComponentInfo{*****Activity}: java.lang.ClassNotFoundException: Didn't find class "*****Activity" on path: /data/app/*******.apk
    复制代码
  • B.查看崩溃类信息
    • 当应用程序尝试使用字符串名称加载类时引发:但无法找到具有指定名称的类的定义。从1.4版开始,已对此异常进行了修改,以符合通用的异常链接机制。在构建时提供并通过{@link#getException()}方法访问的“在加载类时引发的可选异常”现在称为原因,并且可以通过{@link Throwable#getCace()}方法以及前面提到的“遗留方法”进行访问。
    public class ClassNotFoundException extends ReflectiveOperationException {
        private static final long serialVersionUID = 9176873029745254542L;
        private Throwable ex;
        public ClassNotFoundException() {
            super((Throwable)null);  // Disallow initCause
        }
        public ClassNotFoundException(String s) {
            super(s, null);  //  Disallow initCause
        }
        public ClassNotFoundException(String s, Throwable ex) {
            super(s, null);  //  Disallow initCause
            this.ex = ex;
        }
        public Throwable getException() {
            return ex;
        }
        public Throwable getCause() {
            return ex;
        }
    }
    复制代码
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 1。Manifest文件中注册的Activity的名称,有没有写错,包名有没有搞错,有些网友,可能只写一个类名,前面用点号代替,但是这个类不在默认的包内,所以报这个错,那么只要写上类的全名,即可。
    • 2.你的使用的class,是一个外部的JAR包,当在工程中编译使用时,发布成APK并没有包含JAR文件,所以APK在执行的时候就找不到JAR文件,会报错。有些android,需要一些第三方的包,直接将其引入,在以前是可以的,但是在最新的adt中不行,必须在程序中新建一个libs文件夹,将第三方的jar文件copy到libs文件夹中,才行,很多人因为这样才报错,特别是以前的项目,默认并没有这个libs文件夹,但是新版本的adt,默认就建了libs这个文件夹。
    • 3。有一点也很重要,在Java Build Path面板下的Order and Export中,一定要把你引入的jar文件,勾上,否则,跟没引用一样,切记。

4.3 NoClassDefFoundError异常【该异常表示找不到类定义】

  • A.详细崩溃日志信息
    • 经常碰到java.lang.NoClassDefFoundError这样的错误,需要花费很多时间去找错误的原因,具体是哪个类不见了?类明明还在,为什么找不到?而且我们很容易把java.lang.NoClassDefFoundError和java.lang.ClassNotfoundException这两个错误搞混,事实上这两个错误是完全不同的。
  • B.查看崩溃类信息
  • C.项目中异常分析
    • 该异常表示找不到类定义,当JVM或者ClassLoader实例尝试装载该类的定义(这通常是一个方法调用或者new表达式创建一个实例过程的一部分)而这个类定义并没有找时所抛出的错误。
    • NoClassDefFoundError错误的发生,是因为Java虚拟机在编译时能找到合适的类,而在运行时不能找到合适的类导致的错误。
    • 例如在运行时我们想调用某个类的方法或者访问这个类的静态成员的时候,发现这个类不可用,此时Java虚拟机就会抛出NoClassDefFoundError错误。
    • 总结:这个错误,是编译器可用,运行期不可用
  • D.引发崩溃日志的流程分析
    • 对应的Class在java的classpath中不可用
    • 你可能用jar命令运行你的程序,但类并没有在jar文件的manifest文件中的classpath属性中定义
    • 可能程序的启动脚本覆盖了原来的classpath环境变量
    • 因为NoClassDefFoundError是java.lang.LinkageError的一个子类,所以可能由于程序依赖的原生的类库不可用而导致
    • 检查日志文件中是否有java.lang.ExceptionInInitializerError这样的错误,NoClassDefFoundError有可能是由于静态初始化失败导致的
  • F.解决办法
    • 当发生由于缺少jar文件,或者jar文件没有添加到classpath,或者jar的文件名发生变更会导致java.lang.NoClassDefFoundError的错误
    • 由于NoClassDefFoundError是LinkageError的子类,而LinkageError的错误在依赖其他的类时会发生,所以如果你的程序依赖原生的类库和需要的dll不存在时,有可能出现java.lang.NoClassDefFoundError。这种错误也可能抛出java.lang.UnsatisfiedLinkError: no dll in java.library.path Exception Java这样的异常。解决的办法是把依赖的类库和dll跟你的jar包放在一起。
    • 如果你使用Ant构建脚本来生成jar文件和manifest文件,要确保Ant脚本获取的是正确的classpath值写入到manifest.mf文件
    • Jar文件的权限问题也可能导致NoClassDefFoundError,如果你的程序运行在像linux这样多用户的操作系统种,你需要把你应用相关的资源文件,如Jar文件,类库文件,配置文件的权限单独分配给程序所属用户组,如果你使用了多个用户不同程序共享的jar包时,很容易出现权限问题。比如其他用户应用所属权限的jar包你的程序没有权限访问,会导致java.lang.NoClassDefFoundError的错误。
    • 基于XML配置的程序也可能导致NoClassDefFoundError的错误。比如大多数Java的框架像Spring,Struts使用xml配置获取对应的bean信息,如果你输入了错误的名称,程序可能会加载其他错误的类而导致NoClassDefFoundError异常。我们在使用Spring MVC框架或者Apache Struts框架,在部署War文件或者EAR文件时就经常会出现Exception in thread “main” java.lang.NoClassDefFoundError。
  • G.其他延申,常见场景
    • 1.分dex包编程,如果依赖的dex包删除了指定的类,执行初始化方法时将会报错;
    • 2.使用第三方SDK或插件化编程时,动态加载或实例化类失败将会报错;
    • 3.系统资源紧张时,当大量class需要加载到内存的时候,处于竞争关系,部分calss竞争失败,导致加载不成功;
    • 4.装载并初始化一个类时失败(比如静态块抛 java.lang.ExceptionInInitializerError 异常),然后再次引用此类也会提示NoClassDefFoundErr 错误;
    • 5.手机系统版本或硬件设备不匹配(如ble设备只支持18以上SDK),程序引用的class在低版本中不存在,导致NoClassDefFoundErr 错误。
    • 6.so文件找不到,设备平台armeabi-v7a,但是我的so库是放在armeabi中的,解决方法新建一个armeabi-v7a包,并且把armeabi的文件拷贝过来.
  • H.NoClassDefFoundError和ClassNotFoundException区别
    • NoClassDefFoundError发生在JVM在动态运行时,根据你提供的类名,在classpath中找到对应的类进行加载,但当它找不到这个类时,就发生了java.lang.NoClassDefFoundError的错误
    • ClassNotFoundException是在编译的时候在classpath中找不到对应的类而发生的错误

4.4 公司之前项目使用客服udesk,sdk更新后初始化导致崩溃问题

  • 出错原因:初始化udesk客服sdk时,需要传入token,name等信息。token是作为客户的唯一标识,而客服第三方sdk是以token命名创建sqlite数据库,造成数据库名称过长……
  • 报错信息:
    Could not open database, (OS error - 36:File name too long)
    {"code":"4444","message":"未知错误","exception":"Data too long for column 'sdk_token' at row 1"}
    复制代码
  • 日志错误截图
    • image
    • image
  • 解决办法
    • 第一种解决办法:客户端裁剪token前10位[或者15位也行,就是不要长度过长]字符串作为客户唯一标识,经过测试可以解决问题,但是需要发版,并且无法改正之前版本的问题。
    • 第二种解决办法:第三方平台解决,因为之前使用没有问题,后来出现了该问题。可能是他们改代码引起该问题!已经跟第三方发过信息,周一看……

4.5 java.util.concurrent.ExecutionException: com.android.tools.aapt2.Aapt2Exception

  • A.详细崩溃日志信息
    Caused by: java.util.concurrent.ExecutionException: java.util.concurrent.ExecutionException: com.android.tools.aapt2.Aapt2Exception: AAPT2 error: check logs for details
    Caused by: com.android.tools.aapt2.Aapt2Exception: AAPT2 error: check logs for details
    复制代码
  • B.查看崩溃类信息
    public class ExecutionException extends Exception {
        protected ExecutionException() {
            throw new RuntimeException("Stub!");
        }
    
        protected ExecutionException(String message) {
            throw new RuntimeException("Stub!");
        }
    
        public ExecutionException(String message, Throwable cause) {
            throw new RuntimeException("Stub!");
        }
    
        public ExecutionException(Throwable cause) {
            throw new RuntimeException("Stub!");
        }
    }
    复制代码
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 在项目的中,gradle.properties文件中添加一行代码就行,即gradle.properties(Project properties)
    android.enableAapt2=false
    复制代码
  • G.其他延申
    • gradle.properties 里面配置的东西,在gradle 文件里面可以直接引用
    • 在项目根目录的gradle.properties文件配置:
    # 应用版本名称
    VERSION_NAME=1.0.0
    # 应用版本号
    VERSION_CODE=100
    # 支持库版本
    SUPPORT_LIBRARY=24.2.1
    # MIN_SDK_VERSION
    ANDROID_BUILD_MIN_SDK_VERSION=14
    # TARGET_SDK_VERSION
    ANDROID_BUILD_TARGET_SDK_VERSION=24
    # BUILD_SDK_VERSION
    ANDROID_BUILD_SDK_VERSION=24
    # BUILD_TOOLS_VERSION
    ANDROID_BUILD_TOOLS_VERSION=24.0.3
    复制代码
    • 这时候配置app和lib的build.gradle可以这样写:
    android {
        compileSdkVersion project.ANDROID_BUILD_SDK_VERSION as int
        buildToolsVersion project.ANDROID_BUILD_TOOLS_VERSION
     
        defaultConfig {
            applicationId project.APPLICATION_ID // lib项目不需要配置这一项
            versionCode project.VERSION_CODE as int
            versionName project.VERSION_NAME
            minSdkVersion project.ANDROID_BUILD_MIN_SDK_VERSION as int
            targetSdkVersion project.ANDROID_BUILD_TARGET_SDK_VERSION as int
        }
    }
     
    dependencies {
        compile fileTree(include: ['*.jar'], dir: 'libs')
        //这里注意是双引号
        compile "com.android.support:appcompat-v7:${SUPPORT_LIBRARY}"
        compile "com.android.support:design:${SUPPORT_LIBRARY}"
        compile "com.android.support:recyclerview-v7:${SUPPORT_LIBRARY}"
        compile "com.android.support:support-annotations:${SUPPORT_LIBRARY}"
        compile "com.android.support:cardview-v7:${SUPPORT_LIBRARY}"
        compile "com.android.support:support-v4:${SUPPORT_LIBRARY}"
    }
    复制代码

4.6 java.util.concurrent.ExecutionException: com.android.ide.common.process.ProcessException

  • A.详细崩溃日志信息
    • 也就是执行异常,进程异常。
    Caused by: java.util.concurrent.ExecutionException: com.android.ide.common.process.ProcessException: Error while executing process /Users/duanzheng/WorkSpace/AndroidSdk/build-tools/27.0.3/aapt with arguments {package -f --no-crunch -I /Users/duanzheng/WorkSpace/AndroidSdk/platforms/android-27/android.jar -M /Users/duanzheng/.jenkins/workspace/android_hwmc_project/UdeskSDKUI/build/intermediates/manifests/aapt/release/AndroidManifest.xml -S /Users/duanzheng/.jenkins/workspace/android_hwmc_project/UdeskSDKUI/build/intermediates/res/merged/release --non-constant-id -0 apk --no-version-vectors}
    
    Caused by: org.gradle.process.internal.ExecException: Process 'command '/Users/duanzheng/WorkSpace/AndroidSdk/build-tools/27.0.3/aapt'' finished with non-zero exit value 1
    
    
    //注意这里看重点:aapt'' finished with non-zero exit value 1
    复制代码
  • B.查看崩溃类信息
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
  • F.解决办法
    • 网上搜索第一种方法:初步猜测可能由同步下来不需要的build文件造成,clean一下项目看是否能解决。
  • H.aapt是啥

4.7 00768556 /vendor/lib/libllvm-glnext.so [armeabi-v8]无法加载so库导致崩溃

  • A.详细崩溃日志信息
    • 魅族手机,一打开就崩溃。思考[armeabi-v8]是啥?
    1 #00 pc 00768556 /vendor/lib/libllvm-glnext.so [armeabi-v8]
    2 #01 pc 000011e0 <unknown>
    3 java:
    4 [Failed to get java stack] 
    复制代码
  • B.查看崩溃类信息
  • C.项目中异常分析
  • D.引发崩溃日志的流程分析
  • F.解决办法
  • G.其他延申

4.8 Only the original thread that created a view hierarchy can touch its views

  • A.详细崩溃日志信息
    UncaughtException detected: io.reactivex.exceptions.OnErrorNotImplementedException: Only the original thread that created a view hierarchy can touch its views.
    Caused by: android.view.ViewRootImpl$CalledFromWrongThreadException: 
    Only the original thread that created a view hierarchy can touch its views.
    复制代码
  • B.查看崩溃类信息
    • 异常的意思是说只有创建这个view的线程才能操作这个view,普通会认为是将view创建在非UI线程中才会出现这个错误。
  • C.项目中异常分析
    • Android中相关的view和控件操作都不是线程安全的,所以Android才会禁止在非UI线程更新UI,对于显式的非法操作,比如说直接在Activity里创建子线程,然后直接在子线程中操作UI等,Android会直接异常退出,并提示should run on UIThread之类的错误日志信息。
    • 对于隐式非法操作,App不会直接简单粗暴地异常退出,只是出现奇怪结果,Only the original thread that created a view hierarchy can touch its views便是一个例子,字面意思是只有创建视图层次结构的原始线程才能操作它的View,明显是线程安全相关的。
  • D.引发崩溃日志的流程分析
    • 在intentService中发送通知更新购物车页面数据刷新,导致出现该问题。
  • F.解决办法
  • G.其他延申

4.9 NoSuchMethodException android.support.v4.app.Fragment$InstantiationException

  • 详细日志错误
    vity.baojia.ChoiceResultPsActivity}: android.support.v4.app.Fragment$InstantiationException: 
    Unable to instantiate fragment com.cheoo.app.fragment.choiceresultps.QuotationDaQuanFragment: 
    could not find Fragment constructor
    复制代码
  • 可能导致的出现该bug的原因分析
    • 问题主要跟Activity的数据恢复有关,其可能产生的Exception:android.support.v4.app.Fragment$InstantiationException
    • 每个Fragment必须要有一个无参构造方法,这样该Fragment在Activity恢复状态的时候才可以被实例化。
    • 强烈建议,Fragment的子类不要有其他含参构造方法,因为这些构造方法在Fragment重新实例化时不会被调用。取而代之的方式是,通过setArguments(Bundle)设置参数,然后通过getArguments获得参数。如果的Fragment没有无参构造方法,app在恢复Activity时(例如旋转设备),会出现crash。
  • 问题代码如下所示,解决办法就是添加一个无参构造方法即可。
    public static BusinessHotCarFragment newInstance(Map<String,String> map) {
        BusinessHotCarFragment fragment = new BusinessHotCarFragment(map);
        return fragment;
    }
    
    public BusinessHotCarFragment(Map<String,String> map) {
        this.pMap =map;
    }
    
    //解决问题办法,添加一个无参构造方法
    public BusinessHotCarFragment(){}
    复制代码
  • 深入分析为何Fragment需要无参构造函数才可以实例化
    • 既然报的找不到构造方法的错误,先来看一下Fragment的构造函数:
      • 谷歌翻译一下可知。构造函数上有一段注释:默认构造器。每一个Fragment必须有一个无参的构造函数,以便当Activity恢复状态时fragment可以实例化。强烈建议fragment的子类不要有其他的有参构造函数,因为当fragment重新实例化时不会调用这些有参构造函数;如果要传值应该使用setArguments方法,在需要获取这些值时调用getArguments方法。
      /**
       * Default constructor.  <strong>Every</strong> fragment must have an
       * empty constructor, so it can be instantiated when restoring its
       * activity's state.  It is strongly recommended that subclasses do not
       * have other constructors with parameters, since these constructors
       * will not be called when the fragment is re-instantiated; instead,
       * arguments can be supplied by the caller with {@link #setArguments}
       * and later retrieved by the Fragment with {@link #getArguments}.
       *
       * <p>Applications should generally not implement a constructor. Prefer
       * {@link #onAttach(Context)} instead. It is the first place application code can run where
       * the fragment is ready to be used - the point where the fragment is actually associated with
       * its context. Some applications may also want to implement {@link #onInflate} to retrieve
       * attributes from a layout resource, although note this happens when the fragment is attached.
       */
      public Fragment() {
      }
      复制代码
    • 这个异常是从哪里来的?
      • 这一段注释明确的告诉我们使用有参构造函数会出问题,建议使用无参构造函数,但是并没有告诉我们具体是哪里的问题。我们在Fragment源码中搜索could not find Fragment constructor这个异常,发现是在instantiate方法中抛出的。
      • 看上面的代码我们可以知道,Fragment的实例化是通过调用类对象的getConstructor()方法获取构造器对象并调用其newInstance()方法创建对象的。此时还会将args参数设置给Fragment。
      public static Fragment instantiate(Context context, String fname, @Nullable Bundle args) {
          try {
              Class<?> clazz = sClassMap.get(fname);
              if (clazz == null) {
                  // Class not found in the cache, see if it's real, and try to add it
                  clazz = context.getClassLoader().loadClass(fname);
                  sClassMap.put(fname, clazz);
              }
              Fragment f = (Fragment) clazz.getConstructor().newInstance();
              if (args != null) {
                  args.setClassLoader(f.getClass().getClassLoader());
                  f.setArguments(args);
              }
              return f;
          } catch (ClassNotFoundException e) {
              throw new InstantiationException("Unable to instantiate fragment " + fname
                      + ": make sure class name exists, is public, and has an"
                      + " empty constructor that is public", e);
          } catch (java.lang.InstantiationException e) {
              throw new InstantiationException("Unable to instantiate fragment " + fname
                      + ": make sure class name exists, is public, and has an"
                      + " empty constructor that is public", e);
          } catch (IllegalAccessException e) {
              throw new InstantiationException("Unable to instantiate fragment " + fname
                      + ": make sure class name exists, is public, and has an"
                      + " empty constructor that is public", e);
          } catch (NoSuchMethodException e) {
              throw new InstantiationException("Unable to instantiate fragment " + fname
                      + ": could not find Fragment constructor", e);
          } catch (InvocationTargetException e) {
              throw new InstantiationException("Unable to instantiate fragment " + fname
                      + ": calling Fragment constructor caused an exception", e);
          }
      }
      复制代码
    • 这个异常是哪里触发的呢?
      • 找到了具体报错的地方,但是这个方法是在哪里调用触发的呢?在Fragment没有找到调用的地方,由于Fragment是由FragmentManager管理的,在该类发现是在restoreAllState方法中调用的
      • 这方法名意为恢复所有的状态,而其中注释为创建激活Fragment的列表,并将他们从保存的状态中实例化。这个方法应该是Fragment重新实例化时调用的方法。该方法在Fragment的restoreChildFragmentState被调用。
      void restoreAllState(Parcelable state, FragmentManagerNonConfig nonConfig) {
          // Build the full list of active fragments, instantiating them from
          // their saved state.
          mActive = new SparseArray<>(fms.mActive.length);
          for (int i=0; i<fms.mActive.length; i++) {
              FragmentState fs = fms.mActive[i];
              if (fs != null) {
                  FragmentManagerNonConfig childNonConfig = null;
                  if (childNonConfigs != null && i < childNonConfigs.size()) {
                      childNonConfig = childNonConfigs.get(i);
                  }
                  ViewModelStore viewModelStore = null;
                  if (viewModelStores != null && i < viewModelStores.size()) {
                      viewModelStore = viewModelStores.get(i);
                  }
                  Fragment f = fs.instantiate(mHost, mContainer, mParent, childNonConfig,
                          viewModelStore);
                  if (DEBUG) Log.v(TAG, "restoreAllState: active #" + i + ": " + f);
                  mActive.put(f.mIndex, f);
                  // Now that the fragment is instantiated (or came from being
                  // retained above), clear mInstance in case we end up re-restoring
                  // from this FragmentState again.
                  fs.mInstance = null;
              }
          }
      ...   
      }
      复制代码
    • 然后看一下在Fragment的restoreChildFragmentState方法源码。
      • restoreChildFragmentState方法又在Fragment的onCreate方法中调用,这里将保存的savedInstanceState状态又传递给了restoreChildFragmentState以完成Fragment的重新实例化。
      void restoreChildFragmentState(@Nullable Bundle savedInstanceState) {
          if (savedInstanceState != null) {
              Parcelable p = savedInstanceState.getParcelable(
                      FragmentActivity.FRAGMENTS_TAG);
              if (p != null) {
                  if (mChildFragmentManager == null) {
                      instantiateChildFragmentManager();
                  }
                  mChildFragmentManager.restoreAllState(p, mChildNonConfig);
                  mChildNonConfig = null;
                  mChildFragmentManager.dispatchCreate();
              }
          }
      }
      复制代码
    • 接着看看Fragment中的onCreate方法
      @CallSuper
      public void onCreate(@Nullable Bundle savedInstanceState) {
          mCalled = true;
          restoreChildFragmentState(savedInstanceState);
          if (mChildFragmentManager != null
                  && !mChildFragmentManager.isStateAtLeast(Fragment.CREATED)) {
              mChildFragmentManager.dispatchCreate();
          }
      }
      复制代码
    • 得出结论
      • 经过以上的分析,我们就知道了为什么这个错误出在了Fragment的有参构造函数上。因为当Fragment因为某种原因重新创建时,会调用到onCreate方法传入之前保存的状态,在instantiate方法中通过反射无参构造函数创建一个Fragment,并且为Arguments初始化为原来保存的值,而此时如果没有无参构造函数就会抛出异常,造成程序崩溃。

其他介绍

01.关于博客汇总链接

02.关于我的博客

博客开源到GitHub上了,更多可以看:github.com/yangchong21…

关注下面的标签,发现更多相似文章
评论