Android 内存优化总结

一、内存泄漏

1、Handler导致的内存泄漏

// 错误示例
private Handler handler = new Handler(){
    @Override
    public void handleMessage(@NonNull Message msg) {
        super.handleMessage(msg);
    }
};

没有指定的话,这段代码通常会在主线程执行,主线程的handler除了处理当前activity的相关消息外,还会处理其他逻辑中的任务,而如果Activity被finish掉了,那么消息队列的消息依旧会由handler进行处理,若此时handler声明为内部类(非静态内部类),我们知道内部类天然持有外部类的实例引用,那么就会导致activity无法回收,进而导致activity泄露

2、Rxjava导致的内存泄漏

Observable.create(new ObservableOnSubscribe<XXX>() {
    @Override
    public void subscribe(final ObservableEmitter<XXX> emitter) throws Exception {
        Observable.zip(.....)
              .subscribeOn(Schedulers.io())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe(new Consumer<ImpressionBean>() {
                    @Override
                    public void accept(XXX impressionBean) throws Exception {
                        emitter.onNext(impressionBean);
                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(Throwable throwable) throws Exception {
                        emitter.onNext(new XXX());
                    }
                });
    }
});

使用RxJava发布一个订阅后,当页面被finish而订阅逻辑还未完成时,如果没有及时取消订阅,就会导致Activity/Fragment无法被回收,从而引发内存泄漏

3、List导致的内存泄漏

object AppUtil {
    
    private val activityStack = Stack<BaseActivity>()
    
    fun addActivity(activity: BaseActivity) {
        activityStack.add(activity)
    }

    fun removeActivity(activity: BaseActivity) {
        activityStack.remove(activity)
    }
}

我们通常会在一个应用中统计并记录Activty的堆栈,用来判断某个Activty是否已经启动,或者获取到当前正在运行的activity等,在这种情况下,如果添加进去的Activity没有被及时释放,那么在Activity执行finish以后却无法释放该对象,导致出现内存泄漏

4、WebView导致的内存泄漏

@Override
public void onDetachedFromWindow() {
    if (isDestroyed()) return;
    if (!mIsAttachedToWindow) {
        Log.w(TAG, "onDetachedFromWindow called when already detached. Ignoring");
        return;
    }
    mIsAttachedToWindow = false;
    hideAutofillPopup();
    nativeOnDetachedFromWindow(mNativeAwContents);
 
    mContentViewCore.onDetachedFromWindow();
    updateHardwareAcceleratedFeaturesToggle();
 
    if (mComponentCallbacks != null) {
        mContext.unregisterComponentCallbacks(mComponentCallbacks);
        mComponentCallbacks = null;
    }
    mScrollAccessibilityHelper.removePostedCallbacks();
}

系统会在detach进行反注册component callback,注意到 onDetachedFromWindow() 方法的第一行,if (isDestroyed()) return;, 如果 isDestroyed() 返回 true 的话,那么后续的逻辑就不能正常走到,所以就不会执行unregister的操作,通过看代码,可以得到,调用主动调用 destroy()方法,会导致 isDestroyed() 返回 true

5、静态属性导致的内存泄漏

// 错误示例
public class ActivityMemory extends AppCompatActivity {

    private static Configuration configuration;
    
    @Override
    protected void onCreate(@Nullable @org.jetbrains.annotations.Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        configuration = getResources().getConfiguration();
    }
}

静态属性持有了Activity的configuration,而configuration持有了Activity,会导致Actviity无法被系统回收

6、单例导致的内存泄漏

public class AppSettings {

    private static AppSettings sInstance;
    private Context mContext;

    private AppSettings(Context context) {
        this.mContext = context;
    }

    public static AppSettings getInstance(Context context) {
        if (sInstance == null) {
            sInstance = new AppSettings(context);
        }
        return sInstance;
    }
}

AppSettings是个全局单例对象,它持有的Context如果不是Application,则此Context无法被系统释放,从而导致内存泄漏的问题,其本质上也可以认为是静态属性导致的内存泄漏

7、非静态内部类导致的内存泄漏

这里可以参考Handler的示例代码

8、未反注册导致的内存泄漏

public class MainActivity extends AppCompatActivity {

    private BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            // 接收到广播需要做的逻辑
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        this.registerReceiver(mReceiver, new IntentFilter());
    }
}

