Jekyll2023-03-10T14:47:14+08:00http://www.cat-wish.cn/feed.xml个人技术hx胡大嘴的个人技术知识库Allen让 Tapd 的源码关联功能支持 Gitee 平台2019-09-14T00:00:00+08:002019-09-14T00:00:00+08:00http://www.cat-wish.cn/2019/09/14/gitee-and-tapd<p>Tapd 是腾讯提供的越来越完善的项目管理工具,Gitee 是国内相对比较稳的代码托管平台。本文记录了让 Tapd 的源码关联功能支持 Gitee 平台的方法,及摸索过程中遇到的问题的解决步骤。</p>
<h2 id="背景">背景</h2>
<p>想要使用 Tapd + Gitee 的组合来管理业余项目,但 Tapd 目前官方支持的代码托管平台只有 Gitlab、GitHub 和腾讯工蜂,并不能直接支持 Gitee,直觉上 Gitee 是基于 Gitlab 开发的,所以尝试在 Tapd 里开启了 Gitlab 服务,然后直接将 webhook 地址配置到 Gitee 项目里,却并不能生效。</p>
<h2 id="求索">求索</h2>
<p>这种问题我应该肯定不是第一个遇到,于是在 Tapd 的论坛里搜索 Gitee 关键字,果然在帖子 <a href="https://www.tapd.cn/forum/view/67001">https://www.tapd.cn/forum/view/67001</a> 里找到了方案。</p>
<h2 id="方案">方案</h2>
<p>方案的原理简单来说就是 Gitee 在触发 webhook 时,向目标网址发起的请求和 GitLab 很雷同,只是有个别 Header 的名字不一样,但缺失特定的 Header 信息后无法正常触发 Tapd 的源码关联,所以可以通过 Nginx 反向代理来将缺失的 Header 补全,然后将请求转发给 Tapd 即可。</p>
<h3 id="方案示意图">方案示意图</h3>
<p><img src="/images/posts/tools/webhook-gitee.png" alt="" /></p>
<p>对比直接支持的 Gitlab 的示意:</p>
<p><img src="/images/posts/tools/webhook-gitlab.png" alt="" /></p>
<p>所以前提条件是你有一个可以在公网访问到的 Nginx 服务器,且可以自己修改配置。</p>
<p>网友介绍方案及原理的 GitHub 仓库:<a href="https://github.com/notzheng/Tapd-Git-Hooks">https://github.com/notzheng/Tapd-Git-Hooks</a></p>
<h3 id="操作步骤">操作步骤</h3>
<ol>
<li>
<p>在 Tapd 项目里开启 Gitlab 服务;</p>
</li>
<li>
<p>在你可用的公网 Nginx 服务器的配置文件里添加一段配置:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code> server {
listen 80;
server_name tapdhooks.yourdomain.com;
location ~ ^/(\d+)/([a-z0-9]+) {
proxy_set_header X-Gitlab-Event $http_X_Gitee_Event ;
proxy_set_header X-Gitlab-Token $http_X_Gitee_Token ;
proxy_pass https://hook.tapd.cn ;
}
}
</code></pre></div> </div>
</li>
<li>
<p>将 tapdhooks.yourdomain.com 解析到该 Nginx 服务器 IP;</p>
</li>
<li>
<p>将替换过域名的 webhook 链接配置到 Gitee 项目里;</p>
<p>比如原 webhook 链接:https://hook.tapd.cn/32198210/adcc961bc533c74a257ef96295812fa7</p>
<p>将 <code class="language-plaintext highlighter-rouge">https://hook.tapd.cn</code> 替换成 <code class="language-plaintext highlighter-rouge">http://tapdhook.yourdomain.com</code> 得到新的链接</p>
<p>http://tapdhooks.yourdomain.com/32198210/adcc961bc533c74a257ef96295812fa7</p>
</li>
</ol>
<p>搞定!</p>
<h3 id="小插曲">小插曲</h3>
<p>事情就是这么简单,但往往实操的时候不会这么顺利,会有些小插曲,比如我就遇到了。</p>
<p>如上配置之后,我向 Gitee push 代码却发现并没有在 Tapd 看到源码关联,在 Gitee 配置 webhook 的地方 test 了一下,报 502 bad gateway。</p>
<p>把 test 请求在 postman 里构造出来,然后使用 hook.tapd.cn 的原链接,请求是成功的,加上 Nginx 新增的 Header,也没有问题,但换回自己域名的链接就报 502 了。在 Nginx 服务器上将错误日志打印出来:</p>
<blockquote>
<p>2019/09/12 15:51:25 [crit] 24721#24721: *287854 SSL_do_handshake() failed (SSL: error:1411B041:SSL routines:SSL3_GET_NEW_SESSION_TICKET:malloc failure) while SSL handshaking to upstream, client: 28.39.21.123, server: tapdhooks.yourdomain.com, request: “POST /32198210/adcc961bc533c74a257ef96295812fa7 HTTP/1.1”, upstream: “https://119.29.122.86:443/32198210/adcc961bc533c74a257ef96295812fa7”, host: “tapdhooks.yourdomain.com”</p>
</blockquote>
<p>所以是 Nginx 向 https://hook.tapd.cn 链接发起请求时,SSL 握手错误了。</p>
<p>在网上搜了一些网友们的帖子后,得出的结论基本是因为客户端与服务端支持的 SSL protocol 版本不一致导致的,用工具查了一下 Tapd 服务器支持的 protocol 版本是 TLSv2,而我 Nginx 服务器的 OpenSSL 版本较低,可能不支持这个,于是先是升级了服务器上的 OpenSSL 的版本,然后通过重新编译升级了 Nginx 的 OpenSSL 版本,之后问题解决。这两步自己维护 Ngninx 服务器的同学应该不在话下,在此不再赘述,以下是我参考的链接:</p>
<ul>
<li>升级服务器 OpenSSL 版本: <a href="https://blog.csdn.net/l1028386804/article/details/53165252">CentOS之——升级openssl为最新版</a></li>
<li>升级 Nginx 的 OpenSSL 版本:<a href="https://my.oschina.net/u/1449160/blog/220415">nginx旧版本openssl升级</a></li>
</ul>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://www.tapd.cn/forum/view/67001">分享一个让源码关联支持Gogs/Gitee等平台的解决方案</a></li>
<li><a href="https://github.com/notzheng/Tapd-Git-Hooks">Tapd Git Hooks</a></li>
<li><a href="https://my.oschina.net/u/1449160/blog/220415">nginx旧版本openssl升级</a></li>
<li><a href="https://blog.csdn.net/l1028386804/article/details/53165252">CentOS之——升级openssl为最新版</a></li>
</ul>AllenTapd 是腾讯提供的越来越完善的项目管理工具,Gitee 是国内相对比较稳的代码托管平台。本文记录了让 Tapd 的源码关联功能支持 Gitee 平台的方法,及摸索过程中遇到的问题的解决步骤。记一个折磨了我一天半的 Bug2019-05-25T00:00:00+08:002019-05-25T00:00:00+08:00http://www.cat-wish.cn/2019/05/25/a-stupid-bug<p>最近开始学习后台开发,虽然与我以前从事的 Android 开发一样都是使用 Java 语言,但是技术栈完全不同,有太多的必备的「新」概念要去学习,而在对它们,以及别人写的代码有充分的了解之前,就可能会遇上这种一杯茶,一根烟,一个 Bug 一天根本改不完的情况。</p>
<p>最近遇见的这个 Bug 是在修改项目遗留的问题时偶然发现的,简而言之就是这样:</p>
<p><strong>服务 A</strong> 在从外界接收到推送的一条数据后,将数据插入到库里,然后通过 MQ 推送一条消息给 <strong>服务 B</strong>,<strong>服务 B</strong> 会根据收到的消息进行一些处理,其中包括远程调用 <strong>服务 A</strong> 的方法去查询这条数据,但是在测试环境总是报查询不到这条数据。</p>
<p>遇到问题之后,先进行了一些排查:</p>
<ul>
<li>
<p>怀疑传参或者数据插库没有成功,于是将查询参数打印出来,手动复制参数到库里去查——有数据;</p>
</li>
<li>
<p>怀疑实际执行的 SQL 有问题,于是请同事帮忙配置 MyBatis 在日志里输出 SQL,原样复制出来去库里查——有数据;</p>
</li>
<li>
<p>在本地连接测试环境数据库,代码里下断点调试——能正常取到数据;</p>
</li>
</ul>
<p><img src="/images/posts/java/you-kidding-me.jpg" alt="" /></p>
<p>纳闷了一阵以后,继续排查:</p>
<ul>
<li>
<p>怀疑测试环境程序数据库连接有问题,于是测试了一些其它查库的功能——数据正常;</p>
</li>
<li>
<p>怀疑测试环境的包有问题,于是请运维同事将 jar 包从容器里拷贝下来,核对配置——没问题;</p>
</li>
<li>
<p>怀疑测试环境远程调用失败了,于是在远程调用处加日志——没有异常;</p>
</li>
</ul>
<p><img src="/images/posts/java/what-is-wrong.jpeg" alt="" /></p>
<ul>
<li>
<p>怀疑测试环境注册了多余的 <strong>服务 A</strong> 的节点,于是去 Dubbo Admin 里核对节点——数量正常,网段正常;</p>
</li>
<li>
<p>怀疑测试环境的部署的 <strong>服务 A</strong> 的某个节点部署有问题,于是请运维同事一个一个 telnet 上去手动执行远程调用——能正常取到数据;</p>
</li>
<li>
<p>在一条失败 case 之后,马上向 <strong>服务 B</strong> 手动再次推送相同的消息——能取到数据;</p>
</li>
</ul>
<p><img src="/images/posts/java/this-unscientific.jpeg" alt="" /></p>
<p>直到我终于留意到一个现象:从日志来看,<strong>服务 A</strong> 插库与 <strong>服务 B</strong> 远程调用 <strong>服务 A</strong> 的方法的时间只相差 1 毫秒。会不会是一切发生得太快了,库里还查不到刚刚写入的数据?抑或者查询的时候插库还根本没有生效?</p>
<p>带着这个疑惑我终于认真去看插库并发消息那块的代码了,于是就看到这样一段代码:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="nd">@Transactional</span><span class="o">(...)</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">doSomething</span><span class="o">()</span> <span class="o">{</span>
<span class="o">...</span>
<span class="c1">// 插入数据</span>
<span class="c1">// 发送消息</span>
<span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>是的没错,插入数据和发送消息写在了一个事务里面。虽然我对数据库了解不多,但对事务的特性还是有所了解——发送消息的时候,数据库里确实还没有刚刚插入的数据,事务提交后才会生效,也就是说,<strong>服务 B</strong> 收到消息后远程调用回 <strong>服务 A</strong> 想查找刚刚插入的数据,能否查到全凭运气,取决于此时事务已经执行完。</p>
<p>问题时序示意:</p>
<p><img src="/images/posts/java/a-stupid-bug-wrong-sequence.png" alt="" /></p>
<p>要确保消息发出时数据库里已经存在数据了也很简单,将事务粒度控制一下,只包含插入数据这块逻辑即可,插入成功了再发送消息。</p>
<p><em>PS:如果对消息投递可靠性要求高,可能需要对投递消息失败的情况做一些补偿机制。</em></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">doSomething</span><span class="o">()</span> <span class="o">{</span>
<span class="o">...</span>
<span class="c1">// 事务开始</span>
<span class="c1">// 插入数据</span>
<span class="c1">// 事务结束</span>
<span class="k">if</span> <span class="o">(</span><span class="n">插入数据成功</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 发送消息</span>
<span class="o">}</span>
<span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>正常时序示意:</p>
<p><img src="/images/posts/java/a-stupid-bug-normal-sequence.png" alt="" /></p>
<p>总结:</p>
<ol>
<li>
<p>在理解别人写的逻辑的时候不要做预设,你认为别人不可能犯如此低级的错误而直接排除在外的情况,可能恰好是问题所在;</p>
</li>
<li>
<p>在排查可能是时序导致的问题时,少用断点调试,用日志更合适;</p>
</li>
<li>
<p>本地调试时尽量将场景模拟完整一点,从中途某一环开始则有可能越过问题触发条件而无法复现。</p>
</li>
</ol>Allen最近开始学习后台开发,虽然与我以前从事的 Android 开发一样都是使用 Java 语言,但是技术栈完全不同,有太多的必备的「新」概念要去学习,而在对它们,以及别人写的代码有充分的了解之前,就可能会遇上这种一杯茶,一根烟,一个 Bug 一天根本改不完的情况。我的 2018 盘点2019-01-20T00:00:00+08:002019-01-20T00:00:00+08:00http://www.cat-wish.cn/2019/01/20/my-2018<p>又到了回顾以明得失,展望以知进退的时候。</p>
<p>首先想感慨一下我的拖延癌应该已经是确定到了晚期了,2018 年伊始的时候也不是没想过做一下 2017 年盘点,春节后第一次出国游走,回来之后也想着要写一个游记,而现在已经是 2019 年一月的下旬了,它俩还没有影子……咳咳嗯,忘记它们吧。</p>
<p>2018 年实际并没有列出一个明确的目标和计划,来说必须做成哪些事情,所以在回顾的时候,发现并没有参照和评价的标准,这其实是一件很恐怖的事情。目标是如此的重要,而我,可能过了一个没有明确目标的一年。</p>
<h2 id="大事记">大事记</h2>
<ul>
<li>
<p>我们结婚了。</p>
<p>异地恋三年,然后在回武汉大半年后,终于携手小别同学一起走进婚姻的殿堂。回首这不算漫长的岁月,有甜蜜,有煎熬,有过许多争执,所幸最后都能求大同存小异。希望余生每一个或难忘或平淡的日子,都有你一起。</p>
</li>
<li>
<p>转型技术管理。</p>
<p>身处职业焦虑期的我,最近几年也在一直在思索转型之路,寻觅着转型之机。所以在有机会出现,又蒙新老领导举荐的情形下,我踏上了由程序员转向技术管理的征途,由一名自认合格的程序员摇身一变,成为了一名暂不合格的技术管理。而焦虑其实并不会消失,只是会由一种转为另一种,保持危机感,路漫漫其修远兮……</p>
</li>
</ul>
<h2 id="分类记录">分类记录</h2>
<p>按例会罗列 2018 年的一些数字,及与以前的简单对比。</p>
<h3 id="阅读和影音娱乐">阅读和影音娱乐</h3>
<p>数据主要来自豆瓣上的记录。</p>
<p><img src="/images/blog/2018-books-and-movies.jpeg" alt="" /></p>
<p>从豆瓣上统计了下最近四年标记的数据,对比一下发现 2017 年可能于我来说真的是艰难的一年——最近几年里标记非技术类书籍数量的低谷,所谓「闲书」,有闲情逸志了才会想起来去看的书,无关忙碌与否,更多反映的是心情与心态。</p>
<p>2018 年情况似乎有所好转,书籍数量回升,电影电视剧数量锐减,这倒不是因为我花在电影电视上的时间少了,跟着小别同志电视剧都看了几部了,应该只是有很多忘标记了……</p>
<h3 id="网络活动">网络活动</h3>
<h4 id="github">GitHub</h4>
<p>贡献日历:</p>
<p><img src="/images/blog/2018-github.png" alt="" /></p>
<ul>
<li>
<p>过去一年总共有 345 次 Commit/Issue/PR 记录,比 2017 年的 815 次下降明显;</p>
</li>
<li>
<p>最长连击记录只有 8 天(2017 年是 28 天),2018-04-26 到 2018-05-03;</p>
</li>
<li>
<p>有 5 个自然周没有任何记录。</p>
</li>
</ul>
<p>项目方面,awesome-adb 的 Star 数突破了 5K,vim-markdown-toc 的 Star 数超过 200。</p>
<p>另外 Follower 数超过了 500。</p>
<p>与之前几年相比,花在 GitHub 上的时间明显少了。这是一个值得警惕的现象——即使如今职业身份发生了变化,但技术仍是立身之本,不可轻易荒废。</p>
<h4 id="技术社区与博客">技术社区与博客</h4>
<p>与前面两年在掘金、知乎专栏的相对高产相比,这一年我在这方面比较沉默,看得少,输出也少,只有寥寥两篇。</p>
<p>2018 年在个人博客上只发表了 3 篇文章,没错……平均每个季度不足一篇。</p>
<p>期待后续能多做一些复盘,然后将有价值的经验与教训记录成文——不局限于技术,也包括管理经验等等。</p>
<h3 id="运动">运动</h3>
<p>今年的运动量以羽毛球为主,辅以跑步和少量游泳。</p>
<ul>
<li>
<p>羽毛球</p>
<p>作为公司武汉办公室羽毛球活动的主要发起者,2018 年共组织活动 30 次,另外参加民间小伙伴们组织的活动 15 次以上。技术上进步不大,最大的收获是结识了一帮可爱的小伙伴。</p>
</li>
<li>
<p>跑步</p>
<p>主要是在公司与几个小伙伴偶尔下班了会一起去跑,共计 28 次,127 公里。</p>
<p>今年收获的唯一一枚奖牌是光谷半程马拉松活动的健康跑完赛奖牌。</p>
<p><img src="/images/blog/2018-guanggu-running.jpeg" alt="" /></p>
</li>
<li>
<p>游泳</p>
<p>暑假期间去湖北经济学院游过几次,考了武汉的深水证。</p>
</li>
</ul>
<p>主要受益于打羽毛球频度还可以,2018 年体重没有增长,希望自己能持续健康地打下去。</p>
<h3 id="工作">工作</h3>
<p>2018 年换过两次部门,一次主动一次被动,工作岗位也由 Android 开发工程师转为了移动端技术经理。</p>
<p>角色的转变带来了许多新的挑战,以前管理自己得心应手的方式,放到团队和项目的维度可能就不一定好使了,如何打造好的流程与团队,这将是我接下来很长一段时间要持续学习的课题。</p>
<h3 id="游戏">游戏</h3>
<p>这一年不怎么玩游戏了。平时没啥感觉,就是去参加年会的时候发现一堆人围在一起打农药,另一堆人四处溜达或者大眼瞪小眼,有点逗。</p>
<h3 id="旅行">旅行</h3>
<ul>
<li>
<p>2018-02 泰国(曼谷、普吉岛)</p>
<p><img src="/images/blog/2018-phuket.jpg" alt="" /></p>
</li>
<li>
<p>2018-10 河南(郑州、洛阳)</p>
<p><img src="/images/blog/2018-luoyang.jpg" alt="" /></p>
</li>
</ul>
<p>基本达成我和小别同学一起制定的每年一次国内,一次国外的小目标。</p>
<p>近几年悄然发生的一个变化是:以前每到一个地方,可能想着好不容易来一回,该看的该吃的都不想错过。如今我们已经学会享受假期,行程安排是怎么舒服怎么来,真正的放松心情,愉悦自己。</p>
<h3 id="其它">其它</h3>
<p>另外值得一说的还有,今年从水兵同学手上收了一把木吉他,开始了自学之路。周末有空的时候会拿出来练练,练得不勤所以进展也不大,目前只学会了两首最简单的弹唱,但纯把它作为一个业余爱好来讲,我对此还算满意,表扬一下自己。</p>
<p>练琴的时候偶尔会想起,高中时期攒钱买了第一把吉他,只自学了一点点基础知识,后来大学时期送给了表弟;包子离开帝都的时候将他的琴送给了我,也没怎么练过,我离开帝都的时候又留给了另一位同学……不知道它们如今怎么样了哈哈。</p>
<h2 id="结语">结语</h2>
<p>2018 年整体来说过得不坏,升职加薪了,业余也过得还算精彩。暴露的一个大问题就是没有制定很明确的目标和计划。</p>
<p>2019 年得认真做个年度目标和计划,并尽力去达成(此处可以考虑采用 OKR)。毕竟立 FLAG 是用来破的是一回事,立都不立又是另一回事了…… :-P</p>
<p>另外就是要多陪陪家人了,现在离家并不远,但回家的频次并不比在帝都时相距一千多公里时高,不要子欲养而亲不待时后悔。</p>
<p>好了最后给大家拜个晚年,祝大家新年快乐,笃定前行。</p>Allen又到了回顾以明得失,展望以知进退的时候。一份简明的 Markdown 笔记与教程2018-09-06T00:00:00+08:002018-09-06T00:00:00+08:00http://www.cat-wish.cn/2018/09/06/markdown-intro<p>为部门内知识分享准备的素材,记录了 Markdown 的优点、应用场景和编辑工具,介绍了标准语法与扩展语法,以及一些应用 Markdown 的奇技淫巧。个人使用 Markdown 的经验持续补充中,最新完整版请参见</p>
<p><a href="https://github.com/mzlogin/markdown-intro">https://github.com/mzlogin/markdown-intro</a></p>
<hr />
<p>自从 2014 年左右接触到 Markdown 以来,对它的使用就一发而不可收拾。从最开始使用它在 GitHub Pages 里写博客,到用它编辑项目的 README 文件,再到撰写开发文档,编辑微信公众号文章和邮件内容等等,这期间也见证了它在各类平台和网站上的普及和被原生支持,可以说,Markdown 如今已经渗透了我在技术和网络活动的方方面面,成为了我撰写文本文档的首选。</p>
<p>那么首先我们一起来看一下它的「定义」:</p>
<blockquote>
<p>Markdown 是一种轻量级标记语言,创始人为 John Gruber。它允许人们「使用易读易写的纯文本格式编写文档,然后转换成有效的 XHTML(或者 HTML)文档」。——维基百科</p>
</blockquote>
<p>本文档的目的不在于面面俱到地介绍 Markdown,只是作为我对其理解的笔记整理,希望能同时帮助一些对 Markdown 感兴趣的人快速上手,或是作为一个工具,供对其已经有所了解的人在需要时参考。</p>
<p>接下来请随我一起深入了解这门并不神秘的实用标记语言。</p>
<p><strong>目录</strong></p>
<ul id="markdown-toc">
<li><a href="#背景" id="markdown-toc-背景">背景</a> <ul>
<li><a href="#优点" id="markdown-toc-优点">优点</a></li>
<li><a href="#使用场景" id="markdown-toc-使用场景">使用场景</a></li>
<li><a href="#编辑工具" id="markdown-toc-编辑工具">编辑工具</a></li>
</ul>
</li>
<li><a href="#语法" id="markdown-toc-语法">语法</a> <ul>
<li><a href="#标题" id="markdown-toc-标题">标题</a></li>
</ul>
</li>
<li><a href="#atx-style-一级标题" id="markdown-toc-atx-style-一级标题">atx-style 一级标题</a> <ul>
<li><a href="#二级标题" id="markdown-toc-二级标题">二级标题</a> <ul>
<li><a href="#六级标题" id="markdown-toc-六级标题">六级标题</a></li>
</ul>
</li>
</ul>
</li>
<li><a href="#setext-style-一级标题" id="markdown-toc-setext-style-一级标题">Setext-style 一级标题</a> <ul>
<li><a href="#二级标题-1" id="markdown-toc-二级标题-1">二级标题</a> <ul>
<li><a href="#段落" id="markdown-toc-段落">段落</a></li>
<li><a href="#行内格式" id="markdown-toc-行内格式">行内格式</a></li>
<li><a href="#引用块" id="markdown-toc-引用块">引用块</a></li>
<li><a href="#引用块内的标题" id="markdown-toc-引用块内的标题">引用块内的标题</a></li>
<li><a href="#超链接" id="markdown-toc-超链接">超链接</a></li>
<li><a href="#图片" id="markdown-toc-图片">图片</a></li>
<li><a href="#列表" id="markdown-toc-列表">列表</a></li>
<li><a href="#代码块" id="markdown-toc-代码块">代码块</a></li>
<li><a href="#水平分割线" id="markdown-toc-水平分割线">水平分割线</a></li>
<li><a href="#嵌入-html" id="markdown-toc-嵌入-html">嵌入 HTML</a></li>
</ul>
</li>
<li><a href="#扩展语法" id="markdown-toc-扩展语法">扩展语法</a> <ul>
<li><a href="#表格" id="markdown-toc-表格">表格</a></li>
<li><a href="#任务列表" id="markdown-toc-任务列表">任务列表</a></li>
<li><a href="#删除线" id="markdown-toc-删除线">删除线</a></li>
<li><a href="#自动链接" id="markdown-toc-自动链接">自动链接</a></li>
<li><a href="#emoji" id="markdown-toc-emoji">emoji</a></li>
</ul>
</li>
<li><a href="#奇技淫巧" id="markdown-toc-奇技淫巧">奇技淫巧</a> <ul>
<li><a href="#画流程图和时序图" id="markdown-toc-画流程图和时序图">画流程图和时序图</a></li>
<li><a href="#插入数学公式" id="markdown-toc-插入数学公式">插入数学公式</a></li>
<li><a href="#用-markdown-做-ppt" id="markdown-toc-用-markdown-做-ppt">用 Markdown 做 PPT</a></li>
<li><a href="#用-markdown-写微信公众号" id="markdown-toc-用-markdown-写微信公众号">用 Markdown 写微信公众号</a></li>
<li><a href="#更多" id="markdown-toc-更多">更多</a></li>
</ul>
</li>
<li><a href="#参考" id="markdown-toc-参考">参考</a></li>
</ul>
</li>
</ul>
<h2 id="背景">背景</h2>
<h3 id="优点">优点</h3>
<ol>
<li>
<p>专注于文字内容;</p>
</li>
<li>
<p>纯文本,易读易写,可以方便地纳入版本控制;</p>
</li>
<li>
<p>语法简单,没有什么学习成本,能轻松在码字的同时做出美观大方的排版。</p>
</li>
</ol>
<h3 id="使用场景">使用场景</h3>
<ul>
<li>
<p>各类代码托管平台</p>
<p>主流的代码托管平台,如 GitHub、GitLab、BitBucket、Coding、Gitee 等等,都支持 Markdown 语法,很多开源项目的 README、开发文档、帮助文档、Wiki 等都用 Markdown 写作。</p>
</li>
<li>
<p>技术社区和写作平台</p>
<p>StackOverflow、CSDN、掘金、简书、GitBook、有道云笔记</p>
</li>
<li>
<p>论坛</p>
<p>V2EX、光谷社区</p>
</li>
</ul>
<p>个人感觉比较遗憾的一点是各平台可能采用不同语言实现的 Markdown 解析引擎,或采用同一解析引擎的不同版本,而且可能有不同程度的定制与扩展,这导致在不同平台上使用 Markdown 写作时体验并不完全一致。不过幸好对于大家公认的一些标准语法,各家都是支持的。</p>
<h3 id="编辑工具">编辑工具</h3>
<p>理论上任何一款文本编辑器都能用于编辑 Markdown 文档,它们分别提供了不同程度的语法高亮、预览等功能,以下只是列举其中一部分,选择自己称手的即可。</p>
<ul>
<li>
<p>现代编辑器</p>
<p>VSCode / Atom</p>
</li>
<li>
<p>传统编辑器</p>
<p>Vim / Emacs / Sublime Text / Notepad++</p>
</li>
<li>
<p>IDE 自带编辑器</p>
<p>IntelliJ IDEA / Android Studio / WebStorm</p>
</li>
<li>
<p>专用编辑器</p>
<p>Ulysses / Mou / Typora / Markpad</p>
</li>
<li>
<p>在线编辑器</p>
<p>各种支持 Markdown 的网站都提供了在线编辑器</p>
</li>
</ul>
<h2 id="语法">语法</h2>
<h3 id="标题">标题</h3>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code># atx-style 一级标题
## 二级标题
###### 六级标题
Setext-style 一级标题
===
二级标题
---
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<blockquote>
<h1 id="atx-style-一级标题">atx-style 一级标题</h1>
<h2 id="二级标题">二级标题</h2>
<h6 id="六级标题">六级标题</h6>
<h1 id="setext-style-一级标题">Setext-style 一级标题</h1>
<h2 id="二级标题-1">二级标题</h2>
</blockquote>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><h1></span>atx-style 一级标题<span class="nt"></h1></span>
<span class="nt"><h2></span>二级标题<span class="nt"></h2></span>
<span class="nt"><h6></span>六级标题<span class="nt"></h6></span>
<span class="nt"><h1></span>Setext-style 一级标题<span class="nt"></h1></span>
<span class="nt"><h2></span>二级标题<span class="nt"></h2></span>
</code></pre></div></div>
<h3 id="段落">段落</h3>
<p>中间没有空行的连续不断的几行文字被视为一个段落。</p>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>白日依山尽,
黄河入海流。
(句号后面没空格)
欲穷千里目,
更上一层楼。
(句号后面有俩空格)
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<p>白日依山尽,</p>
<p>黄河入海流。
(句号后面没空格)</p>
<p>欲穷千里目,</p>
<p>更上一层楼。<br />
(句号后面有俩空格)</p>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><p></span>白日依山尽,<span class="nt"></p></span>
<span class="nt"><p></span>黄河入海流。
(句号后面没有空格)<span class="nt"></p></span>
<span class="nt"><p></span>欲穷千里目,<span class="nt"></p></span>
<span class="nt"><p></span>
更上一层楼。
<span class="nt"><br></span>
(句号后面有俩空格)
<span class="nt"></p></span>
</code></pre></div></div>
<h3 id="行内格式">行内格式</h3>
<p>对段落或者部分文本的强调效果。</p>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>后面俩字**加黑**
后面俩字*斜体*
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<p>后面俩字<strong>加黑</strong></p>
<p>后面俩字<em>斜体</em></p>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><p></span>
后面俩字
<span class="nt"><strong></span>加黑<span class="nt"></strong></span>
<span class="nt"></p></span>
<span class="nt"><p></span>
后面俩字
<span class="nt"><em></span>斜体<span class="nt"></em></span>
<span class="nt"></p></span>
</code></pre></div></div>
<h3 id="引用块">引用块</h3>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>> 引用块段落一。
>
> 引用块段落二。
>> 内嵌引用块段落一。
>
> ### 引用块内的标题
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<blockquote>
<p>引用块段落一。</p>
<p>引用块段落二。</p>
<blockquote>
<p>内嵌引用块段落一。</p>
</blockquote>
<h3 id="引用块内的标题">引用块内的标题</h3>
</blockquote>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><blockquote></span>
<span class="nt"><p></span>引用块段落一。<span class="nt"></p></span>
<span class="nt"><p></span>引用块段落二。<span class="nt"></p></span>
<span class="nt"><blockquote></span>
<span class="nt"><p></span>内嵌引用块段落一。<span class="nt"></p></span>
<span class="nt"></blockquote></span>
<span class="nt"><h3</span> <span class="na">id=</span><span class="s">"引用块内的标题"</span><span class="nt">></span>引用块内的标题<span class="nt"></h3></span>
<span class="nt"></blockquote></span>
</code></pre></div></div>
<h3 id="超链接">超链接</h3>
<p>Markdown 支持行内式链接和引用式链接。</p>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>行内式 [博客](https://mazhuang.org "我的个人博客") 链接,带 title。
行内式 [GitHub](https://github.com/mzlogin) 链接。
引用式 [博客][1] 链接。
引用式 [GitHub][2] 链接,带 title。
[1]: https://mazhuang.org
[2]: https://github.com/mzlogin "我的 GitHub 主页"
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<p>行内式 <a href="https://mazhuang.org" title="我的个人博客">博客</a> 链接,带 title。</p>
<p>行内式 <a href="https://github.com/mzlogin">GitHub</a> 链接。</p>
<p>引用式 <a href="https://mazhuang.org">博客</a> 链接。</p>
<p>引用式 <a href="https://github.com/mzlogin" title="我的 GitHub 主页">GitHub</a> 链接,带 title。</p>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><p></span>行内式 <span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://mazhuang.org"</span> <span class="na">title=</span><span class="s">"我的个人博客"</span><span class="nt">></span>博客<span class="nt"></a></span> 链接,带 title。<span class="nt"></p></span>
<span class="nt"><p></span>行内式 <span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://github.com/mzlogin"</span><span class="nt">></span>GitHub<span class="nt"></a></span> 链接。<span class="nt"></p></span>
<span class="nt"><p></span>引用式 <span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://mazhuang.org"</span><span class="nt">></span>博客<span class="nt"></a></span> 链接。<span class="nt"></p></span>
<span class="nt"><p></span>引用式 <span class="nt"><a</span> <span class="na">href=</span><span class="s">"https://github.com/mzlogin"</span> <span class="na">title=</span><span class="s">"我的 GitHub 主页"</span><span class="nt">></span>GitHub<span class="nt"></a></span> 链接,带 title。<span class="nt"></p></span>
</code></pre></div></div>
<h3 id="图片">图片</h3>
<p>在超链接的写法前加一个 <code class="language-plaintext highlighter-rouge">!</code>,就是引用图片的方法。</p>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>![Alt text](https://mazhuang.org/favicon.ico "favicon")
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<p><img src="https://mazhuang.org/favicon.ico" alt="Alt text" title="favicon" /></p>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><img</span> <span class="na">src=</span><span class="s">"https://mazhuang.org/favicon.ico"</span> <span class="na">alt=</span><span class="s">"Alt text"</span> <span class="na">title=</span><span class="s">"favicon"</span><span class="nt">></span>
</code></pre></div></div>
<h3 id="列表">列表</h3>
<p>包括有序列表和无序列表。</p>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- 苹果
- 葡萄
- 榴莲
1. 苹果
2. 葡萄
3. 榴莲
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<ul>
<li>苹果</li>
<li>葡萄</li>
<li>榴莲</li>
</ul>
<ol>
<li>苹果</li>
<li>葡萄</li>
<li>榴莲</li>
</ol>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><ul></span>
<span class="nt"><li></span>苹果<span class="nt"></li></span>
<span class="nt"><li></span>葡萄<span class="nt"></li></span>
<span class="nt"><li></span>榴莲<span class="nt"></li></span>
<span class="nt"></ul></span>
<span class="nt"><ol></span>
<span class="nt"><li></span>苹果<span class="nt"></li></span>
<span class="nt"><li></span>葡萄<span class="nt"></li></span>
<span class="nt"><li></span>榴莲<span class="nt"></li></span>
<span class="nt"></ol></span>
</code></pre></div></div>
<p>其中无序列表的标记可以使用 <code class="language-plaintext highlighter-rouge">+</code>、<code class="language-plaintext highlighter-rouge">-</code> 或 <code class="language-plaintext highlighter-rouge">*</code>,有序列表前的数字可以是乱序的。</p>
<h3 id="代码块">代码块</h3>
<p>支持行内代码和代码块。</p>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Android 里使用 `TextUtils` 类的 `isEmpty` 方法来判断字符串是否为空。
```java
if (TextUtils.isEmpty(text)) {
return null;
}
```
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<p>Android 里使用 <code class="language-plaintext highlighter-rouge">TextUtils</code> 类的 <code class="language-plaintext highlighter-rouge">isEmpty</code> 方法来判断字符串是否为空。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">if</span> <span class="o">(</span><span class="nc">TextUtils</span><span class="o">.</span><span class="na">isEmpty</span><span class="o">(</span><span class="n">text</span><span class="o">))</span> <span class="o">{</span>
<span class="k">return</span> <span class="kc">null</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><p></span>Android 里使用 <span class="nt"><code></span>TextUtils<span class="nt"></code></span> 类的 <span class="nt"><code></span>isEmpty<span class="nt"></code></span> 方法来判断字符串是否为空。<span class="nt"></p></span>
<span class="nt"><div</span> <span class="na">class=</span><span class="s">"highlight highlight-source-java"</span><span class="nt">><pre><span</span> <span class="na">class=</span><span class="s">"pl-k"</span><span class="nt">></span>if<span class="nt"></span></span> (<span class="nt"><span</span> <span class="na">class=</span><span class="s">"pl-smi"</span><span class="nt">></span>TextUtils<span class="nt"></span><span</span> <span class="na">class=</span><span class="s">"pl-k"</span><span class="nt">></span>.<span class="nt"></span></span>isEmpty(text)) {
<span class="nt"><span</span> <span class="na">class=</span><span class="s">"pl-k"</span><span class="nt">></span>return<span class="nt"></span></span> <span class="nt"><span</span> <span class="na">class=</span><span class="s">"pl-c1"</span><span class="nt">></span>null<span class="nt"></span></span>;
}<span class="nt"></pre></div></span>
</code></pre></div></div>
<p>上例中的语言标记 <code class="language-plaintext highlighter-rouge">java</code> 可选填,可用于在编辑器和渲染后的效果里添加语法高亮。</p>
<p>块式代码也可以对整个代码段缩进四个空格,或一个 Tab 来实现。</p>
<h3 id="水平分割线">水平分割线</h3>
<p>使用一个单独行里的三个或以上 <code class="language-plaintext highlighter-rouge">*</code>、<code class="language-plaintext highlighter-rouge">-</code> 来生产一条水平分割线,它们之间可以有空格。</p>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>***
-----
- - -
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<hr />
<hr />
<hr />
<p><strong>对应 HTML:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code><hr />
<hr />
<hr />
</code></pre></div></div>
<h3 id="嵌入-html">嵌入 HTML</h3>
<p>Markdown 标记语言的目的不是替代 HTML,也不是发明一种更便捷的插入 HTML 标签的方式。它对应的只是 HTML 标签的一个很小的子集。</p>
<p>对于那些没有办法用 Markdown 语法来对应的 HTML 标签,直接使用 HTML 来写就好了。</p>
<h2 id="扩展语法">扩展语法</h2>
<p>本节的内容是介绍一些受到广泛支持的 Markdown 扩展语法。</p>
<h3 id="表格">表格</h3>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>| 编号 | 姓名(左) | 年龄(右) | 性别(中) |
| ----- | :-------- | ---------: | :------: |
| 0 | 张三 | 28 | 男 |
| 1 | 李四 | 29 | 男 |
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<table>
<thead>
<tr>
<th>编号</th>
<th style="text-align: left">姓名(左)</th>
<th style="text-align: right">年龄(右)</th>
<th style="text-align: center">性别(中)</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td style="text-align: left">张三</td>
<td style="text-align: right">28</td>
<td style="text-align: center">男</td>
</tr>
<tr>
<td>1</td>
<td style="text-align: left">李四</td>
<td style="text-align: right">29</td>
<td style="text-align: center">男</td>
</tr>
</tbody>
</table>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><table></span>
<span class="nt"><thead></span>
<span class="nt"><tr></span>
<span class="nt"><th></span>编号<span class="nt"></th></span>
<span class="nt"><th</span> <span class="na">align=</span><span class="s">"left"</span><span class="nt">></span>姓名(左)<span class="nt"></th></span>
<span class="nt"><th</span> <span class="na">align=</span><span class="s">"right"</span><span class="nt">></span>年龄(右)<span class="nt"></th></span>
<span class="nt"><th</span> <span class="na">align=</span><span class="s">"center"</span><span class="nt">></span>性别(中)<span class="nt"></th></span>
<span class="nt"></tr></span>
<span class="nt"></thead></span>
<span class="nt"><tbody></span>
<span class="nt"><tr></span>
<span class="nt"><td></span>0<span class="nt"></td></span>
<span class="nt"><td</span> <span class="na">align=</span><span class="s">"left"</span><span class="nt">></span>张三<span class="nt"></td></span>
<span class="nt"><td</span> <span class="na">align=</span><span class="s">"right"</span><span class="nt">></span>28<span class="nt"></td></span>
<span class="nt"><td</span> <span class="na">align=</span><span class="s">"center"</span><span class="nt">></span>男<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"><tr></span>
<span class="nt"><td></span>1<span class="nt"></td></span>
<span class="nt"><td</span> <span class="na">align=</span><span class="s">"left"</span><span class="nt">></span>李四<span class="nt"></td></span>
<span class="nt"><td</span> <span class="na">align=</span><span class="s">"right"</span><span class="nt">></span>29<span class="nt"></td></span>
<span class="nt"><td</span> <span class="na">align=</span><span class="s">"center"</span><span class="nt">></span>男<span class="nt"></td></span>
<span class="nt"></tr></span>
<span class="nt"></tbody></span>
<span class="nt"></table></span>
</code></pre></div></div>
<h3 id="任务列表">任务列表</h3>
<p>在 GitHub / GitLab 里有较好的支持。</p>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>- [x] 洗碗
- [ ] 清洗油烟机
- [ ] 拖地
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<ul class="task-list">
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" checked="checked" />洗碗</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />清洗油烟机</li>
<li class="task-list-item"><input type="checkbox" class="task-list-item-checkbox" disabled="disabled" />拖地</li>
</ul>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><ul</span> <span class="na">class=</span><span class="s">"contains-task-list"</span><span class="nt">></span>
<span class="nt"><li</span> <span class="na">class=</span><span class="s">"task-list-item"</span><span class="nt">><input</span> <span class="na">type=</span><span class="s">"checkbox"</span> <span class="na">id=</span><span class="s">""</span> <span class="na">disabled=</span><span class="s">""</span> <span class="na">class=</span><span class="s">"task-list-item-checkbox"</span> <span class="na">checked=</span><span class="s">""</span><span class="nt">></span> 洗碗<span class="nt"></li></span>
<span class="nt"><li</span> <span class="na">class=</span><span class="s">"task-list-item"</span><span class="nt">><input</span> <span class="na">type=</span><span class="s">"checkbox"</span> <span class="na">id=</span><span class="s">""</span> <span class="na">disabled=</span><span class="s">""</span> <span class="na">class=</span><span class="s">"task-list-item-checkbox"</span><span class="nt">></span> 清洗油烟机<span class="nt"></li></span>
<span class="nt"><li</span> <span class="na">class=</span><span class="s">"task-list-item"</span><span class="nt">><input</span> <span class="na">type=</span><span class="s">"checkbox"</span> <span class="na">id=</span><span class="s">""</span> <span class="na">disabled=</span><span class="s">""</span> <span class="na">class=</span><span class="s">"task-list-item-checkbox"</span><span class="nt">></span> 拖地<span class="nt"></li></span>
<span class="nt"></ul></span>
</code></pre></div></div>
<p>如果是在 GitHub / GitLab 的 Issue 里,会附赠任务完成比例提示效果:</p>
<p><img src="https://raw.githubusercontent.com/mzlogin/markdown-intro/master/assets/task-list-1.png" alt="task list 1" /></p>
<p>还可以直接在网页上拖动调整顺序,勾选和取消勾选。</p>
<p><img src="https://raw.githubusercontent.com/mzlogin/markdown-intro/master/assets/task-list-2.png" alt="task list 2" /></p>
<h3 id="删除线">删除线</h3>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>后面三个字打上~~删除线~~。
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<p>后面三个字打上<del>删除线</del>。</p>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><p></span>后面三个字打上<span class="nt"><del></span>删除线<span class="nt"></del></span>。<span class="nt"></p></span>
</code></pre></div></div>
<h3 id="自动链接">自动链接</h3>
<p>自动链接扩展,即:当识别到 URL,或用 <code class="language-plaintext highlighter-rouge"><</code>、<code class="language-plaintext highlighter-rouge">></code> 包括的 URL 时,会自动为其生成 <code class="language-plaintext highlighter-rouge">a</code> 标签。</p>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>https://github.com
<example@gmail.com>
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<p>https://github.com</p>
<p><a href="mailto:example@gmail.com">example@gmail.com</a></p>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><p><a</span> <span class="na">href=</span><span class="s">"https://github.com"</span><span class="nt">></span>https://github.com<span class="nt"></a></p></span>
<span class="nt"><p><a</span> <span class="na">href=</span><span class="s">"mailto:example@gmail.com"</span><span class="nt">></span>example@gmail.com<span class="nt"></a></p></span>
</code></pre></div></div>
<h3 id="emoji">emoji</h3>
<p>以 GitHub Pages 为例。</p>
<p><strong>Markdown:</strong></p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>:camel: :blush: :smile:
</code></pre></div></div>
<p><strong>预览效果:</strong></p>
<p>:camel: :blush: :smile:</p>
<p><strong>对应 HTML:</strong></p>
<div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt"><p></span>
<span class="nt"><img</span> <span class="na">class=</span><span class="s">"emoji"</span> <span class="na">title=</span><span class="s">":camel:"</span> <span class="na">alt=</span><span class="s">":camel:"</span> <span class="na">src=</span><span class="s">"https://assets-cdn.github.com/images/icons/emoji/unicode/1f42b.png"</span> <span class="na">height=</span><span class="s">"20"</span> <span class="na">width=</span><span class="s">"20"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">class=</span><span class="s">"emoji"</span> <span class="na">title=</span><span class="s">":blush:"</span> <span class="na">alt=</span><span class="s">":blush:"</span> <span class="na">src=</span><span class="s">"https://assets-cdn.github.com/images/icons/emoji/unicode/1f60a.png"</span> <span class="na">height=</span><span class="s">"20"</span> <span class="na">width=</span><span class="s">"20"</span><span class="nt">></span>
<span class="nt"><img</span> <span class="na">class=</span><span class="s">"emoji"</span> <span class="na">title=</span><span class="s">":smile:"</span> <span class="na">alt=</span><span class="s">":smile:"</span> <span class="na">src=</span><span class="s">"https://assets-cdn.github.com/images/icons/emoji/unicode/1f604.png"</span> <span class="na">height=</span><span class="s">"20"</span> <span class="na">width=</span><span class="s">"20"</span><span class="nt">></span>
<span class="nt"></p></span>
</code></pre></div></div>
<h2 id="奇技淫巧">奇技淫巧</h2>
<p>脑洞清奇的工程师们还发掘了很多使用 Markdown 的方法,大部分都是引入第三方 JavaScript 插件来实现。对这部分我只做简述,对其中的部分功能比如作图等,还是推荐用专门的可视化工具去做。</p>
<h3 id="画流程图和时序图">画流程图和时序图</h3>
<p>有部分网站和编辑器实现了对 Markdown 里流程图和时序图的支持,比如我们使用的项目管理工具 TAPD 的在线编辑器,还有 VSCode + 插件 Markdown Preview Enhanced 等。</p>
<p>以我们使用的项目管理工具 TAPD 的在线编辑器为例:</p>
<p><img src="https://raw.githubusercontent.com/mzlogin/markdown-intro/master/assets/tapd-markdown-flowchart.png" alt="流程图" /></p>
<p><img src="https://raw.githubusercontent.com/mzlogin/markdown-intro/master/assets/tapd-markdown-seq.png" alt="时序图" /></p>
<h3 id="插入数学公式">插入数学公式</h3>
<p>仍然以 TAPD 为例:</p>
<p><img src="https://raw.githubusercontent.com/mzlogin/markdown-intro/master/assets/tapd-markdown-math.png" alt="数学公式" /></p>
<p>应该是利用 JavaScript 支持了 LaTeX 公式语法。</p>
<h3 id="用-markdown-做-ppt">用 Markdown 做 PPT</h3>
<p>有专门的工具 <a href="https://github.com/yhatt/marp">Marp</a>,另外使用 VSCode + 插件 Markdown Preview Enhanced 也可以实现。</p>
<h3 id="用-markdown-写微信公众号">用 Markdown 写微信公众号</h3>
<p>可以将公众号素材用 Markdown 编辑好后,贴到在线排版工具以后,复制到公众号编辑器里即可。有多种页面主题和代码主题可选择。</p>
<p>我维护的工具地址:<a href="https://md.mazhuang.org">https://md.mazhuang.org</a></p>
<p><img src="https://raw.githubusercontent.com/mzlogin/markdown-intro/master/assets/wechat-markdown.png" alt="微信公众号" /></p>
<h3 id="更多">更多</h3>
<p>想象力丰富的工程师们还扩展了很多基于 Markdown 的玩法,包括但不限于:</p>
<ul>
<li>自动生成 / 更新 Table of Contents</li>
<li>流程图 / 时序图</li>
<li>制作幻灯片</li>
<li>集成 PlantUML / GraphViz 的能力</li>
<li>导出 HTML / PDF / 电子书</li>
<li>…</li>
</ul>
<p>以上功能基本都可以用 VSCode + 插件 Markdown Preview Enhanced 实现。</p>
<p>另外可以参考我以前的一篇博客 <a href="https://mazhuang.org/2017/09/01/markdown-odd-skills/">关于 Markdown 的一些奇技淫巧</a>。</p>
<h2 id="参考">参考</h2>
<ul>
<li><a href="https://daringfireball.net/projects/markdown/syntax">Markdown: Syntax - DARING FIREBALL</a></li>
<li><a href="https://zh.wikipedia.org/wiki/Markdown">Markdown - 维基百科</a></li>
<li><a href="https://github.github.com/gfm/">GitHub Flavored Markdown Spec</a></li>
<li><a href="https://mazhuang.org/2017/09/01/markdown-odd-skills/">关于 Markdown 的一些奇技淫巧</a></li>
</ul>
<hr />
<p>欢迎关注我的微信公众号,接收 markdown-intro 最新动态。</p>
<div align="center"><img width="192px" height="192px" src="https://mazhuang.org/assets/images/qrcode.jpg" /></div>Allen为部门内知识分享准备的素材,记录了 Markdown 的优点、应用场景和编辑工具,介绍了标准语法与扩展语法,以及一些应用 Markdown 的奇技淫巧。个人使用 Markdown 的经验持续补充中,最新完整版请参见Android 源码分析 —— Handler、Looper 和 MessageQueue2018-06-11T00:00:00+08:002018-06-11T00:00:00+08:00http://www.cat-wish.cn/2018/06/11/handler-looper-massagequeue<p>本系列文章在 <a href="https://github.com/mzlogin/rtfsc-android">https://github.com/mzlogin/rtfsc-android</a> 持(jing)续(chang)更(duan)新(geng)中,欢迎有兴趣的童鞋们关注。</p>
<p>书接上文,在分析 Toast 源码的过程中我们涉及到了 Handler,这个在 Android 开发里经常用到的类——线程切换、顺序执行、延时执行等等逻辑里往往少不了它的身影,跟它一起搭配使用的通常是 Runnable 和 Message,还有它身后的好基友 Looper 与 MessageQueue。Runnable 相信大家都很熟悉了,本文的主角就是标题里的三剑客——Handler、Looper 和 MessageQueue,当然少不了说到 Message。</p>
<p>本文使用的工具与源码为:Chrome、插件 insight.io、GitHub 项目 <a href="https://github.com/aosp-mirror/platform_frameworks_base">aosp-mirror/platform_frameworks_base</a></p>
<p><strong>目录</strong></p>
<ul id="markdown-toc">
<li><a href="#初步印象" id="markdown-toc-初步印象">初步印象</a> <ul>
<li><a href="#handler" id="markdown-toc-handler">Handler</a></li>
<li><a href="#looper" id="markdown-toc-looper">Looper</a></li>
<li><a href="#messagequeue" id="markdown-toc-messagequeue">MessageQueue</a></li>
<li><a href="#message" id="markdown-toc-message">Message</a></li>
</ul>
</li>
<li><a href="#提出问题" id="markdown-toc-提出问题">提出问题</a></li>
<li><a href="#解答问题" id="markdown-toc-解答问题">解答问题</a> <ul>
<li><a href="#thread-与-looper" id="markdown-toc-thread-与-looper">Thread 与 Looper</a></li>
<li><a href="#looper-与-messagequeue" id="markdown-toc-looper-与-messagequeue">Looper 与 MessageQueue</a></li>
<li><a href="#handler-与-looper" id="markdown-toc-handler-与-looper">Handler 与 Looper</a></li>
<li><a href="#消息如何分发到对应的-handler" id="markdown-toc-消息如何分发到对应的-handler">消息如何分发到对应的 Handler</a></li>
<li><a href="#handler-能用于线程切换的原理" id="markdown-toc-handler-能用于线程切换的原理">Handler 能用于线程切换的原理</a></li>
<li><a href="#runnable-与-messagequeue" id="markdown-toc-runnable-与-messagequeue">Runnable 与 MessageQueue</a></li>
<li><a href="#能否创建关联到其它线程的-handler" id="markdown-toc-能否创建关联到其它线程的-handler">能否创建关联到其它线程的 Handler</a></li>
<li><a href="#消息可以插队吗" id="markdown-toc-消息可以插队吗">消息可以插队吗</a></li>
<li><a href="#消息可以撤回吗" id="markdown-toc-消息可以撤回吗">消息可以撤回吗</a></li>
<li><a href="#找到主线程消息循环源码" id="markdown-toc-找到主线程消息循环源码">找到主线程消息循环源码</a></li>
</ul>
</li>
<li><a href="#总结" id="markdown-toc-总结">总结</a> <ul>
<li><a href="#结论汇总" id="markdown-toc-结论汇总">结论汇总</a></li>
<li><a href="#遗留知识点" id="markdown-toc-遗留知识点">遗留知识点</a></li>
<li><a href="#本篇用到的源码分析方法" id="markdown-toc-本篇用到的源码分析方法">本篇用到的源码分析方法</a></li>
</ul>
</li>
<li><a href="#后话" id="markdown-toc-后话">后话</a></li>
</ul>
<h2 id="初步印象">初步印象</h2>
<p>按惯例,第一步还是从 Android 的官方 API 文档里来建立对这几个类的初步印象,文档开头的说明里往往有一些比较关键的知识点。</p>
<p>官方文档链接:</p>
<ul>
<li><a href="https://developer.android.google.cn/reference/android/os/Handler">Handler</a></li>
<li><a href="https://developer.android.google.cn/reference/android/os/Looper">Looper</a></li>
<li><a href="https://developer.android.google.cn/reference/android/os/MessageQueue">MessageQueue</a></li>
<li><a href="https://developer.android.google.cn/reference/android/os/Message">Message</a></li>
</ul>
<p>这几个类开头的说明本身也不长,为了避免断章取义误人子弟,就将其直译版完整地放在下面,当然更推荐的方式是自己去看原文。</p>
<h3 id="handler">Handler</h3>
<p>可以用 Handler 发送和处理与某线程的 MessageQueue 相关联的 Message/Runnable 对象。每个 Handler 实例只能与一个线程和它的消息队列相关联。当创建一个 Handler 时,它会绑定到当前线程和消息队列——从那时起,它将 Message 和 Runnable 传递给绑定的消息队列,并在它们从队列里被取出时执行对应逻辑。(<em>译注:此处描述不准确,创建 Handler 时并不一定是绑定到当前线程。</em>)</p>
<p>Handler 主要有两个用途:</p>
<ol>
<li>
<p>在未来某个时间点处理 Messages 或者执行 Runnables;</p>
</li>
<li>
<p>将一段逻辑切换到另一个线程执行。</p>
</li>
</ol>
<p>可以使用 Handler 的以下方法来调度 Messages 和 Runnables:</p>
<ul>
<li>
<p>post(Runnable)</p>
</li>
<li>
<p>postAtTime(Runnable, long)</p>
</li>
<li>
<p>postDelayed(Runnable, Object, long)</p>
</li>
<li>
<p>sendEmptyMessage(int)</p>
</li>
<li>
<p>sendMessage(Message)</p>
</li>
<li>
<p>sendMessageAtTime(Message, long)</p>
</li>
<li>
<p>sendMessageDelayed(Message, long)</p>
</li>
</ul>
<p>其中 postXXX 系列用于将 Runnable 对象加入队列,sendXXX 系列用于将 Message 对象加入队列,Message 对象通常会携带一些数据,可以在 Handler 的 handlerMessage(Message) 方法中处理(需要实现一个 Handler 子类)。</p>
<p>在调用 Handler 的 postXXX 和 sendXXX 时,可以指定当队列准备好时立即处理它们,也可以指定延时一段时间后处理,或某个绝对时间点处理。后面这两种能实现超时、延时、周期循环及其它基于时间的行为。</p>
<p>为应用程序创建一个进程时,其主线程专用于运行消息队列,该消息队列负责管理顶层应用程序对象(activities,broadcast receivers 等)以及它们创建的窗口。我们可以创建自己的线程,然后通过 Handler 与主线程进行通信,方法是从新线程调用我们前面讲到的 postXXX 或 sendXXX 方法,传递的 Runnable 或 Message 将被加入 Handler 关联的消息队列中,并适时进行处理。</p>
<h3 id="looper">Looper</h3>
<p>用于为线程执行消息循环的类。线程默认没有关联的消息循环,如果要创建一个,可以在执行消息循环的线程里面调用 prepare() 方法,然后调用 loop() 处理消息,直到循环停止。</p>
<p>大多数与消息循环的交互都是通过 Handler 类。</p>
<p>下面是实现一个 Looper 线程的典型例子,在 prepare() 和 loop() 之间初始化 Handler 实例,用于与 Looper 通信:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">class</span> <span class="nc">LooperThread</span> <span class="kd">extends</span> <span class="nc">Thread</span> <span class="o">{</span>
<span class="kd">public</span> <span class="nc">Handler</span> <span class="n">mHandler</span><span class="o">;</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span>
<span class="nc">Looper</span><span class="o">.</span><span class="na">prepare</span><span class="o">();</span>
<span class="n">mHandler</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Handler</span><span class="o">()</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleMessage</span><span class="o">(</span><span class="nc">Message</span> <span class="n">msg</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// 在这里处理传入的消息</span>
<span class="o">}</span>
<span class="o">};</span>
<span class="nc">Looper</span><span class="o">.</span><span class="na">loop</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<h3 id="messagequeue">MessageQueue</h3>
<p>持有将被 Looper 分发的消息列表的底层类。消息都是通过与 Looper 关联的 Handler 添加到 MessageQueue,而不是直接操作 MessageQueue。</p>
<p>可以用 Looper.myQueue() 获取当前线程的 MessageQueue 实例。</p>
<h3 id="message">Message</h3>
<p>定义一个可以发送给 Handler 的消息,包含描述和任意数据对象。消息对象有两个额外的 int 字段和一个 object 字段,这可以满足大部分场景的需求了。</p>
<blockquote>
<p>虽然 Message 的构造方法是 public 的,但最推荐的得到一个消息对象的方式是调用 Message.obtain() 或者 Handler.obtainMessage() 系列方法,这些方法会从一个对象回收池里捡回能复用的对象。</p>
</blockquote>
<h2 id="提出问题">提出问题</h2>
<p>根据以上印象,及以前的使用经验,提出以下问题来继续本次源码分析之旅:</p>
<ol>
<li>
<p>Thread 与 Looper,Looper 与 MessageQueue,Handler 与 Looper 之间的数量对应关系是怎样的?</p>
</li>
<li>
<p>如果 Looper 能对应多个 Handler,那通过不同的 Handler 发送的 Message,那处理的时候代码是如何知道该分发到哪一个 Handler 的 handlerMessage 方法的?</p>
</li>
<li>
<p>Handler 能用于线程切换的原理是什么?</p>
</li>
<li>
<p>Runnable 对象也是被添加到 MessageQueue 里吗?</p>
</li>
<li>
<p>可以在 A 线程创建 Handler 关联到 B 线程及其消息循环吗?</p>
</li>
<li>
<p>如何退出消息循环?</p>
</li>
<li>
<p>消息可以插队吗?</p>
</li>
<li>
<p>消息可以撤回吗?</p>
</li>
<li>
<p>上文提到,应用程序的主线程是运行一个消息循环,在代码里是如何反映的?</p>
</li>
</ol>
<h2 id="解答问题">解答问题</h2>
<h3 id="thread-与-looper">Thread 与 Looper</h3>
<p>前文有提到,线程默认是没有消息循环的,需要调用 Looper.prepare() 来达到目的,那么我们对这个问题的探索就从 Looper.prepare() 开始。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/** Initialize the current thread as a looper.
* This gives you a chance to create handlers that then reference
* this looper, before actually starting the loop. Be sure to call
* {@link #loop()} after calling this method, and end it by calling
* {@link #quit()}.
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">prepare</span><span class="o">()</span> <span class="o">{</span>
<span class="n">prepare</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">prepare</span><span class="o">(</span><span class="kt">boolean</span> <span class="n">quitAllowed</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">sThreadLocal</span><span class="o">.</span><span class="na">get</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="s">"Only one Looper may be created per thread"</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">sThreadLocal</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="k">new</span> <span class="nc">Looper</span><span class="o">(</span><span class="n">quitAllowed</span><span class="o">));</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在有参数版本的 prepare 方法里,我们可以得到两个信息:</p>
<ol>
<li>
<p>一个线程里调用多次 Looper.prepare() 会抛出异常,提示 <code class="language-plaintext highlighter-rouge">Only one Looper may be created per thread</code>,即 <strong>一个线程只能创建一个 Looper</strong></p>
</li>
<li>
<p>prepare 里主要干的事就是 <code class="language-plaintext highlighter-rouge">sThreadLocal.set(new Looper(quitAllowed))</code></p>
</li>
</ol>
<p>源码里是怎么限制一个线程只能创建一个 Looper 的呢?调用多次 Looper.prepare() 并不会关联多个 Looper,还会抛出异常,那能不能直接 new 一个 Looper 关联上呢?答案是不可以,Looper 的构造方法是 private 的。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="nf">Looper</span><span class="o">(</span><span class="kt">boolean</span> <span class="n">quitAllowed</span><span class="o">)</span> <span class="o">{</span>
<span class="n">mQueue</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MessageQueue</span><span class="o">(</span><span class="n">quitAllowed</span><span class="o">);</span>
<span class="n">mThread</span> <span class="o">=</span> <span class="nc">Thread</span><span class="o">.</span><span class="na">currentThread</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>在概览整个 Looper 的所有公开方法后,发现只有 prepare 和 prepareMainLooper 是做线程与 Looper 关联的工作的,而 prepareMainLooper 是 Android 环境调用的,不是用来给应用主动调用的。所以从 Looper 源码里掌握的信息来看,想给一个线程关联多个 Looper 的路不通。</p>
<p>另外我们从源码里能观察到,Looper 有一个 final 的 mThread 成员,在构造 Looper 对象的时候赋值为 <code class="language-plaintext highlighter-rouge">Thread.currentThread()</code>,源码里再无可以修改 mThread 值的地方,所以可知 <strong>Looper 只能关联到一个线程,且关联之后不能改变</strong>。</p>
<p>说了这么多,还记得 Looper.prepare() 里干的主要事情是 <code class="language-plaintext highlighter-rouge">sThreadLocal.set(new Looper(quitAllowed))</code> 吗?与之对应的,获取本线程关联的 Looper 对象是使用静态方法 Looper.myLooper():</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// sThreadLocal.get() will return null unless you've called prepare().</span>
<span class="kd">static</span> <span class="kd">final</span> <span class="nc">ThreadLocal</span><span class="o"><</span><span class="nc">Looper</span><span class="o">></span> <span class="n">sThreadLocal</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ThreadLocal</span><span class="o"><</span><span class="nc">Looper</span><span class="o">>();</span>
<span class="c1">// ...</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">prepare</span><span class="o">(</span><span class="kt">boolean</span> <span class="n">quitAllowed</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">sThreadLocal</span><span class="o">.</span><span class="na">get</span><span class="o">()</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="s">"Only one Looper may be created per thread"</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">sThreadLocal</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="k">new</span> <span class="nc">Looper</span><span class="o">(</span><span class="n">quitAllowed</span><span class="o">));</span>
<span class="o">}</span>
<span class="c1">// ...</span>
<span class="cm">/**
* Return the Looper object associated with the current thread. Returns
* null if the calling thread is not associated with a Looper.
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="nd">@Nullable</span> <span class="nc">Looper</span> <span class="nf">myLooper</span><span class="o">()</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">sThreadLocal</span><span class="o">.</span><span class="na">get</span><span class="o">();</span>
<span class="o">}</span>
</code></pre></div></div>
<p>使用了 ThreadLocal 来确保不同的线程调用静态方法 Looper.myLooper() 获取到的是与各自线程关联的 Looper 对象。关于 ThreadLocal,又可以另开一个小话题了。</p>
<p><strong>小结:</strong> Thread 若与 Looper 关联,将会是一一对应的关系,且关联后关系无法改变。</p>
<h3 id="looper-与-messagequeue">Looper 与 MessageQueue</h3>
<p>直接来看源码:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">Looper</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="kd">final</span> <span class="nc">MessageQueue</span> <span class="n">mQueue</span><span class="o">;</span>
<span class="c1">// ...</span>
<span class="kd">private</span> <span class="nf">Looper</span><span class="o">(</span><span class="kt">boolean</span> <span class="n">quitAllowed</span><span class="o">)</span> <span class="o">{</span>
<span class="n">mQueue</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">MessageQueue</span><span class="o">(</span><span class="n">quitAllowed</span><span class="o">);</span>
<span class="c1">// ...</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Looper 对象里有一个 MessageQueue 类型成员,在构造的时候 new 出的,并且它是一个 final,没有地方能修改它的指向。</p>
<p><strong>小结:</strong> Looper 与 MessageQueue 是一一对应的关系。</p>
<h3 id="handler-与-looper">Handler 与 Looper</h3>
<p>在前面略读 Looper 源码的过程中,我发现 Handler 基本没有出场,那么现在,从构造 Handler 的方法开始分析。</p>
<p>Handler 的构造方法有 7 个之多,不过有 3 个标记为 <code class="language-plaintext highlighter-rouge">@hide</code>,所以我们可以直接调用的有 4 个,这 4 个最终调用都到了其它的两个构造方法,捡出来我们要看的重点:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Handler</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="cm">/**
* ...
* @hide
*/</span>
<span class="kd">public</span> <span class="nf">Handler</span><span class="o">(</span><span class="nc">Callback</span> <span class="n">callback</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">async</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="n">mLooper</span> <span class="o">=</span> <span class="nc">Looper</span><span class="o">.</span><span class="na">myLooper</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">mLooper</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span>
<span class="s">"Can't create handler inside thread that has not called Looper.prepare()"</span><span class="o">);</span>
<span class="o">}</span>
<span class="n">mQueue</span> <span class="o">=</span> <span class="n">mLooper</span><span class="o">.</span><span class="na">mQueue</span><span class="o">;</span>
<span class="c1">// ...</span>
<span class="o">}</span>
<span class="cm">/**
* ...
* @hide
*/</span>
<span class="kd">public</span> <span class="nf">Handler</span><span class="o">(</span><span class="nc">Looper</span> <span class="n">looper</span><span class="o">,</span> <span class="nc">Callback</span> <span class="n">callback</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">async</span><span class="o">)</span> <span class="o">{</span>
<span class="n">mLooper</span> <span class="o">=</span> <span class="n">looper</span><span class="o">;</span>
<span class="n">mQueue</span> <span class="o">=</span> <span class="n">mLooper</span><span class="o">.</span><span class="na">mQueue</span><span class="o">;</span>
<span class="c1">// ...</span>
<span class="o">}</span>
<span class="c1">// ...</span>
<span class="kd">final</span> <span class="nc">Looper</span> <span class="n">mLooper</span><span class="o">;</span>
<span class="kd">final</span> <span class="nc">MessageQueue</span> <span class="n">mQueue</span><span class="o">;</span>
<span class="c1">// ...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>Handler 对象里有 final Looper 成员,所以一个 Handler 只会对应一个固定的 Looper 对象。构造 Handler 对象的时候如果不传 Looper 参数,会默认使用当前线程关联的 Looper,如果当前线程没有关联 Looper,会抛出异常。</p>
<p>那么能不能绑定多个 Handler 到同一个 Looper 呢?答案是可以的。在源码里并没有找到相关的限制说明,所以这种适合用个小 Demo 来验证,例如以下例子,就绑定了两个 Handler 到主线程的 Looper 上,并都能正常使用(日志 <code class="language-plaintext highlighter-rouge">receive msg: 1</code> 和 <code class="language-plaintext highlighter-rouge">receive msg: 2</code> 能依次输出)。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">MainActivity</span> <span class="kd">extends</span> <span class="nc">AppCompatActivity</span> <span class="o">{</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="nc">String</span> <span class="no">TAG</span> <span class="o">=</span> <span class="nc">MainActivity</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getSimpleName</span><span class="o">();</span>
<span class="kd">private</span> <span class="nc">Handler</span> <span class="n">mHandler1</span><span class="o">;</span>
<span class="kd">private</span> <span class="nc">Handler</span> <span class="n">mHandler2</span><span class="o">;</span>
<span class="kd">private</span> <span class="nc">Handler</span><span class="o">.</span><span class="na">Callback</span> <span class="n">mCallback</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Handler</span><span class="o">.</span><span class="na">Callback</span><span class="o">()</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">handleMessage</span><span class="o">(</span><span class="nc">Message</span> <span class="n">msg</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Log</span><span class="o">.</span><span class="na">v</span><span class="o">(</span><span class="no">TAG</span><span class="o">,</span> <span class="s">"receive msg: "</span> <span class="o">+</span> <span class="n">msg</span><span class="o">.</span><span class="na">what</span><span class="o">);</span>
<span class="k">return</span> <span class="kc">false</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">};</span>
<span class="nd">@Override</span>
<span class="kd">protected</span> <span class="kt">void</span> <span class="nf">onCreate</span><span class="o">(</span><span class="nc">Bundle</span> <span class="n">savedInstanceState</span><span class="o">)</span> <span class="o">{</span>
<span class="kd">super</span><span class="o">.</span><span class="na">onCreate</span><span class="o">(</span><span class="n">savedInstanceState</span><span class="o">);</span>
<span class="n">setContentView</span><span class="o">(</span><span class="no">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">activity_main</span><span class="o">);</span>
<span class="n">mHandler1</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Handler</span><span class="o">(</span><span class="n">mCallback</span><span class="o">);</span>
<span class="n">mHandler2</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Handler</span><span class="o">(</span><span class="n">mCallback</span><span class="o">);</span>
<span class="n">mHandler1</span><span class="o">.</span><span class="na">sendEmptyMessage</span><span class="o">(</span><span class="mi">1</span><span class="o">);</span>
<span class="n">mHandler2</span><span class="o">.</span><span class="na">sendEmptyMessage</span><span class="o">(</span><span class="mi">2</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p><strong>小结:</strong> Handler 与 Looper 是多对一的关系,创建 Handler 实例时要么提供一个 Looper 实例,要么当前线程有关联的 Looper。</p>
<h3 id="消息如何分发到对应的-handler">消息如何分发到对应的 Handler</h3>
<p>因为消息的分发在是 Looper.loop() 这个过程中,所以我们先来看这个方法:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">loop</span><span class="o">()</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="k">for</span> <span class="o">(;;)</span> <span class="o">{</span>
<span class="nc">Message</span> <span class="n">msg</span> <span class="o">=</span> <span class="n">queue</span><span class="o">.</span><span class="na">next</span><span class="o">();</span> <span class="c1">// might block</span>
<span class="c1">// ...</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">msg</span><span class="o">.</span><span class="na">target</span><span class="o">.</span><span class="na">dispatchMessage</span><span class="o">(</span><span class="n">msg</span><span class="o">);</span>
<span class="c1">// ...</span>
<span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="o">}</span>
<span class="c1">// ...</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>这个方法里做的主要工作是从 MessageQueue 里依次取出 Message,然后调用 Message.target.dispatchMessage 方法,Message 对象的这个 target 成员是什么东东呢?它是一个 Handler,它最终会被设置成 sendMessage 的 Handler:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">class</span> <span class="nc">Handler</span> <span class="o">{</span>
<span class="c1">// 其它 Handler.sendXXX 方法最终都会调用到这个方法</span>
<span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">sendMessageAtTime</span><span class="o">(</span><span class="nc">Message</span> <span class="n">msg</span><span class="o">,</span> <span class="kt">long</span> <span class="n">uptimeMillis</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="k">return</span> <span class="nf">enqueueMessage</span><span class="o">(</span><span class="n">queue</span><span class="o">,</span> <span class="n">msg</span><span class="o">,</span> <span class="n">uptimeMillis</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// ...</span>
<span class="kd">private</span> <span class="kt">boolean</span> <span class="nf">enqueueMessage</span><span class="o">(</span><span class="nc">MessageQueue</span> <span class="n">queue</span><span class="o">,</span> <span class="nc">Message</span> <span class="n">msg</span><span class="o">,</span> <span class="kt">long</span> <span class="n">uptimeMillis</span><span class="o">)</span> <span class="o">{</span>
<span class="n">msg</span><span class="o">.</span><span class="na">target</span> <span class="o">=</span> <span class="k">this</span><span class="o">;</span> <span class="c1">// 就是这里了</span>
<span class="c1">// ...</span>
<span class="o">}</span>
<span class="c1">// ...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>所以是用哪个 Handler.sendMessage,最终就会调用到它的 dispatchMessage 方法:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">handleCallback</span><span class="o">(</span><span class="nc">Message</span> <span class="n">message</span><span class="o">)</span> <span class="o">{</span>
<span class="n">message</span><span class="o">.</span><span class="na">callback</span><span class="o">.</span><span class="na">run</span><span class="o">();</span>
<span class="o">}</span>
<span class="c1">// ...</span>
<span class="cm">/**
* Handle system messages here.
*/</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">dispatchMessage</span><span class="o">(</span><span class="nc">Message</span> <span class="n">msg</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">msg</span><span class="o">.</span><span class="na">callback</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="n">handleCallback</span><span class="o">(</span><span class="n">msg</span><span class="o">);</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">mCallback</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">mCallback</span><span class="o">.</span><span class="na">handleMessage</span><span class="o">(</span><span class="n">msg</span><span class="o">))</span> <span class="o">{</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">handleMessage</span><span class="o">(</span><span class="n">msg</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>消息分发到这个方法以后,执行优先级分别是 Message.callback、Handler.mCallback,最后才是 Handler.handleMesage 方法。</p>
<p><strong>小结:</strong> 在 Handler.sendMessage 时,会将 Message.target 设置为该 Handler 对象,这样从消息队列取出 Message 后,就能调用到该 Handler 的 dispatchMessage 方法来进行处理。</p>
<h3 id="handler-能用于线程切换的原理">Handler 能用于线程切换的原理</h3>
<p>实际上一小节的结论已经近乎揭示了其中的原理,进一步解释一下就是:</p>
<p><strong>小结:</strong> Handler 会对应一个 Looper 和 MessageQueue,而 Looper 与线程又一一对应,所以通过 Handler.sendXXX 和 Hanler.postXXX 添加到 MessageQueue 的 Message,会在这个对应的线程的 Looper.loop() 里取出来,并就地执行 Handler.dispatchMessage,这就可以完成线程切换了。</p>
<h3 id="runnable-与-messagequeue">Runnable 与 MessageQueue</h3>
<p>Handler 的 postXXX 系列方法用于调度 Runnable 对象,那它最后也是和 Message 一样被加到 MessageQueue 的吗?可是 MessageQueue 是用一个元素类型为 Message 的链表来维护消息队列的,类型不匹配。</p>
<p>在 Handler 源码里能找到答案,这里就以 Handler.post(Runnable) 方法为例,其它几个 postXXX 方法情形与此类似。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Causes the Runnable r to be added to the message queue.
* The runnable will be run on the thread to which this handler is
* attached.
*
* @param r The Runnable that will be executed.
*
* @return Returns true if the Runnable was successfully placed in to the
* message queue. Returns false on failure, usually because the
* looper processing the message queue is exiting.
*/</span>
<span class="kd">public</span> <span class="kd">final</span> <span class="kt">boolean</span> <span class="nf">post</span><span class="o">(</span><span class="nc">Runnable</span> <span class="n">r</span><span class="o">)</span>
<span class="o">{</span>
<span class="k">return</span> <span class="nf">sendMessageDelayed</span><span class="o">(</span><span class="n">getPostMessage</span><span class="o">(</span><span class="n">r</span><span class="o">),</span> <span class="mi">0</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// ...</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="nc">Message</span> <span class="nf">getPostMessage</span><span class="o">(</span><span class="nc">Runnable</span> <span class="n">r</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Message</span> <span class="n">m</span> <span class="o">=</span> <span class="nc">Message</span><span class="o">.</span><span class="na">obtain</span><span class="o">();</span>
<span class="n">m</span><span class="o">.</span><span class="na">callback</span> <span class="o">=</span> <span class="n">r</span><span class="o">;</span>
<span class="k">return</span> <span class="n">m</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>可以看到,post 系列方法最终也是调用的 send 系列方法,Runnable 对象是被封装成 Message 对象后加入到消息队列的,Message.callback 被设置为 Runnable 本身,还记得前文 Handler.dispatchMessage 的执行顺序吗?如果 Message.callback 不为空,则执行 Message.callback.run() 后就返回。</p>
<p><strong>小结:</strong> Runnable 被封装成 Message 之后添加到 MessageQueue。</p>
<h3 id="能否创建关联到其它线程的-handler">能否创建关联到其它线程的 Handler</h3>
<p>创建 Handler 时会关联到一个 Looper,而 Looper 是与线程一一绑定的,所以理论上讲,如果能得到要关联的线程的 Looper 实例,这是可以实现的。</p>
<p>在阅读 Looper 源码的过程中,我们有留意到(好吧,其实应该是平时写代码时有用到):</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">Looper</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="kd">private</span> <span class="kd">static</span> <span class="nc">Looper</span> <span class="n">sMainLooper</span><span class="o">;</span> <span class="c1">// guarded by Looper.class</span>
<span class="c1">// ...</span>
<span class="cm">/**
* Returns the application's main looper, which lives in the main thread of the application.
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="nc">Looper</span> <span class="nf">getMainLooper</span><span class="o">()</span> <span class="o">{</span>
<span class="kd">synchronized</span> <span class="o">(</span><span class="nc">Looper</span><span class="o">.</span><span class="na">class</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="n">sMainLooper</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>可见获取主线程的 Looper 是能实现的,平时写代码过程中,如果要从子线程向主线程添加一段执行逻辑,也经常这么干,这是可行的:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 从子线程创建关联到主线程 Looper 的 Handler</span>
<span class="nc">Handler</span> <span class="n">mHandler</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Handler</span><span class="o">(</span><span class="nc">Looper</span><span class="o">.</span><span class="na">getMainLooper</span><span class="o">());</span>
<span class="n">mHandler</span><span class="o">.</span><span class="na">post</span><span class="o">(()</span> <span class="o">-></span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="o">});</span>
</code></pre></div></div>
<p>从子线程创建关联到其它子线程的 Looper 是否可行呢?这个用 Demo 来验证:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">new</span> <span class="nc">Thread</span><span class="o">()</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span>
<span class="n">setName</span><span class="o">(</span><span class="s">"thread-one"</span><span class="o">);</span>
<span class="nc">Looper</span><span class="o">.</span><span class="na">prepare</span><span class="o">();</span>
<span class="kd">final</span> <span class="nc">Looper</span> <span class="n">threadOneLooper</span> <span class="o">=</span> <span class="nc">Looper</span><span class="o">.</span><span class="na">myLooper</span><span class="o">();</span>
<span class="k">new</span> <span class="nf">Thread</span><span class="o">()</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span>
<span class="n">setName</span><span class="o">(</span><span class="s">"thread-two"</span><span class="o">);</span>
<span class="nc">Handler</span> <span class="n">handler</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Handler</span><span class="o">(</span><span class="n">threadOneLooper</span><span class="o">);</span>
<span class="n">handler</span><span class="o">.</span><span class="na">post</span><span class="o">(()</span> <span class="o">-></span> <span class="o">{</span>
<span class="nc">Log</span><span class="o">.</span><span class="na">v</span><span class="o">(</span><span class="s">"test"</span><span class="o">,</span> <span class="nc">Thread</span><span class="o">.</span><span class="na">currentThread</span><span class="o">().</span><span class="na">getName</span><span class="o">());</span>
<span class="o">});</span>
<span class="o">}</span>
<span class="o">}.</span><span class="na">start</span><span class="o">();</span>
<span class="nc">Looper</span><span class="o">.</span><span class="na">loop</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}.</span><span class="na">start</span><span class="o">();</span>
</code></pre></div></div>
<p>执行后日志输出为 <code class="language-plaintext highlighter-rouge">thread-one</code>。</p>
<p><strong>小结:</strong> 可以从一个线程创建关联到另一个线程 Looper 的 Handler,只要能拿到对应线程的 Looper 实例。</p>
<h3 id="消息可以插队吗">消息可以插队吗</h3>
<p>这个问题从API 文档、Handler 源码里都可以找到答案,答案是可以的,使用 Handler.sendMessageAtFrontOfQueue 和 Handler.postAtFrontOfQueue 这两个方法,它们会分别将 Message 和 Runnable(封装后)插入到消息队列的队首。</p>
<p>我目前尚未遇到过这种使用场景。</p>
<p><strong>小结:</strong> 消息可以插队,使用 Handler.xxxAtFrontOfQueue 方法。</p>
<h3 id="消息可以撤回吗">消息可以撤回吗</h3>
<p>同上,可以从 Handler 的 API 文档中找到答案。</p>
<p>可以用 Handler.hasXXX 系列方法判断关联的消息队列里是否有等待中的符合条件的 Message 和 Runnable,用 Handler.removeXXX 系列方法从消息队列里移除等待中的符合条件的 Message 和 Runnable。</p>
<p><strong>小结:</strong> 尚未分发的消息是可以撤回的,处理过的就没法了。</p>
<h3 id="找到主线程消息循环源码">找到主线程消息循环源码</h3>
<p>我们前面提到过一个小细节,就是 Looper.prepareMainLooper 是 Android 环境调用的,而从该方法的注释可知,调用它就是为了初始化主线程 Looper,所以我们要找到主线程消息循环这部分源码,搜索 prepareMainLooper 被哪些地方引用即可。</p>
<p>使用 insight.io 插件的功能,在 Looper.prepareMainLooper 上点一下即可看到引用处列表,一共两处:</p>
<p><img src="/images/posts/android/prepare-main-looper.png" alt="" /></p>
<p>从文件路径和文件名上猜测应该是第一处。</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">ActivityThread</span> <span class="o">{</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">main</span><span class="o">(</span><span class="nc">String</span><span class="o">[]</span> <span class="n">args</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// ...</span>
<span class="nc">Looper</span><span class="o">.</span><span class="na">prepareMainLooper</span><span class="o">();</span>
<span class="c1">// ...</span>
<span class="nc">Looper</span><span class="o">.</span><span class="na">loop</span><span class="o">();</span>
<span class="c1">// ...</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>就是我想象中的模样。这里只是简单找到这个位置,继续深入探索的话可以开启一个新的话题了,后续的篇章里再解决。</p>
<h2 id="总结">总结</h2>
<h3 id="结论汇总">结论汇总</h3>
<ul>
<li>
<p>Thread 若与 Looper 关联,将会是一一对应的关系,且关联后关系无法改变。</p>
</li>
<li>
<p>Looper 与 MessageQueue 是一一对应的关系。</p>
</li>
<li>
<p>Handler 与 Looper 是多对一的关系,创建 Handler 实例时要么提供一个 Looper 实例,要么当前线程有关联的 Looper。</p>
</li>
<li>
<p>在 Handler.sendMessage 时,会将 Message.target 设置为该 Handler 对象,这样从消息队列取出 Message 后,就能调用到该 Handler 的 dispatchMessage 方法来进行处理。</p>
</li>
<li>
<p>Handler 会对应一个 Looper 和 MessageQueue,而 Looper 与线程又一一对应,所以通过 Handler.sendXXX 和 Hanler.postXXX 添加到 MessageQueue 的 Message,会在这个对应的线程的 Looper.loop() 里取出来,并就地执行 Handler.dispatchMessage,这就可以完成线程切换了。</p>
</li>
<li>
<p>Runnable 被封装成 Message 之后添加到 MessageQueue。</p>
</li>
<li>
<p>可以从一个线程创建关联到另一个线程 Looper 的 Handler,只要能拿到对应线程的 Looper 实例。</p>
</li>
<li>
<p>消息可以插队,使用 Handler.xxxAtFrontOfQueue 方法。</p>
</li>
<li>
<p>尚未分发的消息是可以撤回的,处理过的就没法了。</p>
</li>
</ul>
<h3 id="遗留知识点">遗留知识点</h3>
<ol>
<li>
<p>ThreadLocal</p>
</li>
<li>
<p>应用的启动流程</p>
</li>
</ol>
<h3 id="本篇用到的源码分析方法">本篇用到的源码分析方法</h3>
<ol>
<li>文档优先</li>
</ol>
<h2 id="后话">后话</h2>
<p>关于 Handler、Looper 和 MessageQueue 的分析在此先告一段落,这部分的内容比较容易分析,但里面细节挺多的,写得有点杂且不全,有点只见树木不见森林的感觉,想要配合画一些图,但找不到合适的画图形式。对此类主题的解析方式必须要再探索优化一下,大家有好的建议请一定告知。</p>
<hr />
<p>最后,照例要安利一下我的微信公众号「闷骚的程序员」,扫码关注,接收 rtfsc-android 的最近更新。</p>
<div align="center"><img width="192px" height="192px" src="https://mazhuang.org/assets/images/qrcode.jpg" /></div>Allen本系列文章在 https://github.com/mzlogin/rtfsc-android 持(jing)续(chang)更(duan)新(geng)中,欢迎有兴趣的童鞋们关注。光谷社区第三方 Android 客户端 v2.0 发布2018-04-30T00:00:00+08:002018-04-30T00:00:00+08:00http://www.cat-wish.cn/2018/04/30/guanggoo-android-app-new-version<p>我最完整的业余作品 <a href="https://github.com/mzlogin/guanggoo-android">光谷社区第三方 Android 客户端</a> (虽然也还不是很完善)今天发布了重大更新,版本号由 v1.3 更新到 v2.0,欢迎所有身在武汉和心系武汉的朋友们试用体验,也欢迎各路开发者大神们对照源码(后文有链接)交流指点。</p>
<p>本次更新主要是界面和交互风格的全面改版,以及少量的功能更新,将这描述为「重大更新」并非夸张,主要是开发模式相比之前有所变化。</p>
<p>以前基本是自己一个人在战斗,功能自己想着往上加,界面和交互自己想着做,程序员自己设计的东西,大家都懂的——能用但是不好用。</p>
<p>现在就好多了,社区里热心的 UI 设计师小哥 colinlee 为应用设计了界面,我们经过讨论之后确定交互体验,开发完之后再一起视觉走查、完善,妈妈再也不用担心应用界面土得掉渣了,同时也更让我觉得光谷社区是一个有温度有活力的社区,可以找到志趣相投的小伙伴们一起做点有意义的事情。</p>
<p>好了言归正传,先来看一下新版界面:</p>
<p><em>首页</em></p>
<p><img src="https://mazhuang.org/guanggoo-android/screenshots/topic-list.png" alt="" /></p>
<p><em>帖子详情</em></p>
<p><img src="https://mazhuang.org/guanggoo-android/screenshots/topic-detail.png" alt="" /></p>
<p><em>节点列表</em></p>
<p><img src="https://mazhuang.org/guanggoo-android/screenshots/nodes-list.png" alt="" /></p>
<p><em>抽屉</em></p>
<p><img src="https://mazhuang.org/guanggoo-android/screenshots/drawer.png" alt="" /></p>
<p>挺赞的吧~然后是少得可怜的功能更新:</p>
<ol>
<li>
<p>新增关注/取消关注用户的功能;</p>
</li>
<li>
<p>对评论点赞的功能。</p>
</li>
</ol>
<p>其实好多东西都隐含在界面和交互的更新里了,需要慢慢体会……:laughing:</p>
<p>最后奉上下载二维码:</p>
<p><img src="https://mazhuang.org/guanggoo-android/qrcode.png" alt="" /></p>
<p>在微信里识别二维码后需要点击右上角从浏览器打开链接,然后下载 APK 安装。</p>
<p>下载链接:<a href="https://www.coolapk.com/apk/164523">https://www.coolapk.com/apk/164523</a></p>
<p>源码地址:<a href="https://github.com/mzlogin/guanggoo-android">https://github.com/mzlogin/guanggoo-android</a></p>
<p>预计在不久的将来还会再更新几个版本,将一些未经设计的界面进行新界面补全,将消息提醒、今日热议、发贴回贴时上传图片、屏蔽用户等功能逐渐加上去。大家有什么好建议,或者 bug 反馈,可以通过「关于我们」界面里的发邮件吐槽功能让我知道,或者去光谷社区发贴艾特我也行。也欢迎更多开发者朋友一起加入合作,磨炼磨炼技术也好,结识更多朋友也罢,也许人生的新篇章就在不经意间打开了。</p>Allen我最完整的业余作品 光谷社区第三方 Android 客户端 (虽然也还不是很完善)今天发布了重大更新,版本号由 v1.3 更新到 v2.0,欢迎所有身在武汉和心系武汉的朋友们试用体验,也欢迎各路开发者大神们对照源码(后文有链接)交流指点。解决两个 Android 模拟器之间无法网络通信的问题2017-12-03T00:00:00+08:002017-12-03T00:00:00+08:00http://www.cat-wish.cn/2017/12/03/tcp-connect-between-android-emulators<p>本文解决的是一个小众场景的问题:</p>
<p>出差在外,需要调试局域网内的两台 Android 设备之间通过 TCP 通信的情况,可手边又不是随时有多台可用的设备,于是想在笔记本上同时跑两台 Android 模拟器来构造调试环境,但是发现它俩的 IP 地址竟然都是 10.0.2.15,场面一度十分尴尬……</p>
<p><img src="/images/posts/android/ip-address.png" alt="" /></p>
<p>谷狗之后,众多相关的博客和问答贴将我引向了官方文档页面:</p>
<p><a href="https://developer.android.com/studio/run/emulator-networking.html#connecting">Interconnecting emulator instances</a></p>
<p>原来官方指南上解释过相关的知识,现将我关心和以前迷惑的部分翻译摘录如下,如果希望对此有个更全面的了解,还是推荐完整阅读 Android 官方文档里有关 Emulator 的章节 <a href="https://developer.android.com/studio/run/emulator.html">https://developer.android.com/studio/run/emulator.html</a></p>
<p>首先讲一点预备知识,再说解决方案。</p>
<h2 id="模拟器的网络地址空间">模拟器的网络地址空间</h2>
<p>每个模拟器都运行在一个虚拟路由/防火墙服务后面,这个服务将模拟器和宿主机器的网络接口、配置以及 Internet 隔离开来。对模拟器而言,宿主机器和其它模拟器对它是不可见的,它只知道自己是通过以太网连接到路由/防火墙。</p>
<p>每个模拟器的虚拟路由管理 10.0.2/24 的网络地址空间,所有地址都是 10.0.2.xx 格式。地址预分配的情况如下:</p>
<table>
<thead>
<tr>
<th>网络地址</th>
<th>描述</th>
</tr>
</thead>
<tbody>
<tr>
<td>10.0.2.1</td>
<td>路由/网络地址</td>
</tr>
<tr>
<td>10.0.2.2</td>
<td>宿主机器的 loopback interface,相当于电脑上的 127.0.0.1</td>
</tr>
<tr>
<td>10.0.2.3</td>
<td>首选 DNS Server</td>
</tr>
<tr>
<td>10.0.2.4 <br /> 10.0.2.5 <br /> 10.0.2.6</td>
<td>可选的第二、第三、第四 DNS Server</td>
</tr>
<tr>
<td>10.0.2.15</td>
<td>模拟器的网络地址</td>
</tr>
<tr>
<td>127.0.0.1</td>
<td>模拟器的 loopback interface</td>
</tr>
</tbody>
</table>
<p>需要注意的是所有模拟器的网络地址分配都是一样的,这样一来,如果有两个模拟器同时运行在一台电脑上,它们都会有各自的路由,并且给两个模拟器分配的 IP 都是 10.0.2.15。它们被路由隔离,相互不可见。</p>
<p>另外一点就是模拟器上的 127.0.0.1 是指它自己,所以如果想访问宿主机器上运行的服务,要使用 10.0.2.2。</p>
<h2 id="实现两台模拟器之间的通信">实现两台模拟器之间的通信</h2>
<p>现在来解决标题和文首提到的问题,主要用到了网络重定向。</p>
<p>假设开发环境是:</p>
<ul>
<li>
<p>PC 是指运行模拟器的宿主电脑</p>
</li>
<li>
<p>emulator-5554 是模拟器 1,将在 TCP 通信中作为 server 端</p>
</li>
<li>
<p>emulator-5556 是模拟器 2,将在 TCP 通信中作为 client 端</p>
</li>
</ul>
<p>配置步骤:</p>
<ol>
<li>
<p>在 emulator-5554 上运行 server,侦听 10.0.2.15:58080</p>
</li>
<li>
<p>在 PC 上运行 <code class="language-plaintext highlighter-rouge">cat ~/.emulator_console_auth_token</code>,得到一个 token</p>
</li>
<li>
<p>在 PC 上运行</p>
<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code> telnet localhost 5554
auth <token>
redir add tcp:51212:58080
</code></pre></div> </div>
<p><code class="language-plaintext highlighter-rouge"><token></code> 是指第 2 步中得到的 token。</p>
<p>51212 是 PC 端口,58080 是 5554 模拟器的端口。</p>
</li>
<li>
<p>在 emulator-5556 上运行 client 程序,连接 10.0.2.2:51212</p>
</li>
</ol>
<p>至此,两台模拟器之间已经可以通过 TCP 愉快地通信了。</p>
<p>它们之间的网络连接和通信示意图如下:</p>
<p><img src="/images/posts/android/emulators-communication.png" alt="" /></p>
<p><strong>注:</strong></p>
<ul>
<li>
<p>以上步骤中用到的端口号都是可以根据你的需求替换的</p>
</li>
<li>
<p>Windows 下 telnet 命令默认没有启用,具体启用方法请搜狗一下</p>
</li>
</ul>
<h2 id="模拟器的网络限制">模拟器的网络限制</h2>
<ol>
<li>
<p>模拟器上运行的 Apps 可以连接到宿主电脑上的网络,但这是通过模拟器间接实现,不是直接连到宿主电脑的网卡。模拟器可以看作是宿主电脑上运行的一个普通程序。</p>
</li>
<li>
<p>因为模拟器的特殊网络配置,可能无法支持一些网络协议,比如 ping 命令使用的 ICMP 协议。目前,模拟器不支持 IGMP 和 multicast。</p>
<p><em>试验了一下,模拟器的 shell 里 <code class="language-plaintext highlighter-rouge">ping www.sogou.com</code> 一直卡在那,在手机的 shell 里就可以。</em></p>
</li>
</ol>
<h2 id="额外的发现">额外的发现</h2>
<p>在阅读 Android 官方文档里关于模拟器的章节时,意外地发现有一节 <a href="https://developer.android.com/studio/run/emulator-networking.html#calling">Sending a voice call or SMS to another emulator instance</a></p>
<p>就是说模拟器可以给另外的模拟器打电话和发短信,电话号码就是端口号,比如 emulator-5554 模拟器,电话号码就是 5554,这个号码也可以从模拟器的窗口标题栏上找到,比如 <code class="language-plaintext highlighter-rouge">Android Emulator - Nexus_5X_API_19:5554</code>,里面那个 5554 就是。</p>
<h2 id="后话">后话</h2>
<p>天下博文,大部分都逃不出官方文档与公开源码的范畴(比如本文就是),而且都是选定文档里讲的某一小部分来进行讲解演绎,这在作为扩展视野、快速上手、快速解决问题等用途时还是比较实用的,但如果想系统、全面地学习,官方文档一般是更好的选择。</p>Allen本文解决的是一个小众场景的问题:Android 源码分析 —— 从 Toast 出发2017-11-12T00:00:00+08:002017-11-12T00:00:00+08:00http://www.cat-wish.cn/2017/11/12/start-from-toast<p>本系列文章在 <a href="https://github.com/mzlogin/rtfsc-android">https://github.com/mzlogin/rtfsc-android</a> 持续更新中,欢迎有兴趣的童鞋们关注。</p>
<p><img src="/images/posts/android/toast.png" alt="" /></p>
<p>(图 from Android Developers)</p>
<p>Toast 是 Android 开发里较常用的一个类了,有时候用它给用户弹提示信息和界面反馈,有时候用它来作为辅助调试的手段。用得多了,自然想对其表层之下的运行机制有所了解,所以在此将它选为我的第一个 RTFSC Roots。</p>
<p>本篇采用的记录方式是先对它有个整体的了解,然后提出一些问题,再通过阅读源码,对问题进行一一解读而后得出答案。</p>
<p>本文使用的工具与源码为:Chrome、插件 insight.io、GitHub 项目 <a href="https://github.com/aosp-mirror/platform_frameworks_base">aosp-mirror/platform_frameworks_base</a></p>
<p><strong>目录</strong></p>
<!-- vim-markdown-toc GFM -->
<ul>
<li><a href="#toast-印象">Toast 印象</a></li>
<li><a href="#提出问题">提出问题</a></li>
<li><a href="#解答问题">解答问题</a>
<ul>
<li><a href="#toast-的超时时间">Toast 的超时时间</a></li>
<li><a href="#能不能弹一个时间超长的-toast">能不能弹一个时间超长的 Toast?</a></li>
<li><a href="#toast-能不能在非-ui-线程调用">Toast 能不能在非 UI 线程调用?</a></li>
<li><a href="#应用在后台时能不能-toast">应用在后台时能不能 Toast?</a></li>
<li><a href="#toast-数量有没有限制">Toast 数量有没有限制?</a></li>
<li><a href="#toastmaketextshow-具体都做了些什么"><code class="language-plaintext highlighter-rouge">Toast.makeText(…).show()</code> 具体都做了些什么?</a></li>
</ul>
</li>
<li><a href="#总结">总结</a>
<ul>
<li><a href="#补充后的-toast-知识点列表">补充后的 Toast 知识点列表</a></li>
<li><a href="#遗留知识点">遗留知识点</a></li>
<li><a href="#本篇用到的源码分析方法">本篇用到的源码分析方法</a></li>
</ul>
</li>
<li><a href="#后话">后话</a></li>
</ul>
<!-- vim-markdown-toc -->
<h2 id="toast-印象">Toast 印象</h2>
<p>首先我们从 Toast 类的 <a href="1">官方文档</a> 和 <a href="2">API 指南</a> 中可以得出它具备如下特性:</p>
<ol>
<li>
<p>Toast 不是 View,它用于帮助创建并展示包含一条小消息的 View;</p>
</li>
<li>
<p>它的设计理念是尽量不惹眼,但又能展示想让用户看到的信息;</p>
</li>
<li>
<p>被展示时,浮在应用界面之上;</p>
</li>
<li>
<p>永远不会获取到焦点;</p>
</li>
<li>
<p>大小取决于消息的长度;</p>
</li>
<li>
<p>超时后会自动消失;</p>
</li>
<li>
<p>可以自定义显示在屏幕上的位置(默认左右居中显示在靠近屏幕底部的位置);</p>
</li>
<li>
<p>可以使用自定义布局,也只有在自定义布局的时候才需要直接调用 Toast 的构造方法,其它时候都是使用 makeText 方法来创建 Toast;</p>
</li>
<li>
<p>Toast 弹出后当前 Activity 会保持可见性和可交互性;</p>
</li>
<li>
<p>使用 <code class="language-plaintext highlighter-rouge">cancel</code> 方法可以立即将已显示的 Toast 关闭,让未显示的 Toast 不再显示;</p>
</li>
<li>
<p>Toast 也算是一个「通知」,如果弹出状态消息后期望得到用户响应,应该使用 Notification。</p>
</li>
</ol>
<p>不知道你看到这个列表,是否学到了新知识或者明确了以前不确定的东西,反正我在整理列表的时候是有的。</p>
<h2 id="提出问题">提出问题</h2>
<p>根据以上特性,再结合平时对 Toast 的使用,提出如下问题来继续本次源码分析之旅(大致由易到难排列,后文用 小 demo 或者源码分析来解答):</p>
<ol>
<li>
<p>Toast 的超时时间具体是多少?</p>
</li>
<li>
<p>能不能弹一个时间超长的 Toast?</p>
</li>
<li>
<p>Toast 能不能在非 UI 线程调用?</p>
</li>
<li>
<p>应用在后台时能不能 Toast?</p>
</li>
<li>
<p>Toast 数量有没有限制?</p>
</li>
<li>
<p><code class="language-plaintext highlighter-rouge">Toast.makeText(…).show()</code> 具体都做了些什么?</p>
</li>
</ol>
<h2 id="解答问题">解答问题</h2>
<h3 id="toast-的超时时间">Toast 的超时时间</h3>
<p>用这样的一个问题开始「Android 源码分析」,真的好怕被打死……大部分人都会嗤之以鼻:Are you kidding me? So easy. 各位大佬们稍安勿躁,阅读大型源码不是个容易的活,让我们从最简单的开始,一点一点建立自信,将这项伟大的事业进行下去。</p>
<p>面对这个问题,我的第一反应是去查 <code class="language-plaintext highlighter-rouge">Toast.LENGTH_LONG</code> 和 <code class="language-plaintext highlighter-rouge">Toast.LENGTH_SHORT</code> 的值,毕竟平时都是用这两个值来控制显示长/短 Toast 的。</p>
<p>文件 <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/widget/Toast.java">platform_frameworks_base/core/java/android/widget/Toast.java</a> 中能看到它们俩的定义是这样的:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Show the view or text notification for a short period of time. This time
* could be user-definable. This is the default.
* @see #setDuration
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">LENGTH_SHORT</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="cm">/**
* Show the view or text notification for a long period of time. This time
* could be user-definable.
* @see #setDuration
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">LENGTH_LONG</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span>
</code></pre></div></div>
<p>啊哦~原来它们只是两个 flag,并非确切的时间值。</p>
<p>既然是 flag,那自然就会有根据不同的 flag 来设置不同的具体值的地方,于是使用 insight.io 点击 <code class="language-plaintext highlighter-rouge">LENGTH_SHORT</code> 的定义搜索一波 <code class="language-plaintext highlighter-rouge">Toast.LENGTH_SHORT</code> 的引用,在 <a href="https://github.com/aosp-mirror/platform_frameworks_base">aosp-mirror/platform_frameworks_base</a> 里一共有 50 处引用,但都是调用 <code class="language-plaintext highlighter-rouge">Toast.makeText(...)</code> 时出现的。</p>
<p>继续搜索 <code class="language-plaintext highlighter-rouge">Toast.LENGTH_LONG</code> 的引用,在 <a href="https://github.com/aosp-mirror/platform_frameworks_base">aosp-mirror/platform_frameworks_base</a> 中共出现 42 次,其中有两处长得像是我们想找的:</p>
<p>第一处,文件 <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/widget/Toast.java">platform_frameworks_base/core/java/android/widget/Toast.java</a></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">TN</span> <span class="kd">extends</span> <span class="nc">ITransientNotification</span><span class="o">.</span><span class="na">Stub</span> <span class="o">{</span>
<span class="o">...</span>
<span class="kd">static</span> <span class="kd">final</span> <span class="kt">long</span> <span class="no">SHORT_DURATION_TIMEOUT</span> <span class="o">=</span> <span class="mi">4000</span><span class="o">;</span>
<span class="kd">static</span> <span class="kd">final</span> <span class="kt">long</span> <span class="no">LONG_DURATION_TIMEOUT</span> <span class="o">=</span> <span class="mi">7000</span><span class="o">;</span>
<span class="o">...</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleShow</span><span class="o">(</span><span class="nc">IBinder</span> <span class="n">windowToken</span><span class="o">)</span> <span class="o">{</span>
<span class="o">...</span>
<span class="n">mParams</span><span class="o">.</span><span class="na">hideTimeoutMilliseconds</span> <span class="o">=</span> <span class="n">mDuration</span> <span class="o">==</span>
<span class="nc">Toast</span><span class="o">.</span><span class="na">LENGTH_LONG</span> <span class="o">?</span> <span class="no">LONG_DURATION_TIMEOUT</span> <span class="o">:</span> <span class="no">SHORT_DURATION_TIMEOUT</span><span class="o">;</span>
<span class="o">...</span>
<span class="o">}</span>
<span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>这个 hideTimeoutMilliseconds 是干嘛的呢?</p>
<p>文件 <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/view/WindowManager.java">platform_frameworks_base/core/java/android/view/WindowManager.java</a> 里能看到这个</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* ...
* ... . Therefore, we do hide
* such windows to prevent them from overlaying other apps.
*
* @hide
*/</span>
<span class="kd">public</span> <span class="kt">long</span> <span class="n">hideTimeoutMilliseconds</span> <span class="o">=</span> <span class="o">-</span><span class="mi">1</span><span class="o">;</span>
</code></pre></div></div>
<p>在 GitHub 用 blame 查看到改动这一行的最近一次提交 <a href="https://github.com/aosp-mirror/platform_frameworks_base/commit/aa07653d2eea38a7a5bda5944c8a353586916ae9">aa07653d</a>,它的 commit message 能表明它的用途:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Prevent apps to overlay other apps via toast windows
It was possible for apps to put toast type windows
that overlay other apps which toast winodws aren't
removed after a timeout.
Now for apps targeting SDK greater than N MR1 to add a
toast window one needs to have a special token. The token
is added by the notificatoion manager service only for
the lifetime of the shown toast and is then removed
including all windows associated with this token. This
prevents apps to add arbitrary toast windows.
Since legacy apps may rely on the ability to directly
add toasts we mitigate by allowing these apps to still
add such windows for unlimited duration if this app is
the currently focused one, i.e. the user interacts with
it then it can overlay itself, otherwise we make sure
these toast windows are removed after a timeout like
a toast would be.
We don't allow more that one toast window per UID being
added at a time which prevents 1) legacy apps to put the
same toast after a timeout to go around our new policy
of hiding toasts after a while; 2) modern apps to reuse
the passed token to add more than one window; Note that
the notification manager shows toasts one at a time.
</code></pre></div></div>
<p>它并不是用来控制 Toast 的显示时间的,只是为了防止有些应用的 toast 类型的窗口长期覆盖在别的应用上面,而超时自动隐藏这些窗口的时间,可以看作是一种防护措施。</p>
<p>第二处,文件 <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/services/core/java/com/android/server/notification/NotificationManagerService.java">platform_frameworks_base/services/core/java/com/android/server/notification/NotificationManagerService.java</a> 里</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">long</span> <span class="n">delay</span> <span class="o">=</span> <span class="n">r</span><span class="o">.</span><span class="na">duration</span> <span class="o">==</span> <span class="nc">Toast</span><span class="o">.</span><span class="na">LENGTH_LONG</span> <span class="o">?</span> <span class="no">LONG_DELAY</span> <span class="o">:</span> <span class="no">SHORT_DELAY</span><span class="o">;</span>
</code></pre></div></div>
<p>在同一文件里能找到 <code class="language-plaintext highlighter-rouge">LONG_DELAY</code> 与 <code class="language-plaintext highlighter-rouge">SHORT_DELAY</code> 的定义:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">LONG_DELAY</span> <span class="o">=</span> <span class="nc">PhoneWindowManager</span><span class="o">.</span><span class="na">TOAST_WINDOW_TIMEOUT</span><span class="o">;</span>
<span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">SHORT_DELAY</span> <span class="o">=</span> <span class="mi">2000</span><span class="o">;</span> <span class="c1">// 2 seconds</span>
</code></pre></div></div>
<p>点击查看 <code class="language-plaintext highlighter-rouge">PhoneWindowManager.TOAST_WINDOW_TIMEOUT</code> 的定义:</p>
<p>文件 <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/services/core/java/com/android/server/policy/PhoneWindowManager.java">platform_frameworks_base/services/core/java/com/android/server/policy/PhoneWindowManager.java</a></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/** Amount of time (in milliseconds) a toast window can be shown. */</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="kd">final</span> <span class="kt">int</span> <span class="no">TOAST_WINDOW_TIMEOUT</span> <span class="o">=</span> <span class="mi">3500</span><span class="o">;</span> <span class="c1">// 3.5 seconds</span>
</code></pre></div></div>
<p>至此,我们可以得出 <strong>结论:Toast 的长/短超时时间分别为 3.5 秒和 2 秒。</strong></p>
<p><em>Tips: 也可以通过分析代码里的逻辑,一层一层追踪用到 <code class="language-plaintext highlighter-rouge">LENGTH_SHORT</code> 和 <code class="language-plaintext highlighter-rouge">LENGTH_LONG</code> 的地方,最终得出结论,而这里是根据一些合理推断来简化追踪过程,更快达到目标,这在一些场景下是可取和必要的。</em></p>
<h3 id="能不能弹一个时间超长的-toast">能不能弹一个时间超长的 Toast?</h3>
<p>注:这里探讨的是能否直接通过 Toast 提供的公开 API 做到,网络上能搜索到的使用 Timer、反射、自定义等方式达到弹出一个超长时间 Toast 目的的方法不在讨论范围内。</p>
<p>我们在 Toast 类的源码里看一下跟设置时长相关的代码:</p>
<p>文件 <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/widget/Toast.java">platform_frameworks_base/core/java/android/widget/Toast.java</a></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">...</span>
<span class="cm">/** @hide */</span>
<span class="nd">@IntDef</span><span class="o">({</span><span class="no">LENGTH_SHORT</span><span class="o">,</span> <span class="no">LENGTH_LONG</span><span class="o">})</span>
<span class="nd">@Retention</span><span class="o">(</span><span class="nc">RetentionPolicy</span><span class="o">.</span><span class="na">SOURCE</span><span class="o">)</span>
<span class="kd">public</span> <span class="nd">@interface</span> <span class="nc">Duration</span> <span class="o">{}</span>
<span class="o">...</span>
<span class="cm">/**
* Set how long to show the view for.
* @see #LENGTH_SHORT
* @see #LENGTH_LONG
*/</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">setDuration</span><span class="o">(</span><span class="nd">@Duration</span> <span class="kt">int</span> <span class="n">duration</span><span class="o">)</span> <span class="o">{</span>
<span class="n">mDuration</span> <span class="o">=</span> <span class="n">duration</span><span class="o">;</span>
<span class="n">mTN</span><span class="o">.</span><span class="na">mDuration</span> <span class="o">=</span> <span class="n">duration</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">...</span>
<span class="cm">/**
* Make a standard toast that just contains a text view.
*
* @param context The context to use. Usually your {@link android.app.Application}
* or {@link android.app.Activity} object.
* @param text The text to show. Can be formatted text.
* @param duration How long to display the message. Either {@link #LENGTH_SHORT} or
* {@link #LENGTH_LONG}
*
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="nc">Toast</span> <span class="nf">makeText</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nc">CharSequence</span> <span class="n">text</span><span class="o">,</span> <span class="nd">@Duration</span> <span class="kt">int</span> <span class="n">duration</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="nf">makeText</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">text</span><span class="o">,</span> <span class="n">duration</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">...</span>
</code></pre></div></div>
<p>其实从上面 <code class="language-plaintext highlighter-rouge">setDuration</code> 和 <code class="language-plaintext highlighter-rouge">makeText</code> 的注释已经可以看出,duration 只能取值 <code class="language-plaintext highlighter-rouge">LENGTH_SHORT</code> 和 <code class="language-plaintext highlighter-rouge">LENGTH_LONG</code>,除了注释之外,还使用了 <code class="language-plaintext highlighter-rouge">@Duration</code> 注解来保证此事。<code class="language-plaintext highlighter-rouge">Duration</code> 自身使用了 <code class="language-plaintext highlighter-rouge">@IntDef</code> 注解,它用于限制可以取的值。</p>
<p>文件 <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/annotation/IntDef.java">platform_frameworks_base/core/java/android/annotation/IntDef.java</a></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Denotes that the annotated element of integer type, represents
* a logical type and that its value should be one of the explicitly
* named constants. If the {@link #flag()} attribute is set to true,
* multiple constants can be combined.
* ...
*/</span>
</code></pre></div></div>
<p>不信邪的我们可以快速在一个 demo Android 工程里写一句这样的代码试试:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nc">Toast</span><span class="o">.</span><span class="na">makeText</span><span class="o">(</span><span class="k">this</span><span class="o">,</span> <span class="s">"Hello"</span><span class="o">,</span> <span class="mi">2</span><span class="o">);</span>
</code></pre></div></div>
<p>Android Studio 首先就不会同意,警告你 <code class="language-plaintext highlighter-rouge">Must be one of: Toast.LENGTH_SHORT, Toast.LENGTH_LONG</code>,但实际这段代码是可以通过编译的,因为 <code class="language-plaintext highlighter-rouge">Duration</code> 注解的 <code class="language-plaintext highlighter-rouge">Retention</code> 为 <code class="language-plaintext highlighter-rouge">RetentionPolicy.SOURCE</code>,我的理解是该注解主要能用于 IDE 的智能提示警告,编译期就被丢掉了。</p>
<p>但即使 duration 能传入 <code class="language-plaintext highlighter-rouge">LENGTH_SHORT</code> 和 <code class="language-plaintext highlighter-rouge">LENGTH_LONG</code> 以外的值,也并没有什么卵用,别忘了这里设置的只是一个 flag,真正计算的时候是 <code class="language-plaintext highlighter-rouge">long delay = r.duration == Toast.LENGTH_LONG ? LONG_DELAY : SHORT_DELAY;</code>,即 duration 为 <code class="language-plaintext highlighter-rouge">LENGTH_LONG</code> 时时长为 3.5 秒,其它情况都是 2 秒。</p>
<p>所以我们可以得出 <strong>结论:无法通过 Toast 提供的公开 API 直接弹出超长时间的 Toast。</strong>(如节首所述,可以通过一些其它方式实现类似的效果)</p>
<h3 id="toast-能不能在非-ui-线程调用">Toast 能不能在非 UI 线程调用?</h3>
<p>这个问题适合用一个 demo 来解答。</p>
<p>我们创建一个最简单的 App 工程,然后在启动 Activity 的 onCreate 方法里添加这样一段代码:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">new</span> <span class="nc">Thread</span><span class="o">(</span><span class="k">new</span> <span class="nc">Runnable</span><span class="o">()</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span>
<span class="nc">Toast</span><span class="o">.</span><span class="na">makeText</span><span class="o">(</span><span class="nc">MainActivity</span><span class="o">.</span><span class="na">this</span><span class="o">,</span> <span class="s">"Call toast on non-UI thread"</span><span class="o">,</span> <span class="nc">Toast</span><span class="o">.</span><span class="na">LENGTH_SHORT</span><span class="o">)</span>
<span class="o">.</span><span class="na">show</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}).</span><span class="na">start</span><span class="o">();</span>
</code></pre></div></div>
<p>啊哦~很遗憾程序直接挂掉了。</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>11-07 13:35:33.980 2020-2035/org.mazhuang.androiduidemos E/AndroidRuntime: FATAL EXCEPTION: Thread-77
java.lang.RuntimeException: Can't create handler inside thread that has not called Looper.prepare()
at android.widget.Toast$TN.<init>(Toast.java:390)
at android.widget.Toast.<init>(Toast.java:114)
at android.widget.Toast.makeText(Toast.java:277)
at android.widget.Toast.makeText(Toast.java:267)
at org.mazhuang.androiduidemos.MainActivity$1.run(MainActivity.java:27)
at java.lang.Thread.run(Thread.java:856)
</code></pre></div></div>
<p>顺着堆栈里显示的方法调用从下往上一路看过去,</p>
<p>文件 <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/widget/Toast.java">platform_frameworks_base/core/java/android/widget/Toast.java</a></p>
<p>首先是两级 makeText 方法:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 我们的代码里调用的 makeText 方法</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="nc">Toast</span> <span class="nf">makeText</span><span class="o">(</span><span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nc">CharSequence</span> <span class="n">text</span><span class="o">,</span> <span class="nd">@Duration</span> <span class="kt">int</span> <span class="n">duration</span><span class="o">)</span> <span class="o">{</span>
<span class="k">return</span> <span class="nf">makeText</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">text</span><span class="o">,</span> <span class="n">duration</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// 隐藏的 makeText 方法,不能手动调用</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="nc">Toast</span> <span class="nf">makeText</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nd">@Nullable</span> <span class="nc">Looper</span> <span class="n">looper</span><span class="o">,</span>
<span class="nd">@NonNull</span> <span class="nc">CharSequence</span> <span class="n">text</span><span class="o">,</span> <span class="nd">@Duration</span> <span class="kt">int</span> <span class="n">duration</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Toast</span> <span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Toast</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">looper</span><span class="o">);</span> <span class="c1">// 这里的 looper 为 null</span>
<span class="o">...</span>
</code></pre></div></div>
<p>然后到了 Toast 的构造方法:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">Toast</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nd">@Nullable</span> <span class="nc">Looper</span> <span class="n">looper</span><span class="o">)</span> <span class="o">{</span>
<span class="n">mContext</span> <span class="o">=</span> <span class="n">context</span><span class="o">;</span>
<span class="n">mTN</span> <span class="o">=</span> <span class="k">new</span> <span class="no">TN</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getPackageName</span><span class="o">(),</span> <span class="n">looper</span><span class="o">);</span> <span class="c1">// looper 为 null</span>
<span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>到 Toast$TN 的构造方法:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// looper = null</span>
<span class="no">TN</span><span class="o">(</span><span class="nc">String</span> <span class="n">packageName</span><span class="o">,</span> <span class="nd">@Nullable</span> <span class="nc">Looper</span> <span class="n">looper</span><span class="o">)</span> <span class="o">{</span>
<span class="o">...</span>
<span class="k">if</span> <span class="o">(</span><span class="n">looper</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Use Looper.myLooper() if looper is not specified.</span>
<span class="n">looper</span> <span class="o">=</span> <span class="nc">Looper</span><span class="o">.</span><span class="na">myLooper</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">looper</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span>
<span class="s">"Can't toast on a thread that has not called Looper.prepare()"</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">...</span>
<span class="o">}</span>
</code></pre></div></div>
<p>至此,我们已经追踪到了我们的崩溃的 RuntimeException,即要避免进入抛出异常的逻辑,要么调用的时候传递一个 Looper 进来(无法直接实现,能传递 Looper 参数的构造方法与 makeText 方法是 hide 的),要么 <code class="language-plaintext highlighter-rouge">Looper.myLooper()</code> 返回不为 null,提示信息 <code class="language-plaintext highlighter-rouge">Can't create handler inside thread that has not called Looper.prepare()</code> 里给出了方法,那我们在 toast 前面加一句 <code class="language-plaintext highlighter-rouge">Looper.prepare()</code> 试试?这次不崩溃了,但依然不弹出 Toast,毕竟,这个线程在调用完 <code class="language-plaintext highlighter-rouge">show()</code> 方法后就直接结束了,没有调用 <code class="language-plaintext highlighter-rouge">Looper.loop()</code>,至于为什么调用 Toast 的线程结束与否会对 Toast 的显示隐藏等起影响,在本文的后面的章节里会进行分析。</p>
<p>从崩溃提示来看,Android 并没有限制在非 UI 线程里使用 Toast,只是线程得是一个有 Looper 的线程。于是我们尝试构造如下代码,发现可以成功从非 UI 线程弹出 toast 了:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">new</span> <span class="nc">Thread</span><span class="o">(</span><span class="k">new</span> <span class="nc">Runnable</span><span class="o">()</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span>
<span class="kd">final</span> <span class="kt">int</span> <span class="no">MSG_TOAST</span> <span class="o">=</span> <span class="mi">101</span><span class="o">;</span>
<span class="kd">final</span> <span class="kt">int</span> <span class="no">MSG_QUIT</span> <span class="o">=</span> <span class="mi">102</span><span class="o">;</span>
<span class="nc">Looper</span><span class="o">.</span><span class="na">prepare</span><span class="o">();</span>
<span class="kd">final</span> <span class="nc">Handler</span> <span class="n">handler</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Handler</span><span class="o">()</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleMessage</span><span class="o">(</span><span class="nc">Message</span> <span class="n">msg</span><span class="o">)</span> <span class="o">{</span>
<span class="k">switch</span> <span class="o">(</span><span class="n">msg</span><span class="o">.</span><span class="na">what</span><span class="o">)</span> <span class="o">{</span>
<span class="k">case</span> <span class="nl">MSG_TOAST:</span>
<span class="nc">Toast</span><span class="o">.</span><span class="na">makeText</span><span class="o">(</span><span class="nc">MainActivity</span><span class="o">.</span><span class="na">this</span><span class="o">,</span> <span class="s">"Call toast on non-UI thread"</span><span class="o">,</span> <span class="nc">Toast</span><span class="o">.</span><span class="na">LENGTH_SHORT</span><span class="o">)</span>
<span class="o">.</span><span class="na">show</span><span class="o">();</span>
<span class="n">sendEmptyMessageDelayed</span><span class="o">(</span><span class="no">MSG_QUIT</span><span class="o">,</span> <span class="mi">4000</span><span class="o">);</span>
<span class="k">return</span><span class="o">;</span>
<span class="k">case</span> <span class="nl">MSG_QUIT:</span>
<span class="nc">Looper</span><span class="o">.</span><span class="na">myLooper</span><span class="o">().</span><span class="na">quit</span><span class="o">();</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="kd">super</span><span class="o">.</span><span class="na">handleMessage</span><span class="o">(</span><span class="n">msg</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">};</span>
<span class="n">handler</span><span class="o">.</span><span class="na">sendEmptyMessage</span><span class="o">(</span><span class="no">MSG_TOAST</span><span class="o">);</span>
<span class="nc">Looper</span><span class="o">.</span><span class="na">loop</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}).</span><span class="na">start</span><span class="o">();</span>
</code></pre></div></div>
<p>至于为什么 <code class="language-plaintext highlighter-rouge">sendEmptyMesageDelayed(MSG_QUIT, 4000)</code> 里的 delayMillis 我设成了 4000,这里卖个关子,感兴趣的同学可以把这个值调成 0、1000 等等看一下效果,会有一些意想不到的情况发生。</p>
<p>到此,我们可以得出 <strong>结论:可以在非 UI 线程里调用 Toast,但是得是一个有 Looper 的线程。</strong></p>
<p>ps. 上面这一段演示代码让人感觉为了弹出一个 Toast 好麻烦,也可以采用 Activity.runOnUiThread、View.post 等方法从非 UI 线程将逻辑切换到 UI 线程里执行,直接从 UI 线程里弹出,UI 线程是有 Looper 的。</p>
<p><em>知识点:这里如果对 Looper、Handler 和 MessageQueue 有所了解,就容易理解多了,预计下一篇对这三剑客进行讲解。</em></p>
<h3 id="应用在后台时能不能-toast">应用在后台时能不能 Toast?</h3>
<p>这个问题也比较适合用一个简单的 demo 来尝试回答。</p>
<p>在 MainActivity 的 onCreate 里加上这样一段代码:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">view</span><span class="o">.</span><span class="na">postDelayed</span><span class="o">(</span><span class="k">new</span> <span class="nc">Runnable</span><span class="o">()</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">run</span><span class="o">()</span> <span class="o">{</span>
<span class="nc">Toast</span><span class="o">.</span><span class="na">makeText</span><span class="o">(</span><span class="nc">MainActivity</span><span class="o">.</span><span class="na">this</span><span class="o">,</span> <span class="s">"background toast"</span><span class="o">,</span> <span class="nc">Toast</span><span class="o">.</span><span class="na">LENGTH_SHORT</span><span class="o">).</span><span class="na">show</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">},</span> <span class="mi">5000</span><span class="o">);</span>
</code></pre></div></div>
<p>然后待应用启动后按 HOME 键,等几秒看是否能弹出该 Toast 即可。</p>
<p><strong>结论是:应用在后台时可以弹出 Toast。</strong></p>
<h3 id="toast-数量有没有限制">Toast 数量有没有限制?</h3>
<p>这个问题将在下一节中一并解答。</p>
<h3 id="toastmaketextshow-具体都做了些什么"><code class="language-plaintext highlighter-rouge">Toast.makeText(…).show()</code> 具体都做了些什么?</h3>
<p>首先看一下 makeText 方法。</p>
<p>文件 <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/widget/Toast.java">platform_frameworks_base/core/java/android/widget/Toast.java</a></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Make a standard toast to display using the specified looper.
* If looper is null, Looper.myLooper() is used.
* @hide
*/</span>
<span class="kd">public</span> <span class="kd">static</span> <span class="nc">Toast</span> <span class="nf">makeText</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nd">@Nullable</span> <span class="nc">Looper</span> <span class="n">looper</span><span class="o">,</span>
<span class="nd">@NonNull</span> <span class="nc">CharSequence</span> <span class="n">text</span><span class="o">,</span> <span class="nd">@Duration</span> <span class="kt">int</span> <span class="n">duration</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Toast</span> <span class="n">result</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Toast</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">looper</span><span class="o">);</span>
<span class="nc">LayoutInflater</span> <span class="n">inflate</span> <span class="o">=</span> <span class="o">(</span><span class="nc">LayoutInflater</span><span class="o">)</span>
<span class="n">context</span><span class="o">.</span><span class="na">getSystemService</span><span class="o">(</span><span class="nc">Context</span><span class="o">.</span><span class="na">LAYOUT_INFLATER_SERVICE</span><span class="o">);</span>
<span class="nc">View</span> <span class="n">v</span> <span class="o">=</span> <span class="n">inflate</span><span class="o">.</span><span class="na">inflate</span><span class="o">(</span><span class="n">com</span><span class="o">.</span><span class="na">android</span><span class="o">.</span><span class="na">internal</span><span class="o">.</span><span class="na">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">transient_notification</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span>
<span class="nc">TextView</span> <span class="n">tv</span> <span class="o">=</span> <span class="o">(</span><span class="nc">TextView</span><span class="o">)</span><span class="n">v</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="n">com</span><span class="o">.</span><span class="na">android</span><span class="o">.</span><span class="na">internal</span><span class="o">.</span><span class="na">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">message</span><span class="o">);</span>
<span class="n">tv</span><span class="o">.</span><span class="na">setText</span><span class="o">(</span><span class="n">text</span><span class="o">);</span>
<span class="n">result</span><span class="o">.</span><span class="na">mNextView</span> <span class="o">=</span> <span class="n">v</span><span class="o">;</span>
<span class="n">result</span><span class="o">.</span><span class="na">mDuration</span> <span class="o">=</span> <span class="n">duration</span><span class="o">;</span>
<span class="k">return</span> <span class="n">result</span><span class="o">;</span>
<span class="o">}</span>
</code></pre></div></div>
<p>这个方法里就是构造了一个 Toast 对象,将需要展示的 View 准备好,设置好超时时长标记,我们可以看一下 <code class="language-plaintext highlighter-rouge">com.android.internal.R.layout.transient_notification</code> 这个布局的内容:</p>
<p>文件 <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/res/res/layout/transient_notification.xml">platform_frameworks_base/core/res/res/layout/transient_notification.xml</a></p>
<div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code>
<span class="nt"><LinearLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span>
<span class="na">android:layout_width=</span><span class="s">"match_parent"</span>
<span class="na">android:layout_height=</span><span class="s">"match_parent"</span>
<span class="na">android:orientation=</span><span class="s">"vertical"</span>
<span class="na">android:background=</span><span class="s">"?android:attr/toastFrameBackground"</span><span class="nt">></span>
<span class="nt"><TextView</span>
<span class="na">android:id=</span><span class="s">"@android:id/message"</span>
<span class="na">android:layout_width=</span><span class="s">"wrap_content"</span>
<span class="na">android:layout_height=</span><span class="s">"wrap_content"</span>
<span class="na">android:layout_weight=</span><span class="s">"1"</span>
<span class="na">android:layout_marginHorizontal=</span><span class="s">"24dp"</span>
<span class="na">android:layout_marginVertical=</span><span class="s">"15dp"</span>
<span class="na">android:layout_gravity=</span><span class="s">"center_horizontal"</span>
<span class="na">android:textAppearance=</span><span class="s">"@style/TextAppearance.Toast"</span>
<span class="na">android:textColor=</span><span class="s">"@color/primary_text_default_material_light"</span>
<span class="nt">/></span>
<span class="nt"></LinearLayout></span>
</code></pre></div></div>
<p>我们最常见的 Toast 就是从这个布局文件渲染出来的了。</p>
<p>我们继续看一下 makeText 里调用的 Toast 的构造方法里做了哪些事情:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Constructs an empty Toast object. If looper is null, Looper.myLooper() is used.
* @hide
*/</span>
<span class="kd">public</span> <span class="nf">Toast</span><span class="o">(</span><span class="nd">@NonNull</span> <span class="nc">Context</span> <span class="n">context</span><span class="o">,</span> <span class="nd">@Nullable</span> <span class="nc">Looper</span> <span class="n">looper</span><span class="o">)</span> <span class="o">{</span>
<span class="n">mContext</span> <span class="o">=</span> <span class="n">context</span><span class="o">;</span>
<span class="n">mTN</span> <span class="o">=</span> <span class="k">new</span> <span class="no">TN</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getPackageName</span><span class="o">(),</span> <span class="n">looper</span><span class="o">);</span>
<span class="n">mTN</span><span class="o">.</span><span class="na">mY</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">getResources</span><span class="o">().</span><span class="na">getDimensionPixelSize</span><span class="o">(</span>
<span class="n">com</span><span class="o">.</span><span class="na">android</span><span class="o">.</span><span class="na">internal</span><span class="o">.</span><span class="na">R</span><span class="o">.</span><span class="na">dimen</span><span class="o">.</span><span class="na">toast_y_offset</span><span class="o">);</span>
<span class="n">mTN</span><span class="o">.</span><span class="na">mGravity</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">getResources</span><span class="o">().</span><span class="na">getInteger</span><span class="o">(</span>
<span class="n">com</span><span class="o">.</span><span class="na">android</span><span class="o">.</span><span class="na">internal</span><span class="o">.</span><span class="na">R</span><span class="o">.</span><span class="na">integer</span><span class="o">.</span><span class="na">config_toastDefaultGravity</span><span class="o">);</span>
<span class="o">}</span>
</code></pre></div></div>
<p>主要就是构造了一个 TN 对象,计算了位置。</p>
<p>TN 的构造方法:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="no">TN</span><span class="o">(</span><span class="nc">String</span> <span class="n">packageName</span><span class="o">,</span> <span class="nd">@Nullable</span> <span class="nc">Looper</span> <span class="n">looper</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// XXX This should be changed to use a Dialog, with a Theme.Toast</span>
<span class="c1">// defined that sets up the layout params appropriately.</span>
<span class="kd">final</span> <span class="nc">WindowManager</span><span class="o">.</span><span class="na">LayoutParams</span> <span class="n">params</span> <span class="o">=</span> <span class="n">mParams</span><span class="o">;</span>
<span class="n">params</span><span class="o">.</span><span class="na">height</span> <span class="o">=</span> <span class="nc">WindowManager</span><span class="o">.</span><span class="na">LayoutParams</span><span class="o">.</span><span class="na">WRAP_CONTENT</span><span class="o">;</span>
<span class="n">params</span><span class="o">.</span><span class="na">width</span> <span class="o">=</span> <span class="nc">WindowManager</span><span class="o">.</span><span class="na">LayoutParams</span><span class="o">.</span><span class="na">WRAP_CONTENT</span><span class="o">;</span>
<span class="n">params</span><span class="o">.</span><span class="na">format</span> <span class="o">=</span> <span class="nc">PixelFormat</span><span class="o">.</span><span class="na">TRANSLUCENT</span><span class="o">;</span>
<span class="n">params</span><span class="o">.</span><span class="na">windowAnimations</span> <span class="o">=</span> <span class="n">com</span><span class="o">.</span><span class="na">android</span><span class="o">.</span><span class="na">internal</span><span class="o">.</span><span class="na">R</span><span class="o">.</span><span class="na">style</span><span class="o">.</span><span class="na">Animation_Toast</span><span class="o">;</span>
<span class="n">params</span><span class="o">.</span><span class="na">type</span> <span class="o">=</span> <span class="nc">WindowManager</span><span class="o">.</span><span class="na">LayoutParams</span><span class="o">.</span><span class="na">TYPE_TOAST</span><span class="o">;</span>
<span class="n">params</span><span class="o">.</span><span class="na">setTitle</span><span class="o">(</span><span class="s">"Toast"</span><span class="o">);</span>
<span class="n">params</span><span class="o">.</span><span class="na">flags</span> <span class="o">=</span> <span class="nc">WindowManager</span><span class="o">.</span><span class="na">LayoutParams</span><span class="o">.</span><span class="na">FLAG_KEEP_SCREEN_ON</span>
<span class="o">|</span> <span class="nc">WindowManager</span><span class="o">.</span><span class="na">LayoutParams</span><span class="o">.</span><span class="na">FLAG_NOT_FOCUSABLE</span>
<span class="o">|</span> <span class="nc">WindowManager</span><span class="o">.</span><span class="na">LayoutParams</span><span class="o">.</span><span class="na">FLAG_NOT_TOUCHABLE</span><span class="o">;</span>
<span class="n">mPackageName</span> <span class="o">=</span> <span class="n">packageName</span><span class="o">;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">looper</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Use Looper.myLooper() if looper is not specified.</span>
<span class="n">looper</span> <span class="o">=</span> <span class="nc">Looper</span><span class="o">.</span><span class="na">myLooper</span><span class="o">();</span>
<span class="k">if</span> <span class="o">(</span><span class="n">looper</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span>
<span class="s">"Can't toast on a thread that has not called Looper.prepare()"</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="n">mHandler</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Handler</span><span class="o">(</span><span class="n">looper</span><span class="o">,</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="o">...</span>
<span class="o">};</span>
<span class="o">}</span>
</code></pre></div></div>
<p>设置了 LayoutParams 的初始值,在后面 show 的时候会用到,设置了包名和 Looper、Handler。</p>
<p>TN 是 App 中用于与 Notification Service 交互的对象,这里涉及到 Binder 和跨进程通信的知识,这块会在后面开新篇来讲解,这里可以简单地理解一下:Notification Service 是系统为了管理各种 App 的 Notification(包括 Toast)的服务,比如 Toast,由这个服务来统一维护一个待展示 Toast 队列,各 App 需要弹 Toast 的时候就将相关信息发送给这个服务,服务会将其加入队列,然后根据队列的情况,依次通知各 App 展示和隐藏 Toast。</p>
<p>接下来看看 show 方法:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cm">/**
* Show the view for the specified duration.
*/</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">show</span><span class="o">()</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">mNextView</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="k">throw</span> <span class="k">new</span> <span class="nf">RuntimeException</span><span class="o">(</span><span class="s">"setView must have been called"</span><span class="o">);</span>
<span class="o">}</span>
<span class="nc">INotificationManager</span> <span class="n">service</span> <span class="o">=</span> <span class="n">getService</span><span class="o">();</span>
<span class="nc">String</span> <span class="n">pkg</span> <span class="o">=</span> <span class="n">mContext</span><span class="o">.</span><span class="na">getOpPackageName</span><span class="o">();</span>
<span class="no">TN</span> <span class="n">tn</span> <span class="o">=</span> <span class="n">mTN</span><span class="o">;</span>
<span class="n">tn</span><span class="o">.</span><span class="na">mNextView</span> <span class="o">=</span> <span class="n">mNextView</span><span class="o">;</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">service</span><span class="o">.</span><span class="na">enqueueToast</span><span class="o">(</span><span class="n">pkg</span><span class="o">,</span> <span class="n">tn</span><span class="o">,</span> <span class="n">mDuration</span><span class="o">);</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">RemoteException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Empty</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>调用了 INotificationManager 的 enqueueToast 方法,INotificationManager 是一个接口,其实现类在 NotificationManagerService 里,我们来看 enqueueToast 方法的实现:</p>
<p>文件 <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/services/core/java/com/android/server/notification/NotificationManagerService.java">platform_frameworks_base/services/core/java/com/android/server/notification/NotificationManagerService.java</a></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">enqueueToast</span><span class="o">(</span><span class="nc">String</span> <span class="n">pkg</span><span class="o">,</span> <span class="nc">ITransientNotification</span> <span class="n">callback</span><span class="o">,</span> <span class="kt">int</span> <span class="n">duration</span><span class="o">)</span>
<span class="o">{</span>
<span class="o">...</span>
<span class="kd">synchronized</span> <span class="o">(</span><span class="n">mToastQueue</span><span class="o">)</span> <span class="o">{</span>
<span class="o">...</span>
<span class="k">try</span> <span class="o">{</span>
<span class="nc">ToastRecord</span> <span class="n">record</span><span class="o">;</span>
<span class="kt">int</span> <span class="n">index</span> <span class="o">=</span> <span class="n">indexOfToastLocked</span><span class="o">(</span><span class="n">pkg</span><span class="o">,</span> <span class="n">callback</span><span class="o">);</span>
<span class="c1">// If it's already in the queue, we update it in place, we don't</span>
<span class="c1">// move it to the end of the queue.</span>
<span class="k">if</span> <span class="o">(</span><span class="n">index</span> <span class="o">>=</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
<span class="n">record</span> <span class="o">=</span> <span class="n">mToastQueue</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">index</span><span class="o">);</span>
<span class="n">record</span><span class="o">.</span><span class="na">update</span><span class="o">(</span><span class="n">duration</span><span class="o">);</span>
<span class="o">}</span> <span class="k">else</span> <span class="o">{</span>
<span class="c1">// Limit the number of toasts that any given package except the android</span>
<span class="c1">// package can enqueue. Prevents DOS attacks and deals with leaks.</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">isSystemToast</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">int</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="kd">final</span> <span class="kt">int</span> <span class="no">N</span> <span class="o">=</span> <span class="n">mToastQueue</span><span class="o">.</span><span class="na">size</span><span class="o">();</span>
<span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="o">;</span> <span class="n">i</span><span class="o"><</span><span class="no">N</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
<span class="kd">final</span> <span class="nc">ToastRecord</span> <span class="n">r</span> <span class="o">=</span> <span class="n">mToastQueue</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">i</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">r</span><span class="o">.</span><span class="na">pkg</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">pkg</span><span class="o">))</span> <span class="o">{</span>
<span class="n">count</span><span class="o">++;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">count</span> <span class="o">>=</span> <span class="no">MAX_PACKAGE_NOTIFICATIONS</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Slog</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="no">TAG</span><span class="o">,</span> <span class="s">"Package has already posted "</span> <span class="o">+</span> <span class="n">count</span>
<span class="o">+</span> <span class="s">" toasts. Not showing more. Package="</span> <span class="o">+</span> <span class="n">pkg</span><span class="o">);</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="nc">Binder</span> <span class="n">token</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Binder</span><span class="o">();</span>
<span class="n">mWindowManagerInternal</span><span class="o">.</span><span class="na">addWindowToken</span><span class="o">(</span><span class="n">token</span><span class="o">,</span> <span class="no">TYPE_TOAST</span><span class="o">,</span> <span class="no">DEFAULT_DISPLAY</span><span class="o">);</span>
<span class="n">record</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">ToastRecord</span><span class="o">(</span><span class="n">callingPid</span><span class="o">,</span> <span class="n">pkg</span><span class="o">,</span> <span class="n">callback</span><span class="o">,</span> <span class="n">duration</span><span class="o">,</span> <span class="n">token</span><span class="o">);</span>
<span class="n">mToastQueue</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">record</span><span class="o">);</span>
<span class="n">index</span> <span class="o">=</span> <span class="n">mToastQueue</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">-</span> <span class="mi">1</span><span class="o">;</span>
<span class="n">keepProcessAliveIfNeededLocked</span><span class="o">(</span><span class="n">callingPid</span><span class="o">);</span>
<span class="o">}</span>
<span class="c1">// If it's at index 0, it's the current toast. It doesn't matter if it's</span>
<span class="c1">// new or just been updated. Call back and tell it to show itself.</span>
<span class="c1">// If the callback fails, this will remove it from the list, so don't</span>
<span class="c1">// assume that it's valid after this.</span>
<span class="k">if</span> <span class="o">(</span><span class="n">index</span> <span class="o">==</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
<span class="n">showNextToastLocked</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">}</span> <span class="k">finally</span> <span class="o">{</span>
<span class="nc">Binder</span><span class="o">.</span><span class="na">restoreCallingIdentity</span><span class="o">(</span><span class="n">callingId</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>主要就是使用调用方传来的包名、callback 和 duration 构造一个 ToastRecord,然后添加到 mToastQueue 中。如果在 mToastQueue 中已经存在该包名和 callback 的 Toast,则只更新其 duration。</p>
<p>这段代码里有一段可以回答我们的上一个问题 <code class="language-plaintext highlighter-rouge">Toast 数量有没有限制</code> 了:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// Limit the number of toasts that any given package except the android</span>
<span class="c1">// package can enqueue. Prevents DOS attacks and deals with leaks.</span>
<span class="k">if</span> <span class="o">(!</span><span class="n">isSystemToast</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">int</span> <span class="n">count</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span>
<span class="kd">final</span> <span class="kt">int</span> <span class="no">N</span> <span class="o">=</span> <span class="n">mToastQueue</span><span class="o">.</span><span class="na">size</span><span class="o">();</span>
<span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">i</span><span class="o">=</span><span class="mi">0</span><span class="o">;</span> <span class="n">i</span><span class="o"><</span><span class="no">N</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span>
<span class="kd">final</span> <span class="nc">ToastRecord</span> <span class="n">r</span> <span class="o">=</span> <span class="n">mToastQueue</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">i</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">r</span><span class="o">.</span><span class="na">pkg</span><span class="o">.</span><span class="na">equals</span><span class="o">(</span><span class="n">pkg</span><span class="o">))</span> <span class="o">{</span>
<span class="n">count</span><span class="o">++;</span>
<span class="k">if</span> <span class="o">(</span><span class="n">count</span> <span class="o">>=</span> <span class="no">MAX_PACKAGE_NOTIFICATIONS</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">Slog</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="no">TAG</span><span class="o">,</span> <span class="s">"Package has already posted "</span> <span class="o">+</span> <span class="n">count</span>
<span class="o">+</span> <span class="s">" toasts. Not showing more. Package="</span> <span class="o">+</span> <span class="n">pkg</span><span class="o">);</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>即会计算 mToastQueue 里该包名的 Toast 数量,如果超过 50,则将当前申请加入队列的 Toast 抛弃掉。所以上一个问题的 <strong>结论是:Toast 队列里允许每个应用存在不超过 50 个 Toast。</strong></p>
<p>那么构造 ToastRecord 并加入 mToastQueue 之后是如何调度,控制显示和隐藏的呢?enqueueToast 方法里有个逻辑是如果当前列表里只有一个 ToastRecord,则调用 <code class="language-plaintext highlighter-rouge">showNextToastLocked</code>,看一下与该方法相关的代码:</p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@GuardedBy</span><span class="o">(</span><span class="s">"mToastQueue"</span><span class="o">)</span>
<span class="kt">void</span> <span class="nf">showNextToastLocked</span><span class="o">()</span> <span class="o">{</span>
<span class="nc">ToastRecord</span> <span class="n">record</span> <span class="o">=</span> <span class="n">mToastQueue</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="mi">0</span><span class="o">);</span>
<span class="k">while</span> <span class="o">(</span><span class="n">record</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="o">...</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">record</span><span class="o">.</span><span class="na">callback</span><span class="o">.</span><span class="na">show</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">token</span><span class="o">);</span>
<span class="n">scheduleTimeoutLocked</span><span class="o">(</span><span class="n">record</span><span class="o">);</span>
<span class="k">return</span><span class="o">;</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">RemoteException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
<span class="o">...</span>
<span class="k">if</span> <span class="o">(</span><span class="n">index</span> <span class="o">>=</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
<span class="n">mToastQueue</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">index</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">...</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">...</span>
<span class="nd">@GuardedBy</span><span class="o">(</span><span class="s">"mToastQueue"</span><span class="o">)</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">scheduleTimeoutLocked</span><span class="o">(</span><span class="nc">ToastRecord</span> <span class="n">r</span><span class="o">)</span>
<span class="o">{</span>
<span class="n">mHandler</span><span class="o">.</span><span class="na">removeCallbacksAndMessages</span><span class="o">(</span><span class="n">r</span><span class="o">);</span>
<span class="nc">Message</span> <span class="n">m</span> <span class="o">=</span> <span class="nc">Message</span><span class="o">.</span><span class="na">obtain</span><span class="o">(</span><span class="n">mHandler</span><span class="o">,</span> <span class="no">MESSAGE_TIMEOUT</span><span class="o">,</span> <span class="n">r</span><span class="o">);</span>
<span class="kt">long</span> <span class="n">delay</span> <span class="o">=</span> <span class="n">r</span><span class="o">.</span><span class="na">duration</span> <span class="o">==</span> <span class="nc">Toast</span><span class="o">.</span><span class="na">LENGTH_LONG</span> <span class="o">?</span> <span class="no">LONG_DELAY</span> <span class="o">:</span> <span class="no">SHORT_DELAY</span><span class="o">;</span>
<span class="n">mHandler</span><span class="o">.</span><span class="na">sendMessageDelayed</span><span class="o">(</span><span class="n">m</span><span class="o">,</span> <span class="n">delay</span><span class="o">);</span>
<span class="o">}</span>
<span class="kd">private</span> <span class="kt">void</span> <span class="nf">handleTimeout</span><span class="o">(</span><span class="nc">ToastRecord</span> <span class="n">record</span><span class="o">)</span>
<span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="no">DBG</span><span class="o">)</span> <span class="nc">Slog</span><span class="o">.</span><span class="na">d</span><span class="o">(</span><span class="no">TAG</span><span class="o">,</span> <span class="s">"Timeout pkg="</span> <span class="o">+</span> <span class="n">record</span><span class="o">.</span><span class="na">pkg</span> <span class="o">+</span> <span class="s">" callback="</span> <span class="o">+</span> <span class="n">record</span><span class="o">.</span><span class="na">callback</span><span class="o">);</span>
<span class="kd">synchronized</span> <span class="o">(</span><span class="n">mToastQueue</span><span class="o">)</span> <span class="o">{</span>
<span class="kt">int</span> <span class="n">index</span> <span class="o">=</span> <span class="n">indexOfToastLocked</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">pkg</span><span class="o">,</span> <span class="n">record</span><span class="o">.</span><span class="na">callback</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">index</span> <span class="o">>=</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
<span class="n">cancelToastLocked</span><span class="o">(</span><span class="n">index</span><span class="o">);</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">...</span>
<span class="nd">@GuardedBy</span><span class="o">(</span><span class="s">"mToastQueue"</span><span class="o">)</span>
<span class="kt">void</span> <span class="nf">cancelToastLocked</span><span class="o">(</span><span class="kt">int</span> <span class="n">index</span><span class="o">)</span> <span class="o">{</span>
<span class="nc">ToastRecord</span> <span class="n">record</span> <span class="o">=</span> <span class="n">mToastQueue</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">index</span><span class="o">);</span>
<span class="k">try</span> <span class="o">{</span>
<span class="n">record</span><span class="o">.</span><span class="na">callback</span><span class="o">.</span><span class="na">hide</span><span class="o">();</span>
<span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="nc">RemoteException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span>
<span class="o">...</span>
<span class="o">}</span>
<span class="nc">ToastRecord</span> <span class="n">lastToast</span> <span class="o">=</span> <span class="n">mToastQueue</span><span class="o">.</span><span class="na">remove</span><span class="o">(</span><span class="n">index</span><span class="o">);</span>
<span class="n">mWindowManagerInternal</span><span class="o">.</span><span class="na">removeWindowToken</span><span class="o">(</span><span class="n">lastToast</span><span class="o">.</span><span class="na">token</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="no">DEFAULT_DISPLAY</span><span class="o">);</span>
<span class="n">keepProcessAliveIfNeededLocked</span><span class="o">(</span><span class="n">record</span><span class="o">.</span><span class="na">pid</span><span class="o">);</span>
<span class="k">if</span> <span class="o">(</span><span class="n">mToastQueue</span><span class="o">.</span><span class="na">size</span><span class="o">()</span> <span class="o">></span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span>
<span class="c1">// Show the next one. If the callback fails, this will remove</span>
<span class="c1">// it from the list, so don't assume that the list hasn't changed</span>
<span class="c1">// after this point.</span>
<span class="n">showNextToastLocked</span><span class="o">();</span> <span class="c1">// 继续显示队列里的下一个 Toast</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">...</span>
<span class="kd">private</span> <span class="kd">final</span> <span class="kd">class</span> <span class="nc">WorkerHandler</span> <span class="kd">extends</span> <span class="nc">Handler</span>
<span class="o">{</span>
<span class="o">...</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleMessage</span><span class="o">(</span><span class="nc">Message</span> <span class="n">msg</span><span class="o">)</span>
<span class="o">{</span>
<span class="k">switch</span> <span class="o">(</span><span class="n">msg</span><span class="o">.</span><span class="na">what</span><span class="o">)</span>
<span class="o">{</span>
<span class="k">case</span> <span class="nl">MESSAGE_TIMEOUT:</span>
<span class="n">handleTimeout</span><span class="o">((</span><span class="nc">ToastRecord</span><span class="o">)</span><span class="n">msg</span><span class="o">.</span><span class="na">obj</span><span class="o">);</span>
<span class="k">break</span><span class="o">;</span>
<span class="o">...</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>即首先调用 <code class="language-plaintext highlighter-rouge">record.callback.show(record.token)</code>,通知 App 展示该 Toast,然后根据 duration,延时发送一条超时消息 <code class="language-plaintext highlighter-rouge">MESSAGE_TIMEOUT</code>,WorkHandler 收到该消息后,调用 <code class="language-plaintext highlighter-rouge">cancelToastLocked</code> 通知应用隐藏该 Toast,并继续调用 <code class="language-plaintext highlighter-rouge">showNextToastLocked</code> 显示队列里的下一个 Toast。这样一个机制就保证了只要队列里有 ToastRecord,就能依次显示出来。</p>
<p>机制弄清楚了,再详细看一下应用接到通知 show 和 hide 一个 Toast 后是怎么做的:</p>
<p>文件 <a href="https://github.com/aosp-mirror/platform_frameworks_base/blob/master/core/java/android/widget/Toast.java">platform_frameworks_base/core/java/android/widget/Toast.java</a></p>
<div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">static</span> <span class="kd">class</span> <span class="nc">TN</span> <span class="kd">extends</span> <span class="nc">ITransientNotification</span><span class="o">.</span><span class="na">Stub</span> <span class="o">{</span>
<span class="o">...</span>
<span class="no">TN</span><span class="o">(</span><span class="nc">String</span> <span class="n">packageName</span><span class="o">,</span> <span class="nd">@Nullable</span> <span class="nc">Looper</span> <span class="n">looper</span><span class="o">)</span> <span class="o">{</span>
<span class="o">...</span>
<span class="n">mHandler</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">Handler</span><span class="o">(</span><span class="n">looper</span><span class="o">,</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleMessage</span><span class="o">(</span><span class="nc">Message</span> <span class="n">msg</span><span class="o">)</span> <span class="o">{</span>
<span class="k">switch</span> <span class="o">(</span><span class="n">msg</span><span class="o">.</span><span class="na">what</span><span class="o">)</span> <span class="o">{</span>
<span class="k">case</span> <span class="nl">SHOW:</span> <span class="o">{</span>
<span class="nc">IBinder</span> <span class="n">token</span> <span class="o">=</span> <span class="o">(</span><span class="nc">IBinder</span><span class="o">)</span> <span class="n">msg</span><span class="o">.</span><span class="na">obj</span><span class="o">;</span>
<span class="n">handleShow</span><span class="o">(</span><span class="n">token</span><span class="o">);</span>
<span class="k">break</span><span class="o">;</span>
<span class="o">}</span>
<span class="k">case</span> <span class="nl">HIDE:</span> <span class="o">{</span>
<span class="n">handleHide</span><span class="o">();</span>
<span class="o">...</span>
<span class="k">break</span><span class="o">;</span>
<span class="o">}</span>
<span class="o">...</span>
<span class="o">}</span>
<span class="o">}</span>
<span class="o">};</span>
<span class="o">}</span>
<span class="cm">/**
* schedule handleShow into the right thread
*/</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">show</span><span class="o">(</span><span class="nc">IBinder</span> <span class="n">windowToken</span><span class="o">)</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">localLOGV</span><span class="o">)</span> <span class="nc">Log</span><span class="o">.</span><span class="na">v</span><span class="o">(</span><span class="no">TAG</span><span class="o">,</span> <span class="s">"SHOW: "</span> <span class="o">+</span> <span class="k">this</span><span class="o">);</span>
<span class="n">mHandler</span><span class="o">.</span><span class="na">obtainMessage</span><span class="o">(</span><span class="no">SHOW</span><span class="o">,</span> <span class="n">windowToken</span><span class="o">).</span><span class="na">sendToTarget</span><span class="o">();</span>
<span class="o">}</span>
<span class="cm">/**
* schedule handleHide into the right thread
*/</span>
<span class="nd">@Override</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">hide</span><span class="o">()</span> <span class="o">{</span>
<span class="k">if</span> <span class="o">(</span><span class="n">localLOGV</span><span class="o">)</span> <span class="nc">Log</span><span class="o">.</span><span class="na">v</span><span class="o">(</span><span class="no">TAG</span><span class="o">,</span> <span class="s">"HIDE: "</span> <span class="o">+</span> <span class="k">this</span><span class="o">);</span>
<span class="n">mHandler</span><span class="o">.</span><span class="na">obtainMessage</span><span class="o">(</span><span class="no">HIDE</span><span class="o">).</span><span class="na">sendToTarget</span><span class="o">();</span>
<span class="o">}</span>
<span class="o">...</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleShow</span><span class="o">(</span><span class="nc">IBinder</span> <span class="n">windowToken</span><span class="o">)</span> <span class="o">{</span>
<span class="o">...</span>
<span class="n">mWM</span><span class="o">.</span><span class="na">addView</span><span class="o">(</span><span class="n">mView</span><span class="o">,</span> <span class="n">mParams</span><span class="o">);</span>
<span class="o">...</span>
<span class="o">}</span>
<span class="o">...</span>
<span class="kd">public</span> <span class="kt">void</span> <span class="nf">handleHide</span><span class="o">()</span> <span class="o">{</span>
<span class="o">...</span>
<span class="n">mWM</span><span class="o">.</span><span class="na">removeViewImmediate</span><span class="o">(</span><span class="n">mView</span><span class="o">);</span>
<span class="o">...</span>
<span class="o">}</span>
<span class="o">}</span>
</code></pre></div></div>
<p>显示过程:show 方法被远程调用后,先是发送了一个 SHOW 消息,接收到该消息后调用了 handleShow 方法,然后 <code class="language-plaintext highlighter-rouge">mWM.addView</code> 将该 View 添加到窗口。</p>
<p>隐藏过程:hide 方法被远程调用后,先是发送了一个 HIDE 消息,接收到该消息后调用了 handleHide 方法,然后 <code class="language-plaintext highlighter-rouge">mWM.removeViewImmediate</code> 将该 View 从窗口移除。</p>
<p><em>这里插播一条结论,就是前文留下的为什么调用 Toast 的线程线束之后没弹出的 Toast 就无法弹出了的问题,因为 Notification Service 通知应用进程显示或隐藏 Toast 时,使用的是 <code class="language-plaintext highlighter-rouge">mHandler.obtainMessage(SHOW).sendToTarget()</code> 与 <code class="language-plaintext highlighter-rouge">mHandler.obtainMessage(HIDE).sendToTarget()</code>,这个消息发出去后,Handler 对应线程没有在 <code class="language-plaintext highlighter-rouge">Looper.loop()</code> 过程里的话,就没有办法进入到 Handler 的 handleMessage 方法里去,自然也就无法调用显示和隐藏 View 的流程了。<code class="language-plaintext highlighter-rouge">Looper.loop()</code> 相关的知识点将在下篇讲解。</em></p>
<h2 id="总结">总结</h2>
<h3 id="补充后的-toast-知识点列表">补充后的 Toast 知识点列表</h3>
<ol>
<li>
<p>Toast 不是 View,它用于帮助创建并展示包含一条小消息的 View;</p>
</li>
<li>
<p>它的设计理念是尽量不惹眼,但又能展示想让用户看到的信息;</p>
</li>
<li>
<p>被展示时,浮在应用界面之上;</p>
</li>
<li>
<p>永远不会获取到焦点;</p>
</li>
<li>
<p>大小取决于消息的长度;</p>
</li>
<li>
<p>超时后会自动消失;</p>
</li>
<li>
<p>可以自定义显示在屏幕上的位置(默认左右居中显示在靠近屏幕底部的位置);</p>
</li>
<li>
<p>可以使用自定义布局,也只有在自定义布局的时候才需要直接调用 Toast 的构造方法,其它时候都是使用 makeText 方法来创建 Toast;</p>
</li>
<li>
<p>Toast 弹出后当前 Activity 会保持可见性和可交互性;</p>
</li>
<li>
<p>使用 <code class="language-plaintext highlighter-rouge">cancel</code> 方法可以立即将已显示的 Toast 关闭,让未显示的 Toast 不再显示;</p>
</li>
<li>
<p>Toast 也算是一个「通知」,如果弹出状态消息后期望得到用户响应,应该使用 Notification;</p>
</li>
<li>
<p>Toast 的超时时间为 LENGTH_SHORT 对应 2 秒,LENGTH_LONG 对应 3.5 秒;</p>
</li>
<li>
<p>不能通过 Toast 类的公开方法直接弹一个时间超长的 Toast;</p>
</li>
<li>
<p>应用在后台时可以调用 Toast 并正常弹出;</p>
</li>
<li>
<p>Toast 队列里允许单个应用往里添加 50 个 Toast,超出的将被丢弃。</p>
</li>
</ol>
<h3 id="遗留知识点">遗留知识点</h3>
<p>本篇涉及到了一些需要进一步了解的知识点,在后续的篇章中会依次解读:</p>
<ol>
<li>
<p>Handler、Looper 和 MessageQueue</p>
</li>
<li>
<p>WindowManager</p>
</li>
<li>
<p>Binder 与跨进程通信</p>
</li>
</ol>
<h3 id="本篇用到的源码分析方法">本篇用到的源码分析方法</h3>
<ol>
<li>
<p>查找关键变量被引用的地方;</p>
</li>
<li>
<p>按方法调用堆栈一层层逻辑跟踪与分析;</p>
</li>
<li>
<p>使用 git blame 查看关键代码行的变更日志;</p>
</li>
</ol>
<h2 id="后话">后话</h2>
<p>到此,上面提到的几个问题都已经解答完毕,对 Toast 源码的分析也告一段落。</p>
<p>写这篇文章花费的时间比较长,所以并不能按照预计的节奏更新,这里表示抱歉。另外,各位如果有耐心读到这里,觉得本文的思路是否清晰,是否能跟随文章的节奏理解一些东西?因为我也在摸索写这类文章的组织形式,所以也希望能收到反馈和建议,以作改进,先行谢过。</p>
<hr />
<p>最后,照例要安利一下我的微信公众号「闷骚的程序员」,扫码关注,接收 rtfsc-android 的最近更新。</p>
<div align="center"><img width="192px" height="192px" src="https://mazhuang.org/assets/images/qrcode.jpg" /></div>Allen本系列文章在 https://github.com/mzlogin/rtfsc-android 持续更新中,欢迎有兴趣的童鞋们关注。程序员节的过节姿势大全2017-10-24T00:00:00+08:002017-10-24T00:00:00+08:00http://www.cat-wish.cn/2017/10/24/1024-poses<p>今天是 10 月 24 日,不知道你的朋友圈有没有被程序员节刷屏,反正我的是被刷了。</p>
<p>看到 1024 这个数字,相信很多人都怀着特别的感情,比如我,游泳不会止步于 1000 米,肯定会补 24 米凑个整,跑步如果跑到 10 公里,那一定再多跑个 0.24 出来。</p>
<p>搞不好还会想起那些年追过的社区,嗟叹一下逝去的青春:</p>
<p><img src="/images/blog/1024-gold.png" alt="" /></p>
<p>那么,这样一个特别的日子,我的交游圈里大家是以怎样的姿势度过的呢?</p>
<h2 id="程序员们怎么过">程序员们怎么过</h2>
<h3 id="聚众自黑型">聚众自黑型</h3>
<p>作为互联网上最擅长自黑自嘲,以至于现在不明真相的群众都把他们的自黑当真话听的群体,这一天怎么会甘于寂寞,今天微信群里的画风是这样的:</p>
<p><img src="/images/blog/overtime.jpeg" alt="" /></p>
<p><img src="/images/blog/3w-programmer.jpeg" alt="" /></p>
<p><img src="/images/blog/fake.jpeg" alt="" /></p>
<p>(from 掘金.专栏作者群)</p>
<p>大家纷纷表示自己是个假程序员。</p>
<h3 id="感xuan恩yao公司关怀型">感(xuan)恩(yao)公司关怀型</h3>
<p>以重视员工工作体验著称的互联网公司们也没闲着,为程序员们推出了各种福利,所以今天朋友圈里的画风是这样的:</p>
<p><img src="/images/blog/1024-sogou.jpeg" alt="" /></p>
<p><img src="/images/blog/1024-meituan.jpeg" alt="" /></p>
<p><img src="/images/blog/1024-kuaishou.jpeg" alt="" /></p>
<p>还有程序员鼓励师出没:</p>
<p><img src="/images/blog/encourage.jpeg" alt="" /></p>
<p>(from 掘金.专栏作者群)</p>
<h2 id="非程序员们怎么过">非程序员们怎么过</h2>
<p><img src="/images/blog/doubt.jpeg" alt="" /></p>
<p>关我屁事?</p>
<h2 id="公众号们怎么过">公众号们怎么过</h2>
<p>也算一年一度的节,相关的公众号们也没闲着。</p>
<h3 id="科普型">科普型</h3>
<p><img src="/images/blog/kepu.jpeg" alt="" /></p>
<p>(知晓程序员)</p>
<h3 id="在世界中心呼唤爱型">在世界中心呼唤爱型</h3>
<p><img src="/images/blog/request-love.jpeg" alt="" /></p>
<p>(Java 程序员联盟)</p>
<h3 id="趁势搞活动型">趁势搞活动型</h3>
<p><img src="/images/blog/1024-vote.jpeg" alt="" /></p>
<p>(InfoQ)</p>
<p><img src="/images/blog/release-books.jpeg" alt="" /></p>
<p>(码农翻身)</p>
<h3 id="欠揍型">欠揍型</h3>
<p><img src="/images/blog/qianzou.jpeg" alt="" /></p>
<p>(微信派)</p>
<h2 id="掘金怎么过">掘金怎么过</h2>
<p>文首的图就是掘金社区去年 1024 出品,今年他们录了一首歌,链接:</p>
<p><a href="https://juejin.im/post/59ee13d7f265da43284006e3">老子今天不加班,程序员也需要自由</a></p>
<h2 id="后话">后话</h2>
<p>好了,夜已深,打完收工。祝 WE 程序员们少熬夜,保住我们的发际线。</p>Allen今天是 10 月 24 日,不知道你的朋友圈有没有被程序员节刷屏,反正我的是被刷了。发布一款光谷社区第三方 Android App2017-10-14T00:00:00+08:002017-10-14T00:00:00+08:00http://www.cat-wish.cn/2017/10/14/guanggoo-android-app<p>在过去的一个来月,我利用业余时间做了一款光谷社区的第三方 Android 客户端。</p>
<h2 id="前言">前言</h2>
<p>光谷社区是我在决定离开帝都回武汉的过程中,及回武汉之后关注得较多的武汉本土社区,网站 <a href="http://guanggoo.com">http://guanggoo.com</a> 自己的 description 是这样的:</p>
<blockquote>
<p>光谷社区是源自光谷的高端社交网络,这里有关于创业、创意、IT、金融等最热话题的交流,也有招聘问答、活动交友等最新资讯的发布。</p>
</blockquote>
<p>描述得还比较准确。我觉得身在光谷,或者心系光谷的童鞋们可以关注一下。</p>
<h2 id="发布详情">发布详情</h2>
<p>目前支持特性:</p>
<ol>
<li>登录</li>
<li>首页主题列表(三种视图)</li>
<li>主题详情 / 评论列表</li>
<li>节点列表 / 节点主题列表</li>
<li>评论 / 艾特用户</li>
<li>分享主题链接</li>
<li>发表新主题</li>
<li>查看用户信息</li>
</ol>
<p>源码放在 GitHub 上:</p>
<p><a href="https://github.com/mzlogin/guanggoo-android">https://github.com/mzlogin/guanggoo-android</a></p>
<p>部分界面截图:</p>
<p><img src="https://mazhuang.org/guanggoo-android/screenshots/topic-list.png" alt="" /></p>
<p><img src="https://mazhuang.org/guanggoo-android/screenshots/topic-detail.png" alt="" /></p>
<p><img src="https://mazhuang.org/guanggoo-android/screenshots/nodes-list.png" alt="" /></p>
<p><img src="https://mazhuang.org/guanggoo-android/screenshots/drawer.png" alt="" /></p>
<p>更多的功能开发、完善以及优化还在进行中,也希望看到的朋友们下载试用起来,多提建议多交流。</p>
<p>好吧,啰嗦了这么多,哪里能够下载得到呢?</p>
<p><strong>APK 下载链接</strong></p>
<p>(如果是在微信里看到这里,建议长按后复制链接到浏览器打开)</p>
<p><a href="https://mazhuang.org/guanggoo-android/guanggoo-lastest.apk">https://mazhuang.org/guanggoo-android/guanggoo-lastest.apk</a></p>
<p>百度网盘备用链接:</p>
<p><a href="https://pan.baidu.com/s/1pL0t1Zd">https://pan.baidu.com/s/1pL0t1Zd</a></p>
<p><strong>扫描或识别二维码下载</strong></p>
<p>(如果使用微信识别二维码不能开始下载,还是复制上方的链接到浏览器打开下载吧)</p>
<div align="center"><img width="192px" height="192px" src="https://mazhuang.org/guanggoo-android/qrcode.png" /></div>
<h2 id="为什么会做这个">为什么会做这个</h2>
<ol>
<li>
<p>社区目前只有 Web 页面,做了移动端适配,体验也还不错。不过作为一个打开频率较高的应用,我还是希望能用上 App;</p>
</li>
<li>
<p>之前偶然在社区的几个帖子里也有一些用户问到是否有 App 可用,都没有了下文,可以满足一下这部分用户的需求;</p>
</li>
<li>
<p>作为一个长期维护的业余项目,更深刻地体会 App 开发的整个生命周期,也将一些想学习的技术应用到实际项目中;</p>
</li>
<li>
<p>借此机会认识一下光谷技术圈子里志趣相投的朋友。</p>
</li>
</ol>
<h2 id="前缘后续">前缘后续</h2>
<p>上 GitHub 搜索 guanggoo 出来的结果很少,发现有一个 <a href="https://github.com/cauil/react-native-guanggoo">cauil/react-native-guanggoo</a> 的项目适配了 iOS,独缺 Android 客户端,于是决定自己写一个。要不是那一阵刚好闹 Facebook 开源许可证风波,让人没有学习 React Native 的信心和欲望,也许我就学点 React Native 在这位仁兄的基础上开发了。</p>
<p>经过几周业余时间和十一长假期间的开发,目前完成度不算特别高,但常用的功能已经基本可用了,当然还有一些功能比如注册、帖子里的外部链接打开等,我是先抛给了系统浏览器。想着只埋头自己开发也比较枯燥,决定先放出一个版本来让网友们吐吐槽,提提意见,应该能做得更好。</p>
<p>PS: 本文非软文,也没有收取光谷社区任何好处,请光谷社区嘴炮管理员看到这里帮我开通个 VIP,我的社区 ID 是 mzlogin,:-P。</p>
<hr />
<p>好了,最后照例安利一下我自己的微信公众号,近期专注 Java、Android 相关的技术分享,如果你感兴趣,可以关注一下接收最新动态。</p>
<div align="center"><img width="192px" height="192px" src="https://mazhuang.org/assets/images/qrcode.jpg" /></div>Allen在过去的一个来月,我利用业余时间做了一款光谷社区的第三方 Android 客户端。