月度归档: 2021 年 9 月

2021年9月中国采购经理指数运行情况

国家统计局服务业调查中心中国物流与采购联合会   一、中国制造业采购经理指数运行情况   9月份,中国制造业采购经理指数(PMI)为49.6%,低于上月0.5个百分点,降至临界点以下,制造业景气水平有所回落。   从企业规模看,大型企业PMI为50.4%,比上月微升0.1个百分点,继续高于临界点;中型企业PMI为49.7%,比上月下降1.5个百分点,低于临界点;小型企业PMI为47.5%,比上月下降0.7个百分点,低于临界点。   从分类指数看,在构成制造业PMI的5个分类指数中,生产指数、新订单指数、原材料库存指数、从业人员指数和供应商配送时间指数均低于临界点。   生产指数为49.5%,比上月下降1.4个百分点,低于临界点,表明制造业生产放缓。   新订单指数为49.3%,比上月下降0.3个百分点,低于临界点,表明制造业市场需求有所下降。   原材料库存指数为48.2%,虽比上月上升0.5个百分点,但仍低于临界点,表明制造业主要原材料库存量降幅收窄。   从业人员指数为49.0%,比上月下降0.6个百分点,低于临界点,表明制造业企业用工景气度有所下降。   供应商配送时间指数为48.1%,虽比上月回升0.1个百分点,但仍低于临界点,表明制造业原材料供应商交货时间继续延长。 表1 中国制造业PMI及构成指数(经季节调整)                                              单位:% PMI 生产新订单原材料库存从业人员供应商配送时间
2020年9月51.554.052.848.549.650.7
2020年10月51.453.952.848.049.350.6
2020年11月52.154.753.948.649.550.1
2020年12月51.954.253.648.649.649.9
2021年1月51.353.552.349.048.448.8
2021年2月50.651.951.547.748.147.9
2021年3月51.953.953.648.450.150.0
2021年4月51.152.252.048.349.648.7
2021年5月51.052.751.347.748.947.6
2021年6月50.951.951.548.049.247.9
2021年7月50.451.050.947.749.648.9
2021年8月50.150.949.647.749.648.0
2021年9月49.649.549.348.249.048.1 表2 中国制造业PMI其他相关指标情况(经季节调整)                                              单位:% 新出口订单进口采购量主要原材料购进价格出厂价格产成品库存在手订单生产经营活动预期
2020年9月50.850.453.658.552.548.446.158.7
2020年10月51.050.853.158.853.244.947.259.3
2020年11月51.550.953.762.656.545.746.760.1
2020年12月51.350.453.268.058.946.247.159.8
2021年1月50.249.852.067.157.249.047.357.9
2021年2月48.849.651.666.758.548.046.159.2
2021年3月51.251.153.169.459.846.746.658.5
2021年4月50.450.651.766.957.346.846.458.3
2021年5月48.350.951.972.860.646.545.958.2
2021年6月48.149.751.761.251.447.146.657.9
2021年7月47.749.450.862.953.847.646.157.8
2021年8月46.748.350.361.353.447.745.957.5
2021年9月46.246.849.763.556.447.245.656.4   二、中国非制造业采购经理指数运行情况   9月份,非制造业商务活动指数为53.2%,高于上月5.7个百分点,升至临界点以上,非制造业景气水平快速回升。   分行业看,建筑业商务活动指数为57.5%,低于上月3.0个百分点。服务业商务活动指数为52.4%,高于上月7.2个百分点。从行业情况看,铁路运输、航空运输、住宿、电信广播电视及卫星传输服务等行业商务活动指数位于60.0%以上高位景气区间;资本市场服务、房地产等行业商务活动指数低于临界点。   新订单指数为49.0%,比上月回升6.8个百分点,低于临界点,表明非制造业市场需求下降幅度收窄。分行业看,建筑业新订单指数为49.3%,比上月下降2.1个百分点;服务业新订单指数为49.0%,比上月上升8.5个百分点。   投入品价格指数为53.5%,比上月上升2.2个百分点,高于临界点,表明非制造业企业用于经营活动的投入品价格总体水平继续上升。分行业看,建筑业投入品价格指数为62.8%,比上月上升7.5个百分点;服务业投入品价格指数为51.8%,比上月上升1.3个百分点。   销售价格指数为50.5%,比上月上升1.2个百分点,高于临界点,表明非制造业销售价格总体水平有所上涨。分行业看,建筑业销售价格指数为55.8%,比上月上升2.2个百分点;服务业销售价格指数为49.6%,比上月回升1.1个百分点。   从业人员指数为47.8%,高于上月0.8个百分点,表明非制造业企业用工景气度有所改善。分行业看,建筑业从业人员指数为52.6%,比上月上升2.0个百分点;服务业从业人员指数为46.9%,比上月回升0.5个百分点。   业务活动预期指数为59.1%,比上月上升1.7个百分点,位于较高景气区间,表明非制造业企业对近期市场发展信心较强。分行业看,建筑业业务活动预期指数为60.1%,比上月上升1.7个百分点;服务业业务活动预期指数为58.9%,比上月上升1.6个百分点。 表3 中国非制造业主要分类指数(经季节调整)                                             单位:%  商务活动新订单投入品价格销售价格从业人员业务活动预期
2020年9月55.954.050.648.949.163.0
2020年10月56.253.050.949.449.462.9
2020年11月56.452.852.751.048.961.2
2020年12月55.751.954.352.348.760.6
2021年1月52.448.754.551.447.855.1
2021年2月51.448.954.750.148.464.0
2021年3月56.355.956.252.249.763.7
2021年4月54.951.554.951.248.763.0
2021年5月55.252.257.752.848.962.9
2021年6月53.549.653.451.448.060.8
2021年7月53.349.753.551.348.260.7
2021年8月47.542.251.349.347.057.4
2021年9月53.249.053.550.547.859.1 表4 中国非制造业其他分类指数(经季节调整)                                              单位:% 新出口订单在手订单存货供应商配送时间
2020年9月49.146.348.552.2
2020年10月47.044.948.752.3
2020年11月49.045.248.851.8
2020年12月47.544.747.051.2
2021年1月48.044.047.449.8
2021年2月45.744.045.949.8
2021年3月50.345.948.251.8
2021年4月48.145.847.250.9
2021年5月47.644.747.250.8
2021年6月45.443.847.051.0
2021年7月47.744.847.351.3
2021年8月43.942.945.949.2
2021年9月46.444.245.950.4   三、中国综合PMI产出指数运行情况   9月份,综合PMI产出指数为51.7%,比上月上升2.8个百分点,表明我国企业生产经营活动总体较上月有所加快。   附注   1.主要指标解释   采购经理指数(PMI),是通过对企业采购经理的月度调查结果统计汇总、编制而成的指数,它涵盖了企业采购、生产、流通等各个环节,包括制造业和非制造业领域,是国际上通用的监测宏观经济走势的先行性指数之一,具有较强的预测、预警作用。综合PMI产出指数是PMI指标体系中反映当期全行业(制造业和非制造业)产出变化情况的综合指数。PMI高于50%时,反映经济总体较上月扩张;低于50%,则反映经济总体较上月收缩。   2.调查范围   涉及《国民经济行业分类》(GB/T4754-2017)中制造业的31个行业大类,3000家调查样本;非制造业的43个行业大类,4200家调查样本。   3.调查方法   采购经理调查采用PPS(Probability Proportional to Size)抽样方法,以制造业或非制造业行业大类为层,行业样本量按其增加值占全部制造业或非制造业增加值的比重分配,层内样本使用与企业主营业务收入成比例的概率抽取。   本调查由国家统计局直属调查队具体组织实施,利用国家统计联网直报系统对企业采购经理进行月度问卷调查。   4.计算方法   (1)分类指数的计算方法。制造业采购经理调查指标体系包括生产、新订单、新出口订单、在手订单、产成品库存、采购量、进口、主要原材料购进价格、出厂价格、原材料库存、从业人员、供应商配送时间、生产经营活动预期等13个分类指数。非制造业采购经理调查指标体系包括商务活动、新订单、新出口订单、在手订单、存货、投入品价格、销售价格、从业人员、供应商配送时间、业务活动预期等10个分类指数。分类指数采用扩散指数计算方法,即正向回答的企业个数百分比加上回答不变的百分比的一半。由于非制造业没有合成指数,国际上通常用商务活动指数反映非制造业经济发展的总体变化情况。   (2)制造业PMI指数的计算方法。制造业PMI是由5个扩散指数(分类指数)加权计算而成。5个分类指数及其权数是依据其对经济的先行影响程度确定的。具体包括:新订单指数,权数为30%;生产指数,权数为25%;从业人员指数,权数为20%;供应商配送时间指数,权数为15%;原材料库存指数,权数为10%。其中,供应商配送时间指数为逆指数,在合成制造业PMI指数时进行反向运算。   (3)综合PMI产出指数的计算方法。综合PMI产出指数由制造业生产指数与非制造业商务活动指数加权求和而成,权数分别为制造业和非制造业占GDP的比重。   5.季节调整   采购经理调查是一项月度调查,受季节因素影响,数据波动较大。现发布的指数均为季节调整后的数据。

请教一个自建 bitwarden 的问题

系统是 debian 10 , 跑着一个 nginx,按 bitwarden 这个教程 https://bitwarden.com/help/article/install-on-premise/#install-bitwarden下脚本,傻瓜式安装,感觉挺简单,还选了用 certbot 免费生成证书,一启动我傻了? ,端口被占用了,我在 config.yml 里改了端口,重新 rebuild,nginx 配置反代好像都不行,

docker: Error response from daemon: driver failed programming external connectivity on endpoint certbot (): Error starting userland proxy: listen tcp4 0.0.0.0:443: bind: address already in use.

