SegmentFault JessYan Home最新的文章
2018-10-23T15:12:12+08:00
https://segmentfault.com/feeds/blogs
https://creativecommons.org/licenses/by-nc-nd/4.0/
今日头条屏幕适配方案终极版正式发布!
https://segmentfault.com/a/1190000016774816
2018-10-23T15:12:12+08:00
2018-10-23T15:12:12+08:00
JessYanCoding
https://segmentfault.com/u/jessyancoding
0
<p>原文地址: <a href="https://link.segmentfault.com/?enc=zD7BOxzN8NQOOL0oUohTHg%3D%3D.U0VmIpsqSQU48Y5vrxL6BISiiNmuYg130Zy1kEiL5UWWYIUAC0s9%2BiOUJGvHIsOi" rel="nofollow">https://www.jianshu.com/p/4aa...</a></p>
<p>以下是 <strong>骚年你的屏幕适配方式该升级了!</strong> 系列文章,欢迎转发以及分享:</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=UizPDScSBK1zipQCdsQPDQ%3D%3D.byGfx4%2FLFMjSE4HU8aVmuctPpYwIH%2BI%2F3LTxOTL2xIYeWDZ6KYQv31gvBZgcxoTL" rel="nofollow">骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案</a></li>
<li><a href="https://link.segmentfault.com/?enc=0BPKMcLNr5NIjtwsgSIS4w%3D%3D.EwNLHYg1eYN6v8Mt3va%2Br%2F8IauOAZQQVlTVwbJbHkvaQ%2FoO8jXtruLOoEyrTe9PU" rel="nofollow">骚年你的屏幕适配方式该升级了!(二)-SmallestWidth 限定符适配方案</a></li>
<li><a href="https://link.segmentfault.com/?enc=CxZz%2FRyPOR19qNCjtZL9AQ%3D%3D.ve2m6UaWUPXmzljWPlW8vEBSmaYQQ4RlCbHRTF8cdj6shY%2B2Xzqhws7exvsR5Lbq" rel="nofollow">今日头条屏幕适配方案终极版正式发布!</a></li>
</ul>
<h2>前言</h2>
<p>我在前面两篇文章中详细介绍了 <a href="https://link.segmentfault.com/?enc=NHFLvr%2BuNbmQ4DnJ%2FPxTQQ%3D%3D.Fx0M%2F5mmBs3aKKsrFbX7%2FgrYAuTPl1Z4zKClCWq%2FzhIqy828zrp8RZsJlradpRFB" rel="nofollow">今日头条适配方案</a> 和 <a href="https://link.segmentfault.com/?enc=%2FxkqI%2BGhlGlrgGj4MTB7KA%3D%3D.SNorEtF4qnGCt2oLrhehmwlwqww53uZRaguTGa1zd1bJogE%2BVSsAtJbH%2BH%2BJMGpY" rel="nofollow">SmallestWidth 限定符适配方案</a> 的原理,并验证了它们的可行性,以及总结了它们各自的优缺点,可以说这两个方案都是目前比较优秀、比较主流的 <strong>Android</strong> 屏幕适配方案,而且它们都已经拥有了一定的用户基数</p>
<p>但是对于一些才接触这两个方案的朋友,肯定或多或少还是不知道如何选择这两个方案,我虽然在之前的文章中给出了它们各自的优缺点,但是并没有用统一的标准对它们进行更细致的对比,所以也就没办法更形象的体现它们的优劣,那下面我就用统一的标准对它们进行对比,看看它们的对比情况</p>
<h2>方案对比</h2>
<p>我始终坚定地认为在这两个方案中,并不能以单个标准就能评判出谁一定比谁好,因为它们都有各自的优缺点,都不是完美的,从更客观的角度来看,它们谁都不能成为最好的那个,只有可能明确了它们各自的优缺点,知道在它们的优缺点里什么是我能接受的,什么是我不能接受的,是否能为了某些优点做出某些妥协,从而选择出一个最适合自己项目的屏幕适配方案</p>
<p>单纯的争论谁是最好的 <strong>Android</strong> 屏幕适配方案没有任何意义,每个人的需求不一样,站的角度不一样,评判标准也不一样,你能接受的东西他不一定能接受,你觉得不可接受的东西他却觉得可以接受,你有你的理由,他有他的理由,想让一个观点让所有人都能接受太难了!所以我在这里只是列出它们的对比项和对比结果,尽可能的做到客观,最后的选择结果请自行决定,如果还有什么遗漏的对比项,请补充!</p>
<table>
<thead><tr>
<th align="center">对比项目</th>
<th align="center">对比对象 A</th>
<th align="center">对比结果</th>
<th align="center">对比对象 B</th>
</tr></thead>
<tbody>
<tr>
<td align="center">适配效果(越高越好)</td>
<td align="center">今日头条适配方案</td>
<td align="center">≈</td>
<td align="center">SW 限定符适配方案(在未覆盖的机型上会存在一定的误差)</td>
</tr>
<tr>
<td align="center">稳定性(越高越好)</td>
<td align="center">今日头条适配方案</td>
<td align="center"><</td>
<td align="center">SW 限定符适配方案</td>
</tr>
<tr>
<td align="center">灵活性(越高越好)</td>
<td align="center">今日头条适配方案</td>
<td align="center">></td>
<td align="center">SW 限定符适配方案</td>
</tr>
<tr>
<td align="center">扩展性(越高越好)</td>
<td align="center">今日头条适配方案</td>
<td align="center">></td>
<td align="center">SW 限定符适配方案</td>
</tr>
<tr>
<td align="center">侵入性(越低越好)</td>
<td align="center">今日头条适配方案</td>
<td align="center"><</td>
<td align="center">SW 限定符适配方案</td>
</tr>
<tr>
<td align="center">使用成本(越低越好)</td>
<td align="center">今日头条适配方案</td>
<td align="center"><</td>
<td align="center">SW 限定符适配方案</td>
</tr>
<tr>
<td align="center">维护成本(越低越好)</td>
<td align="center">今日头条适配方案</td>
<td align="center"><</td>
<td align="center">SW 限定符适配方案</td>
</tr>
<tr>
<td align="center">性能损耗</td>
<td align="center">今日头条适配方案没有性能损耗</td>
<td align="center">=</td>
<td align="center">SW 限定符适配方案没有性能损耗</td>
</tr>
<tr>
<td align="center">副作用</td>
<td align="center">今日头条适配方案会影响一些三方库和系统控件</td>
<td align="center">≈</td>
<td align="center">SW 限定符适配方案会影响 App 的体积</td>
</tr>
</tbody>
</table>
<p>可以看到 <strong>SmallestWidth 限定符适配方案</strong> 和 <strong>今日头条适配方案</strong> 的适配效果其实都是差不多的,我在前面的文章中也通过公式计算过它们的精确度,<strong>SmallestWidth 限定符适配方案</strong> 运行在未覆盖的机型上虽然也可以适配,但是却会出现一定的误差,所以 <strong>今日头条适配方案</strong> 的适配精确度确实要比 <strong>SmallestWidth 限定符适配方案</strong> 略高的,不过只要 <strong>SmallestWidth 限定符适配方案</strong> 合理的分配资源文件,适配效果的差距应该也不大</p>
<p><strong>SmallestWidth 限定符适配方案</strong> 主打的是稳定性,在运行过程中极少会出现安全隐患,适配范围也可控,不会产生其他未知的影响,而 <strong>今日头条适配方案</strong> 主打的是降低开发成本、提高开发效率,使用上更灵活,也能满足更多的扩展需求,简单一句话概括就是,这两兄弟,一个求稳,一个求快,好了,我就介绍这么多了,自己选择吧!</p>
<h2>AndroidAutoSize</h2>
<p><img src="/img/remote/1460000016774819?w=2048&h=1000" alt="AndroidAutoSize" title="AndroidAutoSize"></p>
<h3>由来</h3>
<p>下面就开始介绍我根据 <strong>今日头条屏幕适配方案</strong> 优化的屏幕适配框架 <a href="https://link.segmentfault.com/?enc=rTC78x5S19pYwxGnDHNBjA%3D%3D.2zblifaIbPwBRv%2BHk%2BkIujOlRJwRoJzksh4tuQLfVXnFt0OJl%2FWDHheoJ0NfHccJIzf9ffiLgp8BHt%2FmZjZBrg%3D%3D" rel="nofollow"><strong>AndroidAutoSize</strong></a>,大家千万不要认为,我推出的屏幕适配框架 <a href="https://link.segmentfault.com/?enc=LxTN%2BWAEeymiu08lhF%2FhNA%3D%3D.r1zZtMR0Pw38QdT73PqLtQ7WY483UtyZuQVRRGcnIXAXhzDiWVpCMpvhQGfLMowf3bMyJOiS%2F5sWa8I3t6EyTg%3D%3D" rel="nofollow"><strong>AndroidAutoSize</strong></a> 是根据 <strong>今日头条屏幕适配方案</strong> 优化的,我本人就一定支持 <strong>今日头条屏幕适配方案</strong> 是最好的 <strong>Android</strong> 屏幕适配方案这个观点,它确实很优秀,但同样也有很多不足,我最真实的观点在上面就已经表述咯,至于我为什么要根据 <strong>今日头条屏幕适配方案</strong> 再封装一个屏幕适配框架,无外乎就以下几点原因:</p>
<ul>
<li>
<strong>SmallestWidth 限定符适配方案</strong> 已经有多个优秀的开源解决方案了,它们已经能满足我们日常开发中的所有需求</li>
<li>
<strong>今日头条</strong> 官方技术团队只公布了 <strong>今日头条屏幕适配方案</strong> 的 <a href="https://link.segmentfault.com/?enc=0zY0vt7hQauW1MkiF1nfcA%3D%3D.5FcA8WRFHSLEpDn3nemCB6bKEZ4HoMjo7gpmu2j96hdLOiWZf83b8E7BFp%2FymQRUVcdfHnQ8DOEh4xdkbakCtg%3D%3D" rel="nofollow">文章</a> 以及核心代码,但并没有在 <strong>Github</strong> 上创建公开的仓库,一个新的方案必定要有一个成长迭代的过程,在此期间,一定需要一个可以把所有使用者聚集起来的公共社区,可以让所有使用该方案的使用者在上面交流,大家一起总结、一起填坑,这样才能让该方案更成熟稳定,这就是开源的力量</li>
<li>
<strong>今日头条</strong> 官方技术团队公布的核心代码并不能满足我的所有需求,已经开源的其他基于 <strong>今日头条屏幕适配方案</strong> 的开源项目以及解决方案也不能满足我的所有需求,而我有更好的实现想法</li>
<li>
<a href="https://link.segmentfault.com/?enc=hxtZ1U7al5fu1cQFS29LAQ%3D%3D.fenVrRATqAcoxSNk9ecPTY1ev1zPn9xfAp4WzYZjec5Xije9cc1gKeAm18iO5rbJ" rel="nofollow">MVPArms</a> 需要一个适配效果还不错并且切换维护成本也比较低的屏幕适配框架,以帮助使用者用较低的成本、工作量将已经停止维护的 <strong>AndroidAutoLayout</strong> 快速替换掉</li>
</ul>
<p>我建议大家都可以去实际体验一下 <strong>今日头条屏幕适配方案</strong> 和 <strong>SmallestWidth 限定符适配方案</strong>,感受下它们的异同,我给的建议是,可以在项目中先使用 <strong>今日头条屏幕适配方案</strong>,感受下它的使用方式以及适配效果,<strong>今日头条屏幕适配方案</strong> 的侵入性非常低,如果在使用过程中遇到什么不能解决的问题,马上可以切换为其他的屏幕适配方案,在切换的过程中也花费不了多少工作量,试错成本非常低</p>
<p>但如果你在项目中先使用 <strong>SmallestWidth 限定符适配方案</strong>,之后在使用的过程中再遇到什么不能解决的问题,这时想切换为其他的屏幕适配方案,这工作量可就大了,每个 <strong>Layout</strong> 文件都含有大量的 <strong>dimens</strong> 引用,改起来这工作量得有多大,想想都觉得后怕,这就是侵入性太高导致的最致命的问题</p>
<p>如果想体验 <strong>今日头条屏幕适配方案</strong>,千万不要错过 <strong>AndroidAutoSize</strong> 哦!仅需一步即可接入项目,下面是 <strong>AndroidAutoSize</strong> 的地址:</p>
<blockquote>Github : <a href="https://link.segmentfault.com/?enc=da7P5gIXuo%2FXmqajTUs3bw%3D%3D.IrPpRAjEwQp%2B3YX8kkNo7fL8KMgadFuO21NkRYTRIP%2BxVCS2s7vIh9tPWxeQ3uaBz8cjFKWmtL3F9ZEmbX4ZvA%3D%3D" rel="nofollow">您的 Star 是我坚持的动力 ✊</a>
</blockquote>
<h4>与今日头条屏幕适配方案的关系</h4>
<p><strong>AndroidAutoSize</strong> 与 <strong>今日头条屏幕适配方案</strong> 的关系,相当于汽车和发动机的关系,<strong>今日头条屏幕适配方案</strong> 官方公布的代码,只实现了修改系统 <strong>density</strong> 的相关逻辑,这的确在屏幕适配中起到了最关键的作用,但这还远远还不够</p>
<p>要想让使用者能够更傻瓜式的使用该方案,并且能够应对日常开发中的所有复杂需求,那在架构框架时,还需要考虑 <strong>API</strong> 的易用性以及合理性、框架的扩展性以及灵活性、功能的全面性、注释和文档的易读性等多个方面的问题</p>
<p>于是我带着我的这些标准在网上搜寻了很久,发现并没有任何一个开源框架或解决方案能够达到我的所有标准,它们大多数还只是停留在将 <strong>今日头条屏幕适配方案</strong> 封装成工具类来引入项目的阶段,这样在功能的扩展上有限制,并且对用户的使用体验也不好,而我想做的是一个全面性的产品级屏幕适配框架,这离我最初的构想,差距还非常大,于是我只好自己动手,将我的所有思想实现,这才有了 <strong>AndroidAutoSize</strong></p>
<p>写完 <strong>AndroidAutoSize</strong> 框架后,因为对 <strong>今日头条屏幕适配方案</strong> 有了更加深入的理解,所以才写了 <a href="https://link.segmentfault.com/?enc=63hE%2BQdcRHL8SOapmwPFtg%3D%3D.qTYrKBFxL8oH5j0F%2Ff3DyI1PLR1Iag4Xk5tN1Y75Gu2FThYPzAqsvN7jiLwaUvWO" rel="nofollow">骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案</a>,以帮助大家更清晰的理解 <strong>今日头条屏幕适配方案</strong></p>
<h4>与 AndroidAutoLayout 的关系</h4>
<p><strong>AndroidAutoSize</strong> 因为名字和 <strong>鸿神</strong> 的 <strong>AndroidAutoLayout</strong> 非常相似,并且在填写设计图尺寸的方式上也极为相似,再加上我写的屏幕适配系列的文章也发布在了 <strong>鸿神</strong> 的公众号上,所以很多人以为 <strong>AndroidAutoSize</strong> 是 <strong>鸿神</strong> 写的 <strong>AndroidAutoLayout</strong> 的升级版,这里我哭笑不得 😂,我只好在这里说一句,大家好,我叫 <a href="https://link.segmentfault.com/?enc=zkspFElXae%2FLON%2BS1bPtdA%3D%3D.4jR62IC7s6DwQFBD46%2FO9m8YNU%2BYnFFY8KYnAhUBYsiBc5NMdP0DIvmkS%2BsFTnY5" rel="nofollow">JessYan</a>,的确可以理解为 <strong>AndroidAutoSize</strong> 是 <strong>AndroidAutoLayout</strong> 的升级版,但是它是我写的,关注一波呗</p>
<p>但 <strong>AndroidAutoSize</strong> 和 <strong>AndroidAutoLayout</strong> 的原理,却天差地别,比如 <strong>AndroidAutoLayout</strong> 只能使用 <strong>px</strong> 作为布局单位,而 <strong>AndroidAutoSize</strong> 恰好相反,在布局中 <strong>dp、sp、pt、in、mm</strong> 所有的单位都能支持,唯独不支持 <strong>px</strong>,但这也意味着 <strong>AndroidAutoSize</strong> 和 <strong>AndroidAutoLayout</strong> 在项目中可以共存,互不影响,所以使用 <strong>AndroidAutoLayout</strong> 的老项目也可以放心的引入 <strong>AndroidAutoSize</strong>,慢慢的完成屏幕适配框架的切换</p>
<p>之所以将框架取名为 <strong>AndroidAutoSize</strong>,第一,是想致敬 <strong>AndroidAutoLayout</strong> 对 <strong>Android</strong> 屏幕适配领域的贡献,第二,也想成为在 <strong>Android</strong> 屏幕适配领域有重要影响力的框架</p>
<h3>结构</h3>
<p>我在上面就已经说了很多开源框架以及解决方案,只是把 <strong>今日头条屏幕适配方案</strong> 简单的封装成一个工具类然后引入项目,这时很多人就会说了 <strong>今日头条屏幕适配方案</strong> 官方公布的全部代码都只有 <strong>30</strong> 行不到,你不把它封装成工具类,那封装成什么?该怎么封装?下面就来看看 <strong>AndroidAutoSize</strong> 的整体结构</p>
<pre><code>├── external
│ ├── ExternalAdaptInfo.java
│ ├── ExternalAdaptManager.java
│── internal
│ ├── CancelAdapt.java
│ ├── CustomAdapt.java
│── unit
│ ├── Subunits.java
│ ├── UnitsManager.java
│── utils
│ ├── AutoSizeUtils.java
│ ├── LogUtils.java
│ ├── Preconditions.java
│ ├── ScreenUtils.java
├── ActivityLifecycleCallbacksImpl.java
├── AutoAdaptStrategy.java
├── AutoSize.java
├── AutoSizeConfig.java
├── DefaultAutoAdaptStrategy.java
├── DisplayMetricsInfo.java
├── FragmentLifecycleCallbacksImpl.java
├── InitProvider.java</code></pre>
<p><strong>AndroidAutoSize</strong> 根据 <strong>今日头条屏幕适配方案</strong> 官方公布的 <strong>30</strong> 行不到的代码,经过不断的优化和扩展,发展成了现在拥有 <strong>18</strong> 个类文件,近 <strong>2000</strong> 行代码的全面性屏幕适配框架,在迭代的过程中完善和优化了很多功能,相比 <strong>今日头条屏幕适配方案</strong> 官方公布的原始代码,<strong>AndroidAutoSize</strong> 更加稳定、更加易用、更加强大,欢迎阅读源码,注释非常详细哦!</p>
<h3>功能介绍</h3>
<p><strong>AndroidAutoSize</strong> 在使用上非常简单,只需要填写设计图尺寸这一步即可接入项目,但需要注意的是,<strong>AndroidAutoSize</strong> 有两种类型的布局单位可以选择,一个是 <strong>主单位 (dp、sp)</strong>,一个是 <strong>副单位 (pt、in、mm)</strong>,两种单位面向的应用场景都有不同,也都有各自的优缺点</p>
<ul>
<li>
<strong>主单位</strong>: 使用 <strong>dp、sp</strong> 为单位进行布局,侵入性最低,会影响其他三方库页面、三方库控件以及系统控件的布局效果,但 <strong>AndroidAutoSize</strong> 也通过这个特性,使用 <strong>ExternalAdaptManager</strong> 实现了在不修改三方库源码的情况下适配三方库的功能</li>
<li>
<strong>副单位</strong>: 使用 <strong>pt、in、mm</strong> 为单位进行布局,侵入性高,对老项目的支持比较好,不会影响其他三方库页面、三方库控件以及系统控件的布局效果,可以彻底的屏蔽修改 <strong>density</strong> 所造成的所有未知和已知问题,但这样 <strong>AndroidAutoSize</strong> 也就无法对三方库进行适配</li>
</ul>
<p>大家可以根据自己的应用场景在 <strong>主单位</strong> 和 <strong>副单位</strong> 中选择一个作为布局单位,建议想引入老项目并且注重稳定性的人群使用 <strong>副单位</strong>,只是想试试本框架,随时可能切换为其他屏幕适配方案的人群使用 <strong>主单位</strong></p>
<p>其实 <strong>AndroidAutoSize</strong> 可以同时支持 <strong>主单位</strong> 和 <strong>副单位</strong>,但 <strong>AndroidAutoSize</strong> 可以同时支持 <strong>主单位</strong> 和 <strong>副单位</strong> 的目的,只是为了让使用者可以在 <strong>主单位</strong> 和 <strong>副单位</strong> 之间灵活切换,因为切换单位的工作量可能非常巨大,不能立即完成,但领导又要求马上打包上线,这时就可以起到一个很好的过渡作用</p>
<h3>主单位</h3>
<p><strong>主单位</strong> 的 <strong>Demo</strong> 在 <a href="https://link.segmentfault.com/?enc=RXcTov73dD4dx%2FRGuwGMeA%3D%3D.gfqA7E0QSDArQOIoPubw8qxmy7cLixptrcSoFz7QjpT2h48fTA4d%2FCVzCbbP3zGL%2FTcVLnHOP2GYLstLjZc7MfoOguXu0K6Dd%2FRVhfnRJkI%3D" rel="nofollow">demo</a></p>
<h4>基本使用</h4>
<p>将 <strong>AndroidAutoSize</strong> 引入项目后,只要在 <strong>app</strong> 的 <strong>AndroidManifest.xml</strong> 中填写上设计图尺寸,无需其他过多配置 (如果你没有其他自定义需求的话),<strong>AndroidAutoSize</strong> 即可自动运行,像下面这样👇</p>
<pre><code class="xml"><manifest>
<application>
<meta-data
android:name="design_width_in_dp"
android:value="360"/>
<meta-data
android:name="design_height_in_dp"
android:value="640"/>
</application>
</manifest></code></pre>
<p>在使用主单位时,<code>design_width_in_dp</code> 和 <code>design_height_in_dp</code> 的单位必须是 <strong>dp</strong>,如果设计师给你的设计图,只标注了 <strong>px</strong> 尺寸 (现在已经有很多 <strong>UI</strong> 工具可以自动标注 <strong>dp</strong> 尺寸了),那请自行根据公式 <strong>dp = px / (DPI / 160)</strong> 将 <strong>px</strong> 尺寸转换为 <strong>dp</strong> 尺寸,如果你不知道 <strong>DPI</strong> 是多少?那请以自己测试机的 <strong>DPI</strong> 为准,如果连怎么得到设备的 <strong>DPI</strong> 都不知道?百度吧好伐,如果你实在找不到设备的 <strong>DPI</strong> 那就直接将 <strong>px</strong> 尺寸除以 <strong>3</strong> 或者 <strong>2</strong> 也是可以的</p>
<p><strong>如果你只是想使用 AndroidAutoSize 的基础功能,AndroidAutoSize 的使用方法在这里就结束了,只需要上面这一步,即可帮助你以最简单的方式接入 AndroidAutoSize,但是作为一个全面性的屏幕适配框架,在保证基础功能的简易性的同时,也必须保证复杂的需求也能在框架内被解决,从而达到一个小闭环,所以下面介绍的内容全是前人踩坑踩出来的一些必备功能,如果你没这个需求,或者觉得麻烦,可以按需查看或者跳过,下面的内容建议和 Demo 配合起来阅读,效果更佳</strong></p>
<h5>注意事项</h5>
<ul>
<li>你在 <strong>AndroidManifest.xml</strong> 中怎么把设计图的 <strong>px</strong> 尺寸转换为 <strong>dp</strong> 尺寸,那在布局时,每个控件的大小也需要以同样的方式将设计图上标注的 <strong>px</strong> 尺寸转换为 <strong>dp</strong> 尺寸,千万不要在 <strong>AndroidManifest.xml</strong> 中填写的是 <strong>dp</strong> 尺寸,却在布局中继续填写设计图上标注的 <strong>px</strong> 尺寸</li>
<li>
<code>design_width_in_dp </code> 和 <code>design_height_in_dp</code> 虽然都需要填写,但是 <strong>AndroidAutoSize</strong> 只会将高度和宽度其中的一个作为基准进行适配,一方作为基准,另一方就会变为备用,默认以宽度为基准进行适配,可以通过 <strong>AutoSizeConfig#setBaseOnWidth(Boolean)</strong> 不停的切换,这意味着最后运行到设备上的布局效果,在高度和宽度中只有一方可以和设计图上一模一样,另外一方会和设计图出现偏差,为什么不像 <strong>AndroidAutoLayout</strong> 一样,高和宽都以设计图的效果等比例完美呈现呢?这也很简单,你无法保证所有设备的高宽比例都和你设计图上的高宽比例一致,特别是在现在全面屏全面推出的情况下,如果这里不这样做的话,当你的项目运行在与设计图高宽比例不一致的设备上时,布局会出现严重的变形,这个几率非常大,详情请看 <a href="https://link.segmentfault.com/?enc=qXlZd9XIQcAEuNKmWGGnjA%3D%3D.tIdU4NXpBYXROSzxCeBKkr2OUSpvxmKPTwzx%2FI8bJMSfl3CxyJZcNGwTySIHHa3AbpNLXO2r7DtuaDhb1CSQ6A%3D%3D" rel="nofollow">这里</a>
</li>
</ul>
<h4>自动运行是如何做到的?</h4>
<p>很多人有疑惑,为什么使用者只需要在 <strong>AndroidManifest.xml</strong> 中填写一下 <strong>meta-data</strong> 标签,其他什么都不做,<strong>AndroidAutoSize</strong> 就能自动运行,并在 <strong>App</strong> 启动时自动解析 <strong>AndroidManifest.xml</strong> 中填写的设计图尺寸,这里很多人不敢相信,问我真的只需要填写下设计图尺寸框架就可以正常运行吗?难道使用了什么 <strong>黑科技</strong>?</p>
<p>其实这里并没有用到什么 <strong>黑科技</strong>,原理反而非常简单,只需要声明一个 <strong>ContentProvider</strong>,在它的 <strong>onCreate</strong> 方法中启动框架即可,在 <strong>App</strong> 启动时,系统会在 <strong>App</strong> 的主进程中自动实例化你声明的这个 <strong>ContentProvider</strong>,并调用它的 <strong>onCreate</strong> 方法,执行时机比 <strong>Application#onCreate</strong> 还靠前,可以做一些初始化的工作,<strong>get</strong> 到了吗?</p>
<p>这里需要注意的是,如果你的项目拥有多进程,系统只会在主进程中实例化一个你声明的 <strong>ContentProvider</strong>,并不会在其他非主进程中实例化 <strong>ContentProvider</strong>,如果在当前进程中 <strong>ContentProvider</strong> 没有被实例化,那 <strong>ContentProvider#onCreate</strong> 就不会被调用,你的初始化代码在当前进程中也就不会执行,这时就需要在 <strong>Application#onCreate</strong> 中调用下 <strong>ContentProvider#query</strong> 执行一下查询操作,这时 <strong>ContentProvider</strong> 就会在当前进程中实例化 (每个进程中只会保证有一个实例),所以应用到框架中就是,如果你需要在多个进程中都进行屏幕适配,那就需要在 <strong>Application#onCreate</strong> 中调用 <strong>AutoSize#initCompatMultiProcess</strong> 方法</p>
<h4>进阶使用</h4>
<p>虽然 <strong>AndroidAutoSize</strong> 不需要其他过多的配置,只需要在 <strong>AndroidManifest.xml</strong> 中填写下设计图尺寸就能正常运行,但 <strong>AndroidAutoSize</strong> 还是为大家准备了很多可配置选项,尽最大可能满足大家日常开发中的所有扩展需求</p>
<p>所有的全局配置选项在 <a href="https://link.segmentfault.com/?enc=1O4ZdKrRAmEgRIzBiJIHcQ%3D%3D.8Z5LeeVWF%2BNukJuCbdnuSov%2BIzWHQWP%2Fx6c8fLxCWQKXfQxhDH9LRCpDapf3o0AeX8JTiMfISWO3kgC4fcQ1QuPWIfz3%2BtaqoVYbArdKgJKcMf6NLGYoPH7joYjXleFWWB3McGSCwePlw8Hk4SwFRhI29xeV1YbOJEc6zqmnx9XXi2%2F%2FUaE6JDrRXZIoaqRJ" rel="nofollow">Demo</a> 中都有介绍,每个 <strong>API</strong> 中也都有详细的注释,在这里就不过多介绍了</p>
<h5>自定义 Activity</h5>
<p>在 <strong>AndroidManifest.xml</strong> 中填写的设计图尺寸,是整个项目的全局设计图尺寸,但是如果某些 <strong>Activity</strong> 页面由于某些原因,设计师单独出图,这个页面的设计图尺寸和在 <strong>AndroidManifest.xml</strong> 中填写的设计图尺寸不一样该怎么办呢?不要急,<strong>AndroidAutoSize</strong> 已经为你考虑好了,让这个页面的 <strong>Activity</strong> 实现 <strong>CustomAdapt</strong> 接口即可实现你的需求,<strong>CustomAdapt</strong> 接口的第一个方法可以修改当前页面的设计图尺寸,第二个方法可以切换当前页面的适配基准,下面的注释都解释的很清楚</p>
<pre><code class="java">public class CustomAdaptActivity extends AppCompatActivity implements CustomAdapt {
/**
* 是否按照宽度进行等比例适配 (为了保证在高宽比不同的屏幕上也能正常适配, 所以只能在宽度和高度之中选择一个作为基准进行适配)
*
* @return {@code true} 为按照宽度进行适配, {@code false} 为按照高度进行适配
*/
@Override
public boolean isBaseOnWidth() {
return false;
}
/**
* 这里使用 iPhone 的设计图, iPhone 的设计图尺寸为 750px * 1334px, 高换算成 dp 为 667 (1334px / 2 = 667dp)
* <p>
* 返回设计图上的设计尺寸, 单位 dp
* {@link #getSizeInDp} 须配合 {@link #isBaseOnWidth()} 使用, 规则如下:
* 如果 {@link #isBaseOnWidth()} 返回 {@code true}, {@link #getSizeInDp} 则应该返回设计图的总宽度
* 如果 {@link #isBaseOnWidth()} 返回 {@code false}, {@link #getSizeInDp} 则应该返回设计图的总高度
* 如果您不需要自定义设计图上的设计尺寸, 想继续使用在 AndroidManifest 中填写的设计图尺寸, {@link #getSizeInDp} 则返回 {@code 0}
*
* @return 设计图上的设计尺寸, 单位 dp
*/
@Override
public float getSizeInDp() {
return 667;
}
}</code></pre>
<p>如果某个 <strong>Activity</strong> 想放弃适配,让这个 <strong>Activity</strong> 实现 <strong>CancelAdapt</strong> 接口即可,比如修改 <strong>density</strong> 影响到了老项目中的某些 <strong>Activity</strong> 页面的布局效果,这时就可以让这个 <strong>Activity</strong> 实现 <strong>CancelAdapt</strong> 接口</p>
<pre><code class="java">public class CancelAdaptActivity extends AppCompatActivity implements CancelAdapt {
}</code></pre>
<h5>自定义 Fragment</h5>
<p><strong>Fragment</strong> 的自定义方式和 <strong>Activity</strong> 是一样的,只不过在使用前需要先在 <strong>App</strong> 初始化时开启对 <strong>Fragment</strong> 的支持</p>
<pre><code class="java">AutoSizeConfig.getInstance().setCustomFragment(true);</code></pre>
<p>实现 <strong>CustomAdapt</strong></p>
<pre><code class="java">public class CustomAdaptFragment extends Fragment implements CustomAdapt {
@Override
public boolean isBaseOnWidth() {
return false;
}
@Override
public float getSizeInDp() {
return 667;
}
}</code></pre>
<p>实现 <strong>CancelAdapt</strong></p>
<pre><code class="java">public class CancelAdaptFragment extends Fragment implements CancelAdapt {
}</code></pre>
<h5>适配三方库页面</h5>
<p>在使用主单位时可以使用 <strong>ExternalAdaptManager</strong> 来实现在不修改三方库源码的情况下,适配三方库的所有页面 (<strong>Activity、Fragment</strong>)</p>
<p>由于 <strong>AndroidAutoSize</strong> 要求需要自定义适配参数或取消适配的页面必须实现 <strong>CustomAdapt</strong>、<strong>CancelAdapt</strong>,这时问题就来了,三方库是通过远程依赖的,我们无法修改它的源码,这时我们怎么让三方库的页面也能实现自定义适配参数或取消适配呢?别急,这个需求 <strong>AndroidAutoSize</strong> 也已经为你考虑好了,当然不会让你将三方库下载到本地然后改源码!</p>
<ul>
<li>通过 <strong>ExternalAdaptManager#addExternalAdaptInfoOfActivity(Class, ExternalAdaptInfo)</strong> 将需要自定义的类和自定义适配参数添加进方法即可替代实现 <strong>CustomAdapt</strong> 的方式,<a href="https://link.segmentfault.com/?enc=VAc6GyoJDp5dHIfg3%2FkSKg%3D%3D.sZ1b73lX0T5p6Fo8NDBTXeyl5XbkYLDG2OZDM8ZtoplttQuyuZddB1Dj%2BDJVpZheAVE%2F12T4Hqt1cBgmqwyCPjtc6eIneX%2BUzcO049NfqOzLOgHX7APn3kZY1oh2Y69bhdLnqIlHsGSbAMRMbcvX2r9vCoqncTgO308sF1tuwHF0eu7CGavNBcnDJy6Kkyx8" rel="nofollow">这里</a> 展示了使用方式,以及详细的注释</li>
<li>通过 <strong>ExternalAdaptManager#addCancelAdaptOfActivity(Class)</strong> 将需要取消适配的类添加进方法即可替代实现 <strong>CancelAdapt</strong> 的方式,<a href="https://link.segmentfault.com/?enc=%2FN1F4ziNATOOfmPCZNigEQ%3D%3D.8oSalGfvq2XQ%2BHH%2FZsuhIotrxGLBK8QgGfiAgnyrM%2F2u5jcRWSdSbrmd%2B%2Buc5%2BuOeXCQy0tA%2B8HODbqk48cp662XrHY45xxDIcJWvMyWAi%2Bo1Vv5zwUU%2FiNWrYLefYwKww1g59wjtnTRY%2BAJ4McpOfRNnSiF0u%2BhpeD5soqdGdeikkAhSuEyi0%2FOEOdakYh0" rel="nofollow">这里</a> 也展示了使用方式,以及详细的注释</li>
</ul>
<p>需要注意的是 <strong>ExternalAdaptManager</strong> 的方法虽然可以添加任何类,但是只能支持 <strong>Activity、Fragment</strong>,并且 <strong>ExternalAdaptManager</strong> 是支持链式调用的,以便于持续添加多个页面</p>
<p>当然 <strong>ExternalAdaptManager</strong> 不仅可以对三方库的页面使用,也可以让自己项目中的 <strong>Activity、Fragment</strong> 不用实现 <strong>CustomAdapt</strong>、<strong>CancelAdapt</strong> 即可达到自定义适配参数和取消适配的功能</p>
<h3>副单位</h3>
<p>前面已经介绍了 <strong>副单位</strong> 的应用场景,这里就直接介绍 <strong>副单位</strong> 如何使用,<strong>副单位</strong> 的 <strong>Demo</strong> 在 <a href="https://link.segmentfault.com/?enc=m3RyfT8VWqWBdByLhLAXBA%3D%3D.ucGhKEp4TMSLUNRypkP7VDBQJJtXKIidl3mST498Cm%2BMR9G59BgQ4JlkhQkvLZUb4VE1KLfrBO7lWHilwZgmYBBuZD1xCgEyZT3xXfpthgM%3D" rel="nofollow">demo-subunits</a></p>
<h4>基本使用</h4>
<p>首先和 <strong>主单位</strong> 一样也需要先在 <strong>app</strong> 的 <strong>AndroidManifest.xml</strong> 中填写上设计图尺寸,但和 <strong>主单位</strong> 不一样的是,当在使用 <strong>副单位</strong> 时 <code>design_width_in_dp</code> 和 <code>design_height_in_dp</code> 的单位不需要一定是 <strong>dp</strong>,可以直接填写设计图的 <strong>px</strong> 尺寸,在布局文件中每个控件的大小也可以直接填写设计图上标注的 <strong>px</strong> 尺寸,无需再将 <strong>px</strong> 转换为 <strong>dp</strong>,这是 <strong>副单位的</strong> 特性之一,可以帮助大家提高开发效率</p>
<pre><code class="xml"><manifest>
<application>
<meta-data
android:name="design_width_in_dp"
android:value="1080"/>
<meta-data
android:name="design_height_in_dp"
android:value="1920"/>
</application>
</manifest></code></pre>
<p>由于 <strong>AndroidAutoSize</strong> 提供了 <strong>pt、in、mm</strong> 三种类型的 <strong>副单位</strong> 供使用者选择,所以在使用 <strong>副单位</strong> 时,还需要在 <strong>APP</strong> 初始化时,通过 <strong>UnitsManager#setSupportSubunits(Subunits)</strong> 方法选择一个你喜欢的副单位,然后在布局文件中使用这个副单位进行布局,三种类型的副单位,其实效果都是一样,大家按喜欢的名字选择即可</p>
<p>由于使用副单位是为了彻底屏蔽修改 <strong>density</strong> 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响,所以在使用副单位时建议调用 <strong>UnitsManager#setSupportDP(false)</strong> 和 <strong>UnitsManager#setSupportSP(false)</strong>,关闭 <strong>AndroidAutoSize</strong> 对 <strong>dp</strong> 和 <strong>sp</strong> 的支持,<strong>AndroidAutoSize</strong> 为什么不在使用 <strong>副单位</strong> 时默认关闭对 <strong>dp</strong>、<strong>sp</strong> 的支持?因为允许同时支持 <strong>主单位</strong> 和 <strong>副单位</strong> 可以帮助使用者在 <strong>主单位</strong> 和 <strong>副单位</strong> 之间切换时更好的过渡,这点在前面就已经提到过</p>
<p><strong>UnitsManager</strong> 的详细使用方法,在 <a href="https://link.segmentfault.com/?enc=X1iDwFWOQtAL4brr0oSAJA%3D%3D.h9lso0kIjeJY7BPEReOttZNPcB4z6Hryna2HYz5BWFlAj1DbeGjpFFAU18%2FRKxB8LTpsU0TWwlazKy%2FxEeQOvp51%2BsjtF1nOw70C3wRjUdIisgutNNepo4H8g7jBx0M6Rt2fRmcZj6AL2vk1l1mhZ99SNbfHwicSdty%2BkGOzC54z1%2FB6vKvqtsarm5vEihsJ8MtZmX9pLbGLL38mzMH1Qg%3D%3D" rel="nofollow">demo-subunits</a> 中都有展示,注释也十分详细</p>
<h4>自定义 <strong>Activity</strong> 和 <strong>Fragment</strong>
</h4>
<p>在使用 <strong>副单位</strong> 时自定义 <strong>Activity</strong> 和 <strong>Fragment</strong> 的方式是和 <strong>主单位</strong> 是一样的,这里就不再过多介绍了</p>
<h4>适配三方库页面</h4>
<p>如果你的项目在使用 <strong>副单位</strong> 并且关闭了对 <strong>主单位 (dp、sp)</strong> 的支持,这时 <strong>ExternalAdaptManager</strong> 对三方库的页面是不起作用的,只对自己项目中的页面起作用,除非三方库的页面也使用了副单位 <strong>(pt、in、mm)</strong> 进行布局</p>
<p>其实 <strong>副单位</strong> 之所以能彻底屏蔽修改 <strong>density</strong> 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响,就是因为三方库页面、三方库控件以及系统控件基本上使用的都是 <strong>dp、sp</strong> 进行布局,所以只要 <strong>AndroidAutoSize</strong> 关闭了对 <strong>dp、sp</strong> 的支持,转而使用 <strong>副单位</strong> 进行布局,就能彻底屏蔽修改 <strong>density</strong> 所造成的对三方库页面、三方库控件以及系统控件的布局效果的影响</p>
<p>但这也同样意味着使用 <strong>副单位</strong> 就不能适配三方库的页面了,<strong>ExternalAdaptManager</strong> 也就对三方库的页面不起作用了</p>
<h3>布局实时预览</h3>
<p>在开发阶段布局时的实时预览是一个很重要的环节,很多情况下 <strong>Android Studio</strong> 提供的默认预览设备并不能完全展示我们的设计图,所以我们就需要自己创建模拟设备,<strong>dp、pt、in、mm</strong> 这四种单位的模拟设备创建方法请看 <a href="https://link.segmentfault.com/?enc=077W3m5lTnSJVpEZht5eyg%3D%3D.4y9dGHgXB5rYDjUAuHGbKT6I8gy68EWPmsOimqetEb%2B1yW9KB3n7githz4g6UjBrMqME53sKb4PKkWfVl9drQ51xE321D5QQkcvewkwqQkbejc6frlLBw5%2Fh2BKg1ou9" rel="nofollow">这里</a></p>
<h2>总结</h2>
<p><a href="https://link.segmentfault.com/?enc=cJ4YT1KIja1%2BuMBjhXrZUQ%3D%3D.AVXGBSHOF68Zu8ZQ4NR2A2OguxIUIcKinOBBxtZW4%2FVu3I5vRoUDT1Ws1t4TCbRyjmaXspsWvlt2Qk3XqEizYg%3D%3D" rel="nofollow"><strong>AndroidAutoSize</strong></a> 在经历了 <strong>240+ commit</strong>、<strong>60+ issues</strong>、<strong>6 个版本</strong> 的洗礼后,逐渐的稳定了下来,已经在上个星期发布了首个正式版,在这里要感谢将 <strong>AndroidAutoSize</strong> 接入到自己项目中的上千个使用者,感谢他们的信赖,<strong>AndroidAutoSize</strong> 创建的初衷就是为了让所有使用 <strong>今日头条屏幕适配方案</strong> 的使用者能有一个可以一起交流、沟通的聚集地,所以后面也会持续的收集并解决 <a href="https://link.segmentfault.com/?enc=otvrXtCKOo1SQNEI2MN%2BmQ%3D%3D.2Gk3y68B4tM87yai5YqZruMQHydVL8qO6%2BqhDfE%2BEU1%2BCtGh0TE%2FuDCNMzlfRAG10YC7lSLc450txdm7UFilfQ%3D%3D" rel="nofollow">今日头条屏幕适配方案的常见问题</a>,让 <strong>今日头条屏幕适配方案</strong> 变得更加成熟、稳定</p>
<p>至此本系列的第三篇文章也就完结了,这也预示着这个系列连载的终结,这篇文章建议结合系列的第一篇文章 <a href="https://link.segmentfault.com/?enc=0GbAZhBJGALDP%2B1QDcoxMA%3D%3D.1NXw16SYZKphx9cOD3aPM4wYVpGRacwOFzw3LKeIr%2F7kpdjjlb2qeY31QrE886PZ" rel="nofollow">骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案</a> 一起看,这样可以对 <strong>今日头条屏幕适配方案</strong> 有一个更深入的理解,如果你能将整个系列的文章都全部认真看完,那你对 <strong>Android</strong> 屏幕适配领域的相关知识绝对会有一个飞速的提升!</p>
<p>当你的项目需要切换某个框架时,你会怎么去考察、分析、对比现有的开源方案,并有足够的理由去选择或优化一个最适合自己项目的方案呢?其实整个系列文章可以看作是我怎么去选择同类型开源方案的过程,你以后当遇到同样的选择也可以参照我的思维方式去处理,当然如果以后面试官问到你屏幕适配相关的问题,你能将我如何选择、分析、对比已有方案的过程以及文章中的核心知识点告诉给面试官,那肯定比你直接说一句我使用的是某某开源库有价值得多</p>
<hr>
<p>以下是 <strong>骚年你的屏幕适配方式该升级了!</strong> 系列文章,欢迎转发以及分享:</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=Nz5ZMmfYfWqF8SKWU9aXQA%3D%3D.0gLd%2Bsvz%2B1NYgm%2FVwCFQRQ0GlJ54pwKuX8kGvJ0opvJbl7d08C8%2F0tYlcnK8QT5a" rel="nofollow">骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案</a></li>
<li><a href="https://link.segmentfault.com/?enc=9KS7TB0RsszSrnT8sBb3hw%3D%3D.0ytncD%2B8F61g96mcHj0z02fm%2BChmjkJhca1PB5gxvoCJh269fh7JlBJuNjcEKuST" rel="nofollow">骚年你的屏幕适配方式该升级了!(二)-smallestWidth 限定符适配方案</a></li>
<li><a href="https://link.segmentfault.com/?enc=7A4VK33LgDMAX8uwyhOxtA%3D%3D.7GyUgoRTSlu0sLXuvw243f3m%2Bi5cocHGnLvqZe14St6PYM9Byks%2BzTSwCWWrsrUF" rel="nofollow">今日头条屏幕适配方案终极版正式发布!</a></li>
</ul>
<hr>
<p><strong>Hello 我叫 JessYan,如果您喜欢我的文章,可以在以下平台关注我</strong></p>
<ul>
<li>个人主页: <a href="https://link.segmentfault.com/?enc=afozIwllMi4CHlsi2u4dxQ%3D%3D.CayzRGWZ4aqpeJPt5meEWojq%2BzsU5zg3GK6%2BWLfNE7c%3D" rel="nofollow">http://jessyan.me</a>
</li>
<li>GitHub: <a href="https://link.segmentfault.com/?enc=%2FmK16YtGgpMIA0B4D0OjSg%3D%3D.W5u8f1zFmdFEsimtrfzvKjywdrVQ2PrCwb1q8VoNq0%2FRr6ZJxleS1vWzSxS3pCdS" rel="nofollow">https://github.com/JessYanCoding</a>
</li>
<li>掘金: <a href="https://link.segmentfault.com/?enc=BHUNrGYccSFDTatNI5YBDw%3D%3D.wFXS8268UO626b9M5wFRECjWLbeA8XbiC3%2F0MrKICejaEWE7YaaH%2Fx1qoU7lJIgLHXrej%2B0Fm7DI%2Ff2ShA8nog%3D%3D" rel="nofollow">https://gold.xitu.io/user/57a...</a>
</li>
<li>简书: <a href="https://link.segmentfault.com/?enc=kLfEK2CSzNnBJi4ny7Lg1g%3D%3D.FIWvVTHyhfR0DoUwS1EjeGKvYl6SJOnmn5kZ1RgbSrmwI3DBZG4PlrNEoP9I2H3t" rel="nofollow">http://www.jianshu.com/u/1d0c...</a>
</li>
<li>微博: <a href="https://link.segmentfault.com/?enc=dg2qO3tFPdx4t33E3YJHYQ%3D%3D.VwoGr%2FgFqFGpDxjtzeAusWFECHLAf9HmHbjb1QXcgSI%3D" rel="nofollow">http://weibo.com/u/1786262517</a>
</li>
</ul>
<p>-- The end</p>
骚年你的屏幕适配方式该升级了!-smallestWidth 限定符适配方案
https://segmentfault.com/a/1190000016456599
2018-09-19T16:23:39+08:00
2018-09-19T16:23:39+08:00
JessYanCoding
https://segmentfault.com/u/jessyancoding
18
<p>原文地址: <a href="https://link.segmentfault.com/?enc=GnTMvWoz7yRGYBJh1%2BKSxw%3D%3D.XEDtVl6fIby64khbHyNEVJJtAXPdRQA7Rb4r%2FUGXgRuxk8wfsNHGRzm8wBvtlU1d" rel="nofollow">https://www.jianshu.com/p/2ad...</a></p>
<p>以下是 <strong>骚年你的屏幕适配方式该升级了!</strong> 系列文章,欢迎转发以及分享:</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=5uz3VaBzK5m%2F3B25kcEbPQ%3D%3D.sQJNw%2FAnucf59tAjkb%2BrfDMikdqO0CjNJsvNtUxTLqG1h1HwYN1NQBIz38tkC37l" rel="nofollow">骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案</a></li>
<li><a href="https://link.segmentfault.com/?enc=GohFolM%2FoRZCKzbcm8g5lQ%3D%3D.J0a7tvYVgPizuN6UJzsYk2%2FSxQsQUrblKOoERYG6eWSVpSiyZt87x5seCN2BF%2Fe8" rel="nofollow">骚年你的屏幕适配方式该升级了!(二)-smallestWidth 限定符适配方案</a></li>
</ul>
<h2>前言</h2>
<p><strong>ok</strong>,根据上一篇文章 <a href="https://link.segmentfault.com/?enc=Ln%2FcSTi6yDk%2B7cAIsbUh1A%3D%3D.T8R9jbAfeDXom8U2SdHJikTa6Zl7KJMTuZVvtPZQvK4B7mzq1486ZGXVTod06UYO" rel="nofollow">骚年你的屏幕适配方式该升级了!-今日头条适配方案</a> 的承诺,本文是这个系列的第二篇文章,这篇文章会详细讲解 <strong>smallestWidth 限定符屏幕适配方案</strong></p>
<p>了解我的朋友一定知道,<a href="https://link.segmentfault.com/?enc=4csOdI2%2Bn7HiljFruKdR9w%3D%3D.UFCsW8dFpYqZCxb3bC9oqGYFyHjATpKTr2HsPe%2F7XGB0Bbyc3%2Fytc1FUhQ4x%2FV3n" rel="nofollow">MVPArms</a> 一直使用的是 <strong>鸿神</strong> 的 <strong>AndroidAutoLayout</strong> 屏幕适配方案,得益于 <strong>AndroidAutoLayout</strong> 的便捷,所以我对屏幕适配领域研究的不是很多,<strong>AndroidAutoLayout</strong> 停止维护后,我也一直在找寻着替代方案,直到 <strong>今日头条屏幕适配方案</strong> 刷屏,后来又无意间看到了 <strong>smallestWidth 限定符屏幕适配方案</strong>,这才慢慢的将研究方向转向了屏幕适配领域</p>
<p>最近一个月才开始慢慢恶补 <strong>Android</strong> 屏幕适配的相关知识,对这两个方案也进行了更深入的研究,可以说从一个小白慢慢成长而来,所以我明白小白的痛,因此在上一篇文章 <a href="https://link.segmentfault.com/?enc=Dy7bwbWfFyKxGVi4%2Fv1NSw%3D%3D.F4J6rZdWmOy8QkwsLA%2BM0MrhKqNpJe4r5b3koKYpJkQoclxmhkwBeew7uGxPB%2FRV" rel="nofollow">骚年你的屏幕适配方式该升级了!-今日头条适配方案</a> 中,把 <strong>今日头条屏幕适配方案</strong> 讲得非常的细,尽量把每一个知识点都描述清晰,深怕小白漏掉每一个细节,这篇文章我也会延续上一篇文章的优良传统,将 <strong>smallestWidth 限定符屏幕适配方案</strong> 的每一个知识点都描述清晰</p>
<p>顺便说一句,感谢大家对 <a href="https://link.segmentfault.com/?enc=PPcmztdXq4SDExVgk9CGZQ%3D%3D.U7fu0tBKJNTwZivip0z7QJIfJ2bORvrWVrwvwZ00MGSc96yHqTs5ZJANauGXw%2F%2FMs4SCw6KtYg4jgqJNAzgCvLUw9A86lGlBnEBRPJUjAZE%3D" rel="nofollow">AndroidAutoSize</a> 的支持,我只是在上一篇文章中提了一嘴我刚发布的屏幕适配框架 <a href="https://link.segmentfault.com/?enc=2xfS2OzK4PNWcEOvhCYHhg%3D%3D.jeAFxwWMTvaPG4agQa%2BHA63G%2By4%2F%2FfkBlAOS0x6rZWrI2STXPrFc9OBH8lAp89UT%2FCqPDUIIEk8oATaYfqNi5jcz0VUOjt1utUMrofb7tXY%3D" rel="nofollow">AndroidAutoSize</a>,还没给出详细的介绍和原理剖析 (原计划在本系列的第三篇文章中发布),<a href="https://link.segmentfault.com/?enc=oiR1b%2FhWHMnE7F4KrhbHwA%3D%3D.r9%2FW6EDHD3WYJasl431HR2QbeoUfQlF2Y2yR2cIcT1AcEal4Mzbdoryf9sLI%2ByovGY2FLMmP7lNtkZML2Gz%2FY8g9odgJ%2BzXz%2F1pwN99%2Fld8%3D" rel="nofollow">AndroidAutoSize</a> 就被大家推上了 <a href="https://link.segmentfault.com/?enc=X7sXCD%2BIjgHMJxzdIKjjNA%3D%3D.eafUySHoyPMCZmNyZv8uHto6PVrluXKx0ySEukGF2VJ7ETDxogSGH%2BAz4MMuvD0p" rel="nofollow">Github Trending</a>,一个多星期就拿了 <strong>2k+ stars</strong>,随着关注度的增加,我在这段时间里也累坏了,<strong>issues</strong> 就没断过,不到半个月就提交了 <strong>200</strong> 多次 <strong>commit</strong>,但累并快乐着,在这里要再次感谢大家对 <a href="https://link.segmentfault.com/?enc=0LcYJcm4qlZ%2FicHC7A1PSg%3D%3D.MBUQZ7dA9mR8GJBp%2FFRvdRtNMYV08x24vcppOu0UTwFAl5LJo%2BIkiKyKQdi%2BEl8ksbPILh9IfYwTQrvkr%2B0Qt1GKbIS2zrOXQ5tmUaP0I4U%3D" rel="nofollow">AndroidAutoSize</a> 的认可</p>
<h2>谈谈对百分比库的看法</h2>
<p>是这样的,在上篇文章中有一些兄弟提了一些观点,我很是认同,但是我站在执行者的角度来看待这个问题,也有一些不同的观点,以下是我在上篇文章中的回复</p>
<p><img src="/img/remote/1460000016456602" alt="" title=""></p>
<p><strong>大家要注意了!这些观点其实针对的是所有以百分比缩放布局的库,而不只是今日头条屏幕适配方案,所以这些观点也同样适用于 smallestWidth 限定符屏幕适配方案,这点有很多人存在误解,所以一定要注意!</strong></p>
<p><img src="/img/remote/1460000016456603" alt="" title=""></p>
<p>上图的每一个方框都代表一种 <strong>Android</strong> 设备的屏幕,<strong>Android</strong> 的 <strong>系统碎片化</strong>、<strong>机型以及屏幕尺寸碎片化</strong>、<strong>屏幕分辨率碎片化</strong> 有多严重大家可以通过 <strong>友盟指数</strong> 了解一下,有些时候在某些事情的决断标准上,并不能按照事情的对错来决断,大多数情况还是要分析成本,收益等多种因素,通过利弊来决断,每个人的利弊标准又都不一样,所以每个人的观点也都会有差别,但也都应该得到尊重,所以我只是说说自己的观点,也不否认任何人的观点</p>
<p>方案是死的人是活的,在某些大屏手机或平板电脑上,您也可以采用其他适配方案和百分比库结合使用,比如针对某个屏幕区间的设备单独出一套设计图以显示比小屏幕手机更多更精细的内容,来达到与百分比库互补的效果,<strong>没有一个方案可以说自己是完美的,但我们能清晰的认识到不同方案的优缺点,将它们的优点相结合,才能应付更复杂的开发需求,产出最好的产品</strong></p>
<p><strong>友情提示:</strong> 下面要介绍的 <strong>smallestWidth 限定符屏幕适配方案</strong>,原理也同样是按照百分比缩放布局,理论上也会存在上面所说的 <strong>大屏手机和小屏手机显示的内容相同</strong> 的问题,选择与否请仔细斟酌</p>
<h2>简介 smallestWidth 限定符适配方案</h2>
<p>这个方案的的使用方式和我们平时在布局中引用 <strong>dimens</strong> 无异,核心点在于生成 <strong>dimens.xml</strong> 文件,但是已经有大神帮我们做了这 <a href="https://link.segmentfault.com/?enc=%2BPaOTZ0gLiI7DiaPBFso1A%3D%3D.GltAxvsPa28OzIFZNA9fJ%2BZEhVmVXTSOatJmyc%2F3ESMYvj5OiQ1IBWTG5NOniSmQ" rel="nofollow">一步</a></p>
<pre><code>├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-800x480
│ ├── ├──values-860x540
│ ├── ├──values-1024x600
│ ├── ├──values-1024x768
│ ├── ├──...
│ ├── ├──values-2560x1440</code></pre>
<p>如果有人还记得上面这种 <strong>宽高限定符屏幕适配方案</strong> 的话,就可以把 <strong>smallestWidth 限定符屏幕适配方案</strong> 当成这种方案的升级版,<strong>smallestWidth 限定符屏幕适配方案</strong> 只是把 <strong>dimens.xml</strong> 文件中的值从 <strong>px</strong> 换成了 <strong>dp</strong>,原理和使用方式都是没变的,这些在上面的文章中都有介绍,下面就直接开始剖析原理,<strong>smallestWidth 限定符屏幕适配方案</strong> 长这样👇</p>
<pre><code>├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-sw320dp
│ ├── ├──values-sw360dp
│ ├── ├──values-sw400dp
│ ├── ├──values-sw411dp
│ ├── ├──values-sw480dp
│ ├── ├──...
│ ├── ├──values-sw600dp
│ ├── ├──values-sw640dp</code></pre>
<h2>原理</h2>
<p>其实 <strong>smallestWidth 限定符屏幕适配方案</strong> 的原理也很简单,开发者先在项目中根据主流屏幕的 <strong>最小宽度 (smallestWidth)</strong> 生成一系列 <strong>values-sw<N>dp</strong> 文件夹 (含有 <strong>dimens.xml</strong> 文件),当把项目运行到设备上时,系统会根据当前设备屏幕的 <strong>最小宽度 (smallestWidth)</strong> 去匹配对应的 <strong>values-sw<N>dp</strong> 文件夹,而对应的 <strong>values-sw<N>dp</strong> 文件夹中的 <strong>dimens.xml</strong> 文字中的值,又是根据当前设备屏幕的 <strong>最小宽度 (smallestWidth)</strong> 而定制的,所以一定能适配当前设备</p>
<p>如果系统根据当前设备屏幕的 <strong>最小宽度 (smallestWidth)</strong> 没找到对应的 <strong>values-sw<N>dp</strong> 文件夹,则会去寻找与之 <strong>最小宽度 (smallestWidth)</strong> 相近的 <strong>values-sw<N>dp</strong> 文件夹,系统只会寻找小于或等于当前设备 <strong>最小宽度 (smallestWidth)</strong> 的 <strong>values-sw<N>dp</strong>,这就是优于 <strong>宽高限定符屏幕适配方案</strong> 的容错率,并且也可以少生成很多 <strong>values-sw<N>dp</strong> 文件夹,减轻 <strong>App</strong> 的体积</p>
<h3>什么是 smallestWidth</h3>
<p><strong>smallestWidth</strong> 翻译为中文的意思就是 <strong>最小宽度</strong>,那这个 <strong>最小宽度</strong> 是什么意思呢?</p>
<p>系统会根据当前设备屏幕的 <strong>最小宽度</strong> 来匹配 <strong>values-sw<N>dp</strong>,为什么不是根据 <strong>宽度</strong> 来匹配,而要加上 <strong>最小</strong> 这两个字呢?</p>
<p>这就要说到,移动设备都是允许屏幕可以旋转的,当屏幕旋转时,屏幕的高宽就会互换,加上 <strong>最小</strong> 这两个字,是因为这个方案是不区分屏幕方向的,它只会把屏幕的高度和宽度中值最小的一方认为是 <strong>最小宽度</strong>,这个 <strong>最小宽度</strong> 是根据屏幕来定的,是固定不变的,意思是不管您怎么旋转屏幕,只要这个屏幕的高度大于宽度,那系统就只会认定宽度的值为 <strong>最小宽度</strong>,反之如果屏幕的宽度大于高度,那系统就会认定屏幕的高度的值为 <strong>最小宽度</strong></p>
<p>如果想让屏幕宽度随着屏幕的旋转而做出改变该怎么办呢?可以再根据 <strong>values-w<N>dp</strong> (去掉 <strong>sw</strong> 中的 <strong>s</strong>) 生成一套资源文件</p>
<p>如果想区分屏幕的方向来做适配该怎么办呢?那就只有再根据 <strong>屏幕方向限定符</strong> 生成一套资源文件咯,后缀加上 <strong>-land</strong> 或 <strong>-port</strong> 即可,像这样,<strong>values-sw400dp-land (最小宽度 400 dp 横向)</strong>,<strong>values-sw400dp-port (最小宽度 400 dp 纵向)</strong></p>
<h3>smallestWidth 的值是怎么算的</h3>
<p>要先算出当前设备的 <strong>smallestWidth</strong> 值我们才能知道当前设备该匹配哪个 <strong>values-sw<N>dp</strong> 文件夹</p>
<p>ok,还是按照上一篇文章的叙述方式,现在来举栗说明,帮助大家更好理解</p>
<p>我们假设设备的屏幕信息是 <strong>1920 * 1080</strong>、<strong>480 dpi</strong></p>
<p>根据上面的规则我们要在屏幕的高度和宽度中选择值最小的一方作为最小宽度,<strong>1080 < 1920</strong>,明显 <strong>1080 px</strong> 就是我们要找的 <strong>最小宽度</strong> 的值,但 <strong>最小宽度</strong> 的单位是 <strong>dp</strong>,所以我们要把 <strong>px</strong> 转换为 <strong>dp</strong></p>
<p>帮助大家再巩固下基础,下面的公式一定不能再忘了!</p>
<p><strong>px / density = dp</strong>,<strong>DPI / 160 = density</strong>,所以最终的公式是 <strong>px / (DPI / 160) = dp</strong></p>
<p>所以我们得到的 <strong>最小宽度</strong> 的值是 <strong>360 dp (1080 / (480 / 160) = 360)</strong></p>
<p>现在我们已经算出了当前设备的最小宽度是 <strong>360 dp</strong>,我们晓得系统会根据这个 <strong>最小宽度</strong> 帮助我们匹配到 <strong>values-sw360dp</strong> 文件夹下的 <strong>dimens.xml</strong> 文件,如果项目中没有 <strong>values-sw360dp</strong> 这个文件夹,系统才会去匹配相近的 <strong>values-sw<N>dp</strong> 文件夹</p>
<p><strong>dimens.xml</strong> 文件是整个方案的核心所在,所以接下来我们再来看看 <strong>values-sw360dp</strong> 文件夹中的这个 <strong>dimens.xml</strong> 是根据什么原理生成的</p>
<h3>dimens.xml 生成原理</h3>
<p>因为我们在项目布局中引用的 <strong>dimens</strong> 的实际值,来源于根据当前设备屏幕的 <strong>最小宽度</strong> 所匹配的 <strong>values-sw<N>dp</strong> 文件夹中的 <strong>dimens.xml</strong>,所以搞清楚 <strong>dimens.xml</strong> 的生成原理,有助于我们理解 <strong>smallestWidth 限定符屏幕适配方案</strong></p>
<p>说到 <strong>dimens.xml</strong> 的生成,就要涉及到两个因数,第一个因素是 <strong>最小宽度基准值</strong>,第二个因素就是您的项目需要适配哪些 <strong>最小宽度</strong>,通俗理解就是需要生成多少个 <strong>values-sw<N>dp</strong> 文件夹</p>
<h4>第一个因素</h4>
<p><strong>最小宽度基准值</strong> 是什么意思呢?简单理解就是您需要把设备的屏幕宽度分为多少份,假设我们现在把项目的 <strong>最小宽度基准值</strong> 定为 <strong>360</strong>,那这个方案就会理解为您想把所有设备的屏幕宽度都分为 <strong>360</strong> 份,方案会帮您在 <strong>dimens.xml</strong> 文件中生成 <strong>1</strong> 到 <strong>360</strong> 的 <strong>dimens</strong> 引用,比如 <strong>values-sw360dp</strong> 中的 <strong>dimens.xml</strong> 是长这样的</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">1dp</dimen>
<dimen name="dp_2">2dp</dimen>
<dimen name="dp_3">3dp</dimen>
<dimen name="dp_4">4dp</dimen>
<dimen name="dp_5">5dp</dimen>
<dimen name="dp_6">6dp</dimen>
<dimen name="dp_7">7dp</dimen>
<dimen name="dp_8">8dp</dimen>
<dimen name="dp_9">9dp</dimen>
<dimen name="dp_10">10dp</dimen>
...
<dimen name="dp_356">356dp</dimen>
<dimen name="dp_357">357dp</dimen>
<dimen name="dp_358">358dp</dimen>
<dimen name="dp_359">359dp</dimen>
<dimen name="dp_360">360dp</dimen>
</resources></code></pre>
<p><strong>values-sw360dp</strong> 指的是当前设备屏幕的 <strong>最小宽度</strong> 为 <strong>360dp</strong> (该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为 <strong>360dp</strong>),把屏幕宽度分为 <strong>360</strong> 份,刚好每份等于 <strong>1dp</strong>,所以每个引用都递增 <strong>1dp</strong>,值最大的 <strong>dimens</strong> 引用 <strong>dp_360</strong> 值也是 <strong>360dp</strong>,刚好覆盖屏幕宽度</p>
<p>下面再来看看将 <strong>最小宽度基准值</strong> 定为 <strong>360</strong> 时,<strong>values-sw400dp</strong> 中的 <strong>dimens.xml</strong> 长什么样</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">1.1111dp</dimen>
<dimen name="dp_2">2.2222dp</dimen>
<dimen name="dp_3">3.3333dp</dimen>
<dimen name="dp_4">4.4444dp</dimen>
<dimen name="dp_5">5.5556dp</dimen>
<dimen name="dp_6">6.6667dp</dimen>
<dimen name="dp_7">7.7778dp</dimen>
<dimen name="dp_8">8.8889dp</dimen>
<dimen name="dp_9">10.0000dp</dimen>
<dimen name="dp_10">11.1111dp</dimen>
...
<dimen name="dp_355">394.4444dp</dimen>
<dimen name="dp_356">395.5556dp</dimen>
<dimen name="dp_357">396.6667dp</dimen>
<dimen name="dp_358">397.7778dp</dimen>
<dimen name="dp_359">398.8889dp</dimen>
<dimen name="dp_360">400.0000dp</dimen>
</resources></code></pre>
<p><strong>values-sw400dp</strong> 指的是当前设备屏幕的 <strong>最小宽度</strong> 为 <strong>400dp</strong> (该设备高度大于宽度,则最小宽度就是宽度,所以该设备宽度为 <strong>400dp</strong>),把屏幕宽度同样分为 <strong>360份</strong>,这时每份就等于 <strong>1.1111dp</strong> 了,每个引用都递增 <strong>1.1111dp</strong>,值最大的 <strong>dimens</strong> 引用 <strong>dp_360</strong> 同样刚好覆盖屏幕宽度,为 <strong>400dp</strong></p>
<p>通过两个 <strong>dimens.xml</strong> 文件的比较,<strong>dimens.xml</strong> 的生成原理一目了然,方案会先确定 <strong>最小宽度基准值</strong>,然后将每个 <strong>values-sw<N>dp</strong> 中的 <strong>dimens.xml</strong> 文件都分配与 <strong>最小宽度基准值</strong> 相同的份数,再根据公式 <strong>屏幕最小宽度 / 份数 (最小宽度基准值)</strong> 求出每份占多少 <strong>dp</strong>,保证不管在哪个 <strong>values-sw<N>dp</strong> 中,<strong>份数 (最小宽度基准值) * 每份占的 dp 值</strong> 的结果都是刚好覆盖屏幕宽度,所以在 <strong>份数</strong> 不变的情况下,只需要根据屏幕的宽度在不同的设备上动态调整 <strong>每份占的 dp 值</strong>,就能完成适配</p>
<p>这样就能保证不管将项目运行到哪个设备上,只要当前设备能匹配到对应的 <strong>values-sw<N>dp</strong> 文件夹,那布局中的 <strong>dimens</strong> 引用就能根据当前屏幕的情况进行缩放,保证能完美适配,如果没有匹配到对应的 <strong>values-sw<N>dp</strong> 文件夹,也没关系,它会去寻找与之相近的 <strong>values-sw<N>dp</strong> 文件夹,虽然在这种情况下,布局中的 <strong>dimens</strong> 引用的值可能有些许误差,但是也能保证最大程度的完成适配</p>
<p>说到这里,那大家就应该就会明白我为什么会说 <strong>smallestWidth 限定符屏幕适配方案</strong> 的原理也同样是按百分比进行布局,如果在布局中,一个 <strong>View</strong> 的宽度引用 <strong>dp_100</strong>,那不管运行到哪个设备上,这个 <strong>View</strong> 的宽度都是当前设备屏幕总宽度的 <strong>360分之100</strong>,前提是项目提供有当前设备屏幕对应的 <strong>values-sw<N>dp</strong>,如果没有对应的 <strong>values-sw<N>dp</strong>,就会去寻找相近的 <strong>values-sw<N>dp</strong>,这时就会存在误差了,至于误差是大是小,这就要看您的第二个因数怎么分配了</p>
<p>其实 <strong>smallestWidth 限定符屏幕适配方案</strong> 的原理和 <strong>今日头条屏幕适配方案</strong> 挺像的,<strong>今日头条屏幕适配方案</strong> 是根据屏幕的宽度或高度动态调整每个设备的 <strong>density</strong> (每 <strong>dp</strong> 占当前设备屏幕多少像素),而 <strong>smallestWidth 限定符屏幕适配方案</strong> 同样是根据屏幕的宽度动态调整每个设备 <strong>每份占的 dp 值</strong></p>
<h4>第二个因素</h4>
<p>第二个因数是需要适配哪些 <strong>最小宽度</strong>?比如您想适配的 <strong>最小宽度</strong> 有 <strong>320dp</strong>、<strong>360dp</strong>、<strong>400dp</strong>、<strong>411dp</strong>、<strong>480dp</strong>,那方案就会为您的项目生成 <strong>values-sw320dp</strong>、<strong>values-sw360dp</strong>、<strong>values-sw400dp</strong>、<strong>values-sw411dp</strong>、<strong>values-sw480dp</strong> 这几个资源文件夹,像这样👇</p>
<pre><code>├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-sw320dp
│ ├── ├──values-sw360dp
│ ├── ├──values-sw400dp
│ ├── ├──values-sw411dp
│ ├── ├──values-sw480dp</code></pre>
<p>方案会为您需要适配的 <strong>最小宽度</strong>,在项目中生成一系列对应的 <strong>values-sw<N>dp</strong>,在前面也说了,如果某个设备没有为它提供对应的 <strong>values-sw<N>dp</strong>,那它就会去寻找相近的 <strong>values-sw<N>dp</strong>,但如果这个相近的 <strong>values-sw<N>dp</strong> 与期望的 <strong>values-sw<N>dp</strong> 差距太大,那适配效果也就会大打折扣</p>
<p>那是不是 <strong>values-sw<N>dp</strong> 文件夹生成的越多,覆盖越多市面上的设备,就越好呢?</p>
<p>也不是,因为每个 <strong>values-sw<N>dp</strong> 文件夹其实都会占用一定的 <strong>App</strong> 体积,<strong>values-sw<N>dp</strong> 文件夹越多,<strong>App</strong> 的体积也就会越大</p>
<p>所以一定要合理分配 <strong>values-sw<N>dp</strong>,以越少的 <strong>values-sw<N>dp</strong> 文件夹,覆盖越多的机型</p>
<h2>验证方案可行性</h2>
<p>原理讲完了,我们还是按照老规矩,来验证一下这个方案是否可行?</p>
<p>假设设计图总宽度为 <strong>375 dp</strong>,一个 <strong>View</strong> 在这个设计图上的尺寸是 <strong>50dp * 50dp</strong>,这个 <strong>View</strong> 的宽度占整个设计图宽度的 <strong>13.3%</strong> (<strong>50 / 375 = 0.133</strong>)</p>
<p>在使用 <strong>smallestWidth 限定符屏幕适配方案</strong> 时,需要提供 <strong>最小宽度基准值</strong> 和需要适配哪些 <strong>最小宽度</strong>,我们就把 <strong>最小宽度基准值</strong> 设置为 <strong>375</strong> (和 <strong>设计图</strong> 一致),这时方案就会为我们需要适配的 <strong>最小宽度</strong> 生成对应的 <strong>values-sw<N>dp</strong> 文件夹,文件夹中的 <strong>dimens.xml</strong> 文件是由从 <strong>1</strong> 到 <strong>375</strong> 组成的 <strong>dimens</strong> 引用,把所有设备的屏幕宽度都分为 <strong>375</strong> 份,所以在布局文件中我们应该把这个 <strong>View</strong> 的高宽都引用 <strong>dp_50</strong></p>
<p>下面就来验证下在使用 <strong>smallestWidth 限定符屏幕适配方案</strong> 的情况下,这个 <strong>View</strong> 与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致</p>
<h3>验证设备 1</h3>
<p><strong>设备 1</strong> 的屏幕总宽度为 <strong>1080 px</strong>,屏幕总高度为 <strong>1920 px</strong>,<strong>DPI</strong> 为 <strong>480</strong></p>
<p><strong>设备 1</strong> 的屏幕高度大于屏幕宽度,所以 <strong>设备 1</strong> 的 <strong>最小宽度</strong> 为屏幕宽度,再根据公式 <strong>px / (DPI / 160) = dp</strong>,求出 <strong>设备 1</strong> 的 <strong>最小宽度</strong> 的值为 <strong>360 dp</strong> (<strong>1080 / (480 / 160) = 360</strong>)</p>
<p>根据 <strong>设备 1</strong> 的 <strong>最小宽度</strong> 应该匹配的是 <strong>values-sw360dp</strong> 这个文件夹,假设 <strong>values-sw360dp</strong> 文件夹及里面的 <strong>dimens.xml</strong> 已经生成,且是按 <strong>最小宽度基准值</strong> 为 <strong>375</strong> 生成的,<strong>360 / 375 = 0.96</strong>,所以每份占的 <strong>dp</strong> 值为 <strong>0.96</strong>,<strong>dimens.xml</strong> 里面的内容是长下面这样的👇</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">0.96dp</dimen>
<dimen name="dp_2">1.92dp</dimen>
<dimen name="dp_3">2.88dp</dimen>
<dimen name="dp_4">3.84dp</dimen>
<dimen name="dp_5">4.8dp</dimen>
...
<dimen name="dp_50">48dp</dimen>
...
<dimen name="dp_371">356.16dp</dimen>
<dimen name="dp_372">357.12dp</dimen>
<dimen name="dp_373">358.08dp</dimen>
<dimen name="dp_374">359.04dp</dimen>
<dimen name="dp_375">360dp</dimen>
</resources></code></pre>
<p>可以看到这个 <strong>View</strong> 在布局中引用的 <strong>dp_50</strong>,最终在 <strong>values-sw360dp</strong> 中定格在了 <strong>48 dp</strong>,所以这个 <strong>View</strong> 在 <strong>设备 1</strong> 上的高宽都为 <strong>48 dp</strong>,系统最后会将高宽都换算成 <strong>px</strong>,根据公式 <strong>dp <em> (DPI / 160) = px</em></strong>,所以这个 <strong>View</strong> 的高宽换算为 <strong>px</strong> 后等于 <strong>144 px</strong> (<strong>48 (480 / 160) = 144</strong>)</p>
<p><strong>144 / 1080 = 0.133</strong>,<strong>View</strong> 的实际宽度与 <strong>屏幕总宽度</strong> 的比例和 <strong>View</strong> 在设计图中的比例一致 (<strong>50 / 375 = 0.133</strong>),所以完成了等比例缩放</p>
<p>某些设备的高宽是和 <strong>设备 1</strong> 相同的,但是 <strong>DPI</strong> 可能不同,而由于 <strong>smallestWidth 限定符屏幕适配方案</strong> 并没有像 <strong>今日头条屏幕适配方案</strong> 一样去自行修改 <strong>density</strong>,所以系统就会使用默认的公式 <strong>DPI / 160</strong> 求出 <strong>density</strong>,<strong>density</strong> 又会影响到 <strong>dp</strong> 和 <strong>px</strong> 的换算,因此 <strong>DPI</strong> 的变化,是有可能会影响到 <strong>smallestWidth 限定符屏幕适配方案</strong> 的</p>
<p>所以我们再来试试在这种特殊情况下 <strong>smallestWidth 限定符屏幕适配方案</strong> 是否也能完成适配</p>
<h3>验证设备 2</h3>
<p><strong>设备 2</strong> 的屏幕总宽度为 <strong>1080 px</strong>,屏幕总高度为 <strong>1920 px</strong>,<strong>DPI</strong> 为 <strong>420</strong></p>
<p><strong>设备 2</strong> 的屏幕高度大于屏幕宽度,所以 <strong>设备 2</strong> 的 <strong>最小宽度</strong> 为屏幕宽度,再根据公式 <strong>px / (DPI / 160) = dp</strong>,求出 <strong>设备 2</strong> 的 <strong>最小宽度</strong> 的值为 <strong>411.429 dp</strong> (<strong>1080 / (420 / 160) = 411.429</strong>)</p>
<p>根据 <strong>设备 2</strong> 的 <strong>最小宽度</strong> 应该匹配的是 <strong>values-sw411dp</strong> 这个文件夹,假设 <strong>values-sw411dp</strong> 文件夹及里面的 <strong>dimens.xml</strong> 已经生成,且是按 <strong>最小宽度基准值</strong> 为 <strong>375</strong> 生成的,<strong>411 / 375 = 1.096</strong>,所以每份占的 <strong>dp</strong> 值为 <strong>1.096</strong>,<strong>dimens.xml</strong> 里面的内容是长下面这样的👇</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="dp_1">1.096dp</dimen>
<dimen name="dp_2">2.192dp</dimen>
<dimen name="dp_3">3.288dp</dimen>
<dimen name="dp_4">4.384dp</dimen>
<dimen name="dp_5">5.48dp</dimen>
...
<dimen name="dp_50">54.8dp</dimen>
...
<dimen name="dp_371">406.616dp</dimen>
<dimen name="dp_372">407.712dp</dimen>
<dimen name="dp_373">408.808dp</dimen>
<dimen name="dp_374">409.904dp</dimen>
<dimen name="dp_375">411dp</dimen>
</resources></code></pre>
<p>可以看到这个 <strong>View</strong> 在布局中引用的 <strong>dp_50</strong>,最终在 <strong>values-sw411dp</strong> 中定格在了 <strong>54.8dp</strong>,所以这个 <strong>View</strong> 在 <strong>设备 2</strong> 上的高宽都为 <strong>54.8 dp</strong>,系统最后会将高宽都换算成 <strong>px</strong>,根据公式 <strong>dp <em> (DPI / 160) = px</em></strong>,所以这个 <strong>View</strong> 的高宽换算为 <strong>px</strong> 后等于 <strong>143.85 px</strong> (<strong>54.8 (420 / 160) = 143.85</strong>)</p>
<p><strong>143.85 / 1080 = 0.133</strong>,<strong>View</strong> 的实际宽度与 <strong>屏幕总宽度</strong> 的比例和 <strong>View</strong> 在设计图中的比例一致 (<strong>50 / 375 = 0.133</strong>),所以完成了等比例缩放</p>
<p>虽然 <strong>View</strong> 在 <strong>设备 2</strong> 上的高宽是 <strong>143.85 px</strong>,比 <strong>设备 1</strong> 的 <strong>144 px</strong> 少了 <strong>0.15 px</strong>,但是误差非常小,整体的比例并没有发生太大的变化,是完全可以接受的</p>
<p>这个误差是怎么引起的呢,因为 <strong>设备 2</strong> 的 <strong>最小宽度</strong> 的实际值是 <strong>411.429 dp</strong>,但是匹配的 <strong>values-sw411dp</strong> 舍去了小数点后面的位数 (<strong>切记!系统会去寻找小于或等于 411.429 dp 的 values-sw<N>dp,所以 values-sw412dp 这个文件夹,设备 2 是匹配不了的</strong>),所以才存在了一定的误差,因此上面介绍的第二个因数是非常重要的,这直接决定误差是大还是小</p>
<p>可以看到即使在高宽一样但 <strong>DPI</strong> 不一样的设备上,<strong>smallestWidth 限定符屏幕适配方案</strong> 也能完成等比例适配,证明这个方案是可行的,如果大家还心存疑虑,也可以再试试其他分辨率的设备,其实到最后得出的比例都是在 <strong>0.133</strong> 左右,唯一的变数就是第二个因数,如果您生成的 <strong>values-sw<N>dp</strong> 与设备实际的 <strong>最小宽度</strong> 差别不大,那误差也就在能接受的范围内,如果差别很大,那就直接 <strong>GG</strong></p>
<h2>优点</h2>
<ol>
<li>非常稳定,极低概率出现意外</li>
<li>不会有任何性能的损耗</li>
<li>适配范围可自由控制,不会影响其他三方库</li>
<li>在插件的配合下,学习成本低</li>
</ol>
<h2>缺点</h2>
<ol>
<li>在布局中引用 <strong>dimens</strong> 的方式,虽然学习成本低,但是在日常维护修改时较麻烦</li>
<li>侵入性高,如果项目想切换为其他屏幕适配方案,因为每个 <strong>Layout</strong> 文件中都存在有大量 <strong>dimens</strong> 的引用,这时修改起来工作量非常巨大,切换成本非常高昂</li>
<li>无法覆盖全部机型,想覆盖更多机型的做法就是生成更多的资源文件,但这样会增加 <strong>App</strong> 体积,在没有覆盖的机型上还会出现一定的误差,所以有时需要在适配效果和占用空间上做一些抉择</li>
<li>如果想使用 <strong>sp</strong>,也需要生成一系列的 <strong>dimens</strong>,导致再次增加 <strong>App</strong> 的体积</li>
<li>不能自动支持横竖屏切换时的适配,如上文所说,如果想自动支持横竖屏切换时的适配,需要使用 <strong>values-w<N>dp</strong> 或 <strong>屏幕方向限定符</strong> 再生成一套资源文件,这样又会再次增加 <strong>App</strong> 的体积</li>
<li>不能以高度为基准进行适配,考虑到这个方案的名字本身就叫 <strong>最小宽度限定符适配方案</strong>,所以在使用这个方案之前就应该要知道这个方案只能以宽度为基准进行适配,为什么现在的屏幕适配方案只能以高度或宽度其中的一个作为基准进行适配,请看 <a href="https://link.segmentfault.com/?enc=ie1AnZ7fFFGdxuAdhDbFrg%3D%3D.8foO6trKZLT5eMPZ7BVbElnaMVmDq36V30VWgc4krrsJzDYDyrEqhlG93lS5nFQt8aVbAPABSHpRu5og93WUpQ%3D%3D" rel="nofollow">这里</a>
</li>
</ol>
<h2>使用中的问题</h2>
<p>这时有人就会问了,设计师给的设计图只标注了 <strong>px</strong>,使用这个方案时,那不是还要先将 <strong>px</strong> 换算成 <strong>dp</strong>?</p>
<p>其实也可以不用换算的,那这是什么骚操作呢?</p>
<p>很简单,你把设计图的 <strong>px</strong> 总宽度设置成 <strong>最小宽度基准值</strong> 就可以了,还是以前面验证可行性的例子</p>
<p>我们在前面验证可行性时把 <strong>最小宽度基准值</strong> 设置成了 <strong>375</strong>,为什么是 <strong>375</strong> 呢?因为设计图的总宽度为 <strong>375 dp</strong>,如果换算成 <strong>px</strong>,总宽度就是 <strong>750 px</strong>,我们这时把 <strong>最小宽度基准值</strong> 设置成 <strong>750</strong>,然后看看 <strong>values-sw360dp</strong> 中的 <strong>dimens.xml</strong> 长什么样👇</p>
<pre><code class="xml"><?xml version="1.0" encoding="UTF-8"?>
<resources>
<dimen name="px_1">0.48dp</dimen>
<dimen name="px_2">0.96dp</dimen>
<dimen name="px_3">1.44dp</dimen>
<dimen name="px_4">1.92dp</dimen>
<dimen name="px_5">2.4dp</dimen>
...
<dimen name="px_50">24dp</dimen>
...
<dimen name="px_100">48dp</dimen>
...
<dimen name="px_746">358.08dp</dimen>
<dimen name="px_747">358.56dp</dimen>
<dimen name="px_748">359.04dp</dimen>
<dimen name="px_749">359.52dp</dimen>
<dimen name="px_750">360dp</dimen>
</resources></code></pre>
<p><strong>360 dp</strong> 被分成了 <strong>750</strong> 份,相比之前的 <strong>375</strong> 份,现在 <strong>每份占的 dp 值</strong> 正好减少了一半,还记得在验证可行性的例子中那个 <strong>View</strong> 的尺寸是多少吗?<strong>50dp <em> 50dp</em></strong>,如果设计图只标注 <strong>px</strong>,那这个 <strong>View</strong> 在设计图上的的尺寸应该是 <strong>100px 100px</strong>,那我们直接根据设计图上标注的 <strong>px</strong>,想都不用想直接在布局中引用 <strong>px_100</strong> 就可以了,因为在 <strong>375</strong> 份时的 <strong>dp_50</strong> 刚好等于 <strong>750</strong> 份时的 <strong>px_100</strong> (值都是 <strong>48 dp</strong>),所以这时的适配效果和之前验证可行性时的适配效果没有任何区别</p>
<p>看懂了吗?直接将 <strong>最小宽度基准值</strong> 和布局中的引用都以 <strong>px</strong> 作为单位就可以直接填写设计图上标注的 <strong>px</strong>!</p>
<h2>总结</h2>
<p>好了,这个系列的第二篇文章讲完了,这篇文章也是按照上篇文章的优良传统,写的非常详细,哪怕是新手我相信也应该能看懂,为什么这么多人都不知道自己该选择什么样的方案,就是因为自己都没搞懂这些方案的原理,懂了原理过后才知道这些方案是否是自己想要的</p>
<p>接下来的第三篇文章会详细讲解两个方案的深入对比以及该如何选择,并剖析我根据 <strong>今日头条屏幕适配方案</strong> 优化的屏幕适配框架 <a href="https://link.segmentfault.com/?enc=BSyFUph8G6lZZU%2Ft6nja9g%3D%3D.NraOG%2FzrMQx6DXBGB9CdsMY%2BiUJsZi0pktBa0HMjX6P6qGT7jgl65HwriM45w5f%2By8RvKUx5vpS%2BCKMyv9I5uQ%3D%3D" rel="nofollow"><strong>AndroidAutoSize</strong></a> 的原理,敬请期待</p>
<p>如果大家想使用 <strong>smallestWidth 限定符屏幕适配方案</strong>,可以参考 <a href="https://link.segmentfault.com/?enc=CYADND7ATbv3MW7EkEMwpA%3D%3D.Ot%2FKFGZELeQFiKghboH9dWqoS%2FtU7t7lFMXERNndsB64eEmcFtD5RtS1q3qXZPF3" rel="nofollow">这篇文章</a>,里面提供有自动生成资源文件的插件和 <strong>Demo</strong>,由于我并没有在项目中使用 <strong>smallestWidth 限定符屏幕适配方案</strong>,所以如果在文章中有遗漏的知识点请谅解以及补充,感谢!</p>
<hr>
<p>以下是 <strong>骚年你的屏幕适配方式该升级了!</strong> 系列文章,欢迎转发以及分享:</p>
<ul>
<li><a href="https://link.segmentfault.com/?enc=HB%2Ff9Zl6zugYRvuXQcGN%2Bg%3D%3D.2Fq%2B1kEI7fhYmSdvJUNm7C6%2BXUQVx%2FLFxkPydYvl6vl2Go1lKZ%2BJQxn17ZcQDkn9" rel="nofollow">骚年你的屏幕适配方式该升级了!(一)-今日头条适配方案</a></li>
<li><a href="https://link.segmentfault.com/?enc=q2ZKk7RpKLt9tIE0IvQOCw%3D%3D.meFkKD%2Fuw85qIcat9XPASXHe3qzSusFPutgJ0UwGCzxZN%2Btm8ZtJ%2FNeGjcY%2BZnWs" rel="nofollow">骚年你的屏幕适配方式该升级了!(二)-smallestWidth 限定符适配方案</a></li>
</ul>
<hr>
<p><strong>Hello 我叫 JessYan,如果您喜欢我的文章,可以在以下平台关注我</strong></p>
<ul>
<li>个人主页: <a href="https://link.segmentfault.com/?enc=D8EnNFBVvd%2FMRfhD7HtUnA%3D%3D.P5bVYccP6hhiK2aCRfCmjKXTOr%2B8LUKToRQbgvxXTpQ%3D" rel="nofollow">http://jessyan.me</a>
</li>
<li>GitHub: <a href="https://link.segmentfault.com/?enc=j48kjJ9NH7HSlONlLszQyA%3D%3D.Ec0%2BvxSk%2BllVWVOJwflJKmZg8yg32LeJMRGrHQAxlOoxERXREG41F5AnM3SYYXFS" rel="nofollow">https://github.com/JessYanCoding</a>
</li>
<li>掘金: <a href="https://link.segmentfault.com/?enc=ubd1x40OsJ%2F3Qay1y%2BbeUQ%3D%3D.rX%2FKDHo3GQKjtWEbRf7ORHxF%2FhtLfG2TlqHM9yKYqZs63ZT%2FMb8IVTefmXwAa4UrTFTkdGBjCltiZ9Vb5WY9FA%3D%3D" rel="nofollow">https://gold.xitu.io/user/57a...</a>
</li>
<li>简书: <a href="https://link.segmentfault.com/?enc=fwyP9MHFevEZIY%2BoPIas8A%3D%3D.MRsSAhDRBlNN3GSoMYQw7qAKy0UeQQL15ZXXewYe85ibtc8mac7TXPWRgqJn8FTO" rel="nofollow">http://www.jianshu.com/u/1d0c...</a>
</li>
<li>微博: <a href="https://link.segmentfault.com/?enc=K6WpmbiRpXUJO9DaLSZ0Cw%3D%3D.8nqCHidfgjzk%2BLUg9fqBxKTsdZ7bYa7%2Bd8ezs0iaWuM%3D" rel="nofollow">http://weibo.com/u/1786262517</a>
</li>
</ul>
<p>-- The end</p>
骚年你的屏幕适配方式该升级了!-今日头条适配方案
https://segmentfault.com/a/1190000016079558
2018-08-20T17:13:34+08:00
2018-08-20T17:13:34+08:00
JessYanCoding
https://segmentfault.com/u/jessyancoding
48
<p>原文地址: <a href="https://link.segmentfault.com/?enc=Bs%2FvieSPck5swIz7W4RceA%3D%3D.hMjK%2BOmNxSqAtPsTwtLu%2FozdMT7TVwiCfcDNvNsPFPnNQef%2FIzute%2FNc3i6R3bb6" rel="nofollow">https://www.jianshu.com/p/55e...</a></p>
<h2>前言</h2>
<p>这个月在 <strong>Android</strong> 技术圈中 <strong>屏幕适配</strong> 这个词曝光率挺高的,为什么这么说呢?因为这个月陆续有多个大佬发布了屏幕适配相关的文章,公布了自己认可的屏幕适配方案</p>
<p>上上个星期 <strong>Blankj</strong> 老师发表了一篇力挺今日头条屏幕适配方案的 <a href="https://link.segmentfault.com/?enc=qjdffDxxiDnDb2JMw%2BcIyw%3D%3D.b2nDvJutCztexnBTCJbYGHbNlewSHgF%2FuwVL8RlJ33r%2Fy8IZVe1rqkPbqK35HMgm" rel="nofollow">文章</a>,提出了很多优化的方案,并开源了相关源码</p>
<p>上个星期 <strong>拉丁吴</strong> 老师在 <strong>鸿神</strong> 的公众号上发布了一篇 <a href="https://link.segmentfault.com/?enc=FEDfSe9uUF95xKCRTeLPfw%3D%3D.zkwoHNwB7IYmS6GCg%2FWyKYpIa2k8It2qZ0FU51JXlFGgwkr%2BvENdjRDzcOgtLxKrRwWb36sBiw1uKwjSa%2B80Gw%3D%3D" rel="nofollow">文章</a>,详细描述了市面上主流的几种屏幕适配方案,并发布了他的 <strong>smallestWidth</strong> 限定符适配方案和相关源码 (其实早就发布了),文章写的很好,建议大家去看看</p>
<p>其实大家最关注的不是市面上有多少种屏幕适配方案,而是自己的项目该选择哪种屏幕适配方案,可以看出两位老师最终选择的屏幕适配方案都是不同的</p>
<p>我下面就来分析分析,我作为一个才接触这两个屏幕适配方案的吃瓜群众,我是怎么来验证这两种屏幕适配方案是否可行,以及怎样根据它们的优缺点来选择一个最适合自己项目的屏幕适配方案</p>
<p><strong>这是我推荐给大家的屏幕适配框架,本来想放到最后作为福利的,害怕大家看不到,所以就将链接放到这里,提前送给大家</strong></p>
<blockquote>Github : <a href="https://link.segmentfault.com/?enc=6jNBkNNxS9MIQU%2B%2BBK2qLw%3D%3D.VXZPAREB8czZTIjrR4Mydw%2Bc8Bzg2uSVGNDYOFImNje5w3Gl%2BQAbg6HiSvT3lHBnyRuGOUrkYf3XGgKc7DvWdw%3D%3D" rel="nofollow">您的 Star 是我坚持的动力 ✊</a>
</blockquote>
<h2>浅谈适配方案</h2>
<p>在 <strong>拉丁吴</strong> 老师的文章中谈到了两个比较经典的屏幕适配方案,在我印象中十分深刻,我想大多数兄弟都用过,在我的开发生涯里也是有很长一段时间都在用这两种屏幕适配方案</p>
<p>第一种就是宽高限定符适配,什么是宽高限定符适配呢</p>
<pre><code>├── src/main
│ ├── res
│ ├── ├──values
│ ├── ├──values-800x480
│ ├── ├──values-860x540
│ ├── ├──values-1024x600
│ ├── ├──values-1024x768
│ ├── ├──...
│ ├── ├──values-2560x1440</code></pre>
<p>就是这种,在资源文件下生成不同分辨率的资源文件,然后在布局文件中引用对应的 <strong>dimens</strong>,大家一定还有印象</p>
<p>第二种就是 <strong>鸿神</strong> 的 <strong>AndroidAutoLayout</strong></p>
<p>这两种方案都已经逐渐退出了历史的舞台,为什么想必大家都知道,不知道的建议看看 <strong>拉丁吴</strong> 老师的文章,所以这两种方案我在文章中就不在阐述了,主要讲讲现在最主流的两种屏幕适配方案,<a href="https://link.segmentfault.com/?enc=gLP50uMLwjflocRtjUrVIg%3D%3D.9tQd%2BScLV3ICyI9CE7TIdti0eQlhl7KqAxXSBp2SkuxFBD7KF1cKfl4GTOg8IN9b2lACcxEwd8NzMe2sCiDA8w%3D%3D" rel="nofollow">今日头条适配方案</a> 和 <a href="https://link.segmentfault.com/?enc=R3ODxuPCtWz%2FHkr4bBMX5w%3D%3D.sBHE6s6XE4WgzbX3LMgHwdlcptmpfCx9uFAM%2BA%2F83rgbfNMluQB6%2FyJUIbDgAJZWw1tGqAYpZfb5esdi1jyZlA%3D%3D" rel="nofollow"><strong>smallestWidth</strong> 限定符适配方案</a></p>
<p>建议大家不清楚这两个方案的先看看这两篇文章,才清楚我在讲什么,后面我要讲解它们的原理,以及验证这两种方案是否真的可行,最后对他们进行深入对比,对于他们的一些缺点给予对应的解决方案,绝对干货</p>
<h2>今日头条屏幕适配方案</h2>
<h3>原理</h3>
<p>上面已经告知,不了解这两个方案的先看看上面的两篇文章,所以这里我就假设大家已经看了上面的文章或者之前就了解过这两个方案,所以在本文中我就不再阐述 <strong>DPI</strong>、<strong>Density</strong> 以及一些比较基础的知识点,上面的文章已经阐述的够清楚了</p>
<p>今日头条屏幕适配方案的核心原理在于,根据以下公式算出 <strong>density</strong></p>
<p><strong>当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density</strong></p>
<p><strong>density</strong> 的意思就是 <strong>1 dp</strong> 占当前设备多少像素</p>
<h4>为什么要算出 <strong>density</strong>,这和屏幕适配有什么关系呢?</h4>
<pre><code>public static float applyDimension(int unit, float value,
DisplayMetrics metrics)
{
switch (unit) {
case COMPLEX_UNIT_PX:
return value;
case COMPLEX_UNIT_DIP:
return value * metrics.density;
case COMPLEX_UNIT_SP:
return value * metrics.scaledDensity;
case COMPLEX_UNIT_PT:
return value * metrics.xdpi * (1.0f/72);
case COMPLEX_UNIT_IN:
return value * metrics.xdpi;
case COMPLEX_UNIT_MM:
return value * metrics.xdpi * (1.0f/25.4f);
}
return 0;
}</code></pre>
<p>大家都知道,不管你在布局文件中填写的是什么单位,最后都会被转化为 <strong>px</strong>,系统就是通过上面的方法,将你在项目中任何地方填写的单位都转换为 <strong>px</strong> 的</p>
<p>所以我们常用的 <strong>px</strong> 转 <strong>dp</strong> 的公式 <strong>dp = px / density</strong>,就是根据上面的方法得来的,<strong>density</strong> 在公式的运算中扮演着至关重要的一步</p>
<p>要看懂下面的内容,还得明白,今日头条的适配方式,今日头条适配方案默认项目中只能以高或宽中的一个作为基准,进行适配,为什么不像 <strong>AndroidAutoLayout</strong> 一样,高以高为基准,宽以宽为基准,同时进行适配呢</p>
<p>这就引出了一个现在比较棘手的问题,大部分市面上的 <strong>Android</strong> 设备的屏幕高宽比都不一致,特别是现在大量全面屏的问世,这个问题更加严重,不同厂商推出的全面屏手机的屏幕高宽比都可能不一致</p>
<p>这时我们只以高或宽其中的一个作为基准进行适配,就会有效的避免布局在高宽比不一致的屏幕上出现变形的问题</p>
<p>明白这个后,我再来说说 <strong>density</strong>,<strong>density</strong> 在每个设备上都是固定的,<strong>DPI / 160 = density</strong>,<strong>屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度</strong></p>
<ul>
<li>设备 1,屏幕宽度为 <strong>1080px</strong>,<strong>480DPI</strong>,屏幕总 <strong>dp</strong> 宽度为 <strong>1080 / (480 / 160) = 360dp</strong>
</li>
<li>设备 2,屏幕宽度为 <strong>1440</strong>,<strong>560DPI</strong>,屏幕总 <strong>dp</strong> 宽度为 <strong>1440 / (560 / 160) = 411dp</strong>
</li>
</ul>
<p>可以看到屏幕的总 <strong>dp</strong> 宽度在不同的设备上是会变化的,但是我们在布局中填写的 <strong>dp</strong> 值却是固定不变的</p>
<p>这会导致什么呢?假设我们布局中有一个 <strong>View</strong> 的宽度为 <strong>100dp</strong>,在设备 1 中 该 <strong>View</strong> 的宽度占整个屏幕宽度的 <strong>27.8%</strong> (<strong>100 / 360 = 0.278</strong>)</p>
<p>但在设备 2 中该 <strong>View</strong> 的宽度就只能占整个屏幕宽度的 <strong>24.3%</strong> (<strong>100 / 411 = 0.243</strong>),可以看到这个 <strong>View</strong> 在像素越高的屏幕上,<strong>dp</strong> 值虽然没变,但是与屏幕的实际比例却发生了较大的变化,所以肉眼的观看效果,会越来越小,这就导致了传统的填写 <strong>dp</strong> 的屏幕适配方式产生了较大的误差</p>
<p>这时我们要想完美适配,那就必须保证这个 <strong>View</strong> 在任何分辨率的屏幕上,与屏幕的比例都是相同的</p>
<p>这时我们该怎么做呢?改变每个 <strong>View</strong> 的 <strong>dp</strong> 值?不现实,在每个设备上都要通过代码动态计算 <strong>View</strong> 的 <strong>dp</strong> 值,工作量太大</p>
<p>如果每个 <strong>View</strong> 的 <strong>dp</strong> 值是固定不变的,那我们只要保证每个设备的屏幕总 <strong>dp</strong> 宽度不变,就能保证每个 <strong>View</strong> 在所有分辨率的屏幕上与屏幕的比例都保持不变,从而完成等比例适配,并且这个屏幕总 <strong>dp</strong> 宽度如果还能保证和设计图的宽度一致的话,那我们在布局时就可以直接按照设计图上的尺寸填写 <strong>dp</strong> 值</p>
<p><strong>屏幕的总 px 宽度 / density = 屏幕的总 dp 宽度</strong></p>
<p>在这个公式中我们要保证 <strong>屏幕的总 dp 宽度</strong> 和 <strong>设计图总宽度</strong> 一致,并且在所有分辨率的屏幕上都保持不变,我们需要怎么做呢?<strong>屏幕的总 px 宽度</strong> 每个设备都不一致,这个值是肯定会变化的,这时今日头条的公式就派上用场了</p>
<p><strong>当前设备屏幕总宽度(单位为像素)/ 设计图总宽度(单位为 dp) = density</strong></p>
<p>这个公式就是把上面公式中的 <strong>屏幕的总 dp 宽度</strong> 换成 <strong>设计图总宽度</strong>,原理都是一样的,只要 <strong>density</strong> 根据不同的设备进行实时计算并作出改变,就能保证 <strong>设计图总宽度</strong> 不变,也就完成了适配</p>
<h3>验证方案可行性</h3>
<p>上面已经把原理分析的很清楚了,很多文章只是一笔带过这个公式,公式虽然很简单但我们还是想晓得这是怎么来的,所以我就反向推理了一遍,如果还是看不懂,那我只能说我尽力了,原理讲完了,那我们再来现场验证一下这个方案是否可行?</p>
<p>假设设计图总宽度为 <strong>375 dp</strong>,一个 <strong>View</strong> 在这个设计图上的尺寸是 <strong>50dp * 50dp</strong>,这个 <strong>View</strong> 的宽度占整个设计图宽度的 <strong>13.3%</strong> (<strong>50 / 375 = 0.133</strong>),那我们就来验证下在使用今日头条屏幕适配方案的情况下,这个 <strong>View</strong> 与屏幕宽度的比例在分辨率不同的设备上是否还能保持和设计图中的比例一致</p>
<h4>验证设备 1</h4>
<p>屏幕总宽度为 <strong>1080 px</strong>,根据今日头条的的公式求出 <strong>density</strong>,<strong>1080 / 375 = 2.88 (density)</strong></p>
<p>这个 <strong>50dp <em> 50dp</em></strong> 的 <strong>View</strong>,系统最后会将高宽都换算成 <strong>px</strong>,<strong>50dp 2.88 = 144 px</strong> (根据公式 <strong>dp * density = px</strong>)</p>
<p><strong>144 / 1080 = 0.133</strong>,<strong>View</strong> 实际宽度与 <strong>屏幕总宽度</strong> 的比例和 <strong>View</strong> 在设计图中的比例一致 (<strong>50 / 375 = 0.133</strong>),所以完成了等比例缩放</p>
<p>某些设备总宽度为 <strong>1080 px</strong>,但是 <strong>DPI</strong> 可能不同,是否会对今日头条适配方案产生影响?其实这个方案根本没有根据 <strong>DPI</strong> 求出 <strong>density</strong>,是根据自己的公式求出的 <strong>density</strong>,所以这对今日头条的方案没有影响</p>
<p>上面只能确定在所有屏幕总宽度为 <strong>1080 px</strong> 的设备上能完成等比例适配,那我们再来试试其他分辨率的设备</p>
<h4>验证设备 2</h4>
<p>屏幕总宽度为 <strong>1440 px</strong>,根据今日头条的的公式求出 <strong>density</strong>,<strong>1440 / 375 = 3.84 (density)</strong></p>
<p>这个 <strong>50dp <em> 50dp</em></strong> 的 <strong>View</strong>,系统最后会将高宽都换算成 <strong>px</strong>,<strong>50dp 3.84 = 192 px</strong> (根据公式 <strong>dp * density = px</strong>)</p>
<p><strong>192 / 1440 = 0.133</strong>,<strong>View</strong> 实际宽度与 <strong>屏幕总宽度</strong> 的比例和 <strong>View</strong> 在设计图中的比例一致 (<strong>50 / 375 = 0.133</strong>),所以也完成了等比例缩放</p>
<p>两个不同分辨率的设备都完成了等比例缩放,证明今日头条屏幕适配方案在不同分辨率的设备上都是有效的,如果大家还心存疑虑,可以再试试其他分辨率的设备,其实到最后得出的比例不会有任何偏差, 都是 <strong>0.133</strong></p>
<h3>优点</h3>
<ol>
<li>使用成本非常低,操作非常简单,使用该方案后在页面布局时不需要额外的代码和操作,这点可以说完虐其他屏幕适配方案</li>
<li>侵入性非常低,该方案和项目完全解耦,在项目布局时不会依赖哪怕一行该方案的代码,而且使用的还是 <strong>Android</strong> 官方的 <strong>API</strong>,意味着当你遇到什么问题无法解决,想切换为其他屏幕适配方案时,基本不需要更改之前的代码,整个切换过程几乎在瞬间完成,会少很多麻烦,节约很多时间,试错成本接近于 0</li>
<li>可适配三方库的控件和系统的控件(不止是是 <strong>Activity</strong> 和 <strong>Fragment</strong>,<strong>Dialog</strong>、<strong>Toast</strong> 等所有系统控件都可以适配),由于修改的 <strong>density</strong> 在整个项目中是全局的,所以只要一次修改,项目中的所有地方都会受益</li>
<li>不会有任何性能的损耗</li>
</ol>
<h3>缺点</h3>
<p>暂时没发现其他什么很明显的缺点,已知的缺点有一个,那就是第三个优点,它既是这个方案的优点也同样是缺点,但是就这一个缺点也是非常致命的</p>
<p>只需要修改一次 <strong>density</strong>,项目中的所有地方都会自动适配,这个看似解放了双手,减少了很多操作,但是实际上反应了一个缺点,那就是只能一刀切的将整个项目进行适配,但适配范围是不可控的</p>
<p>这样不是很好吗?这样本来是很好的,但是应用到这个方案是就不好了,因为我上面的原理也分析了,这个方案依赖于设计图尺寸,但是项目中的系统控件、三方库控件、等非我们项目自身设计的控件,它们的设计图尺寸并不会和我们项目自身的设计图尺寸一样</p>
<p>当这个适配方案不分类型,将所有控件都强行使用我们项目自身的设计图尺寸进行适配时,这时就会出现问题,<strong>当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重</strong></p>
<h4>举个栗子</h4>
<p>假设一个三方库的 <strong>View</strong>,作者在设计时,把它设计为 <strong>100dp * 100dp</strong>,设计图的最大宽度为 <strong>1000dp</strong>,这个 <strong>View</strong> 在设计图中的比例是 <strong>100 / 1000 = 0.1</strong>,意思是这个 <strong>View</strong> 的宽度在设计图中占整个宽度的 <strong>10%</strong>,如果我们要完成等比例适配,那这个三方库 <strong>View</strong> 在所有的设备上与屏幕的总宽度的比例,都必须保持在 <strong>10%</strong></p>
<p>这时在一个使用今日头条屏幕适配方案的项目上,设置的设计图最大宽度如果是 <strong>1000dp</strong>,那这个三方库 <strong>View</strong>,与项目自身都可以完美的适配,但当我们项目自身的设计图最大宽度不是 <strong>1000dp</strong>,是 <strong>500dp</strong> 时,<strong>100 / 500 = 0.2</strong>,可以看到,比例发生了较大的变化,从 <strong>10%</strong> 上升为 <strong>20%</strong>,明显这个三方库 <strong>View</strong> 高于作者的预期,比之前更大了</p>
<p>这就是两个设计图尺寸不一致导致的非常严重的问题,当两个设计图尺寸差距越大,那适配的效果也就天差万别了</p>
<h4>解决方案</h4>
<h5>方案 1</h5>
<p>调整设计图尺寸,因为三方库可能是远程依赖的,无法修改源码,也就无法让三方库来适应我们项目的设计图尺寸,所以只有我们自身作出修改,去适应三方库的设计图尺寸,我们将项目自身的设计图尺寸修改为这个三方库的设计图尺寸,就能完成项目自身和三方库的适配</p>
<p><strong>这时项目的设计图尺寸修改了,所以项目布局文件中的 dp 值,也应该按照修改的设计图尺寸,按比例增减,保持与之前设计图中的比例不变</strong></p>
<p>但是如果为了适配一个三方库修改整个项目的设计图尺寸,是非常不值得的,所以这个方案支持以 <strong>Activity</strong> 为单位修改设计图尺寸,相当于每个 <strong>Activity</strong> 都可以自定义设计图尺寸,因为有些 <strong>Activity</strong> 不会使用三方库 <strong>View</strong>,也就不需要自定义尺寸,所以每个 <strong>Activity</strong> 都有控制权的话,这也是最灵活的</p>
<p>但这也有个问题,当一个 <strong>Activity</strong> 使用了多个设计图尺寸不一样的三方库 <strong>View</strong>,就会同样出现上面的问题,这也就只有把设计图改为与几个三方库比较折中的尺寸,才能勉强缓解这个问题</p>
<h5>方案 2</h5>
<p>第二个方案是最简单的,也是按 <strong>Activity</strong> 为单位,取消当前 <strong>Activity</strong> 的适配效果,改用其他的适配方案</p>
<h2>使用中的问题</h2>
<p>有些文章中提到了今日头条屏幕适配方案可以将设计图尺寸填写成以 <strong>px</strong> 为单位的宽度和高度,这样我们在布局文件中,也就能直接填写设计图上标注的 <strong>px</strong> 值,省掉了将 <strong>px</strong> 换算为 <strong>dp</strong> 的时间 (大部分公司的设计图都只标注 <strong>px</strong> 值),而且照样能完美适配</p>
<p>但是我建议大家千万不要这样做,还是老老实实的以 <strong>dp</strong> 为单位填写 <strong>dp</strong> 值,为什么呢?</p>
<p>直接填写 <strong>px</strong> 虽然刚开始布局的时候很爽,但是这个坑就已经埋上了,会让你后面很爽,有哪些坑?</p>
<h3>第一个坑</h3>
<p>这样无疑于使项目强耦合于这个方案,当你遇到无法解决的问题想切换为其他屏幕适配方案的时候,<strong>layout</strong> 文件里曾经填写的 <strong>px</strong> 值都会作为 <strong>dp</strong></p>
<p>比如你的设计图实际宽度为 <strong>1080px</strong>,你不换算为 <strong>360dp (1080 / 3 = 360)</strong>,却直接将 <strong>1080px</strong> 作为这个方案的设计图尺寸,那你在 <strong>layout</strong> 文件中,填写的也都是设计图上标注的 <strong>px</strong> 值,但是单位却是 <strong>dp</strong></p>
<p>一个在设计图上 <strong>300px * 300px</strong> 的 <strong>View</strong>,你可以直接在 <strong>layout</strong> 文件中填写为 <strong>300dp</strong>,而由于这个方案可以动态改变 <strong>density</strong> 的原因还是可以做到等比例适配,非常爽!</p>
<p>但你不要忘了,这样你就强耦合于这个方案了,因为当你不使用这个方案时,<strong>density</strong> 是不可变的!</p>
<h4>举个栗子</h4>
<p>使用这个方案时,在屏幕宽度为 <strong>1080px</strong> 的设备上,将设计图宽度直接填写为 <strong>1080</strong>,根据今日头条公式</p>
<p><strong>当前设备屏幕总宽度 / 设计图总宽度 = density</strong></p>
<p>这时得出 <strong>density</strong> 为 1 (<strong>1080 / 1080 = 1</strong>),所以你在 <strong>layout</strong> 文件中你填写的 <strong>300dp</strong> 最后转换为 <strong>px</strong> 也是 <strong>300px</strong> (<strong>300dp <em> 1 = 300px</em></strong> 根据公式 <strong>dp density = px</strong>)</p>
<p>在这个方案的帮助下非常完美,和设计图一模一样完成了适配</p>
<p>但当你不使用这个方案时,<strong>density</strong> 的换算公式就变为官方的 <strong>DPI / 160 = density</strong>,<br>在这个屏幕宽度为 <strong>1080px</strong>,<strong>480dpi</strong> 的设备上,<strong>density</strong> 就固定为 3 (<strong>480 / 160 = 3</strong>)</p>
<p>这时再来看看你之前在 <strong>layout</strong> 文件中填写的 <strong>dp</strong>,换算成 <strong>px</strong> 为 <strong>900 px</strong> (<strong>300dp <em> 3 = 900px</em></strong> 根据公式 <strong>dp density = px</strong>)</p>
<p>原本在在设计图上为 <strong>300px</strong> 的 <strong>View</strong>,这时却达到了惊人的 <strong>900px</strong>,3倍的差距,恭喜你,你已经强耦合于这个方案了,你要不所有 <strong>layout</strong> 文件都改一遍,要不继续使用这个方案</p>
<h3>第二个坑</h3>
<p>第二个坑其实就是刚刚在上面说的今日头条适配方案的缺点,<strong>当某个系统控件或三方库控件的设计图尺寸和和我们项目自身的设计图尺寸差距非常大时,这个问题就越严重</strong></p>
<p>你如果直接填写以 <strong>px</strong> 为设计图的尺寸,这不用想,肯定和所有的三方库以及系统控件的设计图尺寸都不一样,而且差距都非常之大,至少两三倍的差距,这时你在当前页面弹个 <strong>Toast</strong> 就可以明显看到,比之前小很多,可以说是天差万别,用其他三方库 <strong>View</strong>,也是一样的,会小很多</p>
<p>因为你以 <strong>px</strong> 为单位填写设计图尺寸,人家却用的 <strong>dp</strong>,差距能不大吗,你如果老老实实用 <strong>dp</strong>,哪怕三方库的设计图尺寸和你项目自身的设计图尺寸不一样,那也差距不大,小到一定程度,基本都不用调整,可以忽略不计,而且很多三方库的设计图尺寸其实也都是那几个大众尺寸,很大可能和你项目自身的设计图尺寸一样</p>
<h2>总结</h2>
<p>可以看到我讲的非常详细,可以说比今日头条官方以及任何博客写的都清楚,从原理到优缺点再到解决方案一应俱全,因为篇幅有限,如果我还想把 <strong>smallestWidth</strong> 限定符适配方案写的这么详细,那估计这篇文章得有一万字了</p>
<p>所以我把这次的屏幕适配文章归位一个系列,一共分为三篇,第一篇详细的讲 <strong>今日头条屏幕适配方案</strong>,第二篇详细的讲 <strong>smallestWidth 限定符适配方案</strong>,第三篇详细讲两个方案的深入对比以及如何选择,并发布我根据 <strong>今日头条屏幕适配方案</strong> 优化的屏幕适配框架 <a href="https://link.segmentfault.com/?enc=jqupeG5n4kUyZqxoOh3oxw%3D%3D.v6VIliRjmsvq9N5ZTIViNf%2FxH8HJvqz6W7NcCorQFKRKuobj6ATTQGLRLUGxshQQxYJUnX7k5FkI8uOP801Tdg%3D%3D" rel="nofollow"><strong>AndroidAutoSize</strong></a></p>
<p><strong>今日头条屏幕适配方案</strong> 官方公布的核心源码只有 30 行不到,但我这个框架的源码有 <strong>1500</strong> 行以上,在保留原有特性的情况下增加了不少功能和特性,功能增加了不少,但是使用上却变简单了</p>
<pre><code class="xml"><manifest>
<application>
<meta-data
android:name="design_width_in_dp"
android:value="360"/>
<meta-data
android:name="design_height_in_dp"
android:value="640"/>
</application>
</manifest></code></pre>
<p>只要这一步填写了设计图的高宽以 <strong>dp</strong> 为单位,你什么都不做,框架就开始适配了</p>
<p>大家可以提前看看我是怎么封装和优化的,我后面的第三篇文章会给出这个框架的原理分析,敬请期待</p>
<hr>
<p><strong>Hello 我叫Jessyan,如果您喜欢我的文章,可以在以下平台关注我</strong></p>
<ul>
<li>GitHub: <a href="https://link.segmentfault.com/?enc=oIw6A3WobPuYzMZp9bWYqQ%3D%3D.XCxP1CBuUDAqPZbn%2FuYpuOc9%2BGu%2BiBRnwzBiRl6pRuh6Hf%2BYcVX2zMx5j2xw3Ehg" rel="nofollow">https://github.com/JessYanCoding</a>
</li>
<li>掘金: <a href="https://link.segmentfault.com/?enc=evLFkGVOcPdkftAmhejqxw%3D%3D.feZfW1spnc4jFMT6ZsXy4IGSddMn56%2BmGwpb0EBbH5XhVnFAmPQQWhfMb9wAxpSI1EWCigIBjshnODCPAAJIQw%3D%3D" rel="nofollow">https://gold.xitu.io/user/57a...</a>
</li>
<li>简书: <a href="https://link.segmentfault.com/?enc=Pr75DIokbbxWXVF%2Fq%2FQFfQ%3D%3D.bh9PKwZUxiZzxsKCDqaRCgso26id%2BorsiPjjLZC66qS7Q14V51sVwUZnLbcpodxx" rel="nofollow">http://www.jianshu.com/u/1d0c...</a>
</li>
<li>微博: <a href="https://link.segmentfault.com/?enc=3fhSce8mKYROiNW1Acd11A%3D%3D.hwRhhf7LGeFGUwVT4zC%2BtsKdi3kBfDWL7fFjLUxm%2FdQ%3D" rel="nofollow">http://weibo.com/u/1786262517</a>
</li>
</ul>
<p>-- The end</p>
解决Retrofit多BaseUrl及运行时动态改变BaseUrl(二)
https://segmentfault.com/a/1190000015572249
2018-07-09T11:45:03+08:00
2018-07-09T11:45:03+08:00
JessYanCoding
https://segmentfault.com/u/jessyancoding
6
<p>原文地址: <a href="https://link.segmentfault.com/?enc=mo9MYzSrTjn3bjIAytWkrQ%3D%3D.K4IkVuceHWcimrGEqwrKektZctCTYzyrW5w5YfXlHWBHsyuCP2pb1Vl8RIPvEGvQ" rel="nofollow">https://www.jianshu.com/p/35a...</a></p>
<h2>前言</h2>
<p>我在之前的文章 <a href="https://link.segmentfault.com/?enc=Pb%2BW9WGzabpcRwEV6thYJw%3D%3D.W6qfQFS5PhOMMn%2BUhtYSzdn3N6ukh0G5tEzQ5esX2VJ711XVVdYuWWXlomJnBh19" rel="nofollow">《解决Retrofit多BaseUrl及运行时动态改变BaseUrl》</a> 中,介绍了市面上能够解决此类问题的 4 个常见的解决方案,并开源了自己经过优化后的解决方案 <a href="https://link.segmentfault.com/?enc=0U6PiHp%2BpaYyBPRuEDWm%2Bw%3D%3D.vS9ps926dEm44DMwxac7J5Tt%2FYuv75VypgG%2BlzS38wLC2bdrefrtP9Bn7bmKgSTa%2Bsu%2BoYlqku4O3NfuZtOMpg%3D%3D" rel="nofollow">RetrofitUrlManager</a>,现在再为大家带来此系列的第二篇文章,这篇文章主要介绍 <a href="https://link.segmentfault.com/?enc=TQpmKlZWtapvPn%2BlV88BJA%3D%3D.OclKJh%2F4hJiNnHW1edHo%2F8lpuYzaXYtRb68FQmCzGOBRkUuFHvTqRTjQJAQiJRM1mRD65pRzneh2iTIbSxHJ8w%3D%3D" rel="nofollow">RetrofitUrlManager</a> 针对 <strong>BaseUrl</strong> 替换逻辑的重大升级,因为这个升级对于 <a href="https://link.segmentfault.com/?enc=cghvDZoaewVQvXrt%2FT9%2BkQ%3D%3D.bls0yFwI1Y6XRhbsdNq8UbjNUllqLauxRuNrGkl%2BoPJgDU47weCJcuWgDp9lCidpRZuf7%2BegsNtnpp0RFVXDpQ%3D%3D" rel="nofollow">RetrofitUrlManager</a> 足够重要,将使 <a href="https://link.segmentfault.com/?enc=gJjULCWuBpF5YEyfod76rQ%3D%3D.UFVpUE32jvlM8y6TVG6Vb1PRrnizAPxqI11ESE%2BcHjMuP1YgQn6yUN34C3ZFGd2jjF6JEQFirlcl5jQJa6376w%3D%3D" rel="nofollow">RetrofitUrlManager</a> 能够从容应对更多复杂的需求,所以单独写一篇文章让更多的人能够知道</p>
<blockquote>Github : <a href="https://link.segmentfault.com/?enc=DG3ybyubfvV00WE9wTgbrg%3D%3D.olOzO99QxKjl5e3n3xLQXaCnLdrlO672Mz9EE5dvH7sWX%2BYz8cRVckXrezVtz21wTsgscEGf0FOXoZnFEYd1QQ%3D%3D" rel="nofollow">您的 Star 是我坚持的动力 ✊</a>
</blockquote>
<h2>为什么不使用多 Retrofit 实例的方案?</h2>
<p>在上篇文章 <a href="https://link.segmentfault.com/?enc=NIhIoRtrGFqvItUeg3Z8Zg%3D%3D.zCMrom4YTqP1uxnXEHxgRF48ydp82cyuoQQJuMxYDfpGZGkJOvsgfJKxvLiPIrJ1" rel="nofollow">《解决Retrofit多BaseUrl及运行时动态改变BaseUrl》</a> 中,4 种方案的特点和不足我都描述的很清楚,建议没看过这篇文章的可以去看看这篇文章,扩宽知识面,在后面的时间里经常有人问我为什么不使用多 <strong>Retrofit</strong> 实例的方案,多个 <strong>Retrofit</strong> 实例看起来并不会占用多少资源啊?</p>
<p>在回答之前为了让看这篇文章的人能了解我在说什么,所以我再粘贴下 <a href="https://link.segmentfault.com/?enc=hdpqAFhFZuOhim46udSypQ%3D%3D.tain6OZmHvhHcXVxtXfQqG2YbTEBn229aSe2cgR4d2dxepLdrPse%2F9IjQL30QzYk" rel="nofollow">上篇文章</a> 中关于这个方案的部分描述</p>
<p><code>民间常用解决方案:</code> <br>`之前也看过很多开源的聚合类 App 源码, 像一些整合 知乎、 豆瓣、 Gank 等多个平台数据的 App, 因为各自平台的域名不同, 所以大多数这类 App 会给每个平台都各自创建一个 Retrofit 对象, 即不同的 BaseUrl 使用不同的 Retrofit 对象来创建 ApiService 进行请求, 这样只要新增一个不同的 BaseUrl, 那就需要重新创建一个新的 Retrofit 对象<br>`</p>
<p>我在这篇文章中重新回答下这个问题,为每个不同的 <strong>BaseUrl</strong> 都创建一个其他配置属性都一模一样的 <strong>Retrofit</strong> 实例不止会造成资源的浪费,还会造成接口管理成本的增加,这个才是最重要的一点, 举个例</p>
<p>我们平时项目中所有的 <strong>ApiService</strong> 都是使用同一个 <strong>Retrofit</strong> 实例的 <strong>Retrofit#create(ApiService)</strong> 方法进行实例化后开始接口的请求</p>
<p>但是当项目中出现多个 <strong>Retrofit</strong> 实例后,我们在开发中不光要区分哪些接口使用哪个 <strong>ApiService</strong>,还要区分哪些 <strong>ApiService</strong> 需要使用哪个 <strong>Retrofit</strong> 实例进行实例化,如果 <strong>ApiService</strong> 使用错误的 <strong>Retrofit</strong> 实例进行实例化,那这个 <strong>ApiService</strong> 的所有接口请求都注定完全失败</p>
<p>越复杂的项目,开发人数越多的项目,出错的风险就越大,并且扩展性也在大打折扣,后面一有变更将会十分痛苦,随着项目中接口的增加,以及 <strong>Retrofit</strong> 实例的增加 (<strong>BaseUrl</strong> 的增加),这个管理成本会成几何倍的增加</p>
<p>使用多 <strong>Retrofit</strong> 实例的方案前期投入成本过高,可能会影响之前项目管理接口的方式,某些封装过 <strong>Retrofit</strong> 的项目,也可能需要大改,对于老项目的接入不利,而使用 <a href="https://link.segmentfault.com/?enc=4v8dIt%2BN%2FyohTY7IzKH27A%3D%3D.8bjUXO%2B4y9kQR95nw0xWVGORolt90UJSaDefrG11VdSYCWI4zoGJwelh20rqngnHWvskUxct5L8twJCsViNgSw%3D%3D" rel="nofollow">RetrofitUrlManager</a> 不仅可以满足多 <strong>BaseUrl</strong> 及运行时动态改变 <strong>BaseUrl</strong> 的需求,还具有热插拔以及低侵入性的特点,在使用过程中将不会影响到之前的接口管理方式和使用方式,还具有极强的扩展性,可应对后面陆续增加的极其复杂的 <strong>BaseUrl</strong> 替换需求</p>
<h2>升级之前的 RetrofitUrlManager 的问题</h2>
<p>此次升级之前的 <strong>RetrofitUrlManager</strong> 版本,只是将 <a href="https://link.segmentfault.com/?enc=U8go9DjZlnzT9lWuk0GdnQ%3D%3D.oOhcxex7VI3bpr1pLUQDY2bfHQZJBWlWk3D2W3Iwy69XdS%2BPWvDULS6tyPdsZtLy" rel="nofollow">上篇文章</a> 的思想完全实现,有了整个框架的基础,但是在动态替换 <strong>BaseUrl</strong> 方面还不够强大,最被大家吐槽的就是只能替换 <strong>BaseUrl</strong> 的域名</p>
<p>比如一个需要替换 <strong>BaseUrl</strong> 的 <strong>Url</strong> 地址为 <code>"https:www.google.com/api/v2"</code>,其中 <code>"https:www.google.com/api"</code> 是我们传给 <strong>Retrofit</strong> 的 <strong>BaseUrl</strong>,这时我们使用 <strong>RetrofitUrlManager</strong> 框架,想把 <strong>BaseUrl</strong> 替换成 <code>"https:www.github.com"</code>,我们期望的替换后的 <strong>URL</strong> 地址是 <code>"https:www.github.com/v2"</code>,但使用框架替换后的实际 <strong>URL</strong> 地址是 <code>"https:www.github.com/api/v2"</code>, <code>"/api"</code> 作为 <strong>BaseUrl</strong> 的一部分并没有被新的 <strong>BaseUrl</strong> 替换掉,只是替换了 <strong>BaseUrl</strong> 中的域名</p>
<h2>RetrofitUrlManager 是如何改善的</h2>
<p>改善之前先要先分析为什么会这样?因为 <strong>RetrofitUrlManager</strong> 框架在拦截器中拦截到的 <strong>URL</strong> 地址是 <strong>Retrofit</strong> 已经把 <strong>BaseUrl</strong> 和接口注解中的相对路径合并后得到的最终路径地址,所以框架并不知道您传给 <strong>Retrofit</strong> 的 <strong>BaseUrl</strong> 除了域名外还包含后面的 <code>"/api"</code>,框架不知道 <strong>BaseUrl</strong> 的具体值,所以框架只会默认所有的 <strong>BaseUrl</strong> 都只含有域名,所以也就只能替换域名</p>
<h2>高级模式</h2>
<p>想要解决此类问题也很简单,告诉 <strong>RetrofitUrlManager</strong> 框架您传给 <strong>Retrofit</strong> 的 <strong>BaseUrl</strong> 具体值即可,所以框架升级后增加了 <strong>RetrofitUrlManager#startAdvancedModel(String)</strong> 方法,在 <strong>App</strong> 初始化时将您传给 <strong>Retrofit</strong> 的 <strong>BaseUrl</strong> 同样传给此方法,即可开启高级模式,高级模式即可替换拥有多个 <strong>pathSegments</strong> 的 <strong>BaseUrl</strong>,不再局限于只能替换域名</p>
<h3>什么是 pathSegment?</h3>
<p>在 <code>"https://www.github.com/wiki/part?name=jess"</code> 中,其中的 <code>"/wiki"</code> 和 <code>"/part"</code> 就是 <strong>pathSegment</strong>, <strong>PathSize</strong> 就是 <strong>pathSegment</strong> 的个数</p>
<p>这个 <strong>URL</strong> 地址的 <strong>PathSize</strong> 就是 2, 第一个 <strong>pathSegment</strong> 是 <code>"/wiki"</code>,第二个 <strong>pathSegment</strong> 是 <code>"/part"</code>,可以粗略的理解为域名后面跟了几个 <code>"/"</code> <strong>PathSize</strong> 就是几</p>
<h3>高级模式的替换规则</h3>
<ol>
<li>旧 <strong>URL</strong> 地址为 <code>"https://www.github.com/wiki/part"</code>,您在 <strong>App</strong> 初始化时传入 <strong>RetrofitUrlManager#startAdvancedModel(String)</strong> 的 <strong>BaseUrl</strong> 为 <code>"https://www.github.com/wiki"</code>,您想替换成的 <strong>BaseUrl</strong> 地址是 <code>"https://www.google.com/api"</code>,经过框架替换后生成的最终 <strong>URL</strong> 地址为 <code>"http://www.google.com/api/part"</code>
</li>
<li>旧 <strong>URL</strong> 地址为 <code>"https://www.github.com/wiki/part"</code>,您在 <strong>App</strong> 初始化时传入 <strong>RetrofitUrlManager#startAdvancedModel(String)</strong> 的 <strong>BaseUrl</strong> 为 <code>"https://www.github.com/wiki"</code>,您想替换成的 <strong>BaseUrl</strong> 地址是 <code>"https://www.google.com"</code>,经过框架替换后生成的最终 <strong>URL</strong> 地址为 <code>"http://www.google.com/part"</code>
</li>
<li>旧 <strong>URL</strong> 地址为 <code>"https://www.github.com/wiki/part"</code>, 您在 <strong>App</strong> 初始化时传入 <strong>RetrofitUrlManager#startAdvancedModel(String)</strong> 的 <strong>BaseUrl</strong> 为 <code>"https://www.github.com"</code>,您想替换成的 <strong>BaseUrl</strong> 地址是 <code>"https://www.google.com/api"</code>,经过框架替换后生成的最终 <strong>URL</strong> 地址为 <code>"http://www.google.com/api/wiki/part"</code>
</li>
</ol>
<h2>超级模式</h2>
<p>超级模式是高级模式的加强版,优先级高于高级模式,按理说高级模式就能满足开发中的大部分需求,那什么又是超级模式呢?那就要先来说说高级模式了</p>
<h3>高级模式的原理</h3>
<p>在高级模式中您需要在 <strong>App</strong> 初始化时将您传给 <strong>Retrofit</strong> 的 <strong>BaseUrl</strong> 同样传给 <strong>RetrofitUrlManager#startAdvancedModel(String)</strong> 一份,用以开启高级模式,成功开启高级模式后,这个传给 <strong>RetrofitUrlManager#startAdvancedModel(String)</strong> 的 <strong>BaseUrl</strong> 就会作为框架替换 <strong>BaseUrl</strong> 的基准</p>
<p>什么叫作基准呢? 用此 <strong>BaseUrl</strong> 开启高级模式,并不意味着框架就只能替换 <strong>域名</strong> 为 <code>"www.github.com"</code> 前两个 <strong>pathSegments</strong> 为 <code>"/wiki/part"</code> 的 <strong>URL</strong>,只要拥有域名以及大于或等于两个 <strong>pathSegments</strong> 的 <strong>URL</strong> 都可以被框架替换,因此高级模式只会保存传入 <strong>RetrofitUrlManager#startAdvancedModel(String)</strong> 的 <strong>BaseUrl</strong> 的格式 (保存 <strong>pathSegments</strong> 的个数),并不是保存具体的值</p>
<h3>高级模式的局限</h3>
<p>如果传给 <strong>RetrofitUrlManager#startAdvancedModel(String)</strong> 的 <strong>BaseUrl</strong> 为 <code>"https://www.github.com/wiki/part"</code> (<strong>PathSize</strong> = 2),框架就会将项目中所有 <strong>URL</strong> 中的 <strong>域名</strong> 以及 <strong>域名</strong> 后面的前两个 <strong>pathSegments</strong> 作为可被替换的 <strong>BaseUrl</strong> 整体,意味着框架只会将 <strong>URL</strong> 中的 <strong>域名</strong> 以及前两个 <strong>pathSegments</strong> 剪切并替换为您期望的 <strong>BaseUrl</strong></p>
<p>这时服务器突然作出调整,项目中的一部分 <strong>URL</strong> 只需要将 <code>"https://www.github.com/wiki"</code> (<strong>PathSize</strong> = 1) 替换掉, 第二个 <strong>pathSegment</strong> <code>"/part"</code> 不再作为 <strong>BaseUrl</strong> 的一部分,不能被替换掉,必须要保留下来</p>
<p>这时项目中就出现了多个需要被替换的 <strong>BaseUrl</strong> 格式 (<strong>PathSize</strong> 不同),有些 <strong>URL</strong> 只需要替换 <strong>域名</strong> 以及前两个 <strong>pathSegments</strong>,有些又只需要替换 <strong>域名</strong> 以及前一个 <strong>pathSegments</strong>,但是在开启高级模式时,只保存了一个 <strong>BaseUrl</strong> 的格式,这时使用高级模式实现此需求就比较棘手</p>
<p>这个需求是一个比较变态的需求,可能很多人遇不上,但是我想让您知道当您遇上了也不要怕,因为 <strong>RetrofitUrlManager</strong> 的超级模式已经帮您考虑周全</p>
<h3>超级模式的用法</h3>
<p>超级模式与高级模式不同的是,开启超级模式并不需要调用 <strong>API</strong>,只须要在需要开启超级模式的 <strong>Url</strong> 尾部加上 <strong>RetrofitUrlManager#IDENTIFICATION_PATH_SIZE (#baseurl_path_size=) + PathSize</strong>,这样就明确的告诉了框架,在这个 <strong>URL</strong> 中需要被替换的 <strong>BaseUrl</strong> 含有几个 <strong>pathSegments</strong>,相当于每个 <strong>URL</strong> 都可以指定自己需要被替换的 <strong>BaseUrl</strong> 的格式,这样就比高级模式只能在 <strong>App</strong> 初始化时,指定一个全局的 <strong>BaseUrl</strong> 格式灵活得多,如果当您开启高级模式的同时也开启了超级模式,由于超级模式的优先级高于高级模式,所以只会执行超级模式</p>
<h3>超级模式的替换规则</h3>
<ol>
<li>旧 <strong>URL</strong> 地址为 <code>"https://www.github.com/wiki/part#baseurl_path_size=1"</code>,<code>"#baseurl_path_size=1"</code> 表示其中 <strong>BaseUrl</strong> 为 <code>"https://www.github.com/wiki"</code>,您想替换成的 <strong>BaseUrl</strong> 地址是 <code>"https://www.google.com/api"</code>,经过框架替换后生成的最终 <strong>URL</strong> 地址为 <code>"http://www.google.com/api/part"</code>
</li>
<li>旧 <strong>URL</strong> 地址为 <code>"https://www.github.com/wiki/part#baseurl_path_size=1"</code>,<code>"#baseurl_path_size=1"</code> 表示其中 <strong>BaseUrl</strong> 为 <code>"https://www.github.com/wiki"</code>,您想替换成的 <strong>BaseUrl</strong> 地址是 <code>"https://www.google.com"</code>,经过框架替换后生成的最终 <strong>URL</strong> 地址为 <code>"http://www.google.com/part"</code>
</li>
<li>旧 <strong>URL</strong> 地址为 <code>"https://www.github.com/wiki/part#baseurl_path_size=0"</code>,<code>"#baseurl_path_size=0"</code> 表示其中 <strong>BaseUrl</strong> 为 <code>"https://www.github.com"</code>,您想替换成的 <strong>BaseUrl</strong> 地址是 <code>"https://www.google.com/api"</code>,经过框架替换后生成的最终 <strong>URL</strong> 地址为 <code>"http://www.google.com/api/wiki/part"</code>
</li>
<li>旧 <strong>URL</strong> 地址为 <code>"https://www.github.com/wiki/part/issues/1#baseurl_path_size=3"</code>,<code>"#baseurl_path_size=3"</code> 表示其中 <strong>BaseUrl</strong> 为 <code>"https://www.github.com/wiki/part/issues"</code>,您想替换成的 <strong>BaseUrl</strong> 地址是 <code>"https://www.google.com/api"</code>,经过框架替换后生成的最终 <strong>URL</strong> 地址为 <code>"http://www.google.com/api/1"</code>
</li>
</ol>
<h2>三种模式比较</h2>
<p>在升级之前,框架就只有一个默认的普通模式 (只能替换域名),在升级之后新增了 <strong>高级模式</strong> 和 <strong>超级模式</strong>,这两个模式让框架变得更加强大,在上面的内容中也详细的介绍了这两个模式,现在就来总结下这三个模式,让大家能够按照自己的需求选择出最适合的模式</p>
<h3>替换 BaseUrl 的自由程度 (可扩展性)</h3>
<h4>普通模式 < 高级模式 < 超级模式</h4>
<ul>
<li>普通模式: 只能替换域名</li>
<li>高级模式: 只能替换在 <strong>RetrofitUrlManager#startAdvancedModel(String)</strong> 中传入的固定 <strong>BaseUrl</strong> 格式</li>
<li>超级模式: 每个 <strong>URL</strong> 都可以随意指定可被替换的 <strong>BaseUrl</strong> 格式</li>
</ul>
<h3>使用上的复杂程度</h3>
<h4>普通模式 < 高级模式 < 超级模式</h4>
<ul>
<li>普通模式: 无需做过多配置</li>
<li>高级模式: 在 <strong>App</strong> 初始化时调用一次 <strong>RetrofitUrlManager#startAdvancedModel(String)</strong> 即可</li>
<li>超级模式: 每个需要开启超级模式的 <strong>URL</strong> 尾部都需要加入 <strong>RetrofitUrlManager#IDENTIFICATION_PATH_SIZE (#baseurl_path_size=) + PathSize</strong>
</li>
</ul>
<h2>总结</h2>
<p>由此可见,自由度越强,使用上也越复杂,所以可以根据自己的需求选择对应的模式,并且也可以根据需求的变化随意升级或降级这三种模式</p>
<p>这次更新让 <a href="https://link.segmentfault.com/?enc=0lfnw79dB0DHwexAz94SIA%3D%3D.rhWr03%2FMnknECUfnNcPGgLOI4I%2F61MOLT6%2FyaQH1HvHMVmhbPHbnFZOh1Him1WobZcGv3Tv4qvOhsr8jGxdSjg%3D%3D" rel="nofollow">RetrofitUrlManager</a> 的能力提升了一个档次,足以应对各种复杂的 <strong>BaseUrl</strong> 替换需求,正因为 <a href="https://link.segmentfault.com/?enc=lQBsijrwwmLfyPzdzV5rAA%3D%3D.po2GObsIC0V8%2BmUhYPGQz6HvObb2ajJne25IqurlHmzijjxj40ox9WknhmJ51MnBk4pDkadqtIbDvP4Ex5T39Q%3D%3D" rel="nofollow">RetrofitUrlManager</a> 极强的扩展性,现在甚至可以做到,让服务器可以通过远程动态控制项目中的多个 <strong>BaseUrl</strong></p>
<p>如果还有什么问题或者需求可以给我提 <strong>Issues</strong>,如果 <a href="https://link.segmentfault.com/?enc=fjWxDKEqOzIcvgQvGv8eLA%3D%3D.6C7JEHxFjgl32oy9iGjG0eYXJ2a1EBKm7AIuoDjI%2FKfPQ9KaTnKdAb1VKIjnSeha3Rl8pQZN2JOuT8RHhMTtyA%3D%3D" rel="nofollow">RetrofitUrlManager</a> 能够给您带来实质的帮助,也请不要吝啬您的 <strong>Star</strong></p>
<hr>
<p><strong>Hello 我叫Jessyan,如果您喜欢我的文章,可以在以下平台关注我</strong></p>
<ul>
<li>GitHub: <a href="https://link.segmentfault.com/?enc=gIo0SC7u4PS%2F2dMV2spRJg%3D%3D.h%2FW%2BMGgbXU7J9yrDN26fGsQSM89rhm8yPIOvyWFyudHSr%2FNLYL2vd8C0ZaEOd9Yz" rel="nofollow">https://github.com/JessYanCoding</a>
</li>
<li>掘金: <a href="https://link.segmentfault.com/?enc=sPg9e0IMQHhrNl689a0d0g%3D%3D.919mZXaZLpXq7wJwpp7uSpWrSLMPsjWILoRVq6UWYSx3xn0fKv2Xl2sL968WuCXqDBPY6h7%2FDutjMIv9nrnY1g%3D%3D" rel="nofollow">https://gold.xitu.io/user/57a...</a>
</li>
<li>简书: <a href="https://link.segmentfault.com/?enc=xGDBdsfBNm1k1yyUOBddlw%3D%3D.IHD4zlIUUs4GwrdtZJgHJMzhagLieOYXzoJBN8bO2Y1Y6HNqLX7hncR8rEaHS6Ff" rel="nofollow">http://www.jianshu.com/u/1d0c...</a>
</li>
<li>微博: <a href="https://link.segmentfault.com/?enc=l9WSIegT6Z49KLnX9HLygQ%3D%3D.LQrCzCPu3y4T%2F5RCZkFBOkBcDoBVyDlQwT5mOugv4RE%3D" rel="nofollow">http://weibo.com/u/1786262517</a>
</li>
</ul>
<p>-- The end</p>
MVPArms官方首发一键生成组件化,体验纯傻瓜式组件化开发
https://segmentfault.com/a/1190000014949237
2018-05-21T10:44:17+08:00
2018-05-21T10:44:17+08:00
JessYanCoding
https://segmentfault.com/u/jessyancoding
0
<p>原文地址: <a href="https://link.segmentfault.com/?enc=GwEbhnLATwkSrjCJlIdgCA%3D%3D.pHtVNIngRUfMPZaqguRf4gkAuvM6Po4ZSLKu0BA%2FhyF0t1%2B1EK4tiRlvc6oa6711" rel="nofollow">https://www.jianshu.com/p/245...</a></p>
<h2>前言</h2>
<p>我在 <a href="https://link.segmentfault.com/?enc=o7VvsL3Q3%2FiyCQY8%2BqRXpg%3D%3D.FZAYaOqmQUZ785KTJrVv03OjaYHBKQtrcehlClA9yjn3q8ztlLURSG1sfURZpzjY" rel="nofollow">上篇文章</a> 中介绍了 <a href="https://link.segmentfault.com/?enc=%2FvKuc9iTCi6RPiC%2F9Ytw8Q%3D%3D.cmQt%2Fj5BnUEK7h53pFpkF90jrlgaEm%2Big1CKa3avp29CHX0dPeL7M3bZaS71YBgN" rel="nofollow">MVPArms</a> 的官方快速组件化方案 <a href="https://link.segmentfault.com/?enc=4%2FqdZCd5qMqxd%2B36WdLtEQ%3D%3D.Gn8dmzKrrWwOf%2Bi%2FfPW%2FdiRr2j0qkTpSRDDLrQbMXhVVFMBKqAsnKDcPM%2Bkt1x51j4J43d6WC7dObM2xhhLpJw%3D%3D" rel="nofollow">ArmsComponent</a></p>
<p>当时一直强调 <strong>ArmsComponent</strong> 是 <strong>快速</strong> 的组件化方案, 但是在文章中只提供了一个近万字的官方文档, 却没展现出这个组件化方案的快速之处</p>
<p>看到近万字的文档后, 新手已经开始瑟瑟发抖了? 觉得入门成本太高想放弃?</p>
<p>写这篇文章的意义就是为了展现 <strong>快速</strong> 这两个字, 到底有多快? 飞快! </p>
<p>现在我可以不开玩笑的告诉大家, 官方文档上介绍的大部分内容和规范, 现在只需要一键就可以生成, 快速并且零差错, 让新手也可以很愉快的玩耍组件化, 极大的提升开发体验和效率, 你还有什么理由不选择 <a href="https://link.segmentfault.com/?enc=48Fzt2t1OPFB4dhkD%2FuAYw%3D%3D.pqhvUS7%2B3ybIS2tnsnwSTvpqSSi3jhEBiPKqN4Dk0EtO8Cul1nk8SL7F5G822duIg1%2Fctltwcnc5mE9lXyEiWA%3D%3D" rel="nofollow">ArmsComponent</a> 开启组件化的大门呢?</p>
<p>好了, 进入正题, 大家直接看下图</p>
<p><img src="/img/remote/1460000014949242?w=1280&h=744" alt="gif" title="gif"></p>
<p>看了这个 <strong>GIF</strong> 图过后, 是不是已经跃跃欲试了? 点几下就可以生成组件? <strong>Are you kidding me?</strong> 那好, 您如果不信的话立马去安装然后试一试!</p>
<blockquote>Github : <a href="https://link.segmentfault.com/?enc=etS3ncFQwxZYR59uWDJNhQ%3D%3D.H9Uj62OB8OXobMd%2FIUw2OfTICWmPjeH4T5bu%2Fv%2BM8uzgiU3iECd0HdM5WcjqFIxZ4ggvCBx1lIxyhCtByAajsQ%3D%3D" rel="nofollow">您的 Star 是我坚持的动力 ✊</a>
</blockquote>
<h2>使用须知</h2>
<ol>
<li>先使用 <strong>ArmsComponent-Template</strong> (<strong>Module</strong> 级一键模板) 一键搭建整体组件架构, 再使用 <a href="https://link.segmentfault.com/?enc=5omtN7okX%2BXAiGhrTpuBAA%3D%3D.1L6U6dPtmvVl1P3NxO6X6wk0ugEkkRBjJLaNeXL%2FZs8A08R%2FPSe4Vtr2JmSh4ETx20wbWsz7VfiWmqVIIf45cQ%3D%3D" rel="nofollow">MVPArmsTemplate</a> (<strong>页面</strong> 级一键模板) 一键生成每个业务页面所需要的 <strong>MVP</strong> 及 <strong>Dagger2</strong> 相关类, 即可让什么都不懂的新手也可以一秒开始组件化项目!</li>
<li>若您基于本模板修改并且开源于网络, 请注明出处, 尊重开源, 才有人愿意开源, 谢谢!</li>
</ol>
<h2>如何安装?</h2>
<p>请将 <strong>NewArmsComponent</strong> 这个文件夹复制到 <strong>AndroidStudio Module</strong> 模版的存放路径, 请注意是复制整个文件夹, 不是里面的内容!</p>
<p><strong>AndroidStudio Module</strong> 模版存放路径 (<strong>请注意 Module 级模板和页面级模板的存放路径不一样, 不要放错了!</strong>):</p>
<ul>
<li>Windows : AS安装目录/plugins/android/lib/templates/gradle-projects</li>
<li>Mac : /Applications/Android Studio.app/Contents/plugins/android/lib/templates/gradle-projects</li>
</ul>
<p><strong>最后记得重启 AndroidStudio !</strong></p>
<h2>如何使用?</h2>
<p>使用时按下图步骤即可, 也可以使用快捷键, <strong>Mac</strong> 的快捷键是在项目名上按 <strong>Command + n</strong>, 选择 <strong>Module</strong>, <strong>Windows</strong> 快捷键自己百度</p>
<p><img src="/img/remote/1460000014949243" alt="step" title="step"></p>
<h2>注意事项</h2>
<ul>
<li>本模板是基于 <a href="https://link.segmentfault.com/?enc=vaLBpZ54HsOCrqFIJefp6g%3D%3D.HuCYcFj%2BuS6cF3lSnoHpPnze2QO5EBcR41CP6fMrv0ZvnaBbVivH%2BL2rgkP8k9HY" rel="nofollow">ArmsComponent</a> 开发的, 所以使用的是 <strong>ArmsComponent</strong> 的整体架构, 最优的方式是直接 <strong>clone</strong> 或下载 <strong>ArmsComponent</strong> 工程后, 在工程上面直接使用本模板, 开始业务的开发, 让您体验纯傻瓜式的组件化开发</li>
<li>使用本模板生成的组件马上就可以独立运行, 但是如果您想要集成调试, 还需要在宿主 <strong>App</strong> 中 <a href="https://link.segmentfault.com/?enc=gPfzRZC3nOJqJIVmhyHQDA%3D%3D.ncFS4lEWL1HZADhkWA2dyBuVg6T6hx903j2P7qiq%2FDRFEy6PJet7HNfrVEmtKOzbr1NeaPvvnwG9eA83bIqnPWwc6KGmFzkrc62LNSoXlWo%3D" rel="nofollow">依赖此组件</a>
</li>
</ul>
<pre><code class="gradle"> dependencies {
if (!isBuildModule.toBoolean()) {
implementation project(":ModuleZhihu")
implementation project(":ModuleGank")
implementation project(":ModuleGold")
implementation project(":组件名(Module name)")
}
}</code></pre>
<ul><li>此模板会持续保持更新, 但只保证能够兼容最新的 <strong>AndroidStudio 稳定版</strong>, 暂不提供其他版本<p><img src="/img/remote/1460000014949244" alt="attention-1" title="attention-1"></p>
<p><img src="/img/remote/1460000014949245" alt="attention-2" title="attention-2"></p>
</li></ul>
<hr>
<p><strong>Hello 我叫Jessyan,如果您喜欢我的文章,可以在以下平台关注我</strong></p>
<ul>
<li>GitHub: <a href="https://link.segmentfault.com/?enc=BzbqJaYvOC4NT%2FH56e2Hsw%3D%3D.KovQr051Wtt7YcoPjt0UrwY8PDc2jzlrAm6cQm5H%2BVsXmDXdx5S34vPxrWQQq72X" rel="nofollow">https://github.com/JessYanCoding</a>
</li>
<li>掘金: <a href="https://link.segmentfault.com/?enc=7YFp%2B%2FdtT6nilNkgqPnJzA%3D%3D.A%2B0G6MFOeCkTNlQI4DrYokb4vJ%2B%2FHMtvPvluODiRULgkESSWa6b3OftCeQFiOX1%2B55apfVOB5iJ7mUzYYPN2rA%3D%3D" rel="nofollow">https://gold.xitu.io/user/57a...</a>
</li>
<li>简书: <a href="https://link.segmentfault.com/?enc=ZV7sqRREeRPM5Wp7rfoRmQ%3D%3D.SyDsRH%2BO2b1etB3%2FVgwij6rXZr8i%2Fy6JqowpNQcKKZPGYW%2F%2FTYIBdyzPrBqWOwBP" rel="nofollow">http://www.jianshu.com/u/1d0c...</a>
</li>
<li>微博: <a href="https://link.segmentfault.com/?enc=g4WeUzcMKn1mmab8JLYfZw%3D%3D.LTHfdhvQvqRzGOy9Q%2FnuwMozWEvw8r%2Fma86GmEOe8k0%3D" rel="nofollow">http://weibo.com/u/1786262517</a>
</li>
</ul>
<p>-- The end</p>
MVPArms官方快速组件化方案开源,来自5K star的信赖
https://segmentfault.com/a/1190000014761025
2018-05-07T16:43:44+08:00
2018-05-07T16:43:44+08:00
JessYanCoding
https://segmentfault.com/u/jessyancoding
1
<p>原文地址: <a href="https://link.segmentfault.com/?enc=rJoO45vVHbka7tWuBcJzPA%3D%3D.OGerlgTwX8hymdJTM7d3w1GHxhqpI0iiFHa3auhjJbh90sSiGM4n6Nl1B20Kvpwg" rel="nofollow">https://www.jianshu.com/p/f67...</a></p>
<ul>
<li>
<p>0 <a href="#0">前言</a></p>
<ul>
<li>0.1 <a href="#0.1">起源</a>
</li>
<li>
<p>0.2 <a href="#0.2">组件化方案分析</a></p>
<ul>
<li>0.2.1 <a href="#0.2.1">业务组件的划分和代码隔离</a>
</li>
<li>0.2.2 <a href="#0.2.2">路由框架</a>
</li>
<li>0.2.3 <a href="#0.2.3">基础库</a>
</li>
</ul>
</li>
<li>0.3 <a href="#0.3">ArmsComponent 的优势</a>
</li>
</ul>
</li>
<li>
<p>1 <a href="#1">简介</a></p>
<ul>
<li>1.1 <a href="#1.1">什么是组件化?</a>
</li>
<li>1.2 <a href="#1.2">为什么要组件化?</a>
</li>
<li>1.3 <a href="#1.3">分析现有的组件化方案</a>
</li>
<li>1.4 <a href="#1.4">如何选择组件化方案?</a>
</li>
</ul>
</li>
<li>
<p>2 <a href="#2">组件化方案描述</a></p>
<ul>
<li>2.1 <a href="#2.1">架构图一览</a>
</li>
<li>
<p>2.2 <a href="#2.2">架构图详解</a></p>
<ul>
<li>2.2.1 <a href="#2.2.1">宿主层</a>
</li>
<li>
<p>2.2.2 <a href="#2.2.2">业务层</a></p>
<ul><li>2.2.2.1 <a href="#2.2.2.1">业务模块的拆分</a>
</li></ul>
</li>
<li>
<p>2.2.3 <a href="#2.2.3">基础层</a></p>
<ul>
<li>2.2.3.1 <a href="#2.2.3.1">核心基础业务</a>
</li>
<li>2.2.3.2 <a href="#2.2.3.2">公共服务</a>
</li>
<li>
<p>2.2.3.3 <a href="#2.2.3.3">基础 SDK</a></p>
<ul>
<li>2.2.3.3.1 <a href="#2.2.3.3.1">MVPArms</a>
</li>
<li>2.2.3.3.2 <a href="#2.2.3.3.2">UI 组件</a>
</li>
<li>2.2.3.3.3 <a href="#2.2.3.3.3">其他 SDK</a>
</li>
</ul>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>2.3 <a href="#2.3">跨组件通信</a></p>
<ul>
<li>2.3.1 <a href="#2.3.1">为什么需要跨组件化通信?</a>
</li>
<li>2.3.2 <a href="#2.3.2">跨组件通信场景</a>
</li>
<li>2.3.3 <a href="#2.3.3">跨组件通信方案</a>
</li>
<li>2.3.4 <a href="#2.3.4">跨组件通信方案分析</a>
</li>
<li>2.3.5 <a href="#2.3.5">跨组件传递复杂数据格式</a>
</li>
</ul>
</li>
<li>
<p>2.4 <a href="#2.4">组件的生命周期</a></p>
<ul>
<li>2.4.1 <a href="#2.4.1">问题分析</a>
</li>
<li>2.4.2 <a href="#2.4.2">可行方案分析</a>
</li>
<li>2.4.3 <a href="#2.4.3">最终执行方案</a>
</li>
</ul>
</li>
</ul>
</li>
<li>
<p>3 <a href="#3">项目讲解</a></p>
<ul>
<li>3.1 <a href="#3.1">如何让组件独立运行?</a>
</li>
<li>3.2 <a href="#3.2">配置 AndroidManifest</a>
</li>
<li>3.3 <a href="#3.3">配置 ConfigModule</a>
</li>
<li>3.4 <a href="#3.4">RouterHub</a>
</li>
<li>3.5 <a href="#3.5">EventBusHub</a>
</li>
<li>3.6 <a href="#3.6">在项目中使用多个不同的域名</a>
</li>
</ul>
</li>
</ul>
<p><a></a></p>
<h2>0 前言</h2>
<p><a href="https://link.segmentfault.com/?enc=PQtfHUOSD9uPez0lVLZLsA%3D%3D.kqakz9mkmnBdwum73rzg%2BqjOfLu%2BVCRg1KTk2mqxU0LQjmRUzCqcCvzsdE0%2FrZDS" rel="nofollow"><strong>MVPArms</strong></a> 从两年前开源至今, 已经累积了近 <strong>5k star</strong>, 获得了上千个商业项目的信赖和认可</p>
<p>回顾两年前, 那时 <strong>MVPArms</strong> 还没诞生, <strong>MVP</strong>、<strong>Dagger2</strong>、<strong>Rxjava</strong>、<strong>Retrofit</strong> 这些技术在国内才刚刚开始流行, 在网上能搜索到的中文学习资料远没有现在这么丰富, 特别是 <strong>Dagger</strong>, 在网上能搜索到的文章甚至有很多讲的是 <strong>Square</strong> 的 <strong>Dagger1</strong>, 学习资料的匮乏加上 <strong>Dagger2</strong> 本身就是块硬骨头, 让本人在学习的道路上不知道多走了多少弯路</p>
<p>从那时开始, 让初学者都能够快速搭建一个 <strong>MVP</strong> + <strong>Dagger2</strong> + <strong>Retrofit</strong> + <strong>Rxjava</strong> 项目的种子就已经深埋在心中, 后面经过不懈的努力, <strong>MVPArms</strong> 终于诞生并开源了, 开源以后只是一直坚持将 <strong>代码</strong>, <strong>注释</strong> 和 <strong>文档</strong> 做到极致, 没想到的是, <strong>MVPArms</strong> 能发展到如今的体量, 感谢开源!</p>
<p><a></a></p>
<h3>0.1 起源</h3>
<p>这是 <strong>MVPArms</strong> 的起源, <strong>ArmsComponent</strong> 的起源同样相似, 从去年开始, 组件化逐渐火热起来, 本人也在去年年初开始在公司项目中进行组件化, 一切还算顺利</p>
<p>那时同样的种子继续埋在了心中, 我想让刚刚接触组件化的初学者也能快速搭建一个中小型的组件化项目, 经过一年的不断优化, 终于决定将其开源(<strong>MVPArms</strong> 官方组件化方案 <strong>ArmsComponent</strong>)</p>
<blockquote>Github : <a href="https://link.segmentfault.com/?enc=jh1z3w6hD2FG5dAhY3%2Bejw%3D%3D.CXwRgsleXb16BdnyoUQt4V0P9hGIhrOuMueGBCgBAvQuFHM5yRLKZf2YX%2FDzJd9Z" rel="nofollow">您的 Star 是我坚持的动力 ✊</a>
</blockquote>
<p><a></a></p>
<h3>0.2 组件化方案分析</h3>
<p>看了很多组件化方案, 所以总结了在组件化中很重要的三个大点:</p>
<ol>
<li>基础库(网络请求、图片加载等)的封装</li>
<li>路由框架(页面跳转, 服务提供)</li>
<li>业务组件的划分和代码隔离</li>
</ol>
<p><a></a></p>
<h4>0.2.1 业务组件的划分和代码隔离</h4>
<p>先说第三点 <strong>业务组件的划分和代码隔离</strong>, 现在大部分的文章都围绕着这点, 我这里发表下个人的观点, 第三点确实是很重要的一点, 不管是大厂的方案还是小厂的方案都有借鉴之处, 但是这点也是最不可能讨论出最终结果和统一解决方案的一点</p>
<p>每个公司、每个项目的情况都不一样, 大厂的方案真的适合您吗? 不一定, 大厂几十个业务群, 几百号开发人员, 他们的组织结构和项目规模都不是普通公司能比拟的, 如果伸拉硬套他们的方案, 进行更严格更细粒度的代码隔离, 可能产出的价值还不及您先前付出的代价, 带来效率的降低, 所以根据项目的实际情况做出灵活的调整才是项目负责人最应该干的事, 这点我在后面会有详细的介绍</p>
<p><a></a></p>
<h4>0.2.2 路由框架</h4>
<p>陆续也有很多组件化方案开源自己的 <strong>路由框架</strong>, 是个很不错的开始, 我觉得大家写的都不错, 各有各的优势, 本方案也决定用别人的 <strong>路由框架</strong>, 自己写的原理也差不多, 还不一定比别人考虑的完善, 还要自己维护, 为什么不选择一个成熟稳定的呢?</p>
<p><a></a></p>
<h4>0.2.3 基础库</h4>
<p>很多组件化文章只是在讲如何拆分以及封装基础库, 但是至今没看到有哪个组件化方案开源过完整的基础库的, 我猜测原因可能是, 组件化方案都是从商业项目不断的业务迭代中逐渐完善的, 基础库也属于公司的核心机密, 所以不可能开源</p>
<p>但是基础库尤其重要, 特别是兼容组件化的基础库, 这关乎到组件化方案的根基, 根基都没有, 何谈其他更高级的功能? 但是从封装到完善一个兼容组件化的基础库是需要很长时间的, 大多数中小公司是不愿意投入这个成本的</p>
<p><a></a></p>
<h3>0.3 ArmsComponent 的优势</h3>
<p><strong>MVPArms</strong> 是一个开源两年, 成熟稳定, 涵盖大量主流技术且兼容组件化的基础库, <strong>MVPArms</strong> 使得 <strong>ArmsComponent</strong> 成为了唯一提供完整基础库的组件化方案, 这就是 <strong>ArmsComponent</strong> 相对于其他组件化方案最大的优势</p>
<p>因为有了 <strong>基础库</strong> 的存在, 再加上已有的 <strong>路由框架</strong>, 组件化中的三个大点就已经占有两个(<strong>业务组件的划分和代码隔离</strong> 在后面会有介绍), 因此使用 <strong>ArmsComponent</strong> 启动一个新项目, 即可快速进行组件化, 将 <strong>Demo</strong> (组件化项目雏形) 克隆下来后, 稍作修改, 马上就可以投入到业务的开发之中</p>
<p><strong>ArmsComponent</strong> 对于新项目以及已经开始使用 <strong>MVPArms</strong> 的项目将会更加便捷, 有着优于其他组件化方案的体验, 对于那些网络请求, 图片加载等基础功能乱七八糟散落到项目各处, 没有统一抽离出来的旧项目, 建议直接使用 <strong>MVPArms</strong>, 开始组件化</p>
<p>如果您不想使用 <strong>MVPArms</strong>, 觉得接入成本太大, 没关系, 借鉴下 <a href="https://link.segmentfault.com/?enc=a85b0be9dB14rRRwpOLDgA%3D%3D.B9bqX8HRvoLexbWZND6DvK8zzD7AngnvV0ShIvua%2F%2BRJ3GSuAkYpKfFg8SbrLmKm" rel="nofollow"><strong>MVPArms</strong></a> 和 <a href="https://link.segmentfault.com/?enc=PGE46CtmOYB7L9MGHs6iKQ%3D%3D.1WOrrFmI%2FfnQ3TBWybo2FLvIhEVjTuDNz1VS89vUX1dv4kvRxBkfUjeXAk5a%2Fuif" rel="nofollow"><strong>ArmsComponent</strong></a> 的代码, 尝试改造自己的项目, 也是个不错的选择</p>
<p><a></a></p>
<h2>1 简介</h2>
<p>好了, 进入正题!</p>
<p><a></a></p>
<h3>1.1 什么是组件化?</h3>
<p>组件化简单概括就是把一个功能完整的 <strong>App</strong> 或模块拆分成多个子模块, 每个子模块可以独立编译和运行, 也可以任意组合成另一个新的 <strong>App</strong> 或模块, 每个模块即不相互依赖但又可以相互交互, 遇到某些特殊情况甚至可以升级或者降级</p>
<p><a></a></p>
<h3>1.2 为什么要组件化?</h3>
<p>现在的项目随着需求的增加规模变得越来越大, 规模的增大带来了很多烦恼, 各种业务错中复杂的交织在一起, 每个业务模块之间, 代码没有约束, 带来了代码边界的模糊, 代码冲突时有发生, 更改一个小问题可能引起一些新的问题, 牵一发而动全身, 增加一个新需求, 瞻前顾后的熟悉了大量前辈们写的代码后才敢动手, 编译时间也不在断增加, 开发效率极度的下降, 在这种情况下组件化的出现就是为了解决以上的烦恼</p>
<p><a></a></p>
<h3>1.3 分析现有的组件化方案</h3>
<p>很多大厂的组件化方案是以 <strong>多工程</strong> + <strong>多 Module</strong> 的结构(微信, 美团等超级 <strong>App</strong> 更是以 <strong>多工程</strong> + <strong>多 Module</strong> + <strong>多 P 工程(以页面为单元的代码隔离方式)</strong> 的三级工程结构), 使用 <strong>Git Submodule</strong> 创建多个子仓库管理各个模块的代码, 并将各个模块的代码打包成 <strong>AAR</strong> 上传至私有 <strong>Maven</strong> 仓库使用远程版本号依赖的方式进行模块间代码的隔离</p>
<p><a></a></p>
<h3>1.4 如何选择组件化方案?</h3>
<p>按照康威定律, 系统架构的设计需要根据组织间的沟通结构, 因为现在大部分项目的规模和开发人员的数量以及结构还不足以需要某些大厂发布的组件化方案支撑(大厂的组织结构和项目规模都非常庞大, 他们的方案不一定完全适合所有公司的项目), 进行更严格更细粒度的代码间以及模块间的隔离, 盲目的使用某些组件化方案, 可能会带来开发效率降低, 开发成本远大于收益等情况, 性价比变低, 作为项目负责人, 应该根据项目目前的规模以及开发人员的组织结构去选择目前最适合的组件化方案, 做到以项目实际情况去制定技术方案, 而不是盲目跟随某些大厂的技术方案让项目和开发人员花费大量时间去调整和适应</p>
<p><a></a></p>
<h2>2 组件化方案描述</h2>
<p><strong>ArmsComponent</strong> 目前采用的是 <strong>单工程</strong> + <strong>多 Module</strong> 的结构, 由于 <strong>Demo</strong> 较小仅仅为了展示基本规范, 所以也只是采用源码依赖并没有做到远程版本号依赖组件, 代码管理也只是采用 <strong>单仓库</strong> + <strong>多分支</strong> 的方式, 这样也是对于开发初期, 项目规模还较小, 开发人员也较少时, 开发效率较高的方案, 如果您的项目规模较大, 开发人员众多, 就可以采用上面提到的 <strong>多工程</strong> + <strong>多 Module</strong>, 并使用私有 <strong>Maven</strong> 仓库管理组件版本</p>
<p><strong>世界上没有一个方案可以完美到兼顾所有情况, 并且还满足所有人, 所有项目的需求, 所以项目负责人必须按照项目实际情况做出灵活的调整, 才能做出最适合自家项目的方案</strong></p>
<p><a></a></p>
<h3>2.1 架构图一览</h3>
<p><img src="/img/remote/1460000014761030?w=1024&h=768" alt="ArmsComponent 组件化架构图" title="ArmsComponent 组件化架构图"><br><p align="center"><font color=gray>ArmsComponent 组件化架构图</font></p></p>
<p><a></a></p>
<h3>2.2 架构图详解</h3>
<p>目前架构一共分为三层, 从低到高依次是基础层, 业务层和宿主层, 由于目前项目较小人员较少所以三层都集中在一个工程中, 但您可以根据项目的规模和开发人员的数量拆分成多个工程协同开发</p>
<p><a></a></p>
<h4>2.2.1 宿主层</h4>
<p>宿主层位于最上层, 主要作用是作为一个 <strong>App</strong> 壳, 将需要的模块组装成一个完整的 <strong>App</strong>, 这一层可以管理整个 <strong>App</strong> 的生命周期(比如 <strong>Application</strong> 的初始化和各种组件以及三方库的初始化)</p>
<p><a></a></p>
<h4>2.2.2 业务层</h4>
<p>业务层位于中层, 里面主要是根据业务需求和应用场景拆分过后的业务模块, 每个模块之间互不依赖, 但又可以相互交互, 比如一个商城 <strong>App</strong> 由 <strong>搜索</strong>, <strong>订单</strong>, <strong>购物车</strong>, <strong>支付</strong> 等业务模块组成</p>
<blockquote><strong>Tips: 每个业务模块都可以拥有自己独有的 SDK 依赖和自己独有的 UI 资源 (如果是其他业务模块都可以通用的 SDK 依赖 和 UI 资源 就可以将它们抽离到 <a href="#2.2.3.3">基础 SDK(CommonSDK 2.2.3.3)</a> 和 <a href="#2.2.3.3.2">UI 组件(CommonRes 2.2.3.3.2)</a> 中)</strong></blockquote>
<p><a></a></p>
<h5>2.2.2.1 业务模块的拆分</h5>
<p>写业务之前先不要急着动手敲码, 应该先根据初期的产品需求到后期的运营规划结合起来清晰的梳理一下业务在未来可能会发生的发展, 确定业务之间的边界, 以及可能会发生的变化, 最后再确定下来真正需要拆分出来的业务模块再进行拆分</p>
<p><a></a></p>
<h4>2.2.3 基础层</h4>
<p>基础层位于最底层, 里面又包括 <strong>核心基础业务模块</strong>、<strong>公共服务模块</strong>、 <strong>基础 SDK 模块</strong>, <strong>核心基础业务模块</strong> 和 <strong>公共服务模块</strong> 主要为业务层的每个模块服务, <strong>基础 SDK 模块</strong> 含有各种功能强大的团队自行封装的 <strong>SDK</strong> 以及第三方 <strong>SDK</strong>, 为整个平台的基础设施建设提供动力</p>
<p><a></a></p>
<h5>2.2.3.1 核心基础业务</h5>
<p><strong>核心基础业务</strong> 为 <strong>业务层</strong> 的每个业务模块提供一些与业务有关的基础服务, 比如在项目中以用户角色分为 2 个端口, 用户可以扮演多个角色, 但是在线上只能同时操作一个端口的业务, 这时每个端口都必须提供一个角色切换的功能, 以供用户随时在多个角色中切换,<br>这时在项目中就需要提供一个用于用户自由切换角色的管理类作为 <strong>核心基础业务</strong> 被这 2 个端口所依赖(类似 拉勾, Boss 直聘等 <strong>App</strong> 可以在招聘者和应聘者之间切换)</p>
<p><strong>核心基础业务</strong> 的划分应该遵循是否为业务层大部分模块都需要的基础业务, 以及一些需要在各个业务模块之间交互的业务, 都可以划分为 <strong>核心基础业务</strong></p>
<p><a></a></p>
<h5>2.2.3.2 公共服务</h5>
<p><strong>公共服务</strong> 是一个名为 <strong>CommonService</strong> 的 <strong>Module</strong>, 主要的作用是用于 <strong>业务层</strong> 各个模块之间的交互(自定义方法和类的调用), 包含自定义 <strong>Service</strong> 接口, 和可用于跨模块传递的自定义类</p>
<p>主要流程是: </p>
<p>提供服务的业务模块:</p>
<blockquote>在公共服务(<strong>CommonService</strong>) 中声明 <strong>Service</strong> 接口 (含有需要被调用的自定义方法), 然后在自己的模块中实现这个 <strong>Service</strong> 接口, 再通过 <strong>ARouter API</strong> 暴露实现类</blockquote>
<p>使用服务的业务模块:</p>
<blockquote>通过 <strong>ARouter</strong> 的 <strong>API</strong> 拿到这个 <strong>Service</strong> 接口(多态持有, 实际持有实现类), 即可调用 <strong>Service</strong> 接口中声明的自定义方法, 这样就可以达到模块之间的交互</blockquote>
<p>跨模块传递的自定义类:</p>
<p>在 <strong>公共服务</strong> 中定义需要跨模块传递的自定义类后 (<strong>Service</strong> 中的自定义方法和 <strong>EventBus</strong> 中的事件实体类都可能需要用到自定义类), 就可以通过 <strong>ARouter API</strong>, 在各个模块的页面之间跨模块传递这个自定义对象 (<strong>ARouter</strong> 要求传递自定义对象必须实现 <strong>SerializationService</strong> 接口)</p>
<blockquote><strong>Tips: 建议在 CommonService 中给每个需要提供服务的业务模块都建立一个单独的包, 然后在这个包下放 Service 接口 和 需要跨模块传递的自定义类, 这样更好管理</strong></blockquote>
<p><strong>掌握公共服务层的用法最好要了解 ARouter 的 API</strong></p>
<blockquote><a href="https://link.segmentfault.com/?enc=pqQFwYaqFxaViTsIfy3PqQ%3D%3D.cIeNE%2FsJu74lMDD6GNZaYZtfpQa8%2F%2BoNSnAy4D%2FxkbZz7EVN3LG3hXnId5n2viBs" rel="nofollow"><strong>点击查阅 ARouter 文档</strong></a></blockquote>
<p><a></a></p>
<h5>2.2.3.3 基础 SDK</h5>
<p><strong>基础 SDK</strong> 是一个名为 <strong>CommonSDK</strong> 的 <strong>Module</strong>, 其中包含了大量功能强大的 <strong>SDK</strong>, 提供给整个架构中的所有模块</p>
<p><a></a><br><strong>2.2.3.3.1 MVPArms</strong></p>
<p><strong>MVPArms</strong> 是整个基础层中最重要的模块, 可谓是整个组件化架构中的心脏, 里面提供了开发一个完整项目所必须的一整套 <strong>API</strong> 和 <strong>SDK</strong>, 是整个项目的脚手架, 我用它来统一整个组件化方案的基础设施, 使每一个模块更加健壮, 因为有了 <strong>MVPArms</strong>, 使得 <strong>ArmsComponent</strong> 成为了唯一提供完整基础框架的组件化方案, 所以学习 <strong>ArmsComponent</strong> 之前必须先学会 <strong>MVPArms</strong></p>
<blockquote>学习 <strong>MVPArms</strong> 时请按以下排列顺序依次学习:</blockquote>
<p>1.<a href="https://link.segmentfault.com/?enc=kbnJQlFk6x5cFV6mSl%2Fcew%3D%3D.Njg9tu4EoDxS2WVQYvQvbGb6BIf5fXFUY2lMm%2Fx4OMybxKXMA2i8arNDjE5D1Cn8" rel="nofollow"><strong>点击学习 Demo</strong></a> </p>
<p>2.<a href="https://link.segmentfault.com/?enc=NcMEneuciFcYUQLESzv%2BtA%3D%3D.r5fUL25sjxtNCNk5bYa6R8K4WLjCp%2FHBheKwde9Mc7SBzoJ%2BKgS9TF2oFo2SHq5p" rel="nofollow"><strong>点击查阅 详细文档</strong></a></p>
<p>3.<a href="https://link.segmentfault.com/?enc=gFdspt8YgEyKSHvj6yjGxQ%3D%3D.nzbP90vUUjwE6Ep8OBvnHrlTsWA%2F34vUz2HtnQ8FZFtbHoGFC4RwSHdNwBxpOoF3EpwDJDhVREQKvNUfgEC4sg%3D%3D" rel="nofollow"><strong>点击下载 一键生成代码插件</strong></a></p>
<p><a></a><br><strong>2.2.3.3.2 UI 组件</strong></p>
<p><strong>基础 SDK</strong> 中的 <strong>UI 组件</strong> 是一个名为 <strong>CommonRes</strong> 的 <strong>Module</strong>, 主要放置一些业务层可以通用的与 <strong>UI</strong> 有关的资源供所有业务层模块使用, 便于重用、管理和规范已有的资源</p>
<blockquote><strong>Tips: 值得注意的是, 业务层的某些模块如果出现有资源名命名相同的情况 (如两个图片命名相同), 当在宿主层集成所有模块时就会出现资源冲突的问题, 这时注意在每个模块的 build.gradle 中使用 resourcePrefix 标签给每个模块下的资源名统一加上不同的前缀即可解决此类问题</strong></blockquote>
<pre><code class="gradle">android {
defaultConfig {
minSdkVersion rootProject.ext.android["minSdkVersion"]
...
}
resourcePrefix "public_"
}</code></pre>
<p>可以放置的资源类型有:</p>
<ul>
<li>通用的 <strong>Style</strong>, <strong>Theme</strong>
</li>
<li>通用的 <strong>Layout</strong>
</li>
<li>通用的 <strong>Color</strong>, <strong>Dimen</strong>, <strong>String</strong>
</li>
<li>通用的 <strong>Shape</strong>, <strong>Selector</strong>, <strong>Interpolator</strong>
</li>
<li>通用的 <strong>图片资源</strong>
</li>
<li>通用的 <strong>动画资源</strong>
</li>
<li>通用的 <strong>自定义 View</strong>
</li>
<li>通用的第三方 <strong>自定义 View</strong>
</li>
</ul>
<p><a></a><br><strong>2.2.3.3.3 其他 SDK</strong></p>
<p><strong>其他 SDK</strong> 主要是 <strong>基础 SDK</strong> 依赖的一些业务层可以通用的 <strong>第三方库</strong> 和 <strong>第三方 SDK</strong> (比如 <strong>ARouter</strong>, <strong>腾讯 X5 内核</strong>), 便于重用、管理和规范已有的 <strong>SDK</strong> 依赖</p>
<p><a></a></p>
<h3>2.3 跨组件通信</h3>
<p><a></a></p>
<h4>2.3.1 为什么需要跨组件通信?</h4>
<p>因为各个业务模块之间是各自独立的, 并不会存在相互依赖的关系, 所以一个业务模块是访问不了其他业务模块的代码的, 如果想从 <strong>A</strong> 业务模块的 <strong>A</strong> 页面跳转到 <strong>B</strong> 业务模块的 <strong>B</strong> 页面, 光靠模块自身是不能实现的, 所以这时必须依靠外界的其他媒介提供这个跨组件通信的服务</p>
<p><a></a></p>
<h4>2.3.2 跨组件通信场景</h4>
<p>跨组件通信主要有以下两种场景:</p>
<ul>
<li>第一种是组件之间的页面跳转 (<strong>Activity</strong> 到 <strong>Activity</strong>, <strong>Fragment</strong> 到 <strong>Fragment</strong>, <strong>Activity</strong> 到 <strong>Fragment</strong>, <strong>Fragment</strong> 到 <strong>Activity</strong>) 以及跳转时的数据传递 (基础数据类型和可序列化的自定义类类型)</li>
<li>第二种是组件之间的自定义类和自定义方法的调用(组件向外提供服务)</li>
</ul>
<p><a></a></p>
<h4>2.3.3 跨组件通信方案</h4>
<p>其实以上两种通信场景甚至其他更高阶的功能在 <strong>ARouter</strong> 中都已经被实现, <strong>ARouter</strong> 是 <strong>Alibaba</strong> 开源的一个 <strong>Android</strong> 路由中间件, 可以满足很多组件化的需求, 也是作为本方案中比较重要的一环, 需要认真看下文档, 了解下基本使用</p>
<p>关于跨组件通信框架, 本方案不做封装, 专业的事交给专业的人做, 此类优秀框架为数众多, 各有特点, 可以根据您的需求选择您喜欢的框架, 不一定非得是 <strong>ARouter</strong>, 选择 <strong>ARouter</strong> 也是因为 <strong>Alibaba</strong> 出品, 开源时间较长, 使用者众多, 相对稳定, 出现问题比较好沟通</p>
<p><a></a></p>
<h4>2.3.4 跨组件通信方案分析</h4>
<p>第一种组件之间的页面跳转不需要过多描述了, 算是 <strong>ARouter</strong> 中最基础的功能, <strong>API</strong> 也比较简单, 跳转时想传递不同类型的数据也提供有相应的 <strong>API</strong> (传递自定义对象, 需要实现 <strong>SerializationService</strong>, 详情请查阅 <a href="https://link.segmentfault.com/?enc=fT0sQfaNuLe5ULNWQfKyqg%3D%3D.xAN3LuEpnkvFU9zBEG7NvsG6W82lHGhG5DATNfx6QJkxWxmh07akrwuysaO1XS8k" rel="nofollow">ARouter 文档</a>);</p>
<p>第二种组件之间的自定义类和自定义方法的调用要稍微复杂点, 需要 <strong>ARouter</strong> 配合架构中的 公共服务(<strong>CommonService</strong>) 实现, 主要流程在 <a href="#2.2.3.2">公共服务(<strong>2.2.3.2</strong>)</a> 中已有介绍, 这里我画了个示意图, 以便大家更好理解</p>
<p><img src="/img/remote/1460000014761031" alt="跨组件通信示意图" title="跨组件通信示意图"><br><p align="center"><font color=gray>跨组件通信示意图</font></p></p>
<p>此种服务提供方式叫作 <strong>接口下沉</strong>, 看图的同时请配合阅读 <a href="#2.2.3.2">公共服务(<strong>2.2.3.2</strong>)</a> 中的主要流程便于理解</p>
<p>本方案中还提供有 <strong>EventBus</strong> 来作为服务提供的另一种方式, 大家知道 <strong>EventBus</strong> 因为其解耦的特性, 如果被滥用的话会使项目调用层次结构混乱, 不便于维护和调试, 所以本方案使用 <a href="https://link.segmentfault.com/?enc=%2BprsBUwm9V7aech%2BkVJ8Hw%3D%3D.M66lEgHNOR4DHycAPNpKtltj6UxcZ9YNXCo7pAuakrfMq3OlcD%2FJKIredwWUzbSR" rel="nofollow"><strong>AndroidEventBus</strong></a> 其独有的 <strong>Tag</strong>, 可以在开发时更容易定位发送事件和接受事件的代码, 如果以组件名来作为 <strong>Tag</strong> 的前缀进行分组, 也可以更好的统一管理和查看每个组件的事件, 当然也不建议大家过多使用 <strong>EventBus</strong></p>
<blockquote><strong>Tips: 每个跨组件通信框架提供服务的方式都不同, 您也可以选择其他框架的服务提供方式</strong></blockquote>
<p><a></a></p>
<h4>2.3.5 跨组件传递复杂数据格式</h4>
<p>在一般情况下基本数据类型就可以满足大多数跨组件传递数据的需求, 但是在某些情况下也会需要传递复杂的自定义数据类型, 传递自定义类型在方案中也提供有两种方式:</p>
<p>第一种在 <a href="#2.2.3.2">公共服务(<strong>2.2.3.2</strong>)</a> 中已提及, 就是在 <strong>公共服务 (CommonService)</strong> 中定义这个自定义类</p>
<p>第二种方式也比较简单, 直接通过解析 <strong>Json</strong> 字符串就可以传递</p>
<p><a></a></p>
<h3>2.4 组件的生命周期</h3>
<p>每个组件 (模块) 在测试阶段都可以独立运行, 在独立运行时每个组件都可以指定自己的 <strong>Application</strong>, 这时组件自己管理生命周期就轻而易举, 比如想在 <strong>onCreate</strong> 中初始化一些代码都可以轻松做到, 但是当进入集成调试阶段, 组件自己的 <strong>Application</strong> 已不可用, 每个组件都只能依赖于宿主的生命周期, 这时每个组件如果需要初始化自己独有的代码, 该怎么办? </p>
<p><a></a></p>
<h4>2.4.1 问题分析</h4>
<p>在集成调试阶段, 宿主依赖所有组件, 但是每个组件却不能依赖宿主, 意思是每个组件根本不知道自己的宿主是谁, 当然也就不能通过访问代码的方式直接调用宿主的方法, 从而在宿主的生命周期里加入自己的逻辑代码</p>
<p>如果直接将每个模块的初始化代码直接复制进宿主的生命周期里, 这样未免过于暴力, 不仅代码耦合不易扩展, 而且代码还极易冲突, 所以修改宿主源码的方式也不可行</p>
<p>所以有没有什么方法可以让每个组件在集成调试阶段都可以独自管理自己的生命周期呢?</p>
<p>其实解决思路很简单, 无非就是在开发时让每个组件可以独立管理自己的生命周期, 在运行时又可以让每个组件的生命周期与宿主的生命周期进行合并 (在不修改或增加宿主代码的情况下完成)</p>
<p><a></a></p>
<h4>2.4.2 可行方案分析</h4>
<p>想在不更改宿主代码的情况下在宿主的生命周期中动态插入每个组件的代码, 这倒有点像 <strong>AOP</strong> 的意思</p>
<p>现有的解决方案大概有三种:</p>
<ol>
<li>在基础层中提供一个用于管理组件生命周期的管理类, 每个组件都手动将自己的生命周期实现类注册进这个管理类, 在集成调试时, 宿主在自己的 <strong>Application</strong> 对应生命周期方法中通过管理类去遍历调用注册的所有生命周期实现类即可</li>
<li>使用 <strong>AnnotationProcessor</strong> 解析注解在编译期间生成源代码自动注册所有组件的生命周期实现类, 然后宿主再在对应的生命周期方法中去调用</li>
<li>使用 <strong>Javassist</strong> 在编译时动态修改 <strong>class</strong> 文件, 直接在宿主的对应生命周期方法中插入每个组件的生命周期逻辑</li>
</ol>
<p>我最后还是选择了第一种方法, 因为后面两种方法虽然使用简单, 还可以自动化的完成所有操作, 非常炫酷, 但是这两种方法技术实现复杂, 在不同的 <strong>Gradle</strong> 版本中还会出现兼容性问题影响整个项目的开发进度, 较难维护, 还会增加编译时间</p>
<p>选择第一种方法虽然增加了几步操作, 但是简单明了, 便与理解和维护, 后续人员加入也可以很快上手, 不受 <strong>Gradle</strong> 版本的影响, 也不会增加编译时间</p>
<p><a></a></p>
<h4>2.4.3 最终执行方案</h4>
<p>第一种方案具体原理也没什么好说的, 比较简单, 大概就是在基础层中定义有生命周期方法 (<strong>attachBaseContext(), onCreate()</strong> ...) 的接口, 每个组件实现这个接口, 然后将实现类注册进基础层的管理器, 宿主通过管理器在对应的生命周期方法中调用所有的接口实现类, 典型的观察者模式, 类似注册点击事件</p>
<p>在 <strong>MVPArms</strong> 中这种方案的实现类叫作 <strong>ConfigModule</strong>, 每个组件都可以声明一个或多个 <strong>ConfigModule</strong> 实现类, 内部实现较为复杂, 实现原理是 <strong>反射</strong> + <strong>代理</strong> + <strong>观察者</strong>, 这个类也是整个 <strong>MVPArms</strong> 框架提供给开发者最重要的类</p>
<p>它可以给 <strong>MVPArms</strong> 框架配置大量的自定义参数, 包括项目中所有生命周期的管理 (<strong>Application</strong>, <strong>Activity</strong>, <strong>Fragment</strong>), 项目中所有网络请求的管理 (<strong>Retrofit</strong>, <strong>Okhttp</strong>, <strong>Glide</strong>),为框架提供了极大的扩展性, 使框架更加灵活</p>
<p><a></a></p>
<h2>3 项目讲解</h2>
<p><img src="/img/remote/1460000014761032" alt="ArmsComponent" title="ArmsComponent"></p>
<blockquote>项目地址 : <a href="https://link.segmentfault.com/?enc=5xDct7FOZSv%2B4t8PrSk2Ow%3D%3D.Ul2E4K7lKeZVCBYFWz4hmCPOgMEck17gJXs3QvhZKSEehSDrGY3pmNDAbfmgA0K4" rel="nofollow">ArmsComponent</a>
</blockquote>
<p><a></a></p>
<h3>3.1 如何让组件独立运行?</h3>
<p>在项目根目录的 <strong>gradle.properties</strong> 中, 改变 <strong>isBuildModule</strong> 的值即可</p>
<pre><code class="gradle">#isBuildModule 为 true 时可以使每个组件独立运行, false 则可以将所有组件集成到宿主 App 中
isBuildModule=true</code></pre>
<p><a></a></p>
<h3>3.2 配置 AndroidManifest</h3>
<p>由于组件在独立运行时和集成到宿主时可能需要 <strong>AndroidManifest</strong> 配置不一样的参数, 比如组件在独立运行时需要其中的一个 <strong>Activity</strong> 配置了 <code><action android:name="android.intent.action.MAIN"/></code> 作为入口, 而当组件集成到宿主中时, 则依赖于宿主的入口, 所以不需要配置 <code><action android:name="android.intent.action.MAIN"/></code>, 这时我们就需要两个不同的 <strong>AndroidManifest</strong> 应对不同的情况</p>
<p>在组件的 <strong>build.gradle</strong> 中加入以下代码, 即可指定不同的 <strong>AndroidManifest</strong>, 具体请看项目代码</p>
<pre><code class="gradle">android {
sourceSets {
main {
jniLibs.srcDirs = ['libs']
if (isBuildModule.toBoolean()) {
manifest.srcFile 'src/main/debug/AndroidManifest.xml'
} else {
manifest.srcFile 'src/main/release/AndroidManifest.xml'
}
}
}
}</code></pre>
<p><a></a></p>
<h3>3.3 配置 ConfigModule(GlobalConfiguration)</h3>
<p><strong>ConfigModule</strong> 在 <a href="#2.4.3">最终执行方案(2.4.3)</a> 中提到过, <strong>GlobalConfiguration</strong> 是实现类, 他可以给框架配置大量的自定义参数</p>
<p>项目 <strong>CommonSDK</strong> 中提供有一个 <strong>GlobalConfiguration</strong> 用于配置每个组件都会用到的公用配置信息, 但是每个组件可能都需要有一些私有配置, 比如初始化一些特有属性, 所以在每个组件中也需要实现 <strong>ConfigModule</strong>, 具体请看项目代码</p>
<p>需要注意的是, 在 <a href="#3.2">配置 AndroidManifest(3.2)</a> 中提到过组件在独立运行时和集成到宿主时所需要的配置是不一样的, 当组件在独立运行时需要在 <strong>AndroidManifest</strong> 中声明自己私有的 <strong>GlobalConfiguration</strong> 和 <strong>CommonSDK</strong> 公有的 <strong>GlobalConfiguration</strong>, 但在集成到宿主时, 由于宿主已经声明了 <strong>CommonSDK</strong> 的公有 <strong>GlobalConfiguration</strong>, 所以在 <strong>AndroidManifest</strong> 只需要声明自己私有的 <strong>GlobalConfiguration</strong>, 这里也说明了 <strong>AndroidManifest</strong> 在不同的情况需要做出不同的配置</p>
<p><a></a></p>
<h3>3.4 RouterHub</h3>
<p><strong>RouterHub</strong> 用来定义路由器的路由地址, 以组件名作为前缀, 对每个组件的路由地址进行分组, 可以统一查看和管理所有分组的路由地址</p>
<p>路由地址的命名规则为 <strong>组件名 + 页面名</strong>, 如订单组件的订单详情页的路由地址可以命名为 <strong>"/order/OrderDetailActivity"</strong></p>
<p><strong>ARouter</strong> 将路由地址中第一个 <strong>'/'</strong> 后面的字符叫作 <strong>Group</strong>, 比如上面的示例路由地址中 <strong>order</strong> 就是 <strong>Group</strong>, 以 <strong>order</strong> 开头的地址都被分配该 <strong>Group</strong> 下</p>
<blockquote><strong>Tips: 切记不同的组件中不能出现名称一样的 Group, 否则会发生该 Group 下的部分路由地址找不到的情况!!!</strong></blockquote>
<p>所以每个组件使用自己的组件名作为 <strong>Group</strong> 是比较好的选择, 毕竟组件不会重名</p>
<p><a></a></p>
<h3>3.5 EventBusHub</h3>
<p><strong>AndroidEventBus</strong> 作为本方案提供的另一种跨组件通信方式 (第一种跨组件通信方式是 <a href="#2.2.3.2">公共服务(<strong>2.2.3.2</strong>)</a>), <strong>AndroidEventBus</strong> 比 <strong>greenrobot</strong> 的 <strong>EventBus</strong> 多了一个 <strong>Tag</strong>, 在组件化中更容定位和管理事件</p>
<p><strong>EventBusHub</strong> 用来定义 <strong>AndroidEventBus</strong> 的 <strong>Tag</strong> 字符串, 以组件名作为 <strong>Tag</strong> 前缀, 对每个组件的事件进行分组</p>
<p><strong>Tag</strong> 的命名规则为 <strong>组件名</strong> + <strong>页面名</strong> + <strong>动作</strong>, <br>比如需要使用 <strong>AndroidEventBus</strong> 通知订单组件的订单详情页进行刷新, 可以将这个刷新方法的 <strong>Tag</strong> 命名为 <strong>"order/OrderDetailActivity/refresh"</strong></p>
<p><a></a></p>
<h3>3.6 在项目中使用多个不同的域名</h3>
<p>在项目中, 有 <strong>知乎</strong> 、<strong>干货集中营</strong>、<strong>稀土掘金</strong> 三个模块, 这三个模块网络接口的域名都不一样, 但是在项目中却可以统一使用框架提供的同一个 <strong>Retrofit</strong> 进行网络请求, 这是怎么做到的呢? 这是采用本人的另一个库 <a href="https://link.segmentfault.com/?enc=Co5P7pHMP%2BT1cb8daR6qzA%3D%3D.R5T2Mq%2FC%2BaNssoyn3YBSQeBs8vqwz6kcdUXvnLMhlIEGkhvWJiFSuhOS%2BEH%2FfFX40nNvC2ySF1ZOrQsqFXAUWA%3D%3D" rel="nofollow">RetrofitUrlManager</a>, 它可以使 <strong>Retrofit</strong> 同时支持多个 <strong>BaseUrl</strong> 以及动态改变 <strong>BaseUrl</strong></p>
<hr>
<p><strong>Hello 我叫Jessyan,如果您喜欢我的文章,可以在以下平台关注我</strong></p>
<ul>
<li>GitHub: <a href="https://link.segmentfault.com/?enc=HrebdKhm3%2FQnG1U%2BCGTXyA%3D%3D.kcyU1Eon9qB7pk3kUSsCkMWVYxaU12Nzqn7c%2FZmSsS%2Fl98VfMw4ZIRxaaLMpetln" rel="nofollow">https://github.com/JessYanCoding</a>
</li>
<li>掘金: <a href="https://link.segmentfault.com/?enc=z2khnck%2BOiXlCRZSyi%2FTFw%3D%3D.uzrwXiKJMrATbFu16ha8zHB39r3uEdgkNLT6GmknherOfHG0NeEKHZ4kdjiF3STlU30lEpd4H2LALa96SRzItQ%3D%3D" rel="nofollow">https://gold.xitu.io/user/57a...</a>
</li>
<li>简书: <a href="https://link.segmentfault.com/?enc=6pDacrCU6VLdf2GPg%2BfmKg%3D%3D.1PuyTWIe8qzCGSyvqwXjf2ZVfncRgDY5onZVh0goRwSlnJUS5DLI%2BArv4A4a8Ieb" rel="nofollow">http://www.jianshu.com/u/1d0c...</a>
</li>
<li>微博: <a href="https://link.segmentfault.com/?enc=zE8C6ixWY27OSHUi6BFhSg%3D%3D.IeunPlssIVc%2Fh%2Bk2%2FNWIb4ZukTZUhIqAEOwffITIZbo%3D" rel="nofollow">http://weibo.com/u/1786262517</a>
</li>
</ul>
<p>-- The end</p>
改造 Android 官方架构组件 ViewModel
https://segmentfault.com/a/1190000012427528
2017-12-14T16:16:57+08:00
2017-12-14T16:16:57+08:00
JessYanCoding
https://segmentfault.com/u/jessyancoding
1
<p>原文地址: <a href="https://link.segmentfault.com/?enc=YuFphhHvPqVeLCnrEd4rKg%3D%3D.yMHFCrngZBK7lUpmskJVK1nQtUdt6a3RPEwmWq3iD9acGmt%2Bp40%2BYUrQLD4%2B5tzw" rel="nofollow">http://www.jianshu.com/p/963a...</a></p>
<h2>前言</h2>
<p>Android 官方架构组件在今年 5 月份 Google I/O 大会上被公布, 直到 11 月份一直都是测试版, 由于工作比较繁忙, 期间我只是看过类似的文章, 但没有在实际项目中使用过, 更没有看过源码, 所以对这几个组件的使用很是生疏, 同时也觉得这几个组件非常高大上, 非常神秘!</p>
<p>直到 11 月份 Android 官方架构组件正式版发布, 并且 Google 也在 <strong>Support Library v26.1.0</strong> 以后的版本中内嵌了 Android 官方架构组件中的生命周期组件, 我想, 这是趋势, 既然 Google 这么推崇, 那我也是时候学习一波并将它们引入 <a href="https://link.segmentfault.com/?enc=z7WgZnZeWCjGiZv%2Bo9y68g%3D%3D.xp6tBo%2BJFJmQJ0YSEtW6VsMzT6VamqBmt7YgIMaKbmFUjWM5jyNpgAdV%2FJZxoNLJ" rel="nofollow"><strong>MVPArms</strong></a> 框架了</p>
<blockquote>Github : <a href="https://link.segmentfault.com/?enc=ngdPZRhxaKE9AtYLSL08JA%3D%3D.VDajVwjTWK0tdRUW1lyxPcf49TYX%2FMe4s9VU3PqMqSf%2BPvVynSrAg%2Bs7QVnwQOrO" rel="nofollow">你的 Star 是我坚持的动力 ✊</a>
</blockquote>
<h2>简单介绍</h2>
<p>因为想将 Android 官方架构组件引入 <a href="https://link.segmentfault.com/?enc=ProGGCuaFT2v36qfFafEpw%3D%3D.YNne6nVW5wMsyYOXF5hZTtkMrGCE0YFMIpoQwKirfRx88Bbn8xmX3avu9r4mNjb%2B" rel="nofollow"><strong>MVPArms</strong></a> 框架之中, 所以我认真学习了 Android 官方架构组件中除了 <strong>Room</strong> 之外的所有源码, 以考察是否整个组件都适合引入 <a href="https://link.segmentfault.com/?enc=VdtDqru9UXT5HhXYCqa%2BEQ%3D%3D.h6iFtfFPis92NOnoIOxa1%2Ftq3w%2FBJpckJMLMpl%2BLbSbQgM99%2F0u%2BNH2khDoP9AO%2B" rel="nofollow"><strong>MVPArms</strong></a> 框架</p>
<p>在学习完源码过后, 发现 Android 官方架构组件其实并没有想象的那么高深, 原理反而是我们在日常开发中都会用到的知识点, 那我就在文章的开头先简单的介绍下 Android 官方架构组件中的这几个组件</p>
<h3>Lifecycles</h3>
<p>生命周期组件是 Android 官方架构组件中的核心组件, 它可以使各种实例作为观察者与 <strong>Activity</strong> 和 <strong>Fragment</strong> 等具有生命周期特性的组件绑定在一起, <strong>LiveData</strong> 和 <strong>ViewModel</strong> 都是基于此组件, 简而言之就是, 你将需要绑定生命周期的实例注册给该组件, 该组件就会在你指定的某个生命周期方法执行时通知这个实例</p>
<p>应用场景很多, 比如之前在 <strong>MVP</strong> 架构中, 你需要在 <strong>Activity</strong> 执行 <strong>onCreate</strong> 时, 让 <strong>Presenter</strong> 初始化一些操作, 这时就不用在 <strong>Activity</strong> 的 <strong>onCreate</strong> 中再调用 <strong>Presenter</strong> 的某个初始化方法了, 直接使用官方的生命周期组件即可完成, 在 <strong>Activity</strong> 执行 <strong>onDestroy</strong> 时需要释放一些对象的资源, 也可以使用到生命周期组件</p>
<h3>LiveData</h3>
<p><strong>LiveData</strong> 具有两个功能, 第一个功能是观察者模式, 在 <strong>Value</strong> 发生变化时通知之前注册的所有观察者, 第二功能是基于生命周期组件与 <strong>Activity</strong> 和 <strong>Fragment</strong> 等具有生命周期特性的组件绑定在一起, 在生命周期发生改变时停止或恢复之前的事件</p>
<p>简而言之就是, 当某个页面请求网络数据成功后需要同步 <strong>UI</strong>, 但这个页面已经不可见, 这时就会停止同步 <strong>UI</strong> 的操作</p>
<h3>ViewModel</h3>
<p><strong>ViewModel</strong> 有两个功能, 第一个功能可以使 <strong>ViewModel</strong> 以及 <strong>ViewModel</strong> 中的数据在屏幕旋转或配置更改引起的 <strong>Activity</strong> 重建时存活下来, 重建后数据可继续使用, 第二个功能可以帮助开发者轻易实现 <strong>Fragment</strong> 与 <strong>Fragment</strong> 之间, <strong>Activity</strong> 与 <strong>Fragment</strong> 之间的通讯以及共享数据</p>
<h2>浅析官方架构组件</h2>
<p>用法就不多说了, 此类文章和 <strong>Demo</strong> 太多了, 明白了它们的功能和应用场景后, 我们才知道它们是否真的适合自己的需求, 而不是盲目跟风, 下面我就来分析下我是如何考察新技术, 以及如何判断这些新技术是否有必要应用到自己的项目中</p>
<h3>Lifecycles</h3>
<p>上面介绍了生命周期组件的功能, 这里就来分析一下生命周期组件是否有必要引入我的框架 <a href="https://link.segmentfault.com/?enc=AHQXrW%2BtUkOfTXJ0smGLBg%3D%3D.PBC09DvYK9lU5GmdZg7XZv%2FAZg3xsJk9zMmNNDQgDr5l5n5XScO6vDwuOEWtbHVD" rel="nofollow"><strong>MVPArms</strong></a></p>
<p>说到生命周期我就想到了我之前在 <a href="https://link.segmentfault.com/?enc=vVlD1AG%2BUvUr5Giqk2kDTA%3D%3D.7HsNk0bCAXJfGqv6bciEL0sCz1UP5NJGs7Ntc6fju3gwMQ%2FGsaCWpF6dRQY%2BJaqi" rel="nofollow">传统MVP用在项目中是真的方便还是累赘?</a> 中讨论的一个内容</p>
<p>现在市面上流行的 <strong>MVP</strong> 架构有两种, <strong>第一种是将 Activity 或 Fragment 作为 View, 抽象一个 Presenter 层出来</strong>, <strong>第二种是将 Activity 或 Fragment 作为 Presenter, 抽象一个 View 层出来</strong></p>
<p>第一种类型代表的框架有 <a href="https://link.segmentfault.com/?enc=lfG6bqsOmoI3BsDM%2F0Xnqw%3D%3D.MCFslRQ6iljmrEG2R5ui41G1QUcEGlVKjgZrWtStmKCxdny3LJL60kHfogVc3hbL" rel="nofollow"><strong>MVPArms</strong></a>, 第二种类型代表的框架有 <a href="https://link.segmentfault.com/?enc=JQwZr5z7vVQu6XNod2F46g%3D%3D.cG57lyFJY2vDxWV5egzFMVdYMb2h37X3Pu35gX0YEUI%3D" rel="nofollow"><strong>TheMVP</strong></a>, 当然第一种类型的 <strong>MVP</strong> 架构在市面上用的是最多的, 那么第二种类型的优点是什么呢? </p>
<p>我在上面这篇文章也说过, 主要优势有两个, <strong>方便重用View</strong>, 以及 <strong>可直接与 Activity 或 Fragment 的生命周期做绑定</strong>, 这样就可以直接使用 Activity 或 Fragment 的生命周期, 不用再去做多余的回调, 当然也有缺点, 我在文章中也有介绍, 有兴趣的可以去看看</p>
<p>第一种类型的 <strong>MVP</strong> 架构是不具有可以和 <strong>Activity</strong> 或 <strong>Fragment</strong> 的生命周期直接做绑定的优势的, 所以很是嫉妒第二种类型的 <strong>MVP</strong> 架构, 这也是两种类型的 <strong>MVP</strong> 架构最大的区别, 但你想的没错, 现在使用生命周期组件就可以使第一种类型的 <strong>MVP</strong> 架构很轻易的具有绑定生命周期的优势, 现在第一种类型的 <strong>MVP</strong> 架构将如虎添翼</p>
<p>经过以上的分析, 我认为生命周期组件对于我的框架来说是很有必要的, 这将使日常开发更加便捷</p>
<h3>LiveData</h3>
<p><strong>LiveData</strong> 与 <strong>RxJava</strong> 都是基于观察者模式, 功能上也有重合, Google 在官方文档上也明确表示, 如果你正在使用 <strong>RxJava</strong>, <strong>Agera</strong> 等类似功能的库, 只要你能正确的处理数据流的生命周期, 就完全可以继续使用它们来替代 <strong>LiveData</strong></p>
<blockquote>Note: If you are already using a library like RxJava or Agera, you can continue using them instead of LiveData. But when you use them or other approaches, make sure you are handling the lifecycle properly such that your data streams pause when the related LifecycleOwner is stopped and the streams are destroyed when the LifecycleOwner is destroyed. You can also add the android.arch.lifecycle:reactivestreams artifact to use LiveData with another reactive streams library (for example, RxJava2).</blockquote>
<p>从官方文档可以看出 Google 对此的建议就是 <strong>RxJava</strong>, <strong>Agera</strong>, <strong>LiveData</strong> 等类似功能的库, 你只使用一个即可</p>
<h4>选择 RxJava 还是 LiveData ?</h4>
<p><strong>LiveData</strong> 和 <strong>RxJava</strong> 的功能的确过于重合, 我也十分赞同 Google 官方的建议, 两者之中选择其一就可以了, 没必要两者都引入项目, 而 <a href="https://link.segmentfault.com/?enc=dqA1s5wrraXrWYXArQLbOg%3D%3D.D0fZZxiB328jXr1Om5JSktEgi1MqM3BEGja4ZB6Vkv%2BsCir9PFA0Ar1IIsB7k%2BUU" rel="nofollow"><strong>MVPArms</strong></a> 框架, 也正好引入了 <strong>RxJava</strong>, 所以我也来分析分析在 <a href="https://link.segmentfault.com/?enc=4sDrwDDxjKEaBLFXtMfcgA%3D%3D.nOhldjeQPbCjJWxa0yQ4G71gqV%2F%2F00B3Ra0zy11qGq3bBtPoD6Gi3Q4UQ6Rwnk53" rel="nofollow"><strong>MVPArms</strong></a> 框架中该选择 <strong>LiveData</strong> 还是 <strong>RxJava</strong>?</p>
<p>于是我认真的研究了其源码, <strong>LiveData</strong> 具有两个功能, 通知观察者更新数据和根据生命周期停止和恢复之前的事件, 而 <strong>Rxjava</strong> 加上 <strong>RxLifecycle</strong>, <strong>RxJava</strong> 加上 <strong>AutoDispose</strong>, 或 <strong>Rxjava</strong> 加上生命周期组件, 也可以轻易做到根据生命周期停止和恢复之前的事件, 在配上 <strong>Rxjava</strong> 强大的操作符, <strong>LiveData</strong> 能做的事 <strong>RxJava</strong> 都能做, <strong>LiveData</strong> 不能做的事 <strong>RxJava</strong> 也能做</p>
<p>并且 <strong>RxJava</strong> 不仅仅只是 <strong>RxJava</strong>, 他还是一个庞大的生态链, 他还有 <strong>RxCache</strong>, <strong>RxLifecycle</strong>, <strong>RxAndroid</strong>, <strong>RxPermission</strong>, <strong>Retrofit-Adapter</strong> 等大量并且强大的衍生库, 我们离开它做很多事都非常不便, 刚刚出生, 羽翼未丰的 <strong>LiveData</strong> 相比于 <strong>RxJava</strong> 将没有任何优势, 甚至显得非常简陋</p>
<p>因此 <strong>LiveData</strong> 和 <strong>RxJava</strong> 之间如果只能选择一个的话, 我没有任何理由选择 <strong>LiveData</strong></p>
<h3>ViewModel</h3>
<p><strong>ViewModel</strong> 中有一个功能让我十分惊艳, 也十分好奇, 它可以使 <strong>ViewModel</strong> 以及 <strong>ViewModel</strong> 中的数据在屏幕旋转或配置更改引起的 <strong>Activity</strong> 重建时存活下来, 重建后数据可继续使用, 这个功能十分实用且十分重要, 因为之前也没有一个官方解决方案, 所以我觉得很有必要将这个功能引入 <a href="https://link.segmentfault.com/?enc=ev18V8MYtGJCjYc0rBId2A%3D%3D.03jTJp30R0WDsZVj%2F0BskZnTqPc5tvO61KV3Imwq8152V%2FmgEOb%2BStmxnTIl4Sm5" rel="nofollow"><strong>MVPArms</strong></a> 框架</p>
<p>同样另外一个功能, 它还可以帮助开发者轻易实现 <strong>Fragment</strong> 与 <strong>Fragment</strong> 之间, <strong>Activity</strong> 与 <strong>Fragment</strong> 之间的通讯以及共享数据, 同样也正是我所需要的官方解决方案</p>
<p>但在我继续深入研究, 准备将它引入到项目中时, 却发现 Google 将这个功能做了高度封装并限制了它的使用范围, 只能用于 <strong>ViewModel</strong></p>
<p>但我想 Google 既然能让 <strong>MVVM</strong> 框架中的 <strong>ViewModel</strong> 具有这些功能, 那我为什么不能将这个功能扩展出来提供给 <strong>MVP</strong> 框架中的 <strong>Presenter</strong>, 乃至其他更多的模块?</p>
<p>于是我认真的研究了其源码, 准备通过修改源码并封装成库的方式, 让更多的开发者在更多的场景下能够使用到这些功能</p>
<h2>改造 ViewModel 组件</h2>
<p>要想改造 <strong>ViewModel 组件</strong> 自然要对它的整个源码分析一遍, 知道其原理, 才知道如何下手</p>
<h3>分析源码</h3>
<p>篇幅有限, 就来简单的分析下源码把, 源码其实也就几个类, 经过了层层封装, 核心代码就在一个叫做 <strong>HolderFragment</strong> 的 <strong>Fragment</strong> 中, </p>
<p>在我看来 <strong>ViewModel 组件</strong> 的核心原理也就是 <strong>HolderFragment</strong> 中的一行代码实现的:</p>
<pre><code class="java">setRetainInstance(true);</code></pre>
<p><strong>setRetainInstance(boolean)</strong> 是 <strong>Fragment</strong> 中的一个方法, 我想很多人应该都知道这个方法的意义</p>
<p>简单来说将这个方法设置为 true 就可以使当前 <strong>Fragment</strong> 在 <strong>Activity</strong> 重建时存活下来, 如果不设置或者设置为 false, 当前 <strong>Fragment</strong> 会在 <strong>Activity</strong> 重建时同样发生重建, 以至于被新建的对象所替代 </p>
<p>意思是只要将这个方法设置为 true, <strong>Fragment</strong> 以及 <strong>Fragment</strong> 之中的所有数据都会在 <strong>Activity</strong> 重建时存活下来</p>
<p>这时我们在 <strong>setRetainInstance(boolean)</strong> 为 true 的 <strong>Fragment</strong> 中放一个专门用于存储 <strong>ViewModel</strong> 的 <strong>Map</strong>, 自然 <strong>Map</strong> 中所有的 <strong>ViewModel</strong> 都会幸免于 <strong>Activity</strong> 重建</p>
<p>于是我们让 <strong>Activity</strong>, <strong>Fragment</strong> 都绑定一个这样的 <strong>Fragment</strong>, 将 <strong>ViewModel</strong> 存放到这个 <strong>Fragment</strong> 的 <strong>Map</strong> 中, <strong>ViewModel 组件</strong> 就这样实现了</p>
<h3>如何改造</h3>
<p>想要知道如何改造, 那我们就要明确这次改造的最终目的是什么, 我们的目的就是要让 <strong>ViewModel 组件</strong> 能用于 <strong>Presenter</strong>, 乃至其他更多的模块, 不止是用于 <strong>ViewModel</strong></p>
<p>那为什么 Google 官方的 <strong>ViewModel 组件</strong> 不能用于其他模块呢, 通过阅读源码可以知道, 是因为 <strong>Google</strong> 把上文提到的 <strong>Map</strong>, 封装了起来, 并没有提供出去, 并且限制了 <strong>ViewModel</strong> 的构建方式</p>
<p><strong>ViewModel 组件</strong> 让一个新的 <strong>ViewModel</strong> 必须继承于它的基类, 并且让开发者必须提供一个 <strong>Factory</strong> 指明当前 <strong>ViewModel</strong> 的构建方式, <strong>ViewModel 组件</strong> 会在合适的时机, 主动去根据 <strong>Factory</strong> 构建 <strong>ViewModel</strong> 实例, 并放入 <strong>Map</strong> 中</p>
<p>这时整个构建过程都被 <strong>ViewModel 组件</strong> 掌控并被限制于 <strong>ViewModel</strong>, 所以我需要做的就是将 <strong>Map</strong> 和 <strong>ViewModel</strong> 的构建方式扩展出来, 将更多的控制权交给外部的开发者</p>
<h3>实践</h3>
<p>经过上面的分析, 思路和方案都有了, 接下来就剩下如何把思路和方案实现了</p>
<p>于是我结合上文分析的思路和方案对官方源码进行了改造并做了适当的优化, <strong>LifecycleModel</strong> 就这样诞生了</p>
<p>这篇文章主要还是讲在完成一个目标前, 在从 0 到 1 期间进行的思路和分析的过程, 至于细节你如果感兴趣的话还是去看我的源码把, 哈哈, 注释很详细哦!</p>
<blockquote>Github : <a href="https://link.segmentfault.com/?enc=rS7zyHxy3MwsWnJHYZsltw%3D%3D.VEhJab3WKKXXCNyJtSljfz%2BCyWesa77Ny6uOlawbCnGVx%2By9Lp2jh1esEbOnO2Dl" rel="nofollow">你的 Star 是我坚持的动力 ✊</a>
</blockquote>
<h2>总结</h2>
<p>一个新技术是否真的适合自己还是需要自己去考察, 不应该盲目跟风, 如果你只知道这个技术很火然后去用它, 不知道为什么用它, 用它的好处, 那你就会一直陷入被动学习的窘境, 一直在学习, 但是总觉得自己跟不上时代的进步, 担惊受怕, 这是现代技术人大部分都存在的处境</p>
<p>至于最近闹的沸沸扬扬的简书 <strong>饱醉豚</strong> 事件, 自从简书 <strong>CEO</strong> 站台后, 已经不再是当事人一个人的事, 而是关乎到简书整个平台, 既然这个 <strong>CEO</strong> 这么傲气, 这个平台都不在乎我们这个群体, 我们也不再去关注这个平台就是了, 流量是跟着原创作者走还是跟着平台, 自己心里没点逼数吗?</p>
<p>简书以及简书 <strong>CEO</strong> 最好做出深刻的道歉, 否则我也会离开简书 (好像我更文频率也不是很高把? 咳咳... 我主打的是质量! 质量! 不是数量, 逃~)</p>
<h2>踩坑</h2>
<p>在实际项目中使用 <strong>ViewModel 组件</strong> 时我也遇到了一些问题, 浪费了我很多时间, 所以有必要分享出来让大家少走弯路</p>
<h4>通过 Activity 获取 ViewModel 时遇到的坑:</h4>
<ul>
<li>在 Application.ActivityLifecycleCallbacks 中的 onActivityCreated 方法中获取 ViewModel 时, Activity 每重建一次, 获取的 ViewModel 都是重新构建后的新实例, 并不能让 ViewModel 以及 ViewModel 中的数据幸免于 Activity 重建, 所以不要此方法中获取 ViewModel</li>
<li>在 Activity 的 onDestroy 方法中不能获取 ViewModel, 会报错</li>
</ul>
<h4>通过 Fragment 获取 ViewModel 时遇到的坑:</h4>
<ul>
<li>在 FragmentManager.FragmentLifecycleCallbacks 中的 onFragmentAttached 方法中获取 ViewModel 时也会出现和 Activity 一样的情况, 获取的 ViewModel 是重新构建后的新实例, ViewModel 以及 ViewModel 中的数据不能幸免于 Activity 重建, 所以也不要此方法中获取 ViewModel</li>
<li>在 FragmentManager.FragmentLifecycleCallbacks 中的 onFragmentDestroyed 方法中也不能获取 ViewModel, 会报错</li>
<li>在 Fragment 的 onDestroy 方法中不能获取 ViewModel, 会报错</li>
</ul>
<hr>
<p><strong>Hello 我叫Jessyan,如果您喜欢我的文章,可以在以下平台关注我</strong></p>
<ul>
<li>GitHub: <a href="https://link.segmentfault.com/?enc=oRv6SqUAuIOMDQnz5sovdQ%3D%3D.83NAaKbU%2FN4WAN2PZWWLM8cVkr3cg5jg%2FhfPEhv%2Fk2g240TBMqaynZzm5A2BuGke" rel="nofollow">https://github.com/JessYanCoding</a>
</li>
<li>掘金: <a href="https://link.segmentfault.com/?enc=3xBJIYy3YXVC2tt%2F%2BJdA4A%3D%3D.iPHswlAFXuAaZsl8J9BD%2F6%2FSABtEOHsScp5DmeCSb0QZ0waHbtSvCegmCkAy4zhhUt21qg7dUvMnXXUYEbb79Q%3D%3D" rel="nofollow">https://gold.xitu.io/user/57a...</a>
</li>
<li>简书: <a href="https://link.segmentfault.com/?enc=gvWcJexJK8QdvCdUp6dDcw%3D%3D.UG%2Fg7KnfaaooE1dHz3HOZy8tdX5wCC%2BoHwkjyzylKKz2d64lRcSEeTv5A7Qs9Jik" rel="nofollow">http://www.jianshu.com/u/1d0c...</a>
</li>
<li>微博: <a href="https://link.segmentfault.com/?enc=ft4Feybu38D2mxcWUylvFw%3D%3D.fF2P1ic4f2lkLzEa%2FhklebVflxGuYuR7rLzq6rfHrNA%3D" rel="nofollow">http://weibo.com/u/1786262517</a>
</li>
</ul>
<p>-- The end</p>