阅读视图

梅雨季节的六月月报

今年的六月,连绵的下雨天和高温天,因为后半个月公司高强度线下办公,每天工作很忙,出行和看书都减少了很多。这是一个繁忙的月份,对于上班的我如此,那些促销的商家如此,即将毕业的学子如此,无论是幼儿园的小朋友还是参加高考的学子。以下是正文。

博客折腾

看到很多人的博客切换页面都很顺滑,因此也想把自己的搞一搞,查了一下可以加一下页面跳转的动画,只需添加如下的CSS即可:

1
2
3
 @view-transition {
 navigation: auto;
}

当然这样的效果和Astro比还是差不少,想要更好的效果看来只能换博客框架了。

另外看到椒盐豆豉的文章介绍的基于cloudflare实现的文章点赞功能,于是照葫芦画瓢也弄了个,欢迎来玩。

买买买

又是一年的六一八,但是现在持续的时间特别长,也没有那种节日的气氛了。

手里的相机只有一个套头28-60和35定,平时拍照也没啥特色,广角和人像反而手机拍的更好。看到不少博主的文章和视频,也是种草了腾龙的28200镜头,正好天猫国际上面近期3700块就能买到,于是剁手。长焦就是好啊,月底娃的毕业典礼上面就咔咔咔的拍了很多照片,虽然照片拍的不好,但是长焦舞台上的小朋友们都能拍得清清楚楚哈。

家里的九号电动车用了差不多四年了,座椅传感器有问题经常骑不了,龙头锁有有问题,经常启动报错,去点了老板一顿吹,最近有国补,又有旧车抵扣差不多不到3000的价格就能买到新车,于是有换新了一辆小电驴。九号虽然偶尔也会出问题,但是智能感应这一块还是挺不错的,我还是挺喜欢的。

月初是端午节,预报几天都有雨,但想着还是要出去,于是选择了去安吉躺着。第一天和第二天都在下雨只好在民宿躺着,当时一直躺着是躺不下的,第二天就在附近转了转,中午吃完饭还是决定开车去转转,于是开车去了江南天池,然而山上大雾基本上啥也没看到。

山上下来之后又去附近知名的余村转了转,听说这里以前很穷,后来因为习近平一句话开始搞生态,搞旅游,这里就发展起来了。这边环境确实不错,四面环山,建设的也挺好,很适合过来度假。

第三天预报是有小雨,但是看天气还可以,还是去了杭宣古道进行徒步,因为刚下过雨,上山的时候路上还有一些水在往下流,路边的小溪中水流也很湍急, 后面的路上一些路段也有点滑,整个路程有11公里多,但是总体难度还好,还是带着小朋友一起走完了全程。山上有个寺庙釜脱寺,有咖啡有住宿,如果有几日清闲过来修养打坐应该是挺好的。

端午后面的周末因为下雨就呆在家没有出门了,而月中的周末,因为想到后面两周要线下办公周末也要加班,还是决定出去露个营。找了个浦江郊野公园旁边的地方,然而黄浦江边围起来在施工,因此看不到江,只能在树林中烧烤了。朋友新买的炭炉花了许久才烧起来,之后大家便是一顿美餐,下午赶在雨下大前返回家中。

后半个月线下办公,也重启了每天挤地铁的日子,重新光顾了需求未曾前往的市区,趁着每天吃饭的时间和午休,逛了逛延中绿地。

这个月因为工作很忙,书影看的都很少。书只看完了《洞穴奇案》,这本书之前罗翔推荐过,最近上架了微信读书,看到了便把他读完了。书上有不少关于法学专业的东西是看不太懂,但是如果不从法学角度看,我们可以从14为法官的观点,加上自己的辩证,对于提高自我的思辨能力还是很有帮助的。

电视方面,看完了《长安的荔枝》电视剧版本,剧集的场景是挺好看的,但是能够看出明显的棚拍和打光。剧情和原著比改了很多,不评价好坏吧,我更喜欢原著一点。原本几万字改成了35集,看的很累,期待一下后面要出的电影版。

总结

忙碌的六月结束了,产品也终于发布到了Google Play,可以想象到的是接下来一段时间依然会很忙碌,就像现在的高温天一样,但忙碌总归还是好的。如此一来,就需要更加高效的利用时间,这样才能有时间做一点自己的事情。

同时小朋友已经放暑假了,可以想象到接下来的日子每天要被影响工作效率,也要想想法子应对。就这样,下个月再写。

看完评论一下吧

  •  

枇杷熟了的五月月报

今年五月挺精彩,中美关税战暂时告一段落了,伯克希尔开年会了,比特币又上新高了,Google IO又来了。为了赶进度,五月真的挺忙的,这些东西也都没有怎么关注。月底也到了,博客还是要来更新一下。

杂谈

Google IO

今年的Google IO,已经过去了几天才想起来看看网上的新闻和官方的视频,因为懒和忙,也没有及时的写篇文章,想要关注信息看官网,或者中文世界已经都有很多文章了,因此我这里简单说说吧。由于Android新版本的发布节奏改变,Android方面的更新很早之前其实就已经发布了。在这次发布会上,新版本的介绍主要大的可能就是系统UI的更新,这个可以说跟小米的HyperOS更像了,而新发布的Live Notification也与国产厂商已经上线一年的新的实时通知很像,爸爸抄儿子,也是倒反天罡了。

除此之外Android上面的介绍,首先还是Compse,Compose功能和性能都有极大的提高,同时对于KMP的支持也更好,使用Kotlin做全平台开发指日可待。

整个IO,AI仍然是全场的热点,Google的模型更棒了,集成的工具更多了,google的智能眼睛又重启了。但问题是,身在中国的我们还是很难用到,各种墙以及谷歌的限制,只有尊贵的美国人才能体验到完整功能。

关于Google IO的详细可以去官网查看。

假药

因为之前在B站看到一个UP主的推荐,于是想要去买一个口腔清新喷剂,这个喷剂看许可属于一类医疗器械,因此我这里标题的是假药。这个喷剂在淘宝上是又官方旗舰店售卖的,但是本着货比三家的原则,在京东也搜了搜,结果京东的不少非自营商家的价格还挺便宜的,于是就选择了一家购买了一瓶。

无奖竞猜,上图中哪个是假货。

收到货后,发现包装的印刷质量比较差,怀疑是假货,于是问商家,告诉我请放心使用。随后又去淘宝旗舰店看了看,发现两个包装是有一些差异的,淘宝买家分享的评价里面的图明显比我这个印刷质量好多了。我购买的这个上面有个正品标识的二维码,扫码查询之后告诉我是正品,然而这个网址是一个不知名的网址,并且下面还有个给商家注册的入口。同时我又去问了淘宝店的客服,确认了他们没有这个正品验证的网址,他们的包装开口方向也与我购买的不同,因此坐实了这是假货。

而商家仍然不认同这是假货,给了我两个回复,一,商品名称和正品是一样的,二,他这个是新包装。只好选择京东投诉,上传了淘宝正品图和假货图,然后经过跟京东反复掰扯了两个星期,京东仍然回复卖家不承认是假货,只答应京东方面赔偿我商品价格,看起来是对于商家无任何处理。

同时我也在12315提交了投诉和举报,因为选择了绿色通道,结果是截止目前为止,没有任何回应。而我也没有精力和时间去找检测机构进行检测,这件事情也只能到此作罢。

在这里建议大家,京东平台而第三方商家购物需要特别慎重。特别是我这种,商家是药店,还销售假货,简直是害人。

月初是五一,回了趟老家,家里大旱,还在种地的很多人在抽水浇地。正值杨树飘絮,漫天飞舞的杨絮,配上干燥的天气让人相当不舒服。

以上为村子里颓败的房屋。

老家坐落在皖北平原,没山没水,也没啥历史名胜,为了发展旅游。县城搞了个遗址公园,种了许多的樱花树,清明节期间樱花盛放,此时已经全部凋谢了。因为小长假的原因,遗址公园还是围起来收费,搞了些表演,也算是吸引一些周边的人民。

附近还搞了一个县博物馆,主要展示了一些本地的历史文化和文物,虽然说不是什么珍贵文物,但是我觉得对于当地的中小学生来说还是挺好的,想我大学之前没出过本县,没见过啥博物馆,现在的小孩有这些东西可以看看。只可惜农村的父母可能也没有这个意识带小孩看这些。

本月的徒步,先是去了一次无锡惠山,早上早起出发,路上不堵,10点多就开到了惠山脚下。跟着两步路的路线走了个爱心线,这条路线比较简单小朋友跟着她的同学一起在前面走的很快,下午三点半就走完了全程。

就在上周去了苏州大阳山,出发前几天都在下雨,本以为山上会很泥泞,但是实际上没有。因为前几天下雨的原因,这天过来徒步登山的人并不多。我们开车过来,把车停到了苏州乐园的停车场,这里每小时4元,20元封顶,然而往前走的时候发现路边停车只要7块钱每次,因为懒还是没有挪车。之后遇到第二个问题,因为是按照别人路线的反穿走的,在一个门那里被保安拦住不让我们进去,说要去正门买票进去。我们只好往回走,从正门附近的一个小豁口钻进去,并且爬了一段比较难走的路才拐到我们原本要走的路线上。而实际上,大多数人走的金蛇线入口,也就是我们终点的地方这里,上山也是不要门票的。大阳山比惠山难度稍高,但是因为我们先走了2公里的公路,总里程10公里,大家走下来感觉也都不算太累。

五月份还是枇杷成熟的季节,家里人也喜欢吃,相比于苏州东山80一斤的白玉枇杷,公园里免费的枇杷虽然酸,但是也不错了。于是我们选择在周末开着车到广富林郊野公园露营摘枇杷,枇杷树有不少棵,也有不少人带着工具过来摘,我们还是收获满满。

这个月主要看了两本书,首先是《巴菲特之道》这本书,想到要看这本是因为伯克希尔开了年会,巴菲特宣布退休,这本书介绍了巴菲特的部分经历,他的投资原则,我也专门写了文章,感兴趣可以看看。

另外还看了一本《寻路中国》,微信读书推荐的,作者是之前在中国工作过十几年的一位美国记者何伟,书中讲述他开车在中国旅行和与人交流的故事,时间大概是在2002年到2007年之间的事情,他见到了中国的工业化进程,农村的变化等等,书的内容比较吸引人,只花了不到两个星期就看完了。

电视剧也看了两部,首先是美剧《最后生还者》第二季上线了,也就花时间先把第一季看了看,目前在看第二季了,个人感觉第二季没有第一季好看,第一季男女主一起找实验室相当于是主线,每集又有独立的剧情。而到了第二季,感觉就是你找我复仇,我找你复仇,然后有出来一大堆新的组织,越往后越有点看不动。 另外还看了去年大火的剧集《我的阿勒泰》,整部剧只有八集,第一季作家告诉女主要“去生活,去爱,去受伤”,之后就是女主在草原生活的故事,最后结尾也算是happy end,不过我感觉可能留一点遗憾或许会更好。另外就是这部剧的画面很好看,草原加雪山真的很美,想要去新疆看看,可惜是很难请个长假。

尾声

五月工作上挺忙的,但是并没有啥产出,并且被谷歌爸爸卡着公司的产品也没法发布,希望六月能够顺利一点。至于我上面的胡言乱语,你就当耳旁风😄。

看完评论一下吧

  •  

读《巴菲特之道》摘抄

刚刚的伯克希尔年会,巴菲特宣布了即将退休,这将又是一个时代终结。于是本月决定看看跟巴菲特相关的书,《巴菲特之道》这本书介绍了巴菲特的投资理念,内容也不长花了几天就看完了。

巴菲特投资哲学的成长

巴菲特从小就接触投资,做生意积累本金,通过股市赚钱。他人生有几个重要的人,格雷厄姆是他的导师,巴菲特从他这里学会了安全边际,也就是要买价格低于价值的股票。巴菲特在学校是格雷厄姆的学生,毕业后也到格雷厄姆的公司工作了几年,从他这里巴菲特还学会了独立思考。

巴菲特读了费雪的《普通股和不普通的利润》之后,在他的投资理念上更加转向费雪。费雪更加关注公司的成长潜力,以及公司是否有好的管理层,这与格雷厄姆的是两种筛选公司的理念。而巴菲特将两种理念融合,发展出自己的投资准则。

查理芒格,跟巴菲特一样是格雷厄姆的学生,他们是一生的事业伙伴,两人建立了密切的共生关系。

巴菲特的投资准则

巴菲特的投资准则有十二条,分为企业、管理、财务、市场共四个分类,他的很多投资都践行了这些准则的部分或者全部,具体书中单独一章进行了讲解。

具体的准则如下:

企业准则

企业应该简单易懂;

企业应该有持续稳定的运营历史;

企业应该有良好的长期远景;

管理准则:

管理层是否理性?

管理层对股东是否坦诚?

管理层能否抗拒惯性驱使?

财务准则:

  1. 重视资产净收益,而不是每股盈利

  2. 计算真正的”股东盈余“

  3. 寻找高利润率的企业

  4. 企业每留存一美元,至少产生一美元的市值

市场准则

  1. 公司是否有价值? 巴菲特通过现金流和合适的折现率确定企业价值,他使用美国政府长期国债利率作为折现率。价值是未来现金流折现后的现值;成长是确定价值的一个因素。

  2. 当前是否是买入的好时机,价格是否好? 合适的价格+公司表现符合预期才能保证成功,也就是安全边际。

心理学和数学在巴菲特投资中的体现

书中关于持股数量的数学分析,虽然说分散投资可以降低风险,但同时也会降低利润。而巴菲特正是集中投资的范本。书中这段话说的很好,“当世界给予你机会的时候,聪明的投资者会出重手。当他们具有极大赢面时,他们会下大注。其余的时间里,他们做的仅仅是等待。”

巴菲特还是典型的长期主义投资者,通过他的准则可以看到他在选择购买的股票时,也就已经相信这家公司在未来的十年能够创造相当的利润。

系统1与系统2

在丹尼尔·卡尼曼的《思考,快与慢》中首次了解了系统1与系统2,在这本书中再次被提及。系统1是我们的直接思维,一般不花时间,会快速做出判断。而系统2的思维方式是我们认知过程的反思,需要我们投入努力。无论是投资还是做决定,我们都有必要训练系统2,去认真思考,进行推敲。同时在作者看来,具有系统2思维方式的人更加有耐心。

总结

限于个人能力,内容写的比较乱。总结一下巴菲特的成功,理性和耐心是他成功的关键。对于普通人,如果不能够做到这些,并且不愿意花费时间去研究公司,那么巴菲特推荐我们去购买指数基金。

最后用书中的一段话作为结尾。一个人在一生中很难做出数以百计的正确决策,只要做出为数不多的智慧决策就已经足够了。

摘抄

理性的基石就是回望过去、总结现在,分析若干可能情况,最终做出抉择的能力。

投资是经过深入分析,可以承诺本金安全并提供满意回报的行为。不能满足这些要求的就是投机。

格雷厄姆的两项投资原则: 一是不要亏损;二是不要忘记第一条。

任何投资的价值都是公司未来现金流的折现。

巴菲特从格雷厄姆那里学到的最为重要的一课就是:成功的投资来源于,购买那些价格大大低于价值的股票。

从格雷厄姆那里,巴菲特学会了独立思考。如果你是在脚踏实地的基础上得出合乎逻辑的结论,就不要因为别人的反对而耽于行动。

从费雪那里,巴菲特学到了沟通的价值

他定义特许经营权企业的产品或服务:①被需要或渴望;②无可替代;③没有管制

巴菲特说:“市场就像上帝一样,帮助那些自助的人;但和上帝不同之处在于,市场不会原谅那些不知道自己在干什么的人。

在你占据优势的时候要加大筹码。

巴菲特说:“我们所要做的全部就是,将盈利概率乘上可能盈利的数量,减去亏损的概率乘上可能亏损的数量。

当世界给予你机会的时候,聪明的投资者会出重手。当他们具有极大赢面时,他们会下大注。其余的时间里,他们做的仅仅是等待。

巴菲特的风险观:风险与股价之波动无关,与那些个股未来产生利润的确定性有关。

短期而言,股市是台投票机;而长期而言,股市是台称重机。

”首先是将股票视为企业一样,“这将给你一个完全不同于股市中大多数人的视角”。其次是安全边际概念,“这将赋予你竞争优势”。再次是对待股市具有一个真正投资者的态度。

为何懂得人们的冲动是如此有价值:①你能从中学会如何避免多数人的错误;②你可以识别他人的错误,并从中捕捉到机会。

单单有智力不足以取得投资成功,与大脑的容量相比,将理性从情绪中分离出来的能力更为重要。

理性的基石就是回望过去、总结现在,分析若干可能情况,最终做出抉择的能力。

看完评论一下吧

  •  

一拖再拖的四月月报

本该写在月底的月报,因为提前回老家被拖了,在老家因为懒也一直拖着没写,回到工作岗位,进入工作状态,这才姗姗动笔。

四月份的工作很忙,有时候晚上甚至搞到很晚,因此做自己事情的时间就少了很多。

折腾

因为想要把用了好多年的HHBK拿出来用的时候,发现之前的连接线找不到了,而这个连接线还是很老的mini B接口,真的很难找到,于是又想着去改造成无线了。之前了解到一个YDKB的改装方案,但是价格要400多,就放弃了。这次在淘宝上一搜索,居然有一个100多块钱的方案,并且支持无线/蓝牙/USB三种模式,usb口也改成了type-c接口,这个价格同时还包含了锂电池,这么实惠的价格,立马就下单了。回来替换也很简单,就是把原来的主控板,换成这个新的主控板就好了,原先的usbhub口位置则是变成了开关按钮和模式切换的按键开口,usb-c在原来min b的位置也勉强能插上线,唯一的缺点就是键位模式设置没有设计在原来键位设置的地方,如果需要修改需要拆开键盘。总体,瑕不掩瑜,推荐尝试改造。

另外,上个月提到我使用Flutter做了一个memos的客户端,经过这个月的修改,和google play的封闭测试,目前已经正式上架google play。感兴趣可前往下载,链接:https://play.google.com/store/apps/details?id=me.isming.fymemos, 同时我也开源了代码,感兴趣可以去github查看,也欢迎贡献代码,链接: GitHub - sangmingming/fymemos: A memos client write in Flutter

清明节去了趟台州,徒步到仙居公盂村,还去了临海台州府城,天台国清寺庙,感兴趣可以看我之前的文章

除了公盂这次徒步,还在月中去了一次苏州西山缥缈峰徒步,缥缈峰难度很低,感觉四五岁的小朋友也可以拿下。因为我们出发比较早,一趟畅通,爬完山之后居然还挺早,又在岛上去看了看东村古村和最佳夕阳观赏点(天气不好,看不到夕阳😂)。

接近月底的周末,因为五一要补一天班,周末只有一天,因此选择在上海找个地方露营。前往广富林郊野公园,发现公园装了收费杆,只有七号门可以进,并且排了很长的队,最终只好在附近找了个农田露营。

这个月主要在看《芯片战争》,看完了余盛的版本,同时这个名字的书还有一个美国的版本,在微信读书看了一小部分,发现其中对于中国的部分有删改,于是又找来了台湾版的译本《晶片战争》,看了一部分。篇幅上来说,余盛版本的更长,其中关于中国的部分篇幅比较长,其中关于中国的介绍是比较乐观的,比如中芯国际,长鑫存储,长江存储等的介绍。而台湾版本的,对于中国的部分不多,评价更加中性,但是因为内容是繁体字,其中很多名词和大陆的说法都不同,看的很慢。对于两本书一起辩证看的,对于我们了解芯片的发展和战争会有更加全面的认识。芯片的设计和生产,涉及到的配套和供应链也很多,而被封锁的中国想要突围,仍然任重道远。

电视剧这个月居然看了两部,首先是王宝强主演的《棋士》,感觉还挺不错的,可以看看。

另外老婆在看一个叫《无忧渡》的电视剧,也跟着一起看完了,这部剧典型的俊男靓女片,男女主都有主角光环,同类型的片子还是《唐朝诡事录》更好看一点。

而看了两部剧的代价就是本来会有点时间练字和看书,被看剧给挤占了,因此以后还是要少看剧。

杂项

看到有网友在玩Slowly这样一个笔友软件,于是也去注册完了完。这个软件基于邮票和根据距离限制邮件送达的时间的设计很有意思。在上面写了公开信,也与几个人有了邮件往来。而每次写信,都要借助翻译软件来优化英语内容,或许也能间接学学英语。立个Flag,在上面找到一两个长期笔友,借此提高一下英语表达的水平。

总结

生活一直在继续,工作繁忙也要抽空多出去多走走。虽然拖了几天但还是把终结补上了,哈哈。也感谢你看到了这里。

看完评论一下吧

  •  

清明台州游记

约了朋友清明节一起去台州旅行,清明当天出发,周一返回,周二上班。去了国清寺,徒步公盂景区,还爬了江南长城,去的时间不短,但是因为景区离得都比较远因此去的地方并不多,下面是详细内容。

Day1 赶路

清明当天开车出发,本想着早上晚点出发下午能早点到,还能看一下国清寺,结果10点从家里出发,过了12点还没出上海,到了1点多才到了嘉绍大桥服务区。于是第一天就直接开到酒店睡觉了,路上吃了饭,冲了电,到了酒店天已经黑的啥都看不见了。 晚上去村子里面充电,发现这边四面被被山包围,主要是括苍山,这个村子也叫括苍村,村里有足球场和篮球场,房子也修建的很整齐,还有个翁森纪念馆,后来查了下是宋代的一个文化人。

Day2 公盂徒步

第二天一早,显示在酒店附近转了转,这是个生态度假酒店,外面有营地,还有种了很多的油菜花,以及一些秋千等设施。

随后,我们就取车前往公盂。公盂景区和神仙居景区是靠在一起的,几年前来过一次,神仙居的风景很棒,但是里面上下都有索道,山上也搞了很多的玻璃栈道,桥啊之类的。因此我决定还是再带着朋友,和小孩一起来徒步公盂。

上一次过来还是我和媳妇两个人,并且走的穿越路线,这次带着娃一起走了个环线。因为是环线所以上次没走的那一半路这次都体验了,那一半更加有趣,路上有溪水相伴,多了更多乐趣。而之前走过的路段,很多地方都增加了台阶,爬行难度有所降低。

徒步到公盂村里面,虽然自上次过来已经过了7,8年了,但是感觉居然没啥变化。房子还是那些房子,旁边的山峰也还是那些山峰,外面每天都在发生着变化,而这里还是多年以前的模样。

因为当天攀爬公盂背的人比较多,在岔路口,我们也决定了放弃攀爬,只希望下次还有机会再来了。

徒步完本打算去仙居县城吃个荣村,结果告知座位已经订满,于是驱车前往临海市区。

Day3 临海一日游

临海最出名的也就是台州府城了,因为是老区,自然不好停车。我们在早饭之后打车来到揽胜门,开启临海一日游。

台州府城墙算是保存比较完整的,也是在山体上,从揽胜门到朝天门这一段都比较陡峭,并且看介绍说明长城的烽火台也是借鉴了这边的地台设计,因此这里又称为江南长城。 我们首先从揽胜门登上198级的台阶,在顾景楼一眺东湖公园。

随后继续登上白云楼,一路上有8个敌台,还有几个不同的城门,并且在朝天门这里有梅园,网上别人发的图片很漂亮,我们来的时候已经没有花了。

最后,我们在城墙上走到兴善门,这里下来就是古街紫阳街了。游客很多,就没去逛,找了个地方吃了个饭。就去了旁边的龙兴寺,这里据说也是日本佛教的发源地,建筑风格都有点像,我觉得寺庙了不好拍照,只拍了个千佛塔,但是居然很多小姐姐在这里摆拍。

逛完龙兴寺,我们便继续去爬了旁边的巾山,山上也有寺庙便没有参观,绕山一圈之后就回酒店了。

家人在酒店休息,我和朋友去充电。等待充电的时候在江边散步,发现墙上被很多人写了不少字,有些字很漂亮,有些写的也很有趣,这里分享一个。

晚上吃了个大排档,本来准备品尝一下荣小馆,然只有4点半的能预约,但是大家中午1点才吃饭,只能作罢。而这个大排档,味道也还不错,卤制的小龙虾加上老板特制的调料,是不一样的风味。来上一些小龙虾,配上一瓶啤酒,晚上回去就睡了个好觉。

Day4 国清寺

今日回程,正好顺路去一下国清寺,这里是隋代的古寺了,据说佛教天台(tai,第一声)宗就是这里发源,这里也是网红景点。周一已经是工作日了,这里人也不算少。跟着前面的车,直接开车进了里面的天台宾馆,这样就少走了不少路。

还没进国清寺,路边的桥和灯,就给人以很好的感觉。

寺庙中大殿,以及五百罗汉堂看的都挺令人震撼的,但是处于对佛祖的尊重,一张照片都没拍,以下是随便拍了一些照片。

既然来到了天台,中午就去当地著名的土灶头吃了一顿,麦饼,鱼头汤,鲜笋味道都挺不错的。

美餐一顿之后,驱车返程,一路上除了到上海之后有点堵,基本都很畅通,三个多小时就到家,这趟旅程就画上句号了。

看完评论一下吧

  •  

草长樱飞的三月月报

三月草长莺飞,各种花绽放,柳树披上新芽,这么好的季节,当然就要多多的户外徒步了。那么就来跟我一起看看我的三月吧。

折腾

工作上面不忙,因此空闲时间比较多,就开始重新学习Flutter,但是单纯的看别人的代码和教程,总还是不算真正的学会,于是自己也想写点东西。之前在用Moememos这个Android客户端,体验还不错,但是他对于文本格式的支持不全,tag都没有渲染,于是就自己动手也做了个Memos的客户端。因为是用Flutter写的,随手就起了个FyMemos 这个名字。 同时为了上架,又重新用家人的身份信息注册了个Google Play的开发者账号(别问我为啥不用自己的,说多了都是泪)。而谷歌从2023年开始,新注册的个人开发者上架应用加了很多的限制,需要先有12个人封闭测试两个星期才能够正式对外发布,因此现在还在封闭测试的状态。如果有感兴趣想体验一下的,欢迎发Gmail邮箱给我进入测试用户组。 因为是使用Flutter编写的,其实Windows,Linux, iPhone都是支持的,但是苹果开发者账号没有,其他可以打包的就等后面功能稳定了继续折腾。

文首也说了,三月万物复苏,适合出行,因此这个月的每个周末都户外了,除了外出露营的一周之外,其余几周都去爬山了。 首先是月初前往苏州东山岛,爬上莫厘峰,这里山上栽了许多枇杷树,适合枇杷成熟过来。

月中去杭州富阳,本来准备去拔山看樱花在茶园盛开的美景,结果樱花的花苞都还没有开。不过富阳的环境很好,呆了两天也很值。因为时间有限,没有去什么收费景区,爬山之后的第二天就沿着富春江自驾转了转,其中东梓关村令人印象深刻,这是个古村落,但是没怎么开发,保留了一些原始风貌。村中一些新的自建房也是经过设计师设计的,跟原有的建筑比较协调。

22号,前往苏州走了灵白线,天气很好,人超多,之前爬山从未遇到过这么多人。而这条线的难度也是很大的,有几个地方的坡度相当大,爬下来也是很累。

月底这周,仍然是去了苏州,走了光福小三尖,因为有小孩也就只能走三尖。这里的花特别的美,桃花,樱花,油菜花,全都有,全都盛开,可惜没带相机,照片没拍多少。而难度相比于灵白线低了很多很多,推荐新手尝试。

周周爬山也没新意了,于是找了个周末去露营,因为淀山湖在养草没法去,就在太浦河边找了个地方喝茶,看船。

另外再发发拍的一些花吧。

看完了阿德勒的《自卑与超越》,他是个体心理学的鼻祖,不少作品比如《被讨厌的勇气》都是根据他的作品内容来创作的。这本书中他主要论述,人的一生都在探寻“我与地球“、“我与他人”、“我与他/她”的关系这三大议题,书本中讨论了儿童性格的形成,家庭,工作婚姻等话题,读了一遍之后很多东西都不太记得了,有必要回头再来研读一下。

另外这个月收到了《读库2502》,目前读完了其中和西游相关的两篇内容。分别是对原著进行解读的“西游记的暗黑属性”,原著其实我没有看过,关于西游的记忆主要还是靠86版的电视剧,通过这个文章的解读,感受到的是完全不同的西游。而最近还在跟的还有刘飞和潇磊的半拿铁西游篇,他们的内容是比较诙谐有趣的,感兴趣的也可以听听。另一篇文章“千人千面”这是对于《黑神话悟空》的剧情介绍,因为之前玩的时候忙于打怪,对于剧情的部分,很多都被略过了,也算是补了一课。

影音这块,这个月居然都没有看,所能一提的也只有看了一些B站的旅行和徒步VLog。

杂项

身体很久不动,之前去徒步都有点跟不上。于是这个月下了决心去跑步,到目前为止跑了有5,6次,勉强五公里可以一口气跑完了。同时去徒步也更轻松一点了,值得后面继续坚持。

另外周末回家,高速上跟车太近,又急刹,结果被后车追尾,所幸问题不大,也是第一次发生跟其他车辆的事故,希望不会有下次了。

总结

春天是美好的,花在盛放,草木在发芽,我们也要多出去感受大自然,四月份继续往外走,另外还要向上生长。

看完评论一下吧

  •  

Chromebook折腾之2025

最近淘了一台洋垃圾Chromebook,折腾了一段时间,目前已经基本在日常使用了。在折腾过程中查找了许多的网上资料,解决了不少中文环境使用ChromeOS的问题,这里就分享一下Chromebook的选购和软件安装。

ChromeOS是什么

ChromeOS是Google从2009年开始开发的项目,可以简单理解为在Linux内核上运行的,以Chrome浏览器作为桌面环境,主要功能也是运行Web应用程序的一个操作系统。在之后,该系统也支持了运行Android应用程序,以及通过容器运行Linux程序,这样一套组合下来,我们就获得了一个原生支持运行Android应用,Linux应用和Web应用的系统,这比在Windows下面折腾Linux子系统,Android子系统要流畅得多。

目前为止,想要运行ChromeOS有两种方式,第一种就是购买ChromeBook,也就是搭载了ChromeOS的笔记本电脑或者触屏电脑。第二种方式,Google在2022年发布了ChromeOS Flex,让用户可以在经过认证的设备上安装ChromeOS Flex,包括一些Mac电脑也是支持的。

而想要激活ChromeOS,你需要有可以顺畅访问Google服务的网络。如果你没有这个条件,来自中国的fydeOS它是一个本地化的ChromeOS,内置了本地化定制和国内可以使用的网络服务,感兴趣可以去他们的官网看看。

Chromebook适合谁

ChromeOS最初设计出来也主要是方便云端办公,提供简单、快速、安全的环境,因此它更适合于对于性能没有要求,而希望简单吗体验的人。比如说:使用在线文档的文字工作者,得益于Google doc,飞书文档,语雀等文字和表格类在线工具,Chromebook简单的功能以及比较长的续航是一个性价比比较高的选择。除此之外,对于性能没有要求的开发者和数码极客或许也会考虑由于一台自己的Chromebook。

最新的Chromebook有两档标准,普通的Chromebook,以及Chromebook Plus,普通的Chromebook可能只搭载Intel Celeron处理器以及4GB的ROM, Plus也只是它性能的两到三倍。目前Chromebook在国内没有销售,通过天猫国际等平台平台购买的新机器一般也都比较贵没有性价比。对于普通用户国内平台在销售的平板电脑或者笔记本都比它有性价比的多。

而对于我所说的极客用户来说,在闲鱼淘一台洋垃圾Chromebook可能是一个比较有性价比的选择。比如我这台Lenovo Duet5,骁龙7C,8GB内存,256GB存储,13寸的OLED屏幕,搭配触控笔加键盘,支持平板模式和桌面模式,只要不到1500块钱,相比于iPad,看起来还是有点性价比的。

Chromebook选购指南

再次强调一下选择Chromebook需要保证有能够激活Google服务的网络环境。不具备的可以考虑fydeos,以及他们的Fydetab Duo设备。

在淘设备的时候,因为我们可能买到的是2019年或者更早发布的设备,我们需要关注设备的自动更新到期时间(简称AUE),所有ChromeOS设备都能够借助于Chrome浏览器几乎同步的更新节奏收到Google的定期更新。因此决定购买之前可以在Google的这个页面看一下该产品型号的AUE日期。

其次,电池健康度也是选择二手Chromebook产品时候值得关注的信息。本身购买Chromebook就是为了优秀的能耗和续航体验,电池不行这些就没办法完全达成了。购买前最好和商家沟通让对方打开「关于 ChromeOS > 诊断」界面并提供截图,可以在这个界面中清楚地看到当前设备的电池寿命、循环计数等信息。从这里可以大概预估该设备之前的运行时长,并且电池寿命高于90%应该是比较好的。我在这里就踩到了坑,因为是专门的二手商家,说了是库存设备,并且说没法激活设备不愿意提供截图导致我收到的设备实际上电池已经循环过了300多次,电池寿命只有86%,同时因为运行时间过长oled屏幕也有一点烧屏了。

最后,屏幕这块OLE屏幕可以让卖家把屏幕亮度跳到最低拍照这样也能看到一部分屏幕的缺陷,以及全白页面拍照测试等。关于型号的话,考虑到Android应用的兼容性,我选择的是ARM芯片的Duet设备,如果更加关注Linux应用的兼容性或许可以考虑X86芯片的设备。设备的型号这块,除了我提到的Duet,Google推出的Pixelbook Go, Pixelbook也是可以考虑的型号。

最后的最后,实际购买之前可以考虑使用现有设备刷如ChromeOS Flex或者fydeOS体验一下再做决定。

ChromeOS 初始化

ChromeOS本身的内核是Linux,但是为了安全,我们是没办法在上面安装Linux应用的,同时Android应用的安装也必须通过Play store才能安装,因此如果想要获得系统的完全控制权是需要开启开发者模式的。开启开发者模式后可以直接安装Android APK文件,同时也拥有了Root权限,可以在系统做修改,比如安装类似Mac下面homebrew的chromebrew工具等。但是代价是,每次启动电脑都会先跳出一个60s的警告页面(可以跳过),而在普通模式和开发者模式之间切换,所有的系统数据都会被清除,需要提前做好备份。

在我体验完开发者模式之后,决定还是回到安全模式。对于大部分人也都不需要开发者模式,我们通过Linux子系统开启Linux子系统的开发者模式,也就可以通过ADB来安装Android应用。因此如果想要开启开发者模式可以查看网上的资料。 初始化,可以通过家庭的软路由,或者手机上面开启Clash作为代理服务,在连接完网络后,修改网络中的代理服务,把手机或者软路由作为Chromebook的代理服务器,从而可以激活服务。同时要系统更新和安装Linux子系统需要稳定的翻墙服务,不然可能会失败。

ChromeOS初体验

ChromeOS内已经内置了一部分Web应用,包括了Google全家桶和一些工具应用。在未连接键盘鼠标前是平板模式,连接了之后为桌面模式。

以上为桌面模式,打开两个应用平铺,左下角为应用列表。

以上为平板模式的桌面

很多场景也可以通过浏览器进行,对于一些提供了PWA的网站,可以点击地址栏的安装按钮,这样就会生成一个桌面图标方便下次使用。也可以在Chrome应用商店安装扩展程序。

因为登录了Google账号,Chrome浏览器上安装的扩展程序,一些设置,书签等也都会同步过来。

同时ChromeOS还支持与Android手机连接,能够对手机进行简单的控制,包括手机的静音,地理位置权限开关,控制手机的热点并连接上网,查看手机最近的照片,打开的Chrome标签页等,如下图所示。

对于中文输入,Chrome内置了拼音输入法,如果使用双拼的话可以考虑使用fydeos提供的真文韵输入法,不过真文韵输入法没有软键盘,在平板模式还是没法使用,另外真文韵在Linux应用也无法使用,解决方法后面再说。

配置Linux子系统

Linux系统模式是未开启的,需要首先到「关于 ChromeOS 」中开发者部分开启,最新版本默认安装的是Debian 12,因为需要到Google的服务器上下载Linux镜像文件,这个过程可能非常慢,我这里差不多半个小时才完成。

有了Linux系统,我们首先需要安装中文环境,执行如下命令安装中文字体:

1
sudo apt install fonts-wqy-microhei fonts-wqy-zenhei fonts-noto-cjk

Linux上面是没法使用系统的输入法的,我们需要安装Linux的中文输入法,我这里就是安装的fcitx5,可以使用如下命令安装:

1
sudo apt install zenity fcitx5 fcitx5-rime

安装之后在 /etc/environment.d/ 文件中创建一个im.conf文件,并且写入如下内容:

1
2
3
4
GTK_IM_MODULE=fcitx
QT_IM_MODULE=fcitx
XMODIFIERS=@im=fcitx
SDL_IM_MODULE=fcitx

之后手动打开fcitx5,并且配置好自己的输入法选项就可以在Linux中使用应用了。

除此之外,就跟正常使用linux一样,安装的Linux应用如果是有桌面图标的也会在Chrome的应用列表中展示,同样对于deb文件,也可以直接在chrome的文件管理器中直接点击安装。

现在ChromeOS也支持了安装多个容器,也就是说可以运行多个不同的Linux,感兴趣的可以看看这位博主的这篇安装ArchLinux的文章

安装微信

微信算是每个人都必须有的通信工具了,在ChromeOS中有两种方式可以安装,一个是安装到Android子系统,直接在Google play下载就行了,另一种则是安装Linux版本的桌面微信。

但既然有这么大的屏幕,当然是桌面版使用体验更好了。我这里介绍一下在我的Debian12下面安装arm版本的微信的过程吧,因为微信的有一些依赖系统内是没有的组件需要安装。

1
sudo apt install libatomic1 -y && wget -O libwebp6.deb https://security.debian.org/pool/updates/main/libw/libwebp/libwebp6_0.6.1-2.1+deb11u2_arm64.deb && sudo dpkg -i libwebp6.deb

除了这个之外还缺少一个libtiff5,debian12上面已经有libtiff6了,我们创建一个链接就可以了。

1
sudo ln -s /usr/lib/aarch64-linux-gnu/libtiff.so.6 /usr/lib/aarch64-linux-gnu/libtiff.so.5

之后我们应该就可以使用Linux版本的微信了。

另外还推荐在Linux子系统安装stalonetray,这样就可以展示Linux的软件的托盘,比如去查看输入法状态,和切换输入选项等。可以参考这篇文章

对于Linux直接在Chrome点击deb文件安装的应用,虽然安装完了但是有可能点击图标打开的时候总是在转圈却打不开,这可能是因为程序出错了,可以在命令行中手动运行,这样错误日志就可以查看了。

配置安装非Google play的Android应用

如果想要安装国内的应用,可能很多都无法在Google play商店下载,一种方式是打开ChromeOS的开发者模式,但是那样每次开机就要忍受开机警告。我这里选择通过Linux子系统来安装。

首先打开「关于 ChromeOS -> Linux开发环境 -> 开发Android应用」,将其中的启用ADB调试打开。

点击启用的时候会有如下提示:

并且如果停用的话也会将Chromebook恢复出厂设置,所有数据被清空,使用这个功能需要谨慎。

再打开Linux命令行,执行如下命令安装adb工具。

1
sudo apt install adb

之后打开「设置 -> 应用 -> 管理Google Play 偏好设置 -> Android设置」,这样就进入Android系统的设置应用中了,可以在关于设备中多次点击版本号,开启Android子系统的开发者模式,在然后到系统,开发者选项中打开ADB调试。之后在linux命令行执行如下命令并显示正常就说明配置好了。

1
adb devices

之后就可以通过ADB安装程序了,当然也可以选择使用adb安装一个国内的应用商店,之后通过应用商店安装应用。

ChromeOS的体验介绍

使用了一段时间之后来说,作为一个轻量化的Linux 本来说,这台设备还是符合期望的。Linux,Android子系统都和宿主系统有着很好的深度绑定,使用子系统的应用也和使用宿主一样有着很好的体验。而在我这里一大缺陷为,因为Linux子系统和Android子系统都被划分到了私有网络,因此它们实际上网络是和Chromeos宿主共享的,但是和局域网的其他设备不是在同一个子网里面的,因此类似LocalSend这种工具是不能使用的。这里目前我的解决办法是使用fydeOS提供的fyDrop工具和其他局域网进行文件传输。

这个设备同时还支持通过usb typec接口连接外接显示器,chromeos有着不错的窗口管理,桌面分屏,这些功能都为使用体验加分许多。

如果只是轻办公我感觉这是一台很棒的设备,但是得益于这个性能,想要在这上面做Android开发,去编译程序那就不敢想象了。而至于要不要入坑,还是要你自己决定。

最后照例来推荐一些可以参考的资料:

  1. fydeOS源于chromeOS,这边的中文资料都可以参考:https://community.fydeos.com/t/topic/40986
  2. Chrome 官方的文档: https://chromeos.dev/en/linux
  3. 解锁开发者模式和一些折腾,可以参考这边文章和博主的其他文章: 打造一台适合生产的Chromebook

看完评论一下吧

  •  

转瞬即逝的二月月报

不知道是因为2月比较短还是因为春节刚回来没多久,感觉二月很快就过完了。如此的快以至于这个月感觉都还没做什么就结束了,现在就来对本月做个回顾吧。

折腾

这个月对于博客的足迹功能做了一些优化,一是给去过的城市或者国家加了地图mask,对于感兴趣的可以查看我的上一篇文章了解。另外对于标记点过于密的问题,也借助chatgpt,做了优化,根据地图的放大级别来把一些点进行合并,这个后面倒是可以写文章再详细说说,另外就是地图的展示还有优化的空间。

购物

这个月购买了种草很久的ChromeBook,在闲鱼淘了个联想的Duet5,支持平板和桌面两种模式,可以安装Linux应用和Android应用,除了性能比较差之外真是程序员的理想装备。但是国内使用加上版本兼容等等,还是遇到了不少问题,对于非技术人员来说体验还是会差一点。而对于一个13寸的大平板,并且只要1000多块钱,这都不是事。到目前为止还没完全驯服,后面使用一段时间再来分享折腾的一些过程。

另外之前使用了几年的手环屏幕和手环本体开胶了,于是乎就趁着有国补,买了个小米Watch 4S,小米这款的外形和华为、OPPO的相比都不够好看,但是因为手机是小米的,之前的一些数据也是在小米的APP里面,还是决定买了小米的。因为esim款是皮质的表带,因此选了它,但是用了半个月感觉也没必要开通esim。功能方面,比之前的手环要多了不少,但是不能安装app,所以本质上还是一个大号手环,另外运动记录不能导出,所以对于非小米手机用户,我是不推荐啦。

对手表爱不释手的小朋友

因为大年初七才从老家回来,这个月也只出去了一次。带着娃去了嘉兴的南北湖风景区,徒步8公里,走了谈仙岭古道,怕了嘉兴最高峰 高阳山。因为当天后面下了小雨,南北湖也没去看。

前一段是比较原始的攀爬路段,甚至还有一些地方比较陡峭,后面回程则是车行道,以及部分谈仙岭古道。在其中的云釉古庵看到了好看的梅花。

月初带这娃去看了哪吒二,娃第一次去电影院,也是我时隔多年再次走进电影院。

哪吒

之前多次尝试带她去看电影均失败,这次因为提前给她看了哪吒第一部,剧情不错,画面她也喜欢,这就顺利把他带进影院了。而哪吒确实也是部不错的片子,再加上同期有没有别的能打的电影,干到100亿票房,确实是很厉害。

周末还带娃去看了个狮子王情景剧表演,表演不到一个小时,剧情没新意,相比于迪士尼的电影狮子王真是差远了,但现场却还是爆满的,如果不是因为在各个幼儿园推广,估计不会有这么多人。另外就是虽然剧情没啥意思,但是现场的很多小朋友看得却是津津有味。

在午休和晚上的时候,这个月把赵本山的新剧《鹊刀门传奇第二部》给看完了,这个剧仍然延续第一部的高效,无厘头,无法在春晚看到赵本山表演,在这里算是重新获得赵氏表演的满足。

文字内容,读库2501看了一部分,其中的电力往事,金谷,英语走出中世纪都还挺有趣的。其他的书籍,最近倒是没看多少,这里也就不说了。

总结

匆匆而过的二月,感觉没做什么事情就结束了,写完之后发现倒也不是什么都没做,那么就这样结束这个月吧。

看完评论一下吧

  •  

使用Leafletjs实现足迹地图功能

我的博客上面挂着一个使用Leaflet实现的足迹地图功能,最近又给他添加了一些功能并且进行了一些美化。之前也有人问题这个怎么实现的,趁着刚折腾完来分享一下。

代码库的选择

早前一直想要做一个足迹的功能,像是国内的百度地图和阿里地图都有js的sdk,但是他们的sdk使用不简单,并且他们的地图只有国内的。后来了解过google map以及mapbox,但是都没有深入研究。后来看到博主水八口记使用了leaflet还比较简单,就使用这个库来实现了我的足迹功能。

地图图层

使用leaflet的一大好处是,你可以自由使用你想要的地图图层,对于符合Leaflet的地图瓦片地址我们是可以直接使用的,通常是类似这种格式的地址: https://{s}.somedomain.com/{foo}/{z}/{x}/{y}.png,其中的{z}/{x}/{y}是必须要支持的,leaflet会在运行的时候替换具体的值,从而请求对应的放大级别(z,zoom), 对应坐标(x, y)的瓦片进行渲染。

一般使用cartocdn提供的openstreetmap的地图时,是可以直接使用的,但是我们如果想要使用mapbox地图或者其他地图供应商的时候,就需要借助插件了,可以在这个页面看看有没有Plugins - Leaflet - a JavaScript library for interactive maps

对于地图图层,leaflet是支持同时加载多个图层的,比如说我可以添加一层底图再添加一层天气卫星图。

我们这里先看一下如何创建一个地图并且设置我们的地图图层. 首先需要引入leaflet的css和js文件

1
2
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY=" crossorigin=""/>
<!-- js引入一定要放到css的后面 --> <script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js" integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo=" crossorigin=""></script>

之后,在我们需要显示地图的位置放一个div元素,并且设置一个id,这样我们在后面的js代码中才能控制它:

1
<div id="footprintmap"></div>

同时我们可以通过css设置这个容器的宽度高度:

1
2
3
4
#footprintmap {
width: 100%;
 height: 180px;
}

这些做完之后就可以在javascript中去创建地图对象,并且给它添加地图图层了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
<script type="text/javascript">

 //地图的版权声明,使用三方地图数据出于对版权的尊重最好加一下
      var cartodbAttribution = '&copy; <a href="https://www.openstreetmap.org/copyright" target="_blank">OpenStreetMap</a> contributors, &copy; <a href="https://carto.com/attribution" target="_blank">CARTO</a>';
      var map = L.map('map', {gestureHandling: true, minZoom: 1, maxZoom: 14}).setView([33.3007613,117.2345622], 4); //创建地图,设置最大最小放大级别,setView设置地图初始化时候的中心点坐标和放大级别
      map.zoomControl.setPosition('topright'); //设置放大控制按钮的位置
      map.createPane('labels');

      map.getPane('labels').style.zIndex = 650;

      map.getPane('labels').style.pointerEvents = 'none';

      L.tileLayer('https://{s}.basemaps.cartocdn.com/light_nolabels/{z}/{x}/{y}.png', {

    attribution: cartodbAttribution

}).addTo(map); //添加地图图层到map对象当中

</script>

添加足迹点到地图中

经过以上的步骤我们就可以在网页上展示一个地图了,而我们实现足迹功能一般会给我们去过的地点打上标记。一种方法是给去过的城市做一个蒙层,一种方式是加一些点标记。这里先看加点标记的方法。

标记在Leaflet中称为Marker, 我们可以使用以下代码添加默认的Market:

1
marker = new L.marker([33.3007613,117.2345622]).bindPopup("popup text").addTo(map);

效果如下:

在上面我们通过bindPopup来设置点击Marker之后弹出的内容,其中我们是可以设置HTML元素的,因此我们就可以显示图片或者超链接之类的内容了。

如果不喜欢这个默认的蓝色Marker,也可以替换为图片。比如我用如下的代码就设置类一个svg图片作为Market标记图:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
function colorMarker() {
  const svgTemplate = `
    <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" class="marker">
      <path fill-opacity=".25" d="M16 32s1.427-9.585 3.761-12.025c4.595-4.805 8.685-.99 8.685-.99s4.044 3.964-.526 8.743C25.514 30.245 16 32 16 32z"/>
      <path stroke="#fff" fill="#ff471a" d="M15.938 32S6 17.938 6 11.938C6 .125 15.938 0 15.938 0S26 .125 26 11.875C26 18.062 15.938 32 15.938 32zM16 6a4 4 0 100 8 4 4 0 000-8z"/>
    </svg>`;
  const icon = L.divIcon({
    className: "marker",
    html: svgTemplate,
    iconSize: [28, 28],
    iconAnchor: [12, 24],
    popupAnchor: [7, -16],
  });
  return icon;
}

marker = new L.marker([lat, lng], {
    icon: colorMarker(),
  }).bindPopup(popupText).addTo(map);

主要是在前面创建marker的时候传的这个icon,你也可以传普通的图片。

如果我们需要展示多个点的时候,我们可以把这些点的数据存储成一个json,并且把他作为一个JavaScript对象加载,再读取他把每个点添加到地图中。 我就创建了一个points.js的文件保存所有的点:

1
2
3
let points = [
    ["<b>北京</b><i>Beijing</i><a href='/2025-01-beijing/'><img src='https://img.isming.me/photo/IMG_20250101_133455.jpg' />北京游流水账</a>", 40.190632,116.412144],
    ["<b>广州</b><i>Guangzhou</i>", 23.1220615,113.3714803],];

内容大概如上:

1
2
<!--加载点数据这样我们在javascript环境中就可以拿到points这个数组-->
 <script type="text/javascript" src="/points.js"></script>

以上加载了点数据,通过下面的代码来读取并且添加点:

1
2
3
4
5
6
7
for (let i = 0; i < points.length; i++) {
//循环遍历所有点,并且保存到如下三个变量中
  const [popupText, lat, lng] = points[i];
  marker = new L.marker([lat, lng], {
    icon: colorMarker(),
  }).bindPopup(popupText).addTo(map);
}

到此为止就完成了足迹点功能的开发。

去过的区域图层开发

而我们要实现去过的城市标记,这个时候就不是一个一个的点了,我们可能给去过的城市添加遮罩,这个其实就是给地图上画一个新的图层。每一个城市本质上就是许多个点围成的多边形,我们可以使用Leaflet提供的polygon方法来绘制,但是我们需要给把每个城市的多边形的各个顶点找到并且组织成一个数组,工作量真的是巨大的。

这样的难题我们不是第一个遇到的,前人已经遇到并且帮我们解决了。在2015年就有了GeoJson这种用Json描述的地理空间数据交换格式,他支持描述点,线,多边形。而Leaflet对齐也有支持。因此,我们只需要找到我们所需要的城市的geojson数据的MultiPolygon或者Polygon数据,就可以在Leaflet中进行绘制了。

对于中国的数据,我们可以在阿里云的datev平台进行下载,你可以省份数据或者按照城市甚至更小的行政单位下载数据。对于国外的数据可以到github上面去查找,这里是一份国家数据: datasets/geo-countries: Country polygons as GeoJSON in a datapackage

对于我们下载的中国的geojson数据,因为比较详细,也就比较大,我们可以使用mapshaper这个工具来对数据进行一些处理,直接使用Simplify功能,使用它减少点的数量,从而减少我们的文件的大小。

按照geojson文件格式,我们对于多个城市需要组成一个类似如下的json:

1
2
3
4
5
6
{
"type": "FeatureCollection", features: [
{"type": "Feature", "geometry": {"type": "Polygon", "coordinates": [[[88.40590939643968,22.55522906690669],[88.36498482718275,22.494854169816982],[88.28898205570562,22.51497913551355],[88.2714429545955,22.55235407180718],[88.32990662496253,22.55235407180718],[88.36498482718275,22.60410398359836],[88.35913846014606,22.62997893949395],[88.38837029532957,22.62710394439444],[88.40590939643968,22.55522906690669]]]}},
...
]
}

对于这样的一个json对象,我们就可以直接使用Leaflet的geojson文件进行加载,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
function onEachFeature(feature, layer) { // does this feature have a property named popupContent?
 if (feature.properties && feature.properties.popupContent) {
 layer.bindPopup(feature.properties.popupContent); //从json文件中读取属性进行popup展示
 }
}

var geojson = L.geoJSON(areas, {
 onEachFeature: onEachFeature,
  style: function (geoJsonFeature) {
    return {
      color: '#ffcc80', //设置遮罩的颜色
      fillOpacity: 0.4, //设置透明度
      stroke: false, //是否要显示边缘线
    };
  }
}).addTo(map);

对于geojson我们也可以在properties中设置弹框的内容进行展示。

总结

到这里我们就完成了基于leaflet的一个足迹地图,既包括足迹点,也包括去过的城市的遮罩。而geojson和Leaflet的功能远远不止这些,感兴趣的可以去看相关文档。另外因为我使用的地图是openstreetmap的数据,关于中国领土有争议的部分标记不正确,这个不在我的解决能力范围之内,只能暂且使用,但是不代表本人观点。

参考资料:

  1. Tutorials - Leaflet - a JavaScript library for interactive maps
  2. https://tomickigrzegorz.github.io/leaflet-examples/
  3. GeoJSON - 维基百科,自由的百科全书
  4. DataV.GeoAtlas地理小工具系列

看完评论一下吧

  •  

过完春节方动笔的一月月报

月初是元旦,月尾是新年。新年第一个月,工作走上正轨,满足小朋友去北京的愿望,以下是详细内容。

工作

十二月进入新的团队,目前算是融入完成。并且答应了老板时间线,结果是我们在年前答应的时间线根本就完成不了,同时设计也跟不上,到目前为止是拖了一周又一周,希望二月结束,应用能够达到上线状态。

这个月初是元旦,月尾是农历新年,因此只有月初去北京玩了一趟,后面就期盼着春节的到来,于24号开车回了池州老家准备过年。在家里仍然继续上班也就没有去哪里玩。

关于月初北京的行程,算是把北京的各个景点都打卡了一遍,感兴趣的可以查看之前的文章

而这个月是小朋友在幼儿园的最后一个生日了,以前没有办过生日宴,这次决定给她搞个生日聚会,花了几天时间策划,买了些道具,准备了一些套圈、智力题之类的游戏,带着她和她的小伙伴们开心的玩了一天。

因为工作比较忙,书和影视内容看的都比较少。

书这块在看余晟的《正则指引》,断断续续的看,才看到第六章,基本是把正则表达式的语法了解清楚,而应用到各种语言的内容和涉及不多。这本书多年前就翻过,因为前段时间的需求涉及到正则表达式,于是就想着再看看,关于正则的各方面介绍的都不错,推荐对于正则感兴趣的读一读。

影视方面,首先是把《人生切割术》第二季的前两集看完了,因为已经过了好长时间,对于第一季的很多内容已经有点忘记了,推荐看的时候还是把第一季先补补。

爱奇艺的新剧《漂白》也刷完了,看豆瓣上面评分挺低的,这个片子还涉及到抄袭,看的过程中确实很多地方设计的不够严谨,但是剧情还可以,值得一看,但是血腥和恶心的画面挺多的,不适合小朋友在旁边的时候观看。

再有归到阳历一月份的就是《央视25年春晚》,几个分会场感觉挺有意思的,武汉的《counting star》好听,王菲的《这世界赠予我的》也好听,其他的话感觉没啥心意,很多节目给人嘎然而知的感觉。但是这也是小朋友要看,陪着她一起看到零点的晚会了,也是一场家庭共同的活动吧。

《哪吒二》在大年初一上映,在老家的我们就陪伴小朋友一起看了哪吒第一部,几个小孩对于哪吒是真的喜欢。今天就带着小朋友去了电影院把《哪吒二》给补上了,小朋友第一次去影院看电影,之前一直不愿意去电影院,去了一次感觉还不错,这是属于二月了,就放到以后再写把。

后记

这个月比较忙碌,学习和看书的时间都少了很多。关于提笔写月报,春节期间用手机写了几个字,但是因为懒惰最终也是没写完。节后开工第一天,算是匆匆写完,给自己一个交代。农历新年对于很多人来说才是新的一年,春节假期结束,重新回到工作岗位,开始新一年的征程。这就祝大家开工大吉吧。

看完评论一下吧

  •  

北京游流水账

北京,之前只因为出差去过几次。因为小朋友在幼儿园熏陶,对北京,对天安门很向往,于是在2025年,就满足她的愿望,前往我们伟大的首都。

作为吃货,到一个地方当然要考虑当地美食。之前去北京吃过全聚德,这次就决定带着媳妇孩子来吃四季民福,提前去现场取了号,最终只用了一个多小时就吃上了。因为排队人太多,体验一般,但是烤鸭还算不错。

除了四季民福,还在五道口吃了一次局气的烤鸭,价格比四季民福要便宜很多,花样也很多,但是过于油腻了,还是四季民福的更好吃一点。

除了烤鸭,另外吃的最多的杂酱面了,五天差不多吃了有五六次。因为都是上的面和酱分离自己拌,小朋友喜欢吃没有加酱和配菜的面,我们喜欢吃杂酱面,真是很方便。另外再有,就是吃老北京铜锅涮肉了,在后海吃了南门涮肉,吃完发现一条街好几家南门涮肉,不知道哪家是正宗的。

卤煮算是特色了,吃了。还不错,其中加的火烧,原先不知道是什么,吃了才知道原来就是我们家那边的大饼。豆汁尝了,但是那个酸臭,完全受不了。

这边的面食很丰富,这种面做的饼和点心。除了这些呢,这里似乎又是美食荒漠,和南方的城市没得比。

介绍了吃的,下面就是每天的流水了。前两晚选择了住在灯市口附近,离古城比较近,游览方便。后面两晚住在了北三环附近,这样去长城和颐和园等地方比较方便。

第一天

乘坐京沪高铁标杆号,4个半小时到达北京。到达之后,直奔天坛公园。买了联票,这样才能进入祈年殿。北京很多公园都是这样,有园中园需要买联票或者进去二次买票。

虽然元旦就放一天假,但是人还是很多,但是跟出租车司机聊,他说这人不算多。挤了好一会,才看到祈年殿的内部。

里面供奉着皇天上帝的排位。

祈年殿的左右两侧偏房中间有天坛的历史状况,结构,修缮情况等的展览。看完出来之后,看到好多人在后面拍照,这边角度不错,我也拍了几张。

快速逛完天坛,去酒店办理入住之后,就去四季民福排队了。排队的时间,就走到东华门转了转。

第二天

早上吃了个庆丰包子之后,就前往天安门广场了,本以为西交民巷这里是最近的,就打车去了这里的安检口,才发现这里走到广场也挺远。另外还有三道安检。此处建议大家,如果预约了去人民大会堂,这里确实近,否则还是算了。

到了广场上,看到毛主席纪念堂还有余票,小朋友也嚷着要看毛主席,就让媳妇带她去看了。对此我没有兴趣,就呆着在广场上吹冷风了。之后穿过地道,去对面爬天安门城楼了。

也是朋友告诉才知道,现在城楼是可以爬的,要微信提前预约购票。城楼上陈列着开国大典的话筒和国徽等物品,以及一些天安门的历史介绍,定点还有人讲解,城楼上观看广场的视野也很好。

城楼上下来,就直奔故宫。

故宫里面还有珍宝馆和钟表馆,只买到了珍宝馆的门票。里面陈列着皇室使用的各种珍贵物品,非常精美和豪华。

里面的狮子很可爱。

建筑都很精美,屋檐上还有很多的小怪兽。

从神武门出来之后,坐车到后海吃饭。吃完饭,就在附近转了转,小朋友对冬泳的大爷很感兴趣,看了两个大爷游完才愿意走。

最后在这边的烟袋斜街转了转,天也就黑了。

第三天

本来这次来北京准备带小朋友到颐和园的昆明湖去溜冰的,但是通过前两天在后海和护城河了解到的情况是,现在不够冷,昆明湖还没全部冻上,冰场还没开。在小红书上查到,团结湖的冰场开了,于是就去了团结湖冰场,没法溜冰,但是有冰上自行车等,玩了半天也挺开心的。

等到吃完饭,换完酒店,购买颐和园的门票时候,发现只能买大门票了,包含里面的苏州街的联票买不到了。就只买了大门票,进去先转了谐趣园,之后转到佛香阁还在卖票,就带着娃进去看了看。

顶部的佛香阁中供着千手观音。

这里是颐和园的至高点,拍城市风景也不错,多等一会的话还可以拍夕阳。

最后在昆明湖边看夕阳。

出来之后,坐着地铁去了奥体公园吃饭,吃完饭打卡一下鸟巢和水立方。

第四天

提前候补到了清河站到八达岭的高铁票,高铁20多分钟到达八达岭长城站。乘坐两段长长的扶梯到达出站口,也首次见识到了斜着运行的箱式电梯。

从高铁站花了五分钟,走到索道站,坐缆车到达八达岭北七楼。爬北八,真是见识到了长城的陡峭,没爬多一会,小朋友这个“好汉”直呼晕,不愿意走。

风很大,小朋友也爬不动了,我们爬到北八后便没有原路返回,而是通过下山通道走回到北六楼,之后走到登楼入口。

最后,差不多12点就下来了,在长城邮局打个卡就乘坐高铁回了市区了。

中午在五道口吃了个午饭,附近转了转,下午决定还是去圆明园看看。买了联票,进去直奔西洋楼遗址,买了个微信讲解给小朋友听。

而遗址中的黄花阵迷宫是最吸引人的,小朋友在这里玩得不亦乐乎,而先到达中间阁楼的人观看迷宫中的人也很有趣。

西洋楼遗址出来后,在长春园又转了转,看了一下圆明园还原模型,最后就出园了。

第五天

最后一天,决定去地坛公园看看。也算跟第一天天坛公园好对应。

天坛公园中的殿,围墙都是圆的,而地坛公园中的方泽坛,以及其中的围墙,都是方的。这与中国传统的天圆地方相契合。地坛公园中另外还有一处特色就是鼓楼。

相比于天坛公园,地坛公园游客很少,上午的地坛公园中,各路跳舞团队云集,有舞扇子舞蹈的,有民族特色舞蹈。

中午去饱餐一顿,下午乘坐京沪牛马号打道回府了。这里要夸一下北京南站,地铁出站之后就是高铁的检票口,真是太方便了。

后记

北京的路太宽,步行真的挺不方便的,出门靠公共交通也挺麻烦,还是打车方便。就这样每天也都是两万步,大人还好,小孩有点吃不消。这边路上的电瓶车除了外卖快递,戴头盔的很少,非机动车不规范横穿马路的,让人难以想象这里是北京。

另外,作为行人过马路,也感受到这边司机的彪悍,左转不让行人,插队等等。

以前是过来做牛马,这次过来玩,体会还是很不同的。另外提醒,过来玩很多地方最好提前做好功课提前预约,比如国博,清华北大,天安门人民大会堂等,提前抢票才有机会看。

看完评论一下吧

  •  

2024年个人总结

2025年已经过去了几天,按照惯例,又到了写年度总结的时候了。这几天回想了一下过去的一年,并没有做成什么事情,其他方面在博客上大抵也可以看到,不过还是写写吧。

健康

3月份摘掉了带了一年多的钢牙套,换上保持器,虽然后面还要持续带保持器,不过还是体验要好很多了。因为带了牙套的原因,平时零食很少吃,这一两年来说体重还略微有下降。

5月份去检查头疼,发现了心脏卵圆孔未闭合,于是做了个微创手术,但是医生告知手术后三个月不能剧烈运动。于是之后便没有运动了,以至于过了三个月之后,不运动的习惯养成了,天天都不动了。到现在为止,最多就是出去走走路了,这是坏毛病,得该。

除此之外,因为久坐,腰椎和坐骨经常会疼痛,去医院拍片子看了,腰椎有点弯曲,坐骨关节有积液,也只能吃点药,并没有什么别的治疗办法。自己需要多注意,目前就是常常提醒自己要多吃钙片,有空还是多走走。最近几个月也是跟着朋友约着出去徒步了几次。

工作

23年进入公司的新项目,到3月份就已经开始出现运行不下去的颓势了,一方面是公司的产品没什么运作,另外是上面减少了投资,因为后面的几个月产品上也迭代也减少很多,产品部分人员也在不断变化,这样我们也没多少事情,一直在忐忑和忧虑中度过,直到11月份被裁。

而11月份找工作,也体会到了市场的萧条,大厂要求高,非重点院校毕业生,已经超过30岁的大龄程序员,最近的几份工作都是无名小厂,也算是Debuff拉满,没有任何大厂给面试机会。面了几家国内公司,也都没有后续消息。

最终在12月入职了前同事推荐的公司,与几个前同事在新公司重聚首。这个项目目前刚刚起步,公司也还稍微有点混乱,只需要公司的业务和各方面都能够尽早走上正轨,我在这里也能够发光发热。

博客

2024年这一年,博客内容量可以说是创历史新高了,这一年写的数量比之前所有的还高。这一切得益于公司没啥事,自己有很多的业余时间。同时因为八九月份写的技术文章参与了掘金的创作者训练营活动,获得了“创作先锋奖”,也算是不错。

从六月份开始写个人月报,现在看来是个不错的尝试。很多事情在月末回顾还能想起来,到年末真的就很难回忆起来了。而借助月报,最近几个月的一些东西这更容易回忆起来。目前坚持了七个月,后面仍然有必要继续坚持。

最后就是已经有两年没怎么更新的博客主题样式今年也迎来了大更新,主题基于ParperMod进行了个性化定制,还增加了足迹地图,更新了自我介绍,详情请看: https://isming.me/2024-08-blog-modify/

而因为博友圈,个站商店等平台,也让我认识了许多优秀的独立博客创作者,让我这个孤岛与其他的岛屿建立起连接。

个人成长

得益于公司比较闲,今年比较多的个人时间,虽然也浪费了很多时间,但也还是做了一些事情。

首先是技术方面的学习,在B站把南京大学jyy的操作系统课给看完了,之前在学校里面这个没有好好学,现在算是补课了。重新学习操作系统,对于很多程序运行的知识,并发等的理解有了新的认识。

新学了Rust语言,跟着openoscamp把rust测试做完了,目前的我使用rust来编写web服务是没啥问题了。而它在系统编程和Android方面的运用,目前还不了解,后面需要有时间需要继续看。

Android系统源码,今年算是真正的自己把一些核心模块的都看完了。而相关的内容也都整理成了文章。不过Android系统的源码量巨大,我所看的这一部分也只是冰山一角,十月之后就没有继续看了。

书籍方面也看了不少,读完的大概有25本。虚构内容方面《一句顶一万句》、《太白金星有点忙》、《食南之徒》都是很有意思的内容。非虚构方面《富兰克林自传》让我学到了很多伟人的优秀品质,《原则》也能够了解到桥水创始人的一些优秀原则,这些对于我们的个人成长都很有益处,当然,知易行难,想要跟着他们的优秀原则或者行为准则来做自己还是很难的,只希望自己能够有一点点的变化也是很好的了。

今年订阅的《读库》大约读了一半的内容,而M套餐的另一本书,全部都没有开封,因此25年还是决定只订阅S套餐了。读库的内容总体上来说还是比较有趣的,主题也比较多,还是值得继续订阅的。

旅行出行

因为23年买了车,今年旅行的次数也是多了很多。今年的长途旅行有成都重庆山东南昌,而短途的皖南的几个地方也是去看了看,上海周边也去了苏州宁波嘉兴湖州等的一些地方。总体还是以中短途为主,基本选择自驾,带着小孩,自驾在时间安排,目的地选择方面都有更多的容错空间,人也不会很累。

短途旅行一般满电出发,不需要充电可以回来。而比较远的,比如去山东,这种服务区充电也都比较方便。目前没有充电焦虑。而最近这两个月去苏州徒步了几次,目前体验感良好,后面是值得去发现更多徒步路线,徒步频率可以提高到两周一次。

生活

小朋友进入到幼儿园大班,已经开始了幼小衔接,在平时的陪学陪练过程中经常会止不住的发火,而此时只能默默告诉自己要忍住,这个时候只能提高嗓门,还没有其他的好办法。

因为这一年大多数时间都是居家办公的状态,生活和工作很多时候分得不是太清,个人也比较松弛和焦虑。松弛是因为工作量很少,大部分时间都在摸鱼做自己的事情。焦虑则是因为工作上面的焦虑。 而每天的生活就比较平淡了,为了增加一些事情,便开始养鱼。缸一直是那个缸,鱼则是换了好几波,到现在稍微稳定了一点。但是生活又重新归于平淡,每周给他们换一次水,定期喂喂食。

和父母不在一起,与他们的通话也比较少,因为不能见面,小朋友对于爷爷奶奶的也没那么的亲。老家除了过年,只中秋节回去了一次。和父母之间,除了小孩之外,其他方面的倒也没什么话题了。新的一年还是要多多关心父母。

总结

苟着的一年结束了,工作上算是一事无成,生活上也不算成功,跟朋友们的交流也不多。但是也还是有一些小进步,技术上有一些小成长,能够带着家人出去看看风景。也能够不被短视频和直播吸引,业余时间看看书,看看喜欢的剧集。

不立志可能什么事情都做不成,立志可能能够完成一般,因此新的一年,仍然要立Flag,坚持读书,重新开始锻炼身体,把难看的字给练一练,蹩脚的英语仍然需要继续花时间练习。同时新的一年也要多和家人朋友们多多互动,多多增加与他人之间的联通。也希望2025年工作上面能够有所起色,让我这个中年人在职场上依然能够站稳脚跟。

今年的总结就到此为止,来年再来看看今年的Flag完成的如何。

看完评论一下吧

  •  

一年结束的十二月月报

十二月也到了尾声,这一年也算结束了,不过在我看来过了农历新年才算一年过完。这个月重新回到职场,到目前为止算是重新适应工作,除此外,也还看了一些书,去爬了两次山,看了一些剧。

工作

工作不好找,基本没什么面试机会,最后还是经前同事内推,一起去了以前同事的创业项目。经过这几天上班,已经熟悉了环境,工作就此走上正轨,希望后面这个创业项目能够成功。

折腾

因为在家工作的原因,最近一直想做个工具,一方面想要做个习惯打卡,另一方面想要做个番茄钟。找到家里有个吃灰多年的Android Thing开发板,于是开干。

首先是使用了windsurf,让它帮我使用Rust写了个Api服务,完整实现了基于jwt的用户登录和token功能,并且实现了习惯的创建和每日打开记录功能。之后又使用docker打包,做成了docker镜像,在我的Nas里面完成了部署。所有的代码和build脚本都在github,感兴趣的可以查看,https://github.com/sangmingming/rust-todo

另外又让AI帮忙,写了一个在Android thing上运行的应用,另外淘宝买了个小喇叭连上去用来播放白噪音,目前基本功能已经满足使用,后续还可以慢慢添加新功能。

这个月出去爬了两次山,一次是月初去宁波四明山和溪口,详见四明山赏秋蜘蛛岭徒步小记。另一次则是周末开车去爬了苏州的穹窿山,走了个爱心线。

附近这种爱心线还挺多的,徒步的人也很多,一般距离就6到8千米,海拔的提升最多三四百米,带着小朋友一起爬不吃力。

之前朋友送了两张上博埃及展的门票,一直拖着没去,这个月快到期了,于是周末还是去转了转。虽然这个展已经开了好几个月了,但依然还是有很多人。拍了一些照片,但是到现在还在相机里面躺着还没有导出,对于这个展览感兴趣的可以看看旅行漫记的上博埃及文明展,他写的是很详细。看完之后又去南京路转了转,自从搬家之后,差不多有两三年没有来这边了,南京路的店铺都换了不少了,以前的几个大的服装店关了,倒是名创优品和popmart的主题店很显眼。

这个月跟着媳妇看了《猎罪图鉴二》,其中的案件还算有意思,但是几个男主炒CP真实让我这个钢铁直男受不了,所以现在的剧都已经开始gay里gay气了吗。

另外周末在家还看了《怪兽电力公司》,虽然是20多年前的动画了,但是剧情和创意还是很精彩,唯一美中不足的点要数,小朋友被吓哭了🤣。

书籍的话,看完了鲁迅和许广平先生的来往信件《两地书》,书中感受到的鲁迅和他小说中的鲁迅真的很不同,写信的鲁迅经常是个话痨,也很风趣,也很细心,他们之间的书信往来从起初的谈论政治,到后面开始互相挂念等等。真的让人感受到从前车马很慢,书信很远。其余时间,则是在看余晟的《正则指引》,以及《Linux命令行与Shell脚本编程大全》,就不题感受了。

后记

此时已是年底,到此本月的月报也算是草草写完。这几天已经有很多人的年度总结,而各个平台也已经开始了年度总结的活动了,而作为拖延症晚期患者的我,年度总结还需要再拖一拖,后面还需要花点时间想想这一年到底做了什么,来年想要做什么。

到此这一年就结束了,祝大家新年快乐喽🎆🎇。

看完评论一下吧

  •  

强大的壳-Shell Script

Shell脚本我们经常会使用,平时自己折腾Nas会用到,工作中为了配置CI会用到,自己的电脑上最近为了配置自己的命令行环境也要使用shell来进行配置。不过之前的shell功力都来自每次使用的时候网上搜索,于是最近就找了一本《Linux命令行与shell脚本编程大全》看了看,看完之后更加感受到Shell的强大,特地写个文章来分享一下。

首先呢,shell它也是一种语言,不过因为使用到的shell环境不同语法会有一些差异,在Linux上我们常用的shell是Bash,在Mac上面常用的shell为zsh,大体的语法相似的。编程语言的基本要素,Shell都是支持的,它支持变量,支持if判断,case选择,循环等结构化的编程逻辑控制,也支持基本的算数运算,同时还支持使用函数来复用代码。 简单介绍一下它的语法,首先是变量。系统为我们已经提供了很多的变量,同时在我们的配置文件中定义的那些变量也是可以读取到的。定义变量语法如下:

1
2
3
4
5
var=value #注意等号两边不能加空格
echo $var #使用的时候前面要加上$符号
echo ${var}

export varb=b #导出成为环境变量

以上方式定义的变量默认是全局的,比如你在一个函数中定义的,外面也能访问,这是时候可以定义局部变量:

1
local local_var=x #只能在函数中使用

除了普通的变量之外,shell中也是支持数组和Map的,当然要bash 4.0以上才能完整支持,使用如下:

1
2
declare -A info # 声明一个map
declare -a array #声明一个数组

而如果只是有这些东西的话,还不至于说Shell强大。而shell中可以直接调用命令以及Linux中的一些程序这才是它的强大之处。在python等其他语言中我们也是可以调用的,但是是都需要通过语言的系统调用才能调用,而shell中则是可以直接调用那些命令,只要这些程序的可执行文件在PATH环境变量中就可以。

而配合Shell的很多特性,又进一步强大了。第一大神器是重定向,重定向支持重定向输入和重定向输出,以下为一些示例:

1
2
3
4
5
6
7
date > test.txt #重定向输出到test.txt文件中,覆盖文件
ls >> test.txt #重定向,但是追加而不是覆盖文件
wc < test.txt #输入重定向
wc << EOF #内敛输入重定向
test a
test b
EOF

因为有了输入输出重定向,我们会有很多的玩法,可以方便的命令的输入写入到我们的文件中,而linux系统中,万物皆为文件,因此理论上可以写入或者读取所有东西。比如,有一个Null设备,我们可以通过以下的命令,来不展示任何运行输出。

1
2
ls >/dev/null 2>&1
ls 1>/dev/null 2>/dev/null

1为标准输出,2为错误输出,未指定的时候默认是把标准输出重定向,这里重定向到null则不会有任何输出,而第一行我们将错误输出又通过&绑定到了标准输出。当然除了这个还有更多的用法。

除了重定向之外的另一大特性则是 管道 。在某些场景重定向已经可以解决了很多功能,但是管道实现会更优雅。管道可以将前一个命令的输出直接传给另一个命令,并且管道的串联没有数量的限制,并且前一个命令产生输出就会传递到第二个命令,不用使用缓冲区或者文件。比如:

1
ls | sort | more

甚至我们还可以将刚刚的输出继续重定向保存到文件

1
ls | sort > files.txt

在很多命令的参数之类的都提供了正则表达式的支持,正则表达式能够让我们更加方便的进行数据匹配,Linux中常用正则为POSIX正则表达式,而它又有两种,基础正则表达式(BRE)和扩展正则表达式(ERE),大部分的Linux/Unix工具都支持BRE引擎规范,仅仅通过BRE就能完成大部分的文本过滤了,但是ERE提供了更强的功能,而有些工具为了速度,也仅仅实现了BRE的部分功能。

BRE支持的语法符号包括,.匹配任意一个字符,[]字符集匹配,[^]字符集否定匹配,^匹配开始位置, $匹配结束位置,()子表达式,*任意次数量匹配(0次或多次),而ERE在BRE的基础上,还支持?最多一次匹配,+匹配至少一次。而它们的更多功能可以参看这篇文章:https://en.wikibooks.org/wiki/Regular_Expressions/POSIX_Basic_Regular_Expressions

有了正则表达式以及许多的处理工具我们就可以做很多的事情了,比如说查找文件,我们可以使用find,查找某个文件夹下面为指定后缀的文件:

1
find . -type f -name "*.java" #find支持的只是通配符,非正则

而配合管道,又可以对find之后的结果进行进一步的处理,比如配合上grep可以进一步对文件的内容进行过滤。

1
2
find . -type f -name "*.sh" |xargs grep "bash" #find 不能通过管道直接传递可以使用xargs或者通过如下方式
find . -type f -name "*.sh" -exec grep "bash" {} \;

对于文本的处理,Linux中又有sed和awk两大杀器,而关于他们的使用已经可以被写成书了。sed全名为Stream editor,也就是流编辑器,通过它可以方便的查找文件内容并替换后输出,awk则是一种模式匹配和文字处理语言,通过他们可以方便的处理文本。比如说我们可以使用sed对一份CSV文件中的手机号码进行打码处理:

1
sed -E 's/([0-9]{3})[0-9]{4}([0-9]{4})/\1**\2/g' input.csv

以上关于命令的介绍只是抛砖引玉,关于他们的使用,我们的电脑中已经给我们提供了详细的介绍,只需要在命令行中输入man commandname就可以了,除此之外,很多的命令也也提供了简单的帮助,只需要输入commandname help, command --help之类的就可以看到。

如果仅仅是语言层面的功能的话,shell相比python是没什么优势的,但是它能够和其他的命令无缝的使用,并且被Mac,Linux,Unix内置可直接使用也是它的一大优势。此外我们还可以通过shell脚本来增强我们的Linux终端,比如说可以定义自己的函数,通过.bashrc引用,可以在终端中直接调用方法名执行。

通过Shell,在Linux下面的体验得到很好的提升,工作效率也可以获得很大的提高,本文只是略微提到其皮毛,希望能够引起你对Shell的兴趣,如果想要更加深入的了解,还是需要去阅读手册或者书籍。

以下是推荐的一些资料可供参考:

  1. Bash脚本编程入门 by阮一峰
  2. Bash脚本进阶指南
  3. Grep,Sek和awk的区别
  4. 《Linux命令行与Shell脚本编程大全》(可以在微信读书中看电子书)
  5. awesome-shell (值得看看的各种资料,也可以去看看别人写的shell脚本)

看完评论一下吧

  •  

Linux重装与dotfile整理分享

最近把电脑上面的Linux系统给重装了,同时呢也要配置新的MacBook,就整理了一个个人的dotfile,这里分享一下linux上的我主要使用的软件,以及我的dotfile内容。

什么是Dotfile

dotfile字面意思就是以.开头的文件,在Linux当中就是隐藏文件,我们大家说的一般指的就是配置文件,比如shell的.bashrc.profile文件等。我在整理自己的dotfile的时候参考了一些网上大神的dotfile文件,这里我主要是包含我的shell的一些配置文件,vimgitrime相关的文件。

我的 Dotfile

为了保持Linux和Mac系统的统一, 我将Linux的Shell也换成了zsh,同时为了简单并没有使用oh-my-zsh,只是添加了一些自己常用的aliases

而Vim则使用neovim,它相当于是重新开发的,我想比vim应该代码上面更加高效,毕竟少了很多的历史包袱。另外它的配置文件可以使用Lua进行编写,而不是使用vim script,这样也是它的一大优点。

除了配置之外,还增加了脚本用用于将这些配置文件自动拷贝到对应的目录,使用以下代码判断是Linux系统还是Mac系统:

1
2
3
4
5
if [ "$(uname -s)" == "Darwin" ]; then
 //action for mac
else
 //action for linux
fi

另外呢,对于Mac系统,在初始化脚本中还添加了homebrew的安装,并且通过使用Brewfile在定义需要安装的一些软件,这样在执行brew bundle的时候可以把这些软件都安装上去。

对于Linux的目前还没做啥,都是通过自己手动安装的,不过一些操作也记录到了shell文件当中了。

Linux上的软件

既然写了文章,就顺便分享一下我的Linux上面还在用的软件吧。 首先是Shell,为了跟Mac保持统一,也改用了zsh,如果你也想要设置zsh为你的默认shell,可以执行如下命令并重启(前提你已经安装的zsh):

1
 sudo chsh -s $(which zsh) $USER

编辑器目前在用的有三款,主要在用neovim,同时代码文件还会使用vscode,因为有些场景neovim操作比较麻烦(对于快捷键不太熟悉),最近也在使用阮一峰老师之前推荐过的zed,据说比vscode性能更高,目前体验是对于很多语言的支持是已经内置了,不需要在安装插件,这点是好评的。

输入法在使用Fcitx5,输入方案则是使用了Rime,Rime的配置则是参考了雾凇拼音,而我主要使用小鹤双拼。

其他还在使用的软件包括:

项目开发: Android studio

截图软件:Flameshot

启动器: ULauncher, 使用简单,支持的插件数量也比较多

文档搜索: Zeal doc,mac 上面dash的window和linux平台开源版本,支持dash的文档。

文件同步: Syncthing

局域网文件传输: LocalSend

聊天软件: Weixin, telegram

文档和博客编辑: Obsidian

网页浏览器: Edge

Linux 开启zram

我的电脑已经有32G的内存了,大部分时候是够用的,但是编译Android系统的时候就不够用了。因此需要想办法,一种方式是弄一个swap空间,但是swap的速度不是很快,经过查询资料了解到现在linux已经有了一种新的虚拟内存技术,也就是zram,它主要功能是虚拟内存压缩,它是通过在RAM中压缩设备的分页,避免在磁盘分页,从而提高性能。

而想要启用它其实很简单,在我们的Ubuntu中,只需要首先关闭原先的swap空间,编辑/etc/fstab文件,将其中的swapfile条目注释掉。之后调用如下命令:

1
sudo swapoff /swapfile

如果你本来就没有设置swap,那就不需要做上面的操作,直接安装zram-config:

1
2
3
sudo apt install zram-config
systemctl enable zram-config //设置开机启动开启zram的服务
systemctl start zram-config //启动zram服务

之后可以调用如下命令验证:

1
cat /proc/swaps

我们在系统监控里面也能看到,不过还是swap。 以上方式开启的zram为物理内存的一半大小,当然也是可以修改的。 修改/usr/bin/init-zram-swapping文件中的mem大小即可。

如果对于我的dotfile感兴趣,可以查看我的repo, 地址为: https://github.com/sangmingming/dotfiles,其中我提到的初始化脚本为script/bootstrap文件。

看完评论一下吧

  •  

四明山赏秋蜘蛛岭徒步小记

月初跟朋友约着一起去了一趟四明山和奉化蜘蛛岭,因为相机的照片和拍的视频一直没整理,内容也就拖着没有发,正好这两日不太忙,就整理一下。

Day 1 四明山赏秋

首先驾车来到了四明湖,四明湖水杉观赏区人特别多,车不太好停。水杉树有一点红的,跟网上的图片比还差一点,晚一两周来估计会更好看。

相机只有个35mm镜头,远处好看的红杉都拍不了,还是要升级装备。 除了杉树旁边还有很多芦苇也很好看。

看完红杉在镇上吃了个面,遂驱车进山,一路上人是真的多,每一个观景台都堵车,也有交警维持秩序,因此观景台就没法停下来观看了。

因为晚上的住宿定在了溪口,随后就继续开车前往溪口了。在路上看到了红枫之乡,风景很不错,就拐了个弯进去,几个人顺着小路爬到了山上。

(以上三张照片有手机拍的和相机拍的,可以猜猜)。枫叶很多已经掉了,村民告诉我们前两周特别好看,那个时候过来看枫叶的人很多。

晚上饭后,趁着给车充电的时间,在溪口转了转,这边的建设真不错。房子的灰色的民国风的建筑,这边又有山有水空气很好。

Day 2 蜘蛛岭徒步

早上早早吃完饭,就开车前往直岱村委会,停了车开始徒步。总长度大约8公里多,因为带着小孩走的很慢。

一路上倒是风景很多样,有普通的树,有枫树,有竹林。

路上遇到两个水潭,而且作为当地村民的引用水源的,潭水碧绿。

最高峰黄泥浆岗976米。

走到村口大树就能看到我们停车的停车场了,也就完成了这个环线。 这一路上有很多的徒步团的标记,完全不用担心迷路。为了方便记录也可以使用两步路来找别人的记录跟着走,如下是我的记录:

为了轻装上阵,相机也没拿,徒步的所有的图片就用手机拍摄了。同时徒步过程中又拿着运动相机拍了一些视频,剪辑如下:

最后回到溪口镇上,吃了个饭,就返程了,结束一个充实的周末。

看完评论一下吧

  •  

中年失业的十一月月报

十一月也到了尾声,这个月真的是跌宕起伏,首先见证了同学的新婚大喜,听闻了川普的胜选,错过了比特币的大涨,当然也有突然到来的被裁员,和就业环境的寒意。虽然如此,还是出去玩了几趟,看了一些书。

失业和找工作

公司状况不太好有一段时间了,我们这段时间的需求也不大多,特朗普刚刚竞选结束,技术Leader就跟我们说要裁员,随后HR火速走流程,也就两三天电脑就锁了,进入失业状态。

复习了一周后就开始投简历了,首先找朋友内推和脉脉聊投了几家大厂。由于年龄不小,学校也非重点院校,加上最近这一两段的工作经历又比较杂,真的是Buff叠满,意料中的这几家都没通过简历筛选。

而后跟朋友聊天,他们目前所在的公司在人员招聘方面也不容乐观,一方面是公司的经营状态其实也都没有那么好,另一方面公司也都卡薪资卡年龄。所幸的是,裁员大礼包支撑几个月是不成问题的,这个时候能做的就是调整好心态,慢慢去找,不要那么的焦虑。

月初回安徽参加大学室友的婚礼,绕道合肥一起去母校转了一圈,合肥这几年的变化挺大的。

学校除了改了名称,其他方面感觉变化倒是不大,最大感受就是学校里面的电瓶车真的是很多。

十月份的时候跟朋友约了几次出去爬山,但是因为一到周末就刮风下雨,不断的被延后,这个月终于成行了。在月中,和朋友一起分别带着各自的小朋友,前往苏州旺山,这条线路倒是不太累,总长度只有不到9公里。

虽然预报没有雨,但是当天还是下着小雨。但总体不耽误我们徒步爬山,旺山上面的徒步团也挺多的。这里离太湖不远,风景也还不错,山里有座乾元寺庙,去其中拜拜佛,保佑找点早到工作。

而上周末天气也是很不错,于是跑到了之前很多人推荐的号称小塞尔达的神仙湖公园转了转。这个湖为坑矿改造,湖水碧绿,湖中间有小的起伏可以走过去,过去转转挺不错,就是开车过去要两个小时体验不太好。

这个月周末出去的比例和之前相比还更高了,因为失业所以基本整天呆在家里,因此到周末还是要出去多走走看看。

这个月看了几集的《守护解放西5》,其中的案件真的挺有意思的,作为下饭榨菜挺不错,其中的一些案件呢也是能够了解到警官办案的辛苦和嫌疑人的无厘头。

上个月开始看的《原则》终于看完了,看完之后发现这本书还有对应的实践书,也找到看了看不过还没看完。另外达利欧还基于他的书做了一个介绍原则的视频,这个视频用30分钟的介绍了他的原则,以及他的走向成功的五步骤法。

马伯庸今年的新书《食南之徒》也花了几天看完了,因为内容很精彩,所以看的很快。按照马亲王在书尾的介绍,这是根据真实历史事件,去展开,扩充成为一本涉及到南越国王死亡真相的悬疑小说,而其中有穿插了很多关于美食的描写,让人读的酣畅淋漓。

前几天提过一本《鱼不存在》也是在孟岩播客推荐之后看的,这本书的题材和叙事方式都很神奇,之前写过文章,这里再多说了。

后记

因为要找工作的原因,这个月跟不少朋友或者微信或者线下聊天,感觉比我这半年跟别人聊的还多。聊了之后也还是感觉整体的行情不好,需要做好长期奋战的准备了。

而所在的几个微信群里,裁员的也不是我一个,其他群友也有被裁的。这个时候所有人都应该调整心态,说不清过完年一切会好一点呢。

每日的复习和刷题其实还是挺难坚持下去的,而余下的时间还是要多看书,有空了出去走走,以及多跟别人交流。

共勉之。

看完评论一下吧

  •  

使用Cuttlefish运行自编译Android固件

最近把本地的Android源码升级到了最新的Android 15,用于看Android源码的Android Studio for Platform也升级到了最新版本,Google的Cuttlefish最近发布了1.0版本,也顺便折腾了一下使用Cuttlefish来运行自己编译的Android系统,这里就介绍一下如何使用和遇到的问题。

Cuttlefish是什么

Cuttlefish是谷歌推出的一种可以配置的虚拟Android设备,它可以运行在我们本地设备上,也可以运行在服务器上面,官方也提供了Docker运行的支持,理论上可以运行在本地或者服务器的Debian设备上,或者运行在Google Compute Engine上。

用官方的化来说,它是一个更接近真实设备的Android模拟器,除了硬件抽象层(HAL)之外,它和实体设备的功能表现基本上是一致的。使用它来做CTS测试,持续集成测试会有更高的保真度。

在命令行中运行它,是没有类似模拟器的UI的,我们可以通过两种方式看到它的UI,一种是通过ADB连接,另一种则是开启它的webrtc功能,在浏览器中查看和交互。而他的虚拟硬件功能,可以让我们模拟多个屏幕,测试蓝牙wifi等各种功能。

安装Cuttlefish,编译Android固件

首先我们需要检查我们的设备是否支持KVM虚拟化,使用下面的命令:

1
grep -c -w "vmx\|svm" /proc/cpuinfo

如果得到一个非0的值,就是支持的。

之后我们需要有一个Android固件,可以选择去Android持续集成网站下载他们编译好的固件,也可以自己编译固件。下载固件要注意下载设备目标中带cf的,并且下载的目标CPU需要和需要运行的宿主机CPU架构一样,ARM就下载ARM的,X86就下载X86_64的,具体的操作可以看官方教程。我这里则是自己编译,使用如下代码设备我要编译的选项:

1
lunch aosp_cf_x86_64_phone-trunk_staging-eng

这样有了固件,还是不能够运行的。我们还需要去编译Cuttlefish,在https://github.com/google/android-cuttlefish下载源码后,在cuttlefish源码目录下执行如下代码编译和Android:

1
2
3
tools/buildutils/build_packages.sh
sudo dpkg -i ./cuttlefish-base_*_*64.deb || sudo apt-get install -f
sudo dpkg -i ./cuttlefish-user_*_*64.deb || sudo apt-get install -f

如果你很幸运的化,上面会一次成功,但是我不是个幸运儿。于是了类似如下的错误:

1
While resolving toolchains for target //src/tools/ak/generatemanifest:generatemanifest (6312974): invalid registered toolchain '@local_jdk//:bootstrap_runtime_toolchain_definition': no such target '@local_jdk//:bootstrap_runtime_toolchain_definition': target 'bootstrap_runtime_toolchain_definition' not declared in package '' defined by /home/sam/.cache/bazel/_bazel_jcater/ddb4e20e0e2e6bca92f5deeef02ce168/external/local_jdk/BUILD.bazel (Tip: use `query "@local_jdk//:*"` to see all the targets in that package)

这个错误的原因呢,就是因为编译cuttlefish的时候使用了bazel这个构建工具,它依赖JDK,而我没有设置JAVA_HOME这个环境变量,因此把它加入到环境变量中就好了。类似如下:

export JAVA_HOME=/usr/lib/jvm/zulu-17-amd64

设置完成之后在Cuttlefish项目目录用如下命令检查一下,看看JAVA_HOME是否设置正确:

1
bazel info java-home

但是搞完之后,在安装这两个deb文件的时候又遇到了问题,告诉我我电脑上的grub-common签名有错误,这个呢是因为我之前添加了铜豌豆的软件源,grub升级的时候升级了铜豌豆的grub软件包,它和ubuntu官方的不同,于是卸载掉铜豌豆软件源,grub-common也重新安装,之后就没问题了。 这些做完之后,我们执行下面的命令设置环境,并且重启电脑就好了。

1
2
sudo usermod -aG kvm,cvdnetwork,render $USER
sudo reboot

使用Cuttlefish

在我们的已经编译完Android系统目录中首先执行如下代码让环境初始化好:

1
2
source ./build/envsetup.sh
lunch aosp_cf_x86_64_phone-trunk_staging-eng

随后执行如下的命令就可以启动Cuttlefish运行Android了:

1
launch_cvd --daemon

如果你是从Android官方下载的,那么会和我这有一些区别,可以去看一下官方教程。

这个时候我们就可以通过adb看看设备是否已经启动了,也可以在浏览器中打开,在本机浏览其打开使用如下地址和端口:

https://localhost:8443/

地址一定要使用https,点击左侧的swtich按钮就可以看到UI了。 webrtc是默认打开的,关于它的命令行更多使用方式可以查看官方文档,可以使用如下的命令查看。

1
launch --help

而关闭Cuttlefish,也很简单,使用如下的命令:

1
stop_cvd

新版Android Studio for Platform使用

2023版本的Android Studio for Platform(以下简称Asfp)在打开的时候是有一个单独的Open Aosp project选项的,而新版本的这个选项去掉了。刚刚使用它的时候我还一脸懵逼,测试了Import和Open都不行,结果最后发现新版的New选项就直接是导入Aosp工程了。

使用方式如下图。

我们可以根据上图选择我们需要导入的Module,选择Asfp给我们生成的项目文件存放的位置,之后Asfp会执行lunch的操作和它需要的一些依赖构建。在我们选定的目录下面也会生成一个asfp-config.json文件,它就是我们的项目设置,如果我们以后有变化了(比如想看不同的模块的代码),也可以直接修改这个文件。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
{
 "repoRoot" : "/home/sam/android/android-source",
 "modulePaths" : [
 "frameworks",
 "packages/inputmethods"
 ],
 "lunchTarget" : "aosp_cf_x86_64_phone-trunk_staging-eng",
 "nativeConfig" : {
 "excludePaths" : [ ],
 "excludeGenPaths" : [ ]
 },
 "syncConfig" : {
 "environmentVars" : { },
 "buildFlags" : [ ]
 }}

参考内容和资料:

  1. Cuttlefish 官方文档: https://source.android.com/docs/devices/cuttlefish
  2. Cuttlefish官方Repo: https://github.com/google/android-cuttlefish
  3. Bazel用户指南:https://bazel.build/docs/user-manual
  4. Android Cuttlefish emulator: https://2net.co.uk/blog/cuttlefish-android12.html

看完评论一下吧

  •  

读《鱼不存在》摘抄

最近因为孟岩在无人知晓播客中推荐了鱼不存在这本书,于是在微信读书中把它很快给读完了。

书不长,挺快就看完了。作者露露·米勒因为自己出轨和男友分手,自己的生活一团糟,所以她才开始研究起大卫·斯塔尔·乔丹。

从书的前半部分我知道了大卫是一名分类学家,同时还是斯坦福大学的首任校长。他的一生在追求建立秩序,小的时候他研究植物的分类,长大了开始研究鱼的分类,给鱼命名。经历了地震之后,他顽强的恢复,并通过用针把鱼和铭牌缝到一起来恢复秩序。

而书的后半部分,则转到了大卫的另一面,他推崇优生学,参与推动美国把优生绝育写到法律当中,作者崇拜的大卫完全变成了另一个人,而这要比希特勒的纳粹理论还要早,并且给纳粹提供了理论。而很多人因为大卫倡导的优生理论被进行绝育或者被歧视,作者访问了其中的两位。而大卫本人却获得了成功的一生。 到书的最后,作者又发现了鱼这个分类被最新的支序分类学判定为不存在,如果这么说的话,大卫的工作是否就不存在了。 书中穿插着对于大卫的描述,也包含了作者自己生活的描述,以及她对于大卫痕迹的追寻。

世界是无序和混沌的,我们每个人都很渺小,大卫用他的力量去构建他认为的秩序,而这也是他做很多事情的动力。做为个体的我们需要接受自己的渺小和日常的混沌。

以下为内容摘抄:

◆ 科学价值与美学趣味不同,前者的特质之一就是关注隐秘角落里微不足道的事物。

◆ 生命没有意义,无所谓意义

◆ 混乱是我们唯一的统治者。

◆ 这就是大卫·斯塔尔·乔丹吸引我的原因。我想知道,是什么驱使他不断举起缝衣针修补世界的混乱,罔顾所有告诫他不会成功的警示。他是否偶然发现了一些技巧,一剂充满希望的解药,用以消除世界的漠然?他是个科学家,所以他的坚持不懈背后也许有什么东西,能够与爸爸的世界观契合,我紧紧抓住这一丝微弱的可能性。或许他发现了关键:如何在毫无希望的世界里拥有希望,如何在黑暗的日子里继续前行,如何在没有上帝支持的时候坚持信念。

◆ 那是舌尖上的蜜糖、无所不能的幻想、秩序带来的愉悦感

◆ 科学世界观的问题在于,当你用它来探寻生活的意义时,它只会告诉你一件事:徒劳无功。

◆ “由此观之,生命何等壮丽恢宏。”

◆ 不可摧毁之物与乐观毫无关系,相反,它比乐观更深刻,处于意识的更深处。不可摧毁之物是我们用各种符号、希望和抱负粉饰的东西,并不要求我们看清它真实的模样。

◆ 学会换一种方式看待发生在自己身上的事情之后,那些经历创伤的人能够更快获得内心的平静。

◆ 我们行走在人世间,心里明白这个世界根本不在意我们的死活,不管我们如何努力,都不一定能够成功。

◆ 我们时刻在同数十亿人竞争,在自然灾害面前毫无还手之力,而我们热爱的每一件事物最终都会走向毁灭的结局

◆ 其中最重要的特质就是遭遇挫折后继续前进的能力,即便没有任何证据显示你的目标有可能实现,你也能不断地奋勇向前

◆ 在混乱的旋涡中,那残酷无情的真相昭然若揭:你无关紧要。

◆ 从星辰、永恒或优生学视角下的完美状态来看,一个人的生命似乎无关紧要,我们不过是一颗微粒上的一颗微粒上的一颗微粒,转瞬即逝。但这也只是无尽观点中的一个观点而已。在弗吉尼亚州林奇堡的一套公寓里,一个看似无关紧要的人会变得意义重大。她是替身母亲,是欢笑之源,她支撑着另一个人度过最黑暗的时光。

◆ 鱼不存在,“鱼类”并不存在。这个对大卫至关重要的分类,他陷入困境之时寻求慰藉的种类,他穷尽一生想看清的物种,根本不存在。

◆ 我们对周围的世界知之甚少,即便对脚边最简单的事物也缺乏了解。我们曾经犯过错,之后还会继续犯错。真正的发展之路并非由确定无疑铺就,而是由疑问筑成,因此需要保持“接受更正”的状态。

关于我的读后感,自认为写的不好,这本书的题材很吸引人,内容既有科普,又有关于大卫的传记,又有关于人生意义和哲理的思考,感兴趣还是要自己去读一读这本书。

最后附上孟岩这期播客的地址:https://www.xiaoyuzhoufm.com/episode/6720836fbad346ebe6399017

看完评论一下吧

  •  

姗姗来迟的十月月报

本来准备一号就写的十月月报,然而在路上写了几笔就放下了,拖到了3号才写出来。本月主要介绍国庆出行,观看小宇宙播客漫游日,买了新NAS等。

国庆节回了一趟媳妇老家,在安庆附近,周边玩的很多,之前已经去过了不少地方,也是担心人多,这次只去了安庆看了博物馆和附近新开的集贤时空,之前的文章都写过.另外去亲戚家的时候,发现周围有个石屋寺,便开车去看了一下顺便爬了那边的大青山,听家长说这个寺庙不是每天都开的,因此我们过去的时候并不太热闹,正殿的门也都是关上的.大青山名字很大,其实也只是一个小山,爬到山顶可以看到远处的长江和长江大桥风景倒是还不错。以下是在那边拍的几张照片。

国庆回去之前就已经购买了一些木工的工具,于是国庆在家的几天用家里的旧木头做了个丑木凳,木工还未能入门,下次回家继续练习。

其余的几个周末,上海基本都是阴雨天,本来计划的外出爬山也只能作罢。而其中的某一个周末,小宇宙在上生新所举行播客漫游日,这个地方没有去过,便带着老婆小孩前往打卡,即参与了小宇宙的活动,也打卡了这边有名的茑屋书店。

小宇宙的活动搞得很不错,现场气氛很浓,有播客主播现场开讲,围了许多观众,非常热闹,而一些专场因为没有提前预约因此没有机会去旁听。

这个活动利用手机的NFC功能来进行现场互动,包括展示个人信息,交换贴纸等,很有意思。另外还设置了几个打卡点进行电子印章打开收集贴纸,这个形式也挺新颖的。在现场通过NFC获取个人数据大屏展示,很有意思,我的播客收听记录如下。

折腾

趁着双十一终于入手了一台成品NAS,型号为TS464C2,详情点击链接了解,除了稳文中提到的服务外,另外和搭建了AlistSun-Panel,前者主要为了接入阿里云盘到NAS,后者则是将众多的内网服务统一放大一个页面上去,它还支持内外网不同的链接,目前使用下来体验不错,下面是我的导航页展示。

另外为了给娃腾个书桌,我在用的桌子给娃了,自己换了一个更大一点的松木桌,不过要提醒大家,淘宝上面400以下的松木桌子购买需慎重,这个油漆气味是真不小。弄了新桌子,电脑,路由器等等各种线路也重新整理了一下。

之前使用Google Sheets也只是简单使用了它的表格功能,最近想要做一个数据统计功能,就问了一下ChatGpt,结果发现Google Sheets可以自己写脚本来更新数据以及和表格进行交互,而最后借助于GPT也实现了相关功能,只能大呼一声牛逼。虽然最近几年各种的airtable,多维表格很流行,看起来他们能够实现的功能,大多数通过Google Sheets依然能够实现,如果你感兴趣也可以去看看Google Apps Script,语法基本和JavaScript差不多。

之前立的学习英语的Flag,这个月坚持了十天就放弃了,其中一个原因是多邻国不太合适,过于简单并且这个方式感觉不太适合我,后面需要找找别的方式学习。

Android源码方面,这个月分析了广播接收器相关和消息循环相关的代码,后面因为更新Android源码到Android 15以及更新Asfp导致新代码阅读有点问题,后面10天内就没有看代码了。

另外业余时间,这个学还开始看了看Rust,这是一门性能媲美C++的语言,它又通过所有权来解决了内存回收的问题,目前还只是了解了一些基础语法、一些基本的使用,后面需要继续学习。为什么要学它呢,因为现在很多地方都引入了它,Android系统源码中也能够看到Rust的身影,很多的系统模块未来也会采用它来进行开发。

这个月影视的主题是漫威,周末下雨待在家的时间把复仇者联盟四部,钢铁侠两部,雷神四部,重新看了一遍,大人小孩都喜欢。之前有的部分并没看,有些剧情因为隔了很久都忘记了,这次算是重新温习了一遍。

《逆行人生》上线了流媒体平台,也抽空看了一下,同样作为大龄程序员,有家要养,看得我鸭梨山大。但是电影部分的内容倒是不大真实,现在的就业环境确实差,但也不至于找不到工作的程度。另外外卖这个行业也不如电影中这么卷,在某天路上与外卖员闲聊后,甚至我都有了去体验一下的冲动,但是听说众包都是垃圾单,还没开始就直接放弃了。

《读库2404》中的文章还有许多没看,这个月看了其中的《我在郑州跑代驾》和《我在上海开出租》,《我在郑州跑代驾》为作者找不到工作后通过代驾赚钱的经历,其中了解到了赚钱的艰辛,《我在上海开出租》则更多是关于从司机的视角看到的乘客众生相。对于我们来说,虽然没有从事相关职业,不过也是了解行业侧面的机会。

上个月看了一半的《简读中国史》看完之后,这个月开始看桥水基金创始人达利欧创作的《原则》一书。这本书也算是一本脍炙人口的畅销书了。目前已经看完他的经历介绍和生活原则的部分,从他的经历了解了他如何白手起家以及他经历的挫折,以及他如达达成现在的成就。这本书就是他介绍的他的帮助他成功的一些践行原则,从目前已经看的部分我的最大感悟就是,做人做事要保持谦逊和心胸开阔,做事情可以遵循五步流程,从而不断的通过失败和问题来驱动自己进步,与人相处要理解人与人的不同。而他所说的原则,其实很多方面可以看到和国内的很多互联网大厂所提倡的文化观或者企业价值其实有很多相似点。目前我对于本书的理解还比较粗浅,仍需要等待读完一遍之后再次进行研读。

总结

在农村待在的国庆节是放松闲适的,回到城市后又进入紧张的工作当中。月初的股市对于我们所有人来说都是当头棒喝,而其后则算是恢复其常态。折腾没有尽头,入了NAS,算是集齐了中年人三宝(NAS,路由,充电头)。

这是我的第五个月月报,感谢你的浏览,下月再会。

看完评论一下吧

  •  

威联通NAS购入初体验以及设置记录

之前是用树莓派连个两盘位硬盘盒运行一些服务,由于它的稳定性加上容量不够,一直想弄一个NAS,趁着双十一到来,就入手了威联通的NAS,本文介绍 一下购入的抉择以及NAS的初始化和相关的设置。

缘起

NAS这个东西知道了很多年了,一直想要搞一个,迫于家里花费的紧张,之前一直是使用一台树莓派4B,其中刷了Openwrt系统,挂载了两块盘的硬盘盒,其中开启了Webdav, Samba,Jellyfin相关的东西。不过因为Jellyfin挂载阿里云盘速度不太理想,有不少视频还是下载到自己的硬盘里面的。同时内,硬盘也出现了拷贝大文件就出现问题,需要重启硬盘盒和系统的问题,这个后续会继续说。

DIY NAS硬件或者成品的NAS也关注了有挺长一段时间,迫于以上问题,以及文件越来越多,当时买的这两块2T的硬盘,容量已经不够用了,想要购买一个NAS的想法更加加强,终于决定今年双十一搞个NAS。

剁手

购买NAS是有两个选择,自己组装硬件,安装飞牛或者黑群晖等NAS系统,又或者购买群晖、威联通等成品NAS。在V2EX发帖求助,以及自己的纠结中,最终在性价比和稳定性等各种因素比较之后,选择入手了威联通TS464C2。

威联通的系统虽然被大家诟病许久,但是它也算是市场上除了群晖之外NAS系统做的最久的厂家了,考虑到文件的安全可靠在文件系统和系统稳定性上面,这两家还是要比国内的新起之辈更加值得信赖的。而我选择的这一款,支持内存扩展,如果以后服务比较多,可以再增加一根内存。4个3.5寸硬盘位加上两个NVME 硬盘位,对于容量的扩展应该很多年都不存在问题了。双十一这块机器只要2000块钱就拿下,而群晖同配置的4盘位差不多要四千,只能说高攀不起。

另外下单了一块国产的NVME 2T硬盘,加入Qtier存储池,希望能提高一些速度。为了拥有更大的容量,经过一些研究,淘宝购入了一块2手服务器硬盘,型号为HC550, 16TB,回来看Smart信息,已经运行了786天,不过其他信息看着都不错。

上电

收到机器,插上硬盘,参照指南开始初始化。威联通提供了比较友好的初始化方法,可以通过网页或者应用对它进行初始化,不过一定要插上硬盘才能开始这一切。

根据指南初始化之后,开始了硬盘的初始化和存储池的设置。之前使用的openwrt中对于硬盘的管理是比较简单的,基本就是实现了基础的Linux功能,把磁盘挂载到指定的目录,硬盘初始化之类的。而QNAP中,“存储与快照总管应用”中,对于硬盘和存储卷的设置则全面,可以设置各种raid的存储池,Qtier,快照,卷等等,也有硬盘的运行情况的显示。我想这就是选择大厂成品NAS的原因,毕竟docker之类的东西大家都很容易做,但是这种积累了很多年的东西不是那么快能够做出来的。

安装软件

在威联通NAS中安装软件可以选择从QNAP的应用中心安装应用,也可以安装Container Station之后通过docker来安装。不过官方的应用中心中的应用中主要是一些官方提供的应用,这个时候我们可以选择第三方的应用中心,这里我推荐一个: https://www.myqnap.org/repo.xml,官方应用商店没有的可以来这里试试。不过这个应用商店中的部分应用是收费的,比如Jellyfin,它提供的版本需要依赖Apache,这个时候你需要去它的网站上面购买,价格还不便宜,当然我是不会去购买的。

除了应用中心安装之外,我们还可以去网上找QPKG文件,比如Jellyfin,我就是使用的pdulvp为QNAP定制的版本,下载地址在:https://github.com/pdulvp/jellyfin-qnap/releases。Jellyfin我不使用官方的docker版本有两个原因,一是使用这个定制版本,可以方便的使用英特尔的集成显卡进行视频的硬解,另一方面是使用docker的化,默认只能映射一个媒体目录到Docker中,想要映射多个目录会麻烦一点,因此使用QPKG更方便。

对于其他的应用,比如FreshRss, VaultWarden则是选择了使用Docker进行部署,Container Station的Docker部署为先写一个compose文件,之后软件会帮助下载相关的容器进行启动,这个有个问题就是创建完compose之后,容器启动起来之后,在web界面上就没法编辑compose文件了,想要编辑的需要用ssh进终端去看,比如我创建的app-freshrss它的compose文件就在/share/Container/container-station-data/application/app-freshrss当中。

另外威联通自带的一些应用,文件管理,QuMagie,特别要说一下QuMagie,它已经可以帮我把相片按照人物,地点,物品等等分类好了,配合上手机App使用流畅很多,再也不用之前那样使用SMB手动同步了。

其他

目前用了这个二手服务其硬盘加上新买的固态硬盘组了一个Qtier池作为主要存储区,家里有块旧的sata固态硬盘就把他搞成高速缓存加速了。原来的两块酷狼硬盘都是EXT4格式,但是插到QNAP上却不能识别,只好放在原来的设备上,新NAS通过Samba访问原先的设备,把文件拷贝过来。

之后把旧的硬盘插上来使用才发现,其中一个硬盘出现了坏道,数量还不少,感觉它应该命不久矣,不敢放什么东西上来了。 而另一块好的硬盘,准备把它作为备份盘,相片,笔记和其他的一些重要文件都定期备份到这个盘上面。因为硬盘数量优先,并没有组RAID还是空间优先,只把重要的文件备份但愿以后不会踩坑。

以上就是这个新NAS的初体验了,后面还要继续增加新的应用,仍然需要摸索,外网访问仍然沿用家里的DDNS和端口转发。目前才用了不到一个星期,还有很多东西没有用到没有涉及,后面有新的体验来再继续写文章分享,也欢迎玩NAS网友一起交流分享,如果有好玩的应用也欢迎评论推荐给我。

看完评论一下吧

  •  

Android源码分析:广播接收器注册与发送广播流程解析

广播,顾名思义就是把一个信息传播出去,在Android中也提供了广播和广播接收器BroadcastReceiver,用来监听特定的事件和发送特定的消息。不过广播分为全局广播和本地广播,本地广播是在Android Jetpack库中所提供,其实现也是基于Handler和消息循环机制,并且这个类Android官方也不推荐使用了。我们这里就来看看Android全局的这个广播。

应用开发者可以自己发送特定的广播,而更多场景则是接收系统发送的广播。注册广播接收器有在AndroidManifest文件中声明和使用代码注册两种方式,在应用的target sdk大于等于Android 8.0(Api Version 26)之后,系统会限制在清单文件中注册。通过清单方式注册的广播,代码中没有注册逻辑,只有PMS中读取它的逻辑,我们这里不进行分析。

注册广播接收器

首先是注册广播接收器,一般注册一个广播接收器的代码如下:

1
2
3
val br: BroadcastReceiver = MyBroadcastReceiver()
val filter = IntentFilter(ACTION_CHARGING)
activity.registerReceiver(br, filter)

使用上面的代码就能注册一个广播接收器,当手机开始充电就会收到通知,会去执行MyBroadcastReceiveronReceive方法。

那我们就从这个registerReceiver来时往里面看,因为Activity是Context的子类,这个注册的方法的实现则是在ContextImpl当中,其中最终调用的方法为registerReceiverInternal,代码如下:

 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
private Intent registerReceiverInternal(BroadcastReceiver receiver, int userId,
 IntentFilter filter, String broadcastPermission,
 Handler scheduler, Context context, int flags) {
 IIntentReceiver rd = null;
 if (receiver != null) {
 if (mPackageInfo != null && context != null) {
 if (scheduler == null) {
 scheduler = mMainThread.getHandler();
 }
 rd = mPackageInfo.getReceiverDispatcher(
 receiver, context, scheduler,
 mMainThread.getInstrumentation(), true);
 } else {
 ...
 }
 }
 try {
 ActivityThread thread = ActivityThread.currentActivityThread();
 Instrumentation instrumentation = thread.getInstrumentation();
 if (instrumentation.isInstrumenting()
 && ((flags & Context.RECEIVER_NOT_EXPORTED) == 0)) {
 flags = flags | Context.RECEIVER_EXPORTED;
 }
 final Intent intent = ActivityManager.getService().registerReceiverWithFeature(
 mMainThread.getApplicationThread(), mBasePackageName, getAttributionTag(),
 AppOpsManager.toReceiverId(receiver), rd, filter, broadcastPermission, userId,
 flags);
 if (intent != null) {
 intent.setExtrasClassLoader(getClassLoader());
 intent.prepareToEnterProcess(
 ActivityThread.isProtectedBroadcast(intent),
 getAttributionSource());
 }
 return intent;
 } catch (RemoteException e) {
 ...
 }
}

我们在注册广播的时候只传了两个参数,但是实际上它还可以传不少的参数,这里userId就是注册的用户id,会被自动 填充成当前进程的用户Id,broadcastPermission表示这个广播的权限,也就是说需要有该权限的应用发送的广播,这个接收者才能接收到。scheduler就是一个Handler,默认不传,在第8行可以看到,会拿当前进程的主线程的Handlerflag是广播的参数,这里比较重要的就是RECEIVER_NOT_EXPORTED,添加了它则广播不会公开暴露,其他应用发送的消息不会被接收。

在第10行,这里创建了一个广播的分发器,在24行,通过AMS去注册广播接收器,只有我们的broadcast会用到contentprovider或者有sticky广播的时候,30行才会执行到,这里跳过。

获取广播分发器

首先来看如何获取广播分发器,这块的代码在LoadedApk.java中,代码如下:

 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
public IIntentReceiver getReceiverDispatcher(BroadcastReceiver r,
 Context context, Handler handler,
 Instrumentation instrumentation, boolean registered) {
 synchronized (mReceivers) {
 LoadedApk.ReceiverDispatcher rd = null;
 ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher> map = null;
 if (registered) {
 map = mReceivers.get(context);
 if (map != null) {
 rd = map.get(r);
 }
 }
 if (rd == null) {
 rd = new ReceiverDispatcher(r, context, handler,
 instrumentation, registered);
 if (registered) {
 if (map == null) {
 map = new ArrayMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>();
 mReceivers.put(context, map);
 }
 map.put(r, rd);
 }
 } else {
 rd.validate(context, handler);
 }
 rd.mForgotten = false;
 return rd.getIIntentReceiver();
 }
}

先来说一下mReceivers,它的结构为ArrayMap<Context, ArrayMap<BroadcastReceiver, ReceiverDispatcher>>,也就是嵌套了两层的ArrayMap,外层是以Context为key,内层以Receiver为key,实际存储的为ReceiverDispatcherReceiverDispatcher内部所放的IIntentReceiver比较重要,也就是我们这个方法所返回的值,它实际是IIntentReceiver.Stub,也就是它的Binder实体类。

这段代码的逻辑也比较清晰,就是根据ContextReceiver到map中去查找看是否之前注册过,如果注册过就已经有这个Dispatcher了,如果没有就创建一个,并且放到map中去,最后返回binder对象出去。

AMS注册广播接收器

在AMS注册的代码很长,我们这里主要研究正常的普通广播注册,关于黏性广播,instantApp的广播,以及广播是否导出等方面都省略不予研究。以下为我们关注的核心代码:

 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
public Intent registerReceiverWithFeature(IApplicationThread caller, String callerPackage,
 String callerFeatureId, String receiverId, IIntentReceiver receiver,
 IntentFilter filter, String permission, int userId, int flags) {
 ...
 synchronized(this) {
 ReceiverList rl = mRegisteredReceivers.get(receiver.asBinder());
 if (rl == null) {
 rl = new ReceiverList(this, callerApp, callingPid, callingUid,
 userId, receiver);
 if (rl.app != null) {
 final int totalReceiversForApp = rl.app.mReceivers.numberOfReceivers();
 if (totalReceiversForApp >= MAX_RECEIVERS_ALLOWED_PER_APP) {
 throw new IllegalStateException("Too many receivers, total of "
 + totalReceiversForApp + ", registered for pid: "
 + rl.pid + ", callerPackage: " + callerPackage);
 }
 rl.app.mReceivers.addReceiver(rl);
 } else {
 try {
 receiver.asBinder().linkToDeath(rl, 0);
 } catch (RemoteException e) {
 return sticky;
 }
 rl.linkedToDeath = true;
 }
 mRegisteredReceivers.put(receiver.asBinder(), rl);
 } else {
 // 处理userId, uid,pid 等不同的错误
 }

 BroadcastFilter bf = new BroadcastFilter(filter, rl, callerPackage, callerFeatureId,
 receiverId, permission, callingUid, userId, instantApp, visibleToInstantApps,
 exported);
 if (rl.containsFilter(filter)) {
 } else {
 rl.add(bf);
 mReceiverResolver.addFilter(getPackageManagerInternal().snapshot(), bf);
 }
 }
 ...
}

在前面ContextImpl中调用AMS注册Reciever的地方,我们传的就是Receiver的Binder实体,这里拿到的是binder引用。在代码中我们可以看到,首先会以我们传过来的receiver的binder对象为key,到mRegisterReceivers当中去获取ReceiverList,这里我们就知道receiver在System_server中是怎样存储的了。如果AMS当中没有,会去创建一个ReceiverList并放置到这个map当中去,如果存在则不需要做什么事情。但是这一步只是放置了Receiver,而我们的Receiver对应的关心的IntentFilter还没使用,这里就需要继续看31行的代码了。在这里这是使用了我们传过来的IntentFilter创建了一个BroadcastFilter对象,并且把它放到了ReceiverList当中,同时还放到了mReceiverResolver当中,这个对象它不是一个Map而是一个IntentResolver,其中会存储我们的BroadcastFilter,具体这里先不分析了。 BroadcastReceiver 存放结构

到这里我们就看完了广播接收器的注册,在App进程和System_Server中分别将其存储,具体两边的数据结构如上图所示。这里可以继续看看发送广播的流程了。

发送广播

一般我们发送广播会调用如下的代码:

1
2
3
4
5
Intent().also { intent -> 
 intent.setAction("com.example.broadcast.MY_NOTIFICATION") 
 intent.putExtra("data", "Nothing to see here, move along.") 
 activity.sendBroadcast(intent)
}

我们通过设置Action来匹配对应的广播接收器,通过设置Data或者Extra,这样广播接收器中可以接收到对应的数据,最后调用sendBroadcast来发送。而sendBroadcast的实现也是在ContextImpl中,源码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Override
public void sendBroadcast(Intent intent) {
 warnIfCallingFromSystemProcess();
 String resolvedType = intent.resolveTypeIfNeeded(getContentResolver());
 try {
 intent.prepareToLeaveProcess(this);
 ActivityManager.getService().broadcastIntentWithFeature(
 mMainThread.getApplicationThread(), getAttributionTag(), intent, resolvedType,
 null, Activity.RESULT_OK, null, null, null, null /*excludedPermissions=*/,
 null, AppOpsManager.OP_NONE, null, false, false, getUserId());
 } catch (RemoteException e) {
 throw e.rethrowFromSystemServer();
 }
}

这里代码比较简单,就是直接调用AMS的broadcastIntentWithFeature来发送广播。

AMS发送广播

这里我们可以直接看AMS中的broadcastIntentWithFeature的源码:

 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
@Override
public final int broadcastIntentWithFeature(IApplicationThread caller, String callingFeatureId,
 Intent intent, String resolvedType, IIntentReceiver resultTo,
 int resultCode, String resultData, Bundle resultExtras,
 String[] requiredPermissions, String[] excludedPermissions,
 String[] excludedPackages, int appOp, Bundle bOptions,
 boolean serialized, boolean sticky, int userId) {
 enforceNotIsolatedCaller("broadcastIntent");
 synchronized(this) {
 intent = verifyBroadcastLocked(intent);

 final ProcessRecord callerApp = getRecordForAppLOSP(caller);
 final int callingPid = Binder.getCallingPid();
 final int callingUid = Binder.getCallingUid();

 final long origId = Binder.clearCallingIdentity();
 try {
 return broadcastIntentLocked(callerApp,
 callerApp != null ? callerApp.info.packageName : null, callingFeatureId,
 intent, resolvedType, resultTo, resultCode, resultData, resultExtras,
 requiredPermissions, excludedPermissions, excludedPackages, appOp, bOptions,
 serialized, sticky, callingPid, callingUid, callingUid, callingPid, userId);
 } finally {
 Binder.restoreCallingIdentity(origId);
 }
 }
}

第10行代码,主要验证Intent,比如检查它的Flag,检查它是否传文件描述符之类的,里面的代码比较简单清晰,这里不单独看了。后面则是获取调用者的进程,uid,pid之类的,最后调用broadcastIntentLocked,这个方法的代码巨多,接近1000行代码,我们同样忽略sticky的广播,也忽略顺序广播,然后来一点一点的看:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
//ActivityManagerService.java 
//final int broadcastIntentLocked(...)
intent = new Intent(intent);
intent.addFlags(Intent.FLAG_EXCLUDE_STOPPED_PACKAGES);
if (!mProcessesReady && (intent.getFlags()&Intent.FLAG_RECEIVER_BOOT_UPGRADE) == 0) {
 intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
}
userId = mUserController.handleIncomingUser(callingPid, callingUid, userId, true,
 ALLOW_NON_FULL, "broadcast", callerPackage);
final String action = intent.getAction();

首先这里的代码是对Intent做一下封装,并且如果系统还在启动,不允许启动应用进程,以及获取当前的用户ID,大部分情况下,我们只需要考虑一个用户的情况。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
if (action != null) {
 ...
 switch (action) {
 ...
 case Intent.ACTION_PACKAGE_DATA_CLEARED:
 {
 Uri data = intent.getData();
 String ssp;
 if (data != null && (ssp = data.getSchemeSpecificPart()) != null) {
 mAtmInternal.onPackageDataCleared(ssp, userId);
 }
 break;
 }
 case Intent.ACTION_TIMEZONE_CHANGED:
 mHandler.sendEmptyMessage(UPDATE_TIME_ZONE);
 break;
 ...
 }
}

对于一些系统的广播事件,除了要发送广播给应用之外,在AMS中,还会根据其广播,来调用相关的服务或者执行相关的逻辑,也会在这里调用其代码。这里我罗列了清除应用数据和时区变化两个广播,其他的感兴趣的可以自行阅读相关代码。

1
2
3
4
5
6
int[] users;
if (userId == UserHandle.USER_ALL) {
 users = mUserController.getStartedUserArray();
} else {
 users = new int[] {userId};
}

以上代码为根据前面拿到的userId,来决定广播要发送给所有人还是仅仅发送给当前用户,并且把userId保存到users数组当中。

获取广播接收者

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
List receivers = null;
List<BroadcastFilter> registeredReceivers = null;
if ((intent.getFlags() & Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
 receivers = collectReceiverComponents(
 intent, resolvedType, callingUid, users, broadcastAllowList);
}
if (intent.getComponent() == null) {
 final PackageDataSnapshot snapshot = getPackageManagerInternal().snapshot();
 if (userId == UserHandle.USER_ALL && callingUid == SHELL_UID) {
 ...
 } else {
 registeredReceivers = mReceiverResolver.queryIntent(snapshot, intent,
 resolvedType, false /*defaultOnly*/, userId);
 }
}

以上为获取我们注册的所有的接收器的代码,其中FLAG_RECEIVER_REGISTERED_ONLY意味着仅仅接收注册过的广播,前面在判断当前系统还未启动完成的时候有添加这个FLAG,其他情况一般不会有这个Flag,这里我们按照没有这个flag处理。那也就会执行第4行的代码。另外下面还有从mReceiverResolver从获取注册的接收器的代码,因为大部分情况不是从shell中执行的,因此也忽略了其代码。

首先看collectReceiverComponents的代码:

 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
private List<ResolveInfo> collectReceiverComponents(Intent intent, String resolvedType,
 int callingUid, int[] users, int[] broadcastAllowList) {
 int pmFlags = STOCK_PM_FLAGS | MATCH_DEBUG_TRIAGED_MISSING;

 List<ResolveInfo> receivers = null;
 HashSet<ComponentName> singleUserReceivers = null;
 boolean scannedFirstReceivers = false;
 for (int user : users) {
 List<ResolveInfo> newReceivers = mPackageManagerInt.queryIntentReceivers(
 intent, resolvedType, pmFlags, callingUid, user, true /* forSend */); //通过PMS,根据intent和uid读取Manifest中注册的接收器
 if (user != UserHandle.USER_SYSTEM && newReceivers != null) {
 for (int i = 0; i < newReceivers.size(); i++) {
 ResolveInfo ri = newReceivers.get(i);
 //如果调用不是系统用户,移除只允许系统用户接收的接收器
 if ((ri.activityInfo.flags & ActivityInfo.FLAG_SYSTEM_USER_ONLY) != 0) {
 newReceivers.remove(i);
 i--;
 }
 }
 }
 // 把别名替换成真实的接收器 
 if (newReceivers != null) {
 for (int i = newReceivers.size() - 1; i >= 0; i--) {
 final ResolveInfo ri = newReceivers.get(i);
 final Resolution<ResolveInfo> resolution =
 mComponentAliasResolver.resolveReceiver(intent, ri, resolvedType,
 pmFlags, user, callingUid, true /* forSend */);
 if (resolution == null) {
 // 未找到对应的接收器,删除这个记录 
 newReceivers.remove(i);
 continue;
 }
 if (resolution.isAlias()) {
 //找到对应的真实的接收器,就把别名的记录替换成真实的目标
 newReceivers.set(i, resolution.getTarget());
 }
 }
 }
 if (newReceivers != null && newReceivers.size() == 0) {
 newReceivers = null;
 }

 if (receivers == null) {
 receivers = newReceivers;
 } else if (newReceivers != null) {
 if (!scannedFirstReceivers) {
 //查找单用户记录的接收器,并且保存
 scannedFirstReceivers = true;
 for (int i = 0; i < receivers.size(); i++) {
 ResolveInfo ri = receivers.get(i);
 if ((ri.activityInfo.flags&ActivityInfo.FLAG_SINGLE_USER) != 0) {
 ComponentName cn = new ComponentName(
 ri.activityInfo.packageName, ri.activityInfo.name);
 if (singleUserReceivers == null) {
 singleUserReceivers = new HashSet<ComponentName>();
 }
 singleUserReceivers.add(cn);
 }
 }
 }
 for (int i = 0; i < newReceivers.size(); i++) {
 ResolveInfo ri = newReceivers.get(i);
 if ((ri.activityInfo.flags & ActivityInfo.FLAG_SINGLE_USER) != 0) {
 ComponentName cn = new ComponentName(
 ri.activityInfo.packageName, ri.activityInfo.name);
 if (singleUserReceivers == null) {
 singleUserReceivers = new HashSet<ComponentName>();
 }
 if (!singleUserReceivers.contains(cn)) {
 //对于单用户的接收器,只存一次到返回结果中
 singleUserReceivers.add(cn);
 receivers.add(ri);
 }
 } else {
 receivers.add(ri);
 }
 }
 }
 }
 ...
 return receivers;
}

以上就根据信息通过PMS获取所有通过Manifest静态注册的广播接收器,对其有一些处理,详见上面的注释。

对于我们在代码中动态注册的接收器,则需要看mReceiverResolver.queryIntent的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
protected final List<R> queryIntent(@NonNull PackageDataSnapshot snapshot, Intent intent,
 String resolvedType, boolean defaultOnly, @UserIdInt int userId, long customFlags) {
 ArrayList<R> finalList = new ArrayList<R>();
 F[] firstTypeCut = null;
 F[] secondTypeCut = null;
 F[] thirdTypeCut = null;
 F[] schemeCut = null;

 if (resolvedType == null && scheme == null && intent.getAction() != null) {
 firstTypeCut = mActionToFilter.get(intent.getAction());
 }

 FastImmutableArraySet<String> categories = getFastIntentCategories(intent);
 Computer computer = (Computer) snapshot;
 if (firstTypeCut != null) {
 buildResolveList(computer, intent, categories, debug, defaultOnly, resolvedType,
 scheme, firstTypeCut, finalList, userId, customFlags);
 }
 sortResults(finalList); //按照IntentFilter的priority优先级降序排序
 return finalList;
}

以上代码中,这个mActionToFilter就是我们前面注册广播时候,将BroadcastFilter添加进去的一个ArrayMap,这里会根据Action去其中取出所有的BroadcastFilter,之后调用buildResolveList将其中的不符合本次广播接收要求的广播接收器给过滤掉,最后按照IntentFilter的优先级降序排列。

到这里我们就有两个列表receivers存放Manifest静态注册的将要本次广播接收者,和registeredReceivers通过代码手动注册的广播接收者。

广播入队列

首先来看通过代码注册的接收器不为空,并且不是有序广播的情况,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
int NR = registeredReceivers != null ? registeredReceivers.size() : 0;
if (!ordered && NR > 0) {
 ...
 final BroadcastQueue queue = broadcastQueueForIntent(intent);
 BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,
 callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
 requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
 registeredReceivers, resultTo, resultCode, resultData, resultExtras, ordered,
 sticky, false, userId, allowBackgroundActivityStarts,
 backgroundActivityStartsToken, timeoutExempt);
 ...
 final boolean replaced = replacePending
 && (queue.replaceParallelBroadcastLocked(r) != null);
 if (!replaced) {
 queue.enqueueParallelBroadcastLocked(r);
 queue.scheduleBroadcastsLocked();
 }
 registeredReceivers = null;
 NR = 0;
}

在这里,第4行会首先根据intent的flag获取对应的BroadcastQueue,这里有四个Queue,不看其代码了,不过逻辑如下:

  1. 如果有FLAG_RECEIVER_OFFLOAD_FOREGROUND 标记,则使用mFgOffloadBroadcastQueue
  2. 如果当前开启了offloadQueue,也就是mEnableOffloadQueue,并且有FLAG_RECEIVER_OFFLOAD标记,则使用mBgOffloadBroadcastQueue
  3. 如果有FLAG_RECEIVER_FOREGROUND,也就是前台时候才接收广播,则使用mFgBroadcastQueue
  4. 如果没有上述标记,则使用mBgBroadcastQueue。 拿到queue之后,会创建一条BroadcastRecord,其中会记录传入的参数,intent,以及接收的registeredReceivers,调用queue的入队方法,最后把registeredReceivers设置为null,计数也清零。具体入队的代码,我们随后再看,这里先看其他情况下的广播入队代码。
 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
int ir = 0;
if (receivers != null) {
 String skipPackages[] = null;
 //对于添加应用,删除应用数据之类的广播,不希望变化的应用能够接收到对应的广播
 //这里设置忽略它们
 if (Intent.ACTION_PACKAGE_ADDED.equals(intent.getAction())
 || Intent.ACTION_PACKAGE_RESTARTED.equals(intent.getAction())
 || Intent.ACTION_PACKAGE_DATA_CLEARED.equals(intent.getAction())) {
 Uri data = intent.getData();
 if (data != null) {
 String pkgName = data.getSchemeSpecificPart();
 if (pkgName != null) {
 skipPackages = new String[] { pkgName };
 }
 }
 } else if (Intent.ACTION_EXTERNAL_APPLICATIONS_AVAILABLE.equals(intent.getAction())) {
 skipPackages = intent.getStringArrayExtra(Intent.EXTRA_CHANGED_PACKAGE_LIST);
 }
 if (skipPackages != null && (skipPackages.length > 0)) {
 //如果Manifest注册的广播接收器的包名和skip的一样,那就移除它们
 for (String skipPackage : skipPackages) {
 if (skipPackage != null) {
 int NT = receivers.size();
 for (int it=0; it<NT; it++) {
 ResolveInfo curt = (ResolveInfo)receivers.get(it);
 if (curt.activityInfo.packageName.equals(skipPackage)) {
 receivers.remove(it);
 it--;
 NT--;
 }
 }
 }
 }
 }

 int NT = receivers != null ? receivers.size() : 0;
 int it = 0;
 ResolveInfo curt = null;
 BroadcastFilter curr = null;
 while (it < NT && ir < NR) {
 if (curt == null) {
 curt = (ResolveInfo)receivers.get(it);
 }
 if (curr == null) {
 curr = registeredReceivers.get(ir);
 }
 if (curr.getPriority() >= curt.priority) {
 //如果动态注册的广播优先级比静态注册的等级高,就把它添加到静态注册的前面。
 receivers.add(it, curr);
 ir++;
 curr = null;
 it++;
 NT++;
 } else {
 // 如果动态注册的广播优先级没有静态注册的等级高,那就移动静态注册的游标,下一轮在执行相关的判断。
 it++;
 curt = null;
 }
 }
}
while (ir < NR) { //如果registeredReceivers中的元素没有全部放到receivers里面,就一个一个的遍历并放进去。
 if (receivers == null) {
 receivers = new ArrayList();
 }
 receivers.add(registeredReceivers.get(ir));
 ir++;
}

以上的代码所做的事情就是首先移除静态注册的广播当中需要忽略的广播接收器,随后将静态注册和动态注册的广播接收器,按照优先级合并到同一个列表当中,当然如果动态注册的前面已经入队过了,这里实际上是不会在合并的。关于合并的代码,就是经典的两列表合并的算法,具体请看代码和注释。

 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
if ((receivers != null && receivers.size() > 0)
 || resultTo != null) {
 BroadcastQueue queue = broadcastQueueForIntent(intent);
 BroadcastRecord r = new BroadcastRecord(queue, intent, callerApp, callerPackage,
 callerFeatureId, callingPid, callingUid, callerInstantApp, resolvedType,
 requiredPermissions, excludedPermissions, excludedPackages, appOp, brOptions,
 receivers, resultTo, resultCode, resultData, resultExtras,
 ordered, sticky, false, userId, allowBackgroundActivityStarts,
 backgroundActivityStartsToken, timeoutExempt);

 final BroadcastRecord oldRecord =
 replacePending ? queue.replaceOrderedBroadcastLocked(r) : null;
 if (oldRecord != null) {
 if (oldRecord.resultTo != null) {
 final BroadcastQueue oldQueue = broadcastQueueForIntent(oldRecord.intent);
 try {
 oldRecord.mIsReceiverAppRunning = true;
 oldQueue.performReceiveLocked(oldRecord.callerApp, oldRecord.resultTo,
 oldRecord.intent,
 Activity.RESULT_CANCELED, null, null,
 false, false, oldRecord.userId, oldRecord.callingUid, callingUid,
 SystemClock.uptimeMillis() - oldRecord.enqueueTime, 0);
 } catch (RemoteException e) {

 }
 }
 } else {
 queue.enqueueOrderedBroadcastLocked(r);
 queue.scheduleBroadcastsLocked();
 }
}else {
 //对于无人关心的广播,也做一下记录
 if (intent.getComponent() == null && intent.getPackage() == null
 && (intent.getFlags()&Intent.FLAG_RECEIVER_REGISTERED_ONLY) == 0) {
 addBroadcastStatLocked(intent.getAction(), callerPackage, 0, 0, 0);
 }
}

以上的代码,跟前面入队的代码也差不多,不过这里如果采用的方法是enqueueOrderedBroadcastLocked,并且多了关于已经发送的广播的替换的逻辑,这里我们先不关注。如果receivers为空,并且符合条件的隐式广播,系统也会对其进行记录,具体,我们这里也不进行分析了。

BroadcastQueue 入队

我们知道前面入队的时候有两个方法,分别是enqueueParallelBroadcastLockedenqueueOrderedBroadcastLocked,我们先来分析前者。

1
2
3
4
5
6
7
public void enqueueParallelBroadcastLocked(BroadcastRecord r) {
 r.enqueueClockTime = System.currentTimeMillis();
 r.enqueueTime = SystemClock.uptimeMillis();
 r.enqueueRealTime = SystemClock.elapsedRealtime();
 mParallelBroadcasts.add(r);
 enqueueBroadcastHelper(r);
}

这里就是将BroadcastRecord放到mParallelBroadcasts列表中,随后执行enqueueBroadcastHelper,我们先看继续看一下enqueueOrderedBroadcastLocked方法。

1
2
3
4
5
6
7
public void enqueueOrderedBroadcastLocked(BroadcastRecord r) {
 r.enqueueClockTime = System.currentTimeMillis();
 r.enqueueTime = SystemClock.uptimeMillis();
 r.enqueueRealTime = SystemClock.elapsedRealtime();
 mDispatcher.enqueueOrderedBroadcastLocked(r);
 enqueueBroadcastHelper(r);
}

这里跟上面很类似,差别是这里把BroadcastRecord入队了mDispatcher,对于普通广播,其内部是把这个记录放到了mOrderedBroadcasts列表。 而enqueueBroadcastHelper方法仅仅用于trace,我们这里不需要关注。

到了这里,我们把广播放到对应的列表了,但是广播还是没有分发出去。

AMS端广播的分发

以上是代码入了BroadcastQueu,接下来就可以看看队列中如何处理它了。首先需要注意一下,记录在入队的同时还调用了BroadcastQueuescheduleBroadcastsLock方法,代码如下:

1
2
3
4
5
6
7
public void scheduleBroadcastsLocked() {
 if (mBroadcastsScheduled) {
 return;
 }
 mHandler.sendMessage(mHandler.obtainMessage(BROADCAST_INTENT_MSG, this));
 mBroadcastsScheduled = true;
}

这里使用了Handler发送了一条BROADCAST_INTENT_MSG消息,我们可以去看一下BroadcastHandlerhandleMessage方法。其中在处理这个消息的时候调用了processNextBroadcast方法,我们可以直接去看其实现:

1
2
3
4
5
private void processNextBroadcast(boolean fromMsg) {
 synchronized (mService) {
 processNextBroadcastLocked(fromMsg, false);
 }
}

这里开启了同步块调用了processNextBroadcastLocked方法,这个方法依然很长,其中涉及到广播的权限判断,对于静态注册的广播,可能还涉及到对应进程的启动等。

动态广播的分发

动态注册的无序广播相对比较简单,这里我们仅仅看一下其中无序广播的分发处理:

 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
if (fromMsg) {
 mBroadcastsScheduled = false; //通过handleMessage过来,把flag设置为false
}
while (mParallelBroadcasts.size() > 0) {
 r = mParallelBroadcasts.remove(0);
 r.dispatchTime = SystemClock.uptimeMillis();
 r.dispatchRealTime = SystemClock.elapsedRealtime();
 r.dispatchClockTime = System.currentTimeMillis();
 r.mIsReceiverAppRunning = true;
 final int N = r.receivers.size();

 for (int i=0; i<N; i++) {
 Object target = r.receivers.get(i);

 deliverToRegisteredReceiverLocked(r,
 (BroadcastFilter) target, false, i); //分发
 }
 addBroadcastToHistoryLocked(r); //把广播添加的历史记录中
}


这里就是遍历`ParallelBroadcasts`中的每一条`BroadcastRecord`,其中会再分别遍历每一个`BroadcastFilter`,调用`deliverToRegisteredReceiverLocked`来分发广播
```java
private void deliverToRegisteredReceiverLocked(BroadcastRecord r,
 BroadcastFilter filter, boolean ordered, int index) {
 boolean skip = false;
 ...

 if (filter.requiredPermission != null) {
 int perm = mService.checkComponentPermission(filter.requiredPermission,
 r.callingPid, r.callingUid, -1, true);
 if (perm != PackageManager.PERMISSION_GRANTED) {
 skip = true;
 } else {
 final int opCode = AppOpsManager.permissionToOpCode(filter.requiredPermission);
 if (opCode != AppOpsManager.OP_NONE
 && mService.getAppOpsManager().noteOpNoThrow(opCode, r.callingUid,
 r.callerPackage, r.callerFeatureId, "Broadcast sent to protected receiver")
 != AppOpsManager.MODE_ALLOWED) {
 skip = true;
 }
 }
 }
 ...
 if (skip) {
 r.delivery[index] = BroadcastRecord.DELIVERY_SKIPPED;
 return;
 }

 r.delivery[index] = BroadcastRecord.DELIVERY_DELIVERED;
 ...
 try {

 if (filter.receiverList.app != null && filter.receiverList.app.isInFullBackup()) {
 if (ordered) {
 skipReceiverLocked(r);
 }
 } else {
 r.receiverTime = SystemClock.uptimeMillis();
 maybeAddAllowBackgroundActivityStartsToken(filter.receiverList.app, r);
 maybeScheduleTempAllowlistLocked(filter.owningUid, r, r.options);
 maybeReportBroadcastDispatchedEventLocked(r, filter.owningUid);
 performReceiveLocked(filter.receiverList.app, filter.receiverList.receiver,
 new Intent(r.intent), r.resultCode, r.resultData,
 r.resultExtras, r.ordered, r.initialSticky, r.userId,
 filter.receiverList.uid, r.callingUid,
 r.dispatchTime - r.enqueueTime,
 r.receiverTime - r.dispatchTime);
 if (filter.receiverList.app != null
 && r.allowBackgroundActivityStarts && !r.ordered) {
 postActivityStartTokenRemoval(filter.receiverList.app, r);
 }
 }
 if (ordered) {
 r.state = BroadcastRecord.CALL_DONE_RECEIVE;
 }
 } catch (RemoteException e) {
 ...
 if (ordered) {
 r.receiver = null;
 r.curFilter = null;
 filter.receiverList.curBroadcast = null;
 }
 }
}

在这个方法中有大段的代码是判断是否需要跳过当前这个广播,我这里仅仅保留了几句权限检查的代码。对于跳过的记录会将其BroadcastRecorddelivery[index]值设置为DELIVERY_SKIPPED, 而成功分发的会设置为DELIVERY_DELIVERED。对于有序广播的分发我们这里也不予分析,直接看无序广播的分发,在分发之前会尝试给对应的接收进程添加后台启动Activity的权限,这个会在分发完成之后恢复原状,调用的是maybeAddAllowBackgroundActivityStartsToken,就不具体分析了。

之后会调用performReceiveLocked去进行真正的分发,代码如下:

 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
void performReceiveLocked(ProcessRecord app, IIntentReceiver receiver,
 Intent intent, int resultCode, String data, Bundle extras,
 boolean ordered, boolean sticky, int sendingUser,
 int receiverUid, int callingUid, long dispatchDelay,
 long receiveDelay) throws RemoteException {
 if (app != null) {
 final IApplicationThread thread = app.getThread();
 if (thread != null) {
 try {
 thread.scheduleRegisteredReceiver(receiver, intent, resultCode,
 data, extras, ordered, sticky, sendingUser,
 app.mState.getReportedProcState());
 } catch (RemoteException ex) {
 ...
 throw ex;
 }
 } else {
 ...
 throw new RemoteException("app.thread must not be null");
 }
 } else {
 receiver.performReceive(intent, resultCode, data, extras, ordered,
 sticky, sendingUser);
 }
 ...
}

在执行分发的代码中,如果我们的ProcessRecord不为空,并且ApplicationThread也存在的情况下,会调用它的scheduleRegisterReceiver方法。如果进程记录为空,则会直接使用IIntentReceiverperformReceiver方法。我们在App中动态注册的情况,ProcessRecord一定是不为空的,我们也以这种情况继续向下分析。

动态注册广播分发App进程逻辑

1
2
3
4
5
6
7
public void scheduleRegisteredReceiver(IIntentReceiver receiver, Intent intent,
 int resultCode, String dataStr, Bundle extras, boolean ordered,
 boolean sticky, int sendingUser, int processState) throws RemoteException {
 updateProcessState(processState, false);
 receiver.performReceive(intent, resultCode, dataStr, extras, ordered,
 sticky, sendingUser);
}

在应用进程中,首先也只是根据AMS传过来的processState更新一下进程的状态,随后还是调用了IIntentReceiverperformReceive方法,performReceiveLoadedApk当中,为内部类InnerReceiver的方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public void performReceive(Intent intent, int resultCode, String data,
 Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
 final LoadedApk.ReceiverDispatcher rd;
 if (intent == null) {
 rd = null;
 } else {
 rd = mDispatcher.get(); //获取ReceiverDispatcher
 }
 if (rd != null) {
 rd.performReceive(intent, resultCode, data, extras,
 ordered, sticky, sendingUser);
 } else {
 IActivityManager mgr = ActivityManager.getService();
 try {
 if (extras != null) {
 extras.setAllowFds(false);
 }
 mgr.finishReceiver(this, resultCode, data, extras, false, intent.getFlags());
 } catch (RemoteException e) {
 throw e.rethrowFromSystemServer();
 }
 }
}

在应用进程中,首先会获取ReceiverDisptcher,这个一般不会为空。但是系统代码比较严谨,也考虑了,不存在的情况会调用AMS的finishReceiver完成整个流程。

对于存在的情况,会调用ReceiverDispatcherperformReceive方法继续分发。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public void performReceive(Intent intent, int resultCode, String data,
 Bundle extras, boolean ordered, boolean sticky, int sendingUser) {
 final Args args = new Args(intent, resultCode, data, extras, ordered,
 sticky, sendingUser);
 ..
 if (intent == null || !mActivityThread.post(args.getRunnable())) {
 if (mRegistered && ordered) {
 IActivityManager mgr = ActivityManager.getService();
 ..
 args.sendFinished(mgr);
 }
 }
}

这里的代码有点绕,不过也还比较清晰,首先是创建了一个Args对象,之后根据java的语法,如果intent不为空的时候会执行如下代码:

1
mActivityThread.post(args.getRunnable())

当这个执行失败的时候,才会看情况执行8行到第10行的代码。而这个Runnable就是应用端真正分发的逻辑,其代码如下:

 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
public final Runnable getRunnable() {
 return () -> {
 final BroadcastReceiver receiver = mReceiver;
 final boolean ordered = mOrdered;


 final IActivityManager mgr = ActivityManager.getService();
 final Intent intent = mCurIntent;

 mCurIntent = null;
 mDispatched = true;
 mRunCalled = true;
 if (receiver == null || intent == null || mForgotten) {
 ...
 return;
 }
 try {
 ClassLoader cl = mReceiver.getClass().getClassLoader();
 intent.setExtrasClassLoader(cl);
 intent.prepareToEnterProcess(ActivityThread.isProtectedBroadcast(intent),
 mContext.getAttributionSource());
 setExtrasClassLoader(cl);
 receiver.setPendingResult(this);
 receiver.onReceive(mContext, intent);
 } catch (Exception e) {
 if (mRegistered && ordered) {
 sendFinished(mgr);
 }
 if (mInstrumentation == null ||
 !mInstrumentation.onException(mReceiver, e)) {
 throw new RuntimeException(
 "Error receiving broadcast " + intent
 + " in " + mReceiver, e);
 }
 }

 if (receiver.getPendingResult() != null) {
 finish();
 }
 };
}

这里的receiver就是我们注册时候的那个BroadcastReceiver,这里将当前的Args对象作为它的PendingResult,在这里调用了它的onReceive方法 ,最后看pendingResult是否为空,不为空则调用PendingResultfinish()方法。当我们在onReceive中编写代码的时候,如果调用了goAsync的话,那这里的PendingResult就会为空。

另外就是我们这个Runnable是使用的mActivityThread的post方法投递出去的,它是一个Handler对象,它是在注册广播接收器的时候指定的,默认是应用的主线程Handler,也就是说广播的执行会在主线程。

但是即使是我们使用goAsync的话,处理完成之后也是需要手动调用finish的,我们后面在来看相关的逻辑。

静态广播的发送

在前面分析的BroadcastQueueprocessNextBroadcastLocked方法中,我们只分析了动态广播的发送,这里再看一下静态广播的发送,首先仍然是看processNextBroadcastLocked中的相关源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
BroadcastRecord r;
do {
 r = mDispatcher.getNextBroadcastLocked(now);
 if (r == null) {
 ...
 return;
 }
 ...

} while(r === null);
...
if (app != null && app.getThread() != null && !app.isKilled()) {
 try {
 app.addPackage(info.activityInfo.packageName,
 info.activityInfo.applicationInfo.longVersionCode, mService.mProcessStats);
 maybeAddAllowBackgroundActivityStartsToken(app, r);
 r.mIsReceiverAppRunning = true;
 processCurBroadcastLocked(r, app);
 return;
 } catch(RemoteException e) {
 ...
 }
}
...

在第3行,会从mDispatcher中拿BroadcastRecord的记录,我们之前在AMS端入队的代码,对于静态注册的广播和有序广播都是放在mDispatcher当中的,这里拿到动态注册的有序广播也会从这里拿,它的后续逻辑跟前面分析的是一样的,这里不再看了。对于静态注册的广播,在调用后续的方法之前,需要先获取对应进程的ProcessRecord,和ApplicationThread,并且进行广播权限的检查,进程是否存活检查这些在我们11行的位置,都省略不看了。如果App进程存活则会走到我们12行的部分,否则会去创建对应的进程,创建完进程会再去分发广播。

动态注册的广播,会传一个IIntentReceiver的Binder到AMS,而静态注册的广播,我们跟着第18行代码processCurBroadcastLocked方法进去一览究竟:

 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
private final void processCurBroadcastLocked(BroadcastRecord r,
 ProcessRecord app) throws RemoteException {
 final IApplicationThread thread = app.getThread();
 ...
 r.receiver = thread.asBinder();
 r.curApp = app;
 final ProcessReceiverRecord prr = app.mReceivers;
 prr.addCurReceiver(r);
 app.mState.forceProcessStateUpTo(ActivityManager.PROCESS_STATE_RECEIVER);
 ...
 r.intent.setComponent(r.curComponent);

 boolean started = false;
 try {
 mService.notifyPackageUse(r.intent.getComponent().getPackageName(),
 PackageManager.NOTIFY_PACKAGE_USE_BROADCAST_RECEIVER);
 thread.scheduleReceiver(new Intent(r.intent), r.curReceiver,
 mService.compatibilityInfoForPackage(r.curReceiver.applicationInfo),
 r.resultCode, r.resultData, r.resultExtras, r.ordered, r.userId,
 app.mState.getReportedProcState());
 started = true;
 } finally {
 if (!started) {
 r.receiver = null;
 r.curApp = null;
 prr.removeCurReceiver(r);
 }
 }

}

在这个方法中,把App的ProcessRecord放到了BroadcastRecord当中,并且把ApplicationThread设置为receiver,最后是调用了ApplicationThreadscheduleReceiver,从而通过binder调用App进程。

静态注册广播分发App进程逻辑

通过Binder调用,在App的ApplicationThread代码中,调用的是如下方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public final void scheduleReceiver(Intent intent, ActivityInfo info,
 CompatibilityInfo compatInfo, int resultCode, String data, Bundle extras,
 boolean sync, int sendingUser, int processState) {
 updateProcessState(processState, false);
 ReceiverData r = new ReceiverData(intent, resultCode, data, extras,
 sync, false, mAppThread.asBinder(), sendingUser);
 r.info = info;
 r.compatInfo = compatInfo;
 sendMessage(H.RECEIVER, r);
}

这里是创建了一个ReceiverData把AMS传过来数据包裹其中,并且通过消息发出去,之后会调用ActivityThreadhandleReceiver方法, 代码如下:

 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
private void handleReceiver(ReceiverData data) {
 String component = data.intent.getComponent().getClassName();

 LoadedApk packageInfo = getPackageInfoNoCheck(
 data.info.applicationInfo, data.compatInfo);

 IActivityManager mgr = ActivityManager.getService();

 Application app;
 BroadcastReceiver receiver;
 ContextImpl context;
 try {
 app = packageInfo.makeApplicationInner(false, mInstrumentation);
 context = (ContextImpl) app.getBaseContext();
 if (data.info.splitName != null) {
 context = (ContextImpl) context.createContextForSplit(data.info.splitName);
 }
 if (data.info.attributionTags != null && data.info.attributionTags.length > 0) {
 final String attributionTag = data.info.attributionTags[0];
 context = (ContextImpl) context.createAttributionContext(attributionTag);
 }
 java.lang.ClassLoader cl = context.getClassLoader();
 data.intent.setExtrasClassLoader(cl);
 data.intent.prepareToEnterProcess(
 isProtectedComponent(data.info) || isProtectedBroadcast(data.intent),
 context.getAttributionSource());
 data.setExtrasClassLoader(cl);
 receiver = packageInfo.getAppFactory()
 .instantiateReceiver(cl, data.info.name, data.intent);
 } catch (Exception e) {
 data.sendFinished(mgr);
 ...
 }

 try {

 sCurrentBroadcastIntent.set(data.intent);
 receiver.setPendingResult(data);
 receiver.onReceive(context.getReceiverRestrictedContext(),
 data.intent);
 } catch (Exception e) {
 data.sendFinished(mgr);
 } finally {
 sCurrentBroadcastIntent.set(null);
 }

 if (receiver.getPendingResult() != null) {
 data.finish();
 }
}

这个代码中主要有两个try-catch的代码块,分别是两个主要的功能区。因为静态注册的广播,我们的广播接收器是没有构建的,AMS传过来的只是广播的类名,因此,第一块代码的功能就是创建广播接收器对象。第二块代码则是去调用广播接收器的onReceive方法,从而传递广播。另外这里会调用PendingResultfinish去执行广播处理完成之后的逻辑,以及告知AMS,不过这里的PendingResult就是前面创建的ReceiverData

完成广播的发送

在分析前面的动态注册广播分发和静态注册广播分发的时候,最终在App进程它们都有一个Data,静态为ReceiverData, 动态为Args,他们都继承了PendingResult,最终都会调用PendingResultfinish方法来完成后面的收尾工作,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
public final void finish() {
 if (mType == TYPE_COMPONENT) {
 final IActivityManager mgr = ActivityManager.getService();
 if (QueuedWork.hasPendingWork()) {
 QueuedWork.queue(new Runnable() {
 @Override public void run() {
 sendFinished(mgr);
 }
 }, false);
 } else {
 sendFinished(mgr);
 }
 } else if (mOrderedHint && mType != TYPE_UNREGISTERED) {
 final IActivityManager mgr = ActivityManager.getService();
 sendFinished(mgr);
 }
}

这里的QueuedWork主要用于运行SharedPreferences写入数据到磁盘,当然这个如果其中有未运行的task则会添加一个Task到其中来运行sendFinished,这样做的目的是为了保证如果当前除了广播接收器没有别的界面或者Service运行的时候,AMS不会杀掉当前的进程。否则会直接运行sendFinished方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
public void sendFinished(IActivityManager am) {
 synchronized (this) {
 if (mFinished) {
 throw new IllegalStateException("Broadcast already finished");
 }
 mFinished = true;
 try {
 if (mResultExtras != null) {
 mResultExtras.setAllowFds(false);
 }
 if (mOrderedHint) {
 am.finishReceiver(mToken, mResultCode, mResultData, mResultExtras,
 mAbortBroadcast, mFlags);
 } else {
 am.finishReceiver(mToken, 0, null, null, false, mFlags);
 }
 } catch (RemoteException ex) {
 }
 }
}

这里就是调用AMS的finishReceiver方法,来告诉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
public void finishReceiver(IBinder who, int resultCode, String resultData,
 Bundle resultExtras, boolean resultAbort, int flags) {
 if (resultExtras != null && resultExtras.hasFileDescriptors()) {
 throw new IllegalArgumentException("File descriptors passed in Bundle");
 }

 final long origId = Binder.clearCallingIdentity();
 try {
 boolean doNext = false;
 BroadcastRecord r;
 BroadcastQueue queue;

 synchronized(this) {
 if (isOnFgOffloadQueue(flags)) {
 queue = mFgOffloadBroadcastQueue;
 } else if (isOnBgOffloadQueue(flags)) {
 queue = mBgOffloadBroadcastQueue;
 } else {
 queue = (flags & Intent.FLAG_RECEIVER_FOREGROUND) != 0
 ? mFgBroadcastQueue : mBgBroadcastQueue;
 }

 r = queue.getMatchingOrderedReceiver(who);
 if (r != null) {
 doNext = r.queue.finishReceiverLocked(r, resultCode,
 resultData, resultExtras, resultAbort, true);
 }
 if (doNext) {
 }
 trimApplicationsLocked(false, OomAdjuster.OOM_ADJ_REASON_FINISH_RECEIVER);
 }

 } finally {
 Binder.restoreCallingIdentity(origId);
 }
}

相关的逻辑从13行开始,首先仍然是根据广播的flag找到之前的BroadcastQueue,之后根据IBinder找到发送的这一条BroadcastRecord,调用Queue的finishReceiverLocked方法。根据它的返回值,再去处理队列中的下一个广播记录。最后的trimApplicationsLocked里面会视情况来决定是否停止App进程,我们这里就不进行分析了。

processNextBroadcastLocaked前面已经分析过了,这里只需要来看finishReceiverLocked方法,代码如下:

 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
public boolean finishReceiverLocked(BroadcastRecord r, int resultCode,
 String resultData, Bundle resultExtras, boolean resultAbort, boolean waitForServices) {
 final int state = r.state;
 final ActivityInfo receiver = r.curReceiver;
 final long finishTime = SystemClock.uptimeMillis();
 final long elapsed = finishTime - r.receiverTime;
 r.state = BroadcastRecord.IDLE;
 final int curIndex = r.nextReceiver - 1;
 if (curIndex >= 0 && curIndex < r.receivers.size() && r.curApp != null) {
 final Object curReceiver = r.receivers.get(curIndex);

 }
 ...

 r.receiver = null;
 r.intent.setComponent(null);
 if (r.curApp != null && r.curApp.mReceivers.hasCurReceiver(r)) {
 r.curApp.mReceivers.removeCurReceiver(r);
 mService.enqueueOomAdjTargetLocked(r.curApp);
 }
 if (r.curFilter != null) {
 r.curFilter.receiverList.curBroadcast = null;
 }
 r.curFilter = null;
 r.curReceiver = null;
 r.curApp = null;
 mPendingBroadcast = null;

 r.resultCode = resultCode;
 r.resultData = resultData;
 r.resultExtras = resultExtras;
 ....
 r.curComponent = null;

 return state == BroadcastRecord.APP_RECEIVE
 || state == BroadcastRecord.CALL_DONE_RECEIVE;
}

在这里,我们最关注的代码就是17行开是的代码,从mReceivers列表中移除BroadcastRecord,并且把ReceiverListcurBroadcast设置为空,并且其他几个参数也设置为空,这样才算完成了广播的分发和处理。

总结

以上就是广播接收器的注册,以及动态、静态广播分发的分析了。关于取消注册是跟注册相关的过程,理解了注册的逻辑,取消注册也可以很快的搞清楚。关于sticky的广播,限于篇幅先不分析了。而有序广播,它在AMS端其实和静态注册的广播是差不多,不过它在调用App进程的时候是有差别的。另外关于权限相关的逻辑,以后在权限代码的分析中可以再进行关注。

看完评论一下吧

  •  

更优雅的RSS使用指南

最近因为Follow的爆火,RSS的内容也跟着一起火了一把。笔者最近也优化了一下自己博客的RSS输出,在这里写一下博客如何更加 优雅的输出RSS,以及在订阅RSS的时候如何更好的发现RSS源。

RSS2.0 与 ATOM

RSS是一种消息来源格式,用于方便的将一个站点的内容以一个指定的格式输出,方便订阅者聚合多个站点的内容。

目前RSS的版本为2.0,而我们大家在使用博客输出RSS文件的时候,除了常用的RSS2.0格式,目前还有一个ATOM格式,其目前的版本为1.0。Atom发展的动机为了解决RSS2.0的问题,它解决了如下问题(来源WikiPedia):

  • RSS 2.0可能包含文本或经过编码的HTML内容,同时却没有提供明确的区分办法;相比之下,Atom则提供了明确的标签(也就是typed)。
  • RSS 2.0的description标签可以包含全文或摘要(尽管该标签的英文含义为描述或摘要)。Atom则分别提供了summary和content标签,用以区分摘要和内容,同时Atom允许在summary中添加非文本内容。
  • RSS 2.0存在多种非标准形式的应用,而Atom具有统一的标准,这便于内容的聚合和发现。
  • Atom有符合XML标准的命名空间,RSS 2.0却没有。
  • Atom通过XML内置的xml:base标签来指示相对地址URI,RSS2.0则无相应的机制区分相对地址和绝对地址。
  • Atom通过XML内置的xml:lang,而RSS采用自己的language标签。
  • Atom强制为每个条目设定唯一的ID,这将便于内容的跟踪和更新。
  • Atom 1.0允许条目单独成为文档,RSS 2.0则只支持完整的种子文档,这可能产生不必要的复杂性和带宽消耗。
  • Atom按照RFC3339标准表示时间 ,而RSS2.0中没有指定统一的时间格式。
  • Atom 1.0具有在IANA注册了的MIME类型,而RSS 2.0所使用的application/rss+xml并未注册。
  • Atom 1.0标准包括一个XML schema,RSS 2.0却没有。
  • Atom是IETF组织标准化程序下的一个开放的发展中标准,RSS 2.0则不属于任何标准化组织,而且它不是开放版权的。

相比之下ATOM协议是有更多的有点,如果你RSS生成程序已经支持了Atom那肯定是优先使用Atom。不过现在基本上99%以上的Rss订阅器或者工具对于两者都有很好的支持,因此如果你现在已经使用了RSS2.0也没必要替换成Atom了。

RSS的自动发现

对于提供Rss订阅的网站,最好的方式是提供相应的连接或者使用Rss图标,告诉访客当前网站的Rss地址。

除了这样之外,我们还应该在网站的源码中添加RSS地址,这样对于一些浏览器插件或者订阅软件可以通过我们的网站页面自动发现RSS订阅地址。

对于RSS2.0的订阅地址可以添加如下代码:

1
<link rel="alternate" type="application/rss+xml" href="/feed.xml" />

对于ATOM的订阅地址可以添加如下代码:

1
<link rel="alternate" type="application/atom+xml" href="atom.xml" title="Site title" />

如果你同时提供了ATOM和RSS2.0两种订阅文件,可以上面两行代码都添加。当然现在一些博客程序的模板文件中已经添加了上面的代码,检查一下即可。

RSS输出的优化

因为我的博客是以RSS2.0格式输出的订阅文件,因此这里我就按照我的优化内容来介绍一下输出相关的优化,对于ATtom可以参考其规范文档。

首先区分介绍和全文的输出。对于只输出描述的网站只需要设置描述部分即可,对于输出了全部的博客,还是建议同时输出描述和全文的。 而RSS2.0不支持输出全文,我们可以用一下的标记来输出全文:

1
<content:encoded>全文内容</content:encoded>

其中的文章html,最好做一下转码。 (以上代码加的有问题,有的RSS识别失败,暂时回退了,有时间换Atom)

其次可以补充一下网站的内容的元数据,比如作者的信息,网站的标题简介等等。

对于文章,也可以在输出的时候输出相关的元数据,如标题,作者,标签等。标签支持设置多个,可以用如下的标记:

1
<category domain="{{ .Permalink }}">{{ .LinkTitle }}</category>

另外在我设置的过程,发现rss是提供了一个comments标记的,设置这个标记后,如果RSS阅读器对此支持,理论上可以直接从RSS阅读器点击跳转到文章的评论页面。

最后,我们可能想要检测要多少通过RSS点击跳转到我们博客的访问量,这个时候可以在输出的链接上面加上特定的参数,这样在我们的统计平台上面就可以看到有多少用户从这里打开页面的,我所加的参数如下:

?utm_source=rss

订阅RSS

目前最流行的订阅RSS的方式要属于Follow了,这里也推荐使用。

除了Follow之外,我还自建了一个FreshRss来订阅一些内容,这个的使用要更早于Follow的出现。现在还不能抛弃它的原因是Follow目前不支持移动端,我使用Android的手机,在移动推荐使用FeedMe来浏览FreshRss的订阅内容。

另外,我们在浏览一些内容或者博客的时候,也需要一个工具来帮助我们方便的查看和订阅RSS源,这个时候就要推荐一下DIYgod大佬开发的浏览器插件RSSHub-Radar,对于我们的博客,如果已经加了我前面说的html代码,它可以自己发现订阅地址,如下图所示:

它还支持配置规则,则一些拥有RSSHub订阅的站点,比如b站,微博,小红书等,可以嗅探到RSShub的订阅地址,如下图所示:

另外,看上面弹出的窗口中是可以直接去预览对应的RSS内的,还可以直接跳转到Follow、FreshRss等订阅源去添加这个订阅源,这些可以在插件的设置中进行设置,如下图所示:

除了上面的设置,这个插件还支持一些其他的设置,读者朋友可以自行探索。

总结

以上就是关于网站配置和rss订阅方面我的一些建议,而本文的标题也有一些标题党了,欢迎吐槽。

资料

如果读者需要查阅ATOM和RSS的维基百科,请查看英文版本,中文版本内容比较简略,很多发展相关的内容都没有。

看完评论一下吧

  •  

Android源码分析:再读消息循环源码

Android消息循环在应用开发中会经常涉及,我以前也分析过。不过那个时候分析的还是以很老的Android源码来进行的,并且只是分析了Java层的代码,当时的文章为:Android消息循环分析。而Native层,以及一些新增的功能,都没有涉及,今天再读源码,对其进行再次分析。

消息循环简化版本

对于应用层的开发者来说,虽然已经过了10年,java层的Api还是跟之前一样的,依然是通过Handler发送消息,Looper会中消息队列中取消息,消息会根据Handler中的callback或者消息自己的callback执,如上图所示。我之前分析的发送消息和处理消息已经比较清楚了,这块不再看了。这里主要分析一下从MessageQueue取消息,之前涉及的文件描述符的监控和Native层的一些实现等进行分析。

java层loop取消息

首先来看java层如何从消息队列取消息的,Looper中有如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
public static void loop() {
 final Looper me = myLooper();
 ...
 me.mInLoop = true;
 Binder.clearCallingIdentity();
 final long ident = Binder.clearCallingIdentity();
 ...
 for (;;) {
 if (!loopOnce(me, ident, thresholdOverride)) {
 return;
 }
 }
}

以上代码核心就是拿到当前线程的Looper然后,在无限循环当中取调用loopOnceloopOnce代码很长,但是忽略错误处理和Log,核心代码如下:

1
2
3
4
5
6
7
8
9
private static boolean loopOnce(final Looper me,
 final long ident, final int thresholdOverride) {
 Message msg = me.mQueue.next(); //从消息队列中取消息
 ...
 msg.target.dispatchMessage(msg); //分发消息
 ...
 msg.recycleUnchecked(); //回收消息,方便下一次发送消息使用
 return true;
}

loopOnce中主要就是去通过MessageQueue取消息,之后在分发消息,并且回收消息。再来看MessageQueuenext方法:

 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
Message next() {
 final long ptr = mPtr;
 ...
 int nextPollTimeoutMillis = 0;
 for (;;) {
 nativePollOnce(ptr, nextPollTimeoutMillis);
 synchronized (this) {
 Message prevMsg = null;
 Message msg = mMessages;
 if (msg != null && msg.target == null) {
 do {
 prevMsg = msg;
 msg = msg.next;
 } while (msg != null && !msg.isAsynchronous());
 }
 if (msg != null) {
 if (now < msg.when) {
 nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE);
 } else {
 mBlock = false;
 if (preMsg != null) {
 prevMsg.next = msg.next;
 } else {
 mMessages = msg.next;
 }
 msg.next = null;
 msg.markInUse();
 return msg;
 }
 } else {
 nextPollTimeoutMillis = -1;
 }
 ...
 }
 ...
 }
}

以上为next方法的简化,在Java层的MessageQueue的实现就是一个链表,因此向其中发送消息或者取消息的过程就是链表添加或者删除的过程。在第21行到第26行就是从链表中删除msg的过程。其中这个链表它的头节点是存放在mMessages这个变量,Message在插入链表的时候,也是按照事件先后运行放到链表当中的。

在这个方法的开头,我们看到mPtr,它就是MessageQueue在native层对应的对象,不过Native的Message和Java层的Message是相互独立的,在读取next的时候,也会通过nativePollOnce来native层来读取一个消息,另外在这里还传了一个nextPollTimeoutMillis,用来告诉native需要等待的时间,具体后面在来具体分析相关代码。

因为我们的消息循环中除了放置我们通过Handler所发送的消息之外,还会存在同步信号的屏障,比如ViewRootImpl就会在每一次scheduleTraversals的时候发送一个屏障消息。屏障消息和普通消息的区别就是没有targetHandler。因此在第10行,当我们检查到是屏障消息的时候,会跳过它, 并且查找它之后的第一条异步消息。 另外就是在这个do-while的循环条件中,我们可以看到它还有判断消息是否为Asynchronous的,我们正常创建的Handler一般async都是false,也就是说消息的这个值也是为false。而异步的,一般会被IMS,WMS,Display,动画等系统组件使用,应用开发者无法使用。

这里我们只要知道,如果有异步消息,就会先执行异步消息。在第17行,这里还会判断消息的事件,如果消息的when比当前事件大的化,那么这个消息还不能够执行,这时候需要去等待,这里就会给nextPollTimeoutMillis去赋值。

Native层的MessageQueue和Looper

我们刚刚看MessageQueue的代码时候,看到mPtr,它对应native层的MessageQueue的指针。它的初始化在MessageQueue的构造方法中,也就是调用nativeInit,其内部源码为调用NativeMessageQueue的构造方法,源码在android_os_MessageQueue.cpp中:

1
2
3
4
5
6
7
8
NativeMessageQueue::NativeMessageQueue() :
 mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) {
 mLooper = Looper::getForThread();
 if (mLooper == NULL) {
 mLooper = new Looper(false);
 Looper::setForThread(mLooper);
 }
}

这里我们可以看到在Native层,创建MessageQueue的时候,也会创建Looper,当然如果当前线程存在Looper则会直接使用。Native层的Looper跟Jav层一样,是存放在ThreadLocal当中的,可以看如下代码:

1
2
3
4
5
sp<Looper> Looper::getForThread() {
 int result = pthread_once(& gTLSOnce, initTLSKey);
 Looper* looper = (Looper*)pthread_getspecific(gTLSKey);
 return sp<Looper>::fromExisting(looper);
}

到这里,我们知道对于一个启动了消息循环的线程,它在Java层和Native层分别会有各自的MessageQueue和Looper,java层通过mPtr来引用Native层的对象,从而使得两层能够产生联系。

Native层pollOnce

之前分析Java层获取消息的时候,会有一个地方调用nativePollOnce,它在native拿到NativeMessageQueue之后会调用它的pollOnce方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
void NativeMessageQueue::pollOnce(JNIEnv* env, jobject pollObj, int timeoutMillis) {
 mPollEnv = env;
 mPollObj = pollObj;
 mLooper->pollOnce(timeoutMillis);
 mPollObj = NULL;
 mPollEnv = NULL;

 if (mExceptionObj) {
 env->Throw(mExceptionObj);
 env->DeleteLocalRef(mExceptionObj);
 mExceptionObj = NULL;
 }
}

这里的pollObj为我们java层的MessageQueue, 这里继续调用了native层的pollOnce,代码如下:

1
2
3
4
5
6
7
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) { //我们的调用流程只会传timeoutMillis
 ...
 for (;;) {
 ...
 result = pollInner(timeoutMillis);
 }
}

这里省略了一些结果处理的代码,我们可以回头在看,这可以看到开启了一个无限循环,并调用pollInner, 这个方法比较长,我们先分块看其中的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
if (timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {
 nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
 int messageTimeoutMillis = toMillisecondTimeoutDelay(now, mNextMessageUptime);
 if (messageTimeoutMillis >= 0
 && (timeoutMillis < 0 || messageTimeoutMillis < timeoutMillis)) {
 timeoutMillis = messageTimeoutMillis;
 }
}
int result = POLL_WAKE;
mResponses.clear(); //清除reponses列表和计数
mResponseIndex = 0;
mPolling = true;

struct epoll_event eventItems[EPOLL_MAX_EVENTS];
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);

mPolling = false;

这里timeoutMillis是我们从java层传过来的下一个消息的执行事件,而mNextMessageUptime是native层的最近一个消息的执行事件,这个根据这两个字段判断需要等待的事件。

在之后调用epoll_wait来等待I/O事件,或者到设置的超时时间结束等待,这样做可以避免Java层和Native层的循环空转。此处的epoll_wait除了避免循环空转还有另一个作用,我们之前在分析IMS也使用过LooperaddFd,这里如果对应的文件描述符有变化,这里就会拿到,并反应在eventCount上,这里我们先不具体分析,后面再看。

Native消息的读取和处理

当等待完成之后,就会去native的消息队列中取消息和处理,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Done: ;
 mNextMessageUptime = LLONG_MAX;
 while (mMessageEnvelopes.size() != 0) {
 nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
 const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
 if (messageEnvelope.uptime <= now) {
 {
 sp<MessageHandler> handler = messageEnvelope.handler;
 Message message = messageEnvelope.message;
 mMessageEnvelopes.removeAt(0);
 mSendingMessage = true;
 mLock.unlock();
 handler->handleMessage(message);
 }

 mLock.lock();
 mSendingMessage = false;
 result = POLL_CALLBACK;
 } else {
 mNextMessageUptime = messageEnvelope.uptime;
 break;
 }
 }

在Native中消息是放在mMessageEnvelope当中,这是一个verctor也就是一个动态大小的数组。不过不看这个的化,我们可以看到这里读取消息,以及读取它的执行时间uptime跟java层的代码是很像是的,甚至比java层还要简单许多,就是直接拿数组的第一条。之后使用MessageHandler执行handleMessage。这里的MessageHandler跟java层的也是很像,这里再列一下MessageEnvelopeMessage的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
struct MessageEnvelope {
 MessageEnvelope() : uptime(0) { }

 MessageEnvelope(nsecs_t u, sp<MessageHandler> h, const Message& m)
 : uptime(u), handler(std::move(h)), message(m) {}

 nsecs_t uptime;
 sp<MessageHandler> handler;
 Message message;
};

struct Message {
 Message() : what(0) { }
 Message(int w) : what(w) { }

 /* The message type. (interpretation is left up to the handler) */
 int what;
};

这里和java层的区别是,拆分成了两个结构体,但是呢比java层的还是要简单很多。到这里Native层和Java层对应的消息循环体系就分析完了。但是Native层除了这个消息循环还有一些其他东西,就是前面说到的文件描述符的消息传递。

文件描述符消息读取和处理

前面在pollOnce中还是有关于文件描述符消息的处理,这里继续分析。前面的epoll_wait就会读取相关的事件,读取完事件之后的处理如下:

 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
if (eventCount < 0) { //如果读出来的eventCount小于0,则说明有错误
 if (errno == EINTR) { //处理错误,并且跳转到Done去读取native层的消息
 goto Done;
 }
 result = POLL_ERROR;
 goto Done;
}

if (eventCount == 0) { //直接超时,没有读到事件
 result = POLL_TIMEOUT;
 goto Done;
}

for (int i = 0; i < eventCount; i++) { //根据返回的条数,来处理消息
 const SequenceNumber seq = eventItems[i].data.u64;
 uint32_t epollEvents = eventItems[i].events;
 if (seq == WAKE_EVENT_FD_SEQ) { //序列为这个序列被定义成为唤醒事件
 if (epollEvents & EPOLLIN) {
 awoken();
 } else {
 }
 } else {
 const auto& request_it = mRequests.find(seq);
 if (request_it != mRequests.end()) {
 const auto& request = request_it->second;
 int events = 0;
 if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
 if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
 if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
 if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
 mResponses.push({.seq = seq, .events = events, .request = request});
 } else {
 ...
 }
 }
}

前面的错误处理我们直接看我的注释即可。后面会根据返回的eventCount来一次对每一个eventItem做处理,其他它的u64为序列号,这些为注册到LoopermRequests的序列号,其中1为WAKE_EVENT_FD_SEQ,也就是mWakeEventFd的序列,这里唤醒我们先不管了,直接看后面的正常的文件描述符事件监听。 这里首先会通过seq找到对应的Request,并根据epollEvents来设置他们的事件类型,之后封装成为Response放到mResponses当中。在这些做完,后面同样是跳转到Done后面的代码块,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
Done: ;
 ...
 for (size_t i = 0; i < mResponses.size(); i++) {
 Response& response = mResponses.editItemAt(i);
 if (response.request.ident == POLL_CALLBACK) {
 int fd = response.request.fd;
 int events = response.events;
 void* data = response.request.data;
 int callbackResult = response.request.callback->handleEvent(fd, events, data);
 if (callbackResult == 0) {
 AutoMutex _l(mLock);
 removeSequenceNumberLocked(response.seq);
 }

 response.request.callback.clear(); //移除response对与callback的引用
 result = POLL_CALLBACK;
 }
 }

这里则是遍历刚刚我们填充的mResponses数组,从其中取出每一个Response,并调用它的Request的Callback回调的handleEvent方法,它的使用我们之前分析IMSServiceManager启动的时候已经见到过了。

以上说的是Java层会初始化Handler和Looper的情况,如果只是Native层使用的话,一般怎么用的呢。我们以BootAnimation中的使用为例,它是在BootAnimation.cpp当中,在初始化BootAnimation对象的时候,会创建一个Looper,代码如下:

1
new Looper(false)

readyToRun中添加文件描述符的监听:

1
2
3
4
5
status_t BootAnimation::readyToRun() {
 ...
 mLooper->addFd(mDisplayEventReceiver->getFd(), 0, Looper::EVENT_INPUT,
 new DisplayEventCallback(this), nullptr);
}

最后去循环调用pollOnce,来获取消息:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
bool BootAnimation::android() {
 do {
 processDisplayEvents();
 ...
 } while (!exitPending());
}

void BootAnimation::processDisplayEvents() {
 mLooper->pollOnce(0);
}

这就是Android Framework当中,大部分的Native场景使用消息循环的方式。而Native中,想要跟Java层一样发送消息,则是调用Looper的sendMessage方法。而Native层的Handler我们可以理解为只是一个Message的回调,和java层的Handler功能不可同日而语。

异步消息

在Java层的消息循环中,消息是有同步和异步之分的,异步消息一般都会伴随则屏障消息,我们之前分析的获取next消息中可以看到,如果第一个消息是屏障消息,会找后面的第一条异步消息来执行。

同时在enqueueMessage的代码中也有如下逻辑:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
//MessageQueue.java
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
 prev = p;
 p = p.next;
 if (p == null || when < p.when) {
 break;
 }
 if (needWake && p.isAsynchronous()) {
 needWake = false;
 }
}
msg.next = p;
prev.next = msg;

插入异步消息会改变唤醒等待的状态,如果链表头是屏障消息,且之前调用next的时候mBlocked设置为了true,且当前是异步消息会设置成唤醒,但是如果当前的消息队列中已经有了比当前消息更早执行的消息,则不会唤醒。

到这就完成了消息循环的所有分析了。也欢迎读者朋友交流探讨。

看完评论一下吧

  •  

Android消息循环分析

我们的常用的系统中,程序的工作通常是有事件驱动和消息驱动两种方式,在Android系统中,Java应用程序是靠消息驱动来工作的。

消息驱动的原理就是:
1. 有一个消息队列,可以往这个队列中投递消息;
2. 有一个消息循环,不断从消息队列中取出消息,然后进行处理。
在Android中通过Looper来封装消息循环,同时在其中封装了一个消息队列MessageQueue。
另外Android给我们提供了一个封装类,来执行消息的投递,消息的处理,即Handler。

在我们的线程中实现消息循环时,需要创建Looper,如:

class LooperThread extends Thread {
public Handler mHandler;
public void run() {
Looper.prepare(); //1.调用prepare
......
Looper.loop(); //2.进入消息循环
}
}

看上面的代码,其实就是先准备Looper,然后进入消息循环。

  1. 在prepare的时候,创建一个Looper,同时在Looper的构造方法中创建一个消息队列MessageQueue,同时将Looper保存到TLV中(这个是关于ThreadLocal的,不太懂,以后研究了再说)
  2. 调用loop进入消息循环,此处其实就是不断到MessageQueue中取消息Message,进行处理。

然后再看我们如何借助Handler来发消息到队列和处理消息

Handler的成员(非全部):

final MessageQueue mQueue;
final Looper mLooper;
final Callback mCallback;

Message的成员(非全部):

Handler target;
Runnable callback;

可以看到Handler的成员包含Looper,通过查看源代码,我们可以发现这个Looper是有两种方式获得的,1是在构造函数传进来,2是使用当前线程的Looper(如果当前线程无Looper,则会报错。我们在Activity中创建Handler不需要传Handler是因为Activity本身已经有一个Looper了),MessageQueue也就是Looper中的消息队列。

然后我们看怎么向消息队列发送消息,Handler有很多方法发送队列(这个自己可以去查),比如我们看sendMessageDelayed(Message msg, long delayMillis)

public final boolean sendMessageDelayed(Message msg, long delayMillis) {
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
// SystemClock.uptimeMillis() 获取开机到现在的时间
}
//最终所有的消息是通过这个发,uptimeMillis是绝对时间(从开机那一秒算起)
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
boolean sent = false;
MessageQueue queue = mQueue;
if (queue != null) {
msg.target = this;
sent = queue.enqueueMessage(msg, uptimeMillis);
}
return sent;
}

看上面的的代码,可以看到Handler将自己设为Message的target,然后然后将msg放到队列中,并且指定执行时间。

消息处理

处理消息,即Looper从MessageQueue中取出队列后,调用msg.target的dispatchMessage方法进行处理,此时会按照消息处理的优先级来处理:

  1. 若msg本身有callback,则交其处理;
  2. 若Handler有全局callback,则交由其处理;
  3. 以上两种都没有,则交给Handler子类实现的handleMessage处理,此时需要重载handleMessage。

我们通常采用第三种方式进行处理。

注意!!!!我们一般是采用多线程,当创建Handler时,LooperThread中可能还未完成Looper的创建,此时,Handler中无Looper,操作会报错。

我们可以采用Android为我们提供的HandlerThread来解决,该类已经创建了Looper,并且通过wait/notifyAll来避免错误的发生,减少我们重复造车的事情。我们创建该对象后,调用getLooper()即可获得Looper(Looper未创建时会等待)。

补充

本文所属为Android中java层的消息循环机制,其在Native层还有消息循环,有单独的Looper。并且2.3以后MessageQueue的核心向Native层下移,native层java层均可以使用。这个我没有过多的研究了!哈哈

PS:本文参考《深入理解Android:卷I》

看完评论一下吧

  •  

安庆游记2-集贤时空和倒扒狮街

之前的文章分享的是去安庆博物馆,在第二天,朋友推荐了集贤时空这个地方,便约着一起去了。

集贤时空位于集贤北路上,由原来的白鳍豚水泥厂改造而成。进去里面会发现里面的商业设施还不完善,许多空间都还在招租,旁边还有二期正在施工,现在也不需要门票。

停完车,看到入口大门,就能感受到满满的大字报气息。

里面的墙壁也是到处充满了毛主席语录

以及其他一些之前比较流行的标语 最为突出的当属以厂房绘制的大邮筒,2B铅笔等,满满的怀旧风。

除了标语和大字报之外,这里也还有一些别的景观,首先是一个根雕展览,里面充满了各种精美的根雕。

以及古居民去的微缩景观,做的栩栩如生,很漂亮。

这个地方目前免费,人还是挺多的,不知道将来如果收费人流量会怎么样。另外现在这里招商的一些商家的店铺招牌,和这个文创园的风格不太搭配,希望园方能控制一下招商品控吧。外面的停车场很大,管理却不是很好,很多人不会停车,画的线也不看,一个车占用两个车位。

中午去亲戚推荐的当地饭店吃了老母鸡汤泡炒米,饭店味道还不错。下午就去了倒扒狮街,去了才知道,这条街我以前是来过的。不过当时还没有做文化包装,当时里面还主要是一些买服装的店铺。

而现在,这个街经过改造,文艺气息浓厚很多,游客也很多。 墙上海子的诗倒是还是和之前看到的一样。 安庆方言听了几年了,这上面的倒是也能看懂一些。 地名放到一起,也是文艺气息十足。 国庆期间,在倒扒狮街的戏台上和钱牌楼的路边舞台,都有表演,包括相声、杂耍、变脸等,这个要给安庆文旅点个赞。

老城的文化开发搞得挺不错的,吸引一大波人流。但是停车开车太难了,所以城墙,迎江寺都没有去看,下次有机会在看喽。

这边的美食,大家一般都比较喜欢吃油炸的,这里也推荐少年宫路的一家517油炸,亲戚经常带小孩去吃,用的油比较放心,味道也不错,价格也小贵。另外就是少年宫路的好吃的挺多,可以前往探索。

看完评论一下吧

  •  

安庆游记1-安庆博物馆

媳妇家在离安庆不远,过个江就到了,因此安庆经常去,不过去安庆也主要是去逛商场和吃饭。之前呢也去过附近的乌龙溪和菜子湖,安庆市区的除了菱湖公园别的也都没去过了。这次国庆过来,就去了一下安庆博物馆、集贤时空和倒扒狮街。本文先介绍一下安庆博物馆的这一部分。

对于安庆博物馆感兴趣也是今年初朋友提到说这里还不错,可以看看。当时因为时间不赶巧,没能前来。这次有空就专门跑过来转了转。 安庆作为安徽之前的省会,并且安徽的简称皖也是来自安庆境内的皖山和皖河,更早时期这里还是古皖国的范围,安庆还是有很多东西值得看一看的。因为黄梅戏在安庆发扬光大,安庆博物馆也是中国黄梅戏博物馆。 安庆博物馆不收门票,不过需要提前预约,因为不像上海等大城市的博物馆那么热门,门票很好预约。因为博物馆建在新区,这边空间也比较开阔,停车也很方便,并且不要停车费。

我们首先观看的是二楼的钱币陈列馆,在这个馆内可以看到从古至今的各种各样的钱币,了解钱币的发展,同时馆内还陈设了安庆造币厂的模型,以及世界各国的钱币。

随后观看了二楼的安庆古代文明陈列,这里了解了一些安庆的一些遗址,以及安庆在古代的发展,以及各种古代的器皿,工具,艺术品等。 其中印象最深的是了解到原来雷池就在安庆。

同时二楼还有安庆的近代文明,这其中主要就是介绍清朝后期到现在的一些历史,以及一些知名的人物,如李大钊,陈独秀等。而建国之后省会就转到合肥了,建国之后的就少有提及 。这里都是一些名人和历史事件比较多,我也就没拍多少照片了。

三楼首先看的是安庆城市记忆馆,这里可以看到安庆以前作为省会的辉煌。包括当时 安庆城的模型,以及迎江寺的模型。

以前安庆人的日常用品,和绣鞋花包等等。

安庆的邮局和知名品牌店铺的还原(这个现在很多博物馆都很喜欢搞,之前在南京博物院也见到过类似的)。

三楼的另一重磅则是黄梅戏艺术陈列馆。入口设计的就很漂亮。

这个馆内主要展示来黄梅戏的又来,以及安庆当地的一些戏曲品种,以及 他们如何最终演化成为黄梅戏。同时还要黄梅戏的服装,道具,乐器等等的展示。

甚至呢,很多的木雕也有戏曲表演的场景。

在一楼首先就是最近的临展,鲁迅的艺术世界,这个展是和北京鲁迅博物馆合办,很多文物也是来自鲁迅博物馆。这里给我印象最深的就是展示了很多鲁迅收藏的艺术作品,版画,剪纸之类的,还有鲁迅所设计的图书封面等。同时这个展馆的海报,内部布置风格也很鲁迅,看了很喜欢。缺点就是展品数量不是特别多。

另外靠近出口的地方还有一个临展,为安庆和美乡村书画摄影展,通过这些摄影和书画作品可以了解到安庆下面的乡村的美丽,因为时间有限只看了一小部分。

最后就是去在博物馆的商店购买纪念品和盖章,结束安庆博物馆之旅。因为时间有限,这里还有三楼的安庆政协发展陈列(门口看到很政治),一楼的安庆的书画陈列馆,感兴趣的也可以看看。

看完评论一下吧

  •  

九月月报-向上冲向前进

九月学生开学,台风来袭,合肥地震,股票上涨。开了一夜的车到达皖南农村后,稍作休息,有空来写一写九月的月报。

游玩

九月没有出远门去玩,月初周末去了一次崇明岛,东滩湿地公园转了一下,这里是鸟类保护生态园,但是确没看到什么鸟,里面的一些设施也有一些荒废,不过环境还不错,到这露营还不错。另外回程在江边看日落,夕阳下的长江大桥很美。

扬子鳄繁衍地但是没看到鳄 黑天鹅与鹅 落日下的飞机 后面,就跟朋友在松江随便找个路边绿地露营,感觉也不错。这点来说松江的绿化和环境是真不错。 路边绿地风景 中秋节,赶回老家,得以躲过台风“贝碧嘉”。在老家就是在田里转一转。抽了半天去了一趟邻县的沱湖,虽说离得不算远,但是长大几十岁之前也未曾来过。来的时间不是很好,荷花都没了,湖面也不是很好看。这里也盛产大闸蟹,没有阳澄湖出名,不过也很不错。湖边街道两边许多店铺出售蟹,因为蟹还每到丰收季节,于是买了一些母蟹晚上大吃一顿。 沱湖 沱湖残荷 后半个月因为又有台风普拉桑,后面就一直未出门喽。

这个月仍然继续在阅读Android源码,Activity启动重新完整分析了,wms,IMS等代码也有所分析,相关内容也整理成了博文,共有10篇相关博文,感兴趣的可以访问查看Android源码解析

另外最近突然又想学学英语,就下载了多邻国,目前坚持一周了。应用内容还算比较简单,不过游戏化的内容,加上一些激励设计,还挺有意思的。英语学了很多年了,每次都半途而废,立个flag,希望能坚持下去。

投资

美联储降息,国内降存款准备金,降房贷利率等等新政策。全球证券市场和虚拟币基本都上涨了一波。其中虚拟币短期呢波动巨大,一直想要等到回调上车,也没等到机会,只好等下一次机会了。而山寨币,则是币圈暴富的秘籍,但是波动巨大不是一般人能玩的。

而A股,之前买股票一直亏损后,就转投指数基金了,之前的亏损在经历这次上涨总算是浮盈了,一些条件单也出掉落袋为安了。各个群里的群友这几天还在杀入,我却也是未加仓,虽然说大水漫灌,但是经济环境似乎并不会很快变化,如果说是牛市来了,那也与我无关。

本月阅读不多,上个月未读完的《西藏西藏》给读完了。又新开始读了一本《简读中国史》,以全球视角来看中国史,另外把中国的不同朝代放到一起分析,分析封建制度的演进变化,目前还没读完,不敢评价说书多好,但是这样的分析视角却是便于我们去更好的认识历史。

电视剧看了一个《月光花谋杀案》,是我喜欢的侦探案件,六集的长度,也不至于花很多时间。剧中小说以剧中案件为基础创作,以小说来引导剧中案件的调查,形式新颖,推荐一看。 电影看了两个。《默杀》和《第二十条》,这两部剧的共同点都是有校园暴力,不过前者是以这个为主线,讨论人性。而后者,则是有一定的喜剧成分,但是讨论的是正当防卫和故意伤害,最后升华了主题。

尾声

台风是强烈的,股票是上涨的,生活是平淡的,九月就这样过去了。我们喜迎国庆,也祝福十月股票继续大涨。

这是我的第四个月月报,下个月再见。最后祝大家国庆节快乐。

看完评论一下吧

  •  

Android源码分析:系统进程中事件的读取与分发

之前分析的是从InputChannel中读取Event,并且向后传递,进行消费和处理的过程。但是之前的部分呢,事件如何产生,事件怎么进入到InputChanel当中的,事件又是如何跨进程到达App进程,这里继续来分析。

以上为system进程的流程的简化图,这里我们可以看到几个重要的组件,这里以触摸事件来进行分析(后文的分析也将会以触摸事件为主进行分析)并且简单的描绘了事件从EventHub到服务端的InputChannel发送事件的全部过程。具体内容一起来看下面的代码。

InputManagerService的创建

因为事件的分发涉及到不少类,我们先从InputManagerService(IMS)的初始化出发,进行分析。入口代码在SystemServer.java中,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
private void startOtherServices(@NonNull TimingsTraceAndSlog t) {
WindowManagerService wm = null;
InputManagerService inputManager = null;
inputManager = new InputManagerService(context);
wm = WindowManagerService.main(context, inputManager, !mFirstBoot, mOnlyCore,
 new PhoneWindowManager(), mActivityManagerService.mActivityTaskManager);
ServiceManager.addService(Context.WINDOW_SERVICE, wm, /* allowIsolated= */ false,
 DUMP_FLAG_PRIORITY_CRITICAL | DUMP_FLAG_PROTO);
ServiceManager.addService(Context.INPUT_SERVICE, inputManager,
 /* allowIsolated= */ false, DUMP_FLAG_PRIORITY_CRITICAL);

inputManager.setWindowManagerCallbacks(wm.getInputManagerCallback());
inputManager.start();
}

这里我们可以看到WMS的创建我们传入了IMS,并且IMS也依赖WindowMnagerCallbacks,我们先看一下IMS的构造方法。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
public InputManagerService(Context context) {
 this(new Injector(context, DisplayThread.get().getLooper()));
}

@VisibleForTesting
InputManagerService(Injector injector) {
 ...
 mHandler = new InputManagerHandler(injector.getLooper());
 mNative = injector.getNativeService(this);
 ...
}

我们主要关注这个mNative的构建,它是NativeImpl,它的创建过程如下:

1
new NativeInputManagerService.NativeImpl(service, mContext, mLooper.getQueue());

这里的Looper是前面传进来的DisplayThread的Looper。在NativeImpl的构造方法中调用了init方法,并获取到了它的native指针,这里需要看com_android_server_input_InputManagerService.cpp中的natvieInit方法,代码如下:

1
2
3
4
5
6
7
8
static jlong nativeInit(JNIEnv* env, jclass /* clazz */,
 jobject serviceObj, jobject contextObj, jobject messageQueueObj) {
 sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj);
 NativeInputManager* im = new NativeInputManager(contextObj, serviceObj,
 messageQueue->getLooper());
 im->incStrong(0);
 return reinterpret_cast<jlong>(im);
}

这里创建了NativeInputManager

NativeInputManager初始化

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
NativeInputManager::NativeInputManager(jobject contextObj,
 jobject serviceObj, const sp<Looper>& looper) :
 mLooper(looper), mInteractive(true) {
 JNIEnv* env = jniEnv();

 mServiceObj = env->NewGlobalRef(serviceObj);

 {
 AutoMutex _l(mLock);
 mLocked.systemUiLightsOut = false;
 mLocked.pointerSpeed = 0;
 mLocked.pointerAcceleration = android::os::IInputConstants::DEFAULT_POINTER_ACCELERATION;
 mLocked.pointerGesturesEnabled = true;
 mLocked.showTouches = false;
 mLocked.pointerDisplayId = ADISPLAY_ID_DEFAULT;
 }
 mInteractive = true;

 InputManager* im = new InputManager(this, this);
 mInputManager = im;
 defaultServiceManager()->addService(String16("inputflinger"), im);
}

这个构造方法中,传入的jobject为我们之前的NativeImpl,后面有需要调用java层的时候会用到它。除此之外我们看到又创建了一个InputManger,并且把它注册到了ServiceManger当中,名称为inputflinger

我们继续看InputManager的初始化代码,它传如的两个参数readerPolicydispatcherPolicy的实现都在NativeInputManager当中。它的代码如下:

1
2
3
4
5
6
7
8
InputManager::InputManager(
 const sp<InputReaderPolicyInterface>& readerPolicy,
 const sp<InputDispatcherPolicyInterface>& dispatcherPolicy) {
 mDispatcher = createInputDispatcher(dispatcherPolicy);
 mClassifier = std::make_unique<InputClassifier>(*mDispatcher);
 mBlocker = std::make_unique<UnwantedInteractionBlocker>(*mClassifier);
 mReader = createInputReader(readerPolicy, *mBlocker);
}

这里首先创建了InputDispatcher,之后创建的mClassifiermBlockerInputDispatcher一样都是继承自InputListenerInterface,它们的作用为在事件经过InputDispatcher分发之前,可以做一些预处理。最后创建InputReader,事件会经由它传递到InputDispatcher,最后再由InputDispatcher分到到InputChannel。下面来详细分析。

事件源的初始化

因为InputDispatcher初始化代码比较简单,我们从createInputReader的源码开始看起来:

1
2
3
4
std::unique_ptr<InputReaderInterface> createInputReader(
 const sp<InputReaderPolicyInterface>& policy, InputListenerInterface& listener) {
 return std::make_unique<InputReader>(std::make_unique<EventHub>(), policy, listener);
}

我们可以看到在创建InputReader之前首先创建了一个EventHub,看名字我们就知道它是一个事件的收集中心。我们看它的构造方法,代码如下:

 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
EventHub::EventHub(void)
 : mBuiltInKeyboardId(NO_BUILT_IN_KEYBOARD),
 mNextDeviceId(1),
 ...
 mPendingINotify(false) {
 ensureProcessCanBlockSuspend();

 mEpollFd = epoll_create1(EPOLL_CLOEXEC); //创建epoll实例,flag表示执行新的exec时候会自动关闭

 mINotifyFd = inotify_init1(IN_CLOEXEC); //创建inotify实例,该实例用于监听文件的变化

 if (std::filesystem::exists(DEVICE_INPUT_PATH, errorCode)) {
 addDeviceInputInotify();
 } else {
 addDeviceInotify();
 isDeviceInotifyAdded = true;

 }

 struct epoll_event eventItem = {};
 eventItem.events = EPOLLIN | EPOLLWAKEUP;
 eventItem.data.fd = mINotifyFd;
 int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mINotifyFd, &eventItem);
 int wakeFds[2];
 result = pipe2(wakeFds, O_CLOEXEC);

 mWakeReadPipeFd = wakeFds[0];
 mWakeWritePipeFd = wakeFds[1];

 result = fcntl(mWakeReadPipeFd, F_SETFL, O_NONBLOCK);

 result = fcntl(mWakeWritePipeFd, F_SETFL, O_NONBLOCK);

 eventItem.data.fd = mWakeReadPipeFd;
 result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeReadPipeFd, &eventItem);
}

从上面的代码我们可以看到这里主要为创建inotify并且通过epoll去监听文件的变化,其中还是用管道创建了wakeReadPipewakeReadPipe的文件描述,用于接收回调。我们先看一下addDeviceInputInotify()方法:

1
2
3
4
void EventHub::addDeviceInputInotify() {
 mDeviceInputWd = inotify_add_watch(mINotifyFd, DEVICE_INPUT_PATH, IN_DELETE | IN_CREATE);

}

其中DEVICE_INPUT_PATH的值为/dev/input,也就是说把这个path放到mINofiyFd的监控当中。对于了解Linux的人应该知道,在Linux中万物结尾文件,因此我们的输入也是文件,当事件发生的时候便会写入到/dev/input下面,文件变化我们也会得到通知。我这里使用ls命令打印了一下我的手机,/dev/input下面有如下文件:

1
event0 event1 event2 event3 event4 event5

具体这些文件的写入,那就是内核和驱动相关的东西了,我们这里不再讨论。而事件的读取,我们后面再进行分析。

IMS的启动

各个对象都构建完成之后,IMS要进行启动,才能够对事件进行处理并且分发。SystemServer中已经调用了IMS的start方法,它其中又会调用NativeInputMangerstart方法,最终会调用 native层的InputManagerstart方法。而其中分别又调用了Dispatcher的start方法和Reader的start方法。我们分别分析。

InputDispater 调用start

1
2
3
4
5
6
7
8
status_t InputDispatcher::start() {
 if (mThread) {
 return ALREADY_EXISTS;
 }
 mThread = std::make_unique<InputThread>(
 "InputDispatcher", [this]() { dispatchOnce(); }, [this]() { mLooper->wake(); });
 return OK;
}

这个方法中主要创建了InputThread,并且给它传了两个lambda,分别执行InputDispatchdispatchOnce方法和执行Looper的wake方法。我们看InputThread的构造方法:

1
2
3
4
5
InputThread::InputThread(std::string name, std::function<void()> loop, std::function<void()> wake)
 : mName(name), mThreadWake(wake) {
 mThread = new InputThreadImpl(loop);
 mThread->run(mName.c_str(), ANDROID_PRIORITY_URGENT_DISPLAY);
}

可以看到其中创建了InputThreadImpl,这个类才是真的继承的系统的Thread类,这里构建完成它就继续调用了它的run方法,这样它就会启动了。这里我们需要注意这个 线程的优先级,为PRIORITY_URGEN_DISPLAY,可以看到优先级是非常高了。

1
2
3
4
bool threadLoop() override {
 mThreadLoop();
 return true;
}

另外就是我们传进来的loop传入了这个对象,并且在它的threadLoop中会执行它。对于native中的线程,我们在threadLoop中实现逻辑就可以了,并且这里我们返回值为true,它会继续循环执行 。而我们传入的另一个lambda,则是在线程推出的时候调用。这个线程循环中执行的就是我们的InputDispatch中 的dispatchOnce方法,也就是消息的投递,后面再来分析。

InputReader调用start方法

1
2
3
4
5
6
7
8
status_t InputReader::start() {
 if (mThread) {
 return ALREADY_EXISTS;
 }
 mThread = std::make_unique<InputThread>(
 "InputReader", [this]() { loopOnce(); }, [this]() { mEventHub->wake(); });
 return OK;
}

这里的初始化,我们可以看到跟前面的InputDispatch很类似,连InputThread用的都是同一个类,内部也就一样有InputThreadImpl了。这里则是调用了InputReader内部的loopOnce方法。到这里系统就完成了输入事件分发的初始化了。

我们在看事件的分发之前,先看一下应用中的接收和系统的InputDispatch进行连接的过程。

InputChannel的注册

我们之前分析应用层的事件传递的时候,只是谈到了InputChannel是在WMS调用如下代码生成的:

1
mInputChannel = mWmService.mInputManager.createInputChannel(name);

但是内部如何创建InputChannel的,以及 这个InputChannel是如何收到消息的我们都没有涉及,我们现在继续分析它一下。这个createInputChannel内部最终会调用到native层的InputDispatchercreateInputChannel方法, 代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Result<std::unique_ptr<InputChannel>> InputDispatcher::createInputChannel(const std::string& name) {
 std::unique_ptr<InputChannel> serverChannel;
 std::unique_ptr<InputChannel> clientChannel;
 status_t result = InputChannel::openInputChannelPair(name, serverChannel, clientChannel);
 ...
 { // acquire lock
 std::scoped_lock _l(mLock);
 const sp<IBinder>& token = serverChannel->getConnectionToken();
 int fd = serverChannel->getFd();
 sp<Connection> connection =
 n1ew Connection(std::move(serverChannel), false /*monitor*/, mIdGenerator);
 ...
 mConnectionsByToken.emplace(token, connection);

 std::function<int(int events)> callback = std::bind(&InputDispatcher::handleReceiveCallback, this, std::placeholders::_1, token);

 mLooper->addFd(fd, 0, ALOOPER_EVENT_INPUT, new LooperEventCallback(callback), nullptr);
 } // release lock

 // Wake the looper because some connections have changed.
 mLooper->wake();
 return clientChannel;
}

首先是第4行代码,这里创建了InputChannel,而它又分为serverChannelclientChannel,返回调用方的是`clientChannel。

我们先进去看看其源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
status_t InputChannel::openInputChannelPair(const std::string& name,
 std::unique_ptr<InputChannel>& outServerChannel,
 std::unique_ptr<InputChannel>& outClientChannel) {
 int sockets[2];
 if (socketpair(AF_UNIX, SOCK_SEQPACKET, 0, sockets)) { //创建socket对
 ..
 return result;
 }
 ..
 sp<IBinder> token = new BBinder();

 std::string serverChannelName = name + " (server)";
 android::base::unique_fd serverFd(sockets[0]); //获取server socket fd
 outServerChannel = InputChannel::create(serverChannelName, std::move(serverFd), token); //创建server InputChannel

 std::string clientChannelName = name + " (client)";
 android::base::unique_fd clientFd(sockets[1]);
 outClientChannel = InputChannel::create(clientChannelName, std::move(clientFd), token); //创建Client InputChannel
 return OK;
}

以上代码我们可以看到就是创建了一对socket,分别放到两个InputChannel当中,并且这里创建了一个BBinder作为两个InputChannel的token,具体用处我们后面会再提到。此时可以继续回看前面的createInputChannel方法,在11行,创建了一个 Connection对象,并且以前面创建的BBinder为key放到了mConnectionsByToken当中,Connection的用处留到后面继续讲。

在15行创建了一个callback,其中会执行InputDispatcherhandleReceiveCallback方法,并且这个callback被添加looper的addFd的时候设置进去了,这里的fd就是之前创建的ServerInputChannel的socket的文件描述符。到这里就完成了初始化,添加了服务端InputChannel的文件描述符监听。

事件触发

我们之前在分析InputManger的启动的时候,已经看到了事件是通过/dev/input来通知到EventHub,而InputReader通过Looper监听了/dev/input的文件描述符,从而让我们事件传递的系统动起来。那么我们首先就从InputReaderloopOnce开始看起来。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
void InputReader::loopOnce() {
 ...
 size_t count = mEventHub->getEvents(timeoutMillis, mEventBuffer, EVENT_BUFFER_SIZE);

 { // acquire lock
 std::scoped_lock _l(mLock);
 mReaderIsAliveCondition.notify_all();

 if (count) {
 processEventsLocked(mEventBuffer, count);
 }
 ...
 } // release lock
 ...
 mQueuedListener.flush();
}

我们这里省略了设备变化,超时等相关的代码,仅仅保留了事件读取相关的部分。我们看到,首先在第3行中,从EventHub中去获取新的事件,之后在第10行,去处理这些事件,第15行会清楚所有的事件,我们分别看看各个里面的逻辑。

从EventHub读取事件

首先是getEvents方法:

 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
size_t EventHub::getEvents(int timeoutMillis, RawEvent* buffer, size_t bufferSize) {
 std::scoped_lock _l(mLock);
 struct input_event readBuffer[bufferSize];
 RawEvent* event = buffer;
 size_t capacity = bufferSize;
 bool awoken = false;
 for (;;) {
 nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);

 bool deviceChanged = false;
 //pendingIndex小于PendingCount,说明之前有事件还为处理完
 while (mPendingEventIndex < mPendingEventCount) {
 const struct epoll_event& eventItem = mPendingEventItems[mPendingEventIndex++];
 ...
 Device* device = getDeviceByFdLocked(eventItem.data.fd);
 if (device == nullptr) { //未能找到device,报错跳出
 continue;
 }

 // EPOLLIN表示有事件可以处理
 if (eventItem.events & EPOLLIN) {
 int32_t readSize =
 read(device->fd, readBuffer, sizeof(struct input_event) * capacity);
 if (readSize == 0 || (readSize < 0 && errno == ENODEV)) {
 // 接收到通知之前,设备以及不见了
 deviceChanged = true;
 closeDeviceLocked(*device);
 } else if() { //其中的错误情况,忽略掉
 } else {
 int32_t deviceId = device->id == mBuiltInKeyboardId ? 0 : device->id;

 size_t count = size_t(readSize) / sizeof(struct input_event); //根据一个事件的大小,来算同一个设备上面读取到的事件的个数
 //以下为具体保存事件到event当中
 for (size_t i = 0; i < count; i++) {
 struct input_event& iev = readBuffer[i];
 event->when = processEventTimestamp(iev);
 event->readTime = systemTime(SYSTEM_TIME_MONOTONIC);
 event->deviceId = deviceId;
 event->type = iev.type;
 event->code = iev.code;
 event->value = iev.value;
 event += 1;
 capacity -= 1;
 }
 if (capacity == 0) { //缓冲区已经满了,无法在记录事件,跳出
 mPendingEventIndex -= 1;
 break;
 }
 }
 } else if (eventItem.events & EPOLLHUP) {
 ...
 } else {
 ...
 }
 }
 ...
 //event和buffer地址不同说明已经拿到事件了,可以跳出循环
 if (event != buffer || awoken) {
 break;
 }

 mPendingEventIndex = 0;
 mLock.unlock(); // poll之前先加锁
 int pollResult = epoll_wait(mEpollFd, mPendingEventItems, EPOLL_MAX_EVENTS, timeoutMillis);
 mLock.lock(); // poll完之后从新加锁

 if (pollResult == 0) {
 // Timed out.
 mPendingEventCount = 0;
 break;
 }

 if (pollResult < 0) {
 mPendingEventCount = 0;
 if (errno != EINTR) {
 usleep(100000);
 }
 } else {
 mPendingEventCount = size_t(pollResult);
 }
 }

 // event为填充之后的指针地址,而buffer为开始的地址,相减获得count
 return event - buffer;
}

这个方法是很复杂的,但是我们主要分析事件的分发,因此其中关于设备变化,设备响应,错误处理等等相关的代码都省略了。这个方法,我们传入了一个RawEvent的指针用来接收事件,另外传了bufferSize来表示我们所能接收的事件数量。这个方法使用了两层循环来进行逻辑的处理,外层的为无限循环。当我们第一次进入这个方法当中,mPendingEventCountmPendingEventIndex都是0,因此不会进入第二层的循环,这个时候会执行到64行,调用epoll_wait系统调用,去读取事件,读取的结果会放到mPendingEventItems当中,之后会算出pendingCount。这样继续循环,我们就可以进入内存循环当中了。 在刚刚的PendingEventItem中并没有存储具体的事件,而是存储的事件发生的设备文件描述符,在内存的循环中,首先会根据设备的描述符查找设备,并对其进行检查。之后再从设备当中读取事件,拼装成为需要向后分发的事件。 这里的count有点让人迷糊,我画了个图如下所示:

其中我们真正读取的事件的数量,是要看有几个设备,每个设备有多少个事件,对其进行计算。 到这里我们就获取到了事件,这里可以回到InputReader中继续往下看了。

InputReader对事件进行处理

在这里的处理调用的是processEventsLocked,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
void InputReader::processEventsLocked(const RawEvent* rawEvents, size_t count) {
 for (const RawEvent* rawEvent = rawEvents; count;) {
 int32_t type = rawEvent->type;
 size_t batchSize = 1;
 //如果不是设备处理相关的事件,则执行。
 if (type < EventHubInterface::FIRST_SYNTHETIC_EVENT) {
 int32_t deviceId = rawEvent->deviceId;
 while (batchSize < count) {
 if (rawEvent[batchSize].type >= EventHubInterface::FIRST_SYNTHETIC_EVENT ||
 rawEvent[batchSize].deviceId != deviceId) {
 //当遇到设备整删除事件,或者不是当前设备的事件,就不能进行批量处理,跳过。
 break;
 }
 batchSize += 1;
 }
 processEventsForDeviceLocked(deviceId, rawEvent, batchSize);
 } else {
 //设备添加删除之类的事件处理,跳过
 }
 count -= batchSize;
 rawEvent += batchSize;
 }
}

这个方法中主要是对与设备增加删除事件和普通事件进行分别处理,如果是普通的事件,会对同一个设备上的事件进行批量处理,批量处理则会调用processEventsForDeviceLocked方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
void InputReader::processEventsForDeviceLocked(int32_t eventHubId, const RawEvent* rawEvents,
 size_t count) {
 auto deviceIt = mDevices.find(eventHubId);
 if (deviceIt == mDevices.end()) {
 //没有找到设备,返回
 return;
 }

 std::shared_ptr<InputDevice>& device = deviceIt->second;
 if (device->isIgnored()) { //是被忽略的设备,跳过
 return;
 }

 device->process(rawEvents, count);
}

这个方法中主要是查找设备,找到未忽略的设备则会调用设备的process方法进行处理。 InputDevice只是设备的抽象,而其中的处理又会调用InputMapper的方法,InputMapper是抽象类,它有许多的实现,比如我们的触摸事件就会有TouchuInputMapperMultiTouchInputMapper,各种不同的InputMapper会对事件进行处理,拼装成符合相关类型的事件,其中逻辑我们就不继续进行追踪了。

对于touch事件,这个process处理完成,在TouchInputMapper中最终会调用dispatchMotion,这个方法代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
void TouchInputMapper::dispatchMotion(...) {
 PointerCoords pointerCoords[MAX_POINTERS];
 PointerProperties pointerProperties[MAX_POINTERS];
 uint32_t pointerCount = 0;
 ...
 const int32_t displayId = getAssociatedDisplayId().value_or(ADISPLAY_ID_NONE);
 const int32_t deviceId = getDeviceId();
 std::vector<TouchVideoFrame> frames = getDeviceContext().getVideoFrames();
 std::for_each(frames.begin(), frames.end(),
 [this](TouchVideoFrame& frame) { frame.rotate(this->mInputDeviceOrientation); });
 NotifyMotionArgs args(getContext()->getNextId(), when, readTime, deviceId, source, displayId,
 policyFlags, action, actionButton, flags, metaState, buttonState,
 MotionClassification::NONE, edgeFlags, pointerCount, pointerProperties,
 pointerCoords, xPrecision, yPrecision, xCursorPosition, yCursorPosition,
 downTime, std::move(frames));
 getListener().notifyMotion(&args);
}

其中有许多关于多点触控,事件处理的判断,这里只关注最后的部分,就是将事件组装成一个NotifyMotionArgs对象,并调用ListenernotifyMotion方法。这里的getListener()内部首先会调用getContenxt获取Context,而这个Context就是InputReader的内部成员mContext,这这个Listener也就是我们之前在初始化InputReader时候它的成员变量mQueuedListener,那我们下面继续去看它的notifyMotion

notifyMotion

1
2
3
4
void QueuedInputListener::notifyMotion(const NotifyMotionArgs* args) {
 traceEvent(__func__, args->id);
 mArgsQueue.emplace_back(std::make_unique<NotifyMotionArgs>(*args));
}

这里是直接把之前的那个变量放到mArgsQueue当中了。这个时候,我们需要留意一下之前InputReadeloopOnce的15行,这里调用的 flush方法,也是这个QueuedInputListener内部的:

1
2
3
4
5
6
void QueuedInputListener::flush() {
 for (const std::unique_ptr<NotifyArgs>& args : mArgsQueue) {
 args->notify(mInnerListener);
 }
 mArgsQueue.clear();
}

这里这是掉用了我们传进来的NotifyArgs的notify方法,并且传过来的参数mInnerListener是我们之前创建InputManager时候创建的,这里会有三层嵌套,首先是UnWantedInteractionBlocker先处理,之后它会按情况传递给InputClassifier处理,最后是在InputDispatcher当中处理。

我们先看看看notify当中做了什么,再继续往后看。

1
2
3
void NotifyMotionArgs::notify(InputListenerInterface& listener) const {
 listener.notifyMotion(this);
}

这里也是比较简单,就是直接调用了linster的notifyMotion方法,我们可以直接去看了。因为我们主要关注 传递,而不关注处理,这里就跳过,直接看InputDispatcher中的这个方法。

 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
void InputDispatcher::notifyMotion(const NotifyMotionArgs* args) {
 if (!validateMotionEvent(args->action, args->actionButton, args->pointerCount,
 args->pointerProperties)) {
 return; //不合法的触摸事件直接返回
 }

 uint32_t policyFlags = args->policyFlags;
 policyFlags |= POLICY_FLAG_TRUSTED;

 android::base::Timer t;

 bool needWake = false;
 { // acquire lock
 mLock.lock();
 ...
 std::unique_ptr<MotionEntry> newEntry =
 std::make_unique<MotionEntry>(args->id, args->eventTime, args->deviceId,
 args->source, args->displayId, policyFlags,
 args->action, args->actionButton, args->flags,
 args->metaState, args->buttonState,
 args->classification, args->edgeFlags,
 args->xPrecision, args->yPrecision,
 args->xCursorPosition, args->yCursorPosition,
 args->downTime, args->pointerCount,
 args->pointerProperties, args->pointerCoords);
 ...
 needWake = enqueueInboundEventLocked(std::move(newEntry));
 mLock.unlock();
 } // release lock

 if (needWake) {
 mLooper->wake();
 }
}

在这里则是执行完一些检查之后,把事件封装成为MotionEntry,调用enqueueInboundEventLocked,最后调用looperwake方法。enqueueInboundEventLocked代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
bool InputDispatcher::enqueueInboundEventLocked(std::unique_ptr<EventEntry> newEntry) {
 bool needWake = mInboundQueue.empty();
 mInboundQueue.push_back(std::move(newEntry));
 EventEntry& entry = *(mInboundQueue.back());
 switch (entry.type) {
 case EventEntry::Type::MOTION: {
 if (shouldPruneInboundQueueLocked(static_cast<MotionEntry&>(entry))) { //返回true的时候,事件会被移除不处理
 mNextUnblockedEvent = mInboundQueue.back();
 needWake = true;
 }
 break;
 }
 ...
 }

 return needWake;
}

在这里,首先把事件放入mInboundQueue这个deque当中,最后根据事件的类型和信息要不要唤醒looper,如果事件不被移除needWake就为false,前面的wake也不会被调用。但是这个是否调用,不影响我们的后续分析,因为InputDispatch中的Thead会一直循环调用。

InputDispatcher分发消息

说到这里,我们就该来看看InputDispatcherdispatchOnce方法了:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
void InputDispatcher::dispatchOnce() {
 nsecs_t nextWakeupTime = LONG_LONG_MAX;
 { // acquire lock
 std::scoped_lock _l(mLock);
 mDispatcherIsAlive.notify_all();

 if (!haveCommandsLocked()) {
 dispatchOnceInnerLocked(&nextWakeupTime);
 }

 if (runCommandsLockedInterruptable()) {
 nextWakeupTime = LONG_LONG_MIN;
 }
 ...
 } // release lock

 //等待下一次调用
 mLooper->pollOnce(timeoutMillis);
}

这里有不少处理下一次唤醒的逻辑,我们都跳过,主要就看一下第8行,进行这一次的实际执行内容:

 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
void InputDispatcher::dispatchOnceInnerLocked(nsecs_t* nextWakeupTime) {
 ...
 if (!mPendingEvent) { //当前没有待处理的pending事件
 if (mInboundQueue.empty()) { //如果事件列表为空
 ...
 if (!mPendingEvent) {
 return;
 }
 } else {
 // 列表中拿一个事件
 mPendingEvent = mInboundQueue.front();
 mInboundQueue.pop_front();
 traceInboundQueueLengthLocked();
 }
 ...
 }

 bool done = false;
 ..
 switch (mPendingEvent->type) {
 ...
 case EventEntry::Type::MOTION: {
 std::shared_ptr<MotionEntry> motionEntry =
 std::static_pointer_cast<MotionEntry>(mPendingEvent);
 ...
 done = dispatchMotionLocked(currentTime, motionEntry, &dropReason, nextWakeupTime);
 break;
 }
 ...
 }

 if (done) {
 ...
 releasePendingEventLocked();
 *nextWakeupTime = LONG_LONG_MIN; // force next poll to wake up immediately
 }
}

我们将这个方法进行了简化,仅仅保留了触摸事件的部分代码。首先判断mPendingEvent是否为空,为空的时候我们需要到mPendingEvent中去拿一个,我们之前插入的是尾部,这里是从头部取的。拿到事件进行完种种处理和判断之后,会调用dispatchMotionLocked进行触摸事件的分发:

 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
bool InputDispatcher::dispatchMotionLocked(nsecs_t currentTime, std::shared_ptr<MotionEntry> entry,
 DropReason* dropReason, nsecs_t* nextWakeupTime) {
 if (!entry->dispatchInProgress) { //设置事件正在处理中
 entry->dispatchInProgress = true;

 }

 if (*dropReason != DropReason::NOT_DROPPED) {
 //对于要抛弃的事件这里进行处理,返回
 return true;
 }

 const bool isPointerEvent = isFromSource(entry->source, AINPUT_SOURCE_CLASS_POINTER); //读取是否为POINTER
 std::vector<InputTarget> inputTargets;

 bool conflictingPointerActions = false;
 InputEventInjectionResult injectionResult;
 if (isPointerEvent) {
 //如果屏幕触摸事件则去找到对应的window
 injectionResult =
 findTouchedWindowTargetsLocked(currentTime, *entry, inputTargets, nextWakeupTime,
 &conflictingPointerActions);
 } else {
 // Non touch event. (eg. trackball)
 injectionResult =
 findFocusedWindowTargetsLocked(currentTime, *entry, inputTargets, nextWakeupTime);
 }
 if (injectionResult == InputEventInjectionResult::PENDING) {
 return false;
 }

 setInjectionResult(*entry, injectionResult);
 ...

 // Dispatch the motion.
 dispatchEventLocked(currentTime, entry, inputTargets);
 return true;
}

这个方法中依然是对于事件做很多的处理和判断,比如否要抛弃等。但是其中最终要的是调用findFocusedWIndowTargetsLocked来找到我们的事件所对应的Window,并且保存相关信息到inputTargets当中,这里获取inputTargets的过程比较复杂,但是简单来说呢就是从之前我们保存在InputDispatcher中的mConnectionsByToken中查找到对应的条目,这里暂不深入分析。拿到这个之后就是调用dispatchEventLocked去分发,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void InputDispatcher::dispatchEventLocked(nsecs_t currentTime,
 std::shared_ptr<EventEntry> eventEntry,
 const std::vector<InputTarget>& inputTargets) {
 ...
 for (const InputTarget& inputTarget : inputTargets) {
 sp<Connection> connection =
 getConnectionLocked(inputTarget.inputChannel->getConnectionToken());
 if (connection != nullptr) {
 prepareDispatchCycleLocked(currentTime, connection, eventEntry, inputTarget);
 } else {

 }
 }
}

通过这里我们可以看到首先是通过inputTarget去拿到connectionToken,再通过它拿到Connection。最后通过调用prepareDispatchCycleLocked

1
2
3
4
5
6
7
void InputDispatcher::prepareDispatchCycleLocked(nsecs_t currentTime,
 const sp<Connection>& connection,
 std::shared_ptr<EventEntry> eventEntry,
 const InputTarget& inputTarget) {
 ...
 enqueueDispatchEntriesLocked(currentTime, connection, eventEntry, inputTarget);
}

这个方法简化的化,这是调用第6行的这个方法,代码如下:

 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
void InputDispatcher::enqueueDispatchEntriesLocked(nsecs_t currentTime,
 const sp<Connection>& connection,
 std::shared_ptr<EventEntry> eventEntry,
 const InputTarget& inputTarget) {

 bool wasEmpty = connection->outboundQueue.empty();

 // Enqueue dispatch entries for the requested modes.
 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
 InputTarget::FLAG_DISPATCH_AS_HOVER_EXIT);
 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
 InputTarget::FLAG_DISPATCH_AS_OUTSIDE);
 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
 InputTarget::FLAG_DISPATCH_AS_HOVER_ENTER);
 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
 InputTarget::FLAG_DISPATCH_AS_IS);
 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
 InputTarget::FLAG_DISPATCH_AS_SLIPPERY_EXIT);
 enqueueDispatchEntryLocked(connection, eventEntry, inputTarget,
 InputTarget::FLAG_DISPATCH_AS_SLIPPERY_ENTER);

 // If the outbound queue was previously empty, start the dispatch cycle going.
 if (wasEmpty && !connection->outboundQueue.empty()) {
 startDispatchCycleLocked(currentTime, connection);
 }
}

其中对于消息会尝试按照每一种mode都调用enqueueDIspatchEntryLocked方法,代码如下:

 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
void InputDispatcher::enqueueDispatchEntryLocked(const sp<Connection>& connection,
 std::shared_ptr<EventEntry> eventEntry,
 const InputTarget& inputTarget,
 int32_t dispatchMode) {

 int32_t inputTargetFlags = inputTarget.flags;
 if (!(inputTargetFlags & dispatchMode)) {
 return;
 }
 inputTargetFlags = (inputTargetFlags & ~InputTarget::FLAG_DISPATCH_MASK) | dispatchMode;

 std::unique_ptr<DispatchEntry> dispatchEntry =
 createDispatchEntry(inputTarget, eventEntry, inputTargetFlags);

 EventEntry& newEntry = *(dispatchEntry->eventEntry);
 // Apply target flags and update the connection's input state.
 switch (newEntry.type) {
 case EventEntry::Type::MOTION: {
 const MotionEntry& motionEntry = static_cast<const MotionEntry&>(newEntry);
 constexpr int32_t DEFAULT_RESOLVED_EVENT_ID =
 static_cast<int32_t>(IdGenerator::Source::OTHER);
 dispatchEntry->resolvedEventId = DEFAULT_RESOLVED_EVENT_ID;
 ...


 if ((motionEntry.flags & AMOTION_EVENT_FLAG_NO_FOCUS_CHANGE) &&
 (motionEntry.policyFlags & POLICY_FLAG_TRUSTED)) {
 break;
 }

 dispatchPointerDownOutsideFocus(motionEntry.source, dispatchEntry->resolvedAction,
 inputTarget.inputChannel->getConnectionToken());
 break;
 }
 ...
 }
 connection->outboundQueue.push_back(dispatchEntry.release());
 traceOutboundQueueLength(*connection);
}

在这个方法中,又把事件封装成为dispatchEntry,并放到Connection内部的outboundQueue这个队列当中。

到这里我们可以回看上面的enqueueDispatchEntriesLocked的最后一块代码,那里有判断了如果这个outboundQueue队列不为空,则会执行最后的startDispatchCycleLocked,代码如下:

 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
void InputDispatcher::startDispatchCycleLocked(nsecs_t currentTime,
 const sp<Connection>& connection) {

 while (connection->status == Connection::Status::NORMAL && !connection->outboundQueue.empty()) {
 DispatchEntry* dispatchEntry = connection->outboundQueue.front();
 dispatchEntry->deliveryTime = currentTime;
 ...
 // Publish the event.
 status_t status;
 const EventEntry& eventEntry = *(dispatchEntry->eventEntry);
 switch (eventEntry.type) {
 ...
 case EventEntry::Type::MOTION: {
 const MotionEntry& motionEntry = static_cast<const MotionEntry&>(eventEntry);
 ...

 std::array<uint8_t, 32> hmac = getSignature(motionEntry, *dispatchEntry);

 status = connection->inputPublisher
 .publishMotionEvent(dispatchEntry->seq,
 dispatchEntry->resolvedEventId,
 motionEntry.deviceId, motionEntry.source,
 motionEntry.displayId, std::move(hmac),
 dispatchEntry->resolvedAction,
 motionEntry.actionButton,
 dispatchEntry->resolvedFlags,
 motionEntry.edgeFlags, motionEntry.metaState,
 motionEntry.buttonState,
 motionEntry.classification,
 dispatchEntry->transform,
 motionEntry.xPrecision, motionEntry.yPrecision,
 motionEntry.xCursorPosition,
 motionEntry.yCursorPosition,
 dispatchEntry->rawTransform,
 motionEntry.downTime, motionEntry.eventTime,
 motionEntry.pointerCount,
 motionEntry.pointerProperties, usingCoords);
 break;
 }
 }
 ...

 }
}

在这个方法中,则是从outboundQueue把所有的事件一条一条的取出来,解包成它要的类型,比如触摸事件就是MotionEntry,经过判断和一些处理之后,调用connection中的inputPublisherpublishMotionEvent方法,这里的inputPublisher我们之前分析创建InputChannel的时候有所了解,创建它所传的InputChannel为Server端的那个。 我们这里看一下它的publishMotionEvent方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
status_t InputPublisher::publishMotionEvent(...) {

 InputMessage msg;
 msg.header.type = InputMessage::Type::MOTION;
 msg.header.seq = seq;
 msg.body.motion.eventId = eventId;
 ...
 msg.body.motion.pointerCount = pointerCount;
 for (uint32_t i = 0; i < pointerCount; i++) {
 msg.body.motion.pointers[i].properties.copyFrom(pointerProperties[i]);
 msg.body.motion.pointers[i].coords.copyFrom(pointerCoords[i]);
 }

 return mChannel->sendMessage(&msg);
}

这里主要创建了InputMessage,将之前MotionEvent的所有参数放进去,通过ServerInputChannel调用sendMessage发送出去,sendMessage的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
status_t InputChannel::sendMessage(const InputMessage* msg) {
 const size_t msgLength = msg->size();
 InputMessage cleanMsg;
 msg->getSanitizedCopy(&cleanMsg);
 ssize_t nWrite;
 do {
 nWrite = ::send(getFd(), &cleanMsg, msgLength, MSG_DONTWAIT | MSG_NOSIGNAL);
 } while (nWrite == -1 && errno == EINTR);

 return OK;
}

我们知道,InputChannel内部的文件描述符为socket的标记,这里调用send方法,也就是通过socket把信息发送出去,这样的话,我们Client端的的socket也就会接收到,通过Looper,Client端的EventListener就可以接收到消息,我们的应用便可以接收到,到这里便把事件分发,完整的串起来了。

总结

以上就是事件在系统进程中的处理,包括它的事件获取,事件处理,最后通过socket发送,这样我们在客户端进程的InputChannel就能够接收到通知,客户端能够处理事件。配合我们之前分析过应用层的事件分发,到这里,不算事件的驱动相关的部分,事件分发的整个流程我们都有所了解了。

在这里事件从系统system_server通过Server的InputChannel发送的客户端的InputChannel,所采用的是unix的socket功能,而不是使用的binder或者其他的跨进程服务。这一块,结合我在网上查找的资料,以及我自己的想法,我想这里这样做的原因是,unix的sockt pair使用上很简单,并且运行效率很高效,不需要像binder一样涉及到进程和线程的切换。另外就是socket使用了fd,目标进程可以直接监听到事件的来临,而不是向binder一样需要有相应的接口涉及,可以更加实时的接收到事件,也不会因为binder线程阻塞而卡顿。

当然这是我的一些想法,也欢迎读者朋友说说你对于这块的想法。

看完评论一下吧

  •  

Android源码分析:从源头分析View事件的传递

对于应用开发者的我们来说,经常会处理按钮点击,键盘输入等事件,而我们的处理一般都是在Activity中或者View中去做的。我们在上一篇文章中分析了View和Activity与Window的关系,其中的ViewRootImpl和我们的事件传递息息相关,上文未能分析,本文将对其进行分析。

事件介绍

事件是什么呢,广义上事件的发生可能在软件也可能在硬件层,在Android设备当中,我们会有可能有键盘触发,触摸触发,鼠标触发的各种事件。我们关注的通常有两种事件: 按键事件(KeyEvent): 这种色包括物理的按键,Home键,音量键,也包括软键盘触发的事件。 触摸事件(TouchEvent): 手指在屏幕上触摸触发的事件,可能是点击,也可能是拖动。

对于按键事件,一般有ACTION_DOWNACTION_UP两种状态,对于KeyEvent所支持的所有keyCode,我们都可以在KeyEvent当中找到。

而对于触摸事件来说,除了DOWNUP两种状态之外,还有ACTION_MOVEACTION_CANCEL等状态。

应用层的事件类图如下图所示:

classDiagram
class Parcelable {
<<interface>>
}
class InputEvent {
<<abstract>>
}
class KeyEvent
class MotionEvent
InputEvent<|--KeyEvent
InputEvent<|--MotionEvent
Parcelable<|..KeyEvent
Parcelable<|..MotionEvent
Parcelable<|..InputEvent

事件传递到View

我们一般处理View的onClick事件,而这个事件是在View的onTouchEvent中进行处理并执行的,在View中我们可以向上追溯到dispatchPointerEvent方法当中,这个方法就是外部向View传递事件的调用。我们知道Android的UI界面中的所有View是一个树形的结构,因此这些事件也就会通过dispatchTouchEvent一层一层的往下传,从而每一个View都能够接收到事件,并决定是否处理。

dispatchPointerEvent是在ViewRootImpl当中调用,代码如下:

1
2
3
4
5
6
7
8
9
private int processPointerEvent(QueuedInputEvent q) {
 final MotionEvent event = (MotionEvent)q.mEvent;
 ...
 boolean handled = mView.dispatchPointerEvent(event);
 maybeUpdatePointerIcon(event);
 maybeUpdateTooltip(event);
 ...
 return handled ? FINISH_HANDLED : FORWARD;
}

在Activity中,它的根视图为DecorViewViewRootImpl在执行它的dispatchPointerEvent方法,它再向下把触摸事件依次向下传递。

除了触摸事件,按键事件也是类似,ViewRootImpl当中会调用View的dispatchKeyEvent方法,View当中会做相应的处理或者向下传递。

ViewRootImpl中对事件的处理

对于ViewRootImpl当中是如何获取事件,并且向后传递的,我们这里以触摸事件为主进行分析,其他事件也类似。

ViewRootImpl中,定义写一些内部类,大概如下:

classDiagram
class InputStage {
<<abstract>>
+InputStage mNext;
+deliver(QueuedInputEvent q)
#finish(QueuedInputEvent q, boolean handled)
#forward(QueuedInputEvent q)
#onDeliverToNext(QueuedInputEvent q)
#onProcess(QueuedInputEvent q)
}
class AsyncInputStage {
<<abstract>>
#defer(QueuedInputEvent q)
}
InputStage <|-- AsyncInputStage
AsyncInputStage <|--NativePreImeInputStage
InputStage <|-- ViewPreImeInputStage
AsyncInputStage <|-- ImeInputStage
InputStage <|-- EarlyPostImeInputStage
AsyncInputStage <|-- NativePostImeInputStage
InputStage <|-- ViewPostImeInputStage
InputStage <|--SyntheticInputStage

上面这几个类就ViewRootImpl中处理事件的类,其初始化代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//ViewRootImpl.java
public void setView(...) {
 ...
 CharSequence counterSuffix = attrs.getTitle();
 mSyntheticInputStage = new SyntheticInputStage();
 InputStage viewPostImeStage = new ViewPostImeInputStage(mSyntheticInputStage);
 InputStage nativePostImeStage = new NativePostImeInputStage(viewPostImeStage,
 "aq:native-post-ime:" + counterSuffix);
 InputStage earlyPostImeStage = new EarlyPostImeInputStage(nativePostImeStage);
 InputStage imeStage = new ImeInputStage(earlyPostImeStage,
 "aq:ime:" + counterSuffix);
 InputStage viewPreImeStage = new ViewPreImeInputStage(imeStage);
 InputStage nativePreImeStage = new NativePreImeInputStage(viewPreImeStage,
 "aq:native-pre-ime:" + counterSuffix);

 mFirstInputStage = nativePreImeStage;
 mFirstPostImeInputStage = earlyPostImeStage;
}

以上代码创建了多个InputStage,它们一起组成了输入事件处理的流水线。其中ViewPostImeInputStage中就会处理与触摸相关的事件,它的onProcess方法代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
protected int onProcess(QueuedInputEvent q) {
 if (q.mEvent instanceof KeyEvent) {
 return processKeyEvent(q);
 } else {
 final int source = q.mEvent.getSource();
 if ((source & InputDevice.SOURCE_CLASS_POINTER) != 0) {
 return processPointerEvent(q);
 } else if ((source & InputDevice.SOURCE_CLASS_TRACKBALL) != 0) {
 return processTrackballEvent(q);
 } else {
 return processGenericMotionEvent(q);
 }
 }
}

可以看到,当我们的输入源为POINTER,触摸屏和鼠标的触发都是这一类。这个时候就会执行上面我们提到的 processPointerEvent方法,之后事件也就会传递到View当中。

这里我们知道了是通过InputStage的流水线拿到的事件,但是这个事件从何处来的呢,我们需要继续向上溯源。

ViewRootImpl从何处获得事件

关于这一点,我们仍然需要关注ViewRootImplsetView方法中的如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
//VieRootImpl.java
InputChannel inputChannel = null;
if ((mWindowAttributes.inputFeatures
 & WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
 inputChannel = new InputChannel();
}
...
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
 getHostVisibility(), mDisplay.getDisplayId(), userId,
 mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
 mTempControls, attachedFrame, compatScale);
...
if (inputChannel != null) {

 mInputEventReceiver = new WindowInputEventReceiver(inputChannel,
 Looper.myLooper());

}

在这里,我们创建了一个InputChannel,但是我们创建的InputChannel仅仅是java层的一个类,没法去获取到事件,随后我们调用WindowSessionaddToDisplayAsUser他就会获得mPtr,也就是Native层的InputChannel,具体内容随后再看相关代码。在15行,这里创建了一个WindowInputEventReceiver,它的参数为inputChannelLooper,这里一起看一下InputEventReceiver的构造方法,代码如下:

1
2
3
4
5
6
7
8
public InputEventReceiver(InputChannel inputChannel, Looper looper) {
 mInputChannel = inputChannel;
 mMessageQueue = looper.getQueue();
 mReceiverPtr = nativeInit(new WeakReference<InputEventReceiver>(this),
 inputChannel, mMessageQueue);

 mCloseGuard.open("InputEventReceiver.dispose");
}

InputEventReceiver的初始化

这里主要是调用了nativeInit方法,并且获取到mReceivePtr,native的代码在android_view_InputEventReceiver.cpp当中:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
static jlong nativeInit(JNIEnv* env, jclass clazz, jobject receiverWeak,
 jobject inputChannelObj, jobject messageQueueObj) {
 std::shared_ptr<InputChannel> inputChannel =
 android_view_InputChannel_getInputChannel(env, inputChannelObj); //获取Native成的InputChannel
 sp<MessageQueue> messageQueue = android_os_MessageQueue_getMessageQueue(env, messageQueueObj); //获取native层的消息队列

 sp<NativeInputEventReceiver> receiver = new NativeInputEventReceiver(env,
 receiverWeak, inputChannel, messageQueue);
 status_t status = receiver->initialize();
 receiver->incStrong(gInputEventReceiverClassInfo.clazz); // 增加引用计数
 return reinterpret_cast<jlong>(receiver.get());
}

在上面的代码中,先是分别获取了Native层的InputChannel和MessageQueue,之后创建了NativeInputEventReceiver,并且调用了它的initialize方法:

1
2
3
4
status_t NativeInputEventReceiver::initialize() {
 setFdEvents(ALOOPER_EVENT_INPUT);
 return OK;
}

内部调用了setFdEvents方法,参数ALOOPER_EVENT_INPUT,这个参数表示监听文件描述符的读操作,其内部代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void NativeInputEventReceiver::setFdEvents(int events) {
 if (mFdEvents != events) {
 mFdEvents = events;
 int fd = mInputConsumer.getChannel()->getFd();
 if (events) {
 mMessageQueue->getLooper()->addFd(fd, 0, events, this, nullptr);
 } else {
 mMessageQueue->getLooper()->removeFd(fd);
 }
 }
}

这里就是拿到InputChannel的文件描述符,并且添加到Looper中去监听它的输入事件。我们暂时不会去阅读硬件层面的触发,以及事件如何发送到InputChannel当中,这里就大胆的假设,InputChannel当中有一个文件描述符,当有事件发生时候,会写入到这个文件当中去。而文件变化,Looper就会收到通知,事件也就发送出来了。

NativeInputEventReceiver 接收事件并分发

这个时候我们可以看一下NativeInputEventReceiver所实现的LooperCallbackhandleEvent方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
int NativeInputEventReceiver::handleEvent(int receiveFd, int events, void* data) {

 constexpr int REMOVE_CALLBACK = 0;
 constexpr int KEEP_CALLBACK = 1;

 if (events & ALOOPER_EVENT_INPUT) {
 JNIEnv* env = AndroidRuntime::getJNIEnv();
 status_t status = consumeEvents(env, false /*consumeBatches*/, -1, nullptr);
 mMessageQueue->raiseAndClearException(env, "handleReceiveCallback");
 return status == OK || status == NO_MEMORY ? KEEP_CALLBACK : REMOVE_CALLBACK;
 }
 ...
 return KEEP_CALLBACK;
}

其中核心代码如上,就是判断如果事件为ALOOPER_EVENT_INPUT,则会调用consumeEvents方法,代码如下:

 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
status_t NativeInputEventReceiver::consumeEvents(JNIEnv* env,
 bool consumeBatches, nsecs_t frameTime, bool* outConsumedBatch) {
 ...
 ScopedLocalRef<jobject> receiverObj(env, nullptr);
 bool skipCallbacks = false;
 for (;;) {
 uint32_t seq;
 InputEvent* inputEvent;

 status_t status = mInputConsumer.consume(&mInputEventFactory,
 consumeBatches, frameTime, &seq, &inputEvent);
 ...
 assert(inputEvent);

 if (!skipCallbacks) {
 if (!receiverObj.get()) {
 receiverObj.reset(jniGetReferent(env, mReceiverWeakGlobal));
 if (!receiverObj.get()) {
 ...
 return DEAD_OBJECT;
 }
 }

 jobject inputEventObj;
 switch (inputEvent->getType()) {
 case AINPUT_EVENT_TYPE_MOTION: {
 MotionEvent* motionEvent = static_cast<MotionEvent*>(inputEvent);
 if ((motionEvent->getAction() & AMOTION_EVENT_ACTION_MOVE) && outConsumedBatch) {
 *outConsumedBatch = true;
 }
 inputEventObj = android_view_MotionEvent_obtainAsCopy(env, motionEvent);
 break;
 }
 ...
 default:
 assert(false); // InputConsumer should prevent this from ever happening
 inputEventObj = nullptr;
 }

 if (inputEventObj) {
 env->CallVoidMethod(receiverObj.get(),
 gInputEventReceiverClassInfo.dispatchInputEvent, seq, inputEventObj);
 ...
 env->DeleteLocalRef(inputEventObj);
 } else {
 ...
 }
 }
 }
}

上面的代码做过简化,switch的case只保留了一个。首先在第10行,我们看到这里调用了mInputConsumerconsume方法。这个InputConsumer是在Receiver创建的时候创建它,它用于到InputChannel中获取消息,并且按照类型包装成InputEvent的具体子类,并写入到inputEvent当中。在后面的Switch判断处,就可以根据它的类型做处理,从而封装成java类型的InputEvent。而receiverObj在第17行,通过jniGetReferent拿到java层的InputEventReceiver的引用,在41行调用了它的dispatchInputEvent方法,从而调用了java层的同名方法,代码如下:

1
2
3
4
private void dispatchInputEvent(int seq, InputEvent event) {
 mSeqMap.put(event.getSequenceNumber(), seq);
 onInputEvent(event);
}

我们再到WindowInputEventReceiver中看onInputEvent方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void onInputEvent(InputEvent event) {
 List<InputEvent> processedEvents;
 try {
 processedEvents =
 mInputCompatProcessor.processInputEventForCompatibility(event);
 } finally {
 }
 if (processedEvents != null) {
 if (processedEvents.isEmpty()) {
 finishInputEvent(event, true);
 } else {
 for (int i = 0; i < processedEvents.size(); i++) {
 enqueueInputEvent(
 processedEvents.get(i), this,
 QueuedInputEvent.FLAG_MODIFIED_FOR_COMPATIBILITY, true);
 }
 }
 } else {
 enqueueInputEvent(event, this, 0, true);
 }
}

其中第4行代码,是为了兼容低版本设计的,只有应用的TargetSDKVersion小于23才会生效,这里我们就不关注它了。因此这里就只会执行第19行的代码,其内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
void enqueueInputEvent(InputEvent event,
 InputEventReceiver receiver, int flags, boolean processImmediately) {
 QueuedInputEvent q = obtainQueuedInputEvent(event, receiver, flags);
 if (event instanceof MotionEvent) {
 MotionEvent me = (MotionEvent) event;
 }
 QueuedInputEvent last = mPendingInputEventTail;
 if (last == null) {
 mPendingInputEventHead = q;
 mPendingInputEventTail = q;
 } else {
 last.mNext = q;
 mPendingInputEventTail = q;
 }
 mPendingInputEventCount += 1;
 if (processImmediately) {
 doProcessInputEvents();
 } else {
 scheduleProcessInputEvents();
 }
}

这里的代码,把我们的Event包装成一个QueuedInputEvent,并且放置到mQueuedInputEventPool这个链表中,具体可以自行看obtainQueuedInputEvent方法。而根据我们之前传递的参数,可以看到这里后面会调用到doProcessInputEvents方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
void doProcessInputEvents() {
 // Deliver all pending input events in the queue. 
 while (mPendingInputEventHead != null) {
 QueuedInputEvent q = mPendingInputEventHead;
 mPendingInputEventHead = q.mNext;
 if (mPendingInputEventHead == null) {
 mPendingInputEventTail = null;
 }
 q.mNext = null;

 mPendingInputEventCount -= 1; mViewFrameInfo.setInputEvent(mInputEventAssigner.processEvent(q.mEvent));

 deliverInputEvent(q);
 }
 //除了我们收到调用来把事件队列的所有事件消费,还有一些消息本来是准备通过Handler发送消息来处理的,既然我们已经手动把所有消息都处理掉了,那么如果有等待处理的消息事件,也就不需要了,下面的代码就是把他们删掉
 if (mProcessInputEventsScheduled) {
 mProcessInputEventsScheduled = false;
 mHandler.removeMessages(MSG_PROCESS_INPUT_EVENTS);
 }
}

这里的代码主要就是遍历之前的链表,把每一条消息都取出来,并且调用deliverInputEvent方法来把它分发掉,同时会把它从链表中删除。

 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
private void deliverInputEvent(QueuedInputEvent q) {
 try {
 if (mInputEventConsistencyVerifier != null) {
 try { //事件一致性检查,避免外面传过来应用无法处理的事件
 mInputEventConsistencyVerifier.onInputEvent(q.mEvent, 0);
 } finally {
 }
 }

 InputStage stage; //选择要使用的入口InputStage
 if (q.shouldSendToSynthesizer()) {
 stage = mSyntheticInputStage;
 } else {
 stage = q.shouldSkipIme() ? mFirstPostImeInputStage : mFirstInputStage;
 }
 ...
 if (stage != null) {
 handleWindowFocusChanged();
 stage.deliver(q); //InputStage 开始分发事件
 } else {
 finishInputEvent(q);
 }
 } finally {

 }
}

在这个方法中,主要就是根据事件的属性选择入口的InputStage,之后调用它的deliver方法,在这个方法中就会按照链式调用,最终能够处理掉的一个InputStage会将它处理,也就是把事件分发到应用中去。

到这里我们就完成了从InputChannel中获取事件,并且通过InputEventReceiver传递到Java层,并且通过InputStage转发到应用的View当中。

InputChannel的初始化

刚刚我们已经基本把事件处理在ViewRootImpl中的部分看完了,而我们在其中创建的InputChannel只是一个壳,想要看看它的真正的初始化,我们沿着之前调用的addToDisplayAsUser继续往后看。IWindowSession是一个AIDL定义的Binder服务,在它的定义中InputChannel使用了out进行修饰,表示它会被binder服务端修改,并写入数据。而这个addToDisplayAsUser方法内部最终会调用WMS的addWindow方法,其中和InputChannel相关代码如下:

1
2
3
4
5
6
7
8
final WindowState win = new WindowState(this, session, client, token, parentWindow,
 appOp[0], attrs, viewVisibility, session.mUid, userId,
 session.mCanAddInternalSystemWindow);
 final boolean openInputChannels = (outInputChannel != null
 && (attrs.inputFeatures & INPUT_FEATURE_NO_INPUT_CHANNEL) == 0);
if (openInputChannels) {
 win.openInputChannel(outInputChannel);
}

这里outInputChannel就是我们从客户端传过来的那个InputChannel的壳,随后便调用了WindowStateopenInputChannel方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void openInputChannel(InputChannel outInputChannel) {
 String name = getName(); //获取window的name
 mInputChannel = mWmService.mInputManager.createInputChannel(name); //创建
 mInputChannelToken = mInputChannel.getToken();
 mInputWindowHandle.setToken(mInputChannelToken);
 mWmService.mInputToWindowMap.put(mInputChannelToken, this);
 if (outInputChannel != null) {
 mInputChannel.copyTo(outInputChannel); //将Native Channel写入我们传入的InputChannel
 } else {
 }
}

这里就是调用InputManager去创建InputChannel,并且把它和我们的WIndow关联,以及保存到我们传入的InputChannel当中,这样我们的View层面就可以通过InputChannel获取到事件了。InputManagerService创建InputChannel的部分这里就不讨论了,留待以后讨论。

总结

到此为止,就分析完了应用侧从WMS到View,如何初始化InputEventReceiver,InputEventReceiver和InputChannel关联起来,事件如何从InputChannel一直传递到我们的View的了。

sequenceDiagram
InputChannel-->>NativeInputEventReceiver: handleEvent
note right of InputChannel: notify has event via Looper
NativeInputEventReceiver->> NativeInputEventReceiver: consumeEvents
NativeInputEventReceiver->>+ InputChannel: consume
note right of InputChannel: get event from InputChannel
InputChannel-->>-NativeInputEventReceiver: return inputEvent
NativeInputEventReceiver->>InputEventReceiver: dispatchInputEvent
InputEventReceiver->>InputEventReceiver: onInputEvent
InputEventReceiver->>ViewRootImpl: enqueueInputEvent
ViewRootImpl->>ViewRootImpl: doProcessInputEvents
ViewRootImpl->>ViewRootImpl: deliverInputEvent
ViewRootImpl->>+ViewPostImeInputStage: deliver
ViewPostImeInputStage->>ViewPostImeInputStage:onProcess
ViewPostImeInputStage->>ViewPostImeInputStage: processPointerEvent
ViewPostImeInputStage-->>+View: dispatchPointerEvent
View->>View:dispatchTouchEvent
View->>View: onTouch
View-->>-ViewPostImeInputStage: return is consume it or not
ViewPostImeInputStage-->>-ViewRootImpl: finish deliver

之前的分析涉及到了InputChannel的初始化和InputEventReceiver的初始化,直接看可以会比较绕人,上面从事件分发角度画了一下事件从InputChannel一直流转到View的一个时序图,希望对于你理解这个流程有所理解。如果哪里存在疏漏,也欢迎读者朋友们评论指点。

看完评论一下吧

  •  

Android源码分析:Window与Activity与View的关联

Activity是四大组件中和UI相关的那个,应用开发过程中,我们所有的界面基本都需要使用Activity才能去渲染和绘制UI,即使是ReactNative,Flutter这种跨平台的方案,在Android中,也需要一个Activity来承载。但是我们的Activity内我们设置的View又是怎么渲染到屏幕上的呢,这背后又有WindowManager和SurfaceFlinger来进行工作。本文就来看看WindowManger如何管理Window,以及Window如何与Activity产生关系的呢。

Activity与Window的初见

Activity的创建是在ActivityThreadperformLaunchActivity中,这里会创建要启动的Activity,并且会调用Activity的attach方法,在这个方法当中就会创建Window,其中和Window相关的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
mWindow = new PhoneWindow(this, window, activityConfigCallback);
mWindow.setWindowControllerCallback(mWindowControllerCallback);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
 mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
 mWindow.setUiOptions(info.uiOptions);
}
mWindow.setWindowManager(
 (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
 mToken, mComponent.flattenToString(),
 (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
if (mParent != null) {
 mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();

这里我们可以看到为Activity创建了Window,目前Android上面的Window实例为PhoneWindow,同时还给Window设置了WindowManager,不过这里的WindowManager仅仅是一个本地服务,它的实现为WindowManagerImpl,它的注册代码在SystemServiceRegister.java中,代码如下:

1
2
3
4
5
6
registerService(Context.WINDOW_SERVICE, WindowManager.class,
 new CachedServiceFetcher<WindowManager>() {
 @Override
 public WindowManager createService(ContextImpl ctx) {
 return new WindowManagerImpl(ctx);
 }});

而我们这个WindowManagerImpl内部持有持有了一个WindowManagerGlobal,看名字就知道它应该会涉及到跨进程通讯,去看它代码就知道它内部有两个成员,分别是sWindowManagerServicesWindowSession,这两个成员就用于跨进程通讯。这里我们先知道有这几个类,后面到用处再继续分析。

---
title: WindowManager相关类图
---
classDiagram
directioni TB
class ViewManager {
<<interface>>
addView(view, params)
updateViewLayout(view, params)
removeView(view)
}
class WindowManager {
<<interface>>
}
class WindowManagerImpl {
- WindowManagerGlobal mGlobal;
- IBinder mWindowContextToken;
- IBinder mDefaultToken;
}
ViewManager <|-- WindowManager
WindowManager <|.. WindowManagerImpl
class WindowManagerGlobal {
IwindowManager sWindowManagerService;
IWindowSession SwindowSession;
}
WindowManagerImpl ..> WindowManagerGlobal
class Window {
<<abstract>>
WindowManager mWindowManager;
}
Window <|.. PhoneWindow
Window ..> WindowManager
class IWindow {
<<Interface>>
}
IWindow <|--W
class ViewRootImpl {
W mWindow
IWindowSession mWindowSession
}
ViewRootImpl .. W
ViewRootImpl .. IWindowSession
IWindowSession .. W

这里只可出了App进程相关的一些类,System_Server相关未列出,后面涉及到相关部分的时候再进行分析。

Window与View的邂逅

我们一般情况下会在Activity的onCreate当中去调用setContentView,只有这样我们的View才能够显示出来。因此我们直接看这个方法的调用:

1
2
3
4
public void setContentView(@LayoutRes int layoutResID) {
 getWindow().setContentView(layoutResID);
 initWindowDecorActionBar();
}

其中就是调用了window的同名方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
public void setContentView(int layoutResID) {
 if (mContentParent == null) {
 installDecor();
 } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
 mContentParent.removeAllViews();
 }

 if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
 final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
 getContext());
 transitionTo(newScene); //执行页面Transition动画
 } else {
 mLayoutInflater.inflate(layoutResID, mContentParent);
 }
 mContentParent.requestApplyInsets();
 final Callback cb = getCallback();
 if (cb != null && !isDestroyed()) {
 cb.onContentChanged();
 }
 mContentParentExplicitlySet = true;
}

这里我们主要是将我们的ContentView添加到mContentParent当中去,这个mContentParent有可能为空,需要我们通过installDecor来创建,代码如下:

 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
private void installDecor() {
 mForceDecorInstall = false;
 if (mDecor == null) {
 mDecor = generateDecor(-1);
 mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
 mDecor.setIsRootNamespace(true);
 if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
 mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
 }
 } else {
 mDecor.setWindow(this);
 }
 if (mContentParent == null) {
 mContentParent = generateLayout(mDecor);

 // Set up decor part of UI to ignore fitsSystemWindows if appropriate. 
 mDecor.makeFrameworkOptionalFitsSystemWindows();

 final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
 R.id.decor_content_parent);

 if (decorContentParent != null) {
 mDecorContentParent = decorContentParent;
 mDecorContentParent.setWindowCallback(getCallback());
 if (mDecorContentParent.getTitle() == null) {
 mDecorContentParent.setWindowTitle(mTitle);
 }

 final int localFeatures = getLocalFeatures();
 for (int i = 0; i < FEATURE_MAX; i++) {
 if ((localFeatures & (1 << i)) != 0) {
 mDecorContentParent.initFeature(i);
 }
 }

 mDecorContentParent.setUiOptions(mUiOptions);

 ...

 PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
 if (!isDestroyed() && (st == null || st.menu == null) && !mIsStartingWindow) {
 invalidatePanelMenu(FEATURE_ACTION_BAR);
 }
 } else {
 mTitleView = findViewById(R.id.title);
 if (mTitleView != null) {
 //title view的设置
 }
 }

 if (mDecor.getBackground() == null && mBackgroundFallbackDrawable != null) {
 mDecor.setBackgroundFallback(mBackgroundFallbackDrawable);
 }

 if (hasFeature(FEATURE_ACTIVITY_TRANSITIONS)) {
 ...
 //页面动画的读取和设置 
 }
 }
}

这里我们可以看到主要做的就是创建了decorView和ContentParent,还有一些动画,标题之类的初始化我们这里就跳过了。DecorView就是App Activity页面最底层的容器,它为我们封装了状态栏,底部导航栏,App页面的内容的展示。而ContentParent的初始化,则是根据Activity的设置,根据是否展示状态栏,是否展示标题栏等,进行加载相应的布局,加载到DecorView当中,最后com.android.internal.R.id.content对应的FrameLayout就会成为ContentParent。 当这一切做完,我们的页面View就成功的添加到Window当中了,但是它是如何展示出来的呢,还需要继续往后看。我们需要前往ActivityThread的handleResumeActivity方法:

 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
//调用Activity的onResume方法
if (!performResumeActivity(r, finalStateRequest, reason)) {
 return;
}
//r为ActivityClientRecord
final Activity a = r.activity;
//检查当前的Activity是否能显示
boolean willBeVisible = !a.mStartedActivity;
if (!willBeVisible) {
 willBeVisible = ActivityClient.getInstance().willActivityBeVisible(
 a.getActivityToken());
}
if (r.window == null && !a.mFinished && willBeVisible) {
 r.window = r.activity.getWindow(); //把activity的window保存到r.window中
 View decor = r.window.getDecorView();
 decor.setVisibility(View.INVISIBLE);
 ViewManager wm = a.getWindowManager();
 WindowManager.LayoutParams l = r.window.getAttributes();
 a.mDecor = decor;
 l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
 l.softInputMode |= forwardBit;
 if (r.mPreserveWindow) {
 a.mWindowAdded = true;
 r.mPreserveWindow = false;
 ViewRootImpl impl = decor.getViewRootImpl();
 if (impl != null) {
 impl.notifyChildRebuilt();
 }
 }
 if (a.mVisibleFromClient) {
 if (!a.mWindowAdded) {
 a.mWindowAdded = true;
 wm.addView(decor, l); //调用windowManager添加decorView
 } else {
 a.onWindowAttributesChanged(l);
 }
 }
} else if (!willBeVisible) {
 r.hideForNow = true;
}

可以看到上面的代码把window保存到了ActivityClientRecord当中,同时调用了WindowManager的addView方法,去添加view。我们继续往后看代码:

 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
if (!r.activity.mFinished && willBeVisible && r.activity.mDecor != null && !r.hideForNow) {
 ViewRootImpl impl = r.window.getDecorView().getViewRootImpl();
 WindowManager.LayoutParams l = impl != null
 ? impl.mWindowAttributes : r.window.getAttributes();
 if ((l.softInputMode
 & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION)
 != forwardBit) {
 l.softInputMode = (l.softInputMode
 & (~WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION))
 | forwardBit;
 if (r.activity.mVisibleFromClient) {
 ViewManager wm = a.getWindowManager();
 View decor = r.window.getDecorView();
 wm.updateViewLayout(decor, l);
 }
 }

 r.activity.mVisibleFromServer = true;
 mNumVisibleActivities++;
 if (r.activity.mVisibleFromClient) {
 r.activity.makeVisible();
 }

 if (shouldSendCompatFakeFocus) {
 if (impl != null) {
 impl.dispatchCompatFakeFocus();
 } else {
 r.window.getDecorView().fakeFocusAfterAttachingToWindow();
 }
 }
}

上面的代码中,我们看到主要做了两件事情,一个是调用updateViewLayout去更新视图的属性,但是updateViewLayout也要属性发生变化,并且有输入法的时候才会执行,另外就是调用activity的makeVisible方法去展示View。

这个过程我们需要分析如下两步。

  1. 调用addView添加decorView
  2. 调用activity.makeVisible来显示 我们分别看一下这两个方法的实现

WMS与ViewRootImpl的遇见:调用WindowManger的addView

1
2
3
4
5
public void addView(@NonNull View view, @NonNull ViewGroup.LayoutParams params) {
 applyTokens(params);
 mGlobal.addView(view, params, mContext.getDisplayNoVerify(), mParentWindow,
 mContext.getUserId());
}

这里就是调用mGlobaladdView方法:

 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
public void addView(View view, ViewGroup.LayoutParams params,
 Display display, Window parentWindow, int userId) {

 final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams) params;
 ....

 ViewRootImpl root;
 View panelParentView = null;

 synchronized (mLock) {

 int index = findViewLocked(view, false);
 ...

 if (windowlessSession == null) {
 root = new ViewRootImpl(view.getContext(), display);
 } else {
 root = new ViewRootImpl(view.getContext(), display,
 windowlessSession, new WindowlessWindowLayout());
 }

 view.setLayoutParams(wparams);

 mViews.add(view);
 mRoots.add(root);
 mParams.add(wparams);

 try {
 root.setView(view, wparams, panelParentView, userId);
 } catch (RuntimeException e) {
 final int viewIndex = (index >= 0) ? index : (mViews.size() - 1);
 // BadTokenException or InvalidDisplayException, clean up. 
 if (viewIndex >= 0) {
 removeViewLocked(viewIndex, true);
 }
 throw e;
 }
 }
}

在正常的App页面,windowlessSession会一直为空,这里就会创建一个ViewRootImpl,并且把我们的DecorView以及WindowParams都传进去。并且viewrootwparams都会按照顺序存到List当中。这里我们需要去看ViewRootImpl的setView方法,其中和添加到屏幕相关的代码如下:

1
2
3
4
5
6
7
requestLayout(); //测量布局
res = mWindowSession.addToDisplayAsUser(mWindow, mWindowAttributes,
 getHostVisibility(), mDisplay.getDisplayId(), userId,
 mInsetsController.getRequestedVisibilities(), inputChannel, mTempInsets,
 mTempControls, attachedFrame, compatScale);
...
view.assignParent(this); //将ViewRootImpl设置为DecorView的parent

这里的mDisplay为外面从Context中所获取的,用于指定当前的UI要显示到哪一个显示器上去。这里的mWindowSession的获取代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//WindowManagerGlobal.java
@UnsupportedAppUsage
public static IWindowSession getWindowSession() {
 synchronized (WindowManagerGlobal.class) {
 if (sWindowSession == null) {
 try {
 @UnsupportedAppUsage
 InputMethodManager.ensureDefaultInstanceForDefaultDisplayIfNecessary();
 IWindowManager windowManager = getWindowManagerService();
 sWindowSession = windowManager.openSession(
 new IWindowSessionCallback.Stub() {
 @Override
 public void onAnimatorScaleChanged(float scale) {
 ValueAnimator.setDurationScale(scale);
 }
 });
 } catch (RemoteException e) {
 throw e.rethrowFromSystemServer();
 }
 }
 return sWindowSession;
 }
}

可以看到就是通过IWindowManger这个Binder服务调用了openSession来获取了一个WindowSession。其代码如下:

1
2
3
4
//WindowManagerService.java
public IWindowSession openSession(IWindowSessionCallback callback) {
 return new Session(this, callback);
}

在System_Server端,创建了一个Session对象来提供相关的服务。它的addToDisplayAsUser方法又调用了WMSaddWindow方法,这个方法比较长我们只看其中和UI展示相关的部分,并且UI类型不是App的普通UI的也都给省略掉。

1
2
3
int res = mPolicy.checkAddPermission(attrs.type, isRoundedCornerOverlay, attrs.packageName,
 appOp);
final DisplayContent displayContent = getDisplayContentOrCreate(displayId, attrs.token);

第一行代码首先是去检查我们当前要展示的view,它的类型是否支持去展示。第3行代码的内容如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
private DisplayContent getDisplayContentOrCreate(int displayId, IBinder token) {
 if (token != null) {
 final WindowToken wToken = mRoot.getWindowToken(token);
 if (wToken != null) {
 return wToken.getDisplayContent();
 }
 }

 return mRoot.getDisplayContentOrCreate(displayId);
}

mRoot为一个RootWindowContainer对象,之前我们在分析Activity的启动过程中已经见到了它,我们的ActivityRecord和Task都存在它当中。这里wToken初始情况一般为null因此会执行下面的getDisplayContentOrCreate方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
DisplayContent getDisplayContentOrCreate(int displayId) {
 DisplayContent displayContent = getDisplayContent(displayId);
 if (displayContent != null) {
 return displayContent;
 }
 ...
 final Display display = mDisplayManager.getDisplay(displayId);
 ...
 displayContent = new DisplayContent(display, this);
 addChild(displayContent, POSITION_BOTTOM);
 return displayContent;
}

这里就是根据displayId从列表中去拿DisplayContent如果不存在就去创建一个并且保存到列表中,方便下次使用。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//WindowManagerService.addWindow
WindowToken token = displayContent.getWindowToken(
 hasParent ? parentWindow.mAttrs.token : attrs.token);
if (token == null) {
{
 ...
 final IBinder binder = attrs.token != null ? attrs.token : client.asBinder();
 token = new WindowToken.Builder(this, binder, type)
 .setDisplayContent(displayContent)
 .setOwnerCanManageAppTokens(session.mCanAddInternalSystemWindow)
 .setRoundedCornerOverlay(isRoundedCornerOverlay)
 .build();
}

继续看addWindow的内容,如果displayContent是新创建的,那么这里拿到的token就会为空,因此这里调用了client.asBinder来获取IBinder,或者直接拿’attr’中的token,这个client为IWindow类型,它在应用侧为W的实例,它是ViewRootImpl的一个内部类。这里创建完WindowToken之后,我们可以继续往后看。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//WindowManagerService.addWindow
final WindowState win = new WindowState(this, session, client, token, parentWindow,
 appOp[0], attrs, viewVisibility, session.mUid, userId,
 session.mCanAddInternalSystemWindow);
final DisplayPolicy displayPolicy = displayContent.getDisplayPolicy();
displayPolicy.adjustWindowParamsLw(win, win.mAttrs);
attrs.flags = sanitizeFlagSlippery(attrs.flags, win.getName(), callingUid, callingPid);
attrs.inputFeatures = sanitizeSpyWindow(attrs.inputFeatures, win.getName(), callingUid,
 callingPid);
win.setRequestedVisibilities(requestedVisibilities);

res = displayPolicy.validateAddingWindowLw(attrs, callingPid, callingUid);

这里创建的WindowState用于保存Window的状态,可以说是Window在WMS中存储的一个章台。随后从DisplayContent中拿到了DisplayPolicy这个类主要是用于控制显示的一些行为,比如状态栏,导航栏的显示状态之类的。这里会根据WindowParams来调整DisplayPolicy的参数,以及调用validateAddingWindowLw检查当前的window是否能够添加的系统界面中,这个app普通type不涉及。

1
2
3
4
//WindowManagerService.addWindow
win.attach();
mWindowMap.put(client.asBinder(), win);
win.initAppOpsState();

win.attach方法如下:

1
2
3
void attach() {
 mSession.windowAddedLocked();
}

其中就调用了SessionwindowAddedLocked方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
void windowAddedLocked() {
 if (mPackageName == null) {
 final WindowProcessController wpc = mService.mAtmService.mProcessMap.getProcess(mPid);
 if (wpc != null) {
 mPackageName = wpc.mInfo.packageName;
 mRelayoutTag = "relayoutWindow: " + mPackageName;
 } else {
 }
 }
 if (mSurfaceSession == null) {
 mSurfaceSession = new SurfaceSession();
 mService.mSessions.add(this);
 if (mLastReportedAnimatorScale != mService.getCurrentAnimatorScale()) {
 mService.dispatchNewAnimatorScaleLocked(this);
 }
 }
 mNumWindow++;
}

对于每个进程第一次使用openSession创建的Session这个地方都会执行,主要就是创建了SurfaceSession,并且保存到WMSmSessions当中去。之后又把client作为key,WindowState为value存放到mWindowMap当中。

1
2
3
//WindowManagerService.addWindow
win.mToken.addWindow(win);
displayPolicy.addWindowLw(win, attrs);

先看这个WindowToken.addWindow方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
void addWindow(final WindowState win) {
 if (mSurfaceControl == null) {
 createSurfaceControl(true /* force */);

 reassignLayer(getSyncTransaction());
 }
 if (!mChildren.contains(win)) {
 addChild(win, mWindowComparator);
 mWmService.mWindowsChanged = true;
 }
}

这里创建了一个SurfaceControl,并且保存到了WindowList当中去。随后再看displayyPolicy.addWindowLw,其中主要用于处理inset相关的处理,这里也先跳过。到此位置addView的代码基本就看完了。

调用activity.makeVisible来显示

1
2
3
4
5
6
7
8
void makeVisible() {
 if (!mWindowAdded) {
 ViewManager wm = getWindowManager();
 wm.addView(mDecor, getWindow().getAttributes());
 mWindowAdded = true;
 }
 mDecor.setVisibility(View.VISIBLE);
}

我们之前已经分析过addView了,这里mWindowAdded也是为true,这里的addView因此是不会被执行的。我们看一下下面的setVisibility,这个就是我们的普通View的方法,还是直接看源码:

1
2
3
4
//View.java
public void setVisibility(@Visibility int visibility) {
 setFlags(visibility, VISIBILITY_MASK);
}

这里是直接调用了setFlags方法,其中和设置显示相关的部分如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
final int newVisibility = flags & VISIBILITY_MASK;
if (newVisibility == VISIBLE) {
 if ((changed & VISIBILITY_MASK) != 0) {
 mPrivateFlags |= PFLAG_DRAWN;
 invalidate(true);

 needGlobalAttributesUpdate(true);
 shouldNotifyFocusableAvailable = hasSize();
 }
}

if ((changed & VISIBILITY_MASK) != 0) {
 if (mParent instanceof ViewGroup) {
 ViewGroup parent = (ViewGroup) mParent;
 parent.onChildVisibilityChanged(this, (changed & VISIBILITY_MASK),
 newVisibility);
 parent.invalidate(true);
 } else if (mParent != null) {
 mParent.invalidateChild(this, null);
 }
}

DecorView的parent为ViewRootImpl,因此上面会调用ViewRootImplinvalidateChild方法,内部会调用如下代码:

 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
public ViewParent invalidateChildInParent(int[] location, Rect dirty) {
 checkThread();

 if (dirty == null) {
 invalidate();
 return null;
 } else if (dirty.isEmpty() && !mIsAnimating) {
 return null;
 }

 if (mCurScrollY != 0 || mTranslator != null) {
 mTempRect.set(dirty);
 dirty = mTempRect;
 if (mCurScrollY != 0) {
 dirty.offset(0, -mCurScrollY);
 }
 if (mTranslator != null) {
 mTranslator.translateRectInAppWindowToScreen(dirty);
 }
 if (mAttachInfo.mScalingRequired) {
 dirty.inset(-1, -1);
 }
 }

 invalidateRectOnScreen(dirty);

 return null;
}

这段代码会检查需要从新绘制的区域,并且放在dirty当中,最后调用invalidateRectOnScreen方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private void invalidateRectOnScreen(Rect dirty) {
 final Rect localDirty = mDirty;

 // Add the new dirty rect to the current one 
 localDirty.union(dirty.left, dirty.top, dirty.right, dirty.bottom);
 final float appScale = mAttachInfo.mApplicationScale;
 final boolean intersected = localDirty.intersect(0, 0,
 (int) (mWidth * appScale + 0.5f), (int) (mHeight * appScale + 0.5f));
 if (!intersected) {
 localDirty.setEmpty();
 }
 if (!mWillDrawSoon && (intersected || mIsAnimating)) {
 scheduleTraversals();
 }
}

这里仍然检查dirty区域,并且去做Traversal。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
void scheduleTraversals() {
 if (!mTraversalScheduled) {
 mTraversalScheduled = true;
 mTraversalBarrier = mHandler.getLooper().getQueue().postSyncBarrier();
 mChoreographer.postCallback(
 Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
 notifyRendererOfFramePending();
 pokeDrawLockIfNeeded();
 }
}

这里就是启动线程去不断的页面的刷新重绘,就不分析了。最终会执行到performTraversals方法,其中有如下代码我们比较关注:

1
2
3
4
if (mFirst || windowShouldResize || viewVisibilityChanged || params != null
 || mForceNextWindowRelayout) {
 relayoutResult = relayoutWindow(params, viewVisibility, insetsPending);
}

当首次执行这个方法的时候mFirst为true,除了这个条件之外,window需要从新计算size,view的可见性变化,windowParams变化等任一条件满足就会执行这里。我们在继续看里面的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
if (relayoutAsync) {
 mWindowSession.relayoutAsync(mWindow, params,
 requestedWidth, requestedHeight, viewVisibility,
 insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mRelayoutSeq,
 mLastSyncSeqId);
} else {
 relayoutResult = mWindowSession.relayout(mWindow, params,
 requestedWidth, requestedHeight, viewVisibility,
 insetsPending ? WindowManagerGlobal.RELAYOUT_INSETS_PENDING : 0, mRelayoutSeq,
 mLastSyncSeqId, mTmpFrames, mPendingMergedConfiguration, mSurfaceControl,
 mTempInsets, mTempControls, mRelayoutBundle);
 ...
}

当view为本地进行Layout且一些其他的条件符合,并且它的位置大小没有变化的时候,才会是relayoutAsync,不过两个最终的在服务端都会调用relayout方法,区别就是这里relayout的时候传过去了一个mSurfaceControl,这个接口是AIDL定义的,这个参数定义的为out,服务端会传输值到这个对象里,我们随后会看到,因为非异步是大多数情况的调用,这里也对他进行分析。在Session的relayout方法中调用了如下代码:

1
2
3
4
int res = mService.relayoutWindow(this, window, attrs,
 requestedWidth, requestedHeight, viewFlags, flags, seq,
 lastSyncSeqId, outFrames, mergedConfiguration, outSurfaceControl, outInsetsState,
 outActiveControls, outSyncSeqIdBundle);

这里就是调用了WMSrelayoutWindow方法,其中我们关注的有一下代码:

1
2
3
4
5
6
7
8
9
final WindowState win = windowForClientLocked(session, client, false);
if (shouldRelayout && outSurfaceControl != null) {
 try {
 result = createSurfaceControl(outSurfaceControl, result, win, winAnimator);
 } catch (Exception e) {
 ...
 return 0;
 }
}

为应用提供画布容器

这里看一下这个createSurfaceControl的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
WindowSurfaceController surfaceController;
try {
 surfaceController = winAnimator.createSurfaceLocked();
} finally {

}
if (surfaceController != null) {
 surfaceController.getSurfaceControl(outSurfaceControl);

} else {
 outSurfaceControl.release();
}

第三行主要是创建一个WindowSurfaceController对象,第8行则是使用这个对象去获取SurfaceControl,我们看一下它的代码:

1
2
3
void getSurfaceControl(SurfaceControl outSurfaceControl) {
 outSurfaceControl.copyFrom(mSurfaceControl, "WindowSurfaceController.getSurfaceControl");
}

SurfaceControlcopyFrom方法代码如下:

1
2
3
4
5
6
7
public void copyFrom(@NonNull SurfaceControl other, String callsite) {
 mName = other.mName;
 mWidth = other.mWidth;
 mHeight = other.mHeight;
 mLocalOwnerView = other.mLocalOwnerView;
 assignNativeObject(nativeCopyFromSurfaceControl(other.mNativeObject), callsite);
}

最主要的是最后的assignNativeObject赋值到我们从app进程传过来的SurfaceControl当中。native层的SurfaceControl有如下几个成员变量:

1
2
3
4
5
6
sp<SurfaceComposerClient> mClient;
sp<IBinder> mHandle;
sp<IGraphicBufferProducer> mGraphicBufferProducer;
mutable Mutex mLock;
mutable sp<Surface> mSurfaceData;
mutable sp<BLASTBufferQueue> mBbq;

其中就有Surface,而我们在服务端拿到的这个SurfaceControl随后会写回客户端,这样App进程就可以把UI元素绘制到这个Surface上面了。

前面有列过客户端WindowManager相关的类,这里在列一下system_server进程中相关的类:

classDiagram
class IWindowManager {
<<interface>>
}
class Stub["IWindowManager.Stub"]
IWindowManager <|..Stub
class WindowManagerService {
WindowManagerPolicy mPolicy
ArraySet~Session~ mSessions
HashMap~IBinder, WindowState~ mWindowMap
RootWindowContainer mRoot
}
Stub <|--WindowManagerService
class IWindowSession {
<<Interface>>
}
class SessionStub["IWindowSession.Stub"] {
<<abstract>>
}
class Session {
WindowManagerService mService
}
IWindowSession <|..SessionStub
SessionStub<|--Session
WindowContainer<|--RootWindowContainer
WindowContainer <|-- WindowToken
WindowContainer <|--WindowState
WindowToken .. WindowManagerService
Session .. WindowManagerService
RootWindowContainer .. WindowManagerService

总结

我们在调用WMS的addWindow的时候,并没有把View直接传过来,所传过来的WindowLayoutParams当中,宽和高是比较重要的信息,因为在对调用这个方法之前,代码中先是执行了requestLayout去测量的布局的尺寸,并且在返回参数中通过Rect返回了画布的尺寸。我们也知道通过SurfaceControl为我们提供了Surface,这样客户端就能够把UI数据写上去了。而这样,这个Window与View就能够与系统的其他服务一起,把我们的UI显示到屏幕上了。

在与WMS初始通信的时候,WMS服务端为App创建了Session这个对象,App通过这个对象来与服务端进行Binder通讯。同时,App进程在创建ViewRootImpl的时候创建了W这个对象,它是IWindow的binder对象,服务端可以通过这个对象来与app进程通讯。为了方便理解,关于服务端和客户端,我又画了如下图,希望对你理解它们有所帮助。

以上就应用的窗口与Activity相关的分析,整体流程还是比较复杂的,如果哪里存在疏漏,也欢迎读者朋友们评论指点。另外关于应用的事件分发也会涉及到WMS和ViewRootImpl,为了使得文章不至于太长,就留到下次再进行分析。

看完评论一下吧

  •  

回皖北农村观察随记

老家在皖北农村,一年回不了几次,上次过年回去的,这次中秋想着回家看看老爹,就回去看了看。正好也记录一下回去的所看所感。

中秋节的节味是越来越淡了,因为大部分的人都在外打工,现在这个节点跟过年比农村依然很冷清。但是跟平时比还人还要多一点点,因为有些住在县城的人,中秋这天还跑回来过节了。

现在大部分人家里的地都承包出去了,还能干活的人都出去打工了。留下来的只剩老弱病,以及带着小孩上学的部分发家庭。

农村的教育

我们村里的小学已经关闭好多年了,附近几个村的基本也都关的差不多了。我以前上的镇子上的初中,以前人多的时候一个年级差不多上千人,现在在那边上学的小妹说,一个年级也就200多人。我想原因是有两方面,一是不少像我这样的,在外地生活的,家里小孩也就不在当地就学了。另一方面是,县城的教育扩张,县城内新建了不少学校,下面农村的居民只要购房就可以在县城上学,不少亲戚家的小孩就为了上学在县城买房了,如此带动了县城的房市,也使得农村的教育更加的衰败。

环境

村子里面的水沟,原先都是互相联通,最后可以连接到更大的河流。而最近十几年的农村建房填沟,导致了水面的面积越来越小,也不能互相联通。叠加上之前的各种乱扔垃圾,现在大部分的水面都飘满了浮萍,浮萍之下也都是黑水。

不少养殖户的污水,以及各种排泄物直接流入水沟,缺少监督和管理。这也是导致水体污染的一打原因,而前几年政府所规划建设的污水处理在附近几个村子还没有能够见到。

当然也有在变好的方面,目前各个村子都放置了垃圾桶,定期有垃圾车过来统一收走垃圾。政府补贴给每个村子找了个村民来打扫各村子的垃圾,每个月能拿到一点钱,这样一来村子的各条路边也都鲜有垃圾踪影。

经济

排除出去打工的人,留在村里的人想要挣点钱还是挺难的。大部分的土地,目前是一小部分人承包的,这样其他人没有土地的束缚都能安心在外工作。而留下大规模承包土地,一方面可以大规模种植提高收益,另外还有部分政府补贴。

在田地里走了一圈,而且在附近乡镇也看过,大部分的农田都种植了玉米。这几年大部分的土地都是这样,秋季种植小麦,收完小麦种植玉米,这两种都很方便机械收割。而今年,由于夏季下雨较少,玉米的收成不是很好,玉米长得不够饱满,而已经获悉的玉米收购价格也不是很好。部分村民已经准备把玉米卖去做青储,收购商会把玉米连秸秆一起收走,免去农民的许多麻烦。

除了种植,部分家里有空间的村民还会选择在家养殖,目前我们村养羊的最多。目前羊都是圈养的,会选择到附近割草喂养,最近县城有草坪种植者会定期割草,于是大家便前往拉草喂养。如此一来,即帮忙处理了割下来的草,又不用找地方割草了。但是大家的羊养的却又不太好,以我父亲养的为例,生下来的小羊羔死了不少,再加上大羊也因为生病死了一两只,导致养了一年多,花钱买了不少羊,但是却没几只羊能卖出去。而想要考养殖挣到点钱,还是挺难的,饲料,买羊,给羊治病的各种花费都不少。

而剩下还有几个为了小孩的教育留在农村的青壮年呢,一般都会想点办法,到县城或者在乡镇上做点生意。我的表哥一家选择了在乡镇上面卖水果,表面上看是在家陪伴小孩,但是每天在摊位上忙碌,又有多少时间能够陪伴小孩呢。

基础设施

由于农村的居住点分散,农村的基础设置建设天然比城市要差很多。不过这几年,我们这里的基础设施倒是有了一些提升。电力和通信这两块已经很多年很稳了,光纤也都通到了各个村子。

前两年,我们村也通了自来水。但是时至今日,很多人还是宁愿自己抽地下水而不愿意使用自来水,许多人还是对自来水不够信任。自来水的水质和稳定性还不够让人信任。但是目前抽取的地下水也越来越差,不过滤也难以直接使用,不少人选择去乡镇购买按量计费的直饮水。还是希望当地水厂能够提高水质和服务,以免大家不用,进而导致水厂无法提供稳定服务。

最近呢,村里也通上了燃气管道,这样看起来大家就可以使用天然气了。但是不同于城市的商品房都预装了天然气入户管道,农村需要各家接入时候铺设入户管道。据介绍安装天然气大约需要两千三百元,大部分人都被这个价格所劝退。对此我觉得,提高初期的燃气价格反倒是比一次性收这么多安装费更容易让人接受。

我家门口的道路是县道,是十来年前修的柏油路,目前是有城乡公交经过。然而之前经常有超载货车经过,再加上养护不好,目前路面已经坑坑洼洼,很多路段破损不堪。这只能归因于贫穷,经济落后,县里没有足够的资金维护道路。

新农村

隔壁的村里,靠近县道的部分居然搞起了新农村。具体搞了什么呢,就是把靠近路边的房子全都刷白了,路边的东西都清理掉了,还种了树,建了围栏。

另外还建了小公园,这公园是真的小,村民说,这个就是浪费钱,在这公园里转还不如去农田里面转一圈呢。

也有好的地方,就是新建了卫生所,大家看个小病会更加方便。

新农村,我想,出发点还是好的,但是实践的太过粗糙,太过注重表面工程。一个是建设的下水道之类的设施太过劣质,窨井盖还没用多久已经烂掉了。另外就是并没有考虑村民的使用需求,没有 把钱花到刀刃上。

总结

皖北的农村,可能跟很多的农村也类似,村里的人越来越少。基础设置也在慢慢的建设,但是人居环境仍然有很大提高的空间。而相比之下,之前去过的浙江农村环境则是要好很多,我们这里还有很大的提升空间,作为在外的游子,也希望家乡能够越来越好。

看完评论一下吧

  •  

Android源码分析:Activity启动流程Task相关分析

Activity的启动分析,很大一块需要了解的是Activity的Task管理,以及启动过程中Task的决策,在之前分析启动流程中,关于Task处理的部分,我这里是简化掉了很多的,今天再来分析一下。

入口与计算启动参数

在之前分析Activity的启动中,已经看到了关于处理Task的代码是在ActivityStart当中的startActivityInner方法当中,这个方法有不少入参,先捋一遍: resultTo为调用的Activity的mToken(IBinder)

ActivityRecord r, //新创建的Record,包含calling信息和要打开的ActivityInfo等
ActivityRecord sourceRecord, //resultTo不为空的时候才会去使用`ActivityRecord isInAnyTask`读取
IvoiceInteractionSession voiceSession, //startVoiceActivity的时候才会传
IvoiceInteractor voiceInteractor, //同上,一般为系统的语音助手界面
int startFlags, //客户端传过来的startFlags一般为0
boolean doResume, //是否需要去resume activity,对于启动Activity场景总是为true
ActivityOptions options, //Activity启动的一些参数,页面跳转动画等
Task inTask, //一般为通过AppTaskImpl启动Activity才会设置值,正常app启动不存在
TaskFragment inTaskFragment, //同上,一般情况为空
int balCode, //Activity后台启动的许可Code,默认为BAL_ALLOW_DEFAULT
NeededGrants intentGrants //Intent访问权限授权

有了所有的入参可以看看computeLaunchingTaskFlags,对于普通应用mInTask为空,mSourceRecord不为空,关注这个方法内的如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
if (mInTask == null) {
 if (mSourceRecord == null) {
 if ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 && mInTask == null) {
 //如果获取不到启动来源的ActivityRecord,且当前要启动的Activity还没有设置NEW_TASK flag,则给他添加
 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
 }
 } else if (mSourceRecord.launchMode == LAUNCH_SINGLE_INSTANCE) {
 //如果来源ActivityRecord是SINGLE INSTANCE,也就是说它是自己独立的任务栈,新启动Activity必须设置NEW_TASK 
 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
 } else if (isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)) {
 //如果新启动的Activity是SingleInstance或者SingleTask,也要添加NEW_TASK flag
 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
 }
}

if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0
 && ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 || mSourceRecord == null)) {
 //如果要启动的Activity设置了分屏的FLAG,但是却没有设置NEW——FLAG或者没有源ActivityRecord,这个时候就需要忽略掉分屏的这个FLAG
 mLaunchFlags &= ~FLAG_ACTIVITY_LAUNCH_ADJACENT;
}

简化版本的流程图如下:

获取当前的顶部Task: getFocusedRootTask

以上是针对LaunchFlag的一部分处理,但并不是全部,暂时继续往后看。随后就是获取task

1
2
3
final Task prevTopRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();
final Task prevTopTask = prevTopRootTask != null ? prevTopRootTask.getTopLeafTask() : null;
final Task reusedTask = getReusableTask();

首先看这个mPreferredTaskDisplayArea,这个表示倾向的Activity显示的TaskDisplay,它的赋值是在前面的setInitialState方法中:

1
2
3
4
5
mSupervisor.getLaunchParamsController().calculate(inTask, r.info.windowLayout, r,
 sourceRecord, options, mRequest, PHASE_DISPLAY, mLaunchParams);
mPreferredTaskDisplayArea = mLaunchParams.hasPreferredTaskDisplayArea()
 ? mLaunchParams.mPreferredTaskDisplayArea
 : mRootWindowContainer.getDefaultTaskDisplayArea();

我们这里就以它是拿的DefaultTaskDisplayArea为例来分析,继续就是看它的getFocusedRootTask,看代码之前先看看这些类的关系图,之前画过Task,WindowContainer相关的,但是还不够全,这里再补充完整一点。

classDiagram
class ConfigurationContainer {
<<abstract>>
}
class WindowContainer {
List<WindowContainer> mChildren
}
class TaskFragment
class Task
class ActivityRecord {
Task task
TaskDisplayArea mHandoverTaskDisplayArea
}
ConfigurationContainer <|--WindowContainer
WindowContainer <|-- TaskFragment
TaskFragment <|--Task
WindowContainer <|--RootWindowContainer
WindowContainer <|-- DisplayArea
DisplayArea <|-- TaskDisplayArea
WindowContainer"0..*" <-- "1*"WindowContainer
WindowContainer <|-- WindowToken
WindowToken <|--ActivityRecord
ActivityRecord --> Task
ActivityRecord --> TaskDisplayArea

当然WindowContainer的子类远不止这些,包括WindowState等等都是它的子类,但是暂时不需要讨论他们,这里暂时先不列出来了。 我们还是先看getFocusedRootTask方法的源码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Task getFocusedRootTask() {
 if (mPreferredTopFocusableRootTask != null) {
 return mPreferredTopFocusableRootTask;
 }

 for (int i = mChildren.size() - 1; i >= 0; --i) {
 final WindowContainer child = mChildren.get(i);
 if (child.asTaskDisplayArea() != null) {
 final Task rootTask = child.asTaskDisplayArea().getFocusedRootTask();
 if (rootTask != null) {
 return rootTask;
 }
 continue;
 }

 final Task rootTask = mChildren.get(i).asTask();
 if (rootTask.isFocusableAndVisible()) {
 return rootTask;
 }
 }

 return null;
}

如果说当前的TaskDisplayArea中,preferredTopFocusableRoot存在就会直接使用,这个会在postionChildTaskAt的时候,如果child放置到顶部,并且它是可获得焦点的,会把他赋值给这个preferredTopFocusableRoot。 我们这里先看它为空的情况。如果它为空,这回到树状结构中查找,遍历树节点如果也是TaskDisplayArea,则会 看他们的focusedRootTask是否存在,如果就返回。如果节点是Task,就会检查这个Task是否为可获得焦点并且可见的,则返回它。否则就返回空。因为我们当前已经打了Activity,这里一般是可以获得值的。

如果拿到了prevTopRootTask,就会去调用getTopLeafTask去获取叶子节点的Task,代码如下:

1
2
3
4
5
6
7
8
public Task getTopLeafTask() {
 for (int i = mChildren.size() - 1; i >= 0; --i) { //从大数开始遍历
 final Task child = mChildren.get(i).asTask();
 if (child == null) continue; //如果不是Task就跳过
 return child.getTopLeafTask(); //继续看它的子节点
 }
 return this; //没有孩子节点,那就是一个叶子节点 
}

以上是获取叶子节点的代码,典型的树的遍历代码。到目前是拿的当前在展示的页面的任务栈。

获取可复用的Task:getReusableTask

而之后的getReusableTask则是获取可以使用的任务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
private Task getReusableTask() {
 //一般是从最近任务打开的页面才会执行这里,我们可以跳过
 if (mOptions != null && mOptions.getLaunchTaskId() != INVALID_TASK_ID) {
 Task launchTask = mRootWindowContainer.anyTaskForId(mOptions.getLaunchTaskId());
 if (launchTask != null) {
 return launchTask;
 }
 return null;
 }
 //如果启动的FLAG是 Single Instance或者SingleTask;又或者是虽然设置了NEW_TASK但是没有设置MULTIPLE_TASK。这些情况都会把新的Activity放到已有的任务栈。
 boolean putIntoExistingTask = ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0 &&
 (mLaunchFlags & FLAG_ACTIVITY_MULTIPLE_TASK) == 0)
 || isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK);
 //因为mInTask为空,后面的resultTo不为空,因此putIntoExistingTask结果为false。当通过startActivityForResult的且requestCode > 0 时候就不为空
 putIntoExistingTask &= mInTask == null && mStartActivity.resultTo == null;
 ActivityRecord intentActivity = null;
 if (putIntoExistingTask) {
 if (LAUNCH_SINGLE_INSTANCE == mLaunchMode) {
 //这种情况只有一个实例,就通过intent和activityInfo去找到它。
 intentActivity = mRootWindowContainer.findActivity(mIntent, mStartActivity.info,
 mStartActivity.isActivityTypeHome());
 } else if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0) {
 //对于分屏的,如果历史栈中有才使用
 intentActivity = mRootWindowContainer.findActivity(mIntent, mStartActivity.info,
 !(LAUNCH_SINGLE_TASK == mLaunchMode));
 } else {
 // 查找最合适的Task给Activity用 
 intentActivity =
 mRootWindowContainer.findTask(mStartActivity, mPreferredTaskDisplayArea);
 }
 }

 if (intentActivity != null && mLaunchMode == LAUNCH_SINGLE_INSTANCE_PER_TASK
 && !intentActivity.getTask().getRootActivity().mActivityComponent.equals(
 mStartActivity.mActivityComponent)) {
 //对于singleInstancePreTask,如果Task的根Activity不是要启动的Activity那么还是不能够复用,因此需要把intentActivity设置为空。
 intentActivity = null;
 }

 if (intentActivity != null
 && (mStartActivity.isActivityTypeHome() || intentActivity.isActivityTypeHome())
 && intentActivity.getDisplayArea() != mPreferredTaskDisplayArea) {
 //
 intentActivity = null;
 }

 return intentActivity != null ? intentActivity.getTask() : null;
}

以上就是根据条件判断是否可以复用栈,如果可以会去拿已经存在的Activity,如果Activity存在,则回去拿它的Task。其中这里有一个singleInstancePreTask的启动模式,这个对于我们很多Android开发这是不熟悉的,它是Android12引入的,它可以说是加强版本的singleInstance,当它是Task栈的根Task的时候就复用,如果不是的就类似singleTask会去打开一个新的Task栈。

这里先来看一下这个findActivity,他也是到RootWindowContainer中去查找,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ActivityRecord findActivity(Intent intent, ActivityInfo info, boolean compareIntentFilters) {
 ComponentName cls = intent.getComponent();
 if (info.targetActivity != null) {
 cls = new ComponentName(info.packageName, info.targetActivity);
 }
 final int userId = UserHandle.getUserId(info.applicationInfo.uid);

 final PooledPredicate p = PooledLambda.obtainPredicate(
 RootWindowContainer::matchesActivity, PooledLambda.__(ActivityRecord.class),
 userId, compareIntentFilters, intent, cls);
 final ActivityRecord r = getActivity(p);
 p.recycle();
 return r;
}

其中第8行就是创建了一个PooledPredicate,在我们调用test方法的时候就 会调用RootWindowContainer::matchesActivity这个方法,这个方法的代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
private static boolean matchesActivity(ActivityRecord r, int userId,
 boolean compareIntentFilters, Intent intent, ComponentName cls) {
 if (!r.canBeTopRunning() || r.mUserId != userId) return false;

 if (compareIntentFilters) {
 if (r.intent.filterEquals(intent)) {
 return true;
 }
 } else {
 if (r.mActivityComponent.equals(cls)) {
 return true;
 }
 }
 return false;
}

首先检查,对应的ActivityRecord是否可以运行在topTask,是否与我们目标要启动的Activity是同样的用户Id,也就是在同一个进程。如果compareIntentFilters为true,还是检查他们的intent是否相同,之后会检查是否为同一个Activity类。对于这个有所了解,我们继续看getActivity的代码,它首先是会调用WindowContainer中的这个方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
ActivityRecord getActivity(Predicate<ActivityRecord> callback, boolean traverseTopToBottom,
 ActivityRecord boundary) {
 if (traverseTopToBottom) {
 for (int i = mChildren.size() - 1; i >= 0; --i) {
 final WindowContainer wc = mChildren.get(i);
 if (wc == boundary) return boundary;

 final ActivityRecord r = wc.getActivity(callback, traverseTopToBottom, boundary);
 if (r != null) {
 return r;
 }
 }
 } else {
 ...
 }

 return null;
}

如果单看上面的代码,我们似乎永远都拿不到ActivityRecord,但是呢ActivityRecord也是WindowContainer的子类,在它当中我们也有同名方法,代码如下:

1
2
3
4
5
@Override
ActivityRecord getActivity(Predicate<ActivityRecord> callback, boolean traverseTopToBottom,
 ActivityRecord boundary) {
 return callback.test(this) ? this : null;
}

这里可以看到,他就是调用了我们刚刚传入的那个PooledPredicate来测试自己是否符合要求,从而我们可以拿到对应的ActivityRecord

计算目标Task: computeTargetTask

到这里我们可以继续看startActivityInner方法中的如下代码:

1
2
3
final Task targetTask = reusedTask != null ? reusedTask : computeTargetTask();
final boolean newTask = targetTask == null;
mTargetTask = targetTask;

如果我们刚刚已经拿到reusedTask,那么目标的task就会使用它,如果拿不到则会调用computeTargetTask去获取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
private Task computeTargetTask() {
 if (mStartActivity.resultTo == null && mInTask == null && !mAddingToTask
 && (mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) != 0) {
 // 同时满足这些条件的情况,不复用task,直接返回空
 return null;
 } else if (mSourceRecord != null) {
 //调用源ActivityRecord,直接复用调用源的Task
 return mSourceRecord.getTask();
 } else if (mInTask != null) {
 //inTask一般是AppTaskImpl指定的,就直接用它,它有可能还没创建这里去创建
 if (!mInTask.isAttached()) {
 getOrCreateRootTask(mStartActivity, mLaunchFlags, mInTask, mOptions);
 }
 return mInTask;
 } else {
 //获取或者创建Task
 final Task rootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, null /* task */,
 mOptions);
 final ActivityRecord top = rootTask.getTopNonFinishingActivity();
 if (top != null) {
 return top.getTask();
 } else {
 rootTask.removeIfPossible("computeTargetTask");
 }
 }
 return null;
}

这里我们继续去看一下getOrCreateRootTask,代码如下:

1
2
3
4
5
6
7
8
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);
}

这里还是先拿到调用端的sourceTask以及是否需要onTop,之后调用了RootWindowContainergetOrCreateRootTask方法,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
Task getOrCreateRootTask(@Nullable ActivityRecord r,
 @Nullable ActivityOptions options, @Nullable Task candidateTask,
 @Nullable Task sourceTask, boolean onTop,
 @Nullable LaunchParamsController.LaunchParams launchParams, int launchFlags) {
 ...
 TaskDisplayArea taskDisplayArea = null;

 final int activityType = resolveActivityType(r, options, candidateTask);
 Task rootTask = null;
 ...
 int windowingMode = launchParams != null ? launchParams.mWindowingMode
 : WindowConfiguration.WINDOWING_MODE_UNDEFINED;
 ....
 if (taskDisplayArea == null) {
 taskDisplayArea = getDefaultTaskDisplayArea();
 }
 return taskDisplayArea.getOrCreateRootTask(r, options, candidateTask, sourceTask,
 launchParams, launchFlags, activityType, onTop);
}

因为我们没有设置什么参数,因此会执行到最后的fallback流程,我们只分析这一部分。默认我们拿到的activityTypeActivity_TYPE_STANDARDgetDefaultTaskDisplayArea会拿到默认的TaskDisplayArea这个之前已经分析过了,最后就是通过它去调用getOrCreateRootTask,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Task getOrCreateRootTask(int windowingMode, int activityType, boolean onTop,
 @Nullable Task candidateTask, @Nullable Task sourceTask,
 @Nullable ActivityOptions options, int launchFlags) {
 final int resolvedWindowingMode =
 windowingMode == WINDOWING_MODE_UNDEFINED ? getWindowingMode() : windowingMode;
 if (!alwaysCreateRootTask(resolvedWindowingMode, activityType)) {
 Task rootTask = getRootTask(resolvedWindowingMode, activityType);
 if (rootTask != null) {
 return rootTask;
 }
 } else if (candidateTask != null) {
 ....
 }
 return new Task.Builder(mAtmService)
 .setWindowingMode(windowingMode)
 .setActivityType(activityType)
 .setOnTop(onTop)
 .setParent(this)
 .setSourceTask(sourceTask)
 .setActivityOptions(options)
 .setLaunchFlags(launchFlags)
 .build();
}

因为我们传进来的windowingModeWINDOWING_MODE_UNDEFINED,因此这里会调用getWindowingMode来设置Mode,这里就是调用系统设置了,不需要看代码。

因为ActivityType是ACTIVITY_TYPE_STAND,所以这里alwaysCreateRootTask为true,因为我们传进来的candidateTask也是空,因此最后就是会创建一个新的Task。但是因为是创建的新task,这个Task里面没有运行中的Activity,因此computeTargetTask还是会返回空。

获取PriorAboveTask和task回收检查

继续回来看startActivityInner内部的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (targetTask != null) { //在DisplayArea中获取在targetTask Root上面的其他root task
 mPriorAboveTask = TaskDisplayArea.getRootTaskAbove(targetTask.getRootTask());
}
//如果newTask为false,则看看目标task 顶部的未finish的ActivityRecord
final ActivityRecord targetTaskTop = newTask
 ? null : targetTask.getTopNonFinishingActivity();
if (targetTaskTop != null) {
 startResult = recycleTask(targetTask, targetTaskTop, reusedTask, intentGrants);
 if (startResult != START_SUCCESS) {
 return startResult;
 }
} else {
 mAddingToTask = true;
}

在可以复用栈的情况下,targetTaskTop不为空,比如singleTask的模式,这个时候会去执行recycleTask。其他情况设置mAddingToTask,表示我们的ActivityRecord需要添加到Task。

1
2
3
4
5
6
7
final Task topRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();
if (topRootTask != null) {
 startResult = deliverToCurrentTopIfNeeded(topRootTask, intentGrants);
 if (startResult != START_SUCCESS) {
 return startResult;
 }
}

如果我们检查topRootTask不为空的情况,这里如果我们的启动模式是singleTask,首先会检查task栈顶未启动的Activity是否与当前要启动的相同,如果相同,则不启动当前Activity,仅仅去执行它的newIntent,具体代码就不分析了。

创建RootTask,处理新Activity的Task

再往后看代码,之后就该创建RootTask了,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
if (mTargetRootTask == null) {
 mTargetRootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, targetTask,
 mOptions);
}
if (newTask) {
 final Task taskToAffiliate = (mLaunchTaskBehind && mSourceRecord != null)
 ? mSourceRecord.getTask() : null;
 setNewTask(taskToAffiliate);
} else if (mAddingToTask) {
 addOrReparentStartingActivity(targetTask, "adding to task");
}

上面调用了getOrCreateRootTask,来创建了新的RootTask,与我们之前分析的类似。同时因为我们之前没有成功创建targetTask,因此这里会执行到setNewTask,而taskToAffiliate没有特殊参数,默认我们先按照空来对待吧。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
private void setNewTask(Task taskToAffiliate) {
 final boolean toTop = !mLaunchTaskBehind && !mAvoidMoveToFront;
 final Task task = mTargetRootTask.reuseOrCreateTask(
 mStartActivity.info, mIntent, mVoiceSession,
 mVoiceInteractor, toTop, mStartActivity, mSourceRecord, mOptions);
 task.mTransitionController.collectExistenceChange(task);
 //把新的ActivityRecord放置到Task列表的顶部
 addOrReparentStartingActivity(task, "setTaskFromReuseOrCreateNewTask");
 if (taskToAffiliate != null) {
 mStartActivity.setTaskToAffiliateWith(taskToAffiliate);
 }
}

这里大多数情况,toTop会是true,我们去看一下这个reuseOrCreateTask方法:

 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
Task reuseOrCreateTask(ActivityInfo info, Intent intent, IVoiceInteractionSession voiceSession,
 IVoiceInteractor voiceInteractor, boolean toTop, ActivityRecord activity,
 ActivityRecord source, ActivityOptions options) {

 Task task;
 if (canReuseAsLeafTask()) { //如果没有Task子节点或者不是组织创建的
 task = reuseAsLeafTask(voiceSession, voiceInteractor, intent, info, activity);
 } else {
 // 创建taskId
 final int taskId = activity != null
 ? mTaskSupervisor.getNextTaskIdForUser(activity.mUserId)
 : mTaskSupervisor.getNextTaskIdForUser();
 final int activityType = getActivityType();
 //创建task,并且当前Task设置为这个Task的Parent,在build当中,把当前的Task放置到Parent的mChildren当中,根据toTop决定是否放置到顶部
 task = new Task.Builder(mAtmService)
 .setTaskId(taskId)
 .setActivityType(activityType != ACTIVITY_TYPE_UNDEFINED ? activityType
 : ACTIVITY_TYPE_STANDARD)
 .setActivityInfo(info)
 .setActivityOptions(options)
 .setIntent(intent)
 .setVoiceSession(voiceSession)
 .setVoiceInteractor(voiceInteractor)
 .setOnTop(toTop)
 .setParent(this)
 .build();
 }

 int displayId = getDisplayId();
 if (displayId == INVALID_DISPLAY) displayId = DEFAULT_DISPLAY;
 final boolean isLockscreenShown = mAtmService.mTaskSupervisor.getKeyguardController()
 .isKeyguardOrAodShowing(displayId);
 if (!mTaskSupervisor.getLaunchParamsController()
 .layoutTask(task, info.windowLayout, activity, source, options)
 && !getRequestedOverrideBounds().isEmpty()
 && task.isResizeable() && !isLockscreenShown) {
 //设置task的布局边界
 task.setBounds(getRequestedOverrideBounds());
 }

 return task;
}

上面代码我们就可以复用或者创建新的task,详见注释。拿到Task,或者我们之前已经有Task的情况下(mAddingToTask为true)的时候,还需要执行addOrReparentStartingActivity,代码如下:

 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
private void addOrReparentStartingActivity(@NonNull Task task, String reason) {
 TaskFragment newParent = task;
 if (mInTaskFragment != null) {
 //我们的场景不涉及InTaskFragment不为空,忽略
 ...
 } else {
 //当clearTop的时候,并且是可嵌入的,这个时候会保存TaskFragment到mAddingToTaskFragment
 TaskFragment candidateTf = mAddingToTaskFragment != null ? mAddingToTaskFragment : null;
 if (candidateTf == null) {
 //获取目标Task的topRunningActivity,新建的Task不存在
 final ActivityRecord top = task.topRunningActivity(false /* focusableOnly */,
 false /* includingEmbeddedTask */);
 if (top != null) {
 candidateTf = top.getTaskFragment();
 }
 }
 if (candidateTf != null && candidateTf.isEmbedded()
 && canEmbedActivity(candidateTf, mStartActivity, task) == EMBEDDING_ALLOWED) {
 //如果拿到了topTask,并且对应的Task是可嵌入的,并且要打开的ActivityRecord也可被嵌入,这把拿到的这个Task作为新的父Task
 newParent = candidateTf;
 }
 }
 //新的ActivityRecord的TaskFragment为空,或者和新的Parent一样,就把这个ActivityRecord放到Task的顶部
 if (mStartActivity.getTaskFragment() == null
 || mStartActivity.getTaskFragment() == newParent) {
 newParent.addChild(mStartActivity, POSITION_TOP);
 } else {
 mStartActivity.reparent(newParent, newParent.getChildCount() /* top */, reason);
 }
}

这里会检查如果新的父Task和我们可以复用的Task是否相同,如果相同,或者ActivityRecord中还没有parent,这个时候就把ActivityRecord添加到Task的孩子列表的顶部。而如果ActivityRecord已经存在了parent并且不是我们将要设置的这个,就需要做reparent,这个步骤代码比较复杂,前面调用检查判断的调用省略,直接看最后的调用,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//WindowContainer.java
void reparent(WindowContainer newParent, int position) {
 final DisplayContent prevDc = oldParent.getDisplayContent();
 final DisplayContent dc = newParent.getDisplayContent();

 mReparenting = true;
 //从旧的parent中移除自己,并把自己添加到新parent的指定位置
 oldParent.removeChild(this);
 newParent.addChild(this, position);
 mReparenting = false;

 // 重新布局layout
 dc.setLayoutNeeded();
 //如果新旧的DisplayContent不同,还需要做displayChange的处理
 if (prevDc != dc) {
 onDisplayChanged(dc);
 prevDc.setLayoutNeeded();
 }
 getDisplayContent().layoutAndAssignWindowLayersIfNeeded();

 onParentChanged(newParent, oldParent);
 onSyncReparent(oldParent, newParent);
}

以上的代码我们看到有做parent的替换,但是复杂点在后面的onParentChanged里面,这里会做SurfaceControl的创建或者reparent,这里就不深入了。除此之外,这里还涉及到动画的处理,我们这里也 不深入了。

继续往后看

1
2
3
4
if (!mAvoidMoveToFront && mDoResume) {
 mTargetRootTask.getRootTask().moveToFront("reuseOrNewTask", targetTask);
 ...
}

这里会在检查我们的TargetRootTask相比与它的RootTask如果不是在顶部的,需要把它移动到顶部。再往后面就是调用TargetRootTask去启动Activity,以及确认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
final boolean isTaskSwitch = startedTask != prevTopTask && !startedTask.isEmbedded();
//启动Activity
mTargetRootTask.startActivityLocked(mStartActivity, topRootTask, newTask, isTaskSwitch,
 mOptions, sourceRecord);
if (mDoResume) {
 final ActivityRecord topTaskActivity = startedTask.topRunningActivityLocked();
 if (!mTargetRootTask.isTopActivityFocusable()
 || (topTaskActivity != null && topTaskActivity.isTaskOverlay()
 && mStartActivity != topTaskActivity)) {
 //如果当前要启动的Activity还没有启动,没有在栈顶端,执行下面的代码
 mTargetRootTask.ensureActivitiesVisible(null /* starting */,
 0 /* configChanges */, !PRESERVE_WINDOWS);
 mTargetRootTask.mDisplayContent.executeAppTransition();
 } else {
 if (mTargetRootTask.isTopActivityFocusable()
 && !mRootWindowContainer.isTopDisplayFocusedRootTask(mTargetRootTask)) {
 mTargetRootTask.moveToFront("startActivityInner");
 }
 mRootWindowContainer.resumeFocusedTasksTopActivities(
 mTargetRootTask, mStartActivity, mOptions, mTransientLaunch);
 }
}
mRootWindowContainer.updateUserRootTask(mStartActivity.mUserId, mTargetRootTask); //更新用户的rootTask

// 更新系统的最近任务
mSupervisor.mRecentTasks.add(startedTask);

到此位置才算是完成了所有Task计算以及Activity的启动。

通过Adb shell看Activity Task栈

前面都是在解读Android的源码可能比较抽象,其中涉及到了挺多WindowContainer和Task等等相关的查找创建的,为了更加形象。我写了个小demo,主页面是普通的launchMode,另外一次打开了一个singleTask启动Mode的和一个singleInstance 启动Mode的页面,然后我们用一下命令进行Activity Task的dump:

1
adb shell dumpsys activity activities > ~/activitytasks.txt

我们就得到了如下的内容(为方便解读,做了删减):

Display #0 (activities from top to bottom):
* Task{8ed7532 #40 type=standard A=10116:com.example.myapplication U=0 visible=true visibleRequested=true mode=fullscreen translucent=false sz=1}
topResumedActivity=ActivityRecord{3653c00 u0 com.example.myapplication/.SimpleInstanceActivity} t40}
* Hist #0: ActivityRecord{3653c00 u0 com.example.myapplication/.SimpleInstanceActivity} t40}
* Task{ac77886 #39 type=standard A=10116:com.example.myapplication U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=2}
mLastPausedActivity: ActivityRecord{6c019a5 u0 com.example.myapplication/.SingleTaskActivity} t39}
* Hist #1: ActivityRecord{6c019a5 u0 com.example.myapplication/.SingleTaskActivity} t39}
* Hist #0: ActivityRecord{ef92174 u0 com.example.myapplication/.MainActivity} t39}
* Task{d8527c1 #1 type=home U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
* Task{d60ff49 #33 type=home I=com.android.launcher3/.uioverrides.QuickstepLauncher U=0 rootTaskId=1 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
mLastPausedActivity: ActivityRecord{868b56f u0 com.android.launcher3/.uioverrides.QuickstepLauncher} t33}
* Hist #0: ActivityRecord{868b56f u0 com.android.launcher3/.uioverrides.QuickstepLauncher} t33}
* Task{2c52978 #36 type=standard A=10044:com.android.documentsui U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
mLastPausedActivity: ActivityRecord{f9c48b6 u0 com.android.documentsui/.files.FilesActivity} t36}
mLastNonFullscreenBounds=Rect(338, 718 - 1103, 2158)
isSleeping=false
* Hist #0: ActivityRecord{f9c48b6 u0 com.android.documentsui/.files.FilesActivity} t36}
* Task{e38c1d6 #35 type=standard A=10044:com.android.documentsui U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=1}
mLastPausedActivity: ActivityRecord{32a5344 u0 com.android.documentsui/.files.FilesActivity} t35}
mLastNonFullscreenBounds=Rect(338, 718 - 1103, 2158)
isSleeping=false
* Hist #0: ActivityRecord{32a5344 u0 com.android.documentsui/.files.FilesActivity} t35}
* Task{1d65c74 #3 type=undefined U=0 visible=false visibleRequested=false mode=fullscreen translucent=true sz=2}
mCreatedByOrganizer=true
* Task{41cb5e3 #5 type=undefined U=0 rootTaskId=3 visible=false visibleRequested=false mode=multi-window translucent=true sz=0}
mBounds=Rect(0, 2960 - 1440, 4440)
mCreatedByOrganizer=true
isSleeping=false
* Task{1cdca12 #4 type=undefined U=0 rootTaskId=3 visible=false visibleRequested=false mode=multi-window translucent=true sz=0}
mCreatedByOrganizer=true
isSleeping=false

这上面就是我们的mRootContainer它当中的的display下面的所有的Task记录,因为我的手机只有一块屏幕,这里只有一个display0, 并且展示了他们的存储关系,这里我们可以看到我们的SimpleInstanceActivity它是在独立的Task当中的。用图表简单描绘一下,结构如下所示:

总结

以上就是Activity Task管理的分析,因为这个流程真的是非常复杂,因此中间的很多步骤还是进行了部分省略。Android系统迭代了这么多年,作为UI展示的组件,Activity承载了太多东西,多屏幕,折叠屏什么的都要支持,因此引入的东西就越来越多。官方也是意识到了这一块的,Activity的管理从AMS抽出来单独的ATMS,ActivityTaskSupervisor的功能也在慢慢抽离到其他的代码中,当前代码里面也添加了很多注释,只要花时间还是能够给搞明白的。

本文仅为一家之言,因为个人疏忽,可能文中也会出现一些错误,欢迎大家指正。

看完评论一下吧

  •  

Android源码分析:Activity启动流程分析

Activity是Android中四大组件使用最多的一种,不准确的说,一个Activity就是一个独立页面的承载,因此看Android系统的源码,Activity的启动也是必须要去阅读的。今天的文章就来介绍Activity的启动。因为之前的文章已经分析了ClientTransaction,因此我们对于AMS调用Activity的生命周期和启动有所了解。并且我们也已经分析过了Binder,对于跨进程通讯我们也比较清楚了,不需要细看。我们也分析了应用进程的启动,我们分析Activity启动过程,就不需要去关注应用进程的启动了。有了这些基础,分析Activity的启动会容易一点点。

发起启动Activity

我们首先来看一下启动Activity的调用,我们通常会使用下面的显示调用来启动一个Activity。

1
2
Intent intent = new Intent(context, AActivity.class);
startActivity(intent);

当然也有可能会使用隐式调用来启动一个Activity,如下:

1
2
Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse("https://isming.me"));
startActivity(intent);

不过以上两者在启动过程中,仅仅是查找目标组件有区别,并且对于隐式调用,可能存在多个可以启动的Activity,这个时候需要让用户选择目标的页面。对于这一块,我们在后面这个地方会考虑部分略过。

Activity中最终会走到 startActivityForResult(intent, requestCode, options)方法中,这里传入的options我们可以用它设置一些东西,比如App跳转的动画等,我们前面的场景的options为空,并且手机上默认的parent activity也为空,因此会执行这一部分:

1
2
3
4
5
6
7
8
Instrumentation.ActivityResult ar = mInstrumentation.execStartActivity(
 this, mMainThread.getApplicationThread(), mToken, this,
 intent, requestCode, options);
if (ar != null) {
 mMainThread.sendActivityResult(
 mToken, mEmbeddedID, requestCode, ar.getResultCode(),
 ar.getResultData());
}

发起端的处理

在Instrumentation.execStartActivity中会调用如下代码:

1
2
3
4
5
6
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);
 notifyStartActivityResult(result, options);
 checkStartActivityResult(result, intent);

早期是直接通过ActivityManagerService去启动新的页面的,在这个commit开始把Activity管理的拆分到ActivityTaskManagerService中去。这里我们看到是去获取ActivityTaskManagerService后面简称ATMS,获取ATMS的代码就不罗列了。

这里传到ATMS的参数,包括,发起应用的ApplicationThread,包名(对于普通应用来说opPackage和packageName是一样的),启动的Intent,token和target一般都是空。

ATMS执行startActivity

最终的执行实际是通过binder调用到ActivityTaskManagerService中的startActivity方法,这个方法中又直接调用了startActivityAsUser,其中会有一些检查,检查调用端的uid和packageName是否匹配和其他一些检查,这里不太关注,我们主要关注以下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
userId = getActivityStartController().checkTargetUser(userId, validateIncomingUser,
 Binder.getCallingPid(), Binder.getCallingUid(), "startActivityAsUser");
 return getActivityStartController().obtainStarter(intent, "startActivityAsUser")
 .setCaller(caller)
 .setCallingPackage(callingPackage)
 .setCallingFeatureId(callingFeatureId)
 .setResolvedType(resolvedType)
 .setResultTo(resultTo)
 .setResultWho(resultWho)
 .setRequestCode(requestCode)
 .setStartFlags(startFlags)
 .setProfilerInfo(profilerInfo)
 .setActivityOptions(opts)
 .setUserId(userId)
 .execute();

从上面的逻辑可以看到,控制Activity启动的代码都放到ActivityStartController中了,首先是获取用户uid,因为每个应用的都会有一个uid,其后就是获取一个ActivityStarter,再通过构建者模式把启动Activity的参数都传到ActivityStarter中去,最后在ActivityStarter的execute()方法中去执行启动的逻辑。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
...
if (mRequest.activityInfo == null) { //如果还没有activitgyInfo去填充
 mRequest.resolveActivity(mSupervisor);
}
...
synchronized (mService.mGlobalLock) {
 res = resolveToHeavyWeightSwitcherIfNeeded(); //检查是否为heavy-weight 进程,系统会限制同一时间只有一个heavy-weight进程
 if (res != START_SUCCESS) {
 return res;
 }
 res = executeRequest(mRequest);
}

其中第2行代码就是根据我们的Intent去查询我们将要打开的目标Activity信息。

解析ActivityInfo

resolveActivity中的核心代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
//ActivityStarter.Request
void resolveActivity(ActivityTaskSupervisor supervisor) {
 resolveInfo = supervisor.resolveIntent(intent, resolvedType, userId,
 0 /* matchFlags */,
 computeResolveFilterUid(callingUid, realCallingUid, filterCallingUid));
 activityInfo = supervisor.resolveActivity(intent, resolveInfo, startFlags,
 profilerInfo);
 if (activityInfo != null) {
 intentGrants = supervisor.mService.mUgmInternal.checkGrantUriPermissionFromIntent(
 intent, resolvedCallingUid, activityInfo.applicationInfo.packageName,
 UserHandle.getUserId(activityInfo.applicationInfo.uid));
 }
}

resolveActivity的工作主要由ActivityTaskSupervisor来完成,首先是resolveIntent来获取ResolveInfo,之后调用resolveActivity获取ActivityInfo,最后再去对Intent中的data Uri做权限检查,我们这里只需要分析前两步骤就可。

resolveIntent方法内部,我们看到是调用了PackageManagerServiceresolveIntent方法,代码如下,具体就不深入探究了。

1
2
3
4
//ActivityTaskSupervisor
return mService.getPackageManagerInternalLocked().resolveIntent(
 intent, resolvedType, modifiedFlags, privateResolveFlags, userId, true,
 filterCallingUid);

resolveActivity代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
ActivityInfo resolveActivity(Intent intent, ResolveInfo rInfo, int startFlags,
 ProfilerInfo profilerInfo) {
 final ActivityInfo aInfo = rInfo != null ? rInfo.activityInfo : null;
 if (aInfo != null) {
 intent.setComponent(new ComponentName(
 aInfo.applicationInfo.packageName, aInfo.name));
 ...
 }
 return aInfo;
}

这里所做的事情则比较简单,就是从前面拿到的ResolveInfo中拿到activityInfo,并且构建一个ComponentName放到Intent中去。到此为止就拿到了要打开的Activity信息。

ActivityStarter.executeRequest

在前面拿到ActivityInfo,并且我们还构建了一个Request,我们就会继续调用executeRequest方法,其中是有大段的代码 是检查权限,以及一些系统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

mInterceptor.setStates(userId, realCallingPid, realCallingUid, startFlags, callingPackage,
 callingFeatureId);
if (mInterceptor.intercept(intent, rInfo, aInfo, resolvedType, inTask, inTaskFragment,
 callingPid, callingUid, checkedOptions)) { //拦截器对要启动的Activity做预处理
 intent = mInterceptor.mIntent;
 rInfo = mInterceptor.mRInfo;
 aInfo = mInterceptor.mAInfo;
 resolvedType = mInterceptor.mResolvedType;
 inTask = mInterceptor.mInTask;
 callingPid = mInterceptor.mCallingPid;
 callingUid = mInterceptor.mCallingUid;
 checkedOptions = mInterceptor.mActivityOptions;

 intentGrants = null;
}

final ActivityRecord r = new ActivityRecord.Builder(mService) //构建ActivityRecord
 .setCaller(callerApp)
 .setLaunchedFromPid(callingPid)
 .setLaunchedFromUid(callingUid)
 .setLaunchedFromPackage(callingPackage)
 .setLaunchedFromFeature(callingFeatureId)
 .setIntent(intent)
 .setResolvedType(resolvedType)
 .setActivityInfo(aInfo)
 .setConfiguration(mService.getGlobalConfiguration())
 ...
 .build();

 mLastStartActivityRecord = r; //保存构建的ActivityRecord

 ...

 mLastStartActivityResult = startActivityUnchecked(r, sourceRecord, voiceSession,
 request.voiceInteractor, startFlags, true /* doResume */, checkedOptions,
 inTask, inTaskFragment, balCode, intentGrants); //执行启动Activity,并保存结果到mLastStartActivityResult中,以及结果中返回这个result

 if (request.outActivity != null) {
 request.outActivity[0] = mLastStartActivityRecord;
 }
 ...

这里我们有一些权限检查和系统处理之类的没有贴,不过还是贴了一下intercept方法,这里就是给了系统的其他代码来修改Intent的机会。之后就会利用我们传进来的信息去创建ActivityRecord,并且调用startActivityUnchecked去进入下一步:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

private int startActivityUnchecked(final ActivityRecord r, ActivityRecord sourceRecord,...,NeededUriGrants intentGrants) {
 int result = START_CANCELED;
 final Task startedActivityRootTask;

 ......
 try {
 ......
 try {
 .....
 result = startActivityInner(r, sourceRecord, voiceSession, voiceInteractor,
 startFlags, doResume, options, inTask, inTaskFragment, balCode,
 intentGrants);
 } finally {
 startedActivityRootTask = handleStartResult(r, options, result, newTransition,
 remoteTransition); //处理启动Activity的结果
 }
 } finally {
 mService.continueWindowLayout(); //wms处理
 }
 postStartActivityProcessing(r, result, startedActivityRootTask);

 return result;
 }

这里又走到了startActivityInner(),startActivityInner()会去计算launch falgs,去判断是否开创建新的Task还是可以复用task,以及调用启动的后续代码,这个方法的代码比较长我们先一点一点的看。

Task的处理

首先来看其中关于flag的处理,首先就是其中调用的computeLaunchingTaskFlags方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
private void computeLaunchingTaskFlags() {
 ...
 if (mInTask == null) {
 if (mSourceRecord == null) {
 if ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 && mInTask == null) {
 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
 }
 } else if (mSourceRecord.launchMode == LAUNCH_SINGLE_INSTANCE) {
 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
 } else if (isLaunchModeOneOf(LAUNCH_SINGLE_INSTANCE, LAUNCH_SINGLE_TASK)) {
 mLaunchFlags |= FLAG_ACTIVITY_NEW_TASK;
 }
 }

 if ((mLaunchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0
 && ((mLaunchFlags & FLAG_ACTIVITY_NEW_TASK) == 0 || mSourceRecord == null)) {
 mLaunchFlags &= ~FLAG_ACTIVITY_LAUNCH_ADJACENT;
 }
}

这里就是对于我们的启动的LaunchFlag做处理,比如说LAUNCH_SIGLE_INSTANCELAUNCH_SINGLE_TASK都给添加FLAG_ACTIVITY_NEW_TASK等。

随后则是计算Task:

1
2
3
4
5
6
7
final Task prevTopRootTask = mPreferredTaskDisplayArea.getFocusedRootTask();
final Task prevTopTask = prevTopRootTask != null ? prevTopRootTask.getTopLeafTask() : null;
final Task reusedTask = getReusableTask();

final Task targetTask = reusedTask != null ? reusedTask : computeTargetTask();
final boolean newTask = targetTask == null;
mTargetTask = targetTask;

getFocusedRootTask会尝试去获取首选的Task,如果不存在也会从当前显示屏获取获取最顶部的可触摸并且在展示的Task。而这个preTopTask如果能够获取到,它又会去获取的它叶子节点。叶子节点的规则就是没有只节点。Task相关类的继承结果如下:

classDiagram
class ConfigurationContainer {
<<abstract>>
}
class WindowContainer
class TaskFragment
class Task
ConfigurationContainer <|--WindowContainer
WindowContainer <|-- TaskFragment
TaskFragment <|--Task
WindowContainer <|--RootWindowContainer
1
2
3
4
if (mTargetRootTask == null) {
 mTargetRootTask = getOrCreateRootTask(mStartActivity, mLaunchFlags, targetTask,
 mOptions);
}

这里最终会拿到RootTask,如果没有也会创建,具体代码这里不分析了。

调用Task的 resumeFocusedTasksTopActivities

之后会调用如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//ActivityStarter
 ...
 mTargetRootTask.startActivityLocked(mStartActivity, topRootTask, newTask, isTaskSwitch,
 mOptions, sourceRecord); //这个名字是startActivityLock但并不是真的打开activity,而是把Activity对应的task放到列表的最前面,以及会展示window动画
 if (mDoResume) {
 if (!mTargetRootTask.isTopActivityFocusable()
 || (topTaskActivity != null && topTaskActivity.isTaskOverlay()
 && mStartActivity != topTaskActivity)) {
 //对样式pip页面或者其他一些情况的处理
 ...
 } else {
 ...
 //真正的启动Activity的代码这里是入口
 mRootWindowContainer.resumeFocusedTasksTopActivities(
 mTargetRootTask, mStartActivity, mOptions, mTransientLaunch);
 }
 ...

 }
 ...

 return START_SUCCESS;
 }

我们看到首先调用了startActivityLocked方法,这里主要做的就是把我们的ActivityReccord放到Task中去,并且展示Activity的启动动画。之后调用的RootContainerresumeFocsedTasksTopActivities才是真正的启动,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
//RootWindowContainer.java
boolean resumeFocusedTasksTopActivities(Task targetRootTask, ActivityRecord target, ActivityOptions targetOptions,
boolean deferPause) {
 ...
 boolean result = false;
 if (targetRootTask != null && (targetRootTask.isTopRootTaskInDisplayArea()
 || getTopDisplayFocusedRootTask() == targetRootTask)) {
 result = targetRootTask.resumeTopActivityUncheckedLocked(
 target,targetOptions, deferPause); //执行启动
 }
 ...
}

后面会走到Task的resumeTopActivityUnCheckedLocked方法

 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
boolean resumeTopActivityUncheckedLocked(ActivityRecord prev, ActivityOptions options,
 boolean deferPause) {
 ...
 if (isLeafTask()) {
 if (isFocusableAndVisible()) { //可触摸可见
 someActivityResumed = resumeTopActivityInnerLocked(prev, options, deferPause);
 }
 } else {
 int idx = mChildren.size() - 1;
 while (idx >= 0) {
 final Task child = (Task) getChildAt(idx--);
 if (!child.isTopActivityFocusable()) {
 continue;
 }
 if (child.getVisibility(null /* starting */)
 != TASK_FRAGMENT_VISIBILITY_VISIBLE) {
 if (child.topRunningActivity() == null) {
 continue;
 }
 break;
 }

 someActivityResumed |= child.resumeTopActivityUncheckedLocked(prev, options,
 deferPause);
 if (idx >= mChildren.size()) {
 idx = mChildren.size() - 1;
 }
 }
 }


}

此处如果当前的Task本来就是叶子节点,那么会调用resumeTopActivityInnerLocked方法,否则会遍历子的task列表,在子task列表中找到符合条件的去执行resumeTopActivityUncheckedLocked方法,如此最后还是会调用到resumeTopActivityInnerLocked方法,而我们再跟进去看,可以看到其中的核心逻辑是调用topFragment的resumeTopActivity方法。

 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
final boolean resumeTopActivity(ActivityRecord prev, ActivityOptions options,
 boolean deferPause) {
 ...
 boolean pausing = !deferPause && taskDisplayArea.pauseBackTasks(next);
 if (mResumedActivity != null) {
 pausing |= startPausing(mTaskSupervisor.mUserLeaving, false /* uiSleeping */,
 next, "resumeTopActivity"); //首先把顶部处于Resumed状态的activity执行pausing
}
if (pausing) {
 //检查即将启动的Activity的Activity的进程有没有起来,如果没有进程去创建进程,创建进程的代码需要单独分析,此处略过
 if (next.attachedToProcess()) {
 next.app.updateProcessInfo(false /* updateServiceConnectionActivities */,
 true /* activityChange */, false /* updateOomAdj */,
 false /* addPendingTopUid */);
 } else if (!next.isProcessRunning()) {
 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 true;
 }
 if (next.attachedToProcess()) { //如何Activity已经在这个进程中了
 ...
 final ClientTransaction transaction =
 ClientTransaction.obtain(next.app.getThread(), next.token); //构建ClientTransaction,传入要打开的Activity对应的applicationThread和IBinder
 ...
 if (next.newIntents != null) { //把intent放进去,后面会把Activity吊起,并 调用onNewIntent()
 transaction.addCallback(
 NewIntentItem.obtain(next.newIntents, true /* resume */));
 }
 ...
 mAtmService.getLifecycleManager().scheduleTransaction(transaction);

 } else {
 ...
 mTaskSupervisor.startSpecificActivity(next, true, true); //启动Activity
 }


}

上面最后的代码可以看到,Activity已经存在的时候是走到onNewIntent, 调用的代码被包装成了ClientTransaction,通过ClientlifecycleManager 的scheduleTransaction方法,最终其实是调用了IApplicationThread的scheduleTransaction,最终通过binder调用到了app进程中的同名方法,这里要去看ActivityThread, ApplicationThread为它的内部类,看它的代码,它实际调用了ActivityThread的同名方法。而启动Activity,我们一路跟着startSpecificActivity()方法进去最终会看到也是通过ClientTransaction,代码如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
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));
// Set desired final state. 
final ActivityLifecycleItem lifecycleItem;
if (andResume) {
 lifecycleItem = ResumeActivityItem.obtain(isTransitionForward,
 r.shouldSendCompatFakeFocus());
} else {
 lifecycleItem = PauseActivityItem.obtain();
}
clientTransaction.setLifecycleStateRequest(lifecycleItem);
mService.getLifecycleManager().scheduleTransaction(clientTransaction);

目标进程执行performLaunchActivity

我们之前已经分析过ClientTransaction,我们知道这个LaunchActivityItem的callback,最后client就是我们的ActivityThread,会执行它的handleLaunchActivity方法,其中最核心的就是如下这一句:

1
final Activity a = performLaunchActivity(r, customIntent);

我们继续往里面看:

 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
//content of performLaunchActivity() function
...
//为Activity创建Base Context
ContextImpl appContext = createBaseContextForActivity(r);
Activity activity = null;
...
//创建Activity实例,就是通过反射来实例化一个Activity实例
java.lang.ClassLoader cl = appContext.getClassLoader();
activity = mInstrumentation.newActivity(
 cl, component.getClassName(), r.intent);
...
//拿到Application的实例,如果缓存中有就用,没有就创建一个新的,这里也不看具体代码了
Application app = r.packageInfo.makeApplicationInner(false, mInstrumentation);
...
//为Activity创建配置,如里面有语言,屏幕设置等等参数,不具体分析了
Configuration config =
 new Configuration(mConfigurationController.getCompatConfiguration());
if (r.overrideConfig != null) {
 config.updateFrom(r.overrideConfig);
}
...
//把Activity和baseContext绑定,并且把一些参数附加到Activity实例上去
appContext.setOuterContext(activity);
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);
...
mInstrumentation.callActivityOnCreate(activity, r.state); //这一步完成,Activity里面会执行完onCreate()
...
r.setState(ON_CREATE);

再来具体看一看Activity的attach方法中做了什么

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
attachBaseContext(context);
mFragments.attachHost(null /*parent*/);
mWindow = new PhoneWindow(this, window, activityConfigCallback);
....
mWindow.setWindowManager(
 (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
 mToken, mComponent.flattenToString(),
 (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;

可以看到在里面绑定了baseContext,以及创建了我们的PhoneWindow,以及把window和windowManager进行绑定,还有其他一些Activity内部会用到的参数的传递。

而mInstrumentation里面最终是会调用Activity的performCreate,其中则会调用activity的onCreate。这里就不贴相关代码了。

这样我们的Activity才走完onCreate,而剩余步骤,我们之前还设置了LifecycleStateRequestResumeActivityItem,因此这是要让我们的Activity最终进入到Resume状态,具体的可以参看ClientTransaction分析。两篇文章配合着一起,就是完整的Activity启动流程了。

总结

从Activity调用startActivity,一直到A T M S调用ActivityStarter的调用时序图如下:

sequenceDiagram
Activity->>Activity: startActivity
Activity->>Instrumentation:execStartActivity
Instrumentation->>ATMS: startActivity
ATMS->>ActivityStarter: execute

从ActivityStarter调用到新的进程处理的时序图如下(省略了到Activity部分的流程):

sequenceDiagram
ActivityStarter->>ActivityStarter: resolveActivity
ActivityStarter->>ActivityStarter: executeRequest
ActivityStarter->>ActivityStarter: startActivityUnchecked
ActivityStarter->>RootWindowContainer: resumeFocusedTasksTopActivities
RootWindowContainer->>Task: resumeTopActivityUncheckedLocked
Task->>Task: resumeTopActivity
Task->>ActivityTaskSupervisor: startSpecificActivity
ActivityTaskSupervisor->>ActivityTaskSupervisor: realStartActivityLocked
ActivityTaskSupervisor->>ActivityThread: handleLaunchActivity
note right of ActivityTaskSupervisor: 通过ClientTransaction
ActivityThread->>ActivityThread: performLaunchActivity

以上就是一个较为精简的Activity启动的流程。其中省略了不少东西,关于startActivityForResult的情况需要获取到打开的Activity的结果的情况这里还没有讨论。

看代码可以发现Activity的启动过程是非常的复杂的,再加上新版本的Android支持多屏幕,折叠屏,分屏,画中画等等非常多的特性,因而Task的复用,新建就很复杂,因此本文这一部分暂时放下,等到以后在写。

如果你也对于Android系统源码感兴趣,欢迎与我交流。博文因为个人局限,也难免会出现差错,欢迎大家指正。

看完评论一下吧

  •  

Android源码分析: 应用进程启动分析

Android应用进程的启动,简单来说就是从zygot进程fork出来一个新进程,并对其进行一些初始化。这样做系统的一些代码和资源等等就不需要重复加载,一些环境变量也都不需要重新设置,可以说是很巧妙的设置。下面就来具体分析一下其初始化过程。

启动时机

应用进程的启动,一般是在创建四大组件,比如说启动Activity,Service,使用ContentProvider,有广播需要处理,这些情况需要创建进程。在我们分析的代码当中,除了这几种情况,BackupAngent也会涉及到创建App进程。

启动进程调用的为AMS当中的startProcessLocked方法, 我们注意看的话,AMS当中还有另一个方法startIsolatedProcess也是用来启动进程的,但是这个方法它启动的进程一般是给系统使用的,我们这里不会分析。

AMS调用启动进程

我们就从AMSstartProcessLocked这个方法开始看起来:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
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 */);
}

这里我们传入的参数intentFlags为0,zygotePolicyFlagsZYGOTE_POLICY_FLAG_LATENCY_SENSITIVE, allowWhileBootingfalse, isolatedfalse。之后startProcessLocked方法内部用调用了ProcessList的同名方法,其中我们关注的核心语句如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
ProcessRecord app;
...
app = getProcessRecordLocked(processName, info.uid); //从缓存中获取ProcessRecord
...
if (app == null) {
 app = newProcessRecordLocked(info, processName, isolated, isolatedUid, isSdkSandbox,
 sdkSandboxUid, sdkSandboxClientAppPackage, hostingRecord);
} else {
 app.addPackage(info.packageName, info.longVersionCode, mService.mProcessStats);
}
...
final boolean success =
 startProcessLocked(app, hostingRecord, zygotePolicyFlags, abiOverride);

以上代码可以看到,会先去获取是否有现有的processRecord可用,有的话就拿出来使用,没有的话会创建新的,之后会调用startProcessLocked方法。ProcessList中使用mProcessNames来存储ProcessRecord与processName和uid的对应关系,查找的逻辑就是从map中查找不再关注。newProcessRecordLocked方法则是创建新的ProcessRecord,并且会把各种信息保存到这个record当中去。我们这里可以继续看startProcessLocked方法,,最终会调用这个方法:

 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
boolean startProcessLocked(ProcessRecord app, HostingRecord hostingRecord,
 int zygotePolicyFlags, boolean disableHiddenApiChecks, boolean disableTestApiChecks,
 String abiOverride) {
 //对于从缓存拿到的ProcessRecord,把原来的信息清掉
 if (app.getPid() > 0 && app.getPid() != ActivityManagerService.MY_PID) {
 mService.removePidLocked(app.getPid(), app);
 app.setBindMountPending(false);
 app.setPid(0);
 app.setStartSeq(0);
 }
 app.unlinkDeathRecipient();
 app.setDyingPid(0);
 ...
 final IPackageManager pm = AppGlobals.getPackageManager();
 permGids pm.getPackageGids(app.info.packageName, =
 MATCH_DIRECT_BOOT_AUTO, app.userId);
 StorageManagerInternal storageManagerInternal = LocalServices.getService(
 StorageManagerInternal.class);
 mountExternal = storageManagerInternal.getExternalStorageMountMode(uid,
 app.info.packageName); //检查外部存储访问权限
 externalStorageAccess = storageManagerInternal.hasExternalStorageAccess(uid,
 app.info.packageName);
 if (pm.checkPermission(Manifest.permission.INSTALL_PACKAGES,
 app.info.packageName, userId)
 == PackageManager.PERMISSION_GRANTED) { //检查安装应用的权限
 Slog.i(TAG, app.info.packageName + " is exempt from freezer");
 app.mOptRecord.setFreezeExempt(true);
 }
 if (app.processInfo != null && app.processInfo.deniedPermissions != null) {
 for (int i = app.processInfo.deniedPermissions.size() - 1; i >= 0; i--) {
 int[] denyGids = mService.mPackageManagerInt.getPermissionGids(
 app.processInfo.deniedPermissions.valueAt(i), app.userId);
 if (denyGids != null) {
 for (int gid : denyGids) {
 permGids = ArrayUtils.removeInt(permGids, gid);
 }
 }
 }
 }

 gids = computeGidsForProcess(mountExternal, uid, permGids, externalStorageAccess); //根据前面的权限和相关信息,计算新启动的进程 需要分配的用户
 ...
 //读取app的debuggable,profileable等标志位
 boolean debuggableFlag = (app.info.flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0;
 boolean isProfileableByShell = app.info.isProfileableByShell();
 boolean isProfileable = app.info.isProfileable();
 if (debuggableFlag) {
 runtimeFlags |= Zygote.DEBUG_ENABLE_JDWP;
 runtimeFlags |= Zygote.DEBUG_JAVA_DEBUGGABLE;
 runtimeFlags |= Zygote.DEBUG_ENABLE_CHECKJNI;


 if (android.provider.Settings.Global.getInt(mService.mContext.getContentResolver(), android.provider.Settings.Global.ART_VERIFIER_VERIFY_DEBUGGABLE, 1) == 0) {
 runtimeFlags |= Zygote.DISABLE_VERIFIER;
 }
 }
 if (isProfileableByShell) {
 runtimeFlags |= Zygote.PROFILE_FROM_SHELL;
 }
 if (isProfileable) {
 runtimeFlags |= Zygote.PROFILEABLE;
 } //把标志位信息保存到runtimeFlags中
 ...//其他一些flag写入到runtimeFlags中去
 if (debuggableFlag) {
 //debuggable时候使用wrap.sh去fork进程
 String wrapperFileName = app.info.nativeLibraryDir + "/wrap.sh";
 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
 try {
 if (new File(wrapperFileName).exists()) {
 invokeWith = "/system/bin/logwrapper " + wrapperFileName;
 }
 } finally {
 StrictMode.setThreadPolicy(oldPolicy);
 }
 }
 String requiredAbi = (abiOverride != null) ? abiOverride : app.info.primaryCpuAbi;
 if (requiredAbi == null) { //设置 app native 库使用的abi,如arm或者x86或者armv8等等
 requiredAbi = Build.SUPPORTED_ABIS[0];
 }
 String instructionSet = null;
 if (app.info.primaryCpuAbi != null) {
 instructionSet = VMRuntime.getInstructionSet(requiredAbi);
 }

 app.setGids(gids);
 app.setRequiredAbi(requiredAbi);
 app.setInstructionSet(instructionSet); //把信息都设置到ProcessRecord中
 final String seInfo = app.info.seInfo
 + (TextUtils.isEmpty(app.info.seInfoUser) ? "" : app.info.seInfoUser);
 final String entryPoint = "android.app.ActivityThread"; //设置进程入口位ActivityThread

 return startProcessLocked(hostingRecord, entryPoint, app, uid, gids,
 runtimeFlags, zygotePolicyFlags, mountExternal, seInfo, requiredAbi,
 instructionSet, invokeWith, startUptime, startElapsedTime);

}

以上代码主要是检查应用的各种权限,对其设置对应权限组的groupId,以及设置应用的Abi等信息。之后又会启动一个新的startProcessLocked方法,其中仍然是给ProcessRecord设置参数,其中很大篇幅的为设置debug和profilable相关的参数设置,这里就不列出参数设置的代码了,只列以下最后启动调用的代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
if (mService.mConstants.FLAG_PROCESS_START_ASYNC) {
 mService.mProcStartHandler.post(() -> handleProcessStart(
 app, entryPoint, gids, runtimeFlags, zygotePolicyFlags, mountExternal,
 requiredAbi, instructionSet, invokeWith, startSeq));
 return true;
} else {
 final Process.ProcessStartResult startResult = startProcess(hostingRecord,
 entryPoint, app,
 uid, gids, runtimeFlags, zygotePolicyFlags, mountExternal, seInfo,
 requiredAbi, instructionSet, invokeWith, startUptime);
 handleProcessStartedLocked(app, startResult.pid, startResult.usingWrapper,
 startSeq, false);
 return app.getPid() > 0;
}

这里有两个分支,这个 FLAG_PROCESS_START_ASYNC 默认为True,是通过系统的Setting去设置的。第一个分支是通过Handle把任务抛出去执行,而直接返回了执行成功,另一个分支则是等待任务执行完成,在根据返回的UID检查是否成功。不过两个分支里面都是执行了startProcess方法,在这个方法中我们关注以下代码:

 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
if (hostingRecord.usesWebviewZygote()) { //webview进程的创建
 startResult = startWebView(entryPoint,
 app.processName, uid, uid, gids, runtimeFlags, mountExternal,
 app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
 app.info.dataDir, null, app.info.packageName,
 app.getDisabledCompatChanges(),
 new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});
} else if (hostingRecord.usesAppZygote()) {
 final AppZygote appZygote = createAppZygoteForProcessIfNeeded(app);

 startResult = appZygote.getProcess().start(entryPoint,
 app.processName, uid, uid, gids, runtimeFlags, mountExternal,
 app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
 app.info.dataDir, null, app.info.packageName,
 /*zygotePolicyFlags=*/ ZYGOTE_POLICY_FLAG_EMPTY, isTopApp,
 app.getDisabledCompatChanges(), pkgDataInfoMap, allowlistedAppDataInfoMap,
 false, false,
 new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});
} else {
 regularZygote = true;
 startResult = Process.start(entryPoint,
 app.processName, uid, uid, gids, runtimeFlags, mountExternal,
 app.info.targetSdkVersion, seInfo, requiredAbi, instructionSet,
 app.info.dataDir, invokeWith, app.info.packageName, zygotePolicyFlags,
 isTopApp, app.getDisabledCompatChanges(), pkgDataInfoMap,
 allowlistedAppDataInfoMap, bindMountAppsData, bindMountAppStorageDirs,
 new String[]{PROC_START_SEQ_IDENT + app.getStartSeq()});
}

以上可以看到我们在创建新的进程的时候,会有三个分支,我们回看我们创建HostingRecord 时候是调用的如下的构造方法:

1
2
3
4
5
public HostingRecord(@NonNull String hostingType, ComponentName hostingName, boolean isTopApp) {
 this(hostingType, hostingName.toShortString(), REGULAR_ZYGOTE,
 null /* definingPackageName */, -1 /* mDefiningUid */, isTopApp /* isTopApp */,
 null /* definingProcessName */, null /* action */, TRIGGER_TYPE_UNKNOWN);
}

Process启动进程调用

因此上面的代码是走到了regular分支,它调用了Processstart方法, Process中又调用了ZYGOTE_PROCESSstart方法, ZYGOTE_PROCESS为一个ZygoteProcess常量,其中又会调用startViaZygoate方法,我们来看看这个方法的代码:

 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
private Process.ProcessStartResult startViaZygote(@NonNull final String processClass, @Nullable final String niceName, final int uid, final int gid, @Nullable final int[] gids,
 int runtimeFlags, int mountExternal,
 int targetSdkVersion,
 @Nullable String seInfo,
 @NonNull String abi,
 @Nullable String instructionSet,
 @Nullable String appDataDir,
 @Nullable String invokeWith,
 boolean startChildZygote,
 @Nullable String packageName,
 int zygotePolicyFlags,
 boolean isTopApp,
 @Nullable long[] disabledCompatChanges,
 @Nullable Map<String, Pair<String, Long>> pkgDataInfoMap,
 @Nullable Map<String, Pair<String, Long>> allowlistedDataInfoList,
 boolean bindMountAppsData,
 boolean bindMountAppStorageDirs,
 @Nullable String[] extraArgs) throws ZygoteStartFailedEx {
 ArrayList<String> argsForZygote = new ArrayList<>();
 argsForZygote.add("--runtime-args");
 argsForZygote.add("--setuid=" + uid);
 argsForZygote.add("--setgid=" + gid);
 argsForZygote.add("--runtime-flags=" + runtimeFlags);
 ....
 synchronized(mLock) {
 return zygoteSendArgsAndGetResult(openZygoteSocketIfNeeded(abi),
 zygotePolicyFlags,
 argsForZygote);
}

上面的代码就是把我们之前所有的各种参数,都拼接起来放到一个字符数组中,后面的openZygoteSocketIfNeeded则是根据abi来于zygote进程建立socket连接,其他的我就要进入zygoteSendArgsAndGetResult方法中查看详情了。

1
2
3
4
5
if (shouldAttemptUsapLaunch(zygotePolicyFlags, args)) {
 return attemptUsapSendArgsAndGetResult(zygoteState, msgStr);
}

return attemptZygoteSendArgsAndGetResult(zygoteState, msgStr);

这里有一个判断是否要使用usap进程池(非专门app使用进程池),不过我看了这里mUsapPoolEnabled字段默认为false,那我们就不看这个分支了。而attemptZygoteSendArgsAndGetResult代码如下:

 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
private Process.ProcessStartResult attemptZygoteSendArgsAndGetResult(
 ZygoteState zygoteState, String msgStr) throws ZygoteStartFailedEx {
 try {
 final BufferedWriter zygoteWriter = zygoteState.mZygoteOutputWriter;
 final DataInputStream zygoteInputStream = zygoteState.mZygoteInputStream;

 zygoteWriter.write(msgStr);
 zygoteWriter.flush();

 Process.ProcessStartResult result = new Process.ProcessStartResult();
 result.pid = zygoteInputStream.readInt();
 result.usingWrapper = zygoteInputStream.readBoolean();

 if (result.pid < 0) {
 throw new ZygoteStartFailedEx("fork() failed");
 }

 return result;
 } catch (IOException ex) {
 zygoteState.close();
 Log.e(LOG_TAG, "IO Exception while communicating with Zygote - "
 + ex.toString());
 throw new ZygoteStartFailedEx(ex);
 }
}

从上面的代码我们可以看到,这里其实很简单,就是通过socket向Zytgote发送了我们启动进程需要的参数,然后再通过socket从Zygote读出创建的进程的pid。

Zygote进程创建子进程

这个时候我们需要来看ZygoteInit的main方法,具体zygote进程是如何在系统启动的时候创建的就不去关注了,这里来关注zygote进程如何去创建应用进程的,这里摘抄了一些它的main函数的代码:

1
2
3
4
5
6
7
preload(bootTimingsTraceLog); //zygote启动之后,预加载代码资源等
zygoteServer = new ZygoteServer(isPrimaryZygote); //创建Zygote 的socket server
caller = zygoteServer.runSelectLoop(abiList); // socket server进入监听状态

if (caller != null) {
 caller.run(); //子进程中的时候caller不为空,会执行,此处会执行我们的ActivityThread的main方法,先分析上面的runSelectLoop,其中会有caller的创建
}

runSelectLoop内我们比较关注的代码如下:

 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
Runnable runSelectLoop(String abiList) {
 while (true) {
 pollReturnValue = Os.poll(pollFDs, pollTimeoutMs);
 if (pollReturnValue == 0) {
 ...
 } else {
 while (--pollIndex >= 0) {
 if (pollIndex == 0) {
 //如果pollIndex为0,则说明没有socket连接,需要创建socket连接
 ZygoteConnection newPeer = acceptCommandPeer(abiList);
 peers.add(newPeer);
 socketFDs.add(newPeer.getFileDescriptor());
 } else if (pollIndex < usapPoolEventFDIndex) { //读取Primary socket
 ZygoteConnection connection = peers.get(pollIndex);
 boolean multipleForksOK = !isUsapPoolEnabled() && ZygoteHooks.isIndefiniteThreadSuspensionSafe();
 final Runnable command = connection.processCommand(this, multipleForksOK);
 if (mIsForkChild) {
 return command; //子进程,返回command
 } else {
 //父进程的一些处理
 }
 ...
 }
 ....
 }
 ...
 }

 }
}

上面的代码省略了一些如果是Usap进程的代码,代码里面有两层的循环,在内层循环中,以pollIndex作为循环的条件,如果pollIndex为0,在acceptCommandPeer中会建立新的Socket Connet,代码里面就是一个ZygoteConnection。如果存在Connect的情况下会,会通过判断当前pollIndex是否小于usapPollEventFDIndex来判断是否是普通的进程创建,之后会调用connection.processCommand来读取socket数据做后续的处理,代码如下:

 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
ZygoteArguments parsedArgs;
try (ZygoteCommandBuffer argBuffer = new ZygoteCommandBuffer(mSocket)) {
 while (true) {
 parsedArgs = ZygoteArguments.getInstance(argBuffer);
 ...
 if (parsedArgs.mInvokeWith != null || parsedArgs.mStartChildZygote
 || !multipleOK || peer.getUid() != Process.SYSTEM_UID) {
 pid = Zygote.forkAndSpecialize(parsedArgs.mUid, parsedArgs.mGid,
 parsedArgs.mGids, parsedArgs.mRuntimeFlags, rlimits,
 parsedArgs.mMountExternal, parsedArgs.mSeInfo, parsedArgs.mNiceName,
 fdsToClose, fdsToIgnore, parsedArgs.mStartChildZygote,
 parsedArgs.mInstructionSet, parsedArgs.mAppDataDir,
 parsedArgs.mIsTopApp, parsedArgs.mPkgDataInfoList,
 parsedArgs.mAllowlistedDataInfoList, parsedArgs.mBindMountAppDataDirs,
 parsedArgs.mBindMountAppStorageDirs); //fork子进程的操作

 try {
 if (pid == 0) { //子进程处理分支
 zygoteServer.setForkChild();

 zygoteServer.closeServerSocket();
 IoUtils.closeQuietly(serverPipeFd);
 serverPipeFd = null;

 return handleChildProc(parsedArgs, childPipeFd,
 parsedArgs.mStartChildZygote);
 } else { //父进程处理分支
 IoUtils.closeQuietly(childPipeFd);
 childPipeFd = null;
 handleParentProc(pid, serverPipeFd);
 return null;
 }
 } finally {
 IoUtils.closeQuietly(childPipeFd);
 IoUtils.closeQuietly(serverPipeFd);
 }
 } else {
 ...
 }
 ...
 }
}

上面代码是处理socket数据的代码,我这里省略了除了创建进程之外的处理其他操作的代码。其中我们可以看到系统是使用了ZygoteArguments来解析我们之前从system_server进程传过来的参数,之后调用Zygote.forkAndSpecialize来创建进程,在linux中,fork完进程之后,是通过pid来判断当前是在父进程还是子进程中的,当前为子进程则pid为0。forkAndSpecialize方法中主要是调用了nativeForkAndSpecialize,这个是native方法,代码在com_android_internal_os_Zygote.cpp中,在native中的方法为com_android_internal_os_Zygote_nativeForkAndSpecialize我们去看看它的代码。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
...
pid_t pid = zygote::ForkCommon(env, /* is_system_server= */ false, fds_to_close, fds_to_ignore, true);
if (pid == 0) {
 SpecializeCommon(env, uid, gid, gids, runtime_flags, rlimits, capabilities, capabilities,
 mount_external, se_info, nice_name, false, is_child_zygote == JNI_TRUE,
 instruction_set, app_data_dir, is_top_app == JNI_TRUE, pkg_data_info_list,
 allowlisted_data_info_list, mount_data_dirs == JNI_TRUE,
 mount_storage_dirs == JNI_TRUE);
}
return pid;

上面的代码可以看到,第一行是去fork子进程,后面会判断是否为子进程,如果为子进程则会为子进程做一些处理。其中我省略了前面一部分fds_to_close 和fds_to_ignore赋值的代码,那些为需要关闭或者忽略的文件描述符,会传到这个ForkCommon方法中,我们具体看看这个方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
SetSignalHandlers(); //设置错误信号监听
BlockSignal(SIGCHLD, fail_fn); //暂时关闭SIGCHLD信号,方便后面关闭fd
__android_log_close(); //关闭log相关的FD
AStatsSocket_close();
...
pid_t pid = fork(); //调用系统调用执行fork进程

if (pid == 0) {
 ...
 PreApplicationInit(); //子进程的初始化,主要是设置当前进程不是zygote进程
 DetachDescriptors(env, fds_to_close, fail_fn); //把传进来的要关闭的fd关掉
 ...
} else {
 ...
}

UnblockSignal(SIGCHLD, fail_fn); //重新打开之前关闭的SIGCHLD信号
return pid;

可以看到上面的代码主要是去调用fork系统调用去从zygote进程fork一个新进程作为应用使用的进程,而SpecializeCommon,我们根据传入的参数和代码可以知道,其中主要是设置子进程的用户组,以及挂载应用目录,一些其他相关的初始化,就不分析其代码了。然后我们就可以继续会到java代码。

创建完子进程后的操作

在前面processCommand方法中,我们知道fork成功之后如果是子进程会执行handleChildProc方法,如果是父进程会执行handleParentProc方法,先来看一下父进程执行的代码:

1
2
3
4
5
6
if (pid > 0) {
 setChildPgid(pid);
}
...
mSocketOutStream.writeInt(pid);
mSocketOutStream.writeBoolean(usingWrapper);

这个方法中我们需要关注的就上面这一部分代码,首先是把这个子进程的pid放到进程的当前进程的孩子进程组中去。后面的就是把子进程的pid和是否使用了wrapper写入到socket中,这样我们之前请求创建进程那个地方就能拿到子进程的id了。

再来看子进程所执行的handleChildProc方法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
closeSocket();
Zygote.setAppProcessName(parsedArgs, TAG);

if (parsedArgs.mInvokeWith != null) {
 WrapperInit.execApplication(parsedArgs.mInvokeWith,
 parsedArgs.mNiceName, parsedArgs.mTargetSdkVersion,
 VMRuntime.getCurrentInstructionSet(),
 pipeFd, parsedArgs.mRemainingArgs);

 // Should not get here. 
 throw new IllegalStateException("WrapperInit.execApplication unexpectedly returned");
} else {
 if (!isZygote) {
 return ZygoteInit.zygoteInit(parsedArgs.mTargetSdkVersion,
 parsedArgs.mDisabledCompatChanges,
 parsedArgs.mRemainingArgs, null /* classLoader */);
 } else {
 return ZygoteInit.childZygoteInit(
 parsedArgs.mRemainingArgs /* classLoader */);
 }
}

首先第一行是关闭socket,前面的native代码其实已经关闭过了socket,但是在java层还是有LocalSocket,也需要关闭。 第二行就是给我们这个进程设置名称。 后面的第一个判断是看我们是否使用wrapper,正常流程不会走到这里,else分支中我们这里也不是fork一个新的zygote进程,因此也只需要看ZygoteInit.zygoteInit这个方法即可。

1
2
3
4
5
6
RuntimeInit.redirectLogStreams(); //关闭默认的log,设置使用android的print来输出system.out和system.error的log

RuntimeInit.commonInit();
ZygoteInit.nativeZygoteInit();
return RuntimeInit.applicationInit(targetSdkVersion, disabledCompatChanges, argv,
 classLoader);

App进程的初始化

RuntimeInit.commonInit中是一些初始化,包括错误处理,时区,网络的userAgent等,不看代码了。nativeZygoteInit的代码在AndroidRuntime.cpp

1
2
3
4
static void com_android_internal_os_ZygoteInit_nativeZygoteInit(JNIEnv* env, jobject clazz)
{
 gCurRuntime->onZygoteInit();
}

此处调用了gCurRuntimeonZygoteInit()方法,而这个方法是AndroidRuntime中的一个虚方法,在app_main.cpp中我们看到实际上对于应用我们有一个子类AppRuntime中实现了这个方法,代码如下:

1
2
3
4
5
6
virtual void onZygoteInit()
{
 sp<ProcessState> proc = ProcessState::self();
 ALOGV("App process: starting thread pool.\n");
 proc->startThreadPool();
}

我们之前分析binder的时候,知道ProcessState这个类binder是有使用的,调用self方法会打开binder驱动,这个代码里面是为binder创建应用进程的线程池,具体这里就不分析了。继续看RuntimeInit.applicationInit代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
protected static Runnable applicationInit(int targetSdkVersion, long[] disabledCompatChanges,
 String[] argv, ClassLoader classLoader) {
 nativeSetExitWithoutCleanup(true);

 VMRuntime.getRuntime().setTargetSdkVersion(targetSdkVersion);
 VMRuntime.getRuntime().setDisabledCompatChanges(disabledCompatChanges);

 final Arguments args = new Arguments(argv);

 return findStaticMain(args.startClass, args.startArgs, classLoader);
}

上面的方面,前面的代码主要是设置targetSdkversion和其他的一些设置,我们主要来看后面的findStaticMain方法:

 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
protected static Runnable findStaticMain(String className, String[] argv,
 ClassLoader classLoader) {
 Class<?> cl;

 try {
 cl = Class.forName(className, true, classLoader);
 } catch (ClassNotFoundException ex) {
 throw new RuntimeException(
 "Missing class when invoking static main " + className,
 ex);
 }

 Method m;
 try {
 m = cl.getMethod("main", new Class[] { String[].class });
 } catch (NoSuchMethodException ex) {
 throw new RuntimeException(
 "Missing static main on " + className, ex);
 } catch (SecurityException ex) {
 throw new RuntimeException(
 "Problem getting static main on " + className, ex);
 }

 int modifiers = m.getModifiers();
 if (! (Modifier.isStatic(modifiers) && Modifier.isPublic(modifiers))) {
 throw new RuntimeException(
 "Main method is not public and static on " + className);
 }
 return new MethodAndArgsCaller(m, argv);
}

可以看到我们通过反射拿到应用的之前设置的应用入口,也就是ActivityThread类,之后再获取到它的main方法,最后组装成一个MethodAndArgsCaller对象,最后返回。从前面的代码我们知道它会在ZygoteInitmain方法中执行。然后我们就可以来分析ActivityThread代码了。

ActivityThread代码执行

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public static void main(String[] args) {
 AndroidOs.install();
 Environment.initForCurrentUser();
 final File configDir = Environment.getUserConfigDirectory(UserHandle.myUserId());
 TrustedCertificateStore.setDefaultUserDirectory(configDir);
 initializeMainlineModules();

 Looper.prepareMainLooper();
 ...
 ActivityThread thread = new ActivityThread();
 thread.attach(false, startSeq);
 Looper.loop();
 throw new RuntimeException("Main thread loop unexpectedly exited");

}

上面的代码可以看到是为应用进程做一些初始化,首先是为sys call使用android的一些定制,其次是指定CA证书的位置,之后安装Mainline的模块,后面是初始化looper进入Looper循环,这样应用的主线程也就完成了初始化。在启动loop之前有一个attach方法,对于应用进程我们传进来的第一个参数为false, 也就是非系统进程,我们来看代码,只看应用进程的分支。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
RuntimeInit.setApplicationObject(mAppThread.asBinder());
final IActivityManager mgr = ActivityManager.getService();
try {
 mgr.attachApplication(mAppThread, startSeq);
} catch (RemoteException ex) {
 throw ex.rethrowFromSystemServer();
}

BinderInternal.addGcWatcher(new Runnable() {
 public void run() {
 //监听gc,当可用内存比较小的时候尝试回收一些Activity
 }
}

ViewRootImpl.ConfigChangedCallback configChangedCallback = (Configuration globalConfig) -> {
 //config 变化的回调,用来更新app得而configuration
}
ViewRootImpl.addConfigCallback(configChangedCallback);

上面的代码主要做了四件事情,其中两个是注册gc的回调和view configration 变化的回调,我们最关注的是调用ActivityManager的atttachApplication,和RuntimeInit的setApplicationObject,它们都用到了mAppThread,这个对象为ApplicationThread,而它是IApplicationThread.aidl的客户端实现。这里首先是把它的IBinder对象传到RuntimeInit中,这样发生一些事情的时候系统可以通知到应用。

回到AMS

另外我们再来看一下ActivityManager的attchApplication方法,它实际调用的是ActivityManagerServiceattachApplication方法,在它内部又调用了attachApplicationLocked方法,这里只看一下我们比较关心的一部分代码:

 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
boolean normalMode = mProcessesReady || isAllowedWhileBooting(app.info);
List<ProviderInfo> providers = normalMode
 ? mCpHelper.generateApplicationProvidersLocked(app)
 : null;
...
final ProviderInfoList providerList = ProviderInfoList.fromList(providers);
...
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 {
 didSomething = mAtmInternal.attachApplication(app.getWindowProcessController());
 } catch (Exception e) {
 Slog.wtf(TAG, "Exception thrown launching activities in " + app, e);
 badApp = true;
 }
}
if (!badApp) {
 try {
 didSomething |= mServices.attachApplicationLocked(app, processName);
 checkTime(startTime, "attachApplicationLocked: after mServices.attachApplicationLocked"); //检查是否有Service需要在当前进程启动
 } catch (Exception e) {
 Slog.wtf(TAG, "Exception thrown starting services in " + app, e);
 badApp = true;
 }
}

if (!badApp && isPendingBroadcastProcessLocked(pid)) {
 try {
 didSomething |= sendPendingBroadcastsLocked(app); //发送pending的广播
 checkTime(startTime, "attachApplicationLocked: after sendPendingBroadcastsLocked");
 } catch (Exception e) {
 // If the app died trying to launch the receiver we declare it 'bad' 
 Slog.wtf(TAG, "Exception thrown dispatching broadcasts in " + app, e);
 badApp = true;
 }
}

上面省略了一些代码,不过我们application需要做的一些核心代码都还在。除了列出的代码外,这里其实还有一些pending service启动,pending 广播的执行,以及ContentProvider的安装等,这些我们先略过。 首先这个normalMode的判断,我们假设当前已经是使用中而不是刚启动手机,而mProcessesReady是在system_server启动之后就赋值为true了,所以对于app启动的状况来说,这里normalMode为true。这里我们需要重点关注的就两个地方,一个是thread.bindApplication,另一处是mAtmInternal.attachApplication。bindApplication会通过binder调用到应用进程的bindApplication方法。

1
2
3
4
AppBindData data = new AppBindData();
data.processName = processName;
....
sendMessage(H.BIND_APPLICATION, data);

这里主要就去构建了AppBindData,使用ActivityThread内部的H来发送消息,消息回调处会调用ActivityThread的handleBindApplication方法。这个方法的代码非常多,前面的一些是设置包名,进程名称等等信息,以及configration信息以及调试器相关的东西,这些我们都不关注,这里跳过。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21

data.info = getPackageInfoNoCheck(data.appInfo, data.compatInfo, isSdkSandbox); //构建apk信息,创建LoadApk对象。

//创建AppContent,并且把confirmration绑定到Context上
final ContextImpl appContext = ContextImpl.createAppContext(this, data.info);
mConfigurationController.updateLocaleListFromAppContext(appContext);

//创建Instrumentation
mInstrumentation = new Instrumentation();
mInstrumentation.basicInit(this);

if (!data.restrictedBackupMode) { //执行安装ContentProvider
 if (!ArrayUtils.isEmpty(data.providers)) {
 installContentProviders(app, data.providers);
 }
}

//创建程序的Application
app = data.info.makeApplicationInner(data.restrictedBackupMode, null);
//调用Application的onCreate方法
mInstrumentation.callApplicationOnCreate(app);

上面的逻辑我们需要关注makeApplicationInner, 后面的callApplicationOnCreate内部就是调用Application的onCreate方法,不再分析了。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
final Application cached = sApplications.get(mPackageName);
if (cached != null) {
 if (!allowDuplicateInstances) {
 mApplication = cached;
 return cached;
 }
}
ContextImpl appContext = ContextImpl.createAppContext(mActivityThread, this);
app = mActivityThread.mInstrumentation.newApplication(
 cl, appClass, appContext);
appContext.setOuterContext(app);

上面的代码主要是从缓存里面取Application,如果没有则通过Instrumentaion去创建新的Applicaion,我们继续去看newApplication的代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public Application newApplication(ClassLoader cl, String className, Context context)
 throws InstantiationException, IllegalAccessException,
 ClassNotFoundException {
 String appClass = mApplicationInfo.getCustomApplicationClassNameForProcess(
 myProcessName);
 Application app = getFactory(context.getPackageName())
 .instantiateApplication(cl, className);
 app.attach(context);
 return app;
}

这里的代码比较简单,就是拿到AppComponentFactory然后通过反射创建App的Application对象,之后调用app的attach方法,attach方法内部会调用attachBaseContext方法。就不往里去看代码了。

对于需要启动Activity的情况,我们需要看ActivityManagerServiceattachApplication,我们需要再看一下mAtmInternal.attachApplication。它会调用ActivityTaskManagerService的内部类LocalService的方法,内部会调用mRootWindowContainer.attachApplication(wpc);,它的内部又会调用mAttachApplicationHelper.process(app),内部又会调用ensureActivitiesVisible方法,一路看进去最终会调用EnsureActivitiesVisibleHelperprocess方法,它的内部会调用setActivityVisibilityState

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
if (!r.attachedToProcess()) {
 makeVisibleAndRestartIfNeeded(mStarting, mConfigChanges, isTop,
 resumeTopActivity && isTop, r);
} else if (r.isVisibleRequested()) {
 // If this activity is already visible, then there is nothing to do here. 
 if (DEBUG_VISIBILITY) {
 Slog.v(TAG_VISIBILITY, "Skipping: already visible at " + r);
 }

 if (r.mClientVisibilityDeferred && mNotifyClients) {
 r.makeActiveIfNeeded(r.mClientVisibilityDeferred ? null : starting);
 r.mClientVisibilityDeferred = false;
 }

 r.handleAlreadyVisible();
 if (mNotifyClients) {
 r.makeActiveIfNeeded(mStarting);
 }
} else {
 r.makeVisibleIfNeeded(mStarting, mNotifyClients);
}

这些就是去执行启动Activity相关的逻辑,这里也先略过。

以下是AMS发起创建新进程的时序图:

sequenceDiagram
AMS->>+ProcessList: startProcessLocked
ProcessList->>ProcessList: newProcessRecordLocked
ProcessList->>ProcessList: startProcess
ProcessList->>+ZygoteProcess: startViaZygote
ZygoteProcess->>ZygoteProcess: openZygoteSocketIfNeeded
ZygoteProcess->>ZygoteProcess: zygoteSendArgsAndGetResult
ZygoteProcess->>ZygoteServer: send args and get Result
ZygoteProcess-->>-ProcessList: return result with pid
ProcessList-->>-AMS: return ProcessRecord

以下是Zygote侧处理fork进程请求的时序图:

sequenceDiagram
ZygoteInit->>ZygoteServer: runSelectLoop
loop 无限循环
ZygoteServer->>ZygoteConnection: processCommand
ZygoteConnection->>Zygote: forkAndSpecialize
ZygoteConnection-->>ZygoteConnection: (Parent): notify child pid by socket
ZygoteServer-->>ZygoteInit: return command(child process)
end
rect rgb(191, 223, 255)
note right of ZygoteInit: fork完子进程执行的内容
ZygoteConnection->>ZygoteInit: zygoteInit
ZygoteInit->>ZygoteInit: nativeZygoteInit
ZygoteInit->>AppRuntime: onZygoteInit
ZygoteInit->>RuntimeInit: findStaticMain
RuntimeInit-->>ZygoteInit: return ActivityThread Main function caller
ZygoteInit->>ActivityThread: main
ActivityThread->>AMS: attachApplication
AMS->>ActivityThread: bindApplication
end

以上就是应用进程启动的完整流程,为了使得流程更加简洁,其中不太重要的步骤有作省略。如果你也对于Android系统源码感兴趣,欢迎与我交流。博文因为个人局限,也难免会出现差错,欢迎大家指正。

看完评论一下吧

  •