bitwarden error 0.0.0.0 debian12 条回复 • 2021-09-06 18:47:34 +08:00
quzard 1
quzard 37 天前 via Android
我用的 docker 版本的 bitwarden,感觉还行,用 nginx 反代容器端口
gromit1337 2
gromit1337 37 天前
@quzard #1 观察了一下这个脚本,跑了之后也是拉的一些 docker 镜像
![]( https://tva1.sinaimg.cn/large/008i3skNgy1gtr6g75leqj60gi0830tk02.jpg)
arischow 3
arischow 37 天前 via iPhone
我用的 bitwarden_rs + traefik,基于 Docker,跑了几个月了。
18x 4
18x 37 天前 via Android
还是用第三方的 Bitwarden_rs 吧 不需要再开会员
thet 5
thet 37 天前 via iPhone
https://github.com/dani-garcia/vaultwarden 用这个
ruxuan1306 6
ruxuan1306 37 天前
docker-compose.yml

“`
version: “3”

services:

bitwarden:
image: bitwardenrs/server
restart: always
ports:
– 80:80
– 3012:3012
volumes:
– ./bwdata:/data
environment:
– WEBSOCKET_ENABLED=true
# – SIGNUPS_ALLOWED=false
– ADMIN_TOKEN=token
“`
ruxuan1306 7
ruxuan1306 37 天前
喔看起来现在改名叫 vaultwarden 了,那上面的镜像换成 vaultwarden 的吧
wd 8
wd 37 天前 via iPhone
楼主如果你不懂建议还是直接考虑用大公司的吧,别回头即使跑起来了但是弄个泄露什么的,你就惨了。这个可不是闹着玩的。
omegatheta 9
omegatheta 37 天前 via Android
certbot 理论上用 80 或者 443 都行的,官方脚本好像要求同时监听。这个错误信息是尝试监听 443 失败。
可以看一下哪个进程占着 443,或者手动生成证书。
XiLingHost 10
XiLingHost 37 天前
不如试试 acme.sh?
AllenHua 11
AllenHua 37 天前
指路: https://hub.docker.com/r/vaultwarden/server ( https://hub.docker.com/r/bitwardenrs/server is deprecated)
楼主的问题可能是本地已经有 nginx listen 443 了吧。建议把安装服务和部署 ssl 分开做。

nginx 配置反代不就是一两行 nginx 配置的事情。可以参考我写的文章: https://hellodk.cn/post/586

另外 certbot,分享一个*佳实践,可以部署 certbot-auto 服务。
certbot-auto 部署*佳实践 Certbot-auto deployment best practices https://community.letsencrypt.org/t/certbot-auto-deployment-best-practices/91979
Aria2Hank 12
Aria2Hank 23 天前
我用的 bitwarden_rs + nginx 配置二级域名

想组装一台家用服务器,用于跑 5 到 6 个虚拟机,求推荐

想组装个家用的小型服务器,稳定性要求不是太高,主要目的是做下列用途

1.大概要安装 5 虚拟机在这台服务器上

2.每个虚拟机都会安装数据库和 web 程序

3.做个小型的共享盘

4.不玩游戏,对显卡没有要求

求推荐这样一台服务器组装清单列表。

第 1 条附言 · 24 天前
感谢各位的热心回答。按照我这个需求,一开始我考虑组装洋垃圾的,类似这种配置:e5-2680v4,x99
确实便宜啊,价格是真香,性能也够牛逼。
但是考虑到自己是家用,个人对噪音和功耗比较在意,还有我也怕用不久容易翻车

所以我有综合了自己的使用需求,做了些调整,因为考虑到家用,不想太大占地方(房子买小了),所以选择了 ITX 机箱
这是我调整后的配置

CPU:酷睿八核(散) I7 10700
散热:长城 CPU 风扇 小钢炮 X500 ( 5177725 )
主板:华擎主板 Intel B560M-ITX/ac
显卡:无
固态:金士顿 SSD A2000 500G ( M.2 NVME )
内存:金士顿 PC 骇客 DDR4 3200 32G (单条)
电源:先马电源 金钻 600M 金牌 SFX 额定 600W
机箱:玩嘉机箱 铝行者 929 全侧透 plus 白色 ITX

等配件快递到了,就开始组装。组装好后,看情况要不要给大家展示组装效果
服务器 组装 虚拟机 家用23 条回复 • 2021-09-15 16:40:41 +08:00
iBugOne 1
iBugOne 28 天前 via Android ❤️ 1
按一般配置装个主机就行了,内存看需求起步 32 GB,*好 64+,然后安装 Proxmox VE 系统

数据库和 web 程序一般都不会很吃内存,只要你不装桌面环境,1 GB 内存都能跑(参考各种云服务器)
alexkkaa 2
alexkkaa 28 天前 via Android
这需要什么要求吗 随便一个台式机都可以啊
vonsy 3
vonsy 28 天前
hpe gen 10 plus, 内存加到 64G, 安装 ESXi
ymmud 4
ymmud 28 天前
itx 主板装个 nas 就行
pixiaotiao 5
pixiaotiao 28 天前 via Android
内存大点就成
stevengoogo 6
stevengoogo 28 天前
@vonsy hpe gen 10 plus 确实是一个不错的选择。自己组装洋垃圾会不会更便宜点
locoz 7
locoz 28 天前
要新设备的话照着 AMD 现在这一代的 CPU 配就行了,搞洋垃圾的话就淘宝直接搜 intel e5,然后会有很多那种不同型号的 CPU 给你列在一个商品里的,你直接看着列表选主频和核心数就行了。
bnm965321 8
bnm965321 28 天前
有什么玩法推荐一下
echo1937 9
echo1937 28 天前
@stevengoogo #6 我自己有差不多的需求。

我是 B550 + 1800X + 64G,虚拟化方案为 Hyper-V,优点和限制是:
1 、*高支持内存 64G ( 16×4 ),也支持 32×4,但是 32G 单条太贵了
2 、CPU 可以从 ZEN1 一直支持到 ZEN3
3 、Hyper-V 支持动态内存(可以在*低内存和*高内存之间自由浮动)
4 、Windows 10 依然可以日常使用,不需要再搞一台机器

E5 的优势和缺点是:
1 、首次拥有成本很低,但是这台机器只能干这个
2 、能耗高、噪音大
3 、可以使用 ESXi
4 、内存可以比较方便扩展到大于 64G,价格低廉
5 、可以装很多扩展卡

总体而言是,如果你有日常台式机的需求,建议选择 1,如果没有选择 E5 也不错。
aptupdate 10
aptupdate 28 天前
我前段时间买了 4×2.5G 网口的 J4125,PVE 系统长期跑 5 个服务,docker 和能单独运行的服务都是用 LXC 里的 alpine 模板,然后 1T 的机械盘当做共享。

用下来将近两个月,对稳定性还有发热和功耗都挺满意的,已经很久没动过了。就是现在 8G 内存(iMac 拿下来的)有点少。昨天看新闻说这季度内存会降价,关注看看如果价格合适就上 16+16 不再担心内存。

建议家用的还是要考虑体积和功耗还有噪音问题,毕竟要长期开着。
Juicpt 11
Juicpt 28 天前
emmmm 捡垃圾 买个二手服务器?dell poweredge 系列的淘宝一堆二手…价格低能租的配置足够高…拿回来装虚拟化美滋滋
cpstar 12
cpstar 28 天前
不知道啥应用,但是感觉容器虚拟化能解决不少问题,不需要开虚机
TinyKube 13
TinyKube 28 天前 via iPhone
@stevengoogo 你的需求 gen10plus 全符合,可以选低配升级 cpu,也可以高配图个省心
imydou 14
imydou 28 天前
@stevengoogo #6 洋垃圾考虑功耗和噪音
xppppsfg 15
xppppsfg 28 天前
一代锐龙吧,核数多,记得去年还是前年看 1700 8 核就只要五六百了
Senorsen 16
Senorsen 28 天前
根据用途 2 来看,更推荐开 k8s 而非虚拟机,提升性能,减少耗电和发热,服务编排也比虚拟机更方便和先进些(虽然是单机)
mingl0280 17
mingl0280 28 天前 via Android
超微 SC826 4U
换静音电源(该机箱支持两路 960/1000 多瓦的静音电源,有两个型号)
主板上超微的 H11SSL
CPU EPYC 二代能搞到*好,一代可能有带宽瓶颈。
128G 内存 DDR4 RECC
反正这种需求你上 ECC 是必须的。
wm5d8b 18
wm5d8b 28 天前 via Android
@Senorsen 跑容器有推荐的宿主机系统吗,带 Web 控制台直接页面管理容器
stevengoogo 19
stevengoogo 28 天前
@echo1937 感谢提供的方案,看起来很符合我现在的需求,可否方便告知你组装这台机器成本是多少钱
echo1937 20
echo1937 28 天前
@stevengoogo #19

B450 大概是 450-550,差别是供电和用料的豪华程度,
Ryzen 1800X 大概是 800-880 吧,
内存大概是 180 元 /8G

主要成本就是这个 3 大件.
pungaagin 21
pungaagin 27 天前 via iPhone
小黄鱼火神革命 d1581,32 个框框,价格惊喜?
locoz 22
locoz 27 天前
@Senorsen #16 确实,他这个完全没必要虚拟机,直接上 k8s 跑容器就行了,要虚拟机的时候再 kubevirt 开也一样。不过家用还是 k3s 舒服点,部署也简单,没必要的东西也阉割掉了。

@wm5d8b #18 宿主机直接 ubuntu 就行了,你要网页上管理容器的话可以用 rancher 或者 kubesphere,前者更偏向于完全管控、后者更偏向于简化操作,看自己取舍。
rosees 23
rosees 14 天前
明知的选择,E5 洋垃圾费电还不太稳定。
内存建议双通道吧。
狗屎顿的 A2000 好像没缓存,推荐铠侠 RC10

国外服务器求推荐,非爬墙

国内服务器,备案麻烦,加上有爬虫需求,需要访问国外站点,国内不支持。

不需要太高配置,性价比高,稳定就可以。

谢谢

服务器 国外 站点 备案17 条回复 • 2021-09-27 10:36:46 +08:00
leavic 1
leavic 31 天前
没有预算一律不推荐
davidyin 2
davidyin 31 天前 via Android
aws
beryl 3
beryl 31 天前
@davidyin 预算 50 以内,每月
beryl 4
beryl 31 天前
@leavic 预算 0-100 之间
yanzhiling2001 5
yanzhiling2001 31 天前
月付是五十刀还是五十人民币啊
pupboss 6
pupboss 31 天前
如果你要选境外服务器,尽量避免亚洲的尽管亚洲离得近,中国到亚洲各个国家速度都不咋地,甚至要去日本美国绕道。美西机房不错,到全球速度都可以,没什么好推荐的基本都是*低 5-10 刀一个月,自测速度还行就可以了
Kinnice 7
Kinnice 31 天前 via Android
vultr,do,aws
lostberryzz 8
lostberryzz 31 天前
三大金刚 vultr do linode
MonikaCeng 9
MonikaCeng 31 天前 via Android
甲骨文永久免费版
aru 10
aru 31 天前
腾讯云轻量
Xusually 11
Xusually 31 天前
稳定、性能:Linode 、DigitalOcean
akira 12
akira 31 天前
性价比,稳定,价格 三者只能满足其二
DO,linode 首选,其次是 谷歌 亚马逊 微软的咯
oldphper 13
oldphper 31 天前
/t/798274 腾讯云轻量,海外机房。
leavic 14
leavic 31 天前
vultr 就可以了,aws 吃码字还能更便宜。
leavic 15
leavic 31 天前
等一下,你这 0-100 是人民币还是美元,年付还是月付?年付人民币的话当我没说。
newsj 16
newsj 21 天前
推荐腾讯云轻量应用服务器(境外地域),也不用备案,包括中国香港、新加坡、硅谷地区。

要说性价比的话,现在轻量服务器性价比很高,而且比你预算更低。https://qcloud1.com/act/lighthouse2021
nanjingwuyanzu 17
nanjingwuyanzu 3 天前
国外良心云就有吧 https://dwz.async.net.cn/Q5DpP1

资源占用*低的情况下 VMware Workstation 卡顿要怎么排查原因?

公司服务器是 DL388G9 E5-2630v4*2 96GB Windows2008R2 VMware Workstation10.跑了 3 个 2008 的虚拟机,没有连接互联网,配置是 2CPU*4 核心 8GB 。虚拟机和宿主机资源占用都很低,CPU 占用率个位数,内存 30%,可是整个 Workstation 运行起来就很慢,切换虚拟机窗口、打开设置之类的都会卡上 10-30 秒,虚拟机桌面的帧率也很低。这种情况要怎么排查原因呢?
虚拟机 vmware 占用 CPU7 条回复 • 2021-09-15 16:36:43 +08:00
tianzi123 1
tianzi123 22 天前
什么硬盘??
vocaloidchina 2
vocaloidchina 21 天前 via iPhone
试试看设备管理器把 intel management engine interface (或者类似设备)取消选中允许关闭设备以节约能耗的选项如何
minami 3
minami 21 天前
没有安装 Tools ?
rexion 4
rexion 21 天前
@tianzi123 是机械硬盘倒是,不过不至于卡成这样吧。。
@minami 只有一个没装,感觉也没什么差异。
@vocaloidchina 没看到有类似的东西,是不是只有 PC 上有。
tianzi123 5
tianzi123 21 天前
@rexion 看下硬盘信息,是不是变黄了,坏道特别多了,迁移到固态试试,只能挨个排查,一般是硬盘的问题大点
rosees 6
rosees 14 天前
机械硬盘就是这么坑,怕还是个 SMR 的机械
rosees 7
rosees 14 天前
而且硬盘占用过高的话会导致数据向中心靠拢,速度更加慢,硬盘的盘片内测速度只有外侧的二分之一

有没有什么便宜的能运行 LOL 的云服务器吗?用来自动观战,求指点

预算每月 50 刀
用来 LoL 指点 观战6 条回复 • 2021-09-15 16:37:35 +08:00
Puteulanus 1
Puteulanus 22 天前
以前用 AWS 的显卡 EC2 串流打过 GTA,不需要长时间使用的话可以看看竞价机型,价格会比正常的实惠很多
cjpjxjx 2
cjpjxjx 22 天前
某宝搜挂机宝,650 显卡机器的机器一个月一百多
binux 3
binux 22 天前 via Android
gforce now + 个便宜的 VPS ?
pacexy1 4
pacexy1 22 天前
感谢楼上各位的回复,这些方案我都看了下,因为需要长期托管,也不需要太高的稳定性,感觉还是远程主机比较合适
fengci 5
fengci 15 天前
https://start.qq.com/index.html
rosees 6
rosees 14 天前
@fengci 这个东西不能用来挂机,主机掉线云端基本马上掉

推荐一款机器做家用服务器!

真是哭了,之前买了一个树莓派 4B 8GB 版,搭了个 gitlab 的 docker 就 tm 几乎满载了。

现在跪求朋友们推荐一款设备做家用服务器,主要是跑跑 docker 什么的,比如 gitlab 、Confluence 等。

Nas 什么的不是硬需求,主要是能跑几个 docker 什么的就行,性能好点的,以后还可能搭一些别的服务啥的。

树莓派真把我整哭了,,,,

价格在 5k 以下,*好是 nuc 这种成型的小主机,不想要自己组,麻烦还不稳定??

另外 nuc 做家用服务器怎么样,具体哪个配置比较合适。。

第 1 条附言 · 14 天前
谢谢大家?热情的帮助,选项已经够多了。哈哈哈,我再考虑考虑,感恩?
Docker NUC gitlab 家用96 条回复 • 2021-09-22 18:06:49 +08:00
vance123 1
vance123 18 天前 via Android ❤️ 2
二手笔记本,贼稳
lushan 2
lushan 18 天前
@vance123 关键是手头没有二手笔记本呀,这样的话是买个 nuc 这种 mini pc 、还是去买个二手笔记本性价比高啊,老哥
vance123 3
vance123 18 天前 via Android
二手笔记本货量大,应该会便宜得多
adrianzhang 4
adrianzhang 18 天前
@vance123 二手笔记本水太深,买来不稳定 lz 更要哭了。

@lushan 推荐占美主机吧,同样通途,我用了 5 年多很稳定,价格在 1000 和以下都有型号。ncu 性能也行,就是功耗比占美大,看你自己考虑吧。
cue 5
cue 18 天前 via Android
N1..
totoro625 6
totoro625 18 天前 via iPhone
nuc8i5 nuc11i3 nuc11i5 不含硬盘 /内存 1k7 1k7 2k3

J4125 系列:
① 宁美 /驰为 1099 ; CPU:J4125 ;重量:145g ;尺寸:62*62*42mm ;内存:6G ;固态:128G
② J4125 的软路由,不含硬盘 /内存 /WiFi,750-1050

Mac mini,售后好

另外,树莓派当服务器都 OK,其他设备只会更好,看你需求了
优先推荐 nuc11i5

不需要 nas 但是也要注意多备份数据
wizzer 7
wizzer 18 天前
ds920+ 不光能稳定跑 docker,还有更多功能不是?
fyibmsd 8
fyibmsd 18 天前 via Android
闲鱼上的刃 7000
lazyrm 9
lazyrm 18 天前 via Android
nuc 做服务器不怎么样,直接 ds920+
wanguorui123 10
wanguorui123 18 天前 via iPhone
为什么不多买几个二手派做集群?
leavic 11
leavic 18 天前
那就 nuc 啊,你自己不是知道答案了吗。
360511404 12
360511404 18 天前 via Android
5k 预算…这买什么都够了吧,我是用的四五台 4 代小主机各司其职,每个都是 4590T 内存 4g8g 不等,
ila 13
ila 18 天前 via Android
@totoro625 请问为什么是 nuc11i5
locoz 14
locoz 18 天前 via Android
说实在的,对于懂技术的人而言,不建议买成品小主机做服务器用,扩展性太差了…人类的欲望和程序的需求是无止尽的,就像你用树莓派之前够用、现在不够用一样,你买个成品小主机要不了多久可能也就不够用了。

5K 以内这个预算水平完全可以买个准系统或者二手品牌服务器(配件被拆到跟准系统一样程度的),组装、选配件之类的问题直接就解决了,不需要你操心。你需要做的只是简单选个 CPU 品牌、主板芯片组和性能水平(含是否可扩展多路 CPU ),买回来装个系统直接用就行,实际上买小主机你也会要选这些,没啥区别。

这么做的话你完全可以是两条 8G 或者一条 16G 内存外加个 SSD 起步,以后如果有啥需求直接加东西就完事,上限到上百 GB 内存、几十块盘、几张 PCI-E 卡都没问题,成品小主机基本没有同等空间和插槽余量,做不到同样效果的。

另外,买准系统的话刚开始是完全可以装在普通机箱里的,风扇也是可以选择自己买猫头鹰之类的高端静音风扇的,大可不必担心外观和噪音问题。只要主板别太老,支持通过 IPMI 之类的控制主板 PWM 调速就行。
cjpjxjx 15
cjpjxjx 18 天前 via iPhone
gen10 plus
jadec0der 16
jadec0der 18 天前
HP ProLiant Microserver Gen10
kekxv 17
kekxv 18 天前 via iPhone ❤️ 1
你把 gitlab 换成 gitea 就可以解决
lushan 18
lushan 18 天前
@locoz 哈喽,所以你说的是 Gen10 这种还是那种二手服务器呀,看起来比较不错
lushan 19
lushan 18 天前
@jadec0der
@cjpjxjx 我看 gen10 这 U 双核性能行不行啊,跑几个 docker 会不会垮
lushan 20
lushan 18 天前
@lazyrm
@wizzer 看起来不错,就是不能扩展,内存只能 8G
lushan 21
lushan 18 天前
@leavic 集思广益一下,看看有没有更优解
jadec0der 22
jadec0der 18 天前
@lushan 你跑什么 docker 应用?大部分应用的瓶颈不在 CPU 上,gen10 的 CPU 不算弱(至少跟群晖比)
lushan 23
lushan 18 天前
@totoro625 我之前看了款 nuc10i7 看起来不错
lushan 24
lushan 18 天前
@jadec0der 目前要跑 gitlab 、Confluence,但以后不知道要跑啥啊,所以性能还是要富裕一些,目前来看得两三倍的性能才能靠谱。
acrisliu 25
acrisliu 18 天前 via iPhone
目前在用软路由跑 docker
locoz 26
locoz 18 天前
@lushan #18 我指的是 4U (常规机箱)大小的那种服务器,不管是买准系统还是买二手的品牌服务器都挺多选择的,Gen10 的话其实还是属于扩展性不高的那种。
encro 27
encro 18 天前
自己组装了一台,体积不算很小,4500 左右,应该用 5800H 系列会比 G 系列体积更加小一点。

OS: Manjaro 21.1.2 Pahvo
Kernel: x86_64 Linux 5.10.61-1-MANJARO
Uptime: 4h 47m
Packages: 1469
Shell: zsh 5.8
Resolution: 3840×2160
DE: GNOME 40.0
WM: Mutter
WM Theme: Adwaita-maia-compact-dark
GTK Theme: Adwaita-maia-compact-dark [GTK2/3]
Icon Theme: Papirus-Dark-Maia
Font: Noto Sans 11
Disk: 59G / 473G (14%)
CPU: AMD Ryzen 7 PRO 4750G with Radeon Graphics @ 16x 3.6GHz
GPU: AMD RENOIR (DRM 3.40.0, 5.10.61-1-MANJARO, LLVM 12.0.1)
RAM: 8135MiB / 31553MiB
iceheart 28
iceheart 18 天前 via Android
gitlab 吃 cpu 吃内存,
个人比较推荐华硕的 pn51,
wangxn 29
wangxn 17 天前
我用的是一台二手的 ThinkPad x230,当初 1000 块买来的。i5 3230M 双核四线程,这个性能树莓派和 Atom 没法比。
笔记本自己有屏幕和键盘,万一出啥问题还是比较容易处理的。老电脑一般配备有线网口,网络也很稳定。
还带个电池,停电时也能撑一会。
czhu 30
czhu 17 天前
HP Microserver gen10 plus
把内存加满
PCI 口加一条 NVMe 的 SSD
系统 ESXi 可以虚拟出各种服务
除了贵 对你的需求没毛病 家用服务器爽歪歪
Puteulanus 31
Puteulanus 17 天前
AMD 的 NUC PN50 好像性能不错
gainsurier 32
gainsurier 17 天前
M1 Mac mini 好了
raptor 33
raptor 17 天前
联想,DELL,HP 的二手 MINI PC,几百块钱的准系统,加上内存硬盘也就一两千,可以干很多事情了
hello267015 34
hello267015 17 天前
Gen10 稳定运行 2 年了,够用,当然现在 Gen10 Plus 出来,貌似强很多
zmxnv123 35
zmxnv123 17 天前
二手 nuc8i5beh 配上一个 16g,大概 2000 左右。
稳成一匹马。
GPU 36
GPU 17 天前
我这几天计划组的。

CPU R5 3500x (本来打算用 2600x,后来发现这个没超线程更好)
微星 B450M 迫击炮
内存 8gx2
机箱 艾罗拉 M40
电源 全汉 MS450

弄这套主要需求是要上 4 个 m.2 ssd 和 万兆网卡。
而便宜的方案只有 AMD 是全系支持 pcie x16 拆分成 4×4 的。
不算硬盘和万兆 2K 内。
GPU 37
GPU 17 天前
NUC 系列推荐 NUC11TNH 型号的。

这个系列支持插一个扩展卡,官方卡基本买不到,不排除会有第三方的扩展卡出来(我自己就是想做一个万兆卡的)。

然后 vPro CPU 型号 (v5,v7) 的没必要买,个人用不上。
huaxing0211 38
huaxing0211 17 天前
我的 thinkpad x250 现在就是家庭服务器,主要用来 aira2 下载,然后共享文件,电视上播放
fiht 39
fiht 17 天前
借楼兜售出售一台 Dell T-620 工作站:¥5000
20C40T 64GB ecc 内存 + 3*2TB SAS 硬盘 + 710 阵列卡

开箱即用,不能再爽。
地点:北京市海淀区,可走德邦物流,物流费用 AA
fiht 40
fiht 17 天前
@fiht 一步到位,大家快来买啊~
littlewing 41
littlewing 17 天前
NUC11
HPE Microserver gen10 plus
xiaket 42
xiaket 17 天前
rpi4 上跑了一群服务包括 photoprism, gitea 等, 也还好吧. photoprism 导入照片的时候会长时间满载, 其余时间基本都是空载啊
cs8425 43
cs8425 17 天前
楼主 rpi4 会满载的根本原因就是 gitlab 太吃
换成 gitea 就没问题了…
非要 gitlab 不可
推荐直接组台桌机当服务器
cs8425 44
cs8425 17 天前
補 #43
自组桌机当家用服务器很多年了
一点也不麻烦
也没稳定性的问题
nuc 做家用服务器反而不好
之前用 NUC8i7BEH 一直过热降频
后面还干脆直接关机
一定要拆版清 CMOS 才能再开机…
Suaxi 45
Suaxi 17 天前
5000 预算可选择的太多了,可以上 8700t 或者洋垃圾
zhangfeiwudi 46
zhangfeiwudi 17 天前
我看淘宝有 家用 E5 服务器整机卖,内存 和 cpu 都能扩
cuebyte 47
cuebyte 17 天前
我的 DeskMini X300 + Ryzen 4650G 小超频一直很稳定,跑的 Debian Testing
herozzm 48
herozzm 17 天前
如果家用还包含了 nas,你需要 diy 多盘位的机箱
lazyrm 49
lazyrm 17 天前 via Android
@lushan 别闹 20g 直接可以上
Zepp 50
Zepp 17 天前
服务器主板、至强 CPU 、内存插满、10G 网卡。未来不需要担心升级了
feather12315 51
feather12315 17 天前 via Android
gen 10 性能都不行的话,各类 nas 机、低功耗小主机可以不用看了。
要么 nuc 类主机( 11 代 i5 、i7 ),要么华擎 x300+Ryzen zen2/zen3 5/7 系。

楼主想要性能,建议后者。

如果想多 3.5 盘位,只能自己用 itx 机箱组了。
klarkzh 52
klarkzh 17 天前 via iPhone
我是这样的
10105f 盒装 580
8G 内存 190
华擎 H470m 丐板 400
亮机卡 30
*后一套一千四百多,正常功耗 33w 。性能比公司发的 10210u+8G 笔记本强多了,我都 ssh 这个机器开发
huxiweng 53
huxiweng 17 天前
有台 2015 的 MacBook Pro 2699 考虑吗?
Jelly99 54
Jelly99 17 天前
5k 以下就别整小主机了吧,拆显卡的整机 3k 左右,扩展性比小主机好,再升升配置完全够用了
aLazarus 55
aLazarus 17 天前
我买的联想 520s mini,长得和机顶盒一样,塞进电视柜里了。缺点是风扇声有点大
m1nm13 56
m1nm13 17 天前
如果需要大硬盘的话,NUC 、笔记本之类的也可以滚了,说白了就只有自组台式机可以。选择很多,全部捡二手,3 代 4 代 IU 的,不包括硬盘一套不到 1K 。或者 ZEN1 ZEN+ ZEN2 的锐龙,因为 AMD 的原因,二手价格贼低,配上 B350 之类的板子,应该也能压到 1K 出头的价格。或者上全新的,十代 I5 板 U 套好像就 1K,其他也买新的的话,大概 2K 吧。
m1nm13 57
m1nm13 17 天前
@m1nm13
不需要大硬盘的话,可能二手笔记本吧。不过二手笔记本基本都是各种传家宝,1K 的预算,鬼知道能买到个什么样水平的。更多钱就没必要了
m1nm13 58
m1nm13 17 天前
@m1nm13
作为一个墙外论坛,要人手机认证,真的 NT 不怕查水表。打了半天,发不出来
m1nm13 59
m1nm13 17 天前
@m1nm13

至于 5K 的预算组,只用 GITLAB 的服务器, 您就是新时代的猪,不狠宰两刀真是对不起自己。别人 5K 陪主机,还带显卡,你 5K 不要显卡,按照现在显卡的价格,5K 的服务器,CPU 性能得和带卡 1W 的主机一样了吧。有必要吗?
m1nm13 60
m1nm13 17 天前
@m1nm13
前几天才卖了 3700X 加全新 B550 板 1K8,你这 5K 的主机,目测能上 5900X,前几天看到好像就 3K2 单 CPU 。还是那句话,你是要开公司吗?用着玩意?很多公司的服务器 CPU 拼编译未必打得过这玩意,毕竟服务器 U 主频低,多线程优化不好的比如 buildroot 啥的,就算有 128 核也是被这种暴打
fiht 61
fiht 17 天前
https://www.v2ex.com/t/801410#reply0

打个广告,兜售个二手塔式工作站,¥5000 抱回家,企业级产品,想怎么玩怎么玩。
webshe11 62
webshe11 17 天前 via Android
淘宝迷你工控主机 我买的 i5-8250U 版本,裸机 2000 块
ElmerZhang 63
ElmerZhang 17 天前
自己组台台式机就好了,这预算攒出来的性能*对够用。
如果偶尔要跑高负载任务的话(比如编译个 openwrt 之类的),不要选迷你主机或者小主机,散热不太好。
直接普通机箱,只做服务器时先随便买个几十块钱的亮机显卡,等显卡价格正常了再加块显卡,可玩性会比较高。
codesaler 64
codesaler 17 天前
nuc
Donahue 65
Donahue 17 天前
5k 预算组台台式机器完全够用了。我前几天 2800 多组了台,
3700x 1250
b450m pro s 299
32g 内存 590
1T pm981a 二手盘 400 (80%健康度)
1250w 矿龙电源 以前买的
撒哈拉机箱 二手 65

性能已经很顶了。 这么多 diy 玩家都是装机,不知道哪里来的不稳定这个说法~
sm0king 66
sm0king 17 天前
@fiht 感觉这是对楼主来说*划算的了。
march1993 67
march1993 17 天前
跑 gitea 啊。。。gitlab 起点太高了
dot 68
dot 17 天前 via Android
组过一台 HP Z230 工作站准系统,CPU 是 E3-1285V6,32G 内存,板载有个 NVME 装系统,还有额外的 SATA 口装了一块 2.5 和一块 3.5 的机械盘。
gBurnX 69
gBurnX 17 天前
如果去掉小主机的需求,可以这样配:

10 核 20 线程,128G D3 RECC 1866 内存:2309 元。
+ 512G M.2 NVMe * 1,2 * 3TB 机械硬盘:3148 元。
+ 1 个 23.5 存壁挂显示器,键鼠:3667 元。

不想自己组装,去电脑城加 50 元让小哥帮你装好。

详单:

主板:洋垃圾 X79 主板,ATX 大板,1CPU 插槽,4 内存插槽( D3 RECC 1333/1666/1866 max128G ),2PCI-E,1PCI-E,1M2-NVME,2*SATA3,4*SATA2,8*USB2 。435 元,拼多多。

CPU:E5-2670 V2,22nm/10C20T/2.5-3.3GHz/25MB/115W/DDR3 RECC 800or1066or1333or1600or1866 max768GB/PCIE 3.0/FCLGA2011/Tcase 82°C/VT-x/VT-d/max 2CPU 。222 元,淘宝省钱版 app 。

CPU 散热器:6 铜管 3 风扇拆机二手,拼多多,69 元。

内存:D3 RECC 1866 32G * 4 = 128G,1260 元。

机箱:拼多多全网*销量爆款,79 元,ATX 。

机箱 12CM 散热风扇:2k 转外观瑕疵版,7.5 元 1 个 * 6 个 = 45 元,拼多多。

电源:京东自营*便宜 600W,199 元。

—小结,不包含存储、显示器:2309 元。

台电 SSD 512G M.2 NVMe:379 元。

3T 机械硬盘拆机板,230 元 /1 个 * 2 = 460 元。

—小结,不包含显示器:3148 元。

显示器,代工厂 23.5 寸壁挂 HDMI 款,499 元。加 10 元让老板再送一条主机电源线,510 元。

键盘鼠标有线套装,PDD,7.9 元,加 2 元让老板送一张鼠标垫,9 元。

—-总结:3667 元。
lushan 70
lushan 17 天前
@m1nm13 “至于 5K 的预算组,只用 GITLAB 的服务器。您就是新时代的猪,不狠宰两刀真是对不起自己” ? 您那只眼看到我只用 GITLAB 了?您眼睛要是不好使,早点去医院看看,不然等您眼睛瞎了,喷人都是个难题
lushan 71
lushan 17 天前
@m1nm13 ”你是要开公司吗?用着玩意?“ 咋了,不开公司不能买吗?我想一步到位挨着您了,是不是我还得给您报批一下才能买吗?
lushan 72
lushan 17 天前
@march1993 其实重点不在 gitlab,哈哈哈,主要是我觉得树莓派对我的需求来说太羸弱了,早晚都得换
lushan 73
lushan 17 天前
看了老哥们的推荐,我就想问问 ecc 重要吗,如果长期运行的话,真的会死机啥的?
lushan 74
lushan 17 天前
@ElmerZhang 如果真的要给他编译的话,我可能偶尔会有编译 aosp 的任务,但是搞个满足编译 aosp 的机器当 server,是不是奢侈一些….
lushan 75
lushan 17 天前
@lushan 我觉得我*后可能在 mini pc 和 服务器之间取个折中,整个无显卡的机箱那种,大家觉得咋样。。
lushan 76
lushan 17 天前
@lushan 不对啊,买个无显卡的主机,会不会装系统的时候插上显示器亮都不亮,那样我装 server 是个问题啊。。
dot 77
dot 17 天前
@lushan #76
如果买没有显卡的主机,那得看主板有没有板载显卡(大多数服务器主板板载 Martox 的显卡),如果也没有的话,只能买带核显的 CPU,否则你只能盲操了,部分 E 系列志强 CPU 后缀 xxx5 是有核显的。
totoro625 78
totoro625 17 天前 via iPhone
@lushan #77 关键词:无显卡启动 /BIOS Headless Mode

bios 设置忽略所有错误就可以
Xhack 79
Xhack 17 天前
j1900 4h4g 跑 8 个服务 包括 gitlab 占用 70%内存 gitlab 需要魔改优化
basefas 80
basefas 17 天前
联想 P500 工作站准系统: 1200
E5-2678-v3: 600
16G DDR4 ECC: 450 x 4
亮机卡 100
硬盘看需求
GM 81
GM 17 天前
有一台闲置的,不知楼主是否有兴趣

机箱看图: https://sm.ms/image/pRYnQc9NfWkqugU

配置是:
永擎 E3C232D2I 服务器 ITX 主板 1151,带远程管理功能,只要连上网,可以远程安装系统。
e3-1265L (好像是 v3 版本)
32G 内存
硬盘不卖
GM 82
GM 17 天前
续 81 楼:
图 2: https://sm.ms/image/TEWG1Vrw4AOZYtk
greenskinmonster 83
greenskinmonster 17 天前
自己装个就好,想小一点,用 ITX 、MATX 架构。我 8 年前买的 i3-3220/微星 B75 MATX 用到现在,24×7 开机。NUC 或者软路由那种机型,不能接 3.5 寸大容量硬盘,局限性还是比较大的。
ElmerZhang 84
ElmerZhang 17 天前
ECC 内存没太大必要,普通家用就可以。
编译 AOSP 我 2000 块组的一台 2600X 也能跑,比我 19 款的 MBP 快多了,只是 acloud 用不了。所以你 5000 预算是肯定够的。
无显卡的话有几种解决方案:
1. CPU 带核显,但是这类 CPU 用作服务器的话性价比就低一些了,毕竟显卡只做亮机用。
2. 亮机卡,淘宝上 50 块搞定。很多主板在用亮机卡装好系统后,把显卡拆掉仍然能跑,微星 B450M Mortar 亲测可行。
3. 用服务器硬件。
xingshu1990 85
xingshu1990 17 天前
@wanguorui123 派 好一些的是 4B 8G 系列(不知道有没有 16G )
集群性能也好不到哪里去,而且一个 4B 8G 的价格也不便宜。
TYB 上有大佬们用派做集群 10 多个起步,这个价格就直接上去了。
tediorelee 86
tediorelee 17 天前 via iPhone
在学长那里 py 了一个 itx 的 diy nas 服务器,G6400+4x2Tb 硬盘,自己加了个 16G 内存共 24G,两个 ssd,全铝机箱,还挺不错?
ivan_wl 87
ivan_wl 16 天前
刚买了个华硕 PN51,Ryzen 5300U,准系统 2100 拿下。类似的还有技嘉 brix,支持 2.5G lan 的。
gBurnX 88
gBurnX 16 天前
@lushan ECC 当然有必要,不然内存颗粒坏了,导致数据损坏,就得不偿失了,而且因内存损坏导致的数据损坏,往往还是无感知的。

不过,纯 ECC 又不好用又贵,建议 RECC 。
gBurnX 89
gBurnX 16 天前
@ElmerZhang ECC 内存非常有必要,因为非 ECC 内存损坏,导致数据损坏,往往是无感知的。
ElmerZhang 90
ElmerZhang 16 天前
@gBurnX 要考虑性价比的好么
内存 ECC 了,其他硬件也得搞到服务器级别才平衡吧?
就随便跑点自用的东西,没必要多花那么多钱。
gBurnX 91
gBurnX 16 天前
@ElmerZhang 你完全搞错了,自己组装服务器,不比你打游戏的机器贵。

有时间建议你学学服务器组装知识,看看那些配件到底要多少钱。
uyz 92
uyz 16 天前
我*近组了个 超微 x11sra-f W2175 扩展性不错 就是板子有点贵
locoz 93
locoz 15 天前
@lushan #73 只要服务器上有服务长期运行,ECC 就很重要,可以避免出现一些奇怪的问题还找不到原因,企业级硬盘的 ECC 功能同理。
locoz 94
locoz 15 天前
@ElmerZhang #90 淘汰产品没多贵的,五千块钱不含显卡和硬盘可以随便搞很高配了,建议多了解一下洋垃圾市场。
rosees 95
rosees 14 天前
10 带 I3,8G 或者 16G 512G SSD 就可以了,也就 2000,待机功耗 30W,很低了~~~用 hyper-v 虚拟机性能还是不错的,主机闲暇时候还能打打游戏
ryd994 96
ryd994 7 天前
1. ECC 真的有用。
2. ECC 不贵。只要你接受上一代二手服务器硬件。ECC Reg 比普通内存还便宜。因为 ECC Reg 的延迟比普通内存大,频率比普通内存低,耗电量比普通内存大。所以一般人不会用,特别是想打游戏的人。老电脑升级只能买普通内存。
但是与此同时,旧服务器到了保修年限必定会被淘汰,企业不差钱。所以二手服务器硬件一直有稳定的供应。这样的供需关系必然就是白菜价。

既然好用还便宜,那为什么不用?不必要就不能用了?

ARouter原理剖析及手动实现

ARouter原理剖析及手动实现

 

前言

路由跳转在项目中用了一段时间了,*近对Android中的ARouter路由原理也是研究了一番,于是就给大家分享一下自己的心得体会,并教大家如何实现一款简易的路由框架。

本篇文章分为两个部分,*部分着重剖析ARouter路由的原理,第二部分会带着大家仿照ARouter撸一个自己的路由框架,我们自己撸的路由框架可能没有Arouter众多的功能如过滤器、provider等,但是却实现了ARouter*核心的功能:路由跳转,同时你也能学会如何去设计一个框架等等。

*部分:ARouter原理剖析

说到路由便不得不提一下Android中的组件化开发思想,组件化是*近比较流行的架构设计方案,它能对代码进行高度的解耦、模块分离等,能*大地提高开发效率(如有同学对组件化有不理解,可以参考网上众多的博客等介绍,然后再阅读demo源码中的组件化配置进行熟悉)。路由和组件化本身没有什么联系,因为路由的责任是负责页面跳转,但是组件化中两个单向依赖的module之间需要互相启动对方的Activity,因为没有相互引用,startActivity()是实现不了的,必须需要一个协定的通信方式,此时类似ARouter和ActivityRouter等的路由框架就派上用场了。

  • *节:ARouter路由跳转的原理

<img src=”http://pcayc3ynm.bkt.clouddn.com/module_1.png” />

如上图,在组件化中,为了业务逻辑的彻底解耦,同时也为了每个module都可以方便的单独运行和调试,上层的各个module不会进行相互依赖(只有在正式联调的时候才会让app壳module去依赖上层的其他组件module),而是共同依赖于base module,base module中会依赖一些公共的第三方库和其他配置。那么在上层的各个module中,如何进行通信呢?

我们知道,传统的Activity之间通信,通过startActivity(intent),而在组件化的项目中,上层的module没有依赖关系(即便两个module有依赖关系,也只能是单向的依赖),那么假如login module中的一个Activity需要启动pay_module中的一个Activity便不能通过startActivity来进行跳转。那么大家想一下还有什么其他办法呢? 可能有同学会想到隐式跳转,这当然也是一种解决方法,但是一个项目中不可能所有的跳转都是隐式的,这样Manifest文件会有很多过滤配置,而且非常不利于后期维护。当然你用反射拿到Activity的class文件也可以实现跳转,但是*:大量的使用反射跳转对性能会有影响,第二:你需要拿到Activity的类文件,在组件开发的时候,想拿到其他module的类文件是很麻烦的,因为组件开发的时候组件module之间是没有相互引用的,你只能通过找到类的路径去反射拿到这个class,那么有没有一种更好的解决办法呢?办法当然是有的。下面看图:

<img src=”http://pcayc3ynm.bkt.clouddn.com/module_2.png” />

在组件化中,我们通常都会在base_module上层再依赖一个router_module,而这个router_module就是负责各个模块之间页面跳转的。

用过ARouter路由框架的同学应该都知道,在每个需要对其他module提供调用的Activity中,都会声明类似下面@Route注解,我们称之为路由地址

@Route(path = "/main/main")
public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
}


