开发者博客 – IT技术 尽在开发者博客

开发者博客 – 科技是第一生产力


  • 首页

  • 归档

  • 搜索

不引入ES,如何利用MySQL实现模糊匹配 1 业务场景概

发表于 2024-02-29
  1. 业务场景概述

目标是实现一个公司的申请审批流程,整个业务流程涉及到两种角色,分别为商务角色与管理员角色。整个流程如下图所示:

流程图
核心流程总结为一句话:商务角色申请添加公司后由管理员进行审批。

商务在添加公司时,可能为了方便,直接填写公司简称,而公司全称可能之前已经被添加过了,为了防止添加重复的公司,所以管理员在针对公司信息审批之前,需要查看以往添加的公司信息里有无同一个公司。

  1. 实现思路

以上是一个业务场景的大概介绍。从技术层面需要考虑实现的功能点:

  • 分词
  • 与库里已有数据进行匹配
  • 按照匹配度对结果进行排序

分词功能有现成的分词器,所以整个需求的核心重点在于如何与数据库中的数据匹配并按照匹配度排序。

  1. 模糊匹配技术选型

  • 方案一:引入ES
  • 方案二:利用MySQL实现

本系统规模较小,单纯为了实现这个功能引入ES成本较大,还要涉及到数据同步等问题,系统复杂性会提高,所以尽量使用MySQL已有的功能进行实现。

MySQL提供了以下三种模糊搜索的方式:

  • like匹配:要求模式串与整个目标字段完全匹配;
  • RegExp正则匹配:要求目标字段包含模式串即可;
  • Fulltext全文索引:在字段类型为CHAR,VARCHAR,TEXT的列上创建全文索引,执行SQL进行查询。

针对于上述业务场景,对相关技术进行优劣分析:

  • like匹配,无法满足需求,所以pass;
  • 全文索引:可定制性差,不支持任意匹配查询,pass;
  • 正则匹配:可实现任意模式匹配,缺点在于执行效率不如全文索引。

针对于这个场景,记录数目相对来说没有那么多,所以对于效率稍低的结果可以接受,因此技术选型方面采用RegExp正则匹配来实现模糊匹配的需求。

  1. 实现效果展示

image.png

  1. 核心代码

整个逻辑基于 提取公司名称关键信息 –>分词 –> 匹配 三个核心步骤。

5.1 提取公司关键信息

对输入的公司名称去除无用信息,保留关键信息。这里的无用信息指的是地名,圆括号,以及集团,股份,有限等。

  • 匹配前处理公司名称
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
ini复制代码    /**
* 匹配前去除公司名称的无意义信息
* @param targetCompanyName
* @return
*/
private String formatCompanyName(String targetCompanyName){

String regex = "(?<province>[^省]+自治区|.*?省|.*?行政区|.*?市)" +
"?(?<city>[^市]+自治州|.*?地区|.*?行政单位|.+盟|市辖区|.*?市|.*?县)" +
"?(?<county>[^(区|市|县|旗|岛)]+区|.*?市|.*?县|.*?旗|.*?岛)" +
"?(?<village>.*)";
Matcher matcher = Pattern.compile(regex).matcher(targetCompanyName);
while(matcher.find()){
String province = matcher.group("province");
log.info("province:{}",province);
if (StringUtils.isNotBlank(province) && targetCompanyName.contains(province)){
targetCompanyName = targetCompanyName.replace(province,"");
}
log.info("处理完省份的公司名称:{}",targetCompanyName);
String city = matcher.group("city");
log.info("city:{}",city);
if (StringUtils.isNotBlank(city) && targetCompanyName.contains(city)){
targetCompanyName = targetCompanyName.replace(city,"");
}
log.info("处理完城市的公司名称:{}",targetCompanyName);
String county = matcher.group("county");
log.info("county:{}",county);
if (StringUtils.isNotBlank(county) && targetCompanyName.contains(county)){
targetCompanyName = targetCompanyName.replace(county,"");
}
log.info("处理完区县级的公司名称:{}",targetCompanyName);
}
String[][] address = AddressUtil.ADDRESS;
for (String [] city: address) {
for (String b : city ) {
if (targetCompanyName.contains(b)){
targetCompanyName = targetCompanyName.replace(b, "");
}
}
}
log.info("处理后的公司名称:{}",targetCompanyName);
return targetCompanyName;
}
  • 地名工具类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
arduino复制代码public class AddressUtil {
public static final String[][] ADDRESS = {
{"北京"},
{"天津"},
{"安徽","安庆","蚌埠","亳州","巢湖","池州","滁州","阜阳","合肥","淮北","淮南","黄山","六安","马鞍山","宿州","铜陵","芜湖","宣城"},
{"澳门"},
{"香港"},
{"福建","福州","龙岩","南平","宁德","莆田","泉州","厦门","漳州"},
{"甘肃","白银","定西","甘南藏族自治州","嘉峪关","金昌","酒泉","兰州","临夏回族自治州","陇南","平凉","庆阳","天水","武威","张掖"},
{"广东","潮州","东莞","佛山","广州","河源","惠州","江门","揭阳","茂名","梅州","清远","汕头","汕尾","韶关","深圳","阳江","云浮","湛江","肇庆","中山","珠海"},
{"广西","百色","北海","崇左","防城港","贵港","桂林","河池","贺州","来宾","柳州","南宁","钦州","梧州","玉林"},
{"贵州","安顺","毕节地区","贵阳","六盘水","黔东南苗族侗族自治州","黔南布依族苗族自治州","黔西南布依族苗族自治州","铜仁地区","遵义"},
{"海南","海口","三亚","直辖县级行政区划"},
{"河北","保定","沧州","承德","邯郸","衡水","廊坊","秦皇岛","石家庄","唐山","邢台","张家口"},
{"河南","安阳","鹤壁","焦作","开封","洛阳","漯河","南阳","平顶山","濮阳","三门峡","商丘","新乡","信阳","许昌","郑州","周口","驻马店"},
{"黑龙江","大庆","大兴安岭地区","哈尔滨","鹤岗","黑河","鸡西","佳木斯","牡丹江","七台河","齐齐哈尔","双鸭山","绥化","伊春"},
{"湖北","鄂州","恩施土家族苗族自治州","黄冈","黄石","荆门","荆州","十堰","随州","武汉","咸宁","襄樊","孝感","宜昌"},
{"湖南","长沙","常德","郴州","衡阳","怀化","娄底","邵阳","湘潭","湘西土家族苗族自治州","益阳","永州","岳阳","张家界","株洲"},
{"吉林","白城","白山","长春","吉林","辽源","四平","松原","通化","延边朝鲜族自治州"},
{"江苏","常州","淮安","连云港","南京","南通","苏州","宿迁","泰州","无锡","徐州","盐城","扬州","镇江"},
{"江西","抚州","赣州","吉安","景德镇","九江","南昌","萍乡","上饶","新余","宜春","鹰潭"},
{"辽宁","鞍山","本溪","朝阳","大连","丹东","抚顺","阜新","葫芦岛","锦州","辽阳","盘锦","沈阳","铁岭","营口"},
{"内蒙古","阿拉善盟","巴彦淖尔","包头","赤峰","鄂尔多斯","呼和浩特","呼伦贝尔","通辽","乌海","乌兰察布","锡林郭勒盟","兴安盟"},
{"宁夏回族","固原","石嘴山","吴忠","银川","中卫"},
{"青海","果洛藏族自治州","海北藏族自治州","海东地区","海南藏族自治州","海西蒙古族藏族自治州","黄南藏族自治州","西宁","玉树藏族自治州"},
{"山东","滨州","德州","东营","菏泽","济南","济宁","莱芜","聊城","临沂","青岛","日照","泰安","威海","潍坊","烟台","枣庄","淄博"},
{"山西","长治","大同","晋城","晋中","临汾","吕梁","朔州","太原","忻州","阳泉","运城"},
{"陕西","安康","宝鸡","汉中","商洛","铜川","渭南","西安","咸阳","延安","榆林"},
{"上海"},
{"四川","阿坝藏族羌族自治州","巴中","成都","达州","德阳","甘孜藏族自治州","广安","广元","乐山","凉山彝族自治州","泸州","眉山","绵阳","内江","南充","攀枝花","遂宁","雅安","宜宾","资阳","自贡"},
{"西藏","阿里地区","昌都地区","拉萨","林芝地区","那曲地区","日喀则地区","山南地区"},
{"新疆维吾尔","阿克苏地区","阿勒泰地区","巴音郭楞蒙古自治州","博尔塔拉蒙古自治州","昌吉回族自治州","哈密地区","和田地区","喀什地区","克拉玛依","克孜勒苏柯尔克孜自治州","塔城地区","吐鲁番地区","乌鲁木齐","伊犁哈萨克自治州","直辖县级行政区划"},
{"云南","保山","楚雄彝族自治州","大理白族自治州","德宏傣族景颇族自治州","迪庆藏族自治州","红河哈尼族彝族自治州","昆明","丽江","临沧","怒江僳僳族自治州","普洱","曲靖","文山壮族苗族自治州","西双版纳傣族自治州","玉溪","昭通"},
{"浙江","杭州","湖州","嘉兴","金华","丽水","宁波","衢州","绍兴","台州","温州","舟山"},
{"重庆"},
{"台湾","台北","高雄","基隆","台中","台南","新竹","嘉义"},
};
}

5.2 分词相关代码

  • pom文件:引入IK分词器相关依赖
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
xml复制代码 <!-- ikAnalyzer 中文分词器  -->
<dependency>
<groupId>com.janeluo</groupId>
<artifactId>ikanalyzer</artifactId>
<version>2012_u6</version>
<exclusions>
<exclusion>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
</exclusion>
<exclusion>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
</exclusion>
</exclusions>
</dependency>

<!-- lucene-queryParser 查询分析器模块 -->
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>7.3.0</version>
</dependency>
  • IKAnalyzerSupport类:用于配置分词器
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Slf4j
public class IKAnalyzerSupport {
/**
* IK分词
* @param target
* @return
*/
public static List<String> iKSegmenterToList(String target) throws Exception {
if (StringUtils.isEmpty(target)){
return new ArrayList();
}
List<String> result = new ArrayList<>();
StringReader sr = new StringReader(target);
// false:关闭智能分词 (对分词的精度影响较大)
IKSegmenter ik = new IKSegmenter(sr, true);
Lexeme lex;
while((lex=ik.next())!=null) {
String lexemeText = lex.getLexemeText();
result.add(lexemeText);
}
return result;
}
}
  • ServiceImpl类:进行分词处理
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
typescript复制代码 /**
* 对目标公司名称进行分词
* @param targetCompanyName
* @return
*/
private String splitWord(String targetCompanyName){
log.info("对处理后端公司名称进行分词");

List<String> splitWord = new ArrayList<>();
String result = targetCompanyName;
try {
splitWord = iKSegmenterToList(targetCompanyName);
result = splitWord.stream().map(String::valueOf).distinct().collect(Collectors.joining("|")) ;
log.info("分词结果:{}",result);
} catch (Exception e) {
log.error("分词报错:{}",e.getMessage());
}
return result;
}

5.3 匹配

  • ServiceImpl类:匹配核心代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
ini复制代码    public JsonResult matchCompanyName(CompanyDTO companyDTO, String accessToken, String localIp) {
// 对公司名称进行处理
String sourceCompanyName = companyDTO.getCompanyName();
String targetCompanyName = sourceCompanyName;
log.info("处理前公司名称:{}",targetCompanyName);
// 处理圆括号
targetCompanyName = targetCompanyName.replaceAll("[(]|[)]|[(]|[)]","");
// 处理公司相关关键词
targetCompanyName = targetCompanyName.replaceAll("[(集团|股份|有限|责任|分公司)]", "");

if (!targetCompanyName.contains("银行")){
// 去除行政区域
targetCompanyName = formatCompanyName(targetCompanyName);
}
// 分词
String splitCompanyName = splitWord(targetCompanyName);
// 匹配
List<Company> matchedCompany = companyRepository.queryMatchCompanyName(splitCompanyName,targetCompanyName);

List<String> result = new ArrayList();
for (Company companyInfo : matchedCompany) {
result.add(companyInfo.getCompanyName());
if (companyDTO.getCompanyId().equals(companyInfo.getCompanyId())){
result.remove(companyInfo.getCompanyName());
}
}
return JsonResult.successResult(result);
}
  • Repository类:编写SQL语句
1
2
3
4
5
6
7
8
9
10
11
ini复制代码/**  
* 模糊匹配公司名称
* @param companyNameRegex 分词后的公司名称
* @param companyName 分词前的公司名称
* @return
*/
@Query(value =
"SELECT * FROM company WHERE isDeleted = '0' and companyName REGEXP ?1
ORDER BY length(REPLACE(companyName,?2,''))/length(companyName) ",
nativeQuery = true)
List<Company> queryMatchCompanyName(String companyNameRegex,String companyName);

按照匹配度排序这个功能点,LENGTH(companyName)返回companyName的长度,LENGTH(REPLACE(companyName, ?2, ''))计算出companyName中关键词出现的次数。通过这种方式,我们可以根据匹配程度进行排序,匹配次数越多的公司名称排序越靠前。

参考资料

  • zhuanlan.zhihu.com/p/343198664 【MySQL模糊搜索】
  • blog.csdn.net/Cy_LightBul… 【IK分词器集成Spring Boot】

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

零基础入门AI:四步快速搭建本地的编程助手 Step 1

发表于 2024-02-28

由于种种原因,小伙伴们在写代码时,不一定能用上Github Copilot;或者由于代码安全的原因,不能使用外网的编程助手。今天我就介绍一种利用开源大语言模型在本地搭建编程助手的方案:

  • IDE插件:Continue
  • 本地开源大语言模型框架:Ollama
  • 本地开源大语言模型(推荐 Code Llama)

该方案适用于 VS Code 和 PyCharm,搭建的步骤类似,如下步骤中以VS Code为例说明。

Step 1: 安装 Continue 插件

在VS Code 和 PyCharm的插件市场中搜索并安装 Continue 插件,以VS Code为例,安装步骤截图如下:
1.png
安装完成后,默认会有一些在线的大语言模型供试用(需要联网)。
2.png

更多关于Continue插件的细节,可以参考Continue插件的官网:Continue官网

Continue插件支持GPT-4 / Claude-2 等收费大语言模型,只需要将自己的 api key 更新到配置文件中的 apiKey 即可,以下步骤将介绍如何使用 Continue 插件调用本地开源大语言模型。

Step 2: 安装本地开源大语言模型框架 Ollama

Ollama 是一个可以在本地部署和管理开源大语言模型的框架,由于它极大的简化了开源大语言模型的安装和配置细节,一经推出就广受好评,目前已在github上获得了46k star。

关于Ollama的更多介绍和安装方法请参见 零基础入门AI:一键本地运行各种开源大语言模型 - Ollama。

Step 3: 下载代码大语言模型 Code Llama

安装Ollama后,即可一键安装开源大语言模型。Ollama支持大部分主流的开源大语言模型,但是在编程助手的使用场景下,我更推荐Meta发布的 CodeLlama,包含 7b / 13b / 34b / 70b 四种参数规模,小伙伴们可以根据本地GPU的性能,下载合适的Code Llama版本

参数版本 链接 命令
7 billion View ollama run codellama:7b
13 billion View ollama run codellama:13b
34 billion View ollama run codellama:34b
70 billion View ollama run codellama:70b

PS: Code Llama除了有不同的参数版本,根据微调语料的不同类型,还有基础、Python、instruct三种类型的微调版本,其中instruct是经过自然语言指令微调的,更适合本文的使用场景,所以推荐使用 instruct 版本,以上表格中的命令默认下载的就是instruct版本。

Step 4: 配置 Continue + Ollama

Ollama服务运行后,默认的API接口地址是localhost:11434,能够为本机提供服务。

也可以在Ollama中配置环境变量 OLLAMA_HOST=0.0.0.0:11434 来对局域网提供服务,API接口地址就是<ollama server IP>:11434。(具体可参见 零基础入门AI:一键本地运行各种开源大语言模型 - Ollama)

将大语言模型的信息和API接口地址添加到 Continue 的配置文件中,举例如下:

