目录
1.ANR定义
在Android当中,如果我们的应用程序有一段时间点击不够灵敏,系统就会向用户显示一个对话框。而这个对话框的内容就是ANR,也就是Application Not Responding。这个对话框让用户可以选择等待,程序可以继续运行;同时也可以让用户选择关闭。所以说对于一个流畅的、用户体验良好的、合理的APP当中,是绝对不能出现ANR的。如果出现ANR,势必要让用户处理这个对话框。而这个对话框是十分影响体验的。
2.产生ANR的时间限制
在默认情况下,在一个Activity当中,最长的执行时间是5秒。如果超过了5秒没有做出响应,那就会出现这个ANR的弹框。而在BroadcastReceiver广播接收者当中,最长的执行时间是10秒。你可以在10秒钟之内作出一些操作。如果超出10秒还没有完成,就会造成ANR。
3.ANR产生的原因
在主线程中做了耗时操作,所以才会导致ANR的弹框。
4.ANR产生的原因详细分析
应用程序的响应性是由Activity Manager和WindowManager系统服务监视的。当它监视到Activity或BroadcastReceiver当中在规定时间没有执行完任务时,Android就会弹出ANR的对话框。
而造成ANR的主要原因有两点:
<1>主线程被IO操作(从4.0之后网络IO不允许在主线程中)阻塞。
<2>主线程中存在耗时的计算。
这两个原因归结到的根本都是在主线程中做了非常耗时的操作,就像下载、IO流的读取等等。所以说要尽量把耗时的网络、数据库读取操作,高耗时的一些计算等都放在子线程中完成。之后可以通过Android中的Handler机制,在子线程的任务完成之后,通过Handler将消息转发到主线程中,在主线程进行相关的UI绘制工作。
5.Android中哪些操作是在主线程
<1>Activity的所有生命周期回调都是执行在主线程的。
<2>Service默认是执行在主线程的(如果Service想做耗时操作可以使用IntentService)。
<3>BroadcastReceiver的onReceive回调是执行在主线程的。
<4>没有使用子线程Looper的Handler的handleMessage,post(Runnable)是执行在主线程的。
<5>AsyncTask的回调中除了doInBackground,其他都是执行在主线程。
6.如何解决ANR
<1>使用AsyncTask处理耗时IO操作。AsyncTask是一个灵活的切换子线程到UI线程的机制。
<2>使用Thread或者HandlerThread提高优先级。我们知道,Thread和HandlerThread它都能开启个子线程,区别是后者HandlerThread它在子线程中可以创建Handler来发送消息。因为它内部创建了Looper并关联了消息队列,所以说它能创建Handler。而Thread中是不能创建Handler的。
<3>使用Handler来处理工作线程的耗时任务。
<4>Activity的onCreate和onResume回调中尽量避免耗时的代码。
7.OOM定义
当前占用的内存加上我们申请的内存资源超过了Dalvik虚拟机的最大内存限制就会抛出Out of Memory异常。
8.OOM出现的原理
在Android中,Android系统会为每一个APP分配一个独立的内存空间,即Dalvik虚拟机空间。这样每个APP都可以运行在独立的空间上,而不受其它APP影响。但是我们要知道,Android系统为每一个Dalvik虚拟机都设定了一个最大的内存限制。如果当前占用的内存,加上我们申请的内存资源超过了这个限制,系统就会抛出OOM异常。
9.OOM出现的主要场景
OOM异常在项目开发过程中经常遇到的就是有关于Bitmap大图加载的时候出现。所以说大部分的OOM问题都和Bitmap加载有关。
10.内存溢出、内存抖动、内存泄漏的区别
<1>内存溢出:即Out of Memory,就是当前占用的内存,加上所要申请的内存资源,超过了虚拟机所能承受的最大内存限制时,它就会抛出内存溢出,即Out of Memory这个异常。
<2>内存抖动:短时间内大量的对象被创建,然后又被马上释放,瞬间产生的对象会严重占用内存区。当达到阈值时,它就会触发GC,即垃圾回收。这样你刚刚产生的对象很快又被回收。也就说每一次分配对象占用很少的内存,但是它们叠加在一起就会造成堆内存的压力,从而触发更多的GC,即产生内存抖动。
<3>内存泄漏:进程中的某些对象,已经没有什么作用了,但它仍然被其它正在使用的对象引用,造成垃圾回收器无法对其回收。内存泄漏累积到一定程度,它会造成内存溢出,也就是OOM。
11.如何解决OOM
<1>Bitmap相关方面
- 图片显示:我们要加载合适的图片。要显示缩略图时,我们不要请求网络加载大图,这也是一种优化机制。比如在listview中,我们会监听滑动事件,在滑动的时候不去调用网络请求。只有监听到listview它滑动停止时,我们再去加载大图,图片显示到ImageView上。
- 及时释放内存:首先我们知道,Android系统它其实有自己的垃圾回收机制,也就是java的垃圾回收机制。它可以不定期回收掉不使用的内存空间。同时我们还知道,Bitmap它的构造方法都是私有的,它是通过BitmapFactory这个类来实例化一个Bitmap。BitmapFactory所有生成Bitmap对象都是通过JNI调用方式实现的。所以Bitmap加载到内存后,它是包含两部分内存区域的。简单说就是一部分为java区,一部分为C区。而这个Bitmap对象它是由java部分分配的,不用的时候它会由java的GC回收机制回收掉。而对应的C的那份内存区域,虚拟机是不能直接回收的,所以只能调用底层功能释放。因此,这里说的释放内存释放的其实是C那部分的内存。释放的方法就是调用Bitmap.recycle(),这个方法里面会使用JNI调用对应的底层C语言方法。但事实上不调用该方法也不会OOM。
- 图片压缩:如果一开始就要加载一个很大很大的大图,这个大图它直接超过了内存分配大小,这样就肯定会导致内存溢出。所以这时候就要对加载Bitmap大小进行控制,也就是进行图片压缩。而对Bitmap进行压缩,需要用到Bitmap一个InSampleSize缩放比例的属性。就是把图片加载到内存之前,我们需要计算一个合适的缩放比,避免不必要的大图载入。
- inBitmap属性:它可以提高Android系统在Bitmap分配和释放的执行效率。这个inBitmap属性可以告知Bitmap的decode解码器,在使用已经存在的内存区域,而不是重新申请一块内存区域放Bitmap。即调用decodeBitmap获取新的Bitmap时,它会使用之前那张Bitmap在堆内存中占用的那块内存区域。也就是说,你即使有成百上千张图片,它也只占用屏幕可放下的那么多图片的内存。
- 捕获异常:Android系统在读取bitmap时,它分给虚拟机中图片的堆栈大小是有限制的。因此为了避免应用在分配bitmap内存时出现OOM,在实例化bitmap时,一定要对Out of Memory进行异常的捕获。注意,如果捕获Exception是捕获不到的,因为OOM是一个Error。
<2>其他方面
- listview相关:一个是使用convertView的复用,另一个是对ListView当中大图控件要用lru机制缓存Bitmap。lru是最近最少使用的图片机制,它是个三级缓存机制。
- 避免在onDraw方法里面执行对象的创建:如果在onDraw方法中频繁调用创建对象操作,手机内存会突然上升,然后释放内存时就会造成频繁的GC,这样就会造成前面所说的内存抖动现象。内存抖动积累到一定程度也会OOM,所以这里也是需要避免的。
- 谨慎使用多进程:多进程就是可以把应用中的部分组件运行在单独的进程当中。比如定位,比如webview也可以单独开启一个进程避免内存泄漏。开启单独进程可以扩大内存占用范围。但一定要谨慎使用。多进程代码更加复杂,逻辑更加繁琐,如果使用不当,不仅会造成内存增长,也会造成其他莫名其妙的crash。
12.Bitmap.recycle()方法说明
recycle表示它在释放bitmap内存的时候,它会释放和这个bitmap有关的native内存,同时它会清理有关数据对象的引用。但是这里处理数据对象的引用不是立即清理数据。就是说它不是你调用完这个recycle方法,它就会直接清理native内存。它只是给垃圾回收器发出一条指令,让它在没有其它对象引用这个bitmap对象的时候,会进行垃圾回收。当调用recycle方法之后,这个Bitmap就会被标明为"dead"状态,这时再调用bitmap相关的其它方法,就会引起异常。同时recycle操作是不可逆的,所以说你一定务必要确认好这个Bitmap在以后的场景下不会被你的程序使用到,然后再去谨慎调用recycle方法。因为你调用recycle方法,这个Bitmap就被标为死亡状态,你不能调用它任何的回调方法。官方建议我们不用主动调用recycle方法,因为垃圾回收器,当没有其它对象引用指向该bitmap的时候,它会主动清理内存。我们要根据具体场景决定该方法的调用。
13.LruCache源码分析
<1>LRU算法的意思是最近最少使用的对象,把它清除出队列。LRU算法它其实是一个泛型类LruCache<K, V>。它内部是用LinkedHashMap实现的。它里面提供了get、put方法来完成缓存的添加和获取操作。当缓存满的时候,LRU算法会提供一个trimToSize方法,把较早的缓存对象移除,然后添加新的缓存对象。
<2>trimToSize方法解析
public void trimToSize(int maxSize) {
while (true) {
K key;
V value;
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName()
+ ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize) {
break;
}
Map.Entry<K, V> toEvict = map.eldest();
if (toEvict == null) {
break;
}
key = toEvict.getKey();
value = toEvict.getValue();
map.remove(key);
size -= safeSizeOf(key, value);
evictionCount++;
}
entryRemoved(true, key, value, null);
}
}
这个方法就是说,当它缓存满的时候,这个算法就会移除较早的缓存对象,然后把新的缓存对象添加到这个队列当中。trimToSize方法中做了一个同步代码块的判断
synchronized (this) {
...
}
在同步代码块中有如下语句
synchronized (this) {
if (size < 0 || (map.isEmpty() && size != 0)) {
throw new IllegalStateException(getClass().getName() + ".sizeOf() is reporting inconsistent results!");
}
if (size <= maxSize) {
break;
}
...
}
这里有两个if判断,就是说当里面元素的尺寸小于了它的最大值,我们就会退出循环。
接下来代码其实是做个判断,它计算剩余的size到底有多少。然后我们可以通过它不断地循环在这个if里边做判断。如果小于它的最大值就终止循环。移除时用的是HashMap的remove方法。remove内部调用了removeEntryForKey,它也是使用key值删除对应的值。接着通过safeSizeOf方法计算当前缓存队列的大小,然后从总大小中把它减掉。
我们看到在trimToSize方法中有这样一行代码
public void trimToSize(int maxSize) {
while (true) {
...
synchronized (this) {
...
size -= safeSizeOf(key, value);
...
}
...
}
}
safeSizeOf方法源码如下
private int safeSizeOf(K key, V value) {
int result = sizeOf(key, value);
if (result < 0) {
throw new IllegalStateException("Negative size: " + key + "=" + value);
}
return result;
}
我们可以看到,safeSizeOf方法内部调用了sizeOf方法然后计算缓存对象的大小。
回到trimToSize,方法最后调用了entryRemoved方法。
public void trimToSize(int maxSize) {
while (true) {
...
entryRemoved(true, key, value, null);
}
}
其实这个方法是一个空方法,如果你想在LRU算法中进行二级缓存的操作,可以实现这个方法,并在里边做一些相应业务逻辑的判断。
总结一下,trimToSize方法它会删除那些最老的、最不常用的缓存对象,同时把我们最新的,用的最多的缓存对象,添加到缓存队列当中。这个方法就是LRU算法最核心的方法。
<3>put方法解析
public final V put(K key, V value) {
if (key == null || value == null) {
throw new NullPointerException("key == null || value == null");
}
V previous;
synchronized (this) {
putCount++;
size += safeSizeOf(key, value);
previous = map.put(key, value);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, value);
}
trimToSize(maxSize);
return previous;
}
我们知道,LruCache是由LinkedHashMap实现的,所以put方法就是把缓存对象添加到队列当中。在put方法中有一个同步代码块,代码如下
public final V put(K key, V value) {
...
synchronized (this) {
...
}
...
}
在该代码块当中,它首先会计算size的大小
public final V put(K key, V value) {
...
synchronized (this) {
...
size += safeSizeOf(key, value);
...
}
...
}
接着它会把对应的key、value添加到HashMap当中,而这里的返回值如果为null,说明没有该key对应的元素,添加成功;如果不为null,则说明HashMap中有该key对应的值,并且此时previous即为该值。
public final V put(K key, V value) {
...
synchronized (this) {
...
previous = map.put(key, value);
...
}
...
}
接着要做个判断,如果previous不为空,则把对应的size减去。
public final V put(K key, V value) {
...
synchronized (this) {
...
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
...
}
这样可以得出最新的size大小,然后根据size大小去做轮询判断。如果size大小未超过最大容量,这时就会跳出这个循环,即trimToSize方法中所做的一样。如果previous不为空,它会进行删除元素的操作
public final V put(K key, V value) {
...
if (previous != null) {
entryRemoved(false, key, previous, value);
}
...
}
entryRemoved是个空方法,里面所有的操作可以根据我们的业务逻辑和不同场景来进行相应的实现。
<4>remove方法解析
public final V remove(K key) {
if (key == null) {
throw new NullPointerException("key == null");
}
V previous;
synchronized (this) {
previous = map.remove(key);
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
if (previous != null) {
entryRemoved(false, key, previous, null);
}
return previous;
}
首先它把对应元素从HashMap中移除,代码如下
public final V remove(K key) {
...
synchronized (this) {
previous = map.remove(key);
...
}
...
}
若previous不为null,说明删除成功,则将对应大小从size中减去。
public final V remove(K key) {
...
synchronized (this) {
...
if (previous != null) {
size -= safeSizeOf(key, previous);
}
}
...
}
最后也会执行entryRemoved方法,同样是空方法,要自己实现业务逻辑。
<5>LruCache算法总结
LruCache内部是一个泛型类,并通过一个LinkedHashMap来实现它的功能。同时它提供了get、put等方法来实现添加和获取缓存的操作。这个类最重要的方法是trimToSize方法。trimToSize方法就是会删除最少使用和用的最久的缓存对象,并把新的缓存对象添加到队列里边。
14.缩略图的获取方法
Bitmap节省内存有很多中方法,最重要的技巧是在合适的时间加载合适大小的Bitmap。我们知道现在的照片,包括一些jpg图是越来越大。所以如果你把这些大图直接加载到内存当中,很容易造成OOM。所以官方给出一套合适的加载到内存中的方法,就是根据给出的相应尺寸,获取指定图片的缩略图。
public class BitmapUtil {
/**
* 计算inSampleSize
* @param options
* @param reqWidth
* @param reqHeight
* @return
*/
public static int calculateInSampleSize(BitmapFactory.Options options
,int reqWidth,int reqHeight){
final int height = options.outHeight;
final int width = options.outWidth;
int inSampleSize = 1;
if (height > reqHeight || width > reqWidth){
if (width > height){
inSampleSize = Math.round((float) height / (float) reqHeight);
}else{
inSampleSize = Math.round((float) width / (float) reqWidth);
}
}
return inSampleSize;
}
/**
* 获取对应的缩略图像
* @param path
* @param maxWidth
* @param maxHeight
* @param autoRotate
* @return
*/
public static Bitmap thumbnail(String path,int maxWidth,int maxHeight
,boolean autoRotate){
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
//获取这个图片的宽高信息到options中,此时返回bm为空
Bitmap bitmap = BitmapFactory.decodeFile(path,options);
options.inJustDecodeBounds = false;
//计算缩放比
int sampleSize = calculateInSampleSize(options,maxWidth,maxHeight);
options.inSampleSize = sampleSize;
options.inPreferredConfig = Bitmap.Config.RGB_565;
options.inPurgeable = true;
options.inInputShareable = true;
if (bitmap != null && !bitmap.isRecycled()){
bitmap.recycle();
}
bitmap = BitmapFactory.decodeFile(path,options);
return bitmap;
}
}
可以看到,缩略图与inSampleSize这个缩减比例分不开,它主要是根据inSampleSize的值,来相应地保存Bitmap到内存当中。
可以从源码中看到,这里有一个很重要的属性inJustDecodeBounds。这个属性也是由BitmapFactory.Options传递过来的。该属性设为true,我们获取到bitmap时图片不是真的加载到内存当中,但我们可以获取图片的相关信息。
这样,我们可以在不把图片加载到内存中的情况下,获取到图片相关的数据,包括它的宽高,从而计算出图片对应的inSampleSize。然后将inJustDecodeBounds设为false,并设置相关属性,最后真正将图片缩略后的图加载到内存当中。
15.三级缓存简要介绍
三级缓存包括网络、本地、内存三级。
使用三级缓存可以减少不必要的网络图片加载,减少流量的损耗。
它的原理是:如果你首次打开APP,你要加载一些图片,这时候你肯定只能从网络上去获取。当你从网络上加载好图片之后,你会把图片保存在SD卡和相应的内存当中各一份。这时当你再次请求同样url的Bitmap时,你就不用从网络上获取了,这时候你直接从本地或内存中获取就可以了。
16.UI卡顿原理
Android系统每隔16ms它会发出信号,触发对UI进行渲染。如果每一次渲染都成功,就能达到流畅画面需要的60fps,也就是每秒60帧。在执行一些动画或listview滑动的时候,我们通常能感觉到有时会有一些卡顿和不流畅。这就是因为这里的操作很复杂,然后产生丢帧现象,最终导致了卡顿。
17.为什么把标准设在60fps
人脑对于画面的连贯性有一定限制,对于手机来说,我们需要感知屏幕操作连贯性,而Android系统把这种流畅的帧率规定在60fps。为了保证不出险丢帧,我们一定要在16ms内处理完这次所有的cpu、gpu的计算、绘制和渲染操作。
18.GC与UI卡顿
每次dalvik虚拟机进行GC的时候,所有的线程它都会暂停。当GC完了,所有线程才能继续执行。当16ms内进行渲染的时候,如果正好遇到了大量的GC操作,这就会导致渲染时间不够,从而造成卡顿问题。大量的GC也会导致内存抖动,所以何时触发GC是要谨慎考虑的。
19.overdraw与UI卡顿
overdraw是过渡绘制的意思。过渡绘制就是屏幕上的某个像素在同一帧的时间内被绘制了多次。这种情况经常出现在多层次的UI结构中。过渡绘制会浪费了CPU与GPU的资源,从而造成UI卡顿。造成过渡绘制的原因可能是layout过于复杂,或UI上层叠太多其它的layout布局,也可能是动画执行次数过多。
20.UI卡顿原因分析
<1>人为在UI线程中做轻微耗时操作,导致UI线程卡顿。
<2>布局layout过于复杂,无法在16ms内完成渲染。
<3>同一时间动画执行的次数过多,导致CPU或GPU负载过重。
<4>View过渡绘制,导致某些像素在同一帧时间内被绘制多次,从而使CPU或GPU负载过重。
<5>View频繁的触发measure,layout,导致measure,layout累计耗时过多及整个View频繁的重新渲染。
<6>内存频繁触发GC过多,导致暂时阻塞渲染操作。
<7>冗余资源及逻辑导致加载和执行缓慢,这就说在开发过程中,一些代码的逻辑、代码的启动顺序,以及一些异步任务的处理,它会导致整个UI卡顿。
<8>ANR
21.UI卡顿优化总结
<1>布局优化,可以使用常见的include、merge、ViewStub标签。尽量不存在冗余嵌套以及过于复杂的布局。尽量使用gone代替invisible。尽量使用weight代替长和宽来减少运算。item如果存在非常复杂的嵌套时,可以考虑使用自定义View来取代。这样可以减少measure测量和layout摆放的次数,从而提高APP的UI性能。
<2>列表及Adapter优化,这主要体现在ListView上。我们尽量复用ListView在Adapter中getView这个方法。不要重复获取实例,尽量使用convertView。列表滑动时不要进行元素的更新。根据ListView滑动监听事件,只有它在滑动到停止时,你再去加载图片,加载数据。而在滑动时,可以只显示默认图片,或者缩略图。
<3>背景和图片等内存分配优化。首先要尽量减少整个布局当中不必要的背景设置。其次图片要进行压缩,否则大量的图片一方面耗费内存资源,另一方面大量的GC操作会影响我们16ms内完成界面的绘制,并会引起内存抖动甚至内存泄漏。
<4>避免ANR,不要在UI线程做耗时操作,一定要开启子线程去做耗时操作。
22.内存泄漏的分析工具
内存泄漏这个现象并不是直接可见的,因为它是在内存当中的堆活动。那么如果我们想要检测代码当中是否有内存泄漏的产生,通常我们可以借助一些工具,比如LeakCanary、MAT来检测我们应用程序是否存在内存泄漏。MAT是一款非常强大的内存分析工具。它功能非常多,它能很精密地去分析内存当中的现象。而LeakCanary是Square开源的一款轻量第三方内存泄漏检测工具。它一旦检测到代码当中有内存泄漏的产生,它会以最直观的形式,即Dialog的形式告诉我们内存泄漏在哪产生的,怎么产生的。它有一个内存泄漏链,所以LeakCanary它其实就是通过获取一个即将要销毁的对象,然后它会在这个对象的生命周期方法当中去监控它的状态。
23.内存泄漏和内存溢出的区别和联系
<1>内存溢出它是由于我们应用消耗的内存,或者说应用申请的内存,它超过了虚拟机所能分配给它的内存的额定大小。那么这时候内存就不够用了,就产生了内存溢出现象。
<2>内存泄漏是指A对象不再被使用了,但是正在使用的B对象却引用着它,造成垃圾回收期无法回收A对象。
<3>内存泄漏它有可能引起内存溢出。因为一旦内存泄漏非常严重,它导致垃圾回收器不能回收它所想要的对象,那么这时候你的内存占用空间就会越来越高,其它对象想要分配内存的额度也越来来越少。这时就会导致内存溢出。
24.为什么产生内存泄漏
时候当一个对象它不再被使用的,它本应该被垃圾回收,这时候由于被另外一个正在使用着的对象持有了想要被垃圾回收器回收这个对象的引用,从而导致该对象不能被回收。这个对象由于不能被回收,它就永远地停留在堆内存当中。这就造成了我们的内存泄漏。在Android当中,有一些对象它只有有限的生命周期,比如AsyncTask,新创建的Runnable,它们都是有限的生命周期。当它们在线程当中执行耗时任务执行完了以后,它们就应该被回收掉,正常情况下应该是这样。但是一旦AsyncTask也好,Runnable也好,它们被其它的实例引用着,而导致它们无法被回收,那么就也会造成内存泄漏。而且随着内存泄漏的积累,APP将会消耗我们的内存,就会导致我们的内存溢出现象。
25.Android常见内存泄漏情况
<1>单例:单例引起的内存泄漏可以归纳为长生命周期对象持有了短生命周期对象的引用,而导致了短生命周期对象在要回收的时候无法回收。我们在单例中解决内存泄漏的方法是传入getApplicationContext,而不是传入Activity的上下文。这样我们就可以保证单例生命周期与应用的生命周期一样长,就防止了内存泄漏的产生。
<2>handler:handler造成内存泄漏是非常常见的。因为我们知道,Handler和Message、MessageQueue它经常是关联到一块的。所以说如果你在一个Activity当中延迟发送一个Message,那么由于这个Message和它的Activity对象一直被一个MessageQueue所持有,而这个Message由于又持有了Handler引用,在延迟发送消息的时候,如果这个Handler所在的Activity要被回收掉,那么由于Activity、Handler、Message它们三个关联到一块了,而导致Activity无法被回收。其实根本原因就是Handler生命周期和Activity生命周期是不一致的,所以说导致内存泄漏的产生。解决方法第一可以将Handler设为静态内部类,同时还可以通过弱引用的方式类引入我们的Activity。最后在Activity生命周期onDestroy的时候我们可以回收remove掉我们的message。这样就可以防止Handler引起的内存泄漏。
<3>线程引起:如AsyncTask、Runnable。如果它们都是以匿名内部类的形式创建的话,它们就会持有当前外部类Activity一个隐式的引用。那么如果Activity在销毁之前,AsyncTask、Runnable内部的耗时任务还没有完成的话,它就会导致Activity的内存无法被垃圾回收器回收掉,从而造成内存泄漏。正确做法和Handler类似,你可以使用静态内部类的方式来避免Activity的内存泄漏,同时在Activity销毁的时候,取消掉AsyncTask和Runnable的任务。这样避免我们内存泄漏的同时,又能节省资源。
<4>WebView:WebView在解析网页的时候,如果页面非常复杂,就会占用非常大的内存。如果页面中包含非常复杂的图片、视频,那么内存占用会更严重。同时,当我们打开新页面时,为了能够快速回退,之前页面占用的内存也不会被释放,这样加载网页较多的时候,就会导致我们系统不堪重负,最终甚至有可能导致强制关闭应用。如果我们在onDestroy中销毁WebView,我们会发现其实并没有什么作用。那么如何解决WebView的内存泄漏问题?其实最好的方法就是把承载WebView的Activity放在一个单独的进程当中。当我们检测到应用占用内存过大时,就主动杀死这个进程。因为系统分配内存它是以进程为准的,进程关闭后,系统就会回收掉所有内存。这是处理WebView内存泄漏的一个很好的方法。
26.单例造成内存泄漏的原因
单例在我们程序当中经常会看到,它就用于解决我们APP当中重复创建对象的问题。但是,由于单例是静态的,静态的就导致它的生命周期和APP的生命周期是一样长的。所以说如果使用不当的话,就会导致我们这个单例它会持有Activity引用,而且是长期持有Activity引用,导致Activity无法被回收,造成内存泄漏。
27.如何解决单例造成的内存泄漏问题
只要传入的上下文为getApplicationContext(),不要用Activity上下文,就可以解决单例的内存泄漏问题。因为Application的生命周期就代表APP的生命周期,而单例的静态属性也使它的生命周期是与APP一致的,这样就不会导致产生内存泄漏。
28.Handler造成内存泄漏的原因
我们先上一段熟悉的代码
private final Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
用上述方法定义的非静态内部类会隐性持有外部类的引用。当我们在Activity中延时发送消息到Handler中时,Message就持有了Handler对象的引用。而Handler是非静态内部类,所以持有了外部类的引用,即持有Activity引用。那么这样GC时,垃圾回收器就无法去回收Activity引用。这样就造成了内存泄漏。
其实Handler造成内存泄漏非常常见,再简单总结一下Handler。Handler经常和Message、MessageQueue关联在一起,所以说一旦你的Handler当中有尚未被处理和发送的Message,那么这时候由于Message它会持有Handler引用,而非静态内部类它又持有外部类即Activity引用。导致外部类Activity无法被回收。
29.如何解决Handler造成的内存泄漏问题
<1>我们要在这个Activity当中避免去使用Handler的非静态内部类,我们可以将这个Handler命名为静态的。
private MyHandler myHandler= new MyHandler(this);
这样它存活的生命周期就和Activity生命周期无关了。
<2>同时我们要重写Hanlder
private static class MyHandler extends Handler {
private WeakReference<Context> reference;
public MyHandler(Context context){
reference = new WeakReference<>(context);
}
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//所在的Activity是什么这里写什么
MainActivity activity = (MainActivity) reference.get();
if (activity!=null){
//执行UI线程操作
}
}
}
在这个Handler当中我们通过弱引用WeakRefernce形式来去引用我们的activity。这样我们就可以避免将Activity作为Context传到我们Handler当中。什么是弱引用,就是在垃圾回收器当中,线程扫描它所管辖内存区域的时候,一旦它发现弱引用对象,这时候它就会根据内存空间足够与否,来去判断是否回收它。由于我们被延时处理的Message它持有了Handler的引用,而这个Handler又持有了Activity的引用,就导致这样一个引用链。
<3>最后在Activity被销毁时要尝试去移除Message,代码如下:
mHandler.removeCallbacksAndMessages(null);
有了这三步,Handler引起的内存泄漏就可以很好地解决了。
30.内存管理机制概述
从操作系统的角度来说,内存其实就是一块数据存储区域。它属于可以被操作系统调度的资源。在现代多进程的操作系统当中,内存管理十分重要。操作系统会为每一个进程合理地分配内存资源。这里从两个角度去分析什么是内存管理机制。首先是分配机制。操作系统它会为每一个进程分配一个合理的内存大小,从而才能保证每一个进程能够正常的运行,不至于内存不够使用或占用太多内存。这就是操作系统里面分配机制的概念。接着是回收机制。内存回收机制是指在系统内存不足的时候,它会有一个合理的回收和再分配内存资源的机制。这样是为了保证新的进程能够正常运行。系统回收内存时需要杀死那些正在占用内存的进程,因此操作系统需要提供一个合理的杀死这些进程的机制,保证将副作用降到最低。而对于Android系统来说,对内存的管理也是有一套自己不一样的办法。与PC端不同,Android作为移动操作系统,一般情况下它的内存资源比PC更少,所以在这里我们就需要更加谨慎地管理内存。
31.Android内存管理机制
<1>分配机制
Android在为每一个进程分配内存的时候,采用了一个弹性的分配方式。所谓弹性的分配方式,是指系统一开始不会为APP分配太多内存。它会给每个APP分配一个小额的量。这个小额的量是根据每一个移动端设备实际的物理RAW尺寸大小来决定的。之后随着APP的不断运行,当前内存可能会不够使用了,这时Android系统会为该APP进程分配额外的内存。但是,这个额外内存的大小不是随意分配的,它有一定的限度。而这个限度的原则就是让更多的APP进程存活在内存当中。这样当用户下次再启动该APP进程时,就不用重新创建进程,只需恢复已有进程即可。这样可以减少应用启动时间,提高用户体验。
<2>回收机制
Android对内存使用遵循尽最大限度使用的宗旨。这继承了Linux的特点。Android系统会在内存中保存尽可能多的数据。所以这样就会有一个缺点,有些进程已经不再使用,但它的数据还保存在内存中。同时,Android不推荐直接退出应用,因为这样当下次用户进入应用时,就不用重新创建进程,减少了启动时间。那么当Android系统发现内存不够使用的时候,Android就会进行内存回收的操作,杀死其他进程,回收足够的内存,从而开启新的进程。这里对于Android进程分配它有一个优先级的概念。它主要分为5个进程:前台进程(屏幕当中显示的进程)、可见进程(前台进程已经不在前台,但用户仍能看见)、服务进程(它会开启服务如推送服务、定位服务等)、后台进程(在后台进行一些计算,这不同于服务进程)、空进程(它是没有任何东西在内存中运行的进程,内存可以随时把它回收掉)。优先级越低的进程,它被系统杀死、被内存回收的概率也越大。在这里我们注意,前台进程、可见进程、服务进程,在正常情况下它是绝对不会被系统杀死的。而后台进程它会被存放在一个缓存列表中,这个缓存列表的数据结构,就是LRU,即最近最少使用的一个缓存方式,先杀死的进程处于这个列表的尾部。空进程其实是为了平衡整个系统性能,Android不会保存这些进程。当Android开始杀死进程的时候,系统它会判断每一个进程杀死后它会带来的回收效益。因为Android总是倾向于杀死一个能回收更多内存的进程。杀死进程越少,对用户影响越小。
31.Android内存管理机制特点(或目标)
<1>更少的占用内存。如果你的APP性能更好,占用手机内存更小,用户的体验和今后选择这个APP的概率都会提升。
<2>在合适的时候,合理地释放系统资源。并不是你所有的对象不要了马上回收掉,那就是最好的内存管理。因为如果你频繁释放对象,会造成内存抖动。而内存抖动会造成很多不好的现象,如UI卡顿、ANR,甚至会造成OOM。
<3>在Android系统内存紧张的时候,一定要能释放大部分不重要的资源,来为Android系统提供可用内存。这句话就是说在系统紧张的时候我们一定要回收掉一些比如Cursor,IO,Socket等资源,来为Android开启其它进程做内存上的准备。
<4>能够很合理的在特殊生命周期中,保存或者还原重要数据,以至于系统能够正确的重新恢复该应用。
32.Android常见内存优化方法
<1>当Service完成任务后,应尽量停止它。我们知道,Service组件进程是比较低的优先级,叫服务进程,所以说这会影响到它的内存回收。在这里我们可以用IntentService来替代Service完成Service所要进行的任务。我们知道IntentService其实它是继承Service的,它也是一个服务。但是它不同于Service的地方是,Service是默认在主线程中执行的,所以在Service中不可以做耗时操作。而IntentService它内部开启一个子线程,所以说在它的onHandleIntent方法里你可以做一些耗时操作。另一个使用IntentService原因是IntentService使用完后会自动退出,而不像Service在执行完后要手动调用才能退出。这时候如果开发人员忘记关闭Service,就会造成内存泄漏。
<2>在UI看不见的时候,释放掉一些只有UI使用的资源。这个在Android系统里面它会根据一个onTrimMemory方法来通知app回收UI的资源。
<3>在内存紧张的时候,尽可能多的释放掉一些非重要资源。它也是在onTrimMemory这个回调方法中,它会通知内存紧张状态,然后app会根据不同的内存紧张等级,来合理的释放资源。
<4>避免滥用Bitmap导致的内存浪费。我们知道,根据当前设备的分辨率,来压缩Bitmap是我们的选择。在使用完Bitmap之后,一定要注意调用Bitmap.recycle()释放Bitmap在C中的内存。我们也可以选择用软引用来使用Bitmap,然后用LRU缓存对LRU进行缓存算法。
<5>使用针对内存优化过的数据容器以及一些数据结构。这里主要是通过官方推荐的一些SparseArray方法它可以替代HashMap,同时我们尽量少用枚举常量,因为它消耗的内存是常量的两倍多。
<6>避免使用依赖注入的框架。我们知道我们现在项目开发过程中会使用到很多IOC框架,比如ButterKnife、Dagger2等。但是使用这些依赖注入的框架,它也有好的地方。但同时它也会带来额外的一些操作,比如扫描APP代码中的注解,这样会需要额外的系统资源。
<7>使用ZIP对齐的APK。zipalign它是一个工具,它会压缩内部的资源,运行时会占用更小的内存。所以说尽量要用zipalign。
<8>使用多进程,把消耗内存过大的模块,或者需要长期后台运行的模块移入到单独进程当中。比如说大家在项目开发过程中,你的项目如果开启定位,可以开启后台定位进程;如果你的项目中需要一个推送,你可以开启个推送进程。同时我们知道,最常用的webview,webview如果你不单独开启进程的话,它会造成内存泄漏。所以说webview你可以单独开启个进程,让它加载url。但是我们知道,使用多进程它是一把双刃剑,错误的使用会给我们带来很多的问题。比如说数据传输的问题和安全的问题。
33.什么是冷启动
在Android当中,系统为每个应用至少分配了一个进程,所以说从进程的角度来说,冷启动就是在应用启动前系统中没有该应用的任何进程信息。它包括Activity,Service等等。所以说,冷启动产生的场景就很容易理解了。比如说我们开启设备后第一次打开这个应用,或者说杀死了这个应用进程,然后再次开启该应用。这种方式应用启动时间最长,应用所做工作也最多。
34.冷启动和热启动的区别
热启动就是用户使用返回键退出应用,然后马上又重新启动应用。这时应用就是热启动。冷启动就是在应用启动的时候,后台没有该应用的进程,这时候系统会重新创建一个新的进程分配给该应用,这种方式就是冷启动。
从冷启动和热启动的启动特点来说,因为系统会重新创建个新的进程分配给它,所以先会创建和初始化Application类,再创建和初始化MainActivity类。然后会进行测量,布局,绘制等等工作。最后显示在界面上。
而热启动从启动特点来说,因为会从已有的进程来启动,所以热启动就不会走Application这个类了,而是直接走MainActivity进行测量布局绘制。所以热启动的过程只需要创建和初始化一个MainActivity就够了,而不必创建和初始化Application类。因为一个应用从新进程的创建到进程的销毁,Application只初始化一次。
35.冷启动时间
这个时间在logcat中会打印。该时间值从应用启动(创建进程)开始计算,到完成视图的第一次绘制(即Activity内容对用户可见)为止。
36.冷启动流程概述
<1>Zygote进程中fork创建出一个新的进程。
<2>创建和初始化Application类,创建第一个Activity类。
<3>inflate布局,此时onCreate/onStart/onResume顺序执行。
<4>contentView的布局经过measure/layout/draw显示在界面上
37.冷启动流程总结
Application的构造器方法 → attachBaseContext() → Activity构造方法 → onCreate → 配置主体中背景等属性 → onStart → onResume → 测量布局绘制显示在界面上
38.对冷启动时间进行优化
<1>应用冷启动是无法避免的,也就是说启动时用户总是需要等待一些时间。我们可以通过减少Application和第一个Activity中的onCreate方法中代码,来减少冷启动时间。但是,在开发过程中,我们经常会用到一些第三方SDK,而这些SDK很多会要求我们在Application中进行初始化操作。所以我们这里最好是使用懒加载的形式,即在使用该SDK前再进行SDK的初始化。如果我们在APP中对某个SDK使用多次,无法精确确认什么时候是第一次使用,也可以在首页数据加载完成后,立刻启动SDK的初始化工作。
<2>不要让Application参与业务的操作。因为Application是所有APP启动的时候第一个所要初始化的地方,所以我们一般在这里进行初始化的是每一个业务模块都会使用到的一些数据。而对于某些个别模块用到的数据我们就不在这里进行初始化了。
<3>不要在Application进行耗时操作,主要是类似将数据从SD卡读取出来的IO操作。
<4>不要以静态变量的方式在Application中保存数据。
<5>尽量减少布局的复杂性和布局的深度。因为在布局View绘制的过程中,测量是非常耗费性能的。View的层级越庞大,APP就会花费越多的时间去填充它,所以说为了减少启动时间,我们可以减少多余的布局嵌套,不去填充不需要在启动时就展示的View。
<6>mainThread中进行资源初始化也会减慢启动速度。可以把资源初始化延迟,放入子线程中实现。
39.不要在静态变量中保存特别核心的数据
因为在Android系统中,应用程序不是安全的,它随时有可能被kill掉,内存被回收。当程序被kill掉后,如果我们再次打开APP,相应的Application和Activity会被重新创建。但此时对应的静态变量也被重新初始化了,从而造成我们数据的丢失。因此数据保存在静态变量中是不安全的,对于重要的数据我们应该保存在文件,sp或者数据库中。而静态变量我们可以保存一些不重要的数据或者是一些常量。
40.SharedPreferences不能跨进程同步
SharedPreferences在多进程中无法跨进程读写数据。因为每个进程都会维护自己SharedPreferences的副本,在它运行过程中,其它进程是完全没有办法去获取这个SharedPreferences的副本。只有在应用结束的时候,我们才能将每个进程的副本持久化地修改到文件系统当中。
41.SharedPreferences文件大小问题
SharedPreferences是以key-value形式保存数据。如果存储数据过大,在读写时会阻塞主线程,这是特别影响性能的,这会引起UI卡顿。因为在解析SharedPreferences文件时,会产生大量临时对象,之后又会频繁垃圾回收,就造成了UI卡顿。同时,大量GC也会造成内存抖动,最后形成一些内存泄漏,最终造成OOM。这里要注意,key-value的值是永远保存在内存当中的,所以说它特别耗内存。
42.内存对象序列化概述
什么是内存对象的序列化。在Java中,将对象的状态信息,转化为可以存储或传输形式的过程,就叫做内存对象的序列化。而在Android开发过程中,不同的Activity之间,传输数据我们可以通过Intent的putExtra方法来传递。比如我们可以传递Java中的八大基本数据类型。但是在八大数据类型以外,如果你要传递一些比较复杂的对象类型的时候,那就需要用到序列化。在Android中,实现序列化有两种方式。第一种,你可以实现Serializable接口。Serializable接口它其实是来自于Java的序列化接口。但是我们要注意,Serializable在序列化时会产生大量的临时变量,从而引起频繁的垃圾回收。频繁的垃圾回收会影响UI性能,造成UI卡顿,内存抖动,最后有可能造成OOM的情况。第二种,是Parcelable,Parcelable是Android中自带的一个序列化方式。特别是在使用内存方面,Parcelable比Serializable更好。但是Parcelable有个明显的缺点,就是不能把硬盘上存储的数据用Parcelable来序列化。Parcelable的本质它是为了更好地实现对象在进程间通信的传递。它其实并不是一个通用的序列化机制。它使用的场合基本都是Android中的进程间通信。所以说如果你要改变任何Parcelable数据当中的底层实现的时候,就有可能导致这些数据的不可读取。所以说还是保守的方式,我们尽量采用Serializable序列化比较安全。
43.内存对象序列化总结
<1>Serializable是java的序列化方式,Parcelable是Android特有的序列化方式。
<2>在使用内存的时候,Parcelable比Serializable性能高。
<3>Serializable在序列化的时候会产生大量的临时变量,从而引起频繁的GC。
<4>Parcelable不能使用在要将数据存储在磁盘上的情况。
44.避免在UI线程中做繁重的操作
UI线程中,复杂的操作会导致动画绘制的延迟,最终会导致明显的卡顿。所以要避免在UI线程中进行网络的访问和IO操作。Android设备的存储,在处理多个并发的读写操作时,支持的不够好。