Android性能优化——内存优化

1,253 阅读9分钟

避免可控的内存泄漏

内存泄漏是内存优化的重点,如何避免、发现和解决内存泄漏十分重要

1. 何为内存泄漏

每个应用程序都需要内存来完成工作,为了确保Android系统的每个应用都有足够的内存,Android系统需要有效地管理内存分配。内存不足时Android运行就会触发GC,GC采用的垃圾标记算法为根搜索算法。而内存泄漏就是指没有用的对象从GC Roots是可达的,导致GC无法回收该对象。一般产生内存泄漏的原因有三大类

  • 开发人员自己编码造成的内存泄漏
  • 由三方框架造成的泄漏
  • 由Android系统或者第三方ROM造成的泄漏

通常来说,第二种和第三种是不可控的,但第一种情况是可控的,下面举例常见的内存泄漏场景


2. 内存泄漏的场景

非静态内部类的静态实例

非静态内部类会持有外部类实例的引用,如果非静态内部类的实例是静态的,就会间接长期维持外部类的引用,阻止被系统回收,代码如下

public class MainActivity extends AppCompatActivity {

    private static Object inner;
    private Button button;

    @Override protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        button = findViewById(R.id.button_test);
        button.setOnClickListener(v -> {
            createInnerClass();
            finish();
        });
    }

    void createInnerClass(){
        class InnerClass{

        }
        inner = new InnerClass();
    }
}

当点击Button时,会在new InnerClass()处创建非静态内部类InnerClass的静态实例inner,该实例的生命周期会和应用程序一样长,并且一直持有SecondActivity的引用,导致SecondActivity无法被回收


多线程相关的匿名内部类/非静态内部类

和前面的非静态内部类意义,匿名内部类也会持有外部类实例的引用,多线程相关的类有AsyncTask类,Thrad类和实现Runnable接口的类等,他们的匿名内部类/非静态内部类如果做耗时操作就会引起内存泄漏,以AsyncTask为例,如下

public class AsyncTaskActivity extends AppCompatActivity {

    private Button button;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async_task);
        button = findViewById(R.id.button_async);
        button.setOnClickListener(v -> {
            startAsyncTask();
            finish();
        });
    }
    
    void startAsyncTask(){
        new AsyncTask<Void, Void, Void>(){
            @Override protected Void doInBackground(Void... voids) {
                while(true);
            }
        }.execute();
    }
}

在startAsyncTask方法里实例化一个AsyncTask,其异步任务在后台执行耗时操作期间,整个Activity被销毁,被AsyncTask持有的Activity实例不会被垃圾回收,直到异步任务结束。同理,自定义的AsyncTask如果是非静态内部类也会发生内存泄漏,解决的办法就是自定义一个静态的AsyncTask,如下

public class AsyncTaskActivity extends AppCompatActivity {

    private Button button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_async_task);
        button = findViewById(R.id.button_async);
        button.setOnClickListener(v -> {
            startAsyncTask();
            finish();
        });
    }

    void startAsyncTask(){
        new MyAsyncTask().execute();
    }
    
    private static class MyAsyncTask extends AsyncTask<Void, Void, Void>{

        @Override protected Void doInBackground(Void... voids) {
            while (true);
        }
    }
}

Handler内存泄漏

Handler的Message被存储在MessageQueue中,有些Message并不能马上被处理,他们在MessageQueue中存在的时间会很长,这就会导致Handler无法被回收,如果Handler是非静态的,则Handler也会导致引用它的Activity或者Service不能被回收

public class HandlerActivity extends AppCompatActivity {
    
    private Button button;
    
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        button = findViewById(R.id.button_handler);
        
        final Handler handler = new Handler(){
            @Override public void handleMessage(@NonNull Message msg) {
                super.handleMessage(msg);
            }
        };
        
        button.setOnClickListener(v -> {
            handler.sendMessageDelayed(Message.obtain(), 6000);
            finish();
        });
    }
}

Handler是非静态的匿名内部类的实例,它会隐性引用外部类HandlerActivity,这个例子就是当我们点击button时,HandlerActivity会结束,但是Handler中的消息还没有被处理,因此HandlerActivity无法被回收。解决的方案有两个,一个是使用静态Handler内部类,Handler持有的对象要用弱引用,如下

public class HandlerActivity extends AppCompatActivity {

