JAVA

内存优化

2019-11-22  本文已影响0人  ArcherZang

为什么要进行内存优化:
APP运行内存限制,OOM导致APP崩溃。
APP性能:流畅性、响应速度和用户体验,因为GC回收系统会暂停一段时间。
减少内存占用,提高应用后台运行时的存活率。

分析内存常用工具:

top/procrank---当前cpu执行状态和进程使用内存状态/所有进程内存使用状况
meinfo---dumpsys
DDMS---Allocation Tracker已放弃修复
MAT
Finder-Activity
LeakCanary
LeakInspector
TraceView
Android Profiler的使用---本人Copy官网Profiler内存使用介绍
Lint(代码扫描工具,可帮助您发现并更正代码结构质量的问题,而无需您实际执行应用,也不必编写测试用例。)
StrictMode(只能检测磁盘、网络访问运行在UI线程和某些方法很慢在UI线程,activity泄漏、SQL未关闭、资源未关闭、BroadcastReceiver未取消以及某个特定对象超过限定值)

Android内存管理
  1. Android平台运行的前提是可用内存是浪费的内存。它试图在任何时候使用所有可用的内存。例如,当用户在APP之间切换时,Android会在最近使用的(LRU)缓存中保留不在前台的APP,即用户看不到的APP,或运行类似音乐播放的前台服务。如果用户稍后返回APP,系统将重用该进程,从而使APP切换更快。

  2. Android设备包含三种不同类型的内存:RAM、zRAM和storage。
    注意:CPU和GPU都访问同一个RAM。

    • RAM是最快的内存类型,但通常大小有限。高端设备通常具有更大的RAM容量。
    • zRAM是用于交换空间的RAM分区。所有内容在放入zRAM时压缩,然后在从zRAM复制时解压缩。当页面移入或移出zRAM时,RAM的这一部分的大小会增大或减小。设备制造商可以设置最大尺寸。
      Storage包含所有持久性数据,如文件系统和所有app、库和平台所包含的代码。存储器的容量比其他两种存储器大得多。在Android上,存储不像在其他Linux实现中那样用于交换空间,因为频繁的写入会导致内存磨损,并缩短存储介质的寿命。
  3. 内存共享(Share memory)
    为了满足RAM的所有需求,Android尝试共享RAM来跨进程通信。它可以做到以下方式:

    • 每个APP都是从一个叫做Zygote的现有进程中fork来的。当系统启动并加载通用framework代码和资源(如Activity themes)时,Zygote进程开启。系统fork Zygote进程后启动新进程,在新进程中加载并运行APP的代码。这种方法允许分配给框架代码和资源的大部分RAM页在所有APP进程中共享。
    • 大多数静态数据都映射到一个进程中。这种技术允许在进程之间共享数据,并允许在需要时将其调出。静态数据示例包括:Dalvik代码(通过将其放置在预链接的.odex文件中进行直接mmapping)、APP资源(通过将资源表设计为可以mmapped和对齐的zip实体APK的结构)和传统项目元素(如.so文件中的native代码)。
    • 在许多地方,Android使用显式分配的共享内存区域(ashmem或gralloc)来跨进程共享相同的动态RAM。例如,window surfaces在app和屏幕合成器之间使用共享内存,而光标缓冲区在content provider和client之间使用共享内存。
      由于共享内存的广泛使用,确定app使用的内存量需要小心。在调查RAM使用情况时,需要使用适当的技术。
  4. 系统级内存回收
    Linux内核保持低和高的可用内存阈值。当空闲内存低于低阈值时,kswapd开始回收内存。当空闲内存达到高阈值,kswapd将停止回收内存。
    系统 Framework 层根据不同组件声明周期或者进程状态通知AMS,按照进程类型,动态分配不同的 adj 值,并且在一定的时机会对所有进程的 adj 进行更新;
    更新 adj 时,AMS会和 lmkd 守护进程进行socket 通信,设置/proc/pid/oom_score_adj;ActivityManagerService在运行时会根据系统的当前配置通知lmkd修正 lmk driver 的参数。
    kswapd无法为系统释放足够的内存。在这种情况下,系统使用onTrimMemory()通知APP内存不足,应该减少其分配。如果这还不够,内核将开始终止进程以释放内存,
    LowMemoryKiller注册了shrinker(Linux Kernel的一个内存管理工具),当kernel需要回收内存时,会回调LowMemoryKiller的lowmem_shrink(),它先检查kernel 剩下多少内存,根据剩下的内存数量来匹配数组 lowmem_minfree[], 找到数组索引值,然后,再使用该索引值,从 lowmem_adj[]这个数组里面就得到目标oom_adj值会被转成oom_score_adj ,最终,在大于等于该目标oom_score_adj 的进程中,杀死拥有最大oom_score_adj 值的进程--send_sig(SIGKILL, selected, 0) 。
    当内存耗尽的时候,OOMKiller会调用 out_of_memory()来select_bad_process(), oom_score最大的值就是那个将要被杀死的bad process。 oom_badness()以oom_score_adj作为基础值,根据是否为管理员进程,占用的内存情况,来计算出最终的oom_score值,分值越高,越容易被杀死。

  5. 当oom_adj = 15, 则 oom_score_adj = 1000;
    当oom_adj < 15, 则 oom_score_adj = oom_adj * 1000/17;

    ADJ Description
    HIDDEN_APP_MAX_ID=15 当前只运行了不可见的Activity组件的进程
    HIDDEN_APP_MIN_ID=9 当前只运行了不可见的Activity组件的进程
    SERVICE_B_ADJ=8 B list of service。和A list相比,他们对用户的黏合度要小一些,长时间未使用的service进程。
    PREVIOUS_APP_ADJ=7 用户前一次交互的进程。按照用户的使用习惯,人们经常会在几个常用进程间切换,所以这类进程得到再次运行的概率比较大
    HOME_APP_ADJ=6 Launcher进程,它对于用户的重要性不言而喻
    SERVICE_ADJ=5 当前运行了application service的进程
    HEAVY_WEIGHT_APP_ADJ=4 重量级APP进程,P以上版本才能配置: android:cantSaveState="true"
    BACKUP_APP_ADJ=3 用于承载backup相关操作的APP进程
    PERCEPTIBLE_APP_ADJ=2 这类进程能被用户感觉到但不可见,如后台运行的音乐播放器
    VISIBLE_APP_ADJ=1 前台APP启动的一些可见的组件的进程,如: 弹出的Email activity
    FOREGROUND_APP_ADJ=0 当前正在前台运行的进程,也就是用户正在交互的那个程序
    PERSISTENT_PROC_ADJ=-12 Persistent性质的进程,如电话、短信、热点、wifi
    SYSTEM_ADJ=-16 系统进程
    NATIVE_ADJ=-17 native进程(不被系统管理)
