性能优化-内存优化

8-《内存优化》

  • 一.基础知识
    • 1.Java的内存分配区域
    • 2.Java的引用类型
    • 3.Java的垃圾回收机制:三个问题
    • 4.Android的内存管理机制
  • 二. Android的内存泄漏、内存溢出、内存抖动概念
    • 0.内存泄露
    • 1.内存溢出![在这里插入图片描述](https://img-blog.csdnimg.cn/8b73ef844f2647009f9448703674bc9c.png)
    • 2.**内存抖动**
    • 3.常见的内存泄漏现象
  • 三. 工具
    • 1.Profiler
    • 2.MAT
    • 3. Leak Canary
    • 4. Leak Canary源码分析,面试会问
    • 5. 三种工具如何选择
  • 四. 写优质的代码
    • 1、关闭无用的Service
    • 2.多进程WebView的优化
    • 3.Glide内存泄漏问题
    • 4.Bitmap造成的内存泄漏(重中之重)

一.基础知识

1.Java的内存分配区域

(1)程序计数器:当前线程的字节码执行位置的指示器。内存空间小,线程私有。如果线程正在执行一个 Java 方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是 Native 方法,这个计数器的值则为 (Undefined)。此内存区域是唯一 一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 情况的区域。
(2)Java虚拟机栈:线程私有,生命周期和线程一致。描述的Java方法执行的内存模型,每个方法在执行的同时会创建一个栈帧(Stack Frame),存储着局部变量、操作数栈、动态链接和方法出口等。每一个方法从调用直至执行结束,就对应着一个栈帧从虚拟机栈中入栈到出栈的过程。

局部变量表:存放了编译期可知的各种基本类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)

StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度。
OutOfMemoryError:如果虚拟机栈可以动态扩展,而扩展时无法申请到足够的内存。
(3)本地方法栈:区别于 Java 虚拟机栈的是,Java 虚拟机栈为虚拟机执行 Java 方法(也就是字节码)服务,而本地方法栈为虚拟机使用到的Native方法服务。也会有StackOverflowError和 OutOfMemoryError 异常。
(4)Java堆:所有对象实例分配的区域。对于绝大多数应用来说,这块区域是 JVM 所管理的内存中最大的一块。线程共享,主要是存放对象实例和数组。内部会划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer, TLAB)。可以位于物理上不连续的空间,但是逻辑上要连续。
OutOfMemoryError:如果堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出该异常。
(5)方法区:所有已经被虚拟机加载的类的信息、常量、静态变量和即时编辑器编译后的代码数据

上面讲述了Java运行时的内存分配区域,作为Android程序员,我们更多的需要关注栈和堆。不过我们可能会产生这样的疑问,栈和堆有什么区别呢?为什么要同时存在这两块区域?带这样的疑问,我们回过头来再来看看这两个区域。
栈(stack)
栈位于通用RAM中。Java中存在一个虚拟的“栈指针”,“栈指针”若向下移动,则分配新的内存;若向上移动,则释放那些内存。这是一种快速高效的内存分配方式,仅次于寄存器。
这种内存分配方式,决定了在创建程序时候,Java编译器必须知道存储在栈内所有数据的确切大小和生命周期,因为它必须生成相应的代码,以便上下移动“栈指针”。栈区为了快速分配内存,限制了程序的灵活性,所以该区域只存放java基本类型数据和对象、数组的引用,对象本身则存放在堆或常量池中
堆(heap)
堆也位在于通用RAM中,用于存放所有的Java对象。堆与栈的不同之处在于,编译器不需要知道要从堆里分配多少存储区域,也不必知道存储的数据在堆里存活多长时间。
因此,在堆里分配存储有很大的灵活性。当你需要创建一个对象的时候,只需要new写一行简单的代码,当执行这行代码时,会自动在堆里进行内存分配。为了这种灵活性,用堆进行存储分配比用栈进行内存分配需要更多的时间。

2.Java的引用类型

在程序编译完,Jvm虚拟机给每个对象分配完内存后,Java的垃圾回收机制会监控每一个对象在内存中的运行状态,包括对象的申请、引用、被引用、赋值等。当某个对象不再被引用变量所引用时,垃圾回收机制就会将其回收,并释放内存空间。
Java中一个对象可以被一个局部变量所引用,也可以被其他类的静态变量引用,或者被其他对象的实例变量引用。当对象被静态变量引用时,只有该类被销毁,该对象才会被销毁、回收。当对象被其他对象的实例变量引用时,只有当引用该对象的对象被销毁或不再被引用时,该对象才会被销毁、回收。
为了更好的管理对象的引用,JDK中提供了四种引用方式,分别是强引用、软引用、弱引用、虚引用。下面分别介绍这几种引用方式和适用场景

  • 强引用