@Route(path = "/module1/module1main")
public class Module1MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_module1_main);
    }
}

那么这个注解有什么用呢,路由框架会在项目的编译期通过注解处理器扫描所有添加@Route注解的Activity类,然后将Route注解中的path地址和Activity.class文件映射关系保存到它自己生成的java文件中。为了让大家理解,我这里来使用近乎伪代码给大家简单演示一下。

public class MyRouters{

    //项目编译后通过apt生成如下方法
    public static HashMap<String, ClassBean> getRouteInfo(HashMap<String, ClassBean> routes) {
        route.put("/main/main", MainActivity.class);
        route.put("/module1/module1main", Module1MainActivity.class);
        route.put("/login/login", LoginActivity.class);
    }
}

这样我们想在app模块的MainActivity跳转到login模块的LoginActivity,那么便只需调用如下:

//不同模块之间启动Activity
public void login(String name, String password) {
    HashMap<String, ClassBean> route = MyRouters.getRouteInfo(new HashMap<String, ClassBean>);
    LoginActivity.class classBean = route.get("/login/login");
    Intent intent = new Intent(this, classBean);
    intent.putExtra("name", name);
    intent.putExtra("password", password);
    startActivity(intent);
}

这样是不是很简单就实现了路由的跳转,既没有隐式意图的繁琐,也没有反射对性能的损耗。用过ARouter的同学应该知道,用ARouter启动Activity应该是下面这个写法

// 2. Jump with parameters
ARouter.getInstance().build("/test/login")
            .withString("password", 666666)
            .withString("name", "小三")
            .navigation();

那么ARouter背后是怎么样实现跳转的呢?实际上它的核心思想跟上面讲解是一样的,我们在代码里加入的@Route注解,会在编译时期通过apt生成一些存储path和activity.class映射关系的类文件,然后app进程启动的时候会加载这些类文件,把保存这些映射关系的数据读到内存里(保存在map里),然后在进行路由跳转的时候,通过build()方法传入要到达页面的路由地址,ARouter会通过它自己存储的路由表找到路由地址对应的Activity.class(activity.class = map.get(path)),然后new Intent(context, activity.Class),当调用ARouter的withString()方法它的内部会调用intent.putExtra(String name, String value),调用navigation()方法,它的内部会调用startActivity(intent)进行跳转,这样便可以实现两个相互没有依赖的module顺利的启动对方的Activity了。

  • 第二节:ARouter映射关系如何生成

通过上节我们知道在Activity类上加上@Route注解之后,便可通过apt生成对应的路由表。那么现在我们来搞清楚,既然路由和Activity的映射关系我们可以很容易地得到(因为代码都是我们写的,当然很容易得到),那么为什么我们要繁琐的通过apt来生成类文件而不是自己直接写一个契约类来保存映射关系呢。如果站在一个框架开发者的角度去理解,就不难明白了,因为框架是给上层业务开发者调用的,如果业务开发者在开发页面的过程中还要时不时的更新或更改契约类文件,不免过于麻烦?如果有自动根据路由地址生成映射表文件的技术该多好啊!

技术当然是有的,那就是被众多框架使用的apt及javapoet技术,那么什么是apt,什么是javapoet呢?我们先来看下图:

<img src=”http://pcayc3ynm.bkt.clouddn.com/apt_javapoet.png” />

APT是Annotation Processing Tool的简称,即注解处理工具。由图可知,apt是在编译期对代码中指定的注解进行解析,然后做一些其他处理(如通过javapoet生成新的Java文件)。我们常用的ButterKnife,其原理就是通过注解处理器在编译期扫描代码中加入的@BindView、@OnClick等注解进行扫描处理,然后生成XXX_ViewBinding类,实现了view的绑定。javapoet是鼎鼎大名的squareup出品的一个开源库,是用来生成java文件的一个library,它提供了简便的api供你去生成一个java文件。可以如下引入javapoet

implementation 'com.squareup:javapoet:1.7.0'

下面我通过demo中的例子带你了解如何通过apt和javapoet技术生成路由映射关系的类文件:

