Android 性能优化 ---- 启动优化
Android 性能优化 ---- 启动优化
1、为什么要进行启动优化
一款应用的第一印象很重要,第一印象往往决定了用户的去留。打开一款应用,如果速度很快,很顺畅,那么很容易让人觉得这款应用背后的技术实力很强,用户潜意识中会对这款应用更加的信赖。
其次,网上也流行一种说法,就是8秒定律,意思是说,如果用户在打开一个页面,在8秒的时间内还没有打开,那么用户大概的会放弃掉,意味着一个用户的流失。从这里就可以看出,启动优化的重要性了。
2、启动的分类
2.1 冷启动
先来看看冷启动的流程图

从图中可以看出,APP启动的过程是:ActivityManagerProxy 通过IPC来调用AMS(ActivityManagerService),AMS通过IPC启动一个APP进程,ApplicationThread通过反射来创建Application并且绑定,最后通过ActivityThread来控制activity的生命周期,在相关页面的生命周期中通过ViewRootImpl来对view的实现。从而完成应用的启动。
2.2 热启动
热启动的速度是最快的,它就是进程从后台切换到前台的一个过程。
2.3 温启动
温启动只会重新走一遍页面的生命周期,但是对于进程,application不会重新在创建。
3、优化方向
上面介绍了启动的几种方式可以看出,我们针对启动优化,基本只是优化冷启动就可以了。但是从冷启动的启动流程中很多都是系统做的,我们没有办法操控。我们能做的,就是application的生命周期和activity的生命周期这部分,启动优化往往就是从这两块入手。
4、启动时间的测量方式
4.1 使用adb 命令方式(线下使用方便)
adb shell am start -W 包名/包名+类名

ThisTime:最后一个activity的启动耗时
TotalTime:所有activity的启动耗时
WaitTime:AMS启动activity的总耗时
这里由于我直接进入到主界面,中间并没有SplashActivity,所有ThisTime 和 TotalTime的时间是一样的
优势:在线下使用方便,适合于跑线下的产品,和获取竞品的时间,然后比对
缺点:不能带到线上,获取的时间,只能说是一个大概时间,不是很严谨。
4.2 手动打点方式

通过System.currentTimeMillis()来打时间戳
缺点:很明显,对代码侵入性非常的大,如果说我想要打出每一个任务花费的时间,那么代码看起来就很恶心了
5、优雅获取方法耗时
5.1 AOP Aspect Oriented Programming 面向切面编程
AOP:通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。它的核心思想就是将应用程序中的业务逻辑处理部分同对其提供通用服务部分即“横切关注点”进行分离。
OOP:引入封装,继承,多态等概念来建立一种对象层次结构,它允许开发者定义纵向的关系,但并不适合横向的关系。
可以说AOP是OOP的一种补充和完善。
5.2 aspectj的使用
AspectJ是一个面向切面编程的框架,是对java的扩展且兼容java,AspectJ定义了AOP语法,它有一个专门的编译器来生成遵守java字节编码规范的Class文件。
在项目的根目录的build.gradle添加依赖:
dependencies {
        classpath 'com.android.tools.build:gradle:3.5.2'
        classpath 'com.hujiang.aspectjx:gradle-android-plugin-aspectjx:2.0.0'
        // NOTE: Do not place your application dependencies here; they belong
        // in the individual module build.gradle files
    }
在app下的build.gradle添加依赖
apply plugin: 'android-aspectjx'
在dependencies中添加
implementation 'org.aspectj:aspectjrt:1.9.4'
然后创建一个类
package com.noahedu.myapplication.aspectj;
import android.util.Log;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
/**
 * @Description: //@Before 在切入点之前运行
 * //    @After("")
 *     //@Around 在切入点前后都运行
 * @Author: huangjialin
 * @CreateDate: 2020/7/10 14:07
 */