这是java默认的引用对象方式,例如:

Object object=new Object();

这里的object就是以强引用的方式引用Object对象,被强引用所引用的java对象,即使内存不足时也绝对不会垃圾回收机制回收。

  • 软引用

软引用需要通过SoftReference类实现,例如:

SoftReference<Object> object=new SoftReference<>();

被弱引用所引用的java对象,在内存充足时,它与强引用相同是不会被jvm的垃圾回收机制回收的,但是当系统内存不足时,垃圾回收机制就会将其回收。
在Android中软引用非常常用,例如:从网络中获取的图片,会将其暂时缓存在内存中,当下次再用时就可以直接从内存中,一般为了防止造成内存泄露,会将其设为软引用。

  • 弱引用

弱引用与软引用有些相似,区别在于弱引用所引用的的对象生命周期更短。弱引用通过WeakReference类实现,例如:

WeakReference<Object> wObject=new WeakReference<>(object);

对于弱引用的对象而言,当jvm的垃圾回收机制运行时,不管内存是否足够,总会回收该对象所占用的内存。

  • 虚引用

软引用和弱引用可以单独使用,但是虚引用却不能单独使用,虚引用的主要作用是跟踪对象被垃圾回收的状态。
被虚引用引用的对象本身并没的太大的意义,对象甚至感觉不到引用的存在,使用虚引用的get()方法也总是为空。
在Android开发中此类引用非常少见,故不做过多介绍。

3.Java的垃圾回收机制:三个问题

问题1-谁是垃圾
(1)引用计数算法
每个对象有一个引用计数属性,新增一个引用时计数加1,引用释放时计数减1,计数为0时可以回收。此方法简单,效率很高,但是主流的JVM并没有选用这种算法来判定可回收对象,因为它有一个致命的缺陷,那就是它无法解决对象之间相互循环引用的的问题,对于循环引用的对象它无法进行回收。例:

public class Object {
    public Object instance;
    public static void main(String[] args) {
        // 1
        Object objectA = new Object();
        Object objectB = new Object();
       
        // 2
        objectA.instance = objectB;
        objectB.instance = objectA;
        
        // 3
        objectA = null;
        objectB = null;
    }

程序启动后,objectA和objectB两个对象被创建并在堆中分配内存,这两个对象都相互持有对方的引用,除此之外,这两个对象再无任何其他引用,实际上这两个对象已经不可能再被访问(引用被置空,无法访问),但是它们因为相互引用着对方,导致它们的引用计数器都不为0,于是引用计数算法无法通知GC收集器回收它们。
实际上,当第1步执行时,两个对象的引用计数器值都为1;当第2步执行时,两个对象的引用计数器都为2;当第3步执行时,二者都清为空值,引用计数器值都变为1。根据引用计数算法的思想,值不为0的对象被认为是存活的,不会被回收;而事实上这两个对象已经不可能再被访问了,应该被回收。

(2) 可达性分析算法(根搜索算法)
在主流的JVM实现中,都是通过可达性分析算法来判定对象是否存活的。可达性分析算法的基本思想是:通过一系列被称为"GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots对象没有任何引用链相连,就认为GC Roots到这个对象是不可达的,判定此对象为不可用对象,可以被回收。

在上图中,objectA、objectB、objectC是可达的,不会被回收;objectD、objectE虽然有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。

问题2-如何回收垃圾

(1)标记 -清除算法
标记-清除 算法是最基础的收集算法,它是由 标记 和 清除 两个步骤组成的。标记的过程其实就是上面的 可达性算法(根搜索) 所标记的不可达对象,当所有的待回收的“垃圾对象”标记完成之后,便进行第二个步骤:统一清除
优点:不需要大规模的复制操作,内存利用效率高

缺点:需要遍历两次堆空间,因此会造成应用程序暂停的时间会随着堆内存空间的增大而增大,而且垃圾回收回来的内存往往是不连续的,因此整理后的堆内存里碎片很多。

(2)标记-整理算法
上述的 标记-清除 算法会产生内存区域使用的间断,所以为了将内存区域尽可能地连续使用, 标记-整理 算法应运而生。

标记-整理 算法也是由两步组成,标记 和 整理。


第一步的 标记 动作也是使用的 根搜索算法,但是在标记完成之后的动作却和 标记-清除算法 天壤之别,该算法并不会直接清除掉可回收对象 ,而是让所有的对象都向一端移动,然后将端边界以外的内存全部清理掉。

该算法所带来的最大的优势便是使得内存上面不会再有碎片问题,并且新对象的分配只需要通过简单的指针碰撞便可完成。

(3)复制算法
无论是标记-清除算法还是垃圾-整理算法,都会涉及句柄的开销或是面对碎片化的内存回收,所以,复制算法 出现了。
复制算法将内存区域均分为了两块(记为S0和S1),而每次在创建对象的时候,只使用其中的一块区域(例如S0),当S0使用完之后,便将S0上面存活的对象全部复制到S1上面去,然后将S0全部清理掉。

复制算法的优势是:① 不会产生内存碎片;② 标记和复制可以同时进行;③ 复制时也只需要移动栈顶指针即可,按顺序分配内存,简单高效;④ 每次只需要回收一块内存区域即可,而不用回收整块内存区域,所以性能会相对高效一点。

但是缺点也是很明显的:可用的内存减小了一半,存在内存浪费的情况。

所以 复制算法 一般会用于对象存活时间比较短的区域,例如 年轻代,而存活时间比较长的 老年代 是不适合的,因为老年代存在大量存活时间长的对象,采用复制算法的时候会要求复制的对象较多,效率也就急剧下降,所以老年代一般会使用上文提到的 标记-整理算法。

(4)分代搜集算法(重要)
分代收集算法是目前大部分JVM的垃圾收集器采用的算法。它的核心思想是根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Tenured Generation)和新生代(Young Generation),在堆区之外还有一个代就是永久代(Permanet Generation)。执勤已经说了垃圾回收机制主要负责回收方法区和堆的垃圾。方法区也叫做永久代。Java堆就包括新生代和老年代两部分

工作流程如下:

1.所有新生成的对象都放在Eden,当 Eden区快要满了,触发Minor GC,把存活对象复制到 Survivor0区,清空 Eden 区;
2.Eden区被清空后,继续对外提供堆内存;当 Eden 区再次被填满,又触发Minor GC,对 Eden区和 S0 区同时进行垃圾回收,把存活对象放入 S1区,同时清空 Eden 区和S0区;
3.不断重复上面的步骤,每进行一次垃圾回收存活的对象年龄就会加1,默认临界值为15;
4.当到达临界年龄,对象就会被复制到老年代;
5.当老年代的被占满,无法再进入对象时,就会进行一次Full GC,也就是新生代、老年代都进行回收,这个垃圾回收的时间比较长。

问题3-什么时候回收

JVM在进行GC时,并非每次都对上面三个内存区域一起回收的,大部分时候回收的都是指新生代。因此GC按照回收的区域又分了两种类型,一种是普通GC(minor GC),一种是全局GC(major GC or Full GC),它们所针对的区域如下。普通GC(minor GC):只针对新生代区域的GC。全局GC(major GC or Full GC):针对年老代的GC,偶尔伴随对新生代的GC以及对永久代的GC。由于年老代与永久代相对来说GC效果不好,而且二者的内存使用增长速度也慢,因此一般情况下,需要经过好几次普通GC,才会触发一次全局GC。

4.Android的内存管理机制

在Android系统中每个APP都有一个独立的主进程,系统给每个进程分配的内存是大小在出厂时就被固定了,不同品牌、内存、系统的手机都是不一样的。一般来说,手机的出厂内存越大,系统能分配给每个进程的内存上限就越大。
众所周知,Android 5.0之后,Google给Android系统更换了一个更高效的虚拟机-ART。早期的Dalvik虚拟机仅有一种内存回收算法,对于内存的回收效率也很低。ART虚拟机则根据APP是运行时的不同情况,采用了多种不同的垃圾回收算法,用来高效的回收内存。
Android对于内存回收还存在一套Low Memory Killer的机制,当系统的可用内存出现紧张的时候,这套机制会全局检查所有正在运行的进程,并根据所需要的内存大小,杀死那些权重较低的进程,并回收它的内存。
在Android中按进程的权重从高到低依次分为:前台进程(正在与用户交互),可见进程(不在与用户交互),服务进程,后台进程和空进程。
性能优化-内存优化_第1张图片