首先*步,定义注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.CLASS)
public @interface Route {
    /**
     * 路由的路径
     * @return
     */
    String path();

    /**
     * 将路由节点进行分组,可以实现动态加载
     * @return
     */
    String group() default "";

}

这里看到Route注解里有path和group,这便是仿照ARouter对路由进行分组。因为当项目变得越来越庞大的时候,为了便于管理和减小首次加载路由表过于耗时的问题,我们对所有的路由进行分组。在ARouter中会要求路由地址至少需要两级,如”/xx/xx”,一个模块下可以有多个分组,这里我们就将路由地址定为必须大于等于两级,其中*级是group。

第二步,在Activity上使用注解

@Route(path = "/main/main")
public class MainActivity extends AppCompatActivity {

}

@Route(path = "/main/main2")
public class Main2Activity extends AppCompatActivity {

}

@Route(path = "/show/info")
public class ShowActivity extends AppCompatActivity {

}

第三步,编写注解处理器,在编译器找到加入注解的类文件,进行处理,这里我只展示关键代码,具体的细节还需要你去demo中仔细研读:

@AutoService(Processor.class)
/**
  处理器接收的参数 替代 {@link AbstractProcessor#getSupportedOptions()} 函数
 */
@SupportedOptions(Constant.ARGUMENTS_NAME)
/**
 * 注册给哪些注解的  替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函数
 */
@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)

public class RouterProcessor extends AbstractProcessor {
    /**
     * key:组名 value:类名
     */
    private Map<String, String> rootMap = new TreeMap<>();
    /**
     * 分组 key:组名 value:对应组的路由信息
     */
    private Map<String, List<RouteMeta>> groupMap = new HashMap<>();

    /**
     *
     * @param set 使用了支持处理注解的节点集合
     * @param roundEnvironment 表示当前或是之前的运行环境,可以通过该对象查找找到的注解。
     * @return true 表示后续处理器不会再处理(已经处理)
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (!Utils.isEmpty(set)) {
            //被Route注解的节点集合
            Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
            if (!Utils.isEmpty(rootElements)) {
                processorRoute(rootElements);
            }
            return true;
        }
        return false;
    }


    //...

}

如代码中所示,要想在编译期对注解做处理,就需要RouterProcessor继承自AbstractProcessor并通过@AutoService注解进行注册,然后实现process()方法。还没有完,你还需要通过@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)指定要处理哪个注解,Constant.ANNOTATION_TYPE_ROUTE便是我们的Route注解的路径。看process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment)方法,set集合就是编译期扫描代码得到的加入了Route注解的文件集合,然后我们就可以在process方法生成java文件了。

这里的@AutoService是为了注册注解处理器,需要我们引入一个google开源的自动注册工具AutoService,如下依赖(当然也可以手动进行注册,不过略微麻烦,这里不太推荐):

implementation 'com.google.auto.service:auto-service:1.0-rc2'

第四步:通过javapoet生成java类:
在第三步中process()方法里有一句代码:processorRoute(rootElements),这个就是生成java文件的方法了,下面我贴出代码:

private void processorRoute(Set<? extends Element> rootElements) {

    //...

    //生成Group记录分组表
    generatedGroup(iRouteGroup);

    //生成Root类 作用:记录<分组,对应的Group类>
    generatedRoot(iRouteRoot, iRouteGroup);
}

processorRoute()方法内容很多,这里我只贴出生成java文件相关,其他代码我会在第二部分手动实现路由框架中详细介绍。如上,generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)就是生成java文件的核心了。这里我只贴出generatedRoot()方法,因为生成类文件的原理都是一样的,至于生成什么功能的类,只要你会一个,举一反三,这便没有什么难度。

/**
 * 生成Root类  作用:记录<分组,对应的Group类>
 * @param iRouteRoot
 * @param iRouteGroup
 */
private void generatedRoot(TypeElement iRouteRoot, TypeElement iRouteGroup) {
    //创建参数类型 Map<String,Class<? extends IRouteGroup>> routes>
    //Wildcard 通配符
    ParameterizedTypeName parameterizedTypeName = ParameterizedTypeName.get(
            ClassName.get(Map.class),
            ClassName.get(String.class),
            ParameterizedTypeName.get(
                    ClassName.get(Class.class),
                    WildcardTypeName.subtypeOf(ClassName.get(iRouteGroup))
            ));
    //生成参数 Map<String,Class<? extends IRouteGroup>> routes> routes
    ParameterSpec parameter = ParameterSpec.builder(parameterizedTypeName, "routes").build();

    //生成函数 public void loadInfo(Map<String,Class<? extends IRouteGroup>> routes> routes)
    MethodSpec.Builder methodBuilder = MethodSpec.methodBuilder(Constant.METHOD_LOAD_INTO)
            .addModifiers(Modifier.PUBLIC)
            .addAnnotation(Override.class)
            .addParameter(parameter);
    //生成函数体
    for (Map.Entry<String, String> entry : rootMap.entrySet()) {
        methodBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(Constant.PACKAGE_OF_GENERATE_FILE, entry.getValue()));
    }
    //生成$Root$类
    String className = Constant.NAME_OF_ROOT + moduleName;
    TypeSpec typeSpec = TypeSpec.classBuilder(className)
            .addSuperinterface(ClassName.get(iRouteRoot))
            .addModifiers(Modifier.PUBLIC)
            .addMethod(methodBuilder.build())
            .build();
    try {
      //生成java文件,PACKAGE_OF_GENERATE_FILE就是生成文件需要的路径
        JavaFile.builder(Constant.PACKAGE_OF_GENERATE_FILE, typeSpec).build().writeTo(filerUtils);
        log.i("Generated RouteRoot:" + Constant.PACKAGE_OF_GENERATE_FILE + "." + className);
    } catch (IOException e) {
        e.printStackTrace();
    }
}

如上,我把每一块代码的作用注释了出来,相信大家很容易就能理解每一个代码段的作用。可见,其实生成文件只是调用一些api而已,只要我们熟知api的调用,生成java文件便没有什么难度。

第二部分:动手实现一个路由框架

通过*部分的讲述,我相信大家对于ARouter的原理已经有了整体轮廓的理解,这一部分,我便会通过代码带你去实现一个自己的路由框架。要实现这个路由框架,我们先来实现生成路由映射文件这一块,因为这一块是路由框架能够运行起来的核心。

  • *节:生成路由映射文件

通过*部分的讲述我们知道在Activity类上加上@Route注解之后,便可通过apt来生成对应的路由表,那么现在我们就来生成这些路由映射文件。首先,我们要理解一个问题,就是我们的路由映射文件是在编译期间生成的,那么在程序的运行期间我们要统一调用这些路由信息,便需要一个统一的调用方式。我们先来定义这个调用方式:

public interface IRouteGroup {
    void loadInto(Map<String, RouteMeta> atlas);
}

public interface IRouteRoot {
    void loadInto(Map<String, Class<? extends IRouteGroup>> routes);
}

我们定义两个接口来对生成的java文件进行约束,IRouteGroup是生成的分组关系契约,IRouteRoot是单个分组路由信息契约,只要我们生成的java文件继承自这个接口并实现loadInto()方法,在运行期间我们就可以统一的调用生成的java文件,获取路由映射信息。

现在我们来把RouterProcessor生成路由映射文件相关的代码补全:

@AutoService(Processor.class)
/**
  处理器接收的参数 替代 {@link AbstractProcessor#getSupportedOptions()} 函数
 */
@SupportedOptions(Constant.ARGUMENTS_NAME)
/**
 * 指定使用的Java版本 替代 {@link AbstractProcessor#getSupportedSourceVersion()} 函数
 */
@SupportedSourceVersion(SourceVersion.RELEASE_7)
/**
 * 注册给哪些注解的  替代 {@link AbstractProcessor#getSupportedAnnotationTypes()} 函数
 */
@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)

public class RouterProcessor extends AbstractProcessor {
    /**
     * key:组名 value:类名
     */
    private Map<String, String> rootMap = new TreeMap<>();
    /**
     * 分组 key:组名 value:对应组的路由信息
     */
    private Map<String, List<RouteMeta>> groupMap = new HashMap<>();

    /**
     * 节点工具类 (类、函数、属性都是节点)
     */
    private Elements elementUtils;

    /**
     * type(类信息)工具类
     */
    private Types typeUtils;

    /**
     * 文件生成器 类/资源
     */
    private Filer filerUtils;

    private String moduleName;

    private Log log;

    @Override
    public synchronized void init(ProcessingEnvironment processingEnvironment) {
        super.init(processingEnvironment);
        //获得apt的日志输出
        log = Log.newLog(processingEnvironment.getMessager());
        elementUtils = processingEnvironment.getElementUtils();
        typeUtils = processingEnvironment.getTypeUtils();
        filerUtils = processingEnvironment.getFiler();

        //参数是模块名 为了防止多模块/组件化开发的时候 生成相同的 xx$$ROOT$$文件
        Map<String, String> options = processingEnvironment.getOptions();
        if (!Utils.isEmpty(options)) {
            moduleName = options.get(Constant.ARGUMENTS_NAME);
        }
        if (Utils.isEmpty(moduleName)) {
            throw new RuntimeException("Not set processor moudleName option !");
        }
        log.i("init RouterProcessor " + moduleName + " success !");
    }

    /**
     *
     * @param set 使用了支持处理注解的节点集合
     * @param roundEnvironment 表示当前或是之前的运行环境,可以通过该对象查找找到的注解。
     * @return true 表示后续处理器不会再处理(已经处理)
     */
    @Override
    public boolean process(Set<? extends TypeElement> set, RoundEnvironment roundEnvironment) {
        if (!Utils.isEmpty(set)) {
            //被Route注解的节点集合
            Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
            if (!Utils.isEmpty(rootElements)) {
                processorRoute(rootElements);
            }
            return true;
        }
        return false;
    }


    //...

}

我们通过@SupportedOptions(Constant.ARGUMENTS_NAME)拿到每个module的名字,用来生成对应module下存放路由信息的类文件名。这里变量Constant.ARGUMENTS_NAME的值就是moduleName,在这之前,我们需要在每个组件module的gradle下配置如下

javaCompileOptions {
            annotationProcessorOptions {
                arguments = [moduleName: project.getName()]
            }
        }

@SupportedAnnotationTypes(Constant.ANNOTATION_TYPE_ROUTE)指定了需要处理的注解的路径地址,在此就是Route.class的路径地址。

RouterProcessor中我们实现了init方法,拿到log apt日志输出工具用以输出apt日志信息,并通过以下代码得到上面提到的每个module配置的moduleName

//参数是模块名 为了防止多模块/组件化开发的时候 生成相同的 xx$$ROOT$$文件
Map<String, String> options = processingEnvironment.getOptions();
if (!Utils.isEmpty(options)) {
    moduleName = options.get(Constant.ARGUMENTS_NAME);
}
if (Utils.isEmpty(moduleName)) {
    throw new RuntimeException("Not set processor moudleName option !");
}

然后在process()方法里开始生成文件名以EaseRouter_Route_moduleName和EaseRouter_Group_moduleName命名的文件。(这里的moduleName指具体的module名,demo中apt相关的代码实现都在easy-compiler module中),生成EaseRouter_Route_moduleName相关文件存储的就是分组关系,生成EaseRouter_Group_moduleName相关文件里存储的就是分组下的路由映射关系。

好了,我们终于可以生成文件了,在process()方法里有如下代码,

if (!Utils.isEmpty(set)) {
    //被Route注解的节点集合
    Set<? extends Element> rootElements = roundEnvironment.getElementsAnnotatedWith(Route.class);
    if (!Utils.isEmpty(rootElements)) {
        processorRoute(rootElements);
    }
    return true;
}
return false;

set就是扫描得到的支持处理注解的节点集合,然后得到rootElements,即被@Route注解的节点集合,此时就可以调用
processorRoute(rootElements)方法去生成文件了。processorRoute(rootElements)方法实现如下:

private void processorRoute(Set<? extends Element> rootElements) {
    //获得Activity这个类的节点信息
    TypeElement activity = elementUtils.getTypeElement(Constant.ACTIVITY);
    TypeElement service = elementUtils.getTypeElement(Constant.ISERVICE);
    for (Element element : rootElements) {
        RouteMeta routeMeta;
        //类信息
        TypeMirror typeMirror = element.asType();
        log.i("Route class:" + typeMirror.toString());
        Route route = element.getAnnotation(Route.class);
        if (typeUtils.isSubtype(typeMirror, activity.asType())) {
            routeMeta = new RouteMeta(RouteMeta.Type.ACTIVITY, route, element);
        } else if (typeUtils.isSubtype(typeMirror, service.asType())) {
            routeMeta = new RouteMeta(RouteMeta.Type.ISERVICE, route, element);
        } else {
            throw new RuntimeException("Just support Activity or IService Route: " + element);
        }
        categories(routeMeta);
    }
    TypeElement iRouteGroup = elementUtils.getTypeElement(Constant.IROUTE_GROUP);
    TypeElement iRouteRoot = elementUtils.getTypeElement(Constant.IROUTE_ROOT);

    //生成Group记录分组表
    generatedGroup(iRouteGroup);

    //生成Root类 作用:记录<分组,对应的Group类>
    generatedRoot(iRouteRoot, iRouteGroup);
}

上面提到的生成的EaseRouter_Route_moduleName文件和EaseRouter_Group_moduleName文件分别实现了IRouteRoot和IRouteGroup接口,就是通过下面这两行代码拿到IRootGroup和IRootRoot的字节码信息,然后传入generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)方法,这两个方法内部会通过javapoet api生成java文件,并实现这两个接口。

TypeElement iRouteGroup = elementUtils.getTypeElement(Constant.IROUTE_GROUP);
TypeElement iRouteRoot = elementUtils.getTypeElement(Constant.IROUTE_ROOT);

generatedGroup(iRouteGroup)和generatedRoot(iRouteRoot, iRouteGroup)就是生成上面提到的EaseRouter_Root_app和EaseRouter_Group_main等文件的具体实现,生成的方法我在*部分已经贴出来过了,这里不再阐述。

好了,现在我们编译下项目就会在每个组件module的build/generated/source/apt目录下生成相关映射文件。这里我把app module编译后生成的文件贴出来,app module编译后会生成EaseRouter_Root_app文件和EaseRouter_Group_main、EEaseRouter_Group_show等文件,EaseRouter_Root_app文件对应于app module的分组,里面记录着本module下所有的分组信息,EaseRouter_Group_main、EaseRouter_Group_show文件分别记载着当前分组下的所有路由地址和ActivityClass映射信息。如下所示:

public class EaseRouter_Root_app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("main", EaseRouter_Group_main.class);
    routes.put("show", EaseRouter_Group_show.class);
  }
}


public class EaseRouter_Group_main implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/main/main",RouteMeta.build(RouteMeta.Type.ACTIVITY,Main2\Activity.class,"/main/main","main"));
    atlas.put("/main/main2",RouteMeta.build(RouteMeta.Type.ACTIVITY,Main2\Activity.class,"/main/main2","main"));
  }
}

public class EaseRouter_Group_show implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/show/info",RouteMeta.build(RouteMeta.Type.ACTIVITY,ShowActivity.class,"/show/info","show"));
  }
}

大家会看到生成的类分别实现了IRouteRoot和IRouteGroup接口,并且实现了loadInto()方法,而loadInto方法通过传入一个特定类型的map就能把分组信息放入map里,只要分组信息存入到特定的map里后,我们就可以随意的从map里取路由地址对应的Activity.class做跳转使用。那么如果我们在login_module中想启动app_module中的MainActivity类,首先,我们已知MainActivity类的路由地址是”/main/main”,*个”/main”代表分组名,那么我们岂不是可以像下面这样调用去得到MainActivity类文件,然后startActivity()跳转到MainActivity。(这里的RouteMeta只是存有Activity class文件的封装类,先不用理会)。

public void test() {
    EaseRouter_Root_app rootApp = new EaseRouter_Root_app();
    HashMap<String, Class<? extends IRouteGroup>> rootMap = new HashMap<>();
    rootApp.loadInto(rootMap);

    //得到/main分组
    Class<? extends IRouteGroup> aClass = rootMap.get("main");
    try {
        HashMap<String, RouteMeta> groupMap = new HashMap<>();
        aClass.newInstance().loadInto(groupMap);
        //得到MainActivity
        RouteMeta main = groupMap.get("/main/main");
        Class<?> mainActivityClass = main.getDestination();

        Intent intent = new Intent(this, mainActivityClass);
        startActivity(intent);
    } catch (InstantiationException e) {
        e.printStackTrace();
    } catch (IllegalAccessException e) {
        e.printStackTrace();
    }

}

可以看到,只要有了这些附带路由映射信息的类文件,并将其保存的映射关系存入map里,我们便能轻易的启动其他module的Activity了。

  • 第二节 路由框架的初始化

上节我们已经通过apt生成了映射文件,并且知道了如何通过映射文件去调用Activity,然而我们要实现一个路由框架,就要考虑在合适的时机拿到这些映射文件中的信息,以供上层业务做跳转使用。那么在什么时机去拿到这些映射文件中的信息呢?首先我们需要在上层业务做路由跳转之前把这些路由映射关系拿到手,但我们不能事先预知上层业务会在什么时候做跳转,那么拿到这些路由关系*好的时机就是应用程序初始化的时候。

知道了在什么时机去拿到映射关系,接下来就要考虑如何拿了。我们在上面已经介绍过实现IRouteRoot接口的所有类文件里保存着各个module的分组文件(分组文件就是实现了IRouteGroup接口的类文件),那么只要拿到所有实现IRouteGroup接口的类的集合,便能得到左右的路由信息了。下面看初始化的代码:

public class MyApplication extends Application {

    @Override
    public void onCreate() {
        super.onCreate();
        EasyRouter.init(this);
    }
}

//我们手动实现的路由框架,我们就叫它EasyRouter
public class EasyRouter {

  private static final String TAG = "EasyRouter";
   private static final String ROUTE_ROOT_PAKCAGE = "com.xsm.easyrouter.routes";
   private static final String SDK_NAME = "EaseRouter";
   private static final String SEPARATOR = "_";
   private static final String SUFFIX_ROOT = "Root";

   private static EasyRouter sInstance;
   private static Application mContext;
   private Handler mHandler;

   private EasyRouter() {
       mHandler = new Handler(Looper.getMainLooper());
   }

   public static EasyRouter getsInstance() {
       synchronized (EasyRouter.class) {
           if (sInstance == null) {
               sInstance = new EasyRouter();
           }
       }
       return sInstance;
   }

