「性能优化系列」APP内存优化理论与实践(1)

  1. 分代垃圾回收算法

分代垃圾回收机制是根据不同对象的不同生命周期,采用不同的回收方法,提高回收效率。

主要分为年轻代和老年代。

年轻代: 用于存放新创建的对象,存活率较低的对象,经过多次GC后,该对象仍然存活,那么就会放入老年代。常用复制算法

老年代: 用于存放存活时间长的对象,常用标记清除或标记整理算法

算法细节:

  • 对象新建,将存放在新生代的Eden区域,注意Suvivor区又分为两块区域,FromSuv和ToSuv;

  • 当年轻代Eden满时,将会触发Minor GC,如果对象仍然存活,对象将会被移动到Fromsuvivor空间,对象还是在新生代;

  • 再次发生minor GC,对象还存活,那么将会采用复制算法,将对象移动到ToSuv区域,此时对象的年龄+1;

  • 再次发生minor GC,对象仍然存活,此时Survivor中跟对象Object同龄的对象还没有达到Surivivor区的一半,所以还是会继续采用复制算法,将fromSuv和ToSuv的区域进行互换;

  • 当多次发生monorGC后,对象实例仍然存活,且此时,此时Survivor中跟对象Object同龄的对象达到Surivivor区的一半,那么对象实例将会移动到老年代区域,或者对象经过多次的回收,年龄达到了15岁,那么也会迁移到老年代。

磨刀不误砍柴工,首先基本上了解了一些内存方面的知识点,那接下来就开始内存优化实践。

二、内存优化实践

======================================================================

内存优化主要分为几个大方向,Bitmap优化内存泄漏内存抖动设备分级等。

2.1、内存抖动


内存抖动指的是内存频繁分配和回收导致内存不稳定,频繁GC,会导致卡顿,甚至会OOM。至于为什么会造成卡顿?是因为在GC时,会触发STW(stop the world)机制,也就是在执行垃圾收集算法时,应用程序的其他所有线程都被挂起(除了垃圾收集器之外),这个时候也就不会处理用户的操作事件,从而出现卡顿。

下面将模拟一个内存抖动情况,并使用Memory profile进行内存抖动的分析。 这里自定义了一个小球加载中的动画效果:

在开始小球动画后,我们打开打开AS自带的Memory,从下图可以看到,内存的走势是在上下起伏,并且内存也在不断的增加,这就代表着内存在不断的分配和回收,这就是内存抖动。

那内存抖动的具体问题出现在哪里?

为了分析内存抖动的问题所在,我们先选取一段内存抖动的地方,可以看到在我们选取的这段时间里,AS的profile 都显示了每个对象的内存分配情况,如下所示。

从上面图片中的红色框里就可以了解到,App产生内存抖动的原因主要就是app heap的前几项,而要匹配到项目中的代码就需要一个一个查看占用内存多的模块。

我们先选择其中一个,在Allocation Call Stack模块中清楚的看到具体所分配的堆栈,同时也找到了造成堆中实例对象多的源代码。

频繁创建对象实例的原代码在com.fuusy.fuperformance.memory.view.WaveView的136行。

哦~~ ,原来是自定义View时,在onDraw中频繁创建Paint所致,解决的办法就是将paint作为全局变量,在外部创建。

这个案例只是内存抖动中一个小小的缩影,当项目越来越大时,排查的工作难度也随之增加,这就要我们在平时开发时,就需要注意代码细节问题,尽可能在coding的过程中就减少内存问题。

内存抖动的注意事项:

  1. 避免在循环和频繁调用的方法中创建对象;

  2. 使用对象池,如Handler、Glide中的对象池。

2.2、内存泄漏


内存泄漏指的是程序中已分配的内存由于某种原因未释放或者无法释放,造成系统内存的浪费。

造成内存泄漏的原因有很多,比如:

  • 长生命周期对象持有短生命周期对象的强引用,从而导致短生命周期对象无法被回收;

  • 异步线程持有短生命周期对象,如Handler、网络请求或者其他工作线程持有短生命周期对象;

  • 资源未及时关闭,如BroadcastReceiver、File、Cursor等;

  • 大量使用静态变量;

当然,在实际项目中查找内存泄漏的原因的方式也有很多,比如主流工具LeakCanary、 MAT等。

