Android 性能优化 ---- 内存优化
1、Android内存管理机制
1.1 Java内存分配模型
先上一张JVM将内存划分区域的图
程序计数器:存储当前线程执行目标方法执行到第几行。
栈内存:Java栈中存放的是一个个栈帧,每个栈帧对应一个被调用的方法。栈帧包括局部标量表,
操作数栈。
本地方法栈:本地方法栈主要是为执行本地方法服务的。而Java栈是为执行Java方法服务的。
方法区:该区域被线程共享。主要存储每个类的信息(类名,方法信息,字段信息等)、静态变量,常量,以及编译器编译后的代码等。
堆:Java中的堆是被线程共享的,且JVM中只有一个堆内存,主要存储对象本身及数组
1.2 Dalvik和ART介绍
Dalvik:Dalvik是Google公司自己设计用于Android平台的Java虚拟机。它可以支持已转换为.dex格式的Java应用程序的运行,.dex格式是专门为Dalvik应用设计的一种压缩格式,适合内存和处理器速度有限的系统,Dalvik经过优化,允许在有限的内存中同时运行多个虚拟机实例,并且每一个Dalvik应用做为独立的Linux进程执行,独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。
ART:ART表示Android Runtime,Dalvik是依靠一个just-In -Time编译器去解释字节码,运行时编译后的应用都需要通过一个解释器在用户的设备上运行,这一机制并不是特别高效,但是能让应用更容易在不同的硬件和架构上运行。ART则是完全改变了这种做法,在安装应用的时候就预编译字节码到机器语言,这一机制叫预编译。在移除解释代码这一过程,应用程序执行将更有效率,启动速度更快。
ART优点:
1.系统性能更高
2.应用启动速度,运行更快,体验更好,触感反馈更加及时。
3.更长的电池续航能力
4.支持更低的硬件
ART缺点:
1.储存空间占用更大。
2.应用安装时间更长。
Dalvik与ART区别
1.Dalvik每次都要编译在运行,art只会安装时启动编译
2.art占用的空间比Dalvik要大,就是用空间换时间
3.art减少编译,减少CPU使用频率,使用明显改善电池续航
4.art启动,运行更快,体验更好,触感反馈更及时。
1.3 为什么要进行内存优化
1.减少oom,提高应用的稳定性
2.减少卡顿,体验更好
3.减少内存占用,应用存活率更高
4.提前处理掉一些异常的隐患
2、Java内存回收算法
2.1判断Java中对象是否存活的算法
2.1.1 引用计数算法
堆内存的每个对象都有一个引用计数器,当对象被引用的时候,计数器+1,当引用失效时计数器-1,当计数器的值为0时,说明该对象没有被引用,就会被认为是垃圾对象,系统将会将其回收内存重新分配。
优点:引用计数器执行简单,判定效率高。
缺点:对于循环引用的对象难以判断出来,同时引用计数器增加了程序执行的开销,在jdk1.1后,就不在使用了。
2.1.1 根搜索法
GC Roots的对象做为起点,然后向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则该对象不可达,也就是说该对象为为垃圾对象,可以被回收。
在Java中,可以做为GC Roots的对象包括一下四种:
1.虚拟机栈中引用的对象
2.方法区中的类静态属性引用的对象
3.方法区中常量引用的对象
4.本地方法栈中JNI的引用对象
2.2 JVM垃圾回收算法
2.2.1 标记清除法
最基础的垃圾收集算法,算法分为标记和清除两个阶段:首先标记出所有需要回收的对象,在标记完成之后统一回收掉所有被标记的对象。
缺点:效率低,其次会产生大量的不连续的内存碎片,导致提前触发另一次垃圾收集动作。
2.2.2 复制回收算法
复制回收算法是将可用内存按容量分成大小相等的两块,每次只使用其中的一块,当这块内存使用完了,就将存活的对象复制到另一块内存上去,然后把使用过的内存空间一次清理掉,这样使得每都次都是对其中一块内存进行回收,内存分配时不用考虑内存碎片等复杂情况。
缺点:可使用内存降为原来的一半。
2.2.3 标记整理法
标记-整理算法在标记-清除算法的基础上做了改进,标记阶段将可回收的对象标记出来,标记完成后不是直接对可回收的对象进行清理,而是让所有存活的对象都向一端移动,在移动的过程中清理掉可回收的对象。
优点:相比于标记清除法来说,标记整理法不会大量产生不连续内存碎片问题。
缺点:如果是在对象存活率较高的情况下会执行较多的复制操作,效率将会降低很多,而在存活率较低的情况下,效率会大大提高。
2.2.4 分代收集回收算法
当前商业虚拟机都是采用的是分代收集算法,根据对象存活的周期不同将内存划分为几块,一般是将java堆分为年轻代,老年代和永久代。然后根据各个年代的特点来采取不同收集算法,年轻代存活率较低,采用复制回收算法,老年代对象存活率较高,采用标记清除法或者是标记整理法来进行回收。
3、内存问题表现形式
3.1 内存抖动
内存波动图呈锯齿状,gc频繁导致卡顿。
3.2 内存泄漏
内存泄露简单来说就是系统分配出去的内存由于某种原因导致没法释放,内存会越来越小,最终导致oom。
3.3 内存溢出
即OOM,OOM时会导致程序异常。Android设备出厂以后,java虚拟机对单个应用的最大内存分配就确定下来了,超出这个值就会OOM。
4、内存优化常用工具
4.1 Memory Profiler
Memory Profiler是Android studio自带的工具,实时图表形式展示应用内存使用的情况,可以用来识别内存泄露,抖动等
注意:如果在控制台中没有找到Profiler,可View -----> Tool Windows ---> Profiler 进行打开
优点:方便直观,便于线下使用
4.2 Memory Analyzer(MAT)
1、强大的java heap分析工具,查找内存泄露及内存占用
2、生成整体报告,便于分析问题
3、可以在线下深入使用
MAT使用:
MAT下载地址:https://www.eclipse.org/mat/downloads.php
获取hprof文件
导出来的Dump是没法直接使用mat打开的,Android SDK自带了一个转换工具在SDK的platform-tools下,其中转换语句为:
cd D:\aa\sdk\platform-tools
hprof-conv aaa.hprof bbb.hprof
注:aaa.hprof表示从profiler中导出来的dump文件,bbb.hprof 表示转化出来的dump文件
使用mat打开转化出来的dump
MAT视图
在MAT窗口上,OverView是一个总体概览,显示总体的内存消耗情况和疑似问题。
1、Histogram:列出内存中的所有实例对象和个数以及大小,在顶部regex区域支撑正则表达式查找
2、Dominator Tree:列出最大的对象及其依赖存活的Object,相比于Histogram,能更方便的看出引用关系。
3、Top Consumers:通过图像列出最大的Object
4、Leak Suspects:通过MAT自动分析内存泄露的原因和泄露的一份总体报告
其中分析内存情况,我们基本用到的就是Histogram和Dominator Tree
Class Name:类名。
Objects:对象实例个数。
Shallow Heap:对象自身占用内存大小,不包括它引用的对象
Retained Heap:是当前对象大小和直接或者间接引用到的对象大小总和,包括递归释放的。、
查找内存泄露方式
步骤1:在Regex通过包名进行匹配,当然也可以通过其他方式进行匹配
步骤二:右键选中怀疑对象,List objects --> with incoming references
注 with outgoing references 他引用了那些对象
with incoming references 那些对象引用了他
步骤三:选择当前的一个 Path to GC Roots/Merge to GC Roots 的 exclude All 弱软虚引用。
图标的左下角出现这个,则表示出现了内存泄露。然后回调代码中分析即可。
4.3 LeakCanary
使用
implementation 'com.squareup.leakcanary:leakcanary-android:1.5.4'
application中
public class App extends Application {
private RefWatcher mRefWatcher;
@Override
public void onCreate() {
super.onCreate();
mRefWatcher = LeakCanary.install(this);
}
public static RefWatcher getRefWatcher(Context context) {
App application = (App) context.getApplicationContext();
return application.mRefWatcher;
}
}
在activity或者fragment中的onDestory()方法调用
RefWatcher refWatcher = App.getRefWatcher(getActivity());
refWatcher.watch(this);
原理
主要是通过WeakReference + ReferenceQueue来判断对象是否被系统GC回收,WeakReference创建时,传入一个ReferenceQueue对象,当WeakReference引用的对象生命周期结束后,会被添加到ReferenceQueue中,当GC过后,对象一直没有被添加进入到ReferenceQueue,可能就会存在内存泄露,再次触发GC,二次确认。
5、常见的内存泄露
1、资源性对象未关闭
对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。
2、注册对象未注销
例如BraodcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。
3、类的静态变量持有大数据对象
尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。
4、单例造成的内存泄漏
优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。
5、非静态内部类的静态实例
该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置空让GC可以回收,否则还是会内存泄漏。
6、Handler临时性内存泄漏
Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息,当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。解决方案如下所示:
1、使用一个静态Handler内部类,然后对Handler持有的对象(一般是Activity)使用弱引用,这样在回收时,也可以回收Handler持有的对象。
2、在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中有待处理的消息需要处理。
需要注意的是,AsyncTask内部也是Handler机制,同样存在内存泄漏风险,但其一般是临时性的。对于类似AsyncTask或是线程造成的内存泄漏,我们也可以将AsyncTask和Runnable类独立出来或者使用静态内部类。
7、容器中的对象没清理造成的内存泄漏
在退出程序之前,将集合里的东西clear,然后置为null,再退出程序
8、WebView
WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。
9、使用ListView时造成的内存泄漏
在构造Adapter时,使用缓存的convertView。
6、优化内存空间的方式
6.1、java对象的引用
强引用:我们平时开发写的代码,基本百分之九十九的都是强引用。
软引用:如果一个对象具有软引用,那么当内存不足时,就会回收它。
弱引用:GC时,只要发现有弱引用,那么就会回收它,当然,有可能存在GC多次才发现
虚引用:虚引用必须要和引用队列关联起来使用。任何时候都有可能被垃圾回收器回收。一般可以用来判断GC的频率,GC频率过高,那么说明内存出了问题。同时也可以监听某个重要的对象是否被回收。
所以,在平时我们编写代码的时候,适当的使用软引用,弱引用,对我们的内存优化也能起到重要的作用。
6.2、减少不必要的内存开销
1、AutoBoxing
自动装箱的核心是吧基础数据类型转换成对应的包装类,比如int 类型只是占用4字节,但是Integer对象占用16字节。
2、内存复用
资源复用:通用的字符串,颜色定义,简单页面布局的复用
视图复用:
3、使用优化过的数据类型
如 SparseArray、SparseBooleanArray、LongSparseArray,使用这些API可以让我们的程序更加高效。HashMap 工具类会相对比较 低效,因为它 需要为每一个键值对都提供一个对象入口,而 SparseArray 就 避免 掉了 基本数据类型转换成对象数据类型的时间。
4、项目中少用枚举,枚举占用内存是常量三倍。
5、在应用可以内存过低时主动释放内存
在application中的 onTrimMemory/onLowMemory,内存紧张时会回调该方法,我们可以在这个方法中释放掉图片缓存,静态缓存来避免被kill。
6、避免创建一些不必要的对象
如在字符串拼接时不要用“+”来进行拼接,而是使用StringBuffer,StringBuilder来替代。因为String 内部是被final修饰的,不可继承,使用+进行拼接是会产生一个新的对象,而占用内存。
7、尽量不要在一些循环的地方创建对象。如自定义的时候在onDraw()方法。
7、优雅的检测大图
项目中会经常遇到这样的情况,我们的布局中,控件的宽高可能只是50 * 50 但是从服务器给过来的图片或者是UI给过来的图片往往会大很多,而如果图片在资源文件下还好,可以直接查看宽高,但是如果从服务器上获取到的呢,这是我们经常会忽略的。而图片过大,占用的内存就更多,这是没有必要的。那么我们怎么检测出服务器给过来的图片过大的呢?
7.1、继承ImageView 重新实现onDraw();
这种方法我们可以重新测量图片的宽高,超过一定的范围,我们就可以输出警告。但是这种方法对代码侵入性很强。如果是有新同学加入,容易造成代码混乱。
7.2、ARTHook
Hook的意思是钩子,也就是在消息过去之前可以把消息勾住,不让其传递,能够针对不同的消息或者api在执行之前,先执行我们自己的操作。
这里推荐使用Epic 框架:https://github.com/tiann/epic
添加依赖
implementation 'me.weishu:epic:0.3.6'
创建一个ImageHook类
package com.optimize.performance.memory;
import android.graphics.Bitmap;
import android.graphics.drawable.BitmapDrawable;
import android.graphics.drawable.Drawable;
import android.util.Log;
import android.view.View;
import android.view.ViewTreeObserver;
import android.widget.ImageView;
import com.optimize.performance.utils.LogUtils;
import com.taobao.android.dexposed.XC_MethodHook;
public class ImageHook extends XC_MethodHook {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
// 实现我们的逻辑
ImageView imageView = (ImageView) param.thisObject;
checkBitmap(imageView,((ImageView) param.thisObject).getDrawable());
}
private static void checkBitmap(Object thiz, Drawable drawable) {
if (drawable instanceof BitmapDrawable && thiz instanceof View) {
final Bitmap bitmap = ((BitmapDrawable) drawable).getBitmap();
if (bitmap != null) {
final View view = (View) thiz;
int width = view.getWidth();
int height = view.getHeight();
if (width > 0 && height > 0) {
// 图标宽高都大于view带下的2倍以上,则警告
if (bitmap.getWidth() >= (width << 1)
&& bitmap.getHeight() >= (height << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), width, height, new RuntimeException("Bitmap size too large"));
}
} else {
final Throwable stackTrace = new RuntimeException();
view.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
@Override
public boolean onPreDraw() {
int w = view.getWidth();
int h = view.getHeight();
if (w > 0 && h > 0) {
if (bitmap.getWidth() >= (w << 1)
&& bitmap.getHeight() >= (h << 1)) {
warn(bitmap.getWidth(), bitmap.getHeight(), w, h, stackTrace);
}
view.getViewTreeObserver().removeOnPreDrawListener(this);
}
return true;
}
});
}
}
}
}
private static void warn(int bitmapWidth, int bitmapHeight, int viewWidth, int viewHeight, Throwable t) {
String warnInfo = new StringBuilder("Bitmap size too large: ")
.append("\n real size: (").append(bitmapWidth).append(',').append(bitmapHeight).append(')')
.append("\n desired size: (").append(viewWidth).append(',').append(viewHeight).append(')')
.append("\n call stack trace: \n").append(Log.getStackTraceString(t)).append('\n')
.toString();
LogUtils.i(warnInfo);
}
}
在application中
DexposedBridge.hookAllConstructors(ImageView.class, new XC_MethodHook() {
@Override
protected void afterHookedMethod(MethodHookParam param) throws Throwable {
super.afterHookedMethod(param);
DexposedBridge.findAndHookMethod(ImageView.class, "setImageBitmap", Bitmap.class, new ImageHook());
}
});
这样在开发者调用setImageBitmap 来设置图片的时候,都会进行对图片的宽高进行比如,如果超出一定的范围则进行提示。
8、建立APM平台
这块内容很多,后面会专门开一篇来讲解。可以参考腾讯的开源框架
https://www.ctolib.com/article/compares/20629