1
2
3
4
5
6
7
json复制代码"models": [
    {
      "title": "codellama:7b",
      "provider": "ollama",
      "model": "codellama:7b",
      "api_base": "localhost:11434"
    }

修改完配置文件后,在 IDE 的 Continue 插件界面选择本地大模型,就可以愉快的使用本地大模型玩耍了:

3.png

至此,Continue插件就可以和本地大语言模型联动,调用本地的大语言模型来辅助编程,实现本地的编程助手。

Bingo! 运行 Continue + Ollama

如下图中的例子,我让大模型帮忙写一下python的函数,用于判断输入的数字是不是质数:
4.png

添加常用的快捷指令 (可选)

阅读 Continue 插件的配置文件,会发现其中已经内置了一些快捷指令,其实就是将一段提示词简化为一个命令,比如:

  • comment: 为选中的代码添加注释,对应的提示词是 Write comments for the highlighted code
  • edit: 编辑选中的代码,对应的提示词是 Edit highlighted code

参照这些示例,我们也可以添加自己常用的快捷指令,比如我添加了一个代码评审的快捷指令 review,具体配置如下:

1
2
3
4
5
6
7
json复制代码"custom_commands": [
    {
      "name": "review",
      "prompt": "Act as a comprehensive code review assistant to help me analyze and improve the selected code",
      "description": "Code review for highlighted code"
    }
  ]

PS:

  • 实际使用时,可以用大语言模型来辅助生成代码、改BUG、添加注释、解释代码等等。
  • 以上例子中,我仅以最小规模的7B版本为例,本地GPU性能更强的话,可以尝试更大参数规模的版本,使用效果会更好一些。
  • 本文推荐的代码大语言模型 Code Llama 的训练语料中,中文语料所占的比例较少,所以使用英文提问的效果会更好一些。
  • Ollama同样也支持一些中文的开源模型,比如qwen 、Yi等等,这些大模型对中文的支持就会更好一些,可以参照文中的方法安装使用。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Android 13源码分析】Activity启动流程-3

发表于 2024-02-28

对WMS很感兴趣,所以决定以在桌面点击应用图标,到应用的Activity显示到屏幕上,这一简单操作为基础,分析整个过程。

其中涉及到非常多的模块,但是首先需要分析的就是Activity的启动流程,由于篇幅原因,分为以下3部分:

【Android 13源码分析】Activity启动流程-1

【Android 13源码分析】Activity启动流程-2

【Android 13源码分析】Activity启动流程-3

虽然整个操作实际上就2S时间,但是整个完整的流程分析下来也需要几个月,而且还是仅仅是启动主流程,

这三篇对Activity启动流程分析只关心主流程,不看具体细节,当然对于一些关键方法会着重介绍,这样以后如果有遇到相关问题的修改可以通过这篇笔记找到具体代码位置,然后根据具体的问题分析修改。

后续会有在Activity启动流程基础上延伸出来的各个分支的记录,比如窗口的创建与挂载,Surface相关,窗口的动画等待,这些流程的分析都需要基于Activity启动流程。

启动Activity的方式有很多,当前以在Launch中点击“电话”图标启动应用为例,本篇为Activity启动的第三篇, 主要是在目标应用进程启动后的逻辑。

  1. 应用进程创建

java的方法的main函数
其实在执行到AMS::attachApplication前,应用进程也有一段逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
java复制代码# ActivityThread
// ApplicationThread 是 AMS 作为 C 端时,与应用进程通信的方式
final ApplicationThread mAppThread = new ApplicationThread();
public static void main(String[] args) {
......
// 重点 *1. 主线程Looper 处理已经创建ActivityThread
// 主线程Looper
Looper.prepareMainLooper();
......
ActivityThread thread = new ActivityThread();
thread.attach(false, startSeq);
// 主线程Looper
Looper.loop();
}
private void attach(boolean system, long startSeq) {
......
final IActivityManager mgr = ActivityManager.getService();
try {
//重点 *2. 将mAppThread告知AMS,用于AMS与应用进程通信
mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
throw ex.rethrowFromSystemServer();
}
......
}

应用进程(电话)创建完毕后,在main方法里会执行attach,就是要将自己的信息告知AMS,毕竟AMS 是管理模块。

  1. AMS收到“电话”进程创建完毕的执行 realStartActivityLocked

因为进程创建也不是由AMS执行,AMS也不知道具体什么时候进程创建好了,所以在应用进程创建好后,会执行 AMS::attachApplication来告知。 这样就可以AMS就知道可以开始处理后续逻辑了

2.1 调用链

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码AMS::attachApplication
AMS::attachApplicationLocked
ActivityThread::bindApplication
ActivityTaskManagerService.LocalService::attachApplication
WindowContainer::forAllRootTasks --- 省略forAllRootTasks等固定堆栈
Task::forAllRootTasks
WindowContainer::forAllActivities
ActivityRecord::forAllActivities
RootWindowContainer.AttachApplicationHelper::test
RootWindowContainer.AttachApplicationHelper::test
ActivityTaskSupervisor::realStartActivityLocked -- 构建LaunchActivityItem

调用链realStartActivityLocked.png

接上一篇知道如果进程启动了ActivityTaskSupervisor::startSpecificActivity就会走进去ActivityTaskSupervisor::realStartActivityLocked。
但是可能会好奇怎么就知道要执行应用MainActivity到onCreate就一定是在这个方法里呢? 调试方法有很多,比如加log,打堆栈,但是对应这个逻辑比较简单的是,需要执行Activity启动到onCreate的控制在LaunchActivityItem中,而LaunchActivityItem在framework的引用出了本身,就只有在ActivityTaskSupervisor。

为什么看ActivityTaskSupervisor.png

2.2 主流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
scss复制代码# AMS 
// 当应用进程调用attachApplication 执行
public final void attachApplication(IApplicationThread thread, long startSeq) {
if (thread == null) {
throw new SecurityException("Invalid application interface");
}
synchronized (this) {
// 获取 应用进程的信息后执行attachApplicationLocked
int callingPid = Binder.getCallingPid();
final int callingUid = Binder.getCallingUid();
final long origId = Binder.clearCallingIdentity();
attachApplicationLocked(thread, callingPid, callingUid, startSeq);
Binder.restoreCallingIdentity(origId);
}
}
private boolean attachApplicationLocked(@NonNull IApplicationThread thread,
int pid, int callingUid, long startSeq) {
// 需要启动应用的进程数据
ProcessRecord app;
......
if (pid != MY_PID && pid >= 0) {
synchronized (mPidsSelfLocked) {
// 通过mPidsSelfLocked获取
app = mPidsSelfLocked.get(pid);
}
......
}
......
// 触发ActivityThread::bindApplication 逻辑
if (app.getIsolatedEntryPoint() != null) {

} else if (instr2 != null) {
// bindApplication
thread.bindApplication(processName, appInfo,
app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,
providerList,
instr2.mClass,
profilerInfo, instr2.mArguments,
instr2.mWatcher,
instr2.mUiAutomationConnection, testMode,
mBinderTransactionTrackingEnabled, enableTrackAllocation,
isRestrictedBackupMode || !normalMode, app.isPersistent(),
new Configuration(app.getWindowProcessController().getConfiguration()),
app.getCompat(), getCommonServicesLocked(app.isolated),
mCoreSettingsObserver.getCoreSettingsLocked(),
buildSerial, autofillOptions, contentCaptureOptions,
app.getDisabledCompatChanges(), serializedSystemFontMap,
app.getStartElapsedTime(), app.getStartUptime());
} else {
// bindApplication
thread.bindApplication(processName, appInfo,
app.sdkSandboxClientAppVolumeUuid, app.sdkSandboxClientAppPackage,
providerList, null, profilerInfo, null, null, null, testMode,
mBinderTransactionTrackingEnabled, enableTrackAllocation,
isRestrictedBackupMode || !normalMode, app.isPersistent(),
new Configuration(app.getWindowProcessController().getConfiguration()),
app.getCompat(), getCommonServicesLocked(app.isolated),
mCoreSettingsObserver.getCoreSettingsLocked(),
buildSerial, autofillOptions, contentCaptureOptions,
app.getDisabledCompatChanges(), serializedSystemFontMap,
app.getStartElapsedTime(), app.getStartUptime());
}
......
if (normalMode) {
try {
// 重点 触发构建 LaunchActivityItem 流程
didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
} catch (Exception e) {
Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
badApp = true;
}
}
......
}

这里是触发 LaunchActivityItem 的流程主线 , mAtmInternal是 ATMS 的内部类 LocalService

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码# ActivityTaskManagerService.LocalService
@Override
public boolean attachApplication(WindowProcessController wpc) throws RemoteException {
......
return mRootWindowContainer.attachApplication(wpc);
......
}
# RootWindowContainer
boolean attachApplication(WindowProcessController app) throws RemoteException {
try {
return mAttachApplicationHelper.process(app);
} finally {
mAttachApplicationHelper.reset();
}
}
# RootWindowContainer.AttachApplicationHelper
boolean process(WindowProcessController app) throws RemoteException {
mApp = app;
for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
// 重点* 调用每个容器的 forAllRootTasks
getChildAt(displayNdx).forAllRootTasks(this);
if (mRemoteException != null) {
throw mRemoteException;
}
}
......
}

这里的重点是执行了forAllRootTasks,当孩子是RootTask容器的时候才会执行Lambda表达式的内容。

AttachApplicationHelper 必然实现了 Consumer 接口, 直接看其 accept 实现即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
java复制代码# RootWindowContainer.AttachApplicationHelper
private class AttachApplicationHelper implements Consumer<Task>, Predicate<ActivityRecord> {
......
boolean process(WindowProcessController app) throws RemoteException {
mApp = app;
for (int displayNdx = getChildCount() - 1; displayNdx >= 0; --displayNdx) {
// 重点*1. 调用每个容器的 forAllRootTasks
getChildAt(displayNdx).forAllRootTasks(this);
if (mRemoteException != null) {
throw mRemoteException;
}
}
......
}
@Override
public void accept(Task rootTask) {
......
// 执行 topRunningActivity
mTop = rootTask.topRunningActivity();
// 重点*2. 执行accept 让容器下的每个 ActivityRecord 执行
rootTask.forAllActivities(this);
}
@Override
public boolean test(ActivityRecord r) {
......
try {
// 重点*3. 执行 realStartActivityLocked 触发创建LaunchActivityItem
if (mTaskSupervisor.realStartActivityLocked(r, mApp,
mTop == r && r.getTask().canBeResumed(r) /* andResume */,
true /* checkConfig */)) {
mHasActivityStarted = true;
}
} catch (RemoteException e) {
Slog.w(TAG, "Exception in new application when starting activity " + mTop, e);
mRemoteException = e;
return true;
}
return false;
}
}

重点分析:
AttachApplicationHelper 实现类Consumer,Predicate接口。 对应要实现的函数分别为accept和test

  1. RootWindowContainer 的 child 自然是 WindowContainer

这里可能有个疑问就是 forAllRootTasks 一个参数的方法有2个重载,参数分别为Consumer 接口和Predicate 接口。 当前的AttachApplicationHelper类这两个都实现了,
怎么判断执行的是哪个呢?
答案也很简单, 只要看一下泛型参数即可。
2. 根据上面的解释, 第二步执行 accept回调, 内部执行 Task::forAllActivities 函数, 但是Task本身没有该方法, 该方法定义在父类 WindowContainer 中

  1. 那么逻辑就来到了气到的realStartActivityLocked ,其内部会构建 LaunchActivityItem,这个在【Activity生命周期事务执行逻辑】有详细说明
    在把 ActivityTaskSupervisor::realStartActivityLocked方法看一下,现在已知 setLifecycleStateRequest为ResumeActivityItem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
scss复制代码# ActivityTaskSupervisor 

boolean realStartActivityLocked(ActivityRecord r, WindowProcessController proc,
boolean andResume, boolean checkConfig) throws RemoteException {
// 重点* 判断是否执行完了pause 。 也就是2个条件之一,必须要执行完pause才可以进入后面
if (!mRootWindowContainer.allPausedActivitiesComplete()) {
// While there are activities pausing we skipping starting any new activities until
// pauses are complete. NOTE: that we also do this for activities that are starting in
// the paused state because they will first be resumed then paused on the client side.
ProtoLog.v(WM_DEBUG_STATES,
"realStartActivityLocked: Skipping start of r=%s some activities pausing...",
r);
return false;
}
......
// 重点*1. 创建Activity启动事务.
final ClientTransaction clientTransaction = ClientTransaction.obtain(proc.getThread(), r.token);
// 判断Activity是否处于向前转场状态
final boolean isTransitionForward = r.isTransitionForward();
// 获取Activity所在 TaskFragment Token
final IBinder fragmentToken = r.getTaskFragment().getFragmentToken();
// 重点*2. 将构建的 LaunchActivityItem 添加到 clientTransaction 中
clientTransaction.addCallback(LaunchActivityItem.obtain(new Intent(r.intent),
System.identityHashCode(r), r.info,
// TODO: Have this take the merged configuration instead of separate global
// and override configs.
mergedConfiguration.getGlobalConfiguration(),
mergedConfiguration.getOverrideConfiguration(), r.compat,
r.getFilteredReferrer(r.launchedFromPackage), task.voiceInteractor,
proc.getReportedProcState(), r.getSavedState(), r.getPersistentSavedState(),
results, newIntents, r.takeOptions(), isTransitionForward,
proc.createProfilerInfoIfNeeded(), r.assistToken, activityClientController,
r.shareableActivityToken, r.getLaunchedFromBubble(), fragmentToken));
// 重点*3. 设置预期的最终状态
final ActivityLifecycleItem lifecycleItem;
if (andResume) {
// Resume逻辑,启动走的这。 表示需要执行到onCreate
lifecycleItem = ResumeActivityItem.obtain(isTransitionForward);
} else {
// Pause 逻辑
lifecycleItem = PauseActivityItem.obtain();
}
clientTransaction.setLifecycleStateRequest(lifecycleItem);
// 重点*4. 调度事务,将clientTransaction添加到生命周期管理器中
mService.getLifecycleManager().scheduleTransaction(clientTransaction);
}

2.2.1 LaunchActivityItem handleLaunchActivity (启动ActivityonCreate)

2.2.1.1 调用链

1
2
3
4
5
6
7
8
9
10
arduino复制代码LaunchActivityItem::execute
ActivityThread::handleLaunchActivity
ActivityThread::performLaunchActivity
Instrumentation::newActivity --- 创建Activity
Activity::attach ---处理Window相关
Window::init
Window::setWindowManager
Instrumentation::callActivityOnCreate ---onCreate流程
Activity::performCreate
Activity::onCreate --over

2.2.1.2 主流程

接下来看 LaunchActivityItem::execute

1
2
3
4
5
6
7
8
9
10
11
12
csharp复制代码# LaunchActivityItem 
public void execute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
ActivityClientRecord r = new ActivityClientRecord(token, mIntent, mIdent, mInfo,
mOverrideConfig, mCompatInfo, mReferrer, mVoiceInteractor, mState, mPersistentState,
mPendingResults, mPendingNewIntents, mActivityOptions, mIsForward, mProfilerInfo,
client, mAssistToken, mShareableActivityToken, mLaunchedFromBubble,
mTaskFragmentToken);
client.handleLaunchActivity(r, pendingActions, null /* customIntent */);
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}

这里的 client 是 ClientTransactionHandler 类型, 而 ActivityThread 是 ClientTransactionHandler子类

重点是这边创建了ActivityClientRecord,第一个参数就是我们要找的token

逻辑来到应用进程 ActivityThread

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
scss复制代码# ActivityThread

public Activity handleLaunchActivity(ActivityClientRecord r,
PendingTransactionActions pendingActions, Intent customIntent) {
......
final Activity a = performLaunchActivity(r, customIntent);
......
}
private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
......
Activity activity = null;
try {
// 重点* 1. 通过Instrumentation 反射创建Activity
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
......
}
try {
......
// 重点* 2. 执行 attach 流程
activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window, r.activityConfigCallback,
r.assistToken, r.shareableActivityToken);
if (r.isPersistable()) {
mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
} else {
重点* 3. OnCreate流程
mInstrumentation.callActivityOnCreate(activity, r.state);
}
}
......
}

# Instrumentation
public void callActivityOnCreate(Activity activity, Bundle icicle) {
prePerformCreate(activity);
// onCreate流程
activity.performCreate(icicle);
postPerformCreate(activity);
}
# Activity
final void performCreate(Bundle icicle) {
performCreate(icicle, null);
}
final void performCreate(Bundle icicle, PersistableBundle persistentState) {
......
if (persistentState != null) {
onCreate(icicle, persistentState);
} else {
// 执行onCreate
onCreate(icicle);
}
......
}

写过应用的都知道,默认都是一个参数的onCreate。
流程结束

tip:在添加回调的时候,还加了个请求的生命周期, 因为 LaunchActivityItem 只是创建,但是创建完成后,需要执行到对应的生命周期。
正常情况都是希望执行到onResume,所以会设置 ResumeActivityItem,不过不是分析重点,可自行了解

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Android 13源码分析】Activity启动流程-2

发表于 2024-02-28

对WMS很感兴趣,所以决定以在桌面点击应用图标,到应用的Activity显示到屏幕上,这一简单操作为基础,分析整个过程。

其中涉及到非常多的模块,但是首先需要分析的就是Activity的启动流程,由于篇幅原因,分为以下3部分:

【Android 13源码分析】Activity启动流程-1

【Android 13源码分析】Activity启动流程-2

【Android 13源码分析】Activity启动流程-3

虽然整个操作实际上就2S时间,但是整个完整的流程分析下来也需要几个月,而且还是仅仅是启动主流程,

这三篇对Activity启动流程分析只关心主流程,不看具体细节,当然对于一些关键方法会着重介绍,这样以后如果有遇到相关问题的修改可以通过这篇笔记找到具体代码位置,然后根据具体的问题分析修改。

后续会有在Activity启动流程基础上延伸出来的各个分支的记录,比如窗口的创建与挂载,Surface相关,窗口的动画等待,这些流程的分析都需要基于Activity启动流程。

启动Activity的方式有很多,当前以在Launch中点击“电话”图标启动应用为例,本篇为第二篇。

接【Android 13源码分析】Activity启动流程-1在ActivityStarer::startActivityInner方法中,通过getOrCreateRootTask方法创建Task,而后通过setNewTask方法将新建的ActivityRecord进行挂在到新建的Task上,接下来将需要处理新的Activity启动和显示逻辑。

后续的流程由resumeFocusedTasksTopActivities执行。

显示Activity resumeFocusedTasksTopActivities

调用链

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码RootWindowContainer::resumeFocusedTasksTopActivities
Task::resumeTopActivityUncheckedLocked
Task::resumeTopActivityInnerLocked
TaskFragment::resumeTopActivity
TaskDisplayArea::pauseBackTasks -- 2.2.1 pause LauncherActivity
WindowContainer::forAllLeafTask
TaskFragment::forAllLeafTaskFragments
TaskFragment::startPausing
TaskFragment::startPausing
TaskFragment::schedulePauseActivity --构建 PauseActivityItem,这里是触发暂停launch
ActivityTaskManagerService::startProcessAsync -- 2.2.2创建“电话”进程

主流程跟踪

经过几次调用会执行到TaskFragment::resumeTopActivity方法,这个方法非常复杂,场景不同执行的分支也不同,值得重点分析。而且很多重要的逻辑都集中在这里,我觉得后续会被重构的。
当前还是只分析launcher 启动“电话”的场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
java复制代码# TaskFragment
final boolean resumeTopActivity(ActivityRecord prev, ActivityOptions options,
boolean deferPause) {
// 这里的next返回的是 电话的main activity,表示下一个需要现实的
ActivityRecord next = topRunningActivity(true /* focusableOnly */);
......
// 如果跳过的这些逻辑都没执行return,则正在开始执行 resume流程,打印关键日志。
// 前面流程如果返回也有响应日志的打印
// 打印日志,需要显示哪个Activity
if (DEBUG_SWITCH) Slog.v(TAG_SWITCH, "Resuming " + next);
......
// 重点* 1. 这里会将 launch的Activity pause 。参数是 电话的ActivityRecord
boolean pausing = !deferPause && taskDisplayArea.pauseBackTasks(next);
......
if (pausing) {
ProtoLog.v(WM_DEBUG_STATES, "resumeTopActivity: Skip resume: need to"+ " start pausing");
if (next.attachedToProcess()) {
......
} else if (!next.isProcessRunning()) {
// 重点*2. 进程没有运行,则触发异步创建进程。 当前逻辑肯定是执行这一条
final boolean isTop = this == taskDisplayArea.getFocusedRootTask();
mAtmService.startProcessAsync(next, false /* knownToBeDead */, isTop,
isTop ? HostingRecord.HOSTING_TYPE_NEXT_TOP_ACTIVITY
: HostingRecord.HOSTING_TYPE_NEXT_ACTIVITY);
}
......
// 注意,这里会return,
return true;
}
......
//后面还有重要逻辑,当前可忽略
}

重点分析:
上面说过这个函数非常复杂,在当前逻辑有2个主线

  1. pause当前Activity,也就是launcher
  2. 异步创建“电话”的进程

在第一步将launcher的Activity执行pauser, 这一步执行到最后也会触发”电话”应用MainActivity的启动,第二步创建“电话”进程,进程创建完肯定也会执行”电话”应用MainActivity的启动,这么看来就有2个地方触发了。

