Android之内存优化与OOM

很久没写博客了,直接上干货:

Android应用程序运行在Dalvik/ART虚拟机,并且每一个应用程序对应有一个单独的Dalvik虚拟机实例。
Dalvik虚拟机实则也算是一个Java虚拟机,只不过它执行的不是class文件,而是dex文件。
Dalvik虚拟机与Java虚拟机共享有差不多的特性,差别在于两者执行的指令集是不一样的,前者的指令集是基于寄存器的,而后者的指令集是基于堆栈的。
下图,读者自行替换
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

总结归类下:

在这里插入图片描述

垃 圾 回 收 算 法

垃圾回收算法的实现设计到大量的程序细节,并且每一个平台的虚拟机操作内存的方式都有不同,所以不需要去了解算法的实现

复 制 算 法( (Copying) )

将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使
用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要按顺序分配内存即可,
实现简单,运行高效。只是这种算法的代价是将内存缩小为了原来的一半。
但是要注意:内存移动是必须实打实的移动(复制), 所 以 对 应 的 引用 用(直 直 接 指针 针)需 需 要 调 整。
复制回收算法适合于新生代,因为大部分对象朝生夕死,那么复制过去的对象比较少,效率自然就高,另外一半的一次性清理是很快的。

在这里插入图片描述

Appel 式回收

一种更加优化的复制回收分代策略:具体做法是分配一块较大的 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)
在这里插入图片描述

标记- 清除算法(Mark-Sweep) )

算法分为“标记”和“清除”两个阶段:首先扫描所有对象标记出需要回收的对象,在标记完成后扫描回收所有被标记的对象,所以需要扫描两遍。
回收效率略低,如果大部分对象是朝生夕死,那么回收效率降低,因为需要大量标记对象和回收对象,对比复制回收效率要低。
它的主要问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连
续内存而不得不提前触发另一次垃圾回收动作。
回收的时候如果需要回收的对象越多,需要做的标记和清除的工作越多,所以标记清除算法适用于老年代。
在这里插入图片描述

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

首先标记出所有需要回收的对象,在标记完成后,后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端
边界以外的内存。标记整理算法虽然 没有内存碎片,但是 效率偏低。
我们看到标记整理与标记清除算法的区别主要在于对象的移动。对象移动不单单会加重系统负担,同时需要全程暂停用户线程才能进行,同时所有引用
对象的地方都需要更新( 直接指针需要调整)。
所以看到,老年代采用的标记整理算法与标记清除算法,各有优点,各有缺点。
在这里插入图片描述

App内存组成以及限制

Android给每个App分配一个VM,让App运行在dalvik上,这样即使App崩溃也不会影响到系统。系统给VM分配了一定的内存大小,App可以申请使用的内存大小不能超过此硬性逻辑限制,就算物理内存富余,如果应用超出VM最大内存,就会出现内存溢出crash

由程序控制操作的内存空间在heap上,分java heapsizenative heapsize

  • Java申请的内存在vm heap上,所以如果java申请的内存大小超过VM的逻辑内存限制,就会出现内存溢出的异常。

  • native层内存申请不受其限制,native层受native process对内存大小的限制

如何查看Android设备对App的内存限制

  1. 主要查看系统配置文件 build.prop,我们可以通过adb shell在 命令行窗口查看

adb shell cat /system/build.prop

在这里插入图片描述

在这里插入图片描述

  1. 通过代码获取
ActivityManager activityManager = (ActivityManager)context.getSystemService(Context.ACTIVITY_SERVICE)
activityManager.getMemoryClass();//以m为单位
  1. 可以修改吗?
  • 修改 \frameworks\base\core\jni\AndroidRuntime.cpp
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全称含义等价
USSUnique Set Size物理内存进程独占的内存
PSSProportional Set Size物理内存PSS= USS+ 按比例包含共享库
RSSResident Set Size物理内存RSS= USS+ 包含共享库
VSSVirtual 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区域。

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中的对象数量有关:

  • 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

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());
  }

Android 内存分析命令介绍

常用的内存调优分析命令:

  1. dumpsys meminfo
  2. procrank
  3. cat /proc/meminfo
  4. free
  5. showmap
  6. 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个指标:

totalfreebufferscachedshmemslab
2857032K998088K78060K78060K312K92392K

执行结果:

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

功能:能否查看更加详细的内存信息

指令: 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

free

主功能:查看可用内存,缺省单位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

主功能:用于查看虚拟地址区域的内存情况

用法:  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时间占比等情况,另外该指令还是周期性地动态输出。

用法:

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大类:

  • 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

