Effective Android:app优化 ------ 内存管理、内存泄漏、冷启动

一. 内存泄漏

其实关于内存泄漏的概念开发人员并不陌生,简单来说就是该被释放的对象,由于被某些实例持有引用,导致GC无法回收,不能及时被释放。以下只是简单介绍概念,详细可研究jvm。

1. java中的内存泄漏

(1) java内存的分配策略

内存策略分为三个,分别是静态分配、栈式分配、堆式分配,对应的内存存储区域如下(注意堆区与栈区的差异):

  • 静态存储区(方法区)
    该区域主要存放一些静态数据、全局变量等等,这些变量在整个程序运行期间一直存在,并且在程序编译时该区域空间已被分配好。

  • 栈区
    当方法被执行时,方法体内的局部变量会在栈区创建内存空间,并在方法结束时自动释放。因为栈内存分配运算内置于处理器中,所以效率很高不过栈区空间有限。

  • 堆区
    在堆区被称为动态内存分配,即new创建出的对象存储空间,当对象无任何引用时会被GC回收。


(2) java是如何管理内存的

不同于C++,java中对象的释放是全权交予GC处理,开发人员只需创建即可。此特性大大简化了编码工作,但是同时加重了虚拟机的工作量,这也是java相对于C、C++运行较慢的原因之一。GC为了能够正确释放对象,而去监控每一个对象运行状态,包括对象的申请、引用、被引用、复制等等,为了更好地理解,查看下图及代码:

class test{
    Public static void main(String args[]){
        Object o1 = new Object();
        Object o2 = new Object();
        o2 = o1;
    }
}

这里写图片描述

该图描述了以上代码内存管理的有向图,Object2是第二次申请的对象,由于在第三行代码赋值后,该对象已无任何引用,为可回收对象。

可以将对象考虑为有向图的顶点,引用关系则是有向边,该图就是从main进程开始的有向图。图中根顶点可达的对象都是有效对象,例如o1、o2、Object1,但是Object2不是根顶点可抵达的,所以此对象可被回收。


(3)java中的内存泄漏

内存泄漏是指无用对象(不再使用的对象)持续占有内存或无用对象的内存得不到及时释放,从而造成内存空间浪费。注意大量的内存泄漏会造成OOM(Out Of Memory)。



2.Android中的内存泄漏

其实关于Android中的内存泄漏涉及到很多问题,但是最常见的是以下几点,依次介绍:

(1)单例模式

单例模式在Android运用极为广泛,但是不恰当的使用容易造成内存泄漏。因为单例的静态特性使它的生命周期和App的生命周期一样。如果一个对象不再被使用,而单例对象还持有该对象的引用,那么该对象无法被正常回收。举个例子,查看以下错误示范代码:

public class AppManager {

    //有内存泄漏的问题:
    private static AppManager instance;
    private Context context;
    private AppManager(Context context) {
        this.context = context;
        //正确的写法------解决内存泄漏
        //this.context = context.getApplicationContext();// 使用Application 的context

   }
    public static AppManager getInstance(Context context) {
        if (instance == null) {
            instance = new AppManager(context);
        }
        return instance;
   }
}

查看该类AppManager 的构造方法中传入的 context,其实是Activity的context,这时若Acticity被回收,因被单例类持有引用而无法回收!所以此处赋值的context应当是Application整个应用 的context,这说明单例类的生命周期同应用保持一致
,而不会出现以上内存泄漏问题。


(2)匿名内部类

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }

    //容易造成内存泄漏的写法:
    class TestResource {
       private static final String TAG = "";
        //...
    }

    //正确方法:static class TestResource 
}

以上代码可以看出,在Activity中定义了一个内部类TestResource ,每次启动Activity时会使用到该类中的一些数据,虽然避免了资源的重复创建,但是这种写法会造成内存泄漏。在java中,非静态内部类会默认持有外部类Activity的引用,在非静态内部类中创建的实例,如TAG,该实例的生命周期同应用一样长,导致Activity一直被持有引用而无法回收,引起内存泄漏的原因。

正确的写法应当将非静态内部类设置为静态内部类,这样该内部类不会一直持有外部内的引用而出现内存泄漏的问题。


(3)Handle
Handle是线程之间发送异步消息的框架,而它造成的内存泄漏在项目开发中很常见,比如网络请求后采用Handle进行的回调,没由处理好容易出现问题,查看以下代码示例:

