写点什么

Android- 面试官:性能优化我就问这些问题!能不能刷到我就看你的造化了

用户头像
Android架构
关注
发布于: 刚刚

在应用程序中我们不仅要避免直接导致 OOM 的场景还要避免间接导致 OOM 的场景。间接的话也就是要避免内存泄漏的场景。


内存泄漏的场景是这个对象不再使用时,应用完整的执行最后的生命周期,但是由于某些原因,对象虽然已经不再使用,仍然会在内存中存在而导致 GC 不会去回收它,这就意味着发生了内存泄漏。(这里可以介绍下 GC 回收机制,回收算法,知识点尽量往外扩展而不脱离本题)


最后在说一下在实际开发中避免内存泄漏的场景:


  1. 资源型对象未关闭: Cursor,File

  2. 注册对象未销毁: 广播,回调监听

  3. 类的静态变量持有大数据对象

  4. 非静态内部类的静态实例

  5. Handler 临时性内存泄漏: 使用静态 + 弱引用,退出即销毁

  6. 容器中的对象没清理造成的内存泄漏

  7. WebView: 使用单独进程


其实这些都是基础,把它记下就行了。记得多了在实际开发中就有印象了。


  • 2.减少卡顿


怎么减少卡顿? 那么我们可以从 2 个原理方面来探讨卡顿的根本原因,第一个原理方面是绘制原理,另一个就是刷新原理。


  1. 绘制原理:



  1. 刷新原理:


View 的 requestLayout 和 ViewRootImpl##setView 最终都会调用 ViewRootImpl 的 requestLayout 方法,然后通过 scheduleTraversals 方法向 Choreographer 提交一个绘制任务,然后再通过 DisplayEventReceiver 向底层请求 vsync 垂直同步信号,当 vsync 信号来的时候,会通过 JNI 回调回来,在通过 Handler 往消息队列 post 一个异步任务,最终是 ViewRootImpl 去执行绘制任务,最后调用 performTraversals 方法,完成绘制。


详细流程可以参考下面流程图:



卡顿的根本原因:


从刷新原理来看卡顿的根本原理是有两个地方会造成掉帧:


一个是主线程有其它耗时操作,导致 doFrame 没有机会在 vsync 信号发出之后 16 毫秒内调用;


还有一个就是当前 doFrame 方法耗时,绘制太久,下一个 vsync 信号来的时候这一帧还没画完,造成掉帧。


既然我们知道了卡顿的根本原因,那么我们就可以监控卡顿,从而可以对卡顿优化做到极致。我们可以从下面四个方面来监控应用程序卡顿:


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


//1. 开启监听 Looper.myLooper().setMessageLogging(newLogPrinter(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);}}}


  1. 基于 Choreographer 回调函数 postFrameCallback 来监控



  1. 基于开源框架 BlockCanary 来监控

  2. 基于开源框架 rabbit-client 来监控


怎么避免卡顿:


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


  1. UI 生命周期的控制

  2. 系统事件的处理

  3. 消息处理

  4. 界面布局

  5. 界面绘制

  6. 界面刷新

  7. ...


还有一个最重要的就是避免内存抖动,不要在短时间内频繁的内存分配和释放。


基于这几点去说卡顿肯定是没有问题的。


  • 3.减少内存占用


可以从如下几个方面去展开说明:


  1. AutoBoxing(自动装箱): 能用小的坚决不用大的。

  2. 内存复用

  3. 使用最优的数据类型

  4. 枚举类型: 使用注解枚举限制替换 Enum

  5. 图片内存优化(这里可以从 Glide 等开源框架去说下它们是怎么设计的)

  6. 选择合适的位图格式

  7. bitmap 内存复用,压缩

  8. 图片的多级缓存

  9. 基本数据类型如果不用修改的建议全部写成 static final,因为 它不需要进行初始化工作,直接打包到 dex 就可以直接使用,并不会在 类 中进行申请内存

  10. 字符串拼接别用 +=,使用 StringBuffer 或 StringBuilder

  11. 不要在 onMeause, onLayout, onDraw 中去刷新 UI

  12. 尽量使用 C++ 代码转换 YUV 格式,别用 Java 代码转换 RGB 等格式,真的很占用内存


  • 4.减少程序异常


减少程序异常那么我们可以从稳定性和 Crash 来分别说明。


