第6章 Crash 异常分析
Android 之所以存在千奇百怪的Crash,主要归结于以下几种情况:
1、Android系统的碎片化。
2、接口返回了脏数据
3、混淆时没有keep要使用的类或方法,也会发生找不到类或方法的Crash。
异常信息中经常会出现“方法名”的内容,这就加大了我们准确定位Crash发生原因的难度。
导致Unkown Source的出现有以下亮点原因
1、执行javac时丢失了文件名或行号。为此,我们在进行javac编译时要保留debug信息
2、执行混淆时丢失了文件名和行号。为此,我们要在ProGuard文件中增加以下语句。
对于测试人员使用的测试包,所使用的渠道号,也要与发到线上的渠道号区分开。
6.1 Java语法相关的异常
6.1.1 空指针
1)方法需要对传入的参数判空后再使用。
2)对于外部接口调用,需要确保返回值中不为空,甚至需要确保执行该就饿口不会抛出其他异常导致程序退出。
3)在app中过多使用全局变量,一旦发生内存内收,这些全局变量会被设置为空。
6.1.2 角标越界
异常中的关键字
IndexOutOfBoundsException 是基类。对于字符串截取时发生的越界,会抛出String IndexOutOfBoundsException的异常信息,而对于数据越姐,则会抛出ArrayIndexOutofBoundsException
6.1.3 试图调用一个空对象的方法
Attempt to invoke virtural method on a null object reference
这种Crash产生,因为我们在使用一个对象的某个方法时,这个对象为空,就是说没有实例化。
在一个activity中,调用另一个activity B的方法,为此在B中建立一个static变量。当这个变量被回收时,就会有上述异常。
还有一种可能,推送。点击推送消息,根据事先定好的协议,跳过首页直接进入二级甚至三级页面。这时,二级页面要使用首页某个对象时,这个对象势必为空,那也会引发同样的异常。
6.1.4 类型转换异常
6.1.5 数字转换错误
6.1.6 声明数组时长度为-1
在声明一个数组时,如果数组长度是由另一个变量动态得到的,要保证中括号【】中的值必须大于0。
6.1.7 遍历集合同时删除其中元素
该问题的解决方案是:需要再定义一个列表用来保存删除的对象。
还有一种情况:那就是在多线程中删除同一个集合中的元素。
6.1.8 比较器使用不当
说起Comparator,是基于插入排序算法与归并排序算法相结合的产物,要比我们日常所使用的冒泡排序算法快很多。
6.2 Activity相关的异常
6.2.1 找不到activity
出现错误的原因,URL不是以http开头,代码就会抛出异常。
这类Crash还有一个发生的场景是,当我们要打开SD卡上的一个html页面时,没有weiIntent指定该Html页面所需要的浏览器。
还有一个原因,如果调用百度地图的openBaiduMapNavi方法导致的Crash。
6.2.3 找不到service
6.2.4 不能启动BroadcastReceiver
使用Activit以外的content来startactivity,比如BroadcastReceiver ,就必须指定为Intent.Flag_activity_new Task。
Calling startactivity() from outside of an activity context requires the Flag_activity_new_task flag
6.2.5 startactivityforesult 不能回传
我们看到,number这个字符串为null时,在执行equal语法时就会崩溃。其实是空指针导致的。
6.2.6 猴急的Fragment
发生这个异常,是因为Fragment在还没有attach到activity时,调用了诸如getResource()这样的方法。
相应的解决方案是,在获取资源前先使用isAdded方法进行判断。
isAdd方法是android系统提供的,它只有在Fragment被添加到所属的activity才会返回true。
6.3.2 序列化时未指定classLoader
使用Parcelable机制的时候,会遇到上述异常信息。
classLoader的概念
当classLoader为空时,系统会采取默认的ClassLoader
Android有两种不同的classloader:framworkClassLoader和apkClassLoader。其中framework Classloader知道怎么加载Android内部的类;apk classloader知道怎么加载我们自己写的类,也知道怎么加载Android系统内部的类。
在app刚启动时,默认Classloader是apkClassLoader,但在系统内存不足时应用被系统回收会再次启动,这个默认Classloader会变为FrameworkClassLoader,所以对于我们自己的类会报ClassNotFoundException。
6.3.3 反序列化时发现类找不到:被proguard混线
ProGuard对于Class.forName(className)中的class是无能为力的,它会将这个class混淆得面目全非,于是在反序列化这个类的时候却发现找不到这个类了,自然就会抛出这种异常信息了,相应的解决方法就是,在ProGuard文件中keep这个类。
6.3.4 反序列化时发现类找不到:传入畸形数据
6.3.5 反序列化时出错
网上也有人说是因为FileDescripter太多而且没有关闭,或looper太多没有退出倒置。
6.4 列表相关的异常
6.4.2 ListView滚动时点击刷新按钮后崩溃
listview滚动的时候,表示它已经获取了adapter的getCounts(),可能是30,也可能是更大。回调用getView()。这个时候将数据clear掉了。
解决方法是,listview滚动的时候,将刷新按钮设置为不可点击。
6.4.3 AbsListview的obtainView返回空指针。
导致空指针的罪魁祸首是AbsListView的obtainView方法获取不到View,究其原因是getView方法在某些时候返回null。
解决方案:getView的第二个参数convertView是不会为null的,在getView返回值的时候,判断以下是否为null,则返回convertView。
6.4.4 Adapter数据源变化但是没调用notifyDataSetChanged
PageAdapter对于notifyDataSetChanged()和getCount()的执行顺序是非常严格,系统跟踪count的值,如果这个值和getCount返回的值不一致,就会跑出这个异常。所以为了保证getCount总是返回一个正确的值,那么在初始化ViewPager时,应先给adapter初始化内容,再将该adapter传给ViewPager,如果不这样处理,在更新adapter的内容后,应该调用以下adapter的notifyDataSetChanged方法。
6.5 窗体相关的异常
6.5.1 窗口句柄泄漏
往往因为我们在非诛仙城中的某些操作不当而产生了一个严重的异常,从而强制关闭当前activity。而在关闭的同时,却没能及时调用disimiss来解除对ProgressDialog等的引用。
解决方法,重写activity的ondestory方法,在方法中调用dismis来解除对ProgressDiaglog等的引用
6.5.2 View not attached to window manager
发生这类exception的场景是:有一个费时的线程任务,在任务开始的时候显示一个对话框,然后当任务成了再销毁对话框。在此期间如果activity因为某种原因被杀掉且又重新启动了,那么当dialog调用dismiss方法的时候,windowManager检查发现dialog所述的activity已经不存在了,所以会报View not attached to window manager
1)正确使用对话框。不要再非UI线程中使用对话框创建,显示和取消对话框。
activity都有相应的操作对话框的回调,比如:
onCreateDialog(),showDialog(),dimissDialog(),removeDialog()
2)一定要让对话框对象在activity的可控制范围之内和生命周期之内,比如对话框一定要是activity的成员变量,并且让对话框变量活跃在activity的onCreate()和onDestory()这两个方法之间。
完美解决:从ProgressDialog中派生出safeProgressDialog子类,通过覆写dismiss方法,在ProgressDialog dismiss方法执行之前判断activity是否存在。
6.5.3 窗体在不恰当地时候获取了焦点
这个问题,是因为PopuWindow显示之前,就把焦点赋予了它,结果当然会Crash了。
解决办法是,在创建PopuWindow的时候不立即调用setFoucsable(true),二十载showatLocation再调用setFoucsable(true),同时,在调用dismiss的时候,调用setFouceable(false)。
6.5.4 token null is not for an application
问题出在AlertDialog.Builder(mContext)这句话,所接受的参数不能是getApplicationContext()获得的Context,而应该是activity实例,因为只有一个activity才能添加一个窗体。
6.5.6 is your activity running
PopuWindow的showAtLocation方法。
当参数parent为空时,就会报上述的错误,说token为空了,无效了。由于popuwindow要依附于一个activity,而activity的onCreate()还没执行完。
6.5.7 添加窗体失败
6.5.9 The specified child already has a parent
在使用儿子的时候,要先调用父亲的remoteView方法,解除父子关系。
6.5.10 子线程不能修改UI
只有原始创建这个视图层次的线程才能修改它的视图,也就是说,必须在程序的诛仙城中更新界面显示的工作。
不建议在子线程中更新UI,会因此产生不可预知的错误。
如何在非UI线程更新视图的需要,第一使用handler,另一种是activity中的runonUiThread方法2:利用activity的runonUIThread方法把更新UI的代码创建在Runnable中,这样Runnable对象就能在UI程序中被调用。如果当前线程是UI线程,那么行动是立即执行,如果当前线程不是UI线程,操作是发布到事件队列的线程中。
6.5.11 不能再子线程操作AlertDialog和Toast
AlertDialog,只要在子线程中操作它,就会报上述的错误信息。
相应的解决方法有多种:
方案1:在外面包一层Looper.prepare()和Looper.loop()
方案2:looper的变形
6.6.2 StackOverflowError
无论哪种情况导致的StackOverFlowError,都是由无限递归引起的。在JVM中有一个栈,预设了一个深度,当超过这个深度时,就会抛出StackOverFlowError。
6.6.4 infiateException之FileNotFoundException
GC导致。activity销毁了,但是里面涉及的资源并没有被回收,于是产生内存泄漏了,但是表现为FileNotFoundException。
对此,解决之道,在activity的onStop方法中,手动释放每一张图片资源。
6.6.5 InfiateExcepiton之缺少构造器
创建自定义view的时候,碰到上述这个异常,反复研究后发现缺少一个构造器造成的,其中第二参数用来将xml文件中的属性初始化。
6.6.7 TransactionTooLargeException
Binder最大通常限制为1MB,如果大于1MB的话,就会抛出TransactionLargeException。
相应的解决方法,不要将大量数据传入Binder,比如说图片。
6.7 系统碎片化相关的异常
6.7.2 RemotViews
6.7.3 pointer out of range
1、首先我们定位问题,在做多点触控放大缩小,操作自己所绘制的图形时发生这个异常,如果是操作图片的放大缩小、多点触控不会出现这个错误。
另一种解决方案是
1)让你的view,创建一个子View继承它们中的某一个。
2)重写这个view的onInterceptTouchEvent和onTouchEvent方法。
3)为上述这两个方法增加try..catch语句,捕获已知的异常 。
6.7.4 SecurityException之一:Intent中图片太大。
一般而言,超过1MB的数据,就不要通过Intent来传递了。
6.7.5 SecurityException之二:动态加载其他apk的activity
如果在apk中使用了动态注册BroadcastReceiver,那么launcher动态加载该apk时,就有可能出现java.lang.SecurityException异常。
6.7.6 SecurityException之三:No permission to modify thread
app经常会申请一些权限,而有些手机的rom处于安全考虑,则会禁止这些权限,那么当app使用到这些权限时,就会发生崩溃。
相应的解决方案是,在执行某些安全相关的操作时,要么加上if语句跳过这个操作,要么使用try..catch捕获这类异常,宁肯点击后没有反应,也不能崩溃了。
6.7.7 view的getDrawingCache()返回null
在上面代码中,width和heigth是所要cache的view绘制的宽度和高度。系统所提供的最大DrawingCache值,这个值是这么计算的,当前屏幕的分辨率的高和宽相乘,再乘以4
6.7.9 Android 2.1 不支持SSL
Android 2.1版本不支持SSL,所以发起https的请求会导致崩溃。
解决方案是:调用https的网络请求时,要实现判断Android系统的版本,版本过低要提示用户不能进行操作。
6.7.10 ViewFlipper引发的血案
6.7.12 Android 2.2不支持xlargeScreens
6.7.13 Pack manager has died
解决方案,每次获取PackManager的时候用try...catch。。捕获异常。
6.7.15 Can not perform this action after onSaveInstanceState
commite方法在activity的onSaveInstanceState()之后调用就会出错,因为onSaveInstanceState方法是在Activity即将被销毁之前调用,以保存activity数据,如果在保存完状态后再给它添加Fragment就会出错。
解决办法就是把commit()方法替换成commitAllowingStateLoss()其效果是一样的。
6.7.16 No transaction active
在事务中,逐条循环插入大量数据时会导致这类崩溃。Android中在Sqlite插入数据的时候默认一条语句就是一个事务,有多少条数据就会有多少次磁盘操作,而且不能保证所有数据都能同时插入。
6.8.2 忘记关闭Cursor
android.database.CursorWindowAllocationException
6.8.3 数据库被锁定
当我们试图在不同线程中创建多个连接时,就会抛出这个异常。相应的解决方案是将数据库做成一个单例。
单例固然能解决单进程操作数据库的情况,但是对于多进程app而言,还是需要ContentProvider
6.8.4 试图再打开已经关闭的对象
在实际应用中,app中的IM,最好的做法是,保持数据库一直处于Open状态,等退出聊天室再执行close方法。
6.8.5 文件加密了或无数据库
6.8.6 WebView中SQLite缓存导致的崩溃
WebView中存在着两种缓存:
1、网页数据缓存,存储打开过的页面及资源
2、Html5缓存,即appCache。
WebView自带的缓存机制里面,会将url保存在webviewCashe.db中,将url内容保存在webviewCashe文件夹下。
而对于databases目录下的webview.db和webviewCashe.db,都会自动生成一个1名为android_metadata的表,只要创建SQLite数据库中的表,就会自动创建这个表,表中只有一个locale字。
6.8.7磁盘读写错误
dbhelper只有在创建数据库、进行事务处理时才会锁住数据库。默认情况下,dbhelper会缓存db实例,执行类似于getWritiableDatabase的操作是立即返回的,并不会上锁。
6.10.7 ViewGroup中的玄机
6.10.8 Monkey点击过快的崩溃
6.10.9 图片缩放很多倍
6.10.10 图片宽高为0
第7章 ProGuard技术详解
7.1 ProGuard简介
4个功能
1、压缩(shrink):侦测并移除代码中无用的类,字段、方法和特性
2、优化(optimize):对字节码进行优化,移除无用的指令
3、混淆(Obfuscate):使用a、b、c、d这样简单而无意义的名称,对类、字段和方法进行重名
4、预检:在java平台上对处理后的代码进行预检
7.3.1 基本混淆
9.1.2 竞品分析要研究的几个方向
1、为什么它们的app体积比我们小。
2、为什么它们的app访问速度比我们快
3、为什么他们的app不发版也能上新功能
4、为什么他们的app基本就不怎么崩溃
Android安装包的结构
resouces.arscz 这个文件是编译后的二进制资源文件的索引,也就是apk文件的资源表
lib目录下的子目录armeabi存放的是一些so文件。
meta_inf目录下存放的是签名信息,用来保证apk包的完整性和系统安全。但这个目录下的文件却不会被签名,从而给了我们无限的想象空间
assets目录下面可以看到很多基础数据,以及一些本地用到的html、css和javascript文件
res目录下的anim子目录很值得研究,这个目录存放app所有的动画效果。Android做动画可以使用xml来配置,而不是写代码。
9.3 竞品技术一瞥:开机速度
9.4 竞品技术二瞥:Html5页面的打开速度
9.4.1 把html5页面嵌入到zip包中。app每次启动的时候,会启动一个线程,异步把zip包解压到本地的某个目录下,然后每次从本地读取html5页面,这样就不用每次从服务器加载html5页面了。
如果zip包里的内容有变化怎么办?比如说新增了图片或是修改了html5页面的内容。我们需要有个版本控制机制。每次加载HTML5页面之前,先问以下服务器,当前HTML页面的版本是什么,如果与本地保存的版本号相同,就直接加载本地的html。,否则就从服务器重新下载一个新的zip包,仍然解压到本地相同的目录下。
9.4.2 zip包的增量更新机制
每次有新的html5,都要下载一个最新的zip包,还是很慢。为此,我们要减小zip的体积。
我说的这种增量包,只包括新增的修改的文件,对于删除的文件,我们不用去管它,就把它扔在手机的本地目录下就好了。
9.5.2 安装包为什么那么大 ?
检查以下自家app包中图片和音频文件的大小。图片但凡大小1MB,都需要瘦身。对于500kb-1MB这个区间内,也有瘦身的可能。
音频文件,这个声音很简单,不应该超过10kb
9.5.3 png和jpg的区别及使用场景
众所周知,png有透明通道,而jpg没有,此外png是无损压缩的,而jpg是有损压缩的,所以png中存储的信息会有很多,体积自然就大了。
但手机对于png情有独钟,会对其进行硬件加速 ,所以我们会发现,同样一张背景图,png虽然体积比jpg大,但加载速度却要快一些。
综上所述,对于app包中的图片,我们都使用png格式,而对于要从网上加载的图片,考虑到流量以及下载速度,则使用jpg格式,因为它有较高的压缩率,体积很小。
但是对于背景图,引导页,这种大尺寸的图片,我们还是倾向于使用jpg格式,虽然加载慢一些,但是体积小,减少了包的体积。
对于Splash广告图,就是每次开启app一闪而过的广告,由于我们隔三差五就要从线上下载
下载广告,所以这里使用jpg格式的图片。