android优化-内存优化

android优化-内存优化

Android小彩虹2021-08-24 23:48:34220A+A-

背景

Android 为每一个 App 分配了一个进程,Android 针对为每个应用分配的堆大小设置了硬性限制,Android 会限制每个应用的堆大小,如果在达到堆的上限之后,还再尝试分配内存,此时会发生 OutOfMemoryError(内存溢出)。

1.1 获取内存


ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)
activityManager.getMemoryClass();

1.2 设置大小的地方

AndroidRuntime.cpp 中 getMemoryClass 就是对应的 heapgrowthlimit 这个值

    parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m");
    parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m");

    parseRuntimeOption("dalvik.vm.heapgrowthlimit", heapgrowthlimitOptsBuf, "-XX:HeapGrowthLimit=");
    parseRuntimeOption("dalvik.vm.heapminfree", heapminfreeOptsBuf, "-XX:HeapMinFree=");
    parseRuntimeOption("dalvik.vm.heapmaxfree", heapmaxfreeOptsBuf, "-XX:HeapMaxFree=");
    parseRuntimeOption("dalvik.vm.heaptargetutilization",
                       heaptargetutilizationOptsBuf,
                       "-XX:HeapTargetUtilization=");

1.3 查看应用堆内存

可以通过 ActivityManager.MemoryInfo 对象查看 当前的可用内存,最大内存,以及 低内存的阈值

ActivityManager activityManager = (ActivityManager) this.getSystemService(ACTIVITY_SERVICE);
ActivityManager.MemoryInfo memoryInfo = new ActivityManager.MemoryInfo();
 // 可用内存
 memoryInfo.availMem;
 // 低内存的阈值
memoryInfo.threshold;
// 总内存
memoryInfo.totalMem;

二 Android内存分配与回收机制

2.1 JVM的内存模型

方法区

它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。1.8之前 更愿意把方法区称为“永久代”,1.8之后永久代移除了,变成了元空间,永久代 和 元空间 都是 JVM 方法区规范的一种实现而已,JDK1.7(包括)之后将常量池从永久代(PermGen)中移动到Java堆内存中了,1.8 永久代删除换成元数据

本地方法栈

本地方法栈跟 Java 虚拟机栈的功能类似,Java 虚拟机栈用于管理 Java 函数的调用,而本地方法栈则用于管理本地方法的调用。但本地方法并不是用 Java 实现的,而是由 C 语言实现的,各版本虚拟机自由实现 ,HotSpot直接把本地方法栈和虚拟机栈合二为一

java堆 又称GC堆

是Java虚拟机所管理的的内存中最大的一块。Java堆是被所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例都在这里分配内存,从内存回收的角度来看,由于现在收集器基本采用分代收集算法,所以Java堆中还可以细分为:新生代和老年代;再细致一点的有Eden空间、From Survivor空间、To Survivor空间等,成员变量就是放在这里的, JDK1.7(包括)之后将常量池从永久代(PermGen)中移动到Java堆内存中了

虚拟机栈

线程私有的,它的生命周期与线程相同。虚拟机栈描述的是Java方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧(Stack-Frame)用于存储局部变量表、操作数栈、动态链接、方法出口等消息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。间,也会oom

程序计数器

此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。它可以看作是当前线程所执行的字节码的行号指示器。class 是用javap命令反编译,可以看出来 每行的序号。主要是用来记录当前线程的位置,当线程切换时,来记录改线程的下一条运行的指令。 这里解释一下为什么每个线程都需要一个线程计数器,JVM的多线程是通过线程轮流切换分配执行时间来实现的,在任何时刻,每个处理器都只会执行一个线程中的指令,当线程进行切换的时,为了线程能恢复当正确的位置,所以每个线程必须有个独立的线程计数器,这样才能保证线程之间不互相影响

2.2 对象的生命周期

在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。上面的这7个阶段,构成了 JVM中对象的完整的生命周期。下面分别介绍对象在处于这7个阶段时的不同情形

创建阶段(Creation)

创建阶段为对象分配内存,调用构造函数,初始化属性。当对象创建后会存储在JVM的heap堆中等待使用,先看是否符合逃逸分析,再看是否开启了线程分配缓冲池,再判断是否是大对象。 据统计 90%的都会用一次,new 出来之后,gc 时就会回收 所以,新对象先放到 edog,回收一次,有可能都回收了,如果没回收进入 form区,开始正规的垃圾回收,form to form to

应用阶段(Using)

至少又一个强引用指向的对象被称为处于使用阶段

不可视阶段(Invisible)

引用对象超出其作用域后就变为不可见的,方法内的对象,在方法外是不可见的

不可到达阶段(Unreachable)

对象处于不可达阶段是指该对象不再被 GC root 引用所持有

可收集回收阶段(Collected)