java虚拟机

Android Runtime(ART)和Dalvik虚拟机使用分页(Paging)和内存映射(mmapping)来管理内存。应用程序通过分配新对象或触摸已映射页面来修改内存都将保留在RAM中,并且不能被调出。应用程序释放内存的唯一方式是垃圾收集器。
Android对每个APP的堆大小设置了硬件限制。
具体的堆大小限制因设备的总体可用RAM大小而异。

  1. Java虚拟机运行时数据区域

    • 程序计数器(Program Counter Register)
      当前线程执行到的字节码的行号指示器,是一块较小的内存空间,每条线程独立存储或不干扰。
      每个线程私有。
      不会抛出OutOfMemoryError。
      执行native方法时值为空。
    • java虚拟机栈(Java Virtual Machine Stacks)
      java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储方法运行时的基础数据结构包括局部变量表、操作数栈、动态链接、方法出口等信息;每个方法从调用到执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
      每个线程私有,生命周期与线程相同。
      会抛出OutOfMemoryError(虚拟机栈动态扩展时没有足够的内存,即不断新建线程)和StackOverflowError(虚拟机栈深度大于允许值,通常1000---2000)。
    • 本地方法栈(Native Method Stack)
      与java虚拟机栈类似,为虚拟机使用到的Native方法服务。
      每个线程私有。
      会抛出OutOfMemoryError和StackOverflowError。
      部分虚拟机将本地方法栈和java虚拟机栈合二为一。
    • Java堆(Java Heap)
      存放对象实例,几乎所有的对象实例都是在这里分配内存包括数组。
      所有线程共享的一块内存区域,随虚拟机启动时创建。
      会抛出OutOfMemoryError(没有内存分配实例,并且无法再扩展时)。
      可以处于物理上不连续的内存空间,只要逻辑上连续,既可以是固定大小,也可以是可扩展的。
    • 方法区(Method Area)别名非堆(No-Heap)或永久代(Permanment Generation)
      用于存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
      所有线程共享。
      会抛出OutOfMemoryError(无法满足内存分配需求时)。
      可以处于物理上不连续的内存空间,只要逻辑上连续,既可以是固定大小,也可以是可扩展的;同时可以选择不实现垃圾收集。
      • 运行时常量池(Runtime Constant Pool)时方法区的一部分
        主要用于存放编译期生成的各种字面量和符号引用(在类加载后进入常量池),还有翻译出来的直接引用和运行期间产生的新常量。
        会抛出OutOfMemoryError(无法满足内存分配需求时)。
        其他介绍
    • 直接内存(Direct Memory)
      使用Native函数库直接分配的堆外内存。通过java堆上的DirectByteBuffer对象作为这块内存的引用进行操作。
      会抛出OutOfMemoryError(当各个内存区域总和大于物理内存限制包括物理的(RAM、SWAP、分页文件)和操作系统的限制,从而导致动态扩展失败)。
  2. 对象可回收判断

    • 引用计数算法
      给对象添加一个引用计数器,每当有一个地方引用它时计数器值加1;当引用失效时,计数器值减1;任何时刻计数器值为0时对象就不可能再被使用。
      Person a = new Person();
      Person b = new Person();
      a.instance = b;
      b.instance = a;
      a = null;
      b = null;
      System.gc();
      //对象被回收,如果引用计数就会永远不为0,导致无法回收。
      
    • 可达性分析算法
      通过一系列的"GC Roots"的对象作为起始点,从这些节点开始向下搜索,当一个对象与"GC Roots"没有任何引用链相连,证明此对象是不可用的。(也称GC Roots到这个对象不可达)。
      引用链:搜索所走过的路径
      GC Roots:虚拟机栈中的引用对象(栈帧中的本地变量表),
      方法区中类静态属性引用的对象和常量引用的对象,
      本地方法栈中JNI引用的对象(native方法)。
  3. 回收区域
    虚拟机栈和本地方法栈自动释放,所以只会回收方法区和java堆

  4. 垃圾收集---垃圾回收器算法
    一个托管内存环境(如ART或Dalvik虚拟机)保持跟踪每个内存分配。一旦确定程序不再使用一块内存,它就会将其释放回堆(heap)中,无需程序员的干预。在托管内存环境中回收未使用内存的机制称为垃圾回收。垃圾收集有两个目标:在程序中查找未来无法访问的数据对象;回收这些对象使用的资源。

    • 垃圾回收算法种类
      标记清除算法
      复制算法(从分两块到分三块,默认8:1 Eden和Survivor,每次只使用Eden和一个Survivor,另一个用于复制,当复制的空间不够就进入老年代)
      标记---整理算法
      分代收集算法


      垃圾回收算法.png
    • 垃圾回收器是垃圾回收算法的实现

    • Android的内存堆是分代的(因为分代收集算法)
      意味着它会根据所分配对象的预期寿命和大小跟踪不同的区域。例如,最近分配的对象属于年轻一代(Young generation)。当一个对象保持足够长的活动时间时,它可以被提升到老年代(older generation),然后是永久代(permanent generation)。
      不同的区域可以使用不同的垃圾回收器

      新生代GC(Minor GC):指发生在新生代的垃圾回收动作。回收速度比较快
      老年代GC(Major GC/ Full GC):指发生在老年代的垃圾回收动作,看垃圾回收器实现,有时会伴随Minor GC。Major GC的速度一般会比Minor GC慢10倍以上。