   public static void init(Application application) {
       mContext = application;
       try {
           loadInfo();
       } catch (Exception e) {
           e.printStackTrace();
           Log.e(TAG, "初始化失败!", e);
       }
   }

   //...
}

可以看到,进程启动的时候我们调用EasyRouter.init()方法,init()方法中调用了loadInfo()方法,而这个loadInfo()便是我们初始化的核心。我把loadInfo的代码贴出来:

private static void loadInfo() throws PackageManager.NameNotFoundException, InterruptedException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    //获得所有 apt生成的路由类的全类名 (路由表)
    Set<String> routerMap = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);
    for (String className : routerMap) {
        if (className.startsWith(ROUTE_ROOT_PAKCAGE + "." + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
            //root中注册的是分组信息 将分组信息加入仓库中
            ((IRouteRoot) Class.forName(className).getConstructor().newInstance()).loadInto(Warehouse.groupsIndex);
        }
    }
    for (Map.Entry<String, Class<? extends IRouteGroup>> stringClassEntry : Warehouse.groupsIndex.entrySet()) {
        Log.d(TAG, "Root映射表[ " + stringClassEntry.getKey() + " : " + stringClassEntry.getValue() + "]");
    }

}

我们首先通过ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE)得到apt生成的所有实现IRouteRoot接口的类文件集合,通过上面的讲解我们知道,拿到这些类文件便可以得到所有的路由地址和Activity映射关系。

这个ClassUtils.getFileNameByPackageName()方法就是具体的实现了,下面我们看具体的代码:

   /**
     * 得到路由表的类名
     * @param context
     * @param packageName
     * @return
     * @throws PackageManager.NameNotFoundException
     * @throws InterruptedException
     */
    public static Set<String> getFileNameByPackageName(Application context, final String packageName)
            throws PackageManager.NameNotFoundException, InterruptedException {
        final Set<String> classNames = new HashSet<>();
        List<String> paths = getSourcePaths(context);
        //使用同步计数器判断均处理完成
        final CountDownLatch countDownLatch = new CountDownLatch(paths.size());
        ThreadPoolExecutor threadPoolExecutor = DefaultPoolExecutor.newDefaultPoolExecutor(paths.size());
        for (final String path : paths) {
            threadPoolExecutor.execute(new Runnable() {
                @Override
                public void run() {
                    DexFile dexFile = null;
                    try {
                        //加载 apk中的dex 并遍历 获得所有包名为 {packageName} 的类
                        dexFile = new DexFile(path);
                        Enumeration<String> dexEntries = dexFile.entries();
                        while (dexEntries.hasMoreElements()) {
                            String className = dexEntries.nextElement();
                            if (!TextUtils.isEmpty(className) && className.startsWith(packageName)) {
                                classNames.add(className);
                            }
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    } finally {
                        if (null != dexFile) {
                            try {
                                dexFile.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            }
                        }
                        //释放一个
                        countDownLatch.countDown();
                    }
                }
            });
        }
        //等待执行完成
        countDownLatch.await();
        return classNames;
    }

这个方法会通过开启子线程,去扫描apk中所有的dex,遍历找到所有包名为packageName的类名,然后将类名再保存到classNames集合里。

List<String> paths = getSourcePaths(context)这句代码会获得所有的apk文件(instant run会产生很多split apk),这个方法的具体实现大家看demo即可,不再阐述。这里用到了CountDownLatch类,会分path一个文件一个文件的检索,等到所有的类文件都找到后便会返回这个Set<String>集合。所以我们可以知道,初始化时找到这些类文件会有一定的耗时,如果你已经看过ARouter的源码便会知道ARouter这里会有一些优化,只会遍历找一次类文件,找到之后就会保存起来,下次app进程启动会检索是否有保存这些文件,如果有就会直接调用保存后的数据去初始化。

  • 第三节 路由跳转实现

经过上节的介绍,我们已经能够在进程初始化的时候拿到所有的路由信息,那么实现跳转便好做了。直接看代码:

@Route(path = "/main/main")
public class MainActivity extends AppCompatActivity {

  public void startModule1MainActivity(View view) {
    EasyRouter.getsInstance().build("/module1/module1main").navigation();
  }

}

在build的时候,传入要跳转的路由地址,build()方法会返回一个Postcard对象,我们称之为跳卡。然后调用Postcard的navigation()方法完成跳转。用过ARouter的对这个跳卡都应该很熟悉吧!Postcard里面保存着跳转的信息。下面我把Postcard类的代码实现粘下来:

public class Postcard extends RouteMeta {
    private Bundle mBundle;
    private int flags = -1;
    //新版风格
    private Bundle optionsCompat;
    //老版
    private int enterAnim;
    private int exitAnim;

    public Postcard(String path, String group) {
        this(path, group, null);
    }

    public Postcard(String path, String group, Bundle bundle) {
        setPath(path);
        setGroup(group);
        this.mBundle = (null == bundle ? new Bundle() : bundle);
    }

    public Bundle getExtras() {return mBundle;}

    public int getEnterAnim() {return enterAnim;}

    public int getExitAnim() {return exitAnim;}

    /**
     * 跳转动画
     * @param enterAnim
     * @param exitAnim
     * @return
     */
    public Postcard withTransition(int enterAnim, int exitAnim) {
        this.enterAnim = enterAnim;
        this.exitAnim = exitAnim;
        return this;
    }

    /**
     * 转场动画
     * @param compat
     * @return
     */
    public Postcard withOptionsCompat(ActivityOptionsCompat compat) {
        if (null != compat) {
            this.optionsCompat = compat.toBundle();
        }
        return this;
    }

    public Postcard withString(@Nullable String key, @Nullable String value) {
        mBundle.putString(key, value);
        return this;
    }


    public Postcard withBoolean(@Nullable String key, boolean value) {
        mBundle.putBoolean(key, value);
        return this;
    }

    public Postcard withInt(@Nullable String key, int value) {
        mBundle.putInt(key, value);
        return this;
    }

    //还有许多给intent中bundle设置值得方法我就不一一列出来了,可以看demo里所有的细节

    public Bundle getOptionsBundle() {
        return optionsCompat;
    }

    public Object navigation() {
        return EasyRouter.getsInstance().navigation(null, this, -1, null);
    }

    public Object navigation(Context context) {
        return EasyRouter.getsInstance().navigation(context, this, -1, null);
    }


    public Object navigation(Context context, NavigationCallback callback) {
        return EasyRouter.getsInstance().navigation(context, this, -1, callback);
    }

    public Object navigation(Context context, int requestCode) {
        return EasyRouter.getsInstance().navigation(context, this, requestCode, null);
    }

    public Object navigation(Context context, int requestCode, NavigationCallback callback) {
        return EasyRouter.getsInstance().navigation(context, this, requestCode, callback);
    }


}

如果你是一个Android开发,Postcard类里面的东西就不用我再给你介绍了吧!(哈哈)我相信你一看就明白了。我们只介绍一个方法navigation(),他有好几个重载方法,方法里面会调用EasyRouter类的navigation()方法。EaseRouter的navigation()方法,就是跳转的核心了。下面请看:

protected Object navigation(Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
    try {
        prepareCard(postcard);
    }catch (NoRouteFoundException e) {
        e.printStackTrace();
        //没找到
        if (null != callback) {
            callback.onLost(postcard);
        }
        return null;
    }
    if (null != callback) {
        callback.onFound(postcard);
    }

    switch (postcard.getType()) {
        case ACTIVITY:
            final Context currentContext = null == context ? mContext : context;
            final Intent intent = new Intent(currentContext, postcard.getDestination());
            intent.putExtras(postcard.getExtras());
            int flags = postcard.getFlags();
            if (-1 != flags) {
                intent.setFlags(flags);
            } else if (!(currentContext instanceof Activity)) {
                intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
            }
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    //可能需要返回码
                    if (requestCode > 0) {
                        ActivityCompat.startActivityForResult((Activity) currentContext, intent,
                                requestCode, postcard.getOptionsBundle());
                    } else {
                        ActivityCompat.startActivity(currentContext, intent, postcard
                                .getOptionsBundle());
                    }

                    if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) &&
                            currentContext instanceof Activity) {
                        //老版本
                        ((Activity) currentContext).overridePendingTransition(postcard
                                        .getEnterAnim()
                                , postcard.getExitAnim());
                    }
                    //跳转完成
                    if (null != callback) {
                        callback.onArrival(postcard);
                    }
                }
            });
            break;
        default:
            break;
    }
    return null;
}

这个方法里先去调用了prepareCard(postcard)方法,prepareCard(postcard)代码我贴出来,

private void prepareCard(Postcard card) {
    RouteMeta routeMeta = Warehouse.routes.get(card.getPath());
    if (null == routeMeta) {
        Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(card.getGroup());
        if (null == groupMeta) {
            throw new NoRouteFoundException("没找到对应路由:分组=" + card.getGroup() + "   路径=" + card.getPath());
        }
        IRouteGroup iGroupInstance;
        try {
            iGroupInstance = groupMeta.getConstructor().newInstance();
        } catch (Exception e) {
            throw new RuntimeException("路由分组映射表记录失败.", e);
        }
        iGroupInstance.loadInto(Warehouse.routes);
        //已经准备过了就可以移除了 (不会一直存在内存中)
        Warehouse.groupsIndex.remove(card.getGroup());
        //再次进入 else
        prepareCard(card);
    } else {
        //类 要跳转的activity 或IService实现类
        card.setDestination(routeMeta.getDestination());
        card.setType(routeMeta.getType());
        switch (routeMeta.getType()) {
            case ISERVICE:
                Class<?> destination = routeMeta.getDestination();
                IService service = Warehouse.services.get(destination);
                if (null == service) {
                    try {
                        service = (IService) destination.getConstructor().newInstance();
                        Warehouse.services.put(destination, service);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
                card.setService(service);
                break;
            default:
                break;
        }
    }
}

注意,Warehouse就是专门用来存放路由映射关系的类,里面保存着存路由信息的map,这在ARouter里面也是一样的。这段代码Warehouse.routes.get(card.getPath())通过path拿到对应的RouteMeta,这个RouteMeta里面保存了activityClass等信息。继续往下看,如果判断拿到的RouteMeta是空,说明这个路由地址还没有加载到map里面(初始化时为了节省性能,只会加载所有的分组信息,而每个分组下的路由映射关系,会使用懒加载,在首次用到的时候去加载),只有在*次用到当前路由地址的时候,会去Warehouse.routes里面拿routeMeta,如果拿到的是空,会根据当前路由地址的group拿到对应的分组,通过反射创建实例,然后调用实例的loadInfo方法,把它里面保存的映射信息添加到Warehouse.routes里面,并且再次调用prepareCard(card),这时再通过Warehouse.routes.get(card.getPath())就可以顺利拿到RouteMeta了。进入else{}里面,调用了card.setDestination(routeMeta.getDestination()),这个setDestination就是将RouteMeta里面保存的activityClass放入Postcard里面,下面switch代码块可以先不用看,这是实现ARouter中通过依赖注入实现Provider 服务的逻辑,有心研究的同学可以去读一下demo。

好了,prepareCard()方法调用完成后,我们的postcard里面就保存了activityClass,然后switch (postcard.getType()){}会判断postcard的type为ACTIVITY,然后通过ActivityCompat.startActivity启动Activity。到这里,路由跳转的实现已经讲解完毕了。

小结

EaseRouter本身只是参照ARouter手动实现的路由框架,并且剔除掉了很多东西,如过滤器等,如果想要用在项目里,建议还是用ARouter更好,毕竟这只是个练手项目,功能也不够全面,当然有同学想对demo扩展后使用那当然更好,遇到什么问题可以及时联系我。我的目的是通过自己手动实现路由框架来加深对知识的理解,如这里面涉及到的知识点apt、javapoet和组件化思路、编写框架的思路等。看到这里,如果感觉干货很多,欢迎关注我的github,里面会有更多干货!

阿里ARouter使用及源码解析(一)

在app的开发中,页面之间的相互跳转是*基本常用的功能。在Android中的跳转一般通过显式intent和隐式intent两种方式实现的,而Android的原生跳转方式会存在一些缺点:

  • 显式intent的实现方式,因为会存在直接的类依赖的问题,导致耦合严重;
  • 隐式intent的实现方式,则会出现规则集中式管理,导致协作变得困难;
  • 可配置性较差,一般而言配置规则都是在Manifest中的,这就导致了扩展性较差;
  • 跳转过程无法控制,一旦使用了StartActivity()就无法插手其中任何环节了,只能交给系统管理;
  • 当多组件化开发,使用原生的路由方式很难实现完全解耦;

而阿里的ARouter路由框架具有解耦、简单易用、支持多模块项目、定制性较强、支持拦截逻辑等诸多优点,很好的解决了上述的问题。关于ARouter具体实现功能,典型应用以及相应技术方案实现的介绍不在这详细介绍,具体可参见开源*佳实践:Android平台页面路由框架ARouter。

阿里ARouter的分析计划

  • 阿里ARouter使用及源码解析(一)
  • 阿里ARouter拦截器使用及源码解析(二)
  • 阿里ARouter参数自动装载使用及源码解析(三)
基本功能使用

1.添加依赖和配置

android {
    defaultConfig {
    ...
    javaCompileOptions {
        annotationProcessorOptions {
        arguments = [ moduleName : project.getName() ]
        }
    }
    }
}

dependencies {
    compile 'com.alibaba:arouter-api:1.2.1.1'
    annotationProcessor 'com.alibaba:arouter-compiler:1.1.2.1'
    ...
}

2.添加注解

// 在支持路由的页面上添加注解(必选)
// 这里的路径需要注意的是至少需要有两级,/xx/xx
@Route(path = "/test/test1")
public class Test1Activity extends AppCompatActivity{

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_test1);
    }
}

3.初始化SDK

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private Button btn1,btn2;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn1 = (Button) findViewById(R.id.btn1);
        btn2 = (Button) findViewById(R.id.btn2);

        btn1.setOnClickListener(this);
        btn2.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn1) {
            // 如果使用了InstantRun,必须在初始化之前开启调试模式,但是上线前需要关闭,InstantRun仅用于开发阶段,
            // 线上开启调试模式有安全风险,可以使用BuildConfig.DEBUG来区分环境
            ARouter.openDebug();
            ARouter.init(getApplication()); // 尽可能早,推荐在Application中初始化
        }
    }
}

4.发起跳转操作

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    private Button btn1,btn2;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        btn1 = (Button) findViewById(R.id.btn1);
        btn2 = (Button) findViewById(R.id.btn2);

        btn1.setOnClickListener(this);
        btn2.setOnClickListener(this);
    }

    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn1) {
            ....
        } else if (v.getId() == R.id.btn2){
            ARouter.getInstance().build("/test/test1").navigation();
        }
    }
}

以上相关代码就是ARouter的*基本功能使用的步骤,下面来分析跳转功能是如何实现的。

原理分析
1.ARouter编译的过程

ARouter在编译期的时候,利用自定义注解完成了页面的自动注册。相关注解源码参见arouter-annotation,编译处理器源码参见arouter-compiler

下面是注解@Route的源码介绍:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.CLASS)
public @interface Route {

    /**
     *路由的路径,标识一个路由节点
     */
    String path();

    /**
     * 将路由节点进行分组,可以实现按组动态加载
     */
    String group() default "";

    /**
     * 路由节点名称,可用于生成javadoc文档
     */
    String name() default "undefined";

    /**
     * 用32位int类型标示,可用于页面的一些配置
     */
    int extras() default Integer.MIN_VALUE;

    /**
     * 路由的优先级
     */
    int priority() default -1;
}

Route中的extra值是个int值,由32位表示,即转换成二进制后,一个int中可以配置31个1或者0,而每一个0或者1都可以表示一项配置(排除符号位),如果从这31个位置中随便挑选出一个表示是否需要登录就可以了,只要将标志位置为1,就可以在声明的拦截器中获取到这个标志位,通过位运算的方式判断目标页面是否需要登录。所以可以通过extra给页面配置30多个属性,然后在拦截器中去进行处理。
ARouter在拦截器中会把目标页面的信息封装一个类Postcard,这个类就包含了目标页面注解上@Route标识的各种信息。关于拦截器的使用以及源码分析,后续会有介绍。

将代码编译一遍,可以看到ARouter生成下面几个源文件:

%title插图%num

上面三个文件均是通过注解处理器RouteProcessor生成的,关于如何自定义注解处理器,可以阅读Android编译时注解APT实战(AbstractProcessor),同时也需要学习JavaPoet的基本使用。下面我们看RouteProcessor是如何生成相关文件的。

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        //判断被注解了的元素集合是否为空
        if (CollectionUtils.isNotEmpty(annotations)) {
            //获取所有被@Route注解的元素集合,Element可以是类、方法、变量等
            Set<? extends Element> routeElements = roundEnv.getElementsAnnotatedWith(Route.class);
            try {
                logger.info(">>> Found routes, start... <<<");
                //具体处理注解,生成java文件的方法
                this.parseRoutes(routeElements);

            } catch (Exception e) {
                logger.error(e);
            }
            return true;
        }

        return false;
    }

process()方法相当于处理器的主函数main(),可以在这个方法中扫描、评估和处理注解的代码,以及生成Java文件。RouteProcessor中调用了parseRoutes(),用来处理所有被@Route注解的元素。在分析上述三个java文件如何生成之前,先看看生成文件的具体代码。

  • ARouter$$Root$$app类
public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("test", ARouter$$Group$$test.class);
  }
}
  • ARouter$$Group$$test类
public class ARouter$$Group$$test implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    atlas.put("/test/test1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/test1", "test", null, -1, -2147483648));
  }
}
  • ARouter$$Providers$$app类
public class ARouter$$Providers$$app implements IProviderGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> providers) {
  }
}

我们接着分析上述三个文件是如何生成的

