写代码的猴子 http://jaeger.itscoder.com/ GitHub Page 博客自定义域名添加 HTTPS 支持 <p>2015 年底,不少互联网公司已经实现了全站 HTTPS 了,如今已经 2017 年了,作为一个开发者,个人博客还没加锁的,不免有点惭愧。最近博客改版,在家打开博客欣赏的时候,发现被糊了一堆牛皮癣,就花了点时间给本站上了 HTTPS。</p> <h3 id="为什么要上-https">为什么要上 HTTPS</h3> <p>关于 HTTPS 就不多介绍了,感兴趣的可以去看下知乎这个问题 <a href="https://www.zhihu.com/question/40371841">为什么 2015 年底各大网站都纷纷用起了 HTTPS? - 知乎</a> 里面提到最多的就是运营商流量劫持,插入牛皮癣广告。</p> <p>下图就是我在家欣赏本站的截图: <img src="/img/postimg/blog_with_ad.jpeg" alt="" /></p> <p>全是治疗脱发的广告,干他大爷的长城宽带 😒</p> <p>我以为就是一时劫持,就没在意了。过了几天,再看看,还是一堆牛皮癣,我就怒了,是时候上 HTTPS 了。</p> <h3 id="上-https-教程">上 HTTPS 教程</h3> <p>搞小程序那会接触了下 HTTPS,那会了解到的 SSL 证书都是需要花钱买的,前阵子看到 <a href="https://certbot.eff.org/">Certbot</a> 的介绍,才知道 <a href="https://letsencrypt.org/">Let’s Encrypt</a> 有提供免费证书的服务:</p> <blockquote> <p>Let’s Encrypt 是一个于2015年三季度推出的数字证书认证机构,将通过旨在消除当前手动创建和安装证书的复杂过程的自动化流程,为安全网站提供免费的SSL/TLS证书。</p> </blockquote> <p>但是 Certbot 适用于博客放在自己主机上的,本站是基于 GitHub 搭建的,因此没法使用 Certbot 服务。如果是直接使用 <code class="highlighter-rouge">username.github.io</code> 这样的域名的话,是默认就支持了 HTTPS 的, 直接访问 <code class="highlighter-rouge">https://username.github.io</code> 即可,自定义域名就需要自己折腾一下了。</p> <p>整个过程也是比较简单的,10 分钟就可以给你的博客加个锁。</p> <p>这里使用 <a href="https://www.netlify.com/">Netlify</a> 提供的服务来完成我们操作。</p> <ol> <li> <p>注册一个 Netlify 帐号,地址 <a href="https://app.netlify.com/signup">Netlify App</a> 选择用 GitHub 注册就好了。</p> </li> <li> <p>添加一个新的站点 <img src="/img/postimg/netlify_add_site.jpg" alt="" /></p> </li> <li> <p>配置站点,简单来说就是添加你的博客仓库地址,然后把博客的构建放在 Netlify 上,按照步骤来即可。 <img src="/img/postimg/netify_select_repo.jpg" alt="" /></p> <p>在最后的 Deploy 步骤中,提示你 Published deploy 就说明好了,直接访问链接,就可以看到你的博客了。 <img src="/img/postimg/deploy_result.jpg" alt="" /></p> </li> <li> <p>点 Back to Deploys 返回到设置页面,在 <code class="highlighter-rouge">Site Details</code> 中可以点击 <code class="highlighter-rouge">Change site name</code>,修改成一个简短点的名字,我这里取名叫 <code class="highlighter-rouge">jaeger</code>,然后就可以通过 <a href="https://jaeger.netlify.com/">https://jaeger.netlify.com/</a> 来访问博客了。</p> </li> <li> <p>设置自己的域名 在 <code class="highlighter-rouge">Domain management</code> 中设置自己的域名,我这里设置成 <code class="highlighter-rouge">jaeger.itscoder.com</code>。 <img src="/img/postimg/domin_setting.jpg" alt="" /></p> </li> <li> <p>在自己的域名管理中设置 DNS 解析,itsCoder 组织的域名使用的是阿里云,在域名管理里面设置如下的域名解析规则:</p> <p><img src="/img/postimg/set_domain_dns.png" alt="" /></p> </li> <li> <p>回到 Netlify ,还是在 <code class="highlighter-rouge">Domain management</code> 中,找到 HTTPS,依次设置如下两个即可,稍等片刻之后,你就发现你的站点加上了小锁了。整个世界都美好了。 <img src="/img/postimg/netlify_https_setting.jpg" alt="" /></p> </li> </ol> <h3 id="最后">最后</h3> <p>欣赏下成果,完美。</p> <p><img src="/img/postimg/laobie_blog.jpg" alt="" /></p> <p>最后再问候下劫持流量插广告的垃圾运营商。</p> <p><img src="/img/postimg/fuck_ad.jpg" alt="" /></p> Wed, 30 Aug 2017 00:00:00 +0000 http://jaeger.itscoder.com//web/2017/08/30/github-page-https.html http://jaeger.itscoder.com//web/2017/08/30/github-page-https.html 2016 年过去了 <center> <iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=80% height=90 src="https://music.163.com/outchain/player?type=2&id=31381877&auto=1&height=66"> </iframe> </center> <p>这是一篇迟到的年终总结。2016 年过去了,村上还是没获诺奖,逼哥的专辑一张比一张贵,能听的歌也越来越少,想去看跨年现场也一直未能成行,《行尸走肉》第七季剧情依旧拖沓,一整年好像也没一部值得看第二遍的电影……</p> <p>这篇总结写写删删,还是不知道从何写起。过去的一年好像过得很充实,但仔细想想,好像什么都差了那么点意思。这和自己一直没想清楚自己到底想要做什么有关,没有长远的目标,只知道做好眼前的事。和大学里一样,时间是没浪费,但是是不是都花在值得的地方,就不得而知了。</p> <p>过去的一年好像一直忙于工作,虽说工作一年多了,但是一直也没有过年假。除了几个法定节假日之外,似乎一直过着上五天班休息二天的日子,忙忙歇歇,一年就到了头。</p> <p>憋了半天还是不知道该写点啥,毕业之后好像越来越不爱表达自己了。去年下半年也关闭了微信朋友圈,活跃的社交平台就剩下了微博,闲下来看看段子,转发点技术微博,哪怕是原创的也似乎都和技术有关。封闭自我的结果就是自己度过了一段灰暗的时期,失眠、焦躁、低落,经常是到了凌晨一两点还处在亢奋的状态,无法入睡。然后也是那段时间听郭德纲的相声、听《晓说》,还别说,这些东西还有点意思。后来不知不觉中也就慢慢习惯了,熬夜到 12 点,上床也就能睡着了。</p> <p>还是写点去年的总结吧,改变以前流水帐的形式,几个关键字总结下吧。</p> <h4 id="怀疑对互联网行业的重新认识">怀疑——对互联网行业的重新认识</h4> <p>熟悉我的人都知道我是从机械专业半路出家到计算机行业的,我本以为这个行业是个相对踏实的行业,因为我一直觉得技术上的东西来不得半点虚假和马虎,对的就是对的,错的就是错的。</p> <p>后来我发现我错了。这个行业比起别的传统行业似乎更加浮躁。技术的更新让很多人产生不安,一直处在患得患失的边缘,小程序一出,就各种原生要失业,颠覆技术圈。加上这个圈子是基于互联网的,互联网上该有的不好现象这个圈子也都有:标题党、鸡汤大 V、撕*大战,这个圈子比我想的要乱的多,踏踏实实搞技术的人反倒少了。</p> <p>去年自己也做了几个很初级的开源项目,但是就这么初级的项目,而且也有较为详细的文档说明,很多人还是会提很多让你无语的问题。这样的开源环境下,我觉得倒是培养了更多的“伸手党”。</p> <p>我一度在怀疑自己当初的决定是不是正确的,好在这个行业还是有很多踏实做事的人,写出来的代码还是不会骗人的,自己对写代码这件事还是喜欢的。</p> <h4 id="itscoder我们的组织">itsCoder——我们的组织</h4> <p>这应该是这一年最大的收获。</p> <p>认识了一群志同道合的朋友,虽然很多到现在都没见过面,但心里已经是老朋友了。</p> <p>itsCoder 中的成员有一个最大的共性:都很上进。大家的起点可能有所差异,但是大家都在努力着前行。这也是支撑 WeeklyBlog 项目不到半年多时间产出 80 多篇高质量文章的最大原因。</p> <p>去年由于个人工作和精力有限,对于 itsCoder 的工作还是很不够的,今年也计划制定一些新的规划,让组织一起做点更有意思的事。</p> <h4 id="认识自己">认识自己</h4> <p>越发觉得最难的事情就是认识自己。</p> <p>特别是在外面的声音特别大的时候,一个人很容易就迷失了自己,无法清醒地认识自己。</p> <blockquote> <p>更重要的是,如果以获得更多的人喜欢,成为自己的生存目标,那才是进入了最大的陷阱。</p> </blockquote> <p>这是去年看到的一句话,我一直放在 Keep 中提醒自己。大多数时候,认识自己的方式似乎都是通过别人的评价来衡量自己,别人的夸赞、贬低都会影响对自己的认知,让人产生自我怀疑。</p> <p>我一直在提醒自己:要清楚自己几斤几两,不要迷失自己。</p> <h4 id="新年计划">新年计划</h4> <ol> <li>技术上还是多学习,Android 深入学习,前端技能点继续点亮。</li> <li>补计算机基础,这个是一而再再而三拖着的计划了,今年要开始着手了</li> <li>itsCoder 新项目开展</li> <li>多读书,不限技术,更多地读一些人文方面的书,扩充自己的视野。</li> </ol> <p>2016 年过去了,我似乎并不怀念它。</p> Sun, 12 Feb 2017 00:00:00 +0000 http://jaeger.itscoder.com//%E9%9A%8F%E7%AC%94/2017/02/12/hello-2017.html http://jaeger.itscoder.com//%E9%9A%8F%E7%AC%94/2017/02/12/hello-2017.html 自定义选择复制功能的实现 <blockquote> <ul> <li>文章来源:itsCoder 的 <a href="https://github.com/itsCoder/weeklyblog">WeeklyBolg</a> 项目</li> <li>itsCoder 主页:<a href="http://itscoder.com/">http://itscoder.com/</a></li> <li>作者:<a href="https://github.com/laobie">写代码的猴子</a></li> <li>审阅者:<a href="https://github.com/jasonim">jasonim (Jiandong Hu)</a></li> </ul> </blockquote> <h3 id="写在前面">写在前面</h3> <p>先来个段子:</p> <blockquote> <p>刚工作时遇到一个特别难搞定的需求,当时没做出来,感到很羞耻。过了几年,再一次遇到这个需求,还是没做出来,只是不再感到羞耻了。</p> </blockquote> <p>在我刚开始工作的时候,也有过一次这样的经历。当时项目中有个需求,让 TextView 中的文本可以选择复制,正常来讲,应该是很容易实现的,直接按照下面的设置就可以了:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mTextView</span><span class="o">.</span><span class="na">setTextIsSelectable</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> </code></pre></div></div> <p>但是,这个简单的实现并不是完美的,主要有几个问题:</p> <ul> <li> <p><strong>不同版本选择复制样式不统一</strong>:在原生系统上 6.0 之前和之后的操作样式是不同的,这里不得不说,6.0 以下的这个选择复制操作交互很不合理,且对应用的界面侵入太多。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/d50f9abab0429d5c.png" alt="" /></p> </li> <li> <p><strong>万恶的国产 ROM 问题</strong>:当时公司测试同事提 bug 反馈,在 vivo 手机上这么设置,长按之后并没有效果。(再一次吐槽乱改系统的国产 ROM,这也是为什么 Android 开发比起 iOS 费事费力的原因之一)</p> </li> <li> <p><strong>可定制性不高</strong>:如果仅仅是一个选择复制的功能,不考虑以上两个问题,还能凑合搞定,但是假如多个需求,选中文字之后直接进行某个操作,比如收藏、发送给好友,此时原生的选择复制功能可能就不足以胜任了。</p> </li> </ul> <p>以上说了这么多,问题的解决办法就是:自己写一个选择复制的功能,这样以上三个问题都能很好地解决了。</p> <p>看起来很容易,但是对于当时刚刚入门的我来说,这是个完全没头绪的任务。</p> <p>时隔一年之后,再遇到这个需求,这次通过 Google、GitHub ,以及参考 SDK 23 中 TextView 源码,基本上实现了自定义选择复制的功能,效果如下:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/378d52583767882d.png" alt="" /></p> <p>保证所有的平台上显示效果一致,弹出的操作菜单可以自己定制,并设置相应的操作。</p> <h3 id="实现要求和要点">实现要求和要点</h3> <p>在开始具体的实现之前,先确定下实现的要求:</p> <ul> <li>尽可能保证和 Android 6.0 原生选择复制一样的交互和基础功能</li> <li>尽可能不需要侵入太多,为了实现选择复制功能,重新自定义 TextView 的方式是不够优雅的,特别是考虑到项目中本来就已经使用了自定义的 TextView ,一旦需求变更,改动成本很大</li> <li>可用的自定义配置</li> </ul> <p>本文最终实现的使用方式如下所示,均满足以上的实现要求:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mSelectableTextHelper</span> <span class="o">=</span> <span class="k">new</span> <span class="n">SelectableTextHelper</span><span class="o">.</span><span class="na">Builder</span><span class="o">(</span><span class="n">mTvTest</span><span class="o">)</span> <span class="o">.</span><span class="na">setSelectedColor</span><span class="o">(</span><span class="n">getResources</span><span class="o">().</span><span class="na">getColor</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">color</span><span class="o">.</span><span class="na">selected_blue</span><span class="o">))</span> <span class="o">.</span><span class="na">setCursorHandleSizeInDp</span><span class="o">(</span><span class="mi">20</span><span class="o">)</span> <span class="o">.</span><span class="na">setCursorHandleColor</span><span class="o">(</span><span class="n">getResources</span><span class="o">().</span><span class="na">getColor</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">color</span><span class="o">.</span><span class="na">cursor_handle_color</span><span class="o">))</span> <span class="o">.</span><span class="na">build</span><span class="o">();</span> </code></pre></div></div> <p>整个自定义的选择复制功能视图上主要有三个部分:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/ea5c7296983d4e68.png" alt="" /></p> <ul> <li>选择游标</li> <li>选中的文本</li> <li>操作框</li> </ul> <p>在具体实现中有以下要点:</p> <ul> <li>自定义选择游标,可以拖动定位选中文本</li> <li>文本的选中状态</li> <li>操作框的显示,以及对应操作的处理</li> <li>在可滑动布局中的特殊处理,例如在 ScrollView 中,当视图滚动时隐藏或者移动选择游标,隐藏操作框,停止滑动时重新显示选择游标和操作框</li> <li>选中文本后,点击 TextView 取消选择</li> </ul> <h3 id="实现思路">实现思路</h3> <p>在开始实践之前,查找资料是少不了的,首先找到了 <a href="http://kymjs.com/code/2016/08/13/01">记划词模块重构感受|开源实验室-张涛</a> 这篇文章,但是这篇文章中更多是提供了一个改进某个开源项目的思路,并没有给出具体的代码,而且连那个开源项目也没给出地址。</p> <p>后来通过搜索关键字,找到了那个开源项目: <a href="https://github.com/zhouray/SelectableTextView">zhouray/SelectableTextView</a></p> <p>如张涛吐槽的那样,这个项目的实现确实不够优雅,主要存在两个问题:</p> <ul> <li>自定义 TextView 实现的,侵入太多</li> <li>解决嵌套在滑动布局中的处理太简单粗暴,竟然自定义了一个 ScrollView 来处理,应用到实际场景中是存在问题的</li> </ul> <p>如果你有时间可以看一下这个项目的代码,在本文后面的实现中,也部分参考了该项目。</p> <p>参考上面提到的文章和开源项目,实现思路基本确定了:</p> <ul> <li>选择游标使用 PopupWindow 实现,并重写 Touch 事件处理逻辑,实现拖动定位选择文本</li> <li>选中文本使用 <code class="highlighter-rouge">BackgroundColorSpan</code> 来显示,比较简单</li> <li>操作框同样使用 PopupWindow 实现,重点是处理好显示的位置</li> </ul> <p>大致的思路确定,接下来就是具体的实现了。</p> <h3 id="具体实现过程">具体实现过程</h3> <p>自定义的选择复制类取名为 <code class="highlighter-rouge">SelectableTextHelper</code>,其有一个字段 <code class="highlighter-rouge">mTextView</code>,持有需要设置选择复制的 <code class="highlighter-rouge">TextView</code> 对象。</p> <h4 id="初步设置">初步设置</h4> <p>由于 <code class="highlighter-rouge">TextView</code> 的文本的 <code class="highlighter-rouge">BufferType</code> 类型是 <code class="highlighter-rouge">SPANNABLE</code> 时才可以设置 Span ,实现选中的效果,因此在一开始先给 TextView 设置下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mTextView</span><span class="o">.</span><span class="na">setText</span><span class="o">(</span><span class="n">mTextView</span><span class="o">.</span><span class="na">getText</span><span class="o">(),</span> <span class="n">TextView</span><span class="o">.</span><span class="na">BufferType</span><span class="o">.</span><span class="na">SPANNABLE</span><span class="o">);</span> </code></pre></div></div> <p>接下来给 TextView 设置相关的点击、长按、Touch 事件:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">mTextView</span><span class="o">.</span><span class="na">setOnLongClickListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnLongClickListener</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">onLongClick</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span> <span class="n">showSelectView</span><span class="o">(</span><span class="n">mTouchX</span><span class="o">,</span> <span class="n">mTouchY</span><span class="o">);</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span> <span class="o">});</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">setOnTouchListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnTouchListener</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">onTouch</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">,</span> <span class="n">MotionEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span> <span class="n">mTouchX</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getX</span><span class="o">();</span> <span class="n">mTouchY</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getY</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="n">mTextView</span><span class="o">.</span><span class="na">setOnClickListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnClickListener</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">onClick</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span> <span class="n">resetSelectionInfo</span><span class="o">();</span> <span class="n">hideSelectView</span><span class="o">();</span> <span class="o">}</span> <span class="o">});</span> </code></pre></div></div> <ul> <li>其中 <code class="highlighter-rouge">onTouch()</code> 记录了触摸点坐标,用于后面的选择文本的位置定位以及选择游标的显示,即传递给 <code class="highlighter-rouge">showSelectView()</code> 方法。</li> <li><code class="highlighter-rouge">onClick()</code> 中的处理比较简单,重置选中文本信息、隐藏选择相关的 View 。</li> </ul> <p>直接看一下 <code class="highlighter-rouge">showSelectView()</code> 和 <code class="highlighter-rouge">hideSelectView()</code> 的实现:</p> <h4 id="显示选择相关组件">显示选择相关组件</h4> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">showSelectView</span><span class="o">(</span><span class="kt">int</span> <span class="n">x</span><span class="o">,</span> <span class="kt">int</span> <span class="n">y</span><span class="o">)</span> <span class="o">{</span> <span class="n">hideSelectView</span><span class="o">();</span> <span class="n">resetSelectionInfo</span><span class="o">();</span> <span class="n">isHide</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">mStartHandle</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="n">mStartHandle</span> <span class="o">=</span> <span class="k">new</span> <span class="n">CursorHandle</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">mEndHandle</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="n">mEndHandle</span> <span class="o">=</span> <span class="k">new</span> <span class="n">CursorHandle</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span> <span class="kt">int</span> <span class="n">startOffset</span> <span class="o">=</span> <span class="n">TextLayoutUtil</span><span class="o">.</span><span class="na">getPreciseOffset</span><span class="o">(</span><span class="n">mTextView</span><span class="o">,</span> <span class="n">x</span><span class="o">,</span> <span class="n">y</span><span class="o">);</span> <span class="kt">int</span> <span class="n">endOffset</span> <span class="o">=</span> <span class="n">startOffset</span> <span class="o">+</span> <span class="n">DEFAULT_SELECTION_LENGTH</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">mTextView</span><span class="o">.</span><span class="na">getText</span><span class="o">()</span> <span class="k">instanceof</span> <span class="n">Spannable</span><span class="o">)</span> <span class="o">{</span> <span class="n">mSpannable</span> <span class="o">=</span> <span class="o">(</span><span class="n">Spannable</span><span class="o">)</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getText</span><span class="o">();</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mSpannable</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">startOffset</span> <span class="o">&gt;=</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getText</span><span class="o">().</span><span class="na">length</span><span class="o">())</span> <span class="o">{</span> <span class="k">return</span><span class="o">;</span> <span class="o">}</span> <span class="n">selectText</span><span class="o">(</span><span class="n">startOffset</span><span class="o">,</span> <span class="n">endOffset</span><span class="o">);</span> <span class="n">showCursorHandle</span><span class="o">(</span><span class="n">mStartHandle</span><span class="o">);</span> <span class="n">showCursorHandle</span><span class="o">(</span><span class="n">mEndHandle</span><span class="o">);</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">show</span><span class="o">();</span> <span class="o">}</span> </code></pre></div></div> <ul> <li>在 show 方法开始,因为之前可能已经显示了选择相关的 View ,比如先长按 TextView 的 A 点,然后弹出选择游标、操作框,此时再长按 B 点,此时再次弹出选择游标和操作框时,就需要先隐藏之前的相关 View 了,这里就这样简单粗暴地处理了下。</li> <li><code class="highlighter-rouge">int startOffset = TextLayoutUtil.getPreciseOffset(mTextView, x, y);</code> 是一个很有意思的地方,这里参考了前面提到的开源项目里面的实现,这个方法通过传入 TextView 中一个点的坐标,就可以计算出来对应的最接近的那个文字的索引,简单说明如下:</li> </ul> <p><img src="http://ac-qygvx1cc.clouddn.com/766d4fc4e12099222bc2.jpeg" alt="" /></p> <p>通过传入『种』那个字附近的某个点的坐标 (x,y),就可以得出『种』在 TextView 的文本中的索引是 9 (从 0 开始计数)。</p> <p><code class="highlighter-rouge">TextLayoutUtil.getPreciseOffset()</code> 方法如下:</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">int</span> <span class="nf">getPreciseOffset</span><span class="o">(</span><span class="n">TextView</span> <span class="n">textView</span><span class="o">,</span> <span class="kt">int</span> <span class="n">x</span><span class="o">,</span> <span class="kt">int</span> <span class="n">y</span><span class="o">)</span> <span class="o">{</span> <span class="n">Layout</span> <span class="n">layout</span> <span class="o">=</span> <span class="n">textView</span><span class="o">.</span><span class="na">getLayout</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">layout</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span> <span class="n">topVisibleLine</span> <span class="o">=</span> <span class="n">layout</span><span class="o">.</span><span class="na">getLineForVertical</span><span class="o">(</span><span class="n">y</span><span class="o">);</span> <span class="kt">int</span> <span class="n">offset</span> <span class="o">=</span> <span class="n">layout</span><span class="o">.</span><span class="na">getOffsetForHorizontal</span><span class="o">(</span><span class="n">topVisibleLine</span><span class="o">,</span> <span class="n">x</span><span class="o">);</span> <span class="kt">int</span> <span class="n">offsetX</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">layout</span><span class="o">.</span><span class="na">getPrimaryHorizontal</span><span class="o">(</span><span class="n">offset</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">offsetX</span> <span class="o">&gt;</span> <span class="n">x</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">layout</span><span class="o">.</span><span class="na">getOffsetToLeftOf</span><span class="o">(</span><span class="n">offset</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="k">return</span> <span class="n">offset</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>这里涉及到 TextView 的文本布局类 Layout ,虽然看过这块的部分源码,但是这里的处理还是有点懵,本文就不多深入了,有兴趣的话可以自行了解下这块的源码。</p> <ul> <li> <p>文本的选中显示是在 <code class="highlighter-rouge">selectText()</code> 方法中处理的,重点是设置 Span 和记录选中的文本信息:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">selectText</span><span class="o">(</span><span class="kt">int</span> <span class="n">startPos</span><span class="o">,</span> <span class="kt">int</span> <span class="n">endPos</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">startPos</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span><span class="o">)</span> <span class="o">{</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span> <span class="o">=</span> <span class="n">startPos</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">endPos</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span><span class="o">)</span> <span class="o">{</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span> <span class="o">=</span> <span class="n">endPos</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span> <span class="o">&gt;</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">;</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span> <span class="o">=</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">;</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span> <span class="o">=</span> <span class="n">temp</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mSpannable</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">mSpan</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mSpan</span> <span class="o">=</span> <span class="k">new</span> <span class="n">BackgroundColorSpan</span><span class="o">(</span><span class="n">mSelectedColor</span><span class="o">);</span> <span class="o">}</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mSelectionContent</span> <span class="o">=</span> <span class="n">mSpannable</span><span class="o">.</span><span class="na">subSequence</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">,</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">).</span><span class="na">toString</span><span class="o">();</span> <span class="n">mSpannable</span><span class="o">.</span><span class="na">setSpan</span><span class="o">(</span><span class="n">mSpan</span><span class="o">,</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">,</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">,</span> <span class="n">Spanned</span><span class="o">.</span><span class="na">SPAN_INCLUSIVE_EXCLUSIVE</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">mSelectListener</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mSelectListener</span><span class="o">.</span><span class="na">onTextSelected</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mSelectionContent</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> <p>其中处理了下可能存在的 endPos 小于 startPos 的情况,进行了一次交换,后面就是设置 <code class="highlighter-rouge">BackgroundColorSpan</code> 已经记录下选中文本的信息,已经设置了选中监听时的回调。</p> <p>其中 mSelectionInfo 是 <code class="highlighter-rouge">SelectionInfo</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">SelectionInfo</span> <span class="o">{</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">mStart</span><span class="o">;</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">mEnd</span><span class="o">;</span> <span class="kd">public</span> <span class="n">String</span> <span class="n">mSelectionContent</span><span class="o">;</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p><code class="highlighter-rouge">showCursorHandle()</code> 方法顾名思义就是显示选择游标,因为是 PopupWindow 实现的,重点就是显示位置的确定,这里再次涉及到 Layout 相关的 API :</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">showCursorHandle</span><span class="o">(</span><span class="n">CursorHandle</span> <span class="n">cursorHandle</span><span class="o">)</span> <span class="o">{</span> <span class="n">Layout</span> <span class="n">layout</span> <span class="o">=</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getLayout</span><span class="o">();</span> <span class="kt">int</span> <span class="n">offset</span> <span class="o">=</span> <span class="n">cursorHandle</span><span class="o">.</span><span class="na">isLeft</span> <span class="o">?</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span> <span class="o">:</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">;</span> <span class="n">cursorHandle</span><span class="o">.</span><span class="na">show</span><span class="o">((</span><span class="kt">int</span><span class="o">)</span> <span class="n">layout</span><span class="o">.</span><span class="na">getPrimaryHorizontal</span><span class="o">(</span><span class="n">offset</span><span class="o">),</span> <span class="n">layout</span><span class="o">.</span><span class="na">getLineBottom</span><span class="o">(</span><span class="n">layout</span><span class="o">.</span><span class="na">getLineForOffset</span><span class="o">(</span><span class="n">offset</span><span class="o">)));</span> <span class="o">}</span> </code></pre></div> </div> <p>这里和之前的是反的,通过文本中的文字索引,来获取到对应的点的坐标。然后显示 PopupWindow 即可。</p> </li> <li> <p>最后是显示操作框,同样是一个 PopupWindow ,这里的细节后面再展开。</p> </li> </ul> <h4 id="隐藏选择相关组件">隐藏选择相关组件</h4> <p>这里没啥好说的,就是判空下左右选择游标和操作框,如果非空,则调用对应的 <code class="highlighter-rouge">dismiss()</code> 方法</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">hideSelectView</span><span class="o">()</span> <span class="o">{</span> <span class="n">isHide</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">mStartHandle</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mStartHandle</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mEndHandle</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mEndHandle</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mOperateWindow</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>这里基本的流程和相关的实现细节已大概讲述了下,接下来就是就是选择游标和操作框的实现。</p> <h4 id="选择游标">选择游标</h4> <p>由于游标的移动涉及到文字的选中,以及操作框的显隐、定位,就直接实现为 <code class="highlighter-rouge">SelectableTextHelper</code> 的内部类。直接上代码:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">class</span> <span class="nc">CursorHandle</span> <span class="kd">extends</span> <span class="n">View</span> <span class="o">{</span> <span class="kd">private</span> <span class="n">PopupWindow</span> <span class="n">mPopupWindow</span><span class="o">;</span> <span class="kd">private</span> <span class="n">Paint</span> <span class="n">mPaint</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mCircleRadius</span> <span class="o">=</span> <span class="n">mCursorHandleSize</span> <span class="o">/</span> <span class="mi">2</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mWidth</span> <span class="o">=</span> <span class="n">mCircleRadius</span> <span class="o">*</span> <span class="mi">2</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mHeight</span> <span class="o">=</span> <span class="n">mCircleRadius</span> <span class="o">*</span> <span class="mi">2</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mPadding</span> <span class="o">=</span> <span class="mi">25</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">isLeft</span><span class="o">;</span> <span class="kd">public</span> <span class="nf">CursorHandle</span><span class="o">(</span><span class="kt">boolean</span> <span class="n">isLeft</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">(</span><span class="n">mContext</span><span class="o">);</span> <span class="k">this</span><span class="o">.</span><span class="na">isLeft</span> <span class="o">=</span> <span class="n">isLeft</span><span class="o">;</span> <span class="n">mPaint</span> <span class="o">=</span> <span class="k">new</span> <span class="n">Paint</span><span class="o">(</span><span class="n">Paint</span><span class="o">.</span><span class="na">ANTI_ALIAS_FLAG</span><span class="o">);</span> <span class="n">mPaint</span><span class="o">.</span><span class="na">setColor</span><span class="o">(</span><span class="n">mCursorHandleColor</span><span class="o">);</span> <span class="n">mPopupWindow</span> <span class="o">=</span> <span class="k">new</span> <span class="n">PopupWindow</span><span class="o">(</span><span class="k">this</span><span class="o">);</span> <span class="n">mPopupWindow</span><span class="o">.</span><span class="na">setClippingEnabled</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span> <span class="n">mPopupWindow</span><span class="o">.</span><span class="na">setWidth</span><span class="o">(</span><span class="n">mWidth</span> <span class="o">+</span> <span class="n">mPadding</span> <span class="o">*</span> <span class="mi">2</span><span class="o">);</span> <span class="n">mPopupWindow</span><span class="o">.</span><span class="na">setHeight</span><span class="o">(</span><span class="n">mHeight</span> <span class="o">+</span> <span class="n">mPadding</span> <span class="o">/</span> <span class="mi">2</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">onDraw</span><span class="o">(</span><span class="n">Canvas</span> <span class="n">canvas</span><span class="o">)</span> <span class="o">{</span> <span class="n">canvas</span><span class="o">.</span><span class="na">drawCircle</span><span class="o">(</span><span class="n">mCircleRadius</span> <span class="o">+</span> <span class="n">mPadding</span><span class="o">,</span> <span class="n">mCircleRadius</span><span class="o">,</span> <span class="n">mCircleRadius</span><span class="o">,</span> <span class="n">mPaint</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">isLeft</span><span class="o">)</span> <span class="o">{</span> <span class="n">canvas</span><span class="o">.</span><span class="na">drawRect</span><span class="o">(</span><span class="n">mCircleRadius</span> <span class="o">+</span> <span class="n">mPadding</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">mCircleRadius</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">+</span> <span class="n">mPadding</span><span class="o">,</span> <span class="n">mCircleRadius</span><span class="o">,</span> <span class="n">mPaint</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">canvas</span><span class="o">.</span><span class="na">drawRect</span><span class="o">(</span><span class="n">mPadding</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">mCircleRadius</span> <span class="o">+</span> <span class="n">mPadding</span><span class="o">,</span> <span class="n">mCircleRadius</span><span class="o">,</span> <span class="n">mPaint</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>直接继承 PopupWindow 的话,没有 onDraw 方法 ,这里直接继承 View ,然后在 CursorHandle 的构造函数中初始化了一个 PopupWindow ,并将 CursorHandle 实例作为 contentView 传递进去,然后在 <code class="highlighter-rouge">onDraw()</code> 方法中绘制了自定义的选择游标,仿照 6.0 的选择游标效果。</p> <p><img src="http://ac-qygvx1cc.clouddn.com/ba2a7ee85d2d0915c2bb.svg" alt="" /></p> <p>这个也是绘制起来也是很简单的,一个正方形和一个圆组合下即可,处理下是左边还是右边就可以了,具体参照上面的代码。</p> <p>接下来就是设置相关的触摸事件,响应拖动游标时更新选中的文本。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">int</span> <span class="n">mAdjustX</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mAdjustY</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mBeforeDragStart</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mBeforeDragEnd</span><span class="o">;</span> <span class="nd">@Override</span> <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">onTouchEvent</span><span class="o">(</span><span class="n">MotionEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span> <span class="k">switch</span> <span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">getAction</span><span class="o">())</span> <span class="o">{</span> <span class="k">case</span> <span class="n">MotionEvent</span><span class="o">.</span><span class="na">ACTION_DOWN</span><span class="o">:</span> <span class="n">mBeforeDragStart</span> <span class="o">=</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">;</span> <span class="n">mBeforeDragEnd</span> <span class="o">=</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">;</span> <span class="n">mAdjustX</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getX</span><span class="o">();</span> <span class="n">mAdjustY</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getY</span><span class="o">();</span> <span class="k">break</span><span class="o">;</span> <span class="k">case</span> <span class="n">MotionEvent</span><span class="o">.</span><span class="na">ACTION_UP</span><span class="o">:</span> <span class="k">case</span> <span class="n">MotionEvent</span><span class="o">.</span><span class="na">ACTION_CANCEL</span><span class="o">:</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">show</span><span class="o">();</span> <span class="k">break</span><span class="o">;</span> <span class="k">case</span> <span class="n">MotionEvent</span><span class="o">.</span><span class="na">ACTION_MOVE</span><span class="o">:</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="kt">int</span> <span class="n">rawX</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getRawX</span><span class="o">();</span> <span class="kt">int</span> <span class="n">rawY</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getRawY</span><span class="o">();</span> <span class="n">update</span><span class="o">(</span><span class="n">rawX</span> <span class="o">+</span> <span class="n">mAdjustX</span> <span class="o">-</span> <span class="n">mWidth</span><span class="o">,</span> <span class="n">rawY</span> <span class="o">+</span> <span class="n">mAdjustY</span> <span class="o">-</span> <span class="n">mHeight</span><span class="o">);</span> <span class="k">break</span><span class="o">;</span> <span class="o">}</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span> </code></pre></div></div> <ul> <li> <p>在游标移动时,隐藏操作框,停止移动时,再显示操作框。</p> </li> <li> <p>在触摸发生移动时,即 <code class="highlighter-rouge">MotionEvent.ACTION_MOVE</code> 时,更新游标位置和选中的文本,<code class="highlighter-rouge">update()</code> 方法如下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">mTempCoors</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="mi">2</span><span class="o">];</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">update</span><span class="o">(</span><span class="kt">int</span> <span class="n">x</span><span class="o">,</span> <span class="kt">int</span> <span class="n">y</span><span class="o">)</span> <span class="o">{</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getLocationInWindow</span><span class="o">(</span><span class="n">mTempCoors</span><span class="o">);</span> <span class="kt">int</span> <span class="n">oldOffset</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">isLeft</span><span class="o">)</span> <span class="o">{</span> <span class="n">oldOffset</span> <span class="o">=</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">;</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">oldOffset</span> <span class="o">=</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">;</span> <span class="o">}</span> <span class="n">y</span> <span class="o">-=</span> <span class="n">mTempCoors</span><span class="o">[</span><span class="mi">1</span><span class="o">];</span> <span class="kt">int</span> <span class="n">offset</span> <span class="o">=</span> <span class="n">TextLayoutUtil</span><span class="o">.</span><span class="na">getHysteresisOffset</span><span class="o">(</span><span class="n">mTextView</span><span class="o">,</span> <span class="n">x</span><span class="o">,</span> <span class="n">y</span><span class="o">,</span> <span class="n">oldOffset</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">offset</span> <span class="o">!=</span> <span class="n">oldOffset</span><span class="o">)</span> <span class="o">{</span> <span class="n">resetSelectionInfo</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">isLeft</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">offset</span> <span class="o">&gt;</span> <span class="n">mBeforeDragEnd</span><span class="o">)</span> <span class="o">{</span> <span class="n">CursorHandle</span> <span class="n">handle</span> <span class="o">=</span> <span class="n">getCursorHandle</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span> <span class="n">changeDirection</span><span class="o">();</span> <span class="n">handle</span><span class="o">.</span><span class="na">changeDirection</span><span class="o">();</span> <span class="n">mBeforeDragStart</span> <span class="o">=</span> <span class="n">mBeforeDragEnd</span><span class="o">;</span> <span class="n">selectText</span><span class="o">(</span><span class="n">mBeforeDragEnd</span><span class="o">,</span> <span class="n">offset</span><span class="o">);</span> <span class="n">handle</span><span class="o">.</span><span class="na">updateCursorHandle</span><span class="o">();</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">selectText</span><span class="o">(</span><span class="n">offset</span><span class="o">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">);</span> <span class="o">}</span> <span class="n">updateCursorHandle</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">offset</span> <span class="o">&lt;</span> <span class="n">mBeforeDragStart</span><span class="o">)</span> <span class="o">{</span> <span class="n">CursorHandle</span> <span class="n">handle</span> <span class="o">=</span> <span class="n">getCursorHandle</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> <span class="n">handle</span><span class="o">.</span><span class="na">changeDirection</span><span class="o">();</span> <span class="n">changeDirection</span><span class="o">();</span> <span class="n">mBeforeDragEnd</span> <span class="o">=</span> <span class="n">mBeforeDragStart</span><span class="o">;</span> <span class="n">selectText</span><span class="o">(</span><span class="n">offset</span><span class="o">,</span> <span class="n">mBeforeDragStart</span><span class="o">);</span> <span class="n">handle</span><span class="o">.</span><span class="na">updateCursorHandle</span><span class="o">();</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">selectText</span><span class="o">(</span><span class="n">mBeforeDragStart</span><span class="o">,</span> <span class="n">offset</span><span class="o">);</span> <span class="o">}</span> <span class="n">updateCursorHandle</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> <p>在一开始的实现中,<code class="highlighter-rouge">update()</code> 方法没这么复杂,但是考虑到左边的游标在移动到右边游标的右边时,如下面的动图所示:</p> <p><img src="http://ww2.sinaimg.cn/large/91e23208jw1f9s285jsn8g20900g0gop.gif" alt="" /></p> <p>此时就需要多一点处理,左边的右边变右边,右边的游标变左边,同时选中的文本也需要重新变换起点位置,原来是 end ,现在则变成了 start 。</p> <p>具体的逻辑实现就是根据之前选中的文本的前后位置信息,进行前后位置的交换。同时调整游标的方向,更新视图,这个逻辑在 <code class="highlighter-rouge">changeDirection()</code> 方法中:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">changeDirection</span><span class="o">()</span> <span class="o">{</span> <span class="n">isLeft</span> <span class="o">=</span> <span class="o">!</span><span class="n">isLeft</span><span class="o">;</span> <span class="n">invalidate</span><span class="o">();</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>更新选择游标位置:由于游标的位置处理成只和选中的文本有关,因而处理起来较为简单,在上面的反转变化中,只要选中的文本正确变化了,那么这里的游标位置更新就是正确的。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">updateCursorHandle</span><span class="o">()</span> <span class="o">{</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getLocationInWindow</span><span class="o">(</span><span class="n">mTempCoors</span><span class="o">);</span> <span class="n">Layout</span> <span class="n">layout</span> <span class="o">=</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getLayout</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">isLeft</span><span class="o">)</span> <span class="o">{</span> <span class="n">mPopupWindow</span><span class="o">.</span><span class="na">update</span><span class="o">((</span><span class="kt">int</span><span class="o">)</span> <span class="n">layout</span><span class="o">.</span><span class="na">getPrimaryHorizontal</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">)</span> <span class="o">-</span> <span class="n">mWidth</span> <span class="o">+</span> <span class="n">getExtraX</span><span class="o">(),</span> <span class="n">layout</span><span class="o">.</span><span class="na">getLineBottom</span><span class="o">(</span><span class="n">layout</span><span class="o">.</span><span class="na">getLineForOffset</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">))</span> <span class="o">+</span> <span class="n">getExtraY</span><span class="o">(),</span> <span class="o">-</span><span class="mi">1</span><span class="o">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">mPopupWindow</span><span class="o">.</span><span class="na">update</span><span class="o">((</span><span class="kt">int</span><span class="o">)</span> <span class="n">layout</span><span class="o">.</span><span class="na">getPrimaryHorizontal</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">)</span> <span class="o">+</span> <span class="n">getExtraX</span><span class="o">(),</span> <span class="n">layout</span><span class="o">.</span><span class="na">getLineBottom</span><span class="o">(</span><span class="n">layout</span><span class="o">.</span><span class="na">getLineForOffset</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">))</span> <span class="o">+</span> <span class="n">getExtraY</span><span class="o">(),</span> <span class="o">-</span><span class="mi">1</span><span class="o">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> </li> </ul> <h4 id="操作框">操作框</h4> <p>操作框的实现则简单的多,就是自定义布局的 PopupWindow ,然后处理下内部的 View 的点击事件即可,直接贴代码:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">class</span> <span class="nc">OperateWindow</span> <span class="o">{</span> <span class="kd">private</span> <span class="n">PopupWindow</span> <span class="n">mWindow</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">mTempCoors</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="mi">2</span><span class="o">];</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mWidth</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mHeight</span><span class="o">;</span> <span class="kd">public</span> <span class="nf">OperateWindow</span><span class="o">(</span><span class="kd">final</span> <span class="n">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span> <span class="n">View</span> <span class="n">contentView</span> <span class="o">=</span> <span class="n">LayoutInflater</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">context</span><span class="o">).</span><span class="na">inflate</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">layout_operate_windows</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span> <span class="n">contentView</span><span class="o">.</span><span class="na">measure</span><span class="o">(</span><span class="n">View</span><span class="o">.</span><span class="na">MeasureSpec</span><span class="o">.</span><span class="na">makeMeasureSpec</span><span class="o">(</span><span class="n">Integer</span><span class="o">.</span><span class="na">MAX_VALUE</span> <span class="o">&gt;&gt;</span> <span class="mi">2</span><span class="o">,</span> <span class="n">View</span><span class="o">.</span><span class="na">MeasureSpec</span><span class="o">.</span><span class="na">AT_MOST</span><span class="o">),</span> <span class="n">View</span><span class="o">.</span><span class="na">MeasureSpec</span><span class="o">.</span><span class="na">makeMeasureSpec</span><span class="o">(</span><span class="n">Integer</span><span class="o">.</span><span class="na">MAX_VALUE</span> <span class="o">&gt;&gt;</span> <span class="mi">2</span><span class="o">,</span> <span class="n">View</span><span class="o">.</span><span class="na">MeasureSpec</span><span class="o">.</span><span class="na">AT_MOST</span><span class="o">));</span> <span class="n">mWidth</span> <span class="o">=</span> <span class="n">contentView</span><span class="o">.</span><span class="na">getMeasuredWidth</span><span class="o">();</span> <span class="n">mHeight</span> <span class="o">=</span> <span class="n">contentView</span><span class="o">.</span><span class="na">getMeasuredHeight</span><span class="o">();</span> <span class="n">mWindow</span> <span class="o">=</span> <span class="k">new</span> <span class="nf">PopupWindow</span><span class="o">(</span><span class="n">contentView</span><span class="o">,</span> <span class="n">ViewGroup</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">ViewGroup</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="kc">false</span><span class="o">);</span> <span class="n">mWindow</span><span class="o">.</span><span class="na">setClippingEnabled</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span> <span class="n">contentView</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">tv_copy</span><span class="o">).</span><span class="na">setOnClickListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnClickListener</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">onClick</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span> <span class="n">ClipboardManager</span> <span class="n">clip</span> <span class="o">=</span> <span class="o">(</span><span class="n">ClipboardManager</span><span class="o">)</span> <span class="n">mContext</span><span class="o">.</span><span class="na">getSystemService</span><span class="o">(</span><span class="n">Context</span><span class="o">.</span><span class="na">CLIPBOARD_SERVICE</span><span class="o">);</span> <span class="n">clip</span><span class="o">.</span><span class="na">setPrimaryClip</span><span class="o">(</span> <span class="n">ClipData</span><span class="o">.</span><span class="na">newPlainText</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mSelectionContent</span><span class="o">,</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mSelectionContent</span><span class="o">));</span> <span class="k">if</span> <span class="o">(</span><span class="n">mSelectListener</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mSelectListener</span><span class="o">.</span><span class="na">onTextSelected</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mSelectionContent</span><span class="o">);</span> <span class="o">}</span> <span class="n">SelectableTextHelper</span><span class="o">.</span><span class="na">this</span><span class="o">.</span><span class="na">resetSelectionInfo</span><span class="o">();</span> <span class="n">SelectableTextHelper</span><span class="o">.</span><span class="na">this</span><span class="o">.</span><span class="na">hideSelectView</span><span class="o">();</span> <span class="o">}</span> <span class="o">});</span> <span class="n">contentView</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">tv_select_all</span><span class="o">).</span><span class="na">setOnClickListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnClickListener</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">onClick</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span> <span class="n">hideSelectView</span><span class="o">();</span> <span class="n">selectText</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getText</span><span class="o">().</span><span class="na">length</span><span class="o">());</span> <span class="n">isHide</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span> <span class="n">showCursorHandle</span><span class="o">(</span><span class="n">mStartHandle</span><span class="o">);</span> <span class="n">showCursorHandle</span><span class="o">(</span><span class="n">mEndHandle</span><span class="o">);</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">show</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">show</span><span class="o">()</span> <span class="o">{</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getLocationInWindow</span><span class="o">(</span><span class="n">mTempCoors</span><span class="o">);</span> <span class="n">Layout</span> <span class="n">layout</span> <span class="o">=</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getLayout</span><span class="o">();</span> <span class="kt">int</span> <span class="n">posX</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">layout</span><span class="o">.</span><span class="na">getPrimaryHorizontal</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">)</span> <span class="o">+</span> <span class="n">mTempCoors</span><span class="o">[</span><span class="mi">0</span><span class="o">];</span> <span class="kt">int</span> <span class="n">posY</span> <span class="o">=</span> <span class="n">layout</span><span class="o">.</span><span class="na">getLineTop</span><span class="o">(</span><span class="n">layout</span><span class="o">.</span><span class="na">getLineForOffset</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">))</span> <span class="o">+</span> <span class="n">mTempCoors</span><span class="o">[</span><span class="mi">1</span><span class="o">]</span> <span class="o">-</span> <span class="n">mHeight</span> <span class="o">-</span> <span class="mi">16</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">posX</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="o">)</span> <span class="n">posX</span> <span class="o">=</span> <span class="mi">16</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">posY</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="o">)</span> <span class="n">posY</span> <span class="o">=</span> <span class="mi">16</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">posX</span> <span class="o">+</span> <span class="n">mWidth</span> <span class="o">&gt;</span> <span class="n">TextLayoutUtil</span><span class="o">.</span><span class="na">getScreenWidth</span><span class="o">(</span><span class="n">mContext</span><span class="o">))</span> <span class="o">{</span> <span class="n">posX</span> <span class="o">=</span> <span class="n">TextLayoutUtil</span><span class="o">.</span><span class="na">getScreenWidth</span><span class="o">(</span><span class="n">mContext</span><span class="o">)</span> <span class="o">-</span> <span class="n">mWidth</span> <span class="o">-</span> <span class="mi">16</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">Build</span><span class="o">.</span><span class="na">VERSION</span><span class="o">.</span><span class="na">SDK_INT</span> <span class="o">&gt;=</span> <span class="n">Build</span><span class="o">.</span><span class="na">VERSION_CODES</span><span class="o">.</span><span class="na">LOLLIPOP</span><span class="o">)</span> <span class="o">{</span> <span class="n">mWindow</span><span class="o">.</span><span class="na">setElevation</span><span class="o">(</span><span class="mi">8</span><span class="n">f</span><span class="o">);</span> <span class="o">}</span> <span class="n">mWindow</span><span class="o">.</span><span class="na">showAtLocation</span><span class="o">(</span><span class="n">mTextView</span><span class="o">,</span> <span class="n">Gravity</span><span class="o">.</span><span class="na">NO_GRAVITY</span><span class="o">,</span> <span class="n">posX</span><span class="o">,</span> <span class="n">posY</span><span class="o">);</span> <span class="o">}</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">dismiss</span><span class="o">()</span> <span class="o">{</span> <span class="n">mWindow</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>在显示的之后,判断了下是否会显示到屏幕外面,如果会超出屏幕,则做一下微调即可。</p> <h3 id="一些细节的处理">一些细节的处理</h3> <h4 id="嵌套在滚动视图中的处理">嵌套在滚动视图中的处理</h4> <p>在一开始的实现要点中就提到,需要注意一下嵌套在滚动视图中的处理,在尝试了一些方法之后,最终直接设置 <code class="highlighter-rouge">OnScrollChangedListener</code> 来解决,具体代码如下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mOnScrollChangedListener</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ViewTreeObserver</span><span class="o">.</span><span class="na">OnScrollChangedListener</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">onScrollChanged</span><span class="o">()</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(!</span><span class="n">isHideWhenScroll</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">isHide</span><span class="o">)</span> <span class="o">{</span> <span class="n">isHideWhenScroll</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">mOperateWindow</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mStartHandle</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mStartHandle</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mEndHandle</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mEndHandle</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> <span class="o">};</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getViewTreeObserver</span><span class="o">().</span><span class="na">addOnScrollChangedListener</span><span class="o">(</span><span class="n">mOnScrollChangedListener</span><span class="o">);</span> </code></pre></div></div> <p>这倒是解决了滑动时可以隐藏相关的选择控件的问题,但是停止滚动之后呢,如何重新显示选择控件呢?</p> <p>在经过一些尝试之后,发现了 <code class="highlighter-rouge">OnPreDrawListener</code> 这个接口,在 TextView 发生滚动时期间一直在被调用,因此在这个接口里处理重新显示选择控件的逻辑是合适的:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mOnPreDrawListener</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ViewTreeObserver</span><span class="o">.</span><span class="na">OnPreDrawListener</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">onPreDraw</span><span class="o">()</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">isHideWhenScroll</span><span class="o">)</span> <span class="o">{</span> <span class="n">isHideWhenScroll</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span> <span class="n">showSelectView</span><span class="o">();</span> <span class="o">}</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span> <span class="o">};</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getViewTreeObserver</span><span class="o">().</span><span class="na">addOnPreDrawListener</span><span class="o">(</span><span class="n">mOnPreDrawListener</span><span class="o">);</span> </code></pre></div></div> <p>在这样的设置之后,确实能保证停止滚动时重新显示选择相关的控件,但是整个滚动过程变得异常卡顿。</p> <p>原因其实很简单,前面也提到了,<code class="highlighter-rouge">onPreDraw</code> 方法在 TextView 发生滚动时期间一直在被调用,然后这里一直处理显示选择控件的逻辑,能不卡顿么?</p> <p>最后的解决方法是在源码中找到的,将 <code class="highlighter-rouge">showSelectView()</code> 方法替换成 <code class="highlighter-rouge">postShowSelectView()</code> 方法,</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">postShowSelectView</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="n">mTextView</span><span class="o">.</span><span class="na">removeCallbacks</span><span class="o">(</span><span class="n">mShowSelectViewRunnable</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">duration</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span> <span class="n">mShowSelectViewRunnable</span><span class="o">.</span><span class="na">run</span><span class="o">();</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">postDelayed</span><span class="o">(</span><span class="n">mShowSelectViewRunnable</span><span class="o">,</span> <span class="n">duration</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="n">Runnable</span> <span class="n">mShowSelectViewRunnable</span> <span class="o">=</span> <span class="k">new</span> <span class="n">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="k">if</span> <span class="o">(</span><span class="n">isHide</span><span class="o">)</span> <span class="k">return</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">mOperateWindow</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">show</span><span class="o">();</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mStartHandle</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">showCursorHandle</span><span class="o">(</span><span class="n">mStartHandle</span><span class="o">);</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mEndHandle</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">showCursorHandle</span><span class="o">(</span><span class="n">mEndHandle</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> <span class="o">};</span> </code></pre></div></div> <p>很巧妙的方法,通过延迟调用具体的逻辑,避免了一直调用显示选择控件的逻辑,又学习到了。</p> <h4 id="textview-移除出-window-时一些处理">TextView 移除出 Window 时一些处理</h4> <p>在一开始没处理这个的时候,一直报如下的错误:</p> <p><img src="http://ww4.sinaimg.cn/large/91e23208jw1f9s28uh9y1j21kw0hbjzv.jpg" alt="" /></p> <p>这么明显的错误可不能不管,处理起来也很简单:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mTextView</span><span class="o">.</span><span class="na">addOnAttachStateChangeListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnAttachStateChangeListener</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">onViewAttachedToWindow</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">)</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">onViewDetachedFromWindow</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span> <span class="n">destroy</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">destroy</span><span class="o">()</span> <span class="o">{</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getViewTreeObserver</span><span class="o">().</span><span class="na">removeOnScrollChangedListener</span><span class="o">(</span><span class="n">mOnScrollChangedListener</span><span class="o">);</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getViewTreeObserver</span><span class="o">().</span><span class="na">removeOnPreDrawListener</span><span class="o">(</span><span class="n">mOnPreDrawListener</span><span class="o">);</span> <span class="n">resetSelectionInfo</span><span class="o">();</span> <span class="n">hideSelectView</span><span class="o">();</span> <span class="n">mStartHandle</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="n">mEndHandle</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="n">mOperateWindow</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="o">}</span> </code></pre></div></div> <p>将上面添加 Listener 也移除,同时隐藏响应的视图并置空。</p> <h3 id="写在最后">写在最后</h3> <p>至此,自定义的选择复制功能完成,效果如下,GitHub 地址:<a href="https://github.com/laobie/SelectableTextHelper">laobie/SelectableTextHelper</a></p> <p><img src="http://ww2.sinaimg.cn/large/91e23208jw1f9s29s2jf7g20900g0npd.gif" alt="" /></p> <p>在开发之初,通过简单的查阅资料,梳理了个大概的实现思路,并考虑到实现中需要注意到的点,保证在开发中保持足够的警惕,不给自己挖坑。在整个开发过程中,通过阅读他人的源码,以及直接看官方的源码,一点点解决所遇到的问题,以及一点点地尝试,都是一次不错的开发经历,也算是弥补了当初没做出来这个任务的缺憾。</p> <p>当然,这个项目还是有很多值得优化的地方,比如一些边界状态的处理,多个 TextView 的选择复制的场景等等,代码上的内部类的使用也是不够优雅的,不能够做到足够的解耦,都是有优化空间的,欢迎沟通交流。</p> Mon, 21 Nov 2016 00:00:00 +0000 http://jaeger.itscoder.com//android/2016/11/21/selectable-text-helper.html http://jaeger.itscoder.com//android/2016/11/21/selectable-text-helper.html 如何设计精准的推送通知?【译】 <blockquote> <ul> <li>原文地址:<a href="http://firstround.com/review/what-you-must-know-to-build-savvy-push-notifications/">What You Must Know To Build Savvy Push Notifications</a></li> <li>原文作者:<a href="https://twitter.com/firstround">First Round</a></li> <li>译文出自:<a href="https://github.com/xitu/gold-miner">掘金翻译计划</a></li> <li>译者:<a href="https://github.com/laobie">写代码的猴子</a></li> <li>校对者:<a href="https://github.com/Ruixi">Ruixi</a>, <a href="https://github.com/rccoder">rccoder (Shangbin Yang)</a></li> </ul> </blockquote> <p>智能手机面世已经近十年时间,但根据 <a href="http://stateofstartups.firstround.com/#highlights">First Round 对初创公司的调查报告</a> 来看,创始人们仍然宣称移动端是最被低估的技术。推送通知在移动设备上潜力极大。企业家 <a href="https://www.linkedin.com/in/aseidman">Ariel Seidman</a> 在 <a href="http://arielseidman.com/post/62564939335/fixing-mobile-push-notifications">改进移动端的推送通知</a> 这篇文章中提到:“确实很难再夸大推送通知的潜力。这是在人类历史上第一次可以同时拍着近 200 万人的肩膀,说‘嘿!注意这个!’” 这也是 <a href="https://slack.com/"><strong>Slack</strong></a> 的 <a href="https://www.linkedin.com/in/noahw"><strong>Noah Weiss</strong></a> 一直笃信世界会通过智能设备变得越来越亲近的原因。</p> <p>供职 Slack 之前,Weiss 在 Foursquare 工作,当时它 <a href="http://techcrunch.com/2013/10/14/with-an-eye-to-more-revenue-foursquare-opens-its-ads-platform-to-all-small-businesses/">通过原生广告服务获利</a> ,并在 2014 年大胆地分成 <a href="https://medium.com/foursquare-direct/the-lego-block-exercise-4c7d60eeb38f#.tmyz2j5o0">两个应用</a>。那时候,每月活跃用户增长五倍之多。 Weiss 还是 Google 结构化数据搜索项目的首席产品经理。最近,Weiss 加入 Slack <a href="https://medium.com/@noah_weiss/starting-up-slack-s-search-learning-intelligence-group-in-the-new-nyc-office-af6523090789#.sqly156er">建立其纽约办事处,领导新的搜索、学习和智能项目组</a>,其任务是开发<a href="http://www.recode.net/2016/6/6/11863534/slack-artificial-intelligence-AI-noah-weiss">新的功能</a> ,使其他公司在使用 Slack 时更加高效。</p> <p>在这次采访中,Weiss 描绘了推送通知的动态演变 —— 阐释了智能手表和应用布满屏幕主屏时代关键的范式转变。在此,他还分享了一些关于初创公司寻求制定推送通知策略、投入、指标和指南的小秘诀。任何想要控制这种高风险、高回报渠道的创业公司都会从 Weiss 这里受益。</p> <blockquote> <p>一个好的推送通知有三个特性:及时性,个性化和可行性。</p> </blockquote> <h3 id="推送通知的演进">推送通知的演进</h3> <p>在分享他的策略之前,Weiss 总结了推送通知的演变,因为它涉及到<strong>三种强大的特质:及时性,个性化和可行性。</strong>他将他们的历史和进展看作是建立未来时的基础。以下是简化的推送通知演变历史的四个阶段:</p> <p><strong>电子邮件是推送通知的前身。</strong> 网络时代初期的推送通知是电子邮件。“在电子邮件和推送通知之间有很多类似的地方。” Weiss 说,“在过去,你通过提供电子邮件地址,允许与网站进行开放式沟通。电子邮件成为将人带回网站的可靠的主要方式,它不是通过门户或书签。并且,电子邮件中有一个取消订阅选项。通知的等效选项是调整推送设置,或者更常见的是卸载应用程序。</p> <p><strong>进化到移动时代。</strong> 当用户在手机上投入更多时,电子邮件开始衰退。“可能很难回想起智能手机之前的时代,人们并不习惯在他们的收件箱里生活。他们每天在电脑上检查电子邮件好几次。“ Weiss 说。 “即使是那些拥有非常成功的电子邮件营销策略的公司也会使用移动设备。还记得 Groupon 提供激光脱毛服务吗?你为什么收到它?你什么时候对脱毛表现出兴趣,或者表示你在手机上做出这种类似的购买决定时?绑定到用户,位置和一天中的某个时间,推送通知变得更有效。他们有着及时性、个性化和可行性的潜力,当然如果做的不好,用户也会感到厌烦。</p> <p><strong>与短信竞争,而不是电子邮件。</strong> 在移动设备上,推送通知更像是短信,而不是电子邮件。“推送的内容是与此刻发生的事物紧密相关的。当你可能不指望你的内容在几天内被阅读,你可以发送一封电子邮件,这对于业内通讯或文摘来说是可以的。” Weiss 说,“然而,实时推送通知所需的及时性或注意力是完全不同的。通过推送通知,你可以有效地与短信和其他个性化的沟通方式竞争。如果别的通知来自某人的配偶、最好的朋友或妈妈,你如何做到个性化?它们必须在同一水平竞争。</p> <p><strong>切割所有应用程序。</strong> 当人们首次使用智能手机时,他们的应用可以摆放在 4x4 网格的主屏幕上。而现在,美国用户的手机上大约平均有 55 个应用。“你需要知道的是,无法让这些应用都被定期使用。如今也很难开发一个应用,让该应用的使用变成日常习惯。” Weiss 说,“开发者的现实是,你的应用可能不会在某人的主屏上,用户也可能不会有一天使用它多次的习惯。这就是通知变得越来越重要的原因。对于大多数应用,推送通知可以完美地提供紧急信息:Uber 到达,登机口变更提醒或者你在 Slack 中被提及。如果用户被 50 多个应用程序淹没,你不能指望他们记住在正确的时间和地点使用你的应用,你需要主动引导他们打开。</p> <h3 id="围绕以下原则构建你的推送通知策略">围绕以下原则构建你的推送通知策略</h3> <p>深度通知策略可以权衡和组织多个因素,例如附近的 WiFi,个性化,社交因素和实时捕捉到的位置等等,都可以用来驱动推送通知。但对于刚刚开始接触推送通知技术的初创公司来说,有一些基本因素需要考虑。从基本到更高级的诀窍,Weiss 讲述了他在开发推送通知系统时学到的基本经验。</p> <p><strong>在应用程序之外促进用户留存</strong></p> <p>从用户保留角度来看,当你的应用超越了功能下限后,用户返回你的应用的次数会减少。你只能在你的应用中塞入那么多功能,并期望新用户在一开始的几个会话中发现这些功能。“移动领域最大的挑战是留住新用户,已经有得到证明的战术来引进新用户:高效的应用安装营销、社交渠道、SEM 和 SEO。然而,真正困难的是让新用户养成一种习惯。” Weiss 说,“有时候,你的应用的改进不会显著影响用户留存的顶峰值,但是在应用之外的投资却可以做到,这里即推送通知的投资。因为一旦有人关闭了你的应用,他们错过了第四个 Tab 下的神奇体验就变得无关紧要了。因为如果他们再也没有打开你的应用,他们永远不会知道他们错过了什么。</p> <p>在为你的应用设计最佳用户体验的过程中,请不要忘记,只有在用户打开应用时,才会享受到这种体验 —— 才会继续回到你的应用。“这总是让我感到惊讶和痛苦:当我看到对一个应用投入令人难以置信的时间和精力,却没有一个策略重新吸引我。” Weiss 说,“当然,大多数年轻的开发人员都不考虑通知。不要犯这个错误。这也是目前移动产品开发中最大的疏忽。”</p> <blockquote> <p>客户需求推进了一个应用,用户留存成了一笔生意。</p> </blockquote> <p><strong>不要在有权限的情况下错误下载。</strong></p> <p>请求获取发送通知的权限不仅是良好的形式,而且在技术上也是必要的。“如果你在 iOS 平台上开发,发送通知是用户必须授权的权限。与 Android 不同,下载应用默认授予权限,你必须提示用户。” Weiss说,“这是一个很关键的时刻,如果用户拒绝授权,应用无法引导用户重新进入授权页面,这极大地降低了他们变成活跃用户的可能性。即使他们接受,这也不是个有约束力的合同。”</p> <p>如果用户厌倦了你的推送通知,最好的情况是他们可以选择在应用中保留哪些通知是活动的,但更可能的是导致他们到手机设置中关闭所有通知或者卸载应用。这实际上是不可逆的。注:提升给用户的第一个通知体验,否则他们会关闭通知渠道。</p> <p>因此,第一步是提示用户在一开始同意接收通知 —— 如果他们说不,其余的建议将变得不再重要。它涉及用户教育,在用户发现有价值的内容之后再弹出提示,或者授权绿灯亮起来时再申请授权许可,可以提升转化率。然后是关于保持信任和保持开放的沟通,这两个步骤有一些不错的文献可以参考,Weiss 推荐了以下的文章:</p> <ul> <li> <p><a href="https://library.launchkit.io/the-right-way-to-ask-users-for-ios-permissions-96fa4eb54f2c#.3u7waqk3w">移动端请求用户权限的正确方式</a> —— <a href="https://twitter.com/mulligan">Brenden Mulligan</a></p> </li> <li> <p><a href="http://andrewchen.co/why-people-are-turning-off-push/">为什么 60% 的用户选择停用推送通知,如何应对这种状况</a> ——<a href="https://twitter.com/andrewchen">Andrew Chen</a></p> </li> <li> <p><a href="https://medium.com/circa/the-right-way-to-ask-users-to-review-your-app-9a32fd604fca#.iz4jrwiin">让用户再次回到你的应用的正确方式</a> ——<a href="https://twitter.com/mg">Matt Galligan</a></p> </li> </ul> <p>考虑到获取通知权限的高风险,这些文章的重点默认是如何规避风险。“如果你足够聪明,那么实际上涉及到通知你会变得非常谨慎。在所有实验中建立安全网,因为任何失误都会产生很大的影响。” Weiss 说,“例如,如果我每周发布一次推送,所有用户都会收到,我会将它作为一个 5% 或 10% 的实验,以覆盖任何导致用户选择退出通知的潜在缺陷。”</p> <p><strong>指定三个指标来衡量通知</strong></p> <p>为了评估你的通知策略,需要给出以下三个指标:<strong>1)选择取消通知权限的用户比率 2)卸载率 和 3)每百次推送的操作次数</strong>。</p> <p>“要评估一个好的通知,你必须在用户主动参与和取消通知之间达到平衡。这是一个棘手的平衡,因为你可能会比较一个短期的主动参与用户数的提升与长期下来的卸载用户数,不能再重新参与。” Weiss 说。 “从设定卸载率和通知禁用率开始,如果你的应用程序是面向消费者的,而且卸载率低于 2%,则表示你处于安全区。所以如果你的每周流失率为 1%,你的增长率为 1.02% 到 2%,这不是毁灭性的。监测所有剧烈的波动,因为一周一周的叠加效应可能会造成损失。”</p> <p>为了评估通知策略的回报,不要考虑打开率而是衡量具体操作。“我建议的一个方法是监控推送通知的时间窗口,统计到达绑定到原始通知的操作的数目。例如,如果通知鼓励用户评价他们最近访问过的地方,分析用户在 2-6 小时的窗口内每百次推送通知的评分数。” Weiss 说,“总是有归属的问题,但如果你在发送通知后定义一个固定的时间窗口进行评估,结果会让你更能接受。</p> <p><strong>…校准指标以用来比较 iOS 和 Android 上的表现。</strong></p> <p>对于那些想要将打开率作为指标进行追踪的人,Weiss 对不同操作系统上的通知的性质有几点看法。“通过电子邮件跟踪打开率是很容易的,但是你要知道 iOS 的打开率远远低于 Android;进行相同的推送,Android 可以显示多达 iOS 平台五倍的打开率。” Weiss 说,“在 Android 用户倾向于处理通知,因为只有在你手动打开每个通知时,通知才会清除,而在 iOS 上,一旦你从锁定屏幕打开一个通知,其他通知就会清除。</p> <p>与其他功能一样,不同的操作系统在收到通知时表现也不同。“例如,Android 上的通知可以内置图片,这样可以提高 15-20% 的互动概率。由于大多数开发人员通常在 iOS 平台上工作,他们认为发送 Android 推送通知也不可以附带图片。” Weiss 说,“还有内置操作按钮,让用户可以直接从通知进行操作。这些也提升了更高的互动概率。即使作为一个 iPhone 用户,我也不得不说,从根本上来说,Android 的通知开发都是更好的。</p> <blockquote> <p>用个性化的内容填充推送通知,让他们听起来像来自一个亲密的朋友。</p> </blockquote> <p><strong>抵制新奇性效应</strong></p> <p>运行推送通知的实验至少六周,12 周是一个不错的选择。 Weiss 明白,进行更长时间的测试是必要的,以表现出所有负面影响。“一般用户将忽略不必要的推送大约一个月,而不采取任何操作,如更改设置或卸载应用。一旦超过这个阈值,烦人的通知很快被清除。” Weiss 说。</p> <p>通知具有强烈的新奇性倾向,这延迟了用户的真实反应。Weiss 曾发起了一个实验来测试用户对表情符号的反应。“我们将文本的长度减半,并添加了相关的表情符号。在实验的前两个星期,我们统计指标达到了顶峰。用户打开应用的操作明显。每周活跃用户数( WAUs )上升。它迷惑性地宣称未来是表情符号的。” Weiss 说,“随着时间的推移,我们继续监控它,增长放缓,然后变平。最后,影响是中性的。这并不是一件坏事,但如果我们基于初步结果就分配资源,那就会导致问题。因此最好花几个月时间而不是几个星期来测试推送通知。</p> <p><strong>如何测试?何时测试?在哪测试?</strong></p> <p>推送通知的“为什么”和“谁”是比较直接的 —— 目标是提升所有用户的参与。然而,在推送通知的方式上却有各种各样的想法。Weiss 在他的职业生涯中,帮助启动了 100 多个通知实验 —— 测试了从一天时间内到触发,到回到首屏。 <a href="http://firstround.com/review/the-right-way-to-ship-software/">与运输软件一样,没有“正确的方式”</a> ,但在这里他分享一些无可争议的点:</p> <ul> <li> <p><strong>只有最紧急的通知才需要开启振动。</strong> “通过推送,你可以控制默认设置是手机振动还是静音。从我所有的用户研究中我发现这是最高风险的决策之一。如果一个通知振动了用户,她发现并不紧急,那么应用程序被卸载的可能性立即暴增。” Weiss 说,“如果它是紧急的 —— 就像你即将错过你的飞机或直接来自同事的紧急消息 —— 一个嗡嗡声可以是一个非常强大和值得称赞的工具。如果没有,这将会产生危险、发生意外,因此,对于从朋友那得到一个赞或者喜欢,不要使用振动。用户平均每天查看手机的时间为 70 到 100 次,他们很可能在接下来的 15 分钟内看到你的消息。”</p> </li> <li> <p><strong>匹配用户的生物节律。</strong> “推送的时间很重要,但没有一个规则来规定绝对最好的窗口时间。但请花一点时间思考下如何监控用户作息进度,避免在用户睡着时发送通知,因为这样你将吵醒他们,或者他们会在早上发现一堆来自你的应用的推送消息。” Weiss 说,“也要考虑你的内容的性质,在上午发送新闻效果不错,以及在上下班路上时发送通知也不错。通过监控用户的参与来提升你的策略。”</p> </li> <li> <p><strong>在你的通知副本中使用各种个性化。</strong> “它产生了巨大的差异。插入用户的名字不算在内,例如’ Noah,这里是你星期二的每日交易!’在你的通知副本中显示你知道的有关用户的信息 —— 否则他们将激活他们天生的过滤器来应对爆炸营销。” Weiss 说到,“ 当用户查看他们的时间线时,Twitter 有一个好的做法,该服务提示你查看 Evelyn,Marcos 和Lydia 的最近一天的推文。这些都是你关注的、可以叫出名字的人。Spotify 对于你经常听的艺术家的新歌也一样处理。</p> </li> <li> <p><strong>像 Uber 一样思考你的推送。</strong> “如果你的 Uber 司机在曼哈顿的任一个街区上放下了你,当你要求在下东区一个特定的街区下车,你会高兴吗?这很显然,但初创公司可能忘记将他们的用户指引到在通知中提示的<strong>准确</strong>界面上。” Weiss 说,“如果通知引导用户进到他们期望的界面,人们就会点击它。如果没有,他们下一次就会忽略它。许多电子商务应用通过将用户引导到通用界面而不是特定项目或页面来解决这个问题。</p> </li> </ul> <blockquote> <p>魔术师把你的选中的牌变到一副牌的最上面。拥有智能通知的应用将拥有更多的手法,在适当的时间将他们的服务呈现到人们的手机上。</p> </blockquote> <h3 id="通知的未来">通知的未来</h3> <p>智能手机和智能手表的屏幕不断变化,但主屏幕的实际空间始终是有限的,无论大小。考虑到手机上保存的应用程序数量激增,此限制是一个约束。以下是 Weiss 从移动操作系统的演变得到对未来通知的想法:</p> <p><strong>让锁屏成为新的主屏幕。</strong> 事实上,人们看到的比手机主屏幕更多的唯一地方就是手机锁屏界面。“你的主屏幕上放置的你想要触手可及的应用,通常限制在不到 20 个。你的锁屏则列出了手机上的数百个应用最近的通知。” Weiss 说,“我认为锁屏将取代主屏幕,将会有一个全新的主屏体验,将应用以流的方式呈现给你。最终排名将不仅仅是取决于最近使用和使用频率。系统通知将感觉像 Twitter 的实时动态,嘈杂的信息流让人感觉像 Facebook 的热门动态。</p> <blockquote> <p>你可以随时切换到某个应用,但通知将是你坚定不移的向导。</p> </blockquote> <p><a href="http://ben-evans.com/benedictevans/2014/8/1/app-unbundling-search-and-discovery">应用绑定和解绑的自然现象</a> ,Weiss 看到一个潮汐般的转变:锁屏将再次重新绑定它们。“在过去三年中,应用生态系统中出现了一个渐进的、巨大的分裂。应用程序已变得针对单一使用场景更加专业化。” Weiss 说,“但随着用户聚集了一堆应用,在正确的时间选择正确的服务变得越来越困难。通知给用户提供及时有用的信号。将会有一个新的导航范例,当用户正在考虑使用某些应用时,智能地控制这些应用。</p> <p><strong>丰富的上下文感知。</strong> 如果用户越来越多地通过发送到锁屏界面的通知流来与应用交互,这将是因为他们确信他们被发送了最及时、最相关的警报。这只会发生在一个强大的的上下文感知中。“手机上的传感器使你能够在移动设备上建立一个感知上下文级别的服务,你永远不可能在桌面或电子邮件上进行这样的感知。你如何把这种感知翻译成真正可行的、及时的、相关的通知?”魏斯问,“这是一个令人振奋的新领域,想象一个服务,可以区分是否有人一个特定的场所停驻,无论是咖啡馆,机场还是健身房。对上下文的独特感知创造了大量发送相关推送通知的新机会。”</p> <blockquote> <p>最好的应用将是那些你不必记住他们的应用,他们会主动提醒你。这种应用将是未来的唯一类型应用。</p> </blockquote> <p>“我最喜欢 Foursquare 在一个城市新的或热门的场所的推送通知。根据你的手机的定位,它可以将你与实际访问的地方关联起来。” Weiss 说,“它给你一个通知,通常每周一次,’嘿,这里有三个城市热门地方,你还没有去过。’这是一个神奇的时刻,当你意识到你仅仅是带着口袋里的手机在周围走了走,也许你甚至整整一个星期都没使用过这个应用。你不需要做任何事,它就将你拉回这个应用,并给你惊喜。”</p> <p>完整利用移动设备上的传感器具有挑战性,但可以从一些基本的方向开始。“虽然大多数开发人员无法简单地建立这种类型的位置解析,但是基于后台定位构建一个模型用来解析一个人在家还是在工作是很容易的。这是两个用来触发相关的推送非常丰富的上下文。” Weiss 说。</p> <h3 id="总结">总结</h3> <p>虽然通知可以提高留存率和互动率,但不要将其视为增长的黑科技。他们有潜力成为与用户互动的最直观、最亲密的方式。为了建立这种可靠的关系,他们必须是及时的、个性化的、可操作的。通知策略必须请求用户的授权,并根据其停用、卸载和每100次通知点击次数来权衡。更好地方式是根据用户主动输入和被动地感知上下文来定制通知。</p> <p>“我们还在移动时代的早期。设备继续改进,将会拥有更大的屏幕,更长的电池寿命或者变成可穿戴的。” Weiss 说,“然而无论硬件如何发展,通知将是你的移动设备最亲密的功能。像亲密的朋友或家人一样,智能通知会记住你的偏好和历史。他们会准确地指引你,让你与亲人保持联系,并在最合适的时间提醒你重要的事情。这大概就是技术的力量。”</p> Wed, 02 Nov 2016 00:00:00 +0000 http://jaeger.itscoder.com//%E4%BA%A7%E5%93%81/2016/11/02/what-you-must-know-to-build-savvy-push-notifications.html http://jaeger.itscoder.com//%E4%BA%A7%E5%93%81/2016/11/02/what-you-must-know-to-build-savvy-push-notifications.html Android 过度绘制优化 <blockquote> <ul> <li>文章来源:itsCoder 的 <a href="https://github.com/itsCoder/weeklyblog">WeeklyBolg</a> 项目</li> <li>itsCoder 主页:<a href="http://itscoder.com/">http://itscoder.com/</a></li> <li>作者:<a href="https://github.com/laobie">Jaeger</a></li> <li>审阅者:<a href="https://github.com/yongyu0102">yongyu0102 (用语)</a></li> </ul> </blockquote> <p>Android 从一诞生到现在已经发布的 7.0 版本,卡顿和不流畅问题却一直被人们所诟病。客观地来讲,Android 的流畅性确实一直不给力,哪怕是某些大厂的 App ,也都不同程度地存在卡顿问题。从开发角度来说,每个开发者都应该关注下性能优化,在平时的开发工作中注意一些细节,尽可能地去优化应用。本文作为性能优化系列的开篇,先从过度绘制优化讲起。</p> <h3 id="过度绘制overdraw的概念">过度绘制(Overdraw)的概念</h3> <blockquote> <p>过度绘制(Overdraw)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构里面,如果不可见的 UI 也在做绘制的操作,会导致某些像素区域被绘制了多次,同时也会浪费大量的 CPU 以及 GPU 资源。</p> </blockquote> <p>在 Android 手机的开发者选项中,有一个『调试 GPU 过度绘制』的选项,该选项开启之后,手机显示如下,显示出来的蓝色、绿色的色块就是过度绘制信息。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/c7de9ce128cd8921.png" alt="" /></p> <p>比如上面界面中的『调试 GPU 过度绘制 』的那个文本显示为蓝色,表示其过度绘制了一次,因为背景是白色的,然后文字是黑色的,导致文字所在的区域就会被绘制两次:一次是背景,一次是文字,所以就产生了过度重绘。</p> <p>在官网的 <a href="https://developer.android.com/studio/profile/dev-options-overdraw.html">Debug GPU Overdraw Walkthrough</a> 说明中对过度重绘做了简单的介绍,其中屏幕上显示不同色块的具体含义如下所示:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/46397b26da912658.png" alt="" /></p> <p>每个颜色的说明如下:</p> <ul> <li><strong>原色</strong>:没有过度绘制</li> <li><strong>蓝色</strong>:1 次过度绘制</li> <li><strong>绿色</strong>:2 次过度绘制</li> <li><strong>粉色</strong>:3 次过度绘制</li> <li><strong>红色</strong>:4 次及以上过度绘制</li> </ul> <p>过度绘制的存在会导致界面显示时浪费不必要的资源去渲染看不见的背景,或者对某些像素区域多次绘制,就会导致界面加载或者滑动时的不流畅、掉帧,对于用户体验来说就是 App 特别的卡顿。为了提升用户体验,提升应用的流畅性,优化过度绘制的工作还是很有必要做的。</p> <h3 id="优化原则">优化原则</h3> <ul> <li>一些过度绘制是无法避免的,比如之前说的文字和背景导致的过度绘制,这种是无法避免的。</li> <li>应用界面中,应该尽可能地将过度绘制控制为 2 次(绿色)及其以下,原色和蓝色是最理想的。</li> <li>粉色和红色应该尽可能避免,在实际项目中避免不了时,应该尽可能减少粉色和红色区域。</li> <li>不允许存在面积超过屏幕 1/4 区域的 3 次(淡红色区域)及其以上过度绘制。</li> </ul> <h3 id="优化方法">优化方法</h3> <p>以下部分是根据我在公司项目的实践来整理出来的一些实际的优化步骤和方法,避免像看完大部分性能优化的文章,然后发现『懂得太多道理还是写不好一个 App』的尴尬局面。</p> <ol> <li> <p>移除默认的 Window 背景</p> <p>一般应用默认继承的主题都会有一个默认的 <code class="highlighter-rouge">windowBackground</code> ,比如默认的 Light 主题:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;style</span> <span class="na">name=</span><span class="s">"Theme.Light"</span><span class="nt">&gt;</span> <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"isLightTheme"</span><span class="nt">&gt;</span>true<span class="nt">&lt;/item&gt;</span> <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"windowBackground"</span><span class="nt">&gt;</span>@drawable/screen_background_selector_light<span class="nt">&lt;/item&gt;</span> ... <span class="nt">&lt;/style&gt;</span> </code></pre></div> </div> <p>但是一般界面都会自己设置界面的背景颜色或者列表页则由 item 的背景来决定,所以默认的 Window 背景基本用不上,如果不移除就会导致所有界面都多 1 次绘制。</p> <p>可以在应用的主题中添加如下的一行属性来移除默认的 Window 背景:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"android:windowBackground"</span><span class="nt">&gt;</span>@android:color/transparent<span class="nt">&lt;/item&gt;</span> <span class="c">&lt;!-- 或者 --&gt;</span> <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"android:windowBackground"</span><span class="nt">&gt;</span>@null<span class="nt">&lt;/item&gt;</span> </code></pre></div> </div> <p>或者在 <code class="highlighter-rouge">BaseActivity</code> 的 <code class="highlighter-rouge">onCreate()</code> 方法中使用下面的代码移除:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">getWindow</span><span class="o">().</span><span class="na">setBackgroundDrawable</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span> <span class="c1">// 或者</span> <span class="n">getWindow</span><span class="o">().</span><span class="na">setBackgroundDrawableResource</span><span class="o">(</span><span class="n">android</span><span class="o">.</span><span class="na">R</span><span class="o">.</span><span class="na">color</span><span class="o">.</span><span class="na">transparent</span><span class="o">);</span> </code></pre></div> </div> <p>移除默认的 Window 背景的工作在项目初期做最好,因为有可能有的界面未设置背景色,这就会导致该界面显示成黑色的背景,如下所示,如果是后期移除的,就需要检查移除默认 Window 背景之后的界面是否显示正常。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/8bb76d317ff0d5ff.png" alt="" /></p> </li> <li> <p>移除不必要的背景</p> <p>还是上面的那个界面,因为移除了默认的 Window 背景,所以在布局中设置背景为白色:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span> <span class="nt">&lt;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:background=</span><span class="s">"@color/white"</span> <span class="na">android:orientation=</span><span class="s">"vertical"</span><span class="nt">&gt;</span> <span class="nt">&lt;android.support.v7.widget.RecyclerView</span> <span class="na">android:id=</span><span class="s">"@+id/rv_apps"</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:visibility=</span><span class="s">"visible"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/LinearLayout&gt;</span> </code></pre></div> </div> <p>然后在列表的 item 的布局如下所示:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span> <span class="nt">&lt;LinearLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span> <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span> <span class="na">android:layout_width=</span><span class="s">"match_parent"</span> <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span> <span class="na">android:background=</span><span class="s">"@color/white"</span> <span class="na">android:orientation=</span><span class="s">"horizontal"</span> <span class="na">android:padding=</span><span class="s">"@dimen/mid_dp"</span><span class="nt">&gt;</span> <span class="nt">&lt;ImageView</span> <span class="na">android:id=</span><span class="s">"@+id/iv_app_icon"</span> <span class="na">android:layout_width=</span><span class="s">"40dp"</span> <span class="na">android:layout_height=</span><span class="s">"40dp"</span> <span class="na">tools:src=</span><span class="s">"@mipmap/ic_launcher"</span><span class="nt">/&gt;</span> <span class="nt">&lt;TextView</span> <span class="na">android:id=</span><span class="s">"@+id/tv_app_label"</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_gravity=</span><span class="s">"center_vertical"</span> <span class="na">android:layout_marginLeft=</span><span class="s">"@dimen/mid_dp"</span> <span class="na">android:textColor=</span><span class="s">"@color/text_gray_main"</span> <span class="na">android:textSize=</span><span class="s">"@dimen/mid_sp"</span> <span class="na">tools:text=</span><span class="s">"test"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/LinearLayout&gt;</span> </code></pre></div> </div> <p>看起来是没问题的,但是因为我界面的背景和 item 布局的背景都是白色,所以 item 布局中的背景是不必要的,可以移除。优化前后的过度绘制结果如下:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/eeffd1ea58fd9598.png" alt="" /></p> <p>很明显优化后过度绘制比之前均少了一次,但是这种场景还是比较特殊的,因为界面背景和 item 的背景色一样,假如不一样的话,就无法避免多 1 次过度绘制了。</p> <p>还有一个比较常见的可优化场景:ViewPager 加多个 Fragment 组成的首页界面,如果你的每个 Fragment 都设置有背景色的话, 你就可以不用给 Activity 的根布局设置背景,如果你还给 ViewPager 还设置了背景,那个这个背景是没必要的,同样可以移除。</p> <p>如果你不知道存在哪些无用的背景,你可以借助 Hierarchy View 来查看,具体的这块可以参照 <a href="http://androidperformance.com/2015/01/13/android-performance-optimization-overdraw-2.html">Android 性能优化之过渡绘制(二)</a> 这篇文章来操作。</p> </li> <li> <p>写合理且高效的布局</p> <p>由于 Android 的布局是通过编写 xml 来实现,相对比较简单,这也就导致很多开发者在写布局时很随意,而不会考虑性能、过度重绘等问题。</p> <p>比如上面列表布局中的分割线,可以按照如下编写布局来实现:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span> <span class="nt">&lt;LinearLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span> <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span> <span class="na">android:layout_width=</span><span class="s">"match_parent"</span> <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span> <span class="na">android:paddingBottom=</span><span class="s">"8dp"</span> <span class="na">android:background=</span><span class="s">"@color/divider_gray"</span><span class="nt">&gt;</span> <span class="nt">&lt;LinearLayout</span> <span class="na">android:padding=</span><span class="s">"@dimen/mid_dp"</span> <span class="na">android:layout_width=</span><span class="s">"match_parent"</span> <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span> <span class="na">android:orientation=</span><span class="s">"horizontal"</span> <span class="na">android:background=</span><span class="s">"@color/white"</span><span class="nt">&gt;</span> <span class="nt">&lt;ImageView</span> <span class="na">android:id=</span><span class="s">"@+id/iv_app_icon"</span> <span class="na">android:layout_width=</span><span class="s">"40dp"</span> <span class="na">android:layout_height=</span><span class="s">"40dp"</span> <span class="na">tools:src=</span><span class="s">"@mipmap/ic_launcher"</span><span class="nt">/&gt;</span> <span class="nt">&lt;TextView</span> <span class="na">android:id=</span><span class="s">"@+id/tv_app_label"</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_gravity=</span><span class="s">"center_vertical"</span> <span class="na">android:layout_marginLeft=</span><span class="s">"@dimen/mid_dp"</span> <span class="na">android:textColor=</span><span class="s">"@color/text_gray_main"</span> <span class="na">android:textSize=</span><span class="s">"@dimen/mid_sp"</span> <span class="na">tools:text=</span><span class="s">"test"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/LinearLayout&gt;</span> <span class="nt">&lt;/LinearLayout&gt;</span> </code></pre></div> </div> <p>这种改变布局实现分割线的方式虽然很快捷方便,但是存在不少问题的:</p> <ul> <li> <p>加深了布局层级,和之前的布局相比多了一级</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/2aceb1e5a933352a.jpg" alt="" /></p> </li> <li> <p>多了 2 次过度绘制</p> </li> </ul> <p>解决方式有两种:</p> <ol> <li>一种是使用 <code class="highlighter-rouge">RelativeLayout</code> 将分割线添加在 item 的布局中,但是这样会导致布局复杂度增加,同时因为 <code class="highlighter-rouge">RelativeLayout</code> 布局的两次测量,也会延长 View 测量的时间,在解决这种需求时并不是一个好的方式。</li> <li>另一种是使用 <code class="highlighter-rouge">RecyclerView</code> 的 <code class="highlighter-rouge">addItemDecoration(ItemDecoration decor)</code> 方法添加分割线,这种方式在你自定义好一个分割线 <code class="highlighter-rouge">ItemDecoration</code> 时是很方便的,网上有很多关于这方面的例子(如果你使用 ListView 的话,则使用 <code class="highlighter-rouge">setDivider(Drawable divider)</code> 方法)。</li> </ol> <p>我们采用第二种解决方法,优化前后的对比如下:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/fad0b600790d3986.png" alt="" /></p> <p>优化后的布局 ImageView 和 item 背景区域均比优化前少了 2 次过度重绘,布局层级也没增加,需求也实现了。</p> <blockquote> <p>注:很多开发者在开发中一般很少注意这种小细节,一般以完成需求为目的,可能还认为这么点细节优化不优化其实也没什么,但是积少成多,小的细节优化多了,整体性能和体验可能就上升了,相反,这个细节不注意那个细节无所谓,最终就导致应用卡顿,体验糟糕。注重细节的开发者运气一般都不会太差。: )</p> </blockquote> </li> <li> <p>自定义控件使用 <code class="highlighter-rouge">clipRect()</code> 和 <code class="highlighter-rouge">quickReject()</code> 优化</p> <p>当某些控件不可见时,如果还继续绘制更新该控件,就会导致过度绘制。但是通过 Canvas <code class="highlighter-rouge">clipRect()</code> 方法可以设置需要绘制的区域,当某个控件或者 View 的部分区域不可见时,就可以减少过度绘制。</p> <p>先看一下 <code class="highlighter-rouge">clipRect()</code> 方法的说明:</p> <blockquote> <p>Intersect the current clip with the specified rectangle, which is expressed in local coordinates.</p> </blockquote> <p>顾名思义就是给 Canvas 设置一个裁剪区,只有在这个裁剪矩形区域内的才会被绘制,区域之外的都不绘制。 <code class="highlighter-rouge">DrawerLayout</code> 就是一个很不错的例子,先来看一下使用 DrawerLayout 布局的过度绘制结果:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/3ac552385fa37312.png" alt="" /></p> <p>按道理左边的抽屉布局出来时,应该是和主界面的布局叠加起来的,但是为什么抽屉的背景过度绘制只有一次呢?如果是叠加的话,那最少是主界面过度绘制次数 +1,但是结果并不是这样。直接看源码:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span> <span class="kd">protected</span> <span class="kt">boolean</span> <span class="nf">drawChild</span><span class="o">(</span><span class="n">Canvas</span> <span class="n">canvas</span><span class="o">,</span> <span class="n">View</span> <span class="n">child</span><span class="o">,</span> <span class="kt">long</span> <span class="n">drawingTim</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">height</span> <span class="o">=</span> <span class="n">getHeight</span><span class="o">();</span> <span class="kd">final</span> <span class="kt">boolean</span> <span class="n">drawingContent</span> <span class="o">=</span> <span class="n">isContentView</span><span class="o">(</span><span class="n">child</span><span class="o">);</span> <span class="kt">int</span> <span class="n">clipLeft</span> <span class="o">=</span> <span class="mi">0</span><span class="o">,</span> <span class="n">clipRight</span> <span class="o">=</span> <span class="n">getWidth</span><span class="o">();</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">restoreCount</span> <span class="o">=</span> <span class="n">canvas</span><span class="o">.</span><span class="na">save</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">drawingContent</span><span class="o">)</span> <span class="o">{</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">childCount</span> <span class="o">=</span> <span class="n">getChildCount</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">&lt;</span> <span class="n">childCount</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="n">View</span> <span class="n">v</span> <span class="o">=</span> <span class="n">getChildAt</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">v</span> <span class="o">==</span> <span class="n">child</span> <span class="o">||</span> <span class="n">v</span><span class="o">.</span><span class="na">getVisibility</span><span class="o">()</span> <span class="o">!=</span> <span class="n">VISIBLE</span> <span class="o">||</span> <span class="o">!</span><span class="n">hasOpaqueBackground</span><span class="o">(</span><span class="n">v</span><span class="o">)</span> <span class="o">||</span> <span class="o">!</span><span class="n">isDrawerView</span><span class="o">(</span><span class="n">v</span><span class="o">)</span> <span class="o">||</span> <span class="n">v</span><span class="o">.</span><span class="na">getHeight</span><span class="o">()</span> <span class="o">&lt;</span> <span class="n">height</span><span class="o">)</span> <span class="o">{</span> <span class="k">continue</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">checkDrawerViewAbsoluteGravity</span><span class="o">(</span><span class="n">v</span><span class="o">,</span> <span class="n">Gravity</span><span class="o">.</span><span class="na">LEFT</span><span class="o">))</span> <span class="o">{</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">vright</span> <span class="o">=</span> <span class="n">v</span><span class="o">.</span><span class="na">getRight</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">vright</span> <span class="o">&gt;</span> <span class="n">clipLeft</span><span class="o">)</span> <span class="n">clipLeft</span> <span class="o">=</span> <span class="n">vright</span><span class="o">;</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">vleft</span> <span class="o">=</span> <span class="n">v</span><span class="o">.</span><span class="na">getLeft</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">vleft</span> <span class="o">&lt;</span> <span class="n">clipRight</span><span class="o">)</span> <span class="n">clipRight</span> <span class="o">=</span> <span class="n">vleft</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="n">canvas</span><span class="o">.</span><span class="na">clipRect</span><span class="o">(</span><span class="n">clipLeft</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">clipRight</span><span class="o">,</span> <span class="n">getHeight</span><span class="o">());</span> <span class="o">}</span> <span class="o">......</span> <span class="o">}</span> </code></pre></div> </div> <p>在 DrawerLayout 的 <code class="highlighter-rouge">drawChild()</code> 方法一开始会判断是是否是 DrawerLayout 的 ContentView,即非抽屉布局,如果是的话,则遍历 DrawerLayout 的 child view,拿到抽屉布局,如果是左边抽屉,则取抽屉布局的右边边界作为裁剪区的左边界,得到的裁剪矩形就是下图中的红色框部分,然后设置裁剪区域。右边抽屉同理。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/f2bd8c92d4f03a9b.jpg" alt="" /></p> <p>这样一来,只有裁剪矩形内的界面需要绘制,自然就减少了抽屉布局的过度绘制。自定义控件时可以参照这个来优化过度绘制问题。</p> <p>除了 <code class="highlighter-rouge">clipRect()</code> 以外,还可以使用 <code class="highlighter-rouge">canvas.quickreject()</code> 来判断和某个矩形相交,如果相交的话,则可以跳过相交的区域减少过度绘制。</p> </li> </ol> <h3 id="优化实践">优化实践</h3> <p>前面其实已经讲了很多了,但是实际去优化过度绘制时,可能还是会比较懵,看着屏幕上的大片大片的红色,不知道从何下手。接下来就以实际项目中的过度绘制优化经历来谈谈,如何进行优化?</p> <p>先上图,前面是未开启 『调试 GPU 过度绘制』 的界面图,中间的是优化前的过度绘制结果,后面的是优化后的过度绘制结果,不难看出来,中间那张图过度绘制是很严重的,一眼看过去一片红,很显然不符合优化原则。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/1bed5940cdfa0701.png" alt="" /></p> <p>优化步骤如下:</p> <ol> <li> <p>先分析每个地方最少可以绘制几次,不合理的地方就可以优化。</p> <p>例如:中间那张图显示的每个 item 的背景是绿色的,也就是 2 次过度绘制,这肯定是不合理的。因为整个界面大背景是灰色的,item 背景是白色的,按道理应该就 1 次过度绘制。检查下来发现没去掉默认的 Window 背景,移除之后 item 背景就变成了蓝色了,也就是 1 次过度绘制。</p> </li> <li> <p>叠加的布局,过度绘制次数是否合理递增</p> <p>还是看中间那张图,item 的背景过度绘制是 2 次,按道理九宫格图片每张图应该是过度绘制 3 次,但是却显示成红色的,显然没有合理递增而出现了跳跃。</p> <p>先猜测是不是因为给九宫格图片控件设置了白色背景?但是想一下就排除了,因为图片间隙的过度绘制次数和 item 背景是相同的。</p> <p>那就是每个 ImageView 有问题了,后来发现之前设置占位图的时候,给每个 ImageView 设置了一个灰色的背景色:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">imageView</span><span class="o">.</span><span class="na">setBackgroundColor</span><span class="o">(</span><span class="n">Color</span><span class="o">.</span><span class="na">parseColor</span><span class="o">(</span><span class="s">"#eeeeee"</span><span class="o">));</span> </code></pre></div> </div> <p>这也就导致了每个 ImageView 的过度绘制直接多了 1 次。</p> <p>这两步优化后,再看最后一张图中的优化结果,基本是可以的了。</p> </li> <li> <p>在 <strong>优化方法</strong> 中讲到的 ViewPager 布局加 Fragment 实现的首页布局,一个不注意很容易出现过度绘制严重的问题,在移除 ViewPager 和 Activity 根布局的白色背景后,以及默认的 Window 背景,原来红成一片的首页现在基本上是大部分蓝色和小部分绿色了。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/5e3d906c721cc9f3.png" alt="" /></p> </li> </ol> <h3 id="小插曲">小插曲</h3> <p>最后来个小插曲,因为开启 『调试 GPU 过度绘制』比较麻烦,我就想找个比较方便快捷的方式,一开始想着写个桌面插件应用,一键切换。</p> <ul> <li> <p>查文档发现没有相关的设置的 API</p> </li> <li> <p>直接翻源码,发现相关的 API 是隐藏的,集中在 <code class="highlighter-rouge">SystemProperties</code> 类中,可以通过如下代码设置:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">SystemProperties</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">HardwareRenderer</span><span class="o">.</span><span class="na">DEBUG_OVERDRAW_PROPERTY</span><span class="o">,</span> <span class="s">"show"</span><span class="o">);</span> </code></pre></div> </div> </li> <li> <p>直接编译源码拿到了没隐藏的 jar 包,暂时能调用到该类,但是运行之后发现需要系统权限才能设置</p> </li> <li> <p>通过一些方式企图让这个 App 获取到系统权限,但是均失败了 : (</p> </li> </ul> <p>如果你对相关的知识有所了解,请联系我和我探讨下,谢谢。</p> <p>不过最后也算是找到了一个比较方便的方法,省去了去设置里面一步步点。直接运行 adb 指令:</p> <p>开启『调试 GPU 过度绘制』:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell setprop debug.hwui.overdraw show </code></pre></div></div> <p>关闭『调试 GPU 过度绘制』:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell setprop debug.hwui.overdraw <span class="nb">false</span> </code></pre></div></div> <p>再取个指令别名,使用起来还是很方便的。</p> <h3 id="参考资料">参考资料</h3> <ul> <li><a href="http://hukai.me/android-performance-render/">Android 性能优化之渲染篇 - 胡凯</a></li> <li><a href="http://androidperformance.com/2014/10/20/android-performance-optimization-overdraw-1.html">Android 性能优化之过渡绘制(一) | Performance</a></li> <li><a href="http://blog.chengyunfeng.com/?p=458#">Android 性能分析案例 - 云在千峰</a></li> <li><a href="http://mrpeak.cn/android/2016/01/11/android-performance-ui">Android UI 性能优化详解</a></li> <li><a href="http://blog.udinic.com/2015/09/15/speed-up-your-app">Speed up your app</a></li> </ul> Thu, 29 Sep 2016 00:00:00 +0000 http://jaeger.itscoder.com//android/2016/09/29/android-performance-overdraw.html http://jaeger.itscoder.com//android/2016/09/29/android-performance-overdraw.html mUrl:自动生成 Markdown 格式的链接 <p>因为懒,花了半个下午时间开发了一个 Chrome 插件,第一次接触这方面东西,写个博客记录下开发过程。</p> <h3 id="需求来源">需求来源</h3> <p>写 Markdown 的时候可能都有这样一个需求:</p> <p>我们需要插入一个文章链接,格式为 Markdown 的格式,例如这样的一个链接:</p> <p><a href="http://jaeger.itscoder.com/">http://jaeger.itscoder.com</a></p> <p>访问之后,假如我们需要将这个地址直接插入到 Markdown 中,格式如下:</p> <div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nv">写代码的猴子</span><span class="p">](</span><span class="sx">http://jaeger.itscoder.com</span><span class="p">)</span> </code></pre></div></div> <p>在没一个方便的工具之前,我们只能通过两次复制来解决:</p> <ul> <li>复制对应的 url:<code class="highlighter-rouge">http://jaeger.itscoder.com/</code></li> <li>复制该 url 对应的标题:写代码的猴子</li> </ul> <p>这样的重复性工作对于开发者来说,一点都不酷。因此就催生了 mUrl 插件的诞生。</p> <h3 id="插件成果">插件成果</h3> <p><img src="http://ac-QYgvX1CC.clouddn.com/d93799d12d3dfe2f.png" alt="" /></p> <ul> <li> <p>源码地址:<a href="https://github.com/laobie/mUrl">laobie/mUrl: a chrome extension: get website url for markdown writer</a></p> </li> <li> <p>Chrome 商店地址:<a href="https://chrome.google.com/webstore/detail/murl/nmhkegedgpbbkcicjgcnbjebdjedljgl?utm_source=chrome-ntp-icon">mUrl - Chrome Web Store</a></p> </li> <li> <p>使用方法:</p> <ul> <li> <p>在 Chrome 应用商店添加插件 mUrl;</p> </li> <li> <p>将 mUrl 放置到地址栏右边,如下所示:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/a74423d1aadad5c3.jpg" alt="" /></p> </li> <li> <p>打开一个网页,然后点这个插件,此时 Markdown 格式的链接就已经复制到剪贴板上了,直接粘贴到 Markdown 文件中即可。</p> </li> </ul> </li> </ul> <h3 id="开发过程">开发过程</h3> <p>这是官方提供的 Chrome 插件的教程:</p> <p><a href="https://developer.chrome.com/extensions/getstarted">Getting Started: Building a Chrome Extension</a></p> <p>跟着教程来的话你就可以创建一个你自己的插件,不过官方的例子由于墙的原因,我并没有成功开发出来(伟大的 GFW)。</p> <p>接下来就以 mUrl 项目为例,讲解下如何开发一个 Chrome 插件。</p> <ol> <li><strong>准备工作</strong> <ul> <li>一个文本编辑器</li> <li>Chrome 浏览器</li> </ul> </li> <li> <p><strong>项目结构</strong></p> <p>新建一个项目文件夹,我这里创建了一个 <code class="highlighter-rouge">mUrl</code> 的文件夹,该文件夹为插件的根目录,里面包含以下文件:</p> <ul> <li> <p><code class="highlighter-rouge">manifest.json</code> 项目配置文件,包含一些插件的基本信息</p> </li> <li> <p><code class="highlighter-rouge">murl_icon.png</code> 插件图标文件</p> </li> <li> <p><code class="highlighter-rouge">popup.html</code> 插件启动时显示的窗体布局</p> </li> <li> <p><code class="highlighter-rouge">popup.js</code> 执行相关逻辑的 JavaScript 脚本</p> </li> </ul> <p>接下来就对这些文件的作用和具体开发过程进行讲解。</p> </li> <li> <p><strong>manifest.json</strong></p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="s2">"manifest_version"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mUrl"</span><span class="p">,</span><span class="w"> </span><span class="s2">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"This extension get url for Markdown writer"</span><span class="p">,</span><span class="w"> </span><span class="s2">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0.1"</span><span class="p">,</span><span class="w"> </span><span class="s2">"browser_action"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">"default_icon"</span><span class="p">:</span><span class="w"> </span><span class="s2">"murl_icon.png"</span><span class="p">,</span><span class="w"> </span><span class="s2">"default_popup"</span><span class="p">:</span><span class="w"> </span><span class="s2">"popup.html"</span><span class="p">,</span><span class="w"> </span><span class="s2">"default_title"</span><span class="p">:</span><span class="s2">"Get Url For Markdown writer"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="s2">"icons"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">"16"</span><span class="p">:</span><span class="s2">"asset/murl_16.png"</span><span class="p">,</span><span class="w"> </span><span class="s2">"48"</span><span class="p">:</span><span class="s2">"asset/murl_48.png"</span><span class="p">,</span><span class="w"> </span><span class="s2">"128"</span><span class="p">:</span><span class="w"> </span><span class="s2">"asset/murl_128.png"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="s2">"permissions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"activeTab"</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div> </div> <p>这是一个 JSON 格式的文件,其中:</p> <ul> <li><code class="highlighter-rouge">manifest_version</code>: 一般默认是 2,不用改动</li> <li><code class="highlighter-rouge">name</code>: 插件的名称,会显示在 Chrome 商店中</li> <li><code class="highlighter-rouge">description</code>: 插件描述</li> <li><code class="highlighter-rouge">version</code>: 插件版本号,升级的时候需要更新</li> <li> <p><code class="highlighter-rouge">browser_action</code>: 和浏览器相关的</p> <ul> <li> <p><code class="highlighter-rouge">default_icon</code>: 显示在地址栏上的图标,19*19 的 png 格式的图片</p> </li> <li> <p><code class="highlighter-rouge">default_popup</code>: 一个 HTML 文件,点击插件时弹出来的界面</p> </li> <li> <p><code class="highlighter-rouge">default_title</code>: 鼠标移动到图标上显示的提示</p> </li> </ul> </li> <li><code class="highlighter-rouge">icons</code>: 图标,数字对应图标的大小,这几个尺寸的图标不是必须的,但是为了保证图标显示正常,建议添加这几个尺寸的图标。</li> <li><code class="highlighter-rouge">permissions</code>: 插件需要的权限,例如读取网页内容的权限,mUrl 只需要获取到当前 tab 的信息,所以只需要添加一个权限。</li> </ul> </li> <li> <p><strong>murl_icon.png</strong></p> <p>插件的图标文件,在 <code class="highlighter-rouge">manifest.json</code> 使用 <code class="highlighter-rouge">default_icon</code> 定义的,大小是 19*19,格式为 png 的图片。</p> </li> <li> <p><strong>popup.html</strong></p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!doctype html&gt;</span> <span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;title&gt;</span>Copy Url For Markdown<span class="nt">&lt;/title&gt;</span> <span class="nt">&lt;style&gt;</span> <span class="nt">body</span> <span class="p">{</span> <span class="nl">font-family</span><span class="p">:</span> <span class="s1">"Segoe UI"</span><span class="p">,</span> <span class="s1">"Lucida Grande"</span><span class="p">,</span> <span class="n">Tahoma</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span> <span class="nl">font-size</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="p">}</span> <span class="nt">&lt;/style&gt;</span> <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"popup.js"</span><span class="nt">&gt;&lt;/script&gt;</span> <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"clipboard.js"</span><span class="nt">&gt;&lt;/script&gt;</span> <span class="nt">&lt;/head&gt;</span> <span class="nt">&lt;body&gt;</span> <span class="nt">&lt;small</span> <span class="na">id=</span><span class="s">"msg_text"</span><span class="nt">&gt;&lt;/small&gt;</span> <span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"md_format_url"</span> <span class="na">value=</span><span class="s">"test"</span><span class="nt">&gt;</span> <span class="nt">&lt;button</span> <span class="na">class=</span><span class="s">"btn"</span> <span class="na">id=</span><span class="s">"btn_copy"</span> <span class="na">data-clipboard-target=</span><span class="s">"#md_format_url"</span><span class="nt">&gt;</span>copy<span class="nt">&lt;/button&gt;</span> <span class="nt">&lt;/body&gt;</span> <span class="nt">&lt;/html&gt;</span> </code></pre></div> </div> <p>就是一个简单的 HTML 文件,其中需要指定下需要使用的 <strong>JavaScript</strong> 文件,比如 mUrl 使用了:</p> <ul> <li> <p>popup.js</p> </li> <li> <p>clipboard.js</p> </li> </ul> <p>这两个文件,就通过以下进行了引用:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"popup.js"</span><span class="nt">&gt;&lt;/script&gt;</span> <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"clipboard.js"</span><span class="nt">&gt;&lt;/script&gt;</span> </code></pre></div> </div> <p>这里需要注意的是,在官方给的 <a href="view-source:https://developer.chrome.com/extensions/examples/tutorials/getstarted/popup.html">popup.html</a> 中有这样的一段注释:</p> <blockquote> <p>JavaScript and HTML must be in separate files: see our Content Security Policy documentation for details and explanation.</p> </blockquote> <p>出于安全性的考虑, JavaScript 文件必须和 HTML 文件分开,而不能为了方便直接在 HTML 文件中执行 JavaScript 脚本。</p> <p>在 <code class="highlighter-rouge">body</code> 中放置了三个元素:</p> <ul> <li> <p><code class="highlighter-rouge">small</code>: 用来显示复制结果的信息</p> </li> <li> <p><code class="highlighter-rouge">input</code>: 输入框,用来填写 Markdown 格式的链接文本</p> <p>因为需要复制到剪切板,我使用了一个 <a href="https://github.com/zenorocha/clipboard.js">zenorocha/clipboard.js</a> 这个项目中的 js 代码,需要从一个元素中复制文本,因此只能添加一个输入框,给输入框设置文本内容,然后复制。</p> </li> <li> <p><code class="highlighter-rouge">button</code>: 一个按钮,用来复制上面的输入框中的文本,后面使用 JavaScript 自动点击这个按钮,实际上也不需要手动去复制。需要注意到还给 button 添加了一个 <code class="highlighter-rouge">data-clipboard-target</code> 属性,这是使用 <code class="highlighter-rouge">clipboard.js</code> 需要设置的属性,在点这个按钮的时候,自动取上面提到 input 输入框的内容,复制到剪贴板。</p> </li> </ul> </li> <li> <p><strong>popup.js</strong></p> <p>在讲解 popup.js 内容之前,先说一下开发时遇到的一个问题:</p> <p>因为需要在点击插件时自动将得到的 Markdown 格式的链接复制到剪贴板上,对于我这个 JavaScript 还没入门的新手来说并不是那么容易,好在通过 Google 搜索了一圈之后,找到了 <a href="https://github.com/zenorocha/clipboard.js">zenorocha/clipboard.js</a> 这项目,解决了这个问题。关于这个项目的具体使用细节可以阅读项目的 README,本文不再赘述。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">getCurrentTabInfo</span><span class="p">(</span><span class="nx">callback</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">queryInfo</span> <span class="o">=</span> <span class="p">{</span> <span class="na">active</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">currentWindow</span><span class="p">:</span> <span class="kc">true</span> <span class="p">};</span> <span class="nx">chrome</span><span class="p">.</span><span class="nx">tabs</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="nx">queryInfo</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">tabs</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">tab</span> <span class="o">=</span> <span class="nx">tabs</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span> <span class="nx">callback</span><span class="p">(</span><span class="nx">tab</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span> <span class="nx">tab</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span> <span class="p">});</span> <span class="p">}</span> <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'DOMContentLoaded'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> <span class="nx">getCurrentTabInfo</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">title</span><span class="p">,</span> <span class="nx">url</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">msgText</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"msg_text"</span><span class="p">);</span> <span class="nx">msgText</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s2">"none"</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">inputText</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"md_format_url"</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">copyBtn</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"btn_copy"</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">clipboard</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Clipboard</span><span class="p">(</span><span class="s1">'.btn'</span><span class="p">);</span> <span class="nx">clipboard</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">'success'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">e</span><span class="p">);</span> <span class="nx">inputText</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s2">"none"</span><span class="p">;</span> <span class="nx">copyBtn</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s2">"none"</span><span class="p">;</span> <span class="nx">msgText</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s2">"success"</span><span class="p">;</span> <span class="nx">msgText</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s2">"block"</span><span class="p">;</span> <span class="p">});</span> <span class="nx">clipboard</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">'error'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">e</span><span class="p">);</span> <span class="nx">alert</span><span class="p">(</span><span class="nx">e</span><span class="p">);</span> <span class="p">});</span> <span class="c1">// 替换标题中的特殊字符,例如“[]()”等</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">title</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">[</span><span class="sr">|</span><span class="se">\\</span><span class="sr">`*_{}</span><span class="se">\[\]</span><span class="sr">()#+</span><span class="se">\-</span><span class="sr">.!</span><span class="se">]</span><span class="sr">/g</span><span class="p">,</span> <span class="s1">'</span><span class="err">\\</span><span class="s1">$&amp;'</span><span class="p">);</span> <span class="c1">// 拼接 Markdown 格式的链接字符串</span> <span class="nx">inputText</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="s2">"["</span> <span class="o">+</span> <span class="nx">title</span> <span class="o">+</span> <span class="s2">"]("</span> <span class="o">+</span> <span class="nx">url</span> <span class="o">+</span> <span class="s2">")"</span><span class="p">;</span> <span class="nx">copyBtn</span><span class="p">.</span><span class="nx">click</span><span class="p">();</span> <span class="p">});</span> <span class="p">});</span> </code></pre></div> </div> <p>popup.js 的代码应该也不难理解:</p> <ul> <li> <p><code class="highlighter-rouge">getCurrentTabInfo()</code> 函数:获取到当期激活的 Tab 的 url 和 title 信息,后面拼接链接需要使用。</p> </li> <li> <p><code class="highlighter-rouge">document.addEventListener('DOMContentLoaded', function () {}</code></p> <p>添加页面加载监听,页面加载时,调用 <code class="highlighter-rouge">getCurrentTabInfo()</code> 函数,获得当前 Tab 的标题和 url:</p> <ul> <li> <p>先获取到之前添加的三个元素,并进行显示和隐藏的设置</p> </li> <li> <p>创建一个 Clipboard 对象,用来复制格式化之后的链接</p> </li> <li> <p>设置复制成功和出错的监听,在复制成功时,将 <strong>输入框</strong> 和 <strong>按钮</strong> 隐藏,只显示 『success』文本,复制出错时则弹一个对话框,显示出错信息。</p> </li> <li> <p>在拼接 Markdown 格式的链接字符串之前,先对标题中的特殊字符进行了替换处理,例如 “[”、“]” 需要替换成 “[”、“]” 。</p> </li> <li> <p>自动点击 <strong>复制</strong> 按钮,将文本复制到剪切板,复制成功,就会响应上面设置 <strong>复制成功</strong> 的监听事件。</p> </li> </ul> </li> </ul> </li> <li>因为项目使用到了 <code class="highlighter-rouge">clipboard.js</code> ,所以需要将该 JavaScript 文件也放置到项目文件夹中。</li> </ol> <p>至此,整个项目基本搞定了,接下来就是运行测试了。</p> <h3 id="运行和发布">运行和发布</h3> <ol> <li> <p>运行</p> <ul> <li> <p>在 Chrome 中打开:<a href="chrome://extensions/">chrome://extensions/</a></p> </li> <li> <p>打开开发者模式,选择项目文件夹,加载插件</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/0a56fe81d8cbdf25.jpg" alt="" /></p> </li> <li> <p>上一步没报错的话,插件就加载成功了,这是你就可以点击插件图标测试下功能是否正常了。</p> </li> <li> <p>注意到 <strong>加载已解压的扩展程序</strong> 右边有一个 <strong>打包扩展程序</strong> 的按钮,点击这个:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/df023de5b50d4617.jpg" alt="" /></p> <p>私有密钥文件第一次可以不选,在第一次打包之后就会自动生成一个 .gem 后缀的私有密钥文件,下次更新插件时需要选择这个密钥文件。</p> <p>打包会生成一个 .crx 格式的文件,这个就是插件的安装文件,发送给其他人,安装上之后就可以使用该插件了。</p> </li> </ul> </li> <li> <p>发布到 Chrome 应用商店</p> <p>在 <a href="https://chrome.google.com/webstore/developer/dashboard?utm_source=chrome-ntp-icon">Developer Dashboard - Chrome Web Store</a> 按照步骤先注册一个开发者帐号,需要支付 $5 ,如果你没 VISA 信用卡的话,可能就比较麻烦了。</p> <p>注册好开发者帐号后,<a href="https://chrome.google.com/webstore/developer/update?utm_source=chrome-ntp-icon&amp;publisherId=g03303820154321143420">Upload - Developer Dashboard</a> 在这个页面按照下面的说明,上传项目的源文件,填写上相关的信息,就可以了,页面上说明很齐全,这里不多讲解了。</p> <p>发布到 Chrome 应用商店之后,别人就可以直接从商店下载安装你开发的插件了。</p> <p>​</p> </li> </ol> <p>最后,再提一下 mUrl 的插件源码地址:<a href="https://github.com/laobie/mUrl">laobie/mUrl</a>,欢迎提 PR 和建议。</p> <h3 id="参考资料">参考资料</h3> <ul> <li> <p><a href="http://www.cnblogs.com/guogangj/p/3235703.html">Chrome插件(Extensions)开发攻略 - guogangj - 博客园</a></p> </li> <li> <p><a href="http://9iphp.com/web/javascript/js-copy-library-clipboard-js.html">纯JavaScript实现的复制剪切库–clipboard.js | Specs’ Blog-就爱PHP</a></p> </li> <li> <p><a href="https://developer.chrome.com/extensions/getstarted">Getting Started: Building a Chrome Extension - Google Chrome</a></p> </li> <li> <p><a href="https://github.com/ku/CreateLink">ku/CreateLink: Make Link alternative to chrome</a></p> </li> </ul> Mon, 26 Sep 2016 00:00:00 +0000 http://jaeger.itscoder.com//chrome%20extension/2016/09/26/chrome-extension-murl.html http://jaeger.itscoder.com//chrome%20extension/2016/09/26/chrome-extension-murl.html 热修复实现:ClassLoader 方式的实现 <blockquote> <ul> <li>文章来源:itsCoder 的 <a href="https://github.com/itsCoder/weeklyblog">WeeklyBolg</a> 项目</li> <li>itsCoder 主页:<a href="http://itscoder.com/">http://itscoder.com/</a></li> <li>作者:<a href="https://github.com/laobie">Jaeger</a></li> <li>审阅者:<a href="https://github.com/hymanme">Hymanme</a>, <a href="https://github.com/brucezz">Brucezz</a></li> </ul> </blockquote> <p>在之前的文章 <a href="http://jaeger.itscoder.com/android/2016/08/27/android-classloader.html">热修复入门:Android 中的 ClassLoader</a> 中,讲解了 Android 中的 ClassLoader 工作原理和通过 ClassLoader 实现热修复的可能性。本文结合 <a href="https://github.com/jasonross/Nuwa">Nuva</a> 项目,来讲讲基于 ClassLoader 方式如何具体实现热修复,阅读本文之前建议先通过前面提到的文章了解下 Android 的 ClassLoader。</p> <h3 id="实现的几个关键点">实现的几个关键点</h3> <p>在讲解实现思路之前,先回顾下 <a href="http://jaeger.itscoder.com/android/2016/08/27/android-classloader.html">热修复入门:Android 中的 ClassLoader</a> 文章中提到的几个关键点,这也是 ClassLoader 方式实现热修复的关键:</p> <ul> <li> <p>在 Android 中,App 安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的。</p> </li> <li> <p>DexClassLoader 可以用来加载 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件</p> </li> <li> <p>DexClassLoader 和 PathClassLoader 的基类 BaseDexClassLoader 查找 class 是通过其内部的 <code class="highlighter-rouge">DexPathList pathList</code> 来查找的</p> </li> <li> <p>DexPathList 内部有一个 <code class="highlighter-rouge">Element[] dexElements</code> 数组,其 <code class="highlighter-rouge">findClass()</code> 方法(源码如下)的实现就是遍历该数组,查找 class ,一旦找到需要的类,就直接返回,停止遍历:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="n">Class</span> <span class="nf">findClass</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">,</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Throwable</span><span class="o">&gt;</span> <span class="n">suppressed</span><span class="o">)</span> <span class="o">{</span> <span class="k">for</span> <span class="o">(</span><span class="n">Element</span> <span class="n">element</span> <span class="o">:</span> <span class="n">dexElements</span><span class="o">)</span> <span class="o">{</span> <span class="n">DexFile</span> <span class="n">dex</span> <span class="o">=</span> <span class="n">element</span><span class="o">.</span><span class="na">dexFile</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">dex</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">Class</span> <span class="n">clazz</span> <span class="o">=</span> <span class="n">dex</span><span class="o">.</span><span class="na">loadClassBinaryName</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">definingContext</span><span class="o">,</span> <span class="n">suppressed</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">clazz</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">clazz</span><span class="o">;</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">dexElementsSuppressedExceptions</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">suppressed</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="n">dexElementsSuppressedExceptions</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> </li> </ul> <h3 id="实现思路">实现思路</h3> <p>基于 ClassLoader 方式实现的热修复思路如下图所示:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/b1c92f1555e7fb4b.jpg" alt="" /></p> <p>主要步骤:</p> <ol> <li> <p>假设 MainActivity 中有一个方法<code class="highlighter-rouge">showMsg</code> ,现在显示的是 “bug” ,需要修复。</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="n">AppCompatActivity</span> <span class="o">{</span> <span class="o">...</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">showMsg</span><span class="o">()</span> <span class="o">{</span> <span class="n">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">"bug"</span><span class="o">,</span> <span class="n">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> </code></pre></div> </div> </li> <li> <p>我们修改 <code class="highlighter-rouge">showMsg()</code> 方法,让其显示正确的结果 “meaasge”。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">showMsg</span><span class="o">()</span> <span class="o">{</span> <span class="n">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">"message"</span><span class="o">,</span> <span class="n">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> </code></pre></div> </div> </li> <li> <p>制作好补丁包,即 patch.jar 文件,该 patch.jar 文件中就包含已经修复了的 dex 文件,注意此时 patch.jar 会包含一个和原来安装 apk 文件中同样的类 <code class="highlighter-rouge">MainActivity</code> 。</p> </li> <li> <p>在 Application 的 <code class="highlighter-rouge">onCreate</code> 方法中检测是否已经下载好补丁包,如果存在补丁包,就通过 DexClassLoader 加载 patch.jar,然后通过反射拿到 DexClassLoader 中的 DexPathList 对象,进而拿到 <code class="highlighter-rouge">Element[] dexElements</code> 数组,这里标记该 Element 数组为 <strong>newDexElements</strong> 。</p> </li> <li> <p>还是通过反射,拿到 App 默认的 ClassLoader 即 PathClassLoader 的 DexPathList 对象,进而拿到 Element 数组,这里标记下该数组为 <strong>baseDexElements</strong> 。</p> </li> <li> <p>将 newDexElements 和 baseDexElements 合成一个新的数组 <strong>allDexElements</strong> ,且保证 newDexElements 中的值在 allDexElements 数组的最前面。</p> </li> <li> <p>然后还是通过通过反射,将合成的 Element 数组设置给 PathClassLoader 的 DexPathList 对象。</p> </li> <li> <p>在 Application 完成初始化之后,会开始加载 <code class="highlighter-rouge">MainActivity</code> ,加载过程就是通过 DexPathList 对象的 <code class="highlighter-rouge">findClass()</code> 方法来完成的,会从头开始遍历其 Element 数组,会优先查找到之前插入的补丁包中的 dexFile,而原 apk 中的则不会查找到,因此就实现了热修复的目的。</p> </li> </ol> <h3 id="基于-classloader-方式实现需要解决的问题">基于 ClassLoader 方式实现需要解决的问题</h3> <p>在对 Nuwa 源码开始解读之前,先说明下在基于 ClassLoader 方式实现热修复需要解决的问题。</p> <ul> <li> <p>CLASS_ISPREVERIFIED 问题</p> <p>odex 文件是 OptimizedDEX 的缩写,表示经过优化的 dex 文件。由于 Android 程序的 apk 文件为 zip 压缩包格式,Dalvik虚拟机每次加载都需要从 apk 中读取 classes.dex 文件,这会耗费很多 cpu 时间,而采用 odex 方式优化的 dex 文件已经包含了加载 dex 必须的依赖库文件列表,Dalvik 虚拟机只需检测并加载所需的依赖库即可执行相应的 dex 文件,大大缩短了读取 dex 文件所需的时间。同时,Android专门提供了一个验证与优化 dex 文件的工具 dexopt,Dalvik 虚拟机在加载一个 dex 文件时,通过指定的验证与优化选项来调用 dexopt 进行相应的验证与优化操作。</p> <p>在 dex 优化过程中:</p> <blockquote> <p>如果某个类直接方法中引用到的类(第一层级关系,不会进行递归搜索)在同一个 dex 中的话,那么这个类就会被打上 <strong>CLASS_ISPREVERIFIED</strong> 标志。</p> </blockquote> <p>打上这个标志的类,其引用到的类就只会在该类所在的 dex 中查找,如果没找到,就直接报以下异常:</p> <div class="language-verilog highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">java</span><span class="o">.</span><span class="n">lang</span><span class="o">.</span><span class="n">IllegalAccessError</span><span class="o">:</span> <span class="n">Class</span> <span class="kt">ref</span> <span class="n">in</span> <span class="n">pre</span><span class="o">-</span><span class="n">verified</span> <span class="kt">class</span> <span class="n">resolved</span> <span class="n">to</span> <span class="n">unexpected</span> <span class="n">implementation</span> </code></pre></div> </div> <p>而 ClassLoader 方式实现的热修复,必然需要在 patch.jar 的 dex 文件中查找其他类。为了防止类打上 CLASS_ISPREVERIFIED 标志,我们只需要在每个类中引用一个单独的 dex 中的类即可。这个 dex 我们命名为 hack.dex,其包含一个 <code class="highlighter-rouge">HackLoad.java</code> ,接下来需要做的就是在除了 Applicaton 类以为的类的默认构造方法中都引用一下 <code class="highlighter-rouge">HackLoad</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="n">AppCompatActivity</span> <span class="o">{</span> <span class="kd">public</span> <span class="nf">MainActivity</span><span class="o">()</span> <span class="o">{</span> <span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">HackLoad</span><span class="o">.</span><span class="na">class</span><span class="o">);</span> <span class="o">}</span> <span class="o">...</span> <span class="o">}</span> </code></pre></div> </div> <p>以上插入外部类防止打上 CLASS_ISPREVERIFIED 标志的操作也叫做打桩。</p> <p>目前开源的热修复项目插入打桩的代码均是通过 javassist 来实现的,本文这里不做详细介绍了,可以参考一下文章来深入了解:</p> <ul> <li><a href="http://www.jianshu.com/p/56facb3732a7">安卓 App 热补丁动态修复实现 - 简书</a></li> <li><a href="https://www.ibm.com/developerworks/cn/java/j-dyn0916/">Java 编程的动态性, 第四部分: 用 Javassist 进行类转换</a></li> </ul> <blockquote> <p>注:Android 官方增加类的验证过程,并打上 CLASS_ISPREVERIFIED 标志,肯定是为了提升性能和效率的,因此这种解决方案对性能确实存在一定的影响,在微信的 Tinker 方案对比中,也给出了实际的效率对比,差距还是挺大的,因此在使用该方式实现热修复需要了解到这一点。</p> </blockquote> <p><img src="http://ac-QYgvX1CC.clouddn.com/04eb03974bad8947.png" alt="" /></p> </li> </ul> <h3 id="nuva-项目的源码解读">Nuva 项目的源码解读</h3> <p>在前面的实现思路分析中,可以说整体思路是比较简单清晰的,按照此思路来,具体的实现其实也不难。接下来就以 Nuwa 项目的源码来解读下具体的实现。</p> <ol> <li> <p>项目结构分析</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/473528c66cc757e3.jpg" alt="" /></p> <p>Nuwa 项目的结构如上图所示,可以看出,项目结构并不复杂:</p> <ul> <li><code class="highlighter-rouge">util/AssetUtils.java</code> Asset 工具类,内部两个方法:复制 Asset 资源和复制文件。</li> <li><code class="highlighter-rouge">util/DexUtils.java</code> dex 工具类,主要是实现将 patch.jar 文件中的 dexFile 插入到 PathClassLoader 对应的 Element 数组的前面。</li> <li><code class="highlighter-rouge">util/ReflectionUtils.java</code> 反射工具类,实现了两个方法:获取和设置无访问权限域(字段)的值。</li> <li><code class="highlighter-rouge">Nuwa.java</code> 项目主类,其包含两个方法:初始化方法,加载补丁方法。</li> </ul> </li> <li> <p>Nuva 的实现过程:初始化和加载 dex</p> <p>在 Nuwa 项目的使用说明中,需要在 Application 中添加如下代码:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nd">@Override</span> <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">attachBaseContext</span><span class="o">(</span><span class="n">Context</span> <span class="n">base</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">.</span><span class="na">attachBaseContext</span><span class="o">(</span><span class="n">base</span><span class="o">);</span> <span class="n">Nuwa</span><span class="o">.</span><span class="na">init</span><span class="o">(</span><span class="k">this</span><span class="o">);</span> <span class="o">}</span> </code></pre></div> </div> <p>直接看 <code class="highlighter-rouge">Nuwa.java</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">Nuwa</span> <span class="o">{</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">TAG</span> <span class="o">=</span> <span class="s">"nuwa"</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">HACK_DEX</span> <span class="o">=</span> <span class="s">"hack.apk"</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">DEX_DIR</span> <span class="o">=</span> <span class="s">"nuwa"</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">DEX_OPT_DIR</span> <span class="o">=</span> <span class="s">"nuwaopt"</span><span class="o">;</span> <span class="cm">/** * 初始时加载 hack.pak 的 dex 文件,处理打桩 * @param context */</span> <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">init</span><span class="o">(</span><span class="n">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span> <span class="n">File</span> <span class="n">dexDir</span> <span class="o">=</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getFilesDir</span><span class="o">(),</span> <span class="n">DEX_DIR</span><span class="o">);</span> <span class="n">dexDir</span><span class="o">.</span><span class="na">mkdir</span><span class="o">();</span> <span class="n">String</span> <span class="n">dexPath</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="k">try</span> <span class="o">{</span> <span class="n">dexPath</span> <span class="o">=</span> <span class="n">AssetUtils</span><span class="o">.</span><span class="na">copyAsset</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">HACK_DEX</span><span class="o">,</span> <span class="n">dexDir</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="n">TAG</span><span class="o">,</span> <span class="s">"copy "</span> <span class="o">+</span> <span class="n">HACK_DEX</span> <span class="o">+</span> <span class="s">" failed"</span><span class="o">);</span> <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span> <span class="o">}</span> <span class="n">loadPatch</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">dexPath</span><span class="o">);</span> <span class="o">}</span> <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">loadPatch</span><span class="o">(</span><span class="n">Context</span> <span class="n">context</span><span class="o">,</span> <span class="n">String</span> <span class="n">dexPath</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</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="o">{</span> <span class="n">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="n">TAG</span><span class="o">,</span> <span class="s">"context is null"</span><span class="o">);</span> <span class="k">return</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(!</span><span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">dexPath</span><span class="o">).</span><span class="na">exists</span><span class="o">())</span> <span class="o">{</span> <span class="n">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="n">TAG</span><span class="o">,</span> <span class="n">dexPath</span> <span class="o">+</span> <span class="s">" is null"</span><span class="o">);</span> <span class="k">return</span><span class="o">;</span> <span class="o">}</span> <span class="n">File</span> <span class="n">dexOptDir</span> <span class="o">=</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getFilesDir</span><span class="o">(),</span> <span class="n">DEX_OPT_DIR</span><span class="o">);</span> <span class="n">dexOptDir</span><span class="o">.</span><span class="na">mkdir</span><span class="o">();</span> <span class="k">try</span> <span class="o">{</span> <span class="n">DexUtils</span><span class="o">.</span><span class="na">injectDexAtFirst</span><span class="o">(</span><span class="n">dexPath</span><span class="o">,</span> <span class="n">dexOptDir</span><span class="o">.</span><span class="na">getAbsolutePath</span><span class="o">());</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="n">TAG</span><span class="o">,</span> <span class="s">"inject "</span> <span class="o">+</span> <span class="n">dexPath</span> <span class="o">+</span> <span class="s">" failed"</span><span class="o">);</span> <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> <p>在 <code class="highlighter-rouge">init()</code> 方法中,通过加载 asset 文件夹中的 hack.apk 文件,将插桩类加载进来,防止之前插桩的那些类报找不到 <code class="highlighter-rouge">HackLoad.class</code> 异常。这里也可以意识到一点,就是 Application 不应该插桩,否则直接报异常出错。</p> <p>接下来的 <code class="highlighter-rouge">loadPatch(Context context, String dexPath)</code> 才是重点,除了在 <code class="highlighter-rouge">init()</code> 方法中被调用以为,后面加载补丁 patch.jar 时也是使用该方法来加载。其需要两个参数:一个是上下文 context,一个是包含 dex 的 jar 或者 apk 文件的路径。</p> <p>注意到其中有这么一段代码:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">File</span> <span class="n">dexOptDir</span> <span class="o">=</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getFilesDir</span><span class="o">(),</span> <span class="n">DEX_OPT_DIR</span><span class="o">);</span> <span class="n">dexOptDir</span><span class="o">.</span><span class="na">mkdir</span><span class="o">();</span> </code></pre></div> </div> <p>这个得到的是一个存放优化后的 dex 文件的路径,这是 DexClassLoader 类的构造方法所需要的:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">DexClassLoader</span><span class="o">(</span><span class="n">String</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">String</span> <span class="n">optimizedDirectory</span><span class="o">,</span> <span class="n">String</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">ClassLoader</span> <span class="n">parent</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">(</span><span class="n">dexPath</span><span class="o">,</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">optimizedDirectory</span><span class="o">),</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">parent</span><span class="o">);</span> <span class="o">}</span> </code></pre></div> </div> <ul> <li><code class="highlighter-rouge">String optimizedDirectory</code> : 用来缓存优化的 dex 文件的路径,即从 apk 或 jar 文件中提取出来的 dex 文件。该路径不可以为空,且应该是应用私有的,有读写权限的路径(实际上也可以使用外部存储空间,但是这样的话就存在代码注入的风险)。</li> </ul> <p>关于 DexClassLoader 的其他细节,可以阅读本文开头提到的那篇文章。</p> <p>接下来就是调用 <code class="highlighter-rouge">DexUtils.injectDexAtFirst()</code> 方法,看该方法的名称就可以知道,是将对应的 dex 注入到所有的 dex 的最前面。</p> </li> <li> <p>注入补丁的 dex</p> <p>注入补丁的过程主要在 DexUtil 类中:</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">DexUtils</span> <span class="o">{</span> <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">injectDexAtFirst</span><span class="o">(</span><span class="n">String</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">String</span> <span class="n">defaultDexOptPath</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">NoSuchFieldException</span><span class="o">,</span> <span class="n">IllegalAccessException</span><span class="o">,</span> <span class="n">ClassNotFoundException</span> <span class="o">{</span> <span class="n">DexClassLoader</span> <span class="n">dexClassLoader</span> <span class="o">=</span> <span class="k">new</span> <span class="n">DexClassLoader</span><span class="o">(</span><span class="n">dexPath</span><span class="o">,</span> <span class="n">defaultDexOptPath</span><span class="o">,</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">getPathClassLoader</span><span class="o">());</span> <span class="n">Object</span> <span class="n">baseDexElements</span> <span class="o">=</span> <span class="n">getDexElements</span><span class="o">(</span><span class="n">getPathList</span><span class="o">(</span><span class="n">getPathClassLoader</span><span class="o">()));</span> <span class="n">Object</span> <span class="n">newDexElements</span> <span class="o">=</span> <span class="n">getDexElements</span><span class="o">(</span><span class="n">getPathList</span><span class="o">(</span><span class="n">dexClassLoader</span><span class="o">));</span> <span class="n">Object</span> <span class="n">allDexElements</span> <span class="o">=</span> <span class="n">combineArray</span><span class="o">(</span><span class="n">newDexElements</span><span class="o">,</span> <span class="n">baseDexElements</span><span class="o">);</span> <span class="n">Object</span> <span class="n">pathList</span> <span class="o">=</span> <span class="n">getPathList</span><span class="o">(</span><span class="n">getPathClassLoader</span><span class="o">());</span> <span class="n">ReflectionUtils</span><span class="o">.</span><span class="na">setField</span><span class="o">(</span><span class="n">pathList</span><span class="o">,</span> <span class="n">pathList</span><span class="o">.</span><span class="na">getClass</span><span class="o">(),</span> <span class="s">"dexElements"</span><span class="o">,</span> <span class="n">allDexElements</span><span class="o">);</span> <span class="o">}</span> <span class="kd">private</span> <span class="kd">static</span> <span class="n">PathClassLoader</span> <span class="nf">getPathClassLoader</span><span class="o">()</span> <span class="o">{</span> <span class="n">PathClassLoader</span> <span class="n">pathClassLoader</span> <span class="o">=</span> <span class="o">(</span><span class="n">PathClassLoader</span><span class="o">)</span> <span class="n">DexUtils</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getClassLoader</span><span class="o">();</span> <span class="k">return</span> <span class="n">pathClassLoader</span><span class="o">;</span> <span class="o">}</span> <span class="kd">private</span> <span class="kd">static</span> <span class="n">Object</span> <span class="nf">getDexElements</span><span class="o">(</span><span class="n">Object</span> <span class="n">paramObject</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">IllegalArgumentException</span><span class="o">,</span> <span class="n">NoSuchFieldException</span><span class="o">,</span> <span class="n">IllegalAccessException</span> <span class="o">{</span> <span class="k">return</span> <span class="n">ReflectionUtils</span><span class="o">.</span><span class="na">getField</span><span class="o">(</span><span class="n">paramObject</span><span class="o">,</span> <span class="n">paramObject</span><span class="o">.</span><span class="na">getClass</span><span class="o">(),</span> <span class="s">"dexElements"</span><span class="o">);</span> <span class="o">}</span> <span class="kd">private</span> <span class="kd">static</span> <span class="n">Object</span> <span class="nf">getPathList</span><span class="o">(</span><span class="n">Object</span> <span class="n">baseDexClassLoader</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">IllegalArgumentException</span><span class="o">,</span> <span class="n">NoSuchFieldException</span><span class="o">,</span> <span class="n">IllegalAccessException</span><span class="o">,</span> <span class="n">ClassNotFoundException</span> <span class="o">{</span> <span class="k">return</span> <span class="n">ReflectionUtils</span><span class="o">.</span><span class="na">getField</span><span class="o">(</span><span class="n">baseDexClassLoader</span><span class="o">,</span> <span class="n">Class</span><span class="o">.</span><span class="na">forName</span><span class="o">(</span><span class="s">"dalvik.system.BaseDexClassLoader"</span><span class="o">),</span> <span class="s">"pathList"</span><span class="o">);</span> <span class="o">}</span> <span class="kd">private</span> <span class="kd">static</span> <span class="n">Object</span> <span class="nf">combineArray</span><span class="o">(</span><span class="n">Object</span> <span class="n">firstArray</span><span class="o">,</span> <span class="n">Object</span> <span class="n">secondArray</span><span class="o">)</span> <span class="o">{</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">localClass</span> <span class="o">=</span> <span class="n">firstArray</span><span class="o">.</span><span class="na">getClass</span><span class="o">().</span><span class="na">getComponentType</span><span class="o">();</span> <span class="kt">int</span> <span class="n">firstArrayLength</span> <span class="o">=</span> <span class="n">Array</span><span class="o">.</span><span class="na">getLength</span><span class="o">(</span><span class="n">firstArray</span><span class="o">);</span> <span class="kt">int</span> <span class="n">allLength</span> <span class="o">=</span> <span class="n">firstArrayLength</span> <span class="o">+</span> <span class="n">Array</span><span class="o">.</span><span class="na">getLength</span><span class="o">(</span><span class="n">secondArray</span><span class="o">);</span> <span class="n">Object</span> <span class="n">result</span> <span class="o">=</span> <span class="n">Array</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="n">localClass</span><span class="o">,</span> <span class="n">allLength</span><span class="o">);</span> <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">k</span> <span class="o">&lt;</span> <span class="n">allLength</span><span class="o">;</span> <span class="o">++</span><span class="n">k</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">k</span> <span class="o">&lt;</span> <span class="n">firstArrayLength</span><span class="o">)</span> <span class="o">{</span> <span class="n">Array</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">result</span><span class="o">,</span> <span class="n">k</span><span class="o">,</span> <span class="n">Array</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">firstArray</span><span class="o">,</span> <span class="n">k</span><span class="o">));</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">Array</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">result</span><span class="o">,</span> <span class="n">k</span><span class="o">,</span> <span class="n">Array</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">secondArray</span><span class="o">,</span> <span class="n">k</span> <span class="o">-</span> <span class="n">firstArrayLength</span><span class="o">));</span> <span class="o">}</span> <span class="o">}</span> <span class="k">return</span> <span class="n">result</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> <p>结合上文实现思路的分析,<code class="highlighter-rouge">injectDexAtFirst()</code> 方法的流程是很清晰的:</p> <ul> <li>通过 <code class="highlighter-rouge">DexClassLoader</code> 加载补丁中的 dex 文件,然后反射得到新的 Element 集合:<code class="highlighter-rouge">newDexElements</code> ;</li> <li>拿到 <code class="highlighter-rouge">PathClassLoader</code> 中的 Element 集合:<code class="highlighter-rouge">baseDexElements</code> ;</li> <li>将 <code class="highlighter-rouge">newDexElements</code> 和 <code class="highlighter-rouge">baseDexElements</code> 组合成整个的 Element 组合,组合是放在 <code class="highlighter-rouge">combineArray</code> 方法中执行的,看看其具体的实现,就可以发现会优先将 newDexElements 中的值放在合成数组的最前面,这也是之前所提到的实现热修复的关键点之一。</li> <li>将合成后的 <code class="highlighter-rouge">allDexElements</code> 设置给 PathClassLoader 的 DexPathList 对应的 Element 数组。</li> </ul> <p>反射工具类的源码如下:</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">ReflectionUtils</span> <span class="o">{</span> <span class="kd">public</span> <span class="kd">static</span> <span class="n">Object</span> <span class="nf">getField</span><span class="o">(</span><span class="n">Object</span> <span class="n">obj</span><span class="o">,</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">cl</span><span class="o">,</span> <span class="n">String</span> <span class="n">field</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">NoSuchFieldException</span><span class="o">,</span> <span class="n">IllegalArgumentException</span><span class="o">,</span> <span class="n">IllegalAccessException</span> <span class="o">{</span> <span class="n">Field</span> <span class="n">localField</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="na">getDeclaredField</span><span class="o">(</span><span class="n">field</span><span class="o">);</span> <span class="n">localField</span><span class="o">.</span><span class="na">setAccessible</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> <span class="k">return</span> <span class="n">localField</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">obj</span><span class="o">);</span> <span class="o">}</span> <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">setField</span><span class="o">(</span><span class="n">Object</span> <span class="n">obj</span><span class="o">,</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">cl</span><span class="o">,</span> <span class="n">String</span> <span class="n">field</span><span class="o">,</span> <span class="n">Object</span> <span class="n">value</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">NoSuchFieldException</span><span class="o">,</span> <span class="n">IllegalArgumentException</span><span class="o">,</span> <span class="n">IllegalAccessException</span> <span class="o">{</span> <span class="n">Field</span> <span class="n">localField</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="na">getDeclaredField</span><span class="o">(</span><span class="n">field</span><span class="o">);</span> <span class="n">localField</span><span class="o">.</span><span class="na">setAccessible</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> <span class="n">localField</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">obj</span><span class="o">,</span> <span class="n">value</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> <p>关于反射,你可以通过 <a href="http://www.jianshu.com/p/1a60d55a94cd">Java 基础与提高干货系列——Java反射机制</a> 来了解,本文就不多做探讨了。</p> <p>至此,Nuva 的关键代码均解读完毕,就该项目而言,代码量并不多,但是整个实现的思路是很巧妙很清晰的,这也是该项目的关键之处。</p> </li> </ol> <h3 id="后续内容">后续内容</h3> <ul> <li>在接下来的系列文章中,还会结合 Nuva 项目,介绍下补丁包 patch.jar 的生成操作。</li> <li>由于本文时间较为仓促,后续有时间的话会补上实践过程。</li> </ul> <h3 id="参考资料">参考资料</h3> <ul> <li><a href="http://blog.sina.com.cn/s/blog_71338cc10102uwgt.html">绕过 Dalvik 验证技术分析</a></li> <li><a href="http://www.jianshu.com/p/56facb3732a7">安卓 App 热补丁动态修复实现 - 简书</a></li> </ul> Tue, 20 Sep 2016 00:00:00 +0000 http://jaeger.itscoder.com//android/2016/09/20/nuva-source-code-analysis.html http://jaeger.itscoder.com//android/2016/09/20/nuva-source-code-analysis.html Android 热修复方案对比 <h3 id="概述">概述</h3> <p>没有 Bug 的程序几乎是不存在的,加上 App 更新版本过程又很繁琐,热修复技术从一提出,就拥有很大的技术需求市场。从去年下半年开始,热修复技术就在 Android 技术社区火了一阵子,最近阿里百川正式开启了<a href="https://hotfix.taobao.com/hotfix/index.htm">HotFix</a> 产品的公测服务,这也意味着开始有平台专门提供热修复服务,让普通的开发者和一些小公司也有机会使用上热修复技术,让这项技术不再是大公司才折腾得起、用得起的。当然使用效果或者说提供的服务质量具体怎么样还有待验证。</p> <h3 id="热修复基本原理">热修复基本原理</h3> <p>热修复的基本原理并不多,目前已知可用的热修复实现的原理主要有以下几种:</p> <ol> <li>基于 Xposed 实现的无侵入的运行时 <a href="http://en.wikipedia.org/wiki/Aspect-oriented_programming">AOP (Aspect-oriented Programming)</a>  框架,可以实现在线修复 Bug,修复粒度方法级别,但是由于对 ART 虚拟机不支持,导致其对 Android 5.0、6.0 均不支持,使用局限性太大。目前基于这一原理实现的解决方案是手淘团队开源的 <a href="https://github.com/alibaba/dexposed">Dexposed</a> 项目。</li> <li>native hook 方式,其核心部分在 JNI 层对方法进行替换,替换有问题的方法,修复粒度方法级别,无法在类中新增和删减字段,可以做到即时生效,该原理的实现方案主要是阿里团队开源的 <a href="https://github.com/alibaba/AndFix">AndFix</a> 。</li> <li>该原理由 QQ 空间技术团队提出,使用新的 ClassLoader 加载 patch.dex,hack 默认的 ClassLoader,替换有问题的类,修复粒度类级别,一般无法做到即时生效,需要在应用下一次启动时生效。目前基于该原理实现的方案有 <a href="https://github.com/jasonross/Nuwa">Nuwa</a>、<a href="https://github.com/dodola/HotFix">HotFix</a>、<a href="https://github.com/dodola/RocooFix">RocooFix</a> 。</li> <li>dex 文件全量替换,基于 DexDiff 技术,对比修复前后的 dex 文件,生成 patch.dex,再根据 patch.dex 更新有问题的 dex 文件。该方案由微信团队提出:<a href="http://bugly.qq.com/bbs/forum.php?mod=viewthread&amp;tid=1264">微信Android热补丁实践演进之路</a> ,暂时还未开源。目前基于这一原理实现的开源方案只有一个:<a href="https://github.com/zzz40500/Tinker_imitator">Tinker_imitator</a> 。</li> </ol> <p>目前热修复的原理基本就这四种,考虑到使用的兼容性、可实现性以及可操作性,基本上能实际应用到项目中的就剩下了 2、3 两种了,至于第 4 种方式,只能等微信团队开源出比较成熟的方案,方可实际应用。</p> <h3 id="开源的热修复方案对比">开源的热修复方案对比</h3> <ul> <li> <p><a href="https://github.com/alibaba/dexposed">Dexposed</a></p> <ul> <li>作者:手淘团队</li> <li>修复粒度:方法级别</li> <li>实现原理:基于 Xposed 实现的无侵入的运行时 AOP 框架</li> </ul> <p>该项目明确表示对 ART 虚拟机的不支持,对于 5.1 和 6.0 系统都没法支持,因此该项目基本没有实际应用到项目的意义,毕竟现在 5.0 以上的份额也挺大了。</p> </li> <li> <p><a href="https://github.com/alibaba/AndFix">AndFix</a></p> <ul> <li>作者:阿里技术团队</li> <li>修复粒度:方法级别</li> <li>实现原理:native hook 方式</li> <li>优点:运行时即可修复,修复及时</li> <li>缺点: <ul> <li>只能修复方法,无法新加类和字段</li> <li>对部分机型不支持</li> <li>方法的参数类型有限制</li> <li>打补丁限制较多,以上的限制在打补丁时均需要注意</li> </ul> </li> </ul> <p>目前阿里百川公测的 <a href="https://hotfix.taobao.com/hotfix/index.htm">阿里百川-HotFix</a> 服务应该就是基于 AndFix 技术,具体的使用细节可以看这篇 <a href="https://baichuan.taobao.com/docs/doc.htm?spm=a3c0d.7629140.0.0.dzpp9X&amp;treeId=234&amp;articleId=105457&amp;docType=1">阿里百川 HotFix Android 接入说明</a> ,可以看到其具体的限制基本和 AndFix 项目类似:</p> <blockquote> <p>4.4 HotFix 的使用中不被允许的情况</p> <ul> <li>暂时不支持新增方法、新增类</li> <li>不支持新增 Field</li> <li>不支持针对同一个方法的多次 patch,如果客户端已经有一个 patch 包在运行,则下一个 patch 不会立即生效。</li> <li>三星 note3、S4、S5 的 5.0 设备以及 X8 6设备不支持(<a href="http://baichuan.taobao.com/docs/doc.htm?spm=a3c0d.7629140.0.0.8K3Zr9&amp;treeId=234&amp;articleId=105460&amp;docType=1#s1">点击查看</a>具体支持的机型)</li> <li>参数包括:long、double、float 的方法不能被 patch</li> <li>被反射调用的方法不能被 patch</li> <li>使用 Annotation 的类不能 patch</li> <li>参数超过 8 的方法不能被 patch</li> <li>泛型参数的方法如果 patch 存在兼容性问题</li> </ul> </blockquote> </li> <li> <p><a href="https://github.com/jasonross/Nuwa">Nuwa</a></p> <ul> <li>作者: <a href="https://github.com/jasonross">Jason Ross</a></li> <li>修复粒度:类级别</li> <li>实现原理:ClassLoader 方式</li> <li>优点:兼容性较好,补丁限制较少,类级别的可以增减少字段,补丁自动化做的很完整</li> <li>缺点: <ul> <li>需要在应用重启后才能应用补丁,实现修复</li> <li>需要在每个类默认构造方法插入一段代码,防止类打上 <strong>CLASS_ISPREVERIFIED</strong> 标志,对运行效率有影响</li> <li>目前 issue 中反馈的兼容性问题较多,源码中确实未对各个 Android 版本做差异化处理,存在兼容性问题</li> <li>作者已经停止维护</li> </ul> </li> </ul> <p>该项目在去年刚出现时应该算比较火热,但是由于存在的兼容性问题,让作者也渐渐放弃了该项目,目前来说将该方案应用到项目中是有一定风险的。</p> </li> <li> <p><a href="https://github.com/dodola/HotFix">HotFix</a></p> <ul> <li>作者:<a href="https://github.com/dodola">dodola</a></li> <li>修复粒度:类级别</li> <li>实现原理:ClassLoader 方式</li> </ul> <p>基于 ClassLoader 方式实现,实际使用存在兼容问题,基本类似 Nuwa ,作者已弃坑,新开项目 RocooFix,该项目停止维护。</p> </li> <li> <p><a href="https://github.com/dodola/RocooFix">RocooFix</a></p> <ul> <li>作者:<a href="https://github.com/dodola">dodola</a></li> <li>修复粒度:类级别</li> <li>实现原理:ClassLoader 方式</li> <li>优点: <ul> <li>兼容性较好,源码中对各 Android 进行了差异化处理,一定程度上解决了兼容性问题</li> <li>实现了两种修复方式:静态修复和动态修复,分别是需要重启修复和无需重启即可修复</li> <li>简化了补丁制作流程</li> </ul> </li> <li>缺点: <ul> <li>需要在每个类默认构造方法插入一段代码(也叫做插桩),防止类打上 <strong>CLASS_ISPREVERIFIED</strong> 标志,对运行效率有影响</li> <li>目前就项目下的 issue 来看,还是会存在兼容性问题,对于采用了 APT 技术的项目也存在一些问题</li> <li>动态修复方式还有待检验,使用的是 <a href="https://github.com/asLody/legend">Legend</a> 项目中的相关技术</li> </ul> </li> </ul> <p>总体来说,该开源方案应该是算比较完整的解决方案,作者目前还在维护,对各个 Android 版本的兼容性也做了不少工作,期待作者的后续更新。</p> </li> <li> <p><a href="https://github.com/zzz40500/Tinker_imitator">Tinker_imitator</a></p> <ul> <li>作者:<a href="https://github.com/zzz40500">zzz40500</a></li> <li>修复粒度:dex 级别</li> <li>实现原理:dex 文件全量替换</li> <li>优点:基于 dex 文件全量替换的实现原理相对于 ClassLoader 方式,在性能上有很大优势</li> <li>缺点: <ul> <li>该方案虽然类似微信提出的热修复解决方案,但是 patch.dex 文件的生成并不是依赖于 DexDiff 算法,而是基于 bsdiff ,所以并不是完整实现了微信提出的方案</li> <li>需要重启应用,下次启动时生效</li> <li>生成新的 dex 文件时内存占用较大</li> </ul> </li> </ul> <p>总体来说,该方案目前还停留在 demo 状态,感觉离实际应用到项目中还需要一段时间,基于 dex 文件全量替换的方式我们更多还是期待微信团队的开源。</p> </li> </ul> <h3 id="对比总结">对比总结</h3> <p>就热修复实现的基本原理而言,目前较为成熟的也就 <strong>native hook 方式</strong> 和 <strong>ClassLoader 方式</strong>,在这两个基本原理上实现的开源方案中,AndFix 和 RocooFix 较为成熟,相关的打补丁配套解决方案也比较完备。</p> <p>如果你选择 AndFix 方案,比较倾向于推荐使用阿里百川的 <a href="https://hotfix.taobao.com/hotfix/index.htm">HotFix</a> 服务,希望该服务在公测之后有一个比较完整的服务方案给出,提供一个保证质量的服务。</p> <p>如果你选择 RocooFix 方案,你可能需要跟进作者的更新,及时反馈相关的问题,帮助作者来完善该项目,使得其在兼容性更加提升一步,同时在配套的生成补丁和下发补丁等方案也保证简单可使用。</p> <p>你也可以选择等待微信团队开源 Tinker 项目,毕竟鹅厂这套解决方案看起来很不错,在其实际应用到微信项目的基础上,开源出完整的解决方案,必将是一件有利于开发者的好事。</p> <p>感谢各大公司的技术团队和开源作者们的工作,正是他们让热修复得以实现,虽然各大解决方案都不是那么完美,但是已经有很大改进了,我们期待着越来越多的公司和开发者能够加入到这一工作中来,让热修复不再 “烫” 手。</p> <h3 id="参考文章">参考文章</h3> <ul> <li><a href="http://bugly.qq.com/bbs/forum.php?mod=viewthread&amp;tid=1264">微信Android热补丁实践演进之路</a></li> <li><a href="http://blog.zhaiyifan.cn/2015/11/20/HotPatchCompare/">各大热补丁方案分析和比较</a></li> <li><a href="http://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&amp;mid=400118620&amp;idx=1&amp;sn=b4fdd5055731290eef12ad0d17f39d4a&amp;scene=0">安卓App热补丁动态修复技术介绍</a></li> </ul> Sun, 28 Aug 2016 00:00:00 +0000 http://jaeger.itscoder.com//android/2016/08/28/android-hot-fix.html http://jaeger.itscoder.com//android/2016/08/28/android-hot-fix.html 热修复入门:Android 中的 ClassLoader <blockquote> <ul> <li>文章来源:itsCoder 的 <a href="https://github.com/itsCoder/weeklyblog">WeeklyBolg</a> 项目</li> <li>itsCoder 主页:<a href="http://itscoder.com/">http://itscoder.com/</a></li> <li>作者:<a href="https://github.com/laobie">Jaeger</a></li> <li>审阅者:<a href="https://github.com/Zheaoli">Zheaoli</a>, <a href="https://github.com/xcc3641">xcc3641</a></li> </ul> </blockquote> <p>从去年下半年开始,热修复技术在 Android 技术社区热了一阵子,这种不用发布新版本就可以修复线上 bug 的技术确实有很大的需求,最近正好在研究一些开源的热修复方案,本文就其中常用的 ClassLoader 方式实现的热修复方案中的 ClassLoader 机制作一个简单的介绍。</p> <h3 id="classloader-简介">ClassLoader 简介</h3> <blockquote> <p>对于 Java 程序来说,编写程序就是编写类,运行程序也就是运行类(编译得到的 class 文件),其中起到关键作用的就是类加载器 ClassLoader。</p> </blockquote> <p>任何一个 Java 程序都是由若干个 class 文件组成的一个完整的 Java 程序,在程序运行时,需要将 class 文件加载到 JVM 中才可以使用,负责加载这些 class 文件的就是 Java 的类加载(ClassLoader)机制。</p> <p><img src="http://ac-qygvx1cc.clouddn.com/78e71017bdd24420.jpeg" alt="" /></p> <p>因此 ClassLoader 的作用简单来说就是加载 class 文件,提供给程序运行时使用。</p> <h4 id="classloader-的双亲委托模型parentdelegation-model">ClassLoader 的双亲委托模型(Parent <em>Delegation Model</em> )</h4> <p>先来看 jdk 中的 ClassLoader 类的构造方法,其需要传入一个父类加载器,并持有该引用。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protected</span> <span class="nf">ClassLoader</span><span class="o">(</span><span class="n">ClassLoader</span> <span class="n">parent</span><span class="o">)</span> <span class="o">{</span> <span class="k">this</span><span class="o">(</span><span class="n">checkCreateClassLoader</span><span class="o">(),</span> <span class="n">parent</span><span class="o">);</span> <span class="o">}</span> </code></pre></div></div> <p>当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程,具体的加载过程如下:</p> <ol> <li>源 ClassLoader 先判断该 Class 是否已加载,如果已加载,则直接返回 Class,如果没有则委托给父类加载器。</li> <li>父类加载器判断是否加载过该 Class,如果已加载,则直接返回 Class,如果没有则委托给祖父类加载器。</li> <li>依此类推,直到始祖类加载器(引用类加载器)。</li> <li>始祖类加载器判断是否加载过该 Class,如果已加载,则直接返回 Class,如果没有则尝试从其对应的类路径下寻找 class 字节码文件并载入。如果载入成功,则直接返回 Class,如果载入失败,则委托给始祖类加载器的子类加载器。</li> <li>始祖类加载器的子类加载器尝试从其对应的类路径下寻找 class 字节码文件并载入。如果载入成功,则直接返回 Class,如果载入失败,则委托给始祖类加载器的孙类加载器。</li> <li>依此类推,直到源 ClassLoader。</li> <li>源 ClassLoader 尝试从其对应的类路径下寻找 class 字节码文件并载入。如果载入成功,则直接返回 Class,如果载入失败,源 ClassLoader 不会再委托其子类加载器,而是抛出异常。</li> </ol> <p>如果需要详细了解 ClassLoader 的信息,可以借助以下文章深入了解:</p> <ul> <li><a href="https://segmentfault.com/a/1190000002579346">JVM 的工作原理,层次结构以及 GC 工作原理</a></li> <li><a href="http://blog.csdn.net/xyang81/article/details/7292380">深入分析Java ClassLoader原理</a></li> <li><a href="http://blog.csdn.net/zhangzeyuaaa/article/details/42499839">类加载机制:全盘负责和双亲委托</a></li> </ul> <h3 id="android-中的-classloader">Android 中的 ClassLoader</h3> <p>Android 的 Dalvik/ART 虚拟机如同标准 Java 的 JVM 虚拟机一样,也是同样需要加载 class 文件到内存中来使用,但是在 ClassLoader 的加载细节上会有略微的差别。</p> <h4 id="android-中的-dex-文件">Android 中的 dex 文件</h4> <p>Android 应用打包成 apk 文件时,class 文件会被打包成一个或者多个 dex 文件。将一个 apk 文件后缀改成 .zip 格式解压后(也可以直接解压,apk 文件本质是个 zip 文件),里面就有 class.dex 文件,由于 Android 的 65K 问题(不要纠结是 64K 还是 65K),使用 MultiDex 就会生成多个 dex 文件。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/3c5e66e9e048d343.jpg" alt="" /></p> <p>当 Android 系统安装一个应用的时候,会针对不同平台对 Dex 进行优化,这个过程由一个专门的工具来处理,叫 DexOpt 。DexOpt 是在第一次加载 Dex 文件的时候执行的,该过程会生成一个 ODEX 文件,即 Optimised Dex。执行 ODEX 的效率会比直接执行 Dex 文件的效率要高很多,加快 App 的启动和响应。</p> <p>ODEX 相关的细节可以阅读以下文章扩展:</p> <ul> <li><a href="http://www.mywiki.cn/hovercool/index.php/ART%E5%92%8CDalvik">ART 和 Dalvik</a></li> <li><a href="http://www.jianshu.com/p/242abfb7eb7f">ODEX格式及生成过程</a></li> <li><a href="http://stackoverflow.com/questions/9593527/what-are-odex-files-in-android">What are ODEX files in Android</a></li> </ul> <blockquote> <p>注:本人的 5.0 机器 ODEX 优化后的文件是在 <code class="highlighter-rouge">/data/dalvilk-cache</code> 文件夹下的,6.0 机器该文件夹下只有 framework 和部分内置的 App 的优化后的 dex 文件,查找相关资料后没有找到明确的说法,目前猜测和 ROM 有关系,后续再深究下这个问题。</p> </blockquote> <p><img src="http://ac-QYgvX1CC.clouddn.com/b79b994f71a47130.png" alt="" /></p> <p>总之,Android 中的 Dalvik/ART 无法像 JVM 那样 <strong>直接</strong> 加载 class 文件和 jar 文件中的 class,需要通过 dx 工具来优化转换成 Dalvik byte code 才行,只能通过 dex 或者 包含 dex 的jar、apk 文件来加载(注意 odex 文件后缀可能是 .dex 或 .odex,也属于 dex 文件),因此 Android 中的 ClassLoader 工作就交给了 BaseDexClassLoader 来处理。</p> <blockquote> <p>注:如果 jar 文件包含有 dex 文件,此时 jar 文件也是可以用来加载的,不过实际加载的还是其中的 dex 文件,不要弄混淆了。</p> </blockquote> <h4 id="basedexclassloader-及其子类">BaseDexClassLoader 及其子类</h4> <p>在 Android 开发者官网上的 <a href="https://developer.android.com/reference/java/lang/ClassLoader.html">ClassLoader</a> 的文档说明中我们可以看到,ClassLoader 是个抽象类,其具体实现的子类有 <code class="highlighter-rouge">BaseDexClassLoader</code> 和 <code class="highlighter-rouge">SecureClassLoader</code> 。</p> <p>SecureClassLoader 的子类是 <code class="highlighter-rouge">URLClassLoader</code> ,其只能用来加载 jar 文件,这在 Android 的 Dalvik/ART 上没法使用的。</p> <p>BaseDexClassLoader 的子类是 <code class="highlighter-rouge">PathClassLoader</code> 和 <code class="highlighter-rouge">DexClassLoader</code> 。</p> <h5 id="pathclassloader">PathClassLoader</h5> <p>PathClassLoader 在应用启动时创建,从 data/app/… 安装目录下加载 apk 文件。</p> <p>其有 2 个构造函数,如下所示,这里遵从之前提到的双亲委托模型:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">PathClassLoader</span><span class="o">(</span><span class="n">String</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">ClassLoader</span> <span class="n">parent</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">(</span><span class="n">dexPath</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">parent</span><span class="o">);</span> <span class="o">}</span> <span class="kd">public</span> <span class="nf">PathClassLoader</span><span class="o">(</span><span class="n">String</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">String</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">ClassLoader</span> <span class="n">parent</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">(</span><span class="n">dexPath</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">parent</span><span class="o">);</span> <span class="o">}</span> </code></pre></div></div> <ul> <li><code class="highlighter-rouge">dexPath</code> : 包含 dex 的 jar 文件或 apk 文件的路径集,多个以文件分隔符分隔,默认是“:”</li> <li><code class="highlighter-rouge">libraryPath</code> : 包含 C/C++ 库的路径集,多个同样以文件分隔符分隔,可以为空</li> </ul> <p>PathClassLoader 里面除了这 2 个构造方法以外就没有其他的代码了,具体的实现都是在 BaseDexClassLoader 里面,其 dexPath 比较受限制,一般是已经安装应用的 apk 文件路径。</p> <p>在 Android 中,App 安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的。</p> <p>我们可以新建一个项目来验证下,在 MainActivity 中添加如下代码:</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="n">AppCompatActivity</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="n">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="n">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">ClassLoader</span> <span class="n">loader</span> <span class="o">=</span> <span class="n">MainActivity</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getClassLoader</span><span class="o">();</span> <span class="k">while</span> <span class="o">(</span><span class="n">loader</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">loader</span><span class="o">.</span><span class="na">toString</span><span class="o">());</span> <span class="n">loader</span> <span class="o">=</span> <span class="n">loader</span><span class="o">.</span><span class="na">getParent</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>输出结果是:</p> <div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> I/System.out: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.jaeger.testclassloader-2/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]] I/System.out: java.lang.BootClassLoader@1d9c6226 </code></pre></div></div> <p><code class="highlighter-rouge">/data/app/com.jaeger.testclassloader-2/base.apk</code> 就是示例应用安装在手机上的位置。</p> <p>BootClassLoader 是 PathClassLoader 的父加载器,其在系统启动时创建,在 App 启动时会将该对象传进来,具体的调用在 <code class="highlighter-rouge">com.android.internal.os.ZygoteInit</code> 的 <code class="highlighter-rouge">main()</code> 方法中调用了 <code class="highlighter-rouge">preload()</code> , 然后调用 <code class="highlighter-rouge">preloadClasses()</code> 方法,在该方法内部调用了 Class 的 <code class="highlighter-rouge">forName()</code> 方法:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Class</span><span class="o">.</span><span class="na">forName</span><span class="o">(</span><span class="n">line</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span> </code></pre></div></div> <p><code class="highlighter-rouge">forName()</code> 方法源码如下,方法内部获取到 BootClassLoader 实例:</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="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">forName</span><span class="o">(</span><span class="n">String</span> <span class="n">className</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">shouldInitialize</span><span class="o">,</span> <span class="n">ClassLoader</span> <span class="n">classLoader</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">ClassNotFoundException</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">classLoader</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">classLoader</span> <span class="o">=</span> <span class="n">BootClassLoader</span><span class="o">.</span><span class="na">getInstance</span><span class="o">();</span> <span class="o">}</span> <span class="c1">// Catch an Exception thrown by the underlying native code. It wraps</span> <span class="c1">// up everything inside a ClassNotFoundException, even if e.g. an</span> <span class="c1">// Error occurred during initialization. This as a workaround for</span> <span class="c1">// an ExceptionInInitializerError that's also wrapped. It is actually</span> <span class="c1">// expected to be thrown. Maybe the same goes for other errors.</span> <span class="c1">// Not wrapping up all the errors will break android though.</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">result</span><span class="o">;</span> <span class="k">try</span> <span class="o">{</span> <span class="n">result</span> <span class="o">=</span> <span class="n">classForName</span><span class="o">(</span><span class="n">className</span><span class="o">,</span> <span class="n">shouldInitialize</span><span class="o">,</span> <span class="n">classLoader</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">ClassNotFoundException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">Throwable</span> <span class="n">cause</span> <span class="o">=</span> <span class="n">e</span><span class="o">.</span><span class="na">getCause</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">cause</span> <span class="k">instanceof</span> <span class="n">LinkageError</span><span class="o">)</span> <span class="o">{</span> <span class="k">throw</span> <span class="o">(</span><span class="n">LinkageError</span><span class="o">)</span> <span class="n">cause</span><span class="o">;</span> <span class="o">}</span> <span class="k">throw</span> <span class="n">e</span><span class="o">;</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>而 PathClassLoader 的实例化又是在哪进行的呢?在源码中寻找下其构造方法调用的地方,结果如下:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/a0a44b8cac607cae.jpg" alt="" /></p> <p>其中:</p> <ul> <li> <p>在 ZygoteInit 中的调用是用来启动相关的系统服务</p> </li> <li> <p>在 ApplicationLoaders 中用来加载系统安装过的 apk,用来加载 apk 内的 class ,其调用是在 LoadApk 类中的 <code class="highlighter-rouge">getClassLoader()</code> 方法中调用的,得到的就是 PathClassLoader:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mClassLoader</span> <span class="o">=</span> <span class="n">ApplicationLoaders</span><span class="o">.</span><span class="na">getDefault</span><span class="o">().</span><span class="na">getClassLoader</span><span class="o">(</span><span class="n">zip</span><span class="o">,</span> <span class="n">lib</span><span class="o">,</span> <span class="n">mBaseClassLoader</span><span class="o">);</span> </code></pre></div> </div> </li> </ul> <h5 id="dexclassloader">DexClassLoader</h5> <p>介绍 DexClassLoader 之前,先来看看其官方描述:</p> <blockquote> <p>A class loader that loads classes from .jar and .apk filescontaining a classes.dex entry. This can be used to execute code notinstalled as part of an application.</p> </blockquote> <p>很明显,对比 PathClassLoader 只能加载已经安装应用的 dex 或 apk 文件,DexClassLoader 则没有此限制,可以从 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。</p> <p>DexClassLoader 的源码里面只有一个构造方法,这里也是遵从双亲委托模型:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">DexClassLoader</span><span class="o">(</span><span class="n">String</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">String</span> <span class="n">optimizedDirectory</span><span class="o">,</span> <span class="n">String</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">ClassLoader</span> <span class="n">parent</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">(</span><span class="n">dexPath</span><span class="o">,</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">optimizedDirectory</span><span class="o">),</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">parent</span><span class="o">);</span> <span class="o">}</span> </code></pre></div></div> <p>参数说明:</p> <ul> <li> <p><code class="highlighter-rouge">String dexPath</code> : 包含 class.dex 的 apk、jar 文件路径 ,多个用文件分隔符(默认是 :)分隔</p> </li> <li> <p><code class="highlighter-rouge">String optimizedDirectory</code> : 用来缓存优化的 dex 文件的路径,即从 apk 或 jar 文件中提取出来的 dex 文件。该路径不可以为空,且应该是应用私有的,有读写权限的路径(实际上也可以使用外部存储空间,但是这样的话就存在代码注入的风险),可以通过以下方式来创建一个这样的路径:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">File</span> <span class="n">dexOutputDir</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">getCodeCacheDir</span><span class="o">();</span> </code></pre></div> </div> <blockquote> <p>注:后续发现,getCodeCacheDir() 方法只能在 API 21 以上可以使用。</p> </blockquote> </li> <li> <p><code class="highlighter-rouge">String libraryPath</code> : 存储 C/C++ 库文件的路径集</p> </li> <li> <p><code class="highlighter-rouge">ClassLoader parent </code>: 父类加载器,遵从双亲委托模型</p> </li> </ul> <p>简单介绍了 PathClassLoader 和 DexClassLoader,但这两者都是对 BaseDexClassLoader 的一层简单封装,真正的实现都在 BaseClassLoader 内。</p> <h5 id="baseclassloader-源码分析">BaseClassLoader 源码分析</h5> <p>先来看一眼 BaseClassLoader 的结构:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/a6f9824c199cf304.jpg" alt="" /></p> <p>其中有个重要的字段 <code class="highlighter-rouge">private final DexPathList pathList</code> ,其继承 ClassLoader 实现的 <code class="highlighter-rouge">findClass()</code> 、<code class="highlighter-rouge">findResource()</code> 均是基于 pathList 来实现的(省略了部分源码):</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nd">@Override</span> <span class="kd">protected</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">findClass</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">ClassNotFoundException</span> <span class="o">{</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Throwable</span><span class="o">&gt;</span> <span class="n">suppressedExceptions</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ArrayList</span><span class="o">&lt;</span><span class="n">Throwable</span><span class="o">&gt;();</span> <span class="n">Class</span> <span class="n">c</span> <span class="o">=</span> <span class="n">pathList</span><span class="o">.</span><span class="na">findClass</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">suppressedExceptions</span><span class="o">);</span> <span class="o">...</span> <span class="k">return</span> <span class="n">c</span><span class="o">;</span> <span class="o">}</span> <span class="nd">@Override</span> <span class="kd">protected</span> <span class="n">URL</span> <span class="nf">findResource</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">pathList</span><span class="o">.</span><span class="na">findResource</span><span class="o">(</span><span class="n">name</span><span class="o">);</span> <span class="o">}</span> <span class="nd">@Override</span> <span class="kd">protected</span> <span class="n">Enumeration</span><span class="o">&lt;</span><span class="n">URL</span><span class="o">&gt;</span> <span class="nf">findResources</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">pathList</span><span class="o">.</span><span class="na">findResources</span><span class="o">(</span><span class="n">name</span><span class="o">);</span> <span class="o">}</span> <span class="nd">@Override</span> <span class="kd">public</span> <span class="n">String</span> <span class="nf">findLibrary</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">pathList</span><span class="o">.</span><span class="na">findLibrary</span><span class="o">(</span><span class="n">name</span><span class="o">);</span> <span class="o">}</span> </code></pre></div></div> <p>那么重要的部分则是在 DexPathList 类的内部了,DexPathList 的构造方法也较为简单,和之前介绍的类似:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">DexPathList</span><span class="o">(</span><span class="n">ClassLoader</span> <span class="n">definingContext</span><span class="o">,</span> <span class="n">String</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">String</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">File</span> <span class="n">optimizedDirectory</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> </code></pre></div></div> <p>接受之前传进来的包含 dex 的 apk/jar/dex 的路径集、native 库的路径集和缓存优化的 dex 文件的路径,然后调用 <code class="highlighter-rouge">makePathElements()</code> 方法生成一个 <code class="highlighter-rouge">Element[] dexElements</code> 数组,Element 是 DexPathList 的一个嵌套类,其有以下字段:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">static</span> <span class="kd">class</span> <span class="nc">Element</span> <span class="o">{</span> <span class="kd">private</span> <span class="kd">final</span> <span class="n">File</span> <span class="n">dir</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">final</span> <span class="kt">boolean</span> <span class="n">isDirectory</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">final</span> <span class="n">File</span> <span class="n">zip</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">final</span> <span class="n">DexFile</span> <span class="n">dexFile</span><span class="o">;</span> <span class="kd">private</span> <span class="n">ZipFile</span> <span class="n">zipFile</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">initialized</span><span class="o">;</span> <span class="o">}</span> </code></pre></div></div> <p><code class="highlighter-rouge">makePathElements() </code> 是如何生成 Element 数组的?继续看源码:</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="n">Element</span><span class="o">[]</span> <span class="nf">makePathElements</span><span class="o">(</span><span class="n">List</span><span class="o">&lt;</span><span class="n">File</span><span class="o">&gt;</span> <span class="n">files</span><span class="o">,</span> <span class="n">File</span> <span class="n">optimizedDirectory</span><span class="o">,</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">IOException</span><span class="o">&gt;</span> <span class="n">suppressedExceptions</span><span class="o">)</span> <span class="o">{</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Element</span><span class="o">&gt;</span> <span class="n">elements</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ArrayList</span><span class="o">&lt;&gt;();</span> <span class="c1">// 遍历所有的包含 dex 的文件</span> <span class="k">for</span> <span class="o">(</span><span class="n">File</span> <span class="n">file</span> <span class="o">:</span> <span class="n">files</span><span class="o">)</span> <span class="o">{</span> <span class="n">File</span> <span class="n">zip</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="n">File</span> <span class="n">dir</span> <span class="o">=</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="s">""</span><span class="o">);</span> <span class="n">DexFile</span> <span class="n">dex</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="n">String</span> <span class="n">path</span> <span class="o">=</span> <span class="n">file</span><span class="o">.</span><span class="na">getPath</span><span class="o">();</span> <span class="n">String</span> <span class="n">name</span> <span class="o">=</span> <span class="n">file</span><span class="o">.</span><span class="na">getName</span><span class="o">();</span> <span class="c1">// 判断是不是 zip 类型</span> <span class="k">if</span> <span class="o">(</span><span class="n">path</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">zipSeparator</span><span class="o">))</span> <span class="o">{</span> <span class="n">String</span> <span class="n">split</span><span class="o">[]</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="na">split</span><span class="o">(</span><span class="n">zipSeparator</span><span class="o">,</span> <span class="mi">2</span><span class="o">);</span> <span class="n">zip</span> <span class="o">=</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">split</span><span class="o">[</span><span class="mi">0</span><span class="o">]);</span> <span class="n">dir</span> <span class="o">=</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">split</span><span class="o">[</span><span class="mi">1</span><span class="o">]);</span> <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">file</span><span class="o">.</span><span class="na">isDirectory</span><span class="o">())</span> <span class="o">{</span> <span class="c1">// 如果是文件夹,则直接添加 Element,这个一般是用来处理 native 库和资源文件</span> <span class="n">elements</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="n">Element</span><span class="o">(</span><span class="n">file</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">));</span> <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">file</span><span class="o">.</span><span class="na">isFile</span><span class="o">())</span> <span class="o">{</span> <span class="c1">// 直接是 .dex 文件,而不是 zip/jar 文件(apk 归为 zip),则直接加载 dex 文件</span> <span class="k">if</span> <span class="o">(</span><span class="n">name</span><span class="o">.</span><span class="na">endsWith</span><span class="o">(</span><span class="n">DEX_SUFFIX</span><span class="o">))</span> <span class="o">{</span> <span class="k">try</span> <span class="o">{</span> <span class="n">dex</span> <span class="o">=</span> <span class="n">loadDexFile</span><span class="o">(</span><span class="n">file</span><span class="o">,</span> <span class="n">optimizedDirectory</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IOException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span> <span class="n">System</span><span class="o">.</span><span class="na">logE</span><span class="o">(</span><span class="s">"Unable to load dex file: "</span> <span class="o">+</span> <span class="n">file</span><span class="o">,</span> <span class="n">ex</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="c1">// 如果是 zip/jar 文件(apk 归为 zip),则将 file 值赋给 zip 字段,再加载 dex 文件</span> <span class="n">zip</span> <span class="o">=</span> <span class="n">file</span><span class="o">;</span> <span class="k">try</span> <span class="o">{</span> <span class="n">dex</span> <span class="o">=</span> <span class="n">loadDexFile</span><span class="o">(</span><span class="n">file</span><span class="o">,</span> <span class="n">optimizedDirectory</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IOException</span> <span class="n">suppressed</span><span class="o">)</span> <span class="o">{</span> <span class="n">suppressedExceptions</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">suppressed</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">System</span><span class="o">.</span><span class="na">logW</span><span class="o">(</span><span class="s">"ClassLoader referenced unknown path: "</span> <span class="o">+</span> <span class="n">file</span><span class="o">);</span> <span class="o">}</span> <span class="k">if</span> <span class="o">((</span><span class="n">zip</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">||</span> <span class="o">(</span><span class="n">dex</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">))</span> <span class="o">{</span> <span class="n">elements</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="n">Element</span><span class="o">(</span><span class="n">dir</span><span class="o">,</span> <span class="kc">false</span><span class="o">,</span> <span class="n">zip</span><span class="o">,</span> <span class="n">dex</span><span class="o">));</span> <span class="o">}</span> <span class="o">}</span> <span class="c1">// list 转为数组</span> <span class="k">return</span> <span class="n">elements</span><span class="o">.</span><span class="na">toArray</span><span class="o">(</span><span class="k">new</span> <span class="n">Element</span><span class="o">[</span><span class="n">elements</span><span class="o">.</span><span class="na">size</span><span class="o">()]);</span> <span class="o">}</span> </code></pre></div></div> <p><code class="highlighter-rouge">loadDexFile()</code> 方法最终会调用 JNI 层的方法来读取 dex 文件,这里不再深入探究,有兴趣的可以阅读 <a href="http://blog.csdn.net/nanzhiwen666/article/details/50515895">从源码分析 Android dexClassLoader 加载机制原理</a> 这篇文章深入了解。</p> <p>接下来看以下 DexPathList 的 <code class="highlighter-rouge">findClass()</code> 方法,其根据传入的完整的类名来加载对应的 class,源码如下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="n">Class</span> <span class="nf">findClass</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">,</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Throwable</span><span class="o">&gt;</span> <span class="n">suppressed</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// 遍历 dexElements 数组,依次寻找对应的 class,一旦找到就终止遍历</span> <span class="k">for</span> <span class="o">(</span><span class="n">Element</span> <span class="n">element</span> <span class="o">:</span> <span class="n">dexElements</span><span class="o">)</span> <span class="o">{</span> <span class="n">DexFile</span> <span class="n">dex</span> <span class="o">=</span> <span class="n">element</span><span class="o">.</span><span class="na">dexFile</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">dex</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">Class</span> <span class="n">clazz</span> <span class="o">=</span> <span class="n">dex</span><span class="o">.</span><span class="na">loadClassBinaryName</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">definingContext</span><span class="o">,</span> <span class="n">suppressed</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">clazz</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">clazz</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> <span class="c1">// 抛出异常</span> <span class="k">if</span> <span class="o">(</span><span class="n">dexElementsSuppressedExceptions</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">suppressed</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="n">dexElementsSuppressedExceptions</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>这里有关于热修复实现的一个点,就是将补丁 dex 文件放到 dexElements 数组前面,这样在加载 class 时,优先找到补丁包中的 dex 文件,加载到 class 之后就不再寻找,从而原来的 apk 文件中同名的类就不会再使用,从而达到修复的目的,虽然说起来较为简单,但是实现起来还有很多细节需要注意,本文先热身,后期再分析具体实现。</p> <p>至此,BaseDexClassLader 寻找 class 的路线就清晰了:</p> <ol> <li>当传入一个完整的类名,调用 BaseDexClassLader 的 <code class="highlighter-rouge">findClass(String name) </code> 方法</li> <li>BaseDexClassLader 的 findClass 方法会交给 DexPathList 的 <code class="highlighter-rouge">findClass(String name, List&lt;Throwable&gt; suppressed </code> 方法处理</li> <li>在 DexPathList 方法的内部,会遍历 dexFile ,通过 DexFile 的 <code class="highlighter-rouge">dex.loadClassBinaryName(name, definingContext, suppressed)</code> 来完成类的加载</li> </ol> <h5 id="实际使用">实际使用</h5> <p>需要注意到的是,在项目中使用 BaseDexClassLoader 或者 DexClassLoader 去加载某个 dex 或者 apk 中的 class 时,是无法调用 <code class="highlighter-rouge">findClass()</code> 方法的,因为该方法是包访问权限,你需要调用 <code class="highlighter-rouge">loadClass(String className)</code> ,该方法其实是 BaseDexClassLoader 的父类 ClassLoader 内实现的:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">loadClass</span><span class="o">(</span><span class="n">String</span> <span class="n">className</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">ClassNotFoundException</span> <span class="o">{</span> <span class="k">return</span> <span class="nf">loadClass</span><span class="o">(</span><span class="n">className</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span> <span class="o">}</span> <span class="kd">protected</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">loadClass</span><span class="o">(</span><span class="n">String</span> <span class="n">className</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">resolve</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">ClassNotFoundException</span> <span class="o">{</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">clazz</span> <span class="o">=</span> <span class="n">findLoadedClass</span><span class="o">(</span><span class="n">className</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">clazz</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">ClassNotFoundException</span> <span class="n">suppressed</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="k">try</span> <span class="o">{</span> <span class="n">clazz</span> <span class="o">=</span> <span class="n">parent</span><span class="o">.</span><span class="na">loadClass</span><span class="o">(</span><span class="n">className</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">ClassNotFoundException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">suppressed</span> <span class="o">=</span> <span class="n">e</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">clazz</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="k">try</span> <span class="o">{</span> <span class="n">clazz</span> <span class="o">=</span> <span class="n">findClass</span><span class="o">(</span><span class="n">className</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">ClassNotFoundException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">e</span><span class="o">.</span><span class="na">addSuppressed</span><span class="o">(</span><span class="n">suppressed</span><span class="o">);</span> <span class="k">throw</span> <span class="n">e</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> <span class="k">return</span> <span class="n">clazz</span><span class="o">;</span> <span class="o">}</span> </code></pre></div></div> <p>上面这段代码结合之前提到的双亲委托模型就很好理解了,先查找当前的 ClassLoader 是否已经加载过,如果没有就交给父 ClassLoader 去加载,如果父 ClassLoader 没有找到,才调用当前 ClassLoader 来加载,此时就是调用上面分析的 <code class="highlighter-rouge">findClass() </code> 方法了。</p> <h3 id="classloader-使用示例">ClassLoader 使用示例</h3> <p>上面说了这么多理论知识,只说不练假把式,接下来实战:从 SD 卡中动态加载一个包含 class.dex 的 jar 文件,加载其中的类,并调用其方法。</p> <ol> <li> <p>新建一个 Java 项目,包含两个文件:<code class="highlighter-rouge">ISayHello.java</code> 和 <code class="highlighter-rouge">HelloAndroid.java</code></p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kn">package</span> <span class="n">com</span><span class="o">.</span><span class="na">jaeger</span><span class="o">;</span> <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">ISayHello</span> <span class="o">{</span> <span class="n">String</span> <span class="nf">say</span><span class="o">();</span> <span class="o">}</span> </code></pre></div> </div> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kn">package</span> <span class="n">com</span><span class="o">.</span><span class="na">jaeger</span><span class="o">;</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">HelloAndroid</span> <span class="kd">implements</span> <span class="n">ISayHello</span> <span class="o">{</span> <span class="nd">@Override</span> <span class="kd">public</span> <span class="n">String</span> <span class="nf">say</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="s">"Hello Android"</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>导出 jar 包</p> <p>这一步使用 IntelliJ IDEA 导出有点问题,最终我是用 Eclipse 导出 jar 包的。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/88ede5c72013c55b.jpg" alt="" /></p> </li> <li> <p>使用 SDK 目录 &gt; platform-tools 里面的 dx 工具生成包含 class.dex 的 jar 包</p> <p>将上一步生成的 <code class="highlighter-rouge">sayhello.jar</code> 放到 你的 SDK 下的 platform-tools 文件夹下,使用下面的命令生成 dex 化的 jar 文件,其中是 output 后面的 <code class="highlighter-rouge">sayhello_dex.jar</code> 就是最终生成的 jar 包。</p> <div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dx --dex --output=sayhello_dex.jar sayhello.jar </code></pre></div> </div> <p>生成 <code class="highlighter-rouge">sayhello_dex.jar</code> 之后,用解压解压后就会发现其已经包含了 class.dex 文件了。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/ba0d600fc2a90e2d.jpg" alt="" /></p> </li> <li> <p>将 <code class="highlighter-rouge">sayhello_dex.jar</code> 文件拷贝到手机存储空间的根目录,不一定是内存卡。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/7efba4a5a816a8e1.png" alt="" /></p> </li> <li> <p>新建一个 Android 项目,在 MainActivity 中添加如下的代码:</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="n">AppCompatActivity</span> <span class="o">{</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">TAG</span> <span class="o">=</span> <span class="s">"TestClassLoader"</span><span class="o">;</span> <span class="kd">private</span> <span class="n">TextView</span> <span class="n">mTvInfo</span><span class="o">;</span> <span class="kd">private</span> <span class="n">Button</span> <span class="n">mBtnLoad</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="n">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="n">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">mTvInfo</span> <span class="o">=</span> <span class="o">(</span><span class="n">TextView</span><span class="o">)</span> <span class="n">findViewById</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">tv_info</span><span class="o">);</span> <span class="n">mBtnLoad</span> <span class="o">=</span> <span class="o">(</span><span class="n">Button</span><span class="o">)</span> <span class="n">findViewById</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">btn_load</span><span class="o">);</span> <span class="n">mBtnLoad</span><span class="o">.</span><span class="na">setOnClickListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnClickListener</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">onClick</span><span class="o">(</span><span class="n">View</span> <span class="n">view</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// 获取到包含 class.dex 的 jar 包文件</span> <span class="kd">final</span> <span class="n">File</span> <span class="n">jarFile</span> <span class="o">=</span> <span class="k">new</span> <span class="nf">File</span><span class="o">(</span><span class="n">Environment</span><span class="o">.</span><span class="na">getExternalStorageDirectory</span><span class="o">().</span><span class="na">getPath</span><span class="o">()</span> <span class="o">+</span> <span class="n">File</span><span class="o">.</span><span class="na">separator</span> <span class="o">+</span> <span class="s">"sayhello_dex.jar"</span><span class="o">);</span> <span class="c1">// 如果没有读权限,确定你在 AndroidManifest 中是否声明了读写权限</span> <span class="n">Log</span><span class="o">.</span><span class="na">d</span><span class="o">(</span><span class="n">TAG</span><span class="o">,</span> <span class="n">jarFile</span><span class="o">.</span><span class="na">canRead</span><span class="o">()</span> <span class="o">+</span> <span class="s">""</span><span class="o">);</span> <span class="k">if</span> <span class="o">(!</span><span class="n">jarFile</span><span class="o">.</span><span class="na">exists</span><span class="o">())</span> <span class="o">{</span> <span class="n">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="n">TAG</span><span class="o">,</span> <span class="s">"sayhello_dex.jar not exists"</span><span class="o">);</span> <span class="k">return</span><span class="o">;</span> <span class="o">}</span> <span class="c1">// getCodeCacheDir() 方法在 API 21 才能使用,实际测试替换成 getExternalCacheDir() 等也是可以的</span> <span class="c1">// 只要有读写权限的路径均可</span> <span class="n">DexClassLoader</span> <span class="n">dexClassLoader</span> <span class="o">=</span> <span class="k">new</span> <span class="nf">DexClassLoader</span><span class="o">(</span><span class="n">jarFile</span><span class="o">.</span><span class="na">getAbsolutePath</span><span class="o">(),</span> <span class="n">getExternalCacheDir</span><span class="o">().</span><span class="na">getAbsolutePath</span><span class="o">(),</span> <span class="kc">null</span><span class="o">,</span> <span class="n">getClassLoader</span><span class="o">());</span> <span class="k">try</span> <span class="o">{</span> <span class="c1">// 加载 HelloAndroid 类</span> <span class="n">Class</span> <span class="n">clazz</span> <span class="o">=</span> <span class="n">dexClassLoader</span><span class="o">.</span><span class="na">loadClass</span><span class="o">(</span><span class="s">"com.jaeger.HelloAndroid"</span><span class="o">);</span> <span class="c1">// 强转成 ISayHello, 注意 ISayHello 的包名需要和 jar 包中的一致</span> <span class="n">ISayHello</span> <span class="n">iSayHello</span> <span class="o">=</span> <span class="o">(</span><span class="n">ISayHello</span><span class="o">)</span> <span class="n">clazz</span><span class="o">.</span><span class="na">newInstance</span><span class="o">();</span> <span class="n">mTvInfo</span><span class="o">.</span><span class="na">setText</span><span class="o">(</span><span class="n">iSayHello</span><span class="o">.</span><span class="na">say</span><span class="o">());</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">ClassNotFoundException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">InstantiationException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IllegalAccessException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</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> </code></pre></div> </div> <p>同时需要新建一个和第一步创建的 Java 项目中包名一致的 <code class="highlighter-rouge">ISayHello</code> 接口:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="n">com</span><span class="o">.</span><span class="na">jaeger</span><span class="o">;</span> <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">ISayHello</span> <span class="o">{</span> <span class="n">String</span> <span class="nf">say</span><span class="o">();</span> <span class="o">}</span> </code></pre></div> </div> <p>这里需要注意几点:</p> <ul> <li>因为需要从存储空间中读取 jar 文件,需要在 AndroidManifest 中声明读写权限</li> <li>ISayHello 接口的包名必须一致</li> <li><code class="highlighter-rouge">getCodeCacheDir()</code> 方法在 API 21 才能使用,实际测试替换成 <code class="highlighter-rouge">getExternalCacheDir()</code> 等也是可以的</li> </ul> </li> <li> <p>接下来就是运行,运行的结果如图,和预期的一样,完美收工。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/4b94e8fbecf66b72.png" alt="" /></p> </li> <li> <p>示例代码以及 jar 包上传到 GitHub 了,请前往 <a href="https://github.com/laobie/TestClassLoader">这里</a> 去查看。</p> </li> </ol> <h3 id="总结">总结</h3> <p>顺着相关资料,从源头开始分析起,然后再进入源码,理清楚具体的运行机制,最终通过简单的示例验证了分析的结果。</p> <p>在这过程中,查阅了不少资料,也阅读了很多前辈的博文,受益匪浅,这也是现在技术圈内很好的氛围。同时在分析中也发现自己对 Android 底层了解相当薄弱,这也是今后需要多学习的地方。</p> <p>鉴于自己能力有限,如果本文中有遗漏或者错误的地方,请在评论区指出或者通过邮件等方式联系我,谢谢。</p> <h3 id="参考资料">参考资料</h3> <ul> <li> <p><a href="https://segmentfault.com/a/1190000002579346">JVM 的工作原理,层次结构以及 GC 工作原理</a></p> </li> <li> <p><a href="http://blog.csdn.net/xyang81/article/details/7292380">深入分析Java ClassLoader原理</a></p> </li> <li> <p><a href="https://segmentfault.com/a/1190000004062880">Android动态加载基础 ClassLoader工作机制</a></p> </li> <li> <p><a href="http://blog.zhaiyifan.cn/2015/11/20/HotPatchCompare/">各大热补丁方案分析和比较</a></p> </li> <li> <p><a href="https://github.com/kaedea/android-dynamical-loading">android-dynamical-loading</a></p> </li> <li> <p><a href="http://bugly.qq.com/bbs/forum.php?mod=viewthread&amp;tid=193">dex分包变形记</a></p> </li> <li> <p><a href="http://blog.csdn.net/mr_liabill/article/details/50497055">Android ClassLoader机制</a></p> </li> <li> <p><a href="http://weli.iteye.com/blog/1682625">彻底搞懂 Java ClassLoader</a></p> </li> <li> <p><a href="http://blog.csdn.net/nanzhiwen666/article/details/50515895">从源码分析 Android dexClassLoader 加载机制原理</a></p> </li> <li> <p><a href="http://www.iloveandroid.net/">码农故事</a></p> </li> </ul> Sat, 27 Aug 2016 00:00:00 +0000 http://jaeger.itscoder.com//android/2016/08/27/android-classloader.html http://jaeger.itscoder.com//android/2016/08/27/android-classloader.html StaticLayout 源码分析 <p>Android 中的文本布局和绘制都是由 Layout 类完成的,而 Layout 类一个重要的子类就是 SaticLayout 类,本文从源码来简单分析文本是如何布局的,具体如段落、折行处理以及省略方式的等等的处理。</p> <h4 id="前言">前言</h4> <p>Android 控件中,看起来最简单、最基础的 TextView 实际上是很复杂的,很多常见的控件都是其子类,例如 Botton、EditText、CheckBox 等,由于作为一个基础控件类,TextView 需要考虑到子类的各种使用场景,满足子类的需求。源码中,TextView 单个类源码就多达 1万行,而且其工作时还依赖很多辅助类。其文本的排版、折行处理,以及最终的显示,均是交给辅助类 Layout 类来处理的。</p> <p>由于 Canvas 本身提供的 drawText 绘制文本是不支持换行的,所以在文本需要换行显示时,就需要用到 Layout 类。我们可以看到官方对 Layout 类的描述:</p> <blockquote> <p>A base class that manages text layout in visual elements on the screen.</p> </blockquote> <p>一个用于管理屏幕上文本布局的基类。</p> <p>其直接子类有 StaticLayout、DynamicLayout、BoringLayout,在官方的文档中提到,如果文本内容会被编辑,应该使用 DynamicLayout,如果文本显示之后不会发生改变,应该使用 StaticLayout,而 BoringLayout 则使用场景极为有限:当你确保你的文本只有一行,且所有的字符均是从左到右显示的(某些语言的文字是从右到左显示的),你才可以使用 BoringLayout。</p> <p>本文将会简单地深入 StaticLayout 的源码,分析下具体是如何工作的。</p> <h4 id="概述">概述</h4> <p>先看 StaticLayout 类的注释:StaticLayout 是一个为不可编辑的文本布局的类,这意味着一旦布局完成,文本内容就不可以改变,如果需要改变的话,应该使用 DynamicLayout 来布局。同时你不应该直接使用 StaticLayout 类,除非你需要实现一个自定义的控件或者自定义显示对象,否则,你应该直接调用 <code class="highlighter-rouge">Canvas.drawText()</code>。因此,在正常的开发工作中,你接触 StaticLayout 的机会应该不多。</p> <p>在 TextView 初始化时,会通过 <code class="highlighter-rouge">makeNewLayout()</code> 方法,根据文本的特点,是否包含 Span,是否单行等,决定创建具体的 Layout 类型。在单纯地使用TextView来展示静态文本的时候,创建的就是 StaticLayout。StaticLayout 的初始化是通过内部类 <code class="highlighter-rouge">StaticLayout.Builder</code> 完成的,然后调用 <code class="highlighter-rouge">generate()</code> 方法完成段落、折行以及缩进之类的处理,在 <code class="highlighter-rouge">generate()</code> 方法中调用了 <code class="highlighter-rouge">out()</code> 方法,完成文本显示的行距、顶部底部留白、省略文本等的处理,这两个方法也是 StaticLayout 源码中两个主要的方法,完成了一系列的文本处理。在 TextView 的 <code class="highlighter-rouge">onDraw(Canvas canvas)</code> 方法中,调用父类 Layout 的 <code class="highlighter-rouge">draw()</code> 方法,改方法会依次调用 <code class="highlighter-rouge">drawBackground()</code> 和 <code class="highlighter-rouge">drawText()</code> 完成背景和文本的绘制。</p> <h4 id="构造方法">构造方法</h4> <p>StaticLayout 有多个构造方法,最完整的构造方法(其他构造方法最终也是调用的这个构造方法)如下所示:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">StaticLayout</span><span class="o">(</span><span class="n">CharSequence</span> <span class="n">source</span><span class="o">,</span> <span class="kt">int</span> <span class="n">bufstart</span><span class="o">,</span> <span class="kt">int</span> <span class="n">bufend</span><span class="o">,</span> <span class="n">TextPaint</span> <span class="n">paint</span><span class="o">,</span> <span class="kt">int</span> <span class="n">outerwidth</span><span class="o">,</span> <span class="n">Alignment</span> <span class="n">align</span><span class="o">,</span> <span class="n">TextDirectionHeuristic</span> <span class="n">textDir</span><span class="o">,</span> <span class="kt">float</span> <span class="n">spacingmult</span><span class="o">,</span> <span class="kt">float</span> <span class="n">spacingadd</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">includepad</span><span class="o">,</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">TruncateAt</span> <span class="n">ellipsize</span><span class="o">,</span> <span class="kt">int</span> <span class="n">ellipsizedWidth</span><span class="o">,</span> <span class="kt">int</span> <span class="n">maxLines</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">((</span><span class="n">ellipsize</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">?</span> <span class="n">source</span> <span class="o">:</span> <span class="o">(</span><span class="n">source</span> <span class="k">instanceof</span> <span class="n">Spanned</span><span class="o">)</span> <span class="o">?</span> <span class="k">new</span> <span class="n">SpannedEllipsizer</span><span class="o">(</span><span class="n">source</span><span class="o">)</span> <span class="o">:</span> <span class="k">new</span> <span class="n">Ellipsizer</span><span class="o">(</span><span class="n">source</span><span class="o">),</span> <span class="n">paint</span><span class="o">,</span> <span class="n">outerwidth</span><span class="o">,</span> <span class="n">align</span><span class="o">,</span> <span class="n">textDir</span><span class="o">,</span> <span class="n">spacingmult</span><span class="o">,</span> <span class="n">spacingadd</span><span class="o">);</span> <span class="n">Builder</span> <span class="n">b</span> <span class="o">=</span> <span class="n">Builder</span><span class="o">.</span><span class="na">obtain</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">bufstart</span><span class="o">,</span> <span class="n">bufend</span><span class="o">,</span> <span class="n">paint</span><span class="o">,</span> <span class="n">outerwidth</span><span class="o">)</span> <span class="o">.</span><span class="na">setAlignment</span><span class="o">(</span><span class="n">align</span><span class="o">)</span> <span class="o">.</span><span class="na">setTextDirection</span><span class="o">(</span><span class="n">textDir</span><span class="o">)</span> <span class="o">.</span><span class="na">setLineSpacing</span><span class="o">(</span><span class="n">spacingadd</span><span class="o">,</span> <span class="n">spacingmult</span><span class="o">)</span> <span class="o">.</span><span class="na">setIncludePad</span><span class="o">(</span><span class="n">includepad</span><span class="o">)</span> <span class="o">.</span><span class="na">setEllipsizedWidth</span><span class="o">(</span><span class="n">ellipsizedWidth</span><span class="o">)</span> <span class="o">.</span><span class="na">setEllipsize</span><span class="o">(</span><span class="n">ellipsize</span><span class="o">)</span> <span class="o">.</span><span class="na">setMaxLines</span><span class="o">(</span><span class="n">maxLines</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">ellipsize</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">Ellipsizer</span> <span class="n">e</span> <span class="o">=</span> <span class="o">(</span><span class="n">Ellipsizer</span><span class="o">)</span> <span class="n">getText</span><span class="o">();</span> <span class="n">e</span><span class="o">.</span><span class="na">mLayout</span> <span class="o">=</span> <span class="k">this</span><span class="o">;</span> <span class="n">e</span><span class="o">.</span><span class="na">mWidth</span> <span class="o">=</span> <span class="n">ellipsizedWidth</span><span class="o">;</span> <span class="n">e</span><span class="o">.</span><span class="na">mMethod</span> <span class="o">=</span> <span class="n">ellipsize</span><span class="o">;</span> <span class="n">mEllipsizedWidth</span> <span class="o">=</span> <span class="n">ellipsizedWidth</span><span class="o">;</span> <span class="n">mColumns</span> <span class="o">=</span> <span class="n">COLUMNS_ELLIPSIZE</span><span class="o">;</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">mColumns</span> <span class="o">=</span> <span class="n">COLUMNS_NORMAL</span><span class="o">;</span> <span class="n">mEllipsizedWidth</span> <span class="o">=</span> <span class="n">outerwidth</span><span class="o">;</span> <span class="o">}</span> <span class="n">mLineDirections</span> <span class="o">=</span> <span class="n">ArrayUtils</span><span class="o">.</span><span class="na">newUnpaddedArray</span><span class="o">(</span><span class="n">Directions</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">mColumns</span><span class="o">);</span> <span class="n">mLines</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">mLineDirections</span><span class="o">.</span><span class="na">length</span><span class="o">];</span> <span class="n">mMaximumVisibleLineCount</span> <span class="o">=</span> <span class="n">maxLines</span><span class="o">;</span> <span class="n">generate</span><span class="o">(</span><span class="n">b</span><span class="o">,</span> <span class="n">b</span><span class="o">.</span><span class="na">mIncludePad</span><span class="o">,</span> <span class="n">b</span><span class="o">.</span><span class="na">mIncludePad</span><span class="o">);</span> <span class="n">Builder</span><span class="o">.</span><span class="na">recycle</span><span class="o">(</span><span class="n">b</span><span class="o">);</span> <span class="o">}</span> </code></pre></div></div> <p>参数说明:</p> <ul> <li><code class="highlighter-rouge">CharSequence source</code> 文本内容</li> <li><code class="highlighter-rouge"> int bufstart, int bufend,</code> 开始位置和结束位置</li> <li><code class="highlighter-rouge"> TextPaint paint</code> 文本画笔对象</li> <li><code class="highlighter-rouge">int outerwidth</code> 布局宽度,超出宽度换行显示</li> <li><code class="highlighter-rouge">Alignment align</code> 对齐方式,默认是<code class="highlighter-rouge">Alignment.ALIGN_LEFT</code></li> <li><code class="highlighter-rouge">TextDirectionHeuristic textDir</code> 文本显示方向</li> <li><code class="highlighter-rouge">float spacingmult</code> 行间距倍数,默认是1</li> <li><code class="highlighter-rouge">float spacingadd</code> 行距增加值,默认是0</li> <li><code class="highlighter-rouge">boolean includepad</code> 文本顶部和底部是否留白</li> <li><code class="highlighter-rouge">TextUtils.TruncateAt ellipsize</code> 文本省略方式,有 START、MIDDLE、 END、MARQUEE 四种省略方式(其实还有一个 END_SMALL,但是 Google 并未开放出来)。</li> <li><code class="highlighter-rouge">int ellipsizedWidth</code> 省略宽度</li> <li><code class="highlighter-rouge">int maxLines</code> 最大行数</li> </ul> <p>细节分析:</p> <ul> <li> <p>构造方法的开始,在调用父类 Layout 构造方法的时候,判断了文本是否需要省略,如果需要省略,则创建一个 Ellipsizer 对象,Ellipsizer 是 Layout 的嵌套内部类,实现了 CharSequence 和 GetChars 接口。该类就是用来对文本进行省略处理的,具体的处理方法是由其 <code class="highlighter-rouge">getChars()</code> 方法完成的。</p> </li> <li> <p>在创建 Ellipsizer 对象之前,还判断了一下需要显示的文本是否是 Spanned ,如果是的话则创建 SpannedEllipsizer 对象,SpannedEllipsizer 类继承 Ellipsizer ,同时实现了 Spanned 接口。</p> </li> <li> <p>StaticLayout.Builder 对象的创建是通过 <code class="highlighter-rouge">Builder.obtain()</code> 方法创建的,在该方法内部可以看到 Builder 对象通过 SynchronizedPool 对象池来管理的,起到缓存的作用,避免 Builder 对象的重复创建,在 StaticLayout 的构造方法的最后也可以看到 <code class="highlighter-rouge">Builder.recycle(b)</code> 的调用,回收 Builder 对象。 Builder 的构造方法如下所示:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">private</span> <span class="nf">Builder</span><span class="o">()</span> <span class="o">{</span> <span class="n">mNativePtr</span> <span class="o">=</span> <span class="n">nNewBuilder</span><span class="o">();</span> <span class="o">}</span> </code></pre></div> </div> <p>其调用了 JNI 层的 <code class="highlighter-rouge">nNewBuilder()</code> 方法,新建了一个 LineBreak 对象,并将其指针指向 java 层,赋值给 Builder 对象的 mNativePtr 字段 ,后面调用 native 方法时,均需要将 mNativePtr 作为参数传递过去。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">static</span> <span class="n">jlong</span> <span class="nf">nNewBuilder</span><span class="o">(</span><span class="n">JNIEnv</span><span class="o">*,</span> <span class="n">jclass</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">reinterpret_cast</span><span class="o">&lt;</span><span class="n">jlong</span><span class="o">&gt;(</span><span class="k">new</span> <span class="n">LineBreaker</span><span class="o">);</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>mLineDirections 需要结合到后面每行文本处理来理解,这里可以大致说一下,StaticLayout 源码中声明了以下的常量:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kt">int</span> <span class="n">COLUMNS_NORMAL</span> <span class="o">=</span> <span class="mi">4</span><span class="o">;</span> <span class="kt">int</span> <span class="n">COLUMNS_ELLIPSIZE</span> <span class="o">=</span> <span class="mi">6</span><span class="o">;</span> <span class="kt">int</span> <span class="n">START</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="kt">int</span> <span class="n">DIR</span> <span class="o">=</span> <span class="n">START</span><span class="o">;</span> <span class="kt">int</span> <span class="n">TAB</span> <span class="o">=</span> <span class="n">START</span><span class="o">;</span> <span class="kt">int</span> <span class="n">TOP</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span> <span class="kt">int</span> <span class="n">DESCENT</span> <span class="o">=</span> <span class="mi">2</span><span class="o">;</span> <span class="kt">int</span> <span class="n">HYPHEN</span> <span class="o">=</span> <span class="mi">3</span><span class="o">;</span> <span class="kt">int</span> <span class="n">ELLIPSIS_START</span> <span class="o">=</span> <span class="mi">4</span><span class="o">;</span> <span class="kt">int</span> <span class="n">ELLIPSIS_COUNT</span> <span class="o">=</span> <span class="mi">5</span><span class="o">;</span> </code></pre></div> </div> <p>其中 COLUMNS_NORMAL 和 COLUMNS_ELLIPSIZE 会赋值给全局变量 mColumns,正如你在构造方法中看到的那样,这个在没一行处理时会用到,每一行文本处理时需要记录四个值,start,top,desent,hyphen 值,当文本需要省略时,还需要记录 ellipsis_start 和 ellipsis_count 值,因此正常的 mColumn 值为4,省略时则是6,因此 mLineDirections 数组大小始终是 mColumn 的倍数,mLine 数组的大小和其保持一致(从后面的分析来看,mLineDirections 数组的大小没必要这么大)。</p> </li> </ul> <h4 id="generate-方法分析">generate 方法分析</h4> <p>StaticLayout 中的 <code class="highlighter-rouge">generate()</code> 方法近 300 行,其完成了文本的段落、折行的处理,建议自行对照源码来阅读下面的分析,本文不贴太多代码。</p> <p>接受的参数:</p> <ul> <li><code class="highlighter-rouge">StaticLayout.Builder b</code> StaticLayout.Builder 对象</li> <li><code class="highlighter-rouge">boolean includepad</code>是否上下保留空白</li> <li><code class="highlighter-rouge">boolean trackpad</code></li> </ul> <p>细节分析:</p> <ol> <li>在方法的开始,创建了很多的局部变量,并将 Builder 对象对应的值赋值给这些变量。 <ul> <li> <p>其中有个 <code class="highlighter-rouge">Paint.FontMetricsInt fm</code> 变量,FontMetricsInt 是 Paint 的内部类,主要用来完成字体测量,其和 <code class="highlighter-rouge">FontMetrics</code> 非常类似,只是在文字测量时,对应的数值均是 int 类型,FontMetrics 是 float 类型。FontMetricsInt 类主要包含保存了字体测量相关的数据,源码如下:</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="kd">class</span> <span class="nc">FontMetricsInt</span> <span class="o">{</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">top</span><span class="o">;</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">ascent</span><span class="o">;</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">descent</span><span class="o">;</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">bottom</span><span class="o">;</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">leading</span><span class="o">;</span> <span class="o">}</span> </code></pre></div> </div> <p>每个值的含义如下图所示,在 baseline 之上为负值,baseline 之下为正值,leading 表示两行文本 baseline 之间的距离,这个值可以由行间距倍数和行间距增加值来调整: <img src="http://ac-qygvx1cc.clouddn.com/00a715d3dc637c92.png" alt="" /></p> <p>在接下来的字体测量中,会使用 fmCache 数组来缓存字体测量的信息,缓存 top, bottom, ascent, 和 descen 四个值,因此 fmCache 数组的大小始终是4的倍数。</p> </li> </ul> </li> <li> <p>接下来就是按照一个个段落来处理文本:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">paraStart</span> <span class="o">=</span> <span class="n">bufStart</span><span class="o">;</span> <span class="n">paraStart</span> <span class="o">&lt;=</span> <span class="n">bufEnd</span><span class="o">;</span> <span class="n">paraStart</span> <span class="o">=</span> <span class="n">paraEnd</span><span class="o">)</span> <span class="o">{</span> <span class="n">paraEnd</span> <span class="o">=</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">indexOf</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">CHAR_NEW_LINE</span><span class="o">,</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">paraEnd</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="o">)</span> <span class="n">paraEnd</span> <span class="o">=</span> <span class="n">bufEnd</span><span class="o">;</span> <span class="k">else</span> <span class="n">paraEnd</span><span class="o">++;</span> <span class="o">...</span> <span class="o">}</span> </code></pre></div> </div> <p>通过查找换行符,确定每个段落的起止位置,接下来的处理,均是对该段落文本的处理。</p> </li> <li> <p>span 文本的处理</p> </li> <li> <p>处理段落文本 :</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">measured</span><span class="o">.</span><span class="na">setPara</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">paraEnd</span><span class="o">,</span> <span class="n">textDir</span><span class="o">,</span> <span class="n">b</span><span class="o">);</span> <span class="kt">char</span><span class="o">[]</span> <span class="n">chs</span> <span class="o">=</span> <span class="n">measured</span><span class="o">.</span><span class="na">mChars</span><span class="o">;</span> <span class="kt">float</span><span class="o">[]</span> <span class="n">widths</span> <span class="o">=</span> <span class="n">measured</span><span class="o">.</span><span class="na">mWidths</span><span class="o">;</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">chdirs</span> <span class="o">=</span> <span class="n">measured</span><span class="o">.</span><span class="na">mLevels</span><span class="o">;</span> <span class="kt">int</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">measured</span><span class="o">.</span><span class="na">mDir</span><span class="o">;</span> <span class="kt">boolean</span> <span class="n">easy</span> <span class="o">=</span> <span class="n">measured</span><span class="o">.</span><span class="na">mEasy</span><span class="o">;</span> </code></pre></div> </div> </li> <li> <p>处理制表位,这里的制表位是使用 <code class="highlighter-rouge">TabStopSpan</code> 方式插入到文本中的,通过 Spanned 接口提供的 <code class="highlighter-rouge">getSpans(int start, int end, Class&lt;T&gt; type)</code> 方法来获取到 TabStopSpan,排序后将所有的制表位的位置存在 variableTabStops 数组中。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span><span class="o">[]</span> <span class="n">variableTabStops</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">spanned</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">TabStopSpan</span><span class="o">[]</span> <span class="n">spans</span> <span class="o">=</span> <span class="n">getParagraphSpans</span><span class="o">(</span><span class="n">spanned</span><span class="o">,</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">paraEnd</span><span class="o">,</span> <span class="n">TabStopSpan</span><span class="o">.</span><span class="na">class</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">spans</span><span class="o">.</span><span class="na">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">stops</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">spans</span><span class="o">.</span><span class="na">length</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">&lt;</span> <span class="n">spans</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span> <span class="n">stops</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="o">=</span> <span class="n">spans</span><span class="o">[</span><span class="n">i</span><span class="o">].</span><span class="na">getTabStop</span><span class="o">();</span> <span class="o">}</span> <span class="n">Arrays</span><span class="o">.</span><span class="na">sort</span><span class="o">(</span><span class="n">stops</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">stops</span><span class="o">.</span><span class="na">length</span><span class="o">);</span> <span class="n">variableTabStops</span> <span class="o">=</span> <span class="n">stops</span><span class="o">;</span> <span class="o">}}</span> </code></pre></div> </div> </li> <li> <p>完成以上处理后,就是交给 JNI 层来处理段落文本,主要处理了段落的制表行缩进、折行等;需要再分析。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">nSetupParagraph</span><span class="o">(</span><span class="n">b</span><span class="o">.</span><span class="na">mNativePtr</span><span class="o">,</span> <span class="n">chs</span><span class="o">,</span> <span class="n">paraEnd</span> <span class="o">-</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">firstWidth</span><span class="o">,</span> <span class="n">firstWidthLineCount</span><span class="o">,</span> <span class="n">restWidth</span><span class="o">,</span> <span class="n">variableTabStops</span><span class="o">,</span> <span class="n">TAB_INCREMENT</span><span class="o">,</span> <span class="n">b</span><span class="o">.</span><span class="na">mBreakStrategy</span><span class="o">,</span> <span class="n">b</span><span class="o">.</span><span class="na">mHyphenationFrequency</span><span class="o">);</span> </code></pre></div> </div> </li> <li> <p>处理缩进的源码如下:</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="n">mLeftIndents</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">mRightIndents</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span> <span class="n">leftLen</span> <span class="o">=</span> <span class="n">mLeftIndents</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="mi">0</span> <span class="o">:</span> <span class="n">mLeftIndents</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="kt">int</span> <span class="n">rightLen</span> <span class="o">=</span> <span class="n">mRightIndents</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="mi">0</span> <span class="o">:</span> <span class="n">mRightIndents</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="kt">int</span> <span class="n">indentsLen</span> <span class="o">=</span> <span class="n">Math</span><span class="o">.</span><span class="na">max</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="n">Math</span><span class="o">.</span><span class="na">min</span><span class="o">(</span><span class="n">leftLen</span><span class="o">,</span> <span class="n">rightLen</span><span class="o">)</span> <span class="o">-</span> <span class="n">mLineCount</span><span class="o">);</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">indents</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">indentsLen</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">&lt;</span> <span class="n">indentsLen</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span> <span class="kt">int</span> <span class="n">leftMargin</span> <span class="o">=</span> <span class="n">mLeftIndents</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="mi">0</span> <span class="o">:</span> <span class="n">mLeftIndents</span><span class="o">[</span><span class="n">Math</span><span class="o">.</span><span class="na">min</span><span class="o">(</span><span class="n">i</span> <span class="o">+</span> <span class="n">mLineCount</span><span class="o">,</span> <span class="n">leftLen</span> <span class="o">-</span> <span class="mi">1</span><span class="o">)];</span> <span class="kt">int</span> <span class="n">rightMargin</span> <span class="o">=</span> <span class="n">mRightIndents</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="mi">0</span> <span class="o">:</span> <span class="n">mRightIndents</span><span class="o">[</span><span class="n">Math</span><span class="o">.</span><span class="na">min</span><span class="o">(</span><span class="n">i</span> <span class="o">+</span> <span class="n">mLineCount</span><span class="o">,</span> <span class="n">rightLen</span> <span class="o">-</span> <span class="mi">1</span><span class="o">)];</span> <span class="n">indents</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="o">=</span> <span class="n">leftMargin</span> <span class="o">+</span> <span class="n">rightMargin</span><span class="o">;</span> <span class="o">}</span> <span class="n">nSetIndents</span><span class="o">(</span><span class="n">b</span><span class="o">.</span><span class="na">mNativePtr</span><span class="o">,</span> <span class="n">indents</span><span class="o">);</span> <span class="o">}</span> </code></pre></div> </div> <p>开始的条件判断使用的 mLeftIndents 和 mRightIndents 变量是通过 Builder 对象来赋值的:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mLeftIndents</span> <span class="o">=</span> <span class="n">b</span><span class="o">.</span><span class="na">mLeftIndents</span><span class="o">;</span> <span class="n">mRightIndents</span> <span class="o">=</span> <span class="n">b</span><span class="o">.</span><span class="na">mRightIndents</span><span class="o">;</span> </code></pre></div> </div> <p>但是比较困惑的是,源码中并没有对 Builder 对象这两个字段赋值的地方,因此这里的条件判断结果都是 false,实际 debug 测试了下,这个地方的判断确实始终是 false,所以具体的逻辑还需要再分析下。可以看见的是,在方法的最后,同样是调用 JNI 层的方法设置缩进。</p> </li> <li> <p>缓存字体测量信息,源码如下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">spanStart</span> <span class="o">=</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">spanEnd</span><span class="o">;</span> <span class="n">spanStart</span> <span class="o">&lt;</span> <span class="n">paraEnd</span><span class="o">;</span> <span class="n">spanStart</span> <span class="o">=</span> <span class="n">spanEnd</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">&gt;=</span> <span class="n">fmCache</span><span class="o">.</span><span class="na">length</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">grow</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">*</span> <span class="mi">2</span><span class="o">];</span> <span class="n">System</span><span class="o">.</span><span class="na">arraycopy</span><span class="o">(</span><span class="n">fmCache</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">grow</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span><span class="o">);</span> <span class="n">fmCache</span> <span class="o">=</span> <span class="n">grow</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">spanEndCacheCount</span> <span class="o">&gt;=</span> <span class="n">spanEndCache</span><span class="o">.</span><span class="na">length</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">grow</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">spanEndCacheCount</span> <span class="o">*</span> <span class="mi">2</span><span class="o">];</span> <span class="n">System</span><span class="o">.</span><span class="na">arraycopy</span><span class="o">(</span><span class="n">spanEndCache</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">grow</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">spanEndCacheCount</span><span class="o">);</span> <span class="n">spanEndCache</span> <span class="o">=</span> <span class="n">grow</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">spanned</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">spanEnd</span> <span class="o">=</span> <span class="n">paraEnd</span><span class="o">;</span> <span class="kt">int</span> <span class="n">spanLen</span> <span class="o">=</span> <span class="n">spanEnd</span> <span class="o">-</span> <span class="n">spanStart</span><span class="o">;</span> <span class="n">measured</span><span class="o">.</span><span class="na">addStyleRun</span><span class="o">(</span><span class="n">paint</span><span class="o">,</span> <span class="n">spanLen</span><span class="o">,</span> <span class="n">fm</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">spanEnd</span> <span class="o">=</span> <span class="n">spanned</span><span class="o">.</span><span class="na">nextSpanTransition</span><span class="o">(</span><span class="n">spanStart</span><span class="o">,</span> <span class="n">paraEnd</span><span class="o">,</span> <span class="n">MetricAffectingSpan</span><span class="o">.</span><span class="na">class</span><span class="o">);</span> <span class="kt">int</span> <span class="n">spanLen</span> <span class="o">=</span> <span class="n">spanEnd</span> <span class="o">-</span> <span class="n">spanStart</span><span class="o">;</span> <span class="n">MetricAffectingSpan</span><span class="o">[]</span> <span class="n">spans</span> <span class="o">=</span> <span class="n">spanned</span><span class="o">.</span><span class="na">getSpans</span><span class="o">(</span><span class="n">spanStart</span><span class="o">,</span> <span class="n">spanEnd</span><span class="o">,</span> <span class="n">MetricAffectingSpan</span><span class="o">.</span><span class="na">class</span><span class="o">);</span> <span class="n">spans</span> <span class="o">=</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">removeEmptySpans</span><span class="o">(</span><span class="n">spans</span><span class="o">,</span> <span class="n">spanned</span><span class="o">,</span> <span class="n">MetricAffectingSpan</span><span class="o">.</span><span class="na">class</span><span class="o">);</span> <span class="n">measured</span><span class="o">.</span><span class="na">addStyleRun</span><span class="o">(</span><span class="n">paint</span><span class="o">,</span> <span class="n">spans</span><span class="o">,</span> <span class="n">spanLen</span><span class="o">,</span> <span class="n">fm</span><span class="o">);</span> <span class="o">}</span> <span class="c1">// the order of storage here (top, bottom, ascent, descent) has to match the code below</span> <span class="c1">// where these values are retrieved</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">0</span><span class="o">]</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span><span class="o">;</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">1</span><span class="o">]</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span><span class="o">;</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">2</span><span class="o">]</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span><span class="o">;</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">3</span><span class="o">]</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span><span class="o">;</span> <span class="n">fmCacheCount</span><span class="o">++;</span> <span class="n">spanEndCache</span><span class="o">[</span><span class="n">spanEndCacheCount</span><span class="o">]</span> <span class="o">=</span> <span class="n">spanEnd</span><span class="o">;</span> <span class="n">spanEndCacheCount</span><span class="o">++;</span> <span class="o">}</span> </code></pre></div> </div> <p>fmCache 的初始化时的大小是 16,因此在每次循环开始时,需要判断下是否需要对 fmCache 扩容,这里的扩容同样保证了 fmCache 的大小是4的倍数,同时每次扩容时都是双倍扩容。</p> <p>这里也会对文本中的 Span 的结束位置使用 spanEndCache 缓存记录下来,这里处理的 span 具体类型是 MetricAffectingSpan,顾名思义就是对字体会有影响的 Span,需要单独拿出来处理,缓存字体测量信息。</p> <p>具体的测量则是交给 MeasuredText 类的 <code class="highlighter-rouge">addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm)</code> 和 <code class="highlighter-rouge">addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len, Paint.FontMetricsInt fm)</code> 方法来处理,具体的处理涉及到文字的排版,感兴趣的可以自己查看源码,这里不再详细分析了。</p> <p>测量完成后,字体测量信息的值4个一组地存储在 fmCache 数组中,spanEnd 值存储在 spanEndCache 数组中。</p> </li> <li> <p>计算每行宽度和折行处理,宽度的计算和折行的处理分别借助 JNI 层的 <code class="highlighter-rouge">nGetWidths()</code> 和 <code class="highlighter-rouge">nComputeLineBreaks()</code> 方法来处理。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">nGetWidths</span><span class="o">(</span><span class="n">b</span><span class="o">.</span><span class="na">mNativePtr</span><span class="o">,</span> <span class="n">widths</span><span class="o">);</span> <span class="c1">// 得到当前行内包含的折行数目</span> <span class="kt">int</span> <span class="n">breakCount</span> <span class="o">=</span> <span class="n">nComputeLineBreaks</span><span class="o">(</span><span class="n">b</span><span class="o">.</span><span class="na">mNativePtr</span><span class="o">,</span> <span class="n">lineBreaks</span><span class="o">,</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">breaks</span><span class="o">,</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">widths</span><span class="o">,</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">flags</span><span class="o">,</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">breaks</span><span class="o">.</span><span class="na">length</span><span class="o">);</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">breaks</span> <span class="o">=</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">breaks</span><span class="o">;</span> <span class="kt">float</span><span class="o">[]</span> <span class="n">lineWidths</span> <span class="o">=</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">widths</span><span class="o">;</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">flags</span> <span class="o">=</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">flags</span><span class="o">;</span> <span class="c1">// 得到剩下的行数 = 最大允许行数 - 当前行数</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">remainingLineCount</span> <span class="o">=</span> <span class="n">mMaximumVisibleLineCount</span> <span class="o">-</span> <span class="n">mLineCount</span><span class="o">;</span> <span class="kd">final</span> <span class="kt">boolean</span> <span class="n">ellipsisMayBeApplied</span> <span class="o">=</span> <span class="n">ellipsize</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="o">(</span><span class="n">ellipsize</span> <span class="o">==</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">TruncateAt</span><span class="o">.</span><span class="na">END</span> <span class="o">||</span> <span class="o">(</span><span class="n">mMaximumVisibleLineCount</span> <span class="o">==</span> <span class="mi">1</span> <span class="o">&amp;&amp;</span> <span class="n">ellipsize</span> <span class="o">!=</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">TruncateAt</span><span class="o">.</span><span class="na">MARQUEE</span><span class="o">));</span> <span class="c1">// 如果剩下的行数小于当前行包含的折行数目,则需要将最后一行和超出的行处理成单行</span> <span class="k">if</span> <span class="o">(</span><span class="n">remainingLineCount</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">remainingLineCount</span> <span class="o">&lt;</span> <span class="n">breakCount</span> <span class="o">&amp;&amp;</span> <span class="n">ellipsisMayBeApplied</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// Treat the last line and overflowed lines as a single line.</span> <span class="n">breaks</span><span class="o">[</span><span class="n">remainingLineCount</span> <span class="o">-</span> <span class="mi">1</span><span class="o">]</span> <span class="o">=</span> <span class="n">breaks</span><span class="o">[</span><span class="n">breakCount</span> <span class="o">-</span> <span class="mi">1</span><span class="o">];</span> <span class="c1">// 计算 width 和 flag 值</span> <span class="kt">float</span> <span class="n">width</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="kt">int</span> <span class="n">flag</span> <span class="o">=</span> <span class="mi">0</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="n">remainingLineCount</span> <span class="o">-</span> <span class="mi">1</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">breakCount</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span> <span class="n">width</span> <span class="o">+=</span> <span class="n">lineWidths</span><span class="o">[</span><span class="n">i</span><span class="o">];</span> <span class="n">flag</span> <span class="o">|=</span> <span class="n">flags</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="o">&amp;</span> <span class="n">TAB_MASK</span><span class="o">;</span> <span class="o">}</span> <span class="n">lineWidths</span><span class="o">[</span><span class="n">remainingLineCount</span> <span class="o">-</span> <span class="mi">1</span><span class="o">]</span> <span class="o">=</span> <span class="n">width</span><span class="o">;</span> <span class="n">flags</span><span class="o">[</span><span class="n">remainingLineCount</span> <span class="o">-</span> <span class="mi">1</span><span class="o">]</span> <span class="o">=</span> <span class="n">flag</span><span class="o">;</span> <span class="c1">// 设置当前行中的折行数为可用的行数</span> <span class="n">breakCount</span> <span class="o">=</span> <span class="n">remainingLineCount</span><span class="o">;</span> <span class="o">}</span> </code></pre></div> </div> <p>处理完折行后,会判断下是否需要省略处理,如果需要,则根据允许的最大行数和当前行包含的折行数目来确定需要处理成省略的那一行,并设置相关的 width 和 flag 信息。</p> </li> <li> <p>处理文本中 Span 和折行:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kt">int</span> <span class="n">here</span> <span class="o">=</span> <span class="n">paraStart</span><span class="o">;</span> <span class="kt">int</span> <span class="n">fmTop</span> <span class="o">=</span> <span class="mi">0</span><span class="o">,</span> <span class="n">fmBottom</span> <span class="o">=</span> <span class="mi">0</span><span class="o">,</span> <span class="n">fmAscent</span> <span class="o">=</span> <span class="mi">0</span><span class="o">,</span> <span class="n">fmDescent</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="kt">int</span> <span class="n">fmCacheIndex</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="kt">int</span> <span class="n">spanEndCacheIndex</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="kt">int</span> <span class="n">breakIndex</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">spanStart</span> <span class="o">=</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">spanEnd</span><span class="o">;</span> <span class="n">spanStart</span> <span class="o">&lt;</span> <span class="n">paraEnd</span><span class="o">;</span> <span class="n">spanStart</span> <span class="o">=</span> <span class="n">spanEnd</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// 从之前存储的数据中获取 span 结束位置</span> <span class="n">spanEnd</span> <span class="o">=</span> <span class="n">spanEndCache</span><span class="o">[</span><span class="n">spanEndCacheIndex</span><span class="o">++];</span> <span class="c1">// 恢复之前存储的字体测量信息</span> <span class="c1">// retrieve cached metrics, order matches above</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span> <span class="o">=</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheIndex</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">0</span><span class="o">];</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span> <span class="o">=</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheIndex</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">1</span><span class="o">];</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span> <span class="o">=</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheIndex</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">2</span><span class="o">];</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span> <span class="o">=</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheIndex</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">3</span><span class="o">];</span> <span class="n">fmCacheIndex</span><span class="o">++;</span> <span class="c1">// 参照前面提到的字体测量的几个值的说明,这里的 top 和 ascent 取值小的,bottom 和 descent 取值大的,保证文本均可以正常显示</span> <span class="k">if</span> <span class="o">(</span><span class="n">fm</span><span class="o">.</span><span class="na">top</span> <span class="o">&lt;</span> <span class="n">fmTop</span><span class="o">)</span> <span class="o">{</span> <span class="n">fmTop</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">fm</span><span class="o">.</span><span class="na">ascent</span> <span class="o">&lt;</span> <span class="n">fmAscent</span><span class="o">)</span> <span class="o">{</span> <span class="n">fmAscent</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">fm</span><span class="o">.</span><span class="na">descent</span> <span class="o">&gt;</span> <span class="n">fmDescent</span><span class="o">)</span> <span class="o">{</span> <span class="n">fmDescent</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">fm</span><span class="o">.</span><span class="na">bottom</span> <span class="o">&gt;</span> <span class="n">fmBottom</span><span class="o">)</span> <span class="o">{</span> <span class="n">fmBottom</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span><span class="o">;</span> <span class="o">}</span> <span class="c1">// 跳过 span 之前的折行</span> <span class="k">while</span> <span class="o">(</span><span class="n">breakIndex</span> <span class="o">&lt;</span> <span class="n">breakCount</span> <span class="o">&amp;&amp;</span> <span class="n">paraStart</span> <span class="o">+</span> <span class="n">breaks</span><span class="o">[</span><span class="n">breakIndex</span><span class="o">]</span> <span class="o">&lt;</span> <span class="n">spanStart</span><span class="o">)</span> <span class="o">{</span> <span class="n">breakIndex</span><span class="o">++;</span> <span class="o">}</span> <span class="c1">// 处理 span 中的折行</span> <span class="k">while</span> <span class="o">(</span><span class="n">breakIndex</span> <span class="o">&lt;</span> <span class="n">breakCount</span> <span class="o">&amp;&amp;</span> <span class="n">paraStart</span> <span class="o">+</span> <span class="n">breaks</span><span class="o">[</span><span class="n">breakIndex</span><span class="o">]</span> <span class="o">&lt;=</span> <span class="n">spanEnd</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span> <span class="n">endPos</span> <span class="o">=</span> <span class="n">paraStart</span> <span class="o">+</span> <span class="n">breaks</span><span class="o">[</span><span class="n">breakIndex</span><span class="o">];</span> <span class="kt">boolean</span> <span class="n">moreChars</span> <span class="o">=</span> <span class="o">(</span><span class="n">endPos</span> <span class="o">&lt;</span> <span class="n">bufEnd</span><span class="o">);</span> <span class="n">v</span> <span class="o">=</span> <span class="n">out</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">here</span><span class="o">,</span> <span class="n">endPos</span><span class="o">,</span> <span class="n">fmAscent</span><span class="o">,</span> <span class="n">fmDescent</span><span class="o">,</span> <span class="n">fmTop</span><span class="o">,</span> <span class="n">fmBottom</span><span class="o">,</span> <span class="n">v</span><span class="o">,</span> <span class="n">spacingmult</span><span class="o">,</span> <span class="n">spacingadd</span><span class="o">,</span> <span class="n">chooseHt</span><span class="o">,</span><span class="n">chooseHtv</span><span class="o">,</span> <span class="n">fm</span><span class="o">,</span> <span class="n">flags</span><span class="o">[</span><span class="n">breakIndex</span><span class="o">],</span> <span class="n">needMultiply</span><span class="o">,</span> <span class="n">chdirs</span><span class="o">,</span> <span class="n">dir</span><span class="o">,</span> <span class="n">easy</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="n">includepad</span><span class="o">,</span> <span class="n">trackpad</span><span class="o">,</span> <span class="n">chs</span><span class="o">,</span> <span class="n">widths</span><span class="o">,</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">ellipsize</span><span class="o">,</span> <span class="n">ellipsizedWidth</span><span class="o">,</span> <span class="n">lineWidths</span><span class="o">[</span><span class="n">breakIndex</span><span class="o">],</span> <span class="n">paint</span><span class="o">,</span> <span class="n">moreChars</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">endPos</span> <span class="o">&lt;</span> <span class="n">spanEnd</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// 如果 Span 文本还未处理完成,则恢复当前的 fontMetrics 信息</span> <span class="c1">// 否则归零处理,处理下一段 Span</span> <span class="c1">// preserve metrics for current span</span> <span class="n">fmTop</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span><span class="o">;</span> <span class="n">fmBottom</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span><span class="o">;</span> <span class="n">fmAscent</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span><span class="o">;</span> <span class="n">fmDescent</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span><span class="o">;</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">fmTop</span> <span class="o">=</span> <span class="n">fmBottom</span> <span class="o">=</span> <span class="n">fmAscent</span> <span class="o">=</span> <span class="n">fmDescent</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="o">}</span> <span class="n">here</span> <span class="o">=</span> <span class="n">endPos</span><span class="o">;</span> <span class="n">breakIndex</span><span class="o">++;</span> <span class="c1">// 如果处理该段落时行数已经超过最大可见行数,则直接终止后面的处理</span> <span class="k">if</span> <span class="o">(</span><span class="n">mLineCount</span> <span class="o">&gt;=</span> <span class="n">mMaximumVisibleLineCount</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="o">}</span> <span class="c1">// 如果段落结束就是整个文本的结束,则跳出处理段落的循环,否则处理下一段。</span> <span class="k">if</span> <span class="o">(</span><span class="n">paraEnd</span> <span class="o">==</span> <span class="n">bufEnd</span><span class="o">)</span> <span class="k">break</span><span class="o">;</span> </code></pre></div> </div> <p>至此,以段落为单位的文本就处理完毕,包括文本的折行、Span 的处理都已完成。</p> </li> <li> <p>当需要处理的文本起止位置相同时(即需要处理的文本为空),且前面是换行符时,此时也需要将该空白处理成一个段落。代码如下:</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="n">bufEnd</span> <span class="o">==</span> <span class="n">bufStart</span> <span class="o">||</span> <span class="n">source</span><span class="o">.</span><span class="na">charAt</span><span class="o">(</span><span class="n">bufEnd</span> <span class="o">-</span> <span class="mi">1</span><span class="o">)</span> <span class="o">==</span> <span class="n">CHAR_NEW_LINE</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="n">mLineCount</span> <span class="o">&lt;</span> <span class="n">mMaximumVisibleLineCount</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// Log.e("text", "output last " + bufEnd);</span> <span class="n">measured</span><span class="o">.</span><span class="na">setPara</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="n">textDir</span><span class="o">,</span> <span class="n">b</span><span class="o">);</span> <span class="n">paint</span><span class="o">.</span><span class="na">getFontMetricsInt</span><span class="o">(</span><span class="n">fm</span><span class="o">);</span> <span class="n">v</span> <span class="o">=</span> <span class="n">out</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span><span class="o">,</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span><span class="o">,</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span><span class="o">,</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span><span class="o">,</span> <span class="n">v</span><span class="o">,</span> <span class="n">spacingmult</span><span class="o">,</span> <span class="n">spacingadd</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">fm</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">needMultiply</span><span class="o">,</span> <span class="n">measured</span><span class="o">.</span><span class="na">mLevels</span><span class="o">,</span> <span class="n">measured</span><span class="o">.</span><span class="na">mDir</span><span class="o">,</span> <span class="n">measured</span><span class="o">.</span><span class="na">mEasy</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="n">includepad</span><span class="o">,</span> <span class="n">trackpad</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">bufStart</span><span class="o">,</span> <span class="n">ellipsize</span><span class="o">,</span> <span class="n">ellipsizedWidth</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">paint</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span> <span class="o">}</span> </code></pre></div> </div> <p>第10点和第11点分析中均出现了 <code class="highlighter-rouge">out()</code> 方法,前面提到,该方法也是 StaticLayout 源码中的一个重要的方法,接下来会分析下 <code class="highlighter-rouge">out</code> 方法中做了什么处理。</p> </li> </ol> <h4 id="out-方法分析">out 方法分析</h4> <p><code class="highlighter-rouge">out()</code> 方法在我看来,就是 layout 中的 out。如果说 <code class="highlighter-rouge">generate()</code> 大部分是处理一些折行、段落相关的数据,那么 <code class="highlighter-rouge">out()</code> 方法就是将这些数据使用起来,真正地布局出来(注意,布局不是显示,显示的话还是在父类的 <code class="highlighter-rouge">drawText()</code> 方法中进行的)。</p> <ol> <li> <p>方法接收的参数如下所示,很多参数都是在 <code class="highlighter-rouge">generate()</code> 中处理获得,参数的含义和前面提到的基本相同。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">out</span><span class="o">(</span><span class="n">CharSequence</span> <span class="n">text</span><span class="o">,</span> <span class="kt">int</span> <span class="n">start</span><span class="o">,</span> <span class="kt">int</span> <span class="n">end</span><span class="o">,</span> <span class="kt">int</span> <span class="n">above</span><span class="o">,</span> <span class="kt">int</span> <span class="n">below</span><span class="o">,</span> <span class="kt">int</span> <span class="n">top</span><span class="o">,</span> <span class="kt">int</span> <span class="n">bottom</span><span class="o">,</span> <span class="kt">int</span> <span class="n">v</span><span class="o">,</span> <span class="kt">float</span> <span class="n">spacingmult</span><span class="o">,</span> <span class="kt">float</span> <span class="n">spacingadd</span><span class="o">,</span> <span class="n">LineHeightSpan</span><span class="o">[]</span> <span class="n">chooseHt</span><span class="o">,</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">chooseHtv</span><span class="o">,</span> <span class="n">Paint</span><span class="o">.</span><span class="na">FontMetricsInt</span> <span class="n">fm</span><span class="o">,</span> <span class="kt">int</span> <span class="n">flags</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">needMultiply</span><span class="o">,</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">chdirs</span><span class="o">,</span> <span class="kt">int</span> <span class="n">dir</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">easy</span><span class="o">,</span> <span class="kt">int</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">includePad</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">trackPad</span><span class="o">,</span> <span class="kt">char</span><span class="o">[]</span> <span class="n">chs</span><span class="o">,</span> <span class="kt">float</span><span class="o">[]</span> <span class="n">widths</span><span class="o">,</span> <span class="kt">int</span> <span class="n">widthStart</span><span class="o">,</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">TruncateAt</span> <span class="n">ellipsize</span><span class="o">,</span> <span class="kt">float</span> <span class="n">ellipsisWidth</span><span class="o">,</span> <span class="kt">float</span> <span class="n">textWidth</span><span class="o">,</span> <span class="n">TextPaint</span> <span class="n">paint</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">moreChars</span><span class="o">)</span> </code></pre></div> </div> </li> <li> <p>对 mLineDirections 和 mLine 扩容处理,根据当前行数,判断下 mLine 数组大小是否足够储存当前行的信息,如果不够,则扩容,对应的 mLineDirections 也进行扩容处理(两个数组大小相同)。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="n">mLineCount</span><span class="o">;</span> <span class="kt">int</span> <span class="n">off</span> <span class="o">=</span> <span class="n">j</span> <span class="o">*</span> <span class="n">mColumns</span><span class="o">;</span> <span class="kt">int</span> <span class="n">want</span> <span class="o">=</span> <span class="n">off</span> <span class="o">+</span> <span class="n">mColumns</span> <span class="o">+</span> <span class="n">TOP</span><span class="o">;</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">lines</span> <span class="o">=</span> <span class="n">mLines</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">want</span> <span class="o">&gt;=</span> <span class="n">lines</span><span class="o">.</span><span class="na">length</span><span class="o">)</span> <span class="o">{</span> <span class="n">Directions</span><span class="o">[]</span> <span class="n">grow2</span> <span class="o">=</span> <span class="n">ArrayUtils</span><span class="o">.</span><span class="na">newUnpaddedArray</span><span class="o">(</span> <span class="n">Directions</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">GrowingArrayUtils</span><span class="o">.</span><span class="na">growSize</span><span class="o">(</span><span class="n">want</span><span class="o">))</span> <span class="n">System</span><span class="o">.</span><span class="na">arraycopy</span><span class="o">(</span><span class="n">mLineDirections</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">grow2</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">mLineDirections</span><span class="o">.</span><span class="na">length</span><span class="o">);</span> <span class="n">mLineDirections</span> <span class="o">=</span> <span class="n">grow2</span><span class="o">;</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">grow</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">grow2</span><span class="o">.</span><span class="na">length</span><span class="o">];</span> <span class="n">System</span><span class="o">.</span><span class="na">arraycopy</span><span class="o">(</span><span class="n">lines</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">grow</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">lines</span><span class="o">.</span><span class="na">length</span><span class="o">);</span> <span class="n">mLines</span> <span class="o">=</span> <span class="n">grow</span><span class="o">;</span> <span class="n">lines</span> <span class="o">=</span> <span class="n">grow</span><span class="o">;</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>待分析</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="n">chooseHt</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span> <span class="o">=</span> <span class="n">above</span><span class="o">;</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span> <span class="o">=</span> <span class="n">below</span><span class="o">;</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span> <span class="o">=</span> <span class="n">top</span><span class="o">;</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span> <span class="o">=</span> <span class="n">bottom</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">&lt;</span> <span class="n">chooseHt</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">chooseHt</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="k">instanceof</span> <span class="n">LineHeightSpan</span><span class="o">.</span><span class="na">WithDensity</span><span class="o">)</span> <span class="o">{</span> <span class="o">((</span><span class="n">LineHeightSpan</span><span class="o">.</span><span class="na">WithDensity</span><span class="o">)</span> <span class="n">chooseHt</span><span class="o">[</span><span class="n">i</span><span class="o">]).</span> <span class="n">chooseHeight</span><span class="o">(</span><span class="n">text</span><span class="o">,</span> <span class="n">start</span><span class="o">,</span> <span class="n">end</span><span class="o">,</span> <span class="n">chooseHtv</span><span class="o">[</span><span class="n">i</span><span class="o">],</span> <span class="n">v</span><span class="o">,</span> <span class="n">fm</span><span class="o">,</span> <span class="n">paint</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">chooseHt</span><span class="o">[</span><span class="n">i</span><span class="o">].</span><span class="na">chooseHeight</span><span class="o">(</span><span class="n">text</span><span class="o">,</span> <span class="n">start</span><span class="o">,</span> <span class="n">end</span><span class="o">,</span> <span class="n">chooseHtv</span><span class="o">[</span><span class="n">i</span><span class="o">],</span> <span class="n">v</span><span class="o">,</span> <span class="n">fm</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> <span class="n">above</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span><span class="o">;</span> <span class="n">below</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span><span class="o">;</span> <span class="n">top</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span><span class="o">;</span> <span class="n">bottom</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span><span class="o">;</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>第一行和最后一行的特殊处理,以及行间距的处理</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 判断是否是第一行</span> <span class="kt">boolean</span> <span class="n">firstLine</span> <span class="o">=</span> <span class="o">(</span><span class="n">j</span> <span class="o">==</span> <span class="mi">0</span><span class="o">);</span> <span class="c1">// 判断是否是最后一行:全部文本的最后一行或者行数等于可见的最大的行数</span> <span class="kt">boolean</span> <span class="n">currentLineIsTheLastVisibleOne</span> <span class="o">=</span> <span class="o">(</span><span class="n">j</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">==</span> <span class="n">mMaximumVisibleLineCount</span><span class="o">);</span> <span class="kt">boolean</span> <span class="n">lastLine</span> <span class="o">=</span> <span class="n">currentLineIsTheLastVisibleOne</span> <span class="o">||</span> <span class="o">(</span><span class="n">end</span> <span class="o">==</span> <span class="n">bufEnd</span><span class="o">);</span> <span class="c1">// 第一行需要处理上面的留白</span> <span class="k">if</span> <span class="o">(</span><span class="n">firstLine</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">trackPad</span><span class="o">)</span> <span class="o">{</span> <span class="n">mTopPadding</span> <span class="o">=</span> <span class="n">top</span> <span class="o">-</span> <span class="n">above</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">includePad</span><span class="o">)</span> <span class="o">{</span> <span class="n">above</span> <span class="o">=</span> <span class="n">top</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="kt">int</span> <span class="n">extra</span><span class="o">;</span> <span class="c1">// 最后一行需要处理下面的留白</span> <span class="k">if</span> <span class="o">(</span><span class="n">lastLine</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">trackPad</span><span class="o">)</span> <span class="o">{</span> <span class="n">mBottomPadding</span> <span class="o">=</span> <span class="n">bottom</span> <span class="o">-</span> <span class="n">below</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">includePad</span><span class="o">)</span> <span class="o">{</span> <span class="n">below</span> <span class="o">=</span> <span class="n">bottom</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="c1">// 处理行间距</span> <span class="k">if</span> <span class="o">(</span><span class="n">needMultiply</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">lastLine</span><span class="o">)</span> <span class="o">{</span> <span class="kt">double</span> <span class="n">ex</span> <span class="o">=</span> <span class="o">(</span><span class="n">below</span> <span class="o">-</span> <span class="n">above</span><span class="o">)</span> <span class="o">*</span> <span class="o">(</span><span class="n">spacingmult</span> <span class="o">-</span> <span class="mi">1</span><span class="o">)</span> <span class="o">+</span> <span class="n">spacingadd</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">ex</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span> <span class="n">extra</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)(</span><span class="n">ex</span> <span class="o">+</span> <span class="n">EXTRA_ROUNDING</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">extra</span> <span class="o">=</span> <span class="o">-(</span><span class="kt">int</span><span class="o">)(-</span><span class="n">ex</span> <span class="o">+</span> <span class="n">EXTRA_ROUNDING</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">extra</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>接下来就是记录每行的文本的信息,需要注意到的是,每行的信息由 lines 中的连续的值来记录,值的数量等于 mColumns 的大小( mColumns 的取值前面有提到)。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 记录每行的起止位置,顶部和底部位置</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">START</span><span class="o">]</span> <span class="o">=</span> <span class="n">start</span><span class="o">;</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">TOP</span><span class="o">]</span> <span class="o">=</span> <span class="n">v</span><span class="o">;</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">DESCENT</span><span class="o">]</span> <span class="o">=</span> <span class="n">below</span> <span class="o">+</span> <span class="n">extra</span><span class="o">;</span> <span class="c1">// 记录下一行的起始位置和顶部位置,v 值会作为返回值返回给调用的地方。</span> <span class="n">v</span> <span class="o">+=</span> <span class="o">(</span><span class="n">below</span> <span class="o">-</span> <span class="n">above</span><span class="o">)</span> <span class="o">+</span> <span class="n">extra</span><span class="o">;</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">mColumns</span> <span class="o">+</span> <span class="n">START</span><span class="o">]</span> <span class="o">=</span> <span class="n">end</span><span class="o">;</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">mColumns</span> <span class="o">+</span> <span class="n">TOP</span><span class="o">]</span> <span class="o">=</span> <span class="n">v</span><span class="o">;</span> <span class="c1">// TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining</span> <span class="c1">// one bit for start field</span> <span class="c1">// 通过位运算记录 tab 和文本方向信息</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">TAB</span><span class="o">]</span> <span class="o">|=</span> <span class="n">flags</span> <span class="o">&amp;</span> <span class="n">TAB_MASK</span><span class="o">;</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">HYPHEN</span><span class="o">]</span> <span class="o">=</span> <span class="n">flags</span><span class="o">;</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">DIR</span><span class="o">]</span> <span class="o">|=</span> <span class="n">dir</span> <span class="o">&lt;&lt;</span> <span class="n">DIR_SHIFT</span><span class="o">;</span> <span class="n">Directions</span> <span class="n">linedirs</span> <span class="o">=</span> <span class="n">DIRS_ALL_LEFT_TO_RIGHT</span><span class="o">;</span> <span class="c1">// easy means all chars &lt; the first RTL, so no emoji, no nothing</span> <span class="c1">// XXX a run with no text or all spaces is easy but might be an empty</span> <span class="c1">// RTL paragraph. Make sure easy is false if this is the case.</span> <span class="c1">// 记录文本的方向</span> <span class="k">if</span> <span class="o">(</span><span class="n">easy</span><span class="o">)</span> <span class="o">{</span> <span class="n">mLineDirections</span><span class="o">[</span><span class="n">j</span><span class="o">]</span> <span class="o">=</span> <span class="n">linedirs</span><span class="o">;</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">mLineDirections</span><span class="o">[</span><span class="n">j</span><span class="o">]</span> <span class="o">=</span> <span class="n">AndroidBidi</span><span class="o">.</span><span class="na">directions</span><span class="o">(</span><span class="n">dir</span><span class="o">,</span> <span class="n">chdirs</span><span class="o">,</span> <span class="n">start</span> <span class="o">-</span> <span class="n">widthStart</span><span class="o">,</span> <span class="n">chs</span><span class="o">,</span> <span class="n">start</span> <span class="o">-</span> <span class="n">widthStart</span><span class="o">,</span> <span class="n">end</span> <span class="o">-</span> <span class="n">start</span><span class="o">);</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>文本省略的处理:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 判读是否需要省略</span> <span class="k">if</span> <span class="o">(</span><span class="n">ellipsize</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// If there is only one line, then do any type of ellipsis except when it is MARQUEE</span> <span class="c1">// if there are multiple lines, just allow END ellipsis on the last line</span> <span class="kt">boolean</span> <span class="n">forceEllipsis</span> <span class="o">=</span> <span class="n">moreChars</span> <span class="o">&amp;&amp;</span> <span class="o">(</span><span class="n">mLineCount</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">==</span> <span class="n">mMaximumVisibleLineCount</span><span class="o">);</span> <span class="kt">boolean</span> <span class="n">doEllipsis</span> <span class="o">=</span> <span class="o">(((</span><span class="n">mMaximumVisibleLineCount</span> <span class="o">==</span> <span class="mi">1</span> <span class="o">&amp;&amp;</span> <span class="n">moreChars</span><span class="o">)</span> <span class="o">||</span> <span class="o">(</span><span class="n">firstLine</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">moreChars</span><span class="o">))</span> <span class="o">&amp;&amp;</span> <span class="n">ellipsize</span> <span class="o">!=</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">TruncateAt</span><span class="o">.</span><span class="na">MARQUEE</span><span class="o">)</span> <span class="o">||</span> <span class="o">(!</span><span class="n">firstLine</span> <span class="o">&amp;&amp;</span> <span class="o">(</span><span class="n">currentLineIsTheLastVisibleOne</span> <span class="o">||</span> <span class="o">!</span><span class="n">moreChars</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="n">ellipsize</span> <span class="o">==</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">TruncateAt</span><span class="o">.</span><span class="na">END</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">doEllipsis</span><span class="o">)</span> <span class="o">{</span> <span class="n">calculateEllipsis</span><span class="o">(</span><span class="n">start</span><span class="o">,</span> <span class="n">end</span><span class="o">,</span> <span class="n">widths</span><span class="o">,</span> <span class="n">widthStart</span><span class="o">,</span> <span class="n">ellipsisWidth</span><span class="o">,</span> <span class="n">ellipsize</span><span class="o">,</span> <span class="n">j</span><span class="o">,</span> <span class="n">textWidth</span><span class="o">,</span> <span class="n">paint</span><span class="o">,</span> <span class="n">forceEllipsis</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> <p>如 Google 的工程师注释所说的那样,如果是指定了最大行数是1,则任何省略方式都可以,如果指定的最大行数不是1,但是只有单行文本时,除了 <code class="highlighter-rouge">MARQUEE</code> 的省略方式不支持以外,其他的省略方式都是支持的。如果是多行省略,且不止一行文本时,只支持在可见的最后一行的最后省略,即 <code class="highlighter-rouge">END</code> 省略方式。</p> <p>省略的计算是通过 <code class="highlighter-rouge">calculateEllipsis()</code> 方法实现的,其内部处理完成会将省略的起始位置和计数复制给 mLines 对应的每行数据的第5和第6个数据(省略时每行的记录的数据个数为6个,即 mColumns 赋的值是 COLUMNS_ELLIPSIZE 的值,即6),<code class="highlighter-rouge">calculateEllipsis()</code>方法的实现这里就不作具体分析了。</p> </li> </ol> <h4 id="总结">总结</h4> <p>至此,StaticLayout 的源码大致分析了一遍,后面需要结合 TextView 和 Layout 来具体看一下,文字到底是怎么绘制到屏幕上的。</p> Fri, 05 Aug 2016 00:00:00 +0000 http://jaeger.itscoder.com//android/2016/08/05/staticlayout-source-analyse.html http://jaeger.itscoder.com//android/2016/08/05/staticlayout-source-analyse.html
This XML file does not appear to have any style information associated with it. The document tree is shown below.
<rss xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/" version="2.0">
<channel>
<title>写代码的猴子</title>
<description/>
<link>http://jaeger.itscoder.com/</link>
<atom:link href="http://jaeger.itscoder.com//feed.xml" rel="self" type="application/rss+xml"/>
<item>
<title>GitHub Page 博客自定义域名添加 HTTPS 支持</title>
<description><p>2015 年底,不少互联网公司已经实现了全站 HTTPS 了,如今已经 2017 年了,作为一个开发者,个人博客还没加锁的,不免有点惭愧。最近博客改版,在家打开博客欣赏的时候,发现被糊了一堆牛皮癣,就花了点时间给本站上了 HTTPS。</p> <h3 id="为什么要上-https">为什么要上 HTTPS</h3> <p>关于 HTTPS 就不多介绍了,感兴趣的可以去看下知乎这个问题 <a href="https://www.zhihu.com/question/40371841">为什么 2015 年底各大网站都纷纷用起了 HTTPS? - 知乎</a> 里面提到最多的就是运营商流量劫持,插入牛皮癣广告。</p> <p>下图就是我在家欣赏本站的截图: <img src="/img/postimg/blog_with_ad.jpeg" alt="" /></p> <p>全是治疗脱发的广告,干他大爷的长城宽带 😒</p> <p>我以为就是一时劫持,就没在意了。过了几天,再看看,还是一堆牛皮癣,我就怒了,是时候上 HTTPS 了。</p> <h3 id="上-https-教程">上 HTTPS 教程</h3> <p>搞小程序那会接触了下 HTTPS,那会了解到的 SSL 证书都是需要花钱买的,前阵子看到 <a href="https://certbot.eff.org/">Certbot</a> 的介绍,才知道 <a href="https://letsencrypt.org/">Let’s Encrypt</a> 有提供免费证书的服务:</p> <blockquote> <p>Let’s Encrypt 是一个于2015年三季度推出的数字证书认证机构,将通过旨在消除当前手动创建和安装证书的复杂过程的自动化流程,为安全网站提供免费的SSL/TLS证书。</p> </blockquote> <p>但是 Certbot 适用于博客放在自己主机上的,本站是基于 GitHub 搭建的,因此没法使用 Certbot 服务。如果是直接使用 <code class="highlighter-rouge">username.github.io</code> 这样的域名的话,是默认就支持了 HTTPS 的, 直接访问 <code class="highlighter-rouge">https://username.github.io</code> 即可,自定义域名就需要自己折腾一下了。</p> <p>整个过程也是比较简单的,10 分钟就可以给你的博客加个锁。</p> <p>这里使用 <a href="https://www.netlify.com/">Netlify</a> 提供的服务来完成我们操作。</p> <ol> <li> <p>注册一个 Netlify 帐号,地址 <a href="https://app.netlify.com/signup">Netlify App</a> 选择用 GitHub 注册就好了。</p> </li> <li> <p>添加一个新的站点 <img src="/img/postimg/netlify_add_site.jpg" alt="" /></p> </li> <li> <p>配置站点,简单来说就是添加你的博客仓库地址,然后把博客的构建放在 Netlify 上,按照步骤来即可。 <img src="/img/postimg/netify_select_repo.jpg" alt="" /></p> <p>在最后的 Deploy 步骤中,提示你 Published deploy 就说明好了,直接访问链接,就可以看到你的博客了。 <img src="/img/postimg/deploy_result.jpg" alt="" /></p> </li> <li> <p>点 Back to Deploys 返回到设置页面,在 <code class="highlighter-rouge">Site Details</code> 中可以点击 <code class="highlighter-rouge">Change site name</code>,修改成一个简短点的名字,我这里取名叫 <code class="highlighter-rouge">jaeger</code>,然后就可以通过 <a href="https://jaeger.netlify.com/">https://jaeger.netlify.com/</a> 来访问博客了。</p> </li> <li> <p>设置自己的域名 在 <code class="highlighter-rouge">Domain management</code> 中设置自己的域名,我这里设置成 <code class="highlighter-rouge">jaeger.itscoder.com</code>。 <img src="/img/postimg/domin_setting.jpg" alt="" /></p> </li> <li> <p>在自己的域名管理中设置 DNS 解析,itsCoder 组织的域名使用的是阿里云,在域名管理里面设置如下的域名解析规则:</p> <p><img src="/img/postimg/set_domain_dns.png" alt="" /></p> </li> <li> <p>回到 Netlify ,还是在 <code class="highlighter-rouge">Domain management</code> 中,找到 HTTPS,依次设置如下两个即可,稍等片刻之后,你就发现你的站点加上了小锁了。整个世界都美好了。 <img src="/img/postimg/netlify_https_setting.jpg" alt="" /></p> </li> </ol> <h3 id="最后">最后</h3> <p>欣赏下成果,完美。</p> <p><img src="/img/postimg/laobie_blog.jpg" alt="" /></p> <p>最后再问候下劫持流量插广告的垃圾运营商。</p> <p><img src="/img/postimg/fuck_ad.jpg" alt="" /></p> </description>
<pubDate>Wed, 30 Aug 2017 00:00:00 +0000</pubDate>
<link>http://jaeger.itscoder.com//web/2017/08/30/github-page-https.html</link>
<guid isPermaLink="true">http://jaeger.itscoder.com//web/2017/08/30/github-page-https.html</guid>
</item>
<item>
<title>2016 年过去了</title>
<description><center> <iframe frameborder="no" border="0" marginwidth="0" marginheight="0" width=80% height=90 src="https://music.163.com/outchain/player?type=2&id=31381877&auto=1&height=66"> </iframe> </center> <p>这是一篇迟到的年终总结。2016 年过去了,村上还是没获诺奖,逼哥的专辑一张比一张贵,能听的歌也越来越少,想去看跨年现场也一直未能成行,《行尸走肉》第七季剧情依旧拖沓,一整年好像也没一部值得看第二遍的电影……</p> <p>这篇总结写写删删,还是不知道从何写起。过去的一年好像过得很充实,但仔细想想,好像什么都差了那么点意思。这和自己一直没想清楚自己到底想要做什么有关,没有长远的目标,只知道做好眼前的事。和大学里一样,时间是没浪费,但是是不是都花在值得的地方,就不得而知了。</p> <p>过去的一年好像一直忙于工作,虽说工作一年多了,但是一直也没有过年假。除了几个法定节假日之外,似乎一直过着上五天班休息二天的日子,忙忙歇歇,一年就到了头。</p> <p>憋了半天还是不知道该写点啥,毕业之后好像越来越不爱表达自己了。去年下半年也关闭了微信朋友圈,活跃的社交平台就剩下了微博,闲下来看看段子,转发点技术微博,哪怕是原创的也似乎都和技术有关。封闭自我的结果就是自己度过了一段灰暗的时期,失眠、焦躁、低落,经常是到了凌晨一两点还处在亢奋的状态,无法入睡。然后也是那段时间听郭德纲的相声、听《晓说》,还别说,这些东西还有点意思。后来不知不觉中也就慢慢习惯了,熬夜到 12 点,上床也就能睡着了。</p> <p>还是写点去年的总结吧,改变以前流水帐的形式,几个关键字总结下吧。</p> <h4 id="怀疑对互联网行业的重新认识">怀疑——对互联网行业的重新认识</h4> <p>熟悉我的人都知道我是从机械专业半路出家到计算机行业的,我本以为这个行业是个相对踏实的行业,因为我一直觉得技术上的东西来不得半点虚假和马虎,对的就是对的,错的就是错的。</p> <p>后来我发现我错了。这个行业比起别的传统行业似乎更加浮躁。技术的更新让很多人产生不安,一直处在患得患失的边缘,小程序一出,就各种原生要失业,颠覆技术圈。加上这个圈子是基于互联网的,互联网上该有的不好现象这个圈子也都有:标题党、鸡汤大 V、撕*大战,这个圈子比我想的要乱的多,踏踏实实搞技术的人反倒少了。</p> <p>去年自己也做了几个很初级的开源项目,但是就这么初级的项目,而且也有较为详细的文档说明,很多人还是会提很多让你无语的问题。这样的开源环境下,我觉得倒是培养了更多的“伸手党”。</p> <p>我一度在怀疑自己当初的决定是不是正确的,好在这个行业还是有很多踏实做事的人,写出来的代码还是不会骗人的,自己对写代码这件事还是喜欢的。</p> <h4 id="itscoder我们的组织">itsCoder——我们的组织</h4> <p>这应该是这一年最大的收获。</p> <p>认识了一群志同道合的朋友,虽然很多到现在都没见过面,但心里已经是老朋友了。</p> <p>itsCoder 中的成员有一个最大的共性:都很上进。大家的起点可能有所差异,但是大家都在努力着前行。这也是支撑 WeeklyBlog 项目不到半年多时间产出 80 多篇高质量文章的最大原因。</p> <p>去年由于个人工作和精力有限,对于 itsCoder 的工作还是很不够的,今年也计划制定一些新的规划,让组织一起做点更有意思的事。</p> <h4 id="认识自己">认识自己</h4> <p>越发觉得最难的事情就是认识自己。</p> <p>特别是在外面的声音特别大的时候,一个人很容易就迷失了自己,无法清醒地认识自己。</p> <blockquote> <p>更重要的是,如果以获得更多的人喜欢,成为自己的生存目标,那才是进入了最大的陷阱。</p> </blockquote> <p>这是去年看到的一句话,我一直放在 Keep 中提醒自己。大多数时候,认识自己的方式似乎都是通过别人的评价来衡量自己,别人的夸赞、贬低都会影响对自己的认知,让人产生自我怀疑。</p> <p>我一直在提醒自己:要清楚自己几斤几两,不要迷失自己。</p> <h4 id="新年计划">新年计划</h4> <ol> <li>技术上还是多学习,Android 深入学习,前端技能点继续点亮。</li> <li>补计算机基础,这个是一而再再而三拖着的计划了,今年要开始着手了</li> <li>itsCoder 新项目开展</li> <li>多读书,不限技术,更多地读一些人文方面的书,扩充自己的视野。</li> </ol> <p>2016 年过去了,我似乎并不怀念它。</p> </description>
<pubDate>Sun, 12 Feb 2017 00:00:00 +0000</pubDate>
<link>http://jaeger.itscoder.com//%E9%9A%8F%E7%AC%94/2017/02/12/hello-2017.html</link>
<guid isPermaLink="true">http://jaeger.itscoder.com//%E9%9A%8F%E7%AC%94/2017/02/12/hello-2017.html</guid>
</item>
<item>
<title>自定义选择复制功能的实现</title>
<description><blockquote> <ul> <li>文章来源:itsCoder 的 <a href="https://github.com/itsCoder/weeklyblog">WeeklyBolg</a> 项目</li> <li>itsCoder 主页:<a href="http://itscoder.com/">http://itscoder.com/</a></li> <li>作者:<a href="https://github.com/laobie">写代码的猴子</a></li> <li>审阅者:<a href="https://github.com/jasonim">jasonim (Jiandong Hu)</a></li> </ul> </blockquote> <h3 id="写在前面">写在前面</h3> <p>先来个段子:</p> <blockquote> <p>刚工作时遇到一个特别难搞定的需求,当时没做出来,感到很羞耻。过了几年,再一次遇到这个需求,还是没做出来,只是不再感到羞耻了。</p> </blockquote> <p>在我刚开始工作的时候,也有过一次这样的经历。当时项目中有个需求,让 TextView 中的文本可以选择复制,正常来讲,应该是很容易实现的,直接按照下面的设置就可以了:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mTextView</span><span class="o">.</span><span class="na">setTextIsSelectable</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> </code></pre></div></div> <p>但是,这个简单的实现并不是完美的,主要有几个问题:</p> <ul> <li> <p><strong>不同版本选择复制样式不统一</strong>:在原生系统上 6.0 之前和之后的操作样式是不同的,这里不得不说,6.0 以下的这个选择复制操作交互很不合理,且对应用的界面侵入太多。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/d50f9abab0429d5c.png" alt="" /></p> </li> <li> <p><strong>万恶的国产 ROM 问题</strong>:当时公司测试同事提 bug 反馈,在 vivo 手机上这么设置,长按之后并没有效果。(再一次吐槽乱改系统的国产 ROM,这也是为什么 Android 开发比起 iOS 费事费力的原因之一)</p> </li> <li> <p><strong>可定制性不高</strong>:如果仅仅是一个选择复制的功能,不考虑以上两个问题,还能凑合搞定,但是假如多个需求,选中文字之后直接进行某个操作,比如收藏、发送给好友,此时原生的选择复制功能可能就不足以胜任了。</p> </li> </ul> <p>以上说了这么多,问题的解决办法就是:自己写一个选择复制的功能,这样以上三个问题都能很好地解决了。</p> <p>看起来很容易,但是对于当时刚刚入门的我来说,这是个完全没头绪的任务。</p> <p>时隔一年之后,再遇到这个需求,这次通过 Google、GitHub ,以及参考 SDK 23 中 TextView 源码,基本上实现了自定义选择复制的功能,效果如下:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/378d52583767882d.png" alt="" /></p> <p>保证所有的平台上显示效果一致,弹出的操作菜单可以自己定制,并设置相应的操作。</p> <h3 id="实现要求和要点">实现要求和要点</h3> <p>在开始具体的实现之前,先确定下实现的要求:</p> <ul> <li>尽可能保证和 Android 6.0 原生选择复制一样的交互和基础功能</li> <li>尽可能不需要侵入太多,为了实现选择复制功能,重新自定义 TextView 的方式是不够优雅的,特别是考虑到项目中本来就已经使用了自定义的 TextView ,一旦需求变更,改动成本很大</li> <li>可用的自定义配置</li> </ul> <p>本文最终实现的使用方式如下所示,均满足以上的实现要求:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mSelectableTextHelper</span> <span class="o">=</span> <span class="k">new</span> <span class="n">SelectableTextHelper</span><span class="o">.</span><span class="na">Builder</span><span class="o">(</span><span class="n">mTvTest</span><span class="o">)</span> <span class="o">.</span><span class="na">setSelectedColor</span><span class="o">(</span><span class="n">getResources</span><span class="o">().</span><span class="na">getColor</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">color</span><span class="o">.</span><span class="na">selected_blue</span><span class="o">))</span> <span class="o">.</span><span class="na">setCursorHandleSizeInDp</span><span class="o">(</span><span class="mi">20</span><span class="o">)</span> <span class="o">.</span><span class="na">setCursorHandleColor</span><span class="o">(</span><span class="n">getResources</span><span class="o">().</span><span class="na">getColor</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">color</span><span class="o">.</span><span class="na">cursor_handle_color</span><span class="o">))</span> <span class="o">.</span><span class="na">build</span><span class="o">();</span> </code></pre></div></div> <p>整个自定义的选择复制功能视图上主要有三个部分:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/ea5c7296983d4e68.png" alt="" /></p> <ul> <li>选择游标</li> <li>选中的文本</li> <li>操作框</li> </ul> <p>在具体实现中有以下要点:</p> <ul> <li>自定义选择游标,可以拖动定位选中文本</li> <li>文本的选中状态</li> <li>操作框的显示,以及对应操作的处理</li> <li>在可滑动布局中的特殊处理,例如在 ScrollView 中,当视图滚动时隐藏或者移动选择游标,隐藏操作框,停止滑动时重新显示选择游标和操作框</li> <li>选中文本后,点击 TextView 取消选择</li> </ul> <h3 id="实现思路">实现思路</h3> <p>在开始实践之前,查找资料是少不了的,首先找到了 <a href="http://kymjs.com/code/2016/08/13/01">记划词模块重构感受|开源实验室-张涛</a> 这篇文章,但是这篇文章中更多是提供了一个改进某个开源项目的思路,并没有给出具体的代码,而且连那个开源项目也没给出地址。</p> <p>后来通过搜索关键字,找到了那个开源项目: <a href="https://github.com/zhouray/SelectableTextView">zhouray/SelectableTextView</a></p> <p>如张涛吐槽的那样,这个项目的实现确实不够优雅,主要存在两个问题:</p> <ul> <li>自定义 TextView 实现的,侵入太多</li> <li>解决嵌套在滑动布局中的处理太简单粗暴,竟然自定义了一个 ScrollView 来处理,应用到实际场景中是存在问题的</li> </ul> <p>如果你有时间可以看一下这个项目的代码,在本文后面的实现中,也部分参考了该项目。</p> <p>参考上面提到的文章和开源项目,实现思路基本确定了:</p> <ul> <li>选择游标使用 PopupWindow 实现,并重写 Touch 事件处理逻辑,实现拖动定位选择文本</li> <li>选中文本使用 <code class="highlighter-rouge">BackgroundColorSpan</code> 来显示,比较简单</li> <li>操作框同样使用 PopupWindow 实现,重点是处理好显示的位置</li> </ul> <p>大致的思路确定,接下来就是具体的实现了。</p> <h3 id="具体实现过程">具体实现过程</h3> <p>自定义的选择复制类取名为 <code class="highlighter-rouge">SelectableTextHelper</code>,其有一个字段 <code class="highlighter-rouge">mTextView</code>,持有需要设置选择复制的 <code class="highlighter-rouge">TextView</code> 对象。</p> <h4 id="初步设置">初步设置</h4> <p>由于 <code class="highlighter-rouge">TextView</code> 的文本的 <code class="highlighter-rouge">BufferType</code> 类型是 <code class="highlighter-rouge">SPANNABLE</code> 时才可以设置 Span ,实现选中的效果,因此在一开始先给 TextView 设置下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mTextView</span><span class="o">.</span><span class="na">setText</span><span class="o">(</span><span class="n">mTextView</span><span class="o">.</span><span class="na">getText</span><span class="o">(),</span> <span class="n">TextView</span><span class="o">.</span><span class="na">BufferType</span><span class="o">.</span><span class="na">SPANNABLE</span><span class="o">);</span> </code></pre></div></div> <p>接下来给 TextView 设置相关的点击、长按、Touch 事件:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">mTextView</span><span class="o">.</span><span class="na">setOnLongClickListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnLongClickListener</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">onLongClick</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span> <span class="n">showSelectView</span><span class="o">(</span><span class="n">mTouchX</span><span class="o">,</span> <span class="n">mTouchY</span><span class="o">);</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span> <span class="o">});</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">setOnTouchListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnTouchListener</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">onTouch</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">,</span> <span class="n">MotionEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span> <span class="n">mTouchX</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getX</span><span class="o">();</span> <span class="n">mTouchY</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getY</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="n">mTextView</span><span class="o">.</span><span class="na">setOnClickListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnClickListener</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">onClick</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span> <span class="n">resetSelectionInfo</span><span class="o">();</span> <span class="n">hideSelectView</span><span class="o">();</span> <span class="o">}</span> <span class="o">});</span> </code></pre></div></div> <ul> <li>其中 <code class="highlighter-rouge">onTouch()</code> 记录了触摸点坐标,用于后面的选择文本的位置定位以及选择游标的显示,即传递给 <code class="highlighter-rouge">showSelectView()</code> 方法。</li> <li><code class="highlighter-rouge">onClick()</code> 中的处理比较简单,重置选中文本信息、隐藏选择相关的 View 。</li> </ul> <p>直接看一下 <code class="highlighter-rouge">showSelectView()</code> 和 <code class="highlighter-rouge">hideSelectView()</code> 的实现:</p> <h4 id="显示选择相关组件">显示选择相关组件</h4> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">showSelectView</span><span class="o">(</span><span class="kt">int</span> <span class="n">x</span><span class="o">,</span> <span class="kt">int</span> <span class="n">y</span><span class="o">)</span> <span class="o">{</span> <span class="n">hideSelectView</span><span class="o">();</span> <span class="n">resetSelectionInfo</span><span class="o">();</span> <span class="n">isHide</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">mStartHandle</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="n">mStartHandle</span> <span class="o">=</span> <span class="k">new</span> <span class="n">CursorHandle</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">mEndHandle</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="n">mEndHandle</span> <span class="o">=</span> <span class="k">new</span> <span class="n">CursorHandle</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span> <span class="kt">int</span> <span class="n">startOffset</span> <span class="o">=</span> <span class="n">TextLayoutUtil</span><span class="o">.</span><span class="na">getPreciseOffset</span><span class="o">(</span><span class="n">mTextView</span><span class="o">,</span> <span class="n">x</span><span class="o">,</span> <span class="n">y</span><span class="o">);</span> <span class="kt">int</span> <span class="n">endOffset</span> <span class="o">=</span> <span class="n">startOffset</span> <span class="o">+</span> <span class="n">DEFAULT_SELECTION_LENGTH</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">mTextView</span><span class="o">.</span><span class="na">getText</span><span class="o">()</span> <span class="k">instanceof</span> <span class="n">Spannable</span><span class="o">)</span> <span class="o">{</span> <span class="n">mSpannable</span> <span class="o">=</span> <span class="o">(</span><span class="n">Spannable</span><span class="o">)</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getText</span><span class="o">();</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mSpannable</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">startOffset</span> <span class="o">&gt;=</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getText</span><span class="o">().</span><span class="na">length</span><span class="o">())</span> <span class="o">{</span> <span class="k">return</span><span class="o">;</span> <span class="o">}</span> <span class="n">selectText</span><span class="o">(</span><span class="n">startOffset</span><span class="o">,</span> <span class="n">endOffset</span><span class="o">);</span> <span class="n">showCursorHandle</span><span class="o">(</span><span class="n">mStartHandle</span><span class="o">);</span> <span class="n">showCursorHandle</span><span class="o">(</span><span class="n">mEndHandle</span><span class="o">);</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">show</span><span class="o">();</span> <span class="o">}</span> </code></pre></div></div> <ul> <li>在 show 方法开始,因为之前可能已经显示了选择相关的 View ,比如先长按 TextView 的 A 点,然后弹出选择游标、操作框,此时再长按 B 点,此时再次弹出选择游标和操作框时,就需要先隐藏之前的相关 View 了,这里就这样简单粗暴地处理了下。</li> <li><code class="highlighter-rouge">int startOffset = TextLayoutUtil.getPreciseOffset(mTextView, x, y);</code> 是一个很有意思的地方,这里参考了前面提到的开源项目里面的实现,这个方法通过传入 TextView 中一个点的坐标,就可以计算出来对应的最接近的那个文字的索引,简单说明如下:</li> </ul> <p><img src="http://ac-qygvx1cc.clouddn.com/766d4fc4e12099222bc2.jpeg" alt="" /></p> <p>通过传入『种』那个字附近的某个点的坐标 (x,y),就可以得出『种』在 TextView 的文本中的索引是 9 (从 0 开始计数)。</p> <p><code class="highlighter-rouge">TextLayoutUtil.getPreciseOffset()</code> 方法如下:</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">int</span> <span class="nf">getPreciseOffset</span><span class="o">(</span><span class="n">TextView</span> <span class="n">textView</span><span class="o">,</span> <span class="kt">int</span> <span class="n">x</span><span class="o">,</span> <span class="kt">int</span> <span class="n">y</span><span class="o">)</span> <span class="o">{</span> <span class="n">Layout</span> <span class="n">layout</span> <span class="o">=</span> <span class="n">textView</span><span class="o">.</span><span class="na">getLayout</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">layout</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span> <span class="n">topVisibleLine</span> <span class="o">=</span> <span class="n">layout</span><span class="o">.</span><span class="na">getLineForVertical</span><span class="o">(</span><span class="n">y</span><span class="o">);</span> <span class="kt">int</span> <span class="n">offset</span> <span class="o">=</span> <span class="n">layout</span><span class="o">.</span><span class="na">getOffsetForHorizontal</span><span class="o">(</span><span class="n">topVisibleLine</span><span class="o">,</span> <span class="n">x</span><span class="o">);</span> <span class="kt">int</span> <span class="n">offsetX</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">layout</span><span class="o">.</span><span class="na">getPrimaryHorizontal</span><span class="o">(</span><span class="n">offset</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">offsetX</span> <span class="o">&gt;</span> <span class="n">x</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">layout</span><span class="o">.</span><span class="na">getOffsetToLeftOf</span><span class="o">(</span><span class="n">offset</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="k">return</span> <span class="n">offset</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="k">return</span> <span class="o">-</span><span class="mi">1</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>这里涉及到 TextView 的文本布局类 Layout ,虽然看过这块的部分源码,但是这里的处理还是有点懵,本文就不多深入了,有兴趣的话可以自行了解下这块的源码。</p> <ul> <li> <p>文本的选中显示是在 <code class="highlighter-rouge">selectText()</code> 方法中处理的,重点是设置 Span 和记录选中的文本信息:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">selectText</span><span class="o">(</span><span class="kt">int</span> <span class="n">startPos</span><span class="o">,</span> <span class="kt">int</span> <span class="n">endPos</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">startPos</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span><span class="o">)</span> <span class="o">{</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span> <span class="o">=</span> <span class="n">startPos</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">endPos</span> <span class="o">!=</span> <span class="o">-</span><span class="mi">1</span><span class="o">)</span> <span class="o">{</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span> <span class="o">=</span> <span class="n">endPos</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span> <span class="o">&gt;</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span> <span class="n">temp</span> <span class="o">=</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">;</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span> <span class="o">=</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">;</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span> <span class="o">=</span> <span class="n">temp</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mSpannable</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">mSpan</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mSpan</span> <span class="o">=</span> <span class="k">new</span> <span class="n">BackgroundColorSpan</span><span class="o">(</span><span class="n">mSelectedColor</span><span class="o">);</span> <span class="o">}</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mSelectionContent</span> <span class="o">=</span> <span class="n">mSpannable</span><span class="o">.</span><span class="na">subSequence</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">,</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">).</span><span class="na">toString</span><span class="o">();</span> <span class="n">mSpannable</span><span class="o">.</span><span class="na">setSpan</span><span class="o">(</span><span class="n">mSpan</span><span class="o">,</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">,</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">,</span> <span class="n">Spanned</span><span class="o">.</span><span class="na">SPAN_INCLUSIVE_EXCLUSIVE</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">mSelectListener</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mSelectListener</span><span class="o">.</span><span class="na">onTextSelected</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mSelectionContent</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> <p>其中处理了下可能存在的 endPos 小于 startPos 的情况,进行了一次交换,后面就是设置 <code class="highlighter-rouge">BackgroundColorSpan</code> 已经记录下选中文本的信息,已经设置了选中监听时的回调。</p> <p>其中 mSelectionInfo 是 <code class="highlighter-rouge">SelectionInfo</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">SelectionInfo</span> <span class="o">{</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">mStart</span><span class="o">;</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">mEnd</span><span class="o">;</span> <span class="kd">public</span> <span class="n">String</span> <span class="n">mSelectionContent</span><span class="o">;</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p><code class="highlighter-rouge">showCursorHandle()</code> 方法顾名思义就是显示选择游标,因为是 PopupWindow 实现的,重点就是显示位置的确定,这里再次涉及到 Layout 相关的 API :</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">showCursorHandle</span><span class="o">(</span><span class="n">CursorHandle</span> <span class="n">cursorHandle</span><span class="o">)</span> <span class="o">{</span> <span class="n">Layout</span> <span class="n">layout</span> <span class="o">=</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getLayout</span><span class="o">();</span> <span class="kt">int</span> <span class="n">offset</span> <span class="o">=</span> <span class="n">cursorHandle</span><span class="o">.</span><span class="na">isLeft</span> <span class="o">?</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span> <span class="o">:</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">;</span> <span class="n">cursorHandle</span><span class="o">.</span><span class="na">show</span><span class="o">((</span><span class="kt">int</span><span class="o">)</span> <span class="n">layout</span><span class="o">.</span><span class="na">getPrimaryHorizontal</span><span class="o">(</span><span class="n">offset</span><span class="o">),</span> <span class="n">layout</span><span class="o">.</span><span class="na">getLineBottom</span><span class="o">(</span><span class="n">layout</span><span class="o">.</span><span class="na">getLineForOffset</span><span class="o">(</span><span class="n">offset</span><span class="o">)));</span> <span class="o">}</span> </code></pre></div> </div> <p>这里和之前的是反的,通过文本中的文字索引,来获取到对应的点的坐标。然后显示 PopupWindow 即可。</p> </li> <li> <p>最后是显示操作框,同样是一个 PopupWindow ,这里的细节后面再展开。</p> </li> </ul> <h4 id="隐藏选择相关组件">隐藏选择相关组件</h4> <p>这里没啥好说的,就是判空下左右选择游标和操作框,如果非空,则调用对应的 <code class="highlighter-rouge">dismiss()</code> 方法</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">hideSelectView</span><span class="o">()</span> <span class="o">{</span> <span class="n">isHide</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">mStartHandle</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mStartHandle</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mEndHandle</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mEndHandle</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mOperateWindow</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>这里基本的流程和相关的实现细节已大概讲述了下,接下来就是就是选择游标和操作框的实现。</p> <h4 id="选择游标">选择游标</h4> <p>由于游标的移动涉及到文字的选中,以及操作框的显隐、定位,就直接实现为 <code class="highlighter-rouge">SelectableTextHelper</code> 的内部类。直接上代码:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">class</span> <span class="nc">CursorHandle</span> <span class="kd">extends</span> <span class="n">View</span> <span class="o">{</span> <span class="kd">private</span> <span class="n">PopupWindow</span> <span class="n">mPopupWindow</span><span class="o">;</span> <span class="kd">private</span> <span class="n">Paint</span> <span class="n">mPaint</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mCircleRadius</span> <span class="o">=</span> <span class="n">mCursorHandleSize</span> <span class="o">/</span> <span class="mi">2</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mWidth</span> <span class="o">=</span> <span class="n">mCircleRadius</span> <span class="o">*</span> <span class="mi">2</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mHeight</span> <span class="o">=</span> <span class="n">mCircleRadius</span> <span class="o">*</span> <span class="mi">2</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mPadding</span> <span class="o">=</span> <span class="mi">25</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">isLeft</span><span class="o">;</span> <span class="kd">public</span> <span class="nf">CursorHandle</span><span class="o">(</span><span class="kt">boolean</span> <span class="n">isLeft</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">(</span><span class="n">mContext</span><span class="o">);</span> <span class="k">this</span><span class="o">.</span><span class="na">isLeft</span> <span class="o">=</span> <span class="n">isLeft</span><span class="o">;</span> <span class="n">mPaint</span> <span class="o">=</span> <span class="k">new</span> <span class="n">Paint</span><span class="o">(</span><span class="n">Paint</span><span class="o">.</span><span class="na">ANTI_ALIAS_FLAG</span><span class="o">);</span> <span class="n">mPaint</span><span class="o">.</span><span class="na">setColor</span><span class="o">(</span><span class="n">mCursorHandleColor</span><span class="o">);</span> <span class="n">mPopupWindow</span> <span class="o">=</span> <span class="k">new</span> <span class="n">PopupWindow</span><span class="o">(</span><span class="k">this</span><span class="o">);</span> <span class="n">mPopupWindow</span><span class="o">.</span><span class="na">setClippingEnabled</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span> <span class="n">mPopupWindow</span><span class="o">.</span><span class="na">setWidth</span><span class="o">(</span><span class="n">mWidth</span> <span class="o">+</span> <span class="n">mPadding</span> <span class="o">*</span> <span class="mi">2</span><span class="o">);</span> <span class="n">mPopupWindow</span><span class="o">.</span><span class="na">setHeight</span><span class="o">(</span><span class="n">mHeight</span> <span class="o">+</span> <span class="n">mPadding</span> <span class="o">/</span> <span class="mi">2</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">onDraw</span><span class="o">(</span><span class="n">Canvas</span> <span class="n">canvas</span><span class="o">)</span> <span class="o">{</span> <span class="n">canvas</span><span class="o">.</span><span class="na">drawCircle</span><span class="o">(</span><span class="n">mCircleRadius</span> <span class="o">+</span> <span class="n">mPadding</span><span class="o">,</span> <span class="n">mCircleRadius</span><span class="o">,</span> <span class="n">mCircleRadius</span><span class="o">,</span> <span class="n">mPaint</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">isLeft</span><span class="o">)</span> <span class="o">{</span> <span class="n">canvas</span><span class="o">.</span><span class="na">drawRect</span><span class="o">(</span><span class="n">mCircleRadius</span> <span class="o">+</span> <span class="n">mPadding</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">mCircleRadius</span> <span class="o">*</span> <span class="mi">2</span> <span class="o">+</span> <span class="n">mPadding</span><span class="o">,</span> <span class="n">mCircleRadius</span><span class="o">,</span> <span class="n">mPaint</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">canvas</span><span class="o">.</span><span class="na">drawRect</span><span class="o">(</span><span class="n">mPadding</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">mCircleRadius</span> <span class="o">+</span> <span class="n">mPadding</span><span class="o">,</span> <span class="n">mCircleRadius</span><span class="o">,</span> <span class="n">mPaint</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>直接继承 PopupWindow 的话,没有 onDraw 方法 ,这里直接继承 View ,然后在 CursorHandle 的构造函数中初始化了一个 PopupWindow ,并将 CursorHandle 实例作为 contentView 传递进去,然后在 <code class="highlighter-rouge">onDraw()</code> 方法中绘制了自定义的选择游标,仿照 6.0 的选择游标效果。</p> <p><img src="http://ac-qygvx1cc.clouddn.com/ba2a7ee85d2d0915c2bb.svg" alt="" /></p> <p>这个也是绘制起来也是很简单的,一个正方形和一个圆组合下即可,处理下是左边还是右边就可以了,具体参照上面的代码。</p> <p>接下来就是设置相关的触摸事件,响应拖动游标时更新选中的文本。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">int</span> <span class="n">mAdjustX</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mAdjustY</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mBeforeDragStart</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mBeforeDragEnd</span><span class="o">;</span> <span class="nd">@Override</span> <span class="kd">public</span> <span class="kt">boolean</span> <span class="nf">onTouchEvent</span><span class="o">(</span><span class="n">MotionEvent</span> <span class="n">event</span><span class="o">)</span> <span class="o">{</span> <span class="k">switch</span> <span class="o">(</span><span class="n">event</span><span class="o">.</span><span class="na">getAction</span><span class="o">())</span> <span class="o">{</span> <span class="k">case</span> <span class="n">MotionEvent</span><span class="o">.</span><span class="na">ACTION_DOWN</span><span class="o">:</span> <span class="n">mBeforeDragStart</span> <span class="o">=</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">;</span> <span class="n">mBeforeDragEnd</span> <span class="o">=</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">;</span> <span class="n">mAdjustX</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getX</span><span class="o">();</span> <span class="n">mAdjustY</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getY</span><span class="o">();</span> <span class="k">break</span><span class="o">;</span> <span class="k">case</span> <span class="n">MotionEvent</span><span class="o">.</span><span class="na">ACTION_UP</span><span class="o">:</span> <span class="k">case</span> <span class="n">MotionEvent</span><span class="o">.</span><span class="na">ACTION_CANCEL</span><span class="o">:</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">show</span><span class="o">();</span> <span class="k">break</span><span class="o">;</span> <span class="k">case</span> <span class="n">MotionEvent</span><span class="o">.</span><span class="na">ACTION_MOVE</span><span class="o">:</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="kt">int</span> <span class="n">rawX</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getRawX</span><span class="o">();</span> <span class="kt">int</span> <span class="n">rawY</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">event</span><span class="o">.</span><span class="na">getRawY</span><span class="o">();</span> <span class="n">update</span><span class="o">(</span><span class="n">rawX</span> <span class="o">+</span> <span class="n">mAdjustX</span> <span class="o">-</span> <span class="n">mWidth</span><span class="o">,</span> <span class="n">rawY</span> <span class="o">+</span> <span class="n">mAdjustY</span> <span class="o">-</span> <span class="n">mHeight</span><span class="o">);</span> <span class="k">break</span><span class="o">;</span> <span class="o">}</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span> </code></pre></div></div> <ul> <li> <p>在游标移动时,隐藏操作框,停止移动时,再显示操作框。</p> </li> <li> <p>在触摸发生移动时,即 <code class="highlighter-rouge">MotionEvent.ACTION_MOVE</code> 时,更新游标位置和选中的文本,<code class="highlighter-rouge">update()</code> 方法如下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">mTempCoors</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="mi">2</span><span class="o">];</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">update</span><span class="o">(</span><span class="kt">int</span> <span class="n">x</span><span class="o">,</span> <span class="kt">int</span> <span class="n">y</span><span class="o">)</span> <span class="o">{</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getLocationInWindow</span><span class="o">(</span><span class="n">mTempCoors</span><span class="o">);</span> <span class="kt">int</span> <span class="n">oldOffset</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">isLeft</span><span class="o">)</span> <span class="o">{</span> <span class="n">oldOffset</span> <span class="o">=</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">;</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">oldOffset</span> <span class="o">=</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">;</span> <span class="o">}</span> <span class="n">y</span> <span class="o">-=</span> <span class="n">mTempCoors</span><span class="o">[</span><span class="mi">1</span><span class="o">];</span> <span class="kt">int</span> <span class="n">offset</span> <span class="o">=</span> <span class="n">TextLayoutUtil</span><span class="o">.</span><span class="na">getHysteresisOffset</span><span class="o">(</span><span class="n">mTextView</span><span class="o">,</span> <span class="n">x</span><span class="o">,</span> <span class="n">y</span><span class="o">,</span> <span class="n">oldOffset</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">offset</span> <span class="o">!=</span> <span class="n">oldOffset</span><span class="o">)</span> <span class="o">{</span> <span class="n">resetSelectionInfo</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">isLeft</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">offset</span> <span class="o">&gt;</span> <span class="n">mBeforeDragEnd</span><span class="o">)</span> <span class="o">{</span> <span class="n">CursorHandle</span> <span class="n">handle</span> <span class="o">=</span> <span class="n">getCursorHandle</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span> <span class="n">changeDirection</span><span class="o">();</span> <span class="n">handle</span><span class="o">.</span><span class="na">changeDirection</span><span class="o">();</span> <span class="n">mBeforeDragStart</span> <span class="o">=</span> <span class="n">mBeforeDragEnd</span><span class="o">;</span> <span class="n">selectText</span><span class="o">(</span><span class="n">mBeforeDragEnd</span><span class="o">,</span> <span class="n">offset</span><span class="o">);</span> <span class="n">handle</span><span class="o">.</span><span class="na">updateCursorHandle</span><span class="o">();</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">selectText</span><span class="o">(</span><span class="n">offset</span><span class="o">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">);</span> <span class="o">}</span> <span class="n">updateCursorHandle</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">offset</span> <span class="o">&lt;</span> <span class="n">mBeforeDragStart</span><span class="o">)</span> <span class="o">{</span> <span class="n">CursorHandle</span> <span class="n">handle</span> <span class="o">=</span> <span class="n">getCursorHandle</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> <span class="n">handle</span><span class="o">.</span><span class="na">changeDirection</span><span class="o">();</span> <span class="n">changeDirection</span><span class="o">();</span> <span class="n">mBeforeDragEnd</span> <span class="o">=</span> <span class="n">mBeforeDragStart</span><span class="o">;</span> <span class="n">selectText</span><span class="o">(</span><span class="n">offset</span><span class="o">,</span> <span class="n">mBeforeDragStart</span><span class="o">);</span> <span class="n">handle</span><span class="o">.</span><span class="na">updateCursorHandle</span><span class="o">();</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">selectText</span><span class="o">(</span><span class="n">mBeforeDragStart</span><span class="o">,</span> <span class="n">offset</span><span class="o">);</span> <span class="o">}</span> <span class="n">updateCursorHandle</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> <p>在一开始的实现中,<code class="highlighter-rouge">update()</code> 方法没这么复杂,但是考虑到左边的游标在移动到右边游标的右边时,如下面的动图所示:</p> <p><img src="http://ww2.sinaimg.cn/large/91e23208jw1f9s285jsn8g20900g0gop.gif" alt="" /></p> <p>此时就需要多一点处理,左边的右边变右边,右边的游标变左边,同时选中的文本也需要重新变换起点位置,原来是 end ,现在则变成了 start 。</p> <p>具体的逻辑实现就是根据之前选中的文本的前后位置信息,进行前后位置的交换。同时调整游标的方向,更新视图,这个逻辑在 <code class="highlighter-rouge">changeDirection()</code> 方法中:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">changeDirection</span><span class="o">()</span> <span class="o">{</span> <span class="n">isLeft</span> <span class="o">=</span> <span class="o">!</span><span class="n">isLeft</span><span class="o">;</span> <span class="n">invalidate</span><span class="o">();</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>更新选择游标位置:由于游标的位置处理成只和选中的文本有关,因而处理起来较为简单,在上面的反转变化中,只要选中的文本正确变化了,那么这里的游标位置更新就是正确的。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">updateCursorHandle</span><span class="o">()</span> <span class="o">{</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getLocationInWindow</span><span class="o">(</span><span class="n">mTempCoors</span><span class="o">);</span> <span class="n">Layout</span> <span class="n">layout</span> <span class="o">=</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getLayout</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">isLeft</span><span class="o">)</span> <span class="o">{</span> <span class="n">mPopupWindow</span><span class="o">.</span><span class="na">update</span><span class="o">((</span><span class="kt">int</span><span class="o">)</span> <span class="n">layout</span><span class="o">.</span><span class="na">getPrimaryHorizontal</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">)</span> <span class="o">-</span> <span class="n">mWidth</span> <span class="o">+</span> <span class="n">getExtraX</span><span class="o">(),</span> <span class="n">layout</span><span class="o">.</span><span class="na">getLineBottom</span><span class="o">(</span><span class="n">layout</span><span class="o">.</span><span class="na">getLineForOffset</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">))</span> <span class="o">+</span> <span class="n">getExtraY</span><span class="o">(),</span> <span class="o">-</span><span class="mi">1</span><span class="o">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">mPopupWindow</span><span class="o">.</span><span class="na">update</span><span class="o">((</span><span class="kt">int</span><span class="o">)</span> <span class="n">layout</span><span class="o">.</span><span class="na">getPrimaryHorizontal</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">)</span> <span class="o">+</span> <span class="n">getExtraX</span><span class="o">(),</span> <span class="n">layout</span><span class="o">.</span><span class="na">getLineBottom</span><span class="o">(</span><span class="n">layout</span><span class="o">.</span><span class="na">getLineForOffset</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mEnd</span><span class="o">))</span> <span class="o">+</span> <span class="n">getExtraY</span><span class="o">(),</span> <span class="o">-</span><span class="mi">1</span><span class="o">,</span> <span class="o">-</span><span class="mi">1</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> </li> </ul> <h4 id="操作框">操作框</h4> <p>操作框的实现则简单的多,就是自定义布局的 PopupWindow ,然后处理下内部的 View 的点击事件即可,直接贴代码:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kd">class</span> <span class="nc">OperateWindow</span> <span class="o">{</span> <span class="kd">private</span> <span class="n">PopupWindow</span> <span class="n">mWindow</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">mTempCoors</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="mi">2</span><span class="o">];</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mWidth</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">int</span> <span class="n">mHeight</span><span class="o">;</span> <span class="kd">public</span> <span class="nf">OperateWindow</span><span class="o">(</span><span class="kd">final</span> <span class="n">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span> <span class="n">View</span> <span class="n">contentView</span> <span class="o">=</span> <span class="n">LayoutInflater</span><span class="o">.</span><span class="na">from</span><span class="o">(</span><span class="n">context</span><span class="o">).</span><span class="na">inflate</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">layout</span><span class="o">.</span><span class="na">layout_operate_windows</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span> <span class="n">contentView</span><span class="o">.</span><span class="na">measure</span><span class="o">(</span><span class="n">View</span><span class="o">.</span><span class="na">MeasureSpec</span><span class="o">.</span><span class="na">makeMeasureSpec</span><span class="o">(</span><span class="n">Integer</span><span class="o">.</span><span class="na">MAX_VALUE</span> <span class="o">&gt;&gt;</span> <span class="mi">2</span><span class="o">,</span> <span class="n">View</span><span class="o">.</span><span class="na">MeasureSpec</span><span class="o">.</span><span class="na">AT_MOST</span><span class="o">),</span> <span class="n">View</span><span class="o">.</span><span class="na">MeasureSpec</span><span class="o">.</span><span class="na">makeMeasureSpec</span><span class="o">(</span><span class="n">Integer</span><span class="o">.</span><span class="na">MAX_VALUE</span> <span class="o">&gt;&gt;</span> <span class="mi">2</span><span class="o">,</span> <span class="n">View</span><span class="o">.</span><span class="na">MeasureSpec</span><span class="o">.</span><span class="na">AT_MOST</span><span class="o">));</span> <span class="n">mWidth</span> <span class="o">=</span> <span class="n">contentView</span><span class="o">.</span><span class="na">getMeasuredWidth</span><span class="o">();</span> <span class="n">mHeight</span> <span class="o">=</span> <span class="n">contentView</span><span class="o">.</span><span class="na">getMeasuredHeight</span><span class="o">();</span> <span class="n">mWindow</span> <span class="o">=</span> <span class="k">new</span> <span class="nf">PopupWindow</span><span class="o">(</span><span class="n">contentView</span><span class="o">,</span> <span class="n">ViewGroup</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">ViewGroup</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="kc">false</span><span class="o">);</span> <span class="n">mWindow</span><span class="o">.</span><span class="na">setClippingEnabled</span><span class="o">(</span><span class="kc">false</span><span class="o">);</span> <span class="n">contentView</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">tv_copy</span><span class="o">).</span><span class="na">setOnClickListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnClickListener</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">onClick</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span> <span class="n">ClipboardManager</span> <span class="n">clip</span> <span class="o">=</span> <span class="o">(</span><span class="n">ClipboardManager</span><span class="o">)</span> <span class="n">mContext</span><span class="o">.</span><span class="na">getSystemService</span><span class="o">(</span><span class="n">Context</span><span class="o">.</span><span class="na">CLIPBOARD_SERVICE</span><span class="o">);</span> <span class="n">clip</span><span class="o">.</span><span class="na">setPrimaryClip</span><span class="o">(</span> <span class="n">ClipData</span><span class="o">.</span><span class="na">newPlainText</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mSelectionContent</span><span class="o">,</span> <span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mSelectionContent</span><span class="o">));</span> <span class="k">if</span> <span class="o">(</span><span class="n">mSelectListener</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mSelectListener</span><span class="o">.</span><span class="na">onTextSelected</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mSelectionContent</span><span class="o">);</span> <span class="o">}</span> <span class="n">SelectableTextHelper</span><span class="o">.</span><span class="na">this</span><span class="o">.</span><span class="na">resetSelectionInfo</span><span class="o">();</span> <span class="n">SelectableTextHelper</span><span class="o">.</span><span class="na">this</span><span class="o">.</span><span class="na">hideSelectView</span><span class="o">();</span> <span class="o">}</span> <span class="o">});</span> <span class="n">contentView</span><span class="o">.</span><span class="na">findViewById</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">tv_select_all</span><span class="o">).</span><span class="na">setOnClickListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnClickListener</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">onClick</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span> <span class="n">hideSelectView</span><span class="o">();</span> <span class="n">selectText</span><span class="o">(</span><span class="mi">0</span><span class="o">,</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getText</span><span class="o">().</span><span class="na">length</span><span class="o">());</span> <span class="n">isHide</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span> <span class="n">showCursorHandle</span><span class="o">(</span><span class="n">mStartHandle</span><span class="o">);</span> <span class="n">showCursorHandle</span><span class="o">(</span><span class="n">mEndHandle</span><span class="o">);</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">show</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">show</span><span class="o">()</span> <span class="o">{</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getLocationInWindow</span><span class="o">(</span><span class="n">mTempCoors</span><span class="o">);</span> <span class="n">Layout</span> <span class="n">layout</span> <span class="o">=</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getLayout</span><span class="o">();</span> <span class="kt">int</span> <span class="n">posX</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)</span> <span class="n">layout</span><span class="o">.</span><span class="na">getPrimaryHorizontal</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">)</span> <span class="o">+</span> <span class="n">mTempCoors</span><span class="o">[</span><span class="mi">0</span><span class="o">];</span> <span class="kt">int</span> <span class="n">posY</span> <span class="o">=</span> <span class="n">layout</span><span class="o">.</span><span class="na">getLineTop</span><span class="o">(</span><span class="n">layout</span><span class="o">.</span><span class="na">getLineForOffset</span><span class="o">(</span><span class="n">mSelectionInfo</span><span class="o">.</span><span class="na">mStart</span><span class="o">))</span> <span class="o">+</span> <span class="n">mTempCoors</span><span class="o">[</span><span class="mi">1</span><span class="o">]</span> <span class="o">-</span> <span class="n">mHeight</span> <span class="o">-</span> <span class="mi">16</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">posX</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="o">)</span> <span class="n">posX</span> <span class="o">=</span> <span class="mi">16</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">posY</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="o">)</span> <span class="n">posY</span> <span class="o">=</span> <span class="mi">16</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">posX</span> <span class="o">+</span> <span class="n">mWidth</span> <span class="o">&gt;</span> <span class="n">TextLayoutUtil</span><span class="o">.</span><span class="na">getScreenWidth</span><span class="o">(</span><span class="n">mContext</span><span class="o">))</span> <span class="o">{</span> <span class="n">posX</span> <span class="o">=</span> <span class="n">TextLayoutUtil</span><span class="o">.</span><span class="na">getScreenWidth</span><span class="o">(</span><span class="n">mContext</span><span class="o">)</span> <span class="o">-</span> <span class="n">mWidth</span> <span class="o">-</span> <span class="mi">16</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">Build</span><span class="o">.</span><span class="na">VERSION</span><span class="o">.</span><span class="na">SDK_INT</span> <span class="o">&gt;=</span> <span class="n">Build</span><span class="o">.</span><span class="na">VERSION_CODES</span><span class="o">.</span><span class="na">LOLLIPOP</span><span class="o">)</span> <span class="o">{</span> <span class="n">mWindow</span><span class="o">.</span><span class="na">setElevation</span><span class="o">(</span><span class="mi">8</span><span class="n">f</span><span class="o">);</span> <span class="o">}</span> <span class="n">mWindow</span><span class="o">.</span><span class="na">showAtLocation</span><span class="o">(</span><span class="n">mTextView</span><span class="o">,</span> <span class="n">Gravity</span><span class="o">.</span><span class="na">NO_GRAVITY</span><span class="o">,</span> <span class="n">posX</span><span class="o">,</span> <span class="n">posY</span><span class="o">);</span> <span class="o">}</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">dismiss</span><span class="o">()</span> <span class="o">{</span> <span class="n">mWindow</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>在显示的之后,判断了下是否会显示到屏幕外面,如果会超出屏幕,则做一下微调即可。</p> <h3 id="一些细节的处理">一些细节的处理</h3> <h4 id="嵌套在滚动视图中的处理">嵌套在滚动视图中的处理</h4> <p>在一开始的实现要点中就提到,需要注意一下嵌套在滚动视图中的处理,在尝试了一些方法之后,最终直接设置 <code class="highlighter-rouge">OnScrollChangedListener</code> 来解决,具体代码如下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mOnScrollChangedListener</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ViewTreeObserver</span><span class="o">.</span><span class="na">OnScrollChangedListener</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">onScrollChanged</span><span class="o">()</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(!</span><span class="n">isHideWhenScroll</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">isHide</span><span class="o">)</span> <span class="o">{</span> <span class="n">isHideWhenScroll</span> <span class="o">=</span> <span class="kc">true</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">mOperateWindow</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mStartHandle</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mStartHandle</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mEndHandle</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mEndHandle</span><span class="o">.</span><span class="na">dismiss</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> <span class="o">};</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getViewTreeObserver</span><span class="o">().</span><span class="na">addOnScrollChangedListener</span><span class="o">(</span><span class="n">mOnScrollChangedListener</span><span class="o">);</span> </code></pre></div></div> <p>这倒是解决了滑动时可以隐藏相关的选择控件的问题,但是停止滚动之后呢,如何重新显示选择控件呢?</p> <p>在经过一些尝试之后,发现了 <code class="highlighter-rouge">OnPreDrawListener</code> 这个接口,在 TextView 发生滚动时期间一直在被调用,因此在这个接口里处理重新显示选择控件的逻辑是合适的:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mOnPreDrawListener</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ViewTreeObserver</span><span class="o">.</span><span class="na">OnPreDrawListener</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">onPreDraw</span><span class="o">()</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">isHideWhenScroll</span><span class="o">)</span> <span class="o">{</span> <span class="n">isHideWhenScroll</span> <span class="o">=</span> <span class="kc">false</span><span class="o">;</span> <span class="n">showSelectView</span><span class="o">();</span> <span class="o">}</span> <span class="k">return</span> <span class="kc">true</span><span class="o">;</span> <span class="o">}</span> <span class="o">};</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getViewTreeObserver</span><span class="o">().</span><span class="na">addOnPreDrawListener</span><span class="o">(</span><span class="n">mOnPreDrawListener</span><span class="o">);</span> </code></pre></div></div> <p>在这样的设置之后,确实能保证停止滚动时重新显示选择相关的控件,但是整个滚动过程变得异常卡顿。</p> <p>原因其实很简单,前面也提到了,<code class="highlighter-rouge">onPreDraw</code> 方法在 TextView 发生滚动时期间一直在被调用,然后这里一直处理显示选择控件的逻辑,能不卡顿么?</p> <p>最后的解决方法是在源码中找到的,将 <code class="highlighter-rouge">showSelectView()</code> 方法替换成 <code class="highlighter-rouge">postShowSelectView()</code> 方法,</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">private</span> <span class="kt">void</span> <span class="nf">postShowSelectView</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="n">mTextView</span><span class="o">.</span><span class="na">removeCallbacks</span><span class="o">(</span><span class="n">mShowSelectViewRunnable</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">duration</span> <span class="o">&lt;=</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span> <span class="n">mShowSelectViewRunnable</span><span class="o">.</span><span class="na">run</span><span class="o">();</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">postDelayed</span><span class="o">(</span><span class="n">mShowSelectViewRunnable</span><span class="o">,</span> <span class="n">duration</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="n">Runnable</span> <span class="n">mShowSelectViewRunnable</span> <span class="o">=</span> <span class="k">new</span> <span class="n">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="k">if</span> <span class="o">(</span><span class="n">isHide</span><span class="o">)</span> <span class="k">return</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">mOperateWindow</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">mOperateWindow</span><span class="o">.</span><span class="na">show</span><span class="o">();</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mStartHandle</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">showCursorHandle</span><span class="o">(</span><span class="n">mStartHandle</span><span class="o">);</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">mEndHandle</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">showCursorHandle</span><span class="o">(</span><span class="n">mEndHandle</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> <span class="o">};</span> </code></pre></div></div> <p>很巧妙的方法,通过延迟调用具体的逻辑,避免了一直调用显示选择控件的逻辑,又学习到了。</p> <h4 id="textview-移除出-window-时一些处理">TextView 移除出 Window 时一些处理</h4> <p>在一开始没处理这个的时候,一直报如下的错误:</p> <p><img src="http://ww4.sinaimg.cn/large/91e23208jw1f9s28uh9y1j21kw0hbjzv.jpg" alt="" /></p> <p>这么明显的错误可不能不管,处理起来也很简单:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mTextView</span><span class="o">.</span><span class="na">addOnAttachStateChangeListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnAttachStateChangeListener</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">onViewAttachedToWindow</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">)</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">onViewDetachedFromWindow</span><span class="o">(</span><span class="n">View</span> <span class="n">v</span><span class="o">)</span> <span class="o">{</span> <span class="n">destroy</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">destroy</span><span class="o">()</span> <span class="o">{</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getViewTreeObserver</span><span class="o">().</span><span class="na">removeOnScrollChangedListener</span><span class="o">(</span><span class="n">mOnScrollChangedListener</span><span class="o">);</span> <span class="n">mTextView</span><span class="o">.</span><span class="na">getViewTreeObserver</span><span class="o">().</span><span class="na">removeOnPreDrawListener</span><span class="o">(</span><span class="n">mOnPreDrawListener</span><span class="o">);</span> <span class="n">resetSelectionInfo</span><span class="o">();</span> <span class="n">hideSelectView</span><span class="o">();</span> <span class="n">mStartHandle</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="n">mEndHandle</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="n">mOperateWindow</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="o">}</span> </code></pre></div></div> <p>将上面添加 Listener 也移除,同时隐藏响应的视图并置空。</p> <h3 id="写在最后">写在最后</h3> <p>至此,自定义的选择复制功能完成,效果如下,GitHub 地址:<a href="https://github.com/laobie/SelectableTextHelper">laobie/SelectableTextHelper</a></p> <p><img src="http://ww2.sinaimg.cn/large/91e23208jw1f9s29s2jf7g20900g0npd.gif" alt="" /></p> <p>在开发之初,通过简单的查阅资料,梳理了个大概的实现思路,并考虑到实现中需要注意到的点,保证在开发中保持足够的警惕,不给自己挖坑。在整个开发过程中,通过阅读他人的源码,以及直接看官方的源码,一点点解决所遇到的问题,以及一点点地尝试,都是一次不错的开发经历,也算是弥补了当初没做出来这个任务的缺憾。</p> <p>当然,这个项目还是有很多值得优化的地方,比如一些边界状态的处理,多个 TextView 的选择复制的场景等等,代码上的内部类的使用也是不够优雅的,不能够做到足够的解耦,都是有优化空间的,欢迎沟通交流。</p> </description>
<pubDate>Mon, 21 Nov 2016 00:00:00 +0000</pubDate>
<link>http://jaeger.itscoder.com//android/2016/11/21/selectable-text-helper.html</link>
<guid isPermaLink="true">http://jaeger.itscoder.com//android/2016/11/21/selectable-text-helper.html</guid>
</item>
<item>
<title>如何设计精准的推送通知?【译】</title>
<description><blockquote> <ul> <li>原文地址:<a href="http://firstround.com/review/what-you-must-know-to-build-savvy-push-notifications/">What You Must Know To Build Savvy Push Notifications</a></li> <li>原文作者:<a href="https://twitter.com/firstround">First Round</a></li> <li>译文出自:<a href="https://github.com/xitu/gold-miner">掘金翻译计划</a></li> <li>译者:<a href="https://github.com/laobie">写代码的猴子</a></li> <li>校对者:<a href="https://github.com/Ruixi">Ruixi</a>, <a href="https://github.com/rccoder">rccoder (Shangbin Yang)</a></li> </ul> </blockquote> <p>智能手机面世已经近十年时间,但根据 <a href="http://stateofstartups.firstround.com/#highlights">First Round 对初创公司的调查报告</a> 来看,创始人们仍然宣称移动端是最被低估的技术。推送通知在移动设备上潜力极大。企业家 <a href="https://www.linkedin.com/in/aseidman">Ariel Seidman</a> 在 <a href="http://arielseidman.com/post/62564939335/fixing-mobile-push-notifications">改进移动端的推送通知</a> 这篇文章中提到:“确实很难再夸大推送通知的潜力。这是在人类历史上第一次可以同时拍着近 200 万人的肩膀,说‘嘿!注意这个!’” 这也是 <a href="https://slack.com/"><strong>Slack</strong></a> 的 <a href="https://www.linkedin.com/in/noahw"><strong>Noah Weiss</strong></a> 一直笃信世界会通过智能设备变得越来越亲近的原因。</p> <p>供职 Slack 之前,Weiss 在 Foursquare 工作,当时它 <a href="http://techcrunch.com/2013/10/14/with-an-eye-to-more-revenue-foursquare-opens-its-ads-platform-to-all-small-businesses/">通过原生广告服务获利</a> ,并在 2014 年大胆地分成 <a href="https://medium.com/foursquare-direct/the-lego-block-exercise-4c7d60eeb38f#.tmyz2j5o0">两个应用</a>。那时候,每月活跃用户增长五倍之多。 Weiss 还是 Google 结构化数据搜索项目的首席产品经理。最近,Weiss 加入 Slack <a href="https://medium.com/@noah_weiss/starting-up-slack-s-search-learning-intelligence-group-in-the-new-nyc-office-af6523090789#.sqly156er">建立其纽约办事处,领导新的搜索、学习和智能项目组</a>,其任务是开发<a href="http://www.recode.net/2016/6/6/11863534/slack-artificial-intelligence-AI-noah-weiss">新的功能</a> ,使其他公司在使用 Slack 时更加高效。</p> <p>在这次采访中,Weiss 描绘了推送通知的动态演变 —— 阐释了智能手表和应用布满屏幕主屏时代关键的范式转变。在此,他还分享了一些关于初创公司寻求制定推送通知策略、投入、指标和指南的小秘诀。任何想要控制这种高风险、高回报渠道的创业公司都会从 Weiss 这里受益。</p> <blockquote> <p>一个好的推送通知有三个特性:及时性,个性化和可行性。</p> </blockquote> <h3 id="推送通知的演进">推送通知的演进</h3> <p>在分享他的策略之前,Weiss 总结了推送通知的演变,因为它涉及到<strong>三种强大的特质:及时性,个性化和可行性。</strong>他将他们的历史和进展看作是建立未来时的基础。以下是简化的推送通知演变历史的四个阶段:</p> <p><strong>电子邮件是推送通知的前身。</strong> 网络时代初期的推送通知是电子邮件。“在电子邮件和推送通知之间有很多类似的地方。” Weiss 说,“在过去,你通过提供电子邮件地址,允许与网站进行开放式沟通。电子邮件成为将人带回网站的可靠的主要方式,它不是通过门户或书签。并且,电子邮件中有一个取消订阅选项。通知的等效选项是调整推送设置,或者更常见的是卸载应用程序。</p> <p><strong>进化到移动时代。</strong> 当用户在手机上投入更多时,电子邮件开始衰退。“可能很难回想起智能手机之前的时代,人们并不习惯在他们的收件箱里生活。他们每天在电脑上检查电子邮件好几次。“ Weiss 说。 “即使是那些拥有非常成功的电子邮件营销策略的公司也会使用移动设备。还记得 Groupon 提供激光脱毛服务吗?你为什么收到它?你什么时候对脱毛表现出兴趣,或者表示你在手机上做出这种类似的购买决定时?绑定到用户,位置和一天中的某个时间,推送通知变得更有效。他们有着及时性、个性化和可行性的潜力,当然如果做的不好,用户也会感到厌烦。</p> <p><strong>与短信竞争,而不是电子邮件。</strong> 在移动设备上,推送通知更像是短信,而不是电子邮件。“推送的内容是与此刻发生的事物紧密相关的。当你可能不指望你的内容在几天内被阅读,你可以发送一封电子邮件,这对于业内通讯或文摘来说是可以的。” Weiss 说,“然而,实时推送通知所需的及时性或注意力是完全不同的。通过推送通知,你可以有效地与短信和其他个性化的沟通方式竞争。如果别的通知来自某人的配偶、最好的朋友或妈妈,你如何做到个性化?它们必须在同一水平竞争。</p> <p><strong>切割所有应用程序。</strong> 当人们首次使用智能手机时,他们的应用可以摆放在 4x4 网格的主屏幕上。而现在,美国用户的手机上大约平均有 55 个应用。“你需要知道的是,无法让这些应用都被定期使用。如今也很难开发一个应用,让该应用的使用变成日常习惯。” Weiss 说,“开发者的现实是,你的应用可能不会在某人的主屏上,用户也可能不会有一天使用它多次的习惯。这就是通知变得越来越重要的原因。对于大多数应用,推送通知可以完美地提供紧急信息:Uber 到达,登机口变更提醒或者你在 Slack 中被提及。如果用户被 50 多个应用程序淹没,你不能指望他们记住在正确的时间和地点使用你的应用,你需要主动引导他们打开。</p> <h3 id="围绕以下原则构建你的推送通知策略">围绕以下原则构建你的推送通知策略</h3> <p>深度通知策略可以权衡和组织多个因素,例如附近的 WiFi,个性化,社交因素和实时捕捉到的位置等等,都可以用来驱动推送通知。但对于刚刚开始接触推送通知技术的初创公司来说,有一些基本因素需要考虑。从基本到更高级的诀窍,Weiss 讲述了他在开发推送通知系统时学到的基本经验。</p> <p><strong>在应用程序之外促进用户留存</strong></p> <p>从用户保留角度来看,当你的应用超越了功能下限后,用户返回你的应用的次数会减少。你只能在你的应用中塞入那么多功能,并期望新用户在一开始的几个会话中发现这些功能。“移动领域最大的挑战是留住新用户,已经有得到证明的战术来引进新用户:高效的应用安装营销、社交渠道、SEM 和 SEO。然而,真正困难的是让新用户养成一种习惯。” Weiss 说,“有时候,你的应用的改进不会显著影响用户留存的顶峰值,但是在应用之外的投资却可以做到,这里即推送通知的投资。因为一旦有人关闭了你的应用,他们错过了第四个 Tab 下的神奇体验就变得无关紧要了。因为如果他们再也没有打开你的应用,他们永远不会知道他们错过了什么。</p> <p>在为你的应用设计最佳用户体验的过程中,请不要忘记,只有在用户打开应用时,才会享受到这种体验 —— 才会继续回到你的应用。“这总是让我感到惊讶和痛苦:当我看到对一个应用投入令人难以置信的时间和精力,却没有一个策略重新吸引我。” Weiss 说,“当然,大多数年轻的开发人员都不考虑通知。不要犯这个错误。这也是目前移动产品开发中最大的疏忽。”</p> <blockquote> <p>客户需求推进了一个应用,用户留存成了一笔生意。</p> </blockquote> <p><strong>不要在有权限的情况下错误下载。</strong></p> <p>请求获取发送通知的权限不仅是良好的形式,而且在技术上也是必要的。“如果你在 iOS 平台上开发,发送通知是用户必须授权的权限。与 Android 不同,下载应用默认授予权限,你必须提示用户。” Weiss说,“这是一个很关键的时刻,如果用户拒绝授权,应用无法引导用户重新进入授权页面,这极大地降低了他们变成活跃用户的可能性。即使他们接受,这也不是个有约束力的合同。”</p> <p>如果用户厌倦了你的推送通知,最好的情况是他们可以选择在应用中保留哪些通知是活动的,但更可能的是导致他们到手机设置中关闭所有通知或者卸载应用。这实际上是不可逆的。注:提升给用户的第一个通知体验,否则他们会关闭通知渠道。</p> <p>因此,第一步是提示用户在一开始同意接收通知 —— 如果他们说不,其余的建议将变得不再重要。它涉及用户教育,在用户发现有价值的内容之后再弹出提示,或者授权绿灯亮起来时再申请授权许可,可以提升转化率。然后是关于保持信任和保持开放的沟通,这两个步骤有一些不错的文献可以参考,Weiss 推荐了以下的文章:</p> <ul> <li> <p><a href="https://library.launchkit.io/the-right-way-to-ask-users-for-ios-permissions-96fa4eb54f2c#.3u7waqk3w">移动端请求用户权限的正确方式</a> —— <a href="https://twitter.com/mulligan">Brenden Mulligan</a></p> </li> <li> <p><a href="http://andrewchen.co/why-people-are-turning-off-push/">为什么 60% 的用户选择停用推送通知,如何应对这种状况</a> ——<a href="https://twitter.com/andrewchen">Andrew Chen</a></p> </li> <li> <p><a href="https://medium.com/circa/the-right-way-to-ask-users-to-review-your-app-9a32fd604fca#.iz4jrwiin">让用户再次回到你的应用的正确方式</a> ——<a href="https://twitter.com/mg">Matt Galligan</a></p> </li> </ul> <p>考虑到获取通知权限的高风险,这些文章的重点默认是如何规避风险。“如果你足够聪明,那么实际上涉及到通知你会变得非常谨慎。在所有实验中建立安全网,因为任何失误都会产生很大的影响。” Weiss 说,“例如,如果我每周发布一次推送,所有用户都会收到,我会将它作为一个 5% 或 10% 的实验,以覆盖任何导致用户选择退出通知的潜在缺陷。”</p> <p><strong>指定三个指标来衡量通知</strong></p> <p>为了评估你的通知策略,需要给出以下三个指标:<strong>1)选择取消通知权限的用户比率 2)卸载率 和 3)每百次推送的操作次数</strong>。</p> <p>“要评估一个好的通知,你必须在用户主动参与和取消通知之间达到平衡。这是一个棘手的平衡,因为你可能会比较一个短期的主动参与用户数的提升与长期下来的卸载用户数,不能再重新参与。” Weiss 说。 “从设定卸载率和通知禁用率开始,如果你的应用程序是面向消费者的,而且卸载率低于 2%,则表示你处于安全区。所以如果你的每周流失率为 1%,你的增长率为 1.02% 到 2%,这不是毁灭性的。监测所有剧烈的波动,因为一周一周的叠加效应可能会造成损失。”</p> <p>为了评估通知策略的回报,不要考虑打开率而是衡量具体操作。“我建议的一个方法是监控推送通知的时间窗口,统计到达绑定到原始通知的操作的数目。例如,如果通知鼓励用户评价他们最近访问过的地方,分析用户在 2-6 小时的窗口内每百次推送通知的评分数。” Weiss 说,“总是有归属的问题,但如果你在发送通知后定义一个固定的时间窗口进行评估,结果会让你更能接受。</p> <p><strong>…校准指标以用来比较 iOS 和 Android 上的表现。</strong></p> <p>对于那些想要将打开率作为指标进行追踪的人,Weiss 对不同操作系统上的通知的性质有几点看法。“通过电子邮件跟踪打开率是很容易的,但是你要知道 iOS 的打开率远远低于 Android;进行相同的推送,Android 可以显示多达 iOS 平台五倍的打开率。” Weiss 说,“在 Android 用户倾向于处理通知,因为只有在你手动打开每个通知时,通知才会清除,而在 iOS 上,一旦你从锁定屏幕打开一个通知,其他通知就会清除。</p> <p>与其他功能一样,不同的操作系统在收到通知时表现也不同。“例如,Android 上的通知可以内置图片,这样可以提高 15-20% 的互动概率。由于大多数开发人员通常在 iOS 平台上工作,他们认为发送 Android 推送通知也不可以附带图片。” Weiss 说,“还有内置操作按钮,让用户可以直接从通知进行操作。这些也提升了更高的互动概率。即使作为一个 iPhone 用户,我也不得不说,从根本上来说,Android 的通知开发都是更好的。</p> <blockquote> <p>用个性化的内容填充推送通知,让他们听起来像来自一个亲密的朋友。</p> </blockquote> <p><strong>抵制新奇性效应</strong></p> <p>运行推送通知的实验至少六周,12 周是一个不错的选择。 Weiss 明白,进行更长时间的测试是必要的,以表现出所有负面影响。“一般用户将忽略不必要的推送大约一个月,而不采取任何操作,如更改设置或卸载应用。一旦超过这个阈值,烦人的通知很快被清除。” Weiss 说。</p> <p>通知具有强烈的新奇性倾向,这延迟了用户的真实反应。Weiss 曾发起了一个实验来测试用户对表情符号的反应。“我们将文本的长度减半,并添加了相关的表情符号。在实验的前两个星期,我们统计指标达到了顶峰。用户打开应用的操作明显。每周活跃用户数( WAUs )上升。它迷惑性地宣称未来是表情符号的。” Weiss 说,“随着时间的推移,我们继续监控它,增长放缓,然后变平。最后,影响是中性的。这并不是一件坏事,但如果我们基于初步结果就分配资源,那就会导致问题。因此最好花几个月时间而不是几个星期来测试推送通知。</p> <p><strong>如何测试?何时测试?在哪测试?</strong></p> <p>推送通知的“为什么”和“谁”是比较直接的 —— 目标是提升所有用户的参与。然而,在推送通知的方式上却有各种各样的想法。Weiss 在他的职业生涯中,帮助启动了 100 多个通知实验 —— 测试了从一天时间内到触发,到回到首屏。 <a href="http://firstround.com/review/the-right-way-to-ship-software/">与运输软件一样,没有“正确的方式”</a> ,但在这里他分享一些无可争议的点:</p> <ul> <li> <p><strong>只有最紧急的通知才需要开启振动。</strong> “通过推送,你可以控制默认设置是手机振动还是静音。从我所有的用户研究中我发现这是最高风险的决策之一。如果一个通知振动了用户,她发现并不紧急,那么应用程序被卸载的可能性立即暴增。” Weiss 说,“如果它是紧急的 —— 就像你即将错过你的飞机或直接来自同事的紧急消息 —— 一个嗡嗡声可以是一个非常强大和值得称赞的工具。如果没有,这将会产生危险、发生意外,因此,对于从朋友那得到一个赞或者喜欢,不要使用振动。用户平均每天查看手机的时间为 70 到 100 次,他们很可能在接下来的 15 分钟内看到你的消息。”</p> </li> <li> <p><strong>匹配用户的生物节律。</strong> “推送的时间很重要,但没有一个规则来规定绝对最好的窗口时间。但请花一点时间思考下如何监控用户作息进度,避免在用户睡着时发送通知,因为这样你将吵醒他们,或者他们会在早上发现一堆来自你的应用的推送消息。” Weiss 说,“也要考虑你的内容的性质,在上午发送新闻效果不错,以及在上下班路上时发送通知也不错。通过监控用户的参与来提升你的策略。”</p> </li> <li> <p><strong>在你的通知副本中使用各种个性化。</strong> “它产生了巨大的差异。插入用户的名字不算在内,例如’ Noah,这里是你星期二的每日交易!’在你的通知副本中显示你知道的有关用户的信息 —— 否则他们将激活他们天生的过滤器来应对爆炸营销。” Weiss 说到,“ 当用户查看他们的时间线时,Twitter 有一个好的做法,该服务提示你查看 Evelyn,Marcos 和Lydia 的最近一天的推文。这些都是你关注的、可以叫出名字的人。Spotify 对于你经常听的艺术家的新歌也一样处理。</p> </li> <li> <p><strong>像 Uber 一样思考你的推送。</strong> “如果你的 Uber 司机在曼哈顿的任一个街区上放下了你,当你要求在下东区一个特定的街区下车,你会高兴吗?这很显然,但初创公司可能忘记将他们的用户指引到在通知中提示的<strong>准确</strong>界面上。” Weiss 说,“如果通知引导用户进到他们期望的界面,人们就会点击它。如果没有,他们下一次就会忽略它。许多电子商务应用通过将用户引导到通用界面而不是特定项目或页面来解决这个问题。</p> </li> </ul> <blockquote> <p>魔术师把你的选中的牌变到一副牌的最上面。拥有智能通知的应用将拥有更多的手法,在适当的时间将他们的服务呈现到人们的手机上。</p> </blockquote> <h3 id="通知的未来">通知的未来</h3> <p>智能手机和智能手表的屏幕不断变化,但主屏幕的实际空间始终是有限的,无论大小。考虑到手机上保存的应用程序数量激增,此限制是一个约束。以下是 Weiss 从移动操作系统的演变得到对未来通知的想法:</p> <p><strong>让锁屏成为新的主屏幕。</strong> 事实上,人们看到的比手机主屏幕更多的唯一地方就是手机锁屏界面。“你的主屏幕上放置的你想要触手可及的应用,通常限制在不到 20 个。你的锁屏则列出了手机上的数百个应用最近的通知。” Weiss 说,“我认为锁屏将取代主屏幕,将会有一个全新的主屏体验,将应用以流的方式呈现给你。最终排名将不仅仅是取决于最近使用和使用频率。系统通知将感觉像 Twitter 的实时动态,嘈杂的信息流让人感觉像 Facebook 的热门动态。</p> <blockquote> <p>你可以随时切换到某个应用,但通知将是你坚定不移的向导。</p> </blockquote> <p><a href="http://ben-evans.com/benedictevans/2014/8/1/app-unbundling-search-and-discovery">应用绑定和解绑的自然现象</a> ,Weiss 看到一个潮汐般的转变:锁屏将再次重新绑定它们。“在过去三年中,应用生态系统中出现了一个渐进的、巨大的分裂。应用程序已变得针对单一使用场景更加专业化。” Weiss 说,“但随着用户聚集了一堆应用,在正确的时间选择正确的服务变得越来越困难。通知给用户提供及时有用的信号。将会有一个新的导航范例,当用户正在考虑使用某些应用时,智能地控制这些应用。</p> <p><strong>丰富的上下文感知。</strong> 如果用户越来越多地通过发送到锁屏界面的通知流来与应用交互,这将是因为他们确信他们被发送了最及时、最相关的警报。这只会发生在一个强大的的上下文感知中。“手机上的传感器使你能够在移动设备上建立一个感知上下文级别的服务,你永远不可能在桌面或电子邮件上进行这样的感知。你如何把这种感知翻译成真正可行的、及时的、相关的通知?”魏斯问,“这是一个令人振奋的新领域,想象一个服务,可以区分是否有人一个特定的场所停驻,无论是咖啡馆,机场还是健身房。对上下文的独特感知创造了大量发送相关推送通知的新机会。”</p> <blockquote> <p>最好的应用将是那些你不必记住他们的应用,他们会主动提醒你。这种应用将是未来的唯一类型应用。</p> </blockquote> <p>“我最喜欢 Foursquare 在一个城市新的或热门的场所的推送通知。根据你的手机的定位,它可以将你与实际访问的地方关联起来。” Weiss 说,“它给你一个通知,通常每周一次,’嘿,这里有三个城市热门地方,你还没有去过。’这是一个神奇的时刻,当你意识到你仅仅是带着口袋里的手机在周围走了走,也许你甚至整整一个星期都没使用过这个应用。你不需要做任何事,它就将你拉回这个应用,并给你惊喜。”</p> <p>完整利用移动设备上的传感器具有挑战性,但可以从一些基本的方向开始。“虽然大多数开发人员无法简单地建立这种类型的位置解析,但是基于后台定位构建一个模型用来解析一个人在家还是在工作是很容易的。这是两个用来触发相关的推送非常丰富的上下文。” Weiss 说。</p> <h3 id="总结">总结</h3> <p>虽然通知可以提高留存率和互动率,但不要将其视为增长的黑科技。他们有潜力成为与用户互动的最直观、最亲密的方式。为了建立这种可靠的关系,他们必须是及时的、个性化的、可操作的。通知策略必须请求用户的授权,并根据其停用、卸载和每100次通知点击次数来权衡。更好地方式是根据用户主动输入和被动地感知上下文来定制通知。</p> <p>“我们还在移动时代的早期。设备继续改进,将会拥有更大的屏幕,更长的电池寿命或者变成可穿戴的。” Weiss 说,“然而无论硬件如何发展,通知将是你的移动设备最亲密的功能。像亲密的朋友或家人一样,智能通知会记住你的偏好和历史。他们会准确地指引你,让你与亲人保持联系,并在最合适的时间提醒你重要的事情。这大概就是技术的力量。”</p> </description>
<pubDate>Wed, 02 Nov 2016 00:00:00 +0000</pubDate>
<link>http://jaeger.itscoder.com//%E4%BA%A7%E5%93%81/2016/11/02/what-you-must-know-to-build-savvy-push-notifications.html</link>
<guid isPermaLink="true">http://jaeger.itscoder.com//%E4%BA%A7%E5%93%81/2016/11/02/what-you-must-know-to-build-savvy-push-notifications.html</guid>
</item>
<item>
<title>Android 过度绘制优化</title>
<description><blockquote> <ul> <li>文章来源:itsCoder 的 <a href="https://github.com/itsCoder/weeklyblog">WeeklyBolg</a> 项目</li> <li>itsCoder 主页:<a href="http://itscoder.com/">http://itscoder.com/</a></li> <li>作者:<a href="https://github.com/laobie">Jaeger</a></li> <li>审阅者:<a href="https://github.com/yongyu0102">yongyu0102 (用语)</a></li> </ul> </blockquote> <p>Android 从一诞生到现在已经发布的 7.0 版本,卡顿和不流畅问题却一直被人们所诟病。客观地来讲,Android 的流畅性确实一直不给力,哪怕是某些大厂的 App ,也都不同程度地存在卡顿问题。从开发角度来说,每个开发者都应该关注下性能优化,在平时的开发工作中注意一些细节,尽可能地去优化应用。本文作为性能优化系列的开篇,先从过度绘制优化讲起。</p> <h3 id="过度绘制overdraw的概念">过度绘制(Overdraw)的概念</h3> <blockquote> <p>过度绘制(Overdraw)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的 UI 结构里面,如果不可见的 UI 也在做绘制的操作,会导致某些像素区域被绘制了多次,同时也会浪费大量的 CPU 以及 GPU 资源。</p> </blockquote> <p>在 Android 手机的开发者选项中,有一个『调试 GPU 过度绘制』的选项,该选项开启之后,手机显示如下,显示出来的蓝色、绿色的色块就是过度绘制信息。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/c7de9ce128cd8921.png" alt="" /></p> <p>比如上面界面中的『调试 GPU 过度绘制 』的那个文本显示为蓝色,表示其过度绘制了一次,因为背景是白色的,然后文字是黑色的,导致文字所在的区域就会被绘制两次:一次是背景,一次是文字,所以就产生了过度重绘。</p> <p>在官网的 <a href="https://developer.android.com/studio/profile/dev-options-overdraw.html">Debug GPU Overdraw Walkthrough</a> 说明中对过度重绘做了简单的介绍,其中屏幕上显示不同色块的具体含义如下所示:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/46397b26da912658.png" alt="" /></p> <p>每个颜色的说明如下:</p> <ul> <li><strong>原色</strong>:没有过度绘制</li> <li><strong>蓝色</strong>:1 次过度绘制</li> <li><strong>绿色</strong>:2 次过度绘制</li> <li><strong>粉色</strong>:3 次过度绘制</li> <li><strong>红色</strong>:4 次及以上过度绘制</li> </ul> <p>过度绘制的存在会导致界面显示时浪费不必要的资源去渲染看不见的背景,或者对某些像素区域多次绘制,就会导致界面加载或者滑动时的不流畅、掉帧,对于用户体验来说就是 App 特别的卡顿。为了提升用户体验,提升应用的流畅性,优化过度绘制的工作还是很有必要做的。</p> <h3 id="优化原则">优化原则</h3> <ul> <li>一些过度绘制是无法避免的,比如之前说的文字和背景导致的过度绘制,这种是无法避免的。</li> <li>应用界面中,应该尽可能地将过度绘制控制为 2 次(绿色)及其以下,原色和蓝色是最理想的。</li> <li>粉色和红色应该尽可能避免,在实际项目中避免不了时,应该尽可能减少粉色和红色区域。</li> <li>不允许存在面积超过屏幕 1/4 区域的 3 次(淡红色区域)及其以上过度绘制。</li> </ul> <h3 id="优化方法">优化方法</h3> <p>以下部分是根据我在公司项目的实践来整理出来的一些实际的优化步骤和方法,避免像看完大部分性能优化的文章,然后发现『懂得太多道理还是写不好一个 App』的尴尬局面。</p> <ol> <li> <p>移除默认的 Window 背景</p> <p>一般应用默认继承的主题都会有一个默认的 <code class="highlighter-rouge">windowBackground</code> ,比如默认的 Light 主题:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;style</span> <span class="na">name=</span><span class="s">"Theme.Light"</span><span class="nt">&gt;</span> <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"isLightTheme"</span><span class="nt">&gt;</span>true<span class="nt">&lt;/item&gt;</span> <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"windowBackground"</span><span class="nt">&gt;</span>@drawable/screen_background_selector_light<span class="nt">&lt;/item&gt;</span> ... <span class="nt">&lt;/style&gt;</span> </code></pre></div> </div> <p>但是一般界面都会自己设置界面的背景颜色或者列表页则由 item 的背景来决定,所以默认的 Window 背景基本用不上,如果不移除就会导致所有界面都多 1 次绘制。</p> <p>可以在应用的主题中添加如下的一行属性来移除默认的 Window 背景:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"android:windowBackground"</span><span class="nt">&gt;</span>@android:color/transparent<span class="nt">&lt;/item&gt;</span> <span class="c">&lt;!-- 或者 --&gt;</span> <span class="nt">&lt;item</span> <span class="na">name=</span><span class="s">"android:windowBackground"</span><span class="nt">&gt;</span>@null<span class="nt">&lt;/item&gt;</span> </code></pre></div> </div> <p>或者在 <code class="highlighter-rouge">BaseActivity</code> 的 <code class="highlighter-rouge">onCreate()</code> 方法中使用下面的代码移除:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">getWindow</span><span class="o">().</span><span class="na">setBackgroundDrawable</span><span class="o">(</span><span class="kc">null</span><span class="o">);</span> <span class="c1">// 或者</span> <span class="n">getWindow</span><span class="o">().</span><span class="na">setBackgroundDrawableResource</span><span class="o">(</span><span class="n">android</span><span class="o">.</span><span class="na">R</span><span class="o">.</span><span class="na">color</span><span class="o">.</span><span class="na">transparent</span><span class="o">);</span> </code></pre></div> </div> <p>移除默认的 Window 背景的工作在项目初期做最好,因为有可能有的界面未设置背景色,这就会导致该界面显示成黑色的背景,如下所示,如果是后期移除的,就需要检查移除默认 Window 背景之后的界面是否显示正常。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/8bb76d317ff0d5ff.png" alt="" /></p> </li> <li> <p>移除不必要的背景</p> <p>还是上面的那个界面,因为移除了默认的 Window 背景,所以在布局中设置背景为白色:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span> <span class="nt">&lt;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:background=</span><span class="s">"@color/white"</span> <span class="na">android:orientation=</span><span class="s">"vertical"</span><span class="nt">&gt;</span> <span class="nt">&lt;android.support.v7.widget.RecyclerView</span> <span class="na">android:id=</span><span class="s">"@+id/rv_apps"</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:visibility=</span><span class="s">"visible"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/LinearLayout&gt;</span> </code></pre></div> </div> <p>然后在列表的 item 的布局如下所示:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span> <span class="nt">&lt;LinearLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span> <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span> <span class="na">android:layout_width=</span><span class="s">"match_parent"</span> <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span> <span class="na">android:background=</span><span class="s">"@color/white"</span> <span class="na">android:orientation=</span><span class="s">"horizontal"</span> <span class="na">android:padding=</span><span class="s">"@dimen/mid_dp"</span><span class="nt">&gt;</span> <span class="nt">&lt;ImageView</span> <span class="na">android:id=</span><span class="s">"@+id/iv_app_icon"</span> <span class="na">android:layout_width=</span><span class="s">"40dp"</span> <span class="na">android:layout_height=</span><span class="s">"40dp"</span> <span class="na">tools:src=</span><span class="s">"@mipmap/ic_launcher"</span><span class="nt">/&gt;</span> <span class="nt">&lt;TextView</span> <span class="na">android:id=</span><span class="s">"@+id/tv_app_label"</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_gravity=</span><span class="s">"center_vertical"</span> <span class="na">android:layout_marginLeft=</span><span class="s">"@dimen/mid_dp"</span> <span class="na">android:textColor=</span><span class="s">"@color/text_gray_main"</span> <span class="na">android:textSize=</span><span class="s">"@dimen/mid_sp"</span> <span class="na">tools:text=</span><span class="s">"test"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/LinearLayout&gt;</span> </code></pre></div> </div> <p>看起来是没问题的,但是因为我界面的背景和 item 布局的背景都是白色,所以 item 布局中的背景是不必要的,可以移除。优化前后的过度绘制结果如下:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/eeffd1ea58fd9598.png" alt="" /></p> <p>很明显优化后过度绘制比之前均少了一次,但是这种场景还是比较特殊的,因为界面背景和 item 的背景色一样,假如不一样的话,就无法避免多 1 次过度绘制了。</p> <p>还有一个比较常见的可优化场景:ViewPager 加多个 Fragment 组成的首页界面,如果你的每个 Fragment 都设置有背景色的话, 你就可以不用给 Activity 的根布局设置背景,如果你还给 ViewPager 还设置了背景,那个这个背景是没必要的,同样可以移除。</p> <p>如果你不知道存在哪些无用的背景,你可以借助 Hierarchy View 来查看,具体的这块可以参照 <a href="http://androidperformance.com/2015/01/13/android-performance-optimization-overdraw-2.html">Android 性能优化之过渡绘制(二)</a> 这篇文章来操作。</p> </li> <li> <p>写合理且高效的布局</p> <p>由于 Android 的布局是通过编写 xml 来实现,相对比较简单,这也就导致很多开发者在写布局时很随意,而不会考虑性能、过度重绘等问题。</p> <p>比如上面列表布局中的分割线,可以按照如下编写布局来实现:</p> <div class="language-xml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;?xml version="1.0" encoding="utf-8"?&gt;</span> <span class="nt">&lt;LinearLayout</span> <span class="na">xmlns:android=</span><span class="s">"http://schemas.android.com/apk/res/android"</span> <span class="na">xmlns:tools=</span><span class="s">"http://schemas.android.com/tools"</span> <span class="na">android:layout_width=</span><span class="s">"match_parent"</span> <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span> <span class="na">android:paddingBottom=</span><span class="s">"8dp"</span> <span class="na">android:background=</span><span class="s">"@color/divider_gray"</span><span class="nt">&gt;</span> <span class="nt">&lt;LinearLayout</span> <span class="na">android:padding=</span><span class="s">"@dimen/mid_dp"</span> <span class="na">android:layout_width=</span><span class="s">"match_parent"</span> <span class="na">android:layout_height=</span><span class="s">"wrap_content"</span> <span class="na">android:orientation=</span><span class="s">"horizontal"</span> <span class="na">android:background=</span><span class="s">"@color/white"</span><span class="nt">&gt;</span> <span class="nt">&lt;ImageView</span> <span class="na">android:id=</span><span class="s">"@+id/iv_app_icon"</span> <span class="na">android:layout_width=</span><span class="s">"40dp"</span> <span class="na">android:layout_height=</span><span class="s">"40dp"</span> <span class="na">tools:src=</span><span class="s">"@mipmap/ic_launcher"</span><span class="nt">/&gt;</span> <span class="nt">&lt;TextView</span> <span class="na">android:id=</span><span class="s">"@+id/tv_app_label"</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_gravity=</span><span class="s">"center_vertical"</span> <span class="na">android:layout_marginLeft=</span><span class="s">"@dimen/mid_dp"</span> <span class="na">android:textColor=</span><span class="s">"@color/text_gray_main"</span> <span class="na">android:textSize=</span><span class="s">"@dimen/mid_sp"</span> <span class="na">tools:text=</span><span class="s">"test"</span><span class="nt">/&gt;</span> <span class="nt">&lt;/LinearLayout&gt;</span> <span class="nt">&lt;/LinearLayout&gt;</span> </code></pre></div> </div> <p>这种改变布局实现分割线的方式虽然很快捷方便,但是存在不少问题的:</p> <ul> <li> <p>加深了布局层级,和之前的布局相比多了一级</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/2aceb1e5a933352a.jpg" alt="" /></p> </li> <li> <p>多了 2 次过度绘制</p> </li> </ul> <p>解决方式有两种:</p> <ol> <li>一种是使用 <code class="highlighter-rouge">RelativeLayout</code> 将分割线添加在 item 的布局中,但是这样会导致布局复杂度增加,同时因为 <code class="highlighter-rouge">RelativeLayout</code> 布局的两次测量,也会延长 View 测量的时间,在解决这种需求时并不是一个好的方式。</li> <li>另一种是使用 <code class="highlighter-rouge">RecyclerView</code> 的 <code class="highlighter-rouge">addItemDecoration(ItemDecoration decor)</code> 方法添加分割线,这种方式在你自定义好一个分割线 <code class="highlighter-rouge">ItemDecoration</code> 时是很方便的,网上有很多关于这方面的例子(如果你使用 ListView 的话,则使用 <code class="highlighter-rouge">setDivider(Drawable divider)</code> 方法)。</li> </ol> <p>我们采用第二种解决方法,优化前后的对比如下:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/fad0b600790d3986.png" alt="" /></p> <p>优化后的布局 ImageView 和 item 背景区域均比优化前少了 2 次过度重绘,布局层级也没增加,需求也实现了。</p> <blockquote> <p>注:很多开发者在开发中一般很少注意这种小细节,一般以完成需求为目的,可能还认为这么点细节优化不优化其实也没什么,但是积少成多,小的细节优化多了,整体性能和体验可能就上升了,相反,这个细节不注意那个细节无所谓,最终就导致应用卡顿,体验糟糕。注重细节的开发者运气一般都不会太差。: )</p> </blockquote> </li> <li> <p>自定义控件使用 <code class="highlighter-rouge">clipRect()</code> 和 <code class="highlighter-rouge">quickReject()</code> 优化</p> <p>当某些控件不可见时,如果还继续绘制更新该控件,就会导致过度绘制。但是通过 Canvas <code class="highlighter-rouge">clipRect()</code> 方法可以设置需要绘制的区域,当某个控件或者 View 的部分区域不可见时,就可以减少过度绘制。</p> <p>先看一下 <code class="highlighter-rouge">clipRect()</code> 方法的说明:</p> <blockquote> <p>Intersect the current clip with the specified rectangle, which is expressed in local coordinates.</p> </blockquote> <p>顾名思义就是给 Canvas 设置一个裁剪区,只有在这个裁剪矩形区域内的才会被绘制,区域之外的都不绘制。 <code class="highlighter-rouge">DrawerLayout</code> 就是一个很不错的例子,先来看一下使用 DrawerLayout 布局的过度绘制结果:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/3ac552385fa37312.png" alt="" /></p> <p>按道理左边的抽屉布局出来时,应该是和主界面的布局叠加起来的,但是为什么抽屉的背景过度绘制只有一次呢?如果是叠加的话,那最少是主界面过度绘制次数 +1,但是结果并不是这样。直接看源码:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@Override</span> <span class="kd">protected</span> <span class="kt">boolean</span> <span class="nf">drawChild</span><span class="o">(</span><span class="n">Canvas</span> <span class="n">canvas</span><span class="o">,</span> <span class="n">View</span> <span class="n">child</span><span class="o">,</span> <span class="kt">long</span> <span class="n">drawingTim</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">height</span> <span class="o">=</span> <span class="n">getHeight</span><span class="o">();</span> <span class="kd">final</span> <span class="kt">boolean</span> <span class="n">drawingContent</span> <span class="o">=</span> <span class="n">isContentView</span><span class="o">(</span><span class="n">child</span><span class="o">);</span> <span class="kt">int</span> <span class="n">clipLeft</span> <span class="o">=</span> <span class="mi">0</span><span class="o">,</span> <span class="n">clipRight</span> <span class="o">=</span> <span class="n">getWidth</span><span class="o">();</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">restoreCount</span> <span class="o">=</span> <span class="n">canvas</span><span class="o">.</span><span class="na">save</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">drawingContent</span><span class="o">)</span> <span class="o">{</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">childCount</span> <span class="o">=</span> <span class="n">getChildCount</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">&lt;</span> <span class="n">childCount</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="n">View</span> <span class="n">v</span> <span class="o">=</span> <span class="n">getChildAt</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">v</span> <span class="o">==</span> <span class="n">child</span> <span class="o">||</span> <span class="n">v</span><span class="o">.</span><span class="na">getVisibility</span><span class="o">()</span> <span class="o">!=</span> <span class="n">VISIBLE</span> <span class="o">||</span> <span class="o">!</span><span class="n">hasOpaqueBackground</span><span class="o">(</span><span class="n">v</span><span class="o">)</span> <span class="o">||</span> <span class="o">!</span><span class="n">isDrawerView</span><span class="o">(</span><span class="n">v</span><span class="o">)</span> <span class="o">||</span> <span class="n">v</span><span class="o">.</span><span class="na">getHeight</span><span class="o">()</span> <span class="o">&lt;</span> <span class="n">height</span><span class="o">)</span> <span class="o">{</span> <span class="k">continue</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">checkDrawerViewAbsoluteGravity</span><span class="o">(</span><span class="n">v</span><span class="o">,</span> <span class="n">Gravity</span><span class="o">.</span><span class="na">LEFT</span><span class="o">))</span> <span class="o">{</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">vright</span> <span class="o">=</span> <span class="n">v</span><span class="o">.</span><span class="na">getRight</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">vright</span> <span class="o">&gt;</span> <span class="n">clipLeft</span><span class="o">)</span> <span class="n">clipLeft</span> <span class="o">=</span> <span class="n">vright</span><span class="o">;</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">vleft</span> <span class="o">=</span> <span class="n">v</span><span class="o">.</span><span class="na">getLeft</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">vleft</span> <span class="o">&lt;</span> <span class="n">clipRight</span><span class="o">)</span> <span class="n">clipRight</span> <span class="o">=</span> <span class="n">vleft</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="n">canvas</span><span class="o">.</span><span class="na">clipRect</span><span class="o">(</span><span class="n">clipLeft</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">clipRight</span><span class="o">,</span> <span class="n">getHeight</span><span class="o">());</span> <span class="o">}</span> <span class="o">......</span> <span class="o">}</span> </code></pre></div> </div> <p>在 DrawerLayout 的 <code class="highlighter-rouge">drawChild()</code> 方法一开始会判断是是否是 DrawerLayout 的 ContentView,即非抽屉布局,如果是的话,则遍历 DrawerLayout 的 child view,拿到抽屉布局,如果是左边抽屉,则取抽屉布局的右边边界作为裁剪区的左边界,得到的裁剪矩形就是下图中的红色框部分,然后设置裁剪区域。右边抽屉同理。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/f2bd8c92d4f03a9b.jpg" alt="" /></p> <p>这样一来,只有裁剪矩形内的界面需要绘制,自然就减少了抽屉布局的过度绘制。自定义控件时可以参照这个来优化过度绘制问题。</p> <p>除了 <code class="highlighter-rouge">clipRect()</code> 以外,还可以使用 <code class="highlighter-rouge">canvas.quickreject()</code> 来判断和某个矩形相交,如果相交的话,则可以跳过相交的区域减少过度绘制。</p> </li> </ol> <h3 id="优化实践">优化实践</h3> <p>前面其实已经讲了很多了,但是实际去优化过度绘制时,可能还是会比较懵,看着屏幕上的大片大片的红色,不知道从何下手。接下来就以实际项目中的过度绘制优化经历来谈谈,如何进行优化?</p> <p>先上图,前面是未开启 『调试 GPU 过度绘制』 的界面图,中间的是优化前的过度绘制结果,后面的是优化后的过度绘制结果,不难看出来,中间那张图过度绘制是很严重的,一眼看过去一片红,很显然不符合优化原则。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/1bed5940cdfa0701.png" alt="" /></p> <p>优化步骤如下:</p> <ol> <li> <p>先分析每个地方最少可以绘制几次,不合理的地方就可以优化。</p> <p>例如:中间那张图显示的每个 item 的背景是绿色的,也就是 2 次过度绘制,这肯定是不合理的。因为整个界面大背景是灰色的,item 背景是白色的,按道理应该就 1 次过度绘制。检查下来发现没去掉默认的 Window 背景,移除之后 item 背景就变成了蓝色了,也就是 1 次过度绘制。</p> </li> <li> <p>叠加的布局,过度绘制次数是否合理递增</p> <p>还是看中间那张图,item 的背景过度绘制是 2 次,按道理九宫格图片每张图应该是过度绘制 3 次,但是却显示成红色的,显然没有合理递增而出现了跳跃。</p> <p>先猜测是不是因为给九宫格图片控件设置了白色背景?但是想一下就排除了,因为图片间隙的过度绘制次数和 item 背景是相同的。</p> <p>那就是每个 ImageView 有问题了,后来发现之前设置占位图的时候,给每个 ImageView 设置了一个灰色的背景色:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">imageView</span><span class="o">.</span><span class="na">setBackgroundColor</span><span class="o">(</span><span class="n">Color</span><span class="o">.</span><span class="na">parseColor</span><span class="o">(</span><span class="s">"#eeeeee"</span><span class="o">));</span> </code></pre></div> </div> <p>这也就导致了每个 ImageView 的过度绘制直接多了 1 次。</p> <p>这两步优化后,再看最后一张图中的优化结果,基本是可以的了。</p> </li> <li> <p>在 <strong>优化方法</strong> 中讲到的 ViewPager 布局加 Fragment 实现的首页布局,一个不注意很容易出现过度绘制严重的问题,在移除 ViewPager 和 Activity 根布局的白色背景后,以及默认的 Window 背景,原来红成一片的首页现在基本上是大部分蓝色和小部分绿色了。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/5e3d906c721cc9f3.png" alt="" /></p> </li> </ol> <h3 id="小插曲">小插曲</h3> <p>最后来个小插曲,因为开启 『调试 GPU 过度绘制』比较麻烦,我就想找个比较方便快捷的方式,一开始想着写个桌面插件应用,一键切换。</p> <ul> <li> <p>查文档发现没有相关的设置的 API</p> </li> <li> <p>直接翻源码,发现相关的 API 是隐藏的,集中在 <code class="highlighter-rouge">SystemProperties</code> 类中,可以通过如下代码设置:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">SystemProperties</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">HardwareRenderer</span><span class="o">.</span><span class="na">DEBUG_OVERDRAW_PROPERTY</span><span class="o">,</span> <span class="s">"show"</span><span class="o">);</span> </code></pre></div> </div> </li> <li> <p>直接编译源码拿到了没隐藏的 jar 包,暂时能调用到该类,但是运行之后发现需要系统权限才能设置</p> </li> <li> <p>通过一些方式企图让这个 App 获取到系统权限,但是均失败了 : (</p> </li> </ul> <p>如果你对相关的知识有所了解,请联系我和我探讨下,谢谢。</p> <p>不过最后也算是找到了一个比较方便的方法,省去了去设置里面一步步点。直接运行 adb 指令:</p> <p>开启『调试 GPU 过度绘制』:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell setprop debug.hwui.overdraw show </code></pre></div></div> <p>关闭『调试 GPU 过度绘制』:</p> <div class="language-shell highlighter-rouge"><div class="highlight"><pre class="highlight"><code>adb shell setprop debug.hwui.overdraw <span class="nb">false</span> </code></pre></div></div> <p>再取个指令别名,使用起来还是很方便的。</p> <h3 id="参考资料">参考资料</h3> <ul> <li><a href="http://hukai.me/android-performance-render/">Android 性能优化之渲染篇 - 胡凯</a></li> <li><a href="http://androidperformance.com/2014/10/20/android-performance-optimization-overdraw-1.html">Android 性能优化之过渡绘制(一) | Performance</a></li> <li><a href="http://blog.chengyunfeng.com/?p=458#">Android 性能分析案例 - 云在千峰</a></li> <li><a href="http://mrpeak.cn/android/2016/01/11/android-performance-ui">Android UI 性能优化详解</a></li> <li><a href="http://blog.udinic.com/2015/09/15/speed-up-your-app">Speed up your app</a></li> </ul> </description>
<pubDate>Thu, 29 Sep 2016 00:00:00 +0000</pubDate>
<link>http://jaeger.itscoder.com//android/2016/09/29/android-performance-overdraw.html</link>
<guid isPermaLink="true">http://jaeger.itscoder.com//android/2016/09/29/android-performance-overdraw.html</guid>
</item>
<item>
<title>mUrl:自动生成 Markdown 格式的链接</title>
<description><p>因为懒,花了半个下午时间开发了一个 Chrome 插件,第一次接触这方面东西,写个博客记录下开发过程。</p> <h3 id="需求来源">需求来源</h3> <p>写 Markdown 的时候可能都有这样一个需求:</p> <p>我们需要插入一个文章链接,格式为 Markdown 的格式,例如这样的一个链接:</p> <p><a href="http://jaeger.itscoder.com/">http://jaeger.itscoder.com</a></p> <p>访问之后,假如我们需要将这个地址直接插入到 Markdown 中,格式如下:</p> <div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">[</span><span class="nv">写代码的猴子</span><span class="p">](</span><span class="sx">http://jaeger.itscoder.com</span><span class="p">)</span> </code></pre></div></div> <p>在没一个方便的工具之前,我们只能通过两次复制来解决:</p> <ul> <li>复制对应的 url:<code class="highlighter-rouge">http://jaeger.itscoder.com/</code></li> <li>复制该 url 对应的标题:写代码的猴子</li> </ul> <p>这样的重复性工作对于开发者来说,一点都不酷。因此就催生了 mUrl 插件的诞生。</p> <h3 id="插件成果">插件成果</h3> <p><img src="http://ac-QYgvX1CC.clouddn.com/d93799d12d3dfe2f.png" alt="" /></p> <ul> <li> <p>源码地址:<a href="https://github.com/laobie/mUrl">laobie/mUrl: a chrome extension: get website url for markdown writer</a></p> </li> <li> <p>Chrome 商店地址:<a href="https://chrome.google.com/webstore/detail/murl/nmhkegedgpbbkcicjgcnbjebdjedljgl?utm_source=chrome-ntp-icon">mUrl - Chrome Web Store</a></p> </li> <li> <p>使用方法:</p> <ul> <li> <p>在 Chrome 应用商店添加插件 mUrl;</p> </li> <li> <p>将 mUrl 放置到地址栏右边,如下所示:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/a74423d1aadad5c3.jpg" alt="" /></p> </li> <li> <p>打开一个网页,然后点这个插件,此时 Markdown 格式的链接就已经复制到剪贴板上了,直接粘贴到 Markdown 文件中即可。</p> </li> </ul> </li> </ul> <h3 id="开发过程">开发过程</h3> <p>这是官方提供的 Chrome 插件的教程:</p> <p><a href="https://developer.chrome.com/extensions/getstarted">Getting Started: Building a Chrome Extension</a></p> <p>跟着教程来的话你就可以创建一个你自己的插件,不过官方的例子由于墙的原因,我并没有成功开发出来(伟大的 GFW)。</p> <p>接下来就以 mUrl 项目为例,讲解下如何开发一个 Chrome 插件。</p> <ol> <li><strong>准备工作</strong> <ul> <li>一个文本编辑器</li> <li>Chrome 浏览器</li> </ul> </li> <li> <p><strong>项目结构</strong></p> <p>新建一个项目文件夹,我这里创建了一个 <code class="highlighter-rouge">mUrl</code> 的文件夹,该文件夹为插件的根目录,里面包含以下文件:</p> <ul> <li> <p><code class="highlighter-rouge">manifest.json</code> 项目配置文件,包含一些插件的基本信息</p> </li> <li> <p><code class="highlighter-rouge">murl_icon.png</code> 插件图标文件</p> </li> <li> <p><code class="highlighter-rouge">popup.html</code> 插件启动时显示的窗体布局</p> </li> <li> <p><code class="highlighter-rouge">popup.js</code> 执行相关逻辑的 JavaScript 脚本</p> </li> </ul> <p>接下来就对这些文件的作用和具体开发过程进行讲解。</p> </li> <li> <p><strong>manifest.json</strong></p> <div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w"> </span><span class="s2">"manifest_version"</span><span class="p">:</span><span class="w"> </span><span class="mi">2</span><span class="p">,</span><span class="w"> </span><span class="s2">"name"</span><span class="p">:</span><span class="w"> </span><span class="s2">"mUrl"</span><span class="p">,</span><span class="w"> </span><span class="s2">"description"</span><span class="p">:</span><span class="w"> </span><span class="s2">"This extension get url for Markdown writer"</span><span class="p">,</span><span class="w"> </span><span class="s2">"version"</span><span class="p">:</span><span class="w"> </span><span class="s2">"0.1"</span><span class="p">,</span><span class="w"> </span><span class="s2">"browser_action"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">"default_icon"</span><span class="p">:</span><span class="w"> </span><span class="s2">"murl_icon.png"</span><span class="p">,</span><span class="w"> </span><span class="s2">"default_popup"</span><span class="p">:</span><span class="w"> </span><span class="s2">"popup.html"</span><span class="p">,</span><span class="w"> </span><span class="s2">"default_title"</span><span class="p">:</span><span class="s2">"Get Url For Markdown writer"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="s2">"icons"</span><span class="p">:</span><span class="w"> </span><span class="p">{</span><span class="w"> </span><span class="s2">"16"</span><span class="p">:</span><span class="s2">"asset/murl_16.png"</span><span class="p">,</span><span class="w"> </span><span class="s2">"48"</span><span class="p">:</span><span class="s2">"asset/murl_48.png"</span><span class="p">,</span><span class="w"> </span><span class="s2">"128"</span><span class="p">:</span><span class="w"> </span><span class="s2">"asset/murl_128.png"</span><span class="w"> </span><span class="p">},</span><span class="w"> </span><span class="s2">"permissions"</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="w"> </span><span class="s2">"activeTab"</span><span class="w"> </span><span class="p">]</span><span class="w"> </span><span class="p">}</span><span class="w"> </span></code></pre></div> </div> <p>这是一个 JSON 格式的文件,其中:</p> <ul> <li><code class="highlighter-rouge">manifest_version</code>: 一般默认是 2,不用改动</li> <li><code class="highlighter-rouge">name</code>: 插件的名称,会显示在 Chrome 商店中</li> <li><code class="highlighter-rouge">description</code>: 插件描述</li> <li><code class="highlighter-rouge">version</code>: 插件版本号,升级的时候需要更新</li> <li> <p><code class="highlighter-rouge">browser_action</code>: 和浏览器相关的</p> <ul> <li> <p><code class="highlighter-rouge">default_icon</code>: 显示在地址栏上的图标,19*19 的 png 格式的图片</p> </li> <li> <p><code class="highlighter-rouge">default_popup</code>: 一个 HTML 文件,点击插件时弹出来的界面</p> </li> <li> <p><code class="highlighter-rouge">default_title</code>: 鼠标移动到图标上显示的提示</p> </li> </ul> </li> <li><code class="highlighter-rouge">icons</code>: 图标,数字对应图标的大小,这几个尺寸的图标不是必须的,但是为了保证图标显示正常,建议添加这几个尺寸的图标。</li> <li><code class="highlighter-rouge">permissions</code>: 插件需要的权限,例如读取网页内容的权限,mUrl 只需要获取到当前 tab 的信息,所以只需要添加一个权限。</li> </ul> </li> <li> <p><strong>murl_icon.png</strong></p> <p>插件的图标文件,在 <code class="highlighter-rouge">manifest.json</code> 使用 <code class="highlighter-rouge">default_icon</code> 定义的,大小是 19*19,格式为 png 的图片。</p> </li> <li> <p><strong>popup.html</strong></p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="cp">&lt;!doctype html&gt;</span> <span class="nt">&lt;html&gt;</span> <span class="nt">&lt;head&gt;</span> <span class="nt">&lt;title&gt;</span>Copy Url For Markdown<span class="nt">&lt;/title&gt;</span> <span class="nt">&lt;style&gt;</span> <span class="nt">body</span> <span class="p">{</span> <span class="nl">font-family</span><span class="p">:</span> <span class="s1">"Segoe UI"</span><span class="p">,</span> <span class="s1">"Lucida Grande"</span><span class="p">,</span> <span class="n">Tahoma</span><span class="p">,</span> <span class="nb">sans-serif</span><span class="p">;</span> <span class="nl">font-size</span><span class="p">:</span> <span class="m">100%</span><span class="p">;</span> <span class="p">}</span> <span class="nt">&lt;/style&gt;</span> <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"popup.js"</span><span class="nt">&gt;&lt;/script&gt;</span> <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"clipboard.js"</span><span class="nt">&gt;&lt;/script&gt;</span> <span class="nt">&lt;/head&gt;</span> <span class="nt">&lt;body&gt;</span> <span class="nt">&lt;small</span> <span class="na">id=</span><span class="s">"msg_text"</span><span class="nt">&gt;&lt;/small&gt;</span> <span class="nt">&lt;input</span> <span class="na">id=</span><span class="s">"md_format_url"</span> <span class="na">value=</span><span class="s">"test"</span><span class="nt">&gt;</span> <span class="nt">&lt;button</span> <span class="na">class=</span><span class="s">"btn"</span> <span class="na">id=</span><span class="s">"btn_copy"</span> <span class="na">data-clipboard-target=</span><span class="s">"#md_format_url"</span><span class="nt">&gt;</span>copy<span class="nt">&lt;/button&gt;</span> <span class="nt">&lt;/body&gt;</span> <span class="nt">&lt;/html&gt;</span> </code></pre></div> </div> <p>就是一个简单的 HTML 文件,其中需要指定下需要使用的 <strong>JavaScript</strong> 文件,比如 mUrl 使用了:</p> <ul> <li> <p>popup.js</p> </li> <li> <p>clipboard.js</p> </li> </ul> <p>这两个文件,就通过以下进行了引用:</p> <div class="language-html highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"popup.js"</span><span class="nt">&gt;&lt;/script&gt;</span> <span class="nt">&lt;script </span><span class="na">src=</span><span class="s">"clipboard.js"</span><span class="nt">&gt;&lt;/script&gt;</span> </code></pre></div> </div> <p>这里需要注意的是,在官方给的 <a href="view-source:https://developer.chrome.com/extensions/examples/tutorials/getstarted/popup.html">popup.html</a> 中有这样的一段注释:</p> <blockquote> <p>JavaScript and HTML must be in separate files: see our Content Security Policy documentation for details and explanation.</p> </blockquote> <p>出于安全性的考虑, JavaScript 文件必须和 HTML 文件分开,而不能为了方便直接在 HTML 文件中执行 JavaScript 脚本。</p> <p>在 <code class="highlighter-rouge">body</code> 中放置了三个元素:</p> <ul> <li> <p><code class="highlighter-rouge">small</code>: 用来显示复制结果的信息</p> </li> <li> <p><code class="highlighter-rouge">input</code>: 输入框,用来填写 Markdown 格式的链接文本</p> <p>因为需要复制到剪切板,我使用了一个 <a href="https://github.com/zenorocha/clipboard.js">zenorocha/clipboard.js</a> 这个项目中的 js 代码,需要从一个元素中复制文本,因此只能添加一个输入框,给输入框设置文本内容,然后复制。</p> </li> <li> <p><code class="highlighter-rouge">button</code>: 一个按钮,用来复制上面的输入框中的文本,后面使用 JavaScript 自动点击这个按钮,实际上也不需要手动去复制。需要注意到还给 button 添加了一个 <code class="highlighter-rouge">data-clipboard-target</code> 属性,这是使用 <code class="highlighter-rouge">clipboard.js</code> 需要设置的属性,在点这个按钮的时候,自动取上面提到 input 输入框的内容,复制到剪贴板。</p> </li> </ul> </li> <li> <p><strong>popup.js</strong></p> <p>在讲解 popup.js 内容之前,先说一下开发时遇到的一个问题:</p> <p>因为需要在点击插件时自动将得到的 Markdown 格式的链接复制到剪贴板上,对于我这个 JavaScript 还没入门的新手来说并不是那么容易,好在通过 Google 搜索了一圈之后,找到了 <a href="https://github.com/zenorocha/clipboard.js">zenorocha/clipboard.js</a> 这项目,解决了这个问题。关于这个项目的具体使用细节可以阅读项目的 README,本文不再赘述。</p> <div class="language-javascript highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">function</span> <span class="nx">getCurrentTabInfo</span><span class="p">(</span><span class="nx">callback</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">queryInfo</span> <span class="o">=</span> <span class="p">{</span> <span class="na">active</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">currentWindow</span><span class="p">:</span> <span class="kc">true</span> <span class="p">};</span> <span class="nx">chrome</span><span class="p">.</span><span class="nx">tabs</span><span class="p">.</span><span class="nx">query</span><span class="p">(</span><span class="nx">queryInfo</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">tabs</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">tab</span> <span class="o">=</span> <span class="nx">tabs</span><span class="p">[</span><span class="mi">0</span><span class="p">];</span> <span class="nx">callback</span><span class="p">(</span><span class="nx">tab</span><span class="p">.</span><span class="nx">title</span><span class="p">,</span> <span class="nx">tab</span><span class="p">.</span><span class="nx">url</span><span class="p">);</span> <span class="p">});</span> <span class="p">}</span> <span class="nb">document</span><span class="p">.</span><span class="nx">addEventListener</span><span class="p">(</span><span class="s1">'DOMContentLoaded'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">()</span> <span class="p">{</span> <span class="nx">getCurrentTabInfo</span><span class="p">(</span><span class="kd">function</span> <span class="p">(</span><span class="nx">title</span><span class="p">,</span> <span class="nx">url</span><span class="p">)</span> <span class="p">{</span> <span class="kd">var</span> <span class="nx">msgText</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"msg_text"</span><span class="p">);</span> <span class="nx">msgText</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s2">"none"</span><span class="p">;</span> <span class="kd">var</span> <span class="nx">inputText</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"md_format_url"</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">copyBtn</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nx">getElementById</span><span class="p">(</span><span class="s2">"btn_copy"</span><span class="p">);</span> <span class="kd">var</span> <span class="nx">clipboard</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Clipboard</span><span class="p">(</span><span class="s1">'.btn'</span><span class="p">);</span> <span class="nx">clipboard</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">'success'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">e</span><span class="p">);</span> <span class="nx">inputText</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s2">"none"</span><span class="p">;</span> <span class="nx">copyBtn</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s2">"none"</span><span class="p">;</span> <span class="nx">msgText</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s2">"success"</span><span class="p">;</span> <span class="nx">msgText</span><span class="p">.</span><span class="nx">style</span><span class="p">.</span><span class="nx">display</span> <span class="o">=</span> <span class="s2">"block"</span><span class="p">;</span> <span class="p">});</span> <span class="nx">clipboard</span><span class="p">.</span><span class="nx">on</span><span class="p">(</span><span class="s1">'error'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="p">{</span> <span class="nx">console</span><span class="p">.</span><span class="nx">log</span><span class="p">(</span><span class="nx">e</span><span class="p">);</span> <span class="nx">alert</span><span class="p">(</span><span class="nx">e</span><span class="p">);</span> <span class="p">});</span> <span class="c1">// 替换标题中的特殊字符,例如“[]()”等</span> <span class="nx">title</span> <span class="o">=</span> <span class="nx">title</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="sr">/</span><span class="se">[</span><span class="sr">|</span><span class="se">\\</span><span class="sr">`*_{}</span><span class="se">\[\]</span><span class="sr">()#+</span><span class="se">\-</span><span class="sr">.!</span><span class="se">]</span><span class="sr">/g</span><span class="p">,</span> <span class="s1">'</span><span class="err">\\</span><span class="s1">$&amp;'</span><span class="p">);</span> <span class="c1">// 拼接 Markdown 格式的链接字符串</span> <span class="nx">inputText</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="s2">"["</span> <span class="o">+</span> <span class="nx">title</span> <span class="o">+</span> <span class="s2">"]("</span> <span class="o">+</span> <span class="nx">url</span> <span class="o">+</span> <span class="s2">")"</span><span class="p">;</span> <span class="nx">copyBtn</span><span class="p">.</span><span class="nx">click</span><span class="p">();</span> <span class="p">});</span> <span class="p">});</span> </code></pre></div> </div> <p>popup.js 的代码应该也不难理解:</p> <ul> <li> <p><code class="highlighter-rouge">getCurrentTabInfo()</code> 函数:获取到当期激活的 Tab 的 url 和 title 信息,后面拼接链接需要使用。</p> </li> <li> <p><code class="highlighter-rouge">document.addEventListener('DOMContentLoaded', function () {}</code></p> <p>添加页面加载监听,页面加载时,调用 <code class="highlighter-rouge">getCurrentTabInfo()</code> 函数,获得当前 Tab 的标题和 url:</p> <ul> <li> <p>先获取到之前添加的三个元素,并进行显示和隐藏的设置</p> </li> <li> <p>创建一个 Clipboard 对象,用来复制格式化之后的链接</p> </li> <li> <p>设置复制成功和出错的监听,在复制成功时,将 <strong>输入框</strong> 和 <strong>按钮</strong> 隐藏,只显示 『success』文本,复制出错时则弹一个对话框,显示出错信息。</p> </li> <li> <p>在拼接 Markdown 格式的链接字符串之前,先对标题中的特殊字符进行了替换处理,例如 “[”、“]” 需要替换成 “[”、“]” 。</p> </li> <li> <p>自动点击 <strong>复制</strong> 按钮,将文本复制到剪切板,复制成功,就会响应上面设置 <strong>复制成功</strong> 的监听事件。</p> </li> </ul> </li> </ul> </li> <li>因为项目使用到了 <code class="highlighter-rouge">clipboard.js</code> ,所以需要将该 JavaScript 文件也放置到项目文件夹中。</li> </ol> <p>至此,整个项目基本搞定了,接下来就是运行测试了。</p> <h3 id="运行和发布">运行和发布</h3> <ol> <li> <p>运行</p> <ul> <li> <p>在 Chrome 中打开:<a href="chrome://extensions/">chrome://extensions/</a></p> </li> <li> <p>打开开发者模式,选择项目文件夹,加载插件</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/0a56fe81d8cbdf25.jpg" alt="" /></p> </li> <li> <p>上一步没报错的话,插件就加载成功了,这是你就可以点击插件图标测试下功能是否正常了。</p> </li> <li> <p>注意到 <strong>加载已解压的扩展程序</strong> 右边有一个 <strong>打包扩展程序</strong> 的按钮,点击这个:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/df023de5b50d4617.jpg" alt="" /></p> <p>私有密钥文件第一次可以不选,在第一次打包之后就会自动生成一个 .gem 后缀的私有密钥文件,下次更新插件时需要选择这个密钥文件。</p> <p>打包会生成一个 .crx 格式的文件,这个就是插件的安装文件,发送给其他人,安装上之后就可以使用该插件了。</p> </li> </ul> </li> <li> <p>发布到 Chrome 应用商店</p> <p>在 <a href="https://chrome.google.com/webstore/developer/dashboard?utm_source=chrome-ntp-icon">Developer Dashboard - Chrome Web Store</a> 按照步骤先注册一个开发者帐号,需要支付 $5 ,如果你没 VISA 信用卡的话,可能就比较麻烦了。</p> <p>注册好开发者帐号后,<a href="https://chrome.google.com/webstore/developer/update?utm_source=chrome-ntp-icon&amp;publisherId=g03303820154321143420">Upload - Developer Dashboard</a> 在这个页面按照下面的说明,上传项目的源文件,填写上相关的信息,就可以了,页面上说明很齐全,这里不多讲解了。</p> <p>发布到 Chrome 应用商店之后,别人就可以直接从商店下载安装你开发的插件了。</p> <p>​</p> </li> </ol> <p>最后,再提一下 mUrl 的插件源码地址:<a href="https://github.com/laobie/mUrl">laobie/mUrl</a>,欢迎提 PR 和建议。</p> <h3 id="参考资料">参考资料</h3> <ul> <li> <p><a href="http://www.cnblogs.com/guogangj/p/3235703.html">Chrome插件(Extensions)开发攻略 - guogangj - 博客园</a></p> </li> <li> <p><a href="http://9iphp.com/web/javascript/js-copy-library-clipboard-js.html">纯JavaScript实现的复制剪切库–clipboard.js | Specs’ Blog-就爱PHP</a></p> </li> <li> <p><a href="https://developer.chrome.com/extensions/getstarted">Getting Started: Building a Chrome Extension - Google Chrome</a></p> </li> <li> <p><a href="https://github.com/ku/CreateLink">ku/CreateLink: Make Link alternative to chrome</a></p> </li> </ul> </description>
<pubDate>Mon, 26 Sep 2016 00:00:00 +0000</pubDate>
<link>http://jaeger.itscoder.com//chrome%20extension/2016/09/26/chrome-extension-murl.html</link>
<guid isPermaLink="true">http://jaeger.itscoder.com//chrome%20extension/2016/09/26/chrome-extension-murl.html</guid>
</item>
<item>
<title>热修复实现:ClassLoader 方式的实现</title>
<description><blockquote> <ul> <li>文章来源:itsCoder 的 <a href="https://github.com/itsCoder/weeklyblog">WeeklyBolg</a> 项目</li> <li>itsCoder 主页:<a href="http://itscoder.com/">http://itscoder.com/</a></li> <li>作者:<a href="https://github.com/laobie">Jaeger</a></li> <li>审阅者:<a href="https://github.com/hymanme">Hymanme</a>, <a href="https://github.com/brucezz">Brucezz</a></li> </ul> </blockquote> <p>在之前的文章 <a href="http://jaeger.itscoder.com/android/2016/08/27/android-classloader.html">热修复入门:Android 中的 ClassLoader</a> 中,讲解了 Android 中的 ClassLoader 工作原理和通过 ClassLoader 实现热修复的可能性。本文结合 <a href="https://github.com/jasonross/Nuwa">Nuva</a> 项目,来讲讲基于 ClassLoader 方式如何具体实现热修复,阅读本文之前建议先通过前面提到的文章了解下 Android 的 ClassLoader。</p> <h3 id="实现的几个关键点">实现的几个关键点</h3> <p>在讲解实现思路之前,先回顾下 <a href="http://jaeger.itscoder.com/android/2016/08/27/android-classloader.html">热修复入门:Android 中的 ClassLoader</a> 文章中提到的几个关键点,这也是 ClassLoader 方式实现热修复的关键:</p> <ul> <li> <p>在 Android 中,App 安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的。</p> </li> <li> <p>DexClassLoader 可以用来加载 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件</p> </li> <li> <p>DexClassLoader 和 PathClassLoader 的基类 BaseDexClassLoader 查找 class 是通过其内部的 <code class="highlighter-rouge">DexPathList pathList</code> 来查找的</p> </li> <li> <p>DexPathList 内部有一个 <code class="highlighter-rouge">Element[] dexElements</code> 数组,其 <code class="highlighter-rouge">findClass()</code> 方法(源码如下)的实现就是遍历该数组,查找 class ,一旦找到需要的类,就直接返回,停止遍历:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="n">Class</span> <span class="nf">findClass</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">,</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Throwable</span><span class="o">&gt;</span> <span class="n">suppressed</span><span class="o">)</span> <span class="o">{</span> <span class="k">for</span> <span class="o">(</span><span class="n">Element</span> <span class="n">element</span> <span class="o">:</span> <span class="n">dexElements</span><span class="o">)</span> <span class="o">{</span> <span class="n">DexFile</span> <span class="n">dex</span> <span class="o">=</span> <span class="n">element</span><span class="o">.</span><span class="na">dexFile</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">dex</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">Class</span> <span class="n">clazz</span> <span class="o">=</span> <span class="n">dex</span><span class="o">.</span><span class="na">loadClassBinaryName</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">definingContext</span><span class="o">,</span> <span class="n">suppressed</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">clazz</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">clazz</span><span class="o">;</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">dexElementsSuppressedExceptions</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">suppressed</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="n">dexElementsSuppressedExceptions</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> </li> </ul> <h3 id="实现思路">实现思路</h3> <p>基于 ClassLoader 方式实现的热修复思路如下图所示:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/b1c92f1555e7fb4b.jpg" alt="" /></p> <p>主要步骤:</p> <ol> <li> <p>假设 MainActivity 中有一个方法<code class="highlighter-rouge">showMsg</code> ,现在显示的是 “bug” ,需要修复。</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="n">AppCompatActivity</span> <span class="o">{</span> <span class="o">...</span> <span class="kd">public</span> <span class="kt">void</span> <span class="nf">showMsg</span><span class="o">()</span> <span class="o">{</span> <span class="n">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">"bug"</span><span class="o">,</span> <span class="n">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> </code></pre></div> </div> </li> <li> <p>我们修改 <code class="highlighter-rouge">showMsg()</code> 方法,让其显示正确的结果 “meaasge”。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="kt">void</span> <span class="nf">showMsg</span><span class="o">()</span> <span class="o">{</span> <span class="n">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">"message"</span><span class="o">,</span> <span class="n">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> </code></pre></div> </div> </li> <li> <p>制作好补丁包,即 patch.jar 文件,该 patch.jar 文件中就包含已经修复了的 dex 文件,注意此时 patch.jar 会包含一个和原来安装 apk 文件中同样的类 <code class="highlighter-rouge">MainActivity</code> 。</p> </li> <li> <p>在 Application 的 <code class="highlighter-rouge">onCreate</code> 方法中检测是否已经下载好补丁包,如果存在补丁包,就通过 DexClassLoader 加载 patch.jar,然后通过反射拿到 DexClassLoader 中的 DexPathList 对象,进而拿到 <code class="highlighter-rouge">Element[] dexElements</code> 数组,这里标记该 Element 数组为 <strong>newDexElements</strong> 。</p> </li> <li> <p>还是通过反射,拿到 App 默认的 ClassLoader 即 PathClassLoader 的 DexPathList 对象,进而拿到 Element 数组,这里标记下该数组为 <strong>baseDexElements</strong> 。</p> </li> <li> <p>将 newDexElements 和 baseDexElements 合成一个新的数组 <strong>allDexElements</strong> ,且保证 newDexElements 中的值在 allDexElements 数组的最前面。</p> </li> <li> <p>然后还是通过通过反射,将合成的 Element 数组设置给 PathClassLoader 的 DexPathList 对象。</p> </li> <li> <p>在 Application 完成初始化之后,会开始加载 <code class="highlighter-rouge">MainActivity</code> ,加载过程就是通过 DexPathList 对象的 <code class="highlighter-rouge">findClass()</code> 方法来完成的,会从头开始遍历其 Element 数组,会优先查找到之前插入的补丁包中的 dexFile,而原 apk 中的则不会查找到,因此就实现了热修复的目的。</p> </li> </ol> <h3 id="基于-classloader-方式实现需要解决的问题">基于 ClassLoader 方式实现需要解决的问题</h3> <p>在对 Nuwa 源码开始解读之前,先说明下在基于 ClassLoader 方式实现热修复需要解决的问题。</p> <ul> <li> <p>CLASS_ISPREVERIFIED 问题</p> <p>odex 文件是 OptimizedDEX 的缩写,表示经过优化的 dex 文件。由于 Android 程序的 apk 文件为 zip 压缩包格式,Dalvik虚拟机每次加载都需要从 apk 中读取 classes.dex 文件,这会耗费很多 cpu 时间,而采用 odex 方式优化的 dex 文件已经包含了加载 dex 必须的依赖库文件列表,Dalvik 虚拟机只需检测并加载所需的依赖库即可执行相应的 dex 文件,大大缩短了读取 dex 文件所需的时间。同时,Android专门提供了一个验证与优化 dex 文件的工具 dexopt,Dalvik 虚拟机在加载一个 dex 文件时,通过指定的验证与优化选项来调用 dexopt 进行相应的验证与优化操作。</p> <p>在 dex 优化过程中:</p> <blockquote> <p>如果某个类直接方法中引用到的类(第一层级关系,不会进行递归搜索)在同一个 dex 中的话,那么这个类就会被打上 <strong>CLASS_ISPREVERIFIED</strong> 标志。</p> </blockquote> <p>打上这个标志的类,其引用到的类就只会在该类所在的 dex 中查找,如果没找到,就直接报以下异常:</p> <div class="language-verilog highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">java</span><span class="o">.</span><span class="n">lang</span><span class="o">.</span><span class="n">IllegalAccessError</span><span class="o">:</span> <span class="n">Class</span> <span class="kt">ref</span> <span class="n">in</span> <span class="n">pre</span><span class="o">-</span><span class="n">verified</span> <span class="kt">class</span> <span class="n">resolved</span> <span class="n">to</span> <span class="n">unexpected</span> <span class="n">implementation</span> </code></pre></div> </div> <p>而 ClassLoader 方式实现的热修复,必然需要在 patch.jar 的 dex 文件中查找其他类。为了防止类打上 CLASS_ISPREVERIFIED 标志,我们只需要在每个类中引用一个单独的 dex 中的类即可。这个 dex 我们命名为 hack.dex,其包含一个 <code class="highlighter-rouge">HackLoad.java</code> ,接下来需要做的就是在除了 Applicaton 类以为的类的默认构造方法中都引用一下 <code class="highlighter-rouge">HackLoad</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="n">AppCompatActivity</span> <span class="o">{</span> <span class="kd">public</span> <span class="nf">MainActivity</span><span class="o">()</span> <span class="o">{</span> <span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">HackLoad</span><span class="o">.</span><span class="na">class</span><span class="o">);</span> <span class="o">}</span> <span class="o">...</span> <span class="o">}</span> </code></pre></div> </div> <p>以上插入外部类防止打上 CLASS_ISPREVERIFIED 标志的操作也叫做打桩。</p> <p>目前开源的热修复项目插入打桩的代码均是通过 javassist 来实现的,本文这里不做详细介绍了,可以参考一下文章来深入了解:</p> <ul> <li><a href="http://www.jianshu.com/p/56facb3732a7">安卓 App 热补丁动态修复实现 - 简书</a></li> <li><a href="https://www.ibm.com/developerworks/cn/java/j-dyn0916/">Java 编程的动态性, 第四部分: 用 Javassist 进行类转换</a></li> </ul> <blockquote> <p>注:Android 官方增加类的验证过程,并打上 CLASS_ISPREVERIFIED 标志,肯定是为了提升性能和效率的,因此这种解决方案对性能确实存在一定的影响,在微信的 Tinker 方案对比中,也给出了实际的效率对比,差距还是挺大的,因此在使用该方式实现热修复需要了解到这一点。</p> </blockquote> <p><img src="http://ac-QYgvX1CC.clouddn.com/04eb03974bad8947.png" alt="" /></p> </li> </ul> <h3 id="nuva-项目的源码解读">Nuva 项目的源码解读</h3> <p>在前面的实现思路分析中,可以说整体思路是比较简单清晰的,按照此思路来,具体的实现其实也不难。接下来就以 Nuwa 项目的源码来解读下具体的实现。</p> <ol> <li> <p>项目结构分析</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/473528c66cc757e3.jpg" alt="" /></p> <p>Nuwa 项目的结构如上图所示,可以看出,项目结构并不复杂:</p> <ul> <li><code class="highlighter-rouge">util/AssetUtils.java</code> Asset 工具类,内部两个方法:复制 Asset 资源和复制文件。</li> <li><code class="highlighter-rouge">util/DexUtils.java</code> dex 工具类,主要是实现将 patch.jar 文件中的 dexFile 插入到 PathClassLoader 对应的 Element 数组的前面。</li> <li><code class="highlighter-rouge">util/ReflectionUtils.java</code> 反射工具类,实现了两个方法:获取和设置无访问权限域(字段)的值。</li> <li><code class="highlighter-rouge">Nuwa.java</code> 项目主类,其包含两个方法:初始化方法,加载补丁方法。</li> </ul> </li> <li> <p>Nuva 的实现过程:初始化和加载 dex</p> <p>在 Nuwa 项目的使用说明中,需要在 Application 中添加如下代码:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nd">@Override</span> <span class="kd">protected</span> <span class="kt">void</span> <span class="nf">attachBaseContext</span><span class="o">(</span><span class="n">Context</span> <span class="n">base</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">.</span><span class="na">attachBaseContext</span><span class="o">(</span><span class="n">base</span><span class="o">);</span> <span class="n">Nuwa</span><span class="o">.</span><span class="na">init</span><span class="o">(</span><span class="k">this</span><span class="o">);</span> <span class="o">}</span> </code></pre></div> </div> <p>直接看 <code class="highlighter-rouge">Nuwa.java</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">Nuwa</span> <span class="o">{</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">TAG</span> <span class="o">=</span> <span class="s">"nuwa"</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">HACK_DEX</span> <span class="o">=</span> <span class="s">"hack.apk"</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">DEX_DIR</span> <span class="o">=</span> <span class="s">"nuwa"</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">DEX_OPT_DIR</span> <span class="o">=</span> <span class="s">"nuwaopt"</span><span class="o">;</span> <span class="cm">/** * 初始时加载 hack.pak 的 dex 文件,处理打桩 * @param context */</span> <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">init</span><span class="o">(</span><span class="n">Context</span> <span class="n">context</span><span class="o">)</span> <span class="o">{</span> <span class="n">File</span> <span class="n">dexDir</span> <span class="o">=</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getFilesDir</span><span class="o">(),</span> <span class="n">DEX_DIR</span><span class="o">);</span> <span class="n">dexDir</span><span class="o">.</span><span class="na">mkdir</span><span class="o">();</span> <span class="n">String</span> <span class="n">dexPath</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="k">try</span> <span class="o">{</span> <span class="n">dexPath</span> <span class="o">=</span> <span class="n">AssetUtils</span><span class="o">.</span><span class="na">copyAsset</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">HACK_DEX</span><span class="o">,</span> <span class="n">dexDir</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IOException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="n">TAG</span><span class="o">,</span> <span class="s">"copy "</span> <span class="o">+</span> <span class="n">HACK_DEX</span> <span class="o">+</span> <span class="s">" failed"</span><span class="o">);</span> <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span> <span class="o">}</span> <span class="n">loadPatch</span><span class="o">(</span><span class="n">context</span><span class="o">,</span> <span class="n">dexPath</span><span class="o">);</span> <span class="o">}</span> <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">loadPatch</span><span class="o">(</span><span class="n">Context</span> <span class="n">context</span><span class="o">,</span> <span class="n">String</span> <span class="n">dexPath</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</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="o">{</span> <span class="n">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="n">TAG</span><span class="o">,</span> <span class="s">"context is null"</span><span class="o">);</span> <span class="k">return</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(!</span><span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">dexPath</span><span class="o">).</span><span class="na">exists</span><span class="o">())</span> <span class="o">{</span> <span class="n">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="n">TAG</span><span class="o">,</span> <span class="n">dexPath</span> <span class="o">+</span> <span class="s">" is null"</span><span class="o">);</span> <span class="k">return</span><span class="o">;</span> <span class="o">}</span> <span class="n">File</span> <span class="n">dexOptDir</span> <span class="o">=</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getFilesDir</span><span class="o">(),</span> <span class="n">DEX_OPT_DIR</span><span class="o">);</span> <span class="n">dexOptDir</span><span class="o">.</span><span class="na">mkdir</span><span class="o">();</span> <span class="k">try</span> <span class="o">{</span> <span class="n">DexUtils</span><span class="o">.</span><span class="na">injectDexAtFirst</span><span class="o">(</span><span class="n">dexPath</span><span class="o">,</span> <span class="n">dexOptDir</span><span class="o">.</span><span class="na">getAbsolutePath</span><span class="o">());</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">Exception</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="n">TAG</span><span class="o">,</span> <span class="s">"inject "</span> <span class="o">+</span> <span class="n">dexPath</span> <span class="o">+</span> <span class="s">" failed"</span><span class="o">);</span> <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> <p>在 <code class="highlighter-rouge">init()</code> 方法中,通过加载 asset 文件夹中的 hack.apk 文件,将插桩类加载进来,防止之前插桩的那些类报找不到 <code class="highlighter-rouge">HackLoad.class</code> 异常。这里也可以意识到一点,就是 Application 不应该插桩,否则直接报异常出错。</p> <p>接下来的 <code class="highlighter-rouge">loadPatch(Context context, String dexPath)</code> 才是重点,除了在 <code class="highlighter-rouge">init()</code> 方法中被调用以为,后面加载补丁 patch.jar 时也是使用该方法来加载。其需要两个参数:一个是上下文 context,一个是包含 dex 的 jar 或者 apk 文件的路径。</p> <p>注意到其中有这么一段代码:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">File</span> <span class="n">dexOptDir</span> <span class="o">=</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">context</span><span class="o">.</span><span class="na">getFilesDir</span><span class="o">(),</span> <span class="n">DEX_OPT_DIR</span><span class="o">);</span> <span class="n">dexOptDir</span><span class="o">.</span><span class="na">mkdir</span><span class="o">();</span> </code></pre></div> </div> <p>这个得到的是一个存放优化后的 dex 文件的路径,这是 DexClassLoader 类的构造方法所需要的:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">DexClassLoader</span><span class="o">(</span><span class="n">String</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">String</span> <span class="n">optimizedDirectory</span><span class="o">,</span> <span class="n">String</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">ClassLoader</span> <span class="n">parent</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">(</span><span class="n">dexPath</span><span class="o">,</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">optimizedDirectory</span><span class="o">),</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">parent</span><span class="o">);</span> <span class="o">}</span> </code></pre></div> </div> <ul> <li><code class="highlighter-rouge">String optimizedDirectory</code> : 用来缓存优化的 dex 文件的路径,即从 apk 或 jar 文件中提取出来的 dex 文件。该路径不可以为空,且应该是应用私有的,有读写权限的路径(实际上也可以使用外部存储空间,但是这样的话就存在代码注入的风险)。</li> </ul> <p>关于 DexClassLoader 的其他细节,可以阅读本文开头提到的那篇文章。</p> <p>接下来就是调用 <code class="highlighter-rouge">DexUtils.injectDexAtFirst()</code> 方法,看该方法的名称就可以知道,是将对应的 dex 注入到所有的 dex 的最前面。</p> </li> <li> <p>注入补丁的 dex</p> <p>注入补丁的过程主要在 DexUtil 类中:</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">DexUtils</span> <span class="o">{</span> <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">injectDexAtFirst</span><span class="o">(</span><span class="n">String</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">String</span> <span class="n">defaultDexOptPath</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">NoSuchFieldException</span><span class="o">,</span> <span class="n">IllegalAccessException</span><span class="o">,</span> <span class="n">ClassNotFoundException</span> <span class="o">{</span> <span class="n">DexClassLoader</span> <span class="n">dexClassLoader</span> <span class="o">=</span> <span class="k">new</span> <span class="n">DexClassLoader</span><span class="o">(</span><span class="n">dexPath</span><span class="o">,</span> <span class="n">defaultDexOptPath</span><span class="o">,</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">getPathClassLoader</span><span class="o">());</span> <span class="n">Object</span> <span class="n">baseDexElements</span> <span class="o">=</span> <span class="n">getDexElements</span><span class="o">(</span><span class="n">getPathList</span><span class="o">(</span><span class="n">getPathClassLoader</span><span class="o">()));</span> <span class="n">Object</span> <span class="n">newDexElements</span> <span class="o">=</span> <span class="n">getDexElements</span><span class="o">(</span><span class="n">getPathList</span><span class="o">(</span><span class="n">dexClassLoader</span><span class="o">));</span> <span class="n">Object</span> <span class="n">allDexElements</span> <span class="o">=</span> <span class="n">combineArray</span><span class="o">(</span><span class="n">newDexElements</span><span class="o">,</span> <span class="n">baseDexElements</span><span class="o">);</span> <span class="n">Object</span> <span class="n">pathList</span> <span class="o">=</span> <span class="n">getPathList</span><span class="o">(</span><span class="n">getPathClassLoader</span><span class="o">());</span> <span class="n">ReflectionUtils</span><span class="o">.</span><span class="na">setField</span><span class="o">(</span><span class="n">pathList</span><span class="o">,</span> <span class="n">pathList</span><span class="o">.</span><span class="na">getClass</span><span class="o">(),</span> <span class="s">"dexElements"</span><span class="o">,</span> <span class="n">allDexElements</span><span class="o">);</span> <span class="o">}</span> <span class="kd">private</span> <span class="kd">static</span> <span class="n">PathClassLoader</span> <span class="nf">getPathClassLoader</span><span class="o">()</span> <span class="o">{</span> <span class="n">PathClassLoader</span> <span class="n">pathClassLoader</span> <span class="o">=</span> <span class="o">(</span><span class="n">PathClassLoader</span><span class="o">)</span> <span class="n">DexUtils</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getClassLoader</span><span class="o">();</span> <span class="k">return</span> <span class="n">pathClassLoader</span><span class="o">;</span> <span class="o">}</span> <span class="kd">private</span> <span class="kd">static</span> <span class="n">Object</span> <span class="nf">getDexElements</span><span class="o">(</span><span class="n">Object</span> <span class="n">paramObject</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">IllegalArgumentException</span><span class="o">,</span> <span class="n">NoSuchFieldException</span><span class="o">,</span> <span class="n">IllegalAccessException</span> <span class="o">{</span> <span class="k">return</span> <span class="n">ReflectionUtils</span><span class="o">.</span><span class="na">getField</span><span class="o">(</span><span class="n">paramObject</span><span class="o">,</span> <span class="n">paramObject</span><span class="o">.</span><span class="na">getClass</span><span class="o">(),</span> <span class="s">"dexElements"</span><span class="o">);</span> <span class="o">}</span> <span class="kd">private</span> <span class="kd">static</span> <span class="n">Object</span> <span class="nf">getPathList</span><span class="o">(</span><span class="n">Object</span> <span class="n">baseDexClassLoader</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">IllegalArgumentException</span><span class="o">,</span> <span class="n">NoSuchFieldException</span><span class="o">,</span> <span class="n">IllegalAccessException</span><span class="o">,</span> <span class="n">ClassNotFoundException</span> <span class="o">{</span> <span class="k">return</span> <span class="n">ReflectionUtils</span><span class="o">.</span><span class="na">getField</span><span class="o">(</span><span class="n">baseDexClassLoader</span><span class="o">,</span> <span class="n">Class</span><span class="o">.</span><span class="na">forName</span><span class="o">(</span><span class="s">"dalvik.system.BaseDexClassLoader"</span><span class="o">),</span> <span class="s">"pathList"</span><span class="o">);</span> <span class="o">}</span> <span class="kd">private</span> <span class="kd">static</span> <span class="n">Object</span> <span class="nf">combineArray</span><span class="o">(</span><span class="n">Object</span> <span class="n">firstArray</span><span class="o">,</span> <span class="n">Object</span> <span class="n">secondArray</span><span class="o">)</span> <span class="o">{</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">localClass</span> <span class="o">=</span> <span class="n">firstArray</span><span class="o">.</span><span class="na">getClass</span><span class="o">().</span><span class="na">getComponentType</span><span class="o">();</span> <span class="kt">int</span> <span class="n">firstArrayLength</span> <span class="o">=</span> <span class="n">Array</span><span class="o">.</span><span class="na">getLength</span><span class="o">(</span><span class="n">firstArray</span><span class="o">);</span> <span class="kt">int</span> <span class="n">allLength</span> <span class="o">=</span> <span class="n">firstArrayLength</span> <span class="o">+</span> <span class="n">Array</span><span class="o">.</span><span class="na">getLength</span><span class="o">(</span><span class="n">secondArray</span><span class="o">);</span> <span class="n">Object</span> <span class="n">result</span> <span class="o">=</span> <span class="n">Array</span><span class="o">.</span><span class="na">newInstance</span><span class="o">(</span><span class="n">localClass</span><span class="o">,</span> <span class="n">allLength</span><span class="o">);</span> <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">k</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="n">k</span> <span class="o">&lt;</span> <span class="n">allLength</span><span class="o">;</span> <span class="o">++</span><span class="n">k</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">k</span> <span class="o">&lt;</span> <span class="n">firstArrayLength</span><span class="o">)</span> <span class="o">{</span> <span class="n">Array</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">result</span><span class="o">,</span> <span class="n">k</span><span class="o">,</span> <span class="n">Array</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">firstArray</span><span class="o">,</span> <span class="n">k</span><span class="o">));</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">Array</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">result</span><span class="o">,</span> <span class="n">k</span><span class="o">,</span> <span class="n">Array</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">secondArray</span><span class="o">,</span> <span class="n">k</span> <span class="o">-</span> <span class="n">firstArrayLength</span><span class="o">));</span> <span class="o">}</span> <span class="o">}</span> <span class="k">return</span> <span class="n">result</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> <p>结合上文实现思路的分析,<code class="highlighter-rouge">injectDexAtFirst()</code> 方法的流程是很清晰的:</p> <ul> <li>通过 <code class="highlighter-rouge">DexClassLoader</code> 加载补丁中的 dex 文件,然后反射得到新的 Element 集合:<code class="highlighter-rouge">newDexElements</code> ;</li> <li>拿到 <code class="highlighter-rouge">PathClassLoader</code> 中的 Element 集合:<code class="highlighter-rouge">baseDexElements</code> ;</li> <li>将 <code class="highlighter-rouge">newDexElements</code> 和 <code class="highlighter-rouge">baseDexElements</code> 组合成整个的 Element 组合,组合是放在 <code class="highlighter-rouge">combineArray</code> 方法中执行的,看看其具体的实现,就可以发现会优先将 newDexElements 中的值放在合成数组的最前面,这也是之前所提到的实现热修复的关键点之一。</li> <li>将合成后的 <code class="highlighter-rouge">allDexElements</code> 设置给 PathClassLoader 的 DexPathList 对应的 Element 数组。</li> </ul> <p>反射工具类的源码如下:</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">ReflectionUtils</span> <span class="o">{</span> <span class="kd">public</span> <span class="kd">static</span> <span class="n">Object</span> <span class="nf">getField</span><span class="o">(</span><span class="n">Object</span> <span class="n">obj</span><span class="o">,</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">cl</span><span class="o">,</span> <span class="n">String</span> <span class="n">field</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">NoSuchFieldException</span><span class="o">,</span> <span class="n">IllegalArgumentException</span><span class="o">,</span> <span class="n">IllegalAccessException</span> <span class="o">{</span> <span class="n">Field</span> <span class="n">localField</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="na">getDeclaredField</span><span class="o">(</span><span class="n">field</span><span class="o">);</span> <span class="n">localField</span><span class="o">.</span><span class="na">setAccessible</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> <span class="k">return</span> <span class="n">localField</span><span class="o">.</span><span class="na">get</span><span class="o">(</span><span class="n">obj</span><span class="o">);</span> <span class="o">}</span> <span class="kd">public</span> <span class="kd">static</span> <span class="kt">void</span> <span class="nf">setField</span><span class="o">(</span><span class="n">Object</span> <span class="n">obj</span><span class="o">,</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">cl</span><span class="o">,</span> <span class="n">String</span> <span class="n">field</span><span class="o">,</span> <span class="n">Object</span> <span class="n">value</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">NoSuchFieldException</span><span class="o">,</span> <span class="n">IllegalArgumentException</span><span class="o">,</span> <span class="n">IllegalAccessException</span> <span class="o">{</span> <span class="n">Field</span> <span class="n">localField</span> <span class="o">=</span> <span class="n">cl</span><span class="o">.</span><span class="na">getDeclaredField</span><span class="o">(</span><span class="n">field</span><span class="o">);</span> <span class="n">localField</span><span class="o">.</span><span class="na">setAccessible</span><span class="o">(</span><span class="kc">true</span><span class="o">);</span> <span class="n">localField</span><span class="o">.</span><span class="na">set</span><span class="o">(</span><span class="n">obj</span><span class="o">,</span> <span class="n">value</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> <p>关于反射,你可以通过 <a href="http://www.jianshu.com/p/1a60d55a94cd">Java 基础与提高干货系列——Java反射机制</a> 来了解,本文就不多做探讨了。</p> <p>至此,Nuva 的关键代码均解读完毕,就该项目而言,代码量并不多,但是整个实现的思路是很巧妙很清晰的,这也是该项目的关键之处。</p> </li> </ol> <h3 id="后续内容">后续内容</h3> <ul> <li>在接下来的系列文章中,还会结合 Nuva 项目,介绍下补丁包 patch.jar 的生成操作。</li> <li>由于本文时间较为仓促,后续有时间的话会补上实践过程。</li> </ul> <h3 id="参考资料">参考资料</h3> <ul> <li><a href="http://blog.sina.com.cn/s/blog_71338cc10102uwgt.html">绕过 Dalvik 验证技术分析</a></li> <li><a href="http://www.jianshu.com/p/56facb3732a7">安卓 App 热补丁动态修复实现 - 简书</a></li> </ul> </description>
<pubDate>Tue, 20 Sep 2016 00:00:00 +0000</pubDate>
<link>http://jaeger.itscoder.com//android/2016/09/20/nuva-source-code-analysis.html</link>
<guid isPermaLink="true">http://jaeger.itscoder.com//android/2016/09/20/nuva-source-code-analysis.html</guid>
</item>
<item>
<title>Android 热修复方案对比</title>
<description><h3 id="概述">概述</h3> <p>没有 Bug 的程序几乎是不存在的,加上 App 更新版本过程又很繁琐,热修复技术从一提出,就拥有很大的技术需求市场。从去年下半年开始,热修复技术就在 Android 技术社区火了一阵子,最近阿里百川正式开启了<a href="https://hotfix.taobao.com/hotfix/index.htm">HotFix</a> 产品的公测服务,这也意味着开始有平台专门提供热修复服务,让普通的开发者和一些小公司也有机会使用上热修复技术,让这项技术不再是大公司才折腾得起、用得起的。当然使用效果或者说提供的服务质量具体怎么样还有待验证。</p> <h3 id="热修复基本原理">热修复基本原理</h3> <p>热修复的基本原理并不多,目前已知可用的热修复实现的原理主要有以下几种:</p> <ol> <li>基于 Xposed 实现的无侵入的运行时 <a href="http://en.wikipedia.org/wiki/Aspect-oriented_programming">AOP (Aspect-oriented Programming)</a>  框架,可以实现在线修复 Bug,修复粒度方法级别,但是由于对 ART 虚拟机不支持,导致其对 Android 5.0、6.0 均不支持,使用局限性太大。目前基于这一原理实现的解决方案是手淘团队开源的 <a href="https://github.com/alibaba/dexposed">Dexposed</a> 项目。</li> <li>native hook 方式,其核心部分在 JNI 层对方法进行替换,替换有问题的方法,修复粒度方法级别,无法在类中新增和删减字段,可以做到即时生效,该原理的实现方案主要是阿里团队开源的 <a href="https://github.com/alibaba/AndFix">AndFix</a> 。</li> <li>该原理由 QQ 空间技术团队提出,使用新的 ClassLoader 加载 patch.dex,hack 默认的 ClassLoader,替换有问题的类,修复粒度类级别,一般无法做到即时生效,需要在应用下一次启动时生效。目前基于该原理实现的方案有 <a href="https://github.com/jasonross/Nuwa">Nuwa</a>、<a href="https://github.com/dodola/HotFix">HotFix</a>、<a href="https://github.com/dodola/RocooFix">RocooFix</a> 。</li> <li>dex 文件全量替换,基于 DexDiff 技术,对比修复前后的 dex 文件,生成 patch.dex,再根据 patch.dex 更新有问题的 dex 文件。该方案由微信团队提出:<a href="http://bugly.qq.com/bbs/forum.php?mod=viewthread&amp;tid=1264">微信Android热补丁实践演进之路</a> ,暂时还未开源。目前基于这一原理实现的开源方案只有一个:<a href="https://github.com/zzz40500/Tinker_imitator">Tinker_imitator</a> 。</li> </ol> <p>目前热修复的原理基本就这四种,考虑到使用的兼容性、可实现性以及可操作性,基本上能实际应用到项目中的就剩下了 2、3 两种了,至于第 4 种方式,只能等微信团队开源出比较成熟的方案,方可实际应用。</p> <h3 id="开源的热修复方案对比">开源的热修复方案对比</h3> <ul> <li> <p><a href="https://github.com/alibaba/dexposed">Dexposed</a></p> <ul> <li>作者:手淘团队</li> <li>修复粒度:方法级别</li> <li>实现原理:基于 Xposed 实现的无侵入的运行时 AOP 框架</li> </ul> <p>该项目明确表示对 ART 虚拟机的不支持,对于 5.1 和 6.0 系统都没法支持,因此该项目基本没有实际应用到项目的意义,毕竟现在 5.0 以上的份额也挺大了。</p> </li> <li> <p><a href="https://github.com/alibaba/AndFix">AndFix</a></p> <ul> <li>作者:阿里技术团队</li> <li>修复粒度:方法级别</li> <li>实现原理:native hook 方式</li> <li>优点:运行时即可修复,修复及时</li> <li>缺点: <ul> <li>只能修复方法,无法新加类和字段</li> <li>对部分机型不支持</li> <li>方法的参数类型有限制</li> <li>打补丁限制较多,以上的限制在打补丁时均需要注意</li> </ul> </li> </ul> <p>目前阿里百川公测的 <a href="https://hotfix.taobao.com/hotfix/index.htm">阿里百川-HotFix</a> 服务应该就是基于 AndFix 技术,具体的使用细节可以看这篇 <a href="https://baichuan.taobao.com/docs/doc.htm?spm=a3c0d.7629140.0.0.dzpp9X&amp;treeId=234&amp;articleId=105457&amp;docType=1">阿里百川 HotFix Android 接入说明</a> ,可以看到其具体的限制基本和 AndFix 项目类似:</p> <blockquote> <p>4.4 HotFix 的使用中不被允许的情况</p> <ul> <li>暂时不支持新增方法、新增类</li> <li>不支持新增 Field</li> <li>不支持针对同一个方法的多次 patch,如果客户端已经有一个 patch 包在运行,则下一个 patch 不会立即生效。</li> <li>三星 note3、S4、S5 的 5.0 设备以及 X8 6设备不支持(<a href="http://baichuan.taobao.com/docs/doc.htm?spm=a3c0d.7629140.0.0.8K3Zr9&amp;treeId=234&amp;articleId=105460&amp;docType=1#s1">点击查看</a>具体支持的机型)</li> <li>参数包括:long、double、float 的方法不能被 patch</li> <li>被反射调用的方法不能被 patch</li> <li>使用 Annotation 的类不能 patch</li> <li>参数超过 8 的方法不能被 patch</li> <li>泛型参数的方法如果 patch 存在兼容性问题</li> </ul> </blockquote> </li> <li> <p><a href="https://github.com/jasonross/Nuwa">Nuwa</a></p> <ul> <li>作者: <a href="https://github.com/jasonross">Jason Ross</a></li> <li>修复粒度:类级别</li> <li>实现原理:ClassLoader 方式</li> <li>优点:兼容性较好,补丁限制较少,类级别的可以增减少字段,补丁自动化做的很完整</li> <li>缺点: <ul> <li>需要在应用重启后才能应用补丁,实现修复</li> <li>需要在每个类默认构造方法插入一段代码,防止类打上 <strong>CLASS_ISPREVERIFIED</strong> 标志,对运行效率有影响</li> <li>目前 issue 中反馈的兼容性问题较多,源码中确实未对各个 Android 版本做差异化处理,存在兼容性问题</li> <li>作者已经停止维护</li> </ul> </li> </ul> <p>该项目在去年刚出现时应该算比较火热,但是由于存在的兼容性问题,让作者也渐渐放弃了该项目,目前来说将该方案应用到项目中是有一定风险的。</p> </li> <li> <p><a href="https://github.com/dodola/HotFix">HotFix</a></p> <ul> <li>作者:<a href="https://github.com/dodola">dodola</a></li> <li>修复粒度:类级别</li> <li>实现原理:ClassLoader 方式</li> </ul> <p>基于 ClassLoader 方式实现,实际使用存在兼容问题,基本类似 Nuwa ,作者已弃坑,新开项目 RocooFix,该项目停止维护。</p> </li> <li> <p><a href="https://github.com/dodola/RocooFix">RocooFix</a></p> <ul> <li>作者:<a href="https://github.com/dodola">dodola</a></li> <li>修复粒度:类级别</li> <li>实现原理:ClassLoader 方式</li> <li>优点: <ul> <li>兼容性较好,源码中对各 Android 进行了差异化处理,一定程度上解决了兼容性问题</li> <li>实现了两种修复方式:静态修复和动态修复,分别是需要重启修复和无需重启即可修复</li> <li>简化了补丁制作流程</li> </ul> </li> <li>缺点: <ul> <li>需要在每个类默认构造方法插入一段代码(也叫做插桩),防止类打上 <strong>CLASS_ISPREVERIFIED</strong> 标志,对运行效率有影响</li> <li>目前就项目下的 issue 来看,还是会存在兼容性问题,对于采用了 APT 技术的项目也存在一些问题</li> <li>动态修复方式还有待检验,使用的是 <a href="https://github.com/asLody/legend">Legend</a> 项目中的相关技术</li> </ul> </li> </ul> <p>总体来说,该开源方案应该是算比较完整的解决方案,作者目前还在维护,对各个 Android 版本的兼容性也做了不少工作,期待作者的后续更新。</p> </li> <li> <p><a href="https://github.com/zzz40500/Tinker_imitator">Tinker_imitator</a></p> <ul> <li>作者:<a href="https://github.com/zzz40500">zzz40500</a></li> <li>修复粒度:dex 级别</li> <li>实现原理:dex 文件全量替换</li> <li>优点:基于 dex 文件全量替换的实现原理相对于 ClassLoader 方式,在性能上有很大优势</li> <li>缺点: <ul> <li>该方案虽然类似微信提出的热修复解决方案,但是 patch.dex 文件的生成并不是依赖于 DexDiff 算法,而是基于 bsdiff ,所以并不是完整实现了微信提出的方案</li> <li>需要重启应用,下次启动时生效</li> <li>生成新的 dex 文件时内存占用较大</li> </ul> </li> </ul> <p>总体来说,该方案目前还停留在 demo 状态,感觉离实际应用到项目中还需要一段时间,基于 dex 文件全量替换的方式我们更多还是期待微信团队的开源。</p> </li> </ul> <h3 id="对比总结">对比总结</h3> <p>就热修复实现的基本原理而言,目前较为成熟的也就 <strong>native hook 方式</strong> 和 <strong>ClassLoader 方式</strong>,在这两个基本原理上实现的开源方案中,AndFix 和 RocooFix 较为成熟,相关的打补丁配套解决方案也比较完备。</p> <p>如果你选择 AndFix 方案,比较倾向于推荐使用阿里百川的 <a href="https://hotfix.taobao.com/hotfix/index.htm">HotFix</a> 服务,希望该服务在公测之后有一个比较完整的服务方案给出,提供一个保证质量的服务。</p> <p>如果你选择 RocooFix 方案,你可能需要跟进作者的更新,及时反馈相关的问题,帮助作者来完善该项目,使得其在兼容性更加提升一步,同时在配套的生成补丁和下发补丁等方案也保证简单可使用。</p> <p>你也可以选择等待微信团队开源 Tinker 项目,毕竟鹅厂这套解决方案看起来很不错,在其实际应用到微信项目的基础上,开源出完整的解决方案,必将是一件有利于开发者的好事。</p> <p>感谢各大公司的技术团队和开源作者们的工作,正是他们让热修复得以实现,虽然各大解决方案都不是那么完美,但是已经有很大改进了,我们期待着越来越多的公司和开发者能够加入到这一工作中来,让热修复不再 “烫” 手。</p> <h3 id="参考文章">参考文章</h3> <ul> <li><a href="http://bugly.qq.com/bbs/forum.php?mod=viewthread&amp;tid=1264">微信Android热补丁实践演进之路</a></li> <li><a href="http://blog.zhaiyifan.cn/2015/11/20/HotPatchCompare/">各大热补丁方案分析和比较</a></li> <li><a href="http://mp.weixin.qq.com/s?__biz=MzI1MTA1MzM2Nw==&amp;mid=400118620&amp;idx=1&amp;sn=b4fdd5055731290eef12ad0d17f39d4a&amp;scene=0">安卓App热补丁动态修复技术介绍</a></li> </ul> </description>
<pubDate>Sun, 28 Aug 2016 00:00:00 +0000</pubDate>
<link>http://jaeger.itscoder.com//android/2016/08/28/android-hot-fix.html</link>
<guid isPermaLink="true">http://jaeger.itscoder.com//android/2016/08/28/android-hot-fix.html</guid>
</item>
<item>
<title>热修复入门:Android 中的 ClassLoader</title>
<description><blockquote> <ul> <li>文章来源:itsCoder 的 <a href="https://github.com/itsCoder/weeklyblog">WeeklyBolg</a> 项目</li> <li>itsCoder 主页:<a href="http://itscoder.com/">http://itscoder.com/</a></li> <li>作者:<a href="https://github.com/laobie">Jaeger</a></li> <li>审阅者:<a href="https://github.com/Zheaoli">Zheaoli</a>, <a href="https://github.com/xcc3641">xcc3641</a></li> </ul> </blockquote> <p>从去年下半年开始,热修复技术在 Android 技术社区热了一阵子,这种不用发布新版本就可以修复线上 bug 的技术确实有很大的需求,最近正好在研究一些开源的热修复方案,本文就其中常用的 ClassLoader 方式实现的热修复方案中的 ClassLoader 机制作一个简单的介绍。</p> <h3 id="classloader-简介">ClassLoader 简介</h3> <blockquote> <p>对于 Java 程序来说,编写程序就是编写类,运行程序也就是运行类(编译得到的 class 文件),其中起到关键作用的就是类加载器 ClassLoader。</p> </blockquote> <p>任何一个 Java 程序都是由若干个 class 文件组成的一个完整的 Java 程序,在程序运行时,需要将 class 文件加载到 JVM 中才可以使用,负责加载这些 class 文件的就是 Java 的类加载(ClassLoader)机制。</p> <p><img src="http://ac-qygvx1cc.clouddn.com/78e71017bdd24420.jpeg" alt="" /></p> <p>因此 ClassLoader 的作用简单来说就是加载 class 文件,提供给程序运行时使用。</p> <h4 id="classloader-的双亲委托模型parentdelegation-model">ClassLoader 的双亲委托模型(Parent <em>Delegation Model</em> )</h4> <p>先来看 jdk 中的 ClassLoader 类的构造方法,其需要传入一个父类加载器,并持有该引用。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">protected</span> <span class="nf">ClassLoader</span><span class="o">(</span><span class="n">ClassLoader</span> <span class="n">parent</span><span class="o">)</span> <span class="o">{</span> <span class="k">this</span><span class="o">(</span><span class="n">checkCreateClassLoader</span><span class="o">(),</span> <span class="n">parent</span><span class="o">);</span> <span class="o">}</span> </code></pre></div></div> <p>当类加载器收到加载类或资源的请求时,通常都是先委托给父类加载器加载,也就是说只有当父类加载器找不到指定类或资源时,自身才会执行实际的类加载过程,具体的加载过程如下:</p> <ol> <li>源 ClassLoader 先判断该 Class 是否已加载,如果已加载,则直接返回 Class,如果没有则委托给父类加载器。</li> <li>父类加载器判断是否加载过该 Class,如果已加载,则直接返回 Class,如果没有则委托给祖父类加载器。</li> <li>依此类推,直到始祖类加载器(引用类加载器)。</li> <li>始祖类加载器判断是否加载过该 Class,如果已加载,则直接返回 Class,如果没有则尝试从其对应的类路径下寻找 class 字节码文件并载入。如果载入成功,则直接返回 Class,如果载入失败,则委托给始祖类加载器的子类加载器。</li> <li>始祖类加载器的子类加载器尝试从其对应的类路径下寻找 class 字节码文件并载入。如果载入成功,则直接返回 Class,如果载入失败,则委托给始祖类加载器的孙类加载器。</li> <li>依此类推,直到源 ClassLoader。</li> <li>源 ClassLoader 尝试从其对应的类路径下寻找 class 字节码文件并载入。如果载入成功,则直接返回 Class,如果载入失败,源 ClassLoader 不会再委托其子类加载器,而是抛出异常。</li> </ol> <p>如果需要详细了解 ClassLoader 的信息,可以借助以下文章深入了解:</p> <ul> <li><a href="https://segmentfault.com/a/1190000002579346">JVM 的工作原理,层次结构以及 GC 工作原理</a></li> <li><a href="http://blog.csdn.net/xyang81/article/details/7292380">深入分析Java ClassLoader原理</a></li> <li><a href="http://blog.csdn.net/zhangzeyuaaa/article/details/42499839">类加载机制:全盘负责和双亲委托</a></li> </ul> <h3 id="android-中的-classloader">Android 中的 ClassLoader</h3> <p>Android 的 Dalvik/ART 虚拟机如同标准 Java 的 JVM 虚拟机一样,也是同样需要加载 class 文件到内存中来使用,但是在 ClassLoader 的加载细节上会有略微的差别。</p> <h4 id="android-中的-dex-文件">Android 中的 dex 文件</h4> <p>Android 应用打包成 apk 文件时,class 文件会被打包成一个或者多个 dex 文件。将一个 apk 文件后缀改成 .zip 格式解压后(也可以直接解压,apk 文件本质是个 zip 文件),里面就有 class.dex 文件,由于 Android 的 65K 问题(不要纠结是 64K 还是 65K),使用 MultiDex 就会生成多个 dex 文件。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/3c5e66e9e048d343.jpg" alt="" /></p> <p>当 Android 系统安装一个应用的时候,会针对不同平台对 Dex 进行优化,这个过程由一个专门的工具来处理,叫 DexOpt 。DexOpt 是在第一次加载 Dex 文件的时候执行的,该过程会生成一个 ODEX 文件,即 Optimised Dex。执行 ODEX 的效率会比直接执行 Dex 文件的效率要高很多,加快 App 的启动和响应。</p> <p>ODEX 相关的细节可以阅读以下文章扩展:</p> <ul> <li><a href="http://www.mywiki.cn/hovercool/index.php/ART%E5%92%8CDalvik">ART 和 Dalvik</a></li> <li><a href="http://www.jianshu.com/p/242abfb7eb7f">ODEX格式及生成过程</a></li> <li><a href="http://stackoverflow.com/questions/9593527/what-are-odex-files-in-android">What are ODEX files in Android</a></li> </ul> <blockquote> <p>注:本人的 5.0 机器 ODEX 优化后的文件是在 <code class="highlighter-rouge">/data/dalvilk-cache</code> 文件夹下的,6.0 机器该文件夹下只有 framework 和部分内置的 App 的优化后的 dex 文件,查找相关资料后没有找到明确的说法,目前猜测和 ROM 有关系,后续再深究下这个问题。</p> </blockquote> <p><img src="http://ac-QYgvX1CC.clouddn.com/b79b994f71a47130.png" alt="" /></p> <p>总之,Android 中的 Dalvik/ART 无法像 JVM 那样 <strong>直接</strong> 加载 class 文件和 jar 文件中的 class,需要通过 dx 工具来优化转换成 Dalvik byte code 才行,只能通过 dex 或者 包含 dex 的jar、apk 文件来加载(注意 odex 文件后缀可能是 .dex 或 .odex,也属于 dex 文件),因此 Android 中的 ClassLoader 工作就交给了 BaseDexClassLoader 来处理。</p> <blockquote> <p>注:如果 jar 文件包含有 dex 文件,此时 jar 文件也是可以用来加载的,不过实际加载的还是其中的 dex 文件,不要弄混淆了。</p> </blockquote> <h4 id="basedexclassloader-及其子类">BaseDexClassLoader 及其子类</h4> <p>在 Android 开发者官网上的 <a href="https://developer.android.com/reference/java/lang/ClassLoader.html">ClassLoader</a> 的文档说明中我们可以看到,ClassLoader 是个抽象类,其具体实现的子类有 <code class="highlighter-rouge">BaseDexClassLoader</code> 和 <code class="highlighter-rouge">SecureClassLoader</code> 。</p> <p>SecureClassLoader 的子类是 <code class="highlighter-rouge">URLClassLoader</code> ,其只能用来加载 jar 文件,这在 Android 的 Dalvik/ART 上没法使用的。</p> <p>BaseDexClassLoader 的子类是 <code class="highlighter-rouge">PathClassLoader</code> 和 <code class="highlighter-rouge">DexClassLoader</code> 。</p> <h5 id="pathclassloader">PathClassLoader</h5> <p>PathClassLoader 在应用启动时创建,从 data/app/… 安装目录下加载 apk 文件。</p> <p>其有 2 个构造函数,如下所示,这里遵从之前提到的双亲委托模型:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">PathClassLoader</span><span class="o">(</span><span class="n">String</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">ClassLoader</span> <span class="n">parent</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">(</span><span class="n">dexPath</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">parent</span><span class="o">);</span> <span class="o">}</span> <span class="kd">public</span> <span class="nf">PathClassLoader</span><span class="o">(</span><span class="n">String</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">String</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">ClassLoader</span> <span class="n">parent</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">(</span><span class="n">dexPath</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">parent</span><span class="o">);</span> <span class="o">}</span> </code></pre></div></div> <ul> <li><code class="highlighter-rouge">dexPath</code> : 包含 dex 的 jar 文件或 apk 文件的路径集,多个以文件分隔符分隔,默认是“:”</li> <li><code class="highlighter-rouge">libraryPath</code> : 包含 C/C++ 库的路径集,多个同样以文件分隔符分隔,可以为空</li> </ul> <p>PathClassLoader 里面除了这 2 个构造方法以外就没有其他的代码了,具体的实现都是在 BaseDexClassLoader 里面,其 dexPath 比较受限制,一般是已经安装应用的 apk 文件路径。</p> <p>在 Android 中,App 安装到手机后,apk 里面的 class.dex 中的 class 均是通过 PathClassLoader 来加载的。</p> <p>我们可以新建一个项目来验证下,在 MainActivity 中添加如下代码:</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="n">AppCompatActivity</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="n">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="n">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">ClassLoader</span> <span class="n">loader</span> <span class="o">=</span> <span class="n">MainActivity</span><span class="o">.</span><span class="na">class</span><span class="o">.</span><span class="na">getClassLoader</span><span class="o">();</span> <span class="k">while</span> <span class="o">(</span><span class="n">loader</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">System</span><span class="o">.</span><span class="na">out</span><span class="o">.</span><span class="na">println</span><span class="o">(</span><span class="n">loader</span><span class="o">.</span><span class="na">toString</span><span class="o">());</span> <span class="n">loader</span> <span class="o">=</span> <span class="n">loader</span><span class="o">.</span><span class="na">getParent</span><span class="o">();</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div></div> <p>输出结果是:</p> <div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code> I/System.out: dalvik.system.PathClassLoader[DexPathList[[zip file "/data/app/com.jaeger.testclassloader-2/base.apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]] I/System.out: java.lang.BootClassLoader@1d9c6226 </code></pre></div></div> <p><code class="highlighter-rouge">/data/app/com.jaeger.testclassloader-2/base.apk</code> 就是示例应用安装在手机上的位置。</p> <p>BootClassLoader 是 PathClassLoader 的父加载器,其在系统启动时创建,在 App 启动时会将该对象传进来,具体的调用在 <code class="highlighter-rouge">com.android.internal.os.ZygoteInit</code> 的 <code class="highlighter-rouge">main()</code> 方法中调用了 <code class="highlighter-rouge">preload()</code> , 然后调用 <code class="highlighter-rouge">preloadClasses()</code> 方法,在该方法内部调用了 Class 的 <code class="highlighter-rouge">forName()</code> 方法:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">Class</span><span class="o">.</span><span class="na">forName</span><span class="o">(</span><span class="n">line</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">null</span><span class="o">);</span> </code></pre></div></div> <p><code class="highlighter-rouge">forName()</code> 方法源码如下,方法内部获取到 BootClassLoader 实例:</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="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">forName</span><span class="o">(</span><span class="n">String</span> <span class="n">className</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">shouldInitialize</span><span class="o">,</span> <span class="n">ClassLoader</span> <span class="n">classLoader</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">ClassNotFoundException</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">classLoader</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">classLoader</span> <span class="o">=</span> <span class="n">BootClassLoader</span><span class="o">.</span><span class="na">getInstance</span><span class="o">();</span> <span class="o">}</span> <span class="c1">// Catch an Exception thrown by the underlying native code. It wraps</span> <span class="c1">// up everything inside a ClassNotFoundException, even if e.g. an</span> <span class="c1">// Error occurred during initialization. This as a workaround for</span> <span class="c1">// an ExceptionInInitializerError that's also wrapped. It is actually</span> <span class="c1">// expected to be thrown. Maybe the same goes for other errors.</span> <span class="c1">// Not wrapping up all the errors will break android though.</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">result</span><span class="o">;</span> <span class="k">try</span> <span class="o">{</span> <span class="n">result</span> <span class="o">=</span> <span class="n">classForName</span><span class="o">(</span><span class="n">className</span><span class="o">,</span> <span class="n">shouldInitialize</span><span class="o">,</span> <span class="n">classLoader</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">ClassNotFoundException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">Throwable</span> <span class="n">cause</span> <span class="o">=</span> <span class="n">e</span><span class="o">.</span><span class="na">getCause</span><span class="o">();</span> <span class="k">if</span> <span class="o">(</span><span class="n">cause</span> <span class="k">instanceof</span> <span class="n">LinkageError</span><span class="o">)</span> <span class="o">{</span> <span class="k">throw</span> <span class="o">(</span><span class="n">LinkageError</span><span class="o">)</span> <span class="n">cause</span><span class="o">;</span> <span class="o">}</span> <span class="k">throw</span> <span class="n">e</span><span class="o">;</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>而 PathClassLoader 的实例化又是在哪进行的呢?在源码中寻找下其构造方法调用的地方,结果如下:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/a0a44b8cac607cae.jpg" alt="" /></p> <p>其中:</p> <ul> <li> <p>在 ZygoteInit 中的调用是用来启动相关的系统服务</p> </li> <li> <p>在 ApplicationLoaders 中用来加载系统安装过的 apk,用来加载 apk 内的 class ,其调用是在 LoadApk 类中的 <code class="highlighter-rouge">getClassLoader()</code> 方法中调用的,得到的就是 PathClassLoader:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mClassLoader</span> <span class="o">=</span> <span class="n">ApplicationLoaders</span><span class="o">.</span><span class="na">getDefault</span><span class="o">().</span><span class="na">getClassLoader</span><span class="o">(</span><span class="n">zip</span><span class="o">,</span> <span class="n">lib</span><span class="o">,</span> <span class="n">mBaseClassLoader</span><span class="o">);</span> </code></pre></div> </div> </li> </ul> <h5 id="dexclassloader">DexClassLoader</h5> <p>介绍 DexClassLoader 之前,先来看看其官方描述:</p> <blockquote> <p>A class loader that loads classes from .jar and .apk filescontaining a classes.dex entry. This can be used to execute code notinstalled as part of an application.</p> </blockquote> <p>很明显,对比 PathClassLoader 只能加载已经安装应用的 dex 或 apk 文件,DexClassLoader 则没有此限制,可以从 SD 卡上加载包含 class.dex 的 .jar 和 .apk 文件,这也是插件化和热修复的基础,在不需要安装应用的情况下,完成需要使用的 dex 的加载。</p> <p>DexClassLoader 的源码里面只有一个构造方法,这里也是遵从双亲委托模型:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">DexClassLoader</span><span class="o">(</span><span class="n">String</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">String</span> <span class="n">optimizedDirectory</span><span class="o">,</span> <span class="n">String</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">ClassLoader</span> <span class="n">parent</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">(</span><span class="n">dexPath</span><span class="o">,</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">optimizedDirectory</span><span class="o">),</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">parent</span><span class="o">);</span> <span class="o">}</span> </code></pre></div></div> <p>参数说明:</p> <ul> <li> <p><code class="highlighter-rouge">String dexPath</code> : 包含 class.dex 的 apk、jar 文件路径 ,多个用文件分隔符(默认是 :)分隔</p> </li> <li> <p><code class="highlighter-rouge">String optimizedDirectory</code> : 用来缓存优化的 dex 文件的路径,即从 apk 或 jar 文件中提取出来的 dex 文件。该路径不可以为空,且应该是应用私有的,有读写权限的路径(实际上也可以使用外部存储空间,但是这样的话就存在代码注入的风险),可以通过以下方式来创建一个这样的路径:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">File</span> <span class="n">dexOutputDir</span> <span class="o">=</span> <span class="n">context</span><span class="o">.</span><span class="na">getCodeCacheDir</span><span class="o">();</span> </code></pre></div> </div> <blockquote> <p>注:后续发现,getCodeCacheDir() 方法只能在 API 21 以上可以使用。</p> </blockquote> </li> <li> <p><code class="highlighter-rouge">String libraryPath</code> : 存储 C/C++ 库文件的路径集</p> </li> <li> <p><code class="highlighter-rouge">ClassLoader parent </code>: 父类加载器,遵从双亲委托模型</p> </li> </ul> <p>简单介绍了 PathClassLoader 和 DexClassLoader,但这两者都是对 BaseDexClassLoader 的一层简单封装,真正的实现都在 BaseClassLoader 内。</p> <h5 id="baseclassloader-源码分析">BaseClassLoader 源码分析</h5> <p>先来看一眼 BaseClassLoader 的结构:</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/a6f9824c199cf304.jpg" alt="" /></p> <p>其中有个重要的字段 <code class="highlighter-rouge">private final DexPathList pathList</code> ,其继承 ClassLoader 实现的 <code class="highlighter-rouge">findClass()</code> 、<code class="highlighter-rouge">findResource()</code> 均是基于 pathList 来实现的(省略了部分源码):</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="nd">@Override</span> <span class="kd">protected</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">findClass</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">ClassNotFoundException</span> <span class="o">{</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Throwable</span><span class="o">&gt;</span> <span class="n">suppressedExceptions</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ArrayList</span><span class="o">&lt;</span><span class="n">Throwable</span><span class="o">&gt;();</span> <span class="n">Class</span> <span class="n">c</span> <span class="o">=</span> <span class="n">pathList</span><span class="o">.</span><span class="na">findClass</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">suppressedExceptions</span><span class="o">);</span> <span class="o">...</span> <span class="k">return</span> <span class="n">c</span><span class="o">;</span> <span class="o">}</span> <span class="nd">@Override</span> <span class="kd">protected</span> <span class="n">URL</span> <span class="nf">findResource</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">pathList</span><span class="o">.</span><span class="na">findResource</span><span class="o">(</span><span class="n">name</span><span class="o">);</span> <span class="o">}</span> <span class="nd">@Override</span> <span class="kd">protected</span> <span class="n">Enumeration</span><span class="o">&lt;</span><span class="n">URL</span><span class="o">&gt;</span> <span class="nf">findResources</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">pathList</span><span class="o">.</span><span class="na">findResources</span><span class="o">(</span><span class="n">name</span><span class="o">);</span> <span class="o">}</span> <span class="nd">@Override</span> <span class="kd">public</span> <span class="n">String</span> <span class="nf">findLibrary</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">pathList</span><span class="o">.</span><span class="na">findLibrary</span><span class="o">(</span><span class="n">name</span><span class="o">);</span> <span class="o">}</span> </code></pre></div></div> <p>那么重要的部分则是在 DexPathList 类的内部了,DexPathList 的构造方法也较为简单,和之前介绍的类似:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">DexPathList</span><span class="o">(</span><span class="n">ClassLoader</span> <span class="n">definingContext</span><span class="o">,</span> <span class="n">String</span> <span class="n">dexPath</span><span class="o">,</span> <span class="n">String</span> <span class="n">libraryPath</span><span class="o">,</span> <span class="n">File</span> <span class="n">optimizedDirectory</span><span class="o">)</span> <span class="o">{</span> <span class="o">...</span> <span class="o">}</span> </code></pre></div></div> <p>接受之前传进来的包含 dex 的 apk/jar/dex 的路径集、native 库的路径集和缓存优化的 dex 文件的路径,然后调用 <code class="highlighter-rouge">makePathElements()</code> 方法生成一个 <code class="highlighter-rouge">Element[] dexElements</code> 数组,Element 是 DexPathList 的一个嵌套类,其有以下字段:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">static</span> <span class="kd">class</span> <span class="nc">Element</span> <span class="o">{</span> <span class="kd">private</span> <span class="kd">final</span> <span class="n">File</span> <span class="n">dir</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">final</span> <span class="kt">boolean</span> <span class="n">isDirectory</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">final</span> <span class="n">File</span> <span class="n">zip</span><span class="o">;</span> <span class="kd">private</span> <span class="kd">final</span> <span class="n">DexFile</span> <span class="n">dexFile</span><span class="o">;</span> <span class="kd">private</span> <span class="n">ZipFile</span> <span class="n">zipFile</span><span class="o">;</span> <span class="kd">private</span> <span class="kt">boolean</span> <span class="n">initialized</span><span class="o">;</span> <span class="o">}</span> </code></pre></div></div> <p><code class="highlighter-rouge">makePathElements() </code> 是如何生成 Element 数组的?继续看源码:</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="n">Element</span><span class="o">[]</span> <span class="nf">makePathElements</span><span class="o">(</span><span class="n">List</span><span class="o">&lt;</span><span class="n">File</span><span class="o">&gt;</span> <span class="n">files</span><span class="o">,</span> <span class="n">File</span> <span class="n">optimizedDirectory</span><span class="o">,</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">IOException</span><span class="o">&gt;</span> <span class="n">suppressedExceptions</span><span class="o">)</span> <span class="o">{</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Element</span><span class="o">&gt;</span> <span class="n">elements</span> <span class="o">=</span> <span class="k">new</span> <span class="n">ArrayList</span><span class="o">&lt;&gt;();</span> <span class="c1">// 遍历所有的包含 dex 的文件</span> <span class="k">for</span> <span class="o">(</span><span class="n">File</span> <span class="n">file</span> <span class="o">:</span> <span class="n">files</span><span class="o">)</span> <span class="o">{</span> <span class="n">File</span> <span class="n">zip</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="n">File</span> <span class="n">dir</span> <span class="o">=</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="s">""</span><span class="o">);</span> <span class="n">DexFile</span> <span class="n">dex</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="n">String</span> <span class="n">path</span> <span class="o">=</span> <span class="n">file</span><span class="o">.</span><span class="na">getPath</span><span class="o">();</span> <span class="n">String</span> <span class="n">name</span> <span class="o">=</span> <span class="n">file</span><span class="o">.</span><span class="na">getName</span><span class="o">();</span> <span class="c1">// 判断是不是 zip 类型</span> <span class="k">if</span> <span class="o">(</span><span class="n">path</span><span class="o">.</span><span class="na">contains</span><span class="o">(</span><span class="n">zipSeparator</span><span class="o">))</span> <span class="o">{</span> <span class="n">String</span> <span class="n">split</span><span class="o">[]</span> <span class="o">=</span> <span class="n">path</span><span class="o">.</span><span class="na">split</span><span class="o">(</span><span class="n">zipSeparator</span><span class="o">,</span> <span class="mi">2</span><span class="o">);</span> <span class="n">zip</span> <span class="o">=</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">split</span><span class="o">[</span><span class="mi">0</span><span class="o">]);</span> <span class="n">dir</span> <span class="o">=</span> <span class="k">new</span> <span class="n">File</span><span class="o">(</span><span class="n">split</span><span class="o">[</span><span class="mi">1</span><span class="o">]);</span> <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">file</span><span class="o">.</span><span class="na">isDirectory</span><span class="o">())</span> <span class="o">{</span> <span class="c1">// 如果是文件夹,则直接添加 Element,这个一般是用来处理 native 库和资源文件</span> <span class="n">elements</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="n">Element</span><span class="o">(</span><span class="n">file</span><span class="o">,</span> <span class="kc">true</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">));</span> <span class="o">}</span> <span class="k">else</span> <span class="k">if</span> <span class="o">(</span><span class="n">file</span><span class="o">.</span><span class="na">isFile</span><span class="o">())</span> <span class="o">{</span> <span class="c1">// 直接是 .dex 文件,而不是 zip/jar 文件(apk 归为 zip),则直接加载 dex 文件</span> <span class="k">if</span> <span class="o">(</span><span class="n">name</span><span class="o">.</span><span class="na">endsWith</span><span class="o">(</span><span class="n">DEX_SUFFIX</span><span class="o">))</span> <span class="o">{</span> <span class="k">try</span> <span class="o">{</span> <span class="n">dex</span> <span class="o">=</span> <span class="n">loadDexFile</span><span class="o">(</span><span class="n">file</span><span class="o">,</span> <span class="n">optimizedDirectory</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IOException</span> <span class="n">ex</span><span class="o">)</span> <span class="o">{</span> <span class="n">System</span><span class="o">.</span><span class="na">logE</span><span class="o">(</span><span class="s">"Unable to load dex file: "</span> <span class="o">+</span> <span class="n">file</span><span class="o">,</span> <span class="n">ex</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="c1">// 如果是 zip/jar 文件(apk 归为 zip),则将 file 值赋给 zip 字段,再加载 dex 文件</span> <span class="n">zip</span> <span class="o">=</span> <span class="n">file</span><span class="o">;</span> <span class="k">try</span> <span class="o">{</span> <span class="n">dex</span> <span class="o">=</span> <span class="n">loadDexFile</span><span class="o">(</span><span class="n">file</span><span class="o">,</span> <span class="n">optimizedDirectory</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IOException</span> <span class="n">suppressed</span><span class="o">)</span> <span class="o">{</span> <span class="n">suppressedExceptions</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="n">suppressed</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">System</span><span class="o">.</span><span class="na">logW</span><span class="o">(</span><span class="s">"ClassLoader referenced unknown path: "</span> <span class="o">+</span> <span class="n">file</span><span class="o">);</span> <span class="o">}</span> <span class="k">if</span> <span class="o">((</span><span class="n">zip</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">||</span> <span class="o">(</span><span class="n">dex</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">))</span> <span class="o">{</span> <span class="n">elements</span><span class="o">.</span><span class="na">add</span><span class="o">(</span><span class="k">new</span> <span class="n">Element</span><span class="o">(</span><span class="n">dir</span><span class="o">,</span> <span class="kc">false</span><span class="o">,</span> <span class="n">zip</span><span class="o">,</span> <span class="n">dex</span><span class="o">));</span> <span class="o">}</span> <span class="o">}</span> <span class="c1">// list 转为数组</span> <span class="k">return</span> <span class="n">elements</span><span class="o">.</span><span class="na">toArray</span><span class="o">(</span><span class="k">new</span> <span class="n">Element</span><span class="o">[</span><span class="n">elements</span><span class="o">.</span><span class="na">size</span><span class="o">()]);</span> <span class="o">}</span> </code></pre></div></div> <p><code class="highlighter-rouge">loadDexFile()</code> 方法最终会调用 JNI 层的方法来读取 dex 文件,这里不再深入探究,有兴趣的可以阅读 <a href="http://blog.csdn.net/nanzhiwen666/article/details/50515895">从源码分析 Android dexClassLoader 加载机制原理</a> 这篇文章深入了解。</p> <p>接下来看以下 DexPathList 的 <code class="highlighter-rouge">findClass()</code> 方法,其根据传入的完整的类名来加载对应的 class,源码如下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="n">Class</span> <span class="nf">findClass</span><span class="o">(</span><span class="n">String</span> <span class="n">name</span><span class="o">,</span> <span class="n">List</span><span class="o">&lt;</span><span class="n">Throwable</span><span class="o">&gt;</span> <span class="n">suppressed</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// 遍历 dexElements 数组,依次寻找对应的 class,一旦找到就终止遍历</span> <span class="k">for</span> <span class="o">(</span><span class="n">Element</span> <span class="n">element</span> <span class="o">:</span> <span class="n">dexElements</span><span class="o">)</span> <span class="o">{</span> <span class="n">DexFile</span> <span class="n">dex</span> <span class="o">=</span> <span class="n">element</span><span class="o">.</span><span class="na">dexFile</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">dex</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">Class</span> <span class="n">clazz</span> <span class="o">=</span> <span class="n">dex</span><span class="o">.</span><span class="na">loadClassBinaryName</span><span class="o">(</span><span class="n">name</span><span class="o">,</span> <span class="n">definingContext</span><span class="o">,</span> <span class="n">suppressed</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">clazz</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">clazz</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> <span class="c1">// 抛出异常</span> <span class="k">if</span> <span class="o">(</span><span class="n">dexElementsSuppressedExceptions</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">suppressed</span><span class="o">.</span><span class="na">addAll</span><span class="o">(</span><span class="n">Arrays</span><span class="o">.</span><span class="na">asList</span><span class="o">(</span><span class="n">dexElementsSuppressedExceptions</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>这里有关于热修复实现的一个点,就是将补丁 dex 文件放到 dexElements 数组前面,这样在加载 class 时,优先找到补丁包中的 dex 文件,加载到 class 之后就不再寻找,从而原来的 apk 文件中同名的类就不会再使用,从而达到修复的目的,虽然说起来较为简单,但是实现起来还有很多细节需要注意,本文先热身,后期再分析具体实现。</p> <p>至此,BaseDexClassLader 寻找 class 的路线就清晰了:</p> <ol> <li>当传入一个完整的类名,调用 BaseDexClassLader 的 <code class="highlighter-rouge">findClass(String name) </code> 方法</li> <li>BaseDexClassLader 的 findClass 方法会交给 DexPathList 的 <code class="highlighter-rouge">findClass(String name, List&lt;Throwable&gt; suppressed </code> 方法处理</li> <li>在 DexPathList 方法的内部,会遍历 dexFile ,通过 DexFile 的 <code class="highlighter-rouge">dex.loadClassBinaryName(name, definingContext, suppressed)</code> 来完成类的加载</li> </ol> <h5 id="实际使用">实际使用</h5> <p>需要注意到的是,在项目中使用 BaseDexClassLoader 或者 DexClassLoader 去加载某个 dex 或者 apk 中的 class 时,是无法调用 <code class="highlighter-rouge">findClass()</code> 方法的,因为该方法是包访问权限,你需要调用 <code class="highlighter-rouge">loadClass(String className)</code> ,该方法其实是 BaseDexClassLoader 的父类 ClassLoader 内实现的:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">loadClass</span><span class="o">(</span><span class="n">String</span> <span class="n">className</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">ClassNotFoundException</span> <span class="o">{</span> <span class="k">return</span> <span class="nf">loadClass</span><span class="o">(</span><span class="n">className</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span> <span class="o">}</span> <span class="kd">protected</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">loadClass</span><span class="o">(</span><span class="n">String</span> <span class="n">className</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">resolve</span><span class="o">)</span> <span class="kd">throws</span> <span class="n">ClassNotFoundException</span> <span class="o">{</span> <span class="n">Class</span><span class="o">&lt;?&gt;</span> <span class="n">clazz</span> <span class="o">=</span> <span class="n">findLoadedClass</span><span class="o">(</span><span class="n">className</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">clazz</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">ClassNotFoundException</span> <span class="n">suppressed</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="k">try</span> <span class="o">{</span> <span class="n">clazz</span> <span class="o">=</span> <span class="n">parent</span><span class="o">.</span><span class="na">loadClass</span><span class="o">(</span><span class="n">className</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">ClassNotFoundException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">suppressed</span> <span class="o">=</span> <span class="n">e</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">clazz</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="k">try</span> <span class="o">{</span> <span class="n">clazz</span> <span class="o">=</span> <span class="n">findClass</span><span class="o">(</span><span class="n">className</span><span class="o">);</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">ClassNotFoundException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">e</span><span class="o">.</span><span class="na">addSuppressed</span><span class="o">(</span><span class="n">suppressed</span><span class="o">);</span> <span class="k">throw</span> <span class="n">e</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="o">}</span> <span class="k">return</span> <span class="n">clazz</span><span class="o">;</span> <span class="o">}</span> </code></pre></div></div> <p>上面这段代码结合之前提到的双亲委托模型就很好理解了,先查找当前的 ClassLoader 是否已经加载过,如果没有就交给父 ClassLoader 去加载,如果父 ClassLoader 没有找到,才调用当前 ClassLoader 来加载,此时就是调用上面分析的 <code class="highlighter-rouge">findClass() </code> 方法了。</p> <h3 id="classloader-使用示例">ClassLoader 使用示例</h3> <p>上面说了这么多理论知识,只说不练假把式,接下来实战:从 SD 卡中动态加载一个包含 class.dex 的 jar 文件,加载其中的类,并调用其方法。</p> <ol> <li> <p>新建一个 Java 项目,包含两个文件:<code class="highlighter-rouge">ISayHello.java</code> 和 <code class="highlighter-rouge">HelloAndroid.java</code></p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kn">package</span> <span class="n">com</span><span class="o">.</span><span class="na">jaeger</span><span class="o">;</span> <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">ISayHello</span> <span class="o">{</span> <span class="n">String</span> <span class="nf">say</span><span class="o">();</span> <span class="o">}</span> </code></pre></div> </div> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kn">package</span> <span class="n">com</span><span class="o">.</span><span class="na">jaeger</span><span class="o">;</span> <span class="kd">public</span> <span class="kd">class</span> <span class="nc">HelloAndroid</span> <span class="kd">implements</span> <span class="n">ISayHello</span> <span class="o">{</span> <span class="nd">@Override</span> <span class="kd">public</span> <span class="n">String</span> <span class="nf">say</span><span class="o">()</span> <span class="o">{</span> <span class="k">return</span> <span class="s">"Hello Android"</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>导出 jar 包</p> <p>这一步使用 IntelliJ IDEA 导出有点问题,最终我是用 Eclipse 导出 jar 包的。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/88ede5c72013c55b.jpg" alt="" /></p> </li> <li> <p>使用 SDK 目录 &gt; platform-tools 里面的 dx 工具生成包含 class.dex 的 jar 包</p> <p>将上一步生成的 <code class="highlighter-rouge">sayhello.jar</code> 放到 你的 SDK 下的 platform-tools 文件夹下,使用下面的命令生成 dex 化的 jar 文件,其中是 output 后面的 <code class="highlighter-rouge">sayhello_dex.jar</code> 就是最终生成的 jar 包。</p> <div class="highlighter-rouge"><div class="highlight"><pre class="highlight"><code>dx --dex --output=sayhello_dex.jar sayhello.jar </code></pre></div> </div> <p>生成 <code class="highlighter-rouge">sayhello_dex.jar</code> 之后,用解压解压后就会发现其已经包含了 class.dex 文件了。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/ba0d600fc2a90e2d.jpg" alt="" /></p> </li> <li> <p>将 <code class="highlighter-rouge">sayhello_dex.jar</code> 文件拷贝到手机存储空间的根目录,不一定是内存卡。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/7efba4a5a816a8e1.png" alt="" /></p> </li> <li> <p>新建一个 Android 项目,在 MainActivity 中添加如下的代码:</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="n">AppCompatActivity</span> <span class="o">{</span> <span class="kd">private</span> <span class="kd">static</span> <span class="kd">final</span> <span class="n">String</span> <span class="n">TAG</span> <span class="o">=</span> <span class="s">"TestClassLoader"</span><span class="o">;</span> <span class="kd">private</span> <span class="n">TextView</span> <span class="n">mTvInfo</span><span class="o">;</span> <span class="kd">private</span> <span class="n">Button</span> <span class="n">mBtnLoad</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="n">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="n">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">mTvInfo</span> <span class="o">=</span> <span class="o">(</span><span class="n">TextView</span><span class="o">)</span> <span class="n">findViewById</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">tv_info</span><span class="o">);</span> <span class="n">mBtnLoad</span> <span class="o">=</span> <span class="o">(</span><span class="n">Button</span><span class="o">)</span> <span class="n">findViewById</span><span class="o">(</span><span class="n">R</span><span class="o">.</span><span class="na">id</span><span class="o">.</span><span class="na">btn_load</span><span class="o">);</span> <span class="n">mBtnLoad</span><span class="o">.</span><span class="na">setOnClickListener</span><span class="o">(</span><span class="k">new</span> <span class="n">View</span><span class="o">.</span><span class="na">OnClickListener</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">onClick</span><span class="o">(</span><span class="n">View</span> <span class="n">view</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// 获取到包含 class.dex 的 jar 包文件</span> <span class="kd">final</span> <span class="n">File</span> <span class="n">jarFile</span> <span class="o">=</span> <span class="k">new</span> <span class="nf">File</span><span class="o">(</span><span class="n">Environment</span><span class="o">.</span><span class="na">getExternalStorageDirectory</span><span class="o">().</span><span class="na">getPath</span><span class="o">()</span> <span class="o">+</span> <span class="n">File</span><span class="o">.</span><span class="na">separator</span> <span class="o">+</span> <span class="s">"sayhello_dex.jar"</span><span class="o">);</span> <span class="c1">// 如果没有读权限,确定你在 AndroidManifest 中是否声明了读写权限</span> <span class="n">Log</span><span class="o">.</span><span class="na">d</span><span class="o">(</span><span class="n">TAG</span><span class="o">,</span> <span class="n">jarFile</span><span class="o">.</span><span class="na">canRead</span><span class="o">()</span> <span class="o">+</span> <span class="s">""</span><span class="o">);</span> <span class="k">if</span> <span class="o">(!</span><span class="n">jarFile</span><span class="o">.</span><span class="na">exists</span><span class="o">())</span> <span class="o">{</span> <span class="n">Log</span><span class="o">.</span><span class="na">e</span><span class="o">(</span><span class="n">TAG</span><span class="o">,</span> <span class="s">"sayhello_dex.jar not exists"</span><span class="o">);</span> <span class="k">return</span><span class="o">;</span> <span class="o">}</span> <span class="c1">// getCodeCacheDir() 方法在 API 21 才能使用,实际测试替换成 getExternalCacheDir() 等也是可以的</span> <span class="c1">// 只要有读写权限的路径均可</span> <span class="n">DexClassLoader</span> <span class="n">dexClassLoader</span> <span class="o">=</span> <span class="k">new</span> <span class="nf">DexClassLoader</span><span class="o">(</span><span class="n">jarFile</span><span class="o">.</span><span class="na">getAbsolutePath</span><span class="o">(),</span> <span class="n">getExternalCacheDir</span><span class="o">().</span><span class="na">getAbsolutePath</span><span class="o">(),</span> <span class="kc">null</span><span class="o">,</span> <span class="n">getClassLoader</span><span class="o">());</span> <span class="k">try</span> <span class="o">{</span> <span class="c1">// 加载 HelloAndroid 类</span> <span class="n">Class</span> <span class="n">clazz</span> <span class="o">=</span> <span class="n">dexClassLoader</span><span class="o">.</span><span class="na">loadClass</span><span class="o">(</span><span class="s">"com.jaeger.HelloAndroid"</span><span class="o">);</span> <span class="c1">// 强转成 ISayHello, 注意 ISayHello 的包名需要和 jar 包中的一致</span> <span class="n">ISayHello</span> <span class="n">iSayHello</span> <span class="o">=</span> <span class="o">(</span><span class="n">ISayHello</span><span class="o">)</span> <span class="n">clazz</span><span class="o">.</span><span class="na">newInstance</span><span class="o">();</span> <span class="n">mTvInfo</span><span class="o">.</span><span class="na">setText</span><span class="o">(</span><span class="n">iSayHello</span><span class="o">.</span><span class="na">say</span><span class="o">());</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">ClassNotFoundException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">InstantiationException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</span><span class="o">();</span> <span class="o">}</span> <span class="k">catch</span> <span class="o">(</span><span class="n">IllegalAccessException</span> <span class="n">e</span><span class="o">)</span> <span class="o">{</span> <span class="n">e</span><span class="o">.</span><span class="na">printStackTrace</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> </code></pre></div> </div> <p>同时需要新建一个和第一步创建的 Java 项目中包名一致的 <code class="highlighter-rouge">ISayHello</code> 接口:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">package</span> <span class="n">com</span><span class="o">.</span><span class="na">jaeger</span><span class="o">;</span> <span class="kd">public</span> <span class="kd">interface</span> <span class="nc">ISayHello</span> <span class="o">{</span> <span class="n">String</span> <span class="nf">say</span><span class="o">();</span> <span class="o">}</span> </code></pre></div> </div> <p>这里需要注意几点:</p> <ul> <li>因为需要从存储空间中读取 jar 文件,需要在 AndroidManifest 中声明读写权限</li> <li>ISayHello 接口的包名必须一致</li> <li><code class="highlighter-rouge">getCodeCacheDir()</code> 方法在 API 21 才能使用,实际测试替换成 <code class="highlighter-rouge">getExternalCacheDir()</code> 等也是可以的</li> </ul> </li> <li> <p>接下来就是运行,运行的结果如图,和预期的一样,完美收工。</p> <p><img src="http://ac-QYgvX1CC.clouddn.com/4b94e8fbecf66b72.png" alt="" /></p> </li> <li> <p>示例代码以及 jar 包上传到 GitHub 了,请前往 <a href="https://github.com/laobie/TestClassLoader">这里</a> 去查看。</p> </li> </ol> <h3 id="总结">总结</h3> <p>顺着相关资料,从源头开始分析起,然后再进入源码,理清楚具体的运行机制,最终通过简单的示例验证了分析的结果。</p> <p>在这过程中,查阅了不少资料,也阅读了很多前辈的博文,受益匪浅,这也是现在技术圈内很好的氛围。同时在分析中也发现自己对 Android 底层了解相当薄弱,这也是今后需要多学习的地方。</p> <p>鉴于自己能力有限,如果本文中有遗漏或者错误的地方,请在评论区指出或者通过邮件等方式联系我,谢谢。</p> <h3 id="参考资料">参考资料</h3> <ul> <li> <p><a href="https://segmentfault.com/a/1190000002579346">JVM 的工作原理,层次结构以及 GC 工作原理</a></p> </li> <li> <p><a href="http://blog.csdn.net/xyang81/article/details/7292380">深入分析Java ClassLoader原理</a></p> </li> <li> <p><a href="https://segmentfault.com/a/1190000004062880">Android动态加载基础 ClassLoader工作机制</a></p> </li> <li> <p><a href="http://blog.zhaiyifan.cn/2015/11/20/HotPatchCompare/">各大热补丁方案分析和比较</a></p> </li> <li> <p><a href="https://github.com/kaedea/android-dynamical-loading">android-dynamical-loading</a></p> </li> <li> <p><a href="http://bugly.qq.com/bbs/forum.php?mod=viewthread&amp;tid=193">dex分包变形记</a></p> </li> <li> <p><a href="http://blog.csdn.net/mr_liabill/article/details/50497055">Android ClassLoader机制</a></p> </li> <li> <p><a href="http://weli.iteye.com/blog/1682625">彻底搞懂 Java ClassLoader</a></p> </li> <li> <p><a href="http://blog.csdn.net/nanzhiwen666/article/details/50515895">从源码分析 Android dexClassLoader 加载机制原理</a></p> </li> <li> <p><a href="http://www.iloveandroid.net/">码农故事</a></p> </li> </ul> </description>
<pubDate>Sat, 27 Aug 2016 00:00:00 +0000</pubDate>
<link>http://jaeger.itscoder.com//android/2016/08/27/android-classloader.html</link>
<guid isPermaLink="true">http://jaeger.itscoder.com//android/2016/08/27/android-classloader.html</guid>
</item>
<item>
<title>StaticLayout 源码分析</title>
<description><p>Android 中的文本布局和绘制都是由 Layout 类完成的,而 Layout 类一个重要的子类就是 SaticLayout 类,本文从源码来简单分析文本是如何布局的,具体如段落、折行处理以及省略方式的等等的处理。</p> <h4 id="前言">前言</h4> <p>Android 控件中,看起来最简单、最基础的 TextView 实际上是很复杂的,很多常见的控件都是其子类,例如 Botton、EditText、CheckBox 等,由于作为一个基础控件类,TextView 需要考虑到子类的各种使用场景,满足子类的需求。源码中,TextView 单个类源码就多达 1万行,而且其工作时还依赖很多辅助类。其文本的排版、折行处理,以及最终的显示,均是交给辅助类 Layout 类来处理的。</p> <p>由于 Canvas 本身提供的 drawText 绘制文本是不支持换行的,所以在文本需要换行显示时,就需要用到 Layout 类。我们可以看到官方对 Layout 类的描述:</p> <blockquote> <p>A base class that manages text layout in visual elements on the screen.</p> </blockquote> <p>一个用于管理屏幕上文本布局的基类。</p> <p>其直接子类有 StaticLayout、DynamicLayout、BoringLayout,在官方的文档中提到,如果文本内容会被编辑,应该使用 DynamicLayout,如果文本显示之后不会发生改变,应该使用 StaticLayout,而 BoringLayout 则使用场景极为有限:当你确保你的文本只有一行,且所有的字符均是从左到右显示的(某些语言的文字是从右到左显示的),你才可以使用 BoringLayout。</p> <p>本文将会简单地深入 StaticLayout 的源码,分析下具体是如何工作的。</p> <h4 id="概述">概述</h4> <p>先看 StaticLayout 类的注释:StaticLayout 是一个为不可编辑的文本布局的类,这意味着一旦布局完成,文本内容就不可以改变,如果需要改变的话,应该使用 DynamicLayout 来布局。同时你不应该直接使用 StaticLayout 类,除非你需要实现一个自定义的控件或者自定义显示对象,否则,你应该直接调用 <code class="highlighter-rouge">Canvas.drawText()</code>。因此,在正常的开发工作中,你接触 StaticLayout 的机会应该不多。</p> <p>在 TextView 初始化时,会通过 <code class="highlighter-rouge">makeNewLayout()</code> 方法,根据文本的特点,是否包含 Span,是否单行等,决定创建具体的 Layout 类型。在单纯地使用TextView来展示静态文本的时候,创建的就是 StaticLayout。StaticLayout 的初始化是通过内部类 <code class="highlighter-rouge">StaticLayout.Builder</code> 完成的,然后调用 <code class="highlighter-rouge">generate()</code> 方法完成段落、折行以及缩进之类的处理,在 <code class="highlighter-rouge">generate()</code> 方法中调用了 <code class="highlighter-rouge">out()</code> 方法,完成文本显示的行距、顶部底部留白、省略文本等的处理,这两个方法也是 StaticLayout 源码中两个主要的方法,完成了一系列的文本处理。在 TextView 的 <code class="highlighter-rouge">onDraw(Canvas canvas)</code> 方法中,调用父类 Layout 的 <code class="highlighter-rouge">draw()</code> 方法,改方法会依次调用 <code class="highlighter-rouge">drawBackground()</code> 和 <code class="highlighter-rouge">drawText()</code> 完成背景和文本的绘制。</p> <h4 id="构造方法">构造方法</h4> <p>StaticLayout 有多个构造方法,最完整的构造方法(其他构造方法最终也是调用的这个构造方法)如下所示:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">public</span> <span class="nf">StaticLayout</span><span class="o">(</span><span class="n">CharSequence</span> <span class="n">source</span><span class="o">,</span> <span class="kt">int</span> <span class="n">bufstart</span><span class="o">,</span> <span class="kt">int</span> <span class="n">bufend</span><span class="o">,</span> <span class="n">TextPaint</span> <span class="n">paint</span><span class="o">,</span> <span class="kt">int</span> <span class="n">outerwidth</span><span class="o">,</span> <span class="n">Alignment</span> <span class="n">align</span><span class="o">,</span> <span class="n">TextDirectionHeuristic</span> <span class="n">textDir</span><span class="o">,</span> <span class="kt">float</span> <span class="n">spacingmult</span><span class="o">,</span> <span class="kt">float</span> <span class="n">spacingadd</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">includepad</span><span class="o">,</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">TruncateAt</span> <span class="n">ellipsize</span><span class="o">,</span> <span class="kt">int</span> <span class="n">ellipsizedWidth</span><span class="o">,</span> <span class="kt">int</span> <span class="n">maxLines</span><span class="o">)</span> <span class="o">{</span> <span class="kd">super</span><span class="o">((</span><span class="n">ellipsize</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">?</span> <span class="n">source</span> <span class="o">:</span> <span class="o">(</span><span class="n">source</span> <span class="k">instanceof</span> <span class="n">Spanned</span><span class="o">)</span> <span class="o">?</span> <span class="k">new</span> <span class="n">SpannedEllipsizer</span><span class="o">(</span><span class="n">source</span><span class="o">)</span> <span class="o">:</span> <span class="k">new</span> <span class="n">Ellipsizer</span><span class="o">(</span><span class="n">source</span><span class="o">),</span> <span class="n">paint</span><span class="o">,</span> <span class="n">outerwidth</span><span class="o">,</span> <span class="n">align</span><span class="o">,</span> <span class="n">textDir</span><span class="o">,</span> <span class="n">spacingmult</span><span class="o">,</span> <span class="n">spacingadd</span><span class="o">);</span> <span class="n">Builder</span> <span class="n">b</span> <span class="o">=</span> <span class="n">Builder</span><span class="o">.</span><span class="na">obtain</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">bufstart</span><span class="o">,</span> <span class="n">bufend</span><span class="o">,</span> <span class="n">paint</span><span class="o">,</span> <span class="n">outerwidth</span><span class="o">)</span> <span class="o">.</span><span class="na">setAlignment</span><span class="o">(</span><span class="n">align</span><span class="o">)</span> <span class="o">.</span><span class="na">setTextDirection</span><span class="o">(</span><span class="n">textDir</span><span class="o">)</span> <span class="o">.</span><span class="na">setLineSpacing</span><span class="o">(</span><span class="n">spacingadd</span><span class="o">,</span> <span class="n">spacingmult</span><span class="o">)</span> <span class="o">.</span><span class="na">setIncludePad</span><span class="o">(</span><span class="n">includepad</span><span class="o">)</span> <span class="o">.</span><span class="na">setEllipsizedWidth</span><span class="o">(</span><span class="n">ellipsizedWidth</span><span class="o">)</span> <span class="o">.</span><span class="na">setEllipsize</span><span class="o">(</span><span class="n">ellipsize</span><span class="o">)</span> <span class="o">.</span><span class="na">setMaxLines</span><span class="o">(</span><span class="n">maxLines</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">ellipsize</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">Ellipsizer</span> <span class="n">e</span> <span class="o">=</span> <span class="o">(</span><span class="n">Ellipsizer</span><span class="o">)</span> <span class="n">getText</span><span class="o">();</span> <span class="n">e</span><span class="o">.</span><span class="na">mLayout</span> <span class="o">=</span> <span class="k">this</span><span class="o">;</span> <span class="n">e</span><span class="o">.</span><span class="na">mWidth</span> <span class="o">=</span> <span class="n">ellipsizedWidth</span><span class="o">;</span> <span class="n">e</span><span class="o">.</span><span class="na">mMethod</span> <span class="o">=</span> <span class="n">ellipsize</span><span class="o">;</span> <span class="n">mEllipsizedWidth</span> <span class="o">=</span> <span class="n">ellipsizedWidth</span><span class="o">;</span> <span class="n">mColumns</span> <span class="o">=</span> <span class="n">COLUMNS_ELLIPSIZE</span><span class="o">;</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">mColumns</span> <span class="o">=</span> <span class="n">COLUMNS_NORMAL</span><span class="o">;</span> <span class="n">mEllipsizedWidth</span> <span class="o">=</span> <span class="n">outerwidth</span><span class="o">;</span> <span class="o">}</span> <span class="n">mLineDirections</span> <span class="o">=</span> <span class="n">ArrayUtils</span><span class="o">.</span><span class="na">newUnpaddedArray</span><span class="o">(</span><span class="n">Directions</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="mi">2</span> <span class="o">*</span> <span class="n">mColumns</span><span class="o">);</span> <span class="n">mLines</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">mLineDirections</span><span class="o">.</span><span class="na">length</span><span class="o">];</span> <span class="n">mMaximumVisibleLineCount</span> <span class="o">=</span> <span class="n">maxLines</span><span class="o">;</span> <span class="n">generate</span><span class="o">(</span><span class="n">b</span><span class="o">,</span> <span class="n">b</span><span class="o">.</span><span class="na">mIncludePad</span><span class="o">,</span> <span class="n">b</span><span class="o">.</span><span class="na">mIncludePad</span><span class="o">);</span> <span class="n">Builder</span><span class="o">.</span><span class="na">recycle</span><span class="o">(</span><span class="n">b</span><span class="o">);</span> <span class="o">}</span> </code></pre></div></div> <p>参数说明:</p> <ul> <li><code class="highlighter-rouge">CharSequence source</code> 文本内容</li> <li><code class="highlighter-rouge"> int bufstart, int bufend,</code> 开始位置和结束位置</li> <li><code class="highlighter-rouge"> TextPaint paint</code> 文本画笔对象</li> <li><code class="highlighter-rouge">int outerwidth</code> 布局宽度,超出宽度换行显示</li> <li><code class="highlighter-rouge">Alignment align</code> 对齐方式,默认是<code class="highlighter-rouge">Alignment.ALIGN_LEFT</code></li> <li><code class="highlighter-rouge">TextDirectionHeuristic textDir</code> 文本显示方向</li> <li><code class="highlighter-rouge">float spacingmult</code> 行间距倍数,默认是1</li> <li><code class="highlighter-rouge">float spacingadd</code> 行距增加值,默认是0</li> <li><code class="highlighter-rouge">boolean includepad</code> 文本顶部和底部是否留白</li> <li><code class="highlighter-rouge">TextUtils.TruncateAt ellipsize</code> 文本省略方式,有 START、MIDDLE、 END、MARQUEE 四种省略方式(其实还有一个 END_SMALL,但是 Google 并未开放出来)。</li> <li><code class="highlighter-rouge">int ellipsizedWidth</code> 省略宽度</li> <li><code class="highlighter-rouge">int maxLines</code> 最大行数</li> </ul> <p>细节分析:</p> <ul> <li> <p>构造方法的开始,在调用父类 Layout 构造方法的时候,判断了文本是否需要省略,如果需要省略,则创建一个 Ellipsizer 对象,Ellipsizer 是 Layout 的嵌套内部类,实现了 CharSequence 和 GetChars 接口。该类就是用来对文本进行省略处理的,具体的处理方法是由其 <code class="highlighter-rouge">getChars()</code> 方法完成的。</p> </li> <li> <p>在创建 Ellipsizer 对象之前,还判断了一下需要显示的文本是否是 Spanned ,如果是的话则创建 SpannedEllipsizer 对象,SpannedEllipsizer 类继承 Ellipsizer ,同时实现了 Spanned 接口。</p> </li> <li> <p>StaticLayout.Builder 对象的创建是通过 <code class="highlighter-rouge">Builder.obtain()</code> 方法创建的,在该方法内部可以看到 Builder 对象通过 SynchronizedPool 对象池来管理的,起到缓存的作用,避免 Builder 对象的重复创建,在 StaticLayout 的构造方法的最后也可以看到 <code class="highlighter-rouge">Builder.recycle(b)</code> 的调用,回收 Builder 对象。 Builder 的构造方法如下所示:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">private</span> <span class="nf">Builder</span><span class="o">()</span> <span class="o">{</span> <span class="n">mNativePtr</span> <span class="o">=</span> <span class="n">nNewBuilder</span><span class="o">();</span> <span class="o">}</span> </code></pre></div> </div> <p>其调用了 JNI 层的 <code class="highlighter-rouge">nNewBuilder()</code> 方法,新建了一个 LineBreak 对象,并将其指针指向 java 层,赋值给 Builder 对象的 mNativePtr 字段 ,后面调用 native 方法时,均需要将 mNativePtr 作为参数传递过去。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kd">static</span> <span class="n">jlong</span> <span class="nf">nNewBuilder</span><span class="o">(</span><span class="n">JNIEnv</span><span class="o">*,</span> <span class="n">jclass</span><span class="o">)</span> <span class="o">{</span> <span class="k">return</span> <span class="n">reinterpret_cast</span><span class="o">&lt;</span><span class="n">jlong</span><span class="o">&gt;(</span><span class="k">new</span> <span class="n">LineBreaker</span><span class="o">);</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>mLineDirections 需要结合到后面每行文本处理来理解,这里可以大致说一下,StaticLayout 源码中声明了以下的常量:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kt">int</span> <span class="n">COLUMNS_NORMAL</span> <span class="o">=</span> <span class="mi">4</span><span class="o">;</span> <span class="kt">int</span> <span class="n">COLUMNS_ELLIPSIZE</span> <span class="o">=</span> <span class="mi">6</span><span class="o">;</span> <span class="kt">int</span> <span class="n">START</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="kt">int</span> <span class="n">DIR</span> <span class="o">=</span> <span class="n">START</span><span class="o">;</span> <span class="kt">int</span> <span class="n">TAB</span> <span class="o">=</span> <span class="n">START</span><span class="o">;</span> <span class="kt">int</span> <span class="n">TOP</span> <span class="o">=</span> <span class="mi">1</span><span class="o">;</span> <span class="kt">int</span> <span class="n">DESCENT</span> <span class="o">=</span> <span class="mi">2</span><span class="o">;</span> <span class="kt">int</span> <span class="n">HYPHEN</span> <span class="o">=</span> <span class="mi">3</span><span class="o">;</span> <span class="kt">int</span> <span class="n">ELLIPSIS_START</span> <span class="o">=</span> <span class="mi">4</span><span class="o">;</span> <span class="kt">int</span> <span class="n">ELLIPSIS_COUNT</span> <span class="o">=</span> <span class="mi">5</span><span class="o">;</span> </code></pre></div> </div> <p>其中 COLUMNS_NORMAL 和 COLUMNS_ELLIPSIZE 会赋值给全局变量 mColumns,正如你在构造方法中看到的那样,这个在没一行处理时会用到,每一行文本处理时需要记录四个值,start,top,desent,hyphen 值,当文本需要省略时,还需要记录 ellipsis_start 和 ellipsis_count 值,因此正常的 mColumn 值为4,省略时则是6,因此 mLineDirections 数组大小始终是 mColumn 的倍数,mLine 数组的大小和其保持一致(从后面的分析来看,mLineDirections 数组的大小没必要这么大)。</p> </li> </ul> <h4 id="generate-方法分析">generate 方法分析</h4> <p>StaticLayout 中的 <code class="highlighter-rouge">generate()</code> 方法近 300 行,其完成了文本的段落、折行的处理,建议自行对照源码来阅读下面的分析,本文不贴太多代码。</p> <p>接受的参数:</p> <ul> <li><code class="highlighter-rouge">StaticLayout.Builder b</code> StaticLayout.Builder 对象</li> <li><code class="highlighter-rouge">boolean includepad</code>是否上下保留空白</li> <li><code class="highlighter-rouge">boolean trackpad</code></li> </ul> <p>细节分析:</p> <ol> <li>在方法的开始,创建了很多的局部变量,并将 Builder 对象对应的值赋值给这些变量。 <ul> <li> <p>其中有个 <code class="highlighter-rouge">Paint.FontMetricsInt fm</code> 变量,FontMetricsInt 是 Paint 的内部类,主要用来完成字体测量,其和 <code class="highlighter-rouge">FontMetrics</code> 非常类似,只是在文字测量时,对应的数值均是 int 类型,FontMetrics 是 float 类型。FontMetricsInt 类主要包含保存了字体测量相关的数据,源码如下:</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="kd">class</span> <span class="nc">FontMetricsInt</span> <span class="o">{</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">top</span><span class="o">;</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">ascent</span><span class="o">;</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">descent</span><span class="o">;</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">bottom</span><span class="o">;</span> <span class="kd">public</span> <span class="kt">int</span> <span class="n">leading</span><span class="o">;</span> <span class="o">}</span> </code></pre></div> </div> <p>每个值的含义如下图所示,在 baseline 之上为负值,baseline 之下为正值,leading 表示两行文本 baseline 之间的距离,这个值可以由行间距倍数和行间距增加值来调整: <img src="http://ac-qygvx1cc.clouddn.com/00a715d3dc637c92.png" alt="" /></p> <p>在接下来的字体测量中,会使用 fmCache 数组来缓存字体测量的信息,缓存 top, bottom, ascent, 和 descen 四个值,因此 fmCache 数组的大小始终是4的倍数。</p> </li> </ul> </li> <li> <p>接下来就是按照一个个段落来处理文本:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">paraStart</span> <span class="o">=</span> <span class="n">bufStart</span><span class="o">;</span> <span class="n">paraStart</span> <span class="o">&lt;=</span> <span class="n">bufEnd</span><span class="o">;</span> <span class="n">paraStart</span> <span class="o">=</span> <span class="n">paraEnd</span><span class="o">)</span> <span class="o">{</span> <span class="n">paraEnd</span> <span class="o">=</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">indexOf</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">CHAR_NEW_LINE</span><span class="o">,</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">paraEnd</span> <span class="o">&lt;</span> <span class="mi">0</span><span class="o">)</span> <span class="n">paraEnd</span> <span class="o">=</span> <span class="n">bufEnd</span><span class="o">;</span> <span class="k">else</span> <span class="n">paraEnd</span><span class="o">++;</span> <span class="o">...</span> <span class="o">}</span> </code></pre></div> </div> <p>通过查找换行符,确定每个段落的起止位置,接下来的处理,均是对该段落文本的处理。</p> </li> <li> <p>span 文本的处理</p> </li> <li> <p>处理段落文本 :</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">measured</span><span class="o">.</span><span class="na">setPara</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">paraEnd</span><span class="o">,</span> <span class="n">textDir</span><span class="o">,</span> <span class="n">b</span><span class="o">);</span> <span class="kt">char</span><span class="o">[]</span> <span class="n">chs</span> <span class="o">=</span> <span class="n">measured</span><span class="o">.</span><span class="na">mChars</span><span class="o">;</span> <span class="kt">float</span><span class="o">[]</span> <span class="n">widths</span> <span class="o">=</span> <span class="n">measured</span><span class="o">.</span><span class="na">mWidths</span><span class="o">;</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">chdirs</span> <span class="o">=</span> <span class="n">measured</span><span class="o">.</span><span class="na">mLevels</span><span class="o">;</span> <span class="kt">int</span> <span class="n">dir</span> <span class="o">=</span> <span class="n">measured</span><span class="o">.</span><span class="na">mDir</span><span class="o">;</span> <span class="kt">boolean</span> <span class="n">easy</span> <span class="o">=</span> <span class="n">measured</span><span class="o">.</span><span class="na">mEasy</span><span class="o">;</span> </code></pre></div> </div> </li> <li> <p>处理制表位,这里的制表位是使用 <code class="highlighter-rouge">TabStopSpan</code> 方式插入到文本中的,通过 Spanned 接口提供的 <code class="highlighter-rouge">getSpans(int start, int end, Class&lt;T&gt; type)</code> 方法来获取到 TabStopSpan,排序后将所有的制表位的位置存在 variableTabStops 数组中。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">int</span><span class="o">[]</span> <span class="n">variableTabStops</span> <span class="o">=</span> <span class="kc">null</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">spanned</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">TabStopSpan</span><span class="o">[]</span> <span class="n">spans</span> <span class="o">=</span> <span class="n">getParagraphSpans</span><span class="o">(</span><span class="n">spanned</span><span class="o">,</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">paraEnd</span><span class="o">,</span> <span class="n">TabStopSpan</span><span class="o">.</span><span class="na">class</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">spans</span><span class="o">.</span><span class="na">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">stops</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">spans</span><span class="o">.</span><span class="na">length</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">&lt;</span> <span class="n">spans</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span> <span class="n">stops</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="o">=</span> <span class="n">spans</span><span class="o">[</span><span class="n">i</span><span class="o">].</span><span class="na">getTabStop</span><span class="o">();</span> <span class="o">}</span> <span class="n">Arrays</span><span class="o">.</span><span class="na">sort</span><span class="o">(</span><span class="n">stops</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">stops</span><span class="o">.</span><span class="na">length</span><span class="o">);</span> <span class="n">variableTabStops</span> <span class="o">=</span> <span class="n">stops</span><span class="o">;</span> <span class="o">}}</span> </code></pre></div> </div> </li> <li> <p>完成以上处理后,就是交给 JNI 层来处理段落文本,主要处理了段落的制表行缩进、折行等;需要再分析。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">nSetupParagraph</span><span class="o">(</span><span class="n">b</span><span class="o">.</span><span class="na">mNativePtr</span><span class="o">,</span> <span class="n">chs</span><span class="o">,</span> <span class="n">paraEnd</span> <span class="o">-</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">firstWidth</span><span class="o">,</span> <span class="n">firstWidthLineCount</span><span class="o">,</span> <span class="n">restWidth</span><span class="o">,</span> <span class="n">variableTabStops</span><span class="o">,</span> <span class="n">TAB_INCREMENT</span><span class="o">,</span> <span class="n">b</span><span class="o">.</span><span class="na">mBreakStrategy</span><span class="o">,</span> <span class="n">b</span><span class="o">.</span><span class="na">mHyphenationFrequency</span><span class="o">);</span> </code></pre></div> </div> </li> <li> <p>处理缩进的源码如下:</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="n">mLeftIndents</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">||</span> <span class="n">mRightIndents</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span> <span class="n">leftLen</span> <span class="o">=</span> <span class="n">mLeftIndents</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="mi">0</span> <span class="o">:</span> <span class="n">mLeftIndents</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="kt">int</span> <span class="n">rightLen</span> <span class="o">=</span> <span class="n">mRightIndents</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="mi">0</span> <span class="o">:</span> <span class="n">mRightIndents</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="kt">int</span> <span class="n">indentsLen</span> <span class="o">=</span> <span class="n">Math</span><span class="o">.</span><span class="na">max</span><span class="o">(</span><span class="mi">1</span><span class="o">,</span> <span class="n">Math</span><span class="o">.</span><span class="na">min</span><span class="o">(</span><span class="n">leftLen</span><span class="o">,</span> <span class="n">rightLen</span><span class="o">)</span> <span class="o">-</span> <span class="n">mLineCount</span><span class="o">);</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">indents</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">indentsLen</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">&lt;</span> <span class="n">indentsLen</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span> <span class="kt">int</span> <span class="n">leftMargin</span> <span class="o">=</span> <span class="n">mLeftIndents</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="mi">0</span> <span class="o">:</span> <span class="n">mLeftIndents</span><span class="o">[</span><span class="n">Math</span><span class="o">.</span><span class="na">min</span><span class="o">(</span><span class="n">i</span> <span class="o">+</span> <span class="n">mLineCount</span><span class="o">,</span> <span class="n">leftLen</span> <span class="o">-</span> <span class="mi">1</span><span class="o">)];</span> <span class="kt">int</span> <span class="n">rightMargin</span> <span class="o">=</span> <span class="n">mRightIndents</span> <span class="o">==</span> <span class="kc">null</span> <span class="o">?</span> <span class="mi">0</span> <span class="o">:</span> <span class="n">mRightIndents</span><span class="o">[</span><span class="n">Math</span><span class="o">.</span><span class="na">min</span><span class="o">(</span><span class="n">i</span> <span class="o">+</span> <span class="n">mLineCount</span><span class="o">,</span> <span class="n">rightLen</span> <span class="o">-</span> <span class="mi">1</span><span class="o">)];</span> <span class="n">indents</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="o">=</span> <span class="n">leftMargin</span> <span class="o">+</span> <span class="n">rightMargin</span><span class="o">;</span> <span class="o">}</span> <span class="n">nSetIndents</span><span class="o">(</span><span class="n">b</span><span class="o">.</span><span class="na">mNativePtr</span><span class="o">,</span> <span class="n">indents</span><span class="o">);</span> <span class="o">}</span> </code></pre></div> </div> <p>开始的条件判断使用的 mLeftIndents 和 mRightIndents 变量是通过 Builder 对象来赋值的:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">mLeftIndents</span> <span class="o">=</span> <span class="n">b</span><span class="o">.</span><span class="na">mLeftIndents</span><span class="o">;</span> <span class="n">mRightIndents</span> <span class="o">=</span> <span class="n">b</span><span class="o">.</span><span class="na">mRightIndents</span><span class="o">;</span> </code></pre></div> </div> <p>但是比较困惑的是,源码中并没有对 Builder 对象这两个字段赋值的地方,因此这里的条件判断结果都是 false,实际 debug 测试了下,这个地方的判断确实始终是 false,所以具体的逻辑还需要再分析下。可以看见的是,在方法的最后,同样是调用 JNI 层的方法设置缩进。</p> </li> <li> <p>缓存字体测量信息,源码如下:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">spanStart</span> <span class="o">=</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">spanEnd</span><span class="o">;</span> <span class="n">spanStart</span> <span class="o">&lt;</span> <span class="n">paraEnd</span><span class="o">;</span> <span class="n">spanStart</span> <span class="o">=</span> <span class="n">spanEnd</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">&gt;=</span> <span class="n">fmCache</span><span class="o">.</span><span class="na">length</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">grow</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">*</span> <span class="mi">2</span><span class="o">];</span> <span class="n">System</span><span class="o">.</span><span class="na">arraycopy</span><span class="o">(</span><span class="n">fmCache</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">grow</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span><span class="o">);</span> <span class="n">fmCache</span> <span class="o">=</span> <span class="n">grow</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">spanEndCacheCount</span> <span class="o">&gt;=</span> <span class="n">spanEndCache</span><span class="o">.</span><span class="na">length</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">grow</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">spanEndCacheCount</span> <span class="o">*</span> <span class="mi">2</span><span class="o">];</span> <span class="n">System</span><span class="o">.</span><span class="na">arraycopy</span><span class="o">(</span><span class="n">spanEndCache</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">grow</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">spanEndCacheCount</span><span class="o">);</span> <span class="n">spanEndCache</span> <span class="o">=</span> <span class="n">grow</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">spanned</span> <span class="o">==</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">spanEnd</span> <span class="o">=</span> <span class="n">paraEnd</span><span class="o">;</span> <span class="kt">int</span> <span class="n">spanLen</span> <span class="o">=</span> <span class="n">spanEnd</span> <span class="o">-</span> <span class="n">spanStart</span><span class="o">;</span> <span class="n">measured</span><span class="o">.</span><span class="na">addStyleRun</span><span class="o">(</span><span class="n">paint</span><span class="o">,</span> <span class="n">spanLen</span><span class="o">,</span> <span class="n">fm</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">spanEnd</span> <span class="o">=</span> <span class="n">spanned</span><span class="o">.</span><span class="na">nextSpanTransition</span><span class="o">(</span><span class="n">spanStart</span><span class="o">,</span> <span class="n">paraEnd</span><span class="o">,</span> <span class="n">MetricAffectingSpan</span><span class="o">.</span><span class="na">class</span><span class="o">);</span> <span class="kt">int</span> <span class="n">spanLen</span> <span class="o">=</span> <span class="n">spanEnd</span> <span class="o">-</span> <span class="n">spanStart</span><span class="o">;</span> <span class="n">MetricAffectingSpan</span><span class="o">[]</span> <span class="n">spans</span> <span class="o">=</span> <span class="n">spanned</span><span class="o">.</span><span class="na">getSpans</span><span class="o">(</span><span class="n">spanStart</span><span class="o">,</span> <span class="n">spanEnd</span><span class="o">,</span> <span class="n">MetricAffectingSpan</span><span class="o">.</span><span class="na">class</span><span class="o">);</span> <span class="n">spans</span> <span class="o">=</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">removeEmptySpans</span><span class="o">(</span><span class="n">spans</span><span class="o">,</span> <span class="n">spanned</span><span class="o">,</span> <span class="n">MetricAffectingSpan</span><span class="o">.</span><span class="na">class</span><span class="o">);</span> <span class="n">measured</span><span class="o">.</span><span class="na">addStyleRun</span><span class="o">(</span><span class="n">paint</span><span class="o">,</span> <span class="n">spans</span><span class="o">,</span> <span class="n">spanLen</span><span class="o">,</span> <span class="n">fm</span><span class="o">);</span> <span class="o">}</span> <span class="c1">// the order of storage here (top, bottom, ascent, descent) has to match the code below</span> <span class="c1">// where these values are retrieved</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">0</span><span class="o">]</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span><span class="o">;</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">1</span><span class="o">]</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span><span class="o">;</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">2</span><span class="o">]</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span><span class="o">;</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheCount</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">3</span><span class="o">]</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span><span class="o">;</span> <span class="n">fmCacheCount</span><span class="o">++;</span> <span class="n">spanEndCache</span><span class="o">[</span><span class="n">spanEndCacheCount</span><span class="o">]</span> <span class="o">=</span> <span class="n">spanEnd</span><span class="o">;</span> <span class="n">spanEndCacheCount</span><span class="o">++;</span> <span class="o">}</span> </code></pre></div> </div> <p>fmCache 的初始化时的大小是 16,因此在每次循环开始时,需要判断下是否需要对 fmCache 扩容,这里的扩容同样保证了 fmCache 的大小是4的倍数,同时每次扩容时都是双倍扩容。</p> <p>这里也会对文本中的 Span 的结束位置使用 spanEndCache 缓存记录下来,这里处理的 span 具体类型是 MetricAffectingSpan,顾名思义就是对字体会有影响的 Span,需要单独拿出来处理,缓存字体测量信息。</p> <p>具体的测量则是交给 MeasuredText 类的 <code class="highlighter-rouge">addStyleRun(TextPaint paint, int len, Paint.FontMetricsInt fm)</code> 和 <code class="highlighter-rouge">addStyleRun(TextPaint paint, MetricAffectingSpan[] spans, int len, Paint.FontMetricsInt fm)</code> 方法来处理,具体的处理涉及到文字的排版,感兴趣的可以自己查看源码,这里不再详细分析了。</p> <p>测量完成后,字体测量信息的值4个一组地存储在 fmCache 数组中,spanEnd 值存储在 spanEndCache 数组中。</p> </li> <li> <p>计算每行宽度和折行处理,宽度的计算和折行的处理分别借助 JNI 层的 <code class="highlighter-rouge">nGetWidths()</code> 和 <code class="highlighter-rouge">nComputeLineBreaks()</code> 方法来处理。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">nGetWidths</span><span class="o">(</span><span class="n">b</span><span class="o">.</span><span class="na">mNativePtr</span><span class="o">,</span> <span class="n">widths</span><span class="o">);</span> <span class="c1">// 得到当前行内包含的折行数目</span> <span class="kt">int</span> <span class="n">breakCount</span> <span class="o">=</span> <span class="n">nComputeLineBreaks</span><span class="o">(</span><span class="n">b</span><span class="o">.</span><span class="na">mNativePtr</span><span class="o">,</span> <span class="n">lineBreaks</span><span class="o">,</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">breaks</span><span class="o">,</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">widths</span><span class="o">,</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">flags</span><span class="o">,</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">breaks</span><span class="o">.</span><span class="na">length</span><span class="o">);</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">breaks</span> <span class="o">=</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">breaks</span><span class="o">;</span> <span class="kt">float</span><span class="o">[]</span> <span class="n">lineWidths</span> <span class="o">=</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">widths</span><span class="o">;</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">flags</span> <span class="o">=</span> <span class="n">lineBreaks</span><span class="o">.</span><span class="na">flags</span><span class="o">;</span> <span class="c1">// 得到剩下的行数 = 最大允许行数 - 当前行数</span> <span class="kd">final</span> <span class="kt">int</span> <span class="n">remainingLineCount</span> <span class="o">=</span> <span class="n">mMaximumVisibleLineCount</span> <span class="o">-</span> <span class="n">mLineCount</span><span class="o">;</span> <span class="kd">final</span> <span class="kt">boolean</span> <span class="n">ellipsisMayBeApplied</span> <span class="o">=</span> <span class="n">ellipsize</span> <span class="o">!=</span> <span class="kc">null</span> <span class="o">&amp;&amp;</span> <span class="o">(</span><span class="n">ellipsize</span> <span class="o">==</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">TruncateAt</span><span class="o">.</span><span class="na">END</span> <span class="o">||</span> <span class="o">(</span><span class="n">mMaximumVisibleLineCount</span> <span class="o">==</span> <span class="mi">1</span> <span class="o">&amp;&amp;</span> <span class="n">ellipsize</span> <span class="o">!=</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">TruncateAt</span><span class="o">.</span><span class="na">MARQUEE</span><span class="o">));</span> <span class="c1">// 如果剩下的行数小于当前行包含的折行数目,则需要将最后一行和超出的行处理成单行</span> <span class="k">if</span> <span class="o">(</span><span class="n">remainingLineCount</span> <span class="o">&gt;</span> <span class="mi">0</span> <span class="o">&amp;&amp;</span> <span class="n">remainingLineCount</span> <span class="o">&lt;</span> <span class="n">breakCount</span> <span class="o">&amp;&amp;</span> <span class="n">ellipsisMayBeApplied</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// Treat the last line and overflowed lines as a single line.</span> <span class="n">breaks</span><span class="o">[</span><span class="n">remainingLineCount</span> <span class="o">-</span> <span class="mi">1</span><span class="o">]</span> <span class="o">=</span> <span class="n">breaks</span><span class="o">[</span><span class="n">breakCount</span> <span class="o">-</span> <span class="mi">1</span><span class="o">];</span> <span class="c1">// 计算 width 和 flag 值</span> <span class="kt">float</span> <span class="n">width</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="kt">int</span> <span class="n">flag</span> <span class="o">=</span> <span class="mi">0</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="n">remainingLineCount</span> <span class="o">-</span> <span class="mi">1</span><span class="o">;</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="n">breakCount</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span> <span class="n">width</span> <span class="o">+=</span> <span class="n">lineWidths</span><span class="o">[</span><span class="n">i</span><span class="o">];</span> <span class="n">flag</span> <span class="o">|=</span> <span class="n">flags</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="o">&amp;</span> <span class="n">TAB_MASK</span><span class="o">;</span> <span class="o">}</span> <span class="n">lineWidths</span><span class="o">[</span><span class="n">remainingLineCount</span> <span class="o">-</span> <span class="mi">1</span><span class="o">]</span> <span class="o">=</span> <span class="n">width</span><span class="o">;</span> <span class="n">flags</span><span class="o">[</span><span class="n">remainingLineCount</span> <span class="o">-</span> <span class="mi">1</span><span class="o">]</span> <span class="o">=</span> <span class="n">flag</span><span class="o">;</span> <span class="c1">// 设置当前行中的折行数为可用的行数</span> <span class="n">breakCount</span> <span class="o">=</span> <span class="n">remainingLineCount</span><span class="o">;</span> <span class="o">}</span> </code></pre></div> </div> <p>处理完折行后,会判断下是否需要省略处理,如果需要,则根据允许的最大行数和当前行包含的折行数目来确定需要处理成省略的那一行,并设置相关的 width 和 flag 信息。</p> </li> <li> <p>处理文本中 Span 和折行:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kt">int</span> <span class="n">here</span> <span class="o">=</span> <span class="n">paraStart</span><span class="o">;</span> <span class="kt">int</span> <span class="n">fmTop</span> <span class="o">=</span> <span class="mi">0</span><span class="o">,</span> <span class="n">fmBottom</span> <span class="o">=</span> <span class="mi">0</span><span class="o">,</span> <span class="n">fmAscent</span> <span class="o">=</span> <span class="mi">0</span><span class="o">,</span> <span class="n">fmDescent</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="kt">int</span> <span class="n">fmCacheIndex</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="kt">int</span> <span class="n">spanEndCacheIndex</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="kt">int</span> <span class="n">breakIndex</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="k">for</span> <span class="o">(</span><span class="kt">int</span> <span class="n">spanStart</span> <span class="o">=</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">spanEnd</span><span class="o">;</span> <span class="n">spanStart</span> <span class="o">&lt;</span> <span class="n">paraEnd</span><span class="o">;</span> <span class="n">spanStart</span> <span class="o">=</span> <span class="n">spanEnd</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// 从之前存储的数据中获取 span 结束位置</span> <span class="n">spanEnd</span> <span class="o">=</span> <span class="n">spanEndCache</span><span class="o">[</span><span class="n">spanEndCacheIndex</span><span class="o">++];</span> <span class="c1">// 恢复之前存储的字体测量信息</span> <span class="c1">// retrieve cached metrics, order matches above</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span> <span class="o">=</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheIndex</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">0</span><span class="o">];</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span> <span class="o">=</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheIndex</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">1</span><span class="o">];</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span> <span class="o">=</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheIndex</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">2</span><span class="o">];</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span> <span class="o">=</span> <span class="n">fmCache</span><span class="o">[</span><span class="n">fmCacheIndex</span> <span class="o">*</span> <span class="mi">4</span> <span class="o">+</span> <span class="mi">3</span><span class="o">];</span> <span class="n">fmCacheIndex</span><span class="o">++;</span> <span class="c1">// 参照前面提到的字体测量的几个值的说明,这里的 top 和 ascent 取值小的,bottom 和 descent 取值大的,保证文本均可以正常显示</span> <span class="k">if</span> <span class="o">(</span><span class="n">fm</span><span class="o">.</span><span class="na">top</span> <span class="o">&lt;</span> <span class="n">fmTop</span><span class="o">)</span> <span class="o">{</span> <span class="n">fmTop</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">fm</span><span class="o">.</span><span class="na">ascent</span> <span class="o">&lt;</span> <span class="n">fmAscent</span><span class="o">)</span> <span class="o">{</span> <span class="n">fmAscent</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">fm</span><span class="o">.</span><span class="na">descent</span> <span class="o">&gt;</span> <span class="n">fmDescent</span><span class="o">)</span> <span class="o">{</span> <span class="n">fmDescent</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">fm</span><span class="o">.</span><span class="na">bottom</span> <span class="o">&gt;</span> <span class="n">fmBottom</span><span class="o">)</span> <span class="o">{</span> <span class="n">fmBottom</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span><span class="o">;</span> <span class="o">}</span> <span class="c1">// 跳过 span 之前的折行</span> <span class="k">while</span> <span class="o">(</span><span class="n">breakIndex</span> <span class="o">&lt;</span> <span class="n">breakCount</span> <span class="o">&amp;&amp;</span> <span class="n">paraStart</span> <span class="o">+</span> <span class="n">breaks</span><span class="o">[</span><span class="n">breakIndex</span><span class="o">]</span> <span class="o">&lt;</span> <span class="n">spanStart</span><span class="o">)</span> <span class="o">{</span> <span class="n">breakIndex</span><span class="o">++;</span> <span class="o">}</span> <span class="c1">// 处理 span 中的折行</span> <span class="k">while</span> <span class="o">(</span><span class="n">breakIndex</span> <span class="o">&lt;</span> <span class="n">breakCount</span> <span class="o">&amp;&amp;</span> <span class="n">paraStart</span> <span class="o">+</span> <span class="n">breaks</span><span class="o">[</span><span class="n">breakIndex</span><span class="o">]</span> <span class="o">&lt;=</span> <span class="n">spanEnd</span><span class="o">)</span> <span class="o">{</span> <span class="kt">int</span> <span class="n">endPos</span> <span class="o">=</span> <span class="n">paraStart</span> <span class="o">+</span> <span class="n">breaks</span><span class="o">[</span><span class="n">breakIndex</span><span class="o">];</span> <span class="kt">boolean</span> <span class="n">moreChars</span> <span class="o">=</span> <span class="o">(</span><span class="n">endPos</span> <span class="o">&lt;</span> <span class="n">bufEnd</span><span class="o">);</span> <span class="n">v</span> <span class="o">=</span> <span class="n">out</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">here</span><span class="o">,</span> <span class="n">endPos</span><span class="o">,</span> <span class="n">fmAscent</span><span class="o">,</span> <span class="n">fmDescent</span><span class="o">,</span> <span class="n">fmTop</span><span class="o">,</span> <span class="n">fmBottom</span><span class="o">,</span> <span class="n">v</span><span class="o">,</span> <span class="n">spacingmult</span><span class="o">,</span> <span class="n">spacingadd</span><span class="o">,</span> <span class="n">chooseHt</span><span class="o">,</span><span class="n">chooseHtv</span><span class="o">,</span> <span class="n">fm</span><span class="o">,</span> <span class="n">flags</span><span class="o">[</span><span class="n">breakIndex</span><span class="o">],</span> <span class="n">needMultiply</span><span class="o">,</span> <span class="n">chdirs</span><span class="o">,</span> <span class="n">dir</span><span class="o">,</span> <span class="n">easy</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="n">includepad</span><span class="o">,</span> <span class="n">trackpad</span><span class="o">,</span> <span class="n">chs</span><span class="o">,</span> <span class="n">widths</span><span class="o">,</span> <span class="n">paraStart</span><span class="o">,</span> <span class="n">ellipsize</span><span class="o">,</span> <span class="n">ellipsizedWidth</span><span class="o">,</span> <span class="n">lineWidths</span><span class="o">[</span><span class="n">breakIndex</span><span class="o">],</span> <span class="n">paint</span><span class="o">,</span> <span class="n">moreChars</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">endPos</span> <span class="o">&lt;</span> <span class="n">spanEnd</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// 如果 Span 文本还未处理完成,则恢复当前的 fontMetrics 信息</span> <span class="c1">// 否则归零处理,处理下一段 Span</span> <span class="c1">// preserve metrics for current span</span> <span class="n">fmTop</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span><span class="o">;</span> <span class="n">fmBottom</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span><span class="o">;</span> <span class="n">fmAscent</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span><span class="o">;</span> <span class="n">fmDescent</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span><span class="o">;</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">fmTop</span> <span class="o">=</span> <span class="n">fmBottom</span> <span class="o">=</span> <span class="n">fmAscent</span> <span class="o">=</span> <span class="n">fmDescent</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="o">}</span> <span class="n">here</span> <span class="o">=</span> <span class="n">endPos</span><span class="o">;</span> <span class="n">breakIndex</span><span class="o">++;</span> <span class="c1">// 如果处理该段落时行数已经超过最大可见行数,则直接终止后面的处理</span> <span class="k">if</span> <span class="o">(</span><span class="n">mLineCount</span> <span class="o">&gt;=</span> <span class="n">mMaximumVisibleLineCount</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="o">}</span> <span class="c1">// 如果段落结束就是整个文本的结束,则跳出处理段落的循环,否则处理下一段。</span> <span class="k">if</span> <span class="o">(</span><span class="n">paraEnd</span> <span class="o">==</span> <span class="n">bufEnd</span><span class="o">)</span> <span class="k">break</span><span class="o">;</span> </code></pre></div> </div> <p>至此,以段落为单位的文本就处理完毕,包括文本的折行、Span 的处理都已完成。</p> </li> <li> <p>当需要处理的文本起止位置相同时(即需要处理的文本为空),且前面是换行符时,此时也需要将该空白处理成一个段落。代码如下:</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="n">bufEnd</span> <span class="o">==</span> <span class="n">bufStart</span> <span class="o">||</span> <span class="n">source</span><span class="o">.</span><span class="na">charAt</span><span class="o">(</span><span class="n">bufEnd</span> <span class="o">-</span> <span class="mi">1</span><span class="o">)</span> <span class="o">==</span> <span class="n">CHAR_NEW_LINE</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="n">mLineCount</span> <span class="o">&lt;</span> <span class="n">mMaximumVisibleLineCount</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// Log.e("text", "output last " + bufEnd);</span> <span class="n">measured</span><span class="o">.</span><span class="na">setPara</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="n">textDir</span><span class="o">,</span> <span class="n">b</span><span class="o">);</span> <span class="n">paint</span><span class="o">.</span><span class="na">getFontMetricsInt</span><span class="o">(</span><span class="n">fm</span><span class="o">);</span> <span class="n">v</span> <span class="o">=</span> <span class="n">out</span><span class="o">(</span><span class="n">source</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span><span class="o">,</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span><span class="o">,</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span><span class="o">,</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span><span class="o">,</span> <span class="n">v</span><span class="o">,</span> <span class="n">spacingmult</span><span class="o">,</span> <span class="n">spacingadd</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">fm</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">needMultiply</span><span class="o">,</span> <span class="n">measured</span><span class="o">.</span><span class="na">mLevels</span><span class="o">,</span> <span class="n">measured</span><span class="o">.</span><span class="na">mDir</span><span class="o">,</span> <span class="n">measured</span><span class="o">.</span><span class="na">mEasy</span><span class="o">,</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="n">includepad</span><span class="o">,</span> <span class="n">trackpad</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="kc">null</span><span class="o">,</span> <span class="n">bufStart</span><span class="o">,</span> <span class="n">ellipsize</span><span class="o">,</span> <span class="n">ellipsizedWidth</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">paint</span><span class="o">,</span> <span class="kc">false</span><span class="o">);</span> <span class="o">}</span> </code></pre></div> </div> <p>第10点和第11点分析中均出现了 <code class="highlighter-rouge">out()</code> 方法,前面提到,该方法也是 StaticLayout 源码中的一个重要的方法,接下来会分析下 <code class="highlighter-rouge">out</code> 方法中做了什么处理。</p> </li> </ol> <h4 id="out-方法分析">out 方法分析</h4> <p><code class="highlighter-rouge">out()</code> 方法在我看来,就是 layout 中的 out。如果说 <code class="highlighter-rouge">generate()</code> 大部分是处理一些折行、段落相关的数据,那么 <code class="highlighter-rouge">out()</code> 方法就是将这些数据使用起来,真正地布局出来(注意,布局不是显示,显示的话还是在父类的 <code class="highlighter-rouge">drawText()</code> 方法中进行的)。</p> <ol> <li> <p>方法接收的参数如下所示,很多参数都是在 <code class="highlighter-rouge">generate()</code> 中处理获得,参数的含义和前面提到的基本相同。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="n">out</span><span class="o">(</span><span class="n">CharSequence</span> <span class="n">text</span><span class="o">,</span> <span class="kt">int</span> <span class="n">start</span><span class="o">,</span> <span class="kt">int</span> <span class="n">end</span><span class="o">,</span> <span class="kt">int</span> <span class="n">above</span><span class="o">,</span> <span class="kt">int</span> <span class="n">below</span><span class="o">,</span> <span class="kt">int</span> <span class="n">top</span><span class="o">,</span> <span class="kt">int</span> <span class="n">bottom</span><span class="o">,</span> <span class="kt">int</span> <span class="n">v</span><span class="o">,</span> <span class="kt">float</span> <span class="n">spacingmult</span><span class="o">,</span> <span class="kt">float</span> <span class="n">spacingadd</span><span class="o">,</span> <span class="n">LineHeightSpan</span><span class="o">[]</span> <span class="n">chooseHt</span><span class="o">,</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">chooseHtv</span><span class="o">,</span> <span class="n">Paint</span><span class="o">.</span><span class="na">FontMetricsInt</span> <span class="n">fm</span><span class="o">,</span> <span class="kt">int</span> <span class="n">flags</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">needMultiply</span><span class="o">,</span> <span class="kt">byte</span><span class="o">[]</span> <span class="n">chdirs</span><span class="o">,</span> <span class="kt">int</span> <span class="n">dir</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">easy</span><span class="o">,</span> <span class="kt">int</span> <span class="n">bufEnd</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">includePad</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">trackPad</span><span class="o">,</span> <span class="kt">char</span><span class="o">[]</span> <span class="n">chs</span><span class="o">,</span> <span class="kt">float</span><span class="o">[]</span> <span class="n">widths</span><span class="o">,</span> <span class="kt">int</span> <span class="n">widthStart</span><span class="o">,</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">TruncateAt</span> <span class="n">ellipsize</span><span class="o">,</span> <span class="kt">float</span> <span class="n">ellipsisWidth</span><span class="o">,</span> <span class="kt">float</span> <span class="n">textWidth</span><span class="o">,</span> <span class="n">TextPaint</span> <span class="n">paint</span><span class="o">,</span> <span class="kt">boolean</span> <span class="n">moreChars</span><span class="o">)</span> </code></pre></div> </div> </li> <li> <p>对 mLineDirections 和 mLine 扩容处理,根据当前行数,判断下 mLine 数组大小是否足够储存当前行的信息,如果不够,则扩容,对应的 mLineDirections 也进行扩容处理(两个数组大小相同)。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="kt">int</span> <span class="n">j</span> <span class="o">=</span> <span class="n">mLineCount</span><span class="o">;</span> <span class="kt">int</span> <span class="n">off</span> <span class="o">=</span> <span class="n">j</span> <span class="o">*</span> <span class="n">mColumns</span><span class="o">;</span> <span class="kt">int</span> <span class="n">want</span> <span class="o">=</span> <span class="n">off</span> <span class="o">+</span> <span class="n">mColumns</span> <span class="o">+</span> <span class="n">TOP</span><span class="o">;</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">lines</span> <span class="o">=</span> <span class="n">mLines</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">want</span> <span class="o">&gt;=</span> <span class="n">lines</span><span class="o">.</span><span class="na">length</span><span class="o">)</span> <span class="o">{</span> <span class="n">Directions</span><span class="o">[]</span> <span class="n">grow2</span> <span class="o">=</span> <span class="n">ArrayUtils</span><span class="o">.</span><span class="na">newUnpaddedArray</span><span class="o">(</span> <span class="n">Directions</span><span class="o">.</span><span class="na">class</span><span class="o">,</span> <span class="n">GrowingArrayUtils</span><span class="o">.</span><span class="na">growSize</span><span class="o">(</span><span class="n">want</span><span class="o">))</span> <span class="n">System</span><span class="o">.</span><span class="na">arraycopy</span><span class="o">(</span><span class="n">mLineDirections</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">grow2</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">mLineDirections</span><span class="o">.</span><span class="na">length</span><span class="o">);</span> <span class="n">mLineDirections</span> <span class="o">=</span> <span class="n">grow2</span><span class="o">;</span> <span class="kt">int</span><span class="o">[]</span> <span class="n">grow</span> <span class="o">=</span> <span class="k">new</span> <span class="kt">int</span><span class="o">[</span><span class="n">grow2</span><span class="o">.</span><span class="na">length</span><span class="o">];</span> <span class="n">System</span><span class="o">.</span><span class="na">arraycopy</span><span class="o">(</span><span class="n">lines</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">grow</span><span class="o">,</span> <span class="mi">0</span><span class="o">,</span> <span class="n">lines</span><span class="o">.</span><span class="na">length</span><span class="o">);</span> <span class="n">mLines</span> <span class="o">=</span> <span class="n">grow</span><span class="o">;</span> <span class="n">lines</span> <span class="o">=</span> <span class="n">grow</span><span class="o">;</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>待分析</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="n">chooseHt</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span> <span class="o">=</span> <span class="n">above</span><span class="o">;</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span> <span class="o">=</span> <span class="n">below</span><span class="o">;</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span> <span class="o">=</span> <span class="n">top</span><span class="o">;</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span> <span class="o">=</span> <span class="n">bottom</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">&lt;</span> <span class="n">chooseHt</span><span class="o">.</span><span class="na">length</span><span class="o">;</span> <span class="n">i</span><span class="o">++)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">chooseHt</span><span class="o">[</span><span class="n">i</span><span class="o">]</span> <span class="k">instanceof</span> <span class="n">LineHeightSpan</span><span class="o">.</span><span class="na">WithDensity</span><span class="o">)</span> <span class="o">{</span> <span class="o">((</span><span class="n">LineHeightSpan</span><span class="o">.</span><span class="na">WithDensity</span><span class="o">)</span> <span class="n">chooseHt</span><span class="o">[</span><span class="n">i</span><span class="o">]).</span> <span class="n">chooseHeight</span><span class="o">(</span><span class="n">text</span><span class="o">,</span> <span class="n">start</span><span class="o">,</span> <span class="n">end</span><span class="o">,</span> <span class="n">chooseHtv</span><span class="o">[</span><span class="n">i</span><span class="o">],</span> <span class="n">v</span><span class="o">,</span> <span class="n">fm</span><span class="o">,</span> <span class="n">paint</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">chooseHt</span><span class="o">[</span><span class="n">i</span><span class="o">].</span><span class="na">chooseHeight</span><span class="o">(</span><span class="n">text</span><span class="o">,</span> <span class="n">start</span><span class="o">,</span> <span class="n">end</span><span class="o">,</span> <span class="n">chooseHtv</span><span class="o">[</span><span class="n">i</span><span class="o">],</span> <span class="n">v</span><span class="o">,</span> <span class="n">fm</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> <span class="n">above</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">ascent</span><span class="o">;</span> <span class="n">below</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">descent</span><span class="o">;</span> <span class="n">top</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">top</span><span class="o">;</span> <span class="n">bottom</span> <span class="o">=</span> <span class="n">fm</span><span class="o">.</span><span class="na">bottom</span><span class="o">;</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>第一行和最后一行的特殊处理,以及行间距的处理</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 判断是否是第一行</span> <span class="kt">boolean</span> <span class="n">firstLine</span> <span class="o">=</span> <span class="o">(</span><span class="n">j</span> <span class="o">==</span> <span class="mi">0</span><span class="o">);</span> <span class="c1">// 判断是否是最后一行:全部文本的最后一行或者行数等于可见的最大的行数</span> <span class="kt">boolean</span> <span class="n">currentLineIsTheLastVisibleOne</span> <span class="o">=</span> <span class="o">(</span><span class="n">j</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">==</span> <span class="n">mMaximumVisibleLineCount</span><span class="o">);</span> <span class="kt">boolean</span> <span class="n">lastLine</span> <span class="o">=</span> <span class="n">currentLineIsTheLastVisibleOne</span> <span class="o">||</span> <span class="o">(</span><span class="n">end</span> <span class="o">==</span> <span class="n">bufEnd</span><span class="o">);</span> <span class="c1">// 第一行需要处理上面的留白</span> <span class="k">if</span> <span class="o">(</span><span class="n">firstLine</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">trackPad</span><span class="o">)</span> <span class="o">{</span> <span class="n">mTopPadding</span> <span class="o">=</span> <span class="n">top</span> <span class="o">-</span> <span class="n">above</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">includePad</span><span class="o">)</span> <span class="o">{</span> <span class="n">above</span> <span class="o">=</span> <span class="n">top</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="kt">int</span> <span class="n">extra</span><span class="o">;</span> <span class="c1">// 最后一行需要处理下面的留白</span> <span class="k">if</span> <span class="o">(</span><span class="n">lastLine</span><span class="o">)</span> <span class="o">{</span> <span class="k">if</span> <span class="o">(</span><span class="n">trackPad</span><span class="o">)</span> <span class="o">{</span> <span class="n">mBottomPadding</span> <span class="o">=</span> <span class="n">bottom</span> <span class="o">-</span> <span class="n">below</span><span class="o">;</span> <span class="o">}</span> <span class="k">if</span> <span class="o">(</span><span class="n">includePad</span><span class="o">)</span> <span class="o">{</span> <span class="n">below</span> <span class="o">=</span> <span class="n">bottom</span><span class="o">;</span> <span class="o">}</span> <span class="o">}</span> <span class="c1">// 处理行间距</span> <span class="k">if</span> <span class="o">(</span><span class="n">needMultiply</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">lastLine</span><span class="o">)</span> <span class="o">{</span> <span class="kt">double</span> <span class="n">ex</span> <span class="o">=</span> <span class="o">(</span><span class="n">below</span> <span class="o">-</span> <span class="n">above</span><span class="o">)</span> <span class="o">*</span> <span class="o">(</span><span class="n">spacingmult</span> <span class="o">-</span> <span class="mi">1</span><span class="o">)</span> <span class="o">+</span> <span class="n">spacingadd</span><span class="o">;</span> <span class="k">if</span> <span class="o">(</span><span class="n">ex</span> <span class="o">&gt;=</span> <span class="mi">0</span><span class="o">)</span> <span class="o">{</span> <span class="n">extra</span> <span class="o">=</span> <span class="o">(</span><span class="kt">int</span><span class="o">)(</span><span class="n">ex</span> <span class="o">+</span> <span class="n">EXTRA_ROUNDING</span><span class="o">);</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">extra</span> <span class="o">=</span> <span class="o">-(</span><span class="kt">int</span><span class="o">)(-</span><span class="n">ex</span> <span class="o">+</span> <span class="n">EXTRA_ROUNDING</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">extra</span> <span class="o">=</span> <span class="mi">0</span><span class="o">;</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>接下来就是记录每行的文本的信息,需要注意到的是,每行的信息由 lines 中的连续的值来记录,值的数量等于 mColumns 的大小( mColumns 的取值前面有提到)。</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 记录每行的起止位置,顶部和底部位置</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">START</span><span class="o">]</span> <span class="o">=</span> <span class="n">start</span><span class="o">;</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">TOP</span><span class="o">]</span> <span class="o">=</span> <span class="n">v</span><span class="o">;</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">DESCENT</span><span class="o">]</span> <span class="o">=</span> <span class="n">below</span> <span class="o">+</span> <span class="n">extra</span><span class="o">;</span> <span class="c1">// 记录下一行的起始位置和顶部位置,v 值会作为返回值返回给调用的地方。</span> <span class="n">v</span> <span class="o">+=</span> <span class="o">(</span><span class="n">below</span> <span class="o">-</span> <span class="n">above</span><span class="o">)</span> <span class="o">+</span> <span class="n">extra</span><span class="o">;</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">mColumns</span> <span class="o">+</span> <span class="n">START</span><span class="o">]</span> <span class="o">=</span> <span class="n">end</span><span class="o">;</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">mColumns</span> <span class="o">+</span> <span class="n">TOP</span><span class="o">]</span> <span class="o">=</span> <span class="n">v</span><span class="o">;</span> <span class="c1">// TODO: could move TAB to share same column as HYPHEN, simplifying this code and gaining</span> <span class="c1">// one bit for start field</span> <span class="c1">// 通过位运算记录 tab 和文本方向信息</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">TAB</span><span class="o">]</span> <span class="o">|=</span> <span class="n">flags</span> <span class="o">&amp;</span> <span class="n">TAB_MASK</span><span class="o">;</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">HYPHEN</span><span class="o">]</span> <span class="o">=</span> <span class="n">flags</span><span class="o">;</span> <span class="n">lines</span><span class="o">[</span><span class="n">off</span> <span class="o">+</span> <span class="n">DIR</span><span class="o">]</span> <span class="o">|=</span> <span class="n">dir</span> <span class="o">&lt;&lt;</span> <span class="n">DIR_SHIFT</span><span class="o">;</span> <span class="n">Directions</span> <span class="n">linedirs</span> <span class="o">=</span> <span class="n">DIRS_ALL_LEFT_TO_RIGHT</span><span class="o">;</span> <span class="c1">// easy means all chars &lt; the first RTL, so no emoji, no nothing</span> <span class="c1">// XXX a run with no text or all spaces is easy but might be an empty</span> <span class="c1">// RTL paragraph. Make sure easy is false if this is the case.</span> <span class="c1">// 记录文本的方向</span> <span class="k">if</span> <span class="o">(</span><span class="n">easy</span><span class="o">)</span> <span class="o">{</span> <span class="n">mLineDirections</span><span class="o">[</span><span class="n">j</span><span class="o">]</span> <span class="o">=</span> <span class="n">linedirs</span><span class="o">;</span> <span class="o">}</span> <span class="k">else</span> <span class="o">{</span> <span class="n">mLineDirections</span><span class="o">[</span><span class="n">j</span><span class="o">]</span> <span class="o">=</span> <span class="n">AndroidBidi</span><span class="o">.</span><span class="na">directions</span><span class="o">(</span><span class="n">dir</span><span class="o">,</span> <span class="n">chdirs</span><span class="o">,</span> <span class="n">start</span> <span class="o">-</span> <span class="n">widthStart</span><span class="o">,</span> <span class="n">chs</span><span class="o">,</span> <span class="n">start</span> <span class="o">-</span> <span class="n">widthStart</span><span class="o">,</span> <span class="n">end</span> <span class="o">-</span> <span class="n">start</span><span class="o">);</span> <span class="o">}</span> </code></pre></div> </div> </li> <li> <p>文本省略的处理:</p> <div class="language-java highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1">// 判读是否需要省略</span> <span class="k">if</span> <span class="o">(</span><span class="n">ellipsize</span> <span class="o">!=</span> <span class="kc">null</span><span class="o">)</span> <span class="o">{</span> <span class="c1">// If there is only one line, then do any type of ellipsis except when it is MARQUEE</span> <span class="c1">// if there are multiple lines, just allow END ellipsis on the last line</span> <span class="kt">boolean</span> <span class="n">forceEllipsis</span> <span class="o">=</span> <span class="n">moreChars</span> <span class="o">&amp;&amp;</span> <span class="o">(</span><span class="n">mLineCount</span> <span class="o">+</span> <span class="mi">1</span> <span class="o">==</span> <span class="n">mMaximumVisibleLineCount</span><span class="o">);</span> <span class="kt">boolean</span> <span class="n">doEllipsis</span> <span class="o">=</span> <span class="o">(((</span><span class="n">mMaximumVisibleLineCount</span> <span class="o">==</span> <span class="mi">1</span> <span class="o">&amp;&amp;</span> <span class="n">moreChars</span><span class="o">)</span> <span class="o">||</span> <span class="o">(</span><span class="n">firstLine</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">moreChars</span><span class="o">))</span> <span class="o">&amp;&amp;</span> <span class="n">ellipsize</span> <span class="o">!=</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">TruncateAt</span><span class="o">.</span><span class="na">MARQUEE</span><span class="o">)</span> <span class="o">||</span> <span class="o">(!</span><span class="n">firstLine</span> <span class="o">&amp;&amp;</span> <span class="o">(</span><span class="n">currentLineIsTheLastVisibleOne</span> <span class="o">||</span> <span class="o">!</span><span class="n">moreChars</span><span class="o">)</span> <span class="o">&amp;&amp;</span> <span class="n">ellipsize</span> <span class="o">==</span> <span class="n">TextUtils</span><span class="o">.</span><span class="na">TruncateAt</span><span class="o">.</span><span class="na">END</span><span class="o">);</span> <span class="k">if</span> <span class="o">(</span><span class="n">doEllipsis</span><span class="o">)</span> <span class="o">{</span> <span class="n">calculateEllipsis</span><span class="o">(</span><span class="n">start</span><span class="o">,</span> <span class="n">end</span><span class="o">,</span> <span class="n">widths</span><span class="o">,</span> <span class="n">widthStart</span><span class="o">,</span> <span class="n">ellipsisWidth</span><span class="o">,</span> <span class="n">ellipsize</span><span class="o">,</span> <span class="n">j</span><span class="o">,</span> <span class="n">textWidth</span><span class="o">,</span> <span class="n">paint</span><span class="o">,</span> <span class="n">forceEllipsis</span><span class="o">);</span> <span class="o">}</span> <span class="o">}</span> </code></pre></div> </div> <p>如 Google 的工程师注释所说的那样,如果是指定了最大行数是1,则任何省略方式都可以,如果指定的最大行数不是1,但是只有单行文本时,除了 <code class="highlighter-rouge">MARQUEE</code> 的省略方式不支持以外,其他的省略方式都是支持的。如果是多行省略,且不止一行文本时,只支持在可见的最后一行的最后省略,即 <code class="highlighter-rouge">END</code> 省略方式。</p> <p>省略的计算是通过 <code class="highlighter-rouge">calculateEllipsis()</code> 方法实现的,其内部处理完成会将省略的起始位置和计数复制给 mLines 对应的每行数据的第5和第6个数据(省略时每行的记录的数据个数为6个,即 mColumns 赋的值是 COLUMNS_ELLIPSIZE 的值,即6),<code class="highlighter-rouge">calculateEllipsis()</code>方法的实现这里就不作具体分析了。</p> </li> </ol> <h4 id="总结">总结</h4> <p>至此,StaticLayout 的源码大致分析了一遍,后面需要结合 TextView 和 Layout 来具体看一下,文字到底是怎么绘制到屏幕上的。</p> </description>
<pubDate>Fri, 05 Aug 2016 00:00:00 +0000</pubDate>
<link>http://jaeger.itscoder.com//android/2016/08/05/staticlayout-source-analyse.html</link>
<guid isPermaLink="true">http://jaeger.itscoder.com//android/2016/08/05/staticlayout-source-analyse.html</guid>
</item>
</channel>
</rss>