内存泄漏

产生的原因:一个长生命周期的对象持有一个短生命周期对象的引用
通俗讲就是该回收的对象,因为引用问题没有被回收,最终会产生OOM
常见发生:

  1. 单例造成的内存泄露
    单例的静态特性导致其生命周期同应用一样长。
    解决方案:
    • 将该属性的引用方式改为弱引用;例如传入View,
    • 如果传入Context,使用ApplicationContext;
      泄漏代码片段:
public class Singleton {
    private static Singleton mInstance;    

    public static getInstance() {
       if (mInstance == null) {            
           synchronized (Singleton.class) {                
               if (mInstance == null) {
                    mInstance = new Singleton();
                }
            }
        }
        return mInstance ;        
    }
  
    public Singleton (View  view) {
        ...
    }
}
/**
修改后
*/
   private WeakReference mWR;
    public Singleton (View  view) {
            mWR = new WeakReference<View>(scrolledView);
    }
  1. InnerClass内部类
    在Java中,非静态内部类 和 匿名类 都会潜在的引用它们所属的外部类,但是,静态内部类却不会。如果这个非静态内部类实例做了一些耗时的操作,就会造成外围对象不会被回收,从而导致内存泄漏。

    • 普通的内部类,监听、callback、动态广播注册,被外部持有
    • Handler造成的内存泄漏(将Handler类独立出来或者使用静态内部类)
    • 线程造成的内存泄漏(将AsyncTask和Runnable类独立出来或者使用静态内部类)
    • bindService造成的内存泄漏

    解决方案:
    (1)将内部类变成静态内部类;或将该内部类抽取出来封装成一个单例
    (2)如果需要使用Context,就使用Application的Context
    (3)如果有强引用Activity或者Activity中的属性,则将引用方式改为弱引用;
    (4) 在业务允许的情况下,在Activity的Destroy或者Stop以及View的onDetachedFromWindow时,应该移除消息队列中的消息,避免Looper线程的消息队列中有待处理的消息需要处理。 mHandler.removeCallbacksAndMessages(null)或者handlerThread.getLooper().quit()
    (5) 同上结束或者取消线程和动画这些耗时任务。
    (6)同上移除监听或者清除回调、unregisterReceiver、unbindService。
    (7) 同上 RxJava 取消任务
    (8)同上Coroutine取消任务

    public class LeakAct extends Activity {  
       @Override
       protected void onCreate(Bundle savedInstanceState) {    
           ...
       } 
       public void test() {    
           new Thread(new Runnable() {      
               @Override
                public void run() {        
                   ...
                }
            }).start();
        }
    }
    /**
    修改后
    */
       public void test() {    
            final TRunnable t = new TRunnable();
            SingletonThreadPool.getInstance.execute(t);
       }
       private static class TRunnable(){
           @Override
            public void run() {        
               ...
            }
        }
    
  2. View也存在内部类问题,一般情况下我们不会在View内部bind service或者注册动态广播。

  3. activity或者view使用不当
    将activity或者view传入其他对象方法或者静态属性,因为其他对象生命周期长而造成内存泄漏。
    解决方案:
    (1)尽量不要把与activity相关的对象写出static,如button drawable
    (2)使用ApplicationContext代替ActivityContext,因为ApplicationContext会随着应用程序的存在而存在,而不依赖于activity的生命周期;
    (3)尽量不要把activity和view作为参数传递使用,如果有需要使用弱引用

  4. 资源对象未关闭
    资源性对象比如 Cursor、Stream、Bitmap等使用了缓冲,在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。
    它们的缓冲不仅存在于 Java 虚拟机内,还存在于 Java 虚拟机外。如果我们仅仅是把它的引用设置为 null,而不关闭它们,往往会造成内存泄露。虽然有些资源性对象,比如 SQLiteCursor在析构函数finalize()会调 close() 关闭,但是这样的效率太低。
    解决方案:
    不使用的时候,立即调用它的 close()或者recycle() 函数,将其关闭掉,然后再置为 null。

  5. WebView造成的泄露
    当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露。
    解决方案:
    为webView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