1.首先获取生成方法的参数的类型和参数名称

 private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
        if (CollectionUtils.isNotEmpty(routeElements)) {
          
            logger.info(">>> Found routes, size is " + routeElements.size() + " <<<");

            rootMap.clear();
             // TypeElement 表示一个类或接口元素
            // public static final String ACTIVITY = "android.app.Activity";
            //得到类activity元素
            TypeElement type_Activity = elementUtil.getTypeElement(ACTIVITY);
            // public static final String SERVICE = "android.app.Service";
            //得到类service的元素
            TypeElement type_Service = elementUtil.getTypeElement(SERVICE);
            // public static final String SERVICE = "android.app.Fragment";
            TypeMirror fragmentTm = elements.getTypeElement(FRAGMENT).asType();
             // public static final String SERVICE = "android.support.v4.app.Fragment";
            TypeMirror fragmentTmV4 = elements.getTypeElement(Consts.FRAGMENT_V4).asType();

            // public static final String IROUTE_GROUP = "com.alibaba.android.arouter.facade.template.IRouteGroup";
            //得到接口IRouteGroup元素
            TypeElement type_IRouteGroup = elementUtil.getTypeElement(IROUTE_GROUP);
          // public static final String IROUTE_GROUP = "com.alibaba.android.arouter.facade.template.IProviderGroup";
            //得到接口IProviderGroup元素
            TypeElement type_IProviderGroup = elementUtil.getTypeElement(IPROVIDER_GROUP);
            //获取RouteMeta,RouteType类名
            ClassName routeMetaCn = ClassName.get(RouteMeta.class);
            ClassName routeTypeCn = ClassName.get(RouteType.class);

            //下面代码是获取生成java文件中方法的参数类型名称和参数名称。
            /*
              获取获取ARouter$$Root$$app 类中方法参数Map<String, Class<? extends IRouteGroup>>类型的名称
             */
            ParameterizedTypeName inputMapTypeOfRoot = ParameterizedTypeName.get(
                    ClassName.get(Map.class),
                    ClassName.get(String.class),
                    ParameterizedTypeName.get(
                            ClassName.get(Class.class),
                            WildcardTypeName.subtypeOf(ClassName.get(type_IRouteGroup))
                    )
            );

            /*
              获取ARouter$$Group$$test,ARouter$$Providers$$app类中方法参数 Map<String, RouteMeta>类型的名称
             */
            ParameterizedTypeName inputMapTypeOfGroup = ParameterizedTypeName.get(
                    ClassName.get(Map.class),
                    ClassName.get(String.class),
                    ClassName.get(RouteMeta.class)
            );

            /*
             获取相关的参数
             */
            //获取ARouter$$Root$$app 类中方法的参数Map<String, Class<? extends IRouteGroup>> routes
            ParameterSpec rootParamSpec = ParameterSpec.builder(inputMapTypeOfRoot, "routes").build();
           //获取ARouter$$Group$$test类中方法的参数Map<String, RouteMeta> atlas
            ParameterSpec groupParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "atlas").build();
             //获取ARouter$$Providers$$app类中方法的参数Map<String, RouteMeta> providers
            ParameterSpec providerParamSpec = ParameterSpec.builder(inputMapTypeOfGroup, "providers").build();  

          
          .....
        }
    }

2.获取了方法的参数的类型和参数名称后,下面便是生成相应的方法

 private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
        if (CollectionUtils.isNotEmpty(routeElements)) {
            ........

            /*
              首先创建ARouter$$Root$$xxx 类中的loadInto()方法
              @Override
              public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {}
             */
            MethodSpec.Builder loadIntoMethodOfRootBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                    .addAnnotation(Override.class)
                    .addModifiers(PUBLIC)
                    .addParameter(rootParamSpec);

            //  遍历所有被@Route注解的元素
            for (Element element : routeElements) {
                TypeMirror tm = element.asType();
                Route route = element.getAnnotation(Route.class);
                RouteMeta routeMete = null;
                
                //判断该元素否为 Activity 、IProvider 、 Service 的子类,然后创建相应的RouteMeta 对象
                if (typeUtil.isSubtype(tm, type_Activity.asType())) {                 // Activity
                    logger.info(">>> Found activity route: " + tm.toString() + " <<<");

                    // 如果是acitiviy类型,获取所有被@Autowired的属性
                    //关于@Autowired的注解,我们之后再进行分析
                    Map<String, Integer> paramsType = new HashMap<>();
                    for (Element field : element.getEnclosedElements()) {
                        if (field.getKind().isField() && field.getAnnotation(Autowired.class) != null && !typeUtil.isSubtype(field.asType(), iProvider)) {
                            // It must be field, then it has annotation, but it not be provider.
                            Autowired paramConfig = field.getAnnotation(Autowired.class);
                            paramsType.put(StringUtils.isEmpty(paramConfig.name()) ? field.getSimpleName().toString() : paramConfig.name(), TypeUtils.typeExchange(field.asType()));
                        }
                    }
                    // ACTIVITY类型节点
                    routeMete = new RouteMeta(route, element, RouteType.ACTIVITY, paramsType);
                } else if (typeUtil.isSubtype(tm, iProvider)) {         // IProvider
                    logger.info(">>> Found provider route: " + tm.toString() + " <<<");
                    //从该判断可看出,如果要想成功注册一个 PROVIDER 类型的路由节点,
                    //一定要实现 com.alibaba.android.arouter.facade.template.IProvider 这个接口
                    routeMete = new RouteMeta(route, element, RouteType.PROVIDER, null);
                } else if (typeUtil.isSubtype(tm, type_Service.asType())) {           // Service
                    logger.info(">>> Found service route: " + tm.toString() + " <<<");
                     //SERVICE类型节点
                    routeMete = new RouteMeta(route, element, RouteType.parse(SERVICE), null);
                } else if (types.isSubtype(tm, fragmentTm) || types.isSubtype(tm, fragmentTmV4)) {
                    logger.info(">>> Found fragment route: " + tm.toString() + " <<<");
                   //FRAGMENT类型节点
                    routeMete = new RouteMeta(route, element, RouteType.parse(FRAGMENT), null);
                }
                
                //routeMete包含了每个路由节点的各种信息,下面的方法的主要功能就是根据@Route注解信息对节点进行分组,保存在groupMap集合中。
               //关于方法的具体实现,后面会有解析
                categories(routeMete);
          
            }

            .........
        }
    }

以上代码主要功能就是遍历所有被@Route注解的元素,然后将每个路由节点的信息按照类型(ACTIVITY类型,实现了IProvider 接口类型以及SERVICE类型)封装到RouteMeta中,*后调用categories(routeMete)方法将节点分组,保存在groupMap集合。

继续往下分析

 private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
        if (CollectionUtils.isNotEmpty(routeElements)) {
            ........

             /*
              然后创建ARouter$$Providers$$xxx 类中的loadInto()方法
             @Override
             public void loadInto(Map<String, RouteMeta> providers) {}
             */
            MethodSpec.Builder loadIntoMethodOfProviderBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                    .addAnnotation(Override.class)
                    .addModifiers(PUBLIC)
                    .addParameter(providerParamSpec);

            //遍历分组的集合,生成相应的java文件
           //因为本文使用的例子没有对页面进行分组,所以只生成了一个组文件ARouter$$Group$$xxx
            for (Map.Entry<String, Set<RouteMeta>> entry : groupMap.entrySet()) {
                String groupName = entry.getKey();
               /*
                  创建ARouter$$Group$$xxx 类中的loadInto()方法
                 @Override
                 public void loadInto(Map<String, RouteMeta> atlas) {}
             */
                MethodSpec.Builder loadIntoMethodOfGroupBuilder = MethodSpec.methodBuilder(METHOD_LOAD_INTO)
                        .addAnnotation(Override.class)
                        .addModifiers(PUBLIC)
                        .addParameter(groupParamSpec);

                // 生成loadInto()方法体
                Set<RouteMeta> groupData = entry.getValue();
                //遍历每个组里面的路由节点
                for (RouteMeta routeMeta : groupData) {
                    switch (routeMeta.getType()) {
                        //如果节点类型是PROVIDER,
                        case PROVIDER:  
                          //获取路由节点元素的接口集合
                            List<? extends TypeMirror> interfaces = ((TypeElement) routeMeta.getRawType()).getInterfaces();
                            for (TypeMirror tm : interfaces) {
                             if (types.isSameType(tm, iProvider)) {   // Its implements iProvider interface himself.
                                   //路由节点元素其中一个接口是 com.alibaba.android.arouter.facade.template.IProvider 
                                  //给ARouter$$Providers$$xxx 类中的loadInto()添加方法体
                                    loadIntoMethodOfProviderBuilder.addStatement(
                                            "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
                                            (routeMeta.getRawType()).toString(),//路由节点元素的全名
                                            routeMetaCn,
                                            routeTypeCn,
                                            ClassName.get((TypeElement) routeMeta.getRawType()),
                                            routeMeta.getPath(),
                                            routeMeta.getGroup());
                                } else if (types.isSubtype(tm, iProvider)) {
                                   //路由节点元素其中一个接口是com.alibaba.android.arouter.facade.template.IProvider 接口的子类型
                                    loadIntoMethodOfProviderBuilder.addStatement(
                                            "providers.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, null, " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
                                            tm.toString(),   //IProvider子类型的全名
                                            routeMetaCn,
                                            routeTypeCn,
                                            ClassName.get((TypeElement) routeMeta.getRawType()),
                                            routeMeta.getPath(),
                                            routeMeta.getGroup());
                                }
                            //上面方法体的代码为:
                          //providers.put("实现接口的名称", RouteMeta.build(RouteType.PROVIDER, 类名.class,   "@Route.path", "@Route.group", null, @Route.priority, @Route.extras));
                            }
                            break;
                        default:
                            break;
                    }

                    // 将路由节点中被@Autowired注解的属性集合转换成字符串
                    StringBuilder mapBodyBuilder = new StringBuilder();
                    //获取路由节点中被@Autowired注解的属性集合
                    Map<String, Integer> paramsType = routeMeta.getParamsType();
                    if (MapUtils.isNotEmpty(paramsType)) {
                        for (Map.Entry<String, Integer> types : paramsType.entrySet()) {
                            mapBodyBuilder.append("put(\"").append(types.getKey()).append("\", ").append(types.getValue()).append("); ");
                        }
                    }
                    String mapBody = mapBodyBuilder.toString();
                    
                    //给ARouter$$Group$$xxx 类中的loadInto()添加方法体
                    //注意:有多个分组就会创建多个组文件
                    loadIntoMethodOfGroupBuilder.addStatement(
                            "atlas.put($S, $T.build($T." + routeMeta.getType() + ", $T.class, $S, $S, " + (StringUtils.isEmpty(mapBody) ? null : ("new java.util.HashMap<String, Integer>(){{" + mapBodyBuilder.toString() + "}}")) + ", " + routeMeta.getPriority() + ", " + routeMeta.getExtra() + "))",
                            routeMeta.getPath(),
                            routeMetaCn,
                            routeTypeCn,
                            ClassName.get((TypeElement) routeMeta.getRawType()),
                            routeMeta.getPath().toLowerCase(),
                            routeMeta.getGroup().toLowerCase());
                }

                  // 真正生成ARouter$$Group$$test JAVA文件
                 //NAME_OF_GROUP = ARouter$$Group$$
                //  groupName = test; 关于groupname的值在方法categories(routeMete)中会有讲解
                String groupFileName = NAME_OF_GROUP + groupName;
                JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                        TypeSpec.classBuilder(groupFileName)
                                .addJavadoc(WARNING_TIPS)
                                .addSuperinterface(ClassName.get(type_IRouteGroup))
                                .addModifiers(PUBLIC)
                                .addMethod(loadIntoMethodOfGroupBuilder.build())
                                .build()
                ).build().writeTo(mFiler);

                logger.info(">>> Generated group: " + groupName + "<<<");
                //将生成的组文件放在rootmap集合中去,为下面生成ARouter$$Root$$xxx文件做准备
                rootMap.put(groupName, groupFileName);
            }

         .......
        }
    }

以上代码主要功能由几点:

  • 遍历groupmap集合给ARouter$$Group$$xxx类中的loadInto()添加方法体,并且生成ARouter$$Group$$xxx JAVA文件,而文件命名为ARouter$$Group$$+groupname,其中有多个分组就会创建多个组文件。比如AROUTER源码中的样例就生成了多个分组文件
%title插图%num
两个分组文件

关于生成的loadInto()中的方法体的例子,来自 AROUTER源码中的样例:

public class ARouter$$Group$$test implements IRouteGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> atlas) {
    //存在被@Autowired注解参数生成的代码
    atlas.put("/test/activity1", RouteMeta.build(RouteType.ACTIVITY, Test1Activity.class, "/test/activity1", "test", new java.util.HashMap<String, Integer>(){{put("name", 18); put("boy", 0); put("age", 3); put("url", 18); }}, -1, -2147483648));
    .....
   //没有被@Autowired注解参数生成的代码
    atlas.put("/test/activity4", RouteMeta.build(RouteType.ACTIVITY, Test4Activity.class, "/test/activity4", "test", null, -1, -2147483648));
    ....
  }
}
  • 遍历每个组里面的路由节点,查找节点类型是否为PROVIDER类型,如果是就向给ARouter$$Providers$$xxx类中的loadInto()添加方法,其文件命名ARouter$$Providers$$+modulename。关于生成的loadInto()中的方法体的例子,来自 AROUTER源码中的样例:
public class ARouter$$Providers$$app implements IProviderGroup {
  @Override
  public void loadInto(Map<String, RouteMeta> providers) {
    providers.put("com.alibaba.android.arouter.demo.testservice.HelloService", RouteMeta.build(RouteType.PROVIDER, HelloServiceImpl.class, "/service/hello", "service", null, -1, -2147483648));
    //路由节点元素其中一个接口是IProvider的子类型
    providers.put("com.alibaba.android.arouter.facade.service.SerializationService", RouteMeta.build(RouteType.PROVIDER, JsonServiceImpl.class, "/service/json", "service", null, -1, -2147483648));
     //路由节点元素其中一个接口是IProvider接口
    providers.put("com.alibaba.android.arouter.demo.testservice.SingleService", RouteMeta.build(RouteType.PROVIDER, SingleService.class, "/service/single", "service", null, -1, -2147483648));
  }
}
  • 将生成的组文件放在rootmap集合中去,为下面生成ARouter$$Root$$xxx文件做准备,其文件命名ARouter$$Root$$+modulename。

我们接着分析parseRoutes()方法*后一段代码,这段代码其实很简单,主要目的就是给ARouter$$Root$$xxx的loadInto()添加方法体,*后生成Router$$Providers$$xxx,ARouter$$Root$$xxx文件

 private void parseRoutes(Set<? extends Element> routeElements) throws IOException {
        if (CollectionUtils.isNotEmpty(routeElements)) {
            ........
            //遍历rootMap集合,给ARouter$$Root$$xxx的`loadInto()`添加方法体
            if (MapUtils.isNotEmpty(rootMap)) {
                // Generate root meta by group name, it must be generated before root, then I can findout the class of group.
                for (Map.Entry<String, String> entry : rootMap.entrySet()) {
                    loadIntoMethodOfRootBuilder.addStatement("routes.put($S, $T.class)", entry.getKey(), ClassName.get(PACKAGE_OF_GENERATE_FILE, entry.getValue()));
                }
            }

            // 生成Router$$Providers$$xxx文件
            String providerMapFileName = NAME_OF_PROVIDER + SEPARATOR + moduleName;
            JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                    TypeSpec.classBuilder(providerMapFileName)
                            .addJavadoc(WARNING_TIPS)
                            .addSuperinterface(ClassName.get(type_IProviderGroup))
                            .addModifiers(PUBLIC)
                            .addMethod(loadIntoMethodOfProviderBuilder.build())
                            .build()
            ).build().writeTo(mFiler);

            logger.info(">>> Generated provider map, name is " + providerMapFileName + " <<<");

            // 生成ARouter$$Root$$xxx文件
            String rootFileName = NAME_OF_ROOT + SEPARATOR + moduleName;
            JavaFile.builder(PACKAGE_OF_GENERATE_FILE,
                    TypeSpec.classBuilder(rootFileName)
                            .addJavadoc(WARNING_TIPS)
                            .addSuperinterface(ClassName.get(elementUtil.getTypeElement(ITROUTE_ROOT)))
                            .addModifiers(PUBLIC)
                            .addMethod(loadIntoMethodOfRootBuilder.build())
                            .build()
            ).build().writeTo(mFiler);

            logger.info(">>> Generated root, name is " + rootFileName + " <<<");
        }
    }

关于生成的loadInto()中的方法体的例子,来自 AROUTER源码中的样例:

public class ARouter$$Root$$app implements IRouteRoot {
  @Override
  public void loadInto(Map<String, Class<? extends IRouteGroup>> routes) {
    routes.put("service", ARouter$$Group$$service.class);
    routes.put("test", ARouter$$Group$$test.class);
  }
}

上面分析的便是parseRoutes()方法所有代码的解析

3.*后我们看下categories()方法是如何分组的

   private void categories(RouteMeta routeMete) {
        //如果路由路径合法,且有groupname进行执行
        if (routeVerify(routeMete)) {
            logger.info(">>> Start categories, group = " + routeMete.getGroup() + ", path = " + routeMete.getPath() + " <<<");
             //根据groupname获取该组的路由节点集合,如果集合为空,则创建一个新的组,将该节点添加进去,并将组集合保存在groupmap中;
          //不为空,则添加到所属的组集合中去
            Set<RouteMeta> routeMetas = groupMap.get(routeMete.getGroup());
            if (CollectionUtils.isEmpty(routeMetas)) {
                Set<RouteMeta> routeMetaSet = new TreeSet<>(new Comparator<RouteMeta>() {
                    @Override
                    public int compare(RouteMeta r1, RouteMeta r2) {
                        try {
                            return r1.getPath().compareTo(r2.getPath());
                        } catch (NullPointerException npe) {
                            logger.error(npe.getMessage());
                            return 0;
                        }
                    }
                });
                routeMetaSet.add(routeMete);
                groupMap.put(routeMete.getGroup(), routeMetaSet);
            } else {
                routeMetas.add(routeMete);
            }
        } else {
            logger.warning(">>> Route meta verify error, group is " + routeMete.getGroup() + " <<<");
        }
    }

//判断路由路径是否合法,并且设置groupname
 private boolean routeVerify(RouteMeta meta) {
        String path = meta.getPath();
        //如果路径为空,或者不是由'/'开头,返回false
        if (StringUtils.isEmpty(path) || !path.startsWith("/")) {   // The path must be start with '/' and not empty!
            return false;
        }

         //如果在@Route注解中没有设置group标识,那么就默认取path路径*段路径名作为groupname
        if (StringUtils.isEmpty(meta.getGroup())) { // Use default group(the first word in path)
            try {
                String defaultGroup = path.substring(1, path.indexOf("/", 1));
                if (StringUtils.isEmpty(defaultGroup)) {
                    return false;
                }

                meta.setGroup(defaultGroup);
                return true;
            } catch (Exception e) {
                logger.error("Failed to extract default group! " + e.getMessage());
                return false;
            }
        }

        return true;
    }

通过分析,如果@Route注解中有设置group标识,作为groupname,如果没有就取/xxx1/xxx2,xxx1作为groupname,并将同一组的路由节点放到同一个集合中去。

