Android性能调优 - 内存优化的OOM、卡顿和 绘制优化

内存优化

好处(内存优化方向):

减少 OOM ,可以提高程序的稳定性。
减少卡顿,提高应用流畅性。
减少内存占用(大图场景),提高应用后台存活性。

因此从这三点入手,看下如何进行内存优化

减少 OOM

线程数太多
打开太多文件
内存不足

在应用开发阶段我比较喜欢用 LeakCanary 这款性能检测工具,好处是它能实时的告诉我具体哪个类发现了内存泄漏(如果你对 LeakCanary 的原理了解的话,可以说一说它是怎么检测的)。

发生 OOM 的场景是当申请 1M 的内存空间时,如果你要往该内存空间存入 2M 的数据,那么此时就会发生 OOM。
在应用程序中我们不仅要避免直接导致 OOM 的场景还要避免间接导致 OOM 的场景。间接的话也就是要避免内存泄漏的场景。(这里可以介绍下 GC 回收机制,回收算法,知识点尽量往外扩展而不脱离本题)

最后在说一下在实际开发中避免内存泄漏的场景:
非静态内部类的静态实例
Handler 临时性内存泄漏: 使用静态 + 弱引用,退出即销毁
资源型对象未关闭: Cursor,File
注册对象未销毁: 广播,回调监听

卡顿优化:

卡顿原因

Android屏幕 1000ms60帧的频率来进行刷新,如果16ms没有刷新完一帧,那就会让用户感觉到卡顿;

图片来自文章:知乎

上述绘制流程每次需要在16ms内完成,因为VSync信号是一直不停地间隔16ms发送;
Choreographer通过DisplayEventReceiver向系统SurfaceFlinger(在时序图最右边没画出)注册下一个VSync信号;
注册的Vsync信号最后传递给Choreographer;
Choreographer收到VSync信号之后,向主线程MessageQueue发送了一个异步消息
最后,异步消息的执行者是跑在主线程中的ViewRootImpl#doTraversal,也就是真正开始绘制一帧的操作(包含measure、layout、draw三个过程);

在这里插入图片描述
从刷新原理来看卡顿的根本原理是有两个地方会造成掉帧:

  1. 一个是主线程有其它耗时操作,导致doFrame 没有机会在 vsync 信号发出之后 16 毫秒内调用;
  2. 还有一个就是当前doFrame方法耗时,绘制太久,下一个 vsync 信号来的时候这一帧还没画完,造成掉帧。

导致根因的具体原因:

系统层面:

  1. SurfaceFlinger 主线程耗时。SurfaceFlinger 负责 Surface 的合成 和 Vsync信号的分发, 一旦
    SurfaceFlinger 主线程调用超时 , 就会产生掉帧。SurfaceFlinger可以按硬件产生的VSync节奏进行工作。
  2. 后台进程活动太多,会导致系统非常繁忙, cpu \ io \ memory 等资源都会被占用,这时候很容易出现卡顿
  3. system_server 的 AMS 锁和 WMS 锁。这时许多系统的关键任务都被阻塞 , 等待锁的释放 , 这时候如果有 App 发来的 Binder 请求带锁,那么也会进入等待状态 , 这时候 App 就会产生性能问题。

应用层层面:

  1. 主线程执行时间长 主线程执行 Input \ Measure \ Layout \ Draw等方法
  2. 主线程 Binder 耗时。像Activity resume 的时候, 与 AMS 通信要持有 AMS 锁(参考上面), 这时候如果碰到后台比较繁忙的时候, 等锁操作就会比较耗时, 导致部分场景因为这个卡顿, 比如多任务手势操作;
卡顿检测方案(线上)

一、基于 Looper 的 Printer 分发消息的时间差值来判断是否卡顿。

//1\. 开启监听
  Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));