总结

  1. dumpsys meminfo适用场景: 查看进程的oom adj,或者dalvik/native等区域内存情况,或者某个进程或apk的内存情况,功能非常强大;
  2. procrank适用场景: 查看进程的VSS/RSS/PSS/USS各个内存指标;
  3. cat /proc/meminfo适用场景: 查看系统的详尽内存信息,包含内核情况;
  4. free适用场景: 只查看系统的可用内存;
  5. showmap适用场景: 查看进程的虚拟地址空间的内存分配情况;
  6. vmstat适用场景: 周期性地打印出进程运行队列、系统切换、CPU时间占比等情况;

Android内存泄漏分析工具

MAT

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

Android Studio Memory-profiler

https://developer.android.com/studio/profile/memory-profiler#performance

LeakCanary

https://github.com/square/leakcanary

GC Log

GC Log分为Dalvik和ART的GC日志
ART的日志与Dalvik的日志差距非常大,除了格式不同之外,打印的时间也不同,非要在慢GC时才打印
除了。下面我们看看这条ART GC Log:
在这里插入图片描述

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等问题。

调查 RAM 使用情况

开发 Android 应用时,请始终注意您的应用使用了多少随机存取存储器 (RAM)。尽管 Dalvik 和 ART 运行时会执行例行垃圾回收 (GC),您仍然需要了解应用何时以及在哪里分配和释放内存。为了提供稳定的用户体验,使 Android 操作系统能够在应用之间快速切换,请确保您的应用在用户不与其交互时不会消耗不必要的内存。

即使您在开发过程中遵循了管理应用的内存的所有最佳做法,您仍然可能泄漏对象或引入其他内存错误。唯一能够确定您的应用尽可能少地使用内存的方法是,利用本文介绍的工具分析应用的内存使用情况。

解读日志消息

开始调查您的应用内存使用情况的最简单切入点是运行时日志消息。有时,发生垃圾回收时,您可以在 logcat 中查看消息。

Dalvik 日志消息

在 Dalvik(而不是 ART)中,每次垃圾回收都会将以下信息打印到 logcat 中:

D/dalvikvm: <GC_Reason> <Amount_freed>, <Heap_stats>, <External_memory_stats>, <Pause_time>

示例:
D/dalvikvm( 9050): GC_CONCURRENT freed 2049K, 65% free 3571K/9991K, external 4703K/5261K, paused 2ms+2ms
  • 垃圾回收原因<GC_Reason>GC_CONCURRENT
    什么触发了垃圾回收以及是哪种回收。可能出现的原因包括:

    • GC_CONCURRENT
      在您的堆开始占用内存时可以释放内存的并发垃圾回收。
    • GC_FOR_MALLOC
      堆已满而系统不得不停止您的应用并回收内存时,您的应用尝试分配内存而引起的垃圾回收。
    • GC_HPROF_DUMP_HEAP
      当您请求创建 HPROF 文件来分析堆时出现的垃圾回收。
    • GC_EXPLICIT
      显式垃圾回收,例如当您调用 gc() 时(您应避免调用,而应信任垃圾回收会根据需要运行)。
    • GC_EXTERNAL_ALLOC
      这仅适用于 API 级别 10 及更低级别(更新版本会在 Dalvik 堆中分配任何内存)。外部分配内存的垃圾回收(例如存储在原生内存或 NIO 字节缓冲区中的像素数据)。
  • 释放量<Amount_freed>freed 2049K
    从此次垃圾回收中回收的内存量。

  • 堆统计数据<Heap_stats>65% free 3571K/9991K
    堆的可用空间百分比与(活动对象数量)/(堆总大小)。

  • 外部内存统计数据<External_memory_stats>external 4703K/5261K
    API 级别 10 及更低级别的外部分配内存(已分配内存量)/(发生回收的限值)。

  • 暂停时间<Pause_time>paused 2ms+2ms
    堆越大,暂停时间越长。并发暂停时间显示了两个暂停:一个出现在回收开始时,另一个出现在回收快要完成时。
    在这些日志消息积聚时,请注意堆统计数据的增大(上面示例中的 3571K/9991K 值)。如果此值继续增大,可能会出现内存泄漏。

ART 日志消息

与 Dalvik 不同,ART 不会为未明确请求的垃圾回收记录消息。只有在认为垃圾回收速度较慢时才会打印垃圾回收。更确切地说,仅在垃圾回收暂停时间超过 5ms 或垃圾回收持续时间超过 100ms 时。如果应用未处于可察觉的暂停进程状态,那么其垃圾回收不会被视为较慢。始终会记录显式垃圾回收。

ART 会在其垃圾回收日志消息中包含以下信息:

I/art: <GC_Reason> <GC_Name> <Objects_freed>(<Size_freed>) AllocSpace Objects, <Large_objects_freed>(<Large_object_size_freed>) <Heap_stats> LOS objects, <Pause_time(s)>
示例:
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
  • 垃圾回收原因<GC_Reason>Explicit
    什么触发了垃圾回收以及是哪种回收。可能出现的原因包括:
    • Concurrent
      不会暂停应用线程的并发垃圾回收。此垃圾回收在后台线程中运行,而且不会阻止分配。
    • Alloc
      您的应用在堆已满时尝试分配内存引起的垃圾回收。在这种情况下,分配线程中发生了垃圾回收。
    • Explicit
      由应用明确请求的垃圾回收,例如,通过调用 gc() 或 gc()。与 Dalvik 相同,在 ART 中,最佳做法是您应信任垃圾回收并避免请求显式垃圾回收(如果可能)。不建议使用显式垃圾回收,因为它们会阻止分配线程并不必要地浪费 CPU 周期。如果显式垃圾回收导致其他线程被抢占,那么它们也可能会导致卡顿(应用中出现间断、抖动或暂停)。
    • NativeAlloc
      原生分配(如位图或 RenderScript 分配对象)导致出现原生内存压力,进而引起的回收。
    • CollectorTransition
      由堆转换引起的回收;此回收由运行时切换垃圾回收引起。回收器转换包括将所有对象从空闲列表空间复制到碰撞指针空间(反之亦然)。当前,回收器转换仅在以下情况下出现:在 RAM 较小的设备上,应用将进程状态从可察觉的暂停状态变更为可察觉的非暂停状态(反之亦然)。
    • HomogeneousSpaceCompact
      齐性空间压缩是空闲列表空间到空闲列表空间压缩,通常在应用进入到可察觉的暂停进程状态时发生。这样做的主要原因是减少 RAM 使用量并对堆进行碎片整理。
    • DisableMovingGc
      这不是真正的垃圾回收原因,但请注意,发生并发堆压缩时,由于使用了 GetPrimitiveArrayCritical,回收遭到阻止。一般情况下,强烈建议不要使用 GetPrimitiveArrayCritical,因为它在移动回收器方面具有限制。
    • HeapTrim
      这不是垃圾回收原因,但请注意,堆修剪完成之前回收会一直受到阻止。
  • 垃圾回收名称<GC_Name>concurrent mark sweep
    ART 具有可以运行的多种不同的垃圾回收。
    • Concurrent mark sweep (CMS)
      整个堆回收器,会释放和回收映像空间以外的所有其他空间。
    • Concurrent partial mark sweep
      几乎整个堆回收器,会回收除了映像空间和 zygote 空间以外的所有其他空间。
    • Concurrent sticky mark sweep
      生成回收器,只能释放自上次垃圾回收以来分配的对象。此垃圾回收比完整或部分标记清除运行得更频繁,因为它更快速且暂停时间更短。
    • Marksweep + semispace
      非并发、复制垃圾回收,用于堆转换以及齐性空间压缩(对堆进行碎片整理)。
  • 释放的对象<Objects_freed>(<Size_freed>) GC freed 104710(7MB)
    此次垃圾回收从非大型对象空间回收的对象数量。
  • 释放的大小AllocSpace Objects AllocSpace objects
    此次垃圾回收从非大型对象空间回收的字节数量。
  • 释放的大型对象<Large_objects_freed>(<Large_object_size_freed>) 21(416KB) LOS objects
    此次垃圾回收从大型对象空间回收的对象数量。
  • 释放的大型对象大小<Heap_stats> LOS objects 25MB/38MB,
    此次垃圾回收从大型对象空间回收的字节数量。
  • 堆统计数据 33% free
    空闲百分比与(活动对象数量)/(堆总大小)。
  • 暂停时间<Pause_time(s)>paused 1.230ms total 67.216ms
    通常情况下,暂停时间与垃圾回收运行时修改的对象引用数量成正比。当前,ART CMS 垃圾回收仅在垃圾回收即将完成时暂停一次。移动的垃圾回收暂停时间较长,会在大部分垃圾回收期间持续出现。
    如果您在 logcat 中看到大量的垃圾回收,请注意堆统计数据的增大(上面示例中的 25MB/38MB 值)。如果此值继续增大,且始终没有变小的趋势,则可能会出现内存泄漏。或者,如果您看到原因为“Alloc”的垃圾回收,那么您的操作已经快要达到堆容量,并且将很快出现 OOM 异常。

访问 Android Monitor

在连接的设备或模拟器上启动您的应用。
选择 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 <package_name|pid> [-d]

-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 列提供的数据也需要关注。您需要关注的不同内存分配(各行)的详细信息如下:

  • Dalvik Heap
    您的应用中 Dalvik 分配占用的 RAM。Pss Total 包括所有 Zygote 分配(如上述 PSS 定义所述,通过进程之间的共享内存量来衡量)。Private Dirty 数值是仅分配到您应用的堆的实际 RAM,由您自己的分配和任何 Zygote 分配页组成,这些分配页自从 Zygote 派生应用进程以来已被修改。
    注:在包含 Dalvik Other 部分的更新的平台版本上,Dalvik 堆的 Pss Total 和 Private Dirty 数值不包括 Dalvik 开销(例如即时 (JIT) 编译和垃圾回收记录),而较旧的版本会在 Dalvik 中将其一并列出。

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。

