2021年Android应用开发性能优化总结

Android面试技术要点汇总
2021-05-17 14:29 · 阅读时长9分钟
小课

在开发Android应用中,性能一直是一个非常重要的指标,如果应用的性能差,出现界面卡顿,等待时间长等问题,用户体验就会非常不好,很有可能就会因此失去部分用户,为了避免这种情况出现,我们在开发时就应该注意性能问题,下面我们就总结一些提升Android应用性能的方法。

我们可以从以下几个方面来提升应用的性能。

  • 减小应用安装包的体积
  • 避免内存泄漏
  • 优化内存使用
  • 优化视图布局
  • 缩短应用启动时间
  • 良好的编码习惯

下面一步一步来看看如果通过这些方面来提升应用的性能。

一、减小应用安装包的体积

减少安装包体积是提升应用性能最简单的方法之一,减少安装包体积可以提升应用加载速度,减少内存使用等等,而且安装包体积太多也会流失一部分潜在的新用户。

  • 使用App Bundle方式在应用市场发布,这样安装包体积能减少很大一部分,它主要通过Dynamic Delivery机制,根据不同的设备配置生成并提供经过优化的 APK,这样最终的安装包只包含该设备需要的代码和资源。
  • 优化资源,移除没有用的资源、压缩必须的资源,这些功能Android开发工具都自带了,我们只需要开启即可,比如通过minifyEnabled配置,可以R8的代码缩减功能,它能够自动移除未使用的代码,通过shrinkResources配置,可以开启资源缩减功能,它能够自动移除未使用的资源文件。使用第三方压缩工具压缩png、jpg等格式的图片或者使用webp格式。
  • 使用Android Size Analyzer来分析,它可以通过分析提供一些如何减小应用的建议。
二、避免内存泄漏

在Android开发中我们会使用到很多系统组件,有些组件拥有自己的生命周期,当这些组件生命周期结束后,别的代码还持有这些组件的引用,那么这些组件对象以及它所引用的资源就不会被及时回收,这样就会造成内存泄漏。比如说最常见一个错误示例如下:

1public class MainActivity extends Activity {
2  //...
3  Handler handler;
4  @Override
5  protected void onCreate(Bundle savedInstanceState) {
6    super.onCreate(savedInstanceState);
7    //...
8    handler = new Handler() {
9      @Override
10      public void handleMessage(Message msg) {}
11    }
12  }
13}

我们在activity中创建了一个handler,如果在acitivity销毁的时候,handler还存在未处理的消息,比如延时消息,此时message对象持有handler的引用,而handler持有acitivity的引用,即使acitivty生命周期已经结束,但是由于还存在引用,所以acitivity对象就不会被回收,它所引用的其它对象或资源也同样不会被回收,这样就造成了内存泄漏。对于组件的引用,我们一般可以通过弱引用的方式来避免内存泄漏,也可以通过监听组件的生命周期,在组件生命周期结束的时候释放对它的引用。

但是内存泄漏一般发生的不那么直接,隐蔽性比较强,所以在实际开发中,我建议使用LeakCanary这个工具来帮助我们发现内存泄漏问题。

Android Studio的Profiler同样也可以帮助查找内存泄漏,使用方式:开启Profiler,选择Memory,选择要内存时间段,dump内存,然后就可以查看内存泄漏情况。

2021年Android应用开发性能优化总结
2021年Android应用开发性能优化总结
三、优化内存使用

在Android开发中,优化内存是非常有必要的,因为移动设备的内存相对有限,如果内存使用超过限制,将会发生OutOfMemory(OOM)异常,而且问题发生点不容易定位,因为它是多方面因素导致的,崩溃点很可能并不是问题发生的根源。

  • 优化图片处理

在一般的Android开发中,图片加载一般是消耗内存的主要因素,所以对图片加载的处理十分重要,缓存、缩放还有回收释放内存等等,这些都比较麻烦,在实际开发中,如果不是特殊要求,我建议使用Glide,它可以完成上述的各种功能。

  • 检测内存情况

通过getMemoryClass来获取内存使用情况,通过onTrimMemory处理内存不足的情况,比如释放缓存的资源等。

四、优化视图布局

视图加载解析默认都是在主线程执行,如果XML视图文件过于复杂,可能会引起主线程卡顿。在编写XML布局时应该注意以下几点:

  • 减少布局嵌套层级。
  • 使用<include/>复用布局。
  • 使用<merge/>替换一些冗余的布局,比如根布局是FrameLayout且没有背景等特殊要求。
  • 复杂的布局尽量使用ConstraintLayout,它的性能有较大的提升。