当垃圾回收器发现该对象已经处于“不可达阶段”而且垃圾回收器已经对该对象的内存空间又一次分配做好准备时,则对象进入了“收集阶段”。假设该对象已经重写了finalize()方法,则会去运行该方法的终端操作

终结阶段(Finalized)

当对象运行完finalize()方法后仍然处于不可达状态时,则该对象进入终结阶段。在该阶段是等待垃圾回收器对该对象空间进行回收。

释放阶段或者再利用(Free)

垃圾回收器对该对象的所占用的内存空间进行回收或者再分配了,则该对象彻底消失了,称之为“对象空间又一次分配阶段”。

2.2 可以作为gc root

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象。

  • 方法区中静态类属性引用的对象。

  • 方法区中常量引用的对象。

  • 本地方法栈中JNI(即一般说的Native方法)引用的对象

2.3 垃圾回收算法

具体的GC可以查看Android GC 原理探究

标记-清理算法(Mark and Sweep GC)

用于老年代、永久代,(扫描两边,第一遍 标记,第二遍清除)先标记处所有的该回收的,然后清除,内存空间不连续,所以当有12个地址值的时候,

  • 假如这里有90%的需要回收,那太麻烦了,假如有10%回收还好点 所以,标记清除 适合老年代 和 永久代
  • 造成内存空间不连续,也就是内存碎片

标记-整理算法 (Mark-Compact)

用于老年代、永久代,先需要从根节点开始对所有可达对象做一次标记,但之后,它并不简单地清理未标记的对象,而是将所有的存活对象压缩到内存的一端。之后,清理边界外所有的空间。这种方法既避免了碎片的产生,又不需要两块相同的内存空间,因此,其性价比比较高,

  • 涉及到了对象移动,地址引用需要更新,用户线程需要暂停,整体效率偏低

复制算法

新生代:它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了, 就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉

  • 效率问题:在对象存活率较高时,复制操作次数多,效率降低;
  • 空间问题:內存缩小了一半;需要額外空间做分配担保(老年代) 这是标准的,但是jvm还有个eden区 8:1:1 以前的利用率是50%,现在有了eden区,和 一个from区,相当于利用率达到90%

2.4 Dalvik虚拟机的GC

主流的大部分Davik采取的都是标记-清理(Mark and Sweep)回收算法,也有实现了拷贝GC的,这一点和HotSpot是不一样的,Dalvik虚拟机的GC 会 Stop The World,这个时候整个程序的线程就会挂起,并且虚拟机内部的所有线程也会同时挂起,等待GC结束。在内存紧张的时候就会频繁执行GC,也就是STW这个动作,这样就会造成丢帧,界面卡顿的现象

2.5 ART的GC

ART运行时内部使用的Java堆的主要组成包括Image Space、Zygote Space、Allocation Space和Large Object Space四个Space,Image Space用来存在一些预加载的类, Zygote Space和Allocation Space与Dalvik虚拟机垃圾收集机制中的Zygote堆和Active堆的作用是一样的,Art在GC上不像Dalvik仅有一种回收算法,Art在不同的情况下会选择不同的回收算法,比如Alloc内存不够的时候会采用非并发GC,而在Alloc后发现内存达到一定阀值的时候又会触发并发GC。同时在前后台的情况下GC策略也不尽相同。后台GC的时候,不会Stop The World,Art相对Dalvik内存分配的效率提高了10倍,GC的效率提高了2-3倍。

三 进程优先级

当手机不能为系统释放足够的内存。在这种情况下,系统会使用 onTrimMemory() 通知应用内存不足,应该减少其分配量。如果这还不够,内核会开始终止进程以释放内存。它会使用低内存终止守护进程 (LMK) 来执行此操作

  • 后台应用:之前运行过且当前不处于活动状态的应用。LMK 将首先从具有最高 oom_adj_score 的应用开始终止后台应用。终止的顺讯是下面的

  • 上一个应用:最近用过的后台应用。上一个应用比后台应用具有更高的优先级(得分更低),因为相比某个后台应用,用户更有可能切换到上一个应用。

  • 主屏幕应用:这是启动器应用。终止该应用会使壁纸消失。

  • 服务:服务由应用启动,可能包括同步或上传到云端。

  • 可觉察的应用:用户可通过某种方式察觉到的非前台应用,例如运行一个显示小界面的搜索进程或听音乐。

  • 前台应用:当前正在使用的应用。终止前台应用看起来就像是应用崩溃了,可能会向用户提示设备出了问题。

  • 持久性(服务):这些是设备的核心服务,例如电话和 WLAN。

  • 系统:系统进程。这些进程被终止后,手机可能看起来即将重新启动。

  • 原生:系统使用的极低级别的进程(例如,kswapd)

四 内存泄漏 以及 检查

