Android开发基础知识

来自Android面试官性能优化面试 10 连炮,你能顶住么?

2020-12-21  本文已影响0人  蓝精灵8091

性能优化是很多 Android 程序员希望彻底掌握的一门技能。很多人都想学好性能优化,希望能够在自己的工作中灵活运用提高性能,从而为用户提供良好的用户体验。

然而,很多人在设计技术方案或者编码时缺乏系统的、方法论级别的指导,导致想做性能优化时缺乏思路。

面试时,性能优化相关的面试题经常会遇到,特别是一些常见的优化。

张工毕业3年了,一直在一家创业公司做Android开发,最近到某知名互联网公司面试,做了笔试题后, 面试官看了觉得还不错,于是想进一步考察张工的实际经验有多少, 就问之前在项目中有做过性能优化吗?

接下来看看面试官提出的性能优化面试题10连炮你能回答多少吧:

1、 你对 APP 的启动有过研究吗? 有做过相关的启动优化吗?

程序员:

之前做热修复的时候研究过 Application 的启动原理。项目中也做过一些启动优化。

面试官:

哦,你之前研究过热修复? (这个时候有可能就会深入的问问热修复的原理,这里咱们就不讨论热修复原理) 那你说说对启动方面都做了哪些优化?

程序员:

Ps:这里会让面试官感觉你是一个注重用户体验的

Ps: 这里会让面试官感觉你对 App 应用的启动流程研究的比较深,有过真实的翻阅底层源码,而并不是背诵答案。

知道了 attachBaseContextonCreate 在应用中最先启动,那么我们就可以通过 TreceView 等性能检测工具,来检测具体函数耗时时间,然后来对其做具体的优化。

Ps:
1、面试官这里会觉得你对启动优化确实了解的不错,有一定的启动优化经验。

2、在第五点面试官会觉得你比较关注该圈子的动态,发现好的解决方案,并能用在自己项目上。这一点是加分项!

  1. 项目不及时需要的代码通过异步加载。
  2. 将对一些使用率不高的初始化,做懒加载。
  3. 将对一些耗时任务通过开启一个 IntentService来处理。
  4. 还通过 redex 重排列 class 文件,将启动阶段需要用到的文件在 APK 文件中排布在一起,尽可能的利用 Linux 文件系统的 pagecache 机制,用最少的磁盘 IO 次数,读取尽可能多的启动阶段需要的文件,减少 IO 开销,从而达到提升启动性能的目的。
  5. 通过抖音发布的文章知晓在 5.0 低版本可以做 MultiDex 优化,在第一次启动的时候,直接加载没有经过 OPT 优化的原始 DEX,先使得 APP 能够正常启动。然后在后台启动一个单独进程,慢慢地做完 DEX 的 OPT 工作,尽可能避免影响到前台 APP 的正常使用。

这一步给面试官的感觉你对整个 Activity 的启动和 View 的绘制还有刷新机制都有深入的研究,那么此刻你肯定给面试官留了一个好印象,说明你平时对这些源码级别的研究比较广泛,透彻。

总结:

最后我基于以上的优化减少了 50% 启动时间。

面试官:

嗯,研究的挺深的,源码平时不少看吧。

程序员:

到这里,我知道这一关算是过了!

2、有做过相关的内存优化吗?

程序员:

有做过,目前的项目内存优化还是挺多的,要不我先说一下优化内存有什么好处吧?咱们不能盲目的去优化!

有的时候对于自己熟悉的领域,一定要主动出击,自己主导这场面试。

面试官:

可以。

Ps:这里大多数面试官会同意你的请求,除非遇见装B的。

程序员:

好处:

  1. 减少 OOM ,可以提高程序的稳定性。
  2. 减少卡顿,提高应用流畅性。
  3. 减少内存占用,提高应用后台存活性。
  4. 减少程序异常,降低应用 Crash 率, 提高稳定性。

那么我基于这四点,我的程序做了如下优化:

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

还有我们要明白为什么应用程序会发送 OOM ,又该怎么去避免它?

发生 OOM 的场景是当申请 1M 的内存空间时,如果你要往该内存空间存入 2M 的数据,那么此时就会发生 OOM。

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

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

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

  1. 资源型对象未关闭: Cursor,File
  2. 注册对象未销毁: 广播,回调监听
  3. 类的静态变量持有大数据对象
  4. 非静态内部类的静态实例
  5. Handler 临时性内存泄漏: 使用静态 + 弱引用,退出即销毁
  6. 容器中的对象没清理造成的内存泄漏
  7. WebView: 使用单独进程

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

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

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



3. 基于开源框架来监控

怎么避免卡顿:

一定要避免在主线程中做耗时任务,总结一下 Android 中主线程的场景:
1\. UI 生命周期的控制 ​ 2\. 系统事件的处理 ​ 3\. 消息处理 ​ 4\. 界面布局 ​ 5\. 界面绘制 ​ 6\. 界面刷新 ​ 7\. ... ​
还有一个最重要的就是避免内存抖动,不要在短时间内频繁的内存分配和释放。

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

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

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

  2. 内存复用

  3. 使用最优的数据类型

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

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

    1.  选择合适的位图格式
    
    2.  bitmap 内存复用,压缩
    
    3.  图片的多级缓存
    
  6. 基本数据类型如果不用修改的建议全部写成 static final,因为 它不需要进行初始化工作,直接打包到 dex 就可以直接使用,并不会在 类 中进行申请内存

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

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

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

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

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

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