@Aspect
public class MyApplicationAspectj {
    @Around("call(* com.noahedu.myapplication.MyApplication.**(..))")
    public void getTime(ProceedingJoinPoint joinPoint){
        Signature signature = joinPoint.getSignature();
        String name = signature.getName();
        long time = System.currentTimeMillis();
        try {
            joinPoint.proceed();
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        Log.e("MyApplicationAspectj " ,(name + " cost " + (System.currentTimeMillis() - time)));
    }
}
这样我们运行的时候,就会直接在logcat中打印出application中的onCreate方法中所有调用方法的耗时情况了
2020-07-10 14:22:27.151 1619-1619/? E/MyApplicationAspectj: taskOne cost 150
2020-07-10 14:22:29.203 1619-1619/com.noahedu.myapplication E/MyApplicationAspectj: taskTwo cost 2052
2020-07-10 14:22:29.554 1619-1619/com.noahedu.myapplication E/MyApplicationAspectj: taskThrid cost 351
2020-07-10 14:22:30.556 1619-1619/com.noahedu.myapplication E/MyApplicationAspectj: taskFour cost 1001
这样我们几乎没有碰Application中的任何代码,也就够得出各个方法的耗时,几乎对代码无侵入。
6、启动优化的工具选择
6.1 traceview
TraceView是Android SDK中内置的一个工具,他可以加载trace文件,以图形化的形式展示相应代码的执行时间,次数及调用栈,便于我们分析。
 Debug.startMethodTracing("MyApplication");
      //TODO
 Debug.stopMethodTracing();
运行项目就可以我们的SD卡中找到对应的trace文件了,如果是Android studio可以直接在右下角找到
DeviceFileExporer -->sdcard --> Android -- > data -->files --->自己项目的包名
然后双击即可查看文件了
优点:使用简单,图形形式展示所执行的时间,调用栈等。
缺点:会影响到我们优化的方向,由于是图形化展示,也是比较占用CPU资源的,所以得到的时间往往是比实际的要大。
7、启动器
上面介绍了多了几个获取任务执行时间的方式和工具,那么当我们知道某个方法耗时了,我们该怎么处理呢?
package com.noahedu.myapplication;
import android.app.Application;
import android.os.Debug;
import android.util.Log;
/**
 * @Description: java类作用描述
 * @Author: huangjialin
 * @CreateDate: 2020/7/10 9:59
 */
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        Debug.startMethodTracing("MyApplication");
        taskOne();
        taskTwo();
        taskThrid();
        taskFour();
        Debug.stopMethodTracing();
    }
    public void taskOne(){
        try {
            Thread.sleep(150);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void taskTwo(){
        try {
            Thread.sleep(2050);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void taskThrid(){
        try {
            Thread.sleep(350);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    public void taskFour(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
现在application的onCreate方法中有几个任务,各个耗时是不一样的。可能很多同学就会说了,异步处理啊,开线程,放到线程池中,或者创建一个IntentService来执行。那么我们就要考虑几个问题了
第一:异步处理,如果在某个页面需要用到某个SDK,但是又没有初始化完成呢?
第二:假如说taskTwo需要taskOne的某个返回值呢?怎么保证taskOne在taskTwo之前执行完毕呢?
第三:开线程,开多少个线程呢?多了会造成资源浪费,少了资源又没有合理的利用。
我个人觉得针对于启动优化,在Application中的onCreate()进行初始化任务操作,我们首先需要对这些任务进行一个优先级划分,针对于那些优先级高的任务,我们可以优先进行处理,对于那些优先级较低的,我们可以适当的延迟进行加载。
其次有很多同学喜欢把那些优先级较低的任务进行延迟加载,比如new Handler().postDelayed(),这种我觉得是非常不可取的,假如说放在postDelayed中的任务耗时2s,延迟1s进行处理,那么在执行2s任务的过程中,有用户进行操作,那岂不是很卡吗,很明显,这是指标不治本的。
7.1 启动器的思想
针对上面说的几个痛点,怎么在处理上面的几个痛点,又能保证代码的可维护性呢?换句话说就是一个新人不需要理解整个过程,直接就可以开干呢?那么,启动器来了。
启动器核心思想:充分利用CPU多核,自动梳理任务顺序
7.2 启动器的原理
1、任务全部封装成Task对象,传入到集合中。
2、根据所有的任务依赖关系,形成一个有向无环图,然后通过拓扑排序排列出任务的执行流程
3、通过CountDownLatch来控制某一个任务是否执行完毕才进行下一步。
4、线程池创建核心线程的数量,由手机的核数量决定的。
7.3启动器使用方式

7.4启动器核心代码

进行任务排序
package com.noahedu.launchertool.launchstarter.sort;
import com.noahedu.launchertool.launchstarter.task.Task;
import com.noahedu.launchertool.launchstarter.utils.DispatcherLog;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import androidx.annotation.NonNull;
import androidx.collection.ArraySet;
public class TaskSortUtil {
    private static List<Task> sNewTasksHigh = new ArrayList<>();// 高优先级的Task
    /**
     * 任务的有向无环图的拓扑排序
     *
     * @return
     */
    public static synchronized List<Task> getSortResult(List<Task> originTasks,
                                                        List<Class<? extends Task>> clsLaunchTasks) {
        long makeTime = System.currentTimeMillis();
        Set<Integer> dependSet = new ArraySet<>();
        Graph graph = new Graph(originTasks.size());
        for (int i = 0; i < originTasks.size(); i++) {
            Task task = originTasks.get(i);
            if (task.isSend() || task.dependsOn() == null || task.dependsOn().size() == 0) {
                continue;
            }
            for (Class cls : task.dependsOn()) {
                int indexOfDepend = getIndexOfTask(originTasks, clsLaunchTasks, cls);
                if (indexOfDepend < 0) {
                    throw new IllegalStateException(task.getClass().getSimpleName() +
                            " depends on " + cls.getSimpleName() + " can not be found in task list ");
                }
                dependSet.add(indexOfDepend);
                graph.addEdge(indexOfDepend, i);
            }
        }
        List<Integer> indexList = graph.topologicalSort();
        List<Task> newTasksAll = getResultTasks(originTasks, dependSet, indexList);
        DispatcherLog.i("task analyse cost makeTime " + (System.currentTimeMillis() - makeTime));
        printAllTaskName(newTasksAll);
        return newTasksAll;
    }
    @NonNull
    private static List<Task> getResultTasks(List<Task> originTasks,
                                             Set<Integer> dependSet, List<Integer> indexList) {
        List<Task> newTasksAll = new ArrayList<>(originTasks.size());
        List<Task> newTasksDepended = new ArrayList<>();// 被别人依赖的
        List<Task> newTasksWithOutDepend = new ArrayList<>();// 没有依赖的
        List<Task> newTasksRunAsSoon = new ArrayList<>();// 需要提升自己优先级的,先执行(这个先是相对于没有依赖的先)
        for (int index : indexList) {
            if (dependSet.contains(index)) {
                newTasksDepended.add(originTasks.get(index));
            } else {
                Task task = originTasks.get(index);
                if (task.needRunAsSoon()) {
                    newTasksRunAsSoon.add(task);
                } else {
                    newTasksWithOutDepend.add(task);
                }
            }
        }
        // 顺序:被别人依赖的————》需要提升自己优先级的————》需要被等待的————》没有依赖的
        sNewTasksHigh.addAll(newTasksDepended);
        sNewTasksHigh.addAll(newTasksRunAsSoon);
        newTasksAll.addAll(sNewTasksHigh);
        newTasksAll.addAll(newTasksWithOutDepend);
        return newTasksAll;
    }
    private static void printAllTaskName(List<Task> newTasksAll) {
        if (true) {
            return;
        }
        for (Task task : newTasksAll) {
            DispatcherLog.i(task.getClass().getSimpleName());
        }
    }
    public static List<Task> getTasksHigh() {
        return sNewTasksHigh;
    }
    /**
     * 获取任务在任务列表中的index
     *
     * @param originTasks
     * @return
     */
    private static int getIndexOfTask(List<Task> originTasks,
                                      List<Class<? extends Task>> clsLaunchTasks, Class cls) {
        int index = clsLaunchTasks.indexOf(cls);
        if (index >= 0) {
            return index;
        }
        // 仅仅是保护性代码
        final int size = originTasks.size();
        for (int i = 0; i < size; i++) {
            if (cls.getSimpleName().equals(originTasks.get(i).getClass().getSimpleName())) {
                return i;
            }
        }
        return index;
    }
}
执行任务代码
package com.noahedu.launchertool.launchstarter.task;
import android.os.Looper;
import android.os.Process;
import com.noahedu.launchertool.launchstarter.TaskDispatcher;
import com.noahedu.launchertool.launchstarter.stat.TaskStat;
import com.noahedu.launchertool.launchstarter.utils.DispatcherLog;
/**
 * 任务真正执行的地方
 */
public class DispatchRunnable implements Runnable {
    private Task mTask;
    private TaskDispatcher mTaskDispatcher;
    public DispatchRunnable(Task task) {
        this.mTask = task;
    }
    public DispatchRunnable(Task task,TaskDispatcher dispatcher) {
        this.mTask = task;
        this.mTaskDispatcher = dispatcher;
    }
    @Override
    public void run() {
        DispatcherLog.i(mTask.getClass().getSimpleName()
                + " begin run" + "  Situation  " + TaskStat.getCurrentSituation());
        Process.setThreadPriority(mTask.priority());
        long startTime = System.currentTimeMillis();
        mTask.setWaiting(true);
        mTask.waitToSatisfy();
        long waitTime = System.currentTimeMillis() - startTime;
        startTime = System.currentTimeMillis();
        // 执行Task
        mTask.setRunning(true);
        mTask.run();
        // 执行Task的尾部任务
        Runnable tailRunnable = mTask.getTailRunnable();
        if (tailRunnable != null) {
            tailRunnable.run();
        }
        if (!mTask.needCall() || !mTask.runOnMainThread()) {
            printTaskLog(startTime, waitTime);
            TaskStat.markTaskDone();
            mTask.setFinished(true);
            if(mTaskDispatcher != null){
                mTaskDispatcher.satisfyChildren(mTask);
                mTaskDispatcher.markTaskDone(mTask);
            }
            DispatcherLog.i(mTask.getClass().getSimpleName() + " finish");
        }
    }
    /**
     * 打印出来Task执行的日志
     *
     * @param startTime
     * @param waitTime
     */
    private void printTaskLog(long startTime, long waitTime) {
        long runTime = System.currentTimeMillis() - startTime;
        if (DispatcherLog.isDebug()) {
            DispatcherLog.i(mTask.getClass().getSimpleName() + "  wait " + waitTime + "    run "
                    + runTime + "   isMain " + (Looper.getMainLooper() == Looper.myLooper())
                    + "  needWait " + (mTask.needWait() || (Looper.getMainLooper() == Looper.myLooper()))
                    + "  ThreadId " + Thread.currentThread().getId()
                    + "  ThreadName " + Thread.currentThread().getName()
                    + "  Situation  " + TaskStat.getCurrentSituation()
            );
        }
    }
}
基本核心代码就是上面这几个,完整的代码会在后面的demo给出
8、其他优化方案
8.1 对延迟任务进行分批初始化,使用IdleHandler特性,进行空闲执行 (适合优先级不是很高,不急于初始化的第三方SDK)
IdleHandler:IdleHandler 可以用来提升性能,主要用在我们希望能够在当前线程消息队列空闲时做些事情(譬如 UI 线程在显示完成后,如果线程空闲我们就可以提前准备其他内容)的情况下,不过最好不要做耗时操作。简单来说就是,looper对象有空的时候就会执行IdleHandler中的任务。
前面说过,在application的任务进行优先级划分,那么如果优先级低的任务,我们是不是可以不再application的onCreate中进行初始化呢?是否可以放到activity中进行呢?如果可以在activity中,那么在activity那个阶段适合呢?其实在onCreate(),onResume()都是可以的。当然我们也可以使用view中的getViewTreeObserver().addOnPreDrawListener进行监听,这个事件是视图将要绘制的时候会回调该方法。我们可以在回调的方法中将要初始化的SDK放大IdleHandler中。
8.2 提前加载SP 可以放到multidex之前加载,利用此阶段的CPU
SharedPreferences,以键值对的形式进行数据的保存的,会一次性加载到内存中,所以我们可以考虑那个阶段的CPU相对来说比较空闲。如可以放到multidex之前加载,充分利用此阶段的CPU进行
demo地址:[https://github.com/343661629/startOptimization]
Android 性能优化 ---- 启动优化的更多相关文章
- 关于android性能,内存优化
		
转:http://www.starming.com/index.php?action=plugin&v=wave&tpl=union&ac=viewgrouppost& ...
 - android 性能分析、优化
		
.主要介绍了一些分析工具,比如GT.ITest等http://www.jianshu.com/p/8b77d394b2a6 .详细介绍啦android平台常见性能优化工具http://blog.csd ...
 - Android性能之启动时间篇
		
背景介绍 Android用户也许会经常碰到以下的问题: 1)应用后台开着,手机很快没电了--应用耗电大: 2)首次/非首次启动应用,进入应用特别慢--应用启动慢: 3)应用使用过程中,越来越卡--CP ...
 - Android内核开发:系统启动速度优化-Android OS启动优化(转)
		
Android系统的启动优化主要分为三大部分: (1) Bootloader优化 (2) Linux Kernel的剪裁与优化 (3) Android OS部分的剪裁与优化 本文重点关注Android ...
 - 关于android应用--内存的优化
		
以下内容为转载自网上,然后自己加工贴合到一块的: 原文地址:http://www.cnblogs.com/frydsh/archive/2012/12/09/2810601.html http://w ...
 - Android性能优化(一)之启动加速35%
		
一.前言 随着项目版本的迭代,App的性能问题会逐渐暴露出来,而好的用户体验与性能表现紧密相关,从本篇文章开始,我将开启一个Android应用性能优化的专题,从理论到实战,从入门到深挖,手把手将性能优 ...
 - Android性能优化-App启动优化
		
原文地址:https://developer.android.com/topic/performance/launch-time.html#common 通常用户期望app响应和加载速度越快越好.一个 ...
 - Android性能优化系列之App启动优化
		
Android性能优化系列之布局优化 Android性能优化系列之内存优化 Android性能优化系列之apk瘦身 应用的启动速度缓慢是我们在开发过程中常常会遇到的问题,比方启动缓慢导致的黑屏.白屏问 ...
 - Android性能优化之App应用启动分析与优化
		
前言: 昨晚新版本终于发布了,但是还是记得有测试反馈app启动好长时间也没进入app主页,所以今天准备加个班总结一下App启动那些事! app的启动方式: 1.)冷启动 当启动应用时,后台没 ...
 
随机推荐
- Tensorflow报错:OMP: Error #15: Initializing libiomp5.dylib, but found libiomp5.dylib already initialized.
			
参考: https://github.com/dmlc/xgboost/issues/1715 解决方法: I solved it with 'conda install nomkl'.
 - 【JMeter_15】JMeter逻辑控制器__仅一次控制器<Once Only Controller>
			
仅一次控制器<Once Only Controller> 业务逻辑: 在每个线程内,该控制器下的内容只会被执行一遍,无论循环多少次,都只执行一遍.<嵌套在循环控制器之内时是个例外,每 ...
 - Jmeter 测试接口
			
创建线程组 添加HTTP请求 查看结果树
 - JavaWeb网上图书商城完整项目--day02-7.提交注册表单功能之流程分析
			
1.点击注册之后将提交的信息传递到UserServlet的public String regist方法进行处理,然后将东西通过service进行处理 业务流程:
 - JSP新闻显示
			
MYSQL数据库创建新闻表,用户登陆时使用SERVLET获取用户名,效验通过后直接跳转新闻列表页面,JSP使用EL显示新闻列表 1.首先创建数据库及用户.新闻表 CREATE DATABASE /*! ...
 - 01[了解] Dubbo
			
什么是Dubbo? 概述 Dubbo是阿里巴巴内部使用的分布式业务框架,2012年由阿里巴巴开源. 由于Dubbo在阿里内部经过广泛的业务验证,在很短时间内,Dubbo就被许多互联网公司所采用,并产生 ...
 - Apache POI 操作Excel(1)--POI简介
			
Apache POI(http://poi.apache.org/)是一个用于读取和编写Microsoft Office文件格式开源的Java项目,现在已经可以操作Excel,PowerPoint,W ...
 - Zookeeper-Access Control List(ACL)
			
概述 Z K作为一个分布式协调框架.内部存储着一些分布式系统运行时状态的元数据.如何有效的保护这些数据的安全.如何做一个比较好的权限控制显得非常的重要. ZK 为我们提供一套完善的 ACL(acces ...
 - 初探numpy——广播和数组操作函数
			
numpy广播(Broadcast) 若数组a,b形状相同,即a.shape==b.shape,那么a+b,a*b的结果就是对应数位的运算 import numpy as np a=np.array( ...
 - nginx location 知识知多少
			
写在之前 众所周知 nginx location 路由转发规则多种多样,尤其是 [ = | ~ | ~* | ^~ ] 这些前缀是什么意思.root 与 alias 是否可以区分开,nginx 作为反 ...