1.内存泄漏
一个长生命周期的对象持有一个短生命周期对象的引用,通俗讲就是该回收的对象,因为引用问题没有被回收,最终会产生OOM。
1.1 非业务需要不要把activity的上下文做参数传递,可以传递application的上下文
- 因为Application的生命周期等于整个应用的生命周期,非必须的情况下用Application的上下文可以避免发生内存泄露
1.2 和Activity有关联的对象不要写成static
- static修饰的成员变量的生命周期等于应用程序的生命周期,不要使用static修饰符Context或者ui控件等等
1.3 非静态内部类和匿名内部类会持有activity引用
- 非静态内部类和匿名内部类默认持有外部类的引用,经常会引起内存泄漏的情况有三种:
1.3.1 非静态内部类的实例的引用被设置为静态
非静态内部类所创建的实例为静态(其生命周期等于应用的生命周期),会因非静态内部类默认持有外部类的引用而导致外部类无法释放,最终造成内存泄露,如下
public class TestActivity extends AppCompatActivity {
// 非静态内部类的实例的引用设置为静态
public static InnerClass innerClass = null;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 保证非静态内部类的实例只有1个
if (innerClass == null)
innerClass = new InnerClass();
}
// 非静态内部类的定义
private class InnerClass {
//...
}
}
解决方法
- 将非静态内部类设置为静态内部类(静态内部类默认不持有外部类的引用)
- 尽量新建一个文件定义类
- 避免非静态内部类所创建的实例为静态
1.3.2 使用非静态内部类和匿名内部类的方式实现多线程,例如Thread、AsyncTask、Timer
正如使用内部类一样,只要不跨越生命周期,内部类是完全没问题的。但是,这些类是用于产生后台线程的,这些Java线程是全局的,而且持有创建者的引用(即匿名类的引用),而匿名类又持有外部类的引用。线程是可能长时间运行的,所以一直持有Activity的引用导致当销毁时无法回收。如下所示
void startAsyncTask() {
new AsyncTask<Void, Void, Void>() {
@Override protected Void doInBackground(Void... params) {
while(true);
}
}.execute();
}
void scheduleTimer() {
new Timer().schedule(new TimerTask() {
@Override
public void run() {
while(true);
}
}, Long.MAX_VALUE >> 1);
}
解决方法
- 使用静态内部类
private static class NimbleTask extends AsyncTask<Void, Void, Void> {
@Override protected Void doInBackground(Void... params) {
while(true);
}
}
void startAsyncTask() {
new NimbleTask().execute();
}
private static class NimbleTimerTask extends TimerTask {
@Override public void run() {
while(true);
}
}
void scheduleTimer() {
new Timer().schedule(new NimbleTimerTask(), Long.MAX_VALUE >> 1);
}
- 在生命周期结束时中断线程
private Thread thread;
@Override
public void onDestroy() {
super.onDestroy();
if (thread != null) {
thread.interrupt();
}
}
void spawnThread() {
thread = new Thread() {
@Override public void run() {
while (!isInterrupted()) {
}
}
}
thread.start();
}
1.3.3 Handle的使用问题
Handler的两种用法 内部类和匿名内部类默认持有外部类引用,在Handler消息队列还有未处理的消息或正在处理消息时,此时若需销毁外部类Activity,但由于消息队列中的Message持有Handler实例的引用,垃圾回收器(GC)无法回收Activity,从而造成内存泄漏
/**
* 方式1:新建Handle子类(内部类)
*/
class FHandler extends Handler {
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
break;
}
}
/**
* 方式2:匿名Handle内部类
*/
Handler handler = new Handler(){
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
break;
}
}
};
解决方法
- 使用静态内部类,同时还可以使用WeakReference弱引用持有Activity实例,断开Message消息 -> Handler实例 -> 外部类 的引用关系
private static class FHandler extends Handler{
// 定义 弱引用实例
private WeakReference<Activity> reference;
// 在构造方法中传入需持有的Activity实例
public FHandler(Activity activity) {
// 使用WeakReference弱引用持有Activity实例
reference = new WeakReference<Activity>(activity);
}
// 通过复写handlerMessage() 从而确定更新UI的操作
@Override
public void handleMessage(Message msg) {
switch (msg.what) {
case 1:
break;
}
}
}
- 当外部类结束生命周期时,清空Handler内消息队列
@Override
protected void onDestroy() {
super.onDestroy();
mHandler.removeCallbacksAndMessages(null);
}
1.4 单例模式持有activity引用
单例模式由于其静态特性,其生命周期的长度等于应用程序的生命周期。若一个对象已不需再使用而单例对象还持有该对象的引用,那么该对象将不能被正常回收从而导致内存泄漏,例如:
//在使用单例setCallback的地方,callback持有外部类引用
public class Singleton {
private static Singleton singleton;
private Callback callback;
public static Singleton getInstance(){
if(singleton==null){
singleton=new Singleton();
}
return singleton;
}
private Singleton () { }
public void setCallback(Callback callback){
this.callback=callback;
}
public Callback getCallback(){
return callback.get();
}
public interface Callback{
void callback();
}
}
//若传入的是Activity的Context,此时单例 则持有该Activity的引用
public class Singleton {
private static Singleton instance;
private Context mContext;
private Singleton (Context context) {
this.mContext = context; // 传递的是Activity的context
}
public Singleton getInstance(Context context) {
if (instance == null) {
instance = new Singleton(context);
}
return instance;
}
}
解决方法
- 单例中成员变量使用弱引用,例如 private WeakReference<Callback> callback
- 需要Context时传递Application的Context,因Application的生命周期等于整个应用的生命周期
2.内存抖动
什么是内存抖动呢?
Android里内存抖动是指内存频繁地分配和回收,而频繁的gc会导致卡顿,严重时还会导致OOM。一个很经典的案例是string拼接创建大量小的对象(比如在一些频繁调用的地方打字符串拼接的log的时候)
而内存抖动为什么会引起OOM呢?
主要原因还是有因为大量小的对象频繁创建,导致内存碎片,从而当需要分配内存时,虽然总体上还是有剩余内存可分配,而由于这些内存不连续,导致无法分配,系统直接就返回OOM了。
2.1 字符串少用加号拼接
- 使用StringBuilder和StringBuffer代替字符串加号拼接
2.2 内存重复申请的问题
- 不要在频繁调用的方法中new对象,例如递归函数 ,回调函数,流的循环读取,自定义View的方法等。
- 自定义View不要在onMeause() onLayout() onDraw() 中去刷新UI(requestLayout)
2.3 避免GC回收将来要复用的对象
对于能够复用的对象,可以使用对象池+LRU算法将它们缓存起来。
public abstract class ObjectPool<T> {
//空闲池,用户从这个里面拿对象
private SparseArray<T> freePool;
//正在使用池,用户正在使用的对象放在这个池记录
private SparseArray<T> lentPool;
//池的最大值
private int maxCapacity;
public ObjectPool(int initialCapacity, int maxCapacity) {
//初始化对象池
initalize(initialCapacity);
this.maxCapacity=maxCapacity;
}
private void initalize(int initialCapacity) {
lentPool=new SparseArray<>();
freePool=new SparseArray<>();
for(int i=0;i<initialCapacity;i++){
freePool.put(i,create());
}
}
/**
* 申请对象
* @return
*/
public T acquire() throws Exception {
T t=null;
synchronized (freePool){
int freeSize=freePool.size();
for(int i=0;i<freeSize;i++){
int key=freePool.keyAt(i);
t=freePool.get(key);
if(t!=null){
this.lentPool.put(key,t);
this.freePool.remove(key);
return t;
}
}
//如果没对象可取了
if(t==null && lentPool.size()+freeSize<maxCapacity){
//这里可以自己处理,超过大小
if(lentPool.size()+freeSize==maxCapacity){
throw new Exception();
}
t=create();
lentPool.put(lentPool.size()+freeSize,t);
}
}
return t;
}
/**
* 回收对象
* @return
*/
public void release(T t){
if(t==null){
return;
}
int key=lentPool.indexOfValue(t);
//释放前可以把这个对象交给用户处理
restore(t);
this.freePool.put(key,t);
this.lentPool.remove(key);
}
protected void restore(T t){
};
protected abstract T create();
public ObjectPool(int maxCapacity) {
this(maxCapacity/2,maxCapacity);
}
}
3.优化内存的良好习惯
3.1 static和static final
static String strVal = "Hello, world!";
编译器会在类首次被使用到的时候,使用初始化<clinit>方法来初始化上面的值,之后访问的时候会需要先到它那里查找,然后才返回数据。我们可以使用static final来提升性能:
static final String strVal = "Hello, world!";
这时再也不需要上面的那个方法来做多余的查找动作了。所以,请尽可能的为常量声明为static final类型的。
3.2 数据类型选择
不要使用比需求更占空间的基本数据类型
- 条件允许下,尽量避免使用float类型,Android系统中float类型的数据存取速度是int类型的一半,尽量优先采用int类型。
3.3 使用SparseArray和Arraymap代替HashMap
利用Android Framework里面优化过的容器类,例如SparseArray, SparseBooleanArray, 与 LongSparseArray。 通常的HashMap的实现方式更加消耗内存,因为它需要一个额外的实例对象来记录Mapping操作。另外,SparseArray更加高效在于他们避免了对key与value的autobox自动装箱,并且避免了装箱后的解箱。
所以数据在千级以内
- 如果key的类型已经确定为int类型,那么使用SparseArray,因为它避免了自动装箱的过程,如果key为long类型,它还提供了一个LongSparseArray来确保key为long类型时的使用
- 如果key类型为其它的类型,则使用ArrayMap
3.4 尽量少使用枚举
每一个枚举值都是一个单例对象,在使用它时会增加额外的内存消耗,所以枚举相比与 Integer 和 String 会占用更多的内存,较多的使用 Enum 会增加 DEX 文件的大小,会造成运行时更多的IO开销,使我们的应用需要更多的空间,特别是分dex多的大型APP,枚举的初始化很容易导致ANR
可以使用自定义注解实现类似效果,例如
public class SHAPE {
public static final int RECTANGLE=0;
public static final int TRIANGLE=1;
public static final int SQUARE=2;
public static final int CIRCLE=3;
@IntDef(flag=true,value={RECTANGLE,TRIANGLE,SQUARE,CIRCLE})
@Target({ElementType.PARAMETER,ElementType.METHOD,ElementType.FIELD})
@Retention(RetentionPolicy.SOURCE)
public @interface Model{
}
private @Model int value=RECTANGLE;
public void setShape(@Model int value){
this.value=value;
}
@Model
public int getShape(){
return this.value;
}
}
3.5 尽量使用IntentService而不是Service
如果应用程序当中需要使用Service来执行后台任务的话,请一定要注意只有当任务正在执行的时候才应该让Service运行起来。另外,当任务执行完之后去停止Service的时候,要小心Service停止失败导致内存泄漏的情况。
当我们启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,这样就会导致这个进程变得非常消耗内存。并且,系统可以在LRU cache当中缓存的进程数量也会减少,导致切换应用程序的时候耗费更多性能。严重的话,甚至有可能会导致崩溃,因为系统在内存非常吃紧的时候可能已无法维护所有正在运行的Service所依赖的进程了。
为了能够控制Service的生命周期,Android官方推荐的最佳解决方案就是使用IntentService,这种Service的最大特点就是当后台任务执行结束后会自动停止,从而极大程度上避免了Service内存泄漏的可能性。
4.Bitmap中优化
当我们读取一个Bitmap图片的时候,有一点一定要注意,就是千万不要去加载不需要的分辨率。在一个很小的ImageView上显示一张高分辨率的图片不会带来任何视觉上的好处,但却会占用我们相当多宝贵的内存。需要仅记的一点是,将一张图片解析成一个Bitmap对象时所占用的内存并不是这个图片在硬盘中的大小,可能一张图片只有100k你觉得它并不大,但是读取到内存当中是按照像素点来算的,比如这张图片是15001000像素,使用的ARGB_8888颜色类型,那么每个像素点就会占用4个字节,总内存就是15001000*4字节,也就是5.7M,这个数据看起来就比较恐怖了。
Android中的图片是以Bitmap方式存在的,Bitmap所占用的内存 = 图片长度 x 图片宽度 x 一个像素点占用的字节数,所以Bitmap的内存优化可以从这三个参数入手,减少任意一个值,就可以减小内存占用。
图片常用压缩格式
- ALPHA_8:表示8位Alpha位图,即透明度占8个位,一个像素点占用1个字节,它没有颜色,只有透明度。
- ARGB_4444:表示16位ARGB位图,即A=4,R=4,G=4,B=4,一个像素点占4+4+4+4=16位,2个字节。
- ARGB_8888:表示32位ARGB位图,即A=8,R=8,G=8,B=8,一个像素点占8+8+8+8=32位,4个字节。
- RGB_565 :表示16位RGB位图,即R=5,G=6,B=5,它没有透明度,一个像素点占5+6+5=16位,2个字节
通过改变图片的格式,可以改变每个像素点占用的内存大小,从而起到压缩作用。
常见压缩方式
1.质量压缩(对内存没有影响)
private void compressQuality() {
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.test);
mSrcSize = bm.getByteCount() + "byte";
ByteArrayOutputStream bos = new ByteArrayOutputStream();
bm.compress(Bitmap.CompressFormat.JPEG, 100, bos);
byte[] bytes = bos.toByteArray();
mSrcBitmap = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
}
质量压缩不会减少图片的像素,它是在保持像素的前提下改变图片的位深及透明度,来达到压缩图片的目的,图片的长,宽,像素都不会改变,那么bitmap所占内存大小是不会变的。我们可以看到有个参数:quality,可以调节你压缩的比例,但是还要注意一点就是,质量压缩对png格式这种图片没有作用,因为png是无损压缩。
质量压缩适合去保存和传递图片
2.内存压缩
2.1 采样率压缩
private void compressSampling() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inSampleSize = 2;
mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test, options);
}
采样率压缩其原理其实也是缩放bitamp的尺寸,通过调节其inSampleSize参数,比如调节为2,宽高会为原来的1/2,内存变回原来的1/4.
2.2 缩放法压缩(martix)
private void compressMatrix() {
Matrix matrix = new Matrix();
matrix.setScale(0.5f, 0.5f);
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.test);
mSrcBitmap = Bitmap.createBitmap(bm, 0, 0, bm.getWidth(), bm.getHeight(), matrix, true);
bm = null;
}
放缩法压缩使用的是通过矩阵对图片进行裁剪,也是通过缩放图片尺寸,来达到压缩图片的效果,和采样率的原理一样。
2.3 createScaledBitmap
private void compressScaleBitmap() {
Bitmap bm = BitmapFactory.decodeResource(getResources(), R.drawable.test);
mSrcBitmap = Bitmap.createScaledBitmap(bm, 600, 900, true);
bm = null;
}
直接将图片压缩成用户所期望的长度和宽度,来减少占用内存。
2.4 RGB_565压缩
private void compressRGB565() {
BitmapFactory.Options options = new BitmapFactory.Options();
options.inPreferredConfig = Bitmap.Config.RGB_565;
mSrcBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.test, options);
}
这是通过压缩像素占用的内存来达到压缩的效果,由于ARGB_4444的画质惨不忍睹,一般假如对图片没有透明度要求的话,可以改成RGB_565,相比ARGB_8888将节省一半的内存开销。