内存抖动

内存频繁的分配与回收,分配速度大于回收速度时没有内存可分配会OOM,或者因为内存不连续导致大对象没有足够空间的内存会OOM。
内存抖动会带来UI的卡顿,因为GC回收会暂停所有线程工作。频繁的GC回收会占用大量的帧绘制时间,从而导致UI卡顿
解决方案:
尽量避免频繁创建大量、临时的小对象

  1. for循环内部创建对象或者字符串加号拼接(一次拼接多一个临时对象StringBuilder,一个对象 = 对象头 + 成员属性,对象头 = MardWord + Klass= 12个字节 )
    解决方案:
    (1)创建对象移到循环外
    (2)循环外创建一个stringbuilder对象然后每次append
    2.(1) 构造ListAdapter时,没有使用缓存的ConvertView
        初始时ListView会从Adapter中根据当前的屏幕布局实例化一定数量的View对象,同时ListView会将这些View对象缓存起来。
        getView()的第二个形参View ConvertView就是被缓存起来的List Item的View对象(初始化时缓存中没有View对象则ConvertView是null)。
      (2)构造RecyclerView.Adapter 时不要在onBindViewHolder中inflate view,因为每显示一条新item就会执行一次。在onCreateViewHolder中inflate view,在onBindViewHolder中绑定数据。
优化内存方式

