Android内存优化总结

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
  • 需要Context时传递Application的Context,因Application的生命周期等于整个应用的生命周期
1.5 WebView内存泄漏

我们都知道,对应 WebView 来说,其 网络延时、引擎 Session 管理、Cookies 管理、引擎内核线程、HTML5 调用系统声音、视频播放组件等产生的引用链条无法及时打断,造成的内存问题基本上可以用”无解“来形容。

解决方案是我们可以 把 WebView 装入另一个进程,使用AIDL与应用的主进程进行通信。具体为在 AndroidManifes 中对当前的 Activity 设置 android:process 属性即可,最后,在 Activity 的 onDestory 中退出进程,这样即可基本上终结 WebView 造成的泄漏。

1.6 资源性对象未关闭

对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。

1.7 注册对象未注销

例如BraodcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。


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!";

编译器会在类首次被使用到的时候,使用初始化方法来初始化上面的值,之后访问的时候会需要先到它那里查找,然后才返回数据。我们可以使用static final来提升性能:

static final String strVal = "Hello, world!";

这时再也不需要上面的那个方法来做多余的查找动作了。所以,请尽可能的为常量声明为static final类型的。

3.2 数据类型选择

不要使用比需求更占空间的基本数据类型

  • 条件允许下,尽量避免使用float类型,Android系统中float类型的数据存取速度是int类型的一半,尽量优先采用int类型。
3.3 循环用foreach少用iterator
3.4 使用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.5 尽量少使用枚举

每一个枚举值都是一个单例对象,在使用它时会增加额外的内存消耗,所以枚举相比与 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.6 尽量使用IntentService而不是Service

如果应用程序当中需要使用Service来执行后台任务的话,请一定要注意只有当任务正在执行的时候才应该让Service运行起来。另外,当任务执行完之后去停止Service的时候,要小心Service停止失败导致内存泄漏的情况。

当我们启动一个Service时,系统会倾向于将这个Service所依赖的进程进行保留,这样就会导致这个进程变得非常消耗内存。并且,系统可以在LRU cache当中缓存的进程数量也会减少,导致切换应用程序的时候耗费更多性能。严重的话,甚至有可能会导致崩溃,因为系统在内存非常吃紧的时候可能已无法维护所有正在运行的Service所依赖的进程了。

为了能够控制Service的生命周期,Android官方推荐的最佳解决方案就是使用IntentService,这种Service的最大特点就是当后台任务执行结束后会自动停止,从而极大程度上避免了Service内存泄漏的可能性。

3.7 在合适的时候适当采用软引用和弱引用。

4.Bitmap中优化

可以参考另一篇文章:
Bitmap内存优化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值