内存优化小结

1. Android内存分析方向

Java中的内存泄露主要特征:可达,无用
无用指的是创建了但是不再使用之后没有释放
能重用但是却创建了新的对象进行处理

2. 常见内存泄漏查找

Context 泄漏, 主要为Activity 传递泄漏, context 未使用applciationConext 在单例创建时。
Handler 泄漏 , handler中持有view ,context 等做耗时操作。
Cursor 泄漏 , cursor未关闭
register 未 unregister
Bitmap
adapter 未使用convertView
不良代码等

3. 优化方案

    1. 谨慎使用服务

        离开了 APP 还在运行服务是最糟糕的内存管理错误之一,当 APP 处在后台,我们应该停止服务,除非它需要运行的任务。
        我们可以使用 JobScheduler 替代实现,JobScheduler 把一些不是特别紧急的任务放到更合适的时机批量处理。
        如果必须使用一个服务,最佳方法是使用 IntentService ,限制服务寿命,所有请求处理完成后,IntentService 会自动停止。

    2. 使用优化的数据容器

        考虑使用优化过数据的容器 SparseArray / SparseBooleanArray / LongSparseArray 代替 HashMap 
        等传统数据结构,通用 HashMap 的实现可以说是相当低效的内存,因为它需要为每个映射一个单独的条目对象。

    3. 避免在 Android 上使用枚举
        枚举往往需要两倍多的内存,静态常量更多,我们应该严格避免在 Android 上使用枚举。

    4. 使用ProGuard来剔除不需要的代码
        使用 ProGuard 来剔除不需要的代码,移除任何冗余的,不必要的,或臃肿的组件,资源或库完善 APP 的内存消耗。

    5. 优化布局层次

        merge标签

        merge可以用来合并布局,减少布局的层级。merge多用于替换顶层FrameLayout或者include布局时,
        用于消除因为引用布局导致的多余嵌套。
        注意:merge标签常用于减少布局嵌套层次,但是只能用于根布局。

        ViewStub标签

        推迟创建对象、延迟初始化,不仅可以提高性能,也可以节省内存(初始化对象不被创建)。Android定义了ViewStub类,
        ViewStub是轻量级且不可见的视图,它没有大小,没有绘制功能,也不参与measure和layout,资源消耗非常低。

        App里常见的视图如蒙层、小红点,以及网络错误、没有数据等公共视图,使用频率并不高,如果每一次都参与绘制其实
        是浪费资源的,都可以借助ViewStub标签进行延迟初始化,仅当使用时才去初始化。

        include标签
        include标签和布局性能关系不大,主要用于布局重用,一般和merge标签配合使用

        越扁平化的视图布局,占用的内存就越少,效率越高。我们需要尽量保证布局足够扁平化,当使用系统提供的View
        无法实现足够扁平的时候考虑使用自定义View来达到目的。

    6. 如果需要使用依赖注入,使用 Dagger 2依赖注入
        如果打算在您的 APP 使用依赖注入框架,可以考虑用 Dagger 2 ,Dagger 不使用反射扫描 APP 的代码,
        Dagger 是静态的,意味着它 编译时不需要占用运行 Android 应用或内存的使用。

    7. 减小Bitmap对象的内存占用
        Bitmap是内存消耗的大头,当使用时要及时回收。另外配置:
        inSampleSize:缩放比例,图片按需加载,避免不必要的大图载入。
        decode format:解码格式,选择ARGB_8888/RBG_565/ARGB_4444/ALPHA_8,存在很大差异。

    8. 使用更小的图片

        在设计给到资源图片的时候,我们需要特别留意这张图片是否存在可以压缩的空间,是否可以使用一张更小的图片。
        尽量使用更小的图片不仅仅可以减少内存的使用,还可以避免出现大量的InflationException。假设有一张很大的图片
        被XML文件直接引用,很有可能在初始化视图的时候就会因为内存不足而发生InflationException,这个问题的根本
        原因其实是发生了OOM。

    9. 内存对象的重复利用
        大多数对象的复用,最终实施的方案都是利用对象池技术,要么是在编写代码的时候显式的在程序里面去创建对象池,
        然后处理好复用的实现逻辑,要么就是利用系统框架既有的某些复用特性达到减少对象的重复创建,从而减少内存的分配与回收。

        1. 复用系统自带的资源
            Android系统本身内置了很多的资源,例如字符串/颜色/图片/动画/样式以及简单布局等等,这些资源都可以
            在应用程序中直接引用。这样做不仅仅可以减少应用程序的自身负重,减小APK的大小,另外还可以一定程度上
            减少内存的开销,复用性更好。但是也有必要留意Android系统的版本差异性,对那些不同系统版本上表现存在很大差异,
            不符合需求的情况,还是需要应用程序自身内置进去。
        2. 注意在ListView/GridView等出现大量重复子组件的视图里面对ConvertView的复用

        3. Bitmap对象的复用
            在ListView与GridView等显示大量图片的控件里面需要使用LRU的机制来缓存处理好的Bitmap

        4. 避免在onDraw方法里面执行对象的创建
            类似onDraw等频繁调用的方法,一定需要注意避免在这里做创建对象的操作,因为他会迅速增加
            内存的使用,而且很容易引起频繁的gc,甚至是内存抖动。

        5. 避免在onDraw方法里面执行对象的创建
            StringBuilder
            在有些时候,代码中会需要使用到大量的字符串拼接的操作,这种时候有必要考虑使用StringBuilder来替代频繁的“+”

    10. Cursor关闭
        如查询数据库的操作,使用到Cursor,也要对Cursor对象及时关闭。

    11. 监听器的注销 / 常见广播接受者的注册与注销
        onCreate register  onDestory unregister,自己手动add的listener,需要记得及时remove这个listener。

    12. 注意Activity的泄漏

        1) 内部类引用导致Activity的泄漏

            最典型的场景是Handler导致的Activity泄漏,如果Handler中有延迟的任务或者是等待执行的任务队列过长,
            都有可能因为Handler继续执行而导致Activity发生泄漏。此时的引用关系链是Looper -> MessageQueue -> 
            Message -> Handler -> Activity。为了解决这个问题,可以在UI退出之前,
            执行remove Handler消息队列中的消息与runnable对象。或者是使用Static + WeakReference
            的方式来达到断开Handler与Activity之间存在引用关系的目的。

        2) Activity Context被传递到其他实例中,这可能导致自身被引用而发生泄漏。

            考虑使用Application Context而不是Activity Context
            对于大部分非必须使用Activity Context的情况(Dialog的Context就必须是Activity Context),
            我们都可以考虑使用Application Context而不是Activity的Context,这样可以避免不经意的Activity泄露。

        3)注意临时Bitmap对象的及时回收

        虽然在大多数情况下,我们会对Bitmap增加缓存机制,但是在某些时候,部分Bitmap是需要及时回收的。
        例如临时创建的某个相对比较大的bitmap对象,在经过变换得到新的bitmap对象之后,应该尽快回收原始的bitmap,
        这样能够更快释放原始bitmap所占用的空间。

    13. 注意WebView的泄漏

    14. 资源文件需要选择合适的文件夹进行存放
        我们知道hdpi/xhdpi/xxhdpi等等不同dpi的文件夹下的图片在不同的设备上会经过scale的处理。
        例如我们只在hdpi的目录下放置了一张100100的图片,那么根据换算关系,xxhdpi的手机去引用那张图片就会
        被拉伸到200*200。需要注意到在这种情况下,内存占用是会显著提高的。对于不希望被拉伸的图片,
        需要放到assets或者nodpi的目录下

    15. Try catch某些大内存分配的操作

        在某些情况下,我们需要事先评估那些可能发生OOM的代码,对于这些可能发生OOM的代码,加入catch机制,
        可以考虑在catch里面尝试一次降级的内存分配操作。例如decode bitmap的时候,catch到OOM,可以尝试把
        采样比例再增加一倍之后,再次尝试decode。

    16. 谨慎使用static对象

        因为static的生命周期过长,和应用的进程保持一致,使用不当很可能导致对象泄漏,在Android中应该谨慎使用static对象。

    17. 谨慎使用“抽象”编程

        很多时候,开发者会使用抽象类作为”好的编程实践”,因为抽象能够提升代码的灵活性与可维护性。
        然而,抽象会导致一个显著的额外内存开销:他们需要同等量的代码用于可执行,
        那些代码会被mapping到内存中,因此如果你的抽象没有显著的提升效率,应该尽量避免他们。

    18. 谨慎使用多进程

         使用多进程可以把应用中的部分组件运行在单独的进程当中,这样可以扩大应用的内存占用范围,
        但是这个技术必须谨慎使用,绝大多数应用都不应该贸然使用多进程,一方面是因为使用多进程会使得
        代码逻辑更加复杂,另外如果使用不当,它可能反而会导致显著增加内存。当你的应用需要运行一个
        常驻后台的任务,而且这个任务并不轻量,可以考虑使用这个技术。

        一个典型的例子是创建一个可以长时间后台播放的Music Player。如果整个应用都运行在一个进程中,
        当后台播放的时候,前台的那些UI资源也没有办法得到释放。类似这样的应用可以切分成2个进程:
        一个用来操作UI,另外一个给后台的Service。

    19. 谨慎使用第三方libraries

        很多开源的library代码都不是为移动网络环境而编写的,如果运用在移动设备上,并不一定适合。
        即使是针对Android而设计的library,也需要特别谨慎,特别是在你不知道引入的library具体做了什么事情的时候。

    20. 考虑不同的实现方式来优化内存占用