    private Button button;
    private MyHandler myHandler = new MyHandler(this);
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_handler);
        button = findViewById(R.id.button_handler);
        button.setOnClickListener(v -> {
            myHandler.sendMessageDelayed(Message.obtain(), 6000);
            finish();
        });
    }
    
    public void show(){}
    
    private static class MyHandler extends Handler{
        private final WeakReference<HandlerActivity> activityWeakReference;
        
        public MyHandler(HandlerActivity activity){
            activityWeakReference = new WeakReference<>(activity);
        }
        
        @Override public void handleMessage(Message message){
            if(activityWeakReference != null && activityWeakReference.get() == null){
                activityWeakReference.get().show();
            }
        }
    }
}

MyHandler是一个静态内部类,它持有的HandlerActivity对象使用了弱引用,这样就避免了内存泄漏。

还有一个解决方案就是在onDestry生命周期回调中移除MessageQueue中的消息,如下所示

@Override public void onDestroy(){
    if(myHandler != null){
        myHandler.removeCallbacksAndMessages(null);
    }
    super.onDestroy();
}

在即将销毁时清除Callbacks和Messages,采用这个方法有可能不能完全清除Handler中的消息,因此还是建议用第一种方式


未正确使用Context

对于不是必须使用Activity的Context的情况,比如Dialog的Context必须使用Activity的Context。我们可以考虑使用Application Context来代替Activity的Context,就可以避免Activity泄漏,比如如下单例模式

public class AppSettings {
    
    private Context appContext;
    private static AppSettings appSettings = new AppSettings();
    public static AppSettings getInstance(){
        return appSettings;
    }
    
    public final void setup(Context context){
        appContext = context;
    }
}

appSettings作为静态对象,其生命周期会长于Activity。当屏幕旋转时,在默认情况下,系统会销毁当前Activity,因为当前Activity调用了setup方法,并传入了Activity的Context,使得Activity被一个单例持有,导致垃圾收集器无法回收,进而产生了内存泄漏,解决方法就是使用Application的Context,如下

public final void setup(Context context){
    appContext = context.getApplicationContext();
}

静态View

使用静态View可以避免每次启动Activity都去读取并渲染View,但是静态View会持有Activity的引用,导致Activity无法被回收,解决的办法就是在onDestroy方法中将静态View置为null,代码如下

public class ViewActivity extends AppCompatActivity {
    private static Button button;

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_view);
        button = findViewById(R.id.button_view);
        button.setOnClickListener(v -> {
            finish();
        });
    }

    @Override protected void onDestroy() {
        button = null;
        super.onDestroy();
    }
}

WebView

不同的Android版本里WebView会有差异,不同厂商定制的ROM的WebView也会有差异,这就导致WebView存在很大的兼容性问题,WebView都会存在内存泄漏问题,在应用中使用一次WebView,内存就不会被释放,通常的解决办法就是为WebView单开一个进程,使用AIDL与应用主进程进行通信,WebView进程可以根据业务需求在合适的时机进行销毁


资源对象未关闭

资源对象比如Cursor、File等,往往都使用了缓冲,会造成内存泄漏,因此在资源对象不使用时,一定要确保关闭其引用并置为null,通常在finally语句中进行资源关闭操作


集合中对象未清理

通常把一些对象的引用加入到了集合中,当不需要该对象时,如果没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就会更加严重


BitMap对象

临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,这样能够更快释放原始bitmap所占用的空间。避免静态变量持有比较大的bitmap对象或者其他大的数据对象,如果已经持有,要尽快置空该静态变量


监听器未关闭

很多系统服务(比如TelephonyMannager、SensorManager)需要register和unregister监听器,我们需要确保在合适的时候及时unregister那些监听器。自己手动add的Listener,要记得在合适的时候及时remove这个Listener


Memory Monitor

这是AS中内置的内存分析工具,在编辑器下方的Profile中打开,比如我们运行上面案例中任意一个代码,可以看到类似如下的内存变动图

image.png

这个工具主要有如下作用

  • 实时显示可用的和分配的Java内存图表
  • 实时显示垃圾收集事件
  • 启动垃圾收集事件
  • 快速测试应用程序的缓慢是否与过度的垃圾收集事件有关
  • 快速测试应用程序崩溃是否与内存耗尽有关

1. 大内存申请与GC

从上图可以看出,分配的内存急剧上升,这就是大内存分配场景,我们要判断是否是合理内存开销,并且对这种大数据进行优化,减少性能损耗。接下来是急剧下降,这表示垃圾收集事件,用于释放内存


