当前位置:首页 > 嵌入式 > 嵌入式软件
[导读]Android是基于Java的,众所周知Java语言的内存管理是其一大特点,不用像C语言那样处理对象的内存分配到回收的全部过程。在Java中我们只需要简单地新建对象就可以了,Java垃圾回收器会负责回收释放对象内存。这么看的话,垃圾回收器会管理内存又怎么还会发生内存泄漏呢?

1、内存泄漏的定义

Android是基于Java的,众所周知Java语言的内存管理是其一大特点,不用像C语言那样处理对象的内存分配到回收的全部过程。在Java中我们只需要简单地新建对象就可以了,Java垃圾回收器会负责回收释放对象内存。这么看的话,垃圾回收器会管理内存又怎么还会发生内存泄漏呢?

其实Java中的内存泄漏的定义是:对象不再被程序所使用,但是由于这些对象被引用着导致GC(GarbageCollector)不能回收它们。

下面这张图可以帮助我们更好地理解对象的状态,以及内存泄漏的情况

左边未引用的对象是会被GC回收的,右边被引用的对象不会被GC回收,但是未使用的对象中除了未引用的对象,还包括已被引用的一部分对象,那么内存泄漏久发生这部分已被引用但未使用的对象。

接下来还有一个疑问:未使用的对象被谁引用会让GC无法回收呢?

现在主流的程序语言的主流实现中,是通过可达性分析(ReachabilityAnalysis)来判断对象是否存活的。这个算法的基本思路是:通过一系列的称为“GCRoots”的对象作为起点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链时,说明此对象不可用,可以被回收了。

可以作为GCRoots的对象包括下面几种:

·虚拟机栈中引用的对象,一般是当前在使用中局部变量

·方法区中类静态属性引用的对象,就是静态变量对应的对象

·方法区中常量引用的对象

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

MAT分析内存泄漏的时候,也是查看对象到GCRoots的引用链,来定位泄漏代码的位置。

所以未使用的对象直接或间接地被GCRoots引用时会让GC无法回收,从而产生内存泄漏。

2、Android的内存管理

了解了Java的内存泄漏的起因,接下来大致了解Android中的内存管理机制。

Google在Android的官网上有这样一篇文章,初步介绍了Android是如何管理应用的进程与内存分配:http://developer.android.com/training/arTIcles/memory.html。Android系统的Dalvik虚拟机扮演了常规的内存垃圾自动回收的角色,Android系统没有为内存提供交换区,它使用paging与memory-mapping(mmapping)的机制来管理内存,下面简要概述一些Android系统中重要的内存管理基础概念。

分配与回收内存

每一个进程的Dalvikheap都反映了使用内存的占用范围。这就是通常逻辑意义上提到的DalvikHeapSize,它可以随着需要进行增长,但是增长行为会有一个系统为它设定的上限。

逻辑上讲的HeapSize和实际物理意义上使用的内存大小是不对等的,ProporTIonalSetSize(PSS)记录了应用程序自身占用以及和其他进程进行共享的内存。

Android系统并不会对Heap中空闲内存区域做碎片整理。系统仅仅会在新的内存分配之前判断Heap的尾端剩余空间是否足够,如果空间不够会触发gc操作,从而腾出更多空闲的内存空间。在Android的高级系统版本里面针对Heap空间有一个GeneraTIonalHeapMemory的模型,最近分配的对象会存放在YoungGeneration区域,当这个对象在这个区域停留的时间达到一定程度,它会被移动到OldGeneration,最后累积一定时间再移动到PermanentGeneration区域。系统会根据内存中不同的内存数据类型分别执行不同的gc操作。例如,刚分配到YoungGeneration区域的对象通常更容易被销毁回收,同时在YoungGeneration区域的gc操作速度会比OldGeneration区域的gc操作速度更快。如下图所示:

每一个Generation的内存区域都有固定的大小,随着新的对象陆续被分配到此区域,当这些对象总的大小快达到这一级别内存区域的阀值时,会触发GC的操作,以便腾出空间来存放其他新的对象。