这是因为是异步创建进程,不知道谁先执行完,但是可以明确的是,”电话”应用MainActivity必须有2个条件:

  1. 前一个Activity执行完pause
  2. 进程创建完成

所以无论2个分支哪一个先执行完,都需要等后一个执行完的来触发后续电话”应用MainActivity的启动。

先看launcher的pause流程

  1. pause 流程 pauseBackTasks

需要显示新的Activity,那么之前的肯定是要执行pause的,就在这里执行。参数next为“电话的”ActivityRecord

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
java复制代码# TaskDisplayArea

// 可以看到“电话”ActivityRecord在这里就被称为resuming
boolean pauseBackTasks(ActivityRecord resuming) {
final int[] someActivityPaused = {0};
forAllLeafTasks(leafTask -> {
// Check if the direct child resumed activity in the leaf task needed to be paused if
// the leaf task is not a leaf task fragment.
if (!leafTask.isLeafTaskFragment()) {
// 当前不会走这里
final ActivityRecord top = topRunningActivity();
final ActivityRecord resumedActivity = leafTask.getResumedActivity();
if (resumedActivity != null && top.getTaskFragment() != leafTask) {
// Pausing the resumed activity because it is occluded by other task fragment.
if (leafTask.startPausing(false /* uiSleeping*/, resuming, "pauseBackTasks")) {
someActivityPaused[0]++;
}
}
}

leafTask.forAllLeafTaskFragments((taskFrag) -> {
final ActivityRecord resumedActivity = taskFrag.getResumedActivity();
if (resumedActivity != null && !taskFrag.canBeResumed(resuming)) {
if (taskFrag.startPausing(false /* uiSleeping*/, resuming, "pauseBackTasks")) {
someActivityPaused[0]++;
}
}
}, true /* traverseTopToBottom */);
}, true /* traverseTopToBottom */);
return someActivityPaused[0] > 0;
}

forAllLeafTasks和forAllLeafTaskFragments在【WMS/AMS 常见方法调用提取】中有解释,那么当前这段方法其实就是让DefaultTaskDisplayArea下的每个叶子LeafTaskFragments执行startPausing。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
typescript复制代码# TaskFragment
boolean startPausing(boolean userLeaving, boolean uiSleeping, ActivityRecord resuming,
String reason) {
......
// 日志输出当前TaskFragment和mResumedActivity的关系。后面会贴上日志证明
ProtoLog.d(WM_DEBUG_STATES, "startPausing: taskFrag =%s " + "mResumedActivity=%s", this,
mResumedActivity);
......
// 后面的prev就是launcher的ActivityRecord了
ActivityRecord prev = mResumedActivity;
......
// 输出日志
ProtoLog.v(WM_DEBUG_STATES, "Moving to PAUSING: %s", prev);
mPausingActivity = prev;
mLastPausedActivity = prev;
if (!prev.finishing && prev.isNoHistory()
&& !mTaskSupervisor.mNoHistoryActivities.contains(prev)) {
mTaskSupervisor.mNoHistoryActivities.add(prev);
}
// 设置window状态为PAUSING
prev.setState(PAUSING, "startPausingLocked");
prev.getTask().touchActiveTime();
......
if (prev.attachedToProcess()) {
// launcher的进程肯定是满足条件的
if (shouldAutoPip) {
boolean didAutoPip = mAtmService.enterPictureInPictureMode(
prev, prev.pictureInPictureArgs);
ProtoLog.d(WM_DEBUG_STATES, "Auto-PIP allowed, entering PIP mode "
+ "directly: %s, didAutoPip: %b", prev, didAutoPip);
} else {
// 重点*1. 根据堆栈是执行这里。上面的PIP日志没输出,也能确定是走的这个
schedulePauseActivity(prev, userLeaving, pauseImmediately, reason);
}
}
......
}

void schedulePauseActivity(ActivityRecord prev, boolean userLeaving,
boolean pauseImmediately, String reason) {
// 输出日志
ProtoLog.v(WM_DEBUG_STATES, "Enqueueing pending pause: %s", prev);
try {
// 输出events 日志
EventLogTags.writeWmPauseActivity(prev.mUserId, System.identityHashCode(prev),
prev.shortComponentName, "userLeaving=" + userLeaving, reason);
// 重点构建并执行PauseActivityItem
mAtmService.getLifecycleManager().scheduleTransaction(prev.app.getThread(),
prev.token, PauseActivityItem.obtain(prev.finishing, userLeaving,
prev.configChangeFlags, pauseImmediately));
} catch (Exception e) {
// Ignore exception, if process died other code will cleanup.
Slog.w(TAG, "Exception thrown during pause", e);
mPausingActivity = null;
mLastPausedActivity = null;
mTaskSupervisor.mNoHistoryActivities.remove(prev);
}
}

最终在TaskFragment::schedulePauseActivity构建并执行了launcher的pause事件。
这块相关的日志输入如图:
因为是后面补充的所以对象不一样,但是能确定这里处理的TaskFragment和mResumedActivity都是launcher对象的。

Plotolog-startPausing.png

tip:
State movement这段的输出是在ActivityRecord::setState 只有状态改变都会输出

接下来的重点是看PauseActivityItem的执行逻辑

1.1 PauseActivityItem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
typescript复制代码# PauseActivityItem

@Override
public void execute(ClientTransactionHandler client, ActivityClientRecord r,
PendingTransactionActions pendingActions) {
Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityPause");
// 重点*1. 触发 handlePauseActivity 流程
client.handlePauseActivity(r, mFinished, mUserLeaving, mConfigChanges, pendingActions,
"PAUSE_ACTIVITY_ITEM");
Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER);
}

@Override
public int getTargetState() {
return ON_PAUSE;
}
// pauser执行后调用 postExecute
@Override
public void postExecute(ClientTransactionHandler client, IBinder token,
PendingTransactionActions pendingActions) {
if (mDontReport) {
return;
}
// 重点*2. 触发启动新的Activity
ActivityClient.getInstance().activityPaused(token);
}

默认已经知道ClientTransaction调用逻辑,不知道移步【】
这里先执行execute 的逻辑,也就是 launcher的pause流程,这个不是主线,单独解释【】。然后执行postExecute。这里会触发新Activity的启动。

1
2
3
4
5
6
7
8
9
scss复制代码# ActivityClient

public void activityPaused(IBinder token) {
try {
getActivityClientController().activityPaused(token);
} catch (RemoteException e) {
e.rethrowFromSystemServer();
}
}

getActivityClientController返回的是ActivityClientController对象。

调用链

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
arduino复制代码ActivityClientController::activityPaused
ActivityRecord::activityPaused
TaskFragment::completePause
RootWindowContainer::resumeFocusedTasksTopActivities --分支1 ,再次执行 resumeFocusedTasksTopActivities
RootWindowContainer::resumeFocusedTasksTopActivities
Task::resumeTopActivityUncheckedLocked
Task::resumeTopActivityInnerLocked
TaskFragment::resumeTopActivity
ActivityTaskSupervisor::startSpecificActivity --触发 startSpecificActivity 判断应用是否创建
RootWindowContainer::ensureActivitiesVisible --分支2
RootWindowContainer::ensureActivitiesVisible
DisplayContent::ensureActivitiesVisible
WindowContainer::forAllRootTasks --忽略固定逻辑
Task::ensureActivitiesVisible
Task::forAllLeafTasks --忽略固定逻辑
TaskFragment::updateActivityVisibilities
EnsureActivitiesVisibleHelper::process
EnsureActivitiesVisibleHelper::setActivityVisibilityState
EnsureActivitiesVisibleHelper::makeVisibleAndRestartIfNeeded
ActivityTaskSupervisor::startSpecificActivity --触发 startSpecificActivity 判断应用是否创建

调用堆栈如下:

pause-startProcessAsync.png

不是每个函数都要去跟,没有意义,跟踪重点函数,关注重点处理的地方即可。 否则下个版本重构后,就完全不一样。 所以只需要记住重点做的事情就好。
这边又2个地方都会出发ActivityTaskSupervisor::startSpecificActivity,而且都是由ActivityClientController::activityPaused为源头。
由activityPaused方法名也能知道这段逻辑是在launcher 执行完pause后再执行的

注意这里在第一个分支 RootWindowContainer::resumeFocusedTasksTopActivities到TaskFragment::resumeTopActivity直接的调用栈和上一篇是一样的,只不过最后执行的不一样而已

pause后,虽然又2个分支,但是根据函数名,其实都是为了能让下一个Activity可见。毕竟不能让用户什么都看不见, 总得显示一个吧

主流程跟踪

接下来进入代码分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
typescript复制代码# ActivityRecord
void activityPaused(boolean timeout) {
......
if (pausingActivity == this) {
// 打印日志
ProtoLog.v(WM_DEBUG_STATES, "Moving to PAUSED: %s %s", this,
(timeout ? "(due to timeout)" : " (pause complete)"));
mAtmService.deferWindowLayout();
try {
// 进入下一步,注入第二个参数为null
taskFragment.completePause(true /* resumeNext */, null /* resumingActivity */);
} finally {
mAtmService.continueWindowLayout();
}
return;
}
......
}
// 打印如下
WindowManager: Moving to PAUSED: ActivityRecord{1f58beb u0 com.android.launcher3/.uioverrides.QuickstepLauncher} t8} (pause complete)

//TaskFragment::completePause
# TaskFragment
@VisibleForTesting
void completePause(boolean resumeNext, ActivityRecord resuming) {
// 拿到先去的Activity,也就是需要 pause的
ActivityRecord prev = mPausingActivity;
ProtoLog.v(WM_DEBUG_STATES, "Complete pause: %s", prev);
if (prev != null) {
......
// 设置窗口状态为PAUSED
prev.setState(PAUSED, "completePausedLocked");
if (prev.finishing) {
...... 如果已经finish,上面还在设置 pause,那正常应该是还没finish
} else if (prev.hasProcess()) {
// 打印状态日志
ProtoLog.v(WM_DEBUG_STATES, "Enqueue pending stop if needed: %s "
+ "wasStopping=%b visibleRequested=%b", prev, wasStopping,prev.mVisibleRequested);
}else {
......
}
if (resumeNext) {
......
// 重点* 1. 第1个分支
mRootWindowContainer.resumeFocusedTasksTopActivities(topRootTask, prev,null /* targetOptions */);
......
}
......
// 重点* 2.
mRootWindowContainer.ensureActivitiesVisible(resuming, 0, !PRESERVE_WINDOWS);
......
}
}

结合前面TaskFragment::resumeTopActivity的创建进程,然后根据堆栈知道这2个分支也也创建新进程,那已知3地方触发创建进程。

这里的 topRootTask 就是SourceActivity“电话”的Activity,prev是launcher的,resuming为null。

1.1.1 分支1 resumeFocusedTasksTopActivities

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
arduino复制代码# TaskFragment
final boolean resumeTopActivity(ActivityRecord prev, ActivityOptions options,
boolean deferPause) {
......
// pause
boolean pausing = !deferPause && taskDisplayArea.pauseBackTasks(next);
......
if (pausing) {
......
// 启动进程
mAtmService.startProcessAsync(next, false /* knownToBeDead */, isTop,
isTop ? HostingRecord.HOSTING_TYPE_NEXT_TOP_ACTIVITY
: HostingRecord.HOSTING_TYPE_NEXT_ACTIVITY);
......
//return,
return true;
}
// 本次逻辑走这
......
// pause 逻辑会再次走到这,新进程目前还没有启动所以走else
if (next.attachedToProcess()) {
......
} else {
......
// 打印log
ProtoLog.d(WM_DEBUG_STATES, "resumeTopActivity: Restarting %s", next);
// 重点* 执行startSpecificActivity
mTaskSupervisor.startSpecificActivity(next, true, true);
}
}

又是TaskFragment::resumeTopActivity方法,这次是直接走到这个方法的最下面,启动Activity。

1.1.2 ensureActivitiesVisible分支

看方法名是为了 Activity的可见
根据上面的调用链,会执行到 DisplayContent::ensureActivitiesVisible

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
scss复制代码# DisplayContent
void ensureActivitiesVisible(ActivityRecord starting, int configChanges,
boolean preserveWindows, boolean notifyClients) {
if (mInEnsureActivitiesVisible) {
// Don't do recursive work.
return;
}
mInEnsureActivitiesVisible = true;
mAtmService.mTaskSupervisor.beginActivityVisibilityUpdate();
try {
forAllRootTasks(rootTask -> {
rootTask.ensureActivitiesVisible(starting, configChanges, preserveWindows,
notifyClients);
});
if (mTransitionController.isCollecting()
&& mWallpaperController.getWallpaperTarget() != null) {
// Also update wallpapers so that their requestedVisibility immediately reflects
// the changes to activity visibility.
// TODO(b/206005136): Move visibleRequested logic up to WindowToken.
mWallpaperController.adjustWallpaperWindows();
}
} finally {
mAtmService.mTaskSupervisor.endActivityVisibilityUpdate();
mInEnsureActivitiesVisible = false;
}
}

这里拿出来主要是遇到了forAllRootTasks这个方法,其实就和之前的forAllLeafTasks使用方式是一样的。对每个rootTask执行Lambda而已。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
java复制代码# Task
void ensureActivitiesVisible(@Nullable ActivityRecord starting, int configChanges,
boolean preserveWindows, boolean notifyClients) {
mTaskSupervisor.beginActivityVisibilityUpdate();
try {
forAllLeafTasks(task -> {
task.updateActivityVisibilities(starting, configChanges, preserveWindows,
notifyClients);
}, true /* traverseTopToBottom */);

if (mTranslucentActivityWaiting != null &&
mUndrawnActivitiesBelowTopTranslucent.isEmpty()) {
// Nothing is getting drawn or everything was already visible, don't wait for
// timeout.
notifyActivityDrawnLocked(null);
}
} finally {
mTaskSupervisor.endActivityVisibilityUpdate();
}
}

这里又有个forAllLeafTasks,调用每个叶子Task的updateActivityVisibilities方法,但是Task的这个方法是气父类TaskFragmet里定义的,所以看TaskFragmet。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
java复制代码# TaskFragmet
private final EnsureActivitiesVisibleHelper mEnsureActivitiesVisibleHelper =
new EnsureActivitiesVisibleHelper(this);
final void updateActivityVisibilities(@Nullable ActivityRecord starting, int configChanges,
boolean preserveWindows, boolean notifyClients) {
mTaskSupervisor.beginActivityVisibilityUpdate();
try {
// 重点
mEnsureActivitiesVisibleHelper.process(
starting, configChanges, preserveWindows, notifyClients);
} finally {
mTaskSupervisor.endActivityVisibilityUpdate();
}
}

我们知道这一条线都是为了处理Activity可见的。 在这定义了一个专门的类来处理。
不过需要注意的是,这个方法会执行多次,因为他是遍历每一个符合条件的子容器,从上到下遍历。

1.1.3 EnsureActivitiesVisibleHelper::process

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
java复制代码# EnsureActivitiesVisibleHelper
void process(@Nullable ActivityRecord starting, int configChanges, boolean preserveWindows, boolean notifyClients) {
// 调用 reset 方法,对传入的参数进行重置处理
reset(starting, configChanges, preserveWindows, notifyClients);
......
for (int i = mTaskFragment.mChildren.size() - 1; i >= 0; --i) {
// 获取当前子元素
final WindowContainer child = mTaskFragment.mChildren.get(i);
// 将当前子元素转换为TaskFragment,只有TaskFragment重写了,Task或ActivityRecord为null
// Task的孩子一般就是ActivityRecord或者Task
final TaskFragment childTaskFragment = child.asTaskFragment();
if (childTaskFragment != null
&& childTaskFragment.getTopNonFinishingActivity() != null) {
......
} else {
// 只有ActivityRecord重写了
setActivityVisibilityState(child.asActivityRecord(), starting, resumeTopActivity);
}
}
}

private void setActivityVisibilityState(ActivityRecord r, ActivityRecord starting,
final boolean resumeTopActivity) {
......
if (reallyVisible) {
.......
if (!r.attachedToProcess()) {
// 主流程
makeVisibleAndRestartIfNeeded(mStarting, mConfigChanges, isTop,
resumeTopActivity && isTop, r);
} else {
......
}
} else {
if (DEBUG_VISIBILITY) {
Slog.v(TAG_VISIBILITY, "Make invisible? " + r
+ " finishing=" + r.finishing + " state=" + r.getState()
+ " containerShouldBeVisible=" + mContainerShouldBeVisible
+ " behindFullyOccludedContainer=" + mBehindFullyOccludedContainer
+ " mLaunchTaskBehind=" + r.mLaunchTaskBehind);
}
// 不可见调用makeInvisible
r.makeInvisible();
}

}

这里的r 表示需要启动的ActivityRecord,当前流程还是onPause的流程。并没有知道创建进程那一步,所以进程是没有attached的。那就会执行makeVisibleAndRestartIfNeeded

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
arduino复制代码# EnsureActivitiesVisibleHelper
private void makeVisibleAndRestartIfNeeded(ActivityRecord starting, int configChanges,
boolean isTop, boolean andResume, ActivityRecord r) {
......
if (!r.mVisibleRequested || r.mLaunchTaskBehind) {
if (DEBUG_VISIBILITY) {
Slog.v(TAG_VISIBILITY, "Starting and making visible: " + r);
}
// 设置Visibility
r.setVisibility(true);
}
if (r != starting) {
mTaskFragment.mTaskSupervisor.startSpecificActivity(r, andResume,
true /* checkConfig */);
}
}

1.1.4 ActivityTaskSupervisor::startSpecificActivity

这个方法也被调用了2次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
java复制代码# ActivityTaskSupervisor

void startSpecificActivity(ActivityRecord r, boolean andResume, boolean checkConfig) {
// Is this activity's application already running?
// 拿到目标进程信息
final WindowProcessController wpc =
mService.getProcessController(r.processName, r.info.applicationInfo.uid);

boolean knownToBeDead = false;
// 进程是否存在,且主线程已执行
if (wpc != null && wpc.hasThread()) {
try {
// 进程进程 则执行 realStartActivityLocked 流程
realStartActivityLocked(r, wpc, andResume, checkConfig);
return;
} catch (RemoteException e) {
Slog.w(TAG, "Exception when starting activity "
+ r.intent.getComponent().flattenToShortString(), e);1111111111
}
......
}
......
// 异步启动进程
mService.startProcessAsync(r, knownToBeDead, isTop,
isTop ? HostingRecord.HOSTING_TYPE_TOP_ACTIVITY
: HostingRecord.HOSTING_TYPE_ACTIVITY);
}

这里有2个分支,如果目标进程创建了,则走realStartActivityLocked,否则执行创建的逻辑。不过就目前2个调用的地方都是执行onPause的逻辑性,这里还是其实都是制作到startProcessAsync。 而不会走进realStartActivityLocked