减少内存泄漏和内存抖动,减少内存消耗;降低UI卡顿和内存OOM发生率,提高应用存活率。

  1. 小心使用代码抽象
    因为抽象可以提高代码的灵活性和维护性。然而,抽象需要相当多的代码来执行,需要更多的时间和内存才能将代码映射到内存中。因此,如果你的抽象并没有带来显著的好处,你应该避免它们。
    在没有 JIT 的设备上,通过具有确切类型的变量来调用方法的确比通过接口进行调用更高效。(因此,例如,在 HashMap map 上调用方法比在 Map map 上成本更低,尽管在这两种情况下,映射都是 HashMap。)实际差异只是慢一点,例如 6%。此外,JIT 也让二者在效率方面难以区分。
    在没有 JIT 的设备上,缓存字段访问的速度比重复访问字段快约 20%。如果有 JIT,则字段访问的成本与本地访问大致相同。

  2. 数据类型
    不要使用比需求更占空间的基本数据类型

  3. static和staticfinal的问题
    static会由编译器调用调用类初始化器的clinit方法进行初始化
    static final会进入dex文件中静态字段初始化器,并不会在类初始化申请内存(没有final会通过字段查询)
    此优化仅适用于原语类型和 String 常量,不适用于任意引用类型。

    static int intVal = 42;
    static String strVal = "Hello, world!";
    

    编译器会生成一个名为 <clinit> 的类初始化器方法,当第一次使用该类时,系统会执行此方法。此方法会将值 42 存储到 intVal,并从类文件字符串常量表中提取 strVal 的引用。以后引用这些值时,可以通过查询字段访问它们。
    我们可以使用“final”关键字加以改进:
    static final int intVal = 42;
    static final String strVal = "Hello, world!";
    常量会进入 dex 文件中的静态字段初始化器。引用 intVal 的代码将直接使用整数值 42,并且对 strVal 的访问将使用成本相对较低的“字符串常量”引用,而非字段查询。

  4. 避免创建不必要的对象
    创建对象就需要分配内存,其成本总是要高于不分配内存。不管哪种垃圾回收器都会带来停顿,一旦回收频繁就会带来卡顿。
    • 如有一个返回字符串的方法,结果无论如何都会附加到某个 StringBuffer,则更改签名和实现,以便函数直接进行附加,而非创建短期的临时对象。
    • 从一组输入数据中提取字符串时,请尝试返回原始数据的子字符串,而非创建副本。您会创建一个新的 String 对象,但它会与这些数据共享 char[]。(需要权衡的是,如果您只使用原始输入中的一小部分,那么如果您采用这种方法,便会将原始输入全部保留在内存中。)
    • 一个 int 数组比一个 Integer 对象数组好得多,
    两个并行的 int 数组也会比一个 (int,int) 对象数组的效率高得多。
    原语类型的任意组合都是如此。
    • 两个并行 Foo[] 和 Bar[] 数组的效果通常比单个自定义 (Foo,Bar) 对象的数组好很多。(如果 API 以供其他代码访问,那就另当别论了。在这些情况下,通常建议在速度方面做一点妥协,从而实现良好的 API 设计。但在自己的内部代码中,应该尝试让代码尽可能高效。)

  5. 对于私有内部类,考虑使用包访问权限,而非私有访问权限
    私有内部类可以访问外部类私有方法:
    问题在于,虚拟机认为从 Foo$Inner 直接访问 Foo 的私有成员不符合规则,因为 FooFoo$Inner 属于不同的类,虽然 Java 语言允许内部类访问外部类的私有成员。为了消除这种差异,编译器会生成一些合成方法:

    /*package*/ static int Foo.access$100(Foo foo) {
        return foo.mValue;
    }
    /*package*/ static void Foo.access$200(Foo foo, int value) {
        foo.doStuff(value);
    }
    

    每当需要访问外部类中的 mValue 字段或调用外部类中的 doStuff() 方法时,内部类代码就会调用这些静态方法。这意味着以上代码实际上可以归结为一种情况,那就是您通过访问器方法访问成员字段。之前我们讨论了访问器的速度比直接访问字段要慢,因此这是一个特定习惯用语会对性能产生“不可见”影响的示例。
    如果您在性能关键位置 (hotspot) 使用这样的代码,则可以将内部类访问的字段和方法声明为拥有包访问权限(而非私有访问权限),从而避免产生相关开销。遗憾的是,这意味着同一软件包中的其他类可以直接访问这些字段,因此不应在公共 API 中使用此方法。

  6. 字符串的连接尽量少用加号(+)或者改用stringbuild

  7. 数组,链表,栈,树,图。。。。。。
    HashMap实现的内存效率很低,因为它需要为每个映射提供一个单独的实体对象。
    数据量千级以内可以使用
    Android框架包括几个优化的数据容器,包括SparseArray、SparseBooleanArray、LongSparseArray、ArrayMap。例如,SparseArray类增删改查的性能不如HashMap但节约内存,因为它们避免了系统自动封装key或者value(这会为每个实体创建另一个或两个对象)。
    HashMap(说明):首先它是一个大数据,当出现hashcode冲突,就是对冲突那一栏是链表的方式处理,如果链表达到阈值就使用二叉树存储;它的数组上会有很多空的栏位。
    SparseArray(说明):两个数组一个key,一个value;
    如有必要,切换到原始数组以获得真正精简的数据结构。key二分查找+比大小找到位置,如果空key就进入;不为空大于它的全部向后移动,然后放入;一定是放满后再扩容。

  8. 集合、数组循环 (Android在2.2的时候引入JIT)
    (1)不要将array.length加入循环,每循环一次都会获取一次length增加开销
    (2)ArrayList 迭代考虑使用手写计数循环,因为比foreach快3倍,不管有没有JIT
    (3)对于其他实现了Iterable 接口的集合以及数组,增强型 for 循环语法与使用显式迭代器完全等效。尽量使用增强型 for 循环,少用iterator, 自动装箱的核心就是把基础数据类型转换成对应的复杂类型。在自动装箱转化时,都会产生一个新的对象,会产生更多的内存和性能开销。如int只占4字节,而Integer对象有16字节,特别是HashMap这类容器,进行增、删、改、查操作时,都会产生大量的自动装箱操作。
    检测方式:使用TraceView查看耗时,如果发现调用了大量的integer.value,就说明发生了AutoBoxing。
    (4)没有JIT的设备,增强型 for 循环语法最快比手写快除了ArrayList。

  9. 枚举优化
    (1)每一个枚举值都是一个单例对象,在使用它时会增加额外的内存消耗,所以枚举相比与 Integer 和 String 会占用更多的内存(运行时的内存分配,一个enum值的声明会消耗至少20bytes。在Android平台上,枚举的内存开销是直接定义常量的三倍以上。)
    (2)较多的使用 Enum 会增加 DEX 文件的大小,会造成运行时更多的IO开销,使我们的应用需要更多的空间(使用枚举类型的dex size是普通常量定义的dex size的13倍以上)
    (3)特别是分dex多的大型APP,枚举的初始化很容易导致ANR
    (4)如果使用枚举是为了类型安全,Android提供了注解的方式检查类型安全。目前提供了int型和String型两种注解方式:IntDef和StringDef,用来提供编译期的类型检查。(注意:使用IntDef和StringDef需要在Gradle配置中引入相应的依赖包:compile 'com.android.support:support-annotations:22.0.0')

    枚举可以进行改进
    public enum SHAPE{
        RECTANGLE,
        TRIANGLE,
        SQUARE,
        CIRCLE
    }
    public class Shape {
    
        public static final int RECTANGLE=0;
        public static final int TRIANGLE=1;
        public static final int SQUARE=2;
        public static final int CIRCLE=3;
    
        private int model;
    
        @Documented
        @IntDef(flag=true,value = {RECTANGLE,TRIANGLE, SQUARE, CIRCLE})
        @Target({ElementType.PARAMETER, ElementType.FIELD, ElementType.METHOD})
        @Retention(RetentionPolicy.CLASS)
        public @interface Model{
    
        }
    
        public void setModel(@Model int model){
            this.model = model;
        }
    }
    
  10. 重复申请内存的问题
    (1)同一个方法多次调用,如递归函数 ,回调函数中new对象,
    (2)在循环中new对象如读流
    (3)在onMeause() onLayout() onDraw() 中new对象,然后刷新UI(requestLayout)

  11. 内存复用,避免GC回收将来要重用的对象

    • 资源复用:通用的字符串、颜色定义、简单页面布局的复用。
    • 线程池:public ThreadPoolExecutor(int corePoolSize,
      int maximumPoolSize,
      long keepAliveTime,
      TimeUnit unit,
      BlockingQueue<Runnable> workQueue,
      ThreadFactory threadFactory,
      RejectedExecutionHandler handler)
      尽量使用线程池去跑任务,而不是动不动就先 new Thread 去跑,这样子线程是得不到复用的。当任务量一大,使用线程池的效率会超乎你想象(具体自己看源码),毕竟 开启一个线程 cpu 内存都是有开销的。
      这里推荐 Rxjava 的第三方库,
    • ConnectionPool 缓存池
      ConnectionPool 缓存池 :复用 tcp socket 套接字,进行网络通讯,每一次 HTTP 请求结束后,并不结束链接,可复用于下次的请求。把网络传输速度极致化。
      一次 http 请求分:
      tcp 三次握手
      数据传输
      tcp 四次分手
    • okio SegmentPool (buffer 复用池)
    • 对象池:android v4 包,最好是创建比较费时的大对象时使用,如果是太简单的对象,再进入池化的时间比自己构建还多。记得用完得归还。
    • Bitmap对象的复用:使用inBitmap属性可以告知Bitmap解码器尝试使用已经存在的内存区域,新解码的bitmap会尝试使用之前那张bitmap在heap中占据的pixel data内存区域。
    • LRU算法---LruCache
      最近最少使用缓存,使用强引用保存需要缓存的对象,它内部维护了一个由LinkedHashMap组成的双向列表,不支持线程安全,LruCache对它进行了封装,添加了线程安全操作。当其中的一个值被访问时,它被放到队列的尾部,当缓存将满时,队列头部的值(最近最少被访问的)被丢弃,之后可以被GC回收。
      (除了普通的get/set方法之外,还有sizeOf方法,它用来返回每个缓存对象的大小。此外,还有entryRemoved方法,当一个缓存对象被丢弃时调用的方法,当第一个参数为true:表明环处对象是为了腾出空间而被清理时。否则,表明缓存对象的entry被remove移除或者被put覆盖时。)
      注意:综合考虑设备内存阈值与其他因素设计合适的缓存大小
      (1) 应用程序剩下了多少可用的内存空间?
      (2) 有多少图片会被一次呈现到屏幕上?有多少图片需要事先缓存好以便快速滑动时能够立即显示到屏幕?
      (3)设备的屏幕大小与密度是多少? 一个xhdpi的设备会比hdpi需要一个更大的Cache来hold住同样数量的图片。
      (4)不同的页面针对Bitmap的设计的尺寸与配置是什么,大概会花费多少内存?
      (5)页面图片被访问的频率?是否存在其中的一部分比其他的图片具有更高的访问频繁?如果是,也许你想要保存那些最常访问的到内存中,或者为不同组别的位图(按访问频率分组)设置多个LruCache容器。
  12. 资源对象未关闭
    资源性对象比如 Cursor、Stream、Bitmap等使用了缓冲,在不使用的时候,应该及时关闭它们,以便它们的缓冲及时回收内存。
    它们的缓冲不仅存在于 Java 虚拟机内,还存在于 Java 虚拟机外。如果我们仅仅是把它的引用设置为 null,而不关闭它们,往往会造成内存泄露。虽然有些资源性对象,比如 SQLiteCursor在析构函数finalize()会调 close() 关闭,但是这样的效率太低。
    解决方案:
    不使用的时候,立即调用它的 close()或者recycle() 函数,将其关闭掉,然后再置为 null。

  1. Activity组件泄漏

    1. 将内部类变成静态内部类;或将该内部类抽取出来封装成一个单例
      非静态内部类和匿名内部类会持有activity引用
    2. 如果需要使用Context,就使用Application的Context
    3. 如果有强引用Activity或者Activity中的属性,则将引用方式改为弱引用;
    4. 不要把Activity有关联的对象写成static如private static Button btn; private static Drawable drawable;
    5. 在业务允许的情况下,在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中有待处理的消息需要处理。 mHandler.removeCallbacksAndMessages(null)或者handlerThread.getLooper().quit()
    6. 同上结束或者取消线程和动画这些耗时任务。
    7. 同上移除监听或者清除回调、unregisterReceiver、unbindService。
    8. 同上 RxJava 取消任务
    9. 同上Coroutine(kotlin)取消任务
  2. View泄漏
    同上,需要在onDetachedFromWindow(onAttachedToWindow是加入窗口)方法中做结束或者取消线程和动画、移除监听或观察者、清除回调,一般情况下我们不会在View内部bind service或者注册动态广播。

  3. ListView/RecyclerView

    • ListView/GridView:
      (1)ConvertView复用
      (2)每次item回收再利用都会重新绑定数据(因为view携带旧数据),只能在ImageView的onDetachFromWindow的时候释放掉图片引用。
      (3)scrollingCache设置为flase,默认是true源至AbsListView与每一帧重绘视图相比动画更平滑,但是耗费内存,因为每滑动一点都会建立缓存。
    • RecyclerView:(1)在onCreateViewHolder中inflate view才会复用View,在onBindViewHolder中绑定数据。
      (2)Item级释放
      每次item被回收刚不可见时是放进mCacheView中,超出2个后0index的被回收进mRecyclePool中后拿出来复用才会重新绑定数据,因此重写Recycler.Adapter中的onViewRecycled()方法来使item被回收进RecyclePool的时候去释放图片引用。
          显示itemView,的onViewAttachedToWindow中做一些注册工作;
          销毁itemView,的onViewDetachedFromWindow中做一些解注册和释放资源的工作。
      在RecyclerView正常滚动时,这两个方法都会被调用。然而页面退出时,onViewDetachedFromWindow并不会被调用,RecycleView.onDetachedFromWindow分发到子LayoutManager的onDetachedFromWindow中做了拦截,我们需要调用setRecycleChildrenOnDetach(true)才能实现在页面退出时,依然调用onViewDetachedFromWindow方法。
      (3)Adapter级释放:
      新设置的Adapter会调用onAttachedToRecyclerView,做一些注册工作
      原有的Adapter会调用onDetachedFromRecyclerView,做一些解注册和其他资源回收的操作。
      记得离开页面调用setAdapter(null),才会调用最新的dapater的onDetachedFromRecyclerView
      (4)RecyclerView级释放:
      显示RecyclerView,onAttachedToWindow中做一些注册工作;
      销毁RecyclerView,onDetachedFromWindow中做一些解注册和释放资源的工作。

    ViewGroup的属性android:animationCache启用动画缓存会消耗更多内存,需要更长的初始化时间,也有可能会频繁GC,但是会提供更好的性能。
    尽可能减少List Item的Layout层次
    在快速滑动时要取消加载图片,并且取消线程任务;一般都会有线程池,排队会导致停下来后加载很长时间才能加载完成,并会产生大量无用对象;不用线程池会瞬间产生大量对象并会导致GC频繁回收。

  4. 优化布局层次,减少内存消耗
    越扁平化的视图布局,占用的内存就越少,效率越高。我们需要尽量保证布局足够扁平化,当使用系统提供的View无法实现足够扁平的情况下适当考虑使用自定义View来达到目的。

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

  6. Try catch某些大内存的分配的操作,可以考虑在catch里面尝试一次降级的内存分配操作
    加载Bitmap:缩放比例、解码格式、局部加载

  7. 服务完成任务后停止服务。否则,可能会无意中导致内存泄漏。
    通常应该避免使用持久性服务,如果需要建议您使用另一种实现,如JobScheduler。
    如果必须使用服务,限制服务使用寿命的最佳方法是使用intent service,它在处理完后立即结束。
    当启动一个服务时,系统希望始终保持该服务的进程运行。这种行为使得服务进程非常昂贵,因为服务使用的RAM对其他进程不可用,而且持续占有。这减少了系统可以保存在LRU缓存中的缓存进程的数量,从而降低了应用程序切换的效率。当内存不足且系统无法维护足够的进程来承载当前运行的所有服务时,它甚至会导致系统中的混乱。

  8. WebView造成的泄露
    当我们不要使用WebView对象时,应该调用它的destory()函数来销毁它,并释放其占用的内存,否则其占用的内存长期也不能被回收,从而造成内存泄露。
    解决方案:
    为webView开启另外一个进程,通过AIDL与主线程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。


    webview.PNG
  1. 谨慎使用多进程
    使用多进程可以把应用中的部分组件运行在单独的进程当中,这样可以扩大应用的内存占用范围,但是这个技术必须谨慎使用,一方面是因为使用多进程会使得代码逻辑更加复杂,另外使用不当,可能反而会导致显著增加内存。当你的应用需要运行一个常驻后台的任务,而且这个任务并不轻量,可以考虑使用这个技术。
    一个典型的例子是创建一个可以长时间后台播放的Music Player。如果整个应用都运行在一个进程中,当后台播放的时候,前台的那些UI资源也没有办法得到释放。类似这样的应用可以切分成2个进程:一个用来操作UI,另外一个给后台的Service。

  2. 谨慎使用large heap
    android设备由于软硬件的差异,heap阀值不同,特殊情况下可以在manifest中使用largeheap=true声明一个更大的heap空间,要谨慎使用,因为额外的空间会影响到系统整体的用户体验,并且会使每次gc的运行时间更长。切换任务时性能也会打折扣,而且large heap并不一定能获取到更大的heap。
    查询内存限制:
    getMemoryClass()是系统为每个应用分配的内存,并没有额外的扩充。
    getLargeMemoryClass()获得应用可使用的最大内存,如果应用设置了largeheap=true。

  3. 选择重写onLowMemory() 与onTrimMemory()方法去释放掉图片缓存、静态缓存来自保。OOM_STORE不仅仅和进程属性有关,也和内存有关。


    onTrimMemory().png
  4. 减小总体APK大小
    通过减小APP的总体大小,可以显著减少APP的内存使用。Bitmap size、 resources、 animation frames和third-party libraries都会影响APK的大小。Android Studio和Android SDK提供了多种工具来帮助减少资源的大小和外部依赖,从代码中删除任何冗余,不必要或臃肿的组件、资源或库。这些工具支持现代代码收缩方法,如R8编译。(Android Studio 3.3和更低版本使用ProGuard而不是R8编译。)

  5. 谨慎使用第三方libraries
    很多开源的library代码都不是为移动网络环境而编写的,如果运用在移动设备上,并不一定适合。即使是针对Android而设计的library,也需要特别谨慎,特别是在你不知道引入的library具体做了什么事情的时候。例如,其中一个library使用的是nano protobufs, 而另外一个使用的是micro protobufs。这样一来,在你的应用里面就有2种protobuf的实现方式。这样类似的冲突还可能发生在输出日志,加载图片,缓存等等模块里面。
    尽管ProGuard可以使用正确的标志帮助删除api和资源,但它不能删除库的大量内部依赖项。这些库中需要的功能可能需要较低级别的依赖项。当您使用库中的acitvity子类(它往往具有广泛的依赖关系)、库使用反射(这是常见的,意味着您需要花费大量时间手动调整ProGuard才能使其工作)等等,这就变得特别有问题。
    另外不要为了1个或者2个功能而导入整个library,如果没有一个合适的库与你的需求相吻合,你应该考虑自己去实现,而不是导入一个大而全的解决方案。
    依赖注入框架,可以减少代码编写,也会面临上面的问题,也要注意运行时框架比编译时框架需要更多的CPU周期和RAM,并且APP启动时明显迟缓,选择合适的很重要。

  6. 使用lite protobufs序列化数据,根据实际情况使用非必须。 IM 系统建议考虑。
    Protocol buffers是Google设计的一种语言中立、平台中立、可扩展的用于序列化类似于XML的结构化数据,但它更小、更快、更简单。使用它,需要客户端代码中使用lite protobufs。常规protobuf会生成非常详细的代码,这可能会导致应用程序中出现许多问题,例如RAM使用增加、APK大小显著增加和执行速度减慢。

    • XML、JSON、Protobuf 都具有数据结构化和数据序列化的能力
    • XML、JSON 更注重 数据结构化,关注人类可读性和语义表达能力。Protobuf 更注重 数据序列化,关注效率、空间、速度,人类可读性差,语义表达能力不足

    protobuf提供了nano、lite、java三个版本供Android选用。
    其实三者是按照库大小依次增大的,理所当然他们支持的protobuf特性也是依次增加的。
    官方推荐Android使用的是lite库,因为其支持大部分特性,包体积又比较小,而且因为没有用到反射,所以不需要处理混淆问题。


    nihao.png
  7. 优先使用库代码(而不是自行编写)
    系统可以自由地用手动汇编替换对库方法的调用,这可能比 JIT 能够为等效 Java 生成的最佳代码效果更好。这种情况的典型示例是 String.indexOf() 以及相关 API,Dalvik 会使用内嵌的内建函数替换它们。同样,在具有 JIT 的 Nexus One 上,System.arraycopy() 方法的速度比手动编码的循环快约 9 倍。

  8. 谨慎使用原生方法
    使用 Android NKD 利用原生代码开发应用不一定比使用 Java 语言编程更高效。首先,Java-原生转换存在一定的成本,并且 JIT 无法在这些范围外进行优化。如果您要分配原生资源(原生堆上的内存、文件描述符或任何其他元素),那么安排对这些资源进行及时回收就可能会困难得多。您还需要针对要在其中运行的每个架构编译代码(而非依赖于其有 JIT)。您可能还需要为您认为相同的架构编译多个版本:为 G1 中的 ARM 处理器编译的原生代码无法充分利用 Nexus One 中的 ARM,而为 Nexus One 中的 ARM 编译的代码也无法在 G1 中的 ARM 上运行。
    原生代码主要适用于您想要将现有原生代码库移植到 Android 的情况,而不适用于对 Android 应用中使用 Java 语言编写的部分进行“加速”。

  9. Bitmap问题(待处理)

    • 矢量图替代bitmap
      适用于简单的图片,更小的内存消耗,更小的物理消耗;具体参考
    • 颜色替代纯色图片