4. Bitmap如何复用

    1. 使用LruCache和DiskLruCache做内存和磁盘缓存;
    2. 使用Bitmap复用,同时针对版本进行兼容。   

    BitmapFactory.Options options = new BitmapFactory.Options();
    // 图片复用,这个属性必须设置;
    options.inMutable = true;
    // 手动设置缩放比例,使其取整数,方便计算、观察数据;
    options.inDensity = 320;
    options.inTargetDensity = 320;
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.resbitmap, options);
    // 对象内存地址;
    Log.i(TAG, "bitmap = " + bitmap);
    Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());

    // 使用inBitmap属性,这个属性必须设置;
    options.inBitmap = bitmap;
    options.inDensity = 320;
    // 设置缩放宽高为原始宽高一半;
    options.inTargetDensity = 160;
    options.inMutable = true;
    Bitmap bitmapReuse = BitmapFactory.decodeResource(getResources(), R.drawable.resbitmap_reuse, options);
    // 复用对象的内存地址;
    Log.i(TAG, "bitmapReuse = " + bitmapReuse);
    Log.i(TAG, "bitmap:ByteCount = " + bitmap.getByteCount() + ":::bitmap:AllocationByteCount = " + bitmap.getAllocationByteCount());
    Log.i(TAG, "bitmapReuse:ByteCount = " + bitmapReuse.getByteCount() + ":::bitmapReuse:AllocationByteCount = " + bitmapReuse.getAllocationByteCount());