这里的广播接收器,如果在Activity的destory中没有反注册,那么这个广播接收器会一直一直持有Activity的引用,从而导致内存泄漏,类似的还有Eventbus、传感接收器,定位接收器等需要反注册的工具

9、Timer导致的内存泄漏

public class MainActivity extends AppCompatActivity {

    private Timer mTimer;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        init();
        mTimer.schedule(mTimerTask, 3000, 3000);
    }

    private void init() {
        mTimer = new Timer();
        mTimerTask = new TimerTask() {
            @Override
            public void run() {
                MainActivity.this.runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        loopViewpager();
                    }
                });
            }
        };
    }

    private void loopViewpager() {
        
    }
}

原理同上,非静态内部类或者匿名内部类默认持有了外部类的对象,如果不及时释放,会导致被持有的对象无法被回收而导致内存泄漏

10、属性动画造成内存泄露

@Override
protected void onCreate() {
    super.onDestroy();
    mAnimator.start();
}

动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放

二、内存检测

1、内存泄漏检测

LeakCanary内存泄漏检测,核心思路是利用RefWatcher.watch() 创建一个 KeyedWeakReference来监控需要被检测的对象,当Activity执行finish方法后,LeakCanary会在后台线程中检测引用的对象是否被释放,如果没有,则调用GC再次尝试释放,如果还是没有被释放,则把内存dump到 .hprof中,通过HeapAnalyzer来分析这个文件,并建立导致内存泄漏的引用链,然后展示出来

2、内存占用检测

通过Android studio自带的Profile检测应用的内存占用情况,执行GC后,打开某一个页面,查看内存占用情况,这个差值就是新页面的内存占用,工具界面如图:

Google官方的详细说明文档:https://developer.android.google.cn/studio/profile/memory-profiler

 

三、内存优化

1、Bitmap优化

1)内存占用优化

Bitmap尽量采用“显示多大,采样多大”的方案,比如ImageView的大小是100*100,则图片的采样也应该是100*100,避免占用内存过大

常规的压缩方案是:

  • 1、质量压缩:(改变图片的位深和透明度)
/**
 * 质量压缩
 * 设置bitmap options属性,降低图片的质量,像素不会减少
 * 第一个参数为需要压缩的bitmap图片对象,第二个参数为压缩后图片保存的位置
 * 设置options 属性0-100,来实现压缩(因为png是无损压缩,所以该属性对png是无效的)
 *
 * @param bmp
 * @param file
 */
public static void qualityCompress(Bitmap bmp, File file) {
    // 0-100 100为不压缩
    int quality = 20;
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    // 把压缩后的数据存放到baos中
    bmp.compress(Bitmap.CompressFormat.JPEG, quality, baos);
    try {
        FileOutputStream fos = new FileOutputStream(file);
        fos.write(baos.toByteArray());
        fos.flush();
        fos.close();
    } catch (Exception e) {
        e.printStackTrace();
    }
}
  • 2、尺寸压缩(改变图片的大小)
   /**
     * 尺寸压缩(通过缩放图片像素来减少图片占用内存大小)
     *
     * @param bmp
     * @param file
     */

 public static void sizeCompress(Bitmap bmp, File file) {
        // 尺寸压缩倍数,值越大,图片尺寸越小
        int ratio = 8;
        // 压缩Bitmap到对应尺寸
        Bitmap result = Bitmap.createBitmap(bmp.getWidth() / ratio, bmp.getHeight() / ratio, Config.ARGB_8888);
        Canvas canvas = new Canvas(result);
        Rect rect = new Rect(0, 0, bmp.getWidth() / ratio, bmp.getHeight() / ratio);
        canvas.drawBitmap(bmp, null, rect, null);

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        // 把压缩后的数据存放到baos中
        result.compress(Bitmap.CompressFormat.JPEG, 100, baos);
        try {
            FileOutputStream fos = new FileOutputStream(file);
            fos.write(baos.toByteArray());
            fos.flush();
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
  }
  • 3、采样率压缩(去除部分图片信号)
 private int computeSize() {
    srcWidth = srcWidth % 2 == 1 ? srcWidth + 1 : srcWidth;
    srcHeight = srcHeight % 2 == 1 ? srcHeight + 1 : srcHeight;

    int longSide = Math.max(srcWidth, srcHeight);
    int shortSide = Math.min(srcWidth, srcHeight);

    float scale = ((float) shortSide / longSide);
    if (scale <= 1 && scale > 0.5625) {
      if (longSide < 1664) {
        return 1;
      } else if (longSide < 4990) {
        return 2;
      } else if (longSide > 4990 && longSide < 10240) {
        return 4;
      } else {
        return longSide / 1280 == 0 ? 1 : longSide / 1280;
      }
    } else if (scale <= 0.5625 && scale > 0.5) {
      return longSide / 1280 == 0 ? 1 : longSide / 1280;
    } else {
      return (int) Math.ceil(longSide / (1280.0 / scale));
    }
  }

2)及时释放内存

