App 启动优化的各种方案以及实例

作为性能优化的第一篇文章,就来讲讲 App 的启动优化。


零、前言

在说 app 启动优化前,我们需要对手机开机(系统启动)过程有一定了解,毕竟你的 app 是运行在安卓手机上的。

1. 手机开机,打开电源,手机硬件本身的引导芯片会加载引导程序 BootLoader 到 RAM 中执行。
2. BootLoader 拉起操作系统
3. Linux 内核启动,开始系统设置,找到 init.rc 的文件启动初始化进程
4. init 进程(id 为 1)初始化和启动服务,之后会开启 Zygote 进程
5. Zygote 进程开始创建 JVM 并注册 JNI 方法,开启 SystemServer
6. 启动 Binder 线程池 和 SystemServiceManger,并启动各种服务(ActivityManagerServiceWindowManagerService等等)
7. ActivityManagerService 启动 Launcher 

这里的 Launcher 其实也是一个 app,我们的 app 在 Launcher 这个 app 里的外在表现就是一个图标,当我们点击应用图标时,也就是执行了 Launcher.java 中的 onClick(View view) 方法,把应用相关信息传入,获取 intent,这里的 intent 就是进程信息。

进程信息传回系统(AMS),系统内部的 Zygote 进程会 fork 出 SystemServer,然后通过 ActivityThread 创建进程,在 ActivityThread 类中的 main 方法中会初始化 application。



一、优化方向

上面的过程都是系统所做的事,作为开发者是无法在这些过程中进行优化的,但是通过上面的了解,我们知道当我们的 app 被点击后,一直到第一个页面渲染到手机屏幕上,我们能优化的地方有两个类:application 类 和 第一个 Activity

说的更为详细点就是我们优化的主要关注点就是 application 的 onCreate() 方法以及第一个 Activity 的 onCreate(),onStart(),onResume() 方法。



二、启动分类

在介绍具体的优化方案前,我们对 app 的启动分类做一个说明。在官方教程中,将 app 的启动分为 3 类:冷启动,热启动以及温启动。


2.1 冷启动

应用从头开始启动,场景就是程序安装后的第一次启动,或者系统终止应用后首次启动。冷启动的流程:

1. 加载并启动 App
2. 启动后立即为该 App 显示一个空白启动窗口
3. 创建 App 进程(创建应用程序对象)
4. 创建主 Activity
5. 加载布局,绘制

2.2 热启动

系统的所有工作就是将您的 Activity 带到前台,简单说就是程序仍驻留在内存中,只是被系统从后台带到前台,因此程序可以避免重复对象初始化,加载布局和渲染。


2.3 温启动

涵盖冷启动期间发生的操作的一系列子集,场景就是不停点击back键,直到所有 activity 都退出,这个时候的启动就是温启动,它必须通过调用 onCreate() 方法重新创建活动。



三 、测量启动时间

既然我们需要对 app 的启动进行优化,实际上就是优化的 app 的启动时间,那么我们就需要先知道我们自己所写的 app 的启动时间。

测量启动时间的方式大概有 3 种,下面就一一介绍下。


3.1 系统日志输出

在 Android 4.4 及更高版本中。logcat 包括一个输出行,其中包含命令为 Displayed 的值,此值代表从 启动进程 到 在屏幕上完成对应 Activity 绘制所经过的时间。

我们启动一个app,在 logcat 中过滤值为 displayed,在日志中可以看到:

2021-09-17 16:56:39.392 995-1025/? I/ActivityManager: Displayed com.sample.performance/.MainActivity: +1s131ms (total +1s466ms)

此日志输出表示我们的程序从启动到 MainActivity 初始化完成经历的时间。


3.2 ADB命令获取

具体的命令如下:

adb shell am start -W [项目包名]/[要启动的Activity的名字]

举例如下:

kongweiweideMacBook-Pro:performance dkw$ adb shell am start -W com.sample.performance/.MainActivity
Starting: Intent { act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] cmp=com.sample.performance/.MainActivity }
Status: ok
Activity: com.sample.performance/.MainActivity
ThisTime: 402
TotalTime: 402
WaitTime: 413
Complete

使用以上命令后,我们看到系统打印了三个 time 值,分别是 ThisTime(最后一个Activity启动耗时)、TotalTime(所有 Activity 启动时间)以及 WaitTime (AMS 启动 Activity 的总耗时 (包括系统自己加载的时间))


3.3 手动代码获取

最后一种,也就是手动打印日志计算启动时间,只能记录应该用内耗时,举个例子:

//记录日志帮助类
public class LauncherTimer {

    public static long startTime;

    public static void logStart() {
        startTime = System.currentTimeMillis();
    }