4.1 内存泄漏的几个场景

  1. 资源性对象未关闭

File,Cursor,Stream当不用的时候及时关闭 2. 单例模式的Context引起的内存泄漏 假如有个单利需要传入Context,我们最好使用 Application ,不要使用 Activity 或者 Service的。可以避免泄漏 3. 非静态内部类创建静态实例引起的内存泄漏 非静态的内部类会自动持有外部类的引用,创建的静态实例就会一直持有的引用,可以考虑把内部类声明为静态的

  1. Handler其实跟上面的一样,非静态的Handler 会引用到外部类的引用,如果用静态Handler的话,会引起 一连串的 静态属性。所以可以记住RxJava

  2. 对象没有反注册的,比如 BroadCastReciver 、 EventBus 、 RxJava的 Disposable.dispose

  3. WebView的泄漏,有的人解决办法是开新进程,但是我也没看到那个几个app开启了新的进程呀(待解决)

  4. 集合对象没有及时清理引起的内存泄漏

  5. 线程造成的内存泄漏以及Runable

4.2 检测内存泄漏

4.2.1 MAT

官方文档

4.2.2 Android Studio的 profiler

官网文档

4.2.3 LeakCanary

前两个都是可视化的,当时一般的时候,我们用 LeakCanary 来检测内存泄漏,原理是

  1. 初始化:直接debugImplementation就能实现,他是在 ContentProvider里面做的初始化,当打包的时候,会合并各个清单文件。里面注册的 ContentProvider,ContentProvider 会在 Application的 attachBaseContext 之后, onCreate之前创建。在ContentProvider的出事时候,
  2. 引用队列可以配合软引用、弱引用及虚引用使用,引用的对象将要被JVM回收时,会将其加入到引用队列中。
  3. 注册 Application.ActivityLifecycleCallbacks 监听Activity的生命周期,以及 fragmentManager.registerFragmentLifecycleCallbacks监听Fragment的生命周期。
  4. 比如监听 onActivityDestroyed方法,当监听到这个方法调用的时候,把Activity 全部放到观察数组中,并且用引用队列包裹这个activity,生成key(UUID),然后过5s,看看引用队列里面有没有这个key,如果有,证明回收了,然后把观察数组中的remove掉这个key,此时如果这个数组里面的count > 0 ,证明有可能是怀疑的泄漏,然后 调用 Runtime.getRuntime().gc(),之后再 看看 引用队列有没有这个数据,如果有,然后把观察数组中的remove掉这个key,之后再看观察数组中的count,如果小于5,只是提示一下。如果count 大于 5 (防止卡顿),就开始使用 shark (2.0之前是haha)分析堆栈信息。用可达到性分析,找到最短的链路,

五 引用类型以及引用队列

我们知道在Java中除了基础的数据类型以外,其它的都为引用类型。而Java根据其生命周期的长短将引用类型又分为强引用、软引用、弱引用、虚引用

5.1 引用类型

强引用

强引用是使用最普遍的引用。如果一个对象具有强引用,JVM 宁愿抛出OOM也不会回收他

软引用

如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用

弱引用

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象

虚引用

相当于没有引用

5.2 引用队列

引用队列可以配合软引用、弱引用及幽灵引用使用,当引用的对象将要被JVM回收时,会将其加入到引用队列中,LeakCanary就是用的这个原理来检测Activity或者Fragment是否有泄漏

开启严格模式

比如在主线程读取数据库,读写文件操作,网络操作,你可以设置StrictMode监听那些潜在问题,出现问题时,打印日志,或者是 直接 抛异常。

public void onCreate() {
     if (DEVELOPER_MODE) {
         StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
                 .detectDiskReads()
                 .detectDiskWrites()
                 .detectNetwork()   // or .detectAll() for all detectable problems
                 .penaltyLog()
                 .build());
         StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
                 .detectLeakedSqlLiteObjects()
                 .detectLeakedClosableObjects()
                 .penaltyLog()
                 .penaltyDeath()
                 .build());
     }
     super.onCreate();
}

六 OOM 以及查找OOM

其实在 Android 中把Bitmap 处理好的话,基本上也就不会OOM了,Bitmap 是个重头戏,当然了在实际项目中我们一般用 Glide 就可以搞定了

6.1 造成OOM原因有哪些

  1. Java堆内存不足
  2. 堆内存碎片太多,导致放一个稍微大点就OOM了
  3. FB句柄太多
  4. 线程数量太多(我在华为荣耀30上,创建了1600多个线程就OOM了)
  5. 虚拟内不足
  6. 内存抖动,会导致GC频繁,界面卡顿,严重可能OOM

6.2 避免OOM的方案

6.2.1 监听低内存的时候释放资源