通常情况下,GC发生的时候,所有的线程都是会被暂停的。执行GC所占用的时间和它发生在哪一个Generation也有关系,Young Generation中的每次GC操作时间是最短的,Old Generation其次,Permanent Generation最长。执行时间的长短也和当前Generation中的对象数量有关,遍历树结构查找20000个对象比起遍历50个对象自然是要慢很多的。

为什么通常情况下,GC发生的时候,所有的线程都会被暂停?

因为每次GC的时候,需要先找到可作为GC Roots的对象,然后以此搜索引用链,这个过程需要在一致性的内存快照中进行。这个“一致性”表示在整个过程中不能出现对象引用关系不断变化的情况,所以需要暂停所有的执行线程。

限制应用的内存

为了整个Android系统的内存控制需要,Android系统为每一个应用程序都设置了一个硬性的Dalvik Heap Size最大限制阈值,这个阈值在不同的设备上会因为RAM大小不同而各有差异。如果你的应用占用内存空间已经接近这个阈值,此时再尝试分配内存的话,很容易引起OutOfMemoryError的错误。

ActivityManager.getMemoryClass()可以用来查询当前应用的Heap Size阈值,这个方法会返回一个整数,表明你的应用的Heap Size阈值是多少Mb(megabates)。

还有一个用adb命令查询的方法:

adb shell getprop dalvik.vm.heapgrowthlimit

3、案例

JOOX是IBG一个核心产品,2014年发布以来已经成为5个国家和地区排名第一的音乐App。东南亚是JOOX的主要发行地区,实际上这些地区还是有很多的低端机型,对App的进行内存优化势在必行。

上面介绍了Android系统内存分配和回收机制,同时也列举了常见的内存问题,但是当我们接到一个内存优化的任务时,我们应该从何开始?下面是一次内存优化的分享。

1. 首先是解决大部分内存泄露。

不管目前App内存占用怎样,理论上不需要的东西最好回收,避免浪费用户内存,减少OOM。实际上自JOOX接入LeakCanary后,每个版本都会做内存泄露检测,经过几个版本的迭代,JOOX已经修复了几十处内存泄露。

2. 通过MAT查看内存占用,优化占用内存较大的地方。

JOOX修复了一系列内存泄露后,内存占用还是居高不下,只能通过MAT查看到底是哪里占用了内存。关于MAT的使用,网上教程无数,简单推荐两篇MAT使用教程,MAT - Memory Analyzer Tool 使用进阶。

点击Android Studio这里可以dump当前的内存快照,因为直接通过Android Sutdio dump出来的hprof文件与标准hprof文件有些差异,我们需要手动进行转换,利用sdk目录/platform-tools/hprof-conv.exe可以直接进行转换,用法:hprof-conv 原文件.hprof 新文件.hprof。只需要输入原文件名还有目标文件名就可以进行转换,转换完就可以直接用MAT打开。

下面就是JOOX打开App,手动进行多次gc的hprof文件。

这里我们看的是Dominator Tree(即内存里占用内存最多的对象列表)。

Shallo Heap:对象本身占用内存的大小,不包含其引用的对象内存。

Retained Heap: Retained heap值的计算方式是将retained set中的所有对象大小叠加。或者说,由于X被释放,导致其它所有被释放对象(包括被递归释放的)所占的heap大小。

第一眼看去 居然有3个8M的对象,加起来就是24M啊 这到底是什么鬼?

我们通过List objects-》with incoming references查看(这里with incoming references表示查看谁引用了这个对象,with outgoing references表示这个对象引用了谁)

通过这个方式我们看到这三张图分别是闪屏,App主背景,App抽屉背景。

这里其实有两个问题:

这几张图原图实际都是1280x720,而在1080p手机上实测这几张图都缩放到了1920x1080

闪屏页面,其实这张图在闪屏显示过后应该可以回收,但是因为历史原因(和JOOX的退出机制有关),这张图被常驻在后台,导致无谓的内存占用。

