Android 性能篇 - 内存优化

内存优化是一个程序员的基本功。有时也要切合项目的实际需求来做选择。

一、解决所有的内存泄漏

内存泄漏概念:
不再使用的对象没有被回收,就是内存泄露。

1. 单利泄漏

主要原因还是因为一般情况下单例都是全局的,有时候会引用一些实际生命周期比较短的变量,导致其无法释放。

例如 :

  • activity 的 content 赋值到单利对象里面的成员量变量

code:

    private static volatile ClassXX instance;
    private  Context context;
    private ClassXX(Context context) {
        this.context = context;
    }

    public static ClassXX getInstance(Context context) {
        if (instance == null) {
            synchronized (instance) {
                if(instance == null) {
                    instance =  new ClassXX(context);
                }
            }
        }

        return instance;
    }

如果这个Context
ActivityContext ,当你的 Activity finish(); 之后Activity 这个对象的内存还是在堆中,没有释放。
因为单利对象持有Activity 的引用,jvm 认为你这个对象还是在使用中,不敢去 回收掉你的 Activity。那单例什么时候被回收?
那就只有等到整个进程被回收了,单例才会被回收。

进程杀死(回收):

  • Process.killProcess(Process.myPid())

  • 用户手动卡片式摧毁 (亲测可行)

解决方法:
  • 传入和单例一样生命周期的对象,如context.getApplication();
  • 不将 context保存在单例的成员变量里面。
2. Handler AsyncTask 等内部类的内存泄漏

主要原因是内部类默认持有外部类的引用

大家应该很喜欢吧 Handler写成一个内部类譬如:

private Handler mMainActivityHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
      super.handleMessage(msg);
 }
};

其实包括我也很喜欢,而且一个Activity 对应一个 Handler,每一个 Handler 负责更新本 Activity 的 UI,一对一关系,分工明确。好用到爆炸。

然而 java 内部类是默认持有一个外部类的引用,因为 jvm 在把.java 源文件编译成 .class 字节码的时候,会在默认的构造函数加入外部类的引用。所以我们在内部类中也能访问外部类的引用。

然后问题就发生了,当前 Handler 持有当前 Activity 的引用,Handler 不释放,Activity 也别想释放了。MMP

(为什么 Handler 有时候会不会被释放?)

解决方法:
  • 构造函数传入 Activity 并用 WeakReference<Activity> mActivity;弱引用保存下来。 GC 的时候会不计入HandlerActivity的引用,可以被回收。
  • Activity OnDestroy 的时候 ,把所有的相关请求终止,并且把消息队列清空 removeCallbacksAndMessages(null); 防止有数据回调到 UI 层。(当然如果不这么做,Activity 照样被回收,但是 Handler 不及时回收而已)

(什么叫 强引用 软引用 弱引用 虚引用 ,以及 Handler 的消息驱动模型是怎么样子的,这里就不展开讲,本文着重内存泄漏)

当然 AsyncTask 和其它对象内部类也是有这种问题,解决方法同上。

3. 资源使用完未关闭

