Android启动优化之多线程依赖线程池

背景

开发中会存在多个任务之间互相依赖,运行非常慢的情况,譬如Android在主线程中初始化多个SDK导致App启动慢的情况,搜索一下发现业界的通用做法是构造任务的有向无环图,拓扑排序生成有序的任务列表,然后用线程池执行任务列表(通俗的说就是先找到没有依赖的任务执行,执行完了以后再找到剩下的没有依赖的任务执行,如此反复直到执行完所有任务),但是这个做法无法解决有的任务需要点击对话框授权的情况,基于这个情况打算再造一个轮子出来。

问题

造轮子之前先梳理了一下对这个轮子的要求,发现除了有向无环图外还是有很多细节要解决的。

-依赖任务多线程启动
-支持交互性任务,先拦截任务,交互完成以后再继续执行
-可视化有向无环图
-可视化任务执行情况
-支持多线程、主线程、主进程、第一个任务、最后一个任务等配置属性

方案

开源

[图片上传失败...(image-6decf4-1663242784585)]

TaskGraph:https://github.com/JonaNorman/TaskGraph

线程池只能执行没有依赖关系的任务,TaskGraph开源库用有向无环图实现多线程依赖线程池,用拦截器实现交互式任务

[图片上传失败...(image-3e3682-1663242784586)]

图中添加了A任务,B任务依赖A任务执行完再执行,其中A任务需要点击对话框才能执行。

TaskGraph taskGraph = new TaskGraph();
taskGraph.addTask(new Task("A",new Runnable() {//添加A任务
   @Override
   public void run() {
   }
}).addTaskInterceptor(new Task.TaskInterceptor() {
   @Override
   public void onIntercept(Task.TaskInterceptorChain interceptorChain) {//拦截A任务,在A任务之前可以插入对话框
       AlertDialog.Builder builder = new AlertDialog.Builder(TaskGraphModule.getTopActivity());
       builder.setPositiveButton("ok", new DialogInterface.OnClickListener() {
           @Override
           public void onClick(DialogInterface dialog, int which) {
               interceptorChain.proceed();//继续
           }
       });
       builder.setNegativeButton("cancel", new DialogInterface.OnClickListener() {
           @Override
           public void onClick(DialogInterface dialog, int which) {
               interceptorChain.cancel();//取消
           }
       });
       builder.show();
   }
}));
taskGraph.addTask(new Task("B",new Runnable() {
   @Override
   public void run() {//添加B任务,B任务依赖A任务先完成
   }
}).dependsOn("A").setMainThread(true));
taskGraph.execute();

可视化有向图

搜索TASK_GRAPH_LOG: graphviz:会输出有向图日志,复制到 graphviz-visual-editor 可视化查看

[图片上传失败...(image-42ce-1663242784586)]

可视化任务执行情况

python systrace.py -o trace.html  -a packagename sched

packagename要替换成运行的app的包名 chrome浏览器打开chrome://tracing/,load 按钮加载trace.html

[图片上传失败...(image-3bc2ae-1663242784586)]

原理

依赖任务多线程启动

[图片上传失败...(image-4f99dc-1663242784586)]

正常的线程池只能执行没有依赖关系的任务,怎么才能让线程池支持运行相互依赖的任务呢?
先找到所有没有进来箭头的节点执行,在该图中也就是A,执行完后删除这个节点和边,
变成了下图

[图片上传失败...(image-4fe818-1663242784586)]
继续以上步骤,找到B运行后删除B,变成下图这样

[图片上传失败...(image-c4fb5b-1663242784586)]
继续以上步骤,找到C D E同时运行,最终所有任务执行完毕。

把上面的步骤翻译成术语

  • 有箭头的图叫有向图
  • 节点有多少个进来的箭头叫入度
  • 没有进来箭头的节点叫入度为0的节点
  • 箭头没有形成环的图叫有向无环图
  • 依次找到所有入度为0的节点叫拓扑排序

这里有个问题,多线程怎么执行拓扑排序的节点,有两种做法

  1. 拓扑排序的节点列表作为runnable提交到线程池,依赖的任务线程等待其他任务完成在执行
  2. 先把入度为0的所有节点提交到线程池,有一个执行完,就触发寻找剩下入度为0的节点继续执行
    两种方案我选了方案2,个人感觉方案2找到的节点执行顺序是最优的,并且不需要线程等待,代码简单而且不需要空占有线程池的线程数量

主要思想:

Grpah图有多个node节点,每个Node节点有一个Vertex顶点,多个入边edge,多个出边edge,
拓扑排序就是找所有node节点入度为0的边移除然后继续找直到找完所有节点,核心代码地址

支持交互性任务

有些任务需要交互输入,完成以后再继续执行,为了实现该功能,可以用拦截器的方式来实现。

[图片上传失败...(image-2b973-1663242784586)]

拦截器的原理就是调用到拦截器时候会用锁等待,如果执行了proceed方法会唤醒锁然后执行下个拦截器,如果执行了cancel会唤醒锁终止所有任务标记cancel状态,每个拦截器必须调用其中一个方法,要不然会一直等待
核心代码如下:代码地址

private void nextIntercept() {
    synchronized (sync) {
        currentInterceptor = taskInterceptorQueue.poll();//获取下一个拦截器
        if (currentInterceptor == null) {
            return;
        }
        currentInterceptor.onIntercept(this);//处罚拦截器
    }
    while (!graphController.isFinished()) {
        synchronized (sync) {
            if (cancel) {//调用cancel方法会把cancel赋值为true
                throw new TaskCancelException();
            } else if (currentInterceptor == proceedInterceptor) {//如果调用了proceed会proceedInterceptor赋值为currentInterceptor
                nextIntercept();//执行下一个拦截器
                break;
            } else {
                try {
                    sync.wait();//等待执行proceed或者cancel方法
                } catch (InterruptedException e) {
                }
            }
        }
    }
}

可视化有向无环图

多个依赖任务添加进去以后如果不能可视化成图就会对影响对任务的把控程度,graphviz是一个图的可视化项目,只要把图的情况写成文本输入就会生成对应图。

  digraph pic {
    A->B;
    B->C;
    }

[图片上传失败...(image-67b9a1-1663242784586)]

可视化任务执行情况

多个任务执行实时运行情况,有助于我们优化任务依赖,主要就是在每个任务执行开始调用Trace.beginSection(name),执行完调用Trace.endSection(),然后用命令

python systrace.py -o trace.html  -a packagename sched

生成trace.html,然后用chrome浏览器打开chrome://tracing/点击load按钮加载trace.html就可以查看每个任务的执行情况

支持多线程、主线程、主进程、第一个任务、最后一个任务等配置属性

任务具有多个属性,多线程、主线程、主进程等属性,该实现只要加对应判断就行,第一个任务和最后一个任务则需要遍历所有任务,添加对应依赖关系。

收获

依赖任务多线程调度本身不是很难,在该开源项目中我收获了很多,包括如何实现有向无环图,如何在多线程中实现任务拦截继发,如何使用graphviz实现可视化图,如何用systemtrace可视化任务执行,希望看完文章的同学也可以从中学到什么,谢谢大家的浏览,如果觉得可以,欢迎大家多多star这个开源项目

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,569评论 4 363
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,499评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,271评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,087评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,474评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,670评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,911评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,636评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,397评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,607评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,093评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,418评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,074评论 3 237
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,092评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,865评论 0 196
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,726评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,627评论 2 270

推荐阅读更多精彩内容