2.3、Bitmap优化


Bitmap作为程序中内存占用的大户,是必须优化的对象,之前写过一篇关于Bitmap优化的文章「性能优化系列」不使用第三方库,Bitmap的优化策略,可参考查看。

Bitmap除了基本优化外,其实还需要在coding的过程中,就将Bitmap内存问题扼杀在摇篮里,本篇文章就将从图片大小监控,重复图片监控两个方向进行阐述。

2.3.1、Bitmap大小监控方案

Bitmap有一种从其尺寸上优化的手段,即当装载图片的容器例如ImageView只有100 * 100,而图片的分辨率为1800 * 800,这个时候将图片直接放置在容器上,很容易OOM,同时也是对图片和内存资源的一种浪费。当容器的宽高都很小于图片的宽高,其实就需要对图片进行尺寸上的压缩.

而比较重要的点就是如何判断Bitmap的尺寸符合图片容器?

想到的第一种方法就是可以自定义一个ImageView,在View中去判断图片以及容器的大小,如果图片太大,则进行尺寸上的压缩或优化。这种方式简单实用,确实也解决了我们的问题,但在实际开发中,除了代码侵入性强外,如果想要开发团队中的每个人加载ImageView时都使用这个控件,也是一件很难展开的事情。

为了更加解耦性和减少代码侵入性,这里介绍一种Bitmap大小的监控方案-ARTHook

ARTHook通俗来讲就是统一添加代码修改原有逻辑。基于ARTHook的框架有很多,这里介绍一个常用框架-Epic。 Epic就是ART上的 Dexposed(支持 Android 5.0 ~ 11)。它可以拦截本进程内部几乎任意的 Java 方法调用,可用于实现 AOP 编程、运行时插桩、性能分析等。

下面就基于Epic框架进行Bitmap大小监控的编写。

添加依赖

dependencies {

implementation ‘me.weishu:epic:0.11.0’

}

新建一个Hook类用来编写图片大小读取,比较,继承XC_MethodHook,并实现其afterHookedMethod,在其方法内实现需要监控的对象,具体监控的方法调用后将会调用该方法。

class BitmapARTHook : XC_MethodHook() {

@Throws(Throwable::class)

override fun afterHookedMethod(param: MethodHookParam) {

super.afterHookedMethod(param)

val imageView = param.thisObject as ImageView

checkBitmap(imageView, (param.thisObject as ImageView).drawable)

}

}

在afterHookedMethod方法里实现我们需要的逻辑,这里需要判断图片大小并给出警示。那就加上图片大小的监测方法。

if (bitmap.width >= width shl 1 && bitmap.height >= height shl 1) {

warn(

bitmap.width,

bitmap.height,

width,

height,

RuntimeException(“Bitmap size too large”)

)

}

详细代码请参考fuusy/FuPerformance

写个测试来看看最终的效果。在xml中新建一个ImageView,宽高都设置为100dp,在Activity中将一张分辨率为1300* 500的图片设置到ImageView中。

val imageView = findViewById(R.id.iv_bitmap)

BitmapFactory.decodeResource(resources, R.mipmap.bitmap1).apply {

imageView.setImageBitmap(this)

}

运行后在终端可以看到图片大小的提示信息。

2.3.2、重复图片监控

张邵文在高手课中提及重复图片指的是 Bitmap 的像素数据完全一致,但是有多个不同的对象存在。给出的方案是使用HAHA 库快速判断内存中是否存在重复的图片,并且将这些重复图片的 PNG、堆栈等信息输出。

需要注意的是需要使用8.0以下的机器,因为8.0以后Bitmap中的buffer已经放到native内存中。 核心代码与思路如下:

//打开hprof文件

HprofBuffer buffer = new MemoryMappedFileBuffer(heapDumpFile);

HprofParser parser = new HprofParser(buffer);

//解析获得快照

com.squareup.haha.perflib.Snapshot snapshot = parser.parse();

snapshot.computeDominators();

//获得Bitmap Class

Collection bitmapClasses = snapshot.findClasses(“android.graphics.Bitmap”);

//获取堆数据,这里包括项目app、系统、default heap的信息,需要进行过滤

Collection heaps = snapshot.getHeaps();