在Bitmap不要使用的时候,及时调用Bitmap.recycle()进行内存释放

3)借助第三方工具

项目中通常是使用Glide加载网络图片,需要注意的是,在加载drawable,mipmap,assets等资源时,也尽量使用Glide来加载,它内部会根据要展示的imageView的大小对图片资源进行压缩,这个地方很多开发人会容易忽略

2、线程池优化

/** 每次只执行一个任务的线程池 */
ExecutorService singleTaskExecutor =  Executors.newSingleThreadExecutor();
 
/** 每次执行限定个数个任务的线程池 */
ExecutorService limitedTaskExecutor = Executors.newFixedThreadPool(3);
 
/** 所有任务都一次性开始的线程池 */
ExecutorService allTaskExecutor = Executors.newCachedThreadPool();
 
/** 创建一个可在指定时间里执行任务的线程池,亦可重复执行 */
ExecutorService scheduledTaskExecutor = Executors.newScheduledThreadPool(3);

线程池的基本思想还是一种对象池的思想,开辟一块内存空间,里面存放了众多(未死亡)的线程,池中线程执行调度由池管理器来处理。当有线程任务时,从池中取一个,执行完成后线程对象归池,这样可以避免反复创建线程对象所带来的性能开销,节省了系统的资源

3、软引用(弱引用)

public class MainActivity extends AppCompatActivity {

    private Handler mHandler;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mHandler = new MyHandler(this);
        start();
    }

    private void start() {
        Message msg = Message.obtain();
        msg.what = 1;
        mHandler.sendMessage(msg);
    }

    private static class MyHandler extends Handler {

        private WeakReference<MainActivity> activityWeakReference;

        public MyHandler(MainActivity activity) {
            activityWeakReference = new WeakReference<>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = activityWeakReference.get();
            if (activity != null) {
                if (msg.what == 1) {
                    // 做相应逻辑
                }
            }
        }
    }
}

对于内部类持有外部类对象的情况,可以采用弱引用的形式,避免内存泄漏

软引用和弱引用的区别:

软引用:只有在内存不足时,JVM虚拟机才会回收该对象

弱引用:JVM进行垃圾回收时总会被回收

4、改为静态内部类

示例代码参考上面的例子,由于静态内部类不持有所在类的引用,同样的静态内部类创建的对象也无法持有外部类的对象,从而避免了内存泄漏的问题

4、及时关闭资源

1)IO流的关闭

stream.close();

2)图片资源的回收

bitmap.recycle();

3)注册对象的释放

(EventBus、广播接收器、传感器监听器等)

 override fun onDestroy() {
     EventBus.getDefault().unregister(this)
     super.onDestroy()
 }

4)属性动画的释放

@Override
protected void onDestroy() {
    super.onDestroy();
    mAnimator.cancel();
}

5)计时器的释放

mTimer.cancel()

5、WebView内存优化

1)单进程处理

单开新的进程用来创建WebView,通过AIDL来与主进程进行通信,当WebView被关闭时,杀掉WebView所在进程,彻底释放内存,详细代码太长,可以参考这里:https://www.jianshu.com/p/b66c225c19e2,单进程的好处在于:1、可以做到彻底释放WebView资源,2、由于WebView内存占用受到显示内容的影响,启用独立进程可以申请更多的空间,避免程序申请内存过大导致被系统回收的问题

2)主动置空

根据上面排查的泄漏原因,通过动态创建的形式添加WebView,并且在WebView需要释放时,提前在父控件中移除后再调用WebView的destroy()方法释放对象

@Override
protected void onDestroy() {
    if (mWebView != null) {
        mWebView.loadDataWithBaseURL(null, "", "text/html", "utf-8", null);
        mWebView.clearHistory();
        ((ViewGroup) mWebView.getParent()).removeView(mWebView);
        mWebView.destroy();
        mWebView = null;
    }
    super.onDestroy();
}

  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值