熟悉Android内存分配机制的朋友都知道,Android为每个进程分配内存时,采用弹性的分配方式,即刚开始并不会给应用分配很多的内存,而是给每一个进程分配一个“够用”的内存大小。
那Android到底为每个应用分配多少内存呢?我们可以实际测试一下:
以本人手上的努比亚NX510J手机为例:

private void getMaxMemoryInfo(){
	Runtime rt = Runtime.getRuntime();
	long maxMemory = rt.maxMemory();
	Log.e("MaxMemory:", Long.toString(maxMemory/(1024*1024)));
	ActivityManager activityManager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
	Log.e("MemoryClass:", Long.toString(activityManager.getMemoryClass()));
	Log.e("LargeMemoryClass:", Long.toString(activityManager.getLargeMemoryClass()));
}

输出结果为:

06-06 15:27:22.740 11917-11917/com.suning.myapp E/MaxMemory:: 192
06-06 15:27:22.740 11917-11917/com.suning.myapp E/MemoryClass:: 192
06-06 15:27:22.740 11917-11917/com.suning.myapp E/LargeMemoryClass:: 512

把AndroidManifest.xml中的application标签加上

...
android:largeHeap="true"
...>
...

输出结果为:

06-06 15:32:06.168 21973-21973/com.suningtang.myapp E/MaxMemory:: 512
06-06 15:32:06.168 21973-21973/com.suningtang.myapp E/MemoryClass:: 192
06-06 15:32:06.168 21973-21973/com.suningtang.myapp E/LargeMemoryClass:: 512

可以看到,设置largeHeap为true时, 通过rt.maxMemory();获取的值为512M。
因此,对于本人这台手机,系统正常分配的内存最多为192M;当设置largeHeap时,最多可申请512M。当超过这个值时,就会出现OOM了。
这个值是在哪设置的呢?查看/system/build.prop文件内容:

shell@NX510J:/ $ cat /system/build.prop | grep heap
dalvik.vm.heapsize=16m
dalvik.vm.heapstartsize=8m    ----起始分配内存
dalvik.vm.heapgrowthlimit=192m ---- 一般情况app申请的最大内存 dalvik.vm.heapsize=512m   ---- 设置largeheap时,App可用的最大内存dalvik.vm.heaptargetutilization=0.75  ---- GC相关
dalvik.vm.heapminfree=512k
dalvik.vm.heapmaxfree=8m     ----- GC机制相关

getMemoryClass()和getLargeMemoryClass()方法最终读取的仍然是dalvik.vm.heapgrowthlimit和dalvik.vm.heapsize的值。而且,dalvik.vm.heapsize默认值为16M,这也是解释了google的原生OS默认值是16M了。而dalvik.vm.heapgrowthlimit和dalvik.vm.heapsize的值各个手机厂家的OS会对这个值进行修改,所以存在差异。
在App中获取内存信息
我们在应用中可以通过ActivityManager的MemoryInfo内部类获取内存信息,方法如下:

private void getMemoryInfo() {
	ActivityManager manager = (ActivityManager) getSystemService(Context.ACTIVITY_SERVICE);
	ActivityManager.MemoryInfo info = new ActivityManager.MemoryInfo();
	manager.getMemoryInfo(info);
	Log.e("Memory","系统总内存:"+(info.totalMem / (1024*1024))+"M");
	Log.e("Memory","系统剩余内存:"+(info.availMem / (1024*1024))+"M");
	Log.e("Memory","系统是否处于低内存运行:"+info.lowMemory );
	Log.e("Memory","系统剩余内存低于"+( info.threshold  / (1024*1024))+"M时为低内存运行");
	}

二. Android的内存泄漏、内存溢出、内存抖动概念

0.内存泄露

即 ML (Memory Leak),指 程序在申请内存后,当该内存不需再使用但却无法被释放,归还给 程序的现象。对应用程序的影响:容易使得应用程序发生内存溢出,即OOM(out of Memory)
发生内存泄露的本质原因
本质原因:持有引用者的生命周期>被引用者的生命周期
解释:本该回收的对象(该对象已经不再被使用),由于某些原因(如被另一个正在使用的对象引用)不能被回收。