long startTime = System.currentTimeMillis();

Tools.print("---------------------- 开始 ----------------------- ");

for (Heap heap : heaps) {

// 只需要分析app和default heap即可

if (!heap.getName().equals(“app”) && !heap.getName().equals(“default”)) {

continue;

}

Tools.print(“HeapName:” + heap.getName());

Map<Integer, List> map = new HashMap<>();

for (ClassObj clazz : bitmapClasses) {

//从heap中获得所有的Bitmap实例

List instances = clazz.getHeapInstances(heap.getId());

for (int i = 0; i < instances.size(); i++) {

//从GcRoot开始遍历搜索,Integer.MAX_VALUE代表无法被搜索到,说明对象没被引用可以被回收

if (instances.get(i).getDistanceToGcRoot() == Integer.MAX_VALUE) {

continue;

}

List analyzerResults;

int curHashCode = Tools.getHashCodeByInstance(instances.get(i));

AnalyzerResult result = Tools.getAnalyzerResult(instances.get(i));

result.setInstance(instances.get(i));

if (map.get(curHashCode) == null){

analyzerResults = new ArrayList<>();

}else {

analyzerResults = map.get(curHashCode);

}

analyzerResults.add(result);

map.put(curHashCode, analyzerResults);

}

}

if (map.isEmpty()){

Tools.print(“当前head暂无bitmap对象”);

}

for (Map.Entry<Integer, List> entry : map.entrySet()){

List analyzerResults = entry.getValue();

//去除size小于2的,剩余的为重复图片。

if (analyzerResults.size() < 2){

continue;

}

}

}

具体方案可参考高手课Demo github.com/simplezhli/… 以及fuusy/FuPerformance

2.4、设备分级


所谓设备分级,指的是根据不同设备环境来考虑不同的内存优化策略。目前市场上手机层出不穷,几乎每一年都会对手机性能进行提升,但是对于性能较差的手机,app应用的运行状况就会较差。

对于低端机用户可以关闭复杂的动画,或者是某些功能;使用 565 格式的图片,使用更小的缓存内存等。在现实环境下,不是每个用户的设备都跟我们的测试机一样高端,在开发过程我们要学会思考功能要不要对低端机开启、在系统资源吃紧的时候能不能做降级。- 张邵文

那如何进行设备分级?

Facebook其实开发了一个 设备年份类库,它使用简单的算法将设备的 RAM、CPU 内核和时钟速度与这些特性被认为是高端的年份相匹配。使得我们能够根据手机的硬件功能编写不同的逻辑。

| RAM | condition | Year Class |

| — | — | — |

| 768MB | 1 core | 2009 |

| | 2+ cores | 2010 |

| 1GB | <1.3GHz | 2011 |

| | 1.3GHz+ | 2012 |

| 1.5GB | <1.8GHz | 2012 |

| | 1.8GHz+ | 2013 |

| 2GB | | 2013 |

| 3GB | | 2014 |

| 5GB | | 2015 |

| more | | 2016 |

而针对设备性能,我们能做的优化就如上面张邵文所说。

  • 是否关闭动画;

  • 图片质量分级;

  • 为低性能设备设计简版应用。

设备分级实践

添加设备年份库的依赖

implementation ‘com.facebook.device.yearclass:yearclass:2.1.0’

获取设备年限以及进行设备分级。

val year = YearClass.get(applicationContext)

Log.d(TAG, “Year: $year”)

when {

year >= 2013 -> {

// Do advanced animation

}

year > 2010 -> {

// Do simple animation

}

else -> {

// Phone too slow, don’t do any animations

}

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后是今天给大家分享的一些独家干货:

【Android开发核心知识点笔记】

【Android思维脑图(技能树)】

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

【Android高级架构视频学习资源】

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

以上Android开发知识点,真正体系化!**

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后是今天给大家分享的一些独家干货:

【Android开发核心知识点笔记】

[外链图片转存中…(img-rrsnh5oV-1713428475018)]

【Android思维脑图(技能树)】

[外链图片转存中…(img-a0n6FoPC-1713428475019)]

【Android核心高级技术PDF文档,BAT大厂面试真题解析】

[外链图片转存中…(img-NNLGKjrc-1713428475020)]

【Android高级架构视频学习资源】

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值