    public static void logEnd(String tag) {
        LogUtil.d("Time", tag+ " launcher time=" + (System.currentTimeMillis() - startTime));
    }
}
//在 application 类中的 attachBaseContext 方法中记录开始时间
 @Override
    protected void attachBaseContext(Context base) {
        LauncherTimer.logStart();
        super.attachBaseContext(base);
    }
// 在 MainActivity 的 onResume 和 onWindowFocusChanged 方法中记录结束时间
 @Override
    protected void onResume() {
        super.onResume();
        LauncherTimer.logEnd("tag1");
    }

    @Override
    public void onWindowFocusChanged(boolean hasFocus) {
        super.onWindowFocusChanged(hasFocus);
        LauncherTimer.logEnd("tag2");
    }

    //首帧绘制
    private void findViews() {
        final View viewById = findViewById(R.id.root);
        viewById.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {

            @Override
            public boolean onPreDraw() {
                viewById.getViewTreeObserver().removeOnPreDrawListener(this);
                LauncherTimer.logEnd("tag3");
                return false;
            }
        });
    }

输出结果:

2021-09-18 09:19:55.208 11182-11214/com.sample.performance D/Time: 2/tag1 launcher time=346
2021-09-18 09:19:55.338 11182-11214/com.sample.performance D/Time: 2/tag3 launcher time=479
2021-09-18 09:19:55.376 11182-11214/com.sample.performance D/Time: 2/tag2 launcher time=517

我们现在了解了如何测量 app 的启动时间,这个是前提,只有知道优化前我们 app 的启动时间,那么我们通过具体的优化方式对 app 启动优化后,两个时间一对比,我们才能知道我们所写的优化代码是否生效。



四、方法耗时统计

前面已经知道我们优化的主要方向,那么在实际开发中,在 application 类的 onCreate 方法中,我们会做一些初始化操作,也包括一些三方库的初始化操作,这时我们就需要知道在 onCreate 方法中的具体每一个方法的耗时。只有知道每一个方法具体的耗时,我们才可以看出来哪些方法耗时多,那么这些方法就是我们优化的主要方向。对于方法的耗时统计,有 3 种方法:traceview 、systrace 以及 aop 。这里主要介绍第一种方式。


4.1 traceview 统计方法耗时

对于第一种 traceview,可以代码统计,也可以采用 android studio 自带的 cpu profiler 统计,具体做法如下:

  @Override
    public void onCreate() {
        super.onCreate();
        //开始记录
        Debug.startMethodTracing("/data/data/com.sample.performance/Launcher.trace");
        //....
        //结束记录
        Debug.stopMethodTracing();
 }

启动程序后,然后如何找到这个 Launcher.trace 文件,在 Android Studio 右边侧边栏中,点击 Device File Expoler ,然后选择运行的是模拟器还是真机,找到 /data/data/com.sample.performance 目录,在这个目录下,就可以看到生成的 Launcher.trace 文件,下载到本地。

接着点击 Android Studio 的底部的 Profiler,选择左上角 + 号按钮,load from file,选择之前下载下来的 Launcher.trace 文件。然后我们就可以用 Profiler 去分析每个方法的耗时了



五、优化方案

上面一系列的各种前期准备,我们终于来到了优化的具体方案,当然还是要结合自己的项目进行选取,这里提供的都是各种方法,有些方法可能会不适用于你的 app。


5.1 伪优化

当系统加载并启动 App 时,需要耗费相应的时间,即使时间不到 1s,用户也会感觉到当点击 App 图标时会有 “延迟” 现象,为了解决这一问题,Goole的做法是在 App 创建的过程中,先展示一个空白页面,让用户体会到点击图标之后立马就有响应,而这个空白页面的颜色则是根据我们在 Manifest 文件中配置的主题背景颜色来决定的,现在一般默认是白色的。

上面的现象就是常说的 “黑白屏” 问题,当然现在只有白屏问题了。这里给出一个较为完美的方案 Android 黑白屏由来以及解决方案


5.2 异步优化

这种方式采用子线程来进行初始化,并行执行,减少执行时间。简单举例如下:

public class ImoocVoiceApplication extends Application {

    private static ImoocVoiceApplication mApplication = null;

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = Math.max(2,Math.min(CPU_COUNT-1,4));

    @Override
    public void onCreate() {
        super.onCreate();
        mApplication = this;
        LaunchTimeUtil.startTime();
        
        // 开启线程池
        ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);