主要是:

  • 广播(BraodcastReceiver)动态注册之后要反注册,推荐在onStart onStop 对应的生命周期执行。
  • 服务(ServiceStart 之后 记得 Stop。启动服务时机看需求。一般不建议在 Application 启动(启动 Service 耗时基本要100ms+)。
  • io Cursor 流要记得 close,一定要在 finallyclose,防止抛异常没执行 close ,那就泄漏了。
  • Bitmap 内存大户,要记得回收 recycle 一下,当然 90% 的场景 Glide 已经帮我们处理的。
4.检测内存泄漏的工具

当然有时候不能完全在写代码的时候规避掉所有的内存泄漏,就要用一些工具检测一下:

  1. LeakCanary
  2. Android Studio profile
  3. MAT

选自己喜欢的工具,去研究一下。(网上很多教程)

二、图片压缩

1. bitmap 压缩

大家都知道 bitmap 占用内存很大,用完之后要 recycle 一下。
不知道大家有没有用过,图片加载出来内存就爆掉了(OOM)情况,本宝宝就遇到过了(心中一千万头草拟吗奔腾而过)。

首先一张图片从网络获下来,从 InputStream 转成 Bitmap,这个 bitmap 占了多少内存怎么计算?

献上代码:
Bitmap.getAllocationByteCount();
其实就是 ByteCount = 长* 宽 * 4(假设这里每一个像素点是是RGB888) 那就是 4 个字节。也有一个像素点 RGB565 占 3 个字节,当然占更多字节的 RGB888 更加高清无码。起初版本 Glide 使用 RGB565,目前 Glide 4.XX 的默认都是 RGB888,当然自己可以配置一下。

为了解决这个问题一般都是通过下面代码:

BitmapFactory.Options options = new BitmapFactory.Options();  
options.inJustDecodeBounds = true;  
// 通过这个bitmap获取图片的宽和高 
Bitmap bitmap = BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg", options); 
float realWidth = options.outWidth;  
float realHeight = options.outHeight;
//计算出scale
options.inSampleSize = scale;  
options.inJustDecodeBounds = false;  
// 注意这次要把options.inJustDecodeBounds 设为 false,这次图片是要读取出来的。
bitmap = BitmapFactory.decodeFile("/sdcard/MTXX/3.jpg", options);  
  1. 先获取他的图片大小,根据自己需要的大小计算出缩放比例。(图片大小都是放在图片的头部,这时候不会去加载整张图片)
  2. 进行缩放,得出符合自己的控件尺寸的大小。
    (当然还有些非法的图片头部是获取不出 长* 宽。这时候记得搞个默认的缩放率,防止 OOM)

有时候为了优化内存,还不如压缩一张图片 所节约的内存来的更快。
譬如 一张 1080 * 1920 图片再乘以 4 等于 7.9 M。
我压缩到 一张缩略图 200*200 等于 156KB。瞬间节约了7M 空间。区别真的太大了,顿时内心 一句 MMP 。

三、解决内存抖动

1.String VS StringBuffer VS StringBuilder

大家应该对着三个类都非常熟悉。那就先看代码:

long time = System.currentTimeMillis();
String s = new String("JAVA");
for(int i = 0 ;i<10000; i++) {
   s = s+"VERSION";
}
Log.d("TestString","Time consumption:"+(System.currentTimeMillis() - time));
time = System.currentTimeMillis();

StringBuilder s1 = new StringBuilder("JAVA");
for(int i = 0 ;i<10000; i++) {
   s1.append("VERSION");
}
Log.d("TestString","Time consumption:"+(System.currentTimeMillis() - time));



D/TestString: Time consumption:3786
D/TestString: Time consumption:2

很明显使用 StringBuilder 去拼接字符,效率大大快于用加号,我们带着问题来找原因。

那我们看一下用 + 号去拼接的字节码:
String拼接字节码
  • 使用+号去拼接字符,jvm 会创建一个临时的 StringBuilder

25 new #24 <java/lang/StringBuilder>

  • 然后把上次的结果集,通过构造函数传入,
29 invokespecial #25 <java/lang/StringBuilder.<init>> //调用构造函数,这串符号引用类似 jni 中反调 java的类查找写法
32 aload_3  //将局变量表Slot 3的元素入栈
  • 再拼接本次需要拼接的字符。然后存到局部变量表中,等待下次循环操作。
   44 astore_3
  • 然后跳转编号17 去继续循环。这时候又重新创建了一个 StringBulider 去拼接。真是啃爹啊。。。

48 goto 17 (-31)

那我们看一下用 StringBuilder 去拼接的字节码:
StringBuilder去拼接的字节码

这个很明显 new StringBulider 字节码在循环体外面,所以并没有循环新建对象。

总结:

通过上面的例子,String 的拼接通过一个 for 循环创建了 10000 个 StringBulider,而且用完就抛弃。特别浪费,在内存吃紧的情况下,很容易引起 gc ,导致 App 卡顿。
也许有同学要问 一个 StringBuilder 的空对象才占堆内存多大?我们来算一算

  • 一个对象 = 对象头 + 成员属性
  • 对象头 = MardWord + Klass= 12个字节 (数组除外)

上图:

image

MardWord 字段大全(出自网上扣得):

image

这个 MardWord 怎么有这么多锁状态,这些锁状态又是什么?
这就要涉及到 synchronized 同步锁的知识,这个不在本文讨论范围之内。

那么 StringBulider 的成员属性有哪些?清单:

static final long serialVersionUID = 4383685877147921099L;
char[] value;
int count;

对象结构图

image

计算下来:12+8+8+4+24 = 56 个字节 10000 个对象 那就是要 560KB 内存。不小吧。当然我们实际需求不可能一次搞这么多个对象,但是多个地方都用 String
去玩的话,积少成多,到时候 APP 内存比别人的高出一大截。那就尴尬了..

四、尽量使用 “池”

我们常见的池有

  • 线程池
  • Lrucache 缓存池
  • okhttp 里面的 ConnectionPoolsocket 复用池)
  • okio SegmentPoolbuffer 复用池)

