性能优化-内存优化
内存管理基础
App内存组成以及限制
Android
给每个App
分配一个VM
,让App运行在dalvik
上,这样即使App
崩溃也不会影响到系统。系统给VM
分配了一定的内存大小,App
可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果应用超出VM
最大内存,就会出现内存溢出crash
。
由程序控制操作的内存空间在heap
上,分java heapsize
和native heapsize
Java申请的内存在
vm heap
上,所以如果java
申请的内存大小超过VM
的逻辑内存限制,就会出现内存溢出的异常。native层内存申请不受其限制,
native
层受native process
对内存大小的限制
如何查看Android设备对App的内存限制
- 主要查看系统配置文件
build.prop
,我们可以通过adb shell
在 命令行窗口查看
adb shell cat /system/build.prop
- 通过代码获取
1 | ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE) |
- 可以修改吗?
- 修改 \frameworks\base\core\jni\AndroidRuntime.cpp
1 | int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote) |
修改 platform/dalvik/+/eclair-release/vm/Init.c
1
2gDvm.heapSizeStart = 2 * 1024 * 1024; // Spec says 16MB; too big for us.
gDvm.heapSizeMax = 16 * 1024 * 1024; // Spec says 75% physical mem
内存指标概念
Item | 全称 | 含义 | 等价 |
---|---|---|---|
USS | Unique Set Size | 物理内存 | 进程独占的内存 |
PSS | Proportional Set Size | 物理内存 | PSS= USS+ 按比例包含共享库 |
RSS | Resident Set Size | 物理内存 | RSS= USS+ 包含共享库 |
VSS | Virtual Set Size | 虚拟内存 | VSS= RSS+ 未分配实际物理内存 |
总结:VSS >= RSS >= PSS >= USS,但/dev/kgsl-3d0部份必须考虑VSS
Android内存分配与回收机制
- 内存分配
Android的Heap空间是一个Generational Heap Memory
的模型,最近分配的对象会存放在Young Generation
区域,当一个对象在这个区域停留的时间达到一定程度,它会被移动到Old Generation
,最后累积一定时间再移动到Permanent Generation
区域。
1、Young Generation
由一个Eden区和两个Survivor区组成,程序中生成的大部分新的对象都在Eden区中,当Eden区满时,还存活的对象将被复制到其中一个Survivor区,当次Survivor区满时,此区存活的对象又被复制到另一个Survivor区,当这个Survivor区也满时,会将其中存活的对象复制到年老代。
2、Old Generation
一般情况下,年老代中的对象生命周期都比较长。
3、Permanent Generation
用于存放静态的类和方法,持久代对垃圾回收没有显著影响。
总结:内存对象的处理过程如下:
- 1、对象创建后在Eden区。
- 2、执行GC后,如果对象仍然存活,则复制到S0区。
- 3、当S0区满时,该区域存活对象将复制到S1区,然后S0清空,接下来S0和S1角色互换。
- 4、当第3步达到一定次数(系统版本不同会有差异)后,存活对象将被复制到Old Generation。
- 5、当这个对象在Old Generation区域停留的时间达到一定程度时,它会被移动到Old Generation,最后累积一定时间再移动到Permanent Generation区域。
系统在Young Generation、Old Generation上采用不同的回收机制。每一个Generation的内存区域都有固定的大小。随着新的对象陆续被分配到此区域,当对象总的大小临近这一级别内存区域的阈值时,会触发GC操作,以便腾出空间来存放其他新的对象。
执行GC占用的时间与Generation和Generation中的对象数量有关:
- Young Generation < Old Generation < Permanent Generation
- Gener中的对象数量与执行时间成正比。
4、Young Generation GC
由于其对象存活时间短,因此基于Copying算法(扫描出存活的对象,并复制到一块新的完全未使用的控件中)来回收。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在Young Generation区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。
5、Old Generation GC
由于其对象存活时间较长,比较稳定,因此采用Mark(标记)算法(扫描出存活的对象,然后再回收未被标记的对象,回收后对空出的空间要么合并,要么标记出来便于下次分配,以减少内存碎片带来的效率损耗)来回收。
GC类型
在Android系统中,GC有三种类型:
- kGcCauseForAlloc:分配内存不够引起的GC,会Stop World。由于是并发GC,其它线程都会停止,直到GC完成。
- kGcCauseBackground:内存达到一定阈值触发的GC,由于是一个后台GC,所以不会引起Stop World。
- kGcCauseExplicit:显示调用时进行的GC,当ART打开这个选项时,使用System.gc时会进行GC。
可达性分析与GCRoots
Android低内存杀进程机制
Anroid基于进程中运行的组件及其状态规定了默认的五个回收优先级:
- Empty process(空进程)
- Background process(后台进程)
- Service process(服务进程)
- Visible process(可见进程)
- Foreground process(前台进程)
系统需要进行内存回收时最先回收空进程,然后是后台进程,以此类推最后才会回收前台进程(一般情况下前台进程就是与用户交互的进程了,如果连前台进程都需要回收那么此时系统几乎不可用了)。
ActivityManagerService
会对所有进程进行评分(存放在变量adj中),然后再讲这个评分更新到内核,由内核去完成真正的内存回收( lowmemorykiller
, Oom_killer
)。这里只是大概的流程,中间过程还是很复杂的
什么是OOM
OOM(OutOfMemoryError)内存溢出错误,在常见的Crash疑难排行榜上,OOM绝对可以名列前茅并且经久不衰。因为它发生时的Crash堆栈信息往往不是导致问题的根本原因,而只是压死骆驼的最后一根稻草
发生OOM的条件
- Android 2.x系统 GC LOG中的dalvik allocated + external allocated + 新分配的大小 >= getMemoryClass()值的时候就会发生OOM。 例如,假设有这么一段Dalvik输出的GC LOG:GC_FOR_MALLOC free 2K, 13% free 32586K/37455K, external 8989K/10356K, paused 20ms,那么32586+8989+(新分配23975)=65550>64M时,就会发生OOM。
- Android 4.x系统 Android 4.x的系统废除了external的计数器,类似bitmap的分配改到dalvik的java heap中申请,只要allocated + 新分配的内存 >= getMemoryClass()的时候就会发生OOM
OOM原因分类
OOM代码分析
Android 虚拟机最终抛出OutOfMemoryError的地方
/art/runtime/thread.cc
1 | void Thread::ThrowOutOfMemoryError(const char* msg) { |
堆内存分配失败
/art/runtime/gc/heap.cc
1 | void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type) { |
创建线程失败
/art/runtime/thread.cc
1 | void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) { |
Android 内存分析命令介绍
常用的内存调优分析命令:
- dumpsys meminfo
- procrank
- cat /proc/meminfo
- free
- showmap
- vmstat
dumpsys meminfo
相关参数的说明:
Pss Total:是一个进程实际使用的内存,该统计方法包括比例分配共享库占用的内存,即如果有三个进程共享了一个共享库,则平摊分配该共享库占用的内存。Pss Total统计方法的一个需要注意的地方是如果使用共享库的一个进程被杀死,则共享库的内存占用按比例分配到其他共享该库的进程中,而不是将内存资源返回给系统,这种情况下PssTotal不能够准确代表内存返回给系统的情况。
Private Dirty:进程私有的脏页内存大小,该统计方法只包括进程私有的被修改的内存。
Private Clear:进程私有的干净页内存大小,该统计方法只包括进程私有的没有被修改的内存。
Swapped Dirty:被交换的脏页内存大小,该内存与其他进程共享。
其中private Dirty + private Clean = Uss,该值是一个进程的使用的私有内存大小,即这些内存唯一被该进程所有。该统计方法真正描述了运行一个进程需要的内存和杀死一个进程释放的内存情况,是怀疑内存泄露最好的统计方法。
共享比例:sharing_proportion = (Pss Total - private_clean - private_dirty) / (shared_clean + shared_dirty)
能够被共享的内存:swappable_pss = (sharing_proportion * shared_clean) + private_clean
Native Heap:本地堆使用的内存,包括C/C++在堆上分配的内存
Dalvik Heap:dalvik虚拟机使用的内存
Dalvik other:除Dalvik和Native之外分配的内存,包括C/C++分配的非堆内存
Cursor:数据库游标文件占用的内存
Ashmem:匿名共享内存
Stack:Dalvik栈占用的内存
Other dev:其他的dev占用的内存
.so mmap:so库占用的内存
.jar mmap:.jar文件占用的内存
.apk mmap:.apk文件占用的内存
.ttf mmap:.ttf文件占用的内存
.dex mmap:.dex文件占用的内存
image mmap:图像文件占用的内存
code mmap:代码文件占用的内存
Other mmap:其他文件占用的内存
Graphics:GPU使用图像时使用的内存
GL:GPU使用GL绘制时使用的内存
Memtrack:GPU使用多媒体、照相机时使用的内存
Unknown:不知道的内存消耗
Heap Size:堆的总内存大小
Heap Alloc:堆分配的内存大小
Heap Free:堆待分配的内存大小
Native Heap | Heap Size : 从mallinfo usmblks获的,当前进程Native堆的最大总共分配内存
Native Heap | Heap Alloc : 从mallinfo uorblks获的,当前进程navtive堆的总共分配内存
Native Heap | Heap Free : 从mallinfo fordblks获的,当前进程Native堆的剩余内存
Native Heap Size ≈ Native Heap Alloc + Native Heap Free
mallinfo是一个C库,mallinfo()函数提供了各种各样通过malloc()函数分配的内存的统计信息。
Dalvik Heap | Heap Size : 从Runtime totalMemory()获得,Dalvik Heap总共的内存大小
Dalvik Heap | Heap Alloc : 从Runtime totalMemory() - freeMemory()获得,Dalvik Heap分配的内存大小
Dalvik Heap | Heap Free : 从Runtime freeMemory()获得,Dalvik Heap剩余的内存大小
Dalvik Heap Size = Dalvik Heap Alloc + Dalvik Heap Free
Obejcts当前进程中的对象个数
Views:当前进程中实例化的视图View对象数量
ViewRootImpl:当前进程中实例化的视图根ViewRootImpl对象数量
AppContexts:当前进程中实例化的应用上下文ContextImpl对象数量
Activities:当前进程中实例化的Activity对象数量
Assets:当前进程的全局资产数量
AssetManagers:当前进程的全局资产管理数量
Local Binders:当前进程有效的本地binder对象数量
Proxy Binders:当前进程中引用的远程binder对象数量
Death Recipients:当前进程到binder的无效链接数量
OpenSSL Sockets:安全套接字对象数量
SQL
MEMORY_USED:当前进程中数据库使用的内存数量,kb
PAGECACHE_OVERFLOW:页面缓存的配置不能够满足的数量,kb
MALLOC_SIZE: 向sqlite3请求的最大内存分配数量,kb
DATABASES
pgsz:数据库的页面大小
dbsz:数据库大小
Lookaside(b):后备使用的内存大小
cache:数据缓存状态
Dbname:数据库表名
Asset Allocations
资源路径和资源大小
procrank
功能: 获取所有进程的内存使用的排行榜,排行是以Pss
的大小而排序。procrank
命令比dumpsys meminfo
命令,能输出更详细的VSS/RSS/PSS/USS内存指标。
最后一行输出下面6个指标:
total | free | buffers | cached | shmem | slab |
---|---|---|---|---|---|
2857032K | 998088K | 78060K | 78060K | 312K | 92392K |
执行结果:
1 | root@Phone:/# procrank |
cat /proc/meminfo
功能:能否查看更加详细的内存信息
1 | 指令: cat /proc/meminfo |
输出结果如下(结果内存值不带小数点,此处添加小数点的目的是为了便于比对大小):
1 | root@phone:/ # cat /proc/meminfo |
对于cache和buffer也是系统可以使用的内存。所以系统总的可用内存为 MemFree+Buffers+Cached
free
主功能:查看可用内存,缺省单位KB。该命令比较简单、轻量,专注于查看剩余内存情况。数据来源于/proc/meminfo。
输出结果:
1 | root@phone:/proc/sys/vm # free |
- 对于
Mem
行,存在的公式关系: total = used + free; - 对于
-/+ buffers
行: 1760936 = 1836040 - 75104(buffers); 1096096 = 1020992 + 75104(buffers);
showmap
主功能:用于查看虚拟地址区域的内存情况
1 | 用法: showmap -a [pid] |
该命令的输出每一行代表一个虚拟地址区域(vm area)
- start addr和end addr:分别代表进程空间的起止虚拟地址;
- virtual size/ RSS /PSS这些前面介绍过;
- shared clean:代表多个进程的虚拟地址可指向这块物理空间,即有多少个进程共享这个库;
- shared: 共享数据
- private: 该进程私有数据
- clean: 干净数据,是指该内存数据与disk数据一致,当内存紧张时,可直接释放内存,不需要回写到disk
- dirty: 脏数据,与disk数据不一致,需要先回写到disk,才能被释放。
vmstat
主功能:不仅可以查看内存情况,还可以查看进程运行队列、系统切换、CPU时间占比等情况,另外该指令还是周期性地动态输出。
用法:
1 | Usage: vmstat [ -n iterations ] [ -d delay ] [ -r header_repeat ] |
输入结果:
1 | root@phone:/ # vmstat |
参数列总共15个参数,分为4大类:
- procs(进程)
- r: Running队列中进程数量
- b: IO wait的进程数量
- memory(内存)
- free: 可用内存大小
- mapped:mmap映射的内存大小
- anon: 匿名内存大小
- slab: slab的内存大小
- system(系统)
- in: 每秒的中断次数(包括时钟中断)
- cs: 每秒上下文切换的次数
- cpu(处理器)
- us: user time
- ni: nice time
- sy: system time
- id: idle time
- wa: iowait time
- ir: interrupt time
总结
dumpsys meminfo
适用场景: 查看进程的oom adj,或者dalvik/native等区域内存情况,或者某个进程或apk的内存情况,功能非常强大;procrank
适用场景: 查看进程的VSS/RSS/PSS/USS各个内存指标;cat /proc/meminfo
适用场景: 查看系统的详尽内存信息,包含内核情况;free
适用场景: 只查看系统的可用内存;showmap
适用场景: 查看进程的虚拟地址空间的内存分配情况;vmstat
适用场景: 周期性地打印出进程运行队列、系统切换、CPU时间占比等情况;
adj
ADJ如何查看
利用adb shell
1 | 1.ps | grep 包名 //查看当前app的进程号 |
ADJ的值的各种含义
1 | ADJ级别 取值 含义 |
ADJ触发顺序
ADJ是一种算法,用于系统判断进程优先级以触发Linux的LMK(LowMemoryKill)机制。一般触发时机(Linux下)是在系统低内存时,为了维护正在运行的进程,杀掉优先级比较低(adj值比较高)的其他进程。在Android中这一机制有所改动。在ActivityManagerService里有具体的计算Adj值的源码。进程刚启动时ADJ等于INVALID_ADJ,当执行完attachApplication(),该该进程的curAdj和setAdj不相等,则会触发执行setOomAdj()将该进程的节点/proc/pid/oom_score_adj写入oomadj值。下图参数为Android原生阈值,当系统剩余空闲内存低于某阈值(比如147MB),则从ADJ大于或等于相应阈值(比如900)的进程中,选择ADJ值最大的进程,如果存在多个ADJ相同的进程,则选择内存最大的进程。 如下是64位机器,LMK默认阈值图:
1 | ----------ADJ----------------Memory Left------------ |
高级进程 ADJ<0的进程
- NATIVE_ADJ(-1000):是由init进程fork出来的Native进程,并不受system管控;
- SYSTEM_ADJ(-900):是指system_server进程;
- PERSISTENT_PROC_ADJ(-800): 是指在AndroidManifest.xml中申明android:persistent=”true”的系统(即带有FLAG_SYSTEM标记)进程,persistent进程一般情况并不会被杀,即便被杀或者发生Crash系统会立即重新拉起该进程。
- PERSISTENT_SERVICE_ADJ(-700):是由startIsolatedProcess()方式启动的进程,或者是由system_server或者persistent进程所绑定(并且带有BIND_ABOVE_CLIENT或者BIND_IMPORTANT)的服务进程
总结
Android进程优先级ADJ的每一个ADJ级别往往都有多种场景,使用adjType完美地区分相同ADJ下的不同场景; 不同ADJ进程所对应的schedGroup不同,从而分配的CPU资源也不同,schedGroup大体分为TOP(T)、前台(F)、后台(B); ADJ跟AMS中的procState有着紧密的联系。
- adj:通过调整oom_score_adj来影响进程寿命(Lowmemorykiller杀进程策略);
- schedGroup:影响进程的CPU资源调度与分配;
- procState:从进程所包含的四大组件运行状态来评估进程状态,影响framework的内存控制策略。比如控制缓存进程和空进程个数上限依赖于procState,再比如控制APP执行handleLowMemory()的触发时机等。
开发需要注意的
- UI进程与Service进程一定要分离,因为对于包含activity的service进程,一旦进入后台就成为”cchstarted-ui-services”类型的cache进程(ADJ>=900),随时可能会被系统回收;而分离后的Service进程服务属于SERVICE_ADJ(500),被杀的可能性相对较小。尤其是系统允许自启动的服务进程必须做UI分离,避免消耗系统较大内存。 只有真正需要用户可感知的应用,才调用startForegroundService()方法来启动前台服务,此时ADJ=PERCEPTIBLE_APP_ADJ(200),常驻内存,并且会在通知栏常驻通知提醒用户,比如音乐播放,地图导航。切勿为了常驻而滥用前台服务,这会严重影响用户体验。
- 进程中的Service工作完成后,务必主动调用stopService或stopSelf来停止服务,避免占据内存,浪费系统资源;
- 不要长时间绑定其他进程的service或者provider,每次使用完成后应立刻释放,避免其他进程常驻于内存;
- APP应该实现接口onTrimMemory()和onLowMemory(),根据TrimLevel适当地将非必须内存在回调方法中加以释放。当系统内存紧张时会回调该接口,减少系统卡顿与杀进程频次。
- 减少在保活上花心思,更应该在优化内存上下功夫,因为在相同ADJ级别的情况下,系统会选择优先杀内存占用的进程。
Android内存泄漏分析工具
MAT
- incoming references这个对象引用了哪些对象
- outgoing references哪些对象引用了这个对象
Android Studio Memory-profiler
https://developer.android.com/studio/profile/memory-profiler#performance
LeakCanary
https://github.com/square/leakcanary
引用队列
通常我们将其ReferenceQueue翻译为引用队列,换言之就是存放引用的队列,保存的是Reference对象。其作用在于Reference对象所引用的对象被GC回收时,该Reference对象将会被加入引用队列中(ReferenceQueue)的队列末尾。
ReferenceQueue常用的方法:
public Reference poll():从队列中取出一个元素,队列为空则返回null;
public Reference remove():从队列中出对一个元素,若没有则阻塞至有可出队元素;
public Reference remove(long timeout):从队列中出对一个元素,若没有则阻塞至有可出对元素或阻塞至超过timeout毫秒;
1 | ReferenceQueue queue=new ReferenceQueue(); |
也即是GC在回收一个对象时,如果发现该对象具有虚引用,那么在回收之前会首先该对象的虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入虚引用来了解被引用的对象是否被GC回收
原理(针对Activity)
LeakCanary
通过监听Activity生命周期,在Activity onDestroy的时候,创建一个弱引用,key跟当前Activity绑定,将key保存到set里面,并且关联一个引用队列,然后在主线程空闲5秒后,开始检测是否内存泄漏,具体检测步骤:
1:判断引用队列中是否有该Activity的引用,有则说明Activity被回收了,移除Set里面对应的key。
2:判断Set里面是否有当前要检测的Activity的key,如果没有,说明Activity对象已经被回收了,没有内存泄漏。如果有,只能说明Activity对象还没有被回收,可能此时已经没有被引用,不一定是内存泄漏。
3:手动触发GC,然后重复1和2操作,确定一下是不是真的内存泄漏。
步骤
- 1、RefWatcher.watch()创建了一个KeyedWeakReference用于去观察对象。
- 2、然后,在后台线程中,它会检测引用是否被清除了,并且是否没有触发GC。
- 3、如果引用仍然没有被清除,那么它将会把堆栈信息保存在文件系统中的.hprof文件里。
- 4、HeapAnalyzerService被开启在一个独立的进程中,并且HeapAnalyzer使用了HAHA开源库解析了指定时刻的堆栈快照文件heap dump。
- 5、从heap dump中,HeapAnalyzer根据一个独特的引用key找到了KeyedWeakReference,并且定位了泄露的引用。
- 6、HeapAnalyzer为了确定是否有泄露,计算了到GC Roots的最短强引用路径,然后建立了导致泄露的链式引用。
- 7、这个结果被传回到app进程中的DisplayLeakService,然后一个泄露通知便展现出来了。
总结:在一个Activity执行完onDestroy()之后,将它放入WeakReference中,然后将这个WeakReference类型的Activity对象与ReferenceQueque关联。这时再从ReferenceQueque中查看是否有没有该对象,如果没有,执行gc,再次查看,还是没有的话则判断发生内存泄露了。最后用HAHA这个开源库去分析dump之后的heap内存。
LeakCanary是如何安装的
LeakCanary在2.0以后的版本中不需要在application完成初始化任务,LeakCanary2.0以后利用了ContentProvider 在 Application 被创建之前被加载的原理,在ContentProvider的onCreate完成了初始化任务。
LeakCanary的Idle机制
在Activity onDestroy的时候,LeakCanary
并没有马上去执行检测任务,而是将任务添加到消息队列的一个idle任务列表里,然后当Handler 在消息队列中获取不到消息,也就是主线程空闲的时候,会去idle任务列表里取任务出来执行。
GC_Log
GC Log分为Dalvik和ART的GC日志
ART的日志与Dalvik的日志差距非常大,除了格式不同之外,打印的时间也不同,非要在慢GC时才打印
除了。下面我们看看这条ART GC Log:
xplicit | full | concurrent mark sweep | freed 104710 (7MB) AllocSpace | 21(416KB)LOS objects | 33% free,25MB/38MB | paused 1.230ms total 67.216ms |
---|---|---|---|---|---|---|
GC产生 | GC类型 | 采集方法 | 释放的数量和占用的空间 | 释放的大对象数量和所占用的空 | 堆中空闲空间的百分比和(对象的个数)/(堆的总空间) | 暂停耗时 |
GC产生的原因如下:
Concurrent、Alloc、Explicit跟Dalvik的基本一样,这里就不重复介绍了。
NativeAlloc:Native内存分配时,比如为Bitmaps或者RenderScript分配对象, 这会导致Native内存压力,从而触发GC。
Background:后台GC,触发是为了给后面的内存申请预留更多空间。
CollectorTransition:由堆转换引起的回收,这是运行时切换GC而引起的。收集器转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,收集器转换仅在以下情况下出现:在内存较小的设备上,App将进程状态从可察觉的暂停状态变更为可察觉的非暂停状态(反之亦然)。
HomogeneousSpaceCompact:齐性空间压缩是指空闲列表到压缩的空闲列表空间,通常发生在当App已经移动到可察觉的暂停进程状态。这样做的主要原因是减少了内存使用并对堆内存进行碎片整理。
DisableMovingGc:不是真正的触发GC原因,发生并发堆压缩时,由于使用了
GetPrimitiveArrayCritical,收集会被阻塞。一般情况下,强烈建议不要使用
GetPrimitiveArrayCritical,因为它在移动收集器方面具有限制。
HeapTrim:不是触发GC原因,但是请注意,收集会一直被阻塞,直到堆内存整理完毕。
GC类型如下:
Full:与Dalvik的FULL GC差不多。
Partial:跟Dalvik的局部GC差不多,策略时不包含Zygote Heap。
Sticky:另外一种局部中的局部GC,选择局部的策略是上次垃圾回收后新分配的对象。
GC采集的方法如下:
mark sweep:先记录全部对象,然后从GC ROOT开始找出间接和直接的对象并标注。利用之前记录的全部对象和标注的对象对比,其余的对象就应该需要垃圾回收了。
concurrent mark sweep:使用mark sweep采集器的并发GC。
mark compact:在标记存活对象的时候,所有的存活对象压缩到内存的一端,而另一端可以更加高效地被回收。
semispace:在做垃圾扫描的时候,把所有引用的对象从一个空间移到另外一个空间,然后直接GC剩余在旧空间中的对象即可。
通过GC日志,我们可以知道GC的量和它对卡顿的影响,也可以初步定位一些如主动调用GC、可分配的内存不足、过多使用Weak Reference等问题。
内存抖动
定义
内存抖动是指内存忽高忽低,有短时间内快速的上升和下落的趋势,内存呈锯齿状。此时会频繁的GC,造成卡顿,甚至有OOM的可能。
短时间创建大量对象被创建然后又马上被释放,瞬间产生的对象会严重占用内存区。
在程序里,每创建一个对象,就会有一块内存分配给它;每分配一块内存,程序的可用内存也就少一块;当程序被占用的内存达到一定临界程度,GC 也就是垃圾回收器(Garbage Collector)就会出动,来释放掉一部分不再被使用的内存。Android 里的 View.onDraw() 方法在每次需要重绘的时候都会被调用,这就意味着,如果你在 onDraw() 里写了创建对象的代码,在界面频繁刷新的时候,你就也会频繁创建出一大批只被使用一次的对象,这就会导致内存占用的迅速攀升;然后很快,可能就会触发 GC 的回收动作,也就是这些被你创建出来的对象被 GC 回收掉。垃圾内存太多了就被清理掉,这是 Java 的工作机制,这不是问题。问题在于,频繁创建这些对象会造成内存不断地攀升,在刚回收了之后又迅速涨起来,那么紧接着就是又一次的回收,对吧?这么往复下来,最终导致一种循环,一种在短时间内反复地发生内存增长和回收的循环。
这种循环往复的状态就像是水波纹的颤动一样,它的专业称呼叫做 Memory Churn,Android 的官方文档里把它翻译做了内存抖动。所以内存抖动其实并不是我们的内存在整体地进行摇晃这样神奇的事情,而仅仅是类似有一根搅拌棒轻轻地在内存的边界上进行搅动的样子——其实翻译成「内存搅动」好像也行哈?
如何定位
对于内存抖动的定位可直接使用Memory Profiler,原因是Memory Profiler可直接反应APP的内存占用,方便进行跟踪
发生内存抖动时,我们选择内存变化锯齿状的区域,然后在Memory Profiler可显示下面的图示
接着我们点击Allocations进行对象分配数量排序,之所以点击这个是因为一般在循环,频繁调用的地方可能发生内存抖动
例如普通循环中,Adapter的get View或者onBinderView等方法中可能会发生内存抖动
所以如果发生了内存抖动,大概率的是在对象数量多的地方出现了问题,因此先进行对象数量排序
原因
大循环中创建对象、自定义View的onDraw()方法中创建对象(屏幕绘制与动画执行时会频繁调用onDraw())。
处理方法
将对象创建放到循环外,对于无法避免的创建对象情况,可采用对象池模型进行缓存,复用对象,需注意用完后要手动释放对象池中对象。
内存泄漏
定义
内存泄露是指不再使用的内存仍然占用着内存空间,因为程序中仍然保存着对它的引用,而使得GC无法将它回收或得到及时释放,从而造成的内存空间浪费的问题,称为内存泄露。
原因
长生命周期的对象持有短生命周期对应的引用,因为短生命周期对象可能不再使用,而因为长生命周期对象持有着对其的引用,因此GC无法将其进行回收。
Android内存泄漏常见场景以及解决方案
1、资源性对象未关闭
对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。
2、注册对象未注销
例如BraodcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。
3、类的静态变量持有大数据对象
尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。
4、单例造成的内存泄漏
优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。
5、非静态内部类的静态实例
该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置空让GC可以回收,否则还是会内存泄漏。
6、Handler临时性内存泄漏
Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息,当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。解决方案如下所示:
- 1、使用一个静态Handler内部类,然后对Handler持有的对象(一般是Activity)使用弱引用,这样在回收时,也可以回收Handler持有的对象。
- 2、在Activity的Destroy或者Stop时,应该移除消息队列中的消息,避免Looper线程的消息队列中有待处理的消息需要处理。
需要注意的是,AsyncTask内部也是Handler机制,同样存在内存泄漏风险,但其一般是临时性的。对于类似AsyncTask或是线程造成的内存泄漏,我们也可以将AsyncTask和Runnable类独立出来或者使用静态内部类。
7、容器中的对象没清理造成的内存泄漏
在退出程序之前,将集合里的东西clear,然后置为null,再退出程序
8、WebView
WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。
9、使用ListView时造成的内存泄漏
在构造Adapter时,使用缓存的convertView。
Bitmap
Bitmap相关方法总结
Bitmap
public void recycle() // 回收位图占用的内存空间,把位图标记为Dead
public final boolean isRecycled() //判断位图内存是否已释放
public final int getWidth() //获取位图的宽度
public final int getHeight() //获取位图的高度
public final boolean isMutable() //图片是否可修改
public int getScaledWidth(Canvas canvas) //获取指定密度转换后的图像的宽度
public int getScaledHeight(Canvas canvas) //获取指定密度转换后的图像的高度
public boolean compress(CompressFormat format, int quality, OutputStream stream) //按指定的图片格式以及画质,将图片转换为输出流
public static Bitmap createBitmap(Bitmap src) //以 src 为原图生成不可变得新图像
public static Bitmap createScaledBitmap(Bitmap src, int dstWidth, int dstHeight, boolean filter) //以 src 为原图,创建新的图像,指定新图像的高宽以及是否可变。
public static Bitmap createBitmap(int width, int height, Config config) //创建指定格式、大小的位图
public static Bitmap createBitmap(Bitmap source, int x, int y, int width, int height) //以 source 为原图,创建新的图片,指定起始坐标以及新图像的高宽。
BitmapFactory工厂类
Option 参数类
public boolean inJustDecodeBounds //如果设置为 true ,不获取图片,不分配内存,但会返回图片的高度宽度信息。如果将这个值置为 true ,那么在解码的时候将不会返回 bitmap ,只会返回这个 bitmap 的尺寸。这个属性的目的是,如果你只想知道一个 bitmap 的尺寸,但又不想将其加载到内存时。这是一个非常有用的属性。
public int inSampleSize //图片缩放的倍数, 这个值是一个 int ,当它小于1的时候,将会被当做1处理,如果大于1,那么就会按照比例 (1 / inSampleSize) 缩小 bitmap 的宽和高、降低分辨率,大于1时这个值将会被处置为2的倍数。例如, width=100,height=100, inSampleSize=2 ,那么就会将 bitmap 处理为, width=50,height=50 ,宽高降为1 / 2,像素数降为1 / 4。
public int outWidth //获取图片的宽度值
public int outHeight //获取图片的高度值 ,表示这个 Bitmap 的宽和高,一般和
inJustDecodeBounds 一起使用来获得 Bitmap 的宽高,但是不加载到内存。
public int inDensity //用于位图的像素压缩比
public int inTargetDensity //用于目标位图的像素压缩比(要生成的位图)
public byte[] inTempStorage //创建临时文件,将图片存储
public boolean inScaled //设置为 true 时进行图片压缩,从 inDensity 到 inTargetDensity
public boolean inDither //如果为 true ,解码器尝试抖动解码
public Bitmap.Config inPreferredConfig //设置解码器,这个值是设置色彩模式,默认值是 ARGB_8888 ,在这个模式下,一个像素点占用4bytes空间,一般对透明度不做要求的话,一般采用 RGB_565 模式,这个模式下一个像素点占用2bytes。
public String outMimeType //设置解码图像
public boolean inPurgeable //当存储 Pixel 的内存空间在系统内存不足时是否可以被回收
public boolean inInputShareable // inPurgeable 为 true 情况下才生效,是否可以共享一个 InputStream
public boolean inPreferQualityOverSpeed //为 true 则优先保证 Bitmap 质量其次是解码速度
public boolean inMutable //配置 Bitmap 是否可以更改,比如:在 Bitmap 上隔几个像素加一条线段
public int inScreenDensity //当前屏幕的像素密度
工厂方法
public static Bitmap decodeFile(String pathName, Options opts) //从文件读取图片
public static Bitmap decodeFile(String pathName)
public static Bitmap decodeStream(InputStream is) //从输入流读取图片
public static Bitmap decodeStream(InputStream is, Rect outPadding, Options opts)
public static Bitmap decodeResource(Resources res, int id) //从资源文件读取图片
public static Bitmap decodeResource(Resources res, int id, Options opts)
public static Bitmap decodeByteArray(byte[] data, int offset, int length) //从数组读取图片
public static Bitmap decodeByteArray(byte[] data, int offset, int length, Options opts)
public static Bitmap decodeFileDescriptor(FileDescriptor fd) //从文件读取文件 与 decodeFile 不同的是这个直接调用JNI函数进行读取 效率比较高
public static Bitmap decodeFileDescriptor(FileDescriptor fd, Rect outPadding, Options opts)
单个像素的字节大小
单个像素的字节大小由Bitmap的一个可配置的参数Config来决定。
Bitmap中,存在一个枚举类Config,定义了Android中支持的Bitmap配置:
Bitmap的加载方式
Bitmap 的加载方式有 Resource 资源加载、本地(SDcard)加载、网络加载等加载方式。
1. 从本地(SDcard)文件读取
方式1
1 | /** |
方式二 (效率高于方式一)
1 | /* |
2. 从输入流中读取文件(网络加载)
1 | /* |
3. Resource资源加载
Res资源加载方式:
1 | public static Bitmap readBitmapFromResource(Resources resources, int resourcesId, int width, int height) { |
此种方式相当的耗费内存 建议采用 decodeStream 代替 decodeResource 可以如下形式:
1 | public static Bitmap readBitmapFromResource(Resources resources, int resourcesId, int width, int height) { |
- BitmapFactory.decodeResource 加载的图片可能会经过缩放,该缩放目前是放在 java 层做的,效率比较低,而且需要消耗 java 层的内存。因此,如果大量使用该接口加载图片,容易导致OOM错误
- BitmapFactory.decodeStream 不会对所加载的图片进行缩放,相比之下占用内存少,效率更高。这两个接口各有用处,如果对性能要求较高,则应该使用 decodeStream ;如果对性能要求不高,且需要 Android 自带的图片自适应缩放功能,则可以使用 decodeResource 。
4. Assets资源加载方式:
1 | /** |
5. 从二进制数据读取图片
1 | public static Bitmap readBitmapFromByteArray(byte[] data, int width, int height) { |
Bitmap | Drawable | InputStream | Byte[ ]之间进行转换
1. Drawable转化成Bitmap
1 | public static Bitmap drawableToBitmap(Drawable drawable) { |
drawable 的获取方式: Drawable drawable = getResources().getDrawable(R.drawable.ic_launcher);
2. Bitmap转换成Drawable
1 | public static Drawable bitmapToDrawable(Resources resources, Bitmap bm) { |
3. Bitmap转换成byte[]
1 | public byte[] bitmap2Bytes(Bitmap bm) { |
4. byte[]转换成Bitmap
1 | Bitmap bitmap = BitmapFactory.decodeByteArray(byte, 0, b.length); |
5. InputStream转换成Bitmap
1 | InputStream is = getResources().openRawResource(id); |
6. InputStream转换成byte[]
1 | InputStream is = getResources().openRawResource(id);//也可以通过其他方式接收一个 InputStream对象 |
Bitmap常用操作
1. 将Bitmap保存为本地文件:
1 | public static void writeBitmapToFile(String filePath, Bitmap b, int quality) { |
2. 图片压缩:
1 | private static Bitmap compressImage(Bitmap image) { |
3. 图片缩放:
1 | /** |
4. 获取图片旋转角度:
1 | /*** |
5. 设置图片旋转角度
1 | private static Bitmap rotateBitmap(Bitmap b, float rotateDegree) { |
6. 通过图片id获得Bitmap:
1 | Bitmap bitmap=BitmapFactory.decodeResource(getResources(), R.drawable.ic_launcher); |
7. 通过 assest获取 获得Drawable bitmap:
1 | InputStream in = this.getAssets().open("ic_launcher"); |
8. 通过 sdcard获得 bitmap
1 | Bitmap bit = BitmapFactory.decodeFile("/sdcard/android.jpg"); |
9. view转Bitmap
1 | public static Bitmap convertViewToBitmap(View view, int bitmapWidth, int bitmapHeight) { |
10. 将控件转换为bitmap
1 | public static Bitmap convertViewToBitMap(View view) { |
11. 放大缩小图片
1 | public static Bitmap zoomBitmap(Bitmap bitmap, int w, int h) { |
12. 获得圆角图片的方法
1 | public static Bitmap getRoundedCornerBitmap(Bitmap bitmap, float roundPx) { |
13. 对 bitmap 进行裁剪
1 | public Bitmap bitmapClip(Context context, int id, int x, int y) { |
Bitmap内存模型
2.3- | 3.0-4.4 | 5.0-7.1 | 8.0 |
---|---|---|---|
Bitmap对象 | java Heap | java Heap | java Heap |
像素数据 | Native Heap | java Heap | Native Heap |
迁移原因 | - | 解决Native Bitmap内存泄露 | 共享整个系统的内存减少OOM |
Bitmap的内存回收
在Android2.3.3之前推荐使用Bitmap.recycle()方法进行Bitmap的内存回收。
备注:只有当确定这个Bitmap不被引用的时候才能调用此方法,否则会有“Canvas: trying to use a recycled bitmap”这个错误。
Android3.0之后
Android3.0之后,并没有强调Bitmap.recycle();而是强调Bitmap的复用
Save a bitmap for later use
使用LruCache对Bitmap进行缓存**,当再次使用到这个Bitmap的时候直接获取,而不用重走编码流程。
Use an existing bitmap
Android3.0(API 11之后)引入了BitmapFactory.Options.inBitmap字段,设置此字段之后解码方法会尝试复用一张存在的Bitmap。这意味着Bitmap的内存被复用,避免了内存的回收及申请过程,显然性能表现更佳。不过,使用这个字段有几点限制:
声明可被复用的Bitmap必须设置inMutable为true;
Android4.4(API 19)之前只有格式为jpg、png,同等宽高(要求苛刻),inSampleSize为1的Bitmap才可以复用;
Android4.4(API 19)之前被复用的Bitmap的inPreferredConfig会覆盖待分配内存的Bitmap设置的inPreferredConfig;
Android4.4(API 19)之后被复用的Bitmap的内存必须大于需要申请内存的Bitmap的内存;
Android4.4(API 19)之前待加载Bitmap的Options.inSampleSize必须明确指定为1
获取Bitmap的大小
getByteCount()
getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。API19开始getAllocationByteCount()方法代替了getByteCount()。
getAllocationByteCount()
API19之后,Bitmap加了一个Api:getAllocationByteCount();代表在内存中为Bitmap分配的内存大小。
1 | public final int getAllocationByteCount() { |
getByteCount()与getAllocationByteCount()的区别
一般情况下两者是相等的
通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小(即mBuffer的长度)
Bitmap占用内存大小计算
Bitmap作为位图,需要读入一张图片每一个像素点的数据,其主要占用内存的地方也正是这些像素数据。对于像素数据总大小,我们可以猜想为:像素总数量 × 每个像素的字节大小,而像素总数量在矩形屏幕表现下,应该是:横向像素数量 × 纵向像素数量,结合得到:
Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 × 纵向像素数量 × 每个像素的字节大小
但真是如此吗?
我们来看下源码,Bitmap的decode过程实际上是在native层完成的,为此,需要从BitmapFactory.cpp#nativeDecodeXXX方法开始跟踪,最终在doDecode方法里面
从上述代码中,我们看到bitmap最终通过canvas绘制出来,而canvas在绘制之前,有一个scale的操作,scale的值由
1 | scale = (float) targetDensity / density; |
这一行代码决定,即缩放的倍率和targetDensity和density相关,而这两个参数都是从传入的options中获取到的
- inDensity:Bitmap位图自身的密度、分辨率
- inTargetDensity: Bitmap最终绘制的目标位置的分辨率
- inScreenDensity: 设备屏幕分辨率
其中inDensity和图片存放的资源文件的目录有关,同一张图片放置在不同目录下会有不同的值:
density | 0.75 | 1 | 1.5 | 2 | 3 | 3.5 | 4 |
---|---|---|---|---|---|---|---|
densityDpi | 120 | 160 | 240 | 320 | 480 | 560 | 640 |
DpiFolder | ldpi | mdpi | hdpi | xhdpi | xxhdpi | xxxhdpi | xxxxhdpi |
可以验证几个结论:
图片放在drawable中,等同于放在drawable-mdpi中,原因为:drawable目录不具有屏幕密度特性,所以采用基准值,即mdpi
图片放在某个特定drawable中,比如drawable-hdpi,如果设备的屏幕密度高于当前drawable目录所代表的密度,则图片会被放大,否则会被缩小
放大或缩小比例 = 设备屏幕密度 / drawable目录所代表的屏幕密度
因此,关于Bitmap占用内存大小的公式,从之前:
Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 × 纵向像素数量 × 每个像素的字节大小
可以更细化为:
Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高× (设备分辨率/资源目录分辨率)^2 × 每个像素的字节大小
从Bitmap的角度解决OOM
图片显示,在显示缩略图时,不要去请求网络加载大图;
加载缩略图,是将一个稍大的图片,加载更小尺寸到内存,也就是用到了上面的inSampleSize。
及时释放内存
- 确保只使用一次的图片进行recycle回收,否则会造成崩溃
- 建议定义图片缓存管理类或者使用三方的图片库(glide)等进行缓存和内存管理
图片压缩
尺寸压缩(匹配控件大小)
质量压缩(计算合理的insampleSize值,减少图片占用内存)
使用BitmapFactory去decode一张大图时,往往会发生OOM,所以,我们计算一个合适的sampleSize是减少图片加载内存的一个重要手段。首先,设置inJustDecodeBounds为true,decodeFile并不占用空间,但是可以拿到原图的宽高,即options.width和options.height,通过一定的计算,就能够计算出合适的inSampleSize。根据该值,可以少加载图片像素到内存中,自然内存占用就会减少。
inBitmap属性,复用已有图片占用内存
捕获异常,避免崩溃,需要捕获oom异常
1
2
3
4
5
6try {
...加载图片的逻辑
} catch (OutOfMemoryError e) {
...质量压缩
}Lru
问LRU算法是怎么实现的?
回答:内部使用LinkedHashMap来缓存key-value的对应关系,提供put和get方法便于缓存内容的添加和获取。同时也提供remove方法删除key对应的value。当缓存达到预设值时,会调用trimToSize方法,把较早的缓存对象移除,以便存放新的缓存。
高分辨率的图片放入对应文件夹
内存优化方案
- 当Service完成任务后,尽量停止它,考虑IntentService,它执行完后自动退出,不像service手动调用stopservice退出
- ui不可见时,释放ui使用的资源onTrimMemory方法
- 内存紧张时,释放不重要的资源onTrimMemory方法
- 避免滥用Bitmap导致的内存浪费,根据分辨率加载Bitmap,使用Bitmap后要调用recycler释放bitmap在c内存中的内存,使用软引用bitmap,使用Lru缓存对Bitmap进行缓存算法
- 使用针对内存优化过的数据容器SparseArray,SparseBooleanArray 和 LongSparseArray,少用hasmap,少用枚举常量
- 避免使用依赖注入的框架
- 使用ZIP对齐的Apk
- 使用多进程