Android浅析之性能优化

作为一个Android开发者,在日常的开发测试工作以及面试中,性能优化是绕不开的话题,这篇文章将从启动、布局、绘制、内存、包体不同维度跟大家系统讲解APP性能优化的方案。

一、启动速度优化

想要进行启动速度优化,我们先从不同场景分析一下APP的启动过程,我们都知道启动场景又分为冷启动、热启动、温启动三种。

冷启动

启动过程:

graph TD
1.加载并启动应用程序 --> 2.显示应用程序空白窗口 -->3.创建应用程序进程 --> 4.启动主线程 --> 5.创建主Activity  --> 6.加载测量绘制布局
复制代码

热启动

热启动相当于用户已经按home键将app置于后台,此时启动app如果activity仍驻留在内存中,将避免重复对象初始化、布局渲染绘制。
热启动和冷启动相同的屏幕行为:显示应用程序空白窗口,直到应用程序完成绘制活动。

温启动

温启动相当于用户已经按back键退出app,此时启动app如果进程仍驻留在内存中,将只会执行创建主activity,执行onCreate()回调;如果app进程已经被回收,将重新加载并启动应用程序,但是在调用onCreate()的时候可以从Bundle(savedInstanceState)获取数据。

了解各场景的启动过程之后,我们再来分析如果着手进行启动速度优化,其实无论何种场景启动,我们的优化点都集中在application和activity的创建和回调流程。

优化点1:在显示应用程序空白窗口时设置启动页背景,避免黑白屏的情况出现

这一点相信很多Android开发都有实践过,不过多描述,就是给启动页单独设置一个theme,然后设置windowBackgroud.

优化点2:避免在应用程序启动时做繁琐的初始化操作

无论是在Application还是主activity的onCreate函数中,都应该避免做较为耗时的初始化操作,比如辅助类型的第三库:bugly、友盟等,我们可以异步初始化,而像地图、视频等某些特定页面场景才会使用的三方库,可以在主界面呈现出来之后初始化,而其他必须的如神策、图片、网络等框架不可避免。

优化点3:避免I/O、网络请求、序列化、过度绘制

在启动过程中,这类耗时操作应尽量避免,I/O、网络请求、序列化,应选择适当的时机,而过度绘制则需要进行布局优化,下面着重讲一下布局优化

二、布局绘制优化

布局优化

在进行布局代码编写时,我们应尽量避免过度绘制的情况出现,所谓过度绘制,就是同一区域被重复多次绘制,而最终的呈现效果仅以最上层为准,白白造成了cpu和gpu的性能损耗,下图诠释了过度绘制的状态: image.png

那我们在开发过程中,如何避免过度绘制的情况出现呢?

优化点1:使用viewStub、merge、include

include标签并不能减少布局嵌套,一般用于布局复用,在开发过程中常与merge配合使用,merge标签包裹的布局在使用时取决于父布局类型,在嵌入父布局后会自动删除标签,减少布局嵌套,与include搭配就能达到减少布局嵌套以及布局复用的效果。而viweStub相当于是一个view的占位,并没有进行实际的绘制操作,当进行特定操作时才会进行实际的渲染绘制,相较于绘制一个view用显示隐藏去控制的方式性能消耗更优。

优化点2:使用ConstraintLayout减少布局嵌套

ConstraintLayout能在同一层级实现复杂的布局效果,相比linearLayout、RelativeLayout等布局有更好的扩展性,可以大幅度减少布局嵌套

绘制优化

布局和绘制是影响界面显示的两个关键因素,而布局的优化简单但是比较低效,而绘制优化是重点,在进行绘制优化前,我们先了解一下Android系统的绘制流程,Android系统每16ms便会发出一次VSYNC信号触发界面刷新渲染,如果我们的应用程序在连续的16ms的过程中,可以完整走完绘制流程,那么界面效果在视觉上 是很流畅的,如下图:

image.png 而造成界面交互效果卡顿的原因,往往是因为app在这16ms内完不成任务,而其中绘制的关键函数就是View的onDraw(),我们进行绘制优化的关键就是优化onDraw()函数.

优化点1:在onDraw()函数中不要进行耗时操作

由于onDraw()函数的调用频率极高,如果在此函数中进行耗时操作或者循环甚至嵌套循环都会消耗大量cpu资源,造成不必要的内存损耗

优化点2:在onDraw()函数中不要创建新的局部对象

