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> 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将节省一半的内存开销。


加载长图Bitmap优化

Android加载长图优化

Android 性能优化(五)之细说 Bitmap

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容