1.内存溢出性能优化-内存优化_第2张图片

2.内存抖动

性能优化-内存优化_第3张图片
优化方案
尽量避免频繁创建大量、临时的小对象
性能优化-内存优化_第4张图片

3.常见的内存泄漏现象

  • 需要回收的对象被静态变量持有

比较典型的例子就是单例中需要传入Context时,我们传入了当前Activity的Context

class Example {
    private static volatile Example ourInstance;

    private Context mContext;

    static Example getInstance(Context context) {
        if (ourInstance == null) {
            synchronized (Example.class) {
                if (ourInstance == null) {
                    ourInstance = new Example(context);
                }
            }
        }
        return ourInstance;
    }

    private Example(Context context) {
        mContext = context;
    }
}

如果我们代码中如果我们传入Activity的Context的那么该Activity占用的内存在app运行周期将无法被回收,具体原因请继续往下看。
这里的Context我们可以用Application的Context替换,因为Application的生命周期就是App的运行周期。

private Example(Context context) {
        mContext = context.getApplicationContext();
}
  • 非静态内部类持有外部类的引用

在java中内部类会隐式持有外部类的引用,一般情况下这并不会造成内存的泄露,但是如果内部类中执行了耗时操作,就有可能会产生内存泄露。

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);
    
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(20000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        thread.start();
    }

