Android面试复习笔记 4
5. 优化问题
1. ANR
Application Not Responding即应用无响应。
通常主线程中进行耗时操作就会引起。应用启动时,系统会为应用创建一个名为“主线程”的执行线程。 此线程非常重要,因为它负责将事件分派给相应的用户界面小部件,其中包括绘图事件。 此外,它也是应用与 Android UI 工具包组件(来自 android.widget 和 android.view 软件包的组件)进行交互的线程。因此,主线程有时也称为 UI 线程。
系统不会为每个组件实例创建单独的线程。运行于同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。 因此,响应系统回调的方法(例如,报告用户操作的 onKeyDown() 或生命周期回调方法)始终在进程的 UI 线程中运行。如果 UI 线程用来需要处理耗时很长的操作(例如,网络访问或数据库查询)将会阻塞整个 UI,被阻塞超过5秒钟时间,就会引发“应用无响应”(ANR)。
1.1 主要分三种情况
A)KeyDispatchTimeout
页面按键无响应超时,这个Key事件分发超时的时间,Android默认是5秒,主要是定义在ActivityTaskManagerService.java
KEY_DISPATCHING_TIMEOUT_MS = 5*1000;
B)BroadcastTimeOut
广播的超时时间,分为FG前台广播和BG后台广播,分别是10秒和60秒(这里是指单个广播接收器可以处理的最大的时间)。定义在ActivityManagerService.java
BROADCAST_FG_TIMEOUT = 101000;
BROADCAST_BG_TIMEOUT = 601000;
前台广播和后台广播都有各自对应的广播队列,主要区别就是队列最大处理时间不同,前者10S后者60S。
发送时系统默认是后台广播。
若要修改为前台广播,intent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND);
如果想要广播能够更快的被接收,那么可以将它定义为前台广播。
因为系统默认是后台广播队列,前台广播队列比较空闲,可以更快响应。
C)ServiceTimeOut
Service的超时时间为20秒(后端是200S),定义在ActiveServices.java
static final int SERVICE_TIMEOUT = 20*1000;
static final int SERVICE_BACKGROUND_TIMEOUT = SERVICE_TIMEOUT * 10;
1.2 如何避免ANR
a)UI主线程尽量只做跟UI相关的工作.
b)耗时的操作,如I/O,网络连接,把它放入单独的线程处理
c)尽量用Handler来处理UI线程和非UI线程之间的交互
2. 内存
2.1 堆、栈和方法区
JAVA是在JVM所虚拟出的内存环境中运行的,内存分为三个区:堆、栈和方法区。
-
栈(stack):是简单的数据结构,程序运行时系统自动分配,使用完毕后自动释放。优点:速度快。
-
堆(heap):用于存放由new创建的对象和数组。在堆中分配的内存,一方面由java虚拟机自动垃圾回收器来管理,另一方面还需要程序员提供修养,防止内存泄露问题。
-
方法区(method):又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的class和static变量。
2.2 内存溢出、内存泄漏、内存抖动
-
内存溢出(Out of Memory):系统会给每个APP分配内存也就是Heap Size值。当APP占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存时就会抛出的Out Of Memory异常。
-
内存泄漏(Memory Leak):当一个对象不在使用了,本应该被垃圾回收器(JVM)回收。但是这个对象由于被其他正在使用的对象所持有,造成无法被回收的结果。内存泄漏最终会导致内存溢出。
-
内存抖动:内存抖动是指在短时间内有大量的对象被创建或者被回收的现象,主要是循环中大量创建、回收对象。这种情况应当尽量避免。
它们三者的重要等级分别:内存溢出 > 内存泄露 > 内存抖动。
内存溢出对我们的App来说,影响是非常大的。有可能导致程序闪退,无响应等现象,因此,我们一定要优先解决OOM的问题。
原因及处理方法:
1. 单例导致内存泄露,它对应应用程序的生命周期,所以我们在构造单例的时候尽量避免使用`Activity`的上下文,而是使用`Application`的上下文。否则被持有的`Activity`在关闭时候无法被回收。
2. 静态变量导致内存泄露,静态变量存储在方法区,它的生命周期从类加载开始,到整个进程结束。一旦静态变量初始化后,它所持有的引用只有等到进程结束才会释放。
3. 非静态内部类导致内存泄露,非静态内部类(包括匿名内部类)默认就会持有外部类的引用,当非静态内部类对象的生命周期比外部类对象的生命周期长时,就会导致内存泄露。
> 非静态内部类导致的内存泄露在Android开发中有一种典型的场景就是使用`Handler`。
>
> 非静态内部类造成内存泄露还有一种情况就是使用`Thread`或者`AsyncTask`。
> 通常在Android开发中如果要使用内部类,但又要规避内存泄露,一般都会采用*静态内部类+弱引用*的方式。
>
> 同时还需要在`Activity`销毁时就将`mHandler`的回调和发送的消息给移除掉。
4. 未取消注册或回调导致内存泄露。
5. 集合中的对象未清理造成内存泄露。
6. 资源未关闭或释放导致内存泄露。在使用`IO`、`File`流或者`Sqlite`、`Cursor`等资源时要及时关闭。
7. 属性动画造成内存泄露。在`Activity`销毁的时候`cancel`掉属性动画,虽然看不到这个动画了,但是它还在不断播放。
8. `Timer`和`TimerTask`导致内存泄露。当我们`Activity`销毁的时,有可能`Timer`还在继续等待执行`TimerTask`,它持有Activity的引用不能被回收,因此当我们Activity销毁的时候要立即`cancel`掉`Timer`和`TimerTask`,以避免发生内存泄漏。
9. `WebView`造成内存泄露。因为WebView在加载网页后会长期占用内存而不能被释放,在销毁`WebView`之前需要先将`WebView从`父容器中移除,后要调用它的`destory()`方法来销毁它以释放内存。
2.3 强引用、软引用、弱引用、虚引用
-
强引用:强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。 当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。
-
软引用:如果一个对象只具有软引用,但内存空间足够时,垃圾回收器就不会回收它;直到虚拟机报告内存不够时才会回收, 只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存。 软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
-
弱引用(谷歌推荐Android使用):只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间是否足够,都会回收它的内存。 不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。 弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。
-
虚引用:虚引用可以理解为虚设的引用,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。 虚引用主要用来跟踪对象被垃圾回收器回收的活动。
虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 (ReferenceQueue)联合使用。 当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。 程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。 如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
2.4 常用检测内存是否泄漏的工具
LeackCanary、Memory Monitor、DDMS
2.5 GC回收机制与优化
当系统内存不足时就会触发GC回收机制。 还有就是当系统空闲时候会触发,在优先级最低的进程中进行。优先级有五个等级:前台进程,可见进程,服务进程,后台进程,空进程。
1)尽早释放无用对象。
2)合理使用软引用(内存不够时候,遇到就会清理)和弱引用(遇到就会清理)。
3)如果需要使用经常使用的图片,可以使用软引用类型。它可以尽可能将图片保存在内存中,供程序调用,而不引起OutOfMemory;
4)尽量少用静态对象变量,静态变量属于全局变量,不会被GC回收,它们会一直占用内存。
5)能用基本类型如int,long,就不用Integer,Long对象。基本类型变量占用的内存资源比相应对象占用的少得多
3. RecyclerView优化
- 数据处理与视图绑定分离
RecyclerView的bindViewHolder方法是在UI线程进行的,如果在该方法进行耗时操作,将会影响滑动的流畅性。
- 数据优化
分页加载远端数据,对拉取的远端数据进行缓存,提高二次加载速度;
- 布局优化
减少布局层级,可以考虑使用自定义View来减少层级,或者更合理的设置布局来减少层级。
- 减少View对象的创建
一个稍微复杂的 Item 会包含大量的 View,而大量的 View 的创建也会消耗大量时间,所以要尽可能简化 ItemView;设计 ItemType 时,对多 ViewType 能够共用的部分尽量设计成自定义 View,减少 View 的构造和嵌套
- 设置高度固定
如果item高度是固定的话,可以使用RecyclerView.setHasFixedSize(true);来避免requestLayout浪费资源。
- 加大RecyclerView的缓存
用空间换时间,来提高滚动的流畅性。
- 减少ItemView监听器的创建
对ItemView设置监听器,不要对每个item都创建一个监听器,而应该共用一个Listener,然后根据ID来进行不同的操作,优化了对象的频繁创建带来的资源消耗。
- 优化滑动操作
设置RecyclerView.addOnScrollListener();来在滑动过程中停止加载的操作
- 回收资源
通过重写RecyclerView.onViewRecycled(holder)来回收资源。
- 共用RecycledViewPool
如果多个 RecycledView 的 Adapter 是一样的,比如嵌套的 RecyclerView 中存在一样的 Adapter,可以通过设置 RecyclerView.setRecycledViewPool(pool); 来共用一个 RecycledViewPool。
- 处理刷新闪烁
用notifyDataSetChange时,适配器不知道整个数据集中的那些内容以及存在,再重新匹配ViewHolder时会花生闪烁。
设置adapter.setHasStableIds(true),并重写getItemId()来给每个Item一个唯一的ID
4. 如何优化页面启动速度
- 不要在Application的构造方法、attachBaseContext()、onCreate()里面进行初始化耗时操作。尽可能使用IntentService代替初始化工作。
- 不要Activity在onCreate、onStart、onResume当中做耗时操作。
- 合理使用延迟加载。
$ UI适配
1.smallestWidth 限定符屏幕适配方案
根据主流屏幕的 最小宽度 (smallestWidth) 生成一系列 values-sw<N>dp 文件夹 (含有 dimens.xml 文件),当把项目运行到设备上时,系统会根据当前设备屏幕的 最小宽度 (smallestWidth) 去匹配对应的 values-sw<N>dp 文件夹,而对应的 values-sw<N>dp 文件夹中的 dimens.xml 文字中的值,又是根据当前设备屏幕的 最小宽度 (smallestWidth) 而定制的,所以一定能适配当前设备。
2.AndroidAutoSize适配
是今日头条适配方案适配方案的升级版。它支持对Activity、Fragment进行取消适配,灵活性会更强。
在确保设计图总宽度(单位dp)一定时,通过修改density值,确保所有不同尺寸分辨率设备计算出的真实宽度值正好是屏幕宽度,这样就能达到适配所有设备的目的。