如果在onDraw()函数中创建新的局部对象,那在进行页面绘制过程中会产生大量的内存垃圾,频繁触发gc,造成执行效率低下

三、内存优化

内存泄漏

内存泄漏会导致内存中存在不可回收的对象,从而导致gc频繁触发,而gc触发时会导致所有线程暂停,在界面呈现的效果就会卡顿,那什么情况下回出现内存泄漏呢,主要分为四个场景。

集合类泄漏

当元素被添加到集合中后,元素依然被引用就会导致集合中的元素无法被回收,从而导致内存泄漏

fun main(){
    val mutableListOf = mutableListOf<Item>()
    for (i in 1..10){
        var item : Item? = Item("$i")
        mutableListOf.add(item!!)
        item = null
    }
    mutableListOf.forEach {
        print(it.name)
    }
}

当list不再需要使用时,同样也需要将内部的元素清空,自身引用释放

mutableListOf.clear()
mutablelistof = null

单例/静态变量引用对象造成内存泄露

object ScreentUtil(var context : Context){
    fun getScreentWidth(){
        context...
    }
}

当我们在Activity中调用该工具类并把自身对象传入,则该工具类将一直持有Activity对象,而该工具类的生命周期与应用程序一致,就会导致Activity无法被回收,从而造成内存泄露。所以在此处我们应该直接使用Application的context,而非activity。

匿名内部类/非静态内部类造成内存泄露

匿名内部类的常用场景比如在Activity中我们实例化了一个匿名内部的handler对象,而在倒计时场景下,我们需要做一些异步的倒计时操作然后通知Activity的控件去进行界面刷新,此时在刷新时有可能Activity已经被关闭了,通知刷新的步骤也就没有必要了,Activity应当是要被回收的,却因为被匿名内部类的handler对象所持有引用,从而造成内存泄露。解决方案就是将handler对象静态化,但是静态化后handler对象没有持有Activity引用了,此时拿不到控件了该怎么办?不慌,这时候我们可以将Activity作为handler的引用对象,但是是作为弱引用对象,弱引用对象在gc回收时是会直接被回收掉的,再也不用担心Activity被引用造成内存泄露了!

示例代码如下:

public class TestActivity extends Activity {
    private TextView mText;
    private MyHandler myHandler = new MyHandler(TestActivity.this);
    private MyThread myThread = new MyThread();

    private static class MyHandler extends Handler {

        WeakReference<TestActivity> weakReference;

        MyHandler(TestActivity testActivity) {
            this.weakReference = new WeakReference<TestActivity>(testActivity);

        }

        @Override
        public void handleMessage(Message msg) {
            super.handleMessage(msg);
            weakReference.get().mText.setText("do someThing");

        }
    }

    private static class MyThread extends Thread {

        @Override
        public void run() {
            super.run();

            try {
                sleep(100000);

            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test);
        mText = findViewById(R.id.mText);
        myHandler.sendEmptyMessageDelayed(0, 100000);
        myThread.start();
    }
    //最后清空这些回调 
    @Override
    protected void onDestroy() {
        super.onDestroy();
        myHandler.removeCallbacksAndMessages(null);
    }

非静态内部类同理,举例场景可以用内部类的AsyncTask去做耗时操作时造成引用Activity无法回收的情况

资源未及时关闭造成内存泄露

  • 网络、文件流未关闭
  • service结束任务后未调用stopSelf
  • 广播、eventBus注册后未及时注销

像这类情况一般可以借助三方工具leakcanary来帮助我们分析解决。

四、包体优化

开启lint资源检查以及资源压缩删减无用资源

图片资源优化

  • 本地图片转换为webp或压缩
  • 网络图片按规格适当质量压缩
  • shape代替图片
  • 动画代替多张图片

代码混淆

远程动态加载资源

例如一些特定场景需要用到的功能库.so文件在应用程序启动后去动态下载

五、电量优化

网络相关

  • 避免不必要的网络请求,比如在断网情况下
  • 网络请求时进行数据压缩,减少I/O执行时长
  • 避免网络轮询

传感器相关

  • GPS根据场景决定使用模式,非必要不适用高精度模式

wakeLock

  • 在进行屏幕唤醒时应当acquire、release成对使用释放;
  • 进行acquire时应设置超时时间,避免出现release无法调用情况;
  • 释放尽量使用try-catch-finally进行调用,保证出现异常时release也可以及时调用

猜你喜欢

转载自juejin.im/post/7111722701751861279