至此关于@Route注解在编译期时生成ARouter$$Root$$xxx,Router$$Providers$$xxx,ARouter$$Group$$xxx三种映射文件的源码分析完毕。

2.ARouter初始化过程

ARouter经过代码编译后,生成了相应的映射文件,我们可以断定,ARouter 的初始化会将这些文件加载到内存中去,形成一个路由表,以供后面路由查找跳转之用。其相关源码可参见 arouter-api

  • ARouterinit()方法
public static void init(Application application) {
        if (!hasInit) {
            logger = _ARouter.logger;
            _ARouter.logger.info(Consts.TAG, "ARouter init start.");
            hasInit = _ARouter.init(application);

            if (hasInit) {
                _ARouter.afterInit();
            }

            _ARouter.logger.info(Consts.TAG, "ARouter init over.");
        }
    }

由上面代码可以看出,其初始化实际上是调用了_ARouter 的 init ()方法,而且其他的跳转方法*终调用的也是_ARouter 种的方法。

  • _ARouterinit()方法
  protected static synchronized boolean init(Application application) {
        mContext = application;
        LogisticsCenter.init(mContext, executor);
        logger.info(Consts.TAG, "ARouter init success!");
        hasInit = true;

        return true;
    }

_ARouter中又调用了LogisticsCenter.init(),继续追踪下去,其中传入了一个线程池executor,这个线程池在拦截器的时候会使用到。

    public synchronized static void init(Context context, ThreadPoolExecutor tpe) throws HandlerException {
        mContext = context;
        executor = tpe;

        try {
             //ROUTE_ROOT_PAKCAGE = "com.alibaba.android.arouter.routes"
            // 获取ROUTE_ROOT_PAKCAGE 包里面的所有文件
            List<String> classFileNames = ClassUtils.getFileNameByPackageName(mContext, ROUTE_ROOT_PAKCAGE);

            //遍历所有ROUTE_ROOT_PAKCAGE 包里的文件
            for (String className : classFileNames) {
                //文件名以“com.alibaba.android.arouter.routes.ARouter$$Root”开头执行下面代码
                if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_ROOT)) {
                    // 通过反射实例化,并且调用loadInto(),目的即是将编译生成的ARouter$$Group$$xxx文件加载到内存中,保存在Warehouse.groupsIndex;
                    ((IRouteRoot) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.groupsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_INTERCEPTORS)) {
                    //文件名以“com.alibaba.android.arouter.routes.ARouter$$Interceptors”开头执行下面代码
                    //  执行编译生成的ARouter$$Interceptors$$xxx的loadInto(),将自定义拦截器类存放在Warehouse.interceptorsIndex中
                    ((IInterceptorGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.interceptorsIndex);
                } else if (className.startsWith(ROUTE_ROOT_PAKCAGE + DOT + SDK_NAME + SEPARATOR + SUFFIX_PROVIDERS)) {
                     //文件名以“com.alibaba.android.arouter.routes.ARouter$$Providers”开头执行下面代码
                   //  执行编译生成的ARouter$$Interceptors$$xxx的loadInto()
                    ((IProviderGroup) (Class.forName(className).getConstructor().newInstance())).loadInto(Warehouse.providersIndex);
                }
            }

            if (Warehouse.groupsIndex.size() == 0) {
                logger.error(TAG, "No mapping files were found, check your configuration please!");
            }

            if (ARouter.debuggable()) {
                logger.debug(TAG, String.format(Locale.getDefault(), "LogisticsCenter has already been loaded, GroupIndex[%d], InterceptorIndex[%d], ProviderIndex[%d]", Warehouse.groupsIndex.size(), Warehouse.interceptorsIndex.size(), Warehouse.providersIndex.size()));
            }
        } catch (Exception e) {
            throw new HandlerException(TAG + "ARouter init logistics center exception! [" + e.getMessage() + "]");
        }
    }
  • _ARouterafterInit()方法
static void afterInit() {
        // 通过路由机制,初始化路由拦截机制。关于路由拦截机制的使用和原理,后续文章会有分析
        interceptorService = (InterceptorService) ARouter.getInstance().build("/arouter/service/interceptor").navigation();
    }

以上就是ARouter初始化的所有代码,关于如何查找到com.alibaba.android.arouter.routes包内所有文件这里便不做过多分析,大家可以去阅读 arouter-api中ClassUtils这个类的源码。
总结下来,其实ARouter 的初始化只做了一件事,找到自己编译期产生的清单文件,把 Group 、Interceptor 、Provider 三种清单加载到 Warehouse 内存仓库中。即下面这些文件,来源自AROUTER源码中的样例

%title插图%num

值得注意的是,在初始化阶段,ARouter 仅载入了 Group 清单,并没有具体载入每个 Group 中包含的具体的路由节点清单,只有当使用到具体的 Group 时,才会加载对应的 Group 列表。这种分组管理,按需加载,大大的降低了初始化时的内存压力。并且Warehouse类中保存了路由清单,并且将使用过的路由对象缓存起来,之后查找都是直接使用缓存的对象 。

3.ARouter调用过程分析

页面跳转*基本方法

ARouter.getInstance().build(“/test/activity2”).navigation();

获取Provider服务(实现了IProvider接口以及IProvider子类接口的服务类)的方法有两种:

1.byName方式
ARouter.getInstance().build(“/service/hello”).navigation()

2.byType方式
ARouter.getInstance().navigation(HelloService.class)

ARouter路由跳转采用链式调用,ARouter.getInstance()其中采用的单例模式,获取ARouter的实例,这个就不作过多分析,主要分析build()navigation()

build()方法
ARouter的build(String path)init()方法一样,调用的是_ARouterbuild(String path)方法。

  protected Postcard build(String path) {
        if (TextUtils.isEmpty(path)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                path = pService.forString(path);
            }
            return build(path, extractGroup(path));
        }
    }

其中extractGroup(String path)就是根据path获取分组名,即path*段“/”符号之间的值

  private String extractGroup(String path) {
        if (TextUtils.isEmpty(path) || !path.startsWith("/")) {
            throw new HandlerException(Consts.TAG + "Extract the default group failed, the path must be start with '/' and contain more than 2 '/'!");
        }

        try {
            //    /xxx1/xxx2   ===>  defaulGroup = xxx1
            String defaultGroup = path.substring(1, path.indexOf("/", 1));
            if (TextUtils.isEmpty(defaultGroup)) {
                throw new HandlerException(Consts.TAG + "Extract the default group failed! There's nothing between 2 '/'!");
            } else {
                return defaultGroup;
            }
        } catch (Exception e) {
            logger.warning(Consts.TAG, "Failed to extract default group! " + e.getMessage());
            return null;
        }
    }

build(String path)方法*终调用的是build(String path, String group)

    protected Postcard build(String path, String group) {
        if (TextUtils.isEmpty(path) || TextUtils.isEmpty(group)) {
            throw new HandlerException(Consts.TAG + "Parameter is invalid!");
        } else {
            PathReplaceService pService = ARouter.getInstance().navigation(PathReplaceService.class);
            if (null != pService) {
                path = pService.forString(path);
            }
            return new Postcard(path, group);
        }
    }

值得注意的是其中ARouter.getInstance().navigation(PathReplaceService.class)就是得到实现PathReplaceService接口的一个服务对象,对原始path进行处理后,生成新的path路径。而这个类需要我们自己自定义去实现,如果没有实现,pService=null,原始path不做任何处理。
下面是PathReplaceService接口,我们可以通过实现forString()forUri()方法,对某些url进行替换处理,跳转到其他的目标页面。

public interface PathReplaceService extends IProvider {

    /**
     * For normal path.
     *
     * @param path raw path
     */
    String forString(String path);

    /**
     * For uri type.
     *
     * @param uri raw uri
     */
    Uri forUri(Uri uri);
}

*后返回一个Postcard实例对象,里面封装了路由节点的路径,分组等节点信息。其实build()方法的目的只有一个就是根据路由,封装成Postcard对象,其对象贯穿之后整个路由过程。Postcard 包含了众多的属性值,提供了路由过程中所有的控制变量。

public final class Postcard extends RouteMeta {
    private Uri uri;
    private Object tag;             // A tag prepare for some thing wrong.
    private Bundle mBundle;         // 传递的参数
    private int flags = -1;         // intent 的flag标志
    private int timeout = 300;      // Navigation timeout, TimeUnit.Second !
    private IProvider provider;     // IProvider服务对象
    private boolean greenChannal;
    private SerializationService serializationService;//序列化服务对象

     // 跳转动画
    private Bundle optionsCompat;    // The transition animation of activity
    private int enterAnim;
    private int exitAnim;

    // copy from RouteMeta 
    private RouteType type;         // 路由节点类型
    private Element rawType;        
    private Class<?> destination;  //需要跳转到的页面
    private String path;            // 路径
    private String group;           // 分组
    private int priority = -1;      // 优先级
    private int extra;              // 配置标识
    private Map<String, Integer> paramsType;  // 路由页面被@Autowired注解属性
    // ......
}

navigation()方法
关于页面跳转的navigation()方法有多个重载的方法,但*终都会调用_ARouter下面这个方法

    protected Object navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        try {
            //首先对postcard进行一些处理,设置postcard的destination,type,priority 等一些属性值,completion()后面会有分析
            LogisticsCenter.completion(postcard);
        } catch (NoRouteFoundException ex) {
            logger.warning(Consts.TAG, ex.getMessage());

            if (debuggable()) { // Show friendly tips for user.
                Toast.makeText(mContext, "There's no route matched!\n" +
                        " Path = [" + postcard.getPath() + "]\n" +
                        " Group = [" + postcard.getGroup() + "]", Toast.LENGTH_LONG).show();
            }
            // 如果处理postcard失败,通过 callback 回调失败结果
           // callback为空的情况下,如果有定义全局的降级处理(DegradeService),则使用全局处理
           //降级处理也需要我们自己实现DegradeService接口
            if (null != callback) {
                callback.onLost(postcard);
            } else {    // No callback for this invoke, then we use the global degrade service.
                DegradeService degradeService = ARouter.getInstance().navigation(DegradeService.class);
                if (null != degradeService) {
                    degradeService.onLost(context, postcard);
                }
            }

            return null;
        }
         //路由处理成功,回调callback.onFound()
        if (null != callback) {
            callback.onFound(postcard);
        }
        
        //目前来说,PROVIDER服务类型,以及FRAGMENT类型不需要通过拦截器外,其他类型均需要通过拦截器
        //关于拦截器相关用法及原理分析在后续的文章中会讲解到,大家去可以关注下
        if (!postcard.isGreenChannel()) {   
            interceptorService.doInterceptions(postcard, new InterceptorCallback() {
                /**
                 * Continue process
                 *
                 * @param postcard route meta
                 */
                @Override
                public void onContinue(Postcard postcard) {
                    _navigation(context, postcard, requestCode, callback);
                }

                /**
                 * Interrupt process, pipeline will be destory when this method called.
                 *
                 * @param exception Reson of interrupt.
                 */
                @Override
                public void onInterrupt(Throwable exception) {
                    if (null != callback) {
                        callback.onInterrupt(postcard);
                    }

                    logger.info(Consts.TAG, "Navigation failed, termination by interceptor : " + exception.getMessage());
                }
            });
        } else {
            return _navigation(context, postcard, requestCode, callback);
        }

        return null;
    }

值得注意的是,当跳转路由处理失败的时候,会获取一个降级服务,我们可以实现DegradeService接口,实现onLost()方法,对路由处理失败的情况进行处理,比如跳转到一个信息提示页面,让用户去更新版本等操作等。下面是DegradeService接口:

public interface DegradeService extends IProvider {

    /**
     * Router has lost.
     *
     * @param postcard meta
     */
    void onLost(Context context, Postcard postcard);
}

通过上面代码的分析,不管是否通过拦截器进行处理,*后都会调用_navigation()达到路由的目的:

private Object _navigation(final Context context, final Postcard postcard, final int requestCode, final NavigationCallback callback) {
        final Context currentContext = null == context ? mContext : context;

        switch (postcard.getType()) {
            case ACTIVITY:
                //下面就是*基本的使用intent进行activity进行跳转
                // 创建intent
                final Intent intent = new Intent(currentContext, postcard.getDestination());
                //设置传参
                intent.putExtras(postcard.getExtras());

                //activity启动标志
                int flags = postcard.getFlags();
                if (-1 != flags) {
                    intent.setFlags(flags);
                } else if (!(currentContext instanceof Activity)) {    // Non activity, need less one flag.
                    intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                }

                // 在主线程中进行跳转
                new Handler(Looper.getMainLooper()).post(new Runnable() {
                    @Override
                    public void run() {
                        //新版本带转场动画的启动方式
                        if (requestCode > 0) {  // Need start for result
                            ActivityCompat.startActivityForResult((Activity) currentContext, intent, requestCode, postcard.getOptionsBundle());
                        } else {
                            ActivityCompat.startActivity(currentContext, intent, postcard.getOptionsBundle());
                        }

                        if ((0 != postcard.getEnterAnim() || 0 != postcard.getExitAnim()) && currentContext instanceof Activity) {    // Old version.
                            //老版本的跳转动画
                            ((Activity) currentContext).overridePendingTransition(postcard.getEnterAnim(), postcard.getExitAnim());
                        }
                        //跳转成功,回调callback.onArrival()
                        if (null != callback) { // Navigation over.
                            callback.onArrival(postcard);
                        }
                    }
                });

                break;
            case PROVIDER:
                return postcard.getProvider();
            case BOARDCAST:
            case CONTENT_PROVIDER:
            case FRAGMENT:
                Class fragmentMeta = postcard.getDestination();
                try {
                     //实例化fragment,并传递参数
                    Object instance = fragmentMeta.getConstructor().newInstance();
                    if (instance instanceof Fragment) {
                        ((Fragment) instance).setArguments(postcard.getExtras());
                    } else if (instance instanceof android.support.v4.app.Fragment) {
                        ((android.support.v4.app.Fragment) instance).setArguments(postcard.getExtras());
                    }

                    return instance;
                } catch (Exception ex) {
                    logger.error(Consts.TAG, "Fetch fragment instance error, " + TextUtils.formatStackTrace(ex.getStackTrace()));
                }
            case METHOD:
            case SERVICE:
            default:
                return null;
        }

        return null;
    }

目前仅ARouter实现了 ACTIVITY , PROVIDER ,FRAGMENT三种种类型。上面关于postcard的provider,destination的值都是在completion()中设置的。我们接着看LogisticsCentercompletion(Postcard postcard)

    public synchronized static void completion(Postcard postcard) {
        if (null == postcard) {
            throw new NoRouteFoundException(TAG + "No postcard!");
        }
      
        // 查找Warehouse仓库的路由节点缓存,看是否已在缓存中
        RouteMeta routeMeta = Warehouse.routes.get(postcard.getPath());
        if (null == routeMeta) {   
          // 如果没有,查找仓库的组别清单中是否存在该组别,组别清单已经在初始化的时候加载到仓库中去了
            Class<? extends IRouteGroup> groupMeta = Warehouse.groupsIndex.get(postcard.getGroup());  
            //如果没有抛出异常
            if (null == groupMeta) {
                throw new NoRouteFoundException(TAG + "There is no route match the path [" + postcard.getPath() + "], in group [" + postcard.getGroup() + "]");
            } else {
                // Load route and cache it into memory, then delete from metas.
                try {
                    if (ARouter.debuggable()) {
                        logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] starts loading, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                    }
                    // 实例化个组别的类,调用loadInto(),将组别中所有的路由节点加载进仓库Warehouse.routes,缓存
                    IRouteGroup iGroupInstance = groupMeta.getConstructor().newInstance();
                    iGroupInstance.loadInto(Warehouse.routes);
                     // 从组别清单中删除已加载的组别,防止重复加载
                    Warehouse.groupsIndex.remove(postcard.getGroup());

                    if (ARouter.debuggable()) {
                        logger.debug(TAG, String.format(Locale.getDefault(), "The group [%s] has already been loaded, trigger by [%s]", postcard.getGroup(), postcard.getPath()));
                    }
                } catch (Exception e) {
                    throw new HandlerException(TAG + "Fatal exception when loading group meta. [" + e.getMessage() + "]");
                }
                //当路由节点加载到缓存中去后,重新查找执行else代码,对postcard进行处理
                completion(postcard);   // Reload
            }
        } else {
            //给postcard设置destination,type,priority等值,供上面讲解到的_navigation()进行使用
            // 其中routeMeta是在ARouter$$Group$$xxx的loadInto中创建的
            postcard.setDestination(routeMeta.getDestination());
            postcard.setType(routeMeta.getType());
            postcard.setPriority(routeMeta.getPriority());
            postcard.setExtra(routeMeta.getExtra());

            //如果通过build(Uri url) 进行跳转的话 通过解析url ,将传参保存进bundle中
            Uri rawUri = postcard.getUri();
            if (null != rawUri) {  
                //splitQueryParameters()就是在uri中携带的参数进行解析
                Map<String, String> resultMap = TextUtils.splitQueryParameters(rawUri);
                Map<String, Integer> paramsType = routeMeta.getParamsType();

                if (MapUtils.isNotEmpty(paramsType)) {
                    // Set value by its type, just for params which annotation by @Param
                    for (Map.Entry<String, Integer> params : paramsType.entrySet()) {
                        setValue(postcard,
                                params.getValue(),
                                params.getKey(),
                                resultMap.get(params.getKey()));
                    }

                    // Save params name which need autoinject.
                    postcard.getExtras().putStringArray(ARouter.AUTO_INJECT, paramsType.keySet().toArray(new String[]{}));
                }

                // Save raw uri
                postcard.withString(ARouter.RAW_URI, rawUri.toString());
            }
            
            //从这里也可以看出PROVIDER,FRAGMENT不需要通过拦截器
            switch (routeMeta.getType()) {
                case PROVIDER:  
                    // 如果是PROVIDER节点类型,从服务节点列表中获取,如果没有,则实例化,并保存在服务节点列表Warehouse.providers中
                  //并将实例化的对象设置给postcard的provider属性
                    Class<? extends IProvider> providerMeta = (Class<? extends IProvider>) routeMeta.getDestination();
                    IProvider instance = Warehouse.providers.get(providerMeta);
                    if (null == instance) { // There's no instance of this provider
                        IProvider provider;
                        try {
                            provider = providerMeta.getConstructor().newInstance();
                            provider.init(mContext);
                            Warehouse.providers.put(providerMeta, provider);
                            instance = provider;
                        } catch (Exception e) {
                            throw new HandlerException("Init provider failed! " + e.getMessage());
                        }
                    }
                    postcard.setProvider(instance);
                    postcard.greenChannel();    // Provider should skip all of interceptors
                    break;
                case FRAGMENT:
                    postcard.greenChannel();    // Fragment needn't interceptors
                default:
                    break;
            }
        }
    }