public class MainActivity extends AppCompatActivity {

    //容易造成内存泄漏的写法:
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            //...
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }
}

在理解第二点后,便可以很快反应以上代码中的Handler是非静态内部类,它会持有外部类的引用导致Activity无法及时释放。

Handler是一个TLS(Thread Local Storage)变量,其生命周期与Activity不一致,因此以上Handler编写方法很难保证其生命周期一致性,所以经常导致内存泄漏,以下是更周到的写法:

public class MainActivity extends AppCompatActivity {
    // 修复内存泄漏的方法:

    private MyHandler mHandler = new MyHandler(this);
    private TextView mTextView ;

    private static class MyHandler extends Handler {
        private WeakReference<Context> reference;
        public MyHandler(Context context) {
            reference = new WeakReference<>(context);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = (MainActivity) reference.get();
            if(activity != null){
//                activity.mTextView.setText("");
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        loadData();
    }

    private void loadData() {
        //...request
        Message message = Message.obtain();
        mHandler.sendMessage(message);
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        mHandler.removeCallbacksAndMessages(null);
    }
}

首先将内部类MyHandler 置为静态内部类,让静态内部类继承Hanlder,在其构造方法中使用外部类的弱引用,这样使其生命周期与Activity保持一致,在最后Activity销毁时,即onDestory方法中调用mHandler的removeCallbacksAndMessages方法,传入参数null,将所有的Callbacks和Messages全部清除掉,完美地避免内存泄漏问题。


(4)避免使用static变量

若将不必要的成员变量设置为静态后,意味着它们的生命周期同整个APP应用一样长。如果你的APP进程是常驻内存的,那即使APP运行在后台,这些静态变量也不会被释放,而根据APP管理机制,占用内存较大的进程会被优于考虑回收,当你的APP进程被回收后,这些变量的存在是不安全的。

解决方法是在类设计的时候需要再三考虑某些成员变量在初始化的时候设置为静态,可以考虑“懒加载”避免使用静态变量。如果使用静态变量不可避免,那么一定要管理好这些静态变量的生命周期。


(5)资源未关闭造成的内存泄漏
这一块涉及的内容较多,例如广播接受者、游标、文档、socket、Bitmap等等,它们依附于容器(例如Activity)中,当容器被销毁时,需要及时关闭和注销这些资源,否则它们不会被回收,而引起内存泄漏。


(6)AsyncTask造成的内存泄漏

AsyncTask引起内存泄漏的原因与第三点Handler相同,主要都是因为持有外部类引用导致外部类无法及时释放,解决方法与Hanlder不同的一点,在onDestory方法中调用AsyncTask的cancel即取消掉该任务。





二. 内存管理

1.内存管理机制概述

从操作系统的角度来说,内部就是一块存储区域,是操作系统可以调度的资源,在多进程的系统中内存管理十分重要。从以下两个角度进行说明:

(1)分配机制
操作系统会为每一个进程合理地分配资源,使得每一个进程正常运行。

(2)回收机制
当系统内存不足时,会有一个合理的回收并且再分配内存资源机制,从而保证新的进程能够正常运行。



2.Android内存管理机制

(1)分配机制
Android系统在给每一个进程分配资源时,采用了弹性的分配装置,即系统最初不会分配给APP进程过多内存,随着APP运行,当前内存不够使用时,系统会额外分配有限度的内存大小。Android系统的最大限度就是让更多的线程存活在内存当中,这样当APP再次启动时,无需重创进程,恢复已有进程即可,减少应用启动时间,提高用户体验。

(2)回收机制
Android系统会尽可能保存多的数据,这是继承了Linux的特点,可是当有的进程不再使用时,它的数据还保存在进程当中,所以官方并不推荐直接退出应用。当内存不足时,系统会杀死部分进程来开启新的进程,进程被分为五类:

  • 前台进程:屏幕当中显示的进程。
  • 可见进程:前台进程不再属于前台,但是用户仍能看到的进程。
  • 服务进程:开启的一些服务进程,例如常见的定位、推送服务等。
  • 后台进程:不同于服务进程,在后台会进行一些计算的进程。
  • 空进程:无任何东西在内存中运行的进程,可随时回收掉。

在Android系统中进程有这样五类分级,因为优先级越低的进程越容易被内存杀死,回收的概率越大。前台、可见、服务进程在正常情况下是不会被系统杀死的,后台进程会被存放到缓存列表中,此列表采用的数据结构便是LRU算法,即最近最少使用算法。而空进程是为了平衡整个系统性能,Android系统中不会保存这些进程。最后需要注意回收效率的概念,当系统开始回收进程时,会判断进程回收后的回收效率,因为系统总是倾向于杀死一个能回收更多内存的进程。



3.内存管理机制的特点

与其说是特点,更像是内存管理机制的一个目标,如下:

  • 更少的占用内存

  • 在合适的时候,合理的释放系统资源

并不是说对象使用完后立即处理,这样频繁的释放对象会造成内存抖动,而大量的内存抖动会造成UI卡顿、ANR、OOM等问题。应当是合理释放资源。

  • 在系统内存紧张的情况下,可释放掉大部分不重要资源,来为Android系统提供可用内存。