这个我们将在第四点会详细的介绍程序的稳定性和 Crash 。


如果说出这些,再实际开发中举例说明一下怎么解决的应该是没有问题的。

歇会儿

四、App 绘制优化

1、你在做布局优化的过程中用到了哪些工具?

我在做布局优化的过程中,用到了很多的工具,但是每一个工具都有它不同的使用场景,不同的场景应该使用不同的工具。下面我从线上和线下两个角度来进行分析。


比如说,我要统计线上的 FPS,我使用的就是 Choreographer 这个类,它具有以下特性:


  • 1、能够获取整体的帧率。

  • 2、能够带到线上使用。

  • 3、它获取的帧率几乎是实时的,能够满足我们的需求。


同时,在线下,如果要去优化布局加载带来的时间消耗,那就需要检测每一个布局的耗时,对此我使用的是 AOP 的方式,它没有侵入性,同时也不需要别的开发同学进行接入,就可以方便地获取每一个布局加载的耗时。如果还要更细粒度地去检测每一个控件的加载耗时,那么就需要使用 LayoutInflaterCompat.setFactory2 这个方法去进行 Hook。


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

2、布局为什么会导致卡顿,你又是如何优化的?

分析完布局的加载流程之后,我们发现有如下四点可能会导致布局卡顿:


  • 1、首先,系统会将我们的 Xml 文件通过 IO 的方式映射的方式加载到我们的内存当中,而 IO 的过程可能会导致卡顿。

  • 2、其次,布局加载的过程是一个反射的过程,而反射的过程也会可能会导致卡顿。

  • 3、同时,这个布局的层级如果比较深,那么进行布局遍历的过程就会比较耗时。

  • 4、最后,不合理的嵌套 RelativeLayout 布局也会导致重绘的次数过多。


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


  • 1、针对布局加载 Xml 文件的优化,我们使用了异步 Inflate 的方式,即 AsyncLayoutInflater。它的核心原理是在子线程中对我们的 Layout 进行加载,而加载完成之后会将 View 通过 Handler 发送到主线程来使用。所以不会阻塞我们的主线程,加载的时间全部是在异步线程中进行消耗的。而这仅仅是一个从侧面缓解的思路。

  • 2、后面,我们发现了一个从根源解决上述痛点的方式,即使用 X2C 框架。它的一个核心原理就是在开发过程我们还是使用的 XML 进行编写布局,但是在编译的时候它会使用 APT 的方式将 XML 布局转换为 Java 的方式进行布局,通过这样的方式去写布局,它有以下优点:1、它省去了使用 IO 的方式去加载 XML 布局的耗时过程。2、它是采用 Java 代码直接 new 的方式去创建控件对象,所以它也没有反射带来的性能损耗。这样就从根本上解决了布局加载过程中带来的问题。

  • 3、然后,我们可以使用 ConstraintLayout 去减少我们界面布局的嵌套层级,如果原始布局层级越深,它能减少的层级就越多。而使用它也能避免嵌套 RelativeLayout 布局导致的重绘次数过多。

  • 4、最后,我们可以使用 AspectJ 框架(即 AOP)和 LayoutInflaterCompat.setFactory2 的方式分别去建立线下全局的布局加载速度和控件加载速度的监控体系。

3、做完布局优化有哪些成果产出?

  • 1、首先,我们建立了一个体系化的监控手段,这里的体系还指的是线上加线下的一个综合方案,针对线下,我们使用 AOP 或者 ARTHook,可以很方便地获取到每一个布局的加载耗时以及每一个控件的加载耗时。针对线上,我们通过 Choreographer.getInstance().postFrameCallback 的方式收集到了 FPS,这样我们可以知道用户在哪些界面出现了丢帧的情况。

  • 2、然后,对于布局监控方面,我们设立了 FPS、布局加载时间、布局层级等一系列指标。

  • 3、最后,在每一个版本上线之前,我们都会对我们的核心路径进行一次 Review,确保我们的 FPS、布局加载时间、布局层级等达到一个合理的状态。

4、你是怎么做卡顿优化的?

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


  • 1、系统工具定位、解决

  • 2、自动化卡顿方案及优化

  • 3、线上监控及线下监测工具的建设