上面代码中,Thread隐式持有了Activity类的引用,当Activity退出时,thread依然在后台执行,那么Activity就会因为被后台线程持有而无法正常回收。
上述的例子只是用Thread举例耗时操作,像AsyncTask,Handler等都存在这样的问题,不过随着耗时操作的执行完毕,线程被正常释放,Activity是可以被正常回收的。这种在Activity中使用内部类执行耗时操作的做法本身就是错误的,也有可能导致其它异常情况的缠身,不提倡这种写法。
如果你一定要这么写,可以改成下面的做法:

 static class MyThread extends Thread {

        private SoftReference<Activity> mActivity;

        public MyThread(Activity activity) {
            mActivity = new SoftReference<>(activity);
        }

        @Override
        public void run() {
            super.run();
            try {
                Thread.sleep(20000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

将内部类更改为静态内部类,静态内部类不会持有外部类的引用,需要我们自行传入,这时我们用外部类的引用设置为软引用,这个jvm在做垃圾回收时,就会回收掉内部类对于外部类(Activity)的引用,这样Activity就可以正常销毁了(需要注意一点的是,上述例子是一个在后台执行的线程,即使Activity被回收了,线程本身并不会被回收)。

  • 资源对象未关闭

在使用IO、File流或者Sqlite、Cursor等资源时要及时关闭。这些资源在进行读写操作时通常都使用了缓冲,如果及时不关闭,这些缓冲对象就会一直被占用而得不到释放,以致发生内存泄露。因此我们在不需要使用它们的时候就及时关闭,以便缓冲能及时得到释放,从而避免内存泄露。

  • 属性动画造成内存泄露

动画同样是一个耗时任务,比如在Activity中启动了属性动画(ObjectAnimator),但是在销毁的时候,没有调用cancle方法,虽然我们看不到动画了,但是这个动画依然会不断地播放下去,动画引用所在的控件,所在的控件引用Activity,这就造成Activity无法正常释放。因此同样要在Activity销毁的时候cancel掉属性动画,避免发生内存泄漏
在Android中甚至是Java中,因为代码编写不当,造成内存泄露的地方有很多,这里不再枚举,会在后续的文章中讲解如何监测内存泄露,并一步步还原出内存泄露的原因。

三. 工具

Android的内存分析工具随着时代的进步,一直在不停的推陈出新,这里只挑选了三个常用、易上手且能覆盖大多场景的工具。
性能优化-内存优化_第5张图片

工具的使用以及LeakCanary的源码分析(面试高频点)视频链接

1.Profiler

功能:捕获堆转储/强制执行GC/实时跟踪内存分配
性能优化-内存优化_第6张图片
主要捕获堆转储hprof文件并另存为到电脑上,注意文件的后缀 面试的时候问过
性能优化-内存优化_第7张图片

2.MAT

内存快照对比,更有效率找出内存泄漏的对象,一般会捕获2个堆转储hprof文件,通过对比的结果定位。
看此博客https://juejin.cn/post/6844903910621052942

3. Leak Canary

Leak Canary是大名鼎鼎的Square公司专门为检测Android内存泄漏而开发一个第三方框架。需要注意的是,LeakCanary只能用来监控内存泄漏,它并不支持监控其他的内存问题。
github地址:github.com/square/leak…

LeakCanary的使用
最新版的LeakCanary在使用时,不需要做任何初始化操作,只需要在项目的build.gradle中添加以下依赖即可。

dependencies  { 
  // debugImplementation,因为LeakCanary应该只在调试版本中运行。
  debugImplementation'com.squareup.leakcanary:leakcanary-android:2.0-beta-2' 
}

运行APP后会在手机生成一个Leaks的APP,当在我们在调试集成了LeakCanary的APP时(仅在debug模式下使用),如果检测到内存泄漏时,LeakCanary将自动在手机上显示通知,并将内存泄漏的信息保存在LeaksAPP中。
性能优化-内存优化_第8张图片

排查内存泄漏
当产生内存泄漏后,LeakCanary会给出如下图所示的内存泄漏的引用链。
性能优化-内存优化_第9张图片

泄漏跟踪中的每个节点都是Java对象,可以是类,对象数组或实例。 每个节点都有一个对下一个节点的引用。在UI中,该引用为紫色。
在LeakCanary给出报告中,每一个节点都标识了是否正在发生泄漏,在它后面的括号中还给出相应的解释。

Leaking:YES 正在发生泄漏,
Leaking:NO 没有发生泄漏,
Leaking:UNKNOWN 未知。

大致观察LeakCanary的报告后,我们就需要开始缩小观察范围来确定内存泄漏的原因。
在LeakCanary有这样一条规则,如果一个节点没有泄漏,那么指向它的任何先前引用都不是泄漏源,也不会泄漏。同样,如果一个节点泄漏,那么泄漏跟踪下的任何节点也会泄漏。由此,我们可以推断出内存泄漏原因出现在最后一次Leaking:NO和第一次Leaking:YES之间类中。
在本例中就对应下图的这四个部分,泄漏的原因往往就出这里面。在报告中用红色下波浪线标出来的部分,是LeakCanary认为导致内存泄漏的原因,也是我们接下来要重点排查的地方。

4. Leak Canary源码分析,面试会问

5. 三种工具如何选择

上面介绍了三种各具特点内存分析与检测工具,在实际的开发中,我们应该根据项目组对于内存关注度的不同,组合使用不同的工具。
小团队
这类型团队是当前国内占比较多的一部分,Android开发组长期只有一两个人,APP的日活跃用户也比较少,却有着大量的需求亟待完成,甚至于需求本身可能都十分模糊。对于这样的团队关注的重心要集中在业务和功能上,如何保证APP不出bug才是重点。
建议:在APP中集成LeakCanary,整理、收集内存泄漏的报告,在空闲时尝试调试内存问题。调试之后一定要做充分测试,防止出现其他bug。

中等规模团队
这类型的团队基本长期都有三个人以上,APP的日活跃用户数比较多。这种团队的leader要适时关注一下APP的使用流畅度,着手解决APP中的内存泄漏、卡顿等问题,并定期发布相关的团队报告,让团队中其他人引以为戒。
建议:在APP中集成LeakCanary,每个开发人员在完成自己开发任务的同时,也要保证自己开发的功能不会出现被LeakCanary捕获的内存泄漏。如果不能根据LeakCanary定位内存泄漏的点,需要进一步使用MAT来排查。
团队的leader在版本发布前,要使用Memory Profiler监测每个新功能的内存时间线,图像的时间线相对平滑则是合格的。如果出现了剧烈波动的锯齿图像,表明出现了内存抖动,要着手修复,保持这样的节奏基本可以避免绝大多数内存方面的性能问题。监测的任务也可以交给团队内的测试人员。

四. 写优质的代码

1、关闭无用的Service

《饭fan》是一个单Activity多Fragment的APP,在App的入口Activity同时启动了两个Service,TinkerService用于检查热修复补丁,UpdateService用于检查是否有更新。
在操作APP一段时间后,使用Memory Profiler检查内存,得到下图
性能优化-内存优化_第10张图片

可以看到内存中依然存在TinkerService和UpdateService。没有特殊指定的service是运行在主线程中的,这些已经无用的Service会拖慢主线程并占据主进程的可用内存。
解决方案:
调用stopService或stopSelf关闭这些service
关闭service后再次使用Memory Profiler检查内存,可以看到,APP占用的总内存已经减少了

性能优化-内存优化_第11张图片

2.多进程WebView的优化

WebView应该是Android中最容易发生内存泄漏的系统组件,往往都是Activity退出时,WebView依然持有activity的引用,导致Activity发生泄漏。 网络上有很多如何防止WebView产生泄漏,但是效果都不好,有的甚至根本没有效果。
解决方案

让持有webview的Activity独立运行在一个进程,在activity的onDestroy中关闭这个进程

让Activity独立运行在一个进程中,可以彻底清除掉webview以及Activity
,但是让持有webview的Activity独立在一个进程中,会产生另一个问题——长时间的白屏。
webview本身初始化以及载入Html页面都需要一定的时间,这段时间会产白屏。
如果在启动Activity时需要额外再创建一个进程,那么白屏的时间就会进一步拉长,有时甚至长达4-5秒。可以下面解决

1.在app启动时,同时启动一个ShoppingService。ShoppingService运行在与WebViewActivity相同的进程中,退出WebViewActivity后当前进程会被关闭,在适当时候再重启ShoppingService。
2.引入腾讯的x5WebView和VasSonic,加快webview初始化速度,同时也提高了WebView在各个系统上兼容性。
3.在webview初始化的同时,使用APP内网络框架来请求Html页面中所需的数据。通过并行的方式,节省webview的加载数据的时间。

优化步骤大致就是以上这些,具体实现的代码请参考《饭fan》中Component_shopping组件。

3.Glide内存泄漏问题

https://www.coder.work/article/670496

4.Bitmap造成的内存泄漏(重中之重)

在Android内存优化中有“一图毁十优”的说法,一般普通的内存泄漏浪费的内存都在几十KB到几MB之间,但是一个bitmap泄漏就有可能浪费几十MB的内存空间,所以bitmap的优化一直是Android内存优化的重中之重。所以我们接下来的就重点介绍Bitmap的优化方案。
1.使用RGB_565解码图片

在开发中大多数的图片加载框架的默认解码方案是ARGB_8888,这种解码方案,每个像素占4个字节,其实还有一种图片解码方案是RGB_565,这种解码方案,每个像素占2个字节,但是在视觉效果上与ARGB_8888差距并不明显。
所以一些页面的缩略图、背景图片以及一些用户感官上认为它就是缩略图的地方可以使用RGB_565来解码,在减小内存占用上,有立竿见影的效果,强烈推荐使用。

2.不要乱放图片

在开发中我们往往会要求美工一张图标切3到5套不同尺寸的,然后分别放置在res下不同的资源目录里面
性能优化-内存优化_第12张图片

Android有一套特殊的适配策略,对放在mipmap目录的图标会忽略屏幕密度,会去尽量匹配大一点的,然后系统自动对图片进行缩放,从而优化显示和节省资源。图片的缩放比率=手机的dpi / mipmap目录的dpi。
放在drawable目录下的会根据ROM的不同得到一个默认的dpi,但是这个dpi并一定是手机屏幕的实际dpi。
例如:如果我们将一张500X500的图标仅放在ldpi(120)下,那么在在480dpi的手机上实际的显示尺寸是2000X2000。
当我们分不清图标应该放在哪个目录下时,应该尽量将高品质的图片放在高密度目录下,这样能控制图片的缩放比率小于1,保证画质的前提,内存也是可控的。

3.控制那些不可控的图片

这是什么意思呢,举一个我曾经实际遇到的例子,我们的APP有一个课件的功能,允许教师上传课件,服务器会把这些课件转成图片返回给APP显示,有个老师上传了一篇PDF格式的论文,服务器转换后每个图片足足有4000X8000这么大,加载每张图片需要消耗内存4000X8000X4/1024/1204=122MB,直接导致了OOM。
在这个例子中教师上传的课件转换后的图片就属于不可控图片,如果服务器不做过滤,那么APP就需要对这些用户上传的图片特殊处理。
处理步骤如下:

  • 从服务器下载的图片获取它的高度和宽度
  • 对于高度或宽度大于手机屏幕尺寸的图片计算缩放比率,并做缩放解码,其实就是二次采样
  • 要对所有的图片解码API(decodexxxx)做OutOfMemoryError的异常处理

4.Bitmap的二次采样

你可能感兴趣的:(性能优化,性能优化,jvm,java)