如果由于业务需要,布局文件确实复杂,可以通过AsyncLayoutInflater来实现异步加载,或者使用Jetpack Compose来实现。

检测布局问题

五、优化应用启动时间

根据Google的调查,75%的用户可能会因为应用启动时间过长而放弃使用应用。应用有三种启动状态,冷启动、温启动或热启动,每种状态都会影响应用向用户显示所需的时间。在冷启动中,应用从头开始启动。在另外两种状态中,系统需要将后台运行的应用带入前台,我们主要是针对冷启动进行优化。

统计应用启动时间

在 Android 4.4(API 级别 19)及更高版本中,logcat 包含一个输出行,其中包含名为 Displayed 的值。此值代表从启动进程到在屏幕上完成对应 Activity 的绘制所用的时间。经过的时间包括以下事件序列:

  1. 启动进程。
  2. 初始化对象。
  3. 创建并初始化 Activity。
  4. 扩充布局。
  5. 首次绘制应用。

可以在logcat中查找到类似于以下示例:

ActivityManager: Displayed com.android.myexample/.StartupTiming: +3s534ms

或通过adb命令工具查看启动应用所用时间,示例如下:

adb [-d|-e|-s <serialNumber>] shell am start -S -W
    com.example.app/.MainActivity
    -c android.intent.category.LAUNCHER
    -a android.intent.action.MAIN

通过手动调用来reportFullyDrawn() 来测量应用启动的时间。比如,对于一些异步、延迟加载的布局的情况。以下是 logcat 输出的示例:

system_process I/ActivityManager: Fully drawn {package}/.MainActivity: +1s54ms

对于线上应用启动时间,我们无法通过上面几种方式来实现,我们需要在找准开始时间点和和结束时间点来记录启动时间,根据Application和Activity的生命周期我们可以知道,从启动应用到展示界面大致会经过以下几个阶段:

-> Application 构造函数
-> Application.attachBaseContext()
-> Application.onCreate()
-> Activity 构造函数
-> Activity.setTheme()
-> Activity.onCreate()
-> Activity.onStart
-> Activity.onResume
-> Activity.onAttachedToWindow
-> Activity.onWindowFocusChanged

一般情况我们选择以Application.attachBaseContext()作为启动开始的时间点,以Activity.onWindowFocusChanged作为启动结束的时间点即可计算出启动时间。

有些情况下,我们可能并不是在进入Activity时就开始绘制界面,而是某些准备工作做完才开始绘制,我们会通过OnPreDrawListener.onPreDraw()来控制何时开始绘制,此时应该在此处记录启动结束时间。

1val content: View = findViewById(android.R.id.content)
2content.viewTreeObserver.addOnPreDrawListener(
3    object : ViewTreeObserver.OnPreDrawListener {
4        override fun onPreDraw(): Boolean {
5            return if (isReady) {
6                content.viewTreeObserver.removeOnPreDrawListener(this)
7                true
8            } else {
9                false
10            }
11        }
12    }
13)

优化应用启动时间

  • 延时加载或初始化一些非必要的第三方库,可以在子线程处理的放在子线程处理。
  • 优化主界面的布局结构,减少嵌套,使用ViewStub延迟加载非隐藏的布局,如果首页使用大量Fragment,使用懒加载的方式加载Fragment。
  • 优化体验,为启动页设置主题背景,当用户点击应用图标后快速查看设置好的背景。

参考:应用启动时间

注释
六、良好的编码习惯
  • 优先使用静态方法,如果方法中并未用到对象的属性,则尽量将方法声明为静态方法,这样能够提升15%-20%的方法调用速度。
  • 对于业务中可以定义为常量的数据,使用static final来修饰。
  • 使用增强型for循环语法替换常规的for循环。
  • 当需要字符串拼接时,优先使用StringBuffer或者StringBuilder,而不是直接使用String + String的方式。
  • 避免自动拆装箱,可以提高效率还可以减少占用内存。
  • 使用SparseArray和ArrayMap替换HashMap。
  • 尽量不使用过时(Deprecation)的API。
  • 避免使用枚举类型,枚举类型比定义常量耗费更多内存。
  • 避免创建大量临时对象,可以减少垃圾回收触发。

参考:性能提示

注释
内存泄漏布局优化应用启动时间内存优化