3、你在项目中有没有遇见卡顿问题?是怎么排查卡顿?又是怎么优化的?

程序员:

有遇见, 比如在主线程中做耗时操作、频繁的创建对象和销毁对象导致 GC 回收频繁、布局的层级多等。

面试官:

嗯,那具体说说是怎么优化的。

程序员:

这里我们还是可以从显示原理和优化建议来展开说明,参考如下:

  1. 显示原理:

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

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

  1. 卡顿的根本原因:

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

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

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

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

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

    1. 基于 Choreographer 回调函数 postFrameCallback 来监控
  1. 基于开源框架 BlockCanary 来监控

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

  3. 怎么可以提高程序运行流畅

1.布局优化:

1.1 布局优化分析工具:

1.2 优化方案:

  1. 提升动画性能

    1. 尽量别用补间动画,改为属性动画,因为通过性能监控发现补间动画重绘非常频繁

    2. 使用硬件加速提高渲染速度,实现平滑的动画效果。

  2. 怎么避免卡顿:

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

  1. UI 生命周期的控制
  2. 系统事件的处理
  3. 消息处理
  4. 界面布局
  5. 界面绘制
  6. 界面刷新
  7. ...

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

4、怎么保证 APP 的稳定运行?

程序员:

保证程序的稳定我们可以从内存、代码质量、Crash、ANR、后台存活等知识点来展开优化。

面试官:

那你具体说说你是怎么做的?

程序员:

1.内存

可以从第二点内存优化来说明

2.代码质量

  1. 团队之前相互代码审查,保证了代码的质量,也可以学习到了其它同事码代码的思想。

  2. 使用 Link 扫描代码,查看是否有缺陷性。

3. Crash

  1. 通过实现 Thread.UncaughtExceptionHandler 接口来全局监控异常状态,发生 Crash 及时上传日志给后台,并且及时通过插件包修复。

  2. Native 线上通过 Bugly 框架实时监控程序异常状况,线下局域网使用 Google 开源的 breakpad 框架。发生异常就搜集日志上传服务器(这里要注意的是日志上传的性能问题,后面省电模块会说明)

4. ANR

5. 后台存活

面试官:

嗯,你对知识点掌握的挺好。

说完这些,这一关也算是过了。

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

程序员:

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

面试官:

那你具体说一下吧

程序员

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

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

程序员:

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

面试官:

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

程序员:

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

7、你在项目中有做过自定义 View 吗?有对它做过什么优化?

程序员:

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

面试官:

那具体说一说

程序员:

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

8、你们项目的耗电量怎么样? 有做过优化吗?

程序员:

在没有优化之前持续工作 30 分钟的耗电量是 8%, 优化后是 4%。

面试官:

那你说一说你是怎么优化的。

程序员:

因为我们产品是一款社交通信的软件,有音视频通话、GPS 定位上报、长连接的场景,所以优化起来确实有点困难。不过最后也还是优化了一半的电量下去。主要做了如下优化:

说出这些之后,在结合项目一个真实的优化点来说明一下。

9、有做过日志优化吗?

程序员:

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

面试官:

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

程序员:

展开上面这些点说明之后,面试官一般不会为难你。

10、你们 APK 有多大?有做过 APK 体积相关的优化吗?

程序员:

有过优化,在没有优化之前项目的包体积大小是 80M,优化之后是 50M.

面试官:

说一说是怎么优化的

程序员:

基于这几点优化方案,一般都能解决 APK 体积问题。最后再把自己项目 APK 体积优化步骤结合上面点说一下就行。

总结

其实性能优化点都是息息相关的,比如卡顿会涉及内存、显示,启动也会涉及 APK dex 的影响。所以说性能优化不仅仅是单方面的优化,一定要掌握最基本的优化方案,才能更加深入探讨性能原理问题。

在这里也建立大家多看流行开源框架源码,比如 Glide (内存方面), OKhttp (网络连接方面) 优化的真的很极致。到这里性能优化方面的知识也就说完了,下来一定好好去消化。

Android及性能优化相关学习资源

其实客户端开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

这里再分享一下我面试期间的复习路线:(以下体系的复习资料是我从各路大佬收集整理好的)

《Android开发七大模块核心知识笔记》

《379页Android开发面试宝典》

历时半年,我们整理了这份市面上最全面的安卓面试题解析大全
包含了腾讯、百度、小米、阿里、乐视、美团、58、猎豹、360、新浪、搜狐等一线互联网公司面试被问到的题目。熟悉本文中列出的知识点会大大增加通过前两轮技术面试的几率。

如何使用它?

1.可以通过目录索引直接翻看需要的知识点,查漏补缺。
2.五角星数表示面试问到的频率,代表重要推荐指数

《333页Android 性能优化PDF宝典》

从原理到实战,一应俱全!这份宝典主要涉及以下三个方面:

1、设计思想与代码质量优化(六大原则、设计模式、数据结构、算法)
2、程序性能优化(启动速度与执行效率优化、布局检测与优化、内存优化、耗电优化、网络传输与数据存储优化、APK 大小优化)
3、开发效率优化(分布式版本控制系统 Git、自动化构建系统 Gradle)

资料太多,全部展示会影响篇幅,暂时就先列举这些部分截图,以上资源均免费分享,以上内容均放在了开源项目:github 中已收录,大家可以自行获取(或者关注主页扫描加微信获取)。

上一篇下一篇

猜你喜欢

热点阅读