我做卡顿优化也是经历了一些阶段,最初我们的项目当中的一些模块出现了卡顿之后,我是通过系统工具进行了定位,我使用了 Systrace,然后看了卡顿周期内的 CPU 状况,同时结合代码,对这个模块进行了重构,将部分代码进行了异步和延迟,在项目初期就是这样解决了问题。但是呢,随着我们项目的扩大,线下卡顿的问题也越来越多,同时,在线上,也有卡顿的反馈,但是线上的反馈卡顿,我们在线下难以复现,于是我们开始寻找自动化的卡顿监测方案,其思路是来自于 Android 的消息处理机制,主线程执行任何代码都会回到 Looper.loop 方法当中,而这个方法中有一个 mLogging 对象,它会在每个 message 的执行前后都会被调用,我们就是利用这个前后处理的时机来做到的自动化监测方案的。同时,在这个阶段,我们也完善了线上 ANR 的上报,我们采取的方式就是监控 ANR 的信息,同时结合了 ANR-WatchDog,作为高版本没有文件权限的一个补充方案。在做完这个卡顿检测方案之后呢,我们还做了线上监控及线下检测工具的建设,最终实现了一整套完善,多维度的解决方案。

5、你是怎么样自动化的获取卡顿信息?

我们的思路是来自于 Android 的消息处理机制,主线程执行任何代码它都会走到 Looper.loop 方法当中,而这个函数当中有一个 mLogging 对象,它会在每个 message 处理前后都会被调用,而主线程发生了卡顿,那就一定会在 dispatchMessage 方法中执行了耗时的代码,那我们在这个 message 执行之前呢,我们可以在子线程当中去 postDelayed 一个任务,这个 Delayed 的时间就是我们设定的阈值,如果主线程的 messaege 在这个阈值之内完成了,那就取消掉这个子线程当中的任务,如果主线程的 message 在阈值之内没有被完成,那子线程当中的任务就会被执行,它会获取到当前主线程执行的一个堆栈,那我们就可以知道哪里发生了卡顿。


经过实践,我们发现这种方案获取的堆栈信息它不一定是准确的,因为获取到的堆栈信息它很可能是主线程最终执行的一个位置,而真正耗时的地方其实已经执行完成了,于是呢,我们就对这个方案做了一些优化,我们采取了高频采集的方案,也就是在一个周期内我们会多次采集主线程的堆栈信息,如果发生了卡顿,那我们就将这些卡顿信息压缩之后上报给 APM 后台,然后找出重复的堆栈信息,这些重复发生的堆栈大概率就是卡顿发生的一个位置,这样就提高了获取卡顿信息的一个准确性。

6、卡顿的一整套解决方案是怎么做的?

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


同时呢,卡顿问题还有很多的难题。比如说有的代码呢,它不到你卡顿的一个阈值,但是执行过多,或者它错误地执行了很多次,它也会导致用户感官上的一个卡顿,所以我们在线下通过 AOP 的方式对常见的耗时代码进行了 Hook,然后对一段时间内获取到的数据进行分析,我们就可以知道这些耗时的代码发生的时机和次数以及耗时情况。然后,看它是不是满足我们的一个预期,不满足预期的话,我们就可以直接到线下进行修改。同时,卡顿监控它还有很多容易被忽略的一个盲区,比如说生命周期的一个间隔,那对于这种特定的问题呢,我们就采用了编译时注解的方式修改了项目当中所有 Handler 的父类,对于其中的两个方法进行了监控,我们就可以知道主线程 message 的执行时间以及它们的调用堆栈。


对于线上卡顿,我们除了计算 App 的卡顿率、ANR 率等常规指标之外呢,我们还计算了页面的秒开率、生命周期的执行时间等等。而且,在卡顿发生的时刻,我们也尽可能多地保存下来了当前的一个场景信息,这为我们之后解决或者复现这个卡顿留下了依据。

五、说说你在项目中网络优化?

程序员:


有,这一点其实可以通过 OKHTTP 连接池和 Http 缓存来说一下(当然这里不会再展开分析 OKHTTP 源码了)


面试官:


那你具体说一下吧


程序员