分析到这里,关于页面基本跳转的原理分析就已经结束了。*后就是关于获取Provider服务两种方法的源码分析。其中byName方式,和页面跳转是一模一样的。我们只需要看看byType方式即可。byType方式*后调用的是_ARouternavigation(Class<? extends T> service)

  protected <T> T navigation(Class<? extends T> service) {
        try {
            // 通过 className 获取 Postcard 对象
            Postcard postcard = LogisticsCenter.buildProvider(service.getName());

            // 兼容1.0.5 compiler sdk版本.
            if (null == postcard) { // No service, or this service in old version.
                postcard = LogisticsCenter.buildProvider(service.getSimpleName());
            }
           // 对 Postcard 对象进行处理
            LogisticsCenter.completion(postcard);
             //返回 Postcard 中的 provider 属性值
            return (T) postcard.getProvider();
        } catch (NoRouteFoundException ex) {
            logger.warning(Consts.TAG, ex.getMessage());
            return null;
        }
    }

上面代码中的completion()方法之前已经分析过了,只需要看下LogisticsCenter.buildProvider(service.getName())即可。

  public static Postcard buildProvider(String serviceName) {
        RouteMeta meta = Warehouse.providersIndex.get(serviceName);

        if (null == meta) {
            return null;
        } else {
            return new Postcard(meta.getPath(), meta.getGroup());
        }
    }

这个方法非常的简单,就是根据服务类名去仓库Warehouse.providersIndex中获去路由节点元素,然后封装在Postcard对象中。服务类清单列表Warehouse.providersIndex中的值是在初始化时缓存的。值得注意的是,PROVIDER 类型的路由节点既存在于对应的分组中,也存在于服务类清单列表中。所以,ARouter 可通过byType,byName两种方式来获取

补充

关于ARouter的基本用法上面只有*基本跳转的介绍,下面对其他一些基本使用进行下补充

  • 带参数跳转
//1.传递参数
 ARouter.getInstance().build("/test/activity1")
                        .withString("name", "老王")
                        .withInt("age", 18)
                        .withBoolean("boy", true)
                        .withLong("high", 180)
                        .withString("url", "https://a.b.c")
                        .withParcelable("pac", testParcelable)
                        .withObject("obj", testObj)
                        .navigation();

//2.直接传递Bundle
  Bundle params = new Bundle();
  ARouter.getInstance()
          .build("/test/activity1")
          .with(params)
          .navigation();

这些传参都是保存在生成的postcard对象中的mBundle属性里,然后在跳转的时候通过intent.putExtras(postcard.getExtras())达到传送参数的目的。
值得注意的是,关于对象的传递有两种,一种是withParcelable()方法,不过此方法需要传递的对象实现Parcelable接口,达到序列化的目的;另外一种是withObject()方法,此方法的原理是将实体类转换成json字符串,通过String的方式进行传递,而且使用这种方式需要实现 SerializationService,并使用@Route注解标注,下面是ARouter样例:

@Route(path = "/service/json")
public class JsonServiceImpl implements SerializationService {
    @Override
    public void init(Context context) {

    }

    @Override
    public <T> T json2Object(String text, Class<T> clazz) {
        return JSON.parseObject(text, clazz);
    }

    @Override
    public String object2Json(Object instance) {
        return JSON.toJSONString(instance);
    }
}

而且,需要在跳转到的页面获取JsonServiceImpl服务,将json字符串转换成对象。

SerializationService serializationService = ARouter.getInstance().navigation(SerializationService.class);
TestObj obj = serializationService.json2Object(getIntent().getString("obj"), TestObj.class);
  • 带返回结果跳转
ARouter.getInstance().build("/test/activity2").navigation(this, 666);

值得注意的是,这时候的 navigation需要传递activit和requestCode。

  • 获取Fragment的实例

定义一个fragment

@Route(path = "/test/fragment")
public class BlankFragment extends Fragment {
    public BlankFragment() {
        //必须要一个空的构造器
    }


    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
                             Bundle savedInstanceState) {
        TextView textView = new TextView(getActivity());
        return textView;
    }

}

获取frament

Fragment fragment = (Fragment) ARouter.getInstance().build("/test/fragment").navigation();
  • 带转场动画跳转
// 转场动画(常规方式)
 ARouter.getInstance() .build("/test/activity2")
                      .withTransition(R.anim.slide_in_bottom, R.anim.slide_out_bottom)
                      .navigation(this);

// 转场动画(API16+)
 ActivityOptionsCompat compat = ActivityOptionsCompat.makeScaleUpAnimation(v, v.getWidth() / 2, v.getHeight() / 2, 0, 0);
ARouter.getInstance().build("/test/activity2").withOptionsCompat(compat) .navigation();
  • 获取服务

服务是全局单例的,只有在*次使用到的时候才会被初始化。
暴露服务,必须实现IProvider 接口 或者其子类型

// 声明接口,其他组件通过接口来调用服务
public interface HelloService extends IProvider {
    String sayHello(String name);
}

// 实现接口
@Route(path = "/service/hello", name = "测试服务")
public class HelloServiceImpl implements HelloService {

    @Override
    public String sayHello(String name) {
    return "hello, " + name;
    }

    @Override
    public void init(Context context) {

    }
}

获取服务

//bytype
HelloService helloService1 = ARouter.getInstance().navigation(HelloService.class);
//byname
HelloService helloService2 = (HelloService) ARouter.getInstance().build("/service/hello").navigation();
  • 多模块结构
%title插图%num

app中可能存在多个模块,每个模块下面都有一个root结点,每个root结点都会管理整个模块中的group节点,每个group结点则包含了该分组下的所有页面,而每个模块允许存在多个分组,每个模块中都会有一个拦截器节点就是Interceptor结点,除此之外每个模块还会有控制拦截反转的provider结点

*后

到此,关于ARouter的基本用法以及原理分析的就全部结束了,如果有不清楚或者错误的地方,希望各位同学指出。关于ARouter拦截器,各种服务,依赖注入等更多进阶用法及源码分析会更新在后续的文章。

如果各位同学认为本文对你有一些帮助,希望能点个喜欢,谢谢!

Android 多线程之HandlerThread 完全详解

之前对线程也写过几篇文章,不过倒是没有针对android,因为java与android在线程方面大部分还是相同,不过本篇我们要介绍的是android的专属类HandlerThread,因为HandlerThread在设置思想上还是挺值得我们学习的,那么我们下面来就了解它吧,我们先来看看HandlerThread有那些特点:

HandlerThread本质上是一个线程类,它继承了Thread;
HandlerThread有自己的内部Looper对象,可以进行looper循环;
通过获取HandlerThread的looper对象传递给Handler对象,可以在handleMessage方法中执行异步任务。
创建HandlerThread后必须先调用HandlerThread.start()方法,Thread会先调用run方法,创建Looper对象。
一、HandlerThread常规使用步骤
了解完上面HandlerThread的一些特点后,我们先来看看HandlerThread使用步骤。
1.创建实例对象

1. HandlerThread handlerThread = new HandlerThread(“downloadImage”);
1
传入参数的作用主要是标记当前线程的名字,可以任意字符串。
2.启动HandlerThread线程

1. //必须先开启线程
2. handlerThread.start();
1
2
到此,我们创建完HandlerThread并启动了线程。那么我们怎么将一个耗时的异步任务投放到HandlerThread线程中去执行呢?接下来看下面步骤:
3.构建循环消息处理机制

/**
* 该callback运行于子线程
*/
class ChildCallback implements Handler.Callback {
@Override
public boolean handleMessage(Message msg) {
//在子线程中进行相应的网络请求

//通知主线程去更新UI
mUIHandler.sendMessage(msg1);
return false;
}
}

4.构建异步handler

//子线程Handler
Handler childHandler = new Handler(handlerThread.getLooper(),new ChildCallback());
1
2
第3步和第4步是构建一个可以用于异步操作的handler,并将前面创建的HandlerThread的Looper对象以及Callback接口类作为参数传递给当前的handler,这样当前的异步handler就拥有了HandlerThread的Looper对象,由于HandlerThread本身是异步线程,因此Looper也与异步线程绑定,从而handlerMessage方法也就可以异步处理耗时任务了,这样我们的Looper+Handler+MessageQueue+Thread异步循环机制构建完成,来看看一个完整的使用案例。

二、HandlerThread的使用案例
主要代码如下:
activity_handler_thread.xml

<?xml version=”1.0″ encoding=”utf-8″?>
<LinearLayout xmlns:android=”http://schemas.android.com/apk/res/android”
android:orientation=”vertical” android:layout_width=”match_parent”
android:layout_height=”match_parent”>

<ImageView
android:id=”@+id/image”
android:layout_width=”match_parent”
android:layout_height=”match_parent” />
</LinearLayout>

HandlerThreadActivity.java

package com.zejian.handlerlooper;
import android.app.Activity;
import android.graphics.Bitmap;
import android.graphics.BitmapFactory;
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
import android.os.Message;
import android.widget.ImageView;
import com.zejian.handlerlooper.model.ImageModel;
import com.zejian.handlerlooper.util.LogUtils;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.URL;
/**
* Created by zejian on 16/9/2.
*/
public class HandlerThreadActivity extends Activity {
/**
* 图片地址集合
*/
private String url[]={
“https://img-blog.csdn.net/20160903083245762”,
“https://img-blog.csdn.net/20160903083252184”,
“https://img-blog.csdn.net/20160903083257871”,
“https://img-blog.csdn.net/20160903083257871”,
“https://img-blog.csdn.net/20160903083311972”,
“https://img-blog.csdn.net/20160903083319668”,
“https://img-blog.csdn.net/20160903083326871”
};
private ImageView imageView;
private Handler mUIHandler = new Handler(){
@Override
public void handleMessage(Message msg) {
LogUtils.e(“次数:”+msg.what);
ImageModel model = (ImageModel) msg.obj;
imageView.setImageBitmap(model.bitmap);
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_handler_thread);
imageView= (ImageView) findViewById(R.id.image);
//创建异步HandlerThread
HandlerThread handlerThread = new HandlerThread(“downloadImage”);
//必须先开启线程
handlerThread.start();
//子线程Handler
Handler childHandler = new Handler(handlerThread.getLooper(),new ChildCallback());
for(int i=0;i<7;i++){
//每个1秒去更新图片
childHandler.sendEmptyMessageDelayed(i,1000*i);
}
}
/**
* 该callback运行于子线程
*/
class ChildCallback implements Handler.Callback {
@Override
public boolean handleMessage(Message msg) {
//在子线程中进行网络请求
Bitmap bitmap=downloadUrlBitmap(url[msg.what]);
ImageModel imageModel=new ImageModel();
imageModel.bitmap=bitmap;
imageModel.url=url[msg.what];
Message msg1 = new Message();
msg1.what = msg.what;
msg1.obj =imageModel;
//通知主线程去更新UI
mUIHandler.sendMessage(msg1);
return false;
}
}
private Bitmap downloadUrlBitmap(String urlString) {
HttpURLConnection urlConnection = null;
BufferedInputStream in = null;
Bitmap bitmap=null;
try {
final URL url = new URL(urlString);
urlConnection = (HttpURLConnection) url.openConnection();
in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
bitmap=BitmapFactory.decodeStream(in);
} catch (final IOException e) {
e.printStackTrace();
} finally {
if (urlConnection != null) {
urlConnection.disconnect();
}
try {
if (in != null) {
in.close();
}
} catch (final IOException e) {
e.printStackTrace();
}
}
return bitmap;
}
}

在这个案例中,我们创建了两个Handler,一个用于更新UI线程的mUIHandler和一个用于异步下载图片的childHandler。*终的结果是childHandler会每个隔1秒钟通过sendEmptyMessageDelayed方法去通知ChildCallback的回调函数handleMessage方法去下载图片并告诉mUIHandler去更新UI界面,以上便是HandlerThread常规使用,实际上在android比较典型的应用是IntentService,这个我们将放在下篇分析,这里就先不深入了,案例运行截图如下:

三.HandlerThread源码解析
HandlerThread的源码不多只有140多行,那就一步一步来分析吧,先来看看其构造函数

/**
* Handy class for starting a new thread that has a looper. The looper can then be
* used to create handler classes. Note that start() must still be called.
*/
public class HandlerThread extends Thread {
int mPriority;//线程优先级
int mTid = -1;
Looper mLooper;//当前线程持有的Looper对象
public HandlerThread(String name) {
super(name);
mPriority = Process.THREAD_PRIORITY_DEFAULT;
}

/**
* Constructs a HandlerThread.
* @param name
* @param priority The priority to run the thread at. The value supplied must be from
* {@link android.os.Process} and not from java.lang.Thread.
*/
public HandlerThread(String name, int priority) {
super(name);
mPriority = priority;
}

/**
* Call back method that can be explicitly overridden if needed to execute some
* setup before Looper loops.
*/
protected void onLooperPrepared() {
}

从源码可以看出HandlerThread继续自Thread,构造函数的传递参数有两个,一个是name指的是线程的名称,一个是priority指的是线程优先级,我们根据需要调用即可。其中成员变量mLooper就是HandlerThread自己持有的Looper对象。onLooperPrepared()该方法是一个空实现,是留给我们必要时可以去重写的,但是注意重写时机是在Looper循环启动前,再看看run方法:

@Override
public void run() {
mTid = Process.myTid();
Looper.prepare();
synchronized (this) {
mLooper = Looper.myLooper();
notifyAll(); //唤醒等待线程
}
Process.setThreadPriority(mPriority);
onLooperPrepared();
Looper.loop();
mTid = -1;
}

前面我们在HandlerThread的常规使用中分析过,在创建HandlerThread对象后必须调用其start()方法才能进行其他操作,而调用start()方法后相当于启动了线程,也就是run方法将会被调用,而我们从run源码中可以看出其执行了Looper.prepare()代码,这时Looper对象将被创建,当Looper对象被创建后将绑定在当前线程(也就是当前异步线程),这样我们才可以把Looper对象赋值给Handler对象,进而确保Handler对象中的handleMessage方法是在异步线程执行的。接着将执行代码:

synchronized (this) {
mLooper = Looper.myLooper();
notifyAll(); //唤醒等待线程
}

这里在Looper对象创建后将其赋值给HandlerThread的内部变量mLooper,并通过notifyAll()方法去唤醒等待线程,*后执行Looper.loop();代码,开启looper循环语句。那这里为什么要唤醒等待线程呢?我们来看看,getLooper方法

public Looper getLooper() {
//先判断当前线程是否启动了
if (!isAlive()) {
return null;
}
// If the thread has been started, wait until the looper has been created.
synchronized (this) {
while (isAlive() && mLooper == null) {
try {
wait();//等待唤醒
} catch (InterruptedException e) {
}
}
}
return mLooper;
}

事实上可以看出外部在通过getLooper方法获取looper对象时会先先判断当前线程是否启动了,如果线程已经启动,那么将会进入同步语句并判断Looper是否为null,为null则代表Looper对象还没有被赋值,也就是还没被创建,此时当前调用线程进入等待阶段,直到Looper对象被创建并通过 notifyAll()方法唤醒等待线程,*后才返回Looper对象,之所以需要等待唤醒机制,是因为Looper的创建是在子线程中执行的,而调用getLooper方法则是在主线程进行的,这样我们就无法保障我们在调用getLooper方法时Looper已经被创建,到这里我们也就明白了在获取mLooper对象时会存在一个同步的问题,只有当线程创建成功并且Looper对象也创建成功之后才能获得mLooper的值,HandlerThread内部则通过等待唤醒机制解决了同步问题。

public boolean quit() {
Looper looper = getLooper();
if (looper != null) {
looper.quit();
return true;
}
return false;
}
public boolean quitSafely() {
Looper looper = getLooper();
if (looper != null) {
looper.quitSafely();
return true;
}
return false;
}

从源码可以看出当我们调用quit方法时,其内部实际上是调用Looper的quit方法而*终执行的则是MessageQueue中的removeAllMessagesLocked方法(Handler消息机制知识点),该方法主要是把MessageQueue消息池中所有的消息全部清空,无论是延迟消息(延迟消息是指通过sendMessageDelayed或通过postDelayed等方法发送)还是非延迟消息。
当调用quitSafely方法时,其内部调用的是Looper的quitSafely方法而*终执行的是MessageQueue中的removeAllFutureMessagesLocked方法,该方法只会清空MessageQueue消息池中所有的延迟消息,并将消息池中所有的非延迟消息派发出去让Handler去处理完成后才停止Looper循环,quitSafely相比于quit方法安全的原因在于清空消息之前会派发所有的非延迟消息。*后需要注意的是Looper的quit方法是基于API 1,而Looper的quitSafely方法则是基于API 18的。
好~,到此对于HandlerThread的所有分析就到此完结。

友情链接: SITEMAP | 旋风加速器官网 | 旋风软件中心 | textarea | 黑洞加速器 | jiaohess | 老王加速器 | 烧饼哥加速器 | 小蓝鸟 | tiktok加速器 | 旋风加速度器 | 旋风加速 | quickq加速器 | 飞驰加速器 | 飞鸟加速器 | 狗急加速器 | hammer加速器 | trafficace | 原子加速器 | 葫芦加速器 | 麦旋风 | 油管加速器 | anycastly | INS加速器 | INS加速器免费版 | 免费vqn加速外网 | 旋风加速器 | 快橙加速器 | 啊哈加速器 | 迷雾通 | 优途加速器 | 海外播 | 坚果加速器 | 海外vqn加速 | 蘑菇加速器 | 毛豆加速器 | 接码平台 | 接码S | 西柚加速器 | 快柠檬加速器 | 黑洞加速 | falemon | 快橙加速器 | anycast加速器 | ibaidu | moneytreeblog | 坚果加速器 | 派币加速器 | 飞鸟加速器 | 毛豆APP | PIKPAK | 安卓vqn免费 | 一元机场加速器 | 一元机场 | 老王加速器 | 黑洞加速器 | 白石山 | 小牛加速器 | 黑洞加速 | 迷雾通官网 | 迷雾通 | 迷雾通加速器 | 十大免费加速神器 | 猎豹加速器 | 蚂蚁加速器 | 坚果加速器 | 黑洞加速 | 银河加速器 | 猎豹加速器 | 海鸥加速器 | 芒果加速器 | 小牛加速器 | 极光加速器 | 黑洞加速 | movabletype中文网 | 猎豹加速器官网 | 烧饼哥加速器官网 | 旋风加速器度器 | 哔咔漫画 | PicACG | 雷霆加速