Android性能优化之启动优化(实战篇)

1,900 阅读6分钟

一、启动优化的意义

用户新安装一个app,第一印象很重要,那首先给用户感受的就是启动时间,如果用户等待时间过长,就体验很差,如果我们能在满足功能的基础上,把启动时间提升一个等级,无疑给我们的产品赢得更多的用户量打下来基础。

二、启动时间检测

//添加监听
mView.getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
          @Override
          public boolean onPreDraw() {//UI开始展示的回调
          //移除监听
             mView.getViewTreeObserver().removeOnPreDrawListener(this);
              LogHelper.i("FeedShow");
              LaunchTimer.endRecord();
              return true;
          }
      }); 

三、启动优化工具---traceview

通过优化工具可以帮助我们快速定位造成app卡顿的原因,优化工具可以选择traceview,使用方式可以参考: blog.csdn.net/m0_64319298… 另外也可以使用另外一个工具android studio自带的cpu profile,这两个工具使用比较相似,但也有一些区别,traceview运行时开销严重,整体都会变慢,可能会带偏优化方向,但是可以埋点 cpu profile可以检测运行时性能,对于检测启动性能则不太适合,各有优缺点分场景使用。

四、优化方案

这里核心思想是讲初始化任务分为三个优先级逐步执行,优先级最高的是必须在进入app之前就要初始化完成,可以通过异步初始化去实现,具体实现方式如下:

异步初始化

/**
 * 异步的Task
 */
public class InitStethoTask extends Task {

    @Override
    public void run() {
        Handler handler = new Handler(Looper.getMainLooper());
        Stetho.initializeWithDefaults(mContext);
    }
}

TaskDispatcher dispatcher = TaskDispatcher.createInstance*();
dispatcher.addTask(new InitStethoTask())
        .start();

通过集成Task然后在run方法里面执行任务就可以了。

如果两个任务之间有依赖关系,异步任务如何保证有序执行?

要保证任务有序执行,需要先对任务进行有向无环图的拓扑排序

有向无环图和拓扑排序

有向五环图:指的是一个无回路的有向图

拓扑排序:将所有活动可排列成一个线性序列,使得每一个活动的所有前驱活动都排列在该活动的前面,这个过程称为拓扑排序

排序代码如下:


/**
* 任务的有向无环图的拓扑排序
* @param originTasks 所有的任务集合
* @param clsLaunchTasks  所有的任务对应的类集合
* @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;
}

CountDownLatch

拿到有序的任务列表以后,如何保证任务按这个列表顺序执行,这里就要用到CountDownLatch进行线程任务阻塞。保证被依赖的任务执行完了,再进行其他任务处理。

/**
 * 需要在getDeviceId之后执行
 */
public class InitUmengTask extends Task {

    @Override
    public List<Class<? extends Task>> dependsOn() {
        //在这里添加需要依赖的任务集合
        List<Class<? extends Task>> task = new ArrayList<>();
        task.add(GetDeviceIdTask.class);
        return task;
    }

    @Override
    public void run() {
        UMConfigure.init(mContext, "58edcfeb310c93091c000be2", "umeng",
                UMConfigure.DEVICE_TYPE_PHONE, "1fe6a20054bcef865eeb0991ee84525b");
    }
}

根据CountDownLatch机制可以实现每个任务执行的时候都会检查是否有阻塞的任务没有执行完,如果没有执行完,继续等待。

@Override
public void run() {
    TraceCompat.beginSection(mTask.getClass().getSimpleName());
    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");
    }
    TraceCompat.endSection();
}

创建线程池的一些知识点

创建这么多异步任务,需要使用线程池进行统一管理,使用线程池需要一些配置,比如核心线程数,最大线程数,那么这个数量这个多少更加合理,根据AsyncTask内部源码的线程池配置,核心线程数是根据CPU的内核数计算而来,这种方式更为合理,不会造成CPU资源浪费或者资源紧张。而最大线程数是MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1这种是推荐写法,也可以跟核心线程保持一样数量。下面是本项目的配置:


/**
 * CPU 密集型任务的线程池
 */
private static ThreadPoolExecutor sCPUThreadPoolExecutor;

/**
 * CPU 核数
 */
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();

/**
 * 线程池线程数
 */
public static final int CORE_POOL_SIZE = Math.max(2, Math.min(CPU_COUNT - 1, 5));

/**
 * 线程池线程数的最大值
 */
private static final int MAXIMUM_POOL_SIZE = CORE_POOL_SIZE;

static {
    sCPUThreadPoolExecutor = new ThreadPoolExecutor(
            CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE_SECONDS, TimeUnit.SECONDS,
            sPoolWorkQueue, sThreadFactory, sHandler);
    sCPUThreadPoolExecutor.allowCoreThreadTimeOut(true);
 
}

延迟初始化

如果任务优先级没那么高,可以通过延迟初始化去执行,为了避免在用户操作app的时候造成卡顿,需要在app没有其他任务执行的时候再去执行这些初始化任务。

IdleHander

IdleHandler是 Android 消息机制中的一个重要特性,它允许开发者在消息队列空闲时执行特定任务。 其工作原理主要包括:通过 addIdleHandler() 方法将 IdleHandler 添加到 MessageQueue 的 mIdleHandlers 列表中;Looper 在从 MessageQueue 中取出消息时,MessageQueue 的 next() 方法会判断队列是否空闲;当队列空闲时,会依次调用 mIdleHandlers 列表中的所有 IdleHandler 的 queueIdle() 方法,并根据返回值决定是否保留该 IdleHandler,具体使用方式如下:


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

/**
 * 使用IdleHandler 可以实现在app在空闲时执行任务
 */
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);
}

以下是模拟如何添加延迟初始化任务的示例代码

public class DelayInitTaskA extends MainTask {

    @Override
    public void run() {
        // 模拟一些操作
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        Log.i("DelayInitTaskA finished", "");
    }
}

DelayInitDispatcher delayInitDispatcher = new DelayInitDispatcher();
delayInitDispatcher.addTask(new DelayInitTaskA())
        .start();

懒加载

对于暂时不回用到的库,可以先不初始化,等到要用的时候再去初始化,实现懒加载的目的。

五、源码地址

感兴趣的小伙伴可以下载源码研究一下任务调度器的设计思路,地址如下: github.com/VincentStor…

六、总结

我们一般项目常规的初始化都是通过同步线程逐个执行,或者个别耗时的开启一个子线程去执行,先不说效率会比较拖沓,在application的代码整洁度上面也不够美观,特别是任务比较多的时候,一个类会显得比较臃肿,引入这个库就可以解决这些问题,让任务变得清晰并且执行效率高。