池的功能:
可以重复利用对象,并且减少内存开销,内存抖动,cpu 开销。

  1. 线程池
public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,  
                              RejectedExecutionHandler handler)

尽量使用线程池去跑任务,而不是动不动就先 new Thread 去跑,这样子线程是得不到复用的。当任务量一大,使用线程池的效率会超乎你想象(具体自己看源码),毕竟 开启一个线程 cpu 内存都是有开销的。

这里推荐 Rxjava 的第三方库,一个将 装饰者模式 玩到上天的 框架,切换线程方便,支持函数式编程 杜绝回调地狱 等等:

Observable.create(new Action1<Emitter<Integer>>() {
@Override
public void call(Emitter<Integer> subscriber) {}
}, Emitter.BackpressureMode.BUFFER)
.subscribeOn(Schedulers.io()) //切换到 io 线程池
.subscribeOn(Schedulers.computation()) //切换 到计算 线程池
.subscribeOn(Schedulers.immediate()) // 使用当前线程
.observeOn(AndroidSchedulers.mainThread()) //切换到 android UI 主线程
.subscribe();
2. Lrucache 缓存池

Lrucache 缓存池:最近最少使用缓存池,底层原理是用 LinkHashMap 实现。

谷歌的 Glide 图片加载库,就是使用了 Lrucache,和 LruDiskCache 对图片进行缓存,进而提高用户体验。

3. ConnectionPool 缓存池

ConnectionPool 缓存池 :复用 tcp socket 套接字,进行网络通讯,每一次 HTTP 请求结束后,并不结束链接,可复用于下次的请求。把网络传输速度极致化。

一次 http 请求分:

  1. tcp 三次握手
  2. 数据传输
  3. tcp 四次分手

如果每一次请求都经历整个流程,可能别人所有数据都加载完毕了,我还在握手中... 这就不能忍。
(当然 http 1.1+ 才支持这个链接复用,具体详细源码 看 OKhttp,本文不做详细展开)

4. okio SegmentPool (buffer 复用池)

SegmentPool:同上。

总结:

对于一些需要 大量频繁生成和回收的对象,建议使用池,如果没有轮子,也是可以手动写一个。

五、其他

  • 常用数据结构优化
  • xml 层级 和 view
1.常用数据结构优化

内存大用户 : HashMap (及其子类)
HashMap 是一个典型的 空间换时间,时间复杂度趋近 o(1)
占用空间 是大于 size / 0.75(负载因子),

/**
* hashMap put 部分源码,
* size 当前已存入数据数目
* threshold = 容量 *0.75
*/

if (++size > threshold)
resize();

通俗点就是 存入100个数据,要占用 133 个数据内存(及以上),所在数据量较小,或者对速度没有那么要求的时候可用 SparseArray(二叉树实现) 代替。

2.xml 层级 和 view

xml 层级最好控制在 5 层以内。

view 的使用多用:

  • ViewStub
  • Include
  • merge
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,242评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,769评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,484评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,133评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,007评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,080评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,496评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,190评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,464评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,549评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,330评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,205评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,567评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,889评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,160评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,475评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,650评论 2 335

推荐阅读更多精彩内容

  • Android 内存管理的目的 内存管理的目的就是让我们在开发中怎么有效的避免我们的应用出现内存泄漏的问题。简单粗...
    晨光光阅读 1,284评论 1 4
  • 内存优化前我们先了解一些和内存相关的概念: 垃圾回收 内存抖动 四种引用 内存泄露 下面我们回到正题, 讲一下如何...
    MZzF2HC阅读 1,662评论 0 6
  • 所有知识点已整理成app app下载地址 J2EE 部分: 1.Switch能否用string做参数? 在 Jav...
    侯蛋蛋_阅读 2,390评论 1 4
  • 被文同时发布在CSDN上,欢迎查看。 APP内存的使用,是评价一款应用性能高低的一个重要指标。虽然现在智能手机的内...
    大圣代阅读 4,801评论 2 54
  • 今天是大年初二,俗称"女婿日"。对于一个26周岁即将步入27岁女孩子来说,没能让母亲迎来称心如意的姑爷也心生愧疚。...
    Jenny芳阅读 473评论 0 0