移动端获取网络数据优化的几个点

  • 1、连接复用:节省连接建立时间,如开启 keep-alive。于 Android 来说默认情况下 HttpURLConnection 和 HttpClient 都开启了 keep-alive。只是 2.2 之前 HttpURLConnection 存在影响连接池的 Bug。

  • 2、请求合并:即将多个请求合并为一个进行请求,比较常见的就是网页中的 CSS Image Sprites。如果某个页面内请求过多,也可以考虑做一定的请求合并。

  • 3、减少请求数据的大小:对于 post 请求,body 可以做 gzip 压缩的,header 也可以做数据压缩(不过只支持 http 2.0)。 返回数据的 body 也可以做 gzip 压缩,body 数据体积可以缩小到原来的 30%左右(也可以考虑压缩返回的 json 数据的 key 数据的体积,尤其是针对返回数据格式变化不大的情况,支付宝聊天返回的数据用到了)。

  • 4、根据用户的当前的网络质量来判断下载什么质量的图片(电商用的比较多)。

  • 5、使用 HttpDNS 优化 DNS:DNS 存在解析慢和 DNS 劫持等问题,DNS 不仅支持 UDP,它还支持 TCP,但是大部分标准的 DNS 都是基于 UDP 与 DNS 服务器的 53 端口进行交互。HTTPDNS 则不同,顾名思义它是利用 HTTP 协议与 DNS 服务器的 80 端口进行交互。不走传统的 DNS 解析,从而绕过运营商的 LocalDNS 服务器,有效的防止了域名劫持,提高域名解析的效率。



说了这些之后,再说一下你当前使用网络框架它们做了哪些优化比如 OKHTTP(Socket 连接池、Http 缓存、责任链)、Retrofit(动态代理)。说了这些一般这关也算是过了。

六、在项目中有用过哪些存储方式? 对它们的性能有过优化吗?

程序员:


主要用过 sp,File,SQLite 存储方式。其中对 sp 和 sqlite 做了优化。


面试官:


那你说说都做了哪些优化?


程序员:



这一块如果你使用过其它第三方的数据库,可以说说它们的原理和它们存取的方式。

七、用过自定义 View 吧?对它做过什么优化?

程序员:


有做过。比如重复绘制,还有大图长图有过优化。


面试官:


那具体说一说


程序员:



最后也是结合真实场景具体说一个。


如何优化自定义 View?为了加速你的 view,对于频繁调用的方法,需要尽量减少不必要的代码。先从 onDraw 开始,需要特别注意不应该在这里做内存分配的事情,因为它会导致 GC,从而导致卡顿。在初始化或者动画间隙期间做分配内存的动作。不要在动画正在执行的时候做内存分配的事情。


你还需要尽可能的减少 onDraw 被调用的次数,大多数时候导


《Android学习笔记总结+最新移动架构视频+大厂安卓面试真题+项目实战源码讲义》
浏览器打开:qq.cn.hn/FTe 免费领取
复制代码


致 onDraw 都是因为调用了 invalidate().因此请尽量减少调用 invaildate()的次数。如果可能的话,尽量调用含有 4 个参数的 invalidate()方法而不是没有参数的 invalidate()。没有参数的 invalidate 会强制重绘整个 view。


另外一个非常耗时的操作是请求 layout。任何时候执行 requestLayout(),会使得 Android UI 系统去遍历整个 View 的层级来计算出每一个 view 的大小。如果找到有冲突的值,它会需要重新计算好几次。另外需要尽量保持 View 的层级是扁平化的,这样对提高效率很有帮助。


如果你有一个复杂的 UI,你应该考虑写一个自定义的 ViewGroup 来执行他的 layout 操作。与内置的 view 不同,自定义的 view 可以使得程序仅仅测量这一部分,这避免了遍历整个 view 的层级结构来计算大小。

八、有做过日志优化吗?

程序员:


有优化,在之前没有考虑任何性能的情况下,我是直接有 log 就写入文件,尽管我开了线程池去写文件,只要软件在运行那么就会频繁的使 CPU 进行工作。这也间接的导致了耗电。


传统日志打印有两个性能问题,一个是反复操作文件描述符表,一个是反复进入内核态。所以需要使用 mmap 的方式去直接读写内存。


面试官:


那你具体说一下,最后怎么解决这个问题的?


程序员:



用户头像

Android架构

关注

还未添加个人签名 2021.10.31 加入

还未添加个人简介

评论

发布
暂无评论
Android-面试官:性能优化我就问这些问题!能不能刷到我就看你的造化了