  • 能够很合理的在特殊生命周期中,保存或还原重要数据,以至于系统能够正确重新恢复该应用。



4.内存优化方法

(1)当Service完成任务后,尽量停止它

Service进程是优先级较低的服务进程,这回影响到内存回收。这里可以用IntentService来代替Service。IntentService是继承与Service的,与之不同的是Service默认运行在主线程执行,即不可做耗时操作;但IntentService内部是开启子线程完成操作,可在它的 onIntentHandler方法中做耗时操作而且它执行完后可自动退出,而Service需手动调用stopService来退出。

(2)在UI不可见的时候,释放掉一些只有UI使用的资源

Android系统中会根据一个onTrimMemory方法回调来通知APP回收UI的资源。

(3)在系统内存紧张的时候,尽可能多的释放掉一些非重要资源

同样也是onTrimMemory回调方法中,它会通知内存紧张状态,APP会根据不同的内存紧张等级来合理释放资源。

(4)避免滥用Bitmap导致的内存浪费
Bitmap的使用在Android中切记要谨慎,由于它还有部分内存存在C中,使用不当(即未释放C中内存)很容易造成内存浪费或更严重的问题。(我写的另一篇有详细讲解,在此不重复讲解:
http://blog.csdn.net/itermeng/article/details/53994876

(5)使用针对内存优化过的数据容器
例如,使用官方推荐的SparseArray来代替消耗很大的HashMap容器;尽量少用枚举常量,因为它消耗的内存是常量的两倍多。

(6)避免使用依赖注入的框架
目前项目开发中经常使用一些依赖注入的框架,比如Butterknife等,这些框架会扫描app中的注解,需要额外的系统资源。

(7)使用多进程
把消耗内存过大的模块或需要长期运行在后台的模块移入到单独的进程当中。例如在实际开发中,定位功能可开启单独线程,推送功能可开启,还有最常用的WebView,若不单独开启一个进程,容易造成内存泄漏。(Ps:多进程使用时一把双刃剑,使用不当会造成数据传输和安全问题)





三. 冷启动优化

1. 冷启动解析

(1)定义

  • 冷启动
    Android系统为每个应用至少分配了一个进程,所以说在应用启动前,系统中没有该应用的任何进程信息(例如activity、service等),这种启动方式是冷启动。出现的场景一般是第一次开启应用,或者应用在后台被杀死再次被启动。

  • 热启动
    后台保留有该应用的进程,重新打开应用,这种启动方式是热启动。出现的场景一般是用户使用返回键退出应用,然后马上又重新启动应用。


(2)冷启动、热启动的区别

  • 后台进程状态不同
    冷启动时后台并无该应用进程;热启动时后台已有该应用进程。

  • 启动特点不同
    冷启动时系统会创建新的进程给应用,所以会创建和初始化Application类,再创建和初始化MainActivity类进行一些测量、绘制等操作,最后显示在布局上;热启动是在已有的进程上启动,所以不会再初始化Application类,而是创建和初始化MainActivity类。注意:一个应用从新进程的创建到进程的销毁,Application只初始化一次。


(3)冷启动时间的计算

这个时间值从应用启动(创建进程)开始计算,到完成视图的第一次绘制(即Activity内容对用户可见)为止。



2. 冷启动流程

  • (1). Zygote进程中fork创建出一个新的进程
  • (2). 创建和初始化Application类,创建MainActivity类
  • (3). 执行Activity声明周期中onCreate()onStart()onResume()
  • (4). contentView的measurelayoutdraw执行完,布局显示在界面上。


3. 冷启动优化方法

(1)减少onCreate()方法的工作量
其实应用的冷启动时无法避免的,用户必须等待,所以尽量减少Application、Actvity中onCreate()方法的工作量。但是在实际中会用到第三方SDK,而Activity初始化过程中会涉及到FrameWork层操作,这时建议在Application里做初始化操作,所以实际开发中会重写Application类。

(2)Application不进行业务有关操作

Application是应用启动首先初始化的,这里尽量做一些抽象操作,可以初始化一些有关场景或业务模块需要用到的数据,而业务逻辑相关的不建议在此操作。

(3)不要在Application中进行耗时操作
例如在Application做一些IO操作,将数据从SD卡读取出来,这是不建议的。

(4)不要以静态变量的方式在Application中保存数据
静态变量的生命周期与应用同步,会造成一些隐患,例如内存泄漏、数据安全等。

(5)减少布局层级
尽量减少布局复杂性、层级,因为布局View绘制的过程中,测量是非常耗费性能的。





四. 其它优化

1. 在Android中不用静态变量存储数据
严格意义来说这点并不是从技术角度来声明,而是Android应用开发角度。因为在Android系统中,应用进程不是安全的,包括Application、进程变量等等,它会被kill掉,或者内存不足时被回收掉。

  • 静态变量等数据由于进程已经被杀死而被初始化

大部分开发者以为Application进程被kill掉后,app会重新启动。实则不然,Andorid系统会重新创建一个新的Application对象,恢复用户上一次离开应用的界面activity,重新初始化这些静态变量,造成数据不安全问题。

  • 使用其它数据传输方式:文件、sharedPreference、contentProvider


2. 有关SharedPreference的安全问题

  • 不能跨进程同步
    即当sharedPreference在多线程进行读写时,不能跨进程去读写数据。在多线程开发中若不正当使用了sp,会导致在不同线程获取到的数据不同。因为在多线程中并不是你调用sp改完数据后,数据就是修改后的结果,每个进程都会维护一份自己的SharedPreference副本,在它运行过程中,其它进程是没有办法去获取它的副本,只有在运行结束的时候,才可以将每个线程中的sharedPreference副本持久化的修改到文件系统当中。避免跨进程操作sp,会引起数据安全问题。

  • 存储SharedPreference文件过大问题
    SharedPreference作为Android五大存储(网络、数据库、文件、SharedPreference、ContentProvider)之一,它并不是一个存储大量数据的工具,而且是通过键值对形式来进行存储,这就注定它只能用于存储一些简单的信息。如果你在sp中存储了大量数据,那么在获取值时可能会阻塞线程,严重会引起界面卡顿,十分影响性能;而且在解析数据时会产生大量临时对象,频繁触发GC也会造成内存抖动,甚至内存泄漏等不良后果。



3. 内存对象序列化

序列化:将对象的状态信息转换为可以存储或传输形式的过程。在Android开发过程中,不同的activity之间可以通过Intent来传输数据,但是只限于Java中的八大数据类型,在此之外的一些复杂数据类型需要使用对象序列化。(在此并不详细介绍概念、用法,只是提示一些注意点)

(1)Serializable
Serializable是来自Java中的序列化接口,它在序列化的时候会产生大量的临时变量从而引起频繁的垃圾回收,GC频繁操作会引起UI卡顿、内存抖动,甚至OOM。

(2)Parcelable
Parcelable是Android系统中的一种序列化方式,特点是在使用内存时,它比Serializable方式更好,但是它有个明显的缺点:Parcelable不能序列化磁盘上存储的数据。Parcelable的本质是为了更好实现对象在进程间通信传递,它其实并不是一个通用的序列化机制,主要使用场合便是Android中的进程间通信。如果考虑稳定因素,还是Serializable方式更加安全。

(3)总结

  • Serializable是Java的序列化方式,Parcelable是Android特有的序列化方式

  • 在使用内存时,ParcelableSerializable性能高。

  • Serializable在序列化的时候会产生大量临时变量,频繁触发GC

  • Parcelable 不能使用在要将数据存储在磁盘上的情况



如有错误还请指正~

希望对你们有帮助 :)

猜你喜欢

转载自blog.csdn.net/ITermeng/article/details/72772593