//2\. 只要分发消息那么就会在之前和之后分别打印消息
public static void loop() {
   final Looper me = myLooper();
   if (me == null) {
       throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");        }    final MessageQueue queue = me.mQueue;       ...

    for (;;) {
        Message msg = queue.next(); // might block
        ...
      //分发之前打印
      final Printer logging = me.mLogging;     if (logging != null) {        logging.println(">>>>> Dispatching to " + msg.target + " " +                        msg.callback + ": " + msg.what);      }

            ...
      try {
       //分发消息
       msg.target.dispatchMessage(msg);
            ...
      //分发之后打印
            if (logging != null) {
        logging.println("<<<<< Finished to " + msg.target + " " + msg.callback);
            }
        }
    }

目前已经有三方库Matrix实现这种监控方案:
LooperMonitor 初始化的时候传入了主线程的 Looper。紧接着通过 resetPrinter() 尝试通过反射的方式去重新设置 Looper 中的 Printer 对象,print中设有dispatch打印卡顿信息的回调;以及最后为了防止反射拿不到print对象,添加空闲handler定时去重试;

private static final LooperMonitor mainMonitor = new LooperMonitor();

private LooperMonitor() {
     // 传入主线程 Looper
     this(Looper.getMainLooper());
 }
 
public LooperMonitor(Looper looper) {
    Objects.requireNonNull(looper);
    this.looper = looper;
    // 设置 Printer 对象
    resetPrinter();
    // 添加空闲 Handler
    addIdleHandler(looper);
}

二、 Choreographer 回调函数 postFrameCallback 来监控
在这里插入图片描述
回调中有做完每一帧的时间戳返回,拿其和当前时间戳进行比较大于16.6的平均时间,就对这一帧做记录;

线下可采用Systrace和LayoutInspect去检测卡顿;(具体看下面)

怎么避免卡顿

一定要避免在主线程中做耗时任务,总结一下 Android 中主线程的场景:

  1. 优化布局
  2. UI 生命周期的控制
  3. 系统事件的处理
  4. 消息处理
  5. 界面布局
  6. 界面绘制
  7. 界面刷新
  8. 借鉴一下下面绘制优化的措施
    最重要的就是避免内存抖动,不要在短时间内频繁的内存分配和释放;

大内存优化措施

  1. 大图片内存优化(这里可以从 Glide 等开源框架去说下它们是怎么设计的)
  2. 不要在 onMeause, onLayout, onDraw 中去刷新 UI
  3. 图片的多级缓存
  4. bitmap 内存复用,压缩
  5. 字符串拼接别用 +=,使用 StringBuffer 或 StringBuilder

绘制优化(卡顿优化)

(和上面卡顿优化有点冲突,结合着一起看)

布局为什么会导致卡顿?

分析完布局的加载流程之后,我们发现有如下四点可能会导致布局卡顿:
1、首先,系统会将我们的Xml文件通过IO的方式映射的方式加载到我们的内存当中,而IO的过程可能会导致卡顿。
2、其次,布局加载的过程是一个反射的过程,而反射的过程也会可能会导致卡顿。
3、同时,这个布局的层级如果比较深,那么进行布局遍历的过程就会比较耗时。
4、最后,不合理的嵌套RelativeLayout布局也会导致重绘的次数过多。

如何监测卡顿?

从线上和线下两个角度来进行分析。

比如说,我要统计线上的FPS,我使用的就是Choreographer这个类,它具有以下特性:
1、能够获取整体的帧率。
2、能够带到线上使用。
3、它获取的帧率几乎是实时的,能够满足我们的需求。

同时,在线下使用Systrace可以很方便地看到每帧的具体耗时以及这一帧在布局当中它真正做了什么。还可以使用LayoutInspector很方便地看到每一个界面的布局层级,帮助我们对层级进行优化。

如下,systrace示例图,可以看到UIThread这一个信息中 包含了ChoreGrapher的doFrame过程中发生的绘制细节耗时等;两个F间就表示一帧
在这里插入图片描述

如何优化布局的?

对此,我们的优化方式有如下几种:

1、针对布局加载Xml文件的优化,我们使用了异步Inflate的方式,即AsyncLayoutInflater。它的核心原理是在子线程中对我们的Layout进行加载,而加载完成之后会将View通过Handler发送到主线程来使用。所以不会阻塞我们的主线程,加载的时间全部是在异步线程中进行消耗的。而这仅仅是一个从侧面缓解的思路。
2、后面,我们发现了一个从根源解决上述痛点的方式,即使用X2C框架。它的一个核心原理就是在开发过程我们还是使用的XML进行编写布局,但是在编译的时候它会使用APT的方式将XML布局转换为Java的方式进行布局,通过这样的方式去写布局,它有以下优点:

- 它省去了使用IO的方式去加载XML布局的耗时过程。
- 它是采用Java代码直接new的方式去创建控件对象,所以它也没有反射带来的性能损耗。这样就从根本上解决了布局加载过程中带来的问题。

3、使用ConstraintLayout去减少我们界面布局的嵌套层级,如果原始布局层级越深,它能减少的层级就越多。而使用它也能避免嵌套RelativeLayout布局导致的重绘次数过多。或者尽量不用RelativeLayout,绘制可缩短时间,原因参考文章:https://blog.csdn.net/emmmsuperdan/article/details/129202763

  1. 避免过度绘制。比如斗地主游戏里,扑克牌A和B叠加放在一起,那么重叠部分只需要画一次就够了。采用懒加载的xml布局,如ViewStub,inClude等来减少measure layout draw时间;
建立卡顿检测完整机制

从项目的初期到壮大期,最后再到成熟期,每一个阶段都针对卡顿优化做了不同的处理。各个阶段所做的事情如下所示:

1、系统工具定位、解决
2、自动化卡顿方案及优化
3、线上监控及线下监测工具的建设

一整套的卡顿解决方案如何去做?

首先,针对卡顿,我们采用了线上、线下工具相结合的方式,线下工具尽早去暴露问题,而针对于线上工具呢,我们侧重于监控的全面性、自动化以及异常感知的灵敏度。

同时呢,卡顿问题还有很多的难题。比如需要高频率的收集日志信息,高频率的收集对后端有一定的压力(采用mars日志库,mmap),而我们高频收集的信息有很大一部分也是重复的,所以就需要日志去重操作;同时大量的字符会带来收集数据方面的困难,通过符号表进行堆栈信息转化后再上报也是要注意的。此外,卡顿监控它还有很多容易被忽略的一个盲区,如生命周期的一个间隔,那对于这种特定的问题呢,可以采用编译时注解的方式修改了项目当中所有Handler的父类,计算主线程message的执行时间以及它们的调用堆栈。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Android OOM(Out of Memory)是一种常见的运行时异常,指的是应用程序内存不足的错误。当应用程序试图使用超过系统分配给它的内存时,就会出现这种异常。这可能是由于应用程序在后台加载大量数据、存储过多的对象或图像,或者由于系统资源管理器分配的内存不足所致。 为了解决Android OOM问题,您可以采取以下几种策略: 1. 优化您的代码以减少内存使用量:使用正确的数据类型,避免创建不必要的对象,限制图像和资源的数量,以及优化后台加载过程等。 2. 回收不再使用的内存:当您的应用程序不再需要使用某些内存时,应该及时回收它们。这可以通过调用垃圾回收器(Garbage Collector)来完成。 3. 避免在主线程上执行耗时操作:如果您的应用程序在主线程上执行耗时操作(如大量数据处理),这可能导致系统资源管理器超载,从而引发OOM异常。应该将这些操作移至后台线程。 4. 使用内存分析工具:内存分析工具可以帮助您识别内存泄漏和无效内存引用等问题,从而避免OOM异常的发生。 5. 配置您的应用程序以适应不同的内存配置:如果您正在开发一个需要大量内存的应用程序,您应该考虑在AndroidManifest.xml文件中配置您的应用程序以适应不同的内存配置。例如,您可以设置您的应用程序需要的最低和最高内存限制。 请注意,解决Android OOM问题是一个复杂的过程,需要您仔细分析和优化您的代码。如果您遇到了OOM问题,建议寻求专业的帮助或与开发社区进行讨论。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值