2. 内存抖动

内存抖动一般指在很短的时间内发生了多次内存分配和释放,严重的内存抖动还会导致应用程序卡顿。内存抖动出现原因主要是短时间频繁的创建对象(可能在循环中创建对象),内存为了应对这种情况,也会频繁的进行GC。非并行GC在进行时,其他线程都会被挂起,等待GC操作完成后恢复工作。如果是频繁的GC就会产生大量的暂停时间,这会导致界面绘制时间减少,从而使得多次绘制一帧的时长超过了16ms,产生的现象就是界面卡顿。综合起来就产生了内存抖动,会产生锯齿状的抖动图


LeakCanary

分析内存问题可以使用MAT,一个基于Eclipse的插件,但会有一些难度,并且效率也不是很高,对于一个内存泄漏问题,可能要进行多次排查和对比。为了能够简单迅速的发现内存泄漏,Square公司基于MAT开源了LeakCanary

1. 使用LeakCanary

首先配置build.gradle

dependencies {
    debugImplementation 'com.squareup.leakcanary:leakcanary-android:1.6.3'
    releaseImplementation 'com.squareup.leakcanary:leakcanary-android-no-op:1.6.3'
}

接下来在Application加入如下代码

public class LeakApplication extends Application {
    @Override public void onCreate() {
    super.onCreate();
    if (LeakCanary.isInAnalyzerProcess(this)) {
      // This process is dedicated to LeakCanary for heap analysis.
      // You should not init your app in this process.
      return;
    }
    LeakCanary.install(this);
  }
}

if条件处的代码用来进行过滤操作,如果当前的进程是用来给LeakCanary进行堆分析的则return,否则会执行LeakCanary的install方法。这样我们就可以使用LeakCanary了,如果检测到某个Activity有内存泄露,LeakCanary就会给出提示


2. LeakCanary应用举例

上面的案例只能检测Activity的内存泄漏,如果还需要检测其他类的内存泄漏,我们就需要RefWatcher来进行监控。改写Application,如下

public class LeakApplication extends Application {

    private RefWatcher refWatcher;

    @Override public void onCreate() {
        super.onCreate();
        refWatcher = setupLeakCanary();
    }
    
    private RefWatcher setupLeakCanary(){
        if(LeakCanary.isInAnalyzerProcess(this)){
            return RefWatcher.DISABLED;
        }
        return LeakCanary.install(this);
    }
    
    public static RefWatcher getRefWatcher(Context context){
        LeakApplication leakApplication = (LeakApplication) context.getApplicationContext();
        return leakApplication.refWatcher;
    }
}

install方法会返回RefWatcher来监控对象,LeakApplication中还要提供getRefWatcher静态方法来返回全局RefWatcher。最后为了举例,我们在一段存在内存泄漏的代码中引入LeakCanary监控,如下

public class LeakActivity extends AppCompatActivity {

    @Override protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_leak);
        LeakThread leakThread = new LeakThread();
        leakThread.start();
    }
    
    class LeakThread extends Thread{
        @Override public void run() {
            try {
                Thread.sleep(6 * 60 * 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override protected void onDestroy() {
        super.onDestroy();
        RefWatcher refWatcher = LeakApplication.getRefWatcher(this);
        refWatcher.watch(this);
    }
}

LeakActivity存在内存泄漏就是因为非静态内部类LeakThread持有外部类的引用,在LeakThread中做耗时操作导致Activity无法释放。拿到refWatcher对象后调用watch方法观察要监控的对象this。当然此演示中onDestroy方法是多余的,因为LeakCanary在调用install方法后会启动一个ActivityRefWatcher类,用于自动监控Activity执行onDestroy方法之后是否发生内存泄漏。这里为了方便举例,如果想监控Fragment,就在Fragment中添加如上的onDestroy方法即可。接下来运行程序,就会在界面上生成Leaks的应用图标。通过不断的切换横竖屏销毁和创建Activity,会闪出一个提示框,通过Notification展示出内存泄漏信息,点击就进入了内存泄漏详情页,会展示一个具体的引用链

image.png

MainActiviy的内部类LeakThread引用了LeakThread的this$0this$0的含义就是内部类自动保留的一个指向所在外部类的引用,而这个外部类就是详情最后一行所给出的MainActiviy的实例,这将会导致MainActivity无法被GC,从而产生内存泄漏

当然,解决方案自然就是将内部类声明为static即可,就不会给出内存泄漏提示了