android性能优化——内存泄漏

时间:2022-10-15 20:55:56

在项目初期阶段或者业务逻辑很简单的时候对于app性能之一块没有太多感觉,但是随着项目版本的迭代和项目业务逻辑越来越大,越来越复杂的时候,就会逐渐感觉到app性能的重要性,所以在项目初期阶段时,就要有app性能这一意识,也便于项目后期的版本迭代和业务扩展;这里所提到的性能优化问题是:内存泄漏

什么是内存泄漏?

通俗一点就是内存没有在GC掌控之内;当一个对象已经不需要再使用了,本该被回收,而有另外一个正在使用的对象持有它的引用从而就导致对象不能回收,这就导致了本该被回收的对象不能被回收而停留在堆内存中,就产生了内存泄漏。

android系统会给正在运行的程序一个独立的进程,并分配一定的内存空间,对于每个程序来说它的内存空间是固定的,所以当每个对象不再使用时,应将其所开辟占用用的内存释放掉,如果发生内存泄漏并大量占用内存,当所耗用的内存超过系统所分配的内存时就会导致内存溢出,从而导致程序闪退。

当定义一些成员变量或者实例化一些对象的时候,就会在系统分配的内存中去占用一些内存,涉及到栈、堆、静态方法区几种内存分配策略;

栈:

在执行函数(方法)时,函数一些内部变量的存储都可以放在栈上面创建,函数执行结束的时候这些存储单元就会自动被释放掉,栈内存包括分配的运算速度很快,因为内置在处理器里面的,当然容量有限;

堆:

也叫做动态内存分配,有时候可以用malloc或者new来申请分配一个内存,在C/C++可能需要自己负责释放,java里面可以依赖GC机制,会将已不在使用或没有其他对象引用的对象回收掉;

静态方法区:

内存在程序编译的时候就已经分配好,这块的内存在程序整个运行期间都一直存在,它主要存放静态数据、全局的static数据和一些常量;

所以讨论内存泄漏,主要讨论堆内存,它存放的就是引用指向的对象实体;但是也要主要静态方法区,大量静态的使用可能不会造成内存泄漏,但是会导致内存一直会被占用而无法释放;所以在开发过程中根据需要可以使用一些java提供的StrongReference(强引用)、SoftReference(软引用)、WeakReference(弱引用)、PhatomReference(虚引用);

StrongReference(强引用):从不回收;生命周期:JVM停止的时候才会停止;

SoftReference(软引用):当内存不足的时候就会进行回收,SoftReference<String>结合ReferenceQueue构造有效期短;生命周期:内存不足时终止;

WeakReference(弱引用):在垃圾回收的时候就会进行回收;生命周期:GC后终止

PhatomReference(虚引用):在垃圾回收的时候;使用:合ReferenceQueue来跟踪对象呗垃圾回收期回收的活动; 生命周期:GC后终止

所以在项目开发过程中需要养成良好的编码习惯,同时尽量了解一些容易引发内存泄漏的场景,及一些检测内存泄漏的工具和方法。

先去看一些容易引发内存泄漏的场景:

场景一:单例使用造成的内存泄漏

在项目开发过程中或多或少会使用到单例,例如:

public class CommUtil {
    private static CommUtil instance;
    private Context context;
    private CommUtil(Context context){
        this.context = context;
    }

    public static CommUtil getInstance(Context context){
        if(instance == null){
            instance = new CommUtil(context);
        }
        return instance;
    }
}
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //创建实例
        CommUtil commUtil = CommUtil.getInstance(this);
    }
}

这是很常见的单例写法及实例化使用,其实自己没有学习性能优化之前就是这样写的,学习之后才发这是一种错误的写法,很容易就会出现内存泄漏;在实例化CommUtil时传入的this代表的是当前的MainActivity,当MainActivity销毁后,CommUtil中仍然持有MainActivity的引用,这样就造成了MainActivity的泄漏;所以在实例化CommUtil的时,传入的上下文可以使用getApplicationContext方式获取,这样就使得CommonUtil生命周期是跟Application进程一致;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //创建实例
        CommUtil commUtil = CommUtil.getInstance(getApplicationContext());
    }
}

场景二:线程使用导致的内存泄漏

项目开发过程中遇到耗时操作时,都会在子线中进行操作;

public class MainActivity extends AppCompatActivity {
    int a=10;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //模拟加载数据
        loadData();
    }

    private void loadData() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                        int b=a;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

这是一个模拟数据加载,当离开MainActivity后,如果new Thread()中持有MainActivity中成员变量的引用且子线程中的逻辑并没执行完毕,这个时候就会造成内存泄漏,new Thread()其实是一个匿名内部类,这是由匿名内部类与MainActivity生命周期不一致,并持有MainActivity内的引用造成的内存泄漏;可以在loadData()方法上加static;

public class MainActivity extends AppCompatActivity {
    int a=10;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        //模拟加载数据
        loadData();
    }

    private static void loadData() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                        int b=a;
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

但是会发现,loadData();加了static后,int b=a;会报错,如果loadData中没有引用成员变量直接加static就可以了,如果有引用相应的成员变量可以采用下面的方式:

public class MainActivity extends AppCompatActivity {
    int a=10;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        new LoadThread(new WeakReference<MainActivity>(this)).start();
    }
    private static class LoadThread extends Thread implement Runnable{
        private WeakReference<MainActivity> mainActivity;//设置软引用保存,当内存一发生GC的时候就会回收。

        public LoadThread(WeakReference<MainActivity> mainActivity) {
            this.mainActivity = mainActivity;
        }

        @Override
        public void run() {
            super.run();
            MainActivity main =  mainActivity.get();
            if(main==null||main.isFinishing()){
                return;
            }
            try {
                int b=main.a;
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

场景三:Handler造成的内存泄漏

Handler所造成的内存泄漏和线程造成的内存泄漏原因和解决方式都差不多;这里直接说解决方式吧;

private static class LoadHander extends Handler{
        private WeakReference<MainActivity> mainActivity;//设置软引用保存,当内存一发生GC的时候就会回收。

        public LoadHander(WeakReference<MainActivity> mainActivity) {
            this.mainActivity = mainActivity;
        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            MainActivity main =  mainActivity.get();
            if(main==null||main.isFinishing()){
                return;
            }
            int b=main.a;
        }
    }

场景四:一些视图监听所造成的泄漏

在获取某个view的高度和宽度的时候,直接获取是获取不到的,可以采用view.post()方法,也可以采用监听该view的视图树;

public class MainActivity extends AppCompatActivity {
    
    @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR2)
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
		
        final TextView tv= (TextView) findViewById(R.id.tv);
		
        tv.getViewTreeObserver().addOnWindowFocusChangeListener(new ViewTreeObserver.OnWindowFocusChangeListener() {
            @Override
            public void onWindowFocusChanged(boolean hasFocus) {
                //计算完后,一定要移除这个监听
                tv.getViewTreeObserver().removeOnWindowFocusChangeListener(this);
            }
        });
    }
}

但是采用监听view视图树的方式需要注意在操作完成后需要将该监听移除调,在添加监听的时候,该监听是添加到了CopyOnWriteArrayList集合中,所以在不需要的时候就要从CopyOnWriteArrayList集合中移除。

当然了造成内存泄漏的场景不只上面这些,在项目开发中要养成性能优化的意识,养成良好的编码习惯;还可以使用工具来检测代码或者程序是否有内存泄漏。

Android Monitor工具

这是android studio自带的一个工具;运行项目,在android studio下方找到Android Monitor,切换到Monitor

android性能优化——内存泄漏

会看到Memory(反映内存情况)、CPU(CPU运行情况)、Network(网络耗用情况)、GPU(虚拟机是没有的,真机会有,反映ui渲染情况),Memory就是程序运行的内存情况,Free(剩下的内存),Allocated(已经使用的内存),在Memory右边有些按钮可以点击;

android性能优化——内存泄漏

手动点击GC(多点击几次),就会将没有持有引用或空闲的内存回收掉;

android性能优化——内存泄漏

多点击几次GC,当内存耗用平稳后,点击形成快照,系统就会根据内存的情况形成一个.hporf文件;

android性能优化——内存泄漏


android性能优化——内存泄漏

多次点击GC后生产的内存快照文件中有两个MainActivity,有一个被CommUtil持有没有释放就容易会造成泄漏。

Lint分析工具

Lint分析工具也是android studio自带的工具,很方便,很好用,功能强大;

  1. 检测资源文件是否有没有用到的资源。
  2. 检测常见内存泄露
  3. 安全问题SDK版本安全问题
  4. 是否有费的代码没有用到
  5. 代码的规范---甚至驼峰命名法也会检测
  6. 自动生成的罗列出来
  7. 没用的导包
  8. 可能的bug

上面这些是可以自动检测的,Analyze--->Inspect Code

android性能优化——内存泄漏

根据弹框选择如下(第一次):

android性能优化——内存泄漏

第一次好像和第二次的选择好像不一样;第二次选择:

android性能优化——内存泄漏

就会出现下面页面:

android性能优化——内存泄漏

可以根据提示对项目进行各方面的优化;

Memory Usage

Memory Usage只能大致粗略的知道程序有没有内存泄漏等,不能定位到大致的造成泄漏的地方;

android性能优化——内存泄漏

点击查看生产的txt文件,找到Objects,主要看Views和Activities,需要注意的是Views的数量并不代表就存在内存泄漏,还是要看Activities的数量,如果数量不对多半会是内存泄漏。

android性能优化——内存泄漏

LeakCanary

LeakCanary是一个第三方的检测内存泄漏,使用方便、简单,可以定位到具体造成泄漏的地方,它是根据程序去进行检测的,反馈的时间会有些迟;

github地址:https://github.com/square/leakcanary

首先引入依赖库:

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

在项目的Application文件中进行注册;

public class ExampleApplication 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);
    // Normal app init code...
  }
}

运行应用程序,会在手机安装相应的检测程序;

android性能优化——内存泄漏

操作应用程序,有造成内存泄漏的时候,LeakCanary就会发送消息在手机通知栏,并显示造成泄漏的地方;

android性能优化——内存泄漏

使用起来还是比较简单的,并且容易定位地方;当然了检测内存泄漏的方法和工具不止上面这些,还有MAT、HeapTool(查看堆信息)、Allaction Tracking等方法,下面提供了一些检测方法和工具使用的word文档地址:

https://pan.baidu.com/s/1r0V6bbSyR3aY7Qy8uqhB1Q