        service.execute(new Runnable() {
            @Override
            public void run() {
                //视频SDK初始化
                VideoHelper.init(ImoocVoiceApplication.this);
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                //音频SDK初始化
                AudioHelper.init(ImoocVoiceApplication.this);
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                //分享SDK初始化
                ShareManager.initSDK(ImoocVoiceApplication.this);
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                //更新组件下载
                UpdateHelper.init(ImoocVoiceApplication.this);
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                //ARouter初始化
                ARouter.init(ImoocVoiceApplication.this);
            }
        });
    }

    public static ImoocVoiceApplication getInstance() {
        return mApplication;
    }
}

这边创建线程池,核心数的写法是参照 API 28 的 AsyncTask 的写法,这样不会造成线程数的浪费。上面的程序运行起来,启动速度会大大增加,但是也不会所有的初始化操作都放在异步线程里。

当然在实际开发中,也有类似这样的场景,比如我们在 application 的 onCreate 方法中,有些方法的初始化操作要先完成,可能在首页需要用到,那么可能就需要用下面的方案:

public class ImoocVoiceApplication extends Application {

    private static ImoocVoiceApplication mApplication = null;

    private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
    private static final int CORE_POOL_SIZE = Math.max(2,Math.min(CPU_COUNT-1,4));
    private CountDownLatch mCountDownLatch = new CountDownLatch(1);;

    @Override
    public void onCreate() {
        super.onCreate();
        mApplication = this;
        LaunchTimeUtil.startTime();

        ExecutorService service = Executors.newFixedThreadPool(CORE_POOL_SIZE);


        service.execute(new Runnable() {
            @Override
            public void run() {
                //视频SDK初始化
                VideoHelper.init(ImoocVoiceApplication.this);
                mCountDownLatch.countDown();
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                //音频SDK初始化
                AudioHelper.init(ImoocVoiceApplication.this);
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                //分享SDK初始化
                ShareManager.initSDK(ImoocVoiceApplication.this);
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                //更新组件下载
                UpdateHelper.init(ImoocVoiceApplication.this);
            }
        });

        service.execute(new Runnable() {
            @Override
            public void run() {
                //ARouter初始化
                ARouter.init(ImoocVoiceApplication.this);
            }
        });

        try {
            mCountDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static ImoocVoiceApplication getInstance() {
        return mApplication;
    }
}

我们以音频组件为例,这边加入的 CountDownLatch 就相当于加了一把锁,意思是音频组件如果在初始化,然后主线程已经执行到下面代码了,在 await 那边会停住,等音频组件初始化结束,执行 mCountDownLatch.countDown(),标记过一次,这边才会放行。这样如果主页面要用到音频组件的东西,不会有异常情况了。


5.3 延迟初始化

比较常规的方案就是可以把优先级不高的初始化代码写在首页数据加载完成后。这时一种做法。

更好一点的做法是初始化立即需要的对象,不要创建全局静态对象,而是移动到单例模式,其中应用仅在第一次访问对象时初始化他们。

最佳的做法是空闲时初始化:监听应用空闲时间,在空闲时间进行初始化。对延迟任务进行分批初始化,利用 IdleHandler 特性,空闲执行。

public class DelayInitDispatcher {

    private Queue<Task> mDelayTasks = new LinkedList<>();

    private MessageQueue.IdleHandler mIdleHandler = new MessageQueue.IdleHandler() {
        @Override
        public boolean queueIdle() {
            if(mDelayTasks.size()>0){
                Task task = mDelayTasks.poll();
                new DispatchRunnable(task).run();
            }
            return !mDelayTasks.isEmpty();
        }
    };

    public DelayInitDispatcher addTask(Task task){
        mDelayTasks.add(task);
        return this;
    }

    public void start(){
        Looper.myQueue().addIdleHandler(mIdleHandler);
    }

}

在这个类中,我们写了一个队列用于存放 task 任务,通过系统自带的 IdleHandler 去处理优先级不高的任务,每次只执行一个,且是在空闲期执行。

场景举例: 比如说 Application 有一个任务在首页加载时不需要用到,那么就可以在首页数据展示的适配器中通过接口回调,在主页面回调方法中加载任务,系统在空闲期就会处理这些任务。

    // 回调方法
    @Override
    public void onFeedShow() {
        DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
        delayInitDispatcher.addTask(new DelayInitTaskA())
                .addTask(new DelayInitTaskB())
                .start();
    }

上面说了一些我在实际开发中所用到的一些优化方案,当然远远不止这些,我们优化的总体方向就是:异步、延迟、懒加载,同时要与实际业务相结合。



写在文末

纸上得来终觉浅,绝知此事要躬行。 《冬夜读书示子聿》-- 陆游

关于 App启动优化 的方案就说到这了,大家可以根据文章中给出的方案,结合自己的项目去优化。


码字不易,如果本篇文章对您哪怕有一点点帮助,请不要吝啬您的点赞,我将持续带来更多优质文章。

你可能感兴趣的:(性能优化,java,android,android,studio)