2 创建进程逻辑

这里是回到执行pause的同级流程,也就是最前面的TaskFragment::resumeTopActivity
执行pause后会执行 ActivityTaskManagerService::startProcessAsync,最后也是通过 ActivityManagerService来触发启动进程的。
看看AMS这块执行进程创建的调用流程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
java复制代码# ATMS  
void startProcessAsync(ActivityRecord activity, boolean knownToBeDead, boolean isTop,
String hostingType) {
try {
if (Trace.isTagEnabled(TRACE_TAG_WINDOW_MANAGER)) {
Trace.traceBegin(TRACE_TAG_WINDOW_MANAGER, "dispatchingStartProcess:"
+ activity.processName);
}
// 发生消息,启动进程,调用ActivityManagerInternal::startProcess
final Message m = PooledLambda.obtainMessage(ActivityManagerInternal::startProcess,
mAmInternal, activity.processName, activity.info.applicationInfo, knownToBeDead,
isTop, hostingType, activity.intent.getComponent());
mH.sendMessage(m);
} finally {
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
}
}
// ActivityManagerInternal::startProcess 的实现在AMS的内部类 LocalService 中
# AMS.LocalService
public void startProcess(String processName, ApplicationInfo info, boolean knownToBeDead,
boolean isTop, String hostingType, ComponentName hostingName) {
try {
if (Trace.isTagEnabled(Trace.TRACE_TAG_ACTIVITY_MANAGER)) {
Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "startProcess:"
+ processName);
}
synchronized (ActivityManagerService.this) {
// 重点调用
startProcessLocked(processName, info, knownToBeDead, 0 /* intentFlags */,
new HostingRecord(hostingType, hostingName, isTop),
ZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE, false /* allowWhileBooting */,
false /* isolated */);
}
} finally {
Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
}
}
@GuardedBy("this")
final ProcessRecord startProcessLocked(String processName,
ApplicationInfo info, boolean knownToBeDead, int intentFlags,
HostingRecord hostingRecord, int zygotePolicyFlags, boolean allowWhileBooting,
boolean isolated) {
return mProcessList.startProcessLocked(processName, info, knownToBeDead, intentFlags,
hostingRecord, zygotePolicyFlags, allowWhileBooting, isolated, 0 /* isolatedUid */,
false /* isSdkSandbox */, 0 /* sdkSandboxClientAppUid */,
null /* sdkSandboxClientAppPackage */,
null /* ABI override */, null /* entryPoint */,
null /* entryPointArgs */, null /* crashHandler */);
}

# ProcessList
boolean startProcessLocked(......) {
......
mService.mProcStartHandler.post(() -> handleProcessStart(
app, entryPoint, gids, runtimeFlags, zygotePolicyFlags, mountExternal,
requiredAbi, instructionSet, invokeWith, startSeq));
......
}
private void handleProcessStart(......) {
创建一个用于启动进程的 Runnable 对象
final Runnable startRunnable = () -> {
try { // 调用 startProcess 方法启动进程,并获取启动结果 ProcessStartResult
final Process.ProcessStartResult startResult = startProcess(app.getHostingRecord(),
entryPoint, app, app.getStartUid(), gids, runtimeFlags, zygotePolicyFlags,
mountExternal, app.getSeInfo(), requiredAbi, instructionSet, invokeWith,
app.getStartTime());
// 在锁定 ActivityManagerService 后,处理进程启动结果
synchronized (mService) {
handleProcessStartedLocked(app, startResult, startSeq);
}
} catch (RuntimeException e) {
......异常处理
}
};
// // 如果有前任进程并且前任进程正在死亡中
final ProcessRecord predecessor = app.mPredecessor;
if (predecessor != null && predecessor.getDyingPid() > 0) {
// 通过前任进程执行启动进程的 Runnable 对象
handleProcessStartWithPredecessor(predecessor, startRunnable);
} else {
// 如果没有前任进程或前任进程未死亡,直接运行启动进程的 Runnable 对象
// 走的是这个逻辑
startRunnable.run();
}
}

通过 startProcess 来启动应用进程

执行完后输出日志

1
ruby复制代码07-26 19:19:05.477  8737  8782 I ActivityManager: Start proc 19643:com.example.myapplication/u0a198 for next-top-activity {com.example.myapplication/com.example.myapplication.MainActivity}

这段日志是在 ProcessList::handleProcessStart 方法里构建了个StringBuilder,然后传递给AMS::reportUidInfoMessageLocked进行打印的。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Android 13源码分析】Activity启动流程-1

发表于 2024-02-28

对WMS很感兴趣,所以决定以在桌面点击应用图标,到应用的Activity显示到屏幕上,这一简单操作为基础,分析整个过程。

其中涉及到非常多的模块,但是首先需要分析的就是Activity的启动流程,由于篇幅原因,分为以下3部分:

【Android 13源码分析】Activity启动流程-1

【Android 13源码分析】Activity启动流程-2

【Android 13源码分析】Activity启动流程-3

虽然整个操作实际上就2S时间,但是整个完整的流程分析下来也需要几个月,而且还是仅仅是启动主流程,

这三篇对Activity启动流程分析只关心主流程,不看具体细节,当然对于一些关键方法会着重介绍,这样以后如果有遇到相关问题的修改可以通过这篇笔记找到具体代码位置,然后根据具体的问题分析修改。

后续会有在Activity启动流程基础上延伸出来的各个分支的记录,比如窗口的创建与挂载,Surface相关,窗口的动画等待,这些流程的分析都需要基于Activity启动流程。

启动Activity的方式有很多,当前以在Launch中点击“电话”图标启动应用为例。

模块框图

一级框图

activity启动堆栈--一级框图.png

对于Activity的启动流程来说,可以分为3个模块:

SourceActivity:执行startActivity方法的Activity,发起请求的Activity,当前就是Launch的Activity

TargetActivity:需要被启动的Activity,当前就是“电话”应用在清单文件配置的MainActivity

AMS: 不仅仅是指AMS这一个类,而是指处理整个启动流程的管理类。

举个例子, 以在公司的工作流程来说, launch模块的开发,在处理一个bug,但是涉及到了通话,那么他肯定是需要找到通讯组的同事来处理这个问题。 但是公司很大,他并不知道通讯模块是谁负责,更不知道这个问题需要交给通讯组具体的哪个同事处理,那么他只需要将自己的要求向公司领导(管理)汇报:需要通讯组的同事处理这个问题。

当前例子设计到launcher和通讯2个模块的开发人员,还涉及到公司的管理者。在Activity启动也是如此, 对于sourceActivity、targetActivity他们并不知道直接作用对方的行为,所以这一流程需要AMS来做管理。

并且,这3个模块也对应3个进程,当前案例来说分别为:launch进程,电话进程,system_service进程。

这里AMS对launch多了一个返回箭头的原因是因为launch肯定是需要执行pause的,但是整个启动流程非常复杂,执行pause的时机launch自身无法控制,只能由AMS控制。

二级框图

activity启动堆栈--二级框图.png

这里有3个颜色,代表3个阶段。

第一阶段:

  1. 由launcher 进程发起启动Activity的请求
  2. AMS处理,创建对应的ActivityRecord和Task,并挂载到窗口层级树中

第二阶段:

  1. AMS 触发launcher 的pause流程
  2. AMS 触发”电话”应用进程的创建
  3. launcher执行pause流程
  4. launcher执行完pause后执行调用AMS的startSpecificActivity方法启动“电话”Activity

ASM 通知 launcher 执行pause和通过 Zygote 创建进程都是异步的,不知道执行的顺序。所以launcher执行completePause后调用启动Activity时,会判断进程是否已经创建完毕。

第三阶段:

  1. “电话”进程创建完毕,通知AMS
  2. AMS触发realStartActivityLocked,通知应用启动Activity
  3. 应用进程执行Activity的启动流程,生命周期 走到onCreate和onResume

这里第二步,里面会判断launcher是否执行完pause了, 如果没有执行则直接return。

也就是说需要启动“电话”的Activity,必须有2个条件:1. 进程创建完毕 2. launcher执行完pause

在新的Activity显示前, launch肯定是要执行pause的。

  1. launch点击图标启动应用进程

这一阶段调用比较简单,堆栈如下:

activity启动堆栈--launch进程.png

其实我们正常通过 startActivity 传递 intent 启动Activity的流程也是一样的。 最终都会调到Instrumentation::execStartActivity。然后开始跨进程与AMS通信。

调用链

Activity::startActivity

1
2
3
4
arduino复制代码    Activity::startActivity
Activity::startActivityForResult
Instrumentation::execStartActivity
ActivityTaskManagerService::startActivity

主流程跟踪

在发起启动Activity的这个应用端,逻辑相对简单,无论哪种参数的 startActivity 方法,最终都是调到 startActivityForResult 方法。
然后在 Instrumentation 最后的处理,然后开始跨进程传递到 system_service 进程中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
less复制代码# Activity
public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
@Nullable Bundle options) {
if (mParent == null) {
options = transferSpringboardActivityOptions(options);
Instrumentation.ActivityResult ar =
mInstrumentation.execStartActivity(
this, mMainThread.getApplicationThread(), mToken, this,
intent, requestCode, options);
......
}
......
}
# Instrumentation
public ActivityResult execStartActivity(
Context who, IBinder contextThread, IBinder token, Activity target,
Intent intent, int requestCode, Bundle options) {
......
// 当前应用进程处理结束,开始传递给ActivityTaskManagerService
int result = ActivityTaskManager.getService().startActivity(whoThread,
who.getOpPackageName(), who.getAttributionTag(), intent,
intent.resolveTypeIfNeeded(who.getContentResolver()), token,
target != null ? target.mEmbeddedID : null, requestCode, 0, null, options);
// 对跨进程启动的结果做check
checkStartActivityResult(result, intent);
......
}

checkStartActivityResult 这个方法,比如常见的未在AndroidManifest.xml注册Activity的报错就在这。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
java复制代码# Instrumentation
public static void checkStartActivityResult(int res, Object intent) {
if (!ActivityManager.isStartResultFatalError(res)) {
return;
}

switch (res) {
case ActivityManager.START_INTENT_NOT_RESOLVED:
case ActivityManager.START_CLASS_NOT_FOUND: // 未在AndroidManifest.xml注册
if (intent instanceof Intent && ((Intent)intent).getComponent() != null)
throw new ActivityNotFoundException(
"Unable to find explicit activity class "
+ ((Intent)intent).getComponent().toShortString()
+ "; have you declared this activity in your AndroidManifest.xml"
+ ", or does your intent not match its declared <intent-filter>?");
throw new ActivityNotFoundException(
"No Activity found to handle " + intent);
case ActivityManager.START_PERMISSION_DENIED:
throw new SecurityException("Not allowed to start activity "
+ intent);
case ActivityManager.START_FORWARD_AND_REQUEST_CONFLICT:
throw new AndroidRuntimeException(
"FORWARD_RESULT_FLAG used while also requesting a result");
case ActivityManager.START_NOT_ACTIVITY:
throw new IllegalArgumentException(
"PendingIntent is not an activity");
case ActivityManager.START_NOT_VOICE_COMPATIBLE:
throw new SecurityException(
"Starting under voice control not allowed for: " + intent);
case ActivityManager.START_VOICE_NOT_ACTIVE_SESSION:
throw new IllegalStateException(
"Session calling startVoiceActivity does not match active session");
case ActivityManager.START_VOICE_HIDDEN_SESSION:
throw new IllegalStateException(
"Cannot start voice activity on a hidden session");
case ActivityManager.START_ASSISTANT_NOT_ACTIVE_SESSION:
throw new IllegalStateException(
"Session calling startAssistantActivity does not match active session");
case ActivityManager.START_ASSISTANT_HIDDEN_SESSION:
throw new IllegalStateException(
"Cannot start assistant activity on a hidden session");
case ActivityManager.START_CANCELED:
throw new AndroidRuntimeException("Activity could not be started for "
+ intent);
default:
throw new AndroidRuntimeException("Unknown error code "
+ res + " when starting " + intent);
}
}
  1. system_service 处理

调用链

1
2
3
4
5
6
7
8
9
10
11
arduino复制代码ActivityTaskManagerService::startActivity
ActivityTaskManagerService::startActivityAsUser
ActivityTaskManagerService::startActivityAsUser
ActivityStartController::obtainStarter
ActivityStarter::execute
ActivityStarter::executeRequest -- 构建 ActivityRecord --2..1 创建ActivityRecord
ActivityStarter::startActivityUnchecked
ActivityStarter::startActivityInner -- 2.2 关键函数startActivityInner
ActivityStarter::getOrCreateRootTask -- 2.2.1 创建或者拿到Task
ActivityStarter::setNewTask -- 2.2.2 将task与activityRecord 绑定
RootWindowContainer::resumeFocusedTasksTopActivities --2.2.3 显示Activity

主流程跟踪

流程来到ActivityTaskManagerService::startActivity,经过2次简单的跳转会执行 startActivityAsUser 方法。
这个方法比较重要,在这里会构建一个 ActivityStartController ,根据类名可以知道这个类是控制Activity启动。

代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
scss复制代码    private int startActivityAsUser(IApplicationThread caller, String callingPackage,
@Nullable String callingFeatureId, Intent intent, String resolvedType,
IBinder resultTo, String resultWho, int requestCode, int startFlags,
ProfilerInfo profilerInfo, Bundle bOptions, int userId, boolean validateIncomingUser) {
......
// 返回的是ActivityStartController
return getActivityStartController().obtainStarter(intent, "startActivityAsUser")
.setCaller(caller)
.setCallingPackage(callingPackage)
.setCallingFeatureId(callingFeatureId)
.setResolvedType(resolvedType)
.setResultTo(resultTo)
.setResultWho(resultWho)
.setRequestCode(requestCode)
.setStartFlags(startFlags)
.setProfilerInfo(profilerInfo)
.setActivityOptions(bOptions)
.setUserId(userId)
.execute();
}

ActivityStartController getActivityStartController() {
return mActivityStartController;
}

ActivityStartController::obtainStarter返回的是ActivityStarter对象

1
2
3
4
scss复制代码# ActivityStartController
ActivityStarter obtainStarter(Intent intent, String reason) {
return mFactory.obtain().setIntent(intent).setReason(reason);
}

所以在ActivityTaskManagerService::startActivityAsUser方法中的build模式,其实是对 ActivityStarter 对象做构建。最终调用其 execute 方法。
然后调用 executeRequest 方法。

1
2
3
4
5
6
7
csharp复制代码# ActivityStarter

int execute() {
......
res = executeRequest(mRequest);
......
}

2.1创建ActivityRecord

ActivityStarter::executeRequest是一个需要注意的方法,因为内部会创建 ActivityRecord 对象,而这个 ActivityRecord 对象持有 token ,这个token就是以后分析其他逻辑一直会出现的token。

应用进程中的Activity在AMS的代表就是ActivityRecord

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
scss复制代码# ActivityStarter
private int executeRequest(Request request) {
......
final ActivityRecord r = new ActivityRecord.Builder(mService)
.setCaller(callerApp)
.setLaunchedFromPid(callingPid)
.setLaunchedFromUid(callingUid)
.setLaunchedFromPackage(callingPackage)
.setLaunchedFromFeature(callingFeatureId)
.setIntent(intent)
.setResolvedType(resolvedType)
.setActivityInfo(aInfo)
.setConfiguration(mService.getGlobalConfiguration())
.setResultTo(resultRecord)
.setResultWho(resultWho)
.setRequestCode(requestCode)
.setComponentSpecified(request.componentSpecified)
.setRootVoiceInteraction(voiceSession != null)
.setActivityOptions(checkedOptions)
.setSourceRecord(sourceRecord)
.build();
......
// 继续执行startActivityUnchecked
mLastStartActivityResult = startActivityUnchecked(r, sourceRecord, voiceSession,
request.voiceInteractor, startFlags, true /* doResume */, checkedOptions,
inTask, inTaskFragment, restrictedBgActivity, intentGrants);
......
}

而 startActivityUnchecked 主要调用的是 startActivityInner , 这个方法是流程的关键点。

2.2 关键函数startActivityInner

这个函数是AMS在Activity启动流程最重要的函数之一,这里涉及到【层级结构树】相关,先看看正常在launch的和启动“电话”后的层级树对比。

前后层级树对比.png

这里首先多了3个东西:1个Task,1个上一步创建的ActivityRecord,还有一个就是WindowState。

另外这个Task还移动到了DefaultTaskDisplayArea的最顶部。这里涉及到的操作如下:

  1. 创建Task
  2. ActivityRecord挂在到这个Task下
  3. 将这个Task移动到最上面

至于最下面的那个 546fce2 为什么是WindowState对象,又是怎么挂在到ActivityRecord上的,这个在【应用的addWindow流程】详细说明了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
java复制代码# ActivityStarter
int startActivityInner(final ActivityRecord r, ActivityRecord sourceRecord,
IVoiceInteractionSession voiceSession, IVoiceInteractor voiceInteractor,
int startFlags, boolean doResume, ActivityOptions options, Task inTask,
TaskFragment inTaskFragment, boolean restrictedBgActivity,
NeededUriGrants intentGrants) {
......
/ computeTargetTask内部会根据具体条件返回Task(比如标志位FLAG_ACTIVITY_NEW_TASK姐需要重新创建Task )
// 这里reusedTask为 null,因为是新启动的应用,所以computeTargetTask也找不到task,最终也为null
/
final Task targetTask = reusedTask != null ? reusedTask : computeTargetTask();
// 那么newTask为true, 表示需要新建一个task
final boolean newTask = targetTask == null;
// 同样为null
mTargetTask = targetTask;
......
if (mTargetRootTask == null) {
// 重点* 1. 创建Task 23
mTargetRootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, targetTask,
mOptions);
}
if (newTask) {
// taskToAffiliate 为null
final Task taskToAffiliate = (mLaunchTaskBehind && mSourceRecord != null)
? mSourceRecord.getTask() : null;
// 重点* 2. 将需要启动的ActivityRecord与 新创建的Task 进行绑定
setNewTask(taskToAffiliate);
} else if (mAddingToTask) {
addOrReparentStartingActivity(targetTask, "adding to task");
}

if (!mAvoidMoveToFront && mDoResume) {
// 移动到栈顶
mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask);
......
}
......
if (mDoResume) {
......
// 重点*3. task 处理完后,需要将task顶部的Activity显示(resume)
mRootWindowContainer.resumeFocusedTasksTopActivities(
mTargetRootTask, mStartActivity, mOptions, mTransientLaunch);
}
......
}

具体原因在代码加了注释,那么第一步就是需要创建一个Task了。

2.2.1 创建Task getOrCreateRootTask

调用链

