Android应用程序运行在Dalvik/ART虚拟机,并且每一个应用程序对应有一个单独的Dalvik虚拟机实例。
Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的不是class文件,而是dex文件。
Dalvik虚拟机与Java虚拟机共享有差不多的特性,差别在于两者执行的指令集是不一样的,前者的指令集是基于寄存器的,而后者的指令集是基于堆栈的。
下图,读者自行替换
垃圾回收算法的实现设计到大量的程序细节,并且每一个平台的虚拟机操作内存的方式都有不同,所以不需要去了解算法的实现
将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使
用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,
实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。
但是要注意:内存移动是必须实打实的移动(复制), 所 以 对 应 的 引用 用(直 直 接 指针 针)需 需 要 调 整。
复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。
一种更加优化的复制回收分代策略:具体做法是分配一块较大的 Eden 区和两块较小的 Survivor 空间(你可以叫做 From 或者 To,也可以叫做 Survivor1
和 Survivor2)
专门研究表明,新生代中的对象 98%是“朝生夕死”的,所以并不需要按照 1:1 的比例来划分内存空间,而是将内存分为一块较大的 Eden 空间和两块较
小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor[1]。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,
最后清理掉 Eden 和刚才用过的 Survivor 空间。
HotSpot 虚拟机默认 Eden 和 Survivor 的大小比例是 8:1,也就是每次新生代中可用内存空间为整个新生代容量的 90%(80%+10%),只有 10%的内存
会被“浪费”。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于 10%的对象存活,当 Survivor 空间不够用时,
需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)
算法分为“标记”和“清除”两个阶段:首先扫描所有对象标记出需要回收的对象,在标记完成后扫描回收所有被标记的对象,所以需要扫描两遍。
回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低。
它的主要问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连
续内存而不得不提前触发另一次垃圾回收动作。
回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代。
首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端
边界以外的内存。标记整理算法虽然 没有内存碎片,但是 效率偏低。
我们看到标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用
对象的地方都需要更新( 直接指针需要调整)。
所以看到,老年代采用的标记整理算法与标记清除算法,各有优点,各有缺点。
Android
给每个App
分配一个VM
,让App运行在dalvik
上,这样即使App
崩溃也不会影响到系统。系统给VM
分配了一定的内存大小,App
可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果应用超出VM
最大内存,就会出现内存溢出crash
。
由程序控制操作的内存空间在heap
上,分java heapsize
和native heapsize
Java申请的内存在vm heap
上,所以如果java
申请的内存大小超过VM
的逻辑内存限制,就会出现内存溢出的异常。
native层内存申请不受其限制,native
层受native process
对内存大小的限制
build.prop
,我们可以通过adb shell
在 命令行窗口查看 adb shell cat /system/build.prop
ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)
activityManager.getMemoryClass();//以m为单位
int AndroidRuntime::startVm(JavaVM** pJavaVM, JNIEnv** pEnv, bool zygote)
{
/*
* The default starting and maximum size of the heap. Larger
* values should be specified in a product property override.
*/
parseRuntimeOption("dalvik.vm.heapstartsize", heapstartsizeOptsBuf, "-Xms", "4m");
parseRuntimeOption("dalvik.vm.heapsize", heapsizeOptsBuf, "-Xmx", "16m");//修改这里
* }
修改 platform/dalvik/+/eclair-release/vm/Init.c
gDvm.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的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
用于存放静态的类和方法,持久代对垃圾回收没有显著影响。
总结:内存对象的处理过程如下:
Java中:
Young Generation区的gc算法是 mimor GC
Old Generation 区的gc算法是 full GC --做内存回收时尽量别在这里做,因为速度慢很多,耗时,卡
Android内存分配:
Dalvik:
Linear Alloc: 匿名共享内存
Zygote Space: Zygote相关信息
Alloc Space : 每个进程 独占
ART:
Non Moving Space :不可移动的
Zygote Space :Zygote相关信息
Alloc Space :每个进程独占
Image Space : 预加载的类信息
Large Obj Space :大对象 bitmap
系统在Young Generation、Old Generation上采用不同的回收机制。每一个Generation的内存区域都有固定的大小。随着新的对象陆续被分配到此区域,当对象总的大小临近这一级别内存区域的阈值时,会触发GC操作,以便腾出空间来存放其他新的对象。
执行GC占用的时间与Generation和Generation中的对象数量有关:
4、Young Generation GC
由于其对象存活时间短,因此基于Copying算法(扫描出存活的对象,并复制到一块新的完全未使用的控件中)来回收。新生代采用空闲指针的方式来控制GC触发,指针保持最后一个分配的对象在Young Generation区间的位置,当有新的对象要分配内存时,用于检查空间是否足够,不够就触发GC。
5、Old Generation GC
由于其对象存活时间较长,比较稳定,因此采用Mark(标记)算法(扫描出存活的对象,然后再回收未被标记的对象,回收后对空出的空间要么合并,要么标记出来便于下次分配,以减少内存碎片带来的效率损耗)来回收。
在Android系统中,GC有三种类型:
Anroid基于进程中运行的组件及其状态规定了默认的五个回收优先级:
系统需要进行内存回收时最先回收空进程,然后是后台进程,以此类推最后才会回收前台进程(一般情况下前台进程就是与用户交互的进程了,如果连前台进程都需要回收那么此时系统几乎不可用了)。
ActivityManagerService
会对所有进程进行评分(存放在变量adj中),然后再讲这个评分更新到内核,由内核去完成真正的内存回收( lowmemorykiller
, Oom_killer
)。这里只是大概的流程,中间过程还是很复杂的
OOM(OutOfMemoryError)内存溢出错误,在常见的Crash疑难排行榜上,OOM绝对可以名列前茅并且经久不衰。因为它发生时的Crash堆栈信息往往不是导致问题的根本原因,而只是压死骆驼的最后一根稻草
Android 虚拟机最终抛出OutOfMemoryError的地方
/art/runtime/thread.cc
void Thread::ThrowOutOfMemoryError(const char* msg) {
LOG(WARNING) << StringPrintf("Throwing OutOfMemoryError \"%s\"%s",
msg, (tls32_.throwing_OutOfMemoryError ? " (recursive case)" : ""));
if (!tls32_.throwing_OutOfMemoryError) {
tls32_.throwing_OutOfMemoryError = true;
ThrowNewException("Ljava/lang/OutOfMemoryError;", msg);
tls32_.throwing_OutOfMemoryError = false;
} else {
Dump(LOG_STREAM(WARNING)); // The pre-allocated OOME has no stack, so help out and log one.
SetException(Runtime::Current()->GetPreAllocatedOutOfMemoryError());
}
}
堆内存分配失败
/art/runtime/gc/heap.cc
void Heap::ThrowOutOfMemoryError(Thread* self, size_t byte_count, AllocatorType allocator_type) {
// If we're in a stack overflow, do not create a new exception. It would require running the
// constructor, which will of course still be in a stack overflow.
if (self->IsHandlingStackOverflow()) {
self->SetException(
Runtime::Current()->GetPreAllocatedOutOfMemoryErrorWhenHandlingStackOverflow());
return;
}
std::ostringstream oss;
size_t total_bytes_free = GetFreeMemory();
//为对象分配内存时达到进程的内存上限
oss << "Failed to allocate a " << byte_count << " byte allocation with " << total_bytes_free
<< " free bytes and " << PrettySize(GetFreeMemoryUntilOOME()) << " until OOM,"
<< " target footprint " << target_footprint_.load(std::memory_order_relaxed)
<< ", growth limit "
<< growth_limit_;
//没有足够大小的连续地址空间
// There is no fragmentation info to log for large-object space.
if (allocator_type != kAllocatorTypeLOS) {
CHECK(space != nullptr) << "allocator_type:" << allocator_type
<< " byte_count:" << byte_count
<< " total_bytes_free:" << total_bytes_free;
space->LogFragmentationAllocFailure(oss, byte_count);
}
}
创建线程失败
/art/runtime/thread.cc
void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
CHECK(java_peer != nullptr);
Thread* self = static_cast<JNIEnvExt*>(env)->GetSelf();
// TODO: remove from thread group?
env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0);
{
std::string msg(child_jni_env_ext.get() == nullptr ?
StringPrintf("Could not allocate JNI Env: %s", error_msg.c_str()) :
StringPrintf("pthread_create (%s stack) failed: %s",
PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
ScopedObjectAccess soa(env);
soa.Self()->ThrowOutOfMemoryError(msg.c_str());
}
常用的内存调优分析命令:
相关参数的说明:
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
资源路径和资源大小
功能: 获取所有进程的内存使用的排行榜,排行是以Pss
的大小而排序。procrank
命令比dumpsys meminfo
命令,能输出更详细的VSS/RSS/PSS/USS内存指标。
最后一行输出下面6个指标:
total | free | buffers | cached | shmem | slab |
---|---|---|---|---|---|
2857032K | 998088K | 78060K | 78060K | 312K | 92392K |
执行结果:
root@Phone:/# procrank
PID Vss Rss Pss Uss cmdline
4395 2270020K 202312K 136099K 121964K com.android.systemui
1192 2280404K 147048K 89883K 84144K system_server
29256 2145676K 97880K 44328K 40676K com.android.settings
501 1458332K 61876K 23609K 9736K zygote
4239 2105784K 68056K 21665K 19592K com.android.phone
479 164392K 24068K 17970K 15364K /system/bin/mediaserver
391 200892K 27272K 15930K 11664K /system/bin/surfaceflinger
...
RAM: 2857032K total, 998088K free, 78060K buffers, c cached, 312K shmem, 92392K slab
功能:能否查看更加详细的内存信息
指令: cat /proc/meminfo
输出结果如下(结果内存值不带小数点,此处添加小数点的目的是为了便于比对大小):
root@phone:/ # cat /proc/meminfo
MemTotal: 2857.032 kB //RAM可用的总大小 (即物理总内存减去系统预留和内核二进制代码大小)
MemFree: 1020.708 kB //RAM未使用的大小
Buffers: 75.104 kB //用于文件缓冲
Cached: 448.244 kB //用于高速缓存
SwapCached: 0 kB //用于swap缓存
Active: 832.900 kB //活跃使用状态,记录最近使用过的内存,通常不回收用于其它目的
Inactive: 391.128 kB //非活跃使用状态,记录最近并没有使用过的内存,能够被回收用于其他目的
Active(anon): 700.744 kB //Active = Active(anon) + Active(file)
Inactive(anon): 228 kB //Inactive = Inactive(anon) + Inactive(file)
Active(file): 132.156 kB
Inactive(file): 390.900 kB
Unevictable: 0 kB
Mlocked: 0 kB
SwapTotal: 524.284 kB //swap总大小
SwapFree: 524.284 kB //swap可用大小
Dirty: 0 kB //等待往磁盘回写的大小
Writeback: 0 kB //正在往磁盘回写的大小
AnonPages: 700.700 kB //匿名页,用户空间的页表,没有对应的文件
Mapped: 187.096 kB //文件通过mmap分配的内存,用于map设备、文件或者库
Shmem: .312 kB
Slab: 91.276 kB //kernel数据结构的缓存大小,Slab=SReclaimable+SUnreclaim
SReclaimable: 32.484 kB //可回收的slab的大小
SUnreclaim: 58.792 kB //不可回收slab的大小
KernelStack: 25.024 kB
PageTables: 23.752 kB //以最低的页表级
NFS_Unstable: 0 kB //不稳定页表的大小
Bounce: 0 kB
WritebackTmp: 0 kB
CommitLimit: 1952.800 kB
Committed_AS: 82204.348 kB //评估完成的工作量,代表最糟糕case下的值,该值也包含swap内存
VmallocTotal: 251658.176 kB //总分配的虚拟地址空间
VmallocUsed: 166.648 kB //已使用的虚拟地址空间
VmallocChunk: 251398.700 kB //虚拟地址空间可用的最大连续内存块
对于cache和buffer也是系统可以使用的内存。所以系统总的可用内存为 MemFree+Buffers+Cached
主功能:查看可用内存,缺省单位KB。该命令比较简单、轻量,专注于查看剩余内存情况。数据来源于/proc/meminfo。
输出结果:
root@phone:/proc/sys/vm # free
total used free shared buffers
Mem: 2857032 1836040 1020992 0 75104
-/+ buffers: 1760936 1096096
Swap: 524284 0 524284
Mem
行,存在的公式关系: total = used + free;-/+ buffers
行: 1760936 = 1836040 - 75104(buffers); 1096096 = 1020992 + 75104(buffers);主功能:用于查看虚拟地址区域的内存情况
用法: showmap -a [pid]
该命令的输出每一行代表一个虚拟地址区域(vm area)
主功能:不仅可以查看内存情况,还可以查看进程运行队列、系统切换、CPU时间占比等情况,另外该指令还是周期性地动态输出。
用法:
Usage: vmstat [ -n iterations ] [ -d delay ] [ -r header_repeat ]
-n iterations 数据循环输出的次数
-d delay 两次数据间的延迟时长(单位:S)
-r header_repeat 循环多少次,再输出一次头信息行
输入结果:
root@phone:/ # vmstat
procs memory system cpu
r b free mapped anon slab in cs flt us ni sy id wa ir
2 0 663436 232836 915192 113960 196 274 0 8 0 2 99 0 0
0 0 663444 232836 915108 113960 180 260 0 7 0 3 99 0 0
0 0 663476 232836 915216 113960 154 224 0 2 0 5 99 0 0
1 0 663132 232836 915304 113960 179 259 0 11 0 3 99 0 0
2 0 663124 232836 915096 113960 110 175 0 4 0 3 99 0 0
参数列总共15个参数,分为4大类:
dumpsys meminfo
适用场景: 查看进程的oom adj,或者dalvik/native等区域内存情况,或者某个进程或apk的内存情况,功能非常强大;procrank
适用场景: 查看进程的VSS/RSS/PSS/USS各个内存指标;cat /proc/meminfo
适用场景: 查看系统的详尽内存信息,包含内核情况;free
适用场景: 只查看系统的可用内存;showmap
适用场景: 查看进程的虚拟地址空间的内存分配情况;vmstat
适用场景: 周期性地打印出进程运行队列、系统切换、CPU时间占比等情况;https://developer.android.com/studio/profile/memory-profiler#performance
https://github.com/square/leakcanary
GC Log分为Dalvik和ART的GC日志
ART的日志与Dalvik的日志差距非常大,除了格式不同之外,打印的时间也不同,非要在慢GC时才打印
除了。下面我们看看这条ART GC Log:
Concurrent、Alloc、Explicit跟Dalvik的基本一样,这里就不重复介绍了。
NativeAlloc:Native内存分配时,比如为Bitmaps或者RenderScript分配对象, 这会导致Native内
存压力,从而触发GC。
Background:后台GC,触发是为了给后面的内存申请预留更多空间。
CollectorTransition:由堆转换引起的回收,这是运行时切换GC而引起的。收集器转换包括将所有
对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,收集器转换仅在以下情况下出现:
在内存较小的设备上,App将进程状态从可察觉的暂停状态变更为可察觉的非暂停状态(反之亦
然)。
HomogeneousSpaceCompact:齐性空间压缩是指空闲列表到压缩的空闲列表空间,通常发生在
当App已经移动到可察觉的暂停进程状态。这样做的主要原因是减少了内存使用并对堆内存进行碎
片整理。
DisableMovingGc:不是真正的触发GC原因,发生并发堆压缩时,由于使用了
GetPrimitiveArrayCritical,收集会被阻塞。一般情况下,强烈建议不要使用
GetPrimitiveArrayCritical,因为它在移动收集器方面具有限制。
HeapTrim:不是触发GC原因,但是请注意,收集会一直被阻塞,直到堆内存整理完毕。
Full:与Dalvik的FULL GC差不多。
Partial:跟Dalvik的局部GC差不多,策略时不包含Zygote Heap。
Sticky:另外一种局部中的局部GC,选择局部的策略是上次垃圾回收后新分配的对象。
mark sweep:先记录全部对象,然后从GC ROOT开始找出间接和直接的对象并标注。利用之前记
录的全部对象和标注的对象对比,其余的对象就应该需要垃圾回收了。
concurrent mark sweep:使用mark sweep采集器的并发GC。
mark compact:在标记存活对象的时候,所有的存活对象压缩到内存的一端,而另一端可以更加
高效地被回收。
semispace:在做垃圾扫描的时候,把所有引用的对象从一个空间移到另外一个空间,然后直接GC
剩余在旧空间中的对象即可。
通过GC日志,我们可以知道GC的量和它对卡顿的影响,也可以初步定位一些如主动调用GC、可分配的
内存不足、过多使用Weak Reference等问题。
开发 Android 应用时,请始终注意您的应用使用了多少随机存取存储器 (RAM)。尽管 Dalvik 和 ART 运行时会执行例行垃圾回收 (GC),您仍然需要了解应用何时以及在哪里分配和释放内存。为了提供稳定的用户体验,使 Android 操作系统能够在应用之间快速切换,请确保您的应用在用户不与其交互时不会消耗不必要的内存。
即使您在开发过程中遵循了管理应用的内存的所有最佳做法,您仍然可能泄漏对象或引入其他内存错误。唯一能够确定您的应用尽可能少地使用内存的方法是,利用本文介绍的工具分析应用的内存使用情况。
开始调查您的应用内存使用情况的最简单切入点是运行时日志消息。有时,发生垃圾回收时,您可以在 logcat 中查看消息。
在 Dalvik(而不是 ART)中,每次垃圾回收都会将以下信息打印到 logcat 中:
D/dalvikvm: , , ,
示例:
D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms
垃圾回收原因
什么触发了垃圾回收以及是哪种回收。可能出现的原因包括:
释放量
从此次垃圾回收中回收的内存量。
堆统计数据
堆的可用空间百分比与(活动对象数量)/(堆总大小)。
外部内存统计数据
API 级别 10 及更低级别的外部分配内存(已分配内存量)/(发生回收的限值)。
暂停时间
堆越大,暂停时间越长。并发暂停时间显示了两个暂停:一个出现在回收开始时,另一个出现在回收快要完成时。
在这些日志消息积聚时,请注意堆统计数据的增大(上面示例中的 3571K/9991K 值)。如果此值继续增大,可能会出现内存泄漏。
与 Dalvik 不同,ART 不会为未明确请求的垃圾回收记录消息。只有在认为垃圾回收速度较慢时才会打印垃圾回收。更确切地说,仅在垃圾回收暂停时间超过 5ms 或垃圾回收持续时间超过 100ms 时。如果应用未处于可察觉的暂停进程状态,那么其垃圾回收不会被视为较慢。始终会记录显式垃圾回收。
ART 会在其垃圾回收日志消息中包含以下信息:
I/art: () AllocSpace Objects, () LOS objects,
示例:
I/art : Explicit concurrent mark sweep GC freed 104710(7MB) AllocSpace objects, 21(416KB) LOS objects, 33% free, 25MB/38MB, paused 1.230ms total 67.216ms
在连接的设备或模拟器上启动您的应用。
选择 View > Tool Windows > Android Monitor。
在 Android Monitor 的左上角,选择 Monitors 标签。
图 1. Android Monitor 及其三个监视器:Memory、CPU 和 GPU。在 Android Studio 中,垂直放大 Android Monitor 面板可以看到 Network 监视器。
堆转储是应用堆中所有对象的快照。堆转储以一种名称为 HPROF 的二进制格式存储,您可以将其上传到分析工具(如 jhat)中。应用的堆转储包含应用堆整体状态的相关信息,以便您能够跟踪在查看堆更新时发现的问题。
在 Memory 监视器的顶部,点击 Dump Java Heap
Android Studio 会创建一个文件名为 application-id_yyyy.mm.dd_hh.mm.hprof 的堆快照文件,在 Android Studio 中打开文件,然后将文件添加到 Captures 标签的 Heap Snapshot 列表中。
在 Captures 标签中,右键点击文件,然后选择 Export to standard .hprof。
注:如果您需要更确切地了解转储的创建时间,可以通过调用 dumpHprofData() 在应用代码的关键点创建堆转储。
使用 Android Monitor 在您与应用交互时查看应用堆的实时更新。实时更新提供了为不同应用操作分配的内存量的相关信息。您可以利用此信息确定是否任何操作占用了过多内存以及是否需要调整以减少占用的内存量。
与您的应用交互,在 Memory 监视器中,查看 Free 和 Alloated 内存。
点击 Dump Java Heap
在 Captures 标签中,双击堆快照文件以打开 HPROF 查看器。
要引起堆分配,请与您的应用交互,然后点击 Initiate GC
继续与您的应用交互,然后启动垃圾回收。观察每次垃圾回收的堆分配更新。确定应用中哪些操作导致过多分配,以及您可以从何处减少分配和释放资源。
堆转储使用与 Java HPROF 工具中类似但不相同的格式提供。Android 堆转储的主要区别是在 Zygote 进程中进行了大量的分配。因为 Zygote 分配在所有应用进程之间分享,所以它们对您自己的堆分析影响不太大。
要分析堆转储,您可以使用标准工具,如 jhat。要使用 jhat,您需要将 HPROF 文件从 Android 格式转换为 Java SE HPROF 格式。要转换为 Java SE HPROF 格式,请使用 ANDROID_SDK/platform-tools/ 目录中提供的 hprof-conv 工具。运行包括两个参数的 hprof-conv 命令:原始 HPROF 文件和转换的 HPROF 文件的写入位置。例如:
hprof-conv heap-original.hprof heap-converted.hprof
您可以将转换的文件加载到可以识别 Java SE HPROF 格式的堆分析工具中。分析期间,请注意由下列任意情况引起的内存泄漏:
长时间引用 Activity、Context、View、Drawable 和其他对象,可能会保持对 Activity 或 Context 容器的引用。
可以保持 Activity 实例的非静态内部类,如 Runnable。
对象保持时间比所需时间长的缓存。
跟踪内存分配可以让您更好地了解分配占用内存的对象的位置。您可以使用分配跟踪器查看特定的内存使用以及分析应用中的关键代码路径,如滚动。
例如,您可以使用分配跟踪器在应用中滑动列表时跟踪分配。跟踪让您可以看到滑动列表所需的所有内存分配,内存分配位于哪些线程上,以及内存分配来自何处。此类信息可以帮助您简化执行路径以减少执行的工作,从而改进应用的整体操作及其界面。
尽管不必要甚至也不可能将所有内存分配从您的性能关键型代码路径中移除,分配跟踪器仍可以帮助您识别代码中的重要问题。例如,应用可以在每次绘制时创建一个新的 Paint 对象。将 Paint 对象全局化是一个有助于提高性能的简单解决方法。
在连接的设备或模拟器上启动您的应用。
在 Android Studio 中,选择 View > Tool Windows > Android Monitor。
在 Android Monitor 的左上角,选择 Monitors 标签。
在内存监视器工具栏中,点击“Allocation Tracker” 开始内存分配。
与您的应用交互。
再次点击“Allocation Tracker” 停止分配跟踪。
Android Studio 会创建一个文件名为 application-id_yyyy.mm.dd_hh.mm.alloc 的分配文件,在 Android Studio 中打开该文件,然后将文件添加到 Captures 标签内的 Allocations 列表中。
在分配文件中,确定您的应用中哪些操作可能会引起过多分配,并确定应在应用中什么位置尝试减少分配和释放资源。
如需了解有关使用分配跟踪器的详细信息,请参阅分配跟踪器。
为了进一步分析,您可能想要使用下面的 adb 命令观察应用内存在不同类型的 RAM 分配之间的划分情况:
adb shell dumpsys meminfo
-d 标志会打印与 Dalvik 和 ART 内存使用情况相关的更多信息。
输出列出了应用的所有当前分配,单位为千字节。
检查此信息时,您应熟悉下列类型的分配:
私有(干净和脏)RAM
这是仅由您的进程使用的内存。这是您的应用进程被破坏时系统可以回收的 RAM 量。通常情况下,最重要的部分是私有脏 RAM,它的开销最大,因为只有您的进程使用它,而且其内容仅存在于 RAM 中,所以无法被分页以进行存储(因为 Android 不使用交换)。所有的 Dalvik 和您进行的原生堆分配都将是私有脏 RAM;您与 Zygote 进程共享的 Dalvik 和原生分配是共享的脏 RAM。
按比例分配占用内存 (PSS)
这表示您的应用的 RAM 使用情况,考虑了在各进程之间共享 RAM 页的情况。您的进程独有的任何 RAM 页会直接影响其 PSS 值,而与其他进程共享的 RAM 页仅影响与共享量成比例的 PSS 值。例如,两个进程之间共享的 RAM 页会将其一半的大小贡献给每个进程的 PSS。
PSS 结果一个比较好的特性是,您可以将所有进程的 PSS 相加来确定所有进程正在使用的实际内存。这意味着 PSS 适合测定进程的实际 RAM 比重和比较其他进程的 RAM 使用情况与可用总 RAM。
例如,下面是 Nexus 5 设备上地图进程的输出。此处信息较多,但讨论的关键点如下所示。
adb shell dumpsys meminfo com.google.android.apps.maps -d
注:您看到的信息可能会与此处显示的信息稍有不同,因为输出的一些详细信息在不同平台版本之间会有所不同。
** MEMINFO in pid 18227 [com.google.android.apps.maps] **
Pss Private Private Swapped Heap Heap Heap
Total Dirty Clean Dirty Size Alloc Free
------ ------ ------ ------ ------ ------ ------
Native Heap 10468 10408 0 0 20480 14462 6017
Dalvik Heap 34340 33816 0 0 62436 53883 8553
Dalvik Other 972 972 0 0
Stack 1144 1144 0 0
Gfx dev 35300 35300 0 0
Other dev 5 0 4 0
.so mmap 1943 504 188 0
.apk mmap 598 0 136 0
.ttf mmap 134 0 68 0
.dex mmap 3908 0 3904 0
.oat mmap 1344 0 56 0
.art mmap 2037 1784 28 0
Other mmap 30 4 0 0
EGL mtrack 73072 73072 0 0
GL mtrack 51044 51044 0 0
Unknown 185 184 0 0
TOTAL 216524 208232 4384 0 82916 68345 14570
Dalvik Details
.Heap 6568 6568 0 0
.LOS 24771 24404 0 0
.GC 500 500 0 0
.JITCache 428 428 0 0
.Zygote 1093 936 0 0
.NonMoving 1908 1908 0 0
.IndirectRef 44 44 0 0
Objects
Views: 90 ViewRootImpl: 1
AppContexts: 4 Activities: 1
Assets: 2 AssetManagers: 2
Local Binders: 21 Proxy Binders: 28
Parcel memory: 18 Parcel count: 74
Death Recipients: 2 OpenSSL Sockets: 2
下面是 Gmail 应用的 Dalvik 上一个较旧版本的 dumpsys:
** MEMINFO in pid 9953 [com.google.android.gm] **
Pss Pss Shared Private Shared Private Heap Heap Heap
Total Clean Dirty Dirty Clean Clean Size Alloc Free
------ ------ ------ ------ ------ ------ ------ ------ ------
Native Heap 0 0 0 0 0 0 7800 7637(6) 126
Dalvik Heap 5110(3) 0 4136 4988(3) 0 0 9168 8958(6) 210
Dalvik Other 2850 0 2684 2772 0 0
Stack 36 0 8 36 0 0
Cursor 136 0 0 136 0 0
Ashmem 12 0 28 0 0 0
Other dev 380 0 24 376 0 4
.so mmap 5443(5) 1996 2584 2664(5) 5788 1996(5)
.apk mmap 235 32 0 0 1252 32
.ttf mmap 36 12 0 0 88 12
.dex mmap 3019(5) 2148 0 0 8936 2148(5)
Other mmap 107 0 8 8 324 68
Unknown 6994(4) 0 252 6992(4) 0 0
TOTAL 24358(1) 4188 9724 17972(2)16388 4260(2)16968 16595 336
Objects
Views: 426 ViewRootImpl: 3(8)
AppContexts: 6(7) Activities: 2(7)
Assets: 2 AssetManagers: 2
Local Binders: 64 Proxy Binders: 34
Death Recipients: 0
OpenSSL Sockets: 1
SQL
MEMORY_USED: 1739
PAGECACHE_OVERFLOW: 1164 MALLOC_SIZE: 62
通常情况下,仅需关注 Pss Total 和 Private Dirty 列。一些情况下,Private Clean 和 Heap Alloc 列提供的数据也需要关注。您需要关注的不同内存分配(各行)的详细信息如下:
Heap Alloc 是 Dalvik 和原生堆分配器为您的应用跟踪的内存量。此值大于 Pss Total 和 Private Dirty,因为您的进程从 Zygote 派生,且包含您的进程与所有其他进程共享的分配。
.so mmap 和 .dex mmap
映射的 .so(原生)和 .dex(Dalvik 或 ART)代码占用的 RAM。Pss Total 数值包括应用之间共享的平台代码;Private Clean 是您的应用自己的代码。通常情况下,实际映射的内存更大 - 此处的 RAM 仅为应用执行的代码当前所需的 RAM。不过,.so mmap 具有较大的私有脏 RAM,因为在加载到其最终地址时对原生代码进行了修改。
.oat mmap
这是代码映像占用的 RAM 量,根据多个应用通常使用的预加载类计算。此映像在所有应用之间共享,不受特定应用影响。
.art mmap
这是堆映像占用的 RAM 量,根据多个应用通常使用的预加载类计算。此映像在所有应用之间共享,不受特定应用影响。尽管 ART 映像包含 Object 实例,它仍然不会计入您的堆大小。
.Heap(仅带有 -d 标志)
这是您的应用的堆内存量。不包括映像中的对象和大型对象空间,但包括 zygote 空间和非移动空间。
.LOS(仅带有 -d 标志)
这是由 ART 大型对象空间占用的 RAM 量。包括 zygote 大型对象。大型对象是所有大于 12KB 的原语数组分配。
.GC(仅带有 -d 标志)
这是内部垃圾回收量(考虑了应用开销)。真的没有任何办法减少这一开销。
.JITCache(仅带有 -d 标志)
这是 JIT 数据和代码缓存占用的内存量。通常情况下为 0,因为所有的应用都会在安装时编译。
.Zygote(仅带有 -d 标志)
这是 zygote 空间占用的内存量。zygote 空间在设备启动时创建且永远不会被分配。
.NonMoving(仅带有 -d 标志)
这是由 ART 非移动空间占用的 RAM 量。非移动空间包含特殊的不可移动对象,例如字段和方法。您可以通过在应用中使用更少的字段和方法来减少这一部分。
.IndirectRef(仅带有 -d 标志)
这是由 ART 间接引用表占用的 RAM 量。通常情况下,此量较小,但如果很高,可以通过减少使用的本地和全局 JNI 引用数量来减少此 RAM 量。
Unknown
系统无法将其分类到其他更具体的一个项中的任何 RAM 页。当前,此类 RAM 页主要包含原生分配,由于地址空间布局随机化 (ASLR) 而无法在收集此数据时通过工具识别。与 Dalvik 堆相同,Unknown 的 Pss Total 考虑了与 Zygote 的共享,且 Private Dirty 是仅由您的应用占有的未知 RAM。
TOTAL
您的进程占用的按比例分配占用内存 (PSS) 总量。等于上方所有 PSS 字段的总和。表示您的进程占用的内存量占整体内存的比重,可以直接与其他进程和可用总 RAM 比较。
Private Dirty 和 Private Clean 是您的进程中的总分配,未与其他进程共享。它们(尤其是 Private Dirty)等于您的进程被破坏后将释放回系统中的 RAM 量。脏 RAM 是因为已被修改而必须保持在 RAM 中的 RAM 页(因为没有交换);干净 RAM 是已从某个持久性文件(例如正在执行的代码)映射的 RAM 页,如果一段时间不用,可以移出分页。
ViewRootImpl
您的进程中当前活动的根视图数量。每个根视图都与一个窗口关联,因此有助于您确定涉及对话框或其他窗口的内存泄漏。
AppContexts 和 Activities
您的进程中当前活动的应用 Context 和 Activity 对象数量。这可以帮助您快速确定由于存在静态引用(比较常见)而无法进行垃圾回收的已泄漏 Activity 对象。这些对象经常拥有很多关联的其他分配,因此成为跟踪大型内存泄漏的一种不错的方式。
注:View 或 Drawable 对象也会保持对其源 Activity 的引用,因此保持 View 或 Drawable 对象也会导致您的应用泄漏 Activity。
使用上面介绍的工具时,您应积极地对自己的应用代码进行测试并尝试强制内存泄漏。在应用中引发内存泄漏的一种方式是,先让其运行一段时间,然后再检查堆。泄漏在堆中将逐渐汇聚到分配顶部。不过,泄漏越小,您越需要运行更长时间的应用才能看到泄漏。
您还可以通过以下方式之一触发内存泄漏:
单个像素的字节大小由Bitmap的一个可配置的参数Config来决定。
Bitmap中,存在一个枚举类Config,定义了Android中支持的Bitmap配置:
getByteCount()
getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。API19开始
getAllocationByteCount()方法代替了getByteCount()。
getAllocationByteCount()
API19之后,Bitmap加了一个Api:getAllocationByteCount();代表在内存中为Bitmap分配的内存大
小。
public final int getAllocationByteCount() {
if (mBuffer == null) {
//mBuffer代表存储Bitmap像素数据的字节数组。
return getByteCount();
}
return mBuffer.length;
}
Bitmap作为位图,需要读入一张图片每一个像素点的数据,其主要占用内存的地方也正是这些像素数
据。对于像素数据总大小,我们可以猜想为:像素总数量 × 每个像素的字节大小,而像素总数量在矩形
屏幕表现下,应该是:横向像素数量 × 纵向像素数量,结合得到:
Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 × 纵向像素数量 × 每个像素的字节大小
但真是如此吗?
我们来看下源码,Bitmap的decode过程实际上是在native层完成的,为此,需要从
BitmapFactory.cpp#nativeDecodeXXX方法开始跟踪,最终在doDecode方法里面
static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable>
stream,
jobject padding, jobject options) {
// Set default values for the options parameters.
int sampleSize = 1;
bool onlyDecodeSize = false;
SkColorType prefColorType = kN32_SkColorType;
bool isHardware = false;
bool isMutable = false;
float scale = 1.0f;
bool requireUnpremultiplied = false;
jobject javaBitmap = NULL;
sk_sp<SkColorSpace> prefColorSpace = nullptr;
// Update with options supplied by the client.
if (options != NULL) {
sampleSize = env->GetIntField(options, gOptions_sampleSizeFieldID);
// Correct a non-positive sampleSize. sampleSize defaults to zero
within the
// options object, which is strange.
if (sampleSize <= 0) {
sampleSize = 1;
}
if (env->GetBooleanField(options, gOptions_justBoundsFieldID)) {
onlyDecodeSize = true;
}
// initialize these, in case we fail later on
env->SetIntField(options, gOptions_widthFieldID, -1);
env->SetIntField(options, gOptions_heightFieldID, -1);
env->SetObjectField(options, gOptions_mimeFieldID, 0);
env->SetObjectField(options, gOptions_outConfigFieldID, 0);
env->SetObjectField(options, gOptions_outColorSpaceFieldID, 0);
jobject jconfig = env->GetObjectField(options,
gOptions_configFieldID);
prefColorType = GraphicsJNI::getNativeBitmapColorType(env,
jconfig);
jobject jcolorSpace = env->GetObjectField(options,
gOptions_colorSpaceFieldID);
prefColorSpace = GraphicsJNI::getNativeColorSpace(env,
jcolorSpace);
isHardware = GraphicsJNI::isHardwareConfig(env, jconfig);
isMutable = env->GetBooleanField(options, gOptions_mutableFieldID);
requireUnpremultiplied = !env->GetBooleanField(options,
gOptions_premultipliedFieldID);
javaBitmap = env->GetObjectField(options, gOptions_bitmapFieldID);
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options,
gOptions_densityFieldID);//TODO: 1
const int targetDensity = env->GetIntField(options,
gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options,
gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density !=
screenDensity) {
scale = (float) targetDensity / density;
}
}
}
if (isMutable && isHardware) {
doThrowIAE(env, "Bitmaps with Config.HARWARE are always
immutable");
return nullObjectReturn("Cannot create mutable hardware bitmap");
}
// Create the codec.
NinePatchPeeker peeker;
std::unique_ptr<SkAndroidCodec> codec;
{
SkCodec::Result result;
std::unique_ptr<SkCodec> c =
SkCodec::MakeFromStream(std::move(stream), &result,
&peeker);
if (!c) {
SkString msg;
msg.printf("Failed to create image decoder with message '%s'",
SkCodec::ResultToString(result));
return nullObjectReturn(msg.c_str());
}
codec = SkAndroidCodec::MakeFromCodec(std::move(c));
if (!codec) {
return nullObjectReturn("SkAndroidCodec::MakeFromCodec returned
null");
}
}
// Do not allow ninepatch decodes to 565. In the past, decodes to 565
// would dither, and we do not want to pre-dither ninepatches, since we
// know that they will be stretched. We no longer dither 565 decodes,
// but we continue to prevent ninepatches from decoding to 565, in
order
// to maintain the old behavior.
if (peeker.mPatch && kRGB_565_SkColorType == prefColorType) {
prefColorType = kN32_SkColorType;
}
// Determine the output size.
SkISize size = codec->getSampledDimensions(sampleSize);
//TODO: 2
int scaledWidth = size.width();
int scaledHeight = size.height();
bool willScale = false;
// Apply a fine scaling step if necessary.
if (needsFineScale(codec->getInfo().dimensions(), size, sampleSize)) {
willScale = true;
scaledWidth = codec->getInfo().width() / sampleSize;
scaledHeight = codec->getInfo().height() / sampleSize;
}
// Set the decode colorType
SkColorType decodeColorType = codec-
>computeOutputColorType(prefColorType);
sk_sp<SkColorSpace> decodeColorSpace = codec->computeOutputColorSpace(
decodeColorType, prefColorSpace);
// Set the options and return if the client only wants the size.
if (options != NULL) {
jstring mimeType = encodedFormatToString(
env, (SkEncodedImageFormat)codec->getEncodedFormat());
if (env->ExceptionCheck()) {
return nullObjectReturn("OOM in encodedFormatToString()");
}
env->SetIntField(options, gOptions_widthFieldID, scaledWidth);
env->SetIntField(options, gOptions_heightFieldID, scaledHeight);
env->SetObjectField(options, gOptions_mimeFieldID, mimeType);
jint configID =
GraphicsJNI::colorTypeToLegacyBitmapConfig(decodeColorType);
if (isHardware) {
configID = GraphicsJNI::kHardware_LegacyBitmapConfig;
}
jobject config = env->CallStaticObjectMethod(gBitmapConfig_class,
gBitmapConfig_nativeToConfigMethodID, configID);
env->SetObjectField(options, gOptions_outConfigFieldID, config);
env->SetObjectField(options, gOptions_outColorSpaceFieldID,
GraphicsJNI::getColorSpace(env, decodeColorSpace,
decodeColorType));
if (onlyDecodeSize) {
return nullptr;
}
}
// Scale is necessary due to density differences.
if (scale != 1.0f) {
willScale = true;
scaledWidth = static_cast<int>(scaledWidth * scale + 0.5f);
scaledHeight = static_cast<int>(scaledHeight * scale + 0.5f);
}
android::Bitmap* reuseBitmap = nullptr;
unsigned int existingBufferSize = 0;
if (javaBitmap != NULL) {
reuseBitmap = &bitmap::toBitmap(env, javaBitmap);
if (reuseBitmap->isImmutable()) {
ALOGW("Unable to reuse an immutable bitmap as an image decoder
target.");
javaBitmap = NULL;
reuseBitmap = nullptr;
} else {
existingBufferSize = bitmap::getBitmapAllocationByteCount(env,
javaBitmap);
}
}
HeapAllocator defaultAllocator;
RecyclingPixelAllocator recyclingAllocator(reuseBitmap,
existingBufferSize);
ScaleCheckingAllocator scaleCheckingAllocator(scale,
existingBufferSize);
SkBitmap::HeapAllocator heapAllocator;
SkBitmap::Allocator* decodeAllocator;
if (javaBitmap != nullptr && willScale) {
// This will allocate pixels using a HeapAllocator, since there
will be an extra
// scaling step that copies these pixels into Java memory. This
allocator
// also checks that the recycled javaBitmap is large enough.
decodeAllocator = &scaleCheckingAllocator;
} else if (javaBitmap != nullptr) {
decodeAllocator = &recyclingAllocator;
} else if (willScale || isHardware) {
// This will allocate pixels using a HeapAllocator,
// for scale case: there will be an extra scaling step.
// for hardware case: there will be extra swizzling & upload to
gralloc step.
decodeAllocator = &heapAllocator;
} else {
decodeAllocator = &defaultAllocator;
}
SkAlphaType alphaType = codec-
>computeOutputAlphaType(requireUnpremultiplied);
const SkImageInfo decodeInfo = SkImageInfo::Make(size.width(),
size.height(),
decodeColorType, alphaType, decodeColorSpace);
// For wide gamut images, we will leave the color space on the
SkBitmap. Otherwise,
// use the default.
SkImageInfo bitmapInfo = decodeInfo;
if (decodeInfo.colorSpace() && decodeInfo.colorSpace()->isSRGB()) {
bitmapInfo =
bitmapInfo.makeColorSpace(GraphicsJNI::colorSpaceForType(decodeColorType));
}
if (decodeColorType == kGray_8_SkColorType) {
// The legacy implementation of BitmapFactory used kAlpha8 for
// grayscale images (before kGray8 existed). While the codec
// recognizes kGray8, we need to decode into a kAlpha8 bitmap
// in order to avoid a behavior change.
bitmapInfo =
bitmapInfo.makeColorType(kAlpha_8_SkColorType).makeAlphaType(kPremul_SkAlp
haType);
}
SkBitmap decodingBitmap;
if (!decodingBitmap.setInfo(bitmapInfo) ||
!decodingBitmap.tryAllocPixels(decodeAllocator)) {
// SkAndroidCodec should recommend a valid SkImageInfo, so
setInfo()
// should only only fail if the calculated value for rowBytes is
too
// large.
// tryAllocPixels() can fail due to OOM on the Java heap, OOM on
the
// native heap, or the recycled javaBitmap being too small to
reuse.
return nullptr;
}
// Use SkAndroidCodec to perform the decode.
SkAndroidCodec::AndroidOptions codecOptions;
codecOptions.fZeroInitialized = decodeAllocator == &defaultAllocator ?
SkCodec::kYes_ZeroInitialized : SkCodec::kNo_ZeroInitialized;
codecOptions.fSampleSize = sampleSize;
SkCodec::Result result = codec->getAndroidPixels(decodeInfo,
decodingBitmap.getPixels(),
decodingBitmap.rowBytes(), &codecOptions);
switch (result) {
case SkCodec::kSuccess:
case SkCodec::kIncompleteInput:
break;
default:
return nullObjectReturn("codec->getAndroidPixels() failed.");
}
// This is weird so let me explain: we could use the scale parameter
// directly, but for historical reasons this is how the corresponding
// Dalvik code has always behaved. We simply recreate the behavior
here.
// The result is slightly different from simply using scale because of
// the 0.5f rounding bias applied when computing the target image size
const float scaleX = scaledWidth / float(decodingBitmap.width());
const float scaleY = scaledHeight / float(decodingBitmap.height());
jbyteArray ninePatchChunk = NULL;
if (peeker.mPatch != NULL) {
if (willScale) {
peeker.scale(scaleX, scaleY, scaledWidth, scaledHeight);
}
size_t ninePatchArraySize = peeker.mPatch->serializedSize();
ninePatchChunk = env->NewByteArray(ninePatchArraySize);
if (ninePatchChunk == NULL) {
return nullObjectReturn("ninePatchChunk == null");
}
jbyte* array = (jbyte*) env-
>GetPrimitiveArrayCritical(ninePatchChunk, NULL);
if (array == NULL) {
return nullObjectReturn("primitive array == null");
}
memcpy(array, peeker.mPatch, peeker.mPatchSize);
env->ReleasePrimitiveArrayCritical(ninePatchChunk, array, 0);
}
jobject ninePatchInsets = NULL;
if (peeker.mHasInsets) {
ninePatchInsets = peeker.createNinePatchInsets(env, scale);
if (ninePatchInsets == NULL) {
return nullObjectReturn("nine patch insets == null");
}
if (javaBitmap != NULL) {
env->SetObjectField(javaBitmap, gBitmap_ninePatchInsetsFieldID,
ninePatchInsets);
}
}
SkBitmap outputBitmap;
if (willScale) {
// Set the allocator for the outputBitmap.
SkBitmap::Allocator* outputAllocator;
if (javaBitmap != nullptr) {
outputAllocator = &recyclingAllocator;
} else {
outputAllocator = &defaultAllocator;
}
SkColorType scaledColorType = decodingBitmap.colorType();
// FIXME: If the alphaType is kUnpremul and the image has alpha,
the
// colors may not be correct, since Skia does not yet support
drawing
// to/from unpremultiplied bitmaps.
outputBitmap.setInfo(
bitmapInfo.makeWH(scaledWidth,
scaledHeight).makeColorType(scaledColorType));
if (!outputBitmap.tryAllocPixels(outputAllocator)) {
// This should only fail on OOM. The recyclingAllocator should
have
// enough memory since we check this before decoding using the
// scaleCheckingAllocator.
return nullObjectReturn("allocation failed for scaled bitmap");
}
SkPaint paint;
// kSrc_Mode instructs us to overwrite the uninitialized pixels in
// outputBitmap. Otherwise we would blend by default, which is not
// what we want.
paint.setBlendMode(SkBlendMode::kSrc);
paint.setFilterQuality(kLow_SkFilterQuality); // bilinear filtering
SkCanvas canvas(outputBitmap, SkCanvas::ColorBehavior::kLegacy);
canvas.scale(scaleX, scaleY);
canvas.drawBitmap(decodingBitmap, 0.0f, 0.0f, &paint);
} else {
outputBitmap.swap(decodingBitmap);
}
if (padding) {
peeker.getPadding(env, padding);
}
// If we get here, the outputBitmap should have an installed pixelref.
if (outputBitmap.pixelRef() == NULL) {
return nullObjectReturn("Got null SkPixelRef");
}
if (!isMutable && javaBitmap == NULL) {
// promise we will never change our pixels (great for sharing and
pictures)
outputBitmap.setImmutable();
}
bool isPremultiplied = !requireUnpremultiplied;
if (javaBitmap != nullptr) {
bitmap::reinitBitmap(env, javaBitmap, outputBitmap.info(),
isPremultiplied);
outputBitmap.notifyPixelsChanged();
// If a java bitmap was passed in for reuse, pass it back
return javaBitmap;
}
int bitmapCreateFlags = 0x0;
if (isMutable) bitmapCreateFlags |=
android::bitmap::kBitmapCreateFlag_Mutable;
if (isPremultiplied) bitmapCreateFlags |=
android::bitmap::kBitmapCreateFlag_Premultiplied;
if (isHardware) {
sk_sp<Bitmap> hardwareBitmap =
Bitmap::allocateHardwareBitmap(outputBitmap);
if (!hardwareBitmap.get()) {
return nullObjectReturn("Failed to allocate a hardware
bitmap");
}
return bitmap::createBitmap(env, hardwareBitmap.release(),
bitmapCreateFlags,
ninePatchChunk, ninePatchInsets, -1);
}
// now create the java bitmap
return bitmap::createBitmap(env,
defaultAllocator.getStorageObjAndReset(),
bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1);
}
static jobject nativeDecodeStream(JNIEnv* env, jobject clazz, jobject is,
jbyteArray storage,
jobject padding, jobject options) {
jobject bitmap = NULL;
std::unique_ptr<SkStream> stream(CreateJavaInputStreamAdaptor(env, is,
storage));
if (stream.get()) {
std::unique_ptr<SkStreamRewindable> bufferedStream(
SkFrontBufferedStream::Make(std::move(stream),
SkCodec::MinBufferedBytesNeeded()));
SkASSERT(bufferedStream.get() != NULL);
bitmap = doDecode(env, std::move(bufferedStream), padding,
options);
}
return bitmap;
}
。。。省略多余代码
if (env->GetBooleanField(options, gOptions_scaledFieldID)) {
const int density = env->GetIntField(options, gOptions_densityFieldID);
const int targetDensity = env->GetIntField(options,
gOptions_targetDensityFieldID);
const int screenDensity = env->GetIntField(options,
gOptions_screenDensityFieldID);
if (density != 0 && targetDensity != 0 && density != screenDensity) {
scale = (float) targetDensity / density;
}
}
...
int scaledWidth = decoded->width();
int scaledHeight = decoded->height();
if (willScale && mode != SkImageDecoder::kDecodeBounds_Mode) {
scaledWidth = int(scaledWidth * scale + 0.5f);
scaledHeight = int(scaledHeight * scale + 0.5f);
}
...
if (willScale) {
const float sx = scaledWidth / float(decoded->width());
const float sy = scaledHeight / float(decoded->height());
bitmap->setConfig(decoded->getConfig(), scaledWidth, scaledHeight);
bitmap->allocPixels(&javaAllocator, NULL);
bitmap->eraseColor(0);
SkPaint paint;
paint.setFilterBitmap(true);
SkCanvas canvas(*bitmap);
canvas.scale(sx, sy);
canvas.drawBitmap(*decoded, 0.0f, 0.0f, &paint);
}
从上述代码中,我们看到bitmap最终通过canvas绘制出来,而canvas在绘制之前,有一个scale的操
作,scale的值由
scale = (float) targetDensity / density;
这一行代码决定,即缩放的倍率和targetDensity和density相关,而这两个参数都是从传入的options中
获取到的
可以配置gradle,例如:只打xxhdpi进apk–>resConfigs “xxhdpi”
如图片解压缩出现OOM的情况,可尝试(伪代码):
try{
decode bitmap
}catch(OutOfMemoryError e){
质量压缩后再次去解码
}
在有些系统上可以使用epic库去hook图片的解压的过程,全盘监控图片加载这块,在这过程中做处理,从而做这方面的优化。
对于资源性对象不再使用时,应该立即调用它的close()函数,将其关闭,然后再置为null。例如Bitmap等资源未关闭会造成内存泄漏,此时我们应该在Activity销毁时及时关闭。
例如BraodcastReceiver、EventBus未注销造成的内存泄漏,我们应该在Activity销毁时及时注销。
尽量避免使用静态变量存储数据,特别是大数据对象,建议使用数据库存储。
优先使用Application的Context,如需使用Activity的Context,可以在传入Context时使用弱引用进行封装,然后,在使用到的地方从弱引用中获取Context,如果获取不到,则直接return即可。
该实例的生命周期和应用一样长,这就导致该静态实例一直持有该Activity的引用,Activity的内存资源不能正常回收。此时,我们可以将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,尽量使用Application Context,如果需要使用Activity Context,就记得用完后置空让GC可以回收,否则还是会内存泄漏。
Message发出之后存储在MessageQueue中,在Message中存在一个target,它是Handler的一个引用,Message在Queue中存在的时间过长,就会导致Handler无法被回收。如果Handler是非静态的,则会导致Activity或者Service不会被回收。并且消息队列是在一个Looper线程中不断地轮询处理消息,当这个Activity退出时,消息队列中还有未处理的消息或者正在处理的消息,并且消息队列中的Message持有Handler实例的引用,Handler又持有Activity的引用,所以导致该Activity的内存资源无法及时回收,引发内存泄漏。解决方案如下所示:
需要注意的是,AsyncTask内部也是Handler机制,同样存在内存泄漏风险,但其一般是临时性的。对于类似AsyncTask或是线程造成的内存泄漏,我们也可以将AsyncTask和Runnable类独立出来或者使用静态内部类。
在退出程序之前,将集合里的东西clear,然后置为null,再退出程序
WebView都存在内存泄漏的问题,在应用中只要使用一次WebView,内存就不会被释放掉。我们可以为WebView开启一个独立的进程,使用AIDL与应用的主进程进行通信,WebView所在的进程可以根据业务的需要选择合适的时机进行销毁,达到正常释放内存的目的。
在构造Adapter时,使用缓存的convertView。