触发内存泄漏

使用上面介绍的工具时,您应积极地对自己的应用代码进行测试并尝试强制内存泄漏。在应用中引发内存泄漏的一种方式是,先让其运行一段时间,然后再检查堆。泄漏在堆中将逐渐汇聚到分配顶部。不过,泄漏越小,您越需要运行更长时间的应用才能看到泄漏。

您还可以通过以下方式之一触发内存泄漏:

  • 将设备从纵向旋转为横向,然后在不同的活动状态下反复操作多次。旋转设备经常会使应用泄漏 Activity、Context 或 View 对象,因为系统会重新创建 Activity,而如果您的应用在其他地方保持对这些对象其中一个的引用,系统将无法对其进行垃圾回收。
  • 处于不同的活动状态时,在您的应用与另一个应用之间切换(导航到主屏幕,然后返回到您的应用)。
    提示:您还可以使用 Monkey 测试框架执行上述步骤。如需了解有关运行 Monkey 测试框架的详细信息,请阅读 monkeyrunner 文档。

Bitmap内存模型

在这里插入图片描述

单个像素的字节大小

单个像素的字节大小由Bitmap的一个可配置的参数Config来决定。
Bitmap中,存在一个枚举类Config,定义了Android中支持的Bitmap配置:
在这里插入图片描述

获取Bitmap的大小

  1. getByteCount()
    getByteCount()方法是在API12加入的,代表存储Bitmap的色素需要的最少内存。API19开始
    getAllocationByteCount()方法代替了getByteCount()。

  2. getAllocationByteCount()
    API19之后,Bitmap加了一个Api:getAllocationByteCount();代表在内存中为Bitmap分配的内存大
    小。

public final int getAllocationByteCount() {
    if (mBuffer == null) {
      //mBuffer代表存储Bitmap像素数据的字节数组。
      return getByteCount();
   }
    return mBuffer.length;
 }
  1. getByteCount()与getAllocationByteCount()的区别
  • 一般情况下两者是相等的
  • 通过复用Bitmap来解码图片,如果被复用的Bitmap的内存比待分配内存的Bitmap大,那么
    getByteCount()表示新解码图片占用内存的大小(并非实际内存大小,实际大小是复用的那个
    Bitmap的大小),getAllocationByteCount()表示被复用Bitmap真实占用的内存大小(即mBuffer
    的长度)

Bitmap的内存回收

  1. 在Android2.3.3之前推荐使用Bitmap.recycle()方法进行Bitmap的内存回收。
    备注:只有当确定这个Bitmap不被引用的时候才能调用此方法,否则会有“Canvas: trying to use
    a recycled bitmap”这个错误。
  2. 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占用内存大小计算

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中
获取到的

  • inDensity:Bitmap位图自身的密度、分辨率
  • inTargetDensity: Bitmap最终绘制的目标位置的分辨率
  • inScreenDensity: 设备屏幕分辨率
    其中inDensity和图片存放的资源文件的目录有关,同一张图片放置在不同目录下会有不同的值:

在这里插入图片描述

可以验证几个结论:
  1. 图片放在drawable中,等同于放在drawable-mdpi中,原因为:drawable目录不具有屏幕密度特
    性,所以采用基准值,即mdpi
  2. 图片放在某个特定drawable中,比如drawable-hdpi,如果设备的屏幕密度高于当前drawable目
    录所代表的密度,则图片会被放大,否则会被缩小
    放大或缩小比例 = 设备屏幕密度 / drawable目录所代表的屏幕密度
  • 因此,关于Bitmap占用内存大小的公式,从之前:
    Bitmap内存占用 ≈ 像素数据总大小 = 横向像素数量 × 纵向像素数量 × 每个像素的字节大小
    可以更细化为:
    Bitmap内存占用 ≈ 像素数据总大小 = 图片宽 × 图片高× (设备分辨率/资源目录分辨率)^2 × 每个像
    素的字节大小

涉及到图片关于优化apk的大小:

可以配置gradle,例如:只打xxhdpi进apk–>resConfigs “xxhdpi”

小技巧:

  • 如图片解压缩出现OOM的情况,可尝试(伪代码):
    try{
    decode bitmap
    }catch(OutOfMemoryError e){
    质量压缩后再次去解码
    }

  • 在有些系统上可以使用epic库去hook图片的解压的过程,全盘监控图片加载这块,在这过程中做处理,从而做这方面的优化。

图片优化总体思想:

  1. 设备分级:
  2. Bitmap优化
    统一图片库
    线上线下监控

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。

内存问题总结:

在这里插入图片描述

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值