优化方式:我们通过将这三张图从xhdpi挪动到xxhdpi(当然这里需要看下图片显示效果有没很大的影响),以及在闪屏显示过后回收闪屏图片。

优化结果:

从原来的8.29x3=24.87M 到 3.68x2=7.36M 优化了17M(有没一种万马奔腾的感觉。。可能有时费大力气优化很多代码也优化不了几百K,所以很多情况下内存优化时优化图片还是比较立竿见影的)。

同样方式我们发现对于一些默认图,实际要求的显示要求并不高(图片相对简单,同时大部分情况下图片加载会成功),比如下面这张banner的背景图:

优化前1.6M左右,优化后700K左右。

同时我们也发现了默认图片一个其他问题,因为历史原因,我们使用的图片加载库,设置默认图片的接口是需要一个bitmap,导致我们原来几乎每个adapter都用BitmapFactory decode了一个bitmap,对同一张默认图片,不但没有复用,还保存了多份,不仅会造成内存浪费,而且导致滑动偶尔会卡顿。这里我们也对默认图片使用全局的bitmap缓存池,App全局只要使用同一张bitmap,都复用了同一份。

另外对于从MAT里看到的图片,有时候因为看不到在项目里面对应的ID,会比较难确认到底是哪一张图,这里stackoverflow上有一种方法,直接用原始数据通过GIM还原这张图片。

这里其实也看到JOOX比较吃亏一个地方,JOOX不少地方都是使用比较复杂的图片,同时有些地方还需要模糊,动画这些都是比较耗内存的操作,Material Design出来后,很多App都遵循MD设计进行改版,通常默认背景,默认图片一般都是纯色,不仅App看起来比较明亮轻快,实际上也省了很多的内存,对此,JOOX后面对低端机型做了对应的优化。

3. 我们也对Bugly上的OOM进行了分析,发现其实有些OOM是可以避免的。

下面这个crash就是上面提到的在LsitView的adapter里不停创建bitmap,这个地方是我们的首页banner位,理论上App一打开就会缓存这张默认背景图片了,而实际在使用过一段时间后,才因为为了解码这张背景图而OOM, 改为用全局缓存解决。

下面这个就是传说中的内存抖动

实际代码如下,因为打Log而进行了字符串拼接,一旦这个函数被比较频繁地调用,那么就很有可能会发生内存抖动。这里我们新版本已经改为使用stringbuilder进行优化。

还有一些比较奇怪的情况,这里是我们扫描歌曲文件头的时候发生的,有些文件头居然有几百M大,导致一次申请了过大的内存,直接OOM,这里暂时也无法修复,直接catch住out of memory error。

4. 同时我们对一些逻辑代码进行调整,比如我们的App主页的第三个tab(Live tab)进行了数据延迟加载,和定时回收。

这里因为这个页面除了有大图还有轮播banner,实际强引用的图片会有多张,如果这个时候切到其他页面进行听歌等行为,这个页面一直在后台缓存,实际是很浪费耗内存的,同时为优化体验,我们又不能直接通过设置主页的viewpager的缓存页数,因为这样经常都会回收,导致影响体验,所以我们在页面不可见后过一段时间,清理掉adapter数据(只是清空adapter里的数据,实际从网络加载回来的数据还在,这里只是为了去掉界面对图片的引用),当页面再次显示时再用已经加载的数据显示,即减少了很多情况下图片的引用,也不影响体验。

5. 最后我们也遇到一个比较奇葩的问题,在我们的Bugly上报上有这样一条上报

我们在stackoverflow上看到了相关的讨论,大致意思是有些情况下比如息屏,或者一些省电模式下,频繁地调System.gc()可能会因为内核状态切换超时的异常。这个问题貌似没有比较好的解决方法,只能是优化内存,尽量减少手动调用System.gc()

优化结果

我们通过启动App后,切换到我的音乐界面,停留1分钟,多次gc后,获取App内存占用

优化前:

优化后:

多次试验结果都差不多,这里只截取了其中一次,有28M的优化效果。

当然不同的场景内存占用不同,同时上面试验结果是通过多次手动触发gc稳定后的结果。对于使用其他第三方工具不手动gc的情况下,试验结果可能会差异比较大。

对于上面提到的JOOX里各种图片背景等问题,我们做了动态的优化,对不同的机型进行优化,对特别低端的机型设置为纯色背景等方式,最终优化效果如下:

平均内存降低41M。

本次总结主要还是从图片方面下手,还有一点逻辑优化,已经基本达到优化目标。

本站声明: 本文章由作者或相关机构授权发布,目的在于传递更多信息,并不代表本站赞同其观点,本站亦不保证或承诺内容真实性等。需要转载请联系该专栏作者,如若文章内容侵犯您的权益,请及时联系本站删除。
换一批
延伸阅读

嵌入式开发作为信息技术领域的重要分支,其涉及的语言种类繁多,各具特色。这些语言的选择取决于目标平台的特性、性能需求、开发者的熟练程度以及项目的具体要求。本文将详细介绍几种常见的嵌入式开发语言,包括C语言、C++、汇编语言...

关键字: 嵌入式开发 C语言

罗德与施瓦茨与SmartViser携手开发了一种用于测试符合欧盟销售的智能手机和平板电脑的新Energy Efficiency Index(EEI)标签法规的解决方案。该解决方案的核心是R&S CMX500,这是...

关键字: 智能手机 Android iOS

Java语言和C语言是两种不同的编程语言,它们在语法、特性和应用领域上有许多差别。下面将详细介绍Java语言和C语言之间的差异以及它们各自的技术特点。

关键字: Java语言 C语言 编程

嵌入式系统是一种专门设计用于特定应用领域的计算机系统,它通常由硬件和软件组成,并且被嵌入到其他设备或系统中,以实现特定的功能。在嵌入式系统的开发过程中,选择适合的编程语言是至关重要的。C语言是一种被广泛应用于嵌入式系统开...

关键字: 嵌入式 计算机 C语言

C语言是一种广泛应用于软件开发领域的编程语言。它是由贝尔实验室的Dennis Ritchie在20世纪70年代初创建的,旨在为UNIX操作系统的开发提供一种高级编程语言。C语言具有简洁、高效、可移植性强等特点,因此成为了...

关键字: C语言 操作系统 应用程序

嵌入式系统是现代生活中无处不在的一部分。它们包括了我们的家电、汽车、智能手机、医疗设备等等。这些系统的工作必须高效、可靠,因为它们往往控制着生活中的关键方面。而C语言作为一种广泛用于嵌入式系统开发的编程语言,其质量和稳定...

关键字: 嵌入式系统 C语言 编程

在嵌入式系统开发领域中,C语言是使用最广泛的编程语言之一。它具有高效、灵活和可移植的特点,成为嵌入式系统设计师的首选语言。本文将介绍C语言编程的基本概念、特点以及在嵌入式系统开发中的应用。

关键字: 嵌入式系统 C语言 编程

C语言编译器是一种用于将C语言源代码转换为可执行程序的软件工具。它的主要功能是将C语言代码翻译成机器语言,以便计算机能够理解和执行。C语言编译器通常包括预处理器、编译器、汇编器和链接器等多个组件,它们协同工作以完成编译过...

关键字: C语言 编译器 Microsoft Visual C++

(全球TMT2023年8月24日讯)2023年8月23日,时值实时3D引擎Unity在华设立合资公司Unity中国一周年之际,Unity中国正式推出Unity中国版引擎——团结引擎。Unity全球CEO John Ri...

关键字: UNITY CE Android 开发者

报告显示:全球电商 App 获客花费接近50亿美元 北京2023年8月23日 /美通社/ -- 全球营销衡量与体验管理平台 AppsFlyer 近日发布《2023 电商 App 营销现状报告》。尽管面临全球经...

关键字: APPS BSP iOS Android
关闭
关闭