5. 正确的异步姿势

    1. Thread:适合处理单个任务的场景

    new Thread(){
        @Override
        public void run() {
            super.run();
            //数据库/网络操作
        }
    }.start();

    缺点:
    创建及销毁线程消耗性能较大;
    缺乏统一的管理;
    优先级与UI线程一致,抢占资源处于同一起跑线;
    匿名内部类默认持有外部类的引用,有内存泄漏的风险;
    需要自己处理线程切换。

    备注:此种姿势最好不要使用,特定场景下(例如App启动阶段为避免在主线程创建线程池的资源消耗)使用的话务必加上优先级的设置。
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);

    2. AysncTask:适合处理单个任务的场景

    优点:

    创建异步任务变得更加简单,同时屏蔽了线程切换;
    在AsyncTask中异步线程的优先级已经被默认设置成了:THREAD_PRIORITY_BACKGROUND,不会与UI线程抢占资源;

    缺点:

    AsyncTask默认是串行的,用于多线程场景下的话需要调用其重载方法executeOnExecutor()传入自定义的线程池,并且自己处理好同步问题;
    匿名内部类默认持有外部类的引用,有内存泄漏的风险。   

    3. HandlerThread :串行场景,并在构造方法中明确指定优先级,适合串行处理多任务的场景。

    通过HandlerThread可以创建一个带有looper的线程,引入了Handler、Looper、MessageQueue等概念,可以实现对工作线程的调度。

    使用

    HandlerThread handlerThread = new HandlerThread("network request", Process.THREAD_PRIORITY_BACKGROUND);
    handlerThread.start();

    Handler handler = new Handler(handlerThread.getLooper()){
        @Override
        public void handleMessage(Message msg) {
            // network request
        }
    };

    优点:

    串行执行,没有并发带来的问题;
    不退出的前提下一直存在,避免线程相关的对象频繁重建和销毁造成的资源消耗。

    缺点:

    串行执行(不同的视角优点也变缺点),并发场景下无能为力;
    不指定优先级的情景下默认优先级为THREAD_PRIORITY_DEFAULT,与UI线程同级别。

    4. IntentService:Service与HandlerThread的组合,内部的工作线程以及调度机制都依赖于HandlerThread,适合处理与UI无关的多任务场景。

    IntentService是继承于Service并处理异步请求的一个类,在IntentService内有一个工作线程来处理耗时操作,
    启动IntentService的方式和启动传统Service一样,同时,当任务执行完后,IntentService会自动停止,
    而不需要我们去手动控制。另外,可以启动IntentService多次,而每一个耗时操作会以工作队列的方式
    在IntentService的onHandleIntent回调方法中执行,并且,每次只会执行一个耗时操作,依次执行。

    优势:

    同HandlerThread的优势;
    开启服务,进程优先级会提升;
    无需手动关闭,执行完之后自动结束。


    5. ThreadPoolExecutor

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

    优势:

    线程的创建和销毁由线程池维护,一个线程在完成任务后并不会立即销毁,而是由后续的任务复用这个线程,从而减少线程的创建和销毁,节约系统的开销;
    线程池旨在线程的复用,这就可以节约我们用以往的方式创建线程和销毁所消耗的时间,减少线程频繁调度的开销,从而节约系统资源,提高系统吞吐量;
    在执行大量异步任务时提高了性能;
    Java内置的一套ExecutorService线程池相关的api,可以更方便的控制线程的最大并发数、线程的定时任务、单线程的顺序执行等。

    注意事项:
    使用线程池需要特别注意同时并发线程数量的控制。因为CPU只能同时执行固定数量的线程数,一旦同时并发的
    线程数量超过CPU能够同时执行的阈值,CPU就需要花费精力来判断到底哪些线程的优先级比较高,在不同的线程之间进行调度切换。

    每开一个新的线程,都会耗费至少64K以上的内存。线程池中存在了过多的并发数量不仅会影响CPU的调度时间而且会减少可用内存;

    线程的优先级具有继承性,在某线程中创建的线程会继承此线程的优先级。那么我们在UI线程中创建了线程池,
    其中的线程优先级是和UI线程优先级一样的;所以仍然可能出现20个同样优先级的线程平等的和UI线程抢占资源。

6. 总结套路:

    1. 解决所有的内存泄漏

    集成LeakCanary,可以方便的定位出90%的内存泄漏问题;
    2. 通过反复进出可疑界面,观察内存增减的情况,Dump Java Heap获取当前堆栈信息使用MAT进行分析。
    内存泄漏的常见情形可参照《Android 内存泄漏分析心得》
    3. 避免内存抖动

    4. 避免在循环中创建临时对象;
    避免在onDraw中创建Paint、Bitmap对象等。
    5. Bitmap的使用

    6. 正确使用异步操作

    7. 使用三方库加载图片一般不会出内存问题,但是需要注意图片使用完毕的释放,而不是被动等待释放。
    8. 使用优化过的数据结构

    9. 使用onTrimMemory根据不同的内存状态做相应处理
    10. Library的使用
    去掉无用的Library,对生成的Apk进行反编译查看使用到的Library,避免出现无用的Lib仍然被打进Apk,避免引入巨大的Library11.使用Proguard进行混淆、压缩。

参考地址

双十二技术哥

Android性能优化(十一)之正确的异步姿势

猜你喜欢

转载自blog.csdn.net/rjgcszlc/article/details/78592589
今日推荐