1
2
3
4
5
6
arduino复制代码ActivityStarter::getOrCreateRootTask
RootWindowContainer::getOrCreateRootTask
RootWindowContainer::getOrCreateRootTask
TaskDisplayArea::getOrCreateRootTask
TaskDisplayArea::getOrCreateRootTask
Task::Build ---创建Task

主流程代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
less复制代码# ActivityStarter
private Task getOrCreateRootTask(ActivityRecord r, int launchFlags, Task task,
ActivityOptions aOptions) {
final boolean onTop =
(aOptions == null || !aOptions.getAvoidMoveToFront()) && !mLaunchTaskBehind;
final Task sourceTask = mSourceRecord != null ? mSourceRecord.getTask() : null;
return mRootWindowContainer.getOrCreateRootTask(r, aOptions, task, sourceTask, onTop,
mLaunchParams, launchFlags);
}
// onTop 表示是否要移到到当前栈顶,那肯定是要的,新启动的Activity当前要再最上面,这里 aOptions 为null,所以为true
// sourceTask 表示从哪里启动的,当前launch所在的Task 就是sourceTask

# RootWindowContainer
Task getOrCreateRootTask(@Nullable ActivityRecord r, @Nullable ActivityOptions options,
@Nullable Task candidateTask, boolean onTop) {
return getOrCreateRootTask(r, options, candidateTask, null /* sourceTask */, onTop,
null /* launchParams */, 0 /* launchFlags */);
}
Task getOrCreateRootTask(@Nullable ActivityRecord r,
@Nullable ActivityOptions options, @Nullable Task candidateTask,
@Nullable Task sourceTask, boolean onTop,
@Nullable LaunchParamsController.LaunchParams launchParams, int launchFlags) {
......
final int activityType = resolveActivityType(r, options, candidateTask);
if (taskDisplayArea != null) {
if (canLaunchOnDisplay(r, taskDisplayArea.getDisplayId())) {
// 重点*1. 传递到TaskDisplayArea
return taskDisplayArea.getOrCreateRootTask(r, options, candidateTask,
sourceTask, launchParams, launchFlags, activityType, onTop);
} else {
taskDisplayArea = null;
}
}
......
}
// 经过同名调用后,逻辑进入到 TaskDisplayArea

# TaskDisplayArea
Task getOrCreateRootTask(int windowingMode, int activityType, boolean onTop,
@Nullable Task candidateTask, @Nullable Task sourceTask,
@Nullable ActivityOptions options, int launchFlags) {
if(....) {
// 拿到之前创建的Task
return candidateTask.getRootTask();
}
......// 第一次显示所以是新建Task
return new Task.Builder(mAtmService)
.setWindowingMode(windowingMode)
.setActivityType(activityType)
.setOnTop(onTop)
.setParent(this) // 主要这个this被设置为Parent。所以直接挂载到了DefaultTaskDisplayArea下
.setSourceTask(sourceTask)
.setActivityOptions(options)
.setLaunchFlags(launchFlags)
.build();
}
// 看方法名是获取或创建Task, 这边是新启动的Activity所以需要创建Task。如果是以默认启动方式打开应用内的另一个Activity,就走的是上面的 return candidateTask.getRootTask();
接下来就是真正触发Task的创建。
// 另外设置的parent就是层级结构树应用所在的名为“DefaultTaskDisplayArea”的TaskDisplayArea
# Task
# Task.Builder
Task build() {
if (mParent != null && mParent instanceof TaskDisplayArea) {
validateRootTask((TaskDisplayArea) mParent);
}

if (mActivityInfo == null) {
mActivityInfo = new ActivityInfo();
mActivityInfo.applicationInfo = new ApplicationInfo();
}

mUserId = UserHandle.getUserId(mActivityInfo.applicationInfo.uid);
mTaskAffiliation = mTaskId;
mLastTimeMoved = System.currentTimeMillis();
mNeverRelinquishIdentity = true;
mCallingUid = mActivityInfo.applicationInfo.uid;
mCallingPackage = mActivityInfo.packageName;
mResizeMode = mActivityInfo.resizeMode;
mSupportsPictureInPicture = mActivityInfo.supportsPictureInPicture();
if (mActivityOptions != null) {
mRemoveWithTaskOrganizer = mActivityOptions.getRemoveWithTaskOranizer();
}
// 重点* 1. 创建task
final Task task = buildInner();
task.mHasBeenVisible = mHasBeenVisible;

// Set activity type before adding the root task to TaskDisplayArea, so home task can
// be cached, see TaskDisplayArea#addRootTaskReferenceIfNeeded().
if (mActivityType != ACTIVITY_TYPE_UNDEFINED) {
task.setActivityType(mActivityType);
}
// 重点* 2. 入栈 这里的 mOnTop为true
if (mParent != null) {
if (mParent instanceof Task) {
final Task parentTask = (Task) mParent;
parentTask.addChild(task, mOnTop ? POSITION_TOP : POSITION_BOTTOM,
(mActivityInfo.flags & FLAG_SHOW_FOR_ALL_USERS) != 0);
} else {
mParent.addChild(task, mOnTop ? POSITION_TOP : POSITION_BOTTOM);
}
}

// Set windowing mode after attached to display area or it abort silently.
if (mWindowingMode != WINDOWING_MODE_UNDEFINED) {
task.setWindowingMode(mWindowingMode, true /* creating */);
}
// 返回
return task;
}

// 创建
Task buildInner() {
return new Task(mAtmService, mTaskId, mIntent, mAffinityIntent, mAffinity,
mRootAffinity, mRealActivity, mOrigActivity, mRootWasReset, mAutoRemoveRecents,
mAskedCompatMode, mUserId, mEffectiveUid, mLastDescription, mLastTimeMoved,
mNeverRelinquishIdentity, mLastTaskDescription, mLastSnapshotData,
mTaskAffiliation, mPrevAffiliateTaskId, mNextAffiliateTaskId, mCallingUid,
mCallingPackage, mCallingFeatureId, mResizeMode, mSupportsPictureInPicture,
mRealActivitySuspended, mUserSetupComplete, mMinWidth, mMinHeight,
mActivityInfo, mVoiceSession, mVoiceInteractor, mCreatedByOrganizer,
mLaunchCookie, mDeferTaskAppear, mRemoveWithTaskOrganizer);
}

小结:

最后描述一下最后创建的2个重点部分:

  1. 看到通过buildInner 创建了一个task,而buildInner 也很简单粗暴,通过各个变量直接new Task 对象。
  2. mParent 不为null, 是 因为在创建的时候 setParent(this),当前的这个this,就是 getDefaultTaskDisplayArea返回的。就是 37层的第二层应用Activity存在的”DefaultTaskDisplayArea”。

在 RootWindowContainer::getOrCreateRootTask 体现。

创建Task-getOrCreateRootTask.png

注意log里的 #17 的这个Task,与前面的层级结构树新增的Task,是对应的上的。而且this= DefaultTaskDisplayArea 说明也确实是往DefaultTaskDisplayArea里添加了。
另外 log里index为3,结合最上面的前后对比,说明也的往顶部添加。

2.2.2 将task与activityRecord setNewTask

调用链

1
2
arduino复制代码    ActivityStarer::setNewTask
ActivityStarer::addOrReparentStartingActivity

主流程代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
java复制代码# ActivityStarer
private void setNewTask(Task taskToAffiliate) {
// 为true
final boolean toTop = !mLaunchTaskBehind && !mAvoidMoveToFront;
// 就是mTargetRootTask,也就是刚刚创建的Task
final Task task = mTargetRootTask.reuseOrCreateTask(
mNewTaskInfo != null ? mNewTaskInfo : mStartActivity.info,
mNewTaskIntent != null ? mNewTaskIntent : mIntent, mVoiceSession,
mVoiceInteractor, toTop, mStartActivity, mSourceRecord, mOptions);
task.mTransitionController.collectExistenceChange(task);
// ActivityRecord的挂载
addOrReparentStartingActivity(task, "setTaskFromReuseOrCreateNewTask");
// 需要注意这里的日志打印
ProtoLog.v(WM_DEBUG_TASKS, "Starting new activity %s in new task %s",
mStartActivity, mStartActivity.getTask());

// mLaunchTaskBehind 为false,所以taskToAffiliate 为null
if (taskToAffiliate != null) {
mStartActivity.setTaskToAffiliateWith(taskToAffiliate);
}
}

这里的task 和mTargetRootTask是同一个对象, 进源码跟到流程也是一样。
然后进入 addOrReparentStartingActivity

1
2
3
4
5
6
7
8
9
10
11
12
13
less复制代码# ActivityStarer
private void addOrReparentStartingActivity(@NonNull Task task, String reason) {
// newParent = task 都是刚刚创建的Task
TaskFragment newParent = task;
......
if (mStartActivity.getTaskFragment() == null
|| mStartActivity.getTaskFragment() == newParent) {
// 重点, 将 ActivityRecord挂在到新创建的Task中,并且是顶部
newParent.addChild(mStartActivity, POSITION_TOP);
} else {
mStartActivity.reparent(newParent, newParent.getChildCount() /* top */, reason);
}
}

这里的逻辑设计到的Task就是上一步创建的Task,mStartActivity则是“电话”在之前逻辑创建的ActivityRecord。

setNewTask的堆栈信息如下

AcvtivityRecord挂在到Task.png

另外这段逻辑里有个ProtoLog打印,日志如下:

setNewTask的ProtoLog.png

小结:

结合逻辑分析+堆栈信息+ProtoLog,可以确认setNewTask做的事情就是将ActivityRecord挂在到Task中,而且在顶部

2.2.2.3 移动Task到容器顶部 moveToFront

这里提一下这个moveToFront方法,因为前面创建Task并添加到 DefaultTaskDisplayArea 时是往顶部添加,后面将ActivityRecord挂在到Task,也是挂在到其顶部。所以这个函数其实没有什么实际操作。但是对于其他场景,这里也是一个重点方法。

调用链

1
2
3
4
5
6
arduino复制代码Task::moveToFront                      -- 2.1.3 移动Task
Task::moveToFrontInner
TaskDisplayArea::positionChildAt
TaskDisplayArea::positionChildTaskAt
ActivityTaskSupervisor::updateTopResumedActivityIfNeeded
ActivityRecord::onTopResumedActivityChanged --触发TopResumedActivityChangeItem

主流程代码

首先确定一个问题。 需要移动到顶部的是哪个task? 这个task所在的是在哪个task? 这里设计到了2个task
在ActivityStarter::startActivityInner的时候调用的是这段代码

1
scss复制代码mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask);

已知mTargetRootTask是新创建给“电话”用的Task, mTargetRootTask.getRootTask()肯定就是还是本身。

tip :getRootTask返回的是顶部的Task, 当天Task上一层是TaskDisplayArea类型 (name为DefaultTaskDisplayArea)
而mTargetRootTask.getParent返回的父容器,则是 name为DefaultTaskDisplayArea的TaskDisplayArea。
具体不深入,以主流程为主.。

到这里,AMS已经将需要的ActivityRecord和Task创建并且挂载到层级树中接下来将是需要处理新的Activity启动和显示逻辑了

2.2.3 显示Activity resumeFocusedTasksTopActivities

篇幅原因,后续流程在下一篇

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

如果iconfont停止服务了,我们怎么办

发表于 2024-02-28

世界最大的无辐摩天轮.webp

前言

个人一直都比较喜欢阿里大佬提供的一些组件服务什么的,这里就只说图标管理吧,最开始的时候我是使用的icomoon.io 后面发现www.iconfont.cn/ 在以前开发是时候我们为了图片的请求少点,提升网站的性能会把这些小图标做到一张图上面专业叫法是雪碧图,后来随着前端发展有了字体图标,不啰嗦了吧,回到正题,由于上次icofont官网挂了之后,导致我的项目无法进行正常迭代,我就决定要自己弄个简单的图标管理。

需求

一般使用需要的是把设计做好的图标或者其他地方下载的svg图标上传到服务器然后可以查看,并且打包成一个js文件加载到项目中。

准备

都说天下文章一大抄,我也是去抄iconfont,我把iconfont的使用demo打开研究了一下。

image.png

image.png

我这里只说Symbol,通过分析看到就是我们需要在项目中引入./iconfont.js

image.png

iconfont把我们上传的图标进行了删减优化,最外层就一个svg标签里面使用了一个symbol标签来包裹之前的图标内容,每一个symbol的id都对应之前的svg图标名称。

iconfont也没有开源,具体怎么做的咱也不知道,只能知道目前这些信息了,然后就按照自己理解开始开发实现了

前端开发

通过上面我们可以知道,现在我们前端需要的是把设计稿的svg图变成symbol里面的内容,打开svg图
可以简单测试下,直接把内容复制然后粘贴带了iconfont.js中,然后运行demo图标正常显示就说明是ok的,不然就是这样做是不行的。
image.png

通过图片我们可以看到,现在需要的就是把svg里面的path改成用symblo来包裹,在开发中svg图我们是通过文档流上传上来的,这个时候我们就需要对文档流进行操作,先把文档流变成字符串,然后js操作字符串拼接达到目的。

  1. 使用到FileReader和readAsText获取到字符串
1
2
3
4
5
6
7
8
9
js复制代码 const fileParse = (file) => {
return new Promise((resolve) => {
const fileReader = new FileReader();
fileReader.readAsText(file, "UTF-8");
fileReader.onload = (e) => {
resolve({ content: e.target?.result, name: file.name })
}
})
}
  1. 字符操作拼接我使用的是cheerio
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
js复制代码  const handleUploadSvg = ($, result) => {
let index = result.indexOf('<svg');
const str = result.slice(index);
const svgNode = $(str);

$('svg').attr('viewBox', svgNode.attr('viewBox'));
const findPath = svgNode.find('path');
if (!findPath.length) {
message.error('图标错误,不存在path')
return '';
};
findPath.each((i, el) => {
$(el).removeAttr('id');
});
const gDom = svgNode.find('g');

gDom.each((i, el) => {
const path = $(el).children('path');
const fill = $(el).attr('fill');
if (fill && fill !== 'none' && path.length && !$(path[0])?.attr?.('fill')) {
$(path[0])?.attr?.('fill', fill)
}
});
removeArrtId($, gDom);

if (gDom.length) {
$('svg').html(svgNode.html());
} else {
$('svg').html(svgNode.find('path'));
}

return $.html("svg")
}

通过上面的操作我们可以得到一个新的svg源码,图标显示还是flow

image.png

然后我把这个字符串传送给后端就行了

后端开发

后端要做的就是把我上传上来的svg字符串拼接到一起,然后以iconfont.js一样通过一个get接口返回给我就可以了,
通过浏览器可以访问到就说明ok了

image.png

其他两种需要用到些第三方库当时也有顺便研究看到了,如果有需要我再写一篇。

文章中会有些不怎么清晰或者有问题的地方还望各位大佬见谅,刚刚开始决定写一些技术相关文章,还很生疏

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

零基础入门AI:一键本地运行各种开源大语言模型 - Olla

发表于 2024-02-28

什么是 Ollama?

Ollama 是一个可以在本地部署和管理开源大语言模型的框架,由于它极大的简化了开源大语言模型的安装和配置细节,一经推出就广受好评,目前已在github上获得了46k star。

不管是著名的羊驼系列,还是最新的AI新贵Mistral,等等各种开源大语言模型,都可以用Ollama实现一键安装并运行,支持的更多模型的列表可以查看Ollama官网。

Model Parameters Size Download
Llama 2 7B 3.8GB ollama run llama2
Mistral 7B 4.1GB ollama run mistral

本文就让我们一起入门Ollama。

如何安装 Ollama框架?

Ollama支持各个平台:Mac、Windows 和 Linux,也提供了docker image。
在Ollama官网或者Github可以下载,然后一键安装Ollama框架:

  • macOS
  • Windows
  • Linux: curl -fsSL https://ollama.com/install.sh | sh

由于Ollama刚支持windows不久,在windows上的相关配置还不够完善,以下我将主要以Linux上运行Ollama来举例说明。

运行 Ollama 服务

在Ollama安装完成后, 一般会自动启动 Ollama 服务,而且会自动设置为开机自启动。安装完成后,可以使用如下命令查看是否Ollama是否正常启动。如下例子中显示“Active: active (running)”表示Ollama已经正常启动。

1
2
3
4
5
6
7
8
9
10
11
12
yaml复制代码$ systemctl status ollama
● ollama.service - Ollama Service
Loaded: loaded (/etc/systemd/system/ollama.service; enabled; vendor preset: enabled)
Drop-In: /etc/systemd/system/ollama.service.d
└─environment.conf
Active: active (running) since Thu 2024-03-07 09:09:39 HKT; 4 days ago
Main PID: 19975 (ollama)
Tasks: 29 (limit: 69456)
Memory: 1.1G
CPU: 14min 44.702s
CGroup: /system.slice/ollama.service
└─19975 /usr/local/bin/ollama serve

在Linux上,如果Ollama未启动,可以用如下命令启动 Ollama 服务:ollama serve,或者 sudo systemctl start ollama。

通过分析Linux的安装脚本install.sh,就会看到其中已经将ollama serve配置为一个系统服务,所以可以使用systemctl来 start / stop ollama进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
ini复制代码    status "Creating ollama systemd service..."
cat <<EOF | $SUDO tee /etc/systemd/system/ollama.service >/dev/null
[Unit]
Description=Ollama Service
After=network-online.target

[Service]
ExecStart=$BINDIR/ollama serve
User=ollama
Group=ollama
Restart=always
RestartSec=3
Environment="PATH=$PATH"

启动Ollama服务后,可以查看当前的Ollama版本,以及常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
sql复制代码~$ ollama -v
ollama version is 0.1.20
~$ ollama --help
Large language model runner

Usage:
ollama [flags]
ollama [command]

Available Commands:
serve Start ollama
create Create a model from a Modelfile
show Show information for a model
run Run a model
pull Pull a model from a registry
push Push a model to a registry
list List models
cp Copy a model
rm Remove a model
help Help about any command

Flags:
-h, --help help for ollama
-v, --version Show version information

Use "ollama [command] --help" for more information about a command.

如何下载并运行大语言模型?

至此,已经完成Ollama框架的安装,接下来,可以用一条命令在本地运行大语言模型。以著名的羊驼举例:ollama run llama2。

如果还没有下载过指定的大语言模型,这条命令将会先执行ollama pull llama2,将大语言模型下载到本地,再在本地运行大语言模型。

下载完成后,运行效果如下:

1
2
3
4
5
6
7
8
less复制代码:~$ ollama run llama2
>>> who are you?

I am LLaMA, an AI assistant developed by Meta AI that can understand and respond to human input in a conversational manner. I am trained on a massive dataset of text from the internet and can
generate human-like responses to a wide range of topics and questions. I can be used to create chatbots, virtual assistants, and other applications that require natural language understanding and
generation capabilities.