您可以在 Activity.onTrimMemory() Fragement.OnTrimMemory() Service.onTrimMemory() ContentProvider.OnTrimMemory() 类中实现 ComponentCallbacks2 接口,或者是在 Application 重写onTrimMemory方法,监听低内存的回调,做相应的释放回调官方文档

6.2.2 使用更加轻量的数据结构

常规 HashMap 实现的内存效率可能十分低下,因为每个映射都需要分别对应一个单独的条目对象。我们可以考虑使用ArrayMap/SparseArray而不是HashMap等传统数据结构,SparseArray 类的效率更高,因为它们可以避免系统需要对键(有时还对值)进行自动装箱,比如ArrayMap用时间换空间,我们可以考虑使用ArrayMap 用的是二分查找的方式,速度没有HashMap快,但是性能比Hashmap好,1000以内的google推荐使用 ArrayMap, SparseArray、SparseBooleanArray、LongSparseArray,使用这些API可以让我们的程序更加高效,他省去了自动装箱封箱功能

6.2.3 推荐使用 MMKV或者是DataStore用来替代SharePreference

MMKV和DataStore使用的都是Protobuf进行数据序列化的,SharePreferenc 使用的是Xml,Protocol Buffers性能优于Xml,MMKV的原理使用的MMap(内存映射),复制一次数据,也可以局部变更,然而 SharePreferenc 使用传统的IO操作,复制两次数据,同时并不能局部改变,每次改变都会写入整个xml,无论是commit还是apply都会造成ARN剖析 SharedPreference apply 引起的 ANR 问题

6.2.4 序列化数据推荐使用 Protobuf

Protobuf 是 Google 设计的一种无关乎语言和平台,并且可扩展的机制,用于对结构化数据进行序列化。该机制与 XML 类似,但更小、更快也更简单。如果您决定针对数据使用 Protobuf,则应始终在客户端代码中使用精简版 Protobuf。常规 Protobuf 会生成极其冗长的代码,这会导致应用出现多种问题,例如 RAM 使用量增多、APK 大小显著增加以及执行速度变慢。

6.2.5 避免内存抖动

正常的GC并不会对性能造成影响,如果在短时间内发生许多垃圾回收事件,就可能会快速耗尽帧时间。系统花在垃圾回收上的时间越多,能够花在呈现或流式传输音频等其他任务上的时间就越少,频繁GC 会有 Stop The World 的操作。会造成卡顿,内存碎片等等。避免在 大量for 分配多个临时对象, 减少在 onDraw() 初始化对象。

6.2.6 避免使用枚举

枚举会使class变大,在项目中宁愿写几个常量用IntDef注解表示 也不用枚举

6.2.7 避免直接 new Thread(),要用线程池 或者 是协程

线程是Android中非常可贵的一个东西,他占用的资源要比普通的类要多很多,我再华为荣耀30手机上,创建了 1600个线程,就OOM了

6.3 Bitmap 的使用

其实这个是个重头戏,android中 只要把 Bitmap 处理好,基本上不会OOM的,当我们在项目中一般用 Glide 基本上搞定,但是还要说几句。

Bitmap格式

  • argb_8888:每个像素占32位,a=8;r=8;g=8;b=8;一共8*4=32位,一个字节8位,共占四个字节;

  • argb_4444:每个像素占16位,2个字节;

  • rgb_565:每个像素占16位,2个字节;g代表的6位无效保留,

  • alpha_8:每个像素占8位,1个字节;

Bitmap 到底有多占内存

Bitmap有Api即 getByteCount(),具体的计算逻辑请看Android坑档案:你的Bitmap究竟占多大内存?

sd卡上 或者 网络下载的

假如 500x500的图片放到sd卡上,或者是网络下载的,当用 argb_8888 格式的时时候 算法是: 500x500x4 = 1000000 B = 1000000 / 1024 / 1024 = 0.95M,rag_565 要比 argb_8888 小一半

放在 drawable-xxdpi 中的,

如果是放在 drawable 文件夹中 ,是会根据 放drawable的文件夹不同以及手机屏幕密度改变而改变的,假如放到 drawable-xxdpi(对应的屏幕密度是480)中的话,手机对应的密度是440的话,还是 argb_8888

 长/文件夹的密度*手机的密度 * 宽/文件夹的密度*手机的密度 * 4=(500/480 * 440) * (500/480 * 440) * 4 = 840277 B = 0.8M 

也可以记 长 * 宽 * (文件夹密度/手机真实的密度)* 图片的格式

Bitmap的压缩

算了 还是看 腾讯的文章吧Android中图片压缩分析

大图检测的plugin

DoraemonKit Hunter

参考链接

google官网管理应用内存
Android内存优化之OOM
Android性能优化典范 - 第2季
计算性能优化
Android性能优化之内存优化
Android线上OOM问题定位组件
Android坑档案:你的Bitmap究竟占多大内存?

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们