关于Bitmap内存分配的变化
Android 3.0之前 Bitmap对象存在Java堆 像素数据存在Native内存中
不手动调用recycle Bitmap Native内存的回收完全依赖finalize 时机不太可控
Android 3.0~Android 7.0 Bitmap对象和像素数据统一放到Java堆中 不调用recycle Bitmap内存也会是随对象一起被回收
不过Bitmap消耗内存太大 还是不合适
Android 8.0 利用NativeAllocationRegistry实现像素数据存放到Native中 并且新增了硬件位图 Hardware Bitmap 可以减少图片内存并且提升绘制效率
NativeAllocationRegistry:
一种实现 可以将 Bitmap内存放到 Native中 也可以做到和对象一起快速释放 同时GC的时候也能考虑到这些内存防止被滥用
误区二: Native内存不用管
将图片的内存放到 Native 中
// 步骤一:申请一张空的 Native Bitmap
Bitmap nativeBitmap = nativeCreateBitmap(dstWidth, dstHeight, nativeConfig, 22);

// 步骤二:申请一张普通的 Java Bitmap
Bitmap srcBitmap = BitmapFactory.decodeResource(res, id);

// 步骤三:使用 Java Bitmap 将内容绘制到 Native Bitmap 中
mNativeCanvas.setBitmap(nativeBitmap);
mNativeCanvas.drawBitmap(srcBitmap, mSrcRect, mDstRect, mPaint);

// 步骤四:释放 Java Bitmap 内存
srcBitmap.recycle();
srcBitmap = null;

最好感言

内存不是占用越少越好,系统内存足时多用一些以获得更好的性能;
内存不足时希望用时分配,及时释放。

设计风格很大程度上会影响到程序的内存与性能,相对来说,如果大量使用类似Material Design的风格,不仅安装包可以变小,还可以减少内存的占用,渲染性能与加载性能都会有一定的提升。

上一篇下一篇

猜你喜欢

热点阅读