>>> Send a message (/? for help)

REST API

Ollama还提供了API接口:

1
2
3
4
5
vbnet复制代码curl http://localhost:11434/api/generate -d '{
"model": "llama2",
"prompt":"Why is the sky blue?",
"stream": false
}'

返回结果如下:

1
2
3
4
5
6
7
8
9
10
11
json复制代码{
"model": "llama2",
"created_at": "2024-02-26T04:35:10.787352404Z",
"response": "The sky appears blue because of a phenomenon called Rayleigh scattering, which occurs when sunlight enters Earth's atmosphere. The sunlight encounters tiny molecules of gases such as nitrogen and oxygen, which scatter the light in all directions. The shorter wavelengths of light, such as blue and violet, are scattered more than the longer wavelengths, such as red and orange. This is known as Rayleigh scattering, named after Lord Rayleigh, who first described the phenomenon in the late 19th century. As a result of this scattering, the light that reaches our eyes from the sun appears blue, especially when viewed from a distance. The closer we get to the horizon, the more the blue color appears to fade, as the light has to travel through more of the atmosphere, which scatters the shorter wavelengths even more. It's worth noting that the exact shade of blue can vary depending on the time of day and atmospheric conditions. For example, during sunrise and sunset, when the sun is low in the sky, the sky can take on a more orange or red hue due to the scattering of light by atmospheric particles. So, to summarize, the sky appears blue because of the way light interacts with the tiny molecules of gases in Earth's atmosphere, particularly nitrogen and oxygen.",
"done": true,
"total_duration": 7001870820,
"load_duration": 4930376,
"prompt_eval_duration": 60907000,
"eval_count": 309,
"eval_duration": 6931593000
}

使用API接口,就可以实现更多灵活的功能,比如与IDE插件配合,实现本地的编程助手,可参考如下文章:
零基础入门AI:搭建本地的编程助手

FAQ

如何查看运行的日志?

在Linux上运行命令journalctl -u ollama,即可查看运行日志。

如何配置本地大模型对局域网提供服务?

在Linux上创建如下配置文件,并配置环境变量 OLLAMA_HOST 来指定对局域网提供服务的地址,再重启Ollama服务即可。

1
2
3
ini复制代码:~$ cat /etc/systemd/system/ollama.service.d/environment.conf
[Service]
Environment=OLLAMA_HOST=0.0.0.0:11434

如此配置后,即可由一台GPU服务器为本地局域网提供大语言模型的服务。

本地有多张GPU,如何用指定的GPU来运行Ollama?

在Linux上创建如下配置文件,并配置环境变量 CUDA_VISIBLE_DEVICES 来指定运行Ollama的GPU,再重启Ollama服务即可。

1
2
3
ini复制代码:~$ cat /etc/systemd/system/ollama.service.d/environment.conf
[Service]
Environment=CUDA_VISIBLE_DEVICES=1,2

下载的大模型存储在哪个路径?

默认情况下,不同操作系统存储的路径如下:

  • macOS: ~/.ollama/models
  • Linux: /usr/share/ollama/.ollama/models
  • Windows: C:\Users<username>.ollama\models

如何修改大模型存储的路径?

Linux平台安装Ollama时,默认安装时会创建用户ollama,再将模型文件存储到该用户的目录/usr/share/ollama/.ollama/models。但由于大模型文件往往特别大,有时需要将大模型文件存储到专门的数据盘,此时就需要修改大模型文件的存储路径。

官方提供的方法是设置环境变量“OLLAMA_MODELS”,但我在Linux上尝试后,并没有成功。

分析Linux版的安装脚本install.sh后,我发现是由于其中创建了用户ollama和用户组ollama,然后将大模型存储到了该用户的目录/usr/share/ollama/.ollama/models,而我的帐户对ollama帐户的一些操作并不能生效,即使我再手动将我的帐户添加进ollama用户组,也仍然会有一些权限问题,导致对ollama帐户的目录操作不生效。

由于新建的ollama帐户并没有给我带来额外的便利,最后我用以下步骤来实现修改大模型文件的存储路径:

  1. 修改安装文件 install.sh,取消其中创建用户ollama的步骤,参考如下:
1
2
3
4
5
6
shell复制代码# if ! id ollama >/dev/null 2>&1; then 
# status "Creating ollama user..."
# $SUDO useradd -r -s /bin/false -m -d /usr/share/ollama ollama
# fi
# status "Adding current user to ollama group..."
# $SUDO usermod -a -G ollama $(whoami)
  1. 修改安装文件 install.sh,使用我的帐户来启动ollama服务,参考如下:
1
2
3
4
5
6
7
8
9
10
ini复制代码    status "Creating ollama systemd service..."
cat <<EOF | $SUDO tee /etc/systemd/system/ollama.service >/dev/null
[Unit]
Description=Ollama Service
After=network-online.target

[Service]
ExecStart=$BINDIR/ollama serve
User=<myusername>
Group=<myusername>
  1. 修改安装文件 install.sh,添加如下配置中指定环境变量OLLAMA_MODELS指定存储路径,再用此安装文件来安装ollama。
1
ini复制代码Environment="OLLAMA_MODELS=/home/paco/lab/LLM/ollama/OLLAMA_MODELS"

或者在安装完成后,创建如下配置文件,并配置环境变量OLLAMA_MODELS来指定存储路径,再重启Ollama服务。

1
2
3
ini复制代码:~$ cat /etc/systemd/system/ollama.service.d/environment.conf
[Service]
Environment=OLLAMA_MODELS=<path>/OLLAMA_MODELS

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

工作三年,为什么你还不会排查堆外内存泄漏?(下) 一 排查

发表于 2024-02-28

image.png

👈👈👈 欢迎点赞收藏关注哟

本文是Java故障案例分析的第三篇, 上一篇中我们分析了一个堆外内存泄漏问题: 工作三年,为什么你还不会排查堆外内存泄漏?(上)(文末附赠一个泄漏排查工具) - 掘金 (juejin.cn)(这篇文章里是使用NMT+PMAP解决的非Netty造成的内存泄漏), 而今天我们要聊的是另外一个非常令人头疼的问题——Netty堆外内存泄漏。本文将带领大家跟我一起排查线上一个真实案例, 并且深入了解Netty堆外内存泄漏的根本原因,最后提供了解决这个问题的三个关键步骤。通过这篇文章,你将获得对堆外内存泄漏排查的深刻理解,并学会如何有效地预防和解决可能的泄漏问题.

结合上一篇文章, 如果面试官再问你堆外内存泄漏排查思路你应该知道怎么回答了吧😌。

如果不想看全文也没关系哦, 可以直接跳到结尾部分, 我帮你总结好了🕶.

一. 排查过程

起因是半夜接到机器内存不足告警电话, 机器宕机😣,于是爬起来解决问题,下面是排查过程.

image.png

1.1 初步定位

初步发现是我们监控系统所在机器内存不足,上机器排查,发现日志中有大量Netty内存泄漏日志:

1
vbnet复制代码LEAK: ByteBuf.release() was not called before it's garbage-collected. Enable advanced leak reporting to find out where the leak occurred. To enable advanced leak reporting, specify the JVM option '-Dio.netty.leakDetectionLevel=advanced' or call ResourceLeakDetector.setLevel()

通过查阅相关文档和源码,当Netty分配的ByteBuf在GC前没有调用release方法就会记录这条泄漏日志,说明是在哪里没有释放,但是单凭这条记录我们无法判断是哪里出了问题,我们要确定两个问题🤔:

1、这些泄漏的Buffer是什么,是在哪里分配的?

2、这个Buffer最后是谁持有的?

按照上面Netty给我们的提示,我们可以设置-Dio.netty.leakDetectionLevel=advanced参数,Netty默认的io.netty.leakDetectionLevel参数是simple,只能输出一条简单的泄漏记录,而要输出更加详细的,需要调整level参数,因此这里我们按照提示设置为advanced上线后同时配置告警继续观察。

1.2 泄漏堆栈分析

果不其然,过了一段时间后,日志里又出现了泄漏日志,而且这次更加详细,包含了堆栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
less复制代码[ERROR] epollEventLoopGroup-3-20 - io.netty.util.ResourceLeakDetector : LEAK: ByteBuf.release() was not called before it's garbage-collected. See https://netty.io/wiki/reference-counted-objects.html for more information.
Recent access records:
#1:
io.netty.buffer.AdvancedLeakAwareByteBuf.readByte(AdvancedLeakAwareByteBuf.java:401)
com.dianping.cat.message.spi.codec.PlainTextMessageCodec$BufferHelper.read(PlainTextMessageCodec.java:485)
com.dianping.cat.message.spi.codec.PlainTextMessageCodec.decodeLine(PlainTextMessageCodec.java:184)
// ..省略部分输出
io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:480)
io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:378)
io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
java.lang.Thread.run(Thread.java:745)
#2:
io.netty.buffer.AdvancedLeakAwareByteBuf.readByte(AdvancedLeakAwareByteBuf.java:401)
// ..省略部分输出
Created at:
io.netty.buffer.PooledByteBufAllocator.newDirectBuffer(PooledByteBufAllocator.java:402)
io.netty.buffer.AbstractByteBufAllocator.directBuffer(AbstractByteBufAllocator.java:188)
io.netty.buffer.AbstractByteBufAllocator.buffer(AbstractByteBufAllocator.java:124)
io.netty.buffer.AbstractByteBuf.readBytes(AbstractByteBuf.java:871)
com.dianping.cat.analysis.TcpSocketReceiver$MessageDecoder.decode(TcpSocketReceiver.java:155)
io.netty.handler.codec.ByteToMessageDecoder.decodeRemovalReentryProtection(ByteToMessageDecoder.java:507)
io.netty.handler.codec.ByteToMessageDecoder.callDecode(ByteToMessageDecoder.java:446)
io.netty.handler.codec.ByteToMessageDecoder.channelRead(ByteToMessageDecoder.java:276)
// ..省略部分输出
io.netty.channel.epoll.EpollEventLoop.processReady(EpollEventLoop.java:480)
io.netty.channel.epoll.EpollEventLoop.run(EpollEventLoop.java:378)
io.netty.util.concurrent.SingleThreadEventExecutor$4.run(SingleThreadEventExecutor.java:986)
io.netty.util.internal.ThreadExecutorMap$2.run(ThreadExecutorMap.java:74)
io.netty.util.concurrent.FastThreadLocalRunnable.run(FastThreadLocalRunnable.java:30)
java.lang.Thread.run(Thread.java:745)
: 3 leak records were discarded because they were duplicates
: 337 leak records were discarded because the leak record count is targeted to 4. Use system property io.netty.leakDetection.targetRecords to increase the limit.

可以看到这些堆栈除了最近的访问记录,还有在哪里创建的,通过分析这下我们知道了:

1、这些Buffer是用户Java应用上报的埋点消息。

2、最后一次访问Buffer是在反序列化Buffer数据的时候。

这里简单介绍下我们的监控系统处理用户埋点的大致流程:

image.png

Netty记录的“泄漏记录”是我们通过ByteBuf的read/write等操作记录的, 比如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java复制代码@Override
public ByteBuf readBytes(ByteBuf dst, int dstIndex, int length) {
recordLeakNonRefCountingOperation(leak); // 这个方法记录当前堆栈
return super.readBytes(dst, dstIndex, length);
}

@Override
public ByteBuf readBytes(byte[] dst) {
recordLeakNonRefCountingOperation(leak);
return super.readBytes(dst);
}

@Override
public ByteBuf readBytes(byte[] dst, int dstIndex, int length) {
recordLeakNonRefCountingOperation(leak);
return super.readBytes(dst, dstIndex, length);
}

@Override
public ByteBuf readBytes(ByteBuffer dst) {
recordLeakNonRefCountingOperation(leak);
return super.readBytes(dst);
}

但是根据我们的上述的流程,解析ByteBuf之后我们会送到另外一个线程异步存储,但是Netty告诉我们最后一次访问还是在反序列化的时候!所以泄漏一定发生在存储之前,反序列化之后!(可能是在异步线程处理发生了某些异常没有catch导致ByteBuf没有被释放).

好,缩小范围之后该怎么确定精确位置进行排查呢?

1.3 精准定位技巧:ByteBuf.touch()

这就有请ByteBuf提供的另外一个方法:touch()了:

1
java复制代码public abstract ByteBuf touch();

在touch中Netty也会和read/write一样记录当前的堆栈,我们只需要在可能发生泄漏的地方加入touch就能在Netty的泄漏日志中看到这个ByteBuf最后是在哪里引用的, 就能解决了!问题解决了吗? 如解!

实际分析代码路径后,我们发现一个方法很可疑,但是这个方法(方法名就叫method吧)比较复杂,我们需要加入多个touch,但这样就出现一个问题:最终在Netty日志里显示的堆栈顶层都是这个方法,我们无法知道是在这个方法的哪一个touch输出的,该怎么区分呢?

发现Netty还提供了一个可以带参数的API,这个API是什么呢?

1
java复制代码public abstract ByteBuf touch(Object hint);

通过查阅文档和源码:如果我们使用这个方法,就能在Netty输出的日志中带上这个hint.toString()返回的字符串,比如我们调用buffer.touch(-5), 那么Netty输出的日志:

image.png

我们在method方法里的关键地方都加上touch调用,并且每个地方的hint都不一样,最后终于定位到了原因!

下面是原因的大致描述:

监控系统每个小时会停止异步线程(通过设置线程相关状态为disable)和队列,创建新的异步线程和队列,但由于我们在放入队列时没有判断异步任务线程状态,每个小时初放入队列的ByteBuf没有被异步线程处理,最后造成内存泄漏!

这次给了我们一个不小的教训, 那么为了防止下次再出现内存泄漏, 我后面给出了一些建议和最佳实践.

image.png

二. 如何更好的避免Netty内存泄漏?

这一节我们主要讲如何通过正确的编码避免Netty内存泄漏, 但我们首先要弄清楚Netty堆外内存为什么会泄漏, 了解它的底层原理, 然后通过一些工程中的最佳实践来降低泄漏的可能性.

2.1 Netty堆外内存为什么会泄漏?

Netty创建的堆外内存基于JDK原生的DirectByteBuffer实现,在创建DirectByteBuffer时能够指定是否使用带Cleaner的构造器,指定Cleaner的话能在自身被回收时释放内存,因为其在创建过程中通过创建Cleaner对象——一个虚引用对象PhantomReference,在DirectByteBuffer被GC时,ReferenceHandler线程会对jdk.internal.ref.Cleaner对象专门处理,调用它的clean方法释放内存。

但是Netty创建的堆外内存使用的是noCleaner策略,即创建DirectByteBuffer通过反射调用不含Cleaner的构造器,这么做的主要原因是:

  • 要触发Cleaner的触发必须要等Buffer被GC,这个是不可控的.
  • 在使用Cleaner的构造器里,如果内存不足,会调用System.gc()方法主动触发GC,这种方式会造成性能问题.

因此在大部分情况下(至少在OpenJDK里)Netty会使用noCleaner策略创建DirectByteBuffer, 所以Netty回收Buffer的方式是通过主动release释放的, 而如果没有正确release就会造成内存泄漏. 但并不是每次release都会真正释放, 具体释放的时机和引用它的计数有关, 因为Netty是使用对象的引用计数来管理对象的生命周期, 而下面我们就来探讨引用计数.

2.2 ByteBuf的生命周期管理–引用计数

Netty使用对象的引用计数来管理对象的生命周期,当一个对象使用完成了之后要把它归还给池子(或者allocator)。ByteBuf就是使用引用计数最好的例子。

初始分配的对象引用计数为1,当调用release方法时,计数减一。

1
2
3
4
5
6
7
8
java复制代码// 初始分配的对象引用计数为1
ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;

// 当调用release方法时,计数减一
boolean destroyed = buf.release();
assert destroyed;
assert buf.refCnt() == 0;

当计数到0时,release方法会返回这个对象会被释放。

如果尝试读取计数为0的对象, 会报出IllegalReferenceCountExeception 异常, 必须注意:

1
2
3
4
5
6
7
java复制代码assert buf.refCnt() == 0;
try {
buf.writeLong(0xdeadbeef);
throw new Error("should not reach here");
} catch (IllegalReferenceCountExeception e) {
// Expected
}

也能通过retain增加计数

1
2
3
4
5
6
7
8
9
java复制代码ByteBuf buf = ctx.alloc().directBuffer();
assert buf.refCnt() == 1;

buf.retain();
assert buf.refCnt() == 2;

boolean destroyed = buf.release();
assert !destroyed;
assert buf.refCnt() == 1;

看起来很简单, 但是一旦ByteBuf 在线程间传递时就不是那么简单了, 因为无法很好确定哪个线程负责释放, 下面我们给出一些可以遵循的规范, 能在最大程度上避免内存泄漏.

2.3 最佳实践: 释放的时机

了解了Netty的引用计数机制后, 下面我们给出一些最佳实践, 在工程中合理运用可以避免内存泄漏的发生:

  • 如果一个组件(“组件”可以是一个函数或线程)A传递给组件B一个引用计数对象, 通常组件A不需要释放, 而是由B决定要不要释放.
  • 如果一个组件消费了这个引用计数对象而且它不会再传递给其他组件那么由这个组件来释放.

下面是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
java复制代码public ByteBuf a(ByteBuf input) {
input.writeByte(42);
return input;
}

public ByteBuf b(ByteBuf input) {
try {
output = input.alloc().directBuffer(input.readableBytes() + 1);
output.writeBytes(input);
output.writeByte(42);
return output;
} finally {
input.release();
}
}

public void c(ByteBuf input) {
System.out.println(input);
input.release();
}

public void main() {
...
ByteBuf buf = ...;
// This will print buf to System.out and destroy it.
c(b(a(buf)));
assert buf.refCnt() == 0;
}
Action 谁应该释放? 谁真正释放了?
main() 创建了 buf buf→main()
main() 调用了 a() 传入了 buf buf→a()
a() 返回了 buf . buf→main()
main() 调用了b() 传入了buf buf→b()
b() 返回了一个 buf的copy buf→b(), copy→main() b() 释放了buf
main() 调用了c() 传入了copy copy→c()
c() 吃掉了 copy copy→c() c() 释放了copy

参考: netty.io/wiki/refere…


四. Tips

1、可以通过“map -histo:live pid”手动触发FullGC,可以让泄漏日志更快打印出来否则要等到对应region回收的时候才能感知到。(Netty的泄漏日志是用WeakReference的原理触发的)

2、simple和advanced级别下的泄漏日志都是采样触发的,采样频率是1/128,但是advanced级别的最近一条是一定记录的。


五. Netty相关参数说明

参数名称 含义 备注
io.netty.leakDetectionLevel 或 io.netty.leakDetection.level 配置内存泄漏检测级别的参数, 默认simple io.netty.leakDetectionLevel是老版本的,推荐用io.netty.leakDetection.level
io.netty.leakDetection.targetRecords 如果当前保存的调用轨迹记录数Record大于参数io.netty.leakDetection.targetRecords配置的值,那么会以一定的概率(1/2^n)删除头结点之后再加入新的记录,当然也有可能不删除头结点直接新增新的, 默认4
io.netty.leakDetection.acquireAndReleaseOnly 控制是否只是在调用增加或减少引用计数器的方法时才调用record方法记录调用轨迹, 默认false
io.netty.leakDetection.samplingInterval 级别simple和advanced下采样频率, 默认128

六. 总结

在这篇文章中,我们一起深入探讨了Netty堆外内存泄漏的根本原因以及解决方法。通过了解Netty的引用计数机制,我们发现内存泄漏的发生并非神秘莫测,而是可以通过合理的管理和释放引用计数来避免。

然而,预防胜于治疗。在文章末尾,我们强调了一些最佳实践和预防措施,希望读者在实际开发中能够更加注重内存管理,避免潜在的问题。

最后我们来总结下Netty内存泄漏的的排查方法, 分为三步:

1、提升泄漏日志输出级别为advanced:java -Dio.netty.leakDetection.level=advanced ...

2、如果还不能定位,在关键位置加入hint的调用

3、如果一个方法内需要定位的地方有多个,加入hint(任意字符串)


🔥如果感觉博主的文章还不错的话,请👍三连支持👍一下博主哦

image.png

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

【Android 13源码分析】WindowContaine

发表于 2024-02-27

在安卓源码的设计中,将将屏幕分为了37层,不同的窗口将在不同的层级中显示。
对这一块的概念以及相关源码做了详细分析,整理出以下几篇。

【Android 13源码分析】WindowContainer窗口层级-1-初识窗口层级树

【Android 13源码分析】WindowContainer窗口层级-2-构建流程

【Android 13源码分析】WindowContainer窗口层级-3-实例分析

当前为第一篇,主要是基础知识介绍。

打开“电话应用”,然后按音量键出现一下界面:

电话应用按音量截图.png

提出2个问题:

  1. 为什么音量窗口会挡住应用窗口?
  2. 为什么不管打开哪个应用都能看到导航栏和状态栏?

再看下面这个图

音量截图屏幕分级.png

左边是将第一张截图的每一个窗口提出来画了的模拟图, 想象一下每个窗口其实都是全屏的,那么他的前后顺序若右图(注意颜色是对应的)。 如果说是按右边的这种层级排序,那么音量键的窗口和状态栏的窗口就会挡住Activity的窗口。

在安卓中窗口是有先后顺序的,越靠近用户的就越靠前,就能挡住底下的窗口。 目前说这么一个结论可能为时过早,后面的内容将详细介绍。

  1. 基础知识介绍

1.1 三维空间概念

1.1.1 三维空间概念–游戏3D世界

下面2个图来自Unity3d官网开发文档:
unity摄像头-1.png
我们玩的王者荣耀,原神等游戏的开发,都是类似在这么一个3D场景下进行的,开发者将使用3D建模工具来创建地形、建筑、植被等地图元素,并通过材质和贴图来增强地图的视觉效果。

比如这张图片里放了3个颜色的柱子。 除了这些物体外,可以看到还有一个摄像机(Camera),它是用来捕捉画面的, 毕竟手机屏幕是2D的,简单来说这个摄像机能捕捉到的画面就是我们手机屏幕上显示的内容,比如这张图片捕捉到的画面是右下角的内容。

像我们玩游戏移动角色,其实就是通过转动这个摄像机(Camera)来完成的。

下面这种图会更加像一个游戏的画面一些。

unity摄像头-2.png

通过看游戏地图的3D开发场景,就是希望能狗更加生动的理解到在2D的手机屏幕下,其实有一个3D的空间,虽然我们的安卓开发不会像游戏开发那么复杂,但是原理也是一样的。

1.1.2 三维空间概念–Android的三维坐标系

Android三维坐标系.png
Android坐标系其实就是一个三维坐标,Z轴向上,X轴向右,Y轴向下。

经常听到的 Z-Order 也就是指窗口在Z轴的排序,离用户越近,Z值越大。并且能够遮挡住后面的窗口。

为了再加深一下安卓窗口的层级关系,下面看一张应用开发的View层级的3D视角。

注意,下面的图片是View,不是本次要讲的窗口,只是为了加深一下层级印象

APP37层-2.png

可以看到在布局了写了37层约束布局,其实第3层加了一个按钮。 右上角我们手机屏幕上只是一个按钮的界面,但是右下角通过Android Studio自带的工具可以发现,整个View树是有很多层的(代码写了37层)。

这里看到的View层级其实和后面要讲就窗口分层,是有相似之处的。 将这里是37层想象层安卓在窗口的分层也是可以的。

1.2 ViewTree

为了后续方便理解窗口树, 先介绍一下安卓开发都知道的View树

红绿布局.png

XML里如图写下2个简单的红绿布局,显示的UI效果绿色的会挡住一部分红色的。但是用工具其实可以发送红色控件其实也是完成绘制的。

红绿布局图层.png

也就是说在同一层级下,在ViewGropu孩子View数组这个集合中,下标越大的View会挡住后面的View。也可以理解层 层级越靠前,就会当初后面的View。

继续增加几个View

红绿布局新增.png

  1. 控件B下新增控件D,D下面放了一个文本控件G
  2. 控件C下面新增了一个文本控件F

新增控件后的三维视角.jpg

左边看到对View做了层级处理,这个就是ViewTree,可以将其画成树图

Tree.png

这个就是View层的View树,能构建出View树的原因是因为在代码中定义了ViewGroup这个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
csharp复制代码# View

// 父容器
protected ViewParent mParent;

# ViewGroup
// 所有子View
private View[] mChildren;

public void addView(View child, int index) {
......
// 内部实现是通过addInArray 将View添加到数组mChildren中
addView(child, index, params);
}

@Override
public void removeView(View view) {
......// 本质还是从mChildren移除
}

安卓常见布局.png

应用布局用的场景的几个类,他们都有一个共同的父类–ViewGroup。

ViewGroup继承了View,所以有父亲(mParent),自身内部又维护了一个View数组(mChildren)表示它的孩子们。 上有父亲下有一群孩子,所以能构建出一个ViewTree。

如果只有一个孩子,那是线性结构,因此孩子必须是多个,所以ViewGroup也是一个View的容器类。 有这么一个类的存在,开发者就可以通过嵌套来行程一个非常复杂的ViewTree结构
这也是应用开发者能写出各种丰富UI的基础。

在窗口这一级别的开发中,也有窗口树,也有对应的容器结构。 后面会详细解释窗口容器类,在介绍之前先了解一下什么是窗口。

ViewTree在代码中也是真实存在的,在Activity中通过以下代码对应ViewTree的变化做监听

1
2
3
scss复制代码    View.getViewTreeObserver().addOnGlobalLayoutListener(
......
)

1.3 什么是窗口

从视觉上,用户在手机屏幕上看到的“一块区域”就是一个窗口,比如前面看到的这张图,每一块都是一个窗口

截图对应窗口.png

从代码上来说,应用端的窗口指的是Window, framework层的窗口指的是WindowState

窗口举例-2.png

为什么会用不一样的类来表示“窗口”呢?

比如说有一个人,他是唯一的,身份证号是他的唯一表示,但是他在不同的系统中,保存的他的数据是不一样的,比如在公司,公司系统对这个人保存的个人基本信息,在交警系统,保存的是这个人的车辆信息和违章信息。

窗口举例-1.png

在生活中,不同系统对一个人关注保存的数据是不一样,在代码中也是一样的。应用开发者为了降低模块的依赖,也会有这种设计。

  1. 初识窗口层级树

2.1 窗口容器类介绍

前面看到了一些View树构建的类,也就是我们说的常见布局,现在列举构建窗口树用的的几个容器类。

类-WindowContainer.png

WindowContainer:

类似ViewGroup的存在,是窗口容器的基类,后面介绍的其他窗口容器类都是它的子类。

有泛型限制,说明容器的内容是有限制的

mParent: 保存当前容器的父窗口引用。

mChildren :保存当前窗口的所有孩子窗口容器集合(有泛型)。根据注释,列表后面的子容器,z-order 越大,离屏幕越近。

父类为ConfigurationContainer,封装了配置的处理,当前类封装了容器的操作。

类-RootWindowContainer.png

RootWindowContainer:

根窗口容器,也是窗口层级树的根。管理DisplayContent

类-DisplayContent.png

DisplayContent:

代表一个屏幕,Android是支持多屏幕的。

继承关系为:
DisplayContent->RootDisplayArea->DisplayArea.Dimmable->DisplayArea->WindowContainer

孩子为DisplayArea(这个规则定义在DisplayArea的子类Dimmable中)

类-WindowState.png

WindowState:

代表一个窗口,本身也是一个容器,比如子窗口就是它的孩子(Popupwindow场景)

类-DisplayArea.png

DisplayArea:
注释:DisplayContent下的窗口容器集合。

说人话:表示一块显示区域,是DisplayContent的子容器。DisplayContent下安卓目前设计为分了37层,每一层都是一个DisplayArea。

有三个直接子类,TaskDisplayArea,DisplayArea.Tokens和DisplayArea.Tokens。

类-TaskDisplayArea.png

TaskDisplayArea:

注释:孩子可以是Task,或者是TaskDisplayArea。(不过目前看到的孩子都是Task类型)。

对应层级树的第二层,专门用来存放应用的窗口图层,非常重要,APP的窗口都在这。也是窗口层级树看的重点区域。

类-DisplayArea.Token.png

DisplayArea.Token:

DisplayArea的子类,并且是其内部类,表示WindowToken的容器。WindowToken的子类是WindowState

类-DisplayArea.Dimmable.png

DisplayArea.Dimmable:

DisplayArea的子类,并且是其内部类,DisplayContent的父类,带模糊效果。孩子是DisplayArea。

类-ImeContainer.png

ImeContainer:

输入法容器,父类是DisplayArea.Token,那么孩子也是WindowToken。输入法专用

类-Task.png

Task:

父类TaskFragment继承WindowContainer,泛型没有限制孩子的类型。但是实际情况下孩子是Task和ActivityRecord类型。

开发过程中经常见到的类,也是应用开发,多窗口开发经常会遇到的。

类-WindowToken.png

WindowToken:

理器中一组相关窗口的容器,是窗口的Token,而窗口的定义WindowState。一般在窗口层级树中WindowToken下面就会挂载一个WindowState。

壁纸用到的WallpaperWindowToken也是其子类。

类-ActivityRecord.png

ActivityRecord:

对应着一个Activity。

是WindowToken的子类,所以孩子也是WindowState。 从应用开发角度,一个Activity下也有一个Window。并且一般作为是Task的孩子。

2.2 Feature介绍

为什么有这个Feature(特征)呢?

AOSP既然将屏幕分了37层,那说明图层之间是有区别的,有不一样的特性,这个就是Feature,比如这一层是不是支持单手操作。
这里列举5个常见的Feature,已经它们所在的层级。

WindowedMagnification

1
2
复制代码拥有特征的层级: 0-31
特征描述: 支持窗口缩放的一块区域,一般是通过辅助服务进行缩小或放大

HideDisplayCutout

拥有特征的层级: 0-14 16 18-23 26-35
特征描述:隐藏剪切区域,即在默认显示设备上隐藏不规则形状的屏幕区域,比如在代码中打开这个功能后,有这个功能的图层就不会延伸到刘海屏区域。

OneHanded

拥有特征的层级:0-23 26-32 34-35
特征描述:表示支持单手操作的图层,这个功能在手机上还是挺常见的

FullscreenMagnification

拥有特征的层级:0-12 15-23 26-27 29-31 33-35
特征描述:支持全屏幕缩放的图层,和上面的不同,这个是全屏缩放,前面那个可以局部

ImePlaceholder

拥有特征的层级: 13-14
特征描述:输入法相关

Feature整理.png

3 WMS的层级结构树

可以通过以下命令来看获取到设备当前的层级结构树

1
复制代码adb shell dumpsys activity containers

在开完机后的launcher就执行了dump命令,然后就能得到下面这么一段输出,乍一看很容易劝退,但是实际上这些东西都是有规律,而且很简单。目前可以先不看,稍后再详细解释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
less复制代码ACTIVITY MANAGER CONTAINERS (dumpsys activity containers)
ROOT type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Display 0 name="Built-in Screen" type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][720,1600] bounds=[0,0][720,1600]
#2 Leaf:36:36 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 WindowToken{451b2bd type=2024 android.os.BinderProxy@4526826} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 47e1803 ScreenDecorOverlayBottom type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowToken{69b9325 type=2024 android.os.BinderProxy@3a8ab1c} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 799b2ab ScreenDecorOverlay type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 HideDisplayCutout:32:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#2 OneHanded:34:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:34:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:34:35 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 FullscreenMagnification:33:33 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:33:33 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 OneHanded:32:32 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:32:32 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowedMagnification:0:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#6 HideDisplayCutout:26:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 OneHanded:26:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#2 FullscreenMagnification:29:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:29:31 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 Leaf:28:28 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:26:27 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:26:27 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#5 Leaf:24:25 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 WindowToken{922c2bc type=2024 android.os.BinderProxy@d50168e} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 48a6245 pip-dismiss-overlay type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowToken{1a3a19a type=2019 android.os.BinderProxy@1ec36bc} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 50a3d66 NavigationBar0 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#4 HideDisplayCutout:18:23 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 OneHanded:18:23 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:18:23 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:18:23 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#3 OneHanded:17:17 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:17:17 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:17:17 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowToken{7472fe2 type=2040 android.os.BinderProxy@1bfb9c4} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 4b26f73 NotificationShade type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#2 HideDisplayCutout:16:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 OneHanded:16:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:16:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:16:16 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 OneHanded:15:15 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:15:15 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:15:15 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowToken{3da7d5c type=2000 android.os.BinderProxy@e2c682e} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 7619865 StatusBar type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 HideDisplayCutout:0:14 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 OneHanded:0:14 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 ImePlaceholder:13:14 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 ImeContainer type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowToken{1397896 type=2011 android.os.Binder@23bebb1} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 d0c3c51 InputMethod type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 FullscreenMagnification:0:12 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#2 Leaf:3:12 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WindowToken{82fa61a type=2038 android.os.BinderProxy@2f86adc} type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 33a873c ShellDropTarget type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 DefaultTaskDisplayArea type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#2 Task=1 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Task=7 type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 ActivityRecord{bd2b1d4 u0 com.android.launcher3/.uioverrides.QuickstepLauncher} t7} type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 ae1df9b com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 b9fa2f0 com.android.launcher3/com.android.launcher3.uioverrides.QuickstepLauncher type=home mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 Task=2 type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Task=3 type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#1 Task=6 type=undefined mode=multi-window override-mode=multi-window requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Task=5 type=undefined mode=multi-window override-mode=multi-window requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Leaf:0:1 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 WallpaperWindowToken{4b4c99a token=android.os.Binder@9258e45} type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 f3495ce com.android.systemui.ImageWallpaper type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]

先看看点击桌面的“电话“进入电话界面后,再执行这个dump命令有什么区别

启动Activity后层级结构树对比.png

tips: com.google.android.dialer 这个包名是”电话”这个应用
这个区别就比较好看出来了,在#1 DefaultTaskDisplayArea下多了一些东西。看到里面也知道这个是增加一些与“电话”这个应用的Activity相关的东西。
然后再按一下音量键盘,看一下区别

按音量按钮后层级结构树对比.png

在其他的场景比如按power出现的弹窗, 出现toast的时候,或者进入分屏都可以进行dump,对比一下差异。
现在可以有以下信息:

  1. 开完机层级结构树就存在
  2. 界面上有相关的Window的操作,都会在层级结构树上体现
  3. 不同的window会被挂在到对应的位置,这个其实就是层级结构树的关键。
    当然哪个Window应该挂在到那一层,怎么个先后顺序,这个我们其实无需过于在意,这个是产品设计。

tips: 可以试试不同Activity启动模式后的区别

3.1 简单分析层级结构树

现在来分析上面那一团输出信息怎么看。(从上到下,从左往右)

1
2
css复制代码ROOT type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]
#0 Display 0 name="Built-in Screen" type=undefined mode=fullscreen override-mode=fullscreen requested-bounds=[0,0][720,1600] bounds=[0,0][720,1600]

上面的ROOT 表示根节点,暂时可以忽略。下面的 #0 Display 0 name=”Built-in Screen”表示当前的手机的第0个屏幕,目前也可忽略,主要是下面那一部分内容。
下面的内容虽然很长,但是我们不要看全部,抓住几个点就够了,以前面这段为例

1
less复制代码 #2 Leaf:36:36 type=undefined mode=fullscreen override-mode=undefined requested-bounds=[0,0][0,0] bounds=[0,0][720,1600]

1. 看所在位置 #X

每一行最前面都是 “#+数字”的形式打头,比如现在看的是 “#2 Leaf:36:36”这里的数字表示这个图层在当前父容器的位置,从0开始。其实和ViewGrop或许childView的一样的。
当前这个为#2 所以他的父容器一共有3个子容器,当前这个处于第三个,也就是最上面。那么和他同级的另外2个怎么找呢?
需要往下看,找到和当前 #2 前面空格一样多的#1 和#0 就是他同级的2个容器了,按照规则能能找下面这2个与他同级的。
需要注意这里说的同级并不是在同一图层,而是在层级树这个树的结构是同级关系

1
2
bash复制代码#1 HideDisplayCutout:32:35
#0 WindowedMagnification:0:31

2. 层级name 名字+起始层级:结束层级
指的是 “Leaf:36:36” 这一段信息,这个的格式为“容器名 起始层级:结束层级”
体现在当前就是这个 #2 的容器叫 “Leaf”,表示一个叶子节点,比较特殊,主要看后面的频繁出现的HideDisplayCutout,ImePlaceholder,OneHanded等这些,都有具体的意义,我们知道所有的东西在源码中都能找到对应的代码, 像提到的HideDisplayCutout,ImePlaceholder,OneHanded在源码中称之为Feature(特征),即表示当前这个容器有具有这个特征,暂时知道就可以,后面会详细介绍源码对这些Feature的具体定义。
然后就是后面的起始层级:结束层级,因为虽然一共是分为37层,但是并不是说有37个Feature,比如“#1 ImePlaceholder:13:14 ” ImePlaceholder看着就是和输入法相关,那就代表着13,14都是和输入法相关的window。
android 13目前一共也只有5个Feature。

另外提一下这里的 “Leaf”代表的不是Feature,而且说当前是某个叶子节点,下面是要挂着具体Window的。

3. 看其他属性,比如type,mode

知道上面这4点基本上就能看到层级结构树的信息了,内容虽然很多,但是我们其实主要关心的还是下面“#1 DefaultTaskDisplayArea”的部分,因为这里放的才是应用相关的窗口,其他的一般都是系统窗口。像应用操作,分屏,小窗,自由窗口操作导致层级改变都体现在这一层,
另外可以留意一下如果是 WindowToken +WindowState的都是系统窗口,比如下面这种形式:

1
2
bash复制代码        #0 WindowToken{1397896
#0 d0c3c51 InputMethod

d0c3c51 这个应该是WindowState的对象名,后面的InputMethod是具体的窗口名
而 ActivityRecord+WindowState就是应用了比如:

1
2
bash复制代码         #0 ActivityRecord{91c971c
#0 9c20028 com.google.android.dialer

这些有个印象就行,不需要硬背,以后看的多了自然就有感觉了。

3.2 层级结构树可视化

单看层级树可能过于枯燥,在刚开始学习的时候一般都会根据信息画出一个层级结构图。
首先根据前面看dump内容的方式,先画出一部分内容如下:

层级结构树第一层.png

这里是DisplayContent下的3个孩子,可以看到已经覆盖了 0-36层。
然后按照这种方式将所有的内容都画出来就可以得到下面这完整的树图:

层级结构树.png

强烈建议想学这一块的同学一定要手动画出这么一个图

android版本相同,画出来的图基本上都是一样的,只会根据出现不同的Window在响应的Leaf,也就是叶子节点会有不同。
这里将叶子节点的颜色涂上了,发现叶子节点下要么为空,要么就是 WindowToken,除了最底层的壁纸,其实壁纸叶子节点下的WallpaperWindowToken这个类,也是继承的WindowToken。

可能之前通过文本的形式还有点陌生,但是转换成图片后,一些常见的东西就都清楚了,比如launcher,StatusBar,NavigationBar,Wallpaper

窗口树和View树还是有差距的,View树上都是View,而窗口树上只有叶子节点上挂着窗口,其他大都都是一些容器和一些。

窗口树是固定37层的, 然后各个图层都有自己支持的Feature这些都是代码中固定好的。 实际开发中,开发中再将自己的窗口根据需求挂到对应的叶子节点上,这个和View树是有区别的。

3.3 为什么这么设计

方便管理
在写应用的时候也会这样定义,这样如果说某个业务的View无论怎么写,他就只能在自己所在的“层级”上显示,不会影响到其他。

窗口这样设计的原因可能还有其他的考虑,在远古时期窗口的顺序好像是经过规则计算的,那么这种计算很麻烦出了问题也不好定位。

现在的这种设计就很合理,符合“单一原则”,各个类型的窗口在自己的层级上,不会影响到其他。

方便功能开发
另外一个优点我认为是这种设计为小窗,分屏这种功能提供了开发的便利,因为只需要将对应的窗口移动到所在的Task就可以了。

像现在的Activity启动,在system_service进程的处理的很多逻辑都是围绕着这个层级树来做的。
比如看一眼“电话”和“短信”2个应用进行分屏操作的前后对比:

分屏前后容器对比.png

通过对比可以发现

  1. Task=5,6 这2个Task是分屏用到的Task,它们有共同的父亲– Task =4。 这3个Task 在开机的时候就创建好了,默认在DefaultTaskDisplayArea孩子里是最后面。
  2. 启动分屏后其实对应窗口容器这边做了2件事
    1. 将Task=4 移到栈顶
    2. 将2个分屏应用的Task分别移到到Task5,6 下

这样分屏就完成了。

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

阿里 P7二面:Redis 执行 Lua,能保证原子性吗?

发表于 2024-02-26

你好,我是猿java。

Redis 和 Lua,两个看似风流马不相及的技术点,为何能产生“爱”的火花,成为工作开发中的黄金搭档?技术面试中更是高频出现,Redis 执行 Lua 到底能不能保证原子性?今天就来聊一聊。

要想弄清楚这个问题,需要对“原子性”这个概念有一个清晰的认识,因此,首先要分析的是原子性的概念。

一、原子性

通常意义的原子性

通常意义上,我们说的原子性是指关系型数据库 RDBMS(比如 MySQL)的原子性,也就是 ACID(Atomicity、Consistency、Isolation、Durability)中 Atomicity这一项特性。

ACID 中的原子性指:事务中的所有操作要么全部执行,要么全部不执行。

这里以银行转账,账户A 给账户B 转账100元为例来解释原子性:

  1. 账户A 减去100元;
  2. 账户B 增加100元;

原子性是指上面两个过程,要么全部执行,要么全部不执行。也就是说,账户A 减去 100元的同时,账户B 必须增加100元,否则,该操作就不具备原子性。Java代码简要实现如下图:

图片

Lua 原子性

在分析 Lua的原子性之前,我们先看看 Lua是什么,下图摘自 Lua官方描述:

image.png

从官方描述可以得知:Lua 是一种功能强大、高效、轻量级、可嵌入的脚本语言。它支持过程编程、面向对象编程、函数式编程、数据驱动编程和数据描述。 Lua 将简单的过程语法与基于关联数组和可扩展语义的强大数据描述结构相结合。Lua 是动态类型的,通过使用基于寄存器的虚拟机解释字节码来运行,并具有自动内存管理和增量垃圾回收功能,使其成为配置、脚本编写和快速原型设计的理想选择。

Lua 本身并没有提供对于原子性的直接支持,它只是一种脚本语言,通常是嵌入到其他宿主程序中运行,比如 Redis。

在 Redis中执行 Lua的原子性是指:整个 Lua脚本在执行期间,会被当作一个整体,不会被其他客户端的命令打断。

为了对 Redis执行 Lua的原子性有一个感官上的认识,这里以 Lua脚本中需要完成 SET key1 value1 和 INCRBY key2 value2 和 SET key3 value3 三个命令为例:

图片

上述例子,整个 luaScript 字符串脚本作为一个整体被执行且不被其他事务打断,这就是一个原子性的操作。

好了,总结下 ACID的原子性和 Redis执行 Lua脚本原子性在概念上的差异:

  • ACID的原子性是指:事务中的命令要么全执行,要么全部不执行;
  • Redis中执行 Lua脚本原子性是指:Lua脚本会作为一个整体执行且不被其他客户端打断,至于 Lua脚本里面的命令是否必须全部成功,或者全部失败,并不要求。关于这一点,在接下来的内容也会详细解释;

在分析原子性概念时,我们可以发现“原子性”其实是事务中的一项特性,因此,接下来分析 Redis的事务。

二、Redis 事务

下图是 Redis官方对事务描述的摘要:

图片

文档看起来很长,总结成一句话:Redis 事务允许执行一批命令,通过执行 MULTI命令开启事务,执行 EXEC命令结束事务,WATCH 和 DISCARD 配合事务一起使用,提供了一种 CAS(check-and-set) 乐观锁的机制。WATCH 用于监听 Key,如果被监听的 Key有任何一个发生变化,则中止事务(被动关闭事务),而 DISCARD 用于主动中止事务。

MULTI/EXEC

用一个示例来理解 MULTI/EXEC:

图片

通过执行的结果可以看出:Redis的事务是以 MULTI命令开启,以 EXEC命令结束,期间所有的命令都是先进入队列,只有执行 EXEC命令时,才会把队列中的所有命令顺序串行执行,并且返回一个所有命令执行结果的数组,包括命令执行的错误信息。

需要注意的是:在 EXEC 执行后,即使事务队列中有命令执行失败,队列中的所有其他命令也会被处理,Redis 不会停止执行这些命令。

DISCARD 和 WATCH 也是 Redis 中用于事务的两个命令,它们与 MULTI 和 EXEC 一起使用,提供更复杂的事务处理机制。

WATCH

WATCH 命令用于监听一个或多个 Key,如果在执行事务期间这些 Key中任何一个Key的 value被其他事务修改,当前整个事务将会被中止。(需要注意:低于 6.0.9 的 Redis 版本,Key过期不会中止事务)

如下示例:事务1 watch key1 key2,事务2在事务1执行期间修改 key2 = 10,当事务1执行 exec命令时,因为 watch监听到 key2被其他事务(事务2)修改了(value=10) , 因此事务1被取消,事务队列中的所有命令被清除,即 set key1 value1 和 incrby key 2两条命令都不执行,key2的 value还是10;

事务1 事务2
watch key1 key2
multi
set key1 value1
incrby key2 2 set key2 10
exec
keys * // 只有key2=10 keys * // 只有key2=10

图片

DISCARD

DISCARD 命令用于中止事务。

如下示例,执行 DISCARD命令后,当前事务被中止,因此,执行 EXEC 时会报“ERR EXEC without MULTI”错误。

图片

事务中的错误

事务中主要会出现两种类型的错误:

  1. 事务命令进入事务队列之前出错。例如,命令语法错误(参数错误、命令名称错误等),或者可能存在一些关键情况,比如内存不足。如下示例,命令incr key2 1/0 在进入事务队列之前报错,所以,当前事务被中止,执行 EXEC命令会报错:

图片
2. 调用 EXEC 命令后,事务队列中的命令执行失败。例如,对字符串值进行加1操作。如下示例,key的 value是字符串,当对 key 执行incr key 操作时报错,因此,该条命令执行失败:

图片

事务回滚

Redis的事务不支持回滚。 官方说明如下:

图片

Redis 不支持事务回滚,因为支持回滚会对 Redis 的简单性和性能产生重大影响。

官方说明简明扼要,其实,多加思考也能理解:”Redis” 是 “REmote DIctionary Server” 的缩写,翻译为“远程字典服务”,设计的初衷是用于缓存,追求快速高效。而了解过 ACID事务的小伙伴应该能明白事务回滚的复杂度,因此,Redis不支持事务回滚似乎也合情合理。

到此,我们也对 Redis事务做个小结:Redis的事务由 MULTI/EXEC 两个命令完成,WATCH/DISCARD 两个命令的加持,给 Redis事务提供了 CAS 乐观锁机制。Redis 事务不支持回滚,它和关系型数据库(比如 MySQL)的事务(ACID)是不一样的。

三、Redis 如何执行 Lua?

分析完原子性和 Redis事务这些理论知识后,我们就得动手实操,看看 Redis是如何执行 Lua的。

一般情况下,Redis执行 Lua常用的方法有 2种:

  1. 原生命令,比如 EVAL/EVALSHA命令等;
  2. 编程工具,比如编程语言中提供的三方工具包或类库;

在编写 Lua脚本时,需要注意区分 redis.call() 和 redis.pcall() 两个命令的使用。

EVAL

语法:

1
css复制代码EVAL script numkeys [key [key ...]] [arg [arg ...]]

EVAL语法很简单,EVAL script numkeys 是必填项,[key [key …]] [arg [arg …]]是选填项。

如下示例截图,分别展示了不传Key,传 1个key 和 2个 key 3种场景:

图片

下图示例展示了 [key [key …]] [arg [arg …]] 和 numkeys 匹配错误时报错的场景:

图片

redis.call()

redis.call() 用于执行 Redis的命令。当命令执行出错时,会阻断整个脚本执行,并将错误信息返回给客户端。

如下示例:当执行INCRBY key2 1/0 失败时,会抛异常,后续流程被阻断,即SET key3 value3没有被执行。

Redis原生命令执行示例如下:

1
less复制代码EVAL "redis.call('SET', 'key1', 'value1'); redis.call('INCRBY', 'key2', 1/0); redis.call('SET', 'key3', 'value3')" 0

图片

使用 Jedis框架执行 Lua示例如下:

图片

查看 Lua执行后各个key的值,截图如下:

图片

redis.pcall()

redis.pcall() 也用于执行 Redis的命令。当命令执行出错时,不会阻断脚本的执行,而是内部捕获错误,并继续执行后续的命令。

如下示例:当执行INCRBY key2 1/0 失败时,不会抛异常,后续流程继续执行,即SET key3 value3 也被执行。

Redis原生命令执行示例:

1
less复制代码EVAL "redis.pcall('SET', 'key1', 'value1'); redis.pcall('INCRBY', 'key2', 1/0); redis.pcall('SET', 'key3', 'value3')" 0

图片

使用 Jedis框架执行 Lua示例:

图片

对于 Lua中 redis.call() 和 redis.pcall() 如何选择,需要根据实际业务来判断,标准是:当 Lua脚本中某条命令执行出错时,是否需要阻断后续的命令执行。

四、如何保证原子性?

首先,可以肯定的是:Redis执行 Lua脚本可以保证原子性,不过这和 Redis Server的部署方式密不可分。

Redis是典型的 C/S(Client/Server) 模型,如下图:

图片

因此,Redis 通常有 3种不同的部署方式,部署方式不同,原子性的保证也不一样。

图片

单机部署

不管 Lua脚本中操作的 key是不是同一个,都能保证原子性;

主从部署

Redis 主从复制是用于将主节点的数据同步到从节点,以保持数据的一致性。而Redis的所有写操作都在主节点上,所以,不管 Lua脚本中操作的 key是不是同一个,都能保证原子性;

需要注意:当主节点执行写命令时,从节点会异步地复制这些写操作。在这个复制的过程中,从节点的数据可能与主节点存在一定的延迟。因此,如果在 Lua 脚本中包含读操作,并且该脚本在主节点上执行,可能会读到最新的数据,但如果在从节点上执行,可能会读到稍有延迟的数据。

Cluster集群部署

如果 Lua脚本操作的 key是同一个,能保证原子性;

如果操作的 Key不相同,可能被 hash 到不同的 slot,也可能 hash 到相同的 slot,所以不一定能保证原子性;

因此,在 Cluster集群部署的环境下使用 Lua脚本时一定要注意:Lua脚本中操作的是同一个 Key;

原子性保证

这里以 Redis单机部署为例:当客户端向服务器发送一个带有 Lua脚本的请求时,Redis会把该脚本当作一个整体,然后加载到一个脚本缓存中,因为 Redis读写命令是单线程操作(关于 Redis的单线程模型和多路复用线程模型会在其他的文章中讲解),最终,Lua脚本的读写在 Redis服务器上可以简单地抽象成下图,所有的 Lua脚本会按照进入顺序放入队列中,然后串行进行读写,这样就保证每个 Lua不会被其他的客户端打断,从而保证了原子性:

图片

五、面试该如何回答?

在面试中,Redis 执行 Lua脚本时,能否保证原子性?这个问题如何作答?

  1. 第一步,需要解释这里的原子性是什么?它和关系数据事务 ACID中的一致性的差异是什么?消除原子性在具体载体(RDBMS/NoSQL)上概念的差异;
  2. 第二步,需要解释 Redis的事务,说明 RDBMS/NoSQL 在事务上的差异点;
  3. 第三步,需要解释 Redis在不同部署方式下原子性能否保证。Redis部署方式有3种:单机部署,主从部署,Cluster集群部署,需要说明在哪些部署方式下能保证原子性,哪些不能保证原子性;
  4. 第四步,解释 Redis 执行 Lua脚本是如何保证原子性;
  5. 第五步,分析下 Redis的单线程模型 和 IO多路复用模型(加分项),这步是可选项;

六、Why Lua?

既然 Redis事务能保证原子性,为什么还需要 Lua脚本呢?

  1. Lua 是一种嵌入式语言,是 Redis官方推荐的脚本语言;
  2. Lua 脚本一般比 MULTI/EXEC 更快、更简单;
  3. Redis 事务中,事务队列中的所有命令都必须在 EXEC命令执行才会被执行,对于多个命令之间存在依赖关系,比如后面的命令需要依赖上一个命令结果的场景,Redis事务无法满足,因此 Lua 脚本更适合复杂的场景;
  4. Redis 事务能做的 Lua能做,Redis事务做不到的 Lua也能做;

七、Lua注意事项

Redis执行 Lua脚本时,Lua的编写需要注意以下几个点:

  1. 不要在 Lua脚本中使用阻塞命令(如BLPOP、BRPOP等)。因此这些命令可能会导致 Redis服务器在执行脚本期间被阻塞,无法处理其他请求;
  2. 不要编写过长的 Lua脚本。因为 Redis读写命令是单线程,过长的脚本,加载,解析,运行会比较耗时,导致其他命令的延迟延迟增加;
  3. 不要在 Lua脚本中进行复杂耗时的逻辑;因为 Redis读写命令是单线程的,长时间运行脚本可能导致其他命令的延迟增加;
  4. Lua脚本中,需要注意区分 redis.call() 和 redis.pcall() 命令;
  5. Lua 索引表从索引 1 开始,而不是 0;

八、总结

  • 原子性需要区分具体使用的载体,在关系型数据库(比如 MySQL))和 No SQL(比如Redis)中,原子性的概念是不相同的;
  • Redis的事务(MULTI/ESXEC)和关系型数据库(比如 MySQL)的事务(ACID)也是不相同的;
  • ACID的原子性指:命令要么全部执行,要么全部不执行;
  • Redis执行 Lua脚本的原子性指:Lua脚本会当作一个整体被执行且不被其他事务打断,但是 Lua 脚本里面的命令无法保证“要么全部执行,要么全部不执行”;
  • Lua脚本使用 redis.pcall() 执行命令出错时会被catch,后续命令会正常执行;
  • Lua脚本使用 redis.call() 执行命令出错时会抛给客户端,后续命令会被阻断;
  • Lua 脚本一般比 MULTI/EXEC 更快、更简单;
  • Redis的部署方式决定了 Redis执行 Lua脚本是否能保证原子性,编写 Lua脚本时,特别需要注意在一个事务中是否要求操作同一个 key;

九、参考资料

Scripting with Lua:redis.io/docs/intera…

Atomicity with Lua:developer.redis.com/develop/jav…

Redis Transactions:redis.io/docs/intera…

The Programming Language Lua:www.lua.org/

十、温馨提示

  • 本文基于 Redis服务器版本为7.0.4,不同的版本,可能略有差异;
  • 本文所有示例都是基于单机环境运行;
  • Redis的命令不区分大小写,但是 Key 和 Value 区分大小写;

如果你发现文章中存在缺点和错误,欢迎批评指正。如果你觉得文章对你有帮助,欢迎关注,点赞,评论,或者转发给更多的小伙伴,获取更多干货资料私信我。

原创好文:
  • 美团一面:Git 是如何工作的?(推荐阅读)
  • 当下环境,程序员需要修炼的 3项技能
  • AI是打工人的下一个就业风口吗?
  • 和斯坦福博士写代码的一个月
  • 肝了一周,这下彻底把 MySQL的锁搞懂了

本文转载自: 掘金

开发者博客 – 和开发相关的 这里全都有

1…545556…956

开发者博客

9558 日志
1953 标签
RSS
© 2025 开发者博客
本站总访问量次
由 Hexo 强力驱动
|
主题 — NexT.Muse v5.1.4
0%