<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Busy.Im</title><link>https://busy.im/</link><description>Recent content on Busy.Im</description><generator>Hugo -- gohugo.io</generator><lastBuildDate>Tue, 20 Oct 2020 21:23:48 +0800</lastBuildDate><atom:link href="https://busy.im/index.xml" rel="self" type="application/rss+xml"/><item><title>Android 自动化编译之 Github Actions</title><link>https://busy.im/post/github-actions-android-ci/</link><pubDate>Tue, 20 Oct 2020 21:23:48 +0800</pubDate><guid>https://busy.im/post/github-actions-android-ci/</guid><description>
&lt;p&gt;&lt;a href=&#34;https://github.com/features/actions&#34;&gt;Actions&lt;/a&gt; 是 Github 2019年推出的自动化集成编译工具，在漫长的 Beta 等待后我也在去年获得了优先体验，当时也写了 &lt;a href=&#34;https://github.com/xdtianyu/actions-android-ci/releases&#34;&gt;actions-android-ci&lt;/a&gt; 的第一个版本，用来编译 &lt;a href=&#34;https://github.com/xdtianyu/CallerInfo&#34;&gt;CallerInfo&lt;/a&gt; 项目。&lt;/p&gt;
&lt;p&gt;最近在写一个私有项目，自动化 CI 自然是必不可少的，为了简单方便，托管在了 Github。使用去年编写的 &lt;a href=&#34;https://github.com/xdtianyu/actions-android-ci/releases/tag/v1.2.1&#34;&gt;ci@v1.2.1&lt;/a&gt; 很快完成了编译，同时增加了对每次编译 &lt;code&gt;apk&lt;/code&gt; 和 &lt;code&gt;mapping.txt&lt;/code&gt; 上传到 &lt;code&gt;artifacts&lt;/code&gt; 的支持。&lt;/p&gt;
&lt;p&gt;但是每次编译都需要将近 8 分钟，而每个月只有 2000 分钟的免费编译时长，因此减少编译时间很重要。查看编译的日志，发现有 30 秒在编译 docker 镜像，还有 6 分钟都在编译，其中大部分时间都在下载 gradle 依赖。&lt;/p&gt;
&lt;p&gt;所以通过预编译 docker 镜像和增加 sdk 及 gradle 依赖缓存可以大幅减少编译运行时间。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/github-actions/github-actions.png&#34; alt=&#34;actions build&#34; /&gt;&lt;/p&gt;
&lt;h3 id=&#34;集成编译优化过程&#34;&gt;集成编译优化过程&lt;/h3&gt;
&lt;h4 id=&#34;1-使用预编译-docker-镜像&#34;&gt;1. 使用预编译 docker 镜像&lt;/h4&gt;
&lt;p&gt;在 v1.2.1 版本， Github Actions 在每次运行时都会从 &lt;a href=&#34;https://github.com/xdtianyu/actions-android-ci/tree/v1.2.1&#34;&gt;actions-android-ci&lt;/a&gt; 仓库拉取最新的代码，然后根据 &lt;code&gt;action.yml&lt;/code&gt; 文件中的配置，编译新的 docker 镜像。由于这个镜像包括 openjdk 等依赖，每次编译都会去拉取大量数据并重新编译 docker image：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-yml&#34; data-lang=&#34;yml&#34;&gt;runs:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;using:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;docker&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;image:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;Dockerfile&amp;#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;直接从 docker hub 拉起最新的镜像则可以减少每次制作新镜像的耗时：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-yml&#34; data-lang=&#34;yml&#34;&gt;runs:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;using:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;docker&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;image:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;docker://xdtianyu/actions-android-ci&amp;#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;2-增加-sdk-及-gradle-缓存&#34;&gt;2. 增加 sdk 及 gradle 缓存&lt;/h4&gt;
&lt;p&gt;这个 docker 复用了 Gitlab CI，而当时为了优化在 Gitlab runner 中编译，已经增加了缓存的支持，只是存储在了 &lt;code&gt;/opt/cache/gradle&lt;/code&gt; &lt;code&gt;/opt/sdk&lt;/code&gt; &lt;code&gt;/opt/ndk&lt;/code&gt; 几个目录下，在 Gitlab Runner 运行时是预先挂载了宿主机的这几个目录来实现 runner 缓存的。&lt;/p&gt;
&lt;p&gt;而 Github 目前是不支持使用 &lt;code&gt;docker volume&lt;/code&gt; 挂载参数的，所以需要将这几个目录设定在可以访问的目录下，最后缓存这几个目录。所以重新编写了下载 sdk 的脚本和环境变量：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;&lt;span style=&#34;color:#366&#34;&gt;echo&lt;/span&gt; &lt;span style=&#34;color:#033&#34;&gt;$GITHUB_WORKSPACE&lt;/span&gt;
&lt;span style=&#34;color:#033&#34;&gt;SDK&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;/opt/sdk
&lt;span style=&#34;color:#033&#34;&gt;NDK&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;/opt/ndk
&lt;span style=&#34;color:#033&#34;&gt;GRADLE&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;/opt/cache/gradle
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;[&lt;/span&gt; ! -z &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$GITHUB_WORKSPACE&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;]&lt;/span&gt;; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;then&lt;/span&gt;
&lt;span style=&#34;color:#033&#34;&gt;SDK&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$GITHUB_WORKSPACE&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;/.opt/sdk&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#033&#34;&gt;NDK&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$GITHUB_WORKSPACE&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;/.opt/ndk&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#033&#34;&gt;GRADLE&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$GITHUB_WORKSPACE&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;/.opt/cache/gradle&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fi&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# ... setup sdk ...&lt;/span&gt;
&lt;span style=&#34;color:#366&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#033&#34;&gt;PATH&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:&lt;span style=&#34;color:#033&#34;&gt;$SDK&lt;/span&gt;/emulator/:&lt;span style=&#34;color:#033&#34;&gt;$SDK&lt;/span&gt;/tools/bin:&lt;span style=&#34;color:#033&#34;&gt;$SDK&lt;/span&gt;/tools:&lt;span style=&#34;color:#033&#34;&gt;$SDK&lt;/span&gt;/platform-tools:&lt;span style=&#34;color:#033&#34;&gt;$NDK&lt;/span&gt;:&lt;span style=&#34;color:#033&#34;&gt;$SDK&lt;/span&gt;/cmake/3.10.2.4988404/bin
&lt;span style=&#34;color:#366&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#033&#34;&gt;ANDROID_HOME&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$SDK&lt;/span&gt;
&lt;span style=&#34;color:#366&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#033&#34;&gt;ANDROID_SDK&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$SDK&lt;/span&gt;
&lt;span style=&#34;color:#366&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#033&#34;&gt;ANDROID_SDK_ROOT&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$SDK&lt;/span&gt;
&lt;span style=&#34;color:#366&#34;&gt;export&lt;/span&gt; &lt;span style=&#34;color:#033&#34;&gt;GRADLE_USER_HOME&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$GRADLE&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;当有 &lt;code&gt;GITHUB_WORKSPACE&lt;/code&gt; 环境变量时，说明当前运行环境为 Github Actions 环境，需要将 sdk 目录设定在 &lt;code&gt;$GITHUB_WORKSPACE/.opt/sdk&lt;/code&gt; 目录下。完整内容请阅读代码 &lt;a href=&#34;https://github.com/xdtianyu/actions-android-ci/blob/v1.3.0/setup-android-sdk.sh&#34;&gt;setup-android-sdk.sh&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;编译新的镜像并上传到 docker hub： &lt;a href=&#34;https://hub.docker.com/r/xdtianyu/actions-android-ci&#34;&gt;xdtianyu/actions-android-ci&lt;/a&gt; (这里使用了 docker hub 的自动编译)。&lt;/p&gt;
&lt;p&gt;然后在 &lt;code&gt;.github/workflows/android.yml&lt;/code&gt; 编译前增加缓存配置：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-yml&#34; data-lang=&#34;yml&#34;&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;name:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;Cache&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;gradle&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;and&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;sdk&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;uses:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;actions/cache@v2&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;env:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;cache-name:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;cache-gradle-and-sdk&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;with:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;path:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30;font-style:italic&#34;&gt;|
&lt;/span&gt;&lt;span style=&#34;color:#c30;font-style:italic&#34;&gt; ${{ github.workspace }}/.opt/cache/gradle/wrapper&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;${{&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;github.workspace&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;}}/.opt/cache/gradle/caches&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;${{&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;github.workspace&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;}}/.opt/sdk&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;key:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;${{&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;runner.os&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;}}-build-${{&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;env.cache-name&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;}}-${{&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;hashFiles(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;**/wrapper/gradle-wrapper.properties&amp;#39;&lt;/span&gt;,&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;**/build.gradle&amp;#39;&lt;/span&gt;)&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;}}&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;restore-keys:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30;font-style:italic&#34;&gt;|
&lt;/span&gt;&lt;span style=&#34;color:#c30;font-style:italic&#34;&gt; ${{ runner.os }}-build-${{ env.cache-name }}-&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;${{&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;runner.os&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;}}-build&lt;span style=&#34;color:#c30;font-style:italic&#34;&gt;-
&lt;/span&gt;&lt;span style=&#34;color:#c30;font-style:italic&#34;&gt; ${{ runner.os }}-&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这样，每次编译时都会下载 &lt;code&gt;.opt/sdk&lt;/code&gt; 等目录下的缓存文件，并在编译完成后检查缓存是否需要更新，如需更新则上传新的目录。&lt;/p&gt;
&lt;p&gt;这里的缓存校验使用了 &lt;code&gt;build.gradle&lt;/code&gt; 和 &lt;code&gt;gradle-wrapper.properties&lt;/code&gt; ，如果这些文件被修改则说明需要更新缓存，否则跳过新建缓存的过程。可以根据自己需要调整这里的校验配置。&lt;/p&gt;
&lt;h4 id=&#34;3-加密的环境变量-secrets&#34;&gt;3. 加密的环境变量 Secrets&lt;/h4&gt;
&lt;p&gt;可以在项目 &lt;code&gt;Settings -&amp;gt; Secrets&lt;/code&gt; 中配置加密的环境变量，这里配置的环境变量很安全，有&lt;a href=&#34;https://docs.github.com/en/free-pro-team@latest/actions/reference/encrypted-secrets&#34;&gt;特殊处理&lt;/a&gt;，不会在日志中显示。同时也不会传递由 &lt;code&gt;Fork&lt;/code&gt; 发起的 &lt;code&gt;Pull Request&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/github-actions/github-secrets.png&#34; alt=&#34;secrets&#34; /&gt;&lt;/p&gt;
&lt;p&gt;我习惯性的使用了上图中的5个环境变量，用来将发布签名加密保存在代码仓库中。其中 &lt;code&gt;ENCRYPTED_IV&lt;/code&gt; 和 &lt;code&gt;ENCRYPTED_KEY&lt;/code&gt; 是使用 openssl 加密的 &lt;code&gt;secrets.tar.enc&lt;/code&gt; 文件相关密钥。通过如下命令解密：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-bash&#34; data-lang=&#34;bash&#34;&gt;openssl aes-256-cbc -K &lt;span style=&#34;color:#033&#34;&gt;$ENCRYPTED_KEY&lt;/span&gt; -iv &lt;span style=&#34;color:#033&#34;&gt;$ENCRYPTED_IV&lt;/span&gt; -in secrets.tar.enc -out secrets.tar -d&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;如果不需要，可以在参考代码脚本中移除这部分内容。&lt;/p&gt;
&lt;p&gt;在这个脚本中有详细的说明可以参考学习： &lt;a href=&#34;https://github.com/xdtianyu/CallerInfo/blob/master/.travis/env.sh#L14&#34;&gt;.travis/env.sh&lt;/a&gt;&lt;/p&gt;
&lt;h4 id=&#34;4-增加-artifacts-上传&#34;&gt;4. 增加 artifacts 上传&lt;/h4&gt;
&lt;p&gt;每次自动编译都会生成 apk 文件，可以将其和混淆文件一起发布出来提供下载。只需要增加如下 artifacts 配置即可完成对每次编译 &lt;code&gt;apk&lt;/code&gt; 和 &lt;code&gt;mapping.txt&lt;/code&gt; 文件上传。类似于 &lt;code&gt;Gitlab job&lt;/code&gt;，在每个 &lt;code&gt;Action&lt;/code&gt; 编译成功后都会有一个 &lt;code&gt;Artifacts&lt;/code&gt; 链接能用来下载：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-yml&#34; data-lang=&#34;yml&#34;&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;name:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;Upload&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;artifacts&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;uses:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;actions/upload-artifact@v2&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;with:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;name:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;artifacts&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;path:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30;font-style:italic&#34;&gt;|
&lt;/span&gt;&lt;span style=&#34;color:#c30;font-style:italic&#34;&gt; app/**/apk/release/*&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;app/&lt;span style=&#34;color:#099&#34;&gt;**/mapping/release/mapping.txt&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/github-actions/github-artifacts.png&#34; alt=&#34;artifacts&#34; /&gt;&lt;/p&gt;
&lt;p&gt;完整的配置文件请阅读 &lt;a href=&#34;https://github.com/xdtianyu/actions-android-ci&#34;&gt;xdtianyu/actions-android-ci&lt;/a&gt; README 文件。&lt;/p&gt;
&lt;h3 id=&#34;总结&#34;&gt;总结&lt;/h3&gt;
&lt;p&gt;我从事开发过程的一大乐趣就是写各种各样的自动化脚本，在工作中也承担了内部 Gitlab 及 CI 的搭建维护工作。在 &lt;a href=&#34;https://github.com/xdtianyu/CallerInfo&#34;&gt;CallerInfo&lt;/a&gt; 项目中，已经集成了各式各样的 CI 服务如：&lt;code&gt;Travis-ci&lt;/code&gt;、 &lt;code&gt;Gitlab runner&lt;/code&gt;、&lt;code&gt;Jenkins&lt;/code&gt; 、&lt;code&gt;AppVeyor&lt;/code&gt;、&lt;code&gt;Github Actions&lt;/code&gt;，也集成过 &lt;code&gt;CircleCI&lt;/code&gt; 等服务，但是担心过多的权限要求导致我的其他私有仓库泄漏，所以没有集成。&lt;/p&gt;
&lt;p&gt;本文主要是对 &lt;code&gt;Gtihub Actions&lt;/code&gt; &lt;code&gt;Android&lt;/code&gt; 应用编译缓存方面的实现，附带的介绍了加密环境变量及上报 &lt;code&gt;artifacts&lt;/code&gt; ，没有涉及版本发布、自动化发布报告(sha1/commits/changes)和其他更多好玩的内容。这些内容已经在 &lt;code&gt;Travis ci&lt;/code&gt; 上实现过了，就不再赘述，感兴趣的读者可以参考学习我的开源项目 &lt;a href=&#34;https://github.com/xdtianyu/CallerInfo/releases&#34;&gt;CallerInfo&lt;/a&gt; 中的 &lt;a href=&#34;https://github.com/xdtianyu/CallerInfo/tree/master/.travis&#34;&gt;.travis&lt;/a&gt; 目录和 &lt;a href=&#34;https://github.com/xdtianyu/CallerInfo/blob/master/.travis.yml#L37&#34;&gt;.travis.yml&lt;/a&gt; 文件。&lt;/p&gt;
&lt;p&gt;CI 平台大同小异，常见的几种平台中，&lt;code&gt;AppVeyor&lt;/code&gt; 是在 Windows 宿主机上编译的，如果你也喜欢写自动化CI，建议也不要错过 &lt;a href=&#34;https://ci.appveyor.com/project/xdtianyu/callerinfo&#34;&gt;AppVeyor&lt;/a&gt;。&lt;/p&gt;</description></item><item><title>使用枚举管理不同渠道版本常量</title><link>https://busy.im/post/use-enum-to-manage-channel-constants/</link><pubDate>Mon, 10 Aug 2020 20:26:48 +0800</pubDate><guid>https://busy.im/post/use-enum-to-manage-channel-constants/</guid><description>
&lt;p&gt;工作中遇到了需要根据不同渠道配置不同广告 ID 需求，而且渠道很多，变动可能很频繁，如何才能高效配置管理呢？通过思考实践，我最终使用了枚举来管理不同渠道的广告 ID。&lt;/p&gt;
&lt;h3 id=&#34;一般的字符串常量&#34;&gt;一般的字符串常量&lt;/h3&gt;
&lt;p&gt;在项目刚开始时，是没有根据不同渠道使用不同广告位ID的需求的，最初只是使用了简单的字符串常量来保存这些初始化数据。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; APP_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;54513248&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; UM_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;RANDOM8TN2zx0QlLRfsXGRNU&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; UM_CHANNEL &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Umeng&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; SPLASH_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;****&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; INTERACTION_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;****&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; VIDEO_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;****&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;****&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; FEED_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;****&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; PROFILE_BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;****&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; SCRATCH_REWARD_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;****&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; SCRATCH_BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;****&amp;#34;&lt;/span&gt;;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;进化为枚举管理&#34;&gt;进化为枚举管理&lt;/h3&gt;
&lt;p&gt;在收到不同渠道不同广告位ID的需求后，首先考虑到便于编译，不能通过简单的更改和注释的方法来管理字符串常量，这样每次渠道编译都要修改，很麻烦。如果使用 &lt;code&gt;flavor&lt;/code&gt; 直接填充变量，数据太多不宜在 &lt;code&gt;build.gradle&lt;/code&gt; 中维护，扩展性也不好。&lt;/p&gt;
&lt;p&gt;也考虑了使用 &lt;code&gt;xml&lt;/code&gt; 中配置 &lt;code&gt;strings-arrays&lt;/code&gt; 字符串配置，想想要调整不同的文件，还要引入到上下文加载，也不是一个好的选择。&lt;/p&gt;
&lt;p&gt;再次想到了使用 &lt;code&gt;interface&lt;/code&gt;来定义一个基类，返回实现不同的渠道实例来填充广告位ID，实现下来代码也很繁琐，有些过渡设计。而考虑到 &lt;code&gt;Enum&lt;/code&gt; 是单实例 &lt;code&gt;Class&lt;/code&gt; 的本质，想法自然向枚举靠拢。&lt;/p&gt;
&lt;h4 id=&#34;1-定义常量&#34;&gt;1. 定义常量&lt;/h4&gt;
&lt;p&gt;首先在枚举中定义常量的列表&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Keep&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;enum&lt;/span&gt; AdsIds {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; APP_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;54513248&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; UM_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;RANDOM8TN2zx0QlLRfsXGRNU&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; UM_CHANNEL &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Umeng&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; SPLASH_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; INTERACTION_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; VIDEO_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; BANNER_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; FEED_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; PROFILE_BANNER_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; SCRATCH_REWARD_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; SCRATCH_BANNER_AD_ID;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;2-增加枚举实例内初始化&#34;&gt;2. 增加枚举实例内初始化&lt;/h4&gt;
&lt;p&gt;要在每个枚举实例内初始化数据，需要增加一个 &lt;code&gt;init()&lt;/code&gt; 抽象方法，同时在枚举的构造方法中调用这个 &lt;code&gt;init()&lt;/code&gt; 方法。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;enum&lt;/span&gt; AdsIds {
...
AdsIds() {
init();
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;abstract&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;init&lt;/span&gt;();
...
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;3-增加枚举实例&#34;&gt;3. 增加枚举实例&lt;/h4&gt;
&lt;p&gt;这时就可以为每个渠道增加枚举实例并初始化常量了：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;enum&lt;/span&gt; AdsIds {
COMMON {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; init() {
SPLASH_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;814272&amp;#34;&lt;/span&gt;;
INTERACTION_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;817562&amp;#34;&lt;/span&gt;;
VIDEO_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;815884&amp;#34;&lt;/span&gt;;
BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;889664&amp;#34;&lt;/span&gt;;
FEED_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;844311&amp;#34;&lt;/span&gt;;
PROFILE_BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;808569&amp;#34;&lt;/span&gt;;
SCRATCH_REWARD_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;815990&amp;#34;&lt;/span&gt;;
SCRATCH_BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;892691&amp;#34;&lt;/span&gt;;
}
},
CHANNEL_A {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; init() {
UM_CHANNEL &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;CHANNEL_1&amp;#34;&lt;/span&gt;;
SPLASH_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;812097&amp;#34;&lt;/span&gt;;
INTERACTION_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;814895&amp;#34;&lt;/span&gt;;
VIDEO_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;877058&amp;#34;&lt;/span&gt;;
BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;879158&amp;#34;&lt;/span&gt;;
FEED_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;896762&amp;#34;&lt;/span&gt;;
PROFILE_BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;832216&amp;#34;&lt;/span&gt;;
}
},
...
;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;注意最后一个枚举的结尾是分号&lt;code&gt;;&lt;/code&gt;，如果需要再增加不同渠道，只需要再增加一个新的枚举实例就可以了。&lt;/p&gt;
&lt;h4 id=&#34;4-代码调用&#34;&gt;4. 代码调用&lt;/h4&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; Constants {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; AdsIds &lt;span style=&#34;color:#c0f&#34;&gt;IDS&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; AdsIds.&lt;span style=&#34;color:#309&#34;&gt;COMMON&lt;/span&gt;;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;使用&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;adsHolder.&lt;span style=&#34;color:#309&#34;&gt;setAdId&lt;/span&gt;(Constants.&lt;span style=&#34;color:#309&#34;&gt;IDS&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;BANNER_AD_ID&lt;/span&gt;);&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;语句调用即可。&lt;/p&gt;
&lt;p&gt;为了配合 &lt;code&gt;flavor&lt;/code&gt; 控制，可以在 &lt;code&gt;Constants&lt;/code&gt; 中增加一个 &lt;code&gt;CHANNEL&lt;/code&gt; 的常量，这部分只做示例，读者可以自己讨论扩展：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Keep&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; Constants {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Change this, use flavor to control CHANNEL...
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Channel&lt;/span&gt; CHANNEL &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Channel.&lt;span style=&#34;color:#309&#34;&gt;COMMON&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; AdsIds &lt;span style=&#34;color:#c0f&#34;&gt;IDS&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; AdsIds.&lt;span style=&#34;color:#309&#34;&gt;valueOf&lt;/span&gt;(CHANNEL.&lt;span style=&#34;color:#309&#34;&gt;name&lt;/span&gt;());
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;enum&lt;/span&gt; Channel {
COMMON,
EXTERNAL,
PLAY_STORE,
CHANNEL_A
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;完整代码示例&#34;&gt;完整代码示例&lt;/h3&gt;
&lt;p&gt;最终的实现简单明了，配置维护方便，代码如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Keep&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;enum&lt;/span&gt; AdsIds {
COMMON {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; init() {
SPLASH_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;814272&amp;#34;&lt;/span&gt;;
INTERACTION_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;817562&amp;#34;&lt;/span&gt;;
VIDEO_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;815884&amp;#34;&lt;/span&gt;;
BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;889664&amp;#34;&lt;/span&gt;;
FEED_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;844311&amp;#34;&lt;/span&gt;;
PROFILE_BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;808569&amp;#34;&lt;/span&gt;;
SCRATCH_REWARD_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;815990&amp;#34;&lt;/span&gt;;
SCRATCH_BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;892691&amp;#34;&lt;/span&gt;;
}
},
CHANNEL_A {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; init() {
UM_CHANNEL &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;CHANNEL_1&amp;#34;&lt;/span&gt;;
SPLASH_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;812097&amp;#34;&lt;/span&gt;;
INTERACTION_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;814895&amp;#34;&lt;/span&gt;;
VIDEO_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;877058&amp;#34;&lt;/span&gt;;
BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;879158&amp;#34;&lt;/span&gt;;
FEED_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;896762&amp;#34;&lt;/span&gt;;
PROFILE_BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;832216&amp;#34;&lt;/span&gt;;
}
},
EXTERNAL {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; init() {
SPLASH_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;845812&amp;#34;&lt;/span&gt;;
INTERACTION_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;870659&amp;#34;&lt;/span&gt;;
VIDEO_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;893231&amp;#34;&lt;/span&gt;;
BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;851338&amp;#34;&lt;/span&gt;;
FEED_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;847696&amp;#34;&lt;/span&gt;;
PROFILE_BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;895238&amp;#34;&lt;/span&gt;;
SCRATCH_REWARD_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;840528&amp;#34;&lt;/span&gt;;
SCRATCH_BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;865717&amp;#34;&lt;/span&gt;;
}
},
PLAY_STORE {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; init() {
UM_CHANNEL &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;PLAY_STORE&amp;#34;&lt;/span&gt;;
SPLASH_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;868959&amp;#34;&lt;/span&gt;;
INTERACTION_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;874990&amp;#34;&lt;/span&gt;;
VIDEO_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;875137&amp;#34;&lt;/span&gt;;
BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;866426&amp;#34;&lt;/span&gt;;
FEED_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;839792&amp;#34;&lt;/span&gt;;
PROFILE_BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;864501&amp;#34;&lt;/span&gt;;
SCRATCH_REWARD_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;897767&amp;#34;&lt;/span&gt;;
SCRATCH_BANNER_AD_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;863937&amp;#34;&lt;/span&gt;;
}
};
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; APP_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;54513248&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; UM_ID &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;RANDOM8TN2zx0QlLRfsXGRNU&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; UM_CHANNEL &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Umeng&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; SPLASH_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; INTERACTION_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; VIDEO_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; BANNER_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; FEED_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; PROFILE_BANNER_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; SCRATCH_REWARD_AD_ID;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; SCRATCH_BANNER_AD_ID;
AdsIds() {
init();
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;abstract&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;init&lt;/span&gt;();
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;blockquote&gt;
&lt;p&gt;上文代码中的 ID 和 Key 都是随机字符串，没有绑定到特定的应用信息。&lt;/p&gt;
&lt;/blockquote&gt;</description></item><item><title>从零开始的 Go 爬虫框架编程实战 - 下篇</title><link>https://busy.im/post/golang-spider-3/</link><pubDate>Sun, 15 Mar 2020 16:46:23 +0800</pubDate><guid>https://busy.im/post/golang-spider-3/</guid><description>
&lt;p&gt;这是一篇讲解如何编写 Go 爬虫框架的编程实战系列长文，是一个爬虫框架程序从无到有的成长进化史。&lt;/p&gt;
&lt;h4 id=&#34;10-多个站点信息合并&#34;&gt;10. 多个站点信息合并&lt;/h4&gt;
&lt;p&gt;在抓取网页时，单一的网页上获取到的内容有限，有时需要在多个网页上获取信息后，综合返回站点信息。当前的数据结构不具备这种能力。为了实现这个能力，需要对 &lt;code&gt;Site&lt;/code&gt; 结构体进行升级，增加&lt;strong&gt;链表&lt;/strong&gt;的支持。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Site &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
Url &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
Next &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Site
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;同时，获取站点信息的 &lt;code&gt;Meta()&lt;/code&gt; 函数增加获取子站点信息的调用，并将子站点信息传递给父站点。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (site Site) &lt;span style=&#34;color:#c0f&#34;&gt;Meta&lt;/span&gt;() Meta {
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; next = Meta{}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; site.Next &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
next = site.Next.&lt;span style=&#34;color:#c0f&#34;&gt;Meta&lt;/span&gt;()
}
meta.Title = &lt;span style=&#34;color:#c0f&#34;&gt;combine&lt;/span&gt;(site.Title.&lt;span style=&#34;color:#c0f&#34;&gt;Value&lt;/span&gt;(doc), next.Title)
meta.Actor = &lt;span style=&#34;color:#c0f&#34;&gt;combine&lt;/span&gt;(site.Actor.&lt;span style=&#34;color:#c0f&#34;&gt;Value&lt;/span&gt;(doc), next.Actor)
meta.Poster = &lt;span style=&#34;color:#c0f&#34;&gt;combine&lt;/span&gt;(site.Poster.&lt;span style=&#34;color:#c0f&#34;&gt;Value&lt;/span&gt;(doc), next.Poster)
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; meta
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;此时站点配置清单内容如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Mgs&lt;/span&gt;(id &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) Site {
mobile &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; Site{
Url: fmt.&lt;span style=&#34;color:#c0f&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://sp.mgstage.com/product/product_detail/SP-%s/&amp;#34;&lt;/span&gt;, id),
UserAgent: MobileUserAgent,
Cookies: []http.Cookie{{Name: &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;adc&amp;#34;&lt;/span&gt;, Value: &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1&amp;#34;&lt;/span&gt;}},
CssSelector: CssSelector{
Title: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.sample-image-wrap.h1 &amp;gt; img&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;Attribute&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;alt&amp;#34;&lt;/span&gt;),
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
},
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; Site{
Url: fmt.&lt;span style=&#34;color:#c0f&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://www.mgstage.com/product/product_detail/%s/&amp;#34;&lt;/span&gt;, id),
UserAgent: UserAgent,
Cookies: mobile.Cookies,
CssSelector: CssSelector{
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
},
Next: &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&lt;/span&gt;mobile,
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;完整内容参考 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/c87db67bb13346bc5b4b29823af88a57cdcc4d43#diff-401e82854c1e7974d7678ab7d2ad326cR27&#34;&gt;commit&lt;/a&gt; 修改记录。&lt;/p&gt;
&lt;h4 id=&#34;11-增加额外字段信息&#34;&gt;11. 增加额外字段信息&lt;/h4&gt;
&lt;p&gt;再增加一个 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/2a83e247f4a1119d11b257dfa57870a07de0fff1#diff-6eb0691197f49a9878848e494a151652R8&#34;&gt;heyzo.go&lt;/a&gt; 的站点配置清单，由于编码过程中需要对中间查找的 &lt;code&gt;provider_id&lt;/code&gt; 属性进行保存，虽然最后没有用到，还是增加了配置清单中额外字段配置的支持。&lt;/p&gt;
&lt;p&gt;首先在 &lt;code&gt;Meta&lt;/code&gt; 结构体中增加一个 &lt;code&gt;Extras&lt;/code&gt; 的 &lt;code&gt;map&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Meta &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
Id &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;`json:&amp;#34;id&amp;#34;`&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
Extras &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;`json:&amp;#34;extras&amp;#34;`&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;然后在 &lt;code&gt;Selector&lt;/code&gt; 结构体中增加一个 &lt;code&gt;Extras&lt;/code&gt; 的 &lt;code&gt;map&lt;/code&gt; :&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; CssSelector &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
Id &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Item
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
Extras &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Item
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selectors CssSelector) &lt;span style=&#34;color:#c0f&#34;&gt;AddExtra&lt;/span&gt;(key &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;, selector &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Item) CssSelector {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; selectors.Extras &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
selectors.Extras = &lt;span style=&#34;color:#366&#34;&gt;make&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Item)
}
selectors.Extras[key] = selector
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; selectors
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;其中 &lt;code&gt;AddExtra()&lt;/code&gt; 函数使选择器支持连续的添加额外字段能力，例如使用如下方法添加：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; Site{
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
CssSelector: CssSelector{
Title: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;h1&amp;#34;&lt;/span&gt;),
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
}.&lt;span style=&#34;color:#c0f&#34;&gt;AddExtra&lt;/span&gt;(providerId, &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;input[name=provider_id]&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;Attribute&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;value&amp;#34;&lt;/span&gt;)),
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;最后在站点抓取站点信息时，对 &lt;code&gt;Extras&lt;/code&gt; 进行解析：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (site Site) &lt;span style=&#34;color:#c0f&#34;&gt;Meta&lt;/span&gt;() Meta {
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// extract extras to meta
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; site.Extras &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
meta.Extras = &lt;span style=&#34;color:#366&#34;&gt;make&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; key, value &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;range&lt;/span&gt; site.Extras {
meta.Extras[key] = value.&lt;span style=&#34;color:#c0f&#34;&gt;Value&lt;/span&gt;(doc)
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; key, value &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;range&lt;/span&gt; next.Extras {
meta.Extras[key] = value
}
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
meta.Extras = next.Extras
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; meta
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;完整内容请参考修改 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/7dccbe00f7fe4dababe46b5da1d5326722b1b34d&#34;&gt;commit&lt;/a&gt; 记录。&lt;/p&gt;
&lt;h4 id=&#34;12-增加正则表达式匹配查找支持&#34;&gt;12. 增加正则表达式匹配查找支持&lt;/h4&gt;
&lt;p&gt;之后又增加了 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/994fbe087ca10e8ca634b82f880e4408b1aba5f9&#34;&gt;fantia.go&lt;/a&gt; 站点配置清单，已可以直接支持。又增加 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/045f3e48a48b5b44307a4046085c86c6e479498f&#34;&gt;getchu.go&lt;/a&gt; 站点配置清单时，由于时间直接存放在大块的 &lt;code&gt;body&lt;/code&gt; 文字中，无法通过 &lt;code&gt;Css Selector&lt;/code&gt;获取到时间信息。而时间又有相对固定的格式如 &lt;code&gt;2017/02/06&lt;/code&gt;，这样结构的字符串很容易通过正则表达式获取到，所以再一次对 &lt;code&gt;Selector&lt;/code&gt; 扩展，增加了正则表达式匹配查找的支持。&lt;/p&gt;
&lt;p&gt;首先 &lt;code&gt;Selector -&amp;gt; Item&lt;/code&gt; 结构体增加正则配置函数及字段：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Item &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
matcher &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selector Item) &lt;span style=&#34;color:#c0f&#34;&gt;Match&lt;/span&gt;(matcher &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Item {
selector.matcher = matcher
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&lt;/span&gt;selector
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;然后实现正则匹配的能力：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selector Item) &lt;span style=&#34;color:#c0f&#34;&gt;matcherValue&lt;/span&gt;(doc &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;goquery.Document) &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
re &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; regexp.&lt;span style=&#34;color:#c0f&#34;&gt;MustCompile&lt;/span&gt;(selector.matcher)
text &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; doc.&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;()
matches &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; re.&lt;span style=&#34;color:#c0f&#34;&gt;FindAllString&lt;/span&gt;(text, &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;len&lt;/span&gt;(matches) &amp;gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; matches[&lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;]
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selector &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Item) &lt;span style=&#34;color:#c0f&#34;&gt;Value&lt;/span&gt;(doc &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;goquery.Document) &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;len&lt;/span&gt;(selector.matcher) &amp;gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; selector.&lt;span style=&#34;color:#c0f&#34;&gt;matcherValue&lt;/span&gt;(doc)
}
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;最后，增加站点配置清单：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Getchu&lt;/span&gt;(id &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) Site {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; Site{
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
CssSelector: CssSelector{
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
Release: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;~&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;Match&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;`\d{4}/\d{2}/\d{2}`&lt;/span&gt;),
Duration: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;~&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;Match&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;`動画.*分`&lt;/span&gt;),
Id: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;input[name=id]&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;Attribute&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;value&amp;#34;&lt;/span&gt;),
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
},
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这里 &lt;code&gt;Match()&lt;/code&gt; 函数即实现了正则匹配的配置，这是对 &lt;code&gt;Selector&lt;/code&gt; 选择器的又一次升级，之后还将继续&lt;a href=&#34;https://github.com/ruriio/tidy/commit/62e6e74eda3e27e3c9a244d4daae41c8809635c7#diff-09589b63c2cd00c4ec987a32bf26c4b2R23&#34;&gt;优化&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;完整内容请查看 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/045f3e48a48b5b44307a4046085c86c6e479498f&#34;&gt;commit&lt;/a&gt; 记录。&lt;/p&gt;
&lt;p&gt;增加了正则匹配之后，又增加了一个 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/62e6e74eda3e27e3c9a244d4daae41c8809635c7&#34;&gt;tokyo.go&lt;/a&gt; 站点配置清单，爬虫框架可以轻松适用。&lt;/p&gt;
&lt;h4 id=&#34;13-增加-json-站点解析能力&#34;&gt;13. 增加 Json 站点解析能力&lt;/h4&gt;
&lt;p&gt;个别的网站如 &lt;code&gt;1pondo&lt;/code&gt; 使用了完全的 &lt;code&gt;Json&lt;/code&gt; 渲染，内容都是在浏览器 &lt;code&gt;xhr&lt;/code&gt; 请求服务器 &lt;code&gt;API&lt;/code&gt; 后动态加载的。一般来说这是最简单的爬虫爬取接口了，但是实际编写这个爬虫时，增加 &lt;code&gt;Json&lt;/code&gt; 的解析确是最困难的部分。&lt;/p&gt;
&lt;p&gt;爬虫的框架整体是基于 &lt;code&gt;CSS Selector&lt;/code&gt; 实现的，它是不能支持文本内容检索的。所以类似正则表达式匹配，需要为 &lt;code&gt;Json&lt;/code&gt; 格式解析另辟蹊径。&lt;/p&gt;
&lt;p&gt;首先，在 &lt;code&gt;Selector -&amp;gt; Item&lt;/code&gt; 结构体中增加匹配 &lt;code&gt;json&lt;/code&gt; 节点的 &lt;code&gt;query&lt;/code&gt; 字段及配置函数 &lt;code&gt;Query()&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Item &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
selector &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
attribute &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
replacer &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;strings.Replacer
preset &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
presets []&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
matcher &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// regex
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; query &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// json
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Query&lt;/span&gt;(query &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Item {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&lt;/span&gt;Item{query: query}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Site&lt;/code&gt; 结构体中增加解析存储，这里使用了 &lt;code&gt;interface{}&lt;/code&gt; 类型作为基本类型，方便之后通过 &lt;code&gt;parseJson()&lt;/code&gt; 函数解析：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Site &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
Json &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;bool&lt;/span&gt;
JsonData &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt;{}
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;.
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (site Site) &lt;span style=&#34;color:#c0f&#34;&gt;parseJson&lt;/span&gt;() Meta {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; meta = Meta{}
body, err &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; ioutil.&lt;span style=&#34;color:#c0f&#34;&gt;ReadAll&lt;/span&gt;(site.&lt;span style=&#34;color:#c0f&#34;&gt;Body&lt;/span&gt;())
err = json.&lt;span style=&#34;color:#c0f&#34;&gt;Unmarshal&lt;/span&gt;(body, &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&lt;/span&gt;site.JsonData)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; err &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
log.&lt;span style=&#34;color:#c0f&#34;&gt;Fatal&lt;/span&gt;(err)
}
data &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;make&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt;{})
m, ok &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; site.JsonData.(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt;{})
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; ok {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; k, v &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;range&lt;/span&gt; m {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;//fmt.Println(k, &amp;#34;=&amp;gt;&amp;#34;, v)
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; data[k] = v
}
}
next &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; Meta{}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; site.Next &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
next = site.Next.&lt;span style=&#34;color:#c0f&#34;&gt;Meta&lt;/span&gt;()
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// extract meta data from json data
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; meta.Title = &lt;span style=&#34;color:#c0f&#34;&gt;oneOf&lt;/span&gt;(site.Title.&lt;span style=&#34;color:#c0f&#34;&gt;Query&lt;/span&gt;(data), next.Title)
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; meta
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;然后实现解析 &lt;code&gt;Json&lt;/code&gt; 的能力，下面代码中 &lt;code&gt;Query()&lt;/code&gt; 函数是实现从 &lt;code&gt;Site.JsonData&lt;/code&gt; 中提取单个字段值，而 &lt;code&gt;Queries()&lt;/code&gt; 则为提取数组值。其中的关键函数或较难理解的函数为 &lt;code&gt;queries()&lt;/code&gt; 函数，将解析到的值转为 &lt;code&gt;[]string&lt;/code&gt; 后返回：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// get item
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selector &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Item) &lt;span style=&#34;color:#c0f&#34;&gt;Query&lt;/span&gt;(data &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt;{}) &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; selector &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;len&lt;/span&gt;(selector.preset) &amp;gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; selector.preset
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;query&lt;/span&gt;(data, selector.query)
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;query&lt;/span&gt;(data &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt;{}, key &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
value &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; data[key]
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; value &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; fmt.&lt;span style=&#34;color:#c0f&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;%v&amp;#34;&lt;/span&gt;, value)
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// get items array
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selector &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Item) &lt;span style=&#34;color:#c0f&#34;&gt;Queries&lt;/span&gt;(data &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt;{}) []&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; selector &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; []&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;{}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; selector.presets &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; selector.presets
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;queries&lt;/span&gt;(data, selector.query)
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;queries&lt;/span&gt;(data &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;map&lt;/span&gt;[&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;]&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt;{}, key &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) []&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; res []&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
x &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; data[key]
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; x &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// if json object is not slice then ignore
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; reflect.&lt;span style=&#34;color:#c0f&#34;&gt;ValueOf&lt;/span&gt;(x).&lt;span style=&#34;color:#c0f&#34;&gt;Kind&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; reflect.Slice {
array &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; x.([]&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt;{})
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; _, v &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;range&lt;/span&gt; array {
value &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; fmt.&lt;span style=&#34;color:#c0f&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;%v&amp;#34;&lt;/span&gt;, v)
res = &lt;span style=&#34;color:#366&#34;&gt;append&lt;/span&gt;(res, value)
}
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; res
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;目前这段解析代码还不是很完美，只能支持单一层级的 &lt;code&gt;Json&lt;/code&gt; 字符串，如果层级较深，则不能解析出来，以后应该还会根据需求优化。&lt;/p&gt;
&lt;p&gt;最后，添加站点配置清单即可从 &lt;code&gt;Json&lt;/code&gt; 数据中获取到站点信息。完整代码请参考 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/b8e1800cd811aedd67e155cb15a34f6b9ef6a331#diff-233f7e13d818f3e80d4ea533fcdde034&#34;&gt;commit&lt;/a&gt; 记录。&lt;/p&gt;
&lt;h4 id=&#34;14-增加装饰器-支持配置清单自定义修改&#34;&gt;14. 增加装饰器，支持配置清单自定义修改&lt;/h4&gt;
&lt;p&gt;在编写 &lt;a href=&#34;https://github.com/ruriio/tidy/blob/09a009c1b5dca63ed465adf11b1b9f950be4ce35/sites/pondo.go&#34;&gt;pondo.go&lt;/a&gt; 清单过程中，因为解析到的 &lt;code&gt;Image&lt;/code&gt; 字段是一个不定的结构类型，可能有两种字段 &lt;code&gt;Img&lt;/code&gt; 和 &lt;code&gt;FileName&lt;/code&gt;，以及有一个 &lt;code&gt;Protected&lt;/code&gt; 类型表示是否可以公开访问：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; PondoImage &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
Img &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Filename &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Protected &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;bool&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;需要对这个字段做特殊处理，而不能简单的依赖爬虫框架库来实现解析逻辑，框架里实现这个解析可能会较为复杂。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Golang&lt;/code&gt; 不是完全面向对象的编程语言，不能简单的通过继承和重写 (override) 来在父类中调用子类方法，需要使用接口实现类似回调的方式。&lt;/p&gt;
&lt;p&gt;首先在 &lt;code&gt;Site&lt;/code&gt; 结构体中增加 &lt;code&gt;Docor&lt;/code&gt; 接口。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Site &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
Decor Decor
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Decor &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt; {
&lt;span style=&#34;color:#c0f&#34;&gt;Decorate&lt;/span&gt;(meta &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Meta) &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Meta
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (site Site) &lt;span style=&#34;color:#c0f&#34;&gt;Decorate&lt;/span&gt;(meta &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Meta) &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Meta {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&lt;/span&gt;site.meta
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;然后在 &lt;code&gt;podon.go&lt;/code&gt; 中实现装饰器并传递给 &lt;code&gt;site&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Pondo&lt;/span&gt;(id &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) Site {
next &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; Site{
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
Json: &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;,
Decor: PondoDecor{},
Selector: Selector{
Images: &lt;span style=&#34;color:#c0f&#34;&gt;Query&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Rows&amp;#34;&lt;/span&gt;),
},
}
site &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; Site{
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
Next: &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&lt;/span&gt;next,
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; site
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; PondoDecor &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
Decor
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; PondoImage &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
Img &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Filename &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Protected &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;bool&lt;/span&gt;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (decor PondoDecor) &lt;span style=&#34;color:#c0f&#34;&gt;Decorate&lt;/span&gt;(meta &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Meta) &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;Meta {
origin &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; meta.Images
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;len&lt;/span&gt;(origin) &amp;gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; images []&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; pondo PondoImage
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; _, s &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;range&lt;/span&gt; origin {
err &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; json.&lt;span style=&#34;color:#c0f&#34;&gt;Unmarshal&lt;/span&gt;([]&lt;span style=&#34;color:#366&#34;&gt;byte&lt;/span&gt;(s), &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&lt;/span&gt;pondo)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; err &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
log.&lt;span style=&#34;color:#c0f&#34;&gt;Fatal&lt;/span&gt;(err)
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; !pondo.Protected {
img &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; pondo.Img
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;len&lt;/span&gt;(img) &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt; {
img = pondo.Filename
}
images = &lt;span style=&#34;color:#366&#34;&gt;append&lt;/span&gt;(images, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://www.1pondo.tv/dyn/dla/images/&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;img)
}
}
meta.Images = images
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; meta
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;最后，修改 &lt;code&gt;Site.Meta()&lt;/code&gt; 函数，增加装饰器调用：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (site Site) &lt;span style=&#34;color:#c0f&#34;&gt;Meta&lt;/span&gt;() Meta {
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; site.Decor &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;site.Decor.&lt;span style=&#34;color:#c0f&#34;&gt;Decorate&lt;/span&gt;(&lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&lt;/span&gt;site.meta)
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; site.meta
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;之后，每次爬虫框架在运行完后会执行站点配置清单的装饰器函数来修改元数据。完整的代码请参考 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/09a009c1b5dca63ed465adf11b1b9f950be4ce35&#34;&gt;commit&lt;/a&gt; 记录。&lt;/p&gt;
&lt;h3 id=&#34;结语&#34;&gt;结语&lt;/h3&gt;
&lt;p&gt;至此，一个完整的站点元数据爬虫框架就完成了，要成为一个完整的应用，还需要增加文件整理，上传下载等类似我之前写的 &lt;code&gt;Dmaster&lt;/code&gt; 程序功能，这些能力将会继续更新。要获取到最新的更新和完整的工程代码，请关注仓库：&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/ruriio/tidy&#34;&gt;https://github.com/ruriio/tidy&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;til-学到的小知识点&#34;&gt;TIL - 学到的小知识点&lt;/h3&gt;
&lt;p&gt;通过这周末两天的实战，学习到了以下几个零碎的小知识点：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Go 中方法和变量名称首字母大小写控制了访问权，大写表示公有，小写私有。&lt;/li&gt;
&lt;li&gt;Go 不是面向对象的编程语言，没有子类函数继承和覆盖重写，父类里调用子类的实现函数，需要通过接口&amp;rdquo;绕路&amp;rdquo;实现。&lt;/li&gt;
&lt;li&gt;CSS 选择器可以使用逗号( &lt;code&gt;,&lt;/code&gt; ) 并联查询；可以使用 &lt;code&gt;.classA.classB&lt;/code&gt; 精确匹配有多个 &lt;code&gt;class&lt;/code&gt;的元素如 &lt;code&gt;&amp;lt;div class=&amp;quot;classA classB&amp;quot;&amp;gt;text&amp;lt;/div&amp;gt;&lt;/code&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://busy.im/post/golang-spider-1/&#34;&gt;从零开始的 Go 爬虫框架编程实战 - 上篇&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://busy.im/post/golang-spider-2/&#34;&gt;从零开始的 Go 爬虫框架编程实战 - 中篇&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;从零开始的 Go 爬虫框架编程实战 - 下篇&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>从零开始的 Go 爬虫框架编程实战 - 中篇</title><link>https://busy.im/post/golang-spider-2/</link><pubDate>Sun, 15 Mar 2020 16:42:56 +0800</pubDate><guid>https://busy.im/post/golang-spider-2/</guid><description>
&lt;p&gt;这是一篇讲解如何编写 Go 爬虫框架的编程实战系列长文，是一个爬虫框架程序从无到有的成长进化史。&lt;/p&gt;
&lt;h4 id=&#34;6-增加通用获取属性的封装&#34;&gt;6. 增加通用获取属性的封装&lt;/h4&gt;
&lt;p&gt;在上一节的实战后，&lt;code&gt;tidy&lt;/code&gt; 已经具备了基本爬虫框架，增加站点配置就可以支持新的站点爬虫。但是只支持文本查询是不够的，例如我们需要获取 &lt;code&gt;&amp;lt;a href=&amp;quot;value&amp;quot;&amp;gt;&lt;/code&gt; 和 &lt;code&gt;&amp;lt;img src=&amp;quot;value&amp;quot;&amp;gt;&lt;/code&gt; 中的 &lt;code&gt;href&lt;/code&gt; 及 &lt;code&gt;src&lt;/code&gt; 属性。我们在之前已经 &lt;code&gt;Selector.Item.Attr(doc)&lt;/code&gt; 支持了属性的查找，需要将这个能力暴露给上次的配置清单。&lt;/p&gt;
&lt;p&gt;首先在 &lt;code&gt;Item&lt;/code&gt; 结构体中增加一个 &lt;code&gt;attribute&lt;/code&gt; 的字段，用来保存清单配置的属性参数。为了结构或接口规范，0保持统一，我将 &lt;code&gt;replacer&lt;/code&gt; 也提取进来。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Item &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
selector &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
attribute &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
replacer &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;strings.Replacer
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;通过如下方法配置这两个字段：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selector Item) &lt;span style=&#34;color:#c0f&#34;&gt;replace&lt;/span&gt;(oldNew &lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) Item {
selector.replacer = strings.&lt;span style=&#34;color:#c0f&#34;&gt;NewReplacer&lt;/span&gt;(oldNew&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; selector
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selector Item) &lt;span style=&#34;color:#c0f&#34;&gt;attr&lt;/span&gt;(attr &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) Item {
selector.attribute = attr
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; selector
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;之后只需要通过 &lt;code&gt;selector(&amp;quot;a&amp;quot;).attr(&amp;quot;href&amp;quot;)&lt;/code&gt; 方法配置，即可获取选择 &lt;code&gt;a&lt;/code&gt; 标签并提取 &lt;code&gt;href&lt;/code&gt; 属性的操作。&lt;/p&gt;
&lt;p&gt;再新增一个 &lt;a href=&#34;https://github.com/ruriio/tidy/blob/eb6e469ad7c051979a7224b84a4e179d63830534/sites/fc2.go&#34;&gt;fc2.go&lt;/a&gt; 的站点，先看升级后最终的配置清单：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Fc2&lt;/span&gt;(id &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) Site {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; Site{
Url: fmt.&lt;span style=&#34;color:#c0f&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://adult.contents.fc2.com/article/%s/&amp;#34;&lt;/span&gt;, id),
UserAgent: MobileUserAgent,
Selector: Selector{
Title: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.items_article_MainitemNameTitle&amp;#34;&lt;/span&gt;),
Actor: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.items_article_seller&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;replace&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;by &amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;),
Poster: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;meta[property^=\&amp;#34;og:image\&amp;#34;]&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;attr&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;content&amp;#34;&lt;/span&gt;),
Producer: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.items_article_seller&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;replace&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;by &amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;),
Sample: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.main-video&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;attr&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;src&amp;#34;&lt;/span&gt;),
Series: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.items_article_seller&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;replace&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;by &amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;),
Release: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.items_article_Releasedate&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;replace&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;販売日 : &amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;),
Duration: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.items_article_MainitemThumb &amp;gt; p&amp;#34;&lt;/span&gt;),
Id: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.items_article_TagArea&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;attr&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;data-id&amp;#34;&lt;/span&gt;),
Label: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;null&amp;#34;&lt;/span&gt;),
Genre: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;null&amp;#34;&lt;/span&gt;),
Images: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;li[data-img^=\&amp;#34;https://storage\&amp;#34;]&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;attr&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;data-img&amp;#34;&lt;/span&gt;),
},
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;可以从这里看到 &lt;code&gt;Selector -&amp;gt; Item&lt;/code&gt; 结构体增加了 &lt;code&gt;replace()&lt;/code&gt; 和 &lt;code&gt;attr()&lt;/code&gt; 函数，有了文本替换和获取标签属性的能力。这是对选择器的第一次升级，可以在这里看到完成的 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/eb6e469ad7c051979a7224b84a4e179d63830534&#34;&gt;commits&lt;/a&gt; 记录。此时 &lt;code&gt;Selector&lt;/code&gt; 已经有完整的扩展能力，为了结构明了，可以单独提取到 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/8a93225e461968f06be35ad5e2321a4e909487f6#diff-83c50a1f0f14e1b36dfb1cbb509f27e4L1&#34;&gt;selector.go&lt;/a&gt; 中了。&lt;/p&gt;
&lt;p&gt;继续增加一个 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/2fe290c0b0334f7219fe592e3385ee7b4ba94a1e&#34;&gt;fc2club.go&lt;/a&gt; 的站点配置清单，可以看到爬虫代码无需做任何修改已经支持了匹配查询。&lt;/p&gt;
&lt;h4 id=&#34;7-增加-gb2312-euc-jp-等编码支持&#34;&gt;7. 增加 &lt;code&gt;gb2312&lt;/code&gt; &lt;code&gt;euc-jp&lt;/code&gt; 等编码支持&lt;/h4&gt;
&lt;p&gt;当前爬虫爬取到的网站内容都是使用 &lt;code&gt;uft-8&lt;/code&gt; 编码的，在抓取 &lt;code&gt;Carib&lt;/code&gt; 站点信息时，出现了乱码的问题，联想到了之前使用 &lt;code&gt;Python&lt;/code&gt; 写爬虫抓取另一个站点时遇到的 &lt;code&gt;euc-jp&lt;/code&gt; 解析错误问题。有了之前 &lt;code&gt;Python&lt;/code&gt; 编程经验，这一次很快就定位到问题，并通过 &lt;code&gt;charset.NewReaderLabel(encoding, body)&lt;/code&gt; 函数解决：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;decodeHTMLBody&lt;/span&gt;(body io.Reader, encoding &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) (io.ReadCloser, &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;error&lt;/span&gt;) {
body, err &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; charset.&lt;span style=&#34;color:#c0f&#34;&gt;NewReaderLabel&lt;/span&gt;(encoding, body)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; err &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
log.&lt;span style=&#34;color:#c0f&#34;&gt;Fatal&lt;/span&gt;(err)
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; ioutil.&lt;span style=&#34;color:#c0f&#34;&gt;NopCloser&lt;/span&gt;(body), &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这里可以通过自动侦测检查网站的编码，也可以在配置清单中指定编码类型。为了准确性，我选择了在配置清单中配置的方法。此时 &lt;code&gt;Site&lt;/code&gt; 结构体经过升级，增加 &lt;code&gt;Charset&lt;/code&gt; 字段：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Site &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
Url &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
UserAgent &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Charset &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
CssSelector
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;在解析前执行此函数即可完成内容转码：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// convert none utf-8 web page to utf-8
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; site.Charset &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt; {
body, err = &lt;span style=&#34;color:#c0f&#34;&gt;decodeHTMLBody&lt;/span&gt;(resp.Body, site.Charset)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; err &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
log.&lt;span style=&#34;color:#c0f&#34;&gt;Fatal&lt;/span&gt;(err)
}
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// load the HTML document
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; doc, err &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; goquery.&lt;span style=&#34;color:#c0f&#34;&gt;NewDocumentFromReader&lt;/span&gt;(body)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;完整修改细节可以参考这里的 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/12c1b41da8478bdb77cbffc3b5bd5d536aeb4328&#34;&gt;commit&lt;/a&gt; 记录。之后在配置清单 &lt;code&gt;Site&lt;/code&gt; 初始化时增加 &lt;code&gt;Charset: &amp;quot;euc-jp&amp;quot;&lt;/code&gt; 配置即可。&lt;/p&gt;
&lt;h4 id=&#34;8-扩展-selector-选择器-增加预设值支持&#34;&gt;8. 扩展 &lt;code&gt;Selector&lt;/code&gt; 选择器，增加预设值支持&lt;/h4&gt;
&lt;p&gt;再次增加一个 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/12c1b41da8478bdb77cbffc3b5bd5d536aeb4328&#34;&gt;Carib.go&lt;/a&gt; 的站点配置清单，由于部分属性在爬取前就已经明确，可以直接预置，需要 &lt;code&gt;Selector -&amp;gt; Item&lt;/code&gt;增加一个预置参数字段 &lt;code&gt;preset&lt;/code&gt; 来储存，并在查找时直接返回这个值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Item &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
selector &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
attribute &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
replacer &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;strings.Replacer
preset &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Preset&lt;/span&gt;(preset &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) Item {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; Item{selector: &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;, attribute: &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;, replacer: strings.&lt;span style=&#34;color:#c0f&#34;&gt;NewReplacer&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;), preset: preset}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selector Item) &lt;span style=&#34;color:#c0f&#34;&gt;Value&lt;/span&gt;(doc &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;goquery.Document) &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;len&lt;/span&gt;(selector.preset) &amp;gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; selector.preset
}
selection &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; doc.&lt;span style=&#34;color:#c0f&#34;&gt;Find&lt;/span&gt;(selector.selector).&lt;span style=&#34;color:#c0f&#34;&gt;First&lt;/span&gt;()
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; selector.&lt;span style=&#34;color:#c0f&#34;&gt;textOrAttr&lt;/span&gt;(selection)
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;可以参考这里的 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/12c1b41da8478bdb77cbffc3b5bd5d536aeb4328#diff-83c50a1f0f14e1b36dfb1cbb509f27e4L24&#34;&gt;commit&lt;/a&gt; 修改记录。&lt;code&gt;Carib.go&lt;/code&gt; 站点配置清单完整内容如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Carib&lt;/span&gt;(id &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) Site {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; Site{
Url: fmt.&lt;span style=&#34;color:#c0f&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://www.caribbeancom.com/moviepages/%s/index.html&amp;#34;&lt;/span&gt;, id),
UserAgent: MobileUserAgent,
Charset: &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;euc-jp&amp;#34;&lt;/span&gt;,
CssSelector: CssSelector{
Title: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;h1[itemprop=name]&amp;#34;&lt;/span&gt;),
Actor: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;a[itemprop=actor]&amp;#34;&lt;/span&gt;),
Poster: &lt;span style=&#34;color:#c0f&#34;&gt;Preset&lt;/span&gt;(fmt.&lt;span style=&#34;color:#c0f&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://www.caribbeancom.com/moviepages/%s/images/l_l.jpg&amp;#34;&lt;/span&gt;, id)),
Producer: &lt;span style=&#34;color:#c0f&#34;&gt;Preset&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Caribbean&amp;#34;&lt;/span&gt;),
Sample: &lt;span style=&#34;color:#c0f&#34;&gt;Preset&lt;/span&gt;(fmt.&lt;span style=&#34;color:#c0f&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://smovie.caribbeancom.com/sample/movies/%s/480p.mp4&amp;#34;&lt;/span&gt;, id)),
Series: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;a[onclick^=gaDetailEvent\\(\\&amp;#39;Series\\ Name\\&amp;#39;]&amp;#34;&lt;/span&gt;),
Release: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;span[itemprop=datePublished]&amp;#34;&lt;/span&gt;),
Duration: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;span[itemprop=duration]&amp;#34;&lt;/span&gt;),
Id: &lt;span style=&#34;color:#c0f&#34;&gt;Preset&lt;/span&gt;(id),
Label: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;null&amp;#34;&lt;/span&gt;),
Genre: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;a[itemprop=genre]&amp;#34;&lt;/span&gt;),
Images: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;a[data-is_sample=&amp;#39;1&amp;#39;]&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;Attribute&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;href&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;Replace&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/movie&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://www.caribbeancom.com/movie&amp;#34;&lt;/span&gt;),
},
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;可以看到这里有四个 &lt;code&gt;Preset&lt;/code&gt; 的属性：&lt;code&gt;Poster&lt;/code&gt; &lt;code&gt;Producer&lt;/code&gt; &lt;code&gt;Id&lt;/code&gt; &lt;code&gt;Sample&lt;/code&gt; ，此时 &lt;code&gt;Selector&lt;/code&gt; 的函数已经变为大写字母开头的公开函数，是因为之前对选择器提取到了 &lt;code&gt;selector&lt;/code&gt; 包中。增加 &lt;code&gt;Preset&lt;/code&gt; 字段是选择期 &lt;code&gt;Selector&lt;/code&gt; 的第二次升级。&lt;/p&gt;
&lt;p&gt;再增加一个 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/bff8c408ce106e499894ca6c5d8db4a058d0730b#diff-a0dc89d3f552df50e1ccfc166b205d79&#34;&gt;caribpr.go&lt;/a&gt; 站点配置清单，可以看到已直接支持。&lt;/p&gt;
&lt;h4 id=&#34;9-支持-cookies-配置&#34;&gt;9. 支持 Cookies 配置&lt;/h4&gt;
&lt;p&gt;在增加 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/37877b6d7370f77c5b26f1f7708baf84d18d6a7d&#34;&gt;mgs.go&lt;/a&gt; 站点时，爬虫需要预置 &lt;code&gt;cookies&lt;/code&gt; 才能正确抓取到网页，所以类似于编码格式 &lt;code&gt;encoding&lt;/code&gt;，需要对 &lt;code&gt;Site&lt;/code&gt; 结构体进行升级。&lt;/p&gt;
&lt;p&gt;首先升级 &lt;code&gt;Site.get()&lt;/code&gt; 函数，在请求时增加 &lt;code&gt;cookies&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Site &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
Cookies []http.Cookie
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (site Site) &lt;span style=&#34;color:#c0f&#34;&gt;get&lt;/span&gt;() (&lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;http.Response, &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;error&lt;/span&gt;) {
client &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&lt;/span&gt;http.Client{}
req, err &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; http.&lt;span style=&#34;color:#c0f&#34;&gt;NewRequest&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;GET&amp;#34;&lt;/span&gt;, site.Url, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; err &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
log.&lt;span style=&#34;color:#c0f&#34;&gt;Fatal&lt;/span&gt;(err)
}
req.Header.&lt;span style=&#34;color:#c0f&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;User-Agent&amp;#34;&lt;/span&gt;, site.UserAgent)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; _, cookie &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;range&lt;/span&gt; site.Cookies {
log.&lt;span style=&#34;color:#c0f&#34;&gt;Println&lt;/span&gt;(cookie)
req.&lt;span style=&#34;color:#c0f&#34;&gt;AddCookie&lt;/span&gt;(&lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&lt;/span&gt;cookie)
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; client.&lt;span style=&#34;color:#c0f&#34;&gt;Do&lt;/span&gt;(req)
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;再次增加配置清单，即可获取到正确的站点信息：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Mgs&lt;/span&gt;(id &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) Site {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; Site{
Url: fmt.&lt;span style=&#34;color:#c0f&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://sp.mgstage.com/product/product_detail/SP-%s/&amp;#34;&lt;/span&gt;, id),
UserAgent: MobileUserAgent,
Cookies: []http.Cookie{{Name: &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;adc&amp;#34;&lt;/span&gt;, Value: &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1&amp;#34;&lt;/span&gt;}},
CssSelector: CssSelector{
Title: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.sample-image-wrap.h1 &amp;gt; img&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;Attribute&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;alt&amp;#34;&lt;/span&gt;),
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
Images: &lt;span style=&#34;color:#c0f&#34;&gt;Selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;a[class^=\&amp;#34;sample-image-wrap sample\&amp;#34;]&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;Attribute&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;href&amp;#34;&lt;/span&gt;),
},
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;完整内容参考 &lt;a href=&#34;https://github.com/ruriio/tidy/commit/37877b6d7370f77c5b26f1f7708baf84d18d6a7d#diff-323145a046da7b6dd948979aef9dea70R80&#34;&gt;commit&lt;/a&gt; 修改记录。&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://busy.im/post/golang-spider-1/&#34;&gt;从零开始的 Go 爬虫框架编程实战 - 上篇&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;从零开始的 Go 爬虫框架编程实战 - 中篇&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://busy.im/post/golang-spider-3/&#34;&gt;从零开始的 Go 爬虫框架编程实战 - 下篇&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>从零开始的 Go 爬虫框架编程实战 - 上篇</title><link>https://busy.im/post/golang-spider-1/</link><pubDate>Sun, 15 Mar 2020 16:40:16 +0800</pubDate><guid>https://busy.im/post/golang-spider-1/</guid><description>
&lt;p&gt;这是一篇讲解如何编写 Go 爬虫框架的编程实战系列长文，是一个爬虫框架程序从无到有的成长进化史。&lt;/p&gt;
&lt;p&gt;我是一个 Android 开发工程师，之前并没有 &lt;code&gt;Golang&lt;/code&gt; 程序的编写历史，只学习过 &lt;code&gt;Golang&lt;/code&gt; 的基本语法。不过我有较多的 &lt;code&gt;Java/Kotlin/Python/Shell/C++&lt;/code&gt; 编程经验，重视编码风格，尊重最佳实践，同时也是一个编程爱好者， &lt;code&gt;Linux&lt;/code&gt;用户。&lt;/p&gt;
&lt;p&gt;编程语言只是一个开发工具，代码的逻辑和架构思想是相同的，这大概也是我能在周末两天内快速完成编写这个程序的原因。&lt;/p&gt;
&lt;p&gt;文中的写法和格式风格都是依据经验和现学现用，如果有不符合代码规范或最佳实践的地方，还请不吝赐教。&lt;/p&gt;
&lt;h3 id=&#34;为什么选择-go-爬虫&#34;&gt;为什么选择 Go 爬虫&lt;/h3&gt;
&lt;p&gt;我写过几个爬虫类型的程序，基本都是在处理整理资源，这些代码大多是 &lt;code&gt;Python&lt;/code&gt; 实现的，没有开源，也基本是自用。比如&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;MinorNews&lt;/code&gt; - 在主流和国外媒体上爬取指定关键字的旅游资讯新闻信息并发送邮件。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;DFront&lt;/code&gt; - 浏览器插件，通过 &lt;code&gt;Ctrl-Q&lt;/code&gt; 快捷键一键发送当前网页资源到后端 &lt;code&gt;Dmaster&lt;/code&gt;。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Dmaster&lt;/code&gt; - 通过 &lt;code&gt;DFront&lt;/code&gt; 接口上报的网址和信息，索引查找对应支持的站点并抓取资源链接，下载压缩、整理等操作，并发送通知提示。&lt;/li&gt;
&lt;li&gt;&lt;code&gt;PhoneNumber&lt;/code&gt; - 这是一个开源的号码查询库，其中有两个数据源是查询网页的搜索结果并解析。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;后端都是用 &lt;code&gt;Python - flask - celery&lt;/code&gt; 编写的，爬虫方面使用的多是 &lt;code&gt;Beautiful soup&lt;/code&gt; 库，爬取新闻时有的网站使用了 &lt;code&gt;PhantomJS&lt;/code&gt; 和 &lt;code&gt;headless browser&lt;/code&gt; 爬取内容。&lt;/p&gt;
&lt;p&gt;从开发效率角度看，&lt;code&gt;Python&lt;/code&gt; 虽然很高效，但是需要集成 &lt;code&gt;Python3&lt;/code&gt; 的环境，即使搭配 &lt;code&gt;Docker&lt;/code&gt; 使用也不是很方便。最近一个月使用 &lt;code&gt;Python&lt;/code&gt; 写了一组提取各个站点资源信息并整理资源到相应目录的脚本，各个脚本文件放到 &lt;code&gt;~/bin&lt;/code&gt; 目录后直接运行，虽然很方面实用，但是没有成系统的架构和统一的程序入口，迁移发布也不是很方便，而且每个脚本重复的代码太多，维护起来效率不高。&lt;/p&gt;
&lt;p&gt;因为 &lt;code&gt;Go&lt;/code&gt; 可以很方便的编译和发布，为了达成统一入口和方便发布的目的，这次的爬虫转入到 &lt;code&gt;Go&lt;/code&gt; 编写爬虫。&lt;/p&gt;
&lt;h3 id=&#34;从零开始-高效的-go-开发环境&#34;&gt;从零开始，高效的 Go 开发环境&lt;/h3&gt;
&lt;p&gt;得益于之前的开源应用，我已经连续两年获取到了 &lt;code&gt;IntelliJ IDEA&lt;/code&gt; 的免费开源许可，可以无限制的在开源项目中使用 &lt;code&gt;IntelliJ&lt;/code&gt; 的全家桶，即全部平台 &lt;code&gt;IDE&lt;/code&gt;，毫无疑问选择了 &lt;code&gt;GoLand&lt;/code&gt; 开发环境。因为平时也会运行一些 &lt;code&gt;Go&lt;/code&gt; 代码编译，所以本地已经有了 &lt;code&gt;Go&lt;/code&gt; 环境，下载 &lt;code&gt;GoLand linux zip&lt;/code&gt; 包以后，直接解压运行 &lt;code&gt;bin/goland.sh&lt;/code&gt; 就可以使用了。&lt;/p&gt;
&lt;p&gt;我之前没有写过 &lt;code&gt;Go&lt;/code&gt; 程序，只写了 &lt;code&gt;Go&lt;/code&gt; 的 &lt;code&gt;hello world&lt;/code&gt;，学习过基本语法，并没有系统的学习和开发 &lt;code&gt;Go&lt;/code&gt; 程序经验。在周末的两天里，从零开始到爬虫框架和 11 个站点爬虫配置，使我再次体会到高效的 &lt;code&gt;IDE&lt;/code&gt; 工具对开发效率的巨大影响。&lt;/p&gt;
&lt;h3 id=&#34;开发过程及框架介绍&#34;&gt;开发过程及框架介绍&lt;/h3&gt;
&lt;p&gt;这次的爬虫起的名字为 &lt;code&gt;Tidy&lt;/code&gt;，预期通过它可以高效系统的整理文件和获取信息，目前只有抓取站点信息的能力，之后会加入文件整理，下载压缩解压等一系列的功能。开放源代码 &lt;a href=&#34;https://github.com/ruriio/tidy&#34;&gt;ruriio/tidy&lt;/a&gt; 。&lt;/p&gt;
&lt;p&gt;开发过程可以从 &lt;a href=&#34;https://github.com/ruriio/tidy/commits/master&#34;&gt;commits&lt;/a&gt; 日志来看，以下内容是对流水帐的整理。&lt;/p&gt;
&lt;h4 id=&#34;1-基础数据结构和架构&#34;&gt;1. 基础数据结构和架构&lt;/h4&gt;
&lt;p&gt;程序使用 &lt;code&gt;Meta&lt;/code&gt; 结构体的基础数据结构，内容包含标题、发行时间、演员、发行商、作品编号等元数据信息，放在 &lt;code&gt;model/meta.go&lt;/code&gt; 内。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Meta &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
Id &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Title &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Actor &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Producer &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Series &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Age &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Sample &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Poster &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Images []&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Label &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Genre &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;为了方便打印，编写了 &lt;code&gt;meta json&lt;/code&gt; 化输出的方法，也放置在 &lt;a href=&#34;https://github.com/ruriio/tidy/blob/bad08b0232318dfd70ff31f5d2ed25def51a2a52/model/meta.go&#34;&gt;meta.go&lt;/a&gt; 文件中。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (meta Meta) &lt;span style=&#34;color:#c0f&#34;&gt;Json&lt;/span&gt;() &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
out, err &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; json.&lt;span style=&#34;color:#c0f&#34;&gt;Marshal&lt;/span&gt;(meta)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; err &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
log.&lt;span style=&#34;color:#c0f&#34;&gt;Panic&lt;/span&gt;(err)
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#366&#34;&gt;string&lt;/span&gt;(out)
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;目录结构方面，使用 &lt;code&gt;model&lt;/code&gt; 目录存放模块代码，使用 &lt;code&gt;sites&lt;/code&gt; 目录存放站点配置代码。在根目录放置了 &lt;code&gt;main.go&lt;/code&gt; &lt;code&gt;scrape.go&lt;/code&gt; 文件作为程序入口和爬虫的逻辑体。&lt;/p&gt;
&lt;p&gt;为了统一站点配置，创建 &lt;a href=&#34;https://github.com/ruriio/tidy/blob/bad08b0232318dfd70ff31f5d2ed25def51a2a52/sites/site.go&#34;&gt;Site&lt;/a&gt; 结构体，包含所有站点相关的信息如 &lt;code&gt;Url&lt;/code&gt; &lt;code&gt;UserAgent&lt;/code&gt; &lt;code&gt;Title&lt;/code&gt; 等。同时将常用的桌面和移动端 &lt;code&gt;UA&lt;/code&gt;内置在常量里，方便调用。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;package&lt;/span&gt; sites
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Site &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
Url &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
UserAgent &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
Title &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; UserAgent &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/75.0.3770.90 Safari/537.36&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; MobileUserAgent &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Mozilla/5.0 (iPhone; CPU iPhone OS 10_3_1 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Version/10.0 Mobile/14E304 Safari/602.1&amp;#34;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;2-基础站点配置清单&#34;&gt;2. 基础站点配置清单&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;Site&lt;/code&gt; 结构体类似于父类或者站点配置清单，真正的站点配置在单个的站点配置清单中返回，如 &lt;a href=&#34;https://github.com/ruriio/tidy/blob/bad08b0232318dfd70ff31f5d2ed25def51a2a52/sites/dmm.go&#34;&gt;dmm.go&lt;/a&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;package&lt;/span&gt; sites
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;fmt&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Dmm&lt;/span&gt;(id &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) Site {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; Site{
Url: &lt;span style=&#34;color:#c0f&#34;&gt;url&lt;/span&gt;(id),
UserAgent: MobileUserAgent,
Title: &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.ttl-grp&amp;#34;&lt;/span&gt;,
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;url&lt;/span&gt;(id &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; fmt.&lt;span style=&#34;color:#c0f&#34;&gt;Sprintf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://www.dmm.co.jp/mono/dvd/-/detail/=/cid=%s/&amp;#34;&lt;/span&gt;, id)
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;在这个站点配置中，初始化了一个 &lt;code&gt;Site&lt;/code&gt; 结构体，并通过 &lt;code&gt;id&lt;/code&gt; 参数填充了 &lt;code&gt;url&lt;/code&gt;，爬起使用移动端的网页，同时对应的 &lt;code&gt;Title&lt;/code&gt; 标题字段，使用了 &lt;code&gt;.ttl-grp&lt;/code&gt; 的 &lt;code&gt;CSS Selector&lt;/code&gt; 。字段这一块会在后续过程中优化，现在的目标是先搭起框架跑起来。如果没有编写爬虫和写前端的经验，建议先阅读下 &lt;a href=&#34;https://www.w3school.com.cn/cssref/css_selectors.asp&#34;&gt;CSS3 选择器&lt;/a&gt; 的相关文档。&lt;/p&gt;
&lt;p&gt;如果要新建站点，只需要再创建一个类似的站点配置清单即可。&lt;/p&gt;
&lt;h4 id=&#34;3-基础爬虫逻辑代码&#34;&gt;3. 基础爬虫逻辑代码&lt;/h4&gt;
&lt;p&gt;爬虫的库有很多，我选择了 &lt;a href=&#34;https://github.com/PuerkitoBio/goquery&#34;&gt;goquery&lt;/a&gt; 库底层爬虫支持库。爬虫逻辑代码是爬虫类型程序的核心，基本都是用配置的 &lt;code&gt;CSS selector&lt;/code&gt; 查找目标然后解析。例如 &lt;a href=&#34;https://github.com/ruriio/tidy/blob/bad08b0232318dfd70ff31f5d2ed25def51a2a52/scrape.go&#34;&gt;scrape.go&lt;/a&gt; 的代码片段。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Scrape&lt;/span&gt;(site Site) Meta {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; meta = Meta{}
log.&lt;span style=&#34;color:#c0f&#34;&gt;Printf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;url: %s&amp;#34;&lt;/span&gt;, site.Url)
resp, err &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;get&lt;/span&gt;(site)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; err &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
log.&lt;span style=&#34;color:#c0f&#34;&gt;Fatal&lt;/span&gt;(err)
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;defer&lt;/span&gt; resp.Body.&lt;span style=&#34;color:#c0f&#34;&gt;Close&lt;/span&gt;()
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; resp.StatusCode &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;200&lt;/span&gt; {
log.&lt;span style=&#34;color:#c0f&#34;&gt;Fatalf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;stats code error: %d %s&amp;#34;&lt;/span&gt;, resp.StatusCode, resp.Status)
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Load the HTML document
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; doc, err &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; goquery.&lt;span style=&#34;color:#c0f&#34;&gt;NewDocumentFromReader&lt;/span&gt;(resp.Body)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; err &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
log.&lt;span style=&#34;color:#c0f&#34;&gt;Fatal&lt;/span&gt;(err)
}
meta.Title = strings.&lt;span style=&#34;color:#c0f&#34;&gt;TrimSpace&lt;/span&gt;(doc.&lt;span style=&#34;color:#c0f&#34;&gt;Find&lt;/span&gt;(site.Title).&lt;span style=&#34;color:#c0f&#34;&gt;First&lt;/span&gt;().&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;())
doc.&lt;span style=&#34;color:#c0f&#34;&gt;Find&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.ttl-grp&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;Each&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt;(i &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt;, s &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;goquery.Selection) {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// For each item found, get the band and title
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; bind &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; s.&lt;span style=&#34;color:#c0f&#34;&gt;Find&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;a&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;()
title &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; s.&lt;span style=&#34;color:#c0f&#34;&gt;Find&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;i&amp;#34;&lt;/span&gt;).&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;()
fmt.&lt;span style=&#34;color:#c0f&#34;&gt;Printf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Review %d: %s - %s\n&amp;#34;&lt;/span&gt;, i, bind, title)
})
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; meta
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;代码片段中，使用 &lt;code&gt;goquery.NewDocumentFromReader(resp.Body)&lt;/code&gt; 创建了一个 &lt;code&gt;Document&lt;/code&gt; 对象，然后通过 &lt;code&gt;doc.Find(site.Title).First().Text()&lt;/code&gt; 查找到目标，并将字符串处理，返回 &lt;code&gt;meta&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;同时也对 &lt;code&gt;http GET&lt;/code&gt; 请求进行了封装，这里填充了 &lt;code&gt;header&lt;/code&gt; 字段，之后会增加 &lt;code&gt;cookie&lt;/code&gt; 支持:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;get&lt;/span&gt;(site Site) (&lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;http.Response, &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;error&lt;/span&gt;) {
client &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&lt;/span&gt;http.Client{}
req, err &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; http.&lt;span style=&#34;color:#c0f&#34;&gt;NewRequest&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;GET&amp;#34;&lt;/span&gt;, site.Url, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; err &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
log.&lt;span style=&#34;color:#c0f&#34;&gt;Fatal&lt;/span&gt;(err)
}
req.Header.&lt;span style=&#34;color:#c0f&#34;&gt;Set&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;User-Agent&amp;#34;&lt;/span&gt;, site.UserAgent)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; client.&lt;span style=&#34;color:#c0f&#34;&gt;Do&lt;/span&gt;(req)
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;同时为了调试方便，增加了打印 &lt;code&gt;html&lt;/code&gt; 的方法:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;printBody&lt;/span&gt;(resp &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;http.Response) {
body, err &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; ioutil.&lt;span style=&#34;color:#c0f&#34;&gt;ReadAll&lt;/span&gt;(resp.Body)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; err &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;nil&lt;/span&gt; {
log.&lt;span style=&#34;color:#c0f&#34;&gt;Fatal&lt;/span&gt;(err)
}
log.&lt;span style=&#34;color:#c0f&#34;&gt;Printf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;body: %s&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#366&#34;&gt;string&lt;/span&gt;(body))
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;4-selector-选择器封装&#34;&gt;4. Selector 选择器封装&lt;/h4&gt;
&lt;p&gt;在上一节的代码片断中，可以看到使用了 &lt;code&gt;doc.Find()&lt;/code&gt; 函数获取文本：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;Site{
Url: &lt;span style=&#34;color:#c0f&#34;&gt;url&lt;/span&gt;(id),
UserAgent: MobileUserAgent,
Title: &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.ttl-grp&amp;#34;&lt;/span&gt;,
}
meta.Title = strings.&lt;span style=&#34;color:#c0f&#34;&gt;TrimSpace&lt;/span&gt;(doc.&lt;span style=&#34;color:#c0f&#34;&gt;Find&lt;/span&gt;(site.Title).&lt;span style=&#34;color:#c0f&#34;&gt;First&lt;/span&gt;().&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;())&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;可以对这一断代码进行封装，作为预置项方便扩展和使用(&lt;a href=&#34;https://github.com/ruriio/tidy/blob/e20457ea9cafb1280a2886ca65471d8618e9a258/sites/site.go&#34;&gt;site.go&lt;/a&gt;)：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Selector &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
Id Item
Title Item
Actor Item
Poster Item
Series Item
Producer Item
Release Item
Duration Item
Sample Item
Images Item
Label Item
Genre Item
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;type&lt;/span&gt; Item &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;struct&lt;/span&gt; {
selector &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
replacer &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;strings.Replacer
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(selector &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) Item {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; Item{selector: selector, replacer: strings.&lt;span style=&#34;color:#c0f&#34;&gt;NewReplacer&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;)}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;replacer&lt;/span&gt;(selector &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;, replacer &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;strings.Replacer) Item {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; Item{selector: selector, replacer: replacer}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;Selector&lt;/code&gt; 中的每一个元素 &lt;code&gt;Item&lt;/code&gt; 对应于 &lt;code&gt;Meta&lt;/code&gt; 中的属性， &lt;code&gt;Item&lt;/code&gt; 又包含一个 &lt;code&gt;selector string&lt;/code&gt; 表示 &lt;code&gt;css selector&lt;/code&gt; 的文本，&lt;code&gt;replacer *strings.Replacer&lt;/code&gt; 是用来扩展 &lt;code&gt;Selector&lt;/code&gt; 的能力，&lt;strong&gt;使它可以支持替换文本的操作，这个能力在后文中会再多次增强&lt;/strong&gt;。此时的 &lt;a href=&#34;https://github.com/ruriio/tidy/blob/e20457ea9cafb1280a2886ca65471d8618e9a258/sites/dmm.go&#34;&gt;dmm.go&lt;/a&gt; 中站点配置也得到升级：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Dmm&lt;/span&gt;(id &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) Site {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; Site{
Url: &lt;span style=&#34;color:#c0f&#34;&gt;url&lt;/span&gt;(id),
UserAgent: MobileUserAgent,
Selector: Selector{
Title: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.ttl-grp&amp;#34;&lt;/span&gt;),
Actor: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;ul.parts-maindata &amp;gt; li &amp;gt; a &amp;gt; span&amp;#34;&lt;/span&gt;),
Poster: &lt;span style=&#34;color:#c0f&#34;&gt;replacer&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.package&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c0f&#34;&gt;NewReplacer&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;ps.jpg&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;pl.jpg&amp;#34;&lt;/span&gt;)),
Producer: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.parts-subdata&amp;#34;&lt;/span&gt;),
Sample: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.play-btn&amp;#34;&lt;/span&gt;),
Series: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;#work-mono-info &amp;gt; dl:nth-child(4) &amp;gt; dd&amp;#34;&lt;/span&gt;),
Release: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;#work-mono-info &amp;gt; dl:nth-child(8) &amp;gt; dd&amp;#34;&lt;/span&gt;),
Duration: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;#work-mono-info &amp;gt; dl:nth-child(9) &amp;gt; dd&amp;#34;&lt;/span&gt;),
Id: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;#work-mono-info &amp;gt; dl:nth-child(10) &amp;gt; dd&amp;#34;&lt;/span&gt;),
Label: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;#work-mono-info &amp;gt; dl:nth-child(6) &amp;gt; dd &amp;gt; ul &amp;gt; li &amp;gt; a&amp;#34;&lt;/span&gt;),
Genre: &lt;span style=&#34;color:#c0f&#34;&gt;selector&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;#work-mono-info &amp;gt; dl.box-genreinfo &amp;gt; dd &amp;gt; ul &amp;gt; li &amp;gt; a&amp;#34;&lt;/span&gt;),
Images: &lt;span style=&#34;color:#c0f&#34;&gt;replacer&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;#sample-list &amp;gt; ul &amp;gt; li &amp;gt; a &amp;gt; span &amp;gt; img&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c0f&#34;&gt;NewReplacer&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;-&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;jp-&amp;#34;&lt;/span&gt;)),
},
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;从这段代码可以看出，站点配置清单中的 &lt;code&gt;Selector&lt;/code&gt; 结构体元素初始化已经变为使用 &lt;code&gt;selector()&lt;/code&gt; 和 &lt;code&gt;replacer()&lt;/code&gt;操作函数执行，这段代码仍将在后文中优化。&lt;/p&gt;
&lt;h4 id=&#34;5-获取标签文字和属性&#34;&gt;5. 获取标签文字和属性&lt;/h4&gt;
&lt;p&gt;在上一节的代码片断中，可以看到使用了 &lt;code&gt;doc.Find(site.Title).First().Text()&lt;/code&gt; 来获取文本，同时也做了去首尾空白的处理，为了代码逻辑更合理，将这一块的代码提取到 &lt;code&gt;Site -&amp;gt; Selector -&amp;gt; Item&lt;/code&gt; 结构体中：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selector Item) &lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;(doc &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;goquery.Document) &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
text &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; strings.&lt;span style=&#34;color:#c0f&#34;&gt;TrimSpace&lt;/span&gt;(doc.&lt;span style=&#34;color:#c0f&#34;&gt;Find&lt;/span&gt;(selector.selector).&lt;span style=&#34;color:#c0f&#34;&gt;First&lt;/span&gt;().&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;())
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; selector.replacer.&lt;span style=&#34;color:#c0f&#34;&gt;Replace&lt;/span&gt;(text)
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selector Item) &lt;span style=&#34;color:#c0f&#34;&gt;Texts&lt;/span&gt;(doc &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;goquery.Document) []&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; texts []&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
doc.&lt;span style=&#34;color:#c0f&#34;&gt;Find&lt;/span&gt;(selector.selector).&lt;span style=&#34;color:#c0f&#34;&gt;Each&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt;(i &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt;, selection &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;goquery.Selection) {
text &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; strings.&lt;span style=&#34;color:#c0f&#34;&gt;TrimSpace&lt;/span&gt;(selection.&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;())
text = selector.replacer.&lt;span style=&#34;color:#c0f&#34;&gt;Replace&lt;/span&gt;(text)
texts = &lt;span style=&#34;color:#366&#34;&gt;append&lt;/span&gt;(texts, text)
})
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; texts
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这时，调用 &lt;code&gt;Selector.Item.Text(doc)&lt;/code&gt; 函数就可以获取到文本或文本数组内容了。进一步扩展，获取标签的属性内容，函数如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selector Item) &lt;span style=&#34;color:#c0f&#34;&gt;Attr&lt;/span&gt;(doc &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;goquery.Document, attr &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
src, exist &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; doc.&lt;span style=&#34;color:#c0f&#34;&gt;Find&lt;/span&gt;(selector.selector).&lt;span style=&#34;color:#c0f&#34;&gt;First&lt;/span&gt;().&lt;span style=&#34;color:#c0f&#34;&gt;Attr&lt;/span&gt;(attr)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; exist {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; selector.replacer.&lt;span style=&#34;color:#c0f&#34;&gt;Replace&lt;/span&gt;(strings.&lt;span style=&#34;color:#c0f&#34;&gt;TrimSpace&lt;/span&gt;(src))
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; (selector Item) &lt;span style=&#34;color:#c0f&#34;&gt;Attrs&lt;/span&gt;(doc &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;goquery.Document, attr &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;) []&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; attrs []&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;string&lt;/span&gt;
doc.&lt;span style=&#34;color:#c0f&#34;&gt;Find&lt;/span&gt;(selector.selector).&lt;span style=&#34;color:#c0f&#34;&gt;Each&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt;(i &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt;, selection &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;goquery.Selection) {
src, exist &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; selection.&lt;span style=&#34;color:#c0f&#34;&gt;Attr&lt;/span&gt;(attr)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; exist {
text &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; strings.&lt;span style=&#34;color:#c0f&#34;&gt;TrimSpace&lt;/span&gt;(src)
text = selector.replacer.&lt;span style=&#34;color:#c0f&#34;&gt;Replace&lt;/span&gt;(text)
attrs = &lt;span style=&#34;color:#366&#34;&gt;append&lt;/span&gt;(attrs, text)
}
})
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; attrs
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;例如，使用 &lt;code&gt;selector.Attr(doc, &amp;quot;href&amp;quot;)&lt;/code&gt; 即可获取到当前 &lt;code&gt;selector&lt;/code&gt; 的 &lt;code&gt;href&lt;/code&gt; 属性值。&lt;/p&gt;
&lt;p&gt;此时在 &lt;a href=&#34;https://github.com/ruriio/tidy/blob/e20457ea9cafb1280a2886ca65471d8618e9a258/scrape.go#L32&#34;&gt;scrape.go&lt;/a&gt; 爬虫逻辑中，通过使用如下封装获取所有的查找值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// extract meta data from web page
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; meta.Title = site.Title.&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;(doc)
meta.Actor = site.Actor.&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;(doc)
meta.Poster = site.Poster.&lt;span style=&#34;color:#c0f&#34;&gt;Image&lt;/span&gt;(doc)
meta.Producer = site.Producer.&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;(doc)
meta.Sample = site.Sample.&lt;span style=&#34;color:#c0f&#34;&gt;Link&lt;/span&gt;(doc)
meta.Series = site.Series.&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;(doc)
meta.Release = site.Release.&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;(doc)
meta.Duration = site.Duration.&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;(doc)
meta.Id = site.Id.&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;(doc)
meta.Label = site.Label.&lt;span style=&#34;color:#c0f&#34;&gt;Text&lt;/span&gt;(doc)
meta.Genre = site.Genre.&lt;span style=&#34;color:#c0f&#34;&gt;Texts&lt;/span&gt;(doc)
meta.Images = site.Images.&lt;span style=&#34;color:#c0f&#34;&gt;Images&lt;/span&gt;(doc)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;可以看到此时 &lt;code&gt;tidy&lt;/code&gt; 已经具备了基本的爬虫框架能力，即只需添加新的站点清单，执行代码就可以获取到新增加站点的信息。&lt;/p&gt;
&lt;p&gt;当前是通过在单元测试代码中打印 &lt;code&gt;meta&lt;/code&gt; 输出来检索信息的，例如：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-go&#34; data-lang=&#34;go&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;func&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;TestScrape&lt;/span&gt;(t &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;testing.T) {
meta &lt;span style=&#34;color:#555&#34;&gt;:=&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Scrape&lt;/span&gt;(sites.&lt;span style=&#34;color:#c0f&#34;&gt;Dmm&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;ssxx678&amp;#34;&lt;/span&gt;))
fmt.&lt;span style=&#34;color:#c0f&#34;&gt;Println&lt;/span&gt;(meta.&lt;span style=&#34;color:#c0f&#34;&gt;Json&lt;/span&gt;())
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;下一步继续增加站点，看在版本不断升级的过程中会遇到什么问题，并不断完善和改进。&lt;/p&gt;
&lt;hr /&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;从零开始的 Go 爬虫框架编程实战 - 上篇&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://busy.im/post/golang-spider-2/&#34;&gt;从零开始的 Go 爬虫框架编程实战 - 中篇&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href=&#34;https://busy.im/post/golang-spider-3/&#34;&gt;从零开始的 Go 爬虫框架编程实战 - 下篇&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item><item><title>Kotlin 代码样式指南</title><link>https://busy.im/post/kotlin-code-style/</link><pubDate>Fri, 28 Feb 2020 00:16:15 +0800</pubDate><guid>https://busy.im/post/kotlin-code-style/</guid><description>
&lt;p&gt;在学习完语言语法后，要学好和用好一门开发语言，代码风格和单元测试是最基础的两个内容。其中单元测试在每个语言中基本相同，而代码风格或代码样式则需要以每个语言的推荐风格为标准，保持统一和规范使用，这在团队协作和开源项目中非常重要。&lt;/p&gt;
&lt;p&gt;具体以方法或函数名命名来说例如 c 语言中使用 &lt;code&gt;your_name&lt;/code&gt; 这种下划线和小写字母的命名方式，而 Android Java 则使用 &lt;code&gt;yourName&lt;/code&gt; 这样的小驼峰式命名法。&lt;/p&gt;
&lt;p&gt;代码样式是前行者在实践中总结出的优良写作风格，遵循代码样式规范，不仅是一种良好的编程习惯，更是一种认真的态度，是对前辈先行者和后进同业者的尊重。&lt;/p&gt;
&lt;p&gt;不同的开发语言总是会有相应的推荐代码风格和单元测试方法，而这些在一般的教材书上却很少提及。我主要从事 Android 开发工作，常使用 Java 和 Kotlin 语言作为开发语言，遵循 Google 的 Android 开发代码样式规范。一般遵守 &lt;code&gt;Android Studio&lt;/code&gt; 或 &lt;code&gt;Intellij Idea&lt;/code&gt; 内置的代码风格规范即可，也可以根据需要自定义 &lt;code&gt;IDE&lt;/code&gt; 中的代码风格&lt;a href=&#34;https://stackoverflow.com/a/53196321/2600042&#34;&gt;配置文件&lt;/a&gt;。通常使用快捷键 ( Linux 下为 &lt;code&gt;CTR+ALT+L&lt;/code&gt;) 快速格式化代码风格。&lt;/p&gt;
&lt;p&gt;本文主要介绍 Kotlin 相对于 Java 在书写样式上的不同，摘自官方文档，全部规范内容可以直接参考文后的 &lt;a href=&#34;https://developer.android.com/kotlin/style-guide&#34;&gt;Kotlin 样式指南&lt;/a&gt;。&lt;/p&gt;
&lt;h4 id=&#34;大括号&#34;&gt;大括号&lt;/h4&gt;
&lt;p&gt;&lt;code&gt;when&lt;/code&gt; 分支不需要大括号，没有 &lt;code&gt;else if/else&lt;/code&gt; 分支且适合放在一行上的 &lt;code&gt;if&lt;/code&gt; 语句主体也不需要大括号。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (string.isEmpty()) &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;when&lt;/span&gt; (value) {
&lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt; -&amp;gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// …
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;除此以外，任何 &lt;code&gt;if&lt;/code&gt;、&lt;code&gt;for&lt;/code&gt;、&lt;code&gt;when&lt;/code&gt; 分支、&lt;code&gt;do&lt;/code&gt; 和 &lt;code&gt;while&lt;/code&gt; 语句都需要大括号，即使主体为空或仅包含一个语句也是如此。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (string.isEmpty())
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// WRONG!
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (string.isEmpty()) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Okay
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;表达式&#34;&gt;表达式&lt;/h4&gt;
&lt;p&gt;仅当整个表达式适合放在一行上时，用作表达式的 &lt;code&gt;if/else&lt;/code&gt; 条件语句才能省略大括号。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; value = &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (string.isEmpty()) &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Okay&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; value = &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (string.isEmpty()) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// WRONG!
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt;
&lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; value = &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (string.isEmpty()) { &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Okay
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;换行&#34;&gt;换行&lt;/h4&gt;
&lt;p&gt;代码的列限制为最多 100 个字符。除非是下面说明的情况，否则任何超过此限制的行都必须换行，如下所述。&lt;/p&gt;
&lt;p&gt;例外情况：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;无法遵循列限制的行（例如，KDoc 中的长网址）&lt;/li&gt;
&lt;li&gt;package 和 import 语句&lt;/li&gt;
&lt;li&gt;注释中可以剪切并粘贴到 shell 中的命令行&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;在何处换行&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;换行的首要原则是：更倾向于在较高的句法级别换行。此外：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;某行在非赋值运算符处换行时，换行符将在该符号前面。
&lt;ul&gt;
&lt;li&gt;这也适用于以下“类似运算符”的符号：&lt;/li&gt;
&lt;li&gt;点分隔符 (&lt;code&gt;.&lt;/code&gt;)。&lt;/li&gt;
&lt;li&gt;成员引用的两个冒号 (&lt;code&gt;::&lt;/code&gt;)。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;某行在赋值运算符处换行时，换行符将在该符号后面。&lt;/li&gt;
&lt;li&gt;方法或构造函数名称始终贴在它后面的左圆括号 (&lt;code&gt;(&lt;/code&gt;) 上。&lt;/li&gt;
&lt;li&gt;逗号 (&lt;code&gt;,&lt;/code&gt;) 始终贴在它前面的标记上。&lt;/li&gt;
&lt;li&gt;lambda 箭头 (&lt;code&gt;-&amp;gt;&lt;/code&gt;) 始终贴在它前面的参数列表上。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;注意&lt;/strong&gt;：换行的主要目标是让代码清晰，而不一定是让代码适合放在最少数量的行中。&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h4 id=&#34;函数&#34;&gt;函数&lt;/h4&gt;
&lt;p&gt;当函数签名不适合放在一行上时，应让每个参数声明独占一行。以这种格式定义的参数应使用单缩进 (+4)。右圆括号 (&lt;code&gt;)&lt;/code&gt;) 和返回类型独占一行，没有额外的缩进。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &amp;lt;T&amp;gt; &lt;span style=&#34;color:#c0f&#34;&gt;Iterable&lt;/span&gt;&amp;lt;T&amp;gt;.joinToString(
separator: CharSequence = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, &amp;#34;&lt;/span&gt;,
prefix: CharSequence = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;,
postfix: CharSequence = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;
): String {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// …
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; }&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;表达式函数&#34;&gt;表达式函数&lt;/h4&gt;
&lt;p&gt;当函数只包含一个表达式时，它可以表示为&lt;a href=&#34;https://kotlinlang.org/docs/reference/functions.html#single-expression-functions&#34;&gt;表达式函数&lt;/a&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;override&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;toString&lt;/span&gt;(): String {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Hey&amp;#34;&lt;/span&gt;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;override&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;toString&lt;/span&gt;(): String = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Hey&amp;#34;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;只有在表达式函数开始一个块时，才应换行。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;main&lt;/span&gt;() = runBlocking {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// …
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;否则，如果表达式函数增长到需要换行，应改用普通函数主体、&lt;code&gt;return&lt;/code&gt; 声明和普通表达式换行规则。&lt;/p&gt;
&lt;h4 id=&#34;属性&#34;&gt;属性&lt;/h4&gt;
&lt;p&gt;当属性初始化式不适合放在一行上时，应在等号 (&lt;code&gt;=&lt;/code&gt;) 后面换行，并使用缩进。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; defaultCharset: Charset? =
EncodingRegistry.getInstance().getDefaultCharsetForPropertiesFiles(file)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;声明 &lt;code&gt;get&lt;/code&gt; 和/或 &lt;code&gt;set&lt;/code&gt; 函数的属性应让每个函数独占一行，并使用正常的缩进 (+4)。对它们进行格式设置时，使用的规则与函数相同。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; directory: File? = &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;set&lt;/span&gt;(value) {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// …
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;只读属性可以使用适合放在一行上的较短语法。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; defaultExtension: String &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;get&lt;/span&gt;() = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;kt&amp;#34;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;枚举类&#34;&gt;枚举类&lt;/h4&gt;
&lt;p&gt;对于没有函数且没有关于其常量的文档的枚举，可以选择性地将其格式设为单行。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;enum&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#0a8;font-weight:bold&#34;&gt;Answer&lt;/span&gt; { YES, NO, MAYBE }&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;将枚举中的常量放在单独的行上时，它们之间不需要空白行，但它们定义主体时除外。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;enum&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#0a8;font-weight:bold&#34;&gt;Answer&lt;/span&gt; {
YES,
NO,
MAYBE {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;override&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;toString&lt;/span&gt;() = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&amp;#34;¯\_(ツ)_/¯&amp;#34;&amp;#34;&amp;#34;&lt;/span&gt;
}
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;由于枚举类是类，因此用于类格式设置的其他所有规则都适用。&lt;/p&gt;
&lt;h4 id=&#34;注解&#34;&gt;注解&lt;/h4&gt;
&lt;p&gt;应将成员或类型注解放在单独的行上，让其紧接在标注的构造前面。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; @Retention(SOURCE)
@Target(FUNCTION, PROPERTY_SETTER, FIELD)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;annotation&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#0a8;font-weight:bold&#34;&gt;Global&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;可以将不带参数的注解放在一行上。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; @JvmField @Volatile
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; disposable: Disposable? = &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;如果只存在一个不带参数的注解，则可以将其与声明放在同一行上。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; @Volatile &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; disposable: Disposable? = &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;
@Test &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;selectAll&lt;/span&gt;() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// …
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;隐式返回-属性类型&#34;&gt;隐式返回/属性类型&lt;/h4&gt;
&lt;p&gt;如果表达式函数主体或属性初始化式是标量值，或者可以根据主体明确推断出返回类型，则可以将其省略。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;override&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;toString&lt;/span&gt;(): String = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Hey&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// becomes
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;override&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;toString&lt;/span&gt;() = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Hey&amp;#34;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; ICON: Icon = IconLoader.getIcon(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/icons/kotlin.png&amp;#34;&lt;/span&gt;)
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// becomes
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; ICON = IconLoader.getIcon(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/icons/kotlin.png&amp;#34;&lt;/span&gt;)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;在编写库时，如果显式类型声明是公共 API 的一部分，则应将其保留。&lt;/p&gt;
&lt;h4 id=&#34;命名&#34;&gt;命名&lt;/h4&gt;
&lt;p&gt;标识符仅使用 ASCII 字母和数字，在下面所述的少数情况下，还会使用下划线。因此，每个有效的标识符名称都可匹配正则表达式 \w+。&lt;/p&gt;
&lt;p&gt;不使用特殊前缀或后缀（如在 name_、mName、s_name 和 kName 示例中看到的前缀或后缀），但后备属性除外（请参阅&lt;a href=&#34;https://developer.android.com/kotlin/style-guide#backing-properties&#34;&gt;后备属性&lt;/a&gt;）。&lt;/p&gt;
&lt;h4 id=&#34;软件包名称&#34;&gt;软件包名称&lt;/h4&gt;
&lt;p&gt;软件包名称全部为小写字母，连续的单词直接连接在一起（没有下划线）。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Okay
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;package&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;com.example.deepspace&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// WRONG!
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;package&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;com.example.deepSpace&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// WRONG!
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;package&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;com.example.deep_space&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;类型名称&#34;&gt;类型名称&lt;/h4&gt;
&lt;p&gt;类名采用 &lt;code&gt;PascalCase&lt;/code&gt; 大小写形式编写，通常是名词或名词短语。例如，&lt;code&gt;Character&lt;/code&gt; 或 &lt;code&gt;ImmutableList&lt;/code&gt;。接口名称也可以是名词或名词短语（例如 &lt;code&gt;List&lt;/code&gt;），但有时还可以是形容词或形容词短语（例如 &lt;code&gt;Readable&lt;/code&gt;）。&lt;/p&gt;
&lt;p&gt;测试类的命名方式是以测试的类的名称开头且以 &lt;code&gt;Test&lt;/code&gt; 结尾。例如，&lt;code&gt;HashTest&lt;/code&gt; 或 &lt;code&gt;HashIntegrationTest&lt;/code&gt;。&lt;/p&gt;
&lt;h4 id=&#34;函数名称&#34;&gt;函数名称&lt;/h4&gt;
&lt;p&gt;函数名称采用 &lt;code&gt;camelCase&lt;/code&gt; 大小写形式编写，通常是动词或动词短语。例如，&lt;code&gt;sendMessage&lt;/code&gt; 或 &lt;code&gt;stop&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;允许在测试函数名称中出现下划线，用于分隔名称的逻辑组成部分。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; @Test &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;pop_emptyStack&lt;/span&gt;() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// …
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;常量名称&#34;&gt;常量名称&lt;/h4&gt;
&lt;p&gt;常量名称使用 &lt;code&gt;UPPER_SNAKE_CASE&lt;/code&gt; 大小写形式：全部为大写字母，单词用下划线分隔。但究竟什么是常量呢？&lt;/p&gt;
&lt;p&gt;常量是没有自定义 &lt;code&gt;get&lt;/code&gt; 函数的 &lt;code&gt;val&lt;/code&gt; 属性，其内容绝对不可变，并且其函数没有可检测到的副作用。这包括不可变类型和不可变类型的不可变集合，以及标量和字符串（如果标记为 &lt;code&gt;const&lt;/code&gt;）。如果某个实例的任何可观察状态可以改变，则它不是常量。仅仅打算永不改变对象是不够的。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; NUMBER = &lt;span style=&#34;color:#f60&#34;&gt;5&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; NAMES = listOf(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Alice&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Bob&amp;#34;&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; AGES = mapOf(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Alice&amp;#34;&lt;/span&gt; to &lt;span style=&#34;color:#f60&#34;&gt;35&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Bob&amp;#34;&lt;/span&gt; to &lt;span style=&#34;color:#f60&#34;&gt;32&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; COMMA_JOINER = Joiner.on(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;,&amp;#39;&lt;/span&gt;) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Joiner is immutable
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; EMPTY_ARRAY = arrayOf()
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这些名称通常是名词或名词短语。&lt;/p&gt;
&lt;p&gt;常量值只能在 &lt;code&gt;object&lt;/code&gt; 内定义或定义为顶级声明。满足常量的要求但是在 &lt;code&gt;class&lt;/code&gt; 内定义的值必须使用非常量名称。&lt;/p&gt;
&lt;p&gt;作为标量值的常量必须使用 &lt;code&gt;const&lt;/code&gt; 修饰符。&lt;/p&gt;
&lt;h4 id=&#34;非常量名称&#34;&gt;非常量名称&lt;/h4&gt;
&lt;p&gt;非常量名称采用 &lt;code&gt;camelCase&lt;/code&gt; 大小写形式编写。这些适用于实例属性、本地属性和参数名称。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; variable = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;var&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; nonConstScalar = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;non-const&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; mutableCollection: MutableSet = HashSet()
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; mutableElements = listOf(mutableInstance)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; mutableValues = mapOf(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Alice&amp;#34;&lt;/span&gt; to mutableInstance, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Bob&amp;#34;&lt;/span&gt; to mutableInstance2)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; logger = Logger.getLogger(MyClass&lt;span style=&#34;color:#555&#34;&gt;::&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt;.java.name)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; nonEmptyArray = arrayOf(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;these&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;can&amp;#34;&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;change&amp;#34;&lt;/span&gt;)
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这些名称通常是名词或名词短语。&lt;/p&gt;
&lt;h4 id=&#34;后备属性&#34;&gt;后备属性&lt;/h4&gt;
&lt;p&gt;需要后备属性时，其名称应与实际属性的名称完全匹配，只不过带有下划线前缀。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; _table: Map? = &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; table: Map
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;get&lt;/span&gt;() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (_table == &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
_table = HashMap()
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; _table &lt;span style=&#34;color:#555&#34;&gt;?:&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;throw&lt;/span&gt; AssertionError()
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;更多关于注释、文档等内容基本和 Java 代码规范一致，请参考下面链接中的文档。&lt;/p&gt;
&lt;h3 id=&#34;参考&#34;&gt;参考&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;http://google.github.io/styleguide/&#34;&gt;Google Style Guides&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://developer.android.com/kotlin/style-guide&#34;&gt;Kotlin 样式指南&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://zh.wikipedia.org/wiki/%E9%A7%9D%E5%B3%B0%E5%BC%8F%E5%A4%A7%E5%B0%8F%E5%AF%AB&#34;&gt;驼峰式大小写&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://zh.wikipedia.org/wiki/%E5%8C%88%E7%89%99%E5%88%A9%E5%91%BD%E5%90%8D%E6%B3%95&#34;&gt;匈牙利命名法&lt;/a&gt;&lt;/p&gt;</description></item><item><title>如何管理多模块工程中的依赖?</title><link>https://busy.im/post/manage-dependencies-in-multi-module-project/</link><pubDate>Tue, 29 Oct 2019 16:42:34 +0800</pubDate><guid>https://busy.im/post/manage-dependencies-in-multi-module-project/</guid><description>
&lt;p&gt;如今，Android 应用程序的模块化已成为 Android 社区中的热门话题。每个模块的依赖性管理比以往任何时候都变得越来越重要。&lt;/p&gt;
&lt;p&gt;通过阅读本文，您将学会使用不同的方法来管理项目的依赖关系：使用根项目的 &lt;code&gt;ext&lt;/code&gt; 块，或通过在模块中引用包含依赖关系的独立文件中的依赖，或使用 &lt;code&gt;buildSrc&lt;/code&gt; 模块管理依赖。&lt;/p&gt;
&lt;h3 id=&#34;1-使用-ext-块&#34;&gt;1. 使用 ext 块&lt;/h3&gt;
&lt;p&gt;我们需要在项目的 &lt;code&gt;build.gradle&lt;/code&gt; 中定义所需的依赖项，以使该变量可以在所有子模块中访问&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;buildscript &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
ext &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
kotlin_version &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;1.3.11&amp;#39;&lt;/span&gt;
appcompat_version &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;28.0.0&amp;#34;&lt;/span&gt;
junit_version &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;4.12&amp;#34;&lt;/span&gt;
test_runner_version &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1.0.2&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt; &lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;我们在 &lt;code&gt;buildscript ext&lt;/code&gt; 块中定义了必需的依赖库版本，并更新模块的依赖关系为此处的版本变量。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;dependencies &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
implementation &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version&amp;#34;&lt;/span&gt;
implementation &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.android.support:appcompat-v7:$appcompat_version&amp;#34;&lt;/span&gt;
testImplementation &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;junit:junit:$junit_version&amp;#34;&lt;/span&gt;
androidTestImplementation &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.android.support.test:runner:$test_runner_version&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;总结：&lt;/strong&gt; 这种方式非常简单明了，支持版本更新检查提示，可以方便的管理和维护。仅在单一项目中集成很合适，但是要在多个工程中共享依赖版本则需要拷贝 &lt;code&gt;ext&lt;/code&gt; 块。&lt;/p&gt;
&lt;h3 id=&#34;2-从单独文件中引用&#34;&gt;2. 从单独文件中引用&lt;/h3&gt;
&lt;p&gt;这个方法与上一个方法类似，但看起来更为简洁，因为所有依赖项和版本均从单独的文件中引用。&lt;/p&gt;
&lt;p&gt;可以参考 &lt;a href=&#34;https://github.com/gokhanaliccii/TrendyGifs/blob/develop/dependencies.gradle&#34;&gt;dependencies.gradle &lt;/a&gt; 文件，我们新建一个 &lt;code&gt;dependencies.gradle&lt;/code&gt; 文件：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;ext &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
kotlinVersion &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;1.3.11&amp;#39;&lt;/span&gt;
appCompatVersion &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1.1.0&amp;#34;&lt;/span&gt;
constraintLayoutVersion &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1.1.0&amp;#34;&lt;/span&gt;
junit4Version &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;4.12&amp;#34;&lt;/span&gt;
testRunnerVersion &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1.2.0&amp;#34;&lt;/span&gt;
libraries &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;[&lt;/span&gt;
kotlinStdLib &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;org.jetbrains.kotlin:kotlin-stdlib-jdk7:&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; kotlinVersion&lt;span style=&#34;color:#555&#34;&gt;,&lt;/span&gt;
appCompat &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;androidx.appcompat:appcompat:&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; appCompatVersion&lt;span style=&#34;color:#555&#34;&gt;,&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;constraintLayout:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;androidx.constraintlayout:constraintlayout:&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; constraintLayoutVersion
&lt;span style=&#34;color:#555&#34;&gt;]&lt;/span&gt;
testLibraries &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;[&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;junit4:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;junit:junit:&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; junit4Version
&lt;span style=&#34;color:#555&#34;&gt;]&lt;/span&gt;
androidTestLibraries &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;[&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;testRunner:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.android.support.test:runner:&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; testRunnerVersion
&lt;span style=&#34;color:#555&#34;&gt;]&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;buildscript &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
apply &lt;span style=&#34;color:#99f&#34;&gt;from:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;dependencies.gradle&amp;#39;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;在项目 &lt;code&gt;buildscript&lt;/code&gt; 块中的 引用 &lt;code&gt;dependencies.gradle&lt;/code&gt; 文件，以便所有模块可以访问依赖变量，并使用依赖变量更新子模块的依赖关系。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;dependencies &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
implementation libraries&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#309&#34;&gt;kotlinStdLib&lt;/span&gt;
implementation libraries&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#309&#34;&gt;appCompat&lt;/span&gt;
testImplementation testLibraries&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#309&#34;&gt;junit4&lt;/span&gt;
androidTestImplementation androidTestLibraries&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#309&#34;&gt;testRunner&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;总结：&lt;/strong&gt; 这种方式可以隐藏在模块中依赖的版本细节，同时 &lt;code&gt;apply from&lt;/code&gt; 方式引入 &lt;code&gt;ext&lt;/code&gt; 块，可以将文件托管到网络上并从网络引入，可以方便的跨多个工程统一管理依赖版本，但是不能很直观的理解，不支持版本更新提示。&lt;/p&gt;
&lt;h3 id=&#34;3-使用-buildsrc&#34;&gt;3. 使用 &lt;code&gt;buildSrc&lt;/code&gt;&lt;/h3&gt;
&lt;p&gt;通过这种方法，我们可以在所有模块的 &lt;code&gt;build.gradle&lt;/code&gt; 文件中使用代码补全。我们可以在项目结构里使用 &lt;code&gt;Kotlin&lt;/code&gt; 或 &lt;code&gt;Java&lt;/code&gt; 来定义和管理依赖项。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用 &lt;code&gt;Java&lt;/code&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;首先，我们需要在项目的根目录中创建 &lt;code&gt;buildSrc&lt;/code&gt; 目录。如果要使用 &lt;code&gt;Java&lt;/code&gt;，则需要创建 &lt;code&gt;src/main/java/Dependency&lt;/code&gt; 类并定义依赖项。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/manage-dependencies-buildsrc.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;我们将类命名为Dependencies，但是您可以根据需要随意命名。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; Dependencies {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; APP_COMPAT_VERSION &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1.1.0&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; KOTLIN_VERSION &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1.3.41&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; APP_COMPAT &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;androidx.appcompat:appcompat:&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; APP_COMPAT_VERSION;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; KOTLIN_STDLIB &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;org.jetbrains.kotlin:kotlin-stdlib-jdk7:&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; KOTLIN_VERSION;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;然后，在模块 &lt;code&gt;build.gralde&lt;/code&gt; 文件中添加依赖关系：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/manage-dependencies-auto-complete.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;使用 Kotlin&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;我们需要在项目的根目录中创建 &lt;code&gt;buildSrc&lt;/code&gt; 目录，并在 &lt;code&gt;buildSrc&lt;/code&gt; 目录中创建 &lt;code&gt;build.gradle.kts&lt;/code&gt;。然后在 &lt;code&gt;build.gradle.kts&lt;/code&gt; 中附加 &lt;code&gt;kotlin-dsl&lt;/code&gt; 插件。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;plugins &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;`&lt;/span&gt;kotlin&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;dsl&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;`&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;
repositories &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
jcenter&lt;span style=&#34;color:#555&#34;&gt;()&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;Versions.APP_COMPAT_VERSION&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;Versions.CONSTRAINT_VERSION&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#0cf;font-weight:bold&#34;&gt;Versions.KOTLIN_VERSION&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;object&lt;/span&gt; &lt;span style=&#34;color:#0a8;font-weight:bold&#34;&gt;Versions&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; APP_COMPAT_VERSION = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1.1.0&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; KOTLIN_VERSION = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1.3.41&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;const&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; CONSTRAINT_VERSION = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1.1.0&amp;#34;&lt;/span&gt;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;object&lt;/span&gt; &lt;span style=&#34;color:#0a8;font-weight:bold&#34;&gt;Dependencies&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; APP_COMPAT = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;androidx.appcompat:appcompat:$APP_COMPAT_VERSION&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; KOTLIN_STDLIB = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;org.jetbrains.kotlin:kotlin-stdlib-jdk7:$KOTLIN_VERSION&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; CONSTRAINT_LAYOUT = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;androidx.constraintlayout:constraintlayout:$CONSTRAINT_VERSION&amp;#34;&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;最后，从 &lt;code&gt;Deps&lt;/code&gt; 对象中引用依赖：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/manage-dependencies-auto-complete-kotlin.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;总结：&lt;/strong&gt; 这种方式可以在代码中很方便的集中管理依赖关系，简单易懂，支持点击跳转和自动补全。但是不支持版本更新检查，也不能方便的跨项目使用，可以通过 &lt;code&gt;git submodule&lt;/code&gt; 的方式跨项目使用。&lt;/p&gt;
&lt;h3 id=&#34;使用场景&#34;&gt;使用场景&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;对于这三种方式，如每一个小结的总结，各有利弊，可以分为如下几个使用场景：&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;1. 如果考虑到版本更新提示，则方式一 &lt;code&gt;ext&lt;/code&gt; 块最佳；&lt;/p&gt;
&lt;p&gt;2. 如果不考虑版本更新提示，需要跨项目管理，则方式二独立文件最佳；&lt;/p&gt;
&lt;p&gt;3. 如果不考虑版本更新提示，需要自动补全和点击跳转，则方式三 &lt;code&gt;buildSrc&lt;/code&gt; 最佳；&lt;/p&gt;
&lt;p&gt;4. 如果不考虑版本更新提示，需要自动补全，且需要跨项目管理，则可以使用方式三 &lt;code&gt;buildSrc&lt;/code&gt; 搭配 &lt;code&gt;git submodule&lt;/code&gt; 管理；&lt;/p&gt;
&lt;p&gt;5. 如果考虑到版本更新提示，且需要跨项目管理，则可以仅在方式二独立文件中包含版本号信息，并托管到远程服务上，只统一管理版本号更新；&lt;/p&gt;
&lt;p&gt;6. 如果需要版本更新提示，且需要自动补全，对不起，目前还没有方法；&lt;/p&gt;
&lt;p&gt;7. 如果您既要版本更新提示，又要跨项目管理，又要自动补全，对不起，目前还没有方法，您可能需要自己编写插件来支持。当然您也可以单独创建一个工程，结合场景4和场景1共用版本号，同时可以使用 &lt;code&gt;ci&lt;/code&gt; 每天自动编译通过 &lt;code&gt;Lint&lt;/code&gt; 检查版本更新，有更新时手动或写脚本自动提交更新。&lt;/p&gt;
&lt;h3 id=&#34;参考&#34;&gt;参考&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://proandroiddev.com/how-to-manage-dependencies-in-multi-module-project-84620afbb415&#34;&gt;How to manage dependencies in a multi module project?&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Android 工程的 Vagrant Gitlab Runner</title><link>https://busy.im/post/vagrant-gitlab-runner-for-android/</link><pubDate>Mon, 29 Jul 2019 15:30:01 +0800</pubDate><guid>https://busy.im/post/vagrant-gitlab-runner-for-android/</guid><description>
&lt;p&gt;Gitlab runner 是一个优秀的持续集成工具，通过 runner 我们可以执行自动化编译、静态代码检查、单元测试、UI 测试，发布等操作，是高效快速迭代开发的必要工具。 GitLab Runner 独立运行于 Gitlab 实例，可以非常容易的扩展和安装。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/vagrant-runner.png&#34; alt=&#34;vagrant runner&#34; /&gt;&lt;/p&gt;
&lt;h3 id=&#34;排队竞争&#34;&gt;排队竞争&lt;/h3&gt;
&lt;p&gt;当团队内项目增多后，代码提交会变得频繁，单一的 runner 实例执行 CI 操作会出现占用时间较长，排队等待时间较长的问题。尤其对于 Android 工程，一个编译任务一般需要几分钟才能执行完毕，这时我们就需要对 runner 服务进行扩充，增加 runner 的数量。&lt;/p&gt;
&lt;p&gt;为了最佳化实践，我们团队使用的是 GitLab Runner + Docker 的运行方式，runner 的每一个编译任务都在 docker 容器 (&lt;a href=&#34;https://github.com/xdtianyu/docker-auto-builds/tree/master/openjdk8&#34;&gt;xdtianyu/openjdk8&lt;/a&gt;) 内运行，容器内集成了 Java/SDK/NDK 基础环境。这种方式在 Linux/MacOS 安装和执行非常简单方便，但是在 Windows 环境下却不那么容易，因为 Gitlab Runner 和 docker 在 Windows 系统上配置和运行都很困难，不能快速分发给不同的 Windows 主机执行。&lt;/p&gt;
&lt;p&gt;而团队内几乎所有开发机还在使用 Windows 系统，所以要利用闲置计算资源扩充 runner 数量就需要一个独立于宿主机环境，能快速分发且操作简单的解决方案。&lt;/p&gt;
&lt;h3 id=&#34;扩充方案&#34;&gt;扩充方案&lt;/h3&gt;
&lt;p&gt;要独立宿主环境，虚拟机是必然选择；要跨平台快速分发虚拟机且操作简单，Vagrant 则是必然选择。具体方案概括为 &lt;code&gt;Vagrant + VirtualBox 虚拟机 + Gitlab Runner + Docker&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;最终的代码请参考 &lt;a href=&#34;https://github.com/xdtianyu/vagrant/tree/master/gitlab-runner&#34;&gt;xdtianyu/vagrant/gitlab-runner&lt;/a&gt;，克隆代码后，只需修改 &lt;code&gt;.env&lt;/code&gt; 文件，添加 Gitlab 实例地址及 Runner Token, 执行 &lt;code&gt;vagrant up --provision&lt;/code&gt; 命令后即可运行扩充 Runner 服务。&lt;/p&gt;
&lt;h3 id=&#34;使用步骤&#34;&gt;使用步骤&lt;/h3&gt;
&lt;p&gt;1. 系统安装 Vagrant 及 VirtualBox，如果是 Windows 系统，请升级 PowserShell 到 3.0 及以上版本。&lt;/p&gt;
&lt;p&gt;2. 克隆代码，同时修改 &lt;code&gt;.env&lt;/code&gt; 文件中的环境变量。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;color:#033&#34;&gt;URL&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;YOUR_GITLAB_URL&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#033&#34;&gt;TOKEN&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;YOUR_TOKEN&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#033&#34;&gt;PROXY_HOST&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;YOUR_PROXY_HOST&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#033&#34;&gt;PROXY_PORT&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;YOUR_PROXY_PORT&amp;#34;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;如果不需要适用代理，请将 &lt;code&gt;PROXY_HOST&lt;/code&gt; &lt;code&gt;PROXY_PORT&lt;/code&gt; 设置为空。&lt;/p&gt;
&lt;p&gt;3. 执行 &lt;code&gt;vagrant&lt;/code&gt; 命令启动 runner 服务&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;vagrant up --provision&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;hr /&gt;
&lt;h3 id=&#34;关闭虚拟机&#34;&gt;关闭虚拟机&lt;/h3&gt;
&lt;p&gt;可以使用如下命令关闭虚拟机&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;vagrant halt&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;您可以通过 &lt;code&gt;vagrant up --provision&lt;/code&gt; 命令再次启动虚拟机。&lt;/p&gt;
&lt;h3 id=&#34;注册&#34;&gt;注册&lt;/h3&gt;
&lt;p&gt;Runner 服务只会在第一次运行时注册，之后运行不会再重复注册。这个功能是通过 &lt;code&gt;bootstrap.sh&lt;/code&gt; 中的如下代码实现的：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;[&lt;/span&gt; ! -z &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;$(&lt;/span&gt;cat /etc/gitlab-runner/config.toml |grep url&lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;\ &lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;&lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;\ \&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$URL&lt;/span&gt;&lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;\&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;)&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;]&lt;/span&gt;; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;then&lt;/span&gt;
&lt;span style=&#34;color:#366&#34;&gt;echo&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Runner is already registered.&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#366&#34;&gt;exit&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fi&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;销毁&#34;&gt;销毁&lt;/h3&gt;
&lt;p&gt;可以使用如下命令销毁虚拟机&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;vagrant destroy
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;之后需要在 GitLab 移除已经销毁的 runner 服务。&lt;/p&gt;
&lt;h3 id=&#34;主机名&#34;&gt;主机名&lt;/h3&gt;
&lt;p&gt;虚拟机的主机名会根据运行其的宿主机自动命名，这个功能是在 &lt;code&gt;Vagrantfile&lt;/code&gt; 文件中的如下代码实现的：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;config&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;vm&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;hostname &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;#{&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;`hostname`&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;0&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;..-&lt;/span&gt;&lt;span style=&#34;color:#f60&#34;&gt;2&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;]&lt;/span&gt;&lt;span style=&#34;color:#a00&#34;&gt;}&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;-runner&amp;#34;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;例如，如果你的电脑主机名为 &lt;code&gt;Peter-Desktop&lt;/code&gt;，那么虚拟机的主机名将会被设置为 &lt;code&gt;Peter-Desktop-runner&lt;/code&gt;。同时， Gitlab Runner 的名称也将被设置为虚拟机主机名，这样可以使同一个 Gitlab 服务中辨识不同 runner 服务变得容易。&lt;/p&gt;
&lt;h3 id=&#34;下载速度慢&#34;&gt;下载速度慢&lt;/h3&gt;
&lt;p&gt;如果您访问 vagrant 托管的速度较慢，可以使用任意文件下载工具下载如下镜像文件到本地：&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://vagrantcloud.com/xdtianyu/boxes/gitlab-runner/versions/1.0.0/providers/virtualbox.box&#34;&gt;https://vagrantcloud.com/xdtianyu/boxes/gitlab-runner/versions/1.0.0/providers/virtualbox.box &lt;/a&gt;&lt;/p&gt;
&lt;p&gt;然后通过如下命令添加虚拟机镜像到仓库：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;vagrant box add xdtianyu/gitlab-runner gitlab-runner.box&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;您也可以将此 box 文件托管到内网服务器，然后修改 &lt;code&gt;Vagrantfile&lt;/code&gt; 文件中的下列行：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;# config.vm.box_url = [&amp;#34;https://YOUR_LAN_HOSTING/gitlab-runner.box&amp;#34;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;例如，您将此文件托管在 &lt;code&gt;http://10.0.0.1:8080/gitlab-runner.box&lt;/code&gt;，则修改配置为：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-ruby&#34; data-lang=&#34;ruby&#34;&gt;config&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;vm&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;box_url &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;[&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;http://10.0.0.1:8080/gitlab-runner.box&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;之后您可以将此仓库包含 &lt;code&gt;.env&lt;/code&gt; 文件分发给同事，他们只需执行 &lt;code&gt;vagrant up --provision&lt;/code&gt; 命令即可快速启动 runner 服务。&lt;/p&gt;
&lt;h3 id=&#34;登录到虚拟机&#34;&gt;登录到虚拟机&lt;/h3&gt;
&lt;p&gt;通过如下命令登录到虚拟机：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;vagrant ssh&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;默认的用户名及 root 密码都是 &lt;code&gt;vagrant&lt;/code&gt;，您无需密码即可通过 &lt;code&gt;sudo&lt;/code&gt; 执行命令。&lt;/p&gt;
&lt;h3 id=&#34;gitlab-ci-yml-示例&#34;&gt;.gitlab-ci.yml 示例&lt;/h3&gt;
&lt;p&gt;请参考 &lt;a href=&#34;https://github.com/xdtianyu/CallerInfo&#34;&gt;xdtianyu/CallerInfo&lt;/a&gt; 仓库中的 &lt;code&gt;.gitlab-ci.yml&lt;/code&gt; 文件。您也可以在此 runner 虚拟机中使用 &lt;code&gt;ndk&lt;/code&gt; 编译、单元测试、UI 测试。&lt;/p&gt;
&lt;h3 id=&#34;项目地址&#34;&gt;项目地址&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/xdtianyu/vagrant&#34;&gt;https://github.com/xdtianyu/vagrant&lt;/a&gt;&lt;/p&gt;</description></item><item><title>分布式 MinIO s3 云存储实践</title><link>https://busy.im/post/distributed-minio/</link><pubDate>Mon, 10 Jun 2019 15:27:57 +0800</pubDate><guid>https://busy.im/post/distributed-minio/</guid><description>
&lt;p&gt;MinIO 是一款优秀的开源云存储服务，兼容亚马逊 S3 云存储。它最适合存储非结构化数据，如照片，视频，日志文件，备份和容器/ VM映像。 对象的大小可以从几KB到最大5TB。&lt;/p&gt;
&lt;p&gt;MinIO 是一个轻量级服务器，可以与应用程序堆栈捆绑在一起，类似于 NodeJS，Redis 和 MySQL。&lt;/p&gt;
&lt;h3 id=&#34;通过-docker-运行&#34;&gt;通过 docker 运行&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;docker pull minio/minio
docker run -p &lt;span style=&#34;color:#f60&#34;&gt;9000&lt;/span&gt;:9000 minio/minio server /data&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;分布式运行实践&#34;&gt;分布式运行实践&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/minio.png&#34; alt=&#34;minio&#34; /&gt;&lt;/p&gt;
&lt;p&gt;要通过分布式方式运行 minio， 需要配置不少于4个的偶数 minio 运行实例。&lt;/p&gt;
&lt;p&gt;我们首先编写一个 DockerFile 文件，用于快速配置及运行 minio 实例，再通过 &lt;code&gt;docker-compose.yml&lt;/code&gt; 传入执行参数 &lt;code&gt;MINIO_VOLUMES&lt;/code&gt; 并运行 docker 实例。&lt;/p&gt;
&lt;p&gt;每一个实例对应不同的 &lt;code&gt;MINIO_VOLUMES&lt;/code&gt; 参数，例如 hk 实例使用参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MINIO_VOLUMES: &amp;quot;https://minio:9199/data/1 https://s3-jp.xdty.org:443/data/2 https://s3-sh.xdty.org:443/data/3 https://s3-tx.xdty.org:443/data/4&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;而 jp 实例使用参数：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;MINIO_VOLUMES: &amp;quot;https://minio:9199/data/2 https://s3-hk.xdty.org:443/data/1 https://s3-sh.xdty.org:443/data/3 https://s3-tx.xdty.org:443/data/4&amp;quot;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;其他节点依次类推，注意这里的节点可以运行在同一个服务器上，例如每个节点实例运行在同一个服务器的不同存储目录下；也可以运行在不同的服务器上。&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;9199&lt;/code&gt; 端口是当前实例 docker 运行监听的端口，由于 minio 分布式实例不能同时使用 HTTP 和 HTTPS ，为了能和其他实例通信，docker 内的实例也启用了 TLS。&lt;/p&gt;
&lt;p&gt;最后通过 nginx 作为前端代理，将 minio 服务转到 443 端口。&lt;/p&gt;
&lt;p&gt;当所有节点实例运行起来后，会和其他节点实例通信并建立连接。这时当用户上传新文件到某一个节点时，其他节点也会同步数据；在某一个节点增加用户也会同步到其他节点。&lt;/p&gt;
&lt;p&gt;更多关于分布式服务的细节请参考官方文档 &lt;a href=&#34;https://docs.min.io/docs/distributed-minio-quickstart-guide.html&#34;&gt;https://docs.min.io/docs/distributed-minio-quickstart-guide.html &lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;示例代码&#34;&gt;示例代码&lt;/h3&gt;
&lt;p&gt;如下代码仅供参考，已经包含了所有运行分布式 minio 实例配置文件。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Dockerfile&lt;/code&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-dockerfile&#34; data-lang=&#34;dockerfile&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;FROM&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt; alpine&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;MAINTAINER&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt; xdtianyu@gmail.com&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;ENV&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt; GID=33 UID=33&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;ADD&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt; https://dl.min.io/server/minio/release/linux-amd64/minio /usr/bin/minio&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;RUN&lt;/span&gt; apk &lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;&lt;/span&gt; --update &lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;&lt;/span&gt; --no-cache &lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;&lt;/span&gt; add su-exec &lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;&lt;/span&gt; ca-certificates &lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;&lt;/span&gt; openssl &lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;&lt;/span&gt; tini &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;\
&lt;/span&gt;&lt;span style=&#34;color:#c30;font-weight:bold&#34;&gt;&lt;/span&gt; chmod +x /usr/bin/minio&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;COPY run.sh /run.sh&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;ENV&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt; MINIO_VOLUMES &amp;#34;/data&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;ENV&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt; MINIO_OPTS &amp;#34;server --address 0.0.0.0:9199&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;ENV&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt; MINIO_ACCESS_KEY &amp;#34;73mPXCPJzhpPLIiFnf&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;ENV&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt; MINIO_SECRET_KEY &amp;#34;M56OsPqKxBFxNqnmSOBVtfbIziKHMCD8iugu&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;VOLUME&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt; &amp;#34;/data&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;EXPOSE&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt; 9199&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;CMD&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt; [&amp;#34;sh&amp;#34;, &amp;#34;run.sh&amp;#34;]&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;run.sh&lt;/code&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;&lt;span style=&#34;color:#099&#34;&gt;#!/bin/sh
&lt;/span&gt;&lt;span style=&#34;color:#099&#34;&gt;&lt;/span&gt;
&lt;span style=&#34;color:#366&#34;&gt;set&lt;/span&gt; -e
mkdir -p /data
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;[&lt;/span&gt; ! -d &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/certs&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;]&lt;/span&gt;; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;then&lt;/span&gt;
mkdir -p /certs
&lt;span style=&#34;color:#366&#34;&gt;cd&lt;/span&gt; /certs
openssl genrsa &lt;span style=&#34;color:#f60&#34;&gt;1024&lt;/span&gt; &amp;gt; private.key
chmod &lt;span style=&#34;color:#f60&#34;&gt;400&lt;/span&gt; private.key
openssl req -new -key private.key -out private.csr -subj &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/C=UK/ST=Warwickshire/L=Leamington/O=OrgName/OU=IT Department/CN=minio&amp;#34;&lt;/span&gt;
openssl x509 -req -days &lt;span style=&#34;color:#f60&#34;&gt;36500&lt;/span&gt; -in private.csr -signkey private.key -out public.crt
&lt;span style=&#34;color:#366&#34;&gt;cd&lt;/span&gt; -
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fi&lt;/span&gt;
chown -R &lt;span style=&#34;color:#033&#34;&gt;$UID&lt;/span&gt;:&lt;span style=&#34;color:#033&#34;&gt;$GID&lt;/span&gt; /data /certs
&lt;span style=&#34;color:#366&#34;&gt;echo&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Starting minio...&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#366&#34;&gt;exec&lt;/span&gt; su-exec &lt;span style=&#34;color:#033&#34;&gt;$UID&lt;/span&gt;:&lt;span style=&#34;color:#033&#34;&gt;$GID&lt;/span&gt; /sbin/tini -- /usr/bin/minio &lt;span style=&#34;color:#033&#34;&gt;$MINIO_OPTS&lt;/span&gt; &lt;span style=&#34;color:#033&#34;&gt;$MINIO_VOLUMES&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;docker-compose.yml&lt;/code&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;version:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;3&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;services:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;nginx:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;image:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;xdtianyu/hub:nginx&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;restart:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;always&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;container_name:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;nginx&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;volumes:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;/etc/nginx/conf.d:/etc/nginx/conf.d&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;/etc/nginx/le:/etc/nginx/le&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;/var/www:/var/www&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;minio:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;image:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;xdtianyu/hub:minio&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;restart:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;always&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;container_name:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;minio&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;ports:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;127.0.0.1:9199:9199&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;volumes:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;/home/minio/xdty/data:/data&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;environment:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;MINIO_ACCESS_KEY:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;xxxxxxxx&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;MINIO_SECRET_KEY:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;xxxxxxxxxxxxxxxx&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;MINIO_OPTS:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;-S /certs server --address minio:9199&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;MINIO_VOLUMES:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://minio:9199/data/1 https://s3-jp.xdty.org:443/data/2 https://s3-sh.xdty.org:443/data/3 https://s3-tx.xdty.org:443/data/4&amp;#34;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;nginx 配置文件 &lt;code&gt;s3-xdty.conf&lt;/code&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-nginx&#34; data-lang=&#34;nginx&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;server&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;include&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;listen.conf&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;server_name&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;s3-jp.xdty.org&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;s3-hk.xdty.org&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;s3-tx.xdty.org&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;s3-sh.xdty.org&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;charset&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;utf-8&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;access_log&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;/var/log/nginx/&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$host.access.log&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;client_max_body_size&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;24M&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;root&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;/var/www/&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$scheme&lt;/span&gt; = &lt;span style=&#34;color:#c30&#34;&gt;http)&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;301&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;https://&lt;/span&gt;&lt;span style=&#34;color:#033&#34;&gt;$http_host$request_uri&lt;/span&gt;;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;location&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;/&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;proxy_pass&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;https://minio:9199&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;include&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;proxy_params&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;proxy_http_version&lt;/span&gt; &lt;span style=&#34;color:#f60&#34;&gt;1&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;.1&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;proxy_set_header&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;Connection&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;;
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;参考&#34;&gt;参考&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/minio/minio&#34;&gt;https://github.com/minio/minio&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://docs.min.io/docs/distributed-minio-quickstart-guide.html&#34;&gt;https://docs.min.io/docs/distributed-minio-quickstart-guide.html&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Android 应用内存数据分析方法</title><link>https://busy.im/post/android-memory-profile/</link><pubDate>Mon, 29 Apr 2019 16:20:44 +0800</pubDate><guid>https://busy.im/post/android-memory-profile/</guid><description>
&lt;p&gt;内存优化是 Android 开发中一个非常重要的环节，如果不注意就可能出现&amp;gt;内存泄漏，内存溢出，应用运行缓慢，效率低下等问题，严重影响用户体验。本文主要介绍
Android 应用内存的抓取和内存数据分析方法。&lt;/p&gt;
&lt;h3 id=&#34;一-android-profiler-分析工具&#34;&gt;一. Android Profiler 分析工具&lt;/h3&gt;
&lt;h4 id=&#34;1-profiler-概览信息&#34;&gt;1. Profiler 概览信息&lt;/h4&gt;
&lt;p&gt;使用 Android Studio profiler 工具对进程内存进行追踪，可以看到如下图的内存概览信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/1.png&#34; alt=&#34;profiler&#34; /&gt;&lt;/p&gt;
&lt;p&gt;从概览信息中可以看到 Java, Code 部分内存占用较高，可以参考 Android 官网 &lt;a href=&#34;https://developer.android.com/studio/profile/memory-profiler?hl=zh-cn&#34;&gt;memory-profiler &lt;/a&gt; 的相关内容。&lt;/p&gt;
&lt;p&gt;内存计数中的类别如下所示：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Java&lt;/strong&gt;：从 Java 或 Kotlin 代码分配的对象内存。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Native&lt;/strong&gt;：从 C 或 C++ 代码分配的对象内存。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;即使您的应用中不使用 C++，您也可能会看到此处使用的一些原生内存，因为 Android 框架使用原生内存代表您处理各种任务，如处理图像资源和其他图形时，即使您编写的代码采用 Java 或 Kotlin 语言。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Graphics&lt;/strong&gt;：图形缓冲区队列向屏幕显示像素（包括 GL 表面、GL 纹理等等）所使用的内存。 （请注意，这是与 CPU 共享的内存，不是 GPU 专用内存。）&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Stack&lt;/strong&gt;： 您的应用中的原生堆栈和 Java 堆栈使用的内存。 这通常与您的应用运行多少线程有关。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Code&lt;/strong&gt;：您的应用用于处理代码和资源（如 dex 字节码、已优化或已编译的 dex 码、.so 库和字体）的内存。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Other&lt;/strong&gt;：您的应用使用的系统不确定如何分类的内存。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Allocated&lt;/strong&gt;：您的应用分配的 Java/Kotlin 对象数。 它没有计入 C 或 C++ 中分配的对象。&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h4 id=&#34;2-profiler-内存分析&#34;&gt;2. Profiler 内存分析&lt;/h4&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/2.png&#34; alt=&#34;memory dump&#34; /&gt;&lt;/p&gt;
&lt;p&gt;点击左上角 &lt;code&gt;dump java heap&lt;/code&gt; 按钮，或者使用 &lt;code&gt;CTRL+D&lt;/code&gt; 快捷键，可以抓取进程的内存信息，在这里可以快速查看对象的分配数量及占用的大小和一些调用的堆栈。这里的功能比较有限，我们需要使用 &lt;code&gt;Export Heap Dump&lt;/code&gt; 按钮将内存数据导出为 &lt;code&gt;hprof&lt;/code&gt; 文件然后再使用 MAT 工具分析。使用 MAT 前，需要使用 SDK 中的 &lt;code&gt;hprof-conv&lt;/code&gt; 工具转换版本：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;hprof-conv memory-20190429T103715.hprof a.hprof
&lt;/code&gt;&lt;/pre&gt;
&lt;h3 id=&#34;二-mat-内存分析工具&#34;&gt;二. MAT 内存分析工具&lt;/h3&gt;
&lt;h4 id=&#34;1-查看内存分配&#34;&gt;1. 查看内存分配&lt;/h4&gt;
&lt;p&gt;MAT 是一个强大的 Java 内存分析工具，可以在 &lt;a href=&#34;https://www.eclipse.org/mat/&#34;&gt;eclipse 官网&lt;/a&gt;下载。打开 &lt;code&gt;Heap Dump&lt;/code&gt; &lt;code&gt;a.hprof&lt;/code&gt; 文件，可以看到内存泄漏的概要信息。
&lt;img src=&#34;https://busy.im/img/android-memory-profile/3.png&#34; alt=&#34;mat overview&#34; /&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;Overview&lt;/code&gt; 界面点击 &lt;code&gt;Leak Suspects&lt;/code&gt;，可以查看内存泄漏的详细信息。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/4.png&#34; alt=&#34;Leak suspects&#34; /&gt;&lt;/p&gt;
&lt;p&gt;在 &lt;code&gt;Overview&lt;/code&gt; 界面点击 &lt;code&gt;Histogram&lt;/code&gt;，可以查看内存分配的详细信息，可以看到对象分配的个数，占用的内存大小等。可以在这个界面查看分析为什么创建了大量的对象，如果不是必要的，可以考虑减少对象的分配来优化内存占用。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/5.png&#34; alt=&#34;Histogram&#34; /&gt;&lt;/p&gt;
&lt;p&gt;可以看到内存中分配了超过 7 万个 String 对象，可以考虑优化，减少 String 对象的创建。&lt;/p&gt;
&lt;p&gt;点击 &lt;code&gt;Dominator tree&lt;/code&gt; 按钮，可以以堆栈的形式查看内存的占用信息&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/6.png&#34; alt=&#34;dominator tree&#34; /&gt;&lt;/p&gt;
&lt;p&gt;从上图中可以看到 Bitmap 对象占用了大量的内存空间，可以考虑在低内存设备上优化。&lt;/p&gt;
&lt;h4 id=&#34;2-定位及导出-bitmap-图片数据&#34;&gt;2. 定位及导出 Bitmap 图片数据&lt;/h4&gt;
&lt;p&gt;对于没有堆栈的对象，可以使用 &lt;code&gt;Path TO GC Roots&lt;/code&gt; 菜单，快速定位引用的位置，&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/7.png&#34; alt=&#34;Bitmap to file&#34; /&gt;&lt;/p&gt;
&lt;p&gt;通过引用的调用关系，就可以快速定位到代码位置了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/8.png&#34; alt=&#34;gc roots&#34; /&gt;&lt;/p&gt;
&lt;p&gt;也可以通过导出内存中的 Bitmap 图片二进制数据，然后查看图片文件来直观的定位图片。&lt;/p&gt;
&lt;p&gt;首先点击 &lt;code&gt;Bitmap&lt;/code&gt; 对象，可以在 &lt;code&gt;Inspector&lt;/code&gt; 中查看属性，可以看到宽度和长度信息 。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/9.png&#34; alt=&#34;bitmap attribute&#34; /&gt;&lt;/p&gt;
&lt;p&gt;然后再在 &lt;code&gt;byte&lt;/code&gt; 对象上点击右键，&lt;code&gt;Copy - Save Value To File&lt;/code&gt; 保存图片数据到文件 &lt;code&gt;a.data&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/10.png&#34; alt=&#34;save bitmap data&#34; /&gt;&lt;/p&gt;
&lt;p&gt;之后通过开源图片处理工具 GIMP 打开刚才导出的 &lt;code&gt;a.data&lt;/code&gt; 文件，图像类型选择 RGB Alpha，填入上一步 Bitmap 属性中看到的长度和高度，然后点击打开即可打开图片。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/11.png&#34; alt=&#34;Bitmap data&#34; /&gt;&lt;/p&gt;
&lt;h4 id=&#34;3-定位及导出字符串&#34;&gt;3. 定位及导出字符串&lt;/h4&gt;
&lt;p&gt;从 &lt;code&gt;Histogram&lt;/code&gt; 中看到内存中分配了大量的 String 对象，首先使用菜单中的 &lt;code&gt;List objects - withoutgoing references&lt;/code&gt; 查看所有的字符串对象。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/12.png&#34; alt=&#34;string list&#34; /&gt;&lt;/p&gt;
&lt;p&gt;之后使用 &lt;code&gt;Path to GC Roots&lt;/code&gt; 菜单即可查看引用关系和调用栈：&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/13.png&#34; alt=&#34;string reference&#34; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/14.png&#34; alt=&#34;string reference&#34; /&gt;&lt;/p&gt;
&lt;p&gt;也可以将这些 String 内容导出到 txt 文件，然后查看内存中具体是哪些字符串。首先在字符串列表选项卡使用快捷键 &lt;code&gt;CTRL + A&lt;/code&gt; 全选，再点击右键使用 &lt;code&gt;Copy - Save Value to File&lt;/code&gt; 菜单即可将文本内容导出到 txt 文件，之后使用常用的文本文档工具查看即可。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/15.png&#34; alt=&#34;export strings&#34; /&gt;&lt;/p&gt;
&lt;h4 id=&#34;4-内存数据安全的思考&#34;&gt;4. 内存数据安全的思考&lt;/h4&gt;
&lt;p&gt;从上述导出 Bitmap 图片和 String 字符串的方法可以看出，如果拿到应用进程内存的拷贝，可以很轻易的拿到敏感的应用数据。例如，如果用户的密码或者应用的 API key 等被储存并以 String 等对象在内存中引用，则可以轻易被获取到。&lt;/p&gt;
&lt;p&gt;针对这个安全问题，应该避免用户密码的本地保存，可以使用 &lt;code&gt;Json web token&lt;/code&gt; 等认证机制避免和服务器交互时用户密码的使用，同时服务端的 API 可以通过应用签名等额外信息作为和客户端通信的辅助验证信息。&lt;/p&gt;
&lt;h3 id=&#34;三-抓取内存的方法&#34;&gt;三. 抓取内存的方法&lt;/h3&gt;
&lt;p&gt;除了通过 Android Studio Profiler 外，还可以通过以下几个方法抓取应用&lt;code&gt;memory dump&lt;/code&gt;。&lt;/p&gt;
&lt;h4 id=&#34;1-通过-adb-命令&#34;&gt;1. 通过 adb 命令&lt;/h4&gt;
&lt;p&gt;可以使用下面命令导出 &lt;code&gt;hprof&lt;/code&gt; 文件，需要应用 apk 是 &lt;code&gt;debug&lt;/code&gt; 版，如果 apk 是 &lt;code&gt;release&lt;/code&gt; 版，则需要手机系统版本是 &lt;code&gt;userdebug&lt;/code&gt; 或者 &lt;code&gt;eng&lt;/code&gt; 版。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;adb shell am dumpheap com.tencent.mm /data/local/tmp/a.hprof
adb pull /data/local/tmp/a.hprof
hprof-conv a.hprof b.hprof&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/android-memory-profile/16.png&#34; alt=&#34;weixin mem&#34; /&gt;&lt;/p&gt;
&lt;p&gt;从上面命令可以看出，只要使用模拟器或者手机是 &lt;code&gt;eng&lt;/code&gt; 或 &lt;code&gt;userdebug&lt;/code&gt; 版就可以轻易抓取 release 版本的应用内存，例如微信、支付宝等应用，可以看到内存安全问题应该得到重视。&lt;/p&gt;
&lt;h4 id=&#34;2-应用内主动抓取&#34;&gt;2. 应用内主动抓取&lt;/h4&gt;
&lt;p&gt;可以在应用内使用 &lt;a href=&#34;https://developer.android.com/reference/android/os/Debug.html#dumpHprofData(java.lang.String)&#34;&gt;android.os.Debug.dumpHprofData(String fileName)&lt;/a&gt; 接口主动抓取应用的内存堆栈。只需要在程序运行时调用 &lt;code&gt;dumpHprofData(String fileName)&lt;/code&gt; 静态接口即可将应用内存导出到文件。如下是一段在应用异常退出时抓取内存的示例代码仅供参考：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; CaptureHeapDumpsApplication &lt;span style=&#34;color:#c0f&#34;&gt;extends&lt;/span&gt; Application {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; FILE_NAME &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/data/local/tmp/heap-dump.hprof&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onCreate() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;super&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;();
Thread.&lt;span style=&#34;color:#309&#34;&gt;currentThread&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;setUncaughtExceptionHandler&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Thread.&lt;span style=&#34;color:#309&#34;&gt;UncaughtExceptionHandler&lt;/span&gt;() {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; uncaughtException(Thread &lt;span style=&#34;color:#c0f&#34;&gt;t&lt;/span&gt;, Throwable &lt;span style=&#34;color:#c0f&#34;&gt;e&lt;/span&gt;) {
String &lt;span style=&#34;color:#c0f&#34;&gt;absolutePath&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; File(FILE_NAME).&lt;span style=&#34;color:#309&#34;&gt;getAbsolutePath&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;try&lt;/span&gt; {
Debug.&lt;span style=&#34;color:#309&#34;&gt;dumpHprofData&lt;/span&gt;(absolutePath);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (IOException &lt;span style=&#34;color:#c0f&#34;&gt;e1&lt;/span&gt;) {
e1.&lt;span style=&#34;color:#309&#34;&gt;printStackTrace&lt;/span&gt;();
}
}
});
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;参考&#34;&gt;参考&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://developer.android.com/studio/profile/memory-profiler?hl=zh-cn&#34;&gt;使用 Memory Profiler 查看 Java 堆和内存分配&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.eclipse.org/mat/&#34;&gt;Memory Analyzer (MAT)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://dzone.com/articles/how-to-capture-heap-dump-from-android-app&#34;&gt;How to Capture Heap Dump From an Android App&lt;/a&gt;&lt;/p&gt;</description></item><item><title>代码仓库服务之 GitLab</title><link>https://busy.im/post/git-repository-manager-gitlab/</link><pubDate>Wed, 16 Jan 2019 22:11:12 +0800</pubDate><guid>https://busy.im/post/git-repository-manager-gitlab/</guid><description>
&lt;p&gt;GitLab 是一个优秀的协作代码仓库服务，拥有几乎所有 GitHub 的功能，可以当作后者的开源实现。不同于纯 git 仓库服务， GitLab 主打社区和自动化 Pipeline 功能，拥有众多优秀扩展如 CI/CD，Pages，Issue board 等。&lt;/p&gt;
&lt;h2 id=&#34;关于-gitlab&#34;&gt;关于 GitLab&lt;/h2&gt;
&lt;p&gt;GitLab是由GitLab Inc.开发，使用MIT许可证的基于网络的Git仓库管理工具，且具有wiki和issue跟踪功能。&lt;/p&gt;
&lt;p&gt;GitLab 由乌克兰程序员 Dmitriy Zaporozhets 和 Valery Sizov 开发，它由 Ruby 写成。后来，一些部分用 Go 语言重写。截止 2018 年 5 月，该公司约有 290 名团队成员，以及 2000 多名开源贡献者。 GitLab 被 IBM，Sony，Jülich Research Center，NASA，Alibaba，Invincea，O’Reilly Media，Leibniz-Rechenzentrum (LRZ)，CERN，SpaceX 等组织使用。&lt;/p&gt;
&lt;h3 id=&#34;ce-与-ee-版本&#34;&gt;CE 与 EE 版本&lt;/h3&gt;
&lt;p&gt;自建的 GitLab 服务分为开源社区版本 (CE) 和企业版本 (EE)，一般来说 CE 版本已经足够团队的开发使用了，企业版多了代码质量、GitLab geo 、Epics 等功能。&lt;/p&gt;
&lt;p&gt;另外如果团队或个人有维护开源项目，也可以申请 GitLab 的开源许可，可以免费获得完整的企业版功能。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2019-01-16201734.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h3 id=&#34;与-gerrit-对比&#34;&gt;与 Gerrit 对比&lt;/h3&gt;
&lt;p&gt;Gerrit 是 Google Android 团队开发的代码仓库服务，主打的特性是代码 Review，不同于 GitHub 和 GitLab 这一类社区化仓库服务主要是以 Pull request 方式迭代代码，相对于 Gerrit，GitLab 服务功能更强大高效更现代化一些。&lt;/p&gt;
&lt;h2 id=&#34;基本使用&#34;&gt;基本使用&lt;/h2&gt;
&lt;h3 id=&#34;添加-ssh-key&#34;&gt;添加 ssh key&lt;/h3&gt;
&lt;p&gt;注册成功后就可以按照提示在 &lt;code&gt;/profile/keys&lt;/code&gt; 添加 ssh key 了，添加完成后就可以 push 提交代码了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2019-01-16203513.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h3 id=&#34;新建工程&#34;&gt;新建工程&lt;/h3&gt;
&lt;p&gt;点击主页的 &lt;code&gt;New Project&lt;/code&gt; 就可以新建工程了，注意这里可以将工程设置三个可见状态，private 私有仓库任何其他人都不可见，也无权限访问和克隆；Internal 内部仓库则是任何注册的用户可见可访问；Public 公开仓库表示未注册的用户也可以访问。&lt;/p&gt;
&lt;p&gt;另外也可以通过模板创建工程，也可以通过 Import project 导入外部的代码仓库。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2019-01-16203941.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h3 id=&#34;新建组&#34;&gt;新建组&lt;/h3&gt;
&lt;p&gt;点击右上角的加号，选择 &lt;code&gt;New Group&lt;/code&gt; 即可创建新的群组，界面和创建新工程类似，也包括三个可见状态，和仓库类似。可以按照实际业务模块新建群组来将多个开发者和项目联系起来，方便管理和协作。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2019-01-16204436.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h3 id=&#34;代码权限&#34;&gt;代码权限&lt;/h3&gt;
&lt;p&gt;可以点击工程设置中的 Permission 选项卡调整工程的权限，这里可以设置代码、Issue、Wiki 等权限。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2019-01-16205106.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h3 id=&#34;issue-boards&#34;&gt;Issue boards&lt;/h3&gt;
&lt;p&gt;Issue boards 类似于 trello，可以高效的可视化管理项目的问题和进度。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2019-01-16205528.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h3 id=&#34;wiki&#34;&gt;WIKI&lt;/h3&gt;
&lt;p&gt;每个工程都会有一个 wiki 库，这里可以托管公共文档， 项目说明文档，操作手册，知识共享等内容。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2019-01-16205828.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h3 id=&#34;集成-ci&#34;&gt;集成 CI&lt;/h3&gt;
&lt;p&gt;GitLab 的一大优势就是集成的 CI，通过 GitLab Runner，可以自动化构建发布任何软件工程。同时可以确保在每次提交时检查代码质量、编译、测试、部署等。通过 Pipeline， 可以方便的将每个阶段独立分离。&lt;/p&gt;
&lt;p&gt;如下代码是一个简短的 Android 工程编译 CI 代码：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;image:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;xdtianyu/docker:openjdk8&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;before_script:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;source&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;/opt/setup-android-sdk.sh&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;stages:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;build&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;build:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;stage:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;build&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;script:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;./gradlew&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;build&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;mkdir&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-p&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;dist&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;find&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;app/build/outputs/&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-name&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;\&lt;span style=&#34;color:#099&#34;&gt;*.apk&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-exec&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;cp&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;{}&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;dist&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;\;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;artifacts:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;paths:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;dist/*&amp;#34;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2019-01-16210347.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2019-01-16210325.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h3 id=&#34;生成-pages&#34;&gt;生成 pages&lt;/h3&gt;
&lt;p&gt;GitLab pages 是另一个强大的功能，可以方便的将文档或者 API 手册发布到 pages 网页服务上。&lt;/p&gt;
&lt;p&gt;例如下面的 CI 代码可以自动运行 hexo 静态博客生成工具，将项目中的 &lt;code&gt;md&lt;/code&gt; 文档生成 html 文件发布到 pages 服务，之后就可以通过浏览器打开网址直接访问了。类似的也可以将自己编写的 html， &lt;code&gt;java api doc&lt;/code&gt;，单元测试报告，覆盖率报告等文件直接发布到 pages。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-yaml&#34; data-lang=&#34;yaml&#34;&gt;image:&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;node:&lt;span style=&#34;color:#f60&#34;&gt;8.11.2&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;before_script:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;git&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;submodule&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;sync&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;--recursive&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;git&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;submodule&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;update&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;--init&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;--recursive&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;&lt;/span&gt;pages:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;cache:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;paths:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;node_modules/&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;script:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;npm&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;install&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;hexo-cli&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-g&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;npm&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;install&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;hexo&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;deploy&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;artifacts:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;paths:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;public&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;only:&lt;span style=&#34;color:#bbb&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;-&lt;span style=&#34;color:#bbb&#34;&gt; &lt;/span&gt;master&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2019-01-16211153.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h2 id=&#34;轻量代码仓库&#34;&gt;轻量代码仓库&lt;/h2&gt;
&lt;p&gt;相对于功能强大的 GitLab，也有一些轻量级的代码仓库服务如 &lt;a href=&#34;https://gitea.io/&#34;&gt;Gitea&lt;/a&gt; , &lt;a href=&#34;https://gogs.io/&#34;&gt;Gogos&lt;/a&gt; 等。可以访问 &lt;a href=&#34;https://github.com/Kickball/awesome-selfhosted#project-management&#34;&gt;awesome-selfhosted#project-management&lt;/a&gt; 查看。&lt;/p&gt;
&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://zh.wikipedia.org/zh-hans/Gitlab&#34;&gt;https://zh.wikipedia.org/zh-hans/Gitlab&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://about.gitlab.com/pricing/#self-managed&#34;&gt;GitLab Pricing&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Android 外置 SD 卡写入权限问题</title><link>https://busy.im/post/android-sdcard-write/</link><pubDate>Tue, 25 Dec 2018 22:20:06 +0800</pubDate><guid>https://busy.im/post/android-sdcard-write/</guid><description>
&lt;p&gt;最近升级到 Android 9.0 后，发现文件管理器在写入外置 SD 卡时出现了写入失败的问题，定位到 &lt;code&gt;File.canWrite()&lt;/code&gt; 方法，发现返回了 &lt;code&gt;false&lt;/code&gt;。经过讨论追踪定位，发现是由于 Google 的一个&lt;a href=&#34;https://android.googlesource.com/platform/frameworks/base/+/86684240eb5753bb97c2cfc93d1d25fa1870f8f1%5E%21/#F1&#34;&gt;更改&lt;/a&gt;导致的:&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;color:#030;font-weight:bold&#34;&gt;diff --git a/data/etc/platform.xml b/data/etc/platform.xml
&lt;/span&gt;&lt;span style=&#34;color:#030;font-weight:bold&#34;&gt;index 04006b1..3021555 100644
&lt;/span&gt;&lt;span style=&#34;color:#030;font-weight:bold&#34;&gt;&lt;/span&gt;&lt;span style=&#34;background-color:#fcc&#34;&gt;--- a/data/etc/platform.xml
&lt;/span&gt;&lt;span style=&#34;background-color:#fcc&#34;&gt;&lt;/span&gt;&lt;span style=&#34;background-color:#cfc&#34;&gt;+++ b/data/etc/platform.xml
&lt;/span&gt;&lt;span style=&#34;background-color:#cfc&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#030;font-weight:bold&#34;&gt;@@ -62,7 +62,6 @@
&lt;/span&gt;&lt;span style=&#34;color:#030;font-weight:bold&#34;&gt;&lt;/span&gt;
&amp;lt;permission name=&amp;#34;android.permission.WRITE_MEDIA_STORAGE&amp;#34; &amp;gt;
&amp;lt;group gid=&amp;#34;media_rw&amp;#34; /&amp;gt;
&lt;span style=&#34;background-color:#fcc&#34;&gt;- &amp;lt;group gid=&amp;#34;sdcard_rw&amp;#34; /&amp;gt;
&lt;/span&gt;&lt;span style=&#34;background-color:#fcc&#34;&gt;&lt;/span&gt; &amp;lt;/permission&amp;gt;
&amp;lt;permission name=&amp;#34;android.permission.ACCESS_MTP&amp;#34; &amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-diff&#34; data-lang=&#34;diff&#34;&gt;&lt;span style=&#34;color:#030;font-weight:bold&#34;&gt;diff --git a/services/core/java/com/android/server/pm/PackageManagerService.java b/services/core/java/com/android/server/pm/PackageManagerService.java
&lt;/span&gt;&lt;span style=&#34;color:#030;font-weight:bold&#34;&gt;index a0cb722..940d19f 100644
&lt;/span&gt;&lt;span style=&#34;color:#030;font-weight:bold&#34;&gt;&lt;/span&gt;&lt;span style=&#34;background-color:#fcc&#34;&gt;--- a/services/core/java/com/android/server/pm/PackageManagerService.java
&lt;/span&gt;&lt;span style=&#34;background-color:#fcc&#34;&gt;&lt;/span&gt;&lt;span style=&#34;background-color:#cfc&#34;&gt;+++ b/services/core/java/com/android/server/pm/PackageManagerService.java
&lt;/span&gt;&lt;span style=&#34;background-color:#cfc&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#030;font-weight:bold&#34;&gt;@@ -20936,9 +20936,6 @@
&lt;/span&gt;&lt;span style=&#34;color:#030;font-weight:bold&#34;&gt;&lt;/span&gt; if (Process.isIsolated(uid)) {
return Zygote.MOUNT_EXTERNAL_NONE;
}
&lt;span style=&#34;background-color:#fcc&#34;&gt;- if (checkUidPermission(WRITE_MEDIA_STORAGE, uid) == PERMISSION_GRANTED) {
&lt;/span&gt;&lt;span style=&#34;background-color:#fcc&#34;&gt;- return Zygote.MOUNT_EXTERNAL_DEFAULT;
&lt;/span&gt;&lt;span style=&#34;background-color:#fcc&#34;&gt;- }
&lt;/span&gt;&lt;span style=&#34;background-color:#fcc&#34;&gt;&lt;/span&gt; if (checkUidPermission(READ_EXTERNAL_STORAGE, uid) == PERMISSION_DENIED) {
return Zygote.MOUNT_EXTERNAL_DEFAULT;
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这里的修改移除了 &lt;code&gt;WRITE_MEDIA_STORAGE&lt;/code&gt; 权限相关权限，导致了外部 SD 卡存储不可写的问题。&lt;/p&gt;
&lt;h2 id=&#34;平台签名应用受影响&#34;&gt;平台签名应用受影响&lt;/h2&gt;
&lt;p&gt;这个修改对系统应用影响较大，在 9.0 之前的平台，申请了 &lt;code&gt;WRITE_MEDIA_STORAGE&lt;/code&gt; 的权限后，平台签名的应用就可以通过 &lt;code&gt;java.io.File&lt;/code&gt; 接口写入外置 SD 卡了。但是这个修改之后，想要写入外置 SD 卡，就需要像第三方应用一样，使用 &lt;code&gt;DocumentFile&lt;/code&gt; 的接口，可以阅读 API 文档 &lt;a href=&#34;https://developer.android.com/guide/topics/providers/document-provider?hl=zh-cn&#34;&gt;存储访问框架&lt;/a&gt; 和 &lt;a href=&#34;https://developer.android.com/training/articles/scoped-directory-access?hl=zh-cn&#34;&gt;使用作用域目录访问&lt;/a&gt; 。&lt;/p&gt;
&lt;p&gt;参考 google 的这个 &lt;a href=&#34;https://issuetracker.google.com/issues/113498210&#34;&gt;bug&lt;/a&gt; ，平台类的应用，如文件管理器、相机、图库甚至 MediaProvider 都会出现外置 SD 卡只能读不可写，即写入失败的问题，因为这些系统应用都没有适配 &lt;code&gt;DocumentProvider&lt;/code&gt; 的写入方式。&lt;/p&gt;
&lt;h2 id=&#34;documentfile-适配方案&#34;&gt;DocumentFile 适配方案&lt;/h2&gt;
&lt;h3 id=&#34;1-请求写入外置-sd-卡权限&#34;&gt;1. 请求写入外置 SD 卡权限&lt;/h3&gt;
&lt;p&gt;早在 Android 4.4，Android 就已经加入了存储访问框架，外置 SD 卡的访问由 DocumentsUI (com.android.documentsui) 提供支持，经过 5.0 版本的完善以及 7.0 的改进，目前有两种请求外置 SD 卡写入权限的交互方法：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Android 7.0 之前，使用 ACTION_OPEN_DOCUMENT_TREE 跳转到 DocumentsUI 的存储选择界面，之后用户手动打开外置存储并选择&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/sdoperatestep.png&#34; alt=&#34;手动选择 sd 卡提示&#34; /&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Android 7.0 及之后，使用 &lt;a href=&#34;https://developer.android.com/reference/android/os/storage/StorageVolume#createAccessIntent(java.lang.String)&#34;&gt;StorageVolume.createAccessIntent(null)&lt;/a&gt; 跳转到权限写入提示框。(这个提示框也是 DocumentsUI 提供的，只是对之前的交互做了改进，避免繁琐的用户操作)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/screenshot2018-12-25-20-45-12.png&#34; alt=&#34;新的权限提示框&#34; /&gt;&lt;/p&gt;
&lt;p&gt;检查权限界面的属性，会发现这个权限提示框其实是 &lt;code&gt;com.android.documentsui/com.android.documentsui.ScopedAccessActivity&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;也就是说 DocumentsUI 为了简化权限请求的流程，已经特意做了一个权限的提示框。&lt;/p&gt;
&lt;p&gt;而 &lt;a href=&#34;https://developer.android.com/reference/android/os/storage/StorageVolume#createAccessIntent(java.lang.String)&#34;&gt;StorageVolume.createAccessIntent(String directoryName)&lt;/a&gt; 可以传入众多媒体类型，包括音乐、图片、电影、文档等，如果传入参数为 &lt;code&gt;null&lt;/code&gt; ，则表示整个外置存储分区。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Parameters&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;directoryName&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;String&lt;/code&gt;: must be one of &lt;code&gt;Environment.DIRECTORY_MUSIC&lt;/code&gt;, &lt;code&gt;Environment.DIRECTORY_PODCASTS&lt;/code&gt;, &lt;code&gt;Environment.DIRECTORY_RINGTONES&lt;/code&gt;, &lt;code&gt;Environment.DIRECTORY_ALARMS&lt;/code&gt;, &lt;code&gt;Environment.DIRECTORY_NOTIFICATIONS&lt;/code&gt;, &lt;code&gt;Environment.DIRECTORY_PICTURES&lt;/code&gt;, &lt;code&gt;Environment.DIRECTORY_MOVIES&lt;/code&gt;, &lt;code&gt;Environment.DIRECTORY_DOWNLOADS&lt;/code&gt;, &lt;code&gt;Environment.DIRECTORY_DCIM&lt;/code&gt;, or &lt;code&gt;Environment.DIRECTORY_DOCUMENTS&lt;/code&gt;, or &lt;code&gt;null&lt;/code&gt; to request access to the entire volume.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Returns&lt;/th&gt;
&lt;th&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;Intent&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;intent to request access, or &lt;code&gt;null&lt;/code&gt; if the requested directory is invalid for that volume.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;strong&gt;权限请求及处理&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;权限请求需要在 Activity 或者 Fragment 中发起，同时在 &lt;code&gt;onActivityResult&lt;/code&gt; 中捕获返回的 &lt;code&gt;Uri&lt;/code&gt;，这个 &lt;code&gt;Uri&lt;/code&gt; 可以保存在本地存储中，方便再次调用。请求的代码封装如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onCreate(Bundle &lt;span style=&#34;color:#c0f&#34;&gt;savedInstanceState&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;super&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(savedInstanceState);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// ...
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;checkWritableRootPath&lt;/span&gt;(getActivity(), rootPath)) {
showOpenDocumentTree();
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// ...
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; showOpenDocumentTree() {
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (android.&lt;span style=&#34;color:#309&#34;&gt;os&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Build&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;VERSION&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;SDK_INT&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;=&lt;/span&gt; android.&lt;span style=&#34;color:#309&#34;&gt;os&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Build&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;VERSION_CODES&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;N&lt;/span&gt;) {
StorageManager &lt;span style=&#34;color:#c0f&#34;&gt;sm&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; getActivity().&lt;span style=&#34;color:#309&#34;&gt;getSystemService&lt;/span&gt;(StorageManager.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
StorageVolume &lt;span style=&#34;color:#c0f&#34;&gt;volume&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; sm.&lt;span style=&#34;color:#309&#34;&gt;getStorageVolume&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; File(rootPath));
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (volume &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
intent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; volume.&lt;span style=&#34;color:#309&#34;&gt;createAccessIntent&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (intent &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
intent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(Intent.&lt;span style=&#34;color:#309&#34;&gt;ACTION_OPEN_DOCUMENT_TREE&lt;/span&gt;);
}
startActivityForResult(intent, DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;OPEN_DOCUMENT_TREE_CODE&lt;/span&gt;);
}
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onActivityResult(&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;requestCode&lt;/span&gt;, &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;resultCode&lt;/span&gt;, Intent &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;super&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;onActivityResult&lt;/span&gt;(requestCode, resultCode, data);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;switch&lt;/span&gt; (requestCode) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;OPEN_DOCUMENT_TREE_CODE&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (data &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; data.&lt;span style=&#34;color:#309&#34;&gt;getData&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
Uri &lt;span style=&#34;color:#c0f&#34;&gt;uri&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; data.&lt;span style=&#34;color:#309&#34;&gt;getData&lt;/span&gt;();
DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;saveTreeUri&lt;/span&gt;(getActivity(), rootPath, uri);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;default&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这里的 &lt;code&gt;rootPath&lt;/code&gt; 是上下文中传入的外置 sd 卡根目录，如 &lt;code&gt;/storage/0000-0000&lt;/code&gt; 这样的路径，可以通过 &lt;code&gt;context.getExternalFilesDirs(&amp;quot;external&amp;quot;)&lt;/code&gt; 方法获取到。&lt;code&gt;DocumentsUtils&lt;/code&gt; 工具类的实现方法见下文。&lt;/p&gt;
&lt;p&gt;其中 &lt;code&gt;DocumentsUtils.checkWritableRootPath()&lt;/code&gt; 方法用来检查 SD 卡根目录是否有写入权限，如果没有则跳转到权限请求；&lt;code&gt;DocumentsUtils.saveTreeUri()&lt;/code&gt; 方法保存返回的 &lt;code&gt;Uri&lt;/code&gt; 信息到本地存储，以便之后查询。&lt;/p&gt;
&lt;h3 id=&#34;2-documentfile-文件操作封装&#34;&gt;2. DocumentFile 文件操作封装&lt;/h3&gt;
&lt;p&gt;由于之前应用使用了 &lt;code&gt;java.io.File&lt;/code&gt; 接口操作外置 SD 卡文件，期望对代码的修改量最小，则最好的方式是对已有的 &lt;code&gt;File&lt;/code&gt; 操作再做一次封装。&lt;/p&gt;
&lt;p&gt;由于 Android 9.0 之前系统应用默认是可以通过 &lt;code&gt;java.io.File&lt;/code&gt; 接口写入外置 SD卡 的，而如果作为公开市场第三方应用却在 4.4 之后就不可写，而且有的厂商定制版本 Android 9.0 外置 SD 卡也是可以直接写入而不需要 DocumentFile 接口，DocumentFile 接口也没有 &lt;code&gt;java.io.File&lt;/code&gt; 效率高。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;所以最好的办法是先检查是否有文件写入权限，如果有写入权限，则直接使用 File 接口操作，如果没有权限再检查文件是否在外置 SD 卡，如果文件在 SD 卡则使用 DocumentFile 接口操作。&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;封装的工具类 &lt;code&gt;DocumentsUtils&lt;/code&gt; 方法说明，&lt;strong&gt;不兼容&lt;/strong&gt; 表示没有封装 DocumentFile 操作：&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;DocumentsUtils 公共方法&lt;/th&gt;
&lt;th&gt;功能描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;void cleanCache()&lt;/td&gt;
&lt;td&gt;清除路径缓存，建议插拔 sd 卡后调用&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean isOnExtSdCard(File file, Context c)&lt;/td&gt;
&lt;td&gt;文件路径是否在外置 SD 卡上&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;DocumentFile getDocumentFile(final File file, final boolean isDirectory, Context context)&lt;/td&gt;
&lt;td&gt;从 File 转到 DocumentFile&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean mkdirs(Context context, File dir)&lt;/td&gt;
&lt;td&gt;创建文件夹&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean delete(Context context, File file)&lt;/td&gt;
&lt;td&gt;删除文件&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean canWrite(File file)&lt;/td&gt;
&lt;td&gt;File 文件是否可写（如果文件不存在，则尝试创建文件再删除检查写入权限）&lt;strong&gt;不兼容&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean canWrite(Context context, File file)&lt;/td&gt;
&lt;td&gt;文件是否可写&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean renameTo(Context context, File src, File dest)&lt;/td&gt;
&lt;td&gt;文件重命名&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean saveTreeUri(Context context, String rootPath, Uri uri)&lt;/td&gt;
&lt;td&gt;保存 path 和 uri 到本地存储&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;boolean checkWritableRootPath(Context context, String rootPath)&lt;/td&gt;
&lt;td&gt;检查路径是否可写，不可写返回 true&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;InputStream getInputStream(Context context, File destFile)&lt;/td&gt;
&lt;td&gt;获取 InputStream，可用于读操作&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;OutputStream getOutputStream(Context context, File destFile)&lt;/td&gt;
&lt;td&gt;获取 OutputStream，可用于写操作&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;封装的工具类 &lt;code&gt;DocumentsUtils.java&lt;/code&gt; 内容如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; DocumentsUtils {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; TAG &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getSimpleName&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; OPEN_DOCUMENT_TREE_CODE &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 8000;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;String&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;sExtSdCardPaths&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;DocumentsUtils&lt;/span&gt;() {
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;cleanCache&lt;/span&gt;() {
sExtSdCardPaths.&lt;span style=&#34;color:#309&#34;&gt;clear&lt;/span&gt;();
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/**
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * Get a list of external SD card paths. (Kitkat or higher.)
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; *
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * @return A list of external SD card paths.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; */&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;@TargetApi&lt;/span&gt;(Build.&lt;span style=&#34;color:#309&#34;&gt;VERSION_CODES&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;KITKAT&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; String[] &lt;span style=&#34;color:#c0f&#34;&gt;getExtSdCardPaths&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (sExtSdCardPaths.&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; 0) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; sExtSdCardPaths.&lt;span style=&#34;color:#309&#34;&gt;toArray&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; String[0]);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (File &lt;span style=&#34;color:#c0f&#34;&gt;file&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; context.&lt;span style=&#34;color:#309&#34;&gt;getExternalFilesDirs&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;external&amp;#34;&lt;/span&gt;)) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (file &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;file.&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(context.&lt;span style=&#34;color:#309&#34;&gt;getExternalFilesDir&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;external&amp;#34;&lt;/span&gt;))) {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;index&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;getAbsolutePath&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;lastIndexOf&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/Android/data&amp;#34;&lt;/span&gt;);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (index &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt; 0) {
Log.&lt;span style=&#34;color:#309&#34;&gt;w&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Unexpected external file dir: &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;getAbsolutePath&lt;/span&gt;());
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
String &lt;span style=&#34;color:#c0f&#34;&gt;path&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;getAbsolutePath&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;substring&lt;/span&gt;(0, index);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;try&lt;/span&gt; {
path &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; File(path).&lt;span style=&#34;color:#309&#34;&gt;getCanonicalPath&lt;/span&gt;();
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (IOException &lt;span style=&#34;color:#c0f&#34;&gt;e&lt;/span&gt;) {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Keep non-canonical path.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; }
sExtSdCardPaths.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(path);
}
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (sExtSdCardPaths.&lt;span style=&#34;color:#309&#34;&gt;isEmpty&lt;/span&gt;()) sExtSdCardPaths.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/storage/sdcard1&amp;#34;&lt;/span&gt;);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; sExtSdCardPaths.&lt;span style=&#34;color:#309&#34;&gt;toArray&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; String[0]);
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/**
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * Determine the main folder of the external SD card containing the given file.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; *
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * @param file the file.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * @return The main folder of the external SD card containing this file, if the file is on an SD
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * card. Otherwise,
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * null is returned.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; */&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;@TargetApi&lt;/span&gt;(Build.&lt;span style=&#34;color:#309&#34;&gt;VERSION_CODES&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;KITKAT&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; String &lt;span style=&#34;color:#c0f&#34;&gt;getExtSdCardFolder&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;File&lt;/span&gt; file, Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;) {
String[] &lt;span style=&#34;color:#c0f&#34;&gt;extSdPaths&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; getExtSdCardPaths(context);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;try&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0; i &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt; extSdPaths.&lt;span style=&#34;color:#309&#34;&gt;length&lt;/span&gt;; i&lt;span style=&#34;color:#555&#34;&gt;++&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (file.&lt;span style=&#34;color:#309&#34;&gt;getCanonicalPath&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;startsWith&lt;/span&gt;(extSdPaths[i])) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; extSdPaths[i];
}
}
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (IOException &lt;span style=&#34;color:#c0f&#34;&gt;e&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/**
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * Determine if a file is on external sd card. (Kitkat or higher.)
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; *
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * @param file The file.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * @return true if on external sd card.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; */&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;@TargetApi&lt;/span&gt;(Build.&lt;span style=&#34;color:#309&#34;&gt;VERSION_CODES&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;KITKAT&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;isOnExtSdCard&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;File&lt;/span&gt; file, Context &lt;span style=&#34;color:#c0f&#34;&gt;c&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; getExtSdCardFolder(file, c) &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/**
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * Get a DocumentFile corresponding to the given file (for writing on ExtSdCard on Android 5).
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * If the file is not
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * existing, it is created.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; *
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * @param file The file.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * @param isDirectory flag indicating if the file should be a directory.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * @return The DocumentFile
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; */&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;getDocumentFile&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;File&lt;/span&gt; file, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;boolean&lt;/span&gt; isDirectory,
Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (Build.&lt;span style=&#34;color:#309&#34;&gt;VERSION&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;SDK_INT&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;=&lt;/span&gt; Build.&lt;span style=&#34;color:#309&#34;&gt;VERSION_CODES&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;KITKAT&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; DocumentFile.&lt;span style=&#34;color:#309&#34;&gt;fromFile&lt;/span&gt;(file);
}
String &lt;span style=&#34;color:#c0f&#34;&gt;baseFolder&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; getExtSdCardFolder(file, context);
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;originalDirectory&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (baseFolder &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
}
String &lt;span style=&#34;color:#c0f&#34;&gt;relativePath&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;try&lt;/span&gt; {
String &lt;span style=&#34;color:#c0f&#34;&gt;fullPath&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;getCanonicalPath&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;baseFolder.&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(fullPath)) {
relativePath &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; fullPath.&lt;span style=&#34;color:#309&#34;&gt;substring&lt;/span&gt;(baseFolder.&lt;span style=&#34;color:#309&#34;&gt;length&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; 1);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
originalDirectory &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;;
}
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (IOException &lt;span style=&#34;color:#c0f&#34;&gt;e&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (Exception &lt;span style=&#34;color:#c0f&#34;&gt;f&lt;/span&gt;) {
originalDirectory &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;//continue
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; }
String &lt;span style=&#34;color:#c0f&#34;&gt;as&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; PreferenceManager.&lt;span style=&#34;color:#309&#34;&gt;getDefaultSharedPreferences&lt;/span&gt;(context).&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(baseFolder,
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
Uri &lt;span style=&#34;color:#c0f&#34;&gt;treeUri&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (as &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) treeUri &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Uri.&lt;span style=&#34;color:#309&#34;&gt;parse&lt;/span&gt;(as);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (treeUri &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// start with root of SD card and then parse through document tree.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;document&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DocumentFile.&lt;span style=&#34;color:#309&#34;&gt;fromTreeUri&lt;/span&gt;(context, treeUri);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (originalDirectory) &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; document;
String[] &lt;span style=&#34;color:#c0f&#34;&gt;parts&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; relativePath.&lt;span style=&#34;color:#309&#34;&gt;split&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/&amp;#34;&lt;/span&gt;);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0; i &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt; parts.&lt;span style=&#34;color:#309&#34;&gt;length&lt;/span&gt;; i&lt;span style=&#34;color:#555&#34;&gt;++&lt;/span&gt;) {
DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;nextDocument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; document.&lt;span style=&#34;color:#309&#34;&gt;findFile&lt;/span&gt;(parts[i]);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (nextDocument &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; ((i &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt; parts.&lt;span style=&#34;color:#309&#34;&gt;length&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt; 1) &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; isDirectory) {
nextDocument &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; document.&lt;span style=&#34;color:#309&#34;&gt;createDirectory&lt;/span&gt;(parts[i]);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
nextDocument &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; document.&lt;span style=&#34;color:#309&#34;&gt;createFile&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;image&amp;#34;&lt;/span&gt;, parts[i]);
}
}
document &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; nextDocument;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; document;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;mkdirs&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, File &lt;span style=&#34;color:#c0f&#34;&gt;dir&lt;/span&gt;) {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;res&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; dir.&lt;span style=&#34;color:#309&#34;&gt;mkdirs&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;res) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;isOnExtSdCard&lt;/span&gt;(dir, context)) {
DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;documentFile&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;getDocumentFile&lt;/span&gt;(dir, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;, context);
res &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; documentFile &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; documentFile.&lt;span style=&#34;color:#309&#34;&gt;canWrite&lt;/span&gt;();
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; res;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;delete&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, File &lt;span style=&#34;color:#c0f&#34;&gt;file&lt;/span&gt;) {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ret&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;delete&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;ret &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;isOnExtSdCard&lt;/span&gt;(file, context)) {
DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;f&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;getDocumentFile&lt;/span&gt;(file, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;, context);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (f &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
ret &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; f.&lt;span style=&#34;color:#309&#34;&gt;delete&lt;/span&gt;();
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; ret;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;canWrite&lt;/span&gt;(File &lt;span style=&#34;color:#c0f&#34;&gt;file&lt;/span&gt;) {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;res&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;exists&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;canWrite&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;res &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;file.&lt;span style=&#34;color:#309&#34;&gt;exists&lt;/span&gt;()) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;try&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;file.&lt;span style=&#34;color:#309&#34;&gt;isDirectory&lt;/span&gt;()) {
res &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;createNewFile&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;delete&lt;/span&gt;();
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
res &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;mkdirs&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;delete&lt;/span&gt;();
}
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (IOException &lt;span style=&#34;color:#c0f&#34;&gt;e&lt;/span&gt;) {
e.&lt;span style=&#34;color:#309&#34;&gt;printStackTrace&lt;/span&gt;();
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; res;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;canWrite&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, File &lt;span style=&#34;color:#c0f&#34;&gt;file&lt;/span&gt;) {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;res&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; canWrite(file);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;res &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;isOnExtSdCard&lt;/span&gt;(file, context)) {
DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;documentFile&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;getDocumentFile&lt;/span&gt;(file, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;, context);
res &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; documentFile &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; documentFile.&lt;span style=&#34;color:#309&#34;&gt;canWrite&lt;/span&gt;();
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; res;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;renameTo&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, File &lt;span style=&#34;color:#c0f&#34;&gt;src&lt;/span&gt;, File &lt;span style=&#34;color:#c0f&#34;&gt;dest&lt;/span&gt;) {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;res&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; src.&lt;span style=&#34;color:#309&#34;&gt;renameTo&lt;/span&gt;(dest);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;res &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; isOnExtSdCard(dest, context)) {
DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;srcDoc&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (isOnExtSdCard(src, context)) {
srcDoc &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; getDocumentFile(src, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;, context);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
srcDoc &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DocumentFile.&lt;span style=&#34;color:#309&#34;&gt;fromFile&lt;/span&gt;(src);
}
DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;destDoc&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; getDocumentFile(dest.&lt;span style=&#34;color:#309&#34;&gt;getParentFile&lt;/span&gt;(), &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;, context);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (srcDoc &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; destDoc &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;try&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (src.&lt;span style=&#34;color:#309&#34;&gt;getParent&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(dest.&lt;span style=&#34;color:#309&#34;&gt;getParent&lt;/span&gt;())) {
res &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; srcDoc.&lt;span style=&#34;color:#309&#34;&gt;renameTo&lt;/span&gt;(dest.&lt;span style=&#34;color:#309&#34;&gt;getName&lt;/span&gt;());
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (Build.&lt;span style=&#34;color:#309&#34;&gt;VERSION&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;SDK_INT&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;=&lt;/span&gt; Build.&lt;span style=&#34;color:#309&#34;&gt;VERSION_CODES&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;N&lt;/span&gt;) {
res &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DocumentsContract.&lt;span style=&#34;color:#309&#34;&gt;moveDocument&lt;/span&gt;(context.&lt;span style=&#34;color:#309&#34;&gt;getContentResolver&lt;/span&gt;(),
srcDoc.&lt;span style=&#34;color:#309&#34;&gt;getUri&lt;/span&gt;(),
srcDoc.&lt;span style=&#34;color:#309&#34;&gt;getParentFile&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;getUri&lt;/span&gt;(),
destDoc.&lt;span style=&#34;color:#309&#34;&gt;getUri&lt;/span&gt;()) &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
}
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (Exception &lt;span style=&#34;color:#c0f&#34;&gt;e&lt;/span&gt;) {
e.&lt;span style=&#34;color:#309&#34;&gt;printStackTrace&lt;/span&gt;();
}
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; res;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; InputStream &lt;span style=&#34;color:#c0f&#34;&gt;getInputStream&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, File &lt;span style=&#34;color:#c0f&#34;&gt;destFile&lt;/span&gt;) {
InputStream &lt;span style=&#34;color:#c0f&#34;&gt;in&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;try&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;canWrite(destFile) &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; isOnExtSdCard(destFile, context)) {
DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;file&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;getDocumentFile&lt;/span&gt;(destFile, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;, context);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (file &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;canWrite&lt;/span&gt;()) {
in &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; context.&lt;span style=&#34;color:#309&#34;&gt;getContentResolver&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;openInputStream&lt;/span&gt;(file.&lt;span style=&#34;color:#309&#34;&gt;getUri&lt;/span&gt;());
}
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
in &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; FileInputStream(destFile);
}
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (FileNotFoundException &lt;span style=&#34;color:#c0f&#34;&gt;e&lt;/span&gt;) {
e.&lt;span style=&#34;color:#309&#34;&gt;printStackTrace&lt;/span&gt;();
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; in;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; OutputStream &lt;span style=&#34;color:#c0f&#34;&gt;getOutputStream&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, File &lt;span style=&#34;color:#c0f&#34;&gt;destFile&lt;/span&gt;) {
OutputStream &lt;span style=&#34;color:#c0f&#34;&gt;out&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;try&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;canWrite(destFile) &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; isOnExtSdCard(destFile, context)) {
DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;file&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;getDocumentFile&lt;/span&gt;(destFile, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;, context);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (file &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;canWrite&lt;/span&gt;()) {
out &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; context.&lt;span style=&#34;color:#309&#34;&gt;getContentResolver&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;openOutputStream&lt;/span&gt;(file.&lt;span style=&#34;color:#309&#34;&gt;getUri&lt;/span&gt;());
}
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
out &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; FileOutputStream(destFile);
}
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (FileNotFoundException &lt;span style=&#34;color:#c0f&#34;&gt;e&lt;/span&gt;) {
e.&lt;span style=&#34;color:#309&#34;&gt;printStackTrace&lt;/span&gt;();
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; out;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;saveTreeUri&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, String &lt;span style=&#34;color:#c0f&#34;&gt;rootPath&lt;/span&gt;, Uri &lt;span style=&#34;color:#c0f&#34;&gt;uri&lt;/span&gt;) {
DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;file&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DocumentFile.&lt;span style=&#34;color:#309&#34;&gt;fromTreeUri&lt;/span&gt;(context, uri);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (file &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;canWrite&lt;/span&gt;()) {
SharedPreferences &lt;span style=&#34;color:#c0f&#34;&gt;perf&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; PreferenceManager.&lt;span style=&#34;color:#309&#34;&gt;getDefaultSharedPreferences&lt;/span&gt;(context);
perf.&lt;span style=&#34;color:#309&#34;&gt;edit&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;putString&lt;/span&gt;(rootPath, uri.&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;()).&lt;span style=&#34;color:#309&#34;&gt;apply&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
Log.&lt;span style=&#34;color:#309&#34;&gt;e&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;no write permission: &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; rootPath);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;checkWritableRootPath&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, String &lt;span style=&#34;color:#c0f&#34;&gt;rootPath&lt;/span&gt;) {
File &lt;span style=&#34;color:#c0f&#34;&gt;root&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; File(rootPath);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;root.&lt;span style=&#34;color:#309&#34;&gt;canWrite&lt;/span&gt;()) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;isOnExtSdCard&lt;/span&gt;(root, context)) {
DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;documentFile&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DocumentsUtils.&lt;span style=&#34;color:#309&#34;&gt;getDocumentFile&lt;/span&gt;(root, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;, context);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; documentFile &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;documentFile.&lt;span style=&#34;color:#309&#34;&gt;canWrite&lt;/span&gt;();
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
SharedPreferences &lt;span style=&#34;color:#c0f&#34;&gt;perf&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; PreferenceManager.&lt;span style=&#34;color:#309&#34;&gt;getDefaultSharedPreferences&lt;/span&gt;(context);
String &lt;span style=&#34;color:#c0f&#34;&gt;documentUri&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; perf.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(rootPath, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;&amp;#34;&lt;/span&gt;);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (documentUri &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; documentUri.&lt;span style=&#34;color:#309&#34;&gt;isEmpty&lt;/span&gt;()) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
DocumentFile &lt;span style=&#34;color:#c0f&#34;&gt;file&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DocumentFile.&lt;span style=&#34;color:#309&#34;&gt;fromTreeUri&lt;/span&gt;(context, Uri.&lt;span style=&#34;color:#309&#34;&gt;parse&lt;/span&gt;(documentUri));
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;(file &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;canWrite&lt;/span&gt;());
}
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://android.googlesource.com/platform/frameworks/base/+/86684240eb5753bb97c2cfc93d1d25fa1870f8f1%5E%21/#F1&#34;&gt;Media process should run with &amp;ldquo;write&amp;rdquo; access.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://issuetracker.google.com/issues/113498210&#34;&gt;[Developer Preview Android P]WRITE_MEDIA_STORAGE is not working for system apps to access the secondary storage.&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/TeamAmaze/AmazeFileManager/blob/master/app/src/main/java/com/amaze/filemanager/filesystem/FileUtil.java&#34;&gt;AmazeFileManager/FileUtil.java&lt;/a&gt;&lt;/p&gt;</description></item><item><title>MotionLayout 实现顶部栏拉伸效果</title><link>https://busy.im/post/motionlayout-topbar/</link><pubDate>Mon, 17 Dec 2018 21:44:56 +0800</pubDate><guid>https://busy.im/post/motionlayout-topbar/</guid><description>
&lt;p&gt;MotionLayout 是 google 新推出的 UI 组件，是 &lt;code&gt;ConstraintLayout 2.0&lt;/code&gt; 库的一个大更新。它继承自 &lt;code&gt;ConstraintLayout&lt;/code&gt; 可以方便的制作复杂动画界面，通过在 xml 中设置起始状态，关键帧，结束状态，快速实现界面动效或动画效果。&lt;/p&gt;
&lt;p&gt;本文以代码示例介绍，如何使用 MotionLayout 实现复杂的凸显型顶部栏(&lt;a href=&#34;https://material.io/design/components/app-bars-top.html#behavior&#34;&gt;Prominent top app bars&lt;/a&gt;) 。&lt;/p&gt;
&lt;h2 id=&#34;实现效果&#34;&gt;实现效果&lt;/h2&gt;
&lt;h3 id=&#34;1-凸显型顶部栏上下滑动拉伸效果&#34;&gt;1. 凸显型顶部栏上下滑动拉伸效果&lt;/h3&gt;
&lt;video width=&#34;270&#34; height=&#34;480&#34; controls poster=&#34;/img/motionlayout-1.png&#34; preload=&#34;none&#34;&gt;
&lt;source src=&#34;https://i.xdty.org/selif/motionlayout-1.mp4&#34; type=&#34;video/mp4&#34;&gt;
https://i.xdty.org/motionlayout-1.mp4
&lt;/video&gt;
&lt;h3 id=&#34;2-上下滑动拉伸效果及旋转和变色效果&#34;&gt;2. 上下滑动拉伸效果及旋转和变色效果&lt;/h3&gt;
&lt;video width=&#34;270&#34; height=&#34;480&#34; controls poster=&#34;/img/motionlayout-2.png&#34; preload=&#34;none&#34;&gt;
&lt;source src=&#34;https://i.xdty.org/selif/motionlayout-2.mp4&#34; type=&#34;video/mp4&#34;&gt;
https://i.xdty.org/motionlayout-2.mp4
&lt;/video&gt;
&lt;h2 id=&#34;代码分析&#34;&gt;代码分析&lt;/h2&gt;
&lt;h3 id=&#34;1-凸显型顶部栏上下滑动实现&#34;&gt;1. 凸显型顶部栏上下滑动实现&lt;/h3&gt;
&lt;p&gt;从界面布局上分析，如果只是静态布局，外部使用 ConstraitLayout，上半部分凸显型顶部栏直接使用布局填充，下半部分使用 RecyclerView 列表，每个 Item 做独立布局实现即可，这是很好实现的。&lt;/p&gt;
&lt;p&gt;但使用常规的方法，实现这个上下滑动效果是比较复杂的。使用 MotionLayout 替换 ConstraitLayout, 再在 &lt;code&gt;app:layoutDescription&lt;/code&gt; 属性中指定描述文件，描述文件中增加起始状态、中间帧、结束状态，就可以快速实现这个效果。&lt;/p&gt;
&lt;p&gt;状态描述文件保存在 &lt;code&gt;res/xml&lt;/code&gt; 目录下，包含 &lt;code&gt;MotionScene&lt;/code&gt; 根节点，&lt;code&gt;Transition&lt;/code&gt; 节点及若干 &lt;code&gt;ConstraintSet&lt;/code&gt; 节点。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;Transition&lt;/code&gt; 定义了起始状态和结束状态及关键帧。例如下面状态片段中，指定了开始状态 &lt;code&gt;@id/expanded&lt;/code&gt; 结束状态 &lt;code&gt;@id/collapsed&lt;/code&gt;，&lt;code&gt;OnSwipe&lt;/code&gt; 操作的方向则是 &lt;code&gt;dragUp&lt;/code&gt;，同时以 &lt;code&gt;@id/list&lt;/code&gt; 顶部为锚点。&lt;code&gt;KeyFrameSet&lt;/code&gt; 中则指定了两个关键帧，进度 &lt;code&gt;60&lt;/code&gt; 时 &lt;code&gt;@id/toolbar_image&lt;/code&gt; 的 &lt;code&gt;alpha&lt;/code&gt; 修改为不透明，而进度到 &lt;code&gt;90&lt;/code&gt; 时则修改为全透明，即滑动快到顶部时隐藏背景图片。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Transition&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:constraintSetEnd=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/collapsed&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:constraintSetStart=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/expanded&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:interpolator=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;bounce&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;OnSwipe&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:dragDirection=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;dragUp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:touchAnchorId=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/list&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:touchAnchorSide=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;top&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;KeyFrameSet&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;KeyAttribute&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:framePosition=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;60&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:target=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/toolbar_image&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;CustomAttribute&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:attributeName=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;imageAlpha&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:customIntegerValue=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;255&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/KeyAttribute&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;KeyAttribute&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:framePosition=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;90&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:target=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/toolbar_image&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;CustomAttribute&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:attributeName=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;imageAlpha&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:customIntegerValue=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/KeyAttribute&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/KeyFrameSet&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/Transition&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;ConstraintSet&lt;/code&gt; 则和 &lt;code&gt;ConstraitLayout&lt;/code&gt; 内容没有太大区别，只是将变动的View 名统一修改为 &lt;code&gt;Constraint&lt;/code&gt;，移除部分不受影响或不变化的属性，将变动的属性填写即可。&lt;/p&gt;
&lt;p&gt;例如下面的代码片段中，扩展状态 &lt;code&gt;@+id/expanded&lt;/code&gt; 下 &lt;code&gt;@+id/toolbar_image&lt;/code&gt; 的高度为 &lt;code&gt;@dimen/notification_toolbar_height&lt;/code&gt;；&lt;code&gt;@+id/mesh_image&lt;/code&gt; 的 &lt;code&gt;alpha&lt;/code&gt; 调整为 &lt;code&gt;1&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;合并状态 &lt;code&gt;@+id/collapsed&lt;/code&gt; 下则刚好相反，&lt;code&gt;@id/toolbar_image&lt;/code&gt; 高度变更为 &lt;code&gt;71dp&lt;/code&gt;，&lt;code&gt;@id/mesh_image&lt;/code&gt; 高度也变更为 &lt;code&gt;71dp&lt;/code&gt; 且透明度调整为 &lt;code&gt;0&lt;/code&gt; 。这样设置后，上下滑动时就能在这两个状态中切换。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;ConstraintSet&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/expanded&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/toolbar_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@dimen/notification_toolbar_height&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/list&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/mesh_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@dimen/notification_toolbar_height&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:alpha=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/list&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;ConstraintSet&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/collapsed&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/toolbar_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;71dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/mesh_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;71dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:alpha=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;由于目前 &lt;code&gt;2.0.0alpha2&lt;/code&gt; 编辑器支持不太好，可以先按照 ConstraitLayout 实现布局，之后再调整 &lt;code&gt;MotionScene&lt;/code&gt; 中的状态信息。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;实现步骤：&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 ConstraitLayout 实现凸显型顶部栏布局&lt;/li&gt;
&lt;li&gt;将 ConstraitLayout 替换为 MotionLayout&lt;/li&gt;
&lt;li&gt;添加 &lt;code&gt;app:layoutDescription=&amp;quot;@xml/collapsing_notification&amp;quot;&lt;/code&gt; 描述&lt;/li&gt;
&lt;li&gt;新建 &lt;code&gt;res/xml/collapsing_notification&lt;/code&gt; 状态描述文件&lt;/li&gt;
&lt;li&gt;填写 &lt;code&gt;Transition&lt;/code&gt; 内容，定义起始状态及关键帧状态&lt;/li&gt;
&lt;li&gt;填写 &lt;code&gt;ConstraintSet&lt;/code&gt; 布局内容，定义起始状态布局和结束状态布局&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;主布局代码 &lt;code&gt;activity_group_notification.xml&lt;/code&gt; 如下：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;color:#099&#34;&gt;&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;utf-8&amp;#34;?&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;androidx.constraintlayout.motion.widget.MotionLayout&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;xmlns:android=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;http://schemas.android.com/apk/res/android&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;xmlns:app=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;http://schemas.android.com/apk/res-auto&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;xmlns:tools=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;http://schemas.android.com/tools&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;match_parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;match_parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layoutDescription=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@xml/collapsing_notification&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:showPaths=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;false&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;tools:context=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.activity.InboxGroupActivity&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;androidx.recyclerview.widget.RecyclerView&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/list&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:background=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@color/notification_content_background&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:clipToPadding=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;false&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:paddingBottom=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/bottom_bar&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/toolbar_image&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;ImageView&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/toolbar_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@dimen/notification_toolbar_height&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:adjustViewBounds=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:background=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@color/notification_top_end&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:contentDescription=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@null&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:fitsSystemWindows=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:scaleType=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;center&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:src=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@drawable/notification_top_background&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/list&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;ImageView&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/mesh_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@dimen/notification_toolbar_height&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:adjustViewBounds=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:contentDescription=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@null&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:fitsSystemWindows=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:scaleType=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;center&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:src=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@drawable/notification_mesh_bg&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/list&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;TextView&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:text=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:textColor=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;?android:attr/textColorPrimaryInverse&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:textSize=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;64sp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:textStyle=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;bold&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/list&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintHorizontal_bias=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0.48&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/toolbar_image&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;TextView&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status_unit&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginStart=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;4dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginEnd=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:gravity=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;center&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:text=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@string/notification_unit&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:textColor=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;?android:attr/textColorPrimaryInverse&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintHorizontal_bias=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0.0&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintVertical_bias=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0.32&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;TextView&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/description&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginStart=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginTop=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginEnd=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginBottom=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:gravity=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;center&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:text=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@string/notification_description&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:textColor=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;?android:attr/textColorPrimaryInverse&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:textSize=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;15sp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/toolbar_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;ImageView&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/home_button&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginTop=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;25dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:contentDescription=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@null&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:paddingStart=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;12dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:paddingTop=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:paddingEnd=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:paddingBottom=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:src=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@drawable/home_arrow&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:tint=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;?android:attr/textColorPrimaryInverse&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;tools:ignore=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;PrivateResource&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;ImageView&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/menu_button&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:contentDescription=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@null&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:paddingStart=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;16dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:paddingTop=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:paddingEnd=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;16dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:paddingBottom=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:src=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@drawable/ic_more_vert_white_24dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:tint=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;?android:attr/textColorPrimaryInverse&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/home_button&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/home_button&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;tools:ignore=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;PrivateResource&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;TextView&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/title&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:text=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@string/notification_title&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:textColor=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;?android:attr/textColorPrimaryInverse&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:textSize=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;16sp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/home_button&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/home_button&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/home_button&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;View&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/bottom_bar&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;match_parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;64dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:background=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;#FFFAFAFA&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;TextView&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/clean_text&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;240dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;34dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginStart=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginTop=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginEnd=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;8dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginBottom=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;-1dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:background=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@drawable/prompt_button_background&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:gravity=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;center&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:text=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@string/notification_clean_btn&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:textColor=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;#FFFFFFFF&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:textSize=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;14sp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/bottom_bar&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;View&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/top_bar_line&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;match_parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1px&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:background=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@color/line_color&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/bottom_bar&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/androidx.constraintlayout.motion.widget.MotionLayout&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;状态描述代码 &lt;code&gt;res/xml/collapsing_notification.xml&lt;/code&gt; 如下：&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;color:#099&#34;&gt;&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;utf-8&amp;#34;?&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;MotionScene&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;xmlns:android=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;http://schemas.android.com/apk/res/android&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;xmlns:app=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;http://schemas.android.com/apk/res-auto&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Transition&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:constraintSetEnd=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/collapsed&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:constraintSetStart=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/expanded&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:interpolator=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;bounce&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;OnSwipe&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:dragDirection=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;dragUp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:touchAnchorId=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/list&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:touchAnchorSide=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;top&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;KeyFrameSet&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;KeyAttribute&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:framePosition=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;60&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:target=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/toolbar_image&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;CustomAttribute&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:attributeName=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;imageAlpha&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:customIntegerValue=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;255&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/KeyAttribute&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;KeyAttribute&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:framePosition=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;90&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:target=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/toolbar_image&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;CustomAttribute&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:attributeName=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;imageAlpha&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:customIntegerValue=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/KeyAttribute&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/KeyFrameSet&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/Transition&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;ConstraintSet&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/expanded&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/toolbar_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@dimen/notification_toolbar_height&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/list&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/mesh_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@dimen/notification_toolbar_height&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:alpha=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/list&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:alpha=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/list&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintHorizontal_bias=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0.48&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/toolbar_image&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status_unit&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:alpha=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintHorizontal_bias=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0.0&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintVertical_bias=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0.32&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/description&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:alpha=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/toolbar_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/ConstraintSet&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;ConstraintSet&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/collapsed&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/toolbar_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;71dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/mesh_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;71dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:alpha=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/status&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:alpha=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/list&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintHorizontal_bias=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0.48&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/toolbar_image&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/status_unit&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:alpha=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintHorizontal_bias=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0.0&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintVertical_bias=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0.32&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@id/description&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrap_content&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:alpha=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;0&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/toolbar_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/status&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/ConstraintSet&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/MotionScene&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;2-上下滑动效果及旋转变色效果融合&#34;&gt;2. 上下滑动效果及旋转变色效果融合&lt;/h3&gt;
&lt;p&gt;要实现 &lt;a href=&#34;https://i.xdty.org/motionlayout-2.mp4&#34;&gt;视频二&lt;/a&gt; 里的效果也很简单，可以拆分为三部分：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;上下滑动实现效果&lt;/li&gt;
&lt;li&gt;背景色渐变切换动画&lt;/li&gt;
&lt;li&gt;背景圆圈图片旋转动画及数字切换&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;上下滑动实现效果，背景圆圈设定 90 度旋转&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;这部分内容和上一小节基本一致，只需加入背景圆圈设定 90 度旋转即可：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;Constraint&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:id=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/cleaner_circle&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_width=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;200dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_height=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;200dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginTop=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;4dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:layout_marginBottom=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;54dp&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:alpha=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;1&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:contentDescription=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@null&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:rotation=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;90&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:src=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@drawable/cleaner_circle_bg&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintBottom_toBottomOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/light_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintEnd_toEndOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;parent&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintStart_toStartOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/toolbar_image&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;app:layout_constraintTop_toTopOf=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@+id/home_button&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;背景颜色渐变动画&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;背景过渡动画由四个 &lt;code&gt;gradient&lt;/code&gt; 背景色组成，通过 &lt;code&gt;AnimationDrawable&lt;/code&gt; 添加需要的过渡背景帧然后播放动画：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@SuppressWarnings&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;ConstantConditions&amp;#34;&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; animateToolbarDrawable(&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;percent&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;AnimationDrawable&lt;/span&gt; animationDrawable &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; AnimationDrawable();
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Integer&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;rgbColors&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;level&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; range(percent);
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;duration&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ANIMATION_DURATION &lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt; level;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (level &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;=&lt;/span&gt; 0) {
animationDrawable.&lt;span style=&#34;color:#309&#34;&gt;addFrame&lt;/span&gt;(getDrawable(R.&lt;span style=&#34;color:#309&#34;&gt;drawable&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;status_color_low&lt;/span&gt;), duration);
rgbColors.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(ContextCompat.&lt;span style=&#34;color:#309&#34;&gt;getColor&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;, R.&lt;span style=&#34;color:#309&#34;&gt;color&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;status_low_end&lt;/span&gt;));
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (level &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;=&lt;/span&gt; 2) {
animationDrawable.&lt;span style=&#34;color:#309&#34;&gt;addFrame&lt;/span&gt;(getDrawable(R.&lt;span style=&#34;color:#309&#34;&gt;drawable&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;status_color_medium&lt;/span&gt;), duration);
rgbColors.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(ContextCompat.&lt;span style=&#34;color:#309&#34;&gt;getColor&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;, R.&lt;span style=&#34;color:#309&#34;&gt;color&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;status_medium_end&lt;/span&gt;));
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (level &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;=&lt;/span&gt; 3) {
animationDrawable.&lt;span style=&#34;color:#309&#34;&gt;addFrame&lt;/span&gt;(getDrawable(R.&lt;span style=&#34;color:#309&#34;&gt;drawable&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;status_color_high&lt;/span&gt;), duration);
rgbColors.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(ContextCompat.&lt;span style=&#34;color:#309&#34;&gt;getColor&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;, R.&lt;span style=&#34;color:#309&#34;&gt;color&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;status_high_end&lt;/span&gt;));
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (level &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;=&lt;/span&gt; 4) {
animationDrawable.&lt;span style=&#34;color:#309&#34;&gt;addFrame&lt;/span&gt;(getDrawable(R.&lt;span style=&#34;color:#309&#34;&gt;drawable&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;status_color_critical&lt;/span&gt;), duration);
rgbColors.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(ContextCompat.&lt;span style=&#34;color:#309&#34;&gt;getColor&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;, R.&lt;span style=&#34;color:#309&#34;&gt;color&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;status_critical_end&lt;/span&gt;));
}
mToolbarImage.&lt;span style=&#34;color:#309&#34;&gt;setImageDrawable&lt;/span&gt;(animationDrawable);
animationDrawable.&lt;span style=&#34;color:#309&#34;&gt;setOneShot&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;);
animationDrawable.&lt;span style=&#34;color:#309&#34;&gt;setEnterFadeDuration&lt;/span&gt;(duration &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt; 200);
animationDrawable.&lt;span style=&#34;color:#309&#34;&gt;setExitFadeDuration&lt;/span&gt;(duration &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt; 300);
animationDrawable.&lt;span style=&#34;color:#309&#34;&gt;start&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ValueAnimator&lt;/span&gt; valueAnimator &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ValueAnimator.&lt;span style=&#34;color:#309&#34;&gt;ofArgb&lt;/span&gt;(toIntArray(rgbColors));
valueAnimator.&lt;span style=&#34;color:#309&#34;&gt;setInterpolator&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; LinearInterpolator());
valueAnimator.&lt;span style=&#34;color:#309&#34;&gt;setDuration&lt;/span&gt;(ANIMATION_DURATION);
valueAnimator.&lt;span style=&#34;color:#309&#34;&gt;addUpdateListener&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ValueAnimator.&lt;span style=&#34;color:#309&#34;&gt;AnimatorUpdateListener&lt;/span&gt;() {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onAnimationUpdate(ValueAnimator &lt;span style=&#34;color:#c0f&#34;&gt;animation&lt;/span&gt;) {
mToolbarImage.&lt;span style=&#34;color:#309&#34;&gt;setBackgroundColor&lt;/span&gt;((Integer) valueAnimator.&lt;span style=&#34;color:#309&#34;&gt;getAnimatedValue&lt;/span&gt;());
}
});
valueAnimator.&lt;span style=&#34;color:#309&#34;&gt;start&lt;/span&gt;();
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt;[] &lt;span style=&#34;color:#c0f&#34;&gt;toIntArray&lt;/span&gt;(List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Integer&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;list&lt;/span&gt;) {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt;[] &lt;span style=&#34;color:#c0f&#34;&gt;ret&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt;[list.&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;()];
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (Integer &lt;span style=&#34;color:#c0f&#34;&gt;e&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; list) {
ret[i&lt;span style=&#34;color:#555&#34;&gt;++&lt;/span&gt;] &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; e;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; ret;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;range&lt;/span&gt;(&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;percent&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (percent &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt; 40) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; 1;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (percent &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt; 65) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; 2;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (percent &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt; 85) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; 3;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (percent &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;=&lt;/span&gt; 100) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; 4;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; 0;
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;gradient&lt;/code&gt; 背景色 &lt;code&gt;status_color_low.xml&lt;/code&gt; 示例：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;color:#099&#34;&gt;&amp;lt;?xml version=&amp;#34;1.0&amp;#34; encoding=&amp;#34;UTF-8&amp;#34;?&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;selector&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;xmlns:android=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;http://schemas.android.com/apk/res/android&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;item&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;shape&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;gradient&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:angle=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;90&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:centerColor=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@color/status_low_center&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:endColor=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@color/status_low_end&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:startColor=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@color/status_low_start&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/shape&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/item&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/selector&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;背景圆圈图片旋转动画及数字切换&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;背景图片旋转图片使用 &lt;code&gt;RotateAnimation&lt;/code&gt; 实现，并设置加速减速产生器，其中的结束角度计算 &lt;code&gt;(360 * Math.ceil(12 / 3.0)&lt;/code&gt; 则是按照动画时长 &lt;code&gt;ANIMATION_DURATION&lt;/code&gt; 估算的一个值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; animateCleanerCircle() {
RotateAnimation &lt;span style=&#34;color:#c0f&#34;&gt;animation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; RotateAnimation(0,
(&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;float&lt;/span&gt;) (360 &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt; Math.&lt;span style=&#34;color:#309&#34;&gt;ceil&lt;/span&gt;(12 &lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt; 3.&lt;span style=&#34;color:#309&#34;&gt;0&lt;/span&gt;)),
Animation.&lt;span style=&#34;color:#309&#34;&gt;RELATIVE_TO_SELF&lt;/span&gt;, 0.&lt;span style=&#34;color:#309&#34;&gt;5f&lt;/span&gt;,
Animation.&lt;span style=&#34;color:#309&#34;&gt;RELATIVE_TO_SELF&lt;/span&gt;, 0.&lt;span style=&#34;color:#309&#34;&gt;5f&lt;/span&gt;);
animation.&lt;span style=&#34;color:#309&#34;&gt;setDuration&lt;/span&gt;((&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;long&lt;/span&gt;) (ANIMATION_DURATION));
animation.&lt;span style=&#34;color:#309&#34;&gt;setRepeatCount&lt;/span&gt;(0);
animation.&lt;span style=&#34;color:#309&#34;&gt;setInterpolator&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; AccelerateDecelerateInterpolator());
mCleanerCircle.&lt;span style=&#34;color:#309&#34;&gt;clearAnimation&lt;/span&gt;();
mCleanerCircle.&lt;span style=&#34;color:#309&#34;&gt;startAnimation&lt;/span&gt;(animation);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;数字切换动画也是类似：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; animateMemoryText(&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;float&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;usedMemory&lt;/span&gt;) {
ValueAnimator &lt;span style=&#34;color:#c0f&#34;&gt;valueAnimator&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ValueAnimator.&lt;span style=&#34;color:#309&#34;&gt;ofFloat&lt;/span&gt;(0, usedMemory);
valueAnimator.&lt;span style=&#34;color:#309&#34;&gt;setDuration&lt;/span&gt;((&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;long&lt;/span&gt;) (ANIMATION_DURATION));
valueAnimator.&lt;span style=&#34;color:#309&#34;&gt;setInterpolator&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; AccelerateDecelerateInterpolator());
valueAnimator.&lt;span style=&#34;color:#309&#34;&gt;addUpdateListener&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ValueAnimator.&lt;span style=&#34;color:#309&#34;&gt;AnimatorUpdateListener&lt;/span&gt;() {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onAnimationUpdate(ValueAnimator &lt;span style=&#34;color:#c0f&#34;&gt;valueAnimator&lt;/span&gt;) {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;float&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;value&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; (&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;float&lt;/span&gt;) valueAnimator.&lt;span style=&#34;color:#309&#34;&gt;getAnimatedValue&lt;/span&gt;();
mMemoryText.&lt;span style=&#34;color:#309&#34;&gt;setText&lt;/span&gt;(String.&lt;span style=&#34;color:#309&#34;&gt;format&lt;/span&gt;(Locale.&lt;span style=&#34;color:#309&#34;&gt;US&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;%.2f&amp;#34;&lt;/span&gt;, value));
}
});
valueAnimator.&lt;span style=&#34;color:#309&#34;&gt;start&lt;/span&gt;();
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&#34;参考资料&#34;&gt;参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://medium.com/google-developers/introduction-to-motionlayout-part-i-29208674b10d&#34;&gt;Introduction to MotionLayout (part I)&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/googlesamples/android-ConstraintLayoutExamples&#34;&gt;Android-ConstraintLayoutExamples&lt;/a&gt;&lt;/p&gt;</description></item><item><title>域名转移到 CloudFlare Registrar 和掉年问题</title><link>https://busy.im/post/recently-renewed-domain-transfer/</link><pubDate>Sat, 08 Dec 2018 19:37:40 +0800</pubDate><guid>https://busy.im/post/recently-renewed-domain-transfer/</guid><description>
&lt;p&gt;CloudFlare 是一家优秀的 CDN 服务提供商，同时提供 DNS 解析服务。最近又推出了域名注册服务 &lt;a href=&#34;https://www.cloudflare.com/products/registrar/&#34;&gt;CloudFlare Registrar&lt;/a&gt;，因为只收取注册局的费用而没有中介费，可以说是最便宜的域名注册服务商了。同时以 CloudFlare 的强大实力和服务质量，相信可以非常放心的将域名托管在这里。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/registrar_pricing__0.5x.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h2 id=&#34;域名转移&#34;&gt;域名转移&lt;/h2&gt;
&lt;p&gt;我之前申请了优先体验，前几天收到了转移邀请邮件，所以就快速转移了三个域名做尝试体验。目前 CloudFlare 还不支持 ccTLD 即国家类域名，如 &lt;code&gt;.in&lt;/code&gt; &lt;code&gt;.io&lt;/code&gt; &lt;code&gt;.im&lt;/code&gt; 这一类域名，需要再等一段时间才能支持，常见的 &lt;code&gt;.com&lt;/code&gt; &lt;code&gt;.net&lt;/code&gt; &lt;code&gt;.org&lt;/code&gt; 和 &lt;code&gt;.xyz&lt;/code&gt; &lt;code&gt;.love&lt;/code&gt; 这种新域名自然是支持的，共有&lt;a href=&#34;https://www.cloudflare.com/tld-policies/&#34;&gt;上百种域名&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/20181208_001.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;从 NameSilo 转移到 CloudFlare 的过程很顺利，没有遇到什么问题，虽然有一些小 Bug，但是可以忽视。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/20181208_002.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;h2 id=&#34;域名转移过期时间掉年问题&#34;&gt;域名转移过期时间掉年问题&lt;/h2&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/20181208_003.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;但是在域名概览里查看注册信息时却发现了过期日期少了一年的问题。在2018/11/01 我收到了 NameSilo 的域名即将过期提醒邮件，然后我就立刻续费了，所以下次过期时间已经被更新为 2019/11/29，而在转移域名后应该会增加一年，但是转移后有两个域名过期日期没有变，以为是新服务测试阶段有 bug，就提了工单，同时也在&lt;a href=&#34;https://community.cloudflare.com/t/domain-registrar-next-renewal-is-not-correct-not-expanded-to-1-year-later/47914&#34;&gt;社区&lt;/a&gt;里咨询。&lt;/p&gt;
&lt;p&gt;社区有个网友反馈也遇到了类似的问题，他等待了两天 whois 信息就正常了。在跟客服沟通时，客服说是域名过期后 45 天内续费再转移导致的注册局退款，即就是域名转移掉年问题，需要找 NameSilo 退款。但是我的域名并没有过期，是过期前一个月就已经续费了的。&lt;/p&gt;
&lt;p&gt;在工单交流的过程中，&lt;strong&gt;CloudFlare 客服甚至提出了他们可以给予退款来补偿我的损失&lt;/strong&gt;，这一点让我很感动，不得不为 CloudFlare 的服务点赞。在搜索后发现续费后 60 天内转移容易出现掉年问题，同时我也在 NameSilo 网站中搜索到了相关的信息 &lt;a href=&#34;https://www.namesilo.com/Support/Transfer-of-Recently-Renewed-Domains&#34;&gt;Transfer-of-Recently-Renewed-Domains&lt;/a&gt; ，所以又发邮件给 NameSilo 询问相关情况。&lt;/p&gt;
&lt;p&gt;NameSilo 没有工单系统，而且邮件回复很慢，等待过程中甚至认为它的服务太差想要将剩余的几个域名尽快转出的想法。在他们回复邮件问我 &amp;ldquo;What would you like from us?&amp;rdquo; 时，我回复了更详细的问题描述，同时张贴了与 Cloudflare 的工单交流记录（网页打印为PDF作为附件）。&lt;/p&gt;
&lt;h2 id=&#34;收到退款&#34;&gt;收到退款&lt;/h2&gt;
&lt;p&gt;之后差不多两天后终于收到了 NameSilo 的回复，他们确认了这个问题正是我在他们网站中找到的转移近期续费域名出现的问题，他们需要时间联系注册局检查退款信息，同时也已经对我的这两个域名发起了退款。NameSilo 也不忘提醒我这不是 CloudFlare 的错误，不应该由后者承担退款。当然我也从没想过去向 CloudFlare 索要退款，即使真的找不到问题原因，我也愿意自己承担损失。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/20181208_004.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;我对这个回复表达了感谢，也对他们长期以来的优质服务表达感谢，同时我也在 CloudFlare 工单中张贴了 NameSilo 的回复和截图，并对他们的热心服务表达了感谢。虽然续费的金额不多，期间我甚至想要放弃这笔退款，没想到 NameSilo 处理的很干脆，对他们的服务信心也大增。至此，掉年的问题圆满解决了。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/20181208_005.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;</description></item><item><title>自建稍后阅读服务之 Wallabag</title><link>https://busy.im/post/read-it-later-wallabag/</link><pubDate>Tue, 13 Nov 2018 16:49:20 +0800</pubDate><guid>https://busy.im/post/read-it-later-wallabag/</guid><description>
&lt;p&gt;&lt;a href=&#34;https://www.wallabag.org/en&#34;&gt;Wallabag&lt;/a&gt; 是一款优秀的开源免费稍后阅读工具，可以将网络上的文章保存并分类，再稍后阅读。类似于 &lt;a href=&#34;https://getpocket.com/&#34;&gt;Pocket&lt;/a&gt; 和 &lt;a href=&#34;https://www.instapaper.com/&#34;&gt;Instapaper&lt;/a&gt; 等商业服务，是它们的可靠替代品。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/wallabag-screen2.png&#34; alt=&#34;screenshot&#34; /&gt;&lt;/p&gt;
&lt;h2 id=&#34;什么是-wallabag&#34;&gt;什么是 wallabag？&lt;/h2&gt;
&lt;p&gt;Wallabag 是一款可自行托管的 PHP 应用程序，让你不再错过任何内容。 点击，保存然后在你有时间时阅读。 它可以提取网页内容，以便你有空时阅读。&lt;/p&gt;
&lt;p&gt;更多信息请访问官网 &lt;a href=&#34;https://wallabag.org/&#34;&gt;https://wallabag.org/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果你没有自己的服务器，请考虑使用 &lt;a href=&#34;https://wallabag.it/&#34;&gt;wallabag.it&lt;/a&gt; 托管解决方案。&lt;/p&gt;
&lt;p&gt;也可以使用我搭建的服务 &lt;a href=&#34;https://wb.xdty.org/&#34;&gt;https://wb.xdty.org/&lt;/a&gt; ，我已经将它作为稍后阅读的生产力工具使用。&lt;/p&gt;
&lt;h2 id=&#34;安装-wallabag&#34;&gt;安装 wallabag&lt;/h2&gt;
&lt;p&gt;请阅读 &lt;a href=&#34;https://doc.wallabag.org/en/admin/installation/requirements.html&#34;&gt;文档&lt;/a&gt; 以查看 wallabag 安装依赖。&lt;/p&gt;
&lt;p&gt;然后，可以通过执行以下命令来安装 wallabag：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;git clone https://github.com/wallabag/wallabag.git
&lt;span style=&#34;color:#366&#34;&gt;cd&lt;/span&gt; wallabag &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; make install&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;现在，&lt;a href=&#34;https://doc.wallabag.org/en/admin/installation/virtualhosts.html&#34;&gt;配置虚拟主机&lt;/a&gt;以使用 wallabag 。&lt;/p&gt;
&lt;h2 id=&#34;特性&#34;&gt;特性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;开源 PHP 程序，易托管&lt;/li&gt;
&lt;li&gt;界面美观，易于阅读&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://chrome.google.com/webstore/detail/wallabagger/gbmgphmejlcoihgedabhgjdkcahacjlj&#34;&gt;浏览器插件&lt;/a&gt; - 一键保存网页到服务&lt;/li&gt;
&lt;li&gt;开放的 API - 可以根据 API 自由编写客户端&lt;/li&gt;
&lt;li&gt;迁移 - 从 Pocket, Readability, Instapaper 或 Pinboard 服务导入数据&lt;/li&gt;
&lt;li&gt;导出 - 可以导出文章到 epub, mobi, pdf 等格式&lt;/li&gt;
&lt;li&gt;随处可用 - 由于是 Web 服务，可以在使用浏览器的系统上使用，同时支持 Android 客户端&lt;/li&gt;
&lt;li&gt;RSS 阅读器兼容&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/wallabag-screen1.png&#34; alt=&#34;screenshot&#34; /&gt;&lt;/p&gt;
&lt;h2 id=&#34;问题&#34;&gt;问题&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;实际使用中遇到部分网页内容不能保存的问题，&lt;del&gt;期待后续版本能支持知乎等网站的抓取&lt;/del&gt;。&lt;/li&gt;
&lt;/ul&gt;
&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;更新-20181212:&lt;/strong&gt; 不能抓取知乎是因为 user-agent 导致的，已在上游提交 PR : &lt;a href=&#34;https://github.com/fivefilters/ftr-site-config/pull/578&#34;&gt;fivefilters/ftr-site-config/pull/578&lt;/a&gt;。&lt;/p&gt;
&lt;p&gt;修改 &lt;code&gt;vendor/j0k3r/graby-site-config/zhihu.com.txt&lt;/code&gt; ，增加如下内容：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;http_header&lt;span style=&#34;color:#555&#34;&gt;(&lt;/span&gt;user-agent&lt;span style=&#34;color:#555&#34;&gt;)&lt;/span&gt;: Mozilla/5.0 &lt;span style=&#34;color:#555&#34;&gt;(&lt;/span&gt;Windows NT &lt;span style=&#34;color:#f60&#34;&gt;6&lt;/span&gt;.1; Win64; x64&lt;span style=&#34;color:#555&#34;&gt;)&lt;/span&gt; AppleWebKit/537.36 &lt;span style=&#34;color:#555&#34;&gt;(&lt;/span&gt;KHTML, like Gecko&lt;span style=&#34;color:#555&#34;&gt;)&lt;/span&gt; Chrome/70.0.3538.110 Safari/537.36&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;然后再重新编译更新下即可：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-shell&#34; data-lang=&#34;shell&#34;&gt;make update&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/blockquote&gt;
&lt;h2 id=&#34;许可证&#34;&gt;许可证&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/wallabag/wallabag/blob/master/COPYING.md&#34;&gt;MIT&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;开源项目地址&#34;&gt;开源项目地址&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/wallabag/wallabag&#34;&gt;https://github.com/wallabag/wallabag&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Java 语言 Glob 语法规则</title><link>https://busy.im/post/java-glob-syntax/</link><pubDate>Tue, 23 Oct 2018 20:01:33 +0800</pubDate><guid>https://busy.im/post/java-glob-syntax/</guid><description>
&lt;p&gt;最近在做 &lt;code&gt;Android&lt;/code&gt; 下应用专清的功能，需要检索微信、微博这类应用的垃圾文件，尝试了 &lt;code&gt;apache&lt;/code&gt; 的 &lt;code&gt;common-io&lt;/code&gt; 以及 &lt;code&gt;java&lt;/code&gt; 的 &lt;code&gt;nio&lt;/code&gt; ，最后使用 &lt;code&gt;nio&lt;/code&gt; 的 &lt;code&gt;Files.walkFileTree&lt;/code&gt; 遍历目录，并使用 &lt;code&gt;PathMatcher&lt;/code&gt; 和 &lt;code&gt;glob&lt;/code&gt; 规则做文件匹配，本文是对 &lt;code&gt;glob&lt;/code&gt; 语法的总结。&lt;/p&gt;
&lt;h2 id=&#34;什么是-glob&#34;&gt;什么是 Glob&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Glob&lt;/code&gt; 是一种模式匹配，类似于正则表达式但是语法相对简单。&lt;code&gt;Glob&lt;/code&gt; 语句是一个含有 &lt;code&gt;*,?{}[]&lt;/code&gt; 这些特殊字符的字符串，并与目标字符串匹配。可以用来做文件路径匹配或文件查找。例如 &lt;code&gt;Linux&lt;/code&gt; 下的命令 &lt;code&gt;ls *.txt&lt;/code&gt; ，其中的 &lt;code&gt;*.txt&lt;/code&gt; 就是一个 &lt;code&gt;glob&lt;/code&gt; 语句。&lt;/p&gt;
&lt;h2 id=&#34;语法规则&#34;&gt;语法规则&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Java&lt;/code&gt; 语言中的 &lt;code&gt;Glob&lt;/code&gt; 语法遵循几个简单的规则：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一个星号 &lt;code&gt;*&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;匹配任意个数的字符，包括空。不包括路径边界 &lt;code&gt;/&lt;/code&gt; 或 &lt;code&gt;\&lt;/code&gt;。例如 &lt;code&gt;/path/*/abc&lt;/code&gt; 可以匹配 &lt;code&gt;/path/a/abc&lt;/code&gt; 和 &lt;code&gt;/path/b/abc&lt;/code&gt; 等。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;两个星号 &lt;code&gt;**&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;和一个星号类似，区别是可以跨路径边界，一般用来匹配多级目录。例如 &lt;code&gt;/path/**/abc&lt;/code&gt; 可以用来匹配 &lt;code&gt;/path/a/abc&lt;/code&gt; 、&lt;code&gt;/path/b/abc&lt;/code&gt; 、&lt;code&gt;/path/a/b/abc&lt;/code&gt;、&lt;code&gt;/path/a/b/c/abc&lt;/code&gt; 等。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;一个问号 &lt;code&gt;?&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;匹配任意一个字符&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;大括号 &lt;code&gt;{}&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;大括号用来指定一个子模式匹配集合，例如：&lt;code&gt;{sun,moon,stars}&lt;/code&gt; 可以匹配 &lt;code&gt;sun&lt;/code&gt; &lt;code&gt;moon&lt;/code&gt; &lt;code&gt;starts&lt;/code&gt; ， &lt;code&gt;{temp*, tmp*}&lt;/code&gt; 可以匹配任何以 &lt;code&gt;temp&lt;/code&gt; &lt;code&gt;tmp&lt;/code&gt; 开头的字符串等。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;方括号&lt;code&gt;[]&lt;/code&gt;&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;方括号表示匹配括号内的任意一个单个字符，当有 &lt;code&gt;-&lt;/code&gt; 时表示匹配任意一个连续范围内的单个字符。例如：&lt;code&gt;[aeiou]&lt;/code&gt; 匹配任意一个小写元音字符，&lt;code&gt;[0-9]&lt;/code&gt; 匹配任意一个数字，&lt;code&gt;[A-Z]&lt;/code&gt; 匹配任意一个大写字母，&lt;code&gt;[a-z,A-Z]&lt;/code&gt; 匹配任意一个大写或小写字母。另外，在方括号内， &lt;code&gt;*&lt;/code&gt; &lt;code&gt;?&lt;/code&gt; &lt;code&gt;\&lt;/code&gt; 字符仅匹配它们自身。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;任意其它字符&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;任意的其他字符表示匹配它们自身。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;反斜杠&lt;code&gt;\&lt;/code&gt;转义&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;匹配 &lt;code&gt;*&lt;/code&gt; &lt;code&gt;?&lt;/code&gt; 或其他特殊字符需要使用反斜杠&lt;code&gt;\&lt;/code&gt;转义。例如： &lt;code&gt;\\&lt;/code&gt; 匹配一个反斜杠，&lt;code&gt;\?&lt;/code&gt; 匹配一个问号。&lt;/p&gt;
&lt;h2 id=&#34;举例&#34;&gt;举例&lt;/h2&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Glob&lt;/th&gt;
&lt;th&gt;说明&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*.html&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配所有 &lt;code&gt;.html&lt;/code&gt; 结尾的字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;???&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配所有由三个数字或字母构成字符串，如 &lt;code&gt;aaa&lt;/code&gt; &lt;code&gt;abc&lt;/code&gt; &lt;code&gt;ab1&lt;/code&gt; &lt;code&gt;123&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*[0-9]*&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配所有含有一个数字的字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;*.{htm,html,pdf}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配所有以 &lt;code&gt;.htm&lt;/code&gt; &lt;code&gt;.html 或&lt;/code&gt; &lt;code&gt;.pdf&lt;/code&gt; 结尾的字符串&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;a?*.java&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配所有以 &lt;code&gt;a&lt;/code&gt; 开头，且&lt;code&gt;a&lt;/code&gt; 之后至少由一个字母或数字，且以 &lt;code&gt;.java&lt;/code&gt; 结尾的字符传&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;{foo*,*[0-9]*}&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;匹配所有以 &lt;code&gt;foo&lt;/code&gt; 开头，或含有数字的字符串，如 &lt;code&gt;foobar&lt;/code&gt; &lt;code&gt;x1y&lt;/code&gt; &lt;code&gt;foo1xyz&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;代码实例&#34;&gt;代码实例&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;JDK&lt;/code&gt; 中 &lt;code&gt;glob&lt;/code&gt; 相关的内容主要集中在 &lt;code&gt;java.nio.file&lt;/code&gt; 包内，其中 &lt;code&gt;glob&lt;/code&gt; 语法转正则表达式的实现在 &lt;code&gt;sun.nio.fs.Globs.java&lt;/code&gt; 中。下面代码是一个检索 &lt;code&gt;/sdcard/tencent&lt;/code&gt; 目录下所有 &lt;code&gt;png&lt;/code&gt; 文件的例子：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;String &lt;span style=&#34;color:#c0f&#34;&gt;match&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;glob:/sdcard/tencent/**/*.png&amp;#34;&lt;/span&gt;;
System.&lt;span style=&#34;color:#309&#34;&gt;out&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;println&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;scanning: &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; match);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;PathMatcher&lt;/span&gt; pathMatcher &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; FileSystems.&lt;span style=&#34;color:#309&#34;&gt;getDefault&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;getPathMatcher&lt;/span&gt;(match);
SimpleFileVisitor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Path&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;fileVisitor&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; SimpleFileVisitor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Path&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt;() {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;FileVisitResult&lt;/span&gt; visitFile(Path &lt;span style=&#34;color:#c0f&#34;&gt;file&lt;/span&gt;, BasicFileAttributes &lt;span style=&#34;color:#c0f&#34;&gt;attrs&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;throws&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;IOException&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (pathMatcher.&lt;span style=&#34;color:#309&#34;&gt;matches&lt;/span&gt;(file)) {
System.&lt;span style=&#34;color:#309&#34;&gt;out&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;println&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;find: &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;toFile&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;())
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;super&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;visitFile&lt;/span&gt;(file, attrs);
}
};
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;try&lt;/span&gt; {
Files.&lt;span style=&#34;color:#309&#34;&gt;walkFileTree&lt;/span&gt;(Paths.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;(dir.&lt;span style=&#34;color:#309&#34;&gt;getPath&lt;/span&gt;()), fileVisitor);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (IOException &lt;span style=&#34;color:#c0f&#34;&gt;e&lt;/span&gt;) {
e.&lt;span style=&#34;color:#309&#34;&gt;printStackTrace&lt;/span&gt;();
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;Glob 语句转正则表达式的实现&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;http://hg.openjdk.java.net/jdk7/jdk7/jdk/file/tip/src/share/classes/sun/nio/fs/Globs.java&#34;&gt;http://hg.openjdk.java.net/jdk7/jdk7/jdk/file/tip/src/share/classes/sun/nio/fs/Globs.java&lt;/a&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;package&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;sun&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;nio&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;fs&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;java&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;util&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;regex&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;PatternSyntaxException&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; Globs {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Globs&lt;/span&gt;() { }
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; regexMetaChars &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.^$+{[]|()&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; globMetaChars &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;\\*?[{&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;isRegexMeta&lt;/span&gt;(&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;char&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;c&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; regexMetaChars.&lt;span style=&#34;color:#309&#34;&gt;indexOf&lt;/span&gt;(c) &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;1;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;isGlobMeta&lt;/span&gt;(&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;char&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;c&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; globMetaChars.&lt;span style=&#34;color:#309&#34;&gt;indexOf&lt;/span&gt;(c) &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;1;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;char&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;EOL&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;//TBD
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;char&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;next&lt;/span&gt;(String &lt;span style=&#34;color:#c0f&#34;&gt;glob&lt;/span&gt;, &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;i&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (i &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt; glob.&lt;span style=&#34;color:#309&#34;&gt;length&lt;/span&gt;()) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; glob.&lt;span style=&#34;color:#309&#34;&gt;charAt&lt;/span&gt;(i);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; EOL;
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/**
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * Creates a regex pattern from the given glob expression.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; *
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * @throws PatternSyntaxException
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; */&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; String &lt;span style=&#34;color:#c0f&#34;&gt;toRegexPattern&lt;/span&gt;(String &lt;span style=&#34;color:#c0f&#34;&gt;globPattern&lt;/span&gt;, &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;isDos&lt;/span&gt;) {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;inGroup&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
StringBuilder &lt;span style=&#34;color:#c0f&#34;&gt;regex&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; StringBuilder(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;^&amp;#34;&lt;/span&gt;);
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;while&lt;/span&gt; (i &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt; globPattern.&lt;span style=&#34;color:#309&#34;&gt;length&lt;/span&gt;()) {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;char&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;c&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; globPattern.&lt;span style=&#34;color:#309&#34;&gt;charAt&lt;/span&gt;(i&lt;span style=&#34;color:#555&#34;&gt;++&lt;/span&gt;);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;switch&lt;/span&gt; (c) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\\&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// escape special characters
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (i &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; globPattern.&lt;span style=&#34;color:#309&#34;&gt;length&lt;/span&gt;()) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; PatternSyntaxException(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;No character to escape&amp;#34;&lt;/span&gt;,
globPattern, i &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt; 1);
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;char&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;next&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; globPattern.&lt;span style=&#34;color:#309&#34;&gt;charAt&lt;/span&gt;(i&lt;span style=&#34;color:#555&#34;&gt;++&lt;/span&gt;);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (isGlobMeta(next) &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; isRegexMeta(next)) {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\\&amp;#39;&lt;/span&gt;);
}
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(next);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;/&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (isDos) {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;\\\\&amp;#34;&lt;/span&gt;);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(c);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;[&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// don&amp;#39;t match name separator in class
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (isDos) {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;[[^\\\\]&amp;amp;&amp;amp;[&amp;#34;&lt;/span&gt;);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;[[^/]&amp;amp;&amp;amp;[&amp;#34;&lt;/span&gt;);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (next(globPattern, i) &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;^&amp;#39;&lt;/span&gt;) {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// escape the regex negation char if it appears
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;\\^&amp;#34;&lt;/span&gt;);
i&lt;span style=&#34;color:#555&#34;&gt;++&lt;/span&gt;;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// negation
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (next(globPattern, i) &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;!&amp;#39;&lt;/span&gt;) {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;^&amp;#39;&lt;/span&gt;);
i&lt;span style=&#34;color:#555&#34;&gt;++&lt;/span&gt;;
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// hyphen allowed at start
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (next(globPattern, i) &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;-&amp;#39;&lt;/span&gt;) {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;-&amp;#39;&lt;/span&gt;);
i&lt;span style=&#34;color:#555&#34;&gt;++&lt;/span&gt;;
}
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;hasRangeStart&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;char&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;last&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;while&lt;/span&gt; (i &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt; globPattern.&lt;span style=&#34;color:#309&#34;&gt;length&lt;/span&gt;()) {
c &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; globPattern.&lt;span style=&#34;color:#309&#34;&gt;charAt&lt;/span&gt;(i&lt;span style=&#34;color:#555&#34;&gt;++&lt;/span&gt;);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (c &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;]&amp;#39;&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (c &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;/&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; (isDos &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; c &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\\&amp;#39;&lt;/span&gt;)) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; PatternSyntaxException(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Explicit &amp;#39;name separator&amp;#39; in class&amp;#34;&lt;/span&gt;,
globPattern, i &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt; 1);
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// TBD: how to specify &amp;#39;]&amp;#39; in a class?
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (c &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\\&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; c &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;[&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt;
c &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;&amp;amp;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; next(globPattern, i) &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;&amp;amp;&amp;#39;&lt;/span&gt;) {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// escape &amp;#39;\&amp;#39;, &amp;#39;[&amp;#39; or &amp;#34;&amp;amp;&amp;amp;&amp;#34; for regex class
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\\&amp;#39;&lt;/span&gt;);
}
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(c);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (c &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;-&amp;#39;&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;hasRangeStart) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; PatternSyntaxException(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Invalid range&amp;#34;&lt;/span&gt;,
globPattern, i &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt; 1);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; ((c &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; next(globPattern, i&lt;span style=&#34;color:#555&#34;&gt;++&lt;/span&gt;)) &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; EOL &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; c &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;]&amp;#39;&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (c &lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt; last) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; PatternSyntaxException(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Invalid range&amp;#34;&lt;/span&gt;,
globPattern, i &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt; 3);
}
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(c);
hasRangeStart &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
hasRangeStart &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;;
last &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; c;
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (c &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;]&amp;#39;&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; PatternSyntaxException(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Missing &amp;#39;]&amp;#34;&lt;/span&gt;, globPattern, i &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt; 1);
}
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;]]&amp;#34;&lt;/span&gt;);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;{&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (inGroup) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; PatternSyntaxException(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Cannot nest groups&amp;#34;&lt;/span&gt;,
globPattern, i &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt; 1);
}
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;(?:(?:&amp;#34;&lt;/span&gt;);
inGroup &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;}&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (inGroup) {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;))&amp;#34;&lt;/span&gt;);
inGroup &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;}&amp;#39;&lt;/span&gt;);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;,&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (inGroup) {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;)|(?:&amp;#34;&lt;/span&gt;);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;,&amp;#39;&lt;/span&gt;);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;*&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (next(globPattern, i) &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;*&amp;#39;&lt;/span&gt;) {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// crosses directory boundaries
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.*&amp;#34;&lt;/span&gt;);
i&lt;span style=&#34;color:#555&#34;&gt;++&lt;/span&gt;;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// within directory boundary
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (isDos) {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;[^\\\\]*&amp;#34;&lt;/span&gt;);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;[^/]*&amp;#34;&lt;/span&gt;);
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;?&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (isDos) {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;[^\\\\]&amp;#34;&lt;/span&gt;);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;[^/]&amp;#34;&lt;/span&gt;);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
&lt;span style=&#34;color:#99f&#34;&gt;
&lt;/span&gt;&lt;span style=&#34;color:#99f&#34;&gt; default:&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (isRegexMeta(c)) {
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\\&amp;#39;&lt;/span&gt;);
}
regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(c);
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (inGroup) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;throw&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; PatternSyntaxException(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Missing &amp;#39;}&amp;#34;&lt;/span&gt;, globPattern, i &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt; 1);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; regex.&lt;span style=&#34;color:#309&#34;&gt;append&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;$&amp;#39;&lt;/span&gt;).&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;();
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; toUnixRegexPattern(String &lt;span style=&#34;color:#c0f&#34;&gt;globPattern&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; toRegexPattern(globPattern, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; toWindowsRegexPattern(String &lt;span style=&#34;color:#c0f&#34;&gt;globPattern&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; toRegexPattern(globPattern, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;);
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://javapapers.com/java/glob-with-java-nio/&#34;&gt;Glob with Java NIO&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://docs.oracle.com/javase/tutorial/essential/io/find.html&#34;&gt;Finding Files&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://docs.oracle.com/javase/tutorial/essential/io/fileOps.html#glob&#34;&gt;What Is a Glob?&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Glob_(programming)&#34;&gt;glob (programming)&lt;/a&gt;&lt;/p&gt;</description></item><item><title>密码管理工具之 keeweb</title><link>https://busy.im/post/password-tool-keeweb/</link><pubDate>Thu, 18 Oct 2018 20:16:05 +0800</pubDate><guid>https://busy.im/post/password-tool-keeweb/</guid><description>
&lt;p&gt;&lt;a href=&#34;https://github.com/keeweb/keeweb&#34;&gt;keeweb&lt;/a&gt; 是一款优秀的免费开源密码管理软件，兼容 &lt;a href=&#34;https://keepass.info/&#34;&gt;KeePass&lt;/a&gt; 数据库，跨平台，也可以托管在任何静态网页服务上作为 web 应用使用。数据库文件可以方便的同步到 &lt;code&gt;Dropbox&lt;/code&gt; 和 &lt;code&gt;Google drive&lt;/code&gt; 。与知名商业类应用 &lt;a href=&#34;https://www.lastpass.com/&#34;&gt;LastPass&lt;/a&gt; 和 &lt;a href=&#34;https://1password.com/&#34;&gt;1Password&lt;/a&gt; 相比，有开源、免费、开放、界面简洁美观等优点。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/screenshot-keeweb.png&#34; alt=&#34;screenshot-keeweb&#34; /&gt;&lt;/p&gt;
&lt;p&gt;因为所有密码和代码由用户控制和存储，所以也不存在因服务商漏洞导致密码数据库泄漏的风险。博主也早已将所有密码托管到自建的 &lt;code&gt;keeweb&lt;/code&gt; 服务上 ( &lt;a href=&#34;https://www.xdty.org/kee/&#34;&gt;https://www.xdty.org/kee/&lt;/a&gt; )，可以在电脑桌面、手机等同步无缝切换，非常方便。&lt;/p&gt;
&lt;h2 id=&#34;特性&#34;&gt;特性&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;跨平台 - Windows, Linux, Mac&lt;/li&gt;
&lt;li&gt;离线网页应用 - &lt;a href=&#34;https://app.keeweb.info/&#34;&gt;https://app.keeweb.info/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;主题 - 暗色亮色主题&lt;/li&gt;
&lt;li&gt;颜色收藏 - 使用颜色标记项目来快速找到它们&lt;/li&gt;
&lt;li&gt;多文件支持 - 打开多个文件，在一个列表中搜索任何条目或查看所有文件中的所有项目&lt;/li&gt;
&lt;li&gt;拖放操作 - 打开密码文件或存储附件&lt;/li&gt;
&lt;li&gt;内嵌图片查看器 - 可以方便的存储银行卡照片&lt;/li&gt;
&lt;li&gt;Dropbox 同步&lt;/li&gt;
&lt;li&gt;轻松标签输入&lt;/li&gt;
&lt;li&gt;受保护的字段&lt;/li&gt;
&lt;li&gt;密码生成器&lt;/li&gt;
&lt;li&gt;离线访问&lt;/li&gt;
&lt;li&gt;快捷键&lt;/li&gt;
&lt;li&gt;高级搜索&lt;/li&gt;
&lt;li&gt;历史记录&lt;/li&gt;
&lt;li&gt;手机浏览器支持&lt;/li&gt;
&lt;li&gt;条目图标&lt;/li&gt;
&lt;li&gt;表视图&lt;/li&gt;
&lt;li&gt;开源免费&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;自托管&#34;&gt;自托管&lt;/h2&gt;
&lt;p&gt;这个应用是纯 HTML 应用，不需要任何后端服务，只需要下载 &lt;a href=&#34;https://github.com/keeweb/keeweb/archive/gh-pages.zip&#34;&gt;gh-pages&lt;/a&gt; 分支并托管在任何静态文件服务上即可。&lt;/p&gt;
&lt;p&gt;要在自托管的服务上开启 &lt;code&gt;Dropbox&lt;/code&gt; 支持，请阅读 &lt;a href=&#34;https://github.com/keeweb/keeweb/wiki/Dropbox-and-GDrive&#34;&gt;WIKI&lt;/a&gt; 文档。&lt;/p&gt;
&lt;h2 id=&#34;许可证&#34;&gt;许可证&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/keeweb/keeweb/blob/master/LICENSE&#34;&gt;MIT&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;开源项目地址&#34;&gt;开源项目地址&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/keeweb/keeweb&#34;&gt;https://github.com/keeweb/keeweb&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Cleaner 应用工程及架构设计</title><link>https://busy.im/post/app-cleaner-architecture/</link><pubDate>Tue, 09 Oct 2018 22:01:58 +0800</pubDate><guid>https://busy.im/post/app-cleaner-architecture/</guid><description>
&lt;p&gt;Cleaner 是一个新开发的工具类应用，主要集成了内存、通知、应用、大文件、冗余文件、相册清理等功能。同时提供对外的数据提供者接口和 &lt;code&gt;Deeplink&lt;/code&gt; 入口。&lt;/p&gt;
&lt;h2 id=&#34;功能模块&#34;&gt;功能模块&lt;/h2&gt;
&lt;p&gt;根据功能最终将工程设计为四个模块开发，功能模块之间互相独立且没有耦合。这四个模块独立生成 &lt;code&gt;aar&lt;/code&gt; 库文件被主应用引用，主应用只提供入口而不提供任何清理功能。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2018-10-09143804.png&#34; alt=&#34;模块依赖关系&#34; /&gt;&lt;/p&gt;
&lt;p&gt;工程结构如下图&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2018-10-09143855.png&#34; alt=&#34;工程结构&#34; /&gt;&lt;/p&gt;
&lt;p&gt;工程主要分为四个子模块：&lt;code&gt;storage&lt;/code&gt; &lt;code&gt;notification&lt;/code&gt; &lt;code&gt;provider&lt;/code&gt; 和 &lt;code&gt;utils&lt;/code&gt;。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&#34;left&#34;&gt;模块&lt;/th&gt;
&lt;th align=&#34;left&#34;&gt;功能&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&#34;left&#34;&gt;storage&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;实现文件类清理、内存清理&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&#34;left&#34;&gt;notification&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;实现通知清理、通知白名单及设置&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&#34;left&#34;&gt;provider&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;数据依赖 storage 和 notification，实现 &lt;code&gt;deeplink&lt;/code&gt; 和对外数据接口&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&#34;left&#34;&gt;utils&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;基础公共方法和基础常量&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&#34;left&#34;&gt;app&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;launcher 应用入口，不提供任何功能&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&#34;left&#34;&gt;example&lt;/td&gt;
&lt;td align=&#34;left&#34;&gt;provider 数据客户端示例&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;代码架构&#34;&gt;代码架构&lt;/h2&gt;
&lt;p&gt;功能模块采用 &lt;code&gt;MVP&lt;/code&gt; 架构。用户交互接口如 &lt;code&gt;Activity&lt;/code&gt; &lt;code&gt;Service&lt;/code&gt; 作为 &lt;code&gt;View&lt;/code&gt; 层抽象，逻辑及数据操作全部放在 &lt;code&gt;Presenter&lt;/code&gt; 中处理，数据操作封装在 &lt;code&gt;Model&lt;/code&gt; 层的 &lt;code&gt;DataSource&lt;/code&gt; 中。每一类的数据源都是一个独立不相关的 &lt;code&gt;DataSource&lt;/code&gt;，如数据库，应用列表，存储文件都放在不同的 &lt;code&gt;DataSource&lt;/code&gt; 中。&lt;/p&gt;
&lt;p&gt;下文以通知清理模块为例介绍代码架构。&lt;/p&gt;
&lt;h3 id=&#34;通知清理-mvp-架构&#34;&gt;通知清理 MVP 架构&lt;/h3&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2018-10-09152708.png&#34; alt=&#34;通知清理 MVP 架构&#34; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通知清理工程目录&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2018-10-09153050.png&#34; alt=&#34;通知清理工程目录&#34; /&gt;&lt;/p&gt;
&lt;h3 id=&#34;mvp-接口&#34;&gt;MVP 接口&lt;/h3&gt;
&lt;p&gt;通知主界面 &lt;code&gt;InboxGroupActivity&lt;/code&gt; 的 &lt;code&gt;MVP&lt;/code&gt; 接口如下&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;interface&lt;/span&gt; NotificationContract {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;View&lt;/span&gt; {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;loadData&lt;/span&gt;(List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Notification&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;notifications&lt;/span&gt;);
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;notifyWhitelistChanged&lt;/span&gt;();
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;notifyDataExported&lt;/span&gt;();
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;notifyDataImported&lt;/span&gt;();
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;notifyCleaned&lt;/span&gt;(&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;size&lt;/span&gt;);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Presenter&lt;/span&gt; {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;takeView&lt;/span&gt;(View &lt;span style=&#34;color:#c0f&#34;&gt;view&lt;/span&gt;);
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;loadHistoryNotifications&lt;/span&gt;();
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;start&lt;/span&gt;();
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;cleanAll&lt;/span&gt;();
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;filter&lt;/span&gt;(String &lt;span style=&#34;color:#c0f&#34;&gt;packageName&lt;/span&gt;);
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;exportHistory&lt;/span&gt;();
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;importHistory&lt;/span&gt;();
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;通知监听服务 &lt;code&gt;NotificationListenerService&lt;/code&gt; 的 &lt;code&gt;MVP&lt;/code&gt; 接口如下&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;interface&lt;/span&gt; ListenerContract {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;View&lt;/span&gt; {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;recheckActiveNotifications&lt;/span&gt;();
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;dispatchIntent&lt;/span&gt;(Notification &lt;span style=&#34;color:#c0f&#34;&gt;notification&lt;/span&gt;);
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;removeNotification&lt;/span&gt;(String &lt;span style=&#34;color:#c0f&#34;&gt;key&lt;/span&gt;);
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;showMissedNotification&lt;/span&gt;(List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Notification&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;notifications&lt;/span&gt;);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Presenter&lt;/span&gt; {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;start&lt;/span&gt;();
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;takeView&lt;/span&gt;(View &lt;span style=&#34;color:#c0f&#34;&gt;view&lt;/span&gt;);
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;addNotification&lt;/span&gt;(Notification &lt;span style=&#34;color:#c0f&#34;&gt;notification&lt;/span&gt;);
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;calculateMissedNotifications&lt;/span&gt;();
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;performNotificationAction&lt;/span&gt;(String &lt;span style=&#34;color:#c0f&#34;&gt;key&lt;/span&gt;);
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;presenter-数据依赖注入&#34;&gt;Presenter 数据依赖注入&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;DataSource&lt;/code&gt; 作为数据源依赖，需要在实例化时注入到 &lt;code&gt;Presenter&lt;/code&gt; 中，工程中没有使用 &lt;code&gt;Dagger&lt;/code&gt;，所以需要在&lt;code&gt;new Presenter&lt;/code&gt; 时手动注入依赖。注入依赖的代码片段如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// InboxGroupActivity Presenter 的数据依赖注入
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;EntityDataStore&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Persistable&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;dataStore&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DataRepository.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;provideDatabaseSource&lt;/span&gt;(
getApplicationContext());
DataRepository.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;init&lt;/span&gt;(dataStore);
AppStore.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;init&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;);
AppDataSource &lt;span style=&#34;color:#c0f&#34;&gt;appDataSource&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; AppDataRepository(AppStore.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;(), getPackageManager(),
dataStore);
mPresenter &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; NotificationPresenter(DataRepository.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;(), appDataSource,
AppStore.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;());
mPresenter.&lt;span style=&#34;color:#309&#34;&gt;takeView&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;);&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// NotificationListenerService Presenter 的数据依赖注入
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;EntityDataStore&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Persistable&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;dataStore&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DataRepository.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;provideDatabaseSource&lt;/span&gt;(
getApplicationContext());
DataRepository.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;init&lt;/span&gt;(dataStore);
Setting &lt;span style=&#34;color:#c0f&#34;&gt;setting&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; SettingImpl(DataRepository.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;());
AppStore.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;init&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;);
AppStore &lt;span style=&#34;color:#c0f&#34;&gt;appStore&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; AppStore.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;();
mPresenter &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ListenerPresenter(DataRepository.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;(), setting, appStore);&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// SettingsActivity Presenter 的数据依赖注入
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;AppStore.&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;get&lt;/span&gt;().init(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;)
mAppStore = AppStore.&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;get&lt;/span&gt;()
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; dataStore = DataRepository.&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;get&lt;/span&gt;().provideDatabaseSource(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;);
DataRepository.&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;get&lt;/span&gt;().init(dataStore)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; mAppDataSource = AppDataRepository(mAppStore, packageManager, dataStore)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; setting = SettingImpl(DataRepository.&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;get&lt;/span&gt;())
mPresenter = SettingsPresenter(mAppDataSource, setting)
mPresenter.takeView(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;)&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;单元测试&#34;&gt;单元测试&lt;/h3&gt;
&lt;p&gt;通知清理模块包含完整的单元测试，同时也集成了 &lt;code&gt;UI&lt;/code&gt; 测试。和白名单设置界面一样，为了熟练 &lt;code&gt;kotlin&lt;/code&gt;语言并节省代码，测试使用了 &lt;code&gt;kotlin&lt;/code&gt; 编写。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2018-10-09155327.png&#34; alt=&#34;通知清理模块单元测试&#34; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;通知监听服务的单元测试代码&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-kotlin&#34; data-lang=&#34;kotlin&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#0a8;font-weight:bold&#34;&gt;ListenerPresenterTest&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;lateinit&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;var&lt;/span&gt; mListenerPresenter: ListenerPresenter
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; mDataSource = mock&amp;lt;DataSource&amp;gt;()
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; mSetting = mock&amp;lt;Setting&amp;gt;()
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; mAppStore = mock&amp;lt;AppStore&amp;gt;()
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; mView = mock&amp;lt;ListenerContract.View&amp;gt;()
@Before
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;setup&lt;/span&gt;() {
doReturn(Single.just(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;)).`when`(mSetting).isFilterEnabled
doReturn(Single.just(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;)).`when`(mDataSource)
.isNotificationWhitelist(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.example.test&amp;#34;&lt;/span&gt;)
doReturn(Single.just(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;)).`when`(mDataSource)
.saveNotification(any())
doReturn(Single.just(ArrayList&amp;lt;Notification&amp;gt;())).`when`(mDataSource)
.notifications
doReturn(Completable.complete()).`when`(mAppStore)
.cache(any&amp;lt;List&amp;lt;Notification&amp;gt;&amp;gt;())
mListenerPresenter = ListenerPresenter(mDataSource, mSetting, mAppStore)
mListenerPresenter.takeView(mView)
}
@Test
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;start&lt;/span&gt;() {
mListenerPresenter.start()
verify(mSetting).isFilterEnabled
verify(mView).recheckActiveNotifications()
}
@Test
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;addNotification&lt;/span&gt;() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; notification = Notification()
notification.packageName = &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.example.test&amp;#34;&lt;/span&gt;
mListenerPresenter.addNotification(notification)
verify(mDataSource).saveNotification(eq(notification))
}
@Test
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;calculateMissedNotifications&lt;/span&gt;() {
mListenerPresenter.calculateMissedNotifications()
verify(mDataSource).notifications
verify(mView).showMissedNotification(any())
}
@Test
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;fun&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;performNotificationAction&lt;/span&gt;() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;val&lt;/span&gt; notification = Notification()
doReturn(Single.just(notification)).`when`(mDataSource)
.getNotification(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.example.test&amp;#34;&lt;/span&gt;)
mListenerPresenter.performNotificationAction(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.example.test&amp;#34;&lt;/span&gt;)
verify(mView).dispatchIntent(eq(notification))
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&#34;deeplink-入口&#34;&gt;DeepLink 入口&lt;/h2&gt;
&lt;p&gt;为了便于第三方应用拉起，将主要界面都做了 &lt;code&gt;DeepLink&lt;/code&gt; 支持，实现放在 &lt;code&gt;provider&lt;/code&gt; 模块。所有的界面注册都统一放在 &lt;code&gt;Router&lt;/code&gt; 中，同时支持带来源和任意参数拉起。&lt;/p&gt;
&lt;p&gt;例如可以使用 &lt;code&gt;com.qiku.android.cleaner:///storage?refer=com.example.app&amp;amp;customData=3&lt;/code&gt; 拉起主界面。&lt;/p&gt;
&lt;p&gt;可以通过手机浏览器打开 &lt;a href=&#34;https://cleaner01.netlify.com&#34;&gt;https://cleaner01.netlify.com&lt;/a&gt; 测试各个 &lt;code&gt;DeepLink&lt;/code&gt; 拉起。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;activity&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.ui.DeepLinkActivity&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:exported=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:noHistory=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:theme=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@android:style/Theme.NoDisplay&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;intent-filter&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;data&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:scheme=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.qiku.android.cleaner&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;action&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.intent.action.VIEW&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;category&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.intent.category.VIEW&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;category&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.intent.category.DEFAULT&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;category&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.intent.category.BROWSABLE&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/intent-filter&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/activity&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; DeepLinkActivity &lt;span style=&#34;color:#c0f&#34;&gt;extends&lt;/span&gt; Activity {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; TAG &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DeepLinkActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getSimpleName&lt;/span&gt;();
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onCreate(Bundle &lt;span style=&#34;color:#c0f&#34;&gt;savedInstanceState&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;super&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(savedInstanceState);
setContentView(R.&lt;span style=&#34;color:#309&#34;&gt;layout&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;activity_deep_link&lt;/span&gt;);
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; getIntent();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (intent &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;getData&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
Router &lt;span style=&#34;color:#c0f&#34;&gt;router&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Router.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;();
router.&lt;span style=&#34;color:#309&#34;&gt;open&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;, intent.&lt;span style=&#34;color:#309&#34;&gt;getData&lt;/span&gt;());
}
finish();
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Router&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; TAG &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Router.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getSimpleName&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; SCHEME &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Constants.&lt;span style=&#34;color:#309&#34;&gt;DEEP_LINK_SCHEME&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; HOST &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Constants.&lt;span style=&#34;color:#309&#34;&gt;DEEP_LINK_HOST&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Map&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;String, Class&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;PATHS&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; HashMap&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Router&lt;/span&gt;() {
register(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/storage&amp;#34;&lt;/span&gt;, CleanerActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
register(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/storage/app&amp;#34;&lt;/span&gt;, AppClearActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
register(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/storage/apk&amp;#34;&lt;/span&gt;, ApkClearActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
register(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/storage/file&amp;#34;&lt;/span&gt;, LargeFilesActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
register(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/storage/redundancy&amp;#34;&lt;/span&gt;, RedundancyActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
register(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/storage/photo&amp;#34;&lt;/span&gt;, PhotoSimilarEntryActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
register(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/storage/memory&amp;#34;&lt;/span&gt;, CleanerMemoryActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
register(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/storage/finish&amp;#34;&lt;/span&gt;, ClearFinishActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
register(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/notification&amp;#34;&lt;/span&gt;, InboxGroupActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
register(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/notification/history&amp;#34;&lt;/span&gt;, InboxActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
register(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/notification/prompt&amp;#34;&lt;/span&gt;, PromptActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
register(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;/notification/whitelist&amp;#34;&lt;/span&gt;, SettingsActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; register(String &lt;span style=&#34;color:#c0f&#34;&gt;path&lt;/span&gt;, Class &lt;span style=&#34;color:#c0f&#34;&gt;activity&lt;/span&gt;) {
PATHS.&lt;span style=&#34;color:#309&#34;&gt;put&lt;/span&gt;(path, activity);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; open(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, Uri &lt;span style=&#34;color:#c0f&#34;&gt;uri&lt;/span&gt;) {
Log.&lt;span style=&#34;color:#309&#34;&gt;d&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;open: &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; uri);
String &lt;span style=&#34;color:#c0f&#34;&gt;scheme&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; uri.&lt;span style=&#34;color:#309&#34;&gt;getScheme&lt;/span&gt;();
String &lt;span style=&#34;color:#c0f&#34;&gt;host&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; uri.&lt;span style=&#34;color:#309&#34;&gt;getHost&lt;/span&gt;();
String &lt;span style=&#34;color:#c0f&#34;&gt;path&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; uri.&lt;span style=&#34;color:#309&#34;&gt;getPath&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (scheme &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; host &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; path &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;scheme.&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(SCHEME) &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;host.&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(HOST) &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;PATHS.&lt;span style=&#34;color:#309&#34;&gt;containsKey&lt;/span&gt;(path)) {
Log.&lt;span style=&#34;color:#309&#34;&gt;e&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;illegal uri&amp;#34;&lt;/span&gt;);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt;;
}
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(context, PATHS.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;(path));
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (String &lt;span style=&#34;color:#c0f&#34;&gt;name&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; uri.&lt;span style=&#34;color:#309&#34;&gt;getQueryParameterNames&lt;/span&gt;()) {
intent.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(name, uri.&lt;span style=&#34;color:#309&#34;&gt;getQueryParameter&lt;/span&gt;(name));
}
context.&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(intent);
Log.&lt;span style=&#34;color:#309&#34;&gt;d&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;start activity: &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (intent.&lt;span style=&#34;color:#309&#34;&gt;getExtras&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
Log.&lt;span style=&#34;color:#309&#34;&gt;d&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;extras: &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;getExtras&lt;/span&gt;());
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; Router &lt;span style=&#34;color:#c0f&#34;&gt;get&lt;/span&gt;() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; SingletonHelper.&lt;span style=&#34;color:#309&#34;&gt;sINSTANCE&lt;/span&gt;;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; SingletonHelper {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Router&lt;/span&gt; sINSTANCE &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Router();
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&#34;cleanerprovider-数据提供者&#34;&gt;CleanerProvider 数据提供者&lt;/h2&gt;
&lt;p&gt;为了第三方应用快速查询清理状态如存储空间，内存占用等信息，cleaner 应用对当前的数据状态做了封装，作为 &lt;code&gt;provider&lt;/code&gt; 提供数据。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;CleanEvent 数据结构&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; CleanEvent {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; String &lt;span style=&#34;color:#c0f&#34;&gt;URL&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;content://com.qiku.android.cleaner/events&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; Uri &lt;span style=&#34;color:#c0f&#34;&gt;CONTENT_URI&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Uri.&lt;span style=&#34;color:#309&#34;&gt;parse&lt;/span&gt;(URL);
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;id&lt;/span&gt;;
&lt;span style=&#34;color:#99f&#34;&gt;@EventType&lt;/span&gt;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;type&lt;/span&gt;; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 功能模块类型
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; String &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt;; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 提示语数字大小
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;float&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;weight&lt;/span&gt;;
String &lt;span style=&#34;color:#c0f&#34;&gt;name&lt;/span&gt;; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 功能模块类型描述
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; String &lt;span style=&#34;color:#c0f&#34;&gt;text&lt;/span&gt;; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 按钮文字
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; String &lt;span style=&#34;color:#c0f&#34;&gt;prompt&lt;/span&gt;; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 提示语
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; String &lt;span style=&#34;color:#c0f&#34;&gt;appName&lt;/span&gt;; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 类型为 应用 时的应用名
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; String &lt;span style=&#34;color:#c0f&#34;&gt;link&lt;/span&gt;; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 调用页面
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;
String &lt;span style=&#34;color:#c0f&#34;&gt;promptText&lt;/span&gt;; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 提示语
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; String &lt;span style=&#34;color:#c0f&#34;&gt;value&lt;/span&gt;; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 数值
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; String &lt;span style=&#34;color:#c0f&#34;&gt;unit&lt;/span&gt;; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 单位
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;version&lt;/span&gt;; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 版本
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;
String &lt;span style=&#34;color:#c0f&#34;&gt;extra&lt;/span&gt;;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;fileCount&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 文件个数
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;long&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;totalLength&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 所有文件大小
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;@Retention&lt;/span&gt;(RetentionPolicy.&lt;span style=&#34;color:#309&#34;&gt;SOURCE&lt;/span&gt;)
&lt;span style=&#34;color:#99f&#34;&gt;@IntDef&lt;/span&gt;({STORAGE, MEMORY, LARGE_FILE, APPLICATION, APK, REDUNDANCY, PHOTO, NOTIFICATION})
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#99f&#34;&gt;@interface&lt;/span&gt; EventType {
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; ERROR &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;1;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; STORAGE &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; MEMORY &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 1;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; LARGE_FILE &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 2;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; APPLICATION &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 3;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; APK &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 4;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; REDUNDANCY &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 5;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; PHOTO &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 6;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; NOTIFICATION &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 7;
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; toString() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;CleanEvent{&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;id=&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; id &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, type=&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; type &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, data=&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; data &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, weight=&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; weight &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, name=&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; name &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, text=&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; text &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, prompt=&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; prompt &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, appName=&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; appName &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, link=&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; link &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, promptText=&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; promptText &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, value=&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; value &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, unit=&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; unit &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, version=&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; version &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, extra=&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; extra &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, fileCount=&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; fileCount &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, totalLength=&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; totalLength &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;}&amp;#39;&lt;/span&gt;;
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;客户端查询数据代码示例&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;List&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;CleanEvent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;getCleanEvents&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;) {
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;CleanEvent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;cleanEventList&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
Cursor &lt;span style=&#34;color:#c0f&#34;&gt;cursor&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; context.&lt;span style=&#34;color:#309&#34;&gt;getContentResolver&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;query&lt;/span&gt;(CleanEvent.&lt;span style=&#34;color:#309&#34;&gt;CONTENT_URI&lt;/span&gt;, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;,
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (cursor &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;cursor.&lt;span style=&#34;color:#309&#34;&gt;moveToFirst&lt;/span&gt;()) {
Log.&lt;span style=&#34;color:#309&#34;&gt;e&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;no data&amp;#34;&lt;/span&gt;);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;do&lt;/span&gt; {
CleanEvent &lt;span style=&#34;color:#c0f&#34;&gt;event&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; CleanEvent();
event.&lt;span style=&#34;color:#309&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; cursor.&lt;span style=&#34;color:#309&#34;&gt;getInt&lt;/span&gt;(cursor.&lt;span style=&#34;color:#309&#34;&gt;getColumnIndex&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;type&amp;#34;&lt;/span&gt;));
event.&lt;span style=&#34;color:#309&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; cursor.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(cursor.&lt;span style=&#34;color:#309&#34;&gt;getColumnIndex&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;data&amp;#34;&lt;/span&gt;));
event.&lt;span style=&#34;color:#309&#34;&gt;weight&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; cursor.&lt;span style=&#34;color:#309&#34;&gt;getFloat&lt;/span&gt;(cursor.&lt;span style=&#34;color:#309&#34;&gt;getColumnIndex&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;weight&amp;#34;&lt;/span&gt;));
event.&lt;span style=&#34;color:#309&#34;&gt;name&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; cursor.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(cursor.&lt;span style=&#34;color:#309&#34;&gt;getColumnIndex&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;name&amp;#34;&lt;/span&gt;));
event.&lt;span style=&#34;color:#309&#34;&gt;text&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; cursor.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(cursor.&lt;span style=&#34;color:#309&#34;&gt;getColumnIndex&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;text&amp;#34;&lt;/span&gt;));
event.&lt;span style=&#34;color:#309&#34;&gt;prompt&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; cursor.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(cursor.&lt;span style=&#34;color:#309&#34;&gt;getColumnIndex&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;prompt&amp;#34;&lt;/span&gt;));
event.&lt;span style=&#34;color:#309&#34;&gt;appName&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; cursor.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(cursor.&lt;span style=&#34;color:#309&#34;&gt;getColumnIndex&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;appName&amp;#34;&lt;/span&gt;));
event.&lt;span style=&#34;color:#309&#34;&gt;link&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; cursor.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(cursor.&lt;span style=&#34;color:#309&#34;&gt;getColumnIndex&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;link&amp;#34;&lt;/span&gt;));
event.&lt;span style=&#34;color:#309&#34;&gt;promptText&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; cursor.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(cursor.&lt;span style=&#34;color:#309&#34;&gt;getColumnIndex&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;promptText&amp;#34;&lt;/span&gt;));
event.&lt;span style=&#34;color:#309&#34;&gt;value&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; cursor.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(cursor.&lt;span style=&#34;color:#309&#34;&gt;getColumnIndex&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;value&amp;#34;&lt;/span&gt;));
event.&lt;span style=&#34;color:#309&#34;&gt;unit&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; cursor.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(cursor.&lt;span style=&#34;color:#309&#34;&gt;getColumnIndex&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;unit&amp;#34;&lt;/span&gt;));
event.&lt;span style=&#34;color:#309&#34;&gt;version&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; cursor.&lt;span style=&#34;color:#309&#34;&gt;getInt&lt;/span&gt;(cursor.&lt;span style=&#34;color:#309&#34;&gt;getColumnIndex&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;version&amp;#34;&lt;/span&gt;));
cleanEventList.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(event);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;while&lt;/span&gt; (cursor.&lt;span style=&#34;color:#309&#34;&gt;moveToNext&lt;/span&gt;());
}
cursor.&lt;span style=&#34;color:#309&#34;&gt;close&lt;/span&gt;();
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; cleanEventList;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>生产力工具之 StackEdit</title><link>https://busy.im/post/productivity-tool-stackedit/</link><pubDate>Tue, 09 Oct 2018 13:29:22 +0800</pubDate><guid>https://busy.im/post/productivity-tool-stackedit/</guid><description>
&lt;p&gt;&lt;a href=&#34;https://stackedit.io/&#34;&gt;StackEdit&lt;/a&gt; 是一款优秀的在线 &lt;code&gt;Markdown&lt;/code&gt; 文本编辑工具，可以方便的创建和管理文档，记录日记等。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/2018-10-09113203.png&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;StackEdit&lt;/code&gt; 开源免费，支持同步文档到 &lt;code&gt;Google drive&lt;/code&gt; &lt;code&gt;Gitlab&lt;/code&gt; &lt;code&gt;Github&lt;/code&gt; &lt;code&gt;Dropbox&lt;/code&gt;，支持实时预览，支持目录结构。&lt;/p&gt;
&lt;h3 id=&#34;stackedit-可以&#34;&gt;StackEdit 可以&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;在线或离线管理多个 Markdown 文件&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;以 Markdown，HTML，PDF，Word，EPUB 导出文件&amp;hellip;&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;在云中同步 Markdown 文件&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;从 Google云端硬盘，Dropbox 和本地硬盘驱动器编辑现有的 Markdown 文件&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;在 Blogger / Blogspot，WordPress，Zendesk上发布您的 Markdown 文件&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;在 GitHub，Gist，Google Drive，Dropbox 上发布您的 Markdown 文件&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;通过 Google Drive，CouchDB 共享工作区&lt;br /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;功能&#34;&gt;功能&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;使用滚动同步功能进行实时 &lt;code&gt;HTML&lt;/code&gt; 预览以绑定编辑器和预览滚动条&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;Markdown Extra / GitHub Flavored Markdown&lt;/code&gt; 支持和 &lt;code&gt;Prism.js&lt;/code&gt; 语法高亮显示&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;使用 &lt;code&gt;KaTeX&lt;/code&gt; 的 &lt;code&gt;LaTeX&lt;/code&gt; 数学表达式&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;使用 Mermaid 的图表和流程图&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;所见即所得控制按钮&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;智能布局&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;离线编辑&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;使用 Google Drive，Dropbox 和 GitHub 进行在线同步&lt;br /&gt;&lt;/li&gt;
&lt;li&gt;只需点击一下即可发布到 Blogger，Dropbox，Gist，GitHub，Google Drive，WordPress，Zendesk&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;开源项目地址&#34;&gt;开源项目地址&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/benweet/stackedit&#34;&gt;https://github.com/benweet/stackedit&lt;/a&gt;&lt;/p&gt;</description></item><item><title>如何使用 Dagger 编写测试</title><link>https://busy.im/post/testing-with-dagger/</link><pubDate>Fri, 05 Oct 2018 14:44:12 +0800</pubDate><guid>https://busy.im/post/testing-with-dagger/</guid><description>
&lt;p&gt;使用像 &lt;code&gt;Dagger&lt;/code&gt; 这样的依赖注入框架的一个好处是可以让你更容易编写测试代码。本文档探讨了测试使用 &lt;code&gt;Dagger&lt;/code&gt; 构建的应用程序的一些策略。&lt;/p&gt;
&lt;h2 id=&#34;替换功能-集成-端到端测试的绑定&#34;&gt;替换功能/集成/端到端测试的绑定&lt;/h2&gt;
&lt;p&gt;功能、集成、端到端测试通常使用生产应用程序，但是使用&lt;a href=&#34;http://googletesting.blogspot.com/2013/07/testing-on-toilet-know-your-test-doubles.html&#34;&gt;伪造的对象&lt;/a&gt;(不要在大型​​功能测试中使用模拟！) 来替代持久化、后端或认证系统，让应用程序的其余部分正常运行。&lt;/p&gt;
&lt;h3 id=&#34;方法1-通过子类化模块-modules-替换绑定-不要这样做&#34;&gt;方法1：通过子类化模块(modules) 替换绑定 (不要这样做！)&lt;/h3&gt;
&lt;p&gt;替换测试组件中的绑定的最简单方法是覆盖子类中的模块的 &lt;code&gt;@Provides&lt;/code&gt; 方法。 （但请看下面的问题。）&lt;/p&gt;
&lt;p&gt;创建 &lt;code&gt;Dagger&lt;/code&gt; 组件的实例时，会传入它使用的模块的实例。（你不必为没有arg构造函数的模块或没有实例方法的模块传递实例，但你可以。）这意味着你可以传递这些模块的子类的实例，并且这些子类可以覆盖某些 &lt;code&gt;@Provides&lt;/code&gt; 方法来替换某些绑定。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Component&lt;/span&gt;(modules &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; {AuthModule.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;, &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/* … */&lt;/span&gt;})
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;MyApplicationComponent&lt;/span&gt; { &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/* … */&lt;/span&gt; }
&lt;span style=&#34;color:#99f&#34;&gt;@Module&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;AuthModule&lt;/span&gt; {
&lt;span style=&#34;color:#99f&#34;&gt;@Provides&lt;/span&gt; AuthManager &lt;span style=&#34;color:#c0f&#34;&gt;authManager&lt;/span&gt;(AuthManagerImpl &lt;span style=&#34;color:#c0f&#34;&gt;impl&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; impl;
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;FakeAuthModule&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;extends&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;AuthModule&lt;/span&gt; {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
AuthManager &lt;span style=&#34;color:#c0f&#34;&gt;authManager&lt;/span&gt;(AuthManagerImpl &lt;span style=&#34;color:#c0f&#34;&gt;impl&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; FakeAuthManager();
}
}
MyApplicationComponent &lt;span style=&#34;color:#c0f&#34;&gt;testingComponent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DaggerMyApplicationComponent.&lt;span style=&#34;color:#309&#34;&gt;builder&lt;/span&gt;()
.&lt;span style=&#34;color:#309&#34;&gt;authModule&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; FakeAuthModule())
.&lt;span style=&#34;color:#309&#34;&gt;build&lt;/span&gt;();&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;但这种方法存在局限性：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用模块子类不能更改绑定图的静态形状：它不能添加或删除绑定，也不能更改绑定的依赖关系。特别的：
&lt;ul&gt;
&lt;li&gt;覆盖 &lt;code&gt;@Provides&lt;/code&gt; 方法不能更改其参数类型，并且缩小返回类型对绑定图没有影响，因为 &lt;code&gt;Dagger&lt;/code&gt; 理解它。在上面的示例中，&lt;code&gt;testingComponent&lt;/code&gt; 仍然需要绑定 &lt;code&gt;AuthManagerImpl&lt;/code&gt; 及其所有依赖项，即使它们未被使用。&lt;/li&gt;
&lt;li&gt;同样，覆盖模块无法向图形添加绑定，包括新的 &lt;code&gt;multibinding&lt;/code&gt;（虽然你仍可以覆盖一个 &lt;code&gt;SET_VALUES&lt;/code&gt; 方法来返回一个不同的集合）。&lt;code&gt;Dagger&lt;/code&gt; 会默默忽略子类中的任何新 &lt;code&gt;@Provides&lt;/code&gt; 方法。实际上，这意味着你的伪造对象无法利用依赖注入。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;&lt;code&gt;@Provides&lt;/code&gt; 以这种方式可覆盖的方法不能是静态的，因此不能省略它们的模块实例。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;方法2-单独的组件配置&#34;&gt;方法2：单独的组件配置&lt;/h3&gt;
&lt;p&gt;另一种方法需要在应用程序中更多地预先设计模块，应用程序的每个配置（生产和测试）都使用不同的组件配置。测试组件类型继承了生产组件类型并安装了一组不同的模块。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Component&lt;/span&gt;(modules &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; {
OAuthModule.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;, &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// real auth
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; FooServiceModule.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;, &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// real backend
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; OtherApplicationModule.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;,
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/* … */&lt;/span&gt; })
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ProductionComponent&lt;/span&gt; {
Server &lt;span style=&#34;color:#c0f&#34;&gt;server&lt;/span&gt;();
}
&lt;span style=&#34;color:#99f&#34;&gt;@Component&lt;/span&gt;(modules &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; {
FakeAuthModule.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;, &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// fake auth
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; FakeFooServiceModule.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;, &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// fake backend
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; OtherApplicationModule.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;,
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/* … */&lt;/span&gt;})
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;interface&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;TestComponent&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;extends&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ProductionComponent&lt;/span&gt; {
FakeAuthManager &lt;span style=&#34;color:#c0f&#34;&gt;fakeAuthManager&lt;/span&gt;();
FakeFooService &lt;span style=&#34;color:#c0f&#34;&gt;fakeFooService&lt;/span&gt;();
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;现在，测试的主要方法是调用&lt;code&gt;DaggerTestComponent.builder()&lt;/code&gt; 而不是&lt;code&gt;DaggerProductionComponent.builder()&lt;/code&gt;。请注意，测试组件接口可以向伪实例（&lt;code&gt;fakeAuthManager()&lt;/code&gt;和&lt;code&gt;fakeFooService()&lt;/code&gt;）添加额外方法，以便测试代码可以在必要时访问它们来实现控制。&lt;/p&gt;
&lt;p&gt;但是，如何设计模块以简化这种模式？&lt;/p&gt;
&lt;h2 id=&#34;组织模块以实现可测试性&#34;&gt;组织模块以实现可测试性&lt;/h2&gt;
&lt;p&gt;模块类是一种实用类：一个独立的 &lt;code&gt;@Providers&lt;/code&gt; 方法的收集器，每一个模块类都可能被注射器使用来提供某种类型给应用程序。&lt;/p&gt;
&lt;p&gt;虽然几个 &lt;code&gt;@Provides&lt;/code&gt; 方法可能相关，因为它们依赖于另一个提供的类型，但它们通常不会明确地相互调用或依赖于相同的可变状态。一些 &lt;code&gt;@Provides&lt;/code&gt; 方法确实引用相同的实例字段，在这种情况下它们实际上不是独立的。这里给出的建议是将 &lt;code&gt;@Provides&lt;/code&gt; 方法视为实用方法，这样模块就可以很容易地被替代从而方便测试。&lt;/p&gt;
&lt;p&gt;那么如何确定哪些 &lt;code&gt;@Provides&lt;/code&gt; 方法应该合并到一个模块类？&lt;/p&gt;
&lt;p&gt;考虑它的一种方法是将绑定分类为已发布的绑定和内部绑定，然后进一步确定哪些已发布的绑定具有合理的替代方案。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;已发布&lt;/strong&gt;的绑定是提供应用程序其他部分使用的功能的绑定。例如 &lt;code&gt;AuthManager&lt;/code&gt; 或 &lt;code&gt;User&lt;/code&gt; 或 &lt;code&gt;DocDatabase&lt;/code&gt; 等类型是已发布类：它们绑定在一个模块中，以便应用程序的其余部分可以使用它们。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;内部&lt;/strong&gt;绑定是除已发布之外的绑定：在某些已发布类型的实现中使用的绑定，并且除此之外不被其他地方使用。例如，&lt;code&gt;OAuth&lt;/code&gt;客户端 &lt;code&gt;ID&lt;/code&gt; 配置的绑定或仅供&lt;code&gt;AuthManager&lt;/code&gt; 的 &lt;code&gt;OAuth&lt;/code&gt; 实现使用而不被程序其他部分使用的 &lt;code&gt;OAuthKeyStore&lt;/code&gt;。这些绑定通常用于包私有类型，或者使用包私有限定符进行限定。&lt;/p&gt;
&lt;p&gt;一些已发布的绑定将具有合理的替代方案，尤其是用于测试，而其他绑定则不会。例如，对于类似&lt;code&gt;AuthManager&lt;/code&gt; 的类型，可能存在替代绑定：一个用于测试，另一个用于不同的身份验证/授权协议。&lt;/p&gt;
&lt;p&gt;但另一方面，如果 &lt;code&gt;AuthManager&lt;/code&gt; 接口有一个返回当前登录用户的方法，您可能希望通过在 &lt;code&gt;AuthManager&lt;/code&gt; 上调用 &lt;code&gt;getCurrentUser()&lt;/code&gt; 来发布为User提供的绑定。这种发布的绑定不太可能需要替代方案。&lt;/p&gt;
&lt;p&gt;一旦您将绑定分类为具有合理替代方案的已发布绑定，没有合理替代方案的发布绑定和内部绑定，考虑将它们安排到这样的模块中：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;每个需要合理替代方案的已发布绑定作为一个模块。（如果你也在编写备选方案，则每个方案都有自己的模块。）该模块只包含一个已发布的绑定，以及发布绑定所需的所有内部绑定。&lt;/li&gt;
&lt;li&gt;所有不需要合理替代方案的已发布绑定都会进入沿功能线组织的模块。&lt;/li&gt;
&lt;li&gt;已发布的绑定模块应各自包含需要公共绑定的无需替代方案模块。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过描述该模块提供了哪些已发布绑定来记录每个模块是个好主意。&lt;/p&gt;
&lt;p&gt;下面是使用认证域的示例。如果存在 &lt;code&gt;AuthManager&lt;/code&gt; 接口，则可能具有 &lt;code&gt;OAuth&lt;/code&gt; 实现和用于测试的虚假实现。如上所述，你不希望在配置之间进行更改当前用户可能存在的明显的绑定。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/**
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * Provides auth bindings that will not change in different auth configurations,
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * such as the current user.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; */&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;@Module&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;AuthModule&lt;/span&gt; {
&lt;span style=&#34;color:#99f&#34;&gt;@Provides&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;User&lt;/span&gt; currentUser(AuthManager &lt;span style=&#34;color:#c0f&#34;&gt;authManager&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; authManager.&lt;span style=&#34;color:#309&#34;&gt;currentUser&lt;/span&gt;();
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Other bindings that don’t differ among AuthManager implementations.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/** Provides a {@link AuthManager} that uses OAuth. */&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;@Module&lt;/span&gt;(includes &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; AuthModule.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Include no-alternative bindings.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;OAuthModule&lt;/span&gt; {
&lt;span style=&#34;color:#99f&#34;&gt;@Provides&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;AuthManager&lt;/span&gt; authManager(OAuthManager &lt;span style=&#34;color:#c0f&#34;&gt;authManager&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; authManager;
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Other bindings used only by OAuthManager.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/** Provides a fake {@link AuthManager} for testing. */&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;@Module&lt;/span&gt;(includes &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; AuthModule.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Include no-alternative bindings.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;FakeAuthModule&lt;/span&gt; {
&lt;span style=&#34;color:#99f&#34;&gt;@Provides&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;AuthManager&lt;/span&gt; authManager(FakeAuthManager &lt;span style=&#34;color:#c0f&#34;&gt;authManager&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; authManager;
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Other bindings used only by FakeAuthManager.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;然后您的生产配置将使用真实模块，测试配置假模块，如上所述。&lt;/p&gt;
&lt;h2 id=&#34;你不必使用-dagger-进行单类单元测试&#34;&gt;你不必使用 &lt;code&gt;Dagger&lt;/code&gt; 进行单类单元测试&lt;/h2&gt;
&lt;p&gt;如果要编写仅测试一个被 &lt;code&gt;@Inject&lt;/code&gt; 注解的类的小型单元测试，则不需要使用 &lt;code&gt;Dagger&lt;/code&gt; 来实例化该类。如果你想编写传统的单元测试，可以直接调用被 &lt;code&gt;@Inject&lt;/code&gt; 注解的构造函数和方法并设置被 &lt;code&gt;@Inject&lt;/code&gt; 注解的字段。如果有需要的话，直接传递假的或模拟的依赖，就像它们没有被注解一样。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; ThingDoer {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; ThingGetter &lt;span style=&#34;color:#c0f&#34;&gt;getter&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; ThingPutter &lt;span style=&#34;color:#c0f&#34;&gt;putter&lt;/span&gt;;
&lt;span style=&#34;color:#99f&#34;&gt;@Inject&lt;/span&gt; ThingDoer(ThingGetter &lt;span style=&#34;color:#c0f&#34;&gt;getter&lt;/span&gt;, ThingPutter &lt;span style=&#34;color:#c0f&#34;&gt;putter&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getter&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; getter;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;putter&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; putter;
}
String &lt;span style=&#34;color:#c0f&#34;&gt;doTheThing&lt;/span&gt;(&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;howManyTimes&lt;/span&gt;) { &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/* … */&lt;/span&gt; }
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; ThingDoerTest {
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; testDoTheThing() {
ThingDoer &lt;span style=&#34;color:#c0f&#34;&gt;doer&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ThingDoer(fakeGetter, fakePutter);
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;done&amp;#34;&lt;/span&gt;, doer.&lt;span style=&#34;color:#309&#34;&gt;doTheThing&lt;/span&gt;(5));
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2 id=&#34;参考&#34;&gt;参考&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://google.github.io/dagger/testing&#34;&gt;Testing with Dagger&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Android 四大组件单元测试之 Receiver</title><link>https://busy.im/post/android-receiver-testing/</link><pubDate>Fri, 05 Oct 2018 14:40:35 +0800</pubDate><guid>https://busy.im/post/android-receiver-testing/</guid><description>
&lt;h2 id=&#34;测试工具&#34;&gt;测试工具&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Junit4&lt;/code&gt; &lt;code&gt;Mockito&lt;/code&gt; &lt;code&gt;Robolectric&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;本文假定读者已经配置好了库及环境。&lt;/p&gt;
&lt;h2 id=&#34;测试内容&#34;&gt;测试内容&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Receiver&lt;/code&gt; 测试相对比较简单，很容易得到 100% 的覆盖率。测试主要覆盖如下场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;可以被 Manifest 中 Intent-filter 正确拉起&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onReceiver(Context, Intent)&lt;/code&gt; 中 &lt;code&gt;intent&lt;/code&gt; 输入&lt;/li&gt;
&lt;li&gt;其他内部逻辑方法&lt;/li&gt;
&lt;li&gt;如果有线程且其中有逻辑代码，分拆成独立方法测试&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;验证条件&#34;&gt;验证条件&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;普通方法验证输入输出&lt;/li&gt;
&lt;li&gt;&lt;code&gt;onReceive()&lt;/code&gt; 可以验证 &lt;code&gt;Context.startService(Intent)&lt;/code&gt; &lt;code&gt;Context.startActivity(Intent)&lt;/code&gt; 方法是否被执行，且捕获 &lt;code&gt;Intent&lt;/code&gt; 参数，验证参数是否与预期一致&lt;/li&gt;
&lt;li&gt;有其他操作如写入数据库等，可以验证这些操作被执行，且参数一致&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过分析验证条件，我们可以总结出需要 &lt;code&gt;mock&lt;/code&gt; 或 &lt;code&gt;spy&lt;/code&gt; 的对象要有 &lt;code&gt;Context&lt;/code&gt; 和 &lt;code&gt;Receiver&lt;/code&gt; 本身。&lt;/p&gt;
&lt;h2 id=&#34;代码实例&#34;&gt;代码实例&lt;/h2&gt;
&lt;p&gt;下面代码以日历中找的一个最简单的 &lt;code&gt;LanguageChangeReceiver&lt;/code&gt; 为例一步一步完成测试覆盖：&lt;/p&gt;
&lt;h3 id=&#34;receiver-代码&#34;&gt;Receiver 代码&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;receiver&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.receiver.LanguageChangeReceiver&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:exported=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;false&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;intent-filter&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;action&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.intent.action.LOCALE_CHANGED&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/intent-filter&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/receiver&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; LanguageChangeReceiver &lt;span style=&#34;color:#c0f&#34;&gt;extends&lt;/span&gt; BroadcastReceiver {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onReceive(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (intent &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;getAction&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt;;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (intent.&lt;span style=&#34;color:#309&#34;&gt;getAction&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;ACTION_LOCALE_CHANGED&lt;/span&gt;)) {
loadFestXmlData(context);
}
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;loadFestXmlData&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Context&lt;/span&gt; context) {
IoThread.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;post&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Runnable() {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; run() {
FestXmlUtils.&lt;span style=&#34;color:#309&#34;&gt;getInstance&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;reloadData&lt;/span&gt;(context);
}
});
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;代码分析&#34;&gt;代码分析&lt;/h3&gt;
&lt;p&gt;从代码可以看出这个 &lt;code&gt;receiver&lt;/code&gt; 只监听了一个 &lt;code&gt;LOCALE_CHANGED&lt;/code&gt; 的事件，我们首先需要对能否接收到这个事件做校验；再看 &lt;code&gt;onReceive(Context context, Intent intent)&lt;/code&gt; 方法，可以看到需要注入 &lt;code&gt;Context&lt;/code&gt; 和 &lt;code&gt;Intent&lt;/code&gt; 依赖，同时需要对 &lt;code&gt;intent&lt;/code&gt; 的数据内容做分支测试。&lt;/p&gt;
&lt;h3 id=&#34;测试代码&#34;&gt;测试代码&lt;/h3&gt;
&lt;p&gt;首先我们需要准备上下文环境，并准备 &lt;code&gt;Context&lt;/code&gt; 和 &lt;code&gt;LanguageChangeReceiver&lt;/code&gt; 对象。可以从 &lt;code&gt;ShadowApplication&lt;/code&gt; 对象中拿到 &lt;code&gt;receiver&lt;/code&gt; 对象，同时使用 &lt;code&gt;spy()&lt;/code&gt; 对 &lt;code&gt;receiver&lt;/code&gt; 和 &lt;code&gt;context&lt;/code&gt; 对象监听，这样我们就可以校验 &lt;code&gt;context&lt;/code&gt; 和 &lt;code&gt;receiver&lt;/code&gt; 中的方法是否被执行。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@RunWith&lt;/span&gt;(RobolectricTestRunner.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; LanguageChangeReceiverTest {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Context&lt;/span&gt; mContext;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;LanguageChangeReceiver&lt;/span&gt; receiver;
&lt;span style=&#34;color:#99f&#34;&gt;@Before&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; setup() {
ShadowApplication &lt;span style=&#34;color:#c0f&#34;&gt;shadowApplication&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Shadows.&lt;span style=&#34;color:#309&#34;&gt;shadowOf&lt;/span&gt;(RuntimeEnvironment.&lt;span style=&#34;color:#309&#34;&gt;application&lt;/span&gt;);
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;ShadowApplication.&lt;span style=&#34;color:#309&#34;&gt;Wrapper&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;receivers&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; shadowApplication.&lt;span style=&#34;color:#309&#34;&gt;getRegisteredReceivers&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (ShadowApplication.&lt;span style=&#34;color:#309&#34;&gt;Wrapper&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;wrapper&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; receivers) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (wrapper.&lt;span style=&#34;color:#309&#34;&gt;getBroadcastReceiver&lt;/span&gt;() &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;instanceof&lt;/span&gt; LanguageChangeReceiver) {
receiver &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; spy((LanguageChangeReceiver) wrapper.&lt;span style=&#34;color:#309&#34;&gt;getBroadcastReceiver&lt;/span&gt;());
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
}
}
mContext &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; spy(RuntimeEnvironment.&lt;span style=&#34;color:#309&#34;&gt;application&lt;/span&gt;);
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;1-测试-intent-filter&#34;&gt;1. 测试 &lt;code&gt;intent-filter&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;我们首先要测试 &lt;code&gt;receiver&lt;/code&gt; 是否能收到期望的事件，需要通过 &lt;code&gt;PackageManager.queryBroadcastReceivers()&lt;/code&gt; 方法拉起期望过滤到的 &lt;code&gt;intent&lt;/code&gt;，然后断言被拉起的广播中有当前的 &lt;code&gt;LanguageChangeReceiver&lt;/code&gt;：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Test if LanguageChangeReceiver could receive defined intent action
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; testIntentHandling() {
ShadowApplication &lt;span style=&#34;color:#c0f&#34;&gt;shadowApplication&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ShadowApplication.&lt;span style=&#34;color:#309&#34;&gt;getInstance&lt;/span&gt;();
PackageManager &lt;span style=&#34;color:#c0f&#34;&gt;packageManager&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
shadowApplication.&lt;span style=&#34;color:#309&#34;&gt;getApplicationContext&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;getPackageManager&lt;/span&gt;();
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;String&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;stringList&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;String&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt;(
Arrays.&lt;span style=&#34;color:#309&#34;&gt;asList&lt;/span&gt;(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.intent.action.LOCALE_CHANGED&amp;#34;&lt;/span&gt;));
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (String &lt;span style=&#34;color:#c0f&#34;&gt;action&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; stringList) {
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(action);
Boolean &lt;span style=&#34;color:#c0f&#34;&gt;hasReceiver&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (ResolveInfo &lt;span style=&#34;color:#c0f&#34;&gt;resolveInfo&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; packageManager.&lt;span style=&#34;color:#309&#34;&gt;queryBroadcastReceivers&lt;/span&gt;(intent, 0)) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (LanguageChangeReceiver.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getCanonicalName&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(
resolveInfo.&lt;span style=&#34;color:#309&#34;&gt;activityInfo&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;name&lt;/span&gt;)) {
hasReceiver &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
}
}
Assert.&lt;span style=&#34;color:#309&#34;&gt;assertTrue&lt;/span&gt;(hasReceiver);
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;2-测试空-intent&#34;&gt;2. 测试空 Intent&lt;/h4&gt;
&lt;p&gt;这是一个边界条件，我们需要测试传入的 &lt;code&gt;Intent&lt;/code&gt; 为空时不会出现异常且没有执行其他非预期操作。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onReceive_withNoIntentAction() {
receiver.&lt;span style=&#34;color:#309&#34;&gt;onReceive&lt;/span&gt;(mContext, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
verify(receiver, never()).&lt;span style=&#34;color:#309&#34;&gt;loadFestXmlData&lt;/span&gt;(any(Context.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;));
receiver.&lt;span style=&#34;color:#309&#34;&gt;onReceive&lt;/span&gt;(mContext, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent());
verify(receiver, never()).&lt;span style=&#34;color:#309&#34;&gt;loadFestXmlData&lt;/span&gt;(any(Context.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;));
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;3-测试预期的-intent-行为&#34;&gt;3. 测试预期的 Intent 行为&lt;/h4&gt;
&lt;p&gt;这个是我们的正常业务逻辑，我们需要传入正确的 &lt;code&gt;Intent&lt;/code&gt; 并验证预期的方法被执行：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onReceive_withAction() {
receiver.&lt;span style=&#34;color:#309&#34;&gt;onReceive&lt;/span&gt;(mContext, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(Intent.&lt;span style=&#34;color:#309&#34;&gt;ACTION_LOCALE_CHANGED&lt;/span&gt;));
verify(receiver).&lt;span style=&#34;color:#309&#34;&gt;loadFestXmlData&lt;/span&gt;(eq(mContext));
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;总结&#34;&gt;总结&lt;/h3&gt;
&lt;p&gt;通过上面三个测试已经完整覆盖了 &lt;code&gt;LanguageChangeReceiver&lt;/code&gt; 所有业务逻辑和分支。测试代码非常简单，更多的代码是在准备上下文环境。如果 &lt;code&gt;Receiver&lt;/code&gt; 中含有大量业务逻辑是需要详细的测试覆盖的，这些测试内容和普通的 &lt;code&gt;mockito&lt;/code&gt; 单元测试没有区别。&lt;/p&gt;
&lt;p&gt;最后附一个业务逻辑较为复杂的 &lt;code&gt;MobileCalendarBackupRecoverReceiver&lt;/code&gt; 测试代码供参考，测试的方法是一样的，只是含有更多的逻辑分支所有测试内容较多。&lt;/p&gt;
&lt;h3 id=&#34;附&#34;&gt;附&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;MobileCalendarBackupRecoverReceiver&lt;/code&gt; 测试实例，包含测试代码和 &lt;code&gt;Receiver&lt;/code&gt; 代码：&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;MobileCalendarBackupRecoverReceiverTest.java&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@RunWith&lt;/span&gt;(RobolectricTestRunner.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; MobileCalendarBackupRecoverReceiverTest {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Context&lt;/span&gt; mContext;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;MobileCalendarBackupRecoverReceiver&lt;/span&gt; receiver;
&lt;span style=&#34;color:#99f&#34;&gt;@Before&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; setup() {
ShadowApplication &lt;span style=&#34;color:#c0f&#34;&gt;shadowApplication&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Shadows.&lt;span style=&#34;color:#309&#34;&gt;shadowOf&lt;/span&gt;(RuntimeEnvironment.&lt;span style=&#34;color:#309&#34;&gt;application&lt;/span&gt;);
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;ShadowApplication.&lt;span style=&#34;color:#309&#34;&gt;Wrapper&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;receivers&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; shadowApplication.&lt;span style=&#34;color:#309&#34;&gt;getRegisteredReceivers&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (ShadowApplication.&lt;span style=&#34;color:#309&#34;&gt;Wrapper&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;wrapper&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; receivers) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (wrapper.&lt;span style=&#34;color:#309&#34;&gt;getBroadcastReceiver&lt;/span&gt;() &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;instanceof&lt;/span&gt; MobileCalendarBackupRecoverReceiver) {
receiver &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; spy(
(MobileCalendarBackupRecoverReceiver) wrapper.&lt;span style=&#34;color:#309&#34;&gt;getBroadcastReceiver&lt;/span&gt;());
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
}
}
mContext &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; spy(RuntimeEnvironment.&lt;span style=&#34;color:#309&#34;&gt;application&lt;/span&gt;);
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; testIntentHandling() {
ShadowApplication &lt;span style=&#34;color:#c0f&#34;&gt;shadowApplication&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ShadowApplication.&lt;span style=&#34;color:#309&#34;&gt;getInstance&lt;/span&gt;();
PackageManager &lt;span style=&#34;color:#c0f&#34;&gt;packageManager&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
shadowApplication.&lt;span style=&#34;color:#309&#34;&gt;getApplicationContext&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;getPackageManager&lt;/span&gt;();
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;String&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;stringList&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;String&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt;(
Arrays.&lt;span style=&#34;color:#309&#34;&gt;asList&lt;/span&gt;(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.qiku.android.mobile.backup.action.BACKUPCALENDAR&amp;#34;&lt;/span&gt;,
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.android.providers.calendar.spacesize&amp;#34;&lt;/span&gt;,
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.qiku.android.mobile.backup.action.CALENDARSIZE&amp;#34;&lt;/span&gt;));
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (String &lt;span style=&#34;color:#c0f&#34;&gt;action&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; stringList) {
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(action);
Boolean &lt;span style=&#34;color:#c0f&#34;&gt;hasReceiver&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (ResolveInfo &lt;span style=&#34;color:#c0f&#34;&gt;resolveInfo&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; packageManager.&lt;span style=&#34;color:#309&#34;&gt;queryBroadcastReceivers&lt;/span&gt;(intent, 0)) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getCanonicalName&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(
resolveInfo.&lt;span style=&#34;color:#309&#34;&gt;activityInfo&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;name&lt;/span&gt;)) {
hasReceiver &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
}
}
Assert.&lt;span style=&#34;color:#309&#34;&gt;assertTrue&lt;/span&gt;(hasReceiver);
}
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onReceive_withoutIntentData() {
receiver.&lt;span style=&#34;color:#309&#34;&gt;onReceive&lt;/span&gt;(mContext, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
verify(receiver, never()).&lt;span style=&#34;color:#309&#34;&gt;getPayload&lt;/span&gt;(any(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;));
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onReceive_withIntentData_querySize() {
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent();
intent.&lt;span style=&#34;color:#309&#34;&gt;setAction&lt;/span&gt;(IntentUtil.&lt;span style=&#34;color:#309&#34;&gt;MOBILE_ACTION_QUERY_SIZE&lt;/span&gt;);
receiver.&lt;span style=&#34;color:#309&#34;&gt;onReceive&lt;/span&gt;(mContext, intent);
verify(receiver).&lt;span style=&#34;color:#309&#34;&gt;getPayload&lt;/span&gt;(any(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;));
ArgumentCaptor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style=&#34;color:#309&#34;&gt;forClass&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
verify(mContext).&lt;span style=&#34;color:#309&#34;&gt;sendBroadcast&lt;/span&gt;(argument.&lt;span style=&#34;color:#309&#34;&gt;capture&lt;/span&gt;());
assertEquals(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { act=com.qiku.android.backup.action.APPLICATIONSIZE cmp=com.qiku.android&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.backup/.receiver.SpaceReceiver (has extras) }&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
assertEquals(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Bundle[{size=0, application=calendar}]&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;getExtras&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onReceive_withIntentData_backup_cancel() {
MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt;();
data.&lt;span style=&#34;color:#309&#34;&gt;action&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; IntentUtil.&lt;span style=&#34;color:#309&#34;&gt;MOBILE_APPLICATION_BACKUP_RECEIVER&lt;/span&gt;;
assertEquals(ProcessResult.&lt;span style=&#34;color:#309&#34;&gt;DATA_INVALID&lt;/span&gt;, receiver.&lt;span style=&#34;color:#309&#34;&gt;processData&lt;/span&gt;(mContext, data));
data.&lt;span style=&#34;color:#309&#34;&gt;notifyAction&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;notify_action_test&amp;#34;&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;operation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Operation&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;CANCEL&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;identity&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 1;
assertEquals(ProcessResult.&lt;span style=&#34;color:#309&#34;&gt;PROCESS_CANCEL&lt;/span&gt;, receiver.&lt;span style=&#34;color:#309&#34;&gt;processData&lt;/span&gt;(mContext, data));
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onReceive_withIntentData_backup_fileError() {
MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt;();
data.&lt;span style=&#34;color:#309&#34;&gt;action&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; IntentUtil.&lt;span style=&#34;color:#309&#34;&gt;MOBILE_APPLICATION_BACKUP_RECEIVER&lt;/span&gt;;
assertEquals(ProcessResult.&lt;span style=&#34;color:#309&#34;&gt;DATA_INVALID&lt;/span&gt;, receiver.&lt;span style=&#34;color:#309&#34;&gt;processData&lt;/span&gt;(mContext, data));
data.&lt;span style=&#34;color:#309&#34;&gt;notifyAction&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;notify_action_test&amp;#34;&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;operation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Operation&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;BACKUP&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;identity&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 1;
when(receiver.&lt;span style=&#34;color:#309&#34;&gt;isFileTooLarge&lt;/span&gt;(mContext, data)).&lt;span style=&#34;color:#309&#34;&gt;thenReturn&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;);
assertEquals(ProcessResult.&lt;span style=&#34;color:#309&#34;&gt;FILE_ERROR&lt;/span&gt;, receiver.&lt;span style=&#34;color:#309&#34;&gt;processData&lt;/span&gt;(mContext, data));
when(receiver.&lt;span style=&#34;color:#309&#34;&gt;isFileTooLarge&lt;/span&gt;(mContext, data)).&lt;span style=&#34;color:#309&#34;&gt;thenReturn&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;);
when(receiver.&lt;span style=&#34;color:#309&#34;&gt;isEmptyBackupFile&lt;/span&gt;(mContext, data)).&lt;span style=&#34;color:#309&#34;&gt;thenReturn&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;);
assertEquals(ProcessResult.&lt;span style=&#34;color:#309&#34;&gt;FILE_ERROR&lt;/span&gt;, receiver.&lt;span style=&#34;color:#309&#34;&gt;processData&lt;/span&gt;(mContext, data));
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onReceive_withIntentData_backup() {
MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt;();
data.&lt;span style=&#34;color:#309&#34;&gt;action&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; IntentUtil.&lt;span style=&#34;color:#309&#34;&gt;MOBILE_APPLICATION_BACKUP_RECEIVER&lt;/span&gt;;
assertEquals(ProcessResult.&lt;span style=&#34;color:#309&#34;&gt;DATA_INVALID&lt;/span&gt;, receiver.&lt;span style=&#34;color:#309&#34;&gt;processData&lt;/span&gt;(mContext, data));
data.&lt;span style=&#34;color:#309&#34;&gt;notifyAction&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;notify_action_test&amp;#34;&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;operation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Operation&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;BACKUP&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;identity&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 1;
assertEquals(ProcessResult.&lt;span style=&#34;color:#309&#34;&gt;FINISHED&lt;/span&gt;, receiver.&lt;span style=&#34;color:#309&#34;&gt;processData&lt;/span&gt;(mContext, data));
verify(receiver).&lt;span style=&#34;color:#309&#34;&gt;startBackupService&lt;/span&gt;(mContext, data);
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onReceive_withIntentData_noSpace() {
MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt;();
data.&lt;span style=&#34;color:#309&#34;&gt;action&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; IntentUtil.&lt;span style=&#34;color:#309&#34;&gt;NO_SPACE_ACTION&lt;/span&gt;;
receiver.&lt;span style=&#34;color:#309&#34;&gt;prepareBackupIntent&lt;/span&gt;();
assertEquals(ProcessResult.&lt;span style=&#34;color:#309&#34;&gt;FINISHED&lt;/span&gt;, receiver.&lt;span style=&#34;color:#309&#34;&gt;processData&lt;/span&gt;(mContext, data));
verify(receiver).&lt;span style=&#34;color:#309&#34;&gt;stopBackupService&lt;/span&gt;(mContext, data);
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; startBackgroundService_withInvalidDataType() {
MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt;();
data.&lt;span style=&#34;color:#309&#34;&gt;dataType&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;notifyAction&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;notify_action_test&amp;#34;&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;operation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Operation&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;BACKUP&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;identity&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 1;
receiver.&lt;span style=&#34;color:#309&#34;&gt;startBackupService&lt;/span&gt;(mContext, data);
verify(receiver).&lt;span style=&#34;color:#309&#34;&gt;notifyBackupRecover&lt;/span&gt;(mContext, data);
ArgumentCaptor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style=&#34;color:#309&#34;&gt;forClass&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
verify(mContext).&lt;span style=&#34;color:#309&#34;&gt;sendBroadcast&lt;/span&gt;(argument.&lt;span style=&#34;color:#309&#34;&gt;capture&lt;/span&gt;());
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { act=notify_action_test (has extras) }&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Bundle[{return=0, percent=0, identity=1, application=calendar}]&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;getExtras&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; startBackgroundService_withInvalidOperation() {
MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt;();
data.&lt;span style=&#34;color:#309&#34;&gt;dataType&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;calendar&amp;#34;&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;notifyAction&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;notify_action_test&amp;#34;&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;identity&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 1;
data.&lt;span style=&#34;color:#309&#34;&gt;operation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Operation&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;CANCEL&lt;/span&gt;;
receiver.&lt;span style=&#34;color:#309&#34;&gt;startBackupService&lt;/span&gt;(mContext, data);
verify(receiver).&lt;span style=&#34;color:#309&#34;&gt;notifyBackupRecover&lt;/span&gt;(mContext, data);
ArgumentCaptor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style=&#34;color:#309&#34;&gt;forClass&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
verify(mContext).&lt;span style=&#34;color:#309&#34;&gt;sendBroadcast&lt;/span&gt;(argument.&lt;span style=&#34;color:#309&#34;&gt;capture&lt;/span&gt;());
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { act=notify_action_test (has extras) }&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Bundle[{return=0, percent=0, identity=1, application=calendar}]&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;getExtras&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; startBackgroundService_withValidOperation() {
MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Data&lt;/span&gt;();
data.&lt;span style=&#34;color:#309&#34;&gt;dataType&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;calendar&amp;#34;&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;notifyAction&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;notify_action_test&amp;#34;&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;identity&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 1;
data.&lt;span style=&#34;color:#309&#34;&gt;operation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;Operation&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;BACKUP&lt;/span&gt;;
receiver.&lt;span style=&#34;color:#309&#34;&gt;prepareBackupIntent&lt;/span&gt;();
receiver.&lt;span style=&#34;color:#309&#34;&gt;startBackupService&lt;/span&gt;(mContext, data);
ArgumentCaptor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style=&#34;color:#309&#34;&gt;forClass&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
verify(receiver).&lt;span style=&#34;color:#309&#34;&gt;startService&lt;/span&gt;(eq(mContext), argument.&lt;span style=&#34;color:#309&#34;&gt;capture&lt;/span&gt;());
assertEquals(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { act=com.qiku.android.mobile.backup.action.BACKUPSERVICE pkg=com.qiku&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.android.calendar (has extras) }&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
assertEquals(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Bundle[{qihoo_type=null, notify_action=notify_action_test, opr_type=0, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;identity=1, folder_path=null}]&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;getExtras&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
}
&lt;span style=&#34;color:#99f&#34;&gt;@After&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; tearDown() {
CalendarDatabaseHelperTest.&lt;span style=&#34;color:#309&#34;&gt;closeDatabase&lt;/span&gt;(RuntimeEnvironment.&lt;span style=&#34;color:#309&#34;&gt;application&lt;/span&gt;);
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;MobileCalendarBackupRecoverReceiver.java&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; MobileCalendarBackupRecoverReceiver &lt;span style=&#34;color:#c0f&#34;&gt;extends&lt;/span&gt; BroadcastReceiver {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; TAG &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; MobileCalendarBackupRecoverReceiver.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getSimpleName&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; STR_OPER_TYPE &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;opr_type&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; STR_NOTIFY_ACTION &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;notify_action&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; STR_FOLD_PATH &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;folder_path&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; STR_PERCENT &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;percent&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/**
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * ics文件最大字节数(800K)
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; */&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; MAX_FILE_SIZE &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 600 &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt; 1024;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; APPLICATION_STR &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;application&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; QIHOO_TYPE &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;qihoo_type&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; IDENTITY_STR &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;identity&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; Intent &lt;span style=&#34;color:#c0f&#34;&gt;mServiceIntent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;mServiceNum&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0;
&lt;span style=&#34;color:#99f&#34;&gt;@IntDef&lt;/span&gt;({ReturnOperation.&lt;span style=&#34;color:#309&#34;&gt;INTERRUPT&lt;/span&gt;, ReturnOperation.&lt;span style=&#34;color:#309&#34;&gt;CANCEL&lt;/span&gt;, ReturnOperation.&lt;span style=&#34;color:#309&#34;&gt;COMPLETE&lt;/span&gt;})
&lt;span style=&#34;color:#99f&#34;&gt;@Retention&lt;/span&gt;(RetentionPolicy.&lt;span style=&#34;color:#309&#34;&gt;SOURCE&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#99f&#34;&gt;@interface&lt;/span&gt; ReturnOperation {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;INTERRUPT&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;CANCEL&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;1;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;COMPLETE&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 1;
}
&lt;span style=&#34;color:#99f&#34;&gt;@IntDef&lt;/span&gt;({Operation.&lt;span style=&#34;color:#309&#34;&gt;CANCEL&lt;/span&gt;, Operation.&lt;span style=&#34;color:#309&#34;&gt;BACKUP&lt;/span&gt;, Operation.&lt;span style=&#34;color:#309&#34;&gt;RESTORE&lt;/span&gt;, Operation.&lt;span style=&#34;color:#309&#34;&gt;UPDATE&lt;/span&gt;})
&lt;span style=&#34;color:#99f&#34;&gt;@Retention&lt;/span&gt;(RetentionPolicy.&lt;span style=&#34;color:#309&#34;&gt;SOURCE&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#99f&#34;&gt;@interface&lt;/span&gt; Operation {
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;CANCEL&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;1;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;BACKUP&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;RESTORE&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 1;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;UPDATE&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 2;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;mIsBackupRecover&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 是否在备份恢复
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onReceive(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (intent &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;getAction&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt;;
}
Data &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; getPayload(intent);
prepareBackupIntent();
processData(context, data);
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;processData&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, Data &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;switch&lt;/span&gt; (data.&lt;span style=&#34;color:#309&#34;&gt;action&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; IntentUtil.&lt;span style=&#34;color:#309&#34;&gt;MOBILE_ACTION_QUERY_SIZE&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
sendSizeBroadcast(context);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; IntentUtil.&lt;span style=&#34;color:#309&#34;&gt;MOBILE_APPLICATION_BACKUP_RECEIVER&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (data.&lt;span style=&#34;color:#309&#34;&gt;isInvalid&lt;/span&gt;()) {
mServiceIntent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
mServiceNum &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; ProcessResult.&lt;span style=&#34;color:#309&#34;&gt;DATA_INVALID&lt;/span&gt;;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (data.&lt;span style=&#34;color:#309&#34;&gt;operation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; Operation.&lt;span style=&#34;color:#309&#34;&gt;CANCEL&lt;/span&gt;) {
Log.&lt;span style=&#34;color:#309&#34;&gt;d&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;cancel&amp;#34;&lt;/span&gt;);
cancelBackupService(context, data);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; ProcessResult.&lt;span style=&#34;color:#309&#34;&gt;PROCESS_CANCEL&lt;/span&gt;;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (isFileTooLarge(context, data) &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; isEmptyBackupFile(context, data)) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; ProcessResult.&lt;span style=&#34;color:#309&#34;&gt;FILE_ERROR&lt;/span&gt;;
}
startBackupService(context, data);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; IntentUtil.&lt;span style=&#34;color:#309&#34;&gt;NO_SPACE_ACTION&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
stopBackupService(context, data);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; ProcessResult.&lt;span style=&#34;color:#309&#34;&gt;FINISHED&lt;/span&gt;;
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;startBackupService&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, Data &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (data.&lt;span style=&#34;color:#309&#34;&gt;dataType&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; data.&lt;span style=&#34;color:#309&#34;&gt;dataType&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;calendar&amp;#34;&lt;/span&gt;)) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;(data.&lt;span style=&#34;color:#309&#34;&gt;operation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; Operation.&lt;span style=&#34;color:#309&#34;&gt;BACKUP&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; data.&lt;span style=&#34;color:#309&#34;&gt;operation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; Operation.&lt;span style=&#34;color:#309&#34;&gt;RESTORE&lt;/span&gt;)) {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;//操作失败
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; notifyBackupRecover(context, data);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (mServiceNum &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; 0) {
mServiceIntent.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(STR_NOTIFY_ACTION, data.&lt;span style=&#34;color:#309&#34;&gt;notifyAction&lt;/span&gt;);
mServiceIntent.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(IDENTITY_STR, data.&lt;span style=&#34;color:#309&#34;&gt;identity&lt;/span&gt;);
mServiceIntent.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(STR_OPER_TYPE, data.&lt;span style=&#34;color:#309&#34;&gt;operation&lt;/span&gt;);
mServiceIntent.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(STR_FOLD_PATH, data.&lt;span style=&#34;color:#309&#34;&gt;folderPath&lt;/span&gt;);
mServiceIntent.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(QIHOO_TYPE, data.&lt;span style=&#34;color:#309&#34;&gt;qihooType&lt;/span&gt;);
mServiceNum &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 1;
Log.&lt;span style=&#34;color:#309&#34;&gt;v&lt;/span&gt;(CalendarConsts.&lt;span style=&#34;color:#309&#34;&gt;QK_CALENDAR&lt;/span&gt;,
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;startService, mServiceNum = &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; mServiceNum);
mIsBackupRecover &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;;
startService(context, mServiceIntent);
}
}
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;//操作失败
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; notifyBackupRecover(context, data);
}
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;startService&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (Build.&lt;span style=&#34;color:#309&#34;&gt;VERSION&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;SDK_INT&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;=&lt;/span&gt; 26) {
context.&lt;span style=&#34;color:#309&#34;&gt;startForegroundService&lt;/span&gt;(intent);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
context.&lt;span style=&#34;color:#309&#34;&gt;startService&lt;/span&gt;(intent);
}
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;prepareBackupIntent&lt;/span&gt;() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (mServiceIntent &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; mServiceNum &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; 0) {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 防止连续收到多个备份恢复的消息。
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 如果 mServiceIntent 不为空或者 mServiceNum 大于0，表示服务已经起来了，就不需要再new Intent。
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; mServiceIntent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent();
mServiceIntent.&lt;span style=&#34;color:#309&#34;&gt;setAction&lt;/span&gt;(IntentUtil.&lt;span style=&#34;color:#309&#34;&gt;MOBILE_BACKUP_SERVICE&lt;/span&gt;);&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;//你定义的service的action
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; mServiceIntent.&lt;span style=&#34;color:#309&#34;&gt;setPackage&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.qiku.android.calendar&amp;#34;&lt;/span&gt;);&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;//这里你需要设置你应用的包名
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; } &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
Log.&lt;span style=&#34;color:#309&#34;&gt;v&lt;/span&gt;(CalendarConsts.&lt;span style=&#34;color:#309&#34;&gt;QK_CALENDAR&lt;/span&gt;,
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;mServiceIntent != null, mServiceNum= &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; mServiceNum);
}
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;isEmptyBackupFile&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, Data &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (data.&lt;span style=&#34;color:#309&#34;&gt;hasPath&lt;/span&gt;()) {
String &lt;span style=&#34;color:#c0f&#34;&gt;fileName&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;\\calendar.vcs&amp;#34;&lt;/span&gt;;
File &lt;span style=&#34;color:#c0f&#34;&gt;file&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; File(data.&lt;span style=&#34;color:#309&#34;&gt;folderPath&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; fileName);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; ((data.&lt;span style=&#34;color:#309&#34;&gt;operation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; Operation.&lt;span style=&#34;color:#309&#34;&gt;RESTORE&lt;/span&gt;) &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; (file.&lt;span style=&#34;color:#309&#34;&gt;exists&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; file.&lt;span style=&#34;color:#309&#34;&gt;length&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; 0)) {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 如果需要恢复的备份文件为空，直接返回恢复成功
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; Log.&lt;span style=&#34;color:#309&#34;&gt;v&lt;/span&gt;(CalendarConsts.&lt;span style=&#34;color:#309&#34;&gt;QK_CALENDAR&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Backup File is empty&amp;#34;&lt;/span&gt;);
data.&lt;span style=&#34;color:#309&#34;&gt;percent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; BackupConsts.&lt;span style=&#34;color:#309&#34;&gt;NUM_100&lt;/span&gt;;
data.&lt;span style=&#34;color:#309&#34;&gt;returnOperation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ReturnOperation.&lt;span style=&#34;color:#309&#34;&gt;COMPLETE&lt;/span&gt;;
notifyBackupRecover(context, data);
mServiceIntent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
mServiceNum &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;;
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;isFileTooLarge&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, Data &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (data.&lt;span style=&#34;color:#309&#34;&gt;hasPath&lt;/span&gt;()) {
String &lt;span style=&#34;color:#c0f&#34;&gt;fileName&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;\\calendar.vcs&amp;#34;&lt;/span&gt;;
File &lt;span style=&#34;color:#c0f&#34;&gt;file&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; File(data.&lt;span style=&#34;color:#309&#34;&gt;folderPath&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; fileName);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 600 * 1024是只恢复的ics文件的大小，该大小的ics文件估计存储了2000条左右的日历，
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 但是如果日程每个字段都填满的话，就只有600条日程。定义该大小的原因是：
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 如果ics文件超出该大小，在framework层解析ics文件时，会出现异常
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (file.&lt;span style=&#34;color:#309&#34;&gt;length&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; MAX_FILE_SIZE) {
notifyBackupRecover(context, data);
Toast.&lt;span style=&#34;color:#309&#34;&gt;makeText&lt;/span&gt;(context, ResUtil.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(R.&lt;span style=&#34;color:#309&#34;&gt;string&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;file_lenth_beyond_limit&lt;/span&gt;),
Toast.&lt;span style=&#34;color:#309&#34;&gt;LENGTH_SHORT&lt;/span&gt;).&lt;span style=&#34;color:#309&#34;&gt;show&lt;/span&gt;();
mServiceIntent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
mServiceNum &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;;
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; cancelBackupService(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, Data &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (context &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; mServiceIntent &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
context.&lt;span style=&#34;color:#309&#34;&gt;stopService&lt;/span&gt;(mServiceIntent);
}
mServiceIntent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
mServiceNum &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0;
data.&lt;span style=&#34;color:#309&#34;&gt;returnOperation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ReturnOperation.&lt;span style=&#34;color:#309&#34;&gt;CANCEL&lt;/span&gt;;
notifyBackupRecover(context, data);
mIsBackupRecover &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
}
Data &lt;span style=&#34;color:#c0f&#34;&gt;getPayload&lt;/span&gt;(Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt;) {
Data &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Data();
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;//标示身份(当前时间，用于验证是否是本次操作)
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; data.&lt;span style=&#34;color:#309&#34;&gt;identity&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;getLongExtra&lt;/span&gt;(IDENTITY_STR, 0);
data.&lt;span style=&#34;color:#309&#34;&gt;notifyAction&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;getStringExtra&lt;/span&gt;(STR_NOTIFY_ACTION);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;//获取处理类型，判断是否为日程类型
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; data.&lt;span style=&#34;color:#309&#34;&gt;dataType&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;getStringExtra&lt;/span&gt;(APPLICATION_STR);
data.&lt;span style=&#34;color:#309&#34;&gt;qihooType&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;getStringExtra&lt;/span&gt;(QIHOO_TYPE);
data.&lt;span style=&#34;color:#309&#34;&gt;operation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;getIntExtra&lt;/span&gt;(STR_OPER_TYPE, Operation.&lt;span style=&#34;color:#309&#34;&gt;BACKUP&lt;/span&gt;);
data.&lt;span style=&#34;color:#309&#34;&gt;folderPath&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;getStringExtra&lt;/span&gt;(STR_FOLD_PATH);
data.&lt;span style=&#34;color:#309&#34;&gt;action&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;getAction&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; data;
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;stopBackupService&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, Data &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt;) {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 空间不足取消操作
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; context.&lt;span style=&#34;color:#309&#34;&gt;stopService&lt;/span&gt;(mServiceIntent);
mServiceIntent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;;
mServiceNum &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 0;
data.&lt;span style=&#34;color:#309&#34;&gt;returnOperation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ReturnOperation.&lt;span style=&#34;color:#309&#34;&gt;CANCEL&lt;/span&gt;;
notifyBackupRecover(context, data);
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;sendSizeBroadcast&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;) {
ListBufferInfoBean &lt;span style=&#34;color:#c0f&#34;&gt;bean&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; provideEditEvent(context);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (bean &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;//每个设置3.5k
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;size&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 7 &lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt; bean.&lt;span style=&#34;color:#309&#34;&gt;getCount&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;/&lt;/span&gt; 2;
Intent &lt;span style=&#34;color:#c0f&#34;&gt;i&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(IntentUtil.&lt;span style=&#34;color:#309&#34;&gt;ACTION_ANSWER_SIZE&lt;/span&gt;);
i.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(APPLICATION_STR, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;calendar&amp;#34;&lt;/span&gt;);
i.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;size&amp;#34;&lt;/span&gt;, size);
i.&lt;span style=&#34;color:#309&#34;&gt;setClassName&lt;/span&gt;(IntentUtil.&lt;span style=&#34;color:#309&#34;&gt;BACKUP_PACKAGE&lt;/span&gt;, IntentUtil.&lt;span style=&#34;color:#309&#34;&gt;SPACE_RECEIVER&lt;/span&gt;);
context.&lt;span style=&#34;color:#309&#34;&gt;sendBroadcast&lt;/span&gt;(i);
}
}
ListBufferInfoBean &lt;span style=&#34;color:#c0f&#34;&gt;provideEditEvent&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; EditEventLogicImpl.&lt;span style=&#34;color:#309&#34;&gt;getInstance&lt;/span&gt;(context).&lt;span style=&#34;color:#309&#34;&gt;getEventsTableInfoForBackUp&lt;/span&gt;(&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;1);
}
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/**
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; * 发广播消息通知备份应用程序
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt; */&lt;/span&gt;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;notifyBackupRecover&lt;/span&gt;(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, Data &lt;span style=&#34;color:#c0f&#34;&gt;data&lt;/span&gt;) {
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(data.&lt;span style=&#34;color:#309&#34;&gt;notifyAction&lt;/span&gt;);
intent.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(APPLICATION_STR, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;calendar&amp;#34;&lt;/span&gt;);
intent.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;return&amp;#34;&lt;/span&gt;, data.&lt;span style=&#34;color:#309&#34;&gt;returnOperation&lt;/span&gt;);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (data.&lt;span style=&#34;color:#309&#34;&gt;percent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; BackupConsts.&lt;span style=&#34;color:#309&#34;&gt;NUM_100&lt;/span&gt;) {
intent.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(STR_PERCENT, BackupConsts.&lt;span style=&#34;color:#309&#34;&gt;NUM_100&lt;/span&gt;);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
intent.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(STR_PERCENT, data.&lt;span style=&#34;color:#309&#34;&gt;percent&lt;/span&gt;);
}
intent.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(IDENTITY_STR, data.&lt;span style=&#34;color:#309&#34;&gt;identity&lt;/span&gt;);
context.&lt;span style=&#34;color:#309&#34;&gt;sendBroadcast&lt;/span&gt;(intent);
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; Data {
&lt;span style=&#34;color:#99f&#34;&gt;@Operation&lt;/span&gt;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;operation&lt;/span&gt;;
&lt;span style=&#34;color:#99f&#34;&gt;@ReturnOperation&lt;/span&gt;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;returnOperation&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ReturnOperation.&lt;span style=&#34;color:#309&#34;&gt;INTERRUPT&lt;/span&gt;;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;long&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;identity&lt;/span&gt;;
String &lt;span style=&#34;color:#c0f&#34;&gt;notifyAction&lt;/span&gt;;
String &lt;span style=&#34;color:#c0f&#34;&gt;dataType&lt;/span&gt;;
String &lt;span style=&#34;color:#c0f&#34;&gt;qihooType&lt;/span&gt;;
String &lt;span style=&#34;color:#c0f&#34;&gt;folderPath&lt;/span&gt;;
String &lt;span style=&#34;color:#c0f&#34;&gt;action&lt;/span&gt;;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;percent&lt;/span&gt;;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;isInvalid&lt;/span&gt;() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; TextUtils.&lt;span style=&#34;color:#309&#34;&gt;isEmpty&lt;/span&gt;(notifyAction) &lt;span style=&#34;color:#555&#34;&gt;||&lt;/span&gt; identity &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; 0;
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;hasPath&lt;/span&gt;() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;TextUtils.&lt;span style=&#34;color:#309&#34;&gt;isEmpty&lt;/span&gt;(folderPath);
}
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Android 四大组件单元测试之 Activity</title><link>https://busy.im/post/android-activity-testing/</link><pubDate>Fri, 05 Oct 2018 14:30:53 +0800</pubDate><guid>https://busy.im/post/android-activity-testing/</guid><description>
&lt;h2 id=&#34;测试工具&#34;&gt;测试工具&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Junit4&lt;/code&gt; &lt;code&gt;Mockito&lt;/code&gt; &lt;code&gt;Robolectric&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;本文假定读者已经配置好了库及环境。&lt;/p&gt;
&lt;h2 id=&#34;测试内容&#34;&gt;测试内容&lt;/h2&gt;
&lt;p&gt;&lt;code&gt;Activity&lt;/code&gt; 测试相对比较简单，很容易得到 100% 的覆盖率。测试主要覆盖如下场景：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;onCreate(Bundle savedInstanceState)&lt;/code&gt; 中的逻辑过程，需要通过 &lt;code&gt;getIntent()&lt;/code&gt; 方法传入不同的 &lt;code&gt;Intent&lt;/code&gt; 覆盖。&lt;/li&gt;
&lt;li&gt;其他内部逻辑方法&lt;/li&gt;
&lt;li&gt;如果有线程且其中有逻辑代码，分拆成独立方法测试&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;验证条件&#34;&gt;验证条件&lt;/h2&gt;
&lt;p&gt;逻辑执行过程中，可以验证 &lt;code&gt;Activity.startActivity(Intent)&lt;/code&gt; 这样的方法是否被执行，及断言他们传入的 &lt;code&gt;intent&lt;/code&gt; 是我们期望的值；可以验证 UI 界面中的状态变化，如 &lt;code&gt;TextView&lt;/code&gt; 的文字等。&lt;/p&gt;
&lt;p&gt;通过分析验证条件，我们可以总结出需要 &lt;code&gt;mock&lt;/code&gt; 或 &lt;code&gt;spy&lt;/code&gt; 的对象要为&lt;code&gt;Activity&lt;/code&gt; 本身。同时我们可以借助 &lt;code&gt;ActivityController&amp;lt;T&amp;gt;&lt;/code&gt; 来模拟一些 &lt;code&gt;Activity&lt;/code&gt; 的行为如生命周期。&lt;/p&gt;
&lt;h2 id=&#34;代码实例&#34;&gt;代码实例&lt;/h2&gt;
&lt;p&gt;下面代码以日历中找的一个最简单的 &lt;code&gt;DeepLinkActivity&lt;/code&gt; 为例一步一步完成测试覆盖：&lt;/p&gt;
&lt;h3 id=&#34;activity-代码&#34;&gt;Activity 代码&lt;/h3&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-xml&#34; data-lang=&#34;xml&#34;&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;activity&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.ui.DeepLinkActivity&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:noHistory=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:exported=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;true&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:screenOrientation=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;portrait&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#309&#34;&gt;android:theme=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;@android:style/Theme.NoDisplay&amp;#34;&lt;/span&gt;&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;intent-filter&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;data&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:scheme=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.qiku.android.calendar&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;action&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.intent.action.VIEW&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;category&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.intent.category.VIEW&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;category&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.intent.category.DEFAULT&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;category&lt;/span&gt; &lt;span style=&#34;color:#309&#34;&gt;android:name=&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.intent.category.BROWSABLE&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;/&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/intent-filter&amp;gt;&lt;/span&gt;
&lt;span style=&#34;color:#309;font-weight:bold&#34;&gt;&amp;lt;/activity&amp;gt;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;code&gt;DeepLinkActivity.java&lt;/code&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; DeepLinkActivity &lt;span style=&#34;color:#c0f&#34;&gt;extends&lt;/span&gt; Activity {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; TAG &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; DeepLinkActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getSimpleName&lt;/span&gt;();
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onCreate(Bundle &lt;span style=&#34;color:#c0f&#34;&gt;savedInstanceState&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;super&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(savedInstanceState);
setContentView(R.&lt;span style=&#34;color:#309&#34;&gt;layout&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;activity_deep_link&lt;/span&gt;);
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; getIntent();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (intent &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;getData&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
openDeepLink(intent.&lt;span style=&#34;color:#309&#34;&gt;getData&lt;/span&gt;());
}
finish();
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; openDeepLink(Uri &lt;span style=&#34;color:#c0f&#34;&gt;deepLink&lt;/span&gt;) {
Log.&lt;span style=&#34;color:#309&#34;&gt;d&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;openDeepLink: &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; deepLink);
String &lt;span style=&#34;color:#c0f&#34;&gt;scheme&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; deepLink.&lt;span style=&#34;color:#309&#34;&gt;getScheme&lt;/span&gt;();
String &lt;span style=&#34;color:#c0f&#34;&gt;host&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; deepLink.&lt;span style=&#34;color:#309&#34;&gt;getHost&lt;/span&gt;();
String &lt;span style=&#34;color:#c0f&#34;&gt;path&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; deepLink.&lt;span style=&#34;color:#309&#34;&gt;getPath&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;Constants.&lt;span style=&#34;color:#309&#34;&gt;DEEP_LINK_SCHEME&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(scheme)) {
Log.&lt;span style=&#34;color:#309&#34;&gt;e&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;wrong scheme: &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; scheme);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt;;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;switch&lt;/span&gt; (host) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;case&lt;/span&gt; Constants.&lt;span style=&#34;color:#309&#34;&gt;DEEP_LINK_HOST_HOME&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (Constants.&lt;span style=&#34;color:#309&#34;&gt;DEEP_LINK_PATH_MONTH&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(path)) {
startActivity(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;, MenuAnimationActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;));
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
startActivity(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;, SplashActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;));
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;default&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;
startActivity(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;, SplashActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;));
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;break&lt;/span&gt;;
}
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;代码分析&#34;&gt;代码分析&lt;/h3&gt;
&lt;p&gt;从代码可以看出这个 &lt;code&gt;Acitivyt&lt;/code&gt; 监听了一个 &lt;code&gt;com.qiku.android.calendar://xxx&lt;/code&gt; 的事件，我们首先需要对能否接收到这个事件做校验；再看 &lt;code&gt;onCreate()&lt;/code&gt; 方法，可以看到需要在 &lt;code&gt;getIntent()&lt;/code&gt; 方法执行时提供不同的数据场景来覆盖业务逻辑。 这些逻辑分支整理为以下几条：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;空 &lt;code&gt;Intent&lt;/code&gt; 或 &lt;code&gt;Intent data&lt;/code&gt; 为空&lt;/li&gt;
&lt;li&gt;错误的 &lt;code&gt;Intent scheme&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;正确 &lt;code&gt;scheme&lt;/code&gt; 时默认拉起界面&lt;/li&gt;
&lt;li&gt;拉起主界面 &lt;code&gt;SplashActivity&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;拉起主界面 &lt;code&gt;MenuAnimationActivity&lt;/code&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;测试代码&#34;&gt;测试代码&lt;/h3&gt;
&lt;p&gt;首先我们需要准备上下文环境，我们可以通过 &lt;code&gt;Robolectric.buildActivity(DeepLinkActivity.class)&lt;/code&gt; 方法构建一个 &lt;code&gt;ActivityController&lt;/code&gt; 对象，同时通过 &lt;code&gt;controller.get()&lt;/code&gt; 来拿到 &lt;code&gt;DeepLinkActivity&lt;/code&gt; 对象：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ActivityController&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;DeepLinkActivity&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;controller&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;DeepLinkActivity&lt;/span&gt; activity;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;DeepLinkActivity&lt;/span&gt; spy;
&lt;span style=&#34;color:#99f&#34;&gt;@Before&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; setup() {
controller &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Robolectric.&lt;span style=&#34;color:#309&#34;&gt;buildActivity&lt;/span&gt;(DeepLinkActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
activity &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; controller.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;();
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;1-测试空-intent-拉起&#34;&gt;1. 测试空 Intent 拉起&lt;/h4&gt;
&lt;p&gt;先对空边界进行覆盖，可以在 &lt;code&gt;onCreate&lt;/code&gt; 方法中看出，当 &lt;code&gt;getIntent()&lt;/code&gt; 返回空时，没有执行任何业务逻辑就退出了。我们首先 &lt;code&gt;spy()&lt;/code&gt; &lt;code&gt;Activity&lt;/code&gt; 对象，同时假定 &lt;code&gt;getIntent()&lt;/code&gt; 被执行时返回一个 &lt;code&gt;null&lt;/code&gt; ，然后调用 &lt;code&gt;onCreate&lt;/code&gt; 方法。这时我们期望 &lt;code&gt;finish()&lt;/code&gt; 方法被执行，同时 &lt;code&gt;startActivity()&lt;/code&gt; 方法没有被执行。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; deepLink_finishActivity() {
spy &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Mockito.&lt;span style=&#34;color:#309&#34;&gt;spy&lt;/span&gt;(activity);
when(spy.&lt;span style=&#34;color:#309&#34;&gt;getIntent&lt;/span&gt;()).&lt;span style=&#34;color:#309&#34;&gt;thenReturn&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
spy.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
verify(spy).&lt;span style=&#34;color:#309&#34;&gt;finish&lt;/span&gt;();
verify(spy, never()).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(any(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;));
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;2-测试错误的-intent-拉起&#34;&gt;2. 测试错误的 Intent 拉起&lt;/h4&gt;
&lt;p&gt;再对错误的 &lt;code&gt;Intent&lt;/code&gt; 拉起做覆盖。和第一个测试相同，我们假定 &lt;code&gt;getIntent()&lt;/code&gt; 返回了一个错误的 &lt;code&gt;Intent&lt;/code&gt;，然后再执行 &lt;code&gt;onCreate()&lt;/code&gt; 方法，我们的期望和第一个测试相同。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; deepLink_wrongSchemeActivity() {
spy &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Mockito.&lt;span style=&#34;color:#309&#34;&gt;spy&lt;/span&gt;(activity);
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent();
intent.&lt;span style=&#34;color:#309&#34;&gt;setData&lt;/span&gt;(Uri.&lt;span style=&#34;color:#309&#34;&gt;parse&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.android.calendar://home&amp;#34;&lt;/span&gt;));
when(spy.&lt;span style=&#34;color:#309&#34;&gt;getIntent&lt;/span&gt;()).&lt;span style=&#34;color:#309&#34;&gt;thenReturn&lt;/span&gt;(intent);
spy.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
verify(spy).&lt;span style=&#34;color:#309&#34;&gt;finish&lt;/span&gt;();
verify(spy, never()).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(any(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;));
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;3-拉起默认的界面&#34;&gt;3. 拉起默认的界面&lt;/h4&gt;
&lt;p&gt;传入了正确的 &lt;code&gt;scheme&lt;/code&gt; 后，如果 &lt;code&gt;HOST&lt;/code&gt; 不匹配需要拉起默认的界面。假定执行 &lt;code&gt;getIntent()&lt;/code&gt; 时，返回了一个含有 &lt;code&gt;com.qiku.android.calendar://home1&lt;/code&gt; 数据的 &lt;code&gt;Intent&lt;/code&gt;，然后执行 &lt;code&gt;onCreate()&lt;/code&gt; 方法，校验 &lt;code&gt;finish()&lt;/code&gt; 方法被执行，再校验 &lt;code&gt;startActivity(Intent)&lt;/code&gt; 方法被执行，同时捕获它的 &lt;code&gt;Intent&lt;/code&gt; 参数，断言这个 &lt;code&gt;Intent&lt;/code&gt; 和我们期望的值相同。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; deepLink_defaultActivity() {
spy &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Mockito.&lt;span style=&#34;color:#309&#34;&gt;spy&lt;/span&gt;(activity);
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent();
intent.&lt;span style=&#34;color:#309&#34;&gt;setData&lt;/span&gt;(Uri.&lt;span style=&#34;color:#309&#34;&gt;parse&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.qiku.android.calendar://home1&amp;#34;&lt;/span&gt;));
when(spy.&lt;span style=&#34;color:#309&#34;&gt;getIntent&lt;/span&gt;()).&lt;span style=&#34;color:#309&#34;&gt;thenReturn&lt;/span&gt;(intent);
spy.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
verify(spy).&lt;span style=&#34;color:#309&#34;&gt;finish&lt;/span&gt;();
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// check startActivity() is called and check argument value
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; ArgumentCaptor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style=&#34;color:#309&#34;&gt;forClass&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
verify(spy).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(argument.&lt;span style=&#34;color:#309&#34;&gt;capture&lt;/span&gt;());
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { cmp=com.qiku.android.calendar/.ui.SplashActivity }&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;4-拉起主界面-splashactivity&#34;&gt;4. 拉起主界面 &lt;code&gt;SplashActivity&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;这个测试和第三个测试基本相同，只是 &lt;code&gt;Intent&lt;/code&gt; &lt;code&gt;Data&lt;/code&gt; 为我们期望的值 &lt;code&gt;com.qiku.android.calendar://home&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; deepLink_startHomeActivity() {
spy &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Mockito.&lt;span style=&#34;color:#309&#34;&gt;spy&lt;/span&gt;(activity);
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent();
intent.&lt;span style=&#34;color:#309&#34;&gt;setData&lt;/span&gt;(Uri.&lt;span style=&#34;color:#309&#34;&gt;parse&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.qiku.android.calendar://home&amp;#34;&lt;/span&gt;));
when(spy.&lt;span style=&#34;color:#309&#34;&gt;getIntent&lt;/span&gt;()).&lt;span style=&#34;color:#309&#34;&gt;thenReturn&lt;/span&gt;(intent);
spy.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// check startActivity() is called and check argument value
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; ArgumentCaptor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style=&#34;color:#309&#34;&gt;forClass&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
verify(spy).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(argument.&lt;span style=&#34;color:#309&#34;&gt;capture&lt;/span&gt;());
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { cmp=com.qiku.android.calendar/.ui.SplashActivity }&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;5-拉起主界面-menuanimationactivity&#34;&gt;5. 拉起主界面 &lt;code&gt;MenuAnimationActivity&lt;/code&gt;&lt;/h4&gt;
&lt;p&gt;这个测试和第三、四个测试基本相同，只是 &lt;code&gt;Intent&lt;/code&gt; &lt;code&gt;Data&lt;/code&gt; 中增加了 &lt;code&gt;path&lt;/code&gt; 字段，为我们期望的值 &lt;code&gt;com.qiku.android.calendar://home/month&lt;/code&gt;，这时我们期望拉起了 &lt;code&gt;MenuAnimationActivity&lt;/code&gt;。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; deepLink_startHomeMonthActivity() {
spy &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Mockito.&lt;span style=&#34;color:#309&#34;&gt;spy&lt;/span&gt;(activity);
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent();
intent.&lt;span style=&#34;color:#309&#34;&gt;setData&lt;/span&gt;(Uri.&lt;span style=&#34;color:#309&#34;&gt;parse&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.qiku.android.calendar://home/month&amp;#34;&lt;/span&gt;));
when(spy.&lt;span style=&#34;color:#309&#34;&gt;getIntent&lt;/span&gt;()).&lt;span style=&#34;color:#309&#34;&gt;thenReturn&lt;/span&gt;(intent);
spy.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// check startActivity() is called and check argument value
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; ArgumentCaptor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style=&#34;color:#309&#34;&gt;forClass&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
verify(spy).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(argument.&lt;span style=&#34;color:#309&#34;&gt;capture&lt;/span&gt;());
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { cmp=com.qiku.android.calendar/.ui.MenuAnimationActivity }&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;总结&#34;&gt;总结&lt;/h3&gt;
&lt;p&gt;通过上面的测试代码，我们已经完整覆盖了 &lt;code&gt;DeepLinkActivity&lt;/code&gt; 的所有业务逻辑分支，可以看出 &lt;code&gt;Activity&lt;/code&gt; 的测试还是很简单的。如果上下文中涉及到生命周期，&lt;code&gt;view&lt;/code&gt; 等内容，仍需要 &lt;code&gt;ActivityController&amp;lt;T&amp;gt;&lt;/code&gt; 的参与。&lt;/p&gt;
&lt;p&gt;最后附一个含有 UI 内容的 &lt;code&gt;FeedSettingsActivity&lt;/code&gt; 单元测试，这个测试中校验了 &lt;code&gt;ListView&lt;/code&gt; 的每个 &lt;code&gt;Item&lt;/code&gt;，同时对标题栏、&lt;code&gt;Item&lt;/code&gt; 点击，返回按钮等事件做了验证。&lt;/p&gt;
&lt;h3 id=&#34;附&#34;&gt;附&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;FeedSettingsActivity.java&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; FeedSettingsActivity &lt;span style=&#34;color:#c0f&#34;&gt;extends&lt;/span&gt; QkCalendarCommonActivity {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; TAG &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; AdViewerActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getSimpleName&lt;/span&gt;();
TopBar &lt;span style=&#34;color:#c0f&#34;&gt;mTopBar&lt;/span&gt;;
ListView &lt;span style=&#34;color:#c0f&#34;&gt;mListView&lt;/span&gt;;
ListAdapter &lt;span style=&#34;color:#c0f&#34;&gt;mAdapter&lt;/span&gt;;
FeedSettings &lt;span style=&#34;color:#c0f&#34;&gt;mSettings&lt;/span&gt;;
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;hasParent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onCreate(Bundle &lt;span style=&#34;color:#c0f&#34;&gt;savedInstanceState&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;super&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(savedInstanceState);
setBodyLayout(R.&lt;span style=&#34;color:#309&#34;&gt;layout&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;activity_feed_manage&lt;/span&gt;);
mSettings &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; FeedSettingsImpl.&lt;span style=&#34;color:#309&#34;&gt;getInstance&lt;/span&gt;();
mSettings.&lt;span style=&#34;color:#309&#34;&gt;init&lt;/span&gt;(getApplicationContext());
mListView &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; findViewById(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;list&lt;/span&gt;);
mAdapter &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ListAdapter(mSettings);
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;ListItem&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;items&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; loadListItems();
mAdapter.&lt;span style=&#34;color:#309&#34;&gt;reload&lt;/span&gt;(items);
mListView.&lt;span style=&#34;color:#309&#34;&gt;setAdapter&lt;/span&gt;(mAdapter);
mListView.&lt;span style=&#34;color:#309&#34;&gt;setOnItemClickListener&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; AdapterView.&lt;span style=&#34;color:#309&#34;&gt;OnItemClickListener&lt;/span&gt;() {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onItemClick(AdapterView&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;?&amp;gt;&lt;/span&gt; parent, View &lt;span style=&#34;color:#c0f&#34;&gt;view&lt;/span&gt;, &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;position&lt;/span&gt;, &lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;long&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;id&lt;/span&gt;) {
ListItem &lt;span style=&#34;color:#c0f&#34;&gt;item&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; (ListItem) mAdapter.&lt;span style=&#34;color:#309&#34;&gt;getItem&lt;/span&gt;(position);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (item.&lt;span style=&#34;color:#309&#34;&gt;hasChild&lt;/span&gt;()) {
mAdapter.&lt;span style=&#34;color:#309&#34;&gt;reload&lt;/span&gt;(item.&lt;span style=&#34;color:#309&#34;&gt;children&lt;/span&gt;);
mAdapter.&lt;span style=&#34;color:#309&#34;&gt;notifyDataSetChanged&lt;/span&gt;();
hasParent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;;
setTitle(item.&lt;span style=&#34;color:#309&#34;&gt;title&lt;/span&gt;);
}
}
});
}
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onBackPressed() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (hasParent) {
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;ListItem&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;items&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; loadListItems();
mAdapter.&lt;span style=&#34;color:#309&#34;&gt;reload&lt;/span&gt;(items);
mAdapter.&lt;span style=&#34;color:#309&#34;&gt;notifyDataSetChanged&lt;/span&gt;();
hasParent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;;
setTitle(R.&lt;span style=&#34;color:#309&#34;&gt;string&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;feed_manager&lt;/span&gt;);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;super&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;onBackPressed&lt;/span&gt;();
}
}
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onResume() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;super&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;onResume&lt;/span&gt;();
setTitle(R.&lt;span style=&#34;color:#309&#34;&gt;string&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;feed_manager&lt;/span&gt;);
}
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onCreateTopBar(TopBar &lt;span style=&#34;color:#c0f&#34;&gt;topBar&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;super&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;onCreateTopBar&lt;/span&gt;(topBar);
topBar.&lt;span style=&#34;color:#309&#34;&gt;setTopBarStyle&lt;/span&gt;(TopBar.&lt;span style=&#34;color:#309&#34;&gt;TopBarStyle&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;TOP_BAR_NOTMAL_STYLE&lt;/span&gt;);
topBar.&lt;span style=&#34;color:#309&#34;&gt;setDisplayUpView&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;);
mTopBar &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; topBar;
getLayoutInflater();
}
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; setTitle(CharSequence &lt;span style=&#34;color:#c0f&#34;&gt;title&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;((String) title).&lt;span style=&#34;color:#309&#34;&gt;isEmpty&lt;/span&gt;()) {
mTopBar.&lt;span style=&#34;color:#309&#34;&gt;setTopBarTitle&lt;/span&gt;(title);
}
}
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;ListItem&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;loadListItems&lt;/span&gt;() {
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;ListItem&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;listItems&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
listItems.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ListItem(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;黄历八字&amp;#34;&lt;/span&gt;, FeedTypeEx.&lt;span style=&#34;color:#309&#34;&gt;FORTUNE&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;()));
listItems.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ListItem(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;天气&amp;#34;&lt;/span&gt;, FeedTypeEx.&lt;span style=&#34;color:#309&#34;&gt;WEATHER&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;()));
listItems.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ListItem(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;历史上的今天&amp;#34;&lt;/span&gt;, FeedType.&lt;span style=&#34;color:#309&#34;&gt;HISTORY&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;()));
listItems.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ListItem(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;星座&amp;#34;&lt;/span&gt;, loadSignItems()));
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;StoryType&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;storyTypes&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mSettings.&lt;span style=&#34;color:#309&#34;&gt;getAllStoryTypes&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (StoryType &lt;span style=&#34;color:#c0f&#34;&gt;storyType&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; storyTypes) {
listItems.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ListItem(storyType.&lt;span style=&#34;color:#309&#34;&gt;getName&lt;/span&gt;(), FeedType.&lt;span style=&#34;color:#309&#34;&gt;STORY&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;_&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; storyType.&lt;span style=&#34;color:#309&#34;&gt;getId&lt;/span&gt;()));
}
listItems.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ListItem(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;今日要闻&amp;#34;&lt;/span&gt;, FeedTypeEx.&lt;span style=&#34;color:#309&#34;&gt;FLOW_NEWS&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;()));
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; listItems;
}
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;ListItem&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;loadSignItems&lt;/span&gt;() {
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;ListItem&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;listItems&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (SignType &lt;span style=&#34;color:#c0f&#34;&gt;type&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; SignType.&lt;span style=&#34;color:#309&#34;&gt;values&lt;/span&gt;()) {
listItems.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ListItem(type.&lt;span style=&#34;color:#309&#34;&gt;getName&lt;/span&gt;(), type.&lt;span style=&#34;color:#309&#34;&gt;key&lt;/span&gt;()));
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; listItems;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; ListAdapter &lt;span style=&#34;color:#c0f&#34;&gt;extends&lt;/span&gt; BaseAdapter {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;FeedSettings&lt;/span&gt; mSettings;
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;ListItem&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;items&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
ListAdapter(FeedSettings &lt;span style=&#34;color:#c0f&#34;&gt;settings&lt;/span&gt;) {
mSettings &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; settings;
}
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;void&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;reload&lt;/span&gt;(List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;ListItem&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;types&lt;/span&gt;) {
items.&lt;span style=&#34;color:#309&#34;&gt;clear&lt;/span&gt;();
items.&lt;span style=&#34;color:#309&#34;&gt;addAll&lt;/span&gt;(types);
}
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; getCount() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; items.&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;();
}
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Object&lt;/span&gt; getItem(&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;position&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; items.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;(position);
}
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;long&lt;/span&gt; getItemId(&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;position&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; position;
}
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;View&lt;/span&gt; getView(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; position, View &lt;span style=&#34;color:#c0f&#34;&gt;convertView&lt;/span&gt;, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ViewGroup&lt;/span&gt; parent) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ListItem&lt;/span&gt; item &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; items.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;(position);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (convertView &lt;span style=&#34;color:#555&#34;&gt;==&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
convertView &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; View.&lt;span style=&#34;color:#309&#34;&gt;inflate&lt;/span&gt;(parent.&lt;span style=&#34;color:#309&#34;&gt;getContext&lt;/span&gt;(), R.&lt;span style=&#34;color:#309&#34;&gt;layout&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;calendar_setting_item&lt;/span&gt;,
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
convertView.&lt;span style=&#34;color:#309&#34;&gt;setTag&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ViewHolder(convertView));
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ViewHolder&lt;/span&gt; viewHolder &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; (ViewHolder) convertView.&lt;span style=&#34;color:#309&#34;&gt;getTag&lt;/span&gt;();
viewHolder.&lt;span style=&#34;color:#309&#34;&gt;mainTitle&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;setText&lt;/span&gt;(item.&lt;span style=&#34;color:#309&#34;&gt;title&lt;/span&gt;);
viewHolder.&lt;span style=&#34;color:#309&#34;&gt;subTitle&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;setVisibility&lt;/span&gt;(View.&lt;span style=&#34;color:#309&#34;&gt;GONE&lt;/span&gt;);
convertView.&lt;span style=&#34;color:#309&#34;&gt;setClickable&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt;);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (item.&lt;span style=&#34;color:#309&#34;&gt;hasChild&lt;/span&gt;()) {
viewHolder.&lt;span style=&#34;color:#309&#34;&gt;switchButton&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;setVisibility&lt;/span&gt;(View.&lt;span style=&#34;color:#309&#34;&gt;GONE&lt;/span&gt;);
viewHolder.&lt;span style=&#34;color:#309&#34;&gt;arrow&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;setVisibility&lt;/span&gt;(View.&lt;span style=&#34;color:#309&#34;&gt;VISIBLE&lt;/span&gt;);
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
viewHolder.&lt;span style=&#34;color:#309&#34;&gt;arrow&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;setVisibility&lt;/span&gt;(View.&lt;span style=&#34;color:#309&#34;&gt;GONE&lt;/span&gt;);
viewHolder.&lt;span style=&#34;color:#309&#34;&gt;switchButton&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;setVisibility&lt;/span&gt;(View.&lt;span style=&#34;color:#309&#34;&gt;VISIBLE&lt;/span&gt;);
viewHolder.&lt;span style=&#34;color:#309&#34;&gt;switchButton&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;setOnCheckedChangeListener&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
viewHolder.&lt;span style=&#34;color:#309&#34;&gt;switchButton&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;setCheckedImmediately&lt;/span&gt;(mSettings.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;(item.&lt;span style=&#34;color:#309&#34;&gt;key&lt;/span&gt;));
viewHolder.&lt;span style=&#34;color:#309&#34;&gt;switchButton&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;setOnCheckedChangeListener&lt;/span&gt;(
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; CompoundButton.&lt;span style=&#34;color:#309&#34;&gt;OnCheckedChangeListener&lt;/span&gt;() {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onCheckedChanged(CompoundButton &lt;span style=&#34;color:#c0f&#34;&gt;buttonView&lt;/span&gt;,
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;isChecked&lt;/span&gt;) {
mSettings.&lt;span style=&#34;color:#309&#34;&gt;setValue&lt;/span&gt;(item.&lt;span style=&#34;color:#309&#34;&gt;key&lt;/span&gt;, isChecked);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (isChecked &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; item.&lt;span style=&#34;color:#309&#34;&gt;key&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(FeedTypeEx.&lt;span style=&#34;color:#309&#34;&gt;WEATHER&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;())) {
WeatherController.&lt;span style=&#34;color:#309&#34;&gt;getInstance&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;init&lt;/span&gt;(parent.&lt;span style=&#34;color:#309&#34;&gt;getContext&lt;/span&gt;());
}
}
});
convertView.&lt;span style=&#34;color:#309&#34;&gt;setClickable&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;);
convertView.&lt;span style=&#34;color:#309&#34;&gt;setOnClickListener&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; View.&lt;span style=&#34;color:#309&#34;&gt;OnClickListener&lt;/span&gt;() {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; onClick(View &lt;span style=&#34;color:#c0f&#34;&gt;v&lt;/span&gt;) {
viewHolder.&lt;span style=&#34;color:#309&#34;&gt;switchButton&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;toggle&lt;/span&gt;();
}
});
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; convertView;
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;class&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ViewHolder&lt;/span&gt; {
TextView &lt;span style=&#34;color:#c0f&#34;&gt;mainTitle&lt;/span&gt;;
TextView &lt;span style=&#34;color:#c0f&#34;&gt;subTitle&lt;/span&gt;;
QkSwitch &lt;span style=&#34;color:#c0f&#34;&gt;switchButton&lt;/span&gt;;
ImageView &lt;span style=&#34;color:#c0f&#34;&gt;arrow&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ViewHolder&lt;/span&gt;(View &lt;span style=&#34;color:#c0f&#34;&gt;parent&lt;/span&gt;) {
mainTitle &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; parent.&lt;span style=&#34;color:#309&#34;&gt;findViewById&lt;/span&gt;(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;main_title&lt;/span&gt;);
subTitle &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; parent.&lt;span style=&#34;color:#309&#34;&gt;findViewById&lt;/span&gt;(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;sub_title&lt;/span&gt;);
switchButton &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; parent.&lt;span style=&#34;color:#309&#34;&gt;findViewById&lt;/span&gt;(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;common_switch_button&lt;/span&gt;);
arrow &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; parent.&lt;span style=&#34;color:#309&#34;&gt;findViewById&lt;/span&gt;(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;icon_arrow&lt;/span&gt;);
}
}
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; ListItem {
ListItem(String &lt;span style=&#34;color:#c0f&#34;&gt;title&lt;/span&gt;, String &lt;span style=&#34;color:#c0f&#34;&gt;key&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;title&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; title;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;key&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; key;
}
ListItem(String &lt;span style=&#34;color:#c0f&#34;&gt;title&lt;/span&gt;, List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;ListItem&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;children&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;title&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; title;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;this&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;children&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;addAll&lt;/span&gt;(children);
}
String &lt;span style=&#34;color:#c0f&#34;&gt;title&lt;/span&gt;;
String &lt;span style=&#34;color:#c0f&#34;&gt;key&lt;/span&gt;;
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;ListItem&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;children&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;boolean&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;hasChild&lt;/span&gt;() {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; children.&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; 0;
}
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; toString() {
String &lt;span style=&#34;color:#c0f&#34;&gt;s&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;title=&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; title &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\&amp;#39;&amp;#39;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, key=&amp;#39;&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; key &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;\&amp;#39;&amp;#39;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (children.&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; 0) {
s &lt;span style=&#34;color:#555&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;, children=&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; children;
}
s &lt;span style=&#34;color:#555&#34;&gt;+=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;}&amp;#39;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; s;
}
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;FeedSettingsActivityTest.java&lt;/strong&gt;&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@RunWith&lt;/span&gt;(RobolectricTestRunner.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; FeedSettingsActivityTest {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ActivityController&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;FeedSettingsActivity&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;controller&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;FeedSettingsActivity&lt;/span&gt; activity;
&lt;span style=&#34;color:#99f&#34;&gt;@Before&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; setup() {
controller &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Robolectric.&lt;span style=&#34;color:#309&#34;&gt;buildActivity&lt;/span&gt;(FeedSettingsActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
activity &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; controller.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;();
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; feedSettingList() {
FeedSettingsActivity &lt;span style=&#34;color:#c0f&#34;&gt;spy&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; spy(activity);
spy.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
verify(spy).&lt;span style=&#34;color:#309&#34;&gt;loadListItems&lt;/span&gt;();
verify(spy).&lt;span style=&#34;color:#309&#34;&gt;loadSignItems&lt;/span&gt;();
assertEquals(5, spy.&lt;span style=&#34;color:#309&#34;&gt;mAdapter&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;items&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;());
assertEquals(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;[{title=&amp;#39;黄历八字&amp;#39;, key=&amp;#39;fortune&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;天气&amp;#39;, key=&amp;#39;weather&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;历史上的今天&amp;#39;, key=&amp;#39;history&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;星座&amp;#39;, key=&amp;#39;null&amp;#39;, children=[&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;白羊座&amp;#39;, key=&amp;#39;sign_aries&amp;#39;},&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34; {title=&amp;#39;金牛座&amp;#39;, key=&amp;#39;sign_taurus&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;双子座&amp;#39;, key=&amp;#39;sign_gemini&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;巨蟹座&amp;#39;, key=&amp;#39;sign_cancer&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;狮子座&amp;#39;, key=&amp;#39;sign_leo&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;处女座&amp;#39;, key=&amp;#39;sign_virgo&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;天秤座&amp;#39;, key=&amp;#39;sign_libra&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;天蝎座&amp;#39;, key=&amp;#39;sign_scorpio&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;射手座&amp;#39;, key=&amp;#39;sign_sagittarius&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;摩羯座&amp;#39;, key=&amp;#39;sign_capricorn&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;水瓶座&amp;#39;, key=&amp;#39;sign_aquarius&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;双鱼座&amp;#39;, key=&amp;#39;sign_pisces&amp;#39;}]}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;今日要闻&amp;#39;, key=&amp;#39;flow_news&amp;#39;}]&amp;#34;&lt;/span&gt;,
spy.&lt;span style=&#34;color:#309&#34;&gt;mAdapter&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;items&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; signParentItemClick() {
FeedSettingsActivity &lt;span style=&#34;color:#c0f&#34;&gt;spy&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; spy(activity);
spy.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
spy.&lt;span style=&#34;color:#309&#34;&gt;mListView&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;performItemClick&lt;/span&gt;(spy.&lt;span style=&#34;color:#309&#34;&gt;mListView&lt;/span&gt;, 3, 0);
assertEquals(12, spy.&lt;span style=&#34;color:#309&#34;&gt;mAdapter&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;items&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;());
assertEquals(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;[{title=&amp;#39;白羊座&amp;#39;, key=&amp;#39;sign_aries&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;金牛座&amp;#39;, key=&amp;#39;sign_taurus&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;双子座&amp;#39;, key=&amp;#39;sign_gemini&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;巨蟹座&amp;#39;, key=&amp;#39;sign_cancer&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;狮子座&amp;#39;, key=&amp;#39;sign_leo&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;处女座&amp;#39;, key=&amp;#39;sign_virgo&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;天秤座&amp;#39;, key=&amp;#39;sign_libra&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;天蝎座&amp;#39;, key=&amp;#39;sign_scorpio&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;射手座&amp;#39;, key=&amp;#39;sign_sagittarius&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;摩羯座&amp;#39;, key=&amp;#39;sign_capricorn&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;水瓶座&amp;#39;, key=&amp;#39;sign_aquarius&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;双鱼座&amp;#39;, key=&amp;#39;sign_pisces&amp;#39;}]&amp;#34;&lt;/span&gt;,
spy.&lt;span style=&#34;color:#309&#34;&gt;mAdapter&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;items&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
assertTrue(spy.&lt;span style=&#34;color:#309&#34;&gt;hasParent&lt;/span&gt;);
spy.&lt;span style=&#34;color:#309&#34;&gt;onBackPressed&lt;/span&gt;();
assertEquals(5, spy.&lt;span style=&#34;color:#309&#34;&gt;mAdapter&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;items&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;());
assertEquals(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;[{title=&amp;#39;黄历八字&amp;#39;, key=&amp;#39;fortune&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;天气&amp;#39;, key=&amp;#39;weather&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;历史上的今天&amp;#39;, key=&amp;#39;history&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;星座&amp;#39;, key=&amp;#39;null&amp;#39;, children=[&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;白羊座&amp;#39;, key=&amp;#39;sign_aries&amp;#39;},&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34; {title=&amp;#39;金牛座&amp;#39;, key=&amp;#39;sign_taurus&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;双子座&amp;#39;, key=&amp;#39;sign_gemini&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;巨蟹座&amp;#39;, key=&amp;#39;sign_cancer&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;狮子座&amp;#39;, key=&amp;#39;sign_leo&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;处女座&amp;#39;, key=&amp;#39;sign_virgo&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;天秤座&amp;#39;, key=&amp;#39;sign_libra&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;天蝎座&amp;#39;, key=&amp;#39;sign_scorpio&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;射手座&amp;#39;, key=&amp;#39;sign_sagittarius&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;摩羯座&amp;#39;, key=&amp;#39;sign_capricorn&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;水瓶座&amp;#39;, key=&amp;#39;sign_aquarius&amp;#39;}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;双鱼座&amp;#39;, key=&amp;#39;sign_pisces&amp;#39;}]}, &amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;{title=&amp;#39;今日要闻&amp;#39;, key=&amp;#39;flow_news&amp;#39;}]&amp;#34;&lt;/span&gt;,
spy.&lt;span style=&#34;color:#309&#34;&gt;mAdapter&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;items&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
assertFalse(spy.&lt;span style=&#34;color:#309&#34;&gt;hasParent&lt;/span&gt;);
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; backButtonPressed() {
FeedSettingsActivity &lt;span style=&#34;color:#c0f&#34;&gt;spy&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; spy(activity);
spy.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
assertFalse(spy.&lt;span style=&#34;color:#309&#34;&gt;hasParent&lt;/span&gt;);
spy.&lt;span style=&#34;color:#309&#34;&gt;mListView&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;performItemClick&lt;/span&gt;(spy.&lt;span style=&#34;color:#309&#34;&gt;mListView&lt;/span&gt;, 3, 0);
assertTrue(spy.&lt;span style=&#34;color:#309&#34;&gt;hasParent&lt;/span&gt;);
spy.&lt;span style=&#34;color:#309&#34;&gt;onBackPressed&lt;/span&gt;();
assertFalse(spy.&lt;span style=&#34;color:#309&#34;&gt;hasParent&lt;/span&gt;);
assertFalse(activity.&lt;span style=&#34;color:#309&#34;&gt;isFinishing&lt;/span&gt;());
spy.&lt;span style=&#34;color:#309&#34;&gt;onBackPressed&lt;/span&gt;();
assertTrue(activity.&lt;span style=&#34;color:#309&#34;&gt;isFinishing&lt;/span&gt;());
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; feedSettingTitle() {
Locale &lt;span style=&#34;color:#c0f&#34;&gt;locale&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Locale(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;zh&amp;#34;&lt;/span&gt;);
Locale.&lt;span style=&#34;color:#309&#34;&gt;setDefault&lt;/span&gt;(locale);
RuntimeEnvironment.&lt;span style=&#34;color:#309&#34;&gt;setQualifiers&lt;/span&gt;(locale.&lt;span style=&#34;color:#309&#34;&gt;getLanguage&lt;/span&gt;());
FeedSettingsActivity &lt;span style=&#34;color:#c0f&#34;&gt;spy&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; spy(activity);
spy.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
spy.&lt;span style=&#34;color:#309&#34;&gt;onResume&lt;/span&gt;();
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;订阅管理&amp;#34;&lt;/span&gt;, spy.&lt;span style=&#34;color:#309&#34;&gt;mTopBar&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getTopBarTitle&lt;/span&gt;());
spy.&lt;span style=&#34;color:#309&#34;&gt;mListView&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;performItemClick&lt;/span&gt;(spy.&lt;span style=&#34;color:#309&#34;&gt;mListView&lt;/span&gt;, 3, 0);
assertTrue(spy.&lt;span style=&#34;color:#309&#34;&gt;hasParent&lt;/span&gt;);
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;星座&amp;#34;&lt;/span&gt;, spy.&lt;span style=&#34;color:#309&#34;&gt;mTopBar&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getTopBarTitle&lt;/span&gt;());
spy.&lt;span style=&#34;color:#309&#34;&gt;onBackPressed&lt;/span&gt;();
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;订阅管理&amp;#34;&lt;/span&gt;, spy.&lt;span style=&#34;color:#309&#34;&gt;mTopBar&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getTopBarTitle&lt;/span&gt;());
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; listView_item() {
FeedSettingsActivity &lt;span style=&#34;color:#c0f&#34;&gt;spy&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; spy(activity);
spy.&lt;span style=&#34;color:#309&#34;&gt;onCreate&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;);
spy.&lt;span style=&#34;color:#309&#34;&gt;onResume&lt;/span&gt;();
controller.&lt;span style=&#34;color:#309&#34;&gt;visible&lt;/span&gt;();
View &lt;span style=&#34;color:#c0f&#34;&gt;view&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; spy.&lt;span style=&#34;color:#309&#34;&gt;mListView&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getChildAt&lt;/span&gt;(0);
FeedSettingsActivity.&lt;span style=&#34;color:#309&#34;&gt;ListAdapter&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;ViewHolder&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;viewHolder&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
(FeedSettingsActivity.&lt;span style=&#34;color:#309&#34;&gt;ListAdapter&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;ViewHolder&lt;/span&gt;) view.&lt;span style=&#34;color:#309&#34;&gt;getTag&lt;/span&gt;();
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;黄历八字&amp;#34;&lt;/span&gt;, viewHolder.&lt;span style=&#34;color:#309&#34;&gt;mainTitle&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;getText&lt;/span&gt;());
assertTrue(viewHolder.&lt;span style=&#34;color:#309&#34;&gt;switchButton&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;isEnabled&lt;/span&gt;());
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;</description></item><item><title>Android 开发人员自动化测试</title><link>https://busy.im/post/android-testing/</link><pubDate>Fri, 05 Oct 2018 13:09:22 +0800</pubDate><guid>https://busy.im/post/android-testing/</guid><description>
&lt;p&gt;编写测试的目的是为了验证程序是否正确执行、行为无误及是否稳定可用。同时，拥有充分测试代码的项目易于维护，便于交接、团队协作。&lt;/p&gt;
&lt;p&gt;有很多的 Android 架构如 MVP、Clean、DataBinding 等，这些架构的一个共同点是分离视图界面、数据模型、业务/控制逻辑，最大限度的降低代码耦合，减少函数/方法的副作用。网上介绍这些架构、框架的文章很多，但是很少有提及为什么要这么做。使用规范的代码架构，降低代码耦合，直观上可以使代码易于维护，提高代码利用率、便于重构，其实最直接的作用是为了方便的写测试代码，这一点却很少有人提及。&lt;/p&gt;
&lt;p&gt;以 MVP 架构举例，将 APP 代码分离为三个层次：Model, View, Presenter。Model 层封装数据模型，不做任何逻辑操作，View 层即 Activity、Fragment 等只用来展示界面、返回界面事件，Presenter 层来和数据交互，控制界面显示逻辑，响应界面事件及数据变化。View 不直接和 Model 交互，同时 Model 和 Presenter 不依赖于 Android 系统库的代码，这样可以方便的在本地电脑对 Presenter 和 Model 做单元测试。&lt;/p&gt;
&lt;p&gt;本文档主要是对 Android 开发人员如何编写单元测试及 UI 自动化测试代码的整理，其中大部分篇幅是对 Android 官方文档翻译，要阅读英文原文，请点击每小节的参考链接或文档最后的参考资料链接。&lt;/p&gt;
&lt;h2 id=&#34;一-测试类型&#34;&gt;一. 测试类型&lt;/h2&gt;
&lt;p&gt;Android 测试类型可以分为三大类：单元测试、集成测试、压力测试。单元测试又可以分为本地单元测试、仪表 (Instrumented) 单元测试；集成测试分为应用内、跨应用测试；压力测试可以分为 Monkey 、Monkey Runner。除了压力测试中的 Monkey 测试，其他五类测试都需要开发人员编写测试代码。&lt;/p&gt;
&lt;p&gt;其中 Monkey 测试不需要开发人员编写测试代码，而 Monkey Runner 是 Python 程序，一般 Android 开发人员不直接编写此类测试代码。由于篇幅关系，本文不讨论压力测试的内容。&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th align=&#34;center&#34;&gt;类型&lt;/th&gt;
&lt;th align=&#34;center&#34;&gt;子类型&lt;/th&gt;
&lt;th&gt;描述&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td align=&#34;center&#34;&gt;单元测试&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;本地单元测试&lt;/td&gt;
&lt;td&gt;运行在本地 Java 虚拟机，不依赖于 Android 框架或者可以通过 Mock 来 模拟 Android 框架的测试。因为不依赖与 Android 框架及模拟器或开发机，执行快&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&#34;center&#34;&gt;&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;Instrumented 单元测试&lt;/td&gt;
&lt;td&gt;运行于开发机或模拟器，这些测试可以访问应用的运行状态信息，如应用的 Context。用这些测试来测试不能通过Mock模拟而必须依赖于 Android 框架的组件。因为要运行在真实的 Android 环境下，执行较慢。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&#34;center&#34;&gt;集成测试&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;仅应用内组件测试&lt;/td&gt;
&lt;td&gt;这类测试用来确认目标应用在用户执行某特定操作或输入时，能够正确响应。例如，可以用来确认用户与应用交互时响应了正确的用户界面。UI 测试框架如 Espresso 允许开发者程序化的模仿用户行为并验证复杂的程序内用户交互。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&#34;center&#34;&gt;&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;跨应用组件测试&lt;/td&gt;
&lt;td&gt;这类测试用来验证用户在多个不同应用之间或应用与系统交互时是否正确。例如可以通过这类测试来验证用户操作后系统状态栏的通知是否正确，也可以通过这类测试来点击状态栏通知、点击权限确认按钮。UI 测试框架如 UI Automator 可以用来创建此类测试。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&#34;center&#34;&gt;压力测试&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;Monkey&lt;/td&gt;
&lt;td&gt;Monkey 是一个命令行工具，可以通过 adb 接口随机触摸、点击、按键或触发手势。它可以循环执行这些随机的触摸等事件，并返回遇到的错误。&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td align=&#34;center&#34;&gt;&lt;/td&gt;
&lt;td align=&#34;center&#34;&gt;monkeyrunner&lt;/td&gt;
&lt;td&gt;monkeyrunner 是一个 Python 程序，是一个测试 API 及运行环境。API 包括 连接设备、安装/卸载软件包、截取屏幕、对比两个图片及运行测试软件包等。使用这个 API 可以编写强大复杂的测试。&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;h2 id=&#34;二-测试支持库&#34;&gt;二. 测试支持库&lt;/h2&gt;
&lt;p&gt;Android 官方的测试支持库包括 AndroidJUnit4，Espresso，UI Automator 。其中 Espresso 和 UI Automator 可以结合在一起使用，来模拟完整的用户行为。&lt;/p&gt;
&lt;h3 id=&#34;1-androidjunit4&#34;&gt;1. AndroidJUnit4&lt;/h3&gt;
&lt;p&gt;AndroidJUnit4 是 Android 单元测试的主要方法，在本地计算机或持续集成环境中运行，不需要测试机或者模拟器。&lt;/p&gt;
&lt;h3 id=&#34;2-espresso&#34;&gt;2. Espresso&lt;/h3&gt;
&lt;p&gt;Espresso 是用来测试 Android 应用界面的主要方法，运行于测试机或者持续集成环境中的模拟器。不同于 UI Automator，Espresso 只能用来测试应用本身的界面，但是 Espresso 更加便捷易用，可以非常轻松的实现点击、滑动、长按、触摸等操作。&lt;/p&gt;
&lt;h3 id=&#34;3-ui-automator&#34;&gt;3. UI Automator&lt;/h3&gt;
&lt;p&gt;UIAutomator 也是用来测试 Android 应用界面的主要方法，运行于测试机或者持续集成环境中的模拟器。不同于 Espresso， UIAutomator 还可以用来测试与系统界面的交互，如状态栏、通知、系统权限对话框等。&lt;/p&gt;
&lt;p&gt;这些测试编写完成后，都可以通过自动集成环境（如 Jenkins、Gitlab Runner 、Travis-ci、AppVeyor 等）自动编译、检查错误并生成报告。其中本地单元测试最容易集成但功能有限；仪表 (Instrumented) 单元测试、Espresso、 UI Automator 都需要在自动集成环境的无用户界面环境下启动模拟器并运行，可以附加配置自动抓日志、截屏等方式自动编译、检查错误并生成报告，集成配置相对会复杂一些。&lt;/p&gt;
&lt;h2 id=&#34;三-编写-android-测试&#34;&gt;三. 编写 Android 测试&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 强烈建议使用 Android Studio 编写测试应用，因为它已经集成了工程配置、依赖库和便利的包管理。本文档假定你已经在使用 Android Studio。&lt;/p&gt;
&lt;h3 id=&#34;1-本地单元测试&#34;&gt;1. 本地单元测试&lt;/h3&gt;
&lt;p&gt;本小节介绍如何编写本地单元测试，这类测试无依赖或者可以通过 mock 伪造依赖，运行于本地 JVM 。&lt;/p&gt;
&lt;p&gt;参考 &lt;a href=&#34;https://developer.android.com/training/testing/unit-testing/local-unit-tests.html&#34;&gt;Building Local Unit Tests&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;当你编写的单元测试没有依赖或者对 Android 仅有一些简单依赖时，你就可以在本地开发环境运行测试。这个测试过程很高效，因为本地测试可以避免加载目标应用，同时也避免了将测试代码加载到开发机或者模拟器上，所以节省了测试执行过程花费的时间。通常，你需要使用一个 mocking 框架，例如 &lt;a href=&#34;https://github.com/mockito/mockito&#34;&gt;Mockito&lt;/a&gt; 来满足代码的依赖关系。&lt;/p&gt;
&lt;h4 id=&#34;配置测试环境&#34;&gt;配置测试环境&lt;/h4&gt;
&lt;p&gt;在你的 Android Studio 工程中，你必须将本地单元测试的代码存放在 &lt;code&gt;module-name/src/test/java/&lt;/code&gt; 目录下。这个目录在你新建工程时已经被默认创建。你同时也需要为你的工程配置测试依赖库，这个标准 API 库是由 JUnit 4 框架提供的。如果你的测试代码需要依赖 Android，则需要引入 &lt;a href=&#34;https://github.com/mockito/mockito&#34;&gt;Mockito&lt;/a&gt; 库来简化你的本地测试。阅读更多关于如何在你本地单元测试中使用 mock objects，请查看 &lt;a href=&#34;https://developer.android.com/training/testing/unit-testing/local-unit-tests.html#mocking-dependencies&#34;&gt;Mocking Android dependencies&lt;/a&gt; 。&lt;/p&gt;
&lt;p&gt;在你 app 顶层的 build.gradble 文件中，你需要指定如下依赖库：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;dependencies &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Required -- JUnit 4 framework
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; testCompile &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;junit:junit:4.12&amp;#39;&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Optional -- Mockito framework
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; testCompile &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;org.mockito:mockito-core:1.10.19&amp;#39;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;创建本地单元测试类&#34;&gt;创建本地单元测试类&lt;/h4&gt;
&lt;p&gt;本地单元测试类应该是一个纯粹的 JUnit 4 单元测试类。 &lt;a href=&#34;http://junit.org/&#34;&gt;JUnit&lt;/a&gt; 是一个广泛使用的 Java 单元测试框架。JUnit 4 是此框架的最新版本，与之前的几个版本相比，这个版本可以以更简洁和灵活的方式写测试。不同于之前的版本，你的测试类不再需要继承自 &lt;code&gt;junit.framework.TestCase&lt;/code&gt; ，同时你也不再需要为测试方法增加 &lt;code&gt;test&lt;/code&gt; 关键字前缀，或者使用任何 &lt;code&gt;jnit.framework&lt;/code&gt; 或 &lt;code&gt;jnit.extensions&lt;/code&gt; 包。&lt;/p&gt;
&lt;p&gt;创建一个基本的 JUnit 4 测试类，你需要创建一个含有至少一个测试方法的 Java 类。一个测试方法需要以 &lt;code&gt;@Test&lt;/code&gt; 注解开始，同时包含一个执行并验证组件中单一功能的代码块。&lt;/p&gt;
&lt;p&gt;例如下面的代码块，测试方法 &lt;code&gt;emailValidator_CorrectEmailSimple_ReturnsTrue&lt;/code&gt; 验证 &lt;code&gt;isValidEmail()&lt;/code&gt; 方法返回正确的值。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Test&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;java&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;util&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;regex&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Pattern&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; org.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Assert&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;assertFalse&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; org.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Assert&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;assertTrue&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; EmailValidatorTest {
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; emailValidator_CorrectEmailSimple_ReturnsTrue() {
assertThat(EmailValidator.&lt;span style=&#34;color:#309&#34;&gt;isValidEmail&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;name@email.com&amp;#34;&lt;/span&gt;), is(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;));
}
...
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;使用 &lt;a href=&#34;http://junit.org/javadoc/latest/org/junit/Assert.html&#34;&gt;junit.Assert&lt;/a&gt; 一系列方法来验证检查组件执行的状态或结果是否符合预期，为了更好的可读性，你可以使用&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/hamcrest&#34;&gt;Hamcrest matchers&lt;/a&gt; （如 &lt;code&gt;is()&lt;/code&gt; 和 &lt;code&gt;equalTo()&lt;/code&gt; 方法）来匹配返回值和预期结果。&lt;/p&gt;
&lt;h4 id=&#34;模拟-mock-android-依赖&#34;&gt;模拟 (mock) Android 依赖&lt;/h4&gt;
&lt;p&gt;Android Gradle 插件默认会使用一个修改版的 &lt;code&gt;android.jar&lt;/code&gt; 库来执行你的本地单元测试，这个修改版的库不包含任何实现代码。与此同时，你本地的单元测试调用的 Android 类会抛出一个异常。这样做是为了确保你的单元测试代码不依赖于任何特定的 Android 平台行为（除非你明确 mock 这些类）。&lt;/p&gt;
&lt;p&gt;你可以使用 mocking 框架来模拟你代码中的外部依赖，来轻松的测试你的组件与依赖交互的结果符合预期。通过 Mock 对象替换 Android 依赖，你可以将单元测试和 Android 系统的其他部分分离，而只验证某些有简单依赖的代码块能够被正确执行。Java 的模拟框架 &lt;a href=&#34;https://github.com/mockito/mockito&#34;&gt;Mockito&lt;/a&gt; 从 1.9.5 版本开始兼容 Android 单元测试。使用 Mockito， 你可以配置 mock 对象在调用时返回一些特定的值。&lt;/p&gt;
&lt;p&gt;使用此框架为你本地的单元测试添加 mock 对象，请遵守如下编程模式：&lt;/p&gt;
&lt;p&gt;1. 在 build.gradle 中导入 Mockito 库依赖，如上文 &lt;em&gt;设置你的测试环境&lt;/em&gt; 描述。&lt;/p&gt;
&lt;p&gt;2. 在你单元测试的类声明前添加 &lt;code&gt;@RunWith(MockitoJUnitRunner.class)&lt;/code&gt; 注解。这个注解告诉 Mockito 测试执行者验证开发者是否正确的使用此框架，同时简化 mock 对象的初始化。&lt;/p&gt;
&lt;p&gt;3. 要为一个 Android 依赖如 Context 创建 mock 对象，请在变量声明前添加 &lt;code&gt;@Mock&lt;/code&gt; 注解。&lt;/p&gt;
&lt;p&gt;4. 为了保持或者说模仿依赖行为，你可以使用 &lt;code&gt;when()&lt;/code&gt; 和 &lt;code&gt;thenReturn()&lt;/code&gt; 方法来指定当满足某一特定条件时返回某个值。&lt;/p&gt;
&lt;p&gt;下面的代码示例展示了在单元测试中如何 mock 一个 Context 对象。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; org.&lt;span style=&#34;color:#309&#34;&gt;hamcrest&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;MatcherAssert&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;assertThat&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; org.&lt;span style=&#34;color:#309&#34;&gt;hamcrest&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;CoreMatchers&lt;/span&gt;.&lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; org.&lt;span style=&#34;color:#309&#34;&gt;mockito&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Mockito&lt;/span&gt;.&lt;span style=&#34;color:#555&#34;&gt;*&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Test&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;runner&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;RunWith&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;mockito&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Mock&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;mockito&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;runners&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;MockitoJUnitRunner&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;content&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;SharedPreferences&lt;/span&gt;;
&lt;span style=&#34;color:#99f&#34;&gt;@RunWith&lt;/span&gt;(MockitoJUnitRunner.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; UnitTestSample {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; FAKE_STRING &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;HELLO WORLD&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#99f&#34;&gt;@Mock&lt;/span&gt;
Context &lt;span style=&#34;color:#c0f&#34;&gt;mMockContext&lt;/span&gt;;
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; readStringFromContext_LocalizedString() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Given a mocked Context injected into the object under test...
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; when(mMockContext.&lt;span style=&#34;color:#309&#34;&gt;getString&lt;/span&gt;(R.&lt;span style=&#34;color:#309&#34;&gt;string&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;hello_word&lt;/span&gt;))
.&lt;span style=&#34;color:#309&#34;&gt;thenReturn&lt;/span&gt;(FAKE_STRING);
ClassUnderTest &lt;span style=&#34;color:#c0f&#34;&gt;myObjectUnderTest&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ClassUnderTest(mMockContext);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// ...when the string is returned from the object under test...
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; String &lt;span style=&#34;color:#c0f&#34;&gt;result&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; myObjectUnderTest.&lt;span style=&#34;color:#309&#34;&gt;getHelloWorldString&lt;/span&gt;();
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// ...then the result should be the expected one.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; assertThat(result, is(FAKE_STRING));
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;想要学习更多如何使用 Mockito 框架库的内容，请参考 &lt;a href=&#34;http://site.mockito.org/mockito/docs/current/org/mockito/Mockito.html&#34;&gt;Mockito API reference&lt;/a&gt; 及 &lt;a href=&#34;https://github.com/googlesamples/android-testing/tree/master/unit/BasicSample&#34;&gt;sample code&lt;/a&gt; 中的 &lt;code&gt;SharedPreferencesHelperTest&lt;/code&gt; 类。&lt;/p&gt;
&lt;h4 id=&#34;error-method-not-mocked&#34;&gt;Error: &amp;ldquo;Method &amp;hellip; not mocked&amp;rdquo;&lt;/h4&gt;
&lt;p&gt;如果你运行的测试调用了没有 mock 的 Android SDK API，你会收到一个 method is not mocked 的错误。这是因为单元测试执行过程中使用的 &lt;code&gt;android.jar&lt;/code&gt; 文件没有包含任何实际的实现代码（这些 API 仅被提供在物理设备的系统镜像中）。&lt;/p&gt;
&lt;p&gt;相对的，所有的方法调用默认会抛出异常。这是为了确保你的单元测试中仅仅测试你的代码块，而你的代码不依赖于任何特定的 Android 平台行为（那些你没有明确模拟即 mock 的行为）。&lt;/p&gt;
&lt;p&gt;如果你认为抛出异常是个问题，你可以修改方法行为使它不抛出异常而返回空或者零，你需要在 &lt;code&gt;build.gradle&lt;/code&gt; 文件中添加如下的代码段：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;android &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;...&lt;/span&gt;
testOptions &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
unitTests&lt;span style=&#34;color:#555&#34;&gt;.&lt;/span&gt;&lt;span style=&#34;color:#309&#34;&gt;returnDefaultValues&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;true&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 应该谨慎设置 &lt;code&gt;returnDefaultValues&lt;/code&gt; 属性为 &lt;code&gt;true&lt;/code&gt; 。返回空或零值可能会使你的测试变得不可靠，将可能很难调试或者可能会允许失败的测试通过。此方法应该仅作为不能解决问题时的最后手段。&lt;/p&gt;
&lt;h4 id=&#34;运行本地单元测试&#34;&gt;运行本地单元测试&lt;/h4&gt;
&lt;p&gt;使用如下步骤运行你本地的单元测试：&lt;/p&gt;
&lt;p&gt;1. 确保你的工程已经和 Gradle 同步，点击工具栏的 &lt;strong&gt;Sync Project&lt;/strong&gt; &lt;img src=&#34;https://developer.android.com/images/tools/sync-project.png&#34; alt=&#34;img&#34; /&gt;按钮。&lt;/p&gt;
&lt;p&gt;2. 使用如下几种途径之一运行你的测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只运行一个测试，打开 &lt;strong&gt;Project&lt;/strong&gt; 窗口， 在这个测试上点击右键再点击 &lt;strong&gt;Run&lt;/strong&gt; &lt;img src=&#34;https://developer.android.com/images/tools/as-run.png&#34; alt=&#34;img&#34; /&gt;&lt;/li&gt;
&lt;li&gt;要测试某个类的所有测试，右键点击这个类或者类中的一个方法，再点击 &lt;strong&gt;Run&lt;/strong&gt; &lt;img src=&#34;https://developer.android.com/images/tools/as-run.png&#34; alt=&#34;img&#34; /&gt;&lt;/li&gt;
&lt;li&gt;要测试某个目录中的所有测试，右键点击这个目录，再选择 &lt;strong&gt;Run tests&lt;/strong&gt; &lt;img src=&#34;https://developer.android.com/images/tools/as-run.png&#34; alt=&#34;img&#34; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Android gradle 插件会编译位于默认目录 (&lt;code&gt;src/test/java/&lt;/code&gt;) 的本地单元测试，编译一个测试 app，然后使用默认的 test runner 类在本地执行它。之后 Android Studio 会在 Run 窗口显示执行结果。&lt;/p&gt;
&lt;h3 id=&#34;2-仪表-instrumented-单元测试&#34;&gt;2. 仪表 (Instrumented) 单元测试&lt;/h3&gt;
&lt;p&gt;本小节介绍如何编写 Instrumented 单元测试，这类测试不能通过 mock 来伪造依赖，需要运行在开发机或者模拟器上。&lt;/p&gt;
&lt;p&gt;参考 &lt;a href=&#34;https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests.html&#34;&gt;Building Instrumented Unit Tests&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Instrumented 测试或者称为仪表测试是运行在物理设备或者模拟器上，可以利用 Android 框架 API 及支持 API，如 Android 测试支持库。如果你的测试需要访问&lt;strong&gt;运行时&lt;/strong&gt;仪表信息如 APP 的 Context，或者你的测试需要真实的 Android 框架组件实现如 Parcelable 或 SharedPreferences 对象，则你应该创建 Instrumented 测试。&lt;/p&gt;
&lt;p&gt;使用 Instrumented 单元测试可以帮助你减少编写和维护 mock 代码。当然你也可以自由的使用上一节的 mocking 框架来模拟任何依赖关系。&lt;/p&gt;
&lt;h4 id=&#34;配置测试环境-1&#34;&gt;配置测试环境&lt;/h4&gt;
&lt;p&gt;在你的 Android Studio 工程中，你必须将 Instrumented 测试代码保存在 &lt;code&gt;module-name/src/androidTest/java/&lt;/code&gt; 目录下。在你创建工程时这个目录已经被默认创建，同时包含了一个 Instrumented 测试示例代码。&lt;/p&gt;
&lt;p&gt;在你开始前，你应该先在 Android SDK 里下载 Android Testing Support Library，这个支持库提供了一些API 允许你快速的编写和运行 instrumented 测试代码。此测试支持库包含了 JUnit 4 test runner (&lt;a href=&#34;https://developer.android.com/tools/testing-support-library/index.html#AndroidJUnitRunner&#34;&gt;AndroidJUnitRunner&lt;/a&gt; ) 和 UI 测试工具 (&lt;a href=&#34;https://developer.android.com/tools/testing-support-library/index.html#Espresso&#34;&gt;Espresso&lt;/a&gt; 和 &lt;a href=&#34;https://developer.android.com/tools/testing-support-library/index.html#UIAutomator&#34;&gt;UI Automator&lt;/a&gt;) 的 API。&lt;/p&gt;
&lt;p&gt;和本地单元测试一样，为了访问测试支持库提供的 API，你也需要配置测试依赖。为了简化你的测试开发，你应该包含 &lt;a href=&#34;https://github.com/hamcrest&#34;&gt;Hamcrest&lt;/a&gt; 库，使用这个库的匹配器 API，你可以创建更灵活的断言。&lt;/p&gt;
&lt;p&gt;在顶层的 &lt;code&gt;build.gradle&lt;/code&gt; 文件中，你需要指定如下依赖库：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;dependencies &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
androidTestCompile &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;com.android.support:support-annotations:24.0.0&amp;#39;&lt;/span&gt;
androidTestCompile &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;com.android.support.test:runner:0.5&amp;#39;&lt;/span&gt;
androidTestCompile &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;com.android.support.test:rules:0.5&amp;#39;&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Optional -- Hamcrest library
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; androidTestCompile &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;org.hamcrest:hamcrest-library:1.3&amp;#39;&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Optional -- UI testing with Espresso
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; androidTestCompile &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;com.android.support.test.espresso:espresso-core:2.2.2&amp;#39;&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Optional -- UI testing with UI Automator
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; androidTestCompile &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;com.android.support.test.uiautomator:uiautomator-v18:2.1.2&amp;#39;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; 如果你的配置中引入了 &lt;code&gt;support-annotations&lt;/code&gt; 编译依赖库和 &lt;code&gt;espresso-core&lt;/code&gt; 测试编译依赖库，那么你可能会遇到依赖冲突的失败提示。为了修改这个问题，你需要更新 &lt;code&gt;espresso-core&lt;/code&gt; 依赖如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;androidTestCompile&lt;span style=&#34;color:#555&#34;&gt;(&lt;/span&gt;&lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;com.android.support.test.espresso:espresso-core:2.2.2&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
exclude &lt;span style=&#34;color:#99f&#34;&gt;group:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;com.android.support&amp;#39;&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;,&lt;/span&gt; &lt;span style=&#34;color:#99f&#34;&gt;module:&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;support-annotations&amp;#39;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;})&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;要使用 JUnit 4 测试类，请确保指定 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html&#34;&gt;&lt;code&gt;AndroidJUnitRunner&lt;/code&gt;&lt;/a&gt; 为默认的仪表测试执行者，可以在 app 模块下的 &lt;code&gt;build.gradle&lt;/code&gt; 文件中加入如下代码：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;android &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
defaultConfig &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
testInstrumentationRunner &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.support.test.runner.AndroidJUnitRunner&amp;#34;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;创建-instrumented-单元测试类&#34;&gt;创建 Instrumented 单元测试类&lt;/h4&gt;
&lt;p&gt;你的 Instrumented 单元测试类应该按照 JUnit 4 测试类格式编写。要了解更多创建 JUnit 4 测试类和使用 JUnit 4 断言和注解，请阅读上一节本地单元测试中创建本地单元测试类的内容。&lt;/p&gt;
&lt;p&gt;要创建一个 instrumented JUnit 4 测试类，你需要为该类开头添加一个 &lt;code&gt;@RunWith(AndroidJUnit4.class)&lt;/code&gt; 的注解。如下示例代码展示了一个用来测试 &lt;code&gt;LogHistory&lt;/code&gt; 类是否正确实现 Parcelable 接口的 instrumented 单元测试。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;os&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Parcel&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;support&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;test&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;runner&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;AndroidJUnit4&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;util&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Pair&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Test&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;runner&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;RunWith&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;java&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;util&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;List&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; org.&lt;span style=&#34;color:#309&#34;&gt;hamcrest&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Matchers&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;is&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; org.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Assert&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;assertThat&lt;/span&gt;;
&lt;span style=&#34;color:#99f&#34;&gt;@RunWith&lt;/span&gt;(AndroidJUnit4.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;)
&lt;span style=&#34;color:#99f&#34;&gt;@SmallTest&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; LogHistoryAndroidUnitTest {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; TEST_STRING &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;This is a string&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;long&lt;/span&gt; TEST_LONG &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 12345678L;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;LogHistory&lt;/span&gt; mLogHistory;
&lt;span style=&#34;color:#99f&#34;&gt;@Before&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; createLogHistory() {
mLogHistory &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; LogHistory();
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; logHistory_ParcelableWriteRead() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Set up the Parcelable object to send and receive.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; mLogHistory.&lt;span style=&#34;color:#309&#34;&gt;addEntry&lt;/span&gt;(TEST_STRING, TEST_LONG);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Write the data.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; Parcel &lt;span style=&#34;color:#c0f&#34;&gt;parcel&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; Parcel.&lt;span style=&#34;color:#309&#34;&gt;obtain&lt;/span&gt;();
mLogHistory.&lt;span style=&#34;color:#309&#34;&gt;writeToParcel&lt;/span&gt;(parcel, mLogHistory.&lt;span style=&#34;color:#309&#34;&gt;describeContents&lt;/span&gt;());
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// After you&amp;#39;re done with writing, you need to reset the parcel for reading.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; parcel.&lt;span style=&#34;color:#309&#34;&gt;setDataPosition&lt;/span&gt;(0);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Read the data.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; LogHistory &lt;span style=&#34;color:#c0f&#34;&gt;createdFromParcel&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; LogHistory.&lt;span style=&#34;color:#309&#34;&gt;CREATOR&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;createFromParcel&lt;/span&gt;(parcel);
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Pair&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;String, Long&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;createdFromParcelData&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; createdFromParcel.&lt;span style=&#34;color:#309&#34;&gt;getData&lt;/span&gt;();
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Verify that the received data is correct.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; assertThat(createdFromParcelData.&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;(), is(1));
assertThat(createdFromParcelData.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;(0).&lt;span style=&#34;color:#309&#34;&gt;first&lt;/span&gt;, is(TEST_STRING));
assertThat(createdFromParcelData.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;(0).&lt;span style=&#34;color:#309&#34;&gt;second&lt;/span&gt;, is(TEST_LONG));
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;创建测试套件&#34;&gt;创建测试套件&lt;/h4&gt;
&lt;p&gt;要组织 instrumented 单元测试，开发者可以将这些测试分组，之后将这些测试一起执行。测试套件可以被嵌套，测试套件可以和其他测试套件组合在一起来同时测试其他类组件。&lt;/p&gt;
&lt;p&gt;一个测试套件被包含在一个测试包中，类似于 main 应用包。按惯例，测试套件的包名通常会以 &lt;code&gt;.suite&lt;/code&gt; 结尾，例如 &lt;code&gt;com.example.android.testing.mysample.suite&lt;/code&gt;。&lt;/p&gt;
&lt;p&gt;要为单元测试创建一个测试套件，请导入 JUnit &lt;a href=&#34;http://junit.sourceforge.net/javadoc/org/junit/runner/RunWith.html&#34;&gt;&lt;code&gt;RunWith&lt;/code&gt;&lt;/a&gt; 和 &lt;a href=&#34;http://junit.sourceforge.net/javadoc/org/junit/runners/Suite.html&#34;&gt;&lt;code&gt;Suite&lt;/code&gt;&lt;/a&gt; 类。在你的测试套件类中，添加 &lt;code&gt;@RunWith(Suite.class)&lt;/code&gt; 和 &lt;code&gt;@Suite.SuitClasses()&lt;/code&gt; 注解。然后将独立的测试类或测试套件加入到 &lt;code&gt;@Suite.SuiteClasses()&lt;/code&gt; 的参数中。下面示例代码展示了一个名称为 &lt;code&gt;UnitTestSuite&lt;/code&gt; 的测试套件，它包含了 &lt;code&gt;CalculatorInstrumentationTest&lt;/code&gt; and &lt;code&gt;CalculatorAddParameterizedTest&lt;/code&gt; 两个单元测试。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;com&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;example&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;testing&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;mysample&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;CalculatorAddParameterizedTest&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;com&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;example&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;testing&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;mysample&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;CalculatorInstrumentationTest&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;runner&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;RunWith&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;runners&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Suite&lt;/span&gt;;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Runs all unit tests.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#99f&#34;&gt;@RunWith&lt;/span&gt;(Suite.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;)
&lt;span style=&#34;color:#99f&#34;&gt;@Suite.SuiteClasses&lt;/span&gt;({CalculatorInstrumentationTest.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;,
CalculatorAddParameterizedTest.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;})
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; UnitTestSuite {}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;运行-instrumented-单元测试&#34;&gt;运行 Instrumented 单元测试&lt;/h4&gt;
&lt;p&gt;请按照如下步骤运行 Instrumented 单元测试：&lt;/p&gt;
&lt;p&gt;1. 确保你的工程和 Gradle 保持同步，点击工具栏的 &lt;strong&gt;Sync Project&lt;/strong&gt; &lt;img src=&#34;https://developer.android.com/images/tools/sync-project.png&#34; alt=&#34;img&#34; /&gt; 按钮。&lt;/p&gt;
&lt;p&gt;2. 按照以下步骤之一运行测试：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;只运行一个测试，打开 &lt;strong&gt;Project&lt;/strong&gt; 窗口， 在这个测试上点击右键再点击 &lt;strong&gt;Run&lt;/strong&gt; &lt;img src=&#34;https://developer.android.com/images/tools/as-run.png&#34; alt=&#34;img&#34; /&gt;&lt;/li&gt;
&lt;li&gt;要测试某个类的所有测试，右键点击这个类或者类中的一个方法，再点击 &lt;strong&gt;Run&lt;/strong&gt; &lt;img src=&#34;https://developer.android.com/images/tools/as-run.png&#34; alt=&#34;img&#34; /&gt;&lt;/li&gt;
&lt;li&gt;要测试某个目录中的所有测试，右键点击这个目录，再选择 &lt;strong&gt;Run tests&lt;/strong&gt; &lt;img src=&#34;https://developer.android.com/images/tools/as-run.png&#34; alt=&#34;img&#34; /&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;位于默认位置 (&lt;code&gt;src/androidTest/java/&lt;/code&gt;) 的 Instrumented 测试代码会被 Android Gradle 插件编译，生成一个测试 APK 和一个生产 APK 文件，同时安装两个 APK 文件到已连接的设备或模拟器，然后运行测试。之后 Android Studio 会在 Run 窗口展示 Instrumented 测试的执行结果。&lt;/p&gt;
&lt;h3 id=&#34;3-自动化用户界面测试&#34;&gt;3. 自动化用户界面测试&lt;/h3&gt;
&lt;p&gt;本小节介绍如何编写运行于单 APP 和多个 APP ，用于验证用户交互处理是否正确的测试。&lt;/p&gt;
&lt;p&gt;参考 &lt;a href=&#34;https://developer.android.com/training/testing/ui-testing/index.html&#34;&gt;Automating User Interface Tests&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;用户界面(UI) 测试可以确保应用满足功能要求，同时也验证应用具有较高的质量，由此保证应用能够成功被用户接受。&lt;/p&gt;
&lt;p&gt;一个简单的 UI 测试方法是让测试人员在目标应用上执行一系列预定的用户操作，然后验证应用行为是否符合预期。然而，这种人工方式有很多缺点如耗时、乏味、容易出错。一个更高效的方法是编写可以运行触发特定操作的自动化 UI 测试。自动化测试的方法有很多优点如快速、可靠、可重复。&lt;/p&gt;
&lt;p&gt;要自动化UI 测试你的应用，你需要将测试代码放置到 (&lt;code&gt;src/androidTest/java&lt;/code&gt;) 目录。Android 的 Gradle 插件会基于你的测试代码编译一个测试 app，然后将它加载到目标设备上。你可以在你的测试代码中，使用 UI 测试框架模拟用户行为来操作目标应用，达到测试特定使用场景的目的。&lt;/p&gt;
&lt;p&gt;一般会创建两种 Android 应用自动化 UI 测试：&lt;/p&gt;
&lt;p&gt;1. 单应用 UI 测试：这种测试用来验证目标应用在用户执行某特定操作或输入后能得到期望的行为。例如验证点击了设置菜单可以跳转到设置界面。这种测试允许开发者检查目标应用是否能在用户交互后响应了正确的界面。 UI 测试框架如 Espresso 允许开发者通过编程模拟用户操作并测试复杂的应用内用户交互行为。&lt;/p&gt;
&lt;p&gt;2. 跨应用 UI 测试：这种测试用来验证多个应用之间的交互行为或应用于系统之间的交互行为。例如，开发者可以测试相机应用将图片分享给三方的社交媒体应用或分享给 Android 默认相册应用行为是否正确。支持跨应用的 UI 测试框架如 UI Automator 允许你创建此类场景的测试。&lt;/p&gt;
&lt;p&gt;本节后的内容将介绍如何使用 Android 测试支持库提供的工具和 API 来编写这些自动化测试。开始之前你应该已经按照上文配置测试环境的内容下载安装了测试支持库。&lt;/p&gt;
&lt;h4 id=&#34;3-1-单应用-ui-测试-espresso&#34;&gt;3.1. 单应用 UI 测试 - Espresso&lt;/h4&gt;
&lt;p&gt;测试单应用的用户交互行为可以帮助开发者确保用户在与应用交互时不会遇到不期望的结果或者糟糕的用户体验。如果开发者需要验证应用的用户界面功能正常，则应该习惯于创建用户界面测试。&lt;/p&gt;
&lt;p&gt;由 Android 测试支持库提供的 Espresso 测试框架，提供了编写 UI 测试的 API 来模拟单应用用户交互行为。 Espresso 库可以运行在 Android 2.3.3 (API level 10) 和更高的 设备上。在测试应用时使用 Espresso 的一个关键的好处是它提供了自动化同步测试行为。Espresso 检测到应用的主线程空闲后，会寻找合适的时机运行你的测试命令，这样提高了开发者编写的测试的可靠性。这种特性同时可以将开发者从不得不增加如 &lt;code&gt;Thread.sleep()&lt;/code&gt; 延时到测试代码的应急方案中解放出来。&lt;/p&gt;
&lt;h5 id=&#34;配置-espresso&#34;&gt;配置 Espresso&lt;/h5&gt;
&lt;p&gt;开始编写 Espresso UI 测试前，开发者需要确保正确放置测试代码到目录，并且已经添加了工程依赖，如上文中配置测试环境的内容。&lt;/p&gt;
&lt;p&gt;在你 app 模块的 build.gradle 文件中，你必须已经配置了 Espresso 库依赖：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;dependencies &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Other dependencies ...
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; androidTestCompile &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;com.android.support.test.espresso:espresso-core:2.2.2&amp;#39;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;关闭测试设备中的动画效果 &amp;ndash; 当测试设备中有系统动画时，可能会引起不可预料的结果而导致你的测试失败。在设备或模拟器中的 &lt;code&gt;设置 - 开发者选项&lt;/code&gt; 中关闭所有的下列动画选项：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Window animation scale&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transition animation scale&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Animator duration scale&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你想要配置你的工程使用所有的 Espresso 功能而不仅仅是核心提供的 API，请阅读这里的 &lt;a href=&#34;https://google.github.io/android-testing-support-library/docs/espresso/index.html&#34;&gt;资源&lt;/a&gt; 。&lt;/p&gt;
&lt;h5 id=&#34;创建-espresso-测试类&#34;&gt;创建 Espresso 测试类&lt;/h5&gt;
&lt;p&gt;请遵守如下编程模型来创建一个 Espresso 测试类：&lt;/p&gt;
&lt;p&gt;1. 通过 &lt;code&gt;onView()&lt;/code&gt; 方法找到你想要测试的 Acvitivity UI 组件，或者&lt;code&gt;onData()&lt;/code&gt; 方法找到&lt;code&gt;AdapterView&lt;/code&gt;中的元素。 例如应用中的登录按钮。&lt;/p&gt;
&lt;p&gt;2. 通过 &lt;code&gt;ViewInteraction.perform()&lt;/code&gt; 或 &lt;code&gt;DataInteraction.perform()&lt;/code&gt;方法模拟一个特定的用户交互操作例如点击登录按钮。要序列化的在该组件上执行多个操作，可以在方法中使用逗号分隔的列表参数。&lt;/p&gt;
&lt;p&gt;3. 如果有必要，重复上边的步骤，来模拟用户在多个 Activity 间操作目标应用。&lt;/p&gt;
&lt;p&gt;4. 使用 &lt;code&gt;ViewAssertions&lt;/code&gt; 方法来检查上述交互操作执行后 UI 返回了期望的状态或行为。&lt;/p&gt;
&lt;p&gt;这几步在下文会有更加详细的描述。下面示例是一个基本的代码片段：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;onView(withId(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;my_view&lt;/span&gt;)) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// withId(R.id.my_view) is a ViewMatcher
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; .&lt;span style=&#34;color:#309&#34;&gt;perform&lt;/span&gt;(click()) &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// click() is a ViewAction
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; .&lt;span style=&#34;color:#309&#34;&gt;check&lt;/span&gt;(matches(isDisplayed())); &lt;span style=&#34;color:#555&#34;&gt;//&lt;/span&gt; matches(isDisplayed()) is &lt;span style=&#34;color:#c0f&#34;&gt;a&lt;/span&gt; ViewAssertion&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h5 id=&#34;结合-activitytestrule-使用-espresso&#34;&gt;结合 ActivityTestRule 使用 Espresso&lt;/h5&gt;
&lt;p&gt;下面部分描述了如何创建一个 JUnit 4 风格的 Espresso 测试，以及如何使用 &lt;code&gt;ActivityTestRule&lt;/code&gt; 来减少重复的代码。通过使用 &lt;code&gt;ActivityTestRule&lt;/code&gt;， 测试框架会为每个注解为 &lt;code&gt;@Test&lt;/code&gt; 的方法运行要测试的 Activity。测试前，注解为 &lt;code&gt;@Before&lt;/code&gt; 的所有方法都会被执行。在每个测试完成后，所有注解为 &lt;code&gt;@After&lt;/code&gt; 的方法都会被执行，之后测试框架会关闭 Activity。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;package&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;com&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;example&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;testing&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;espresso&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;BasicSample&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Before&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Rule&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Test&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;runner&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;RunWith&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;support&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;test&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;rule&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;ActivityTestRule&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;support&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;test&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;runner&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;AndroidJUnit4&lt;/span&gt;;
...
&lt;span style=&#34;color:#99f&#34;&gt;@RunWith&lt;/span&gt;(AndroidJUnit4.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;)
&lt;span style=&#34;color:#99f&#34;&gt;@LargeTest&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; ChangeTextBehaviorTest {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; mStringToBetyped;
&lt;span style=&#34;color:#99f&#34;&gt;@Rule&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ActivityTestRule&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;MainActivity&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;mActivityRule&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ActivityTestRule&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;(
MainActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
&lt;span style=&#34;color:#99f&#34;&gt;@Before&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; initValidString() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Specify a valid string.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; mStringToBetyped &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Espresso&amp;#34;&lt;/span&gt;;
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; changeText_sameActivity() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Type text and then press the button.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; onView(withId(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;editTextUserInput&lt;/span&gt;))
.&lt;span style=&#34;color:#309&#34;&gt;perform&lt;/span&gt;(typeText(mStringToBetyped), closeSoftKeyboard());
onView(withId(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;changeTextBt&lt;/span&gt;)).&lt;span style=&#34;color:#309&#34;&gt;perform&lt;/span&gt;(click());
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Check that the text was changed.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; onView(withId(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;textToBeChanged&lt;/span&gt;))
.&lt;span style=&#34;color:#309&#34;&gt;check&lt;/span&gt;(matches(withText(mStringToBetyped)));
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h5 id=&#34;访问-ui-组件&#34;&gt;访问 UI 组件&lt;/h5&gt;
&lt;p&gt;在 Espresso 与应用交互前，开发者必须指定 UI 组件或 *view*， Espresso 支持使用 &lt;a href=&#34;http://hamcrest.org/&#34;&gt;Hamcrest matchers&lt;/a&gt; 来匹配查找应用或者 adapter 中的 view 。&lt;/p&gt;
&lt;p&gt;要找到 view， 使用 &lt;code&gt;onView()&lt;/code&gt; 方法并传入一个你的目标视图的 &lt;code&gt;view matcher&lt;/code&gt;。在下一节 指定一个 view matcher 有更详细的介绍。 &lt;code&gt;onView()&lt;/code&gt; 方法会返回一个 &lt;code&gt;ViewInteraction&lt;/code&gt; 对象来允许开发者的测试和视图交互。然而，如果你想要获取一个 RecyclerView 中的视图，调用 &lt;code&gt;onView()&lt;/code&gt; 方法可能会失效。在这种场景下，应该使用下文的在 AdapterView 中获取视图的方法。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; &lt;code&gt;onView()&lt;/code&gt; 方法不会检查开发者指定的视图是否有效，Espresso 也仅在当前的布局层搜索匹配，如果找不到视图则会抛出一个 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/NoMatchingViewException.html&#34;&gt;&lt;code&gt;NoMatchingViewException&lt;/code&gt;&lt;/a&gt; 异常。&lt;/p&gt;
&lt;p&gt;下面的代码片段展示了开发者如何编写一个测试来访问一个 &lt;code&gt;EditText&lt;/code&gt; 成员，输入一个字符串，关闭虚拟键盘，然后执行一个按钮点击。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; testChangeText_sameActivity() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Type text and then press the button.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; onView(withId(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;editTextUserInput&lt;/span&gt;))
.&lt;span style=&#34;color:#309&#34;&gt;perform&lt;/span&gt;(typeText(STRING_TO_BE_TYPED), closeSoftKeyboard());
onView(withId(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;changeTextButton&lt;/span&gt;)).&lt;span style=&#34;color:#309&#34;&gt;perform&lt;/span&gt;(click());
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Check that the text was changed.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; ...
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h5 id=&#34;指定一个视图匹配器-view-matcher&#34;&gt;指定一个视图匹配器 View Matcher&lt;/h5&gt;
&lt;p&gt;开发者可以使用如下方法来指定一个视图匹配器：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;使用 &lt;code&gt;ViewMatchers&lt;/code&gt; 类中的方法，例如，要查找一个显示了某字符串的视图，可以使用如下方法：&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;onView(withText(&amp;quot;Sign-in&amp;quot;));
&lt;/code&gt;&lt;/pre&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;类似的可以使用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/matcher/ViewMatchers.html#withId(int)&#34;&gt;&lt;code&gt;withId()&lt;/code&gt;&lt;/a&gt; 方法并提供视图的资源 ID (&lt;code&gt;R.id&lt;/code&gt;) 如下所示：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt; onView(withId(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;button_signin&lt;/span&gt;));&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Android 的资源 ID 在设计上并不是唯一的。 如果开发者编写的测试尝试匹配一个被多个视图使用了的资源 ID Espresso 则会抛出 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/AmbiguousViewMatcherException.html&#34;&gt;&lt;code&gt;AmbiguousViewMatcherException&lt;/code&gt;&lt;/a&gt; 异常。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;使用 Hamcrest &lt;code&gt;Matchers&lt;/code&gt; 类。可以使用 &lt;code&gt;allOf()&lt;/code&gt; 方法来连接多个匹配器，如 &lt;code&gt;containsString()&lt;/code&gt; 和 &lt;code&gt;instanceOf()&lt;/code&gt; 。这个方法允许开发者更精确的过滤匹配结果，如下代码所示：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;onView(allOf(withId(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;button_signin&lt;/span&gt;), withText(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Sign-in&amp;#34;&lt;/span&gt;)));&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;可以使用 &lt;code&gt;not&lt;/code&gt; 关键字来过滤不匹配的视图，如下代码所示：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt; onView(allOf(withId(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;button_signin&lt;/span&gt;), not(withText(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Sign-out&amp;#34;&lt;/span&gt;))));&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;要在代码中使用这些方法， 请导入 &lt;code&gt;org.hamcrest.Matchers&lt;/code&gt; 包。要学习更多关于 Hamcrest 匹配器的内容，请阅读 &lt;a href=&#34;http://hamcrest.org/&#34;&gt;Hamcrest 网站&lt;/a&gt; 。&lt;/p&gt;
&lt;h5 id=&#34;在-adapterview-中查找视图&#34;&gt;在 AdapterView 中查找视图&lt;/h5&gt;
&lt;p&gt;在一个 &lt;code&gt;AdapterView&lt;/code&gt; 部件中， 子视图是被运行时动态加载的。如果要测试的目标视图在 &lt;code&gt;AdapterView&lt;/code&gt; (例如 &lt;code&gt;ListView&lt;/code&gt;, &lt;code&gt;GridView&lt;/code&gt;, 或 &lt;code&gt;Spinner&lt;/code&gt;) 中， &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/Espresso.html#onView(org.hamcrest.Matcher)&#34;&gt;&lt;code&gt;onView()&lt;/code&gt;&lt;/a&gt; 可能会失效，因为可能只有视图的一部分被加载在可见布局层。&lt;/p&gt;
&lt;p&gt;取而代之的，可以使用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/Espresso.html#onData(org.hamcrest.Matcher)&#34;&gt;&lt;code&gt;onData()&lt;/code&gt;&lt;/a&gt; 方法来获取一个 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/DataInteraction.html&#34;&gt;&lt;code&gt;DataInteraction&lt;/code&gt;&lt;/a&gt; 对象然后访问目标视图元素。 Espresso 会处理加载目标视图到当前的视图层。 Espresso 同样会处理目标视图元素的滚动操作， 同时将目标视图聚焦。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/Espresso.html#onData(org.hamcrest.Matcher)&#34;&gt;&lt;code&gt;onData()&lt;/code&gt;&lt;/a&gt; 方法不会检查开发者指定的数据对应于某个视图。 Espresso 只在当前视图层查找。 如果没有找到任何匹配，将会抛出 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/NoMatchingViewException.html&#34;&gt;&lt;code&gt;NoMatchingViewException&lt;/code&gt;&lt;/a&gt; 异常。&lt;/p&gt;
&lt;p&gt;下面的代码展示了如何使用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/Espresso.html#onData(org.hamcrest.Matcher)&#34;&gt;&lt;code&gt;onData()&lt;/code&gt;&lt;/a&gt; 方法和 Hamcrest 来匹配查找包含给定字符串的行。 在这个例子中 &lt;code&gt;LongListActivity&lt;/code&gt; 类包含了由 &lt;code&gt;SimpleAdapter&lt;/code&gt; 暴露的字符串列表。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;onData(allOf(is(instanceOf(Map.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;)),
hasEntry(equalTo(LongListActivity.&lt;span style=&#34;color:#309&#34;&gt;ROW_TEXT&lt;/span&gt;), is(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;test input&amp;#34;&lt;/span&gt;)));&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h5 id=&#34;模拟或执行用户操作&#34;&gt;模拟或执行用户操作&lt;/h5&gt;
&lt;p&gt;使用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/ViewInteraction.html#perform(android.support.test.espresso.ViewAction...)&#34;&gt;&lt;code&gt;ViewInteraction.perform()&lt;/code&gt;&lt;/a&gt; 或 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/DataInteraction.html#perform(android.support.test.espresso.ViewAction...)&#34;&gt;&lt;code&gt;DataInteraction.perform()&lt;/code&gt;&lt;/a&gt; 方法来模拟用户在 UI 组件上的交互行为。 开发者必须在参数中传递一个或多个 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/ViewAction.html&#34;&gt;&lt;code&gt;ViewAction&lt;/code&gt;&lt;/a&gt; 对象，Espresso 会按照给定的参数顺序在主线程执行每个操作。&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/action/ViewActions.html&#34;&gt;&lt;code&gt;ViewActions&lt;/code&gt;&lt;/a&gt; 类提供了一系列的帮助方法用来模拟常见行为。开发者可以使用这些方法来快捷方便的模拟行为而不是自己去创建配置独立的 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/ViewAction.html&#34;&gt;&lt;code&gt;ViewAction&lt;/code&gt;&lt;/a&gt; 对象。包括如下行为：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/action/ViewActions.html#click()&#34;&gt;&lt;code&gt;ViewActions.click()&lt;/code&gt;&lt;/a&gt;：点击视图。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/action/ViewActions.html#typeText(java.lang.String)&#34;&gt;&lt;code&gt;ViewActions.typeText()&lt;/code&gt;&lt;/a&gt;：点击视图并输入字符串。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/action/ViewActions.html#scrollTo()&#34;&gt;&lt;code&gt;ViewActions.scrollTo()&lt;/code&gt;&lt;/a&gt;： 滚动到视图， 目标视图必须是 &lt;code&gt;ScrollView&lt;/code&gt; 子类而且它的&lt;a href=&#34;http://developer.android.com/reference/android/view/View.html#attr_android:visibility&#34;&gt;&lt;code&gt;android:visibility&lt;/code&gt;&lt;/a&gt; 属性必须是 &lt;code&gt;VISIBLE&lt;/code&gt;。对于扩展了 &lt;code&gt;AdapterView&lt;/code&gt; (如 &lt;code&gt;ListView&lt;/code&gt;) 的视图，&lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/Espresso.html#onData(org.hamcrest.Matcher)&#34;&gt;&lt;code&gt;onData()&lt;/code&gt;&lt;/a&gt; 方法会处理好滚动。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/action/ViewActions.html#pressKey(int)&#34;&gt;&lt;code&gt;ViewActions.pressKey()&lt;/code&gt;&lt;/a&gt;：执行一个特定的按键事件。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/action/ViewActions.html#clearText()&#34;&gt;&lt;code&gt;ViewActions.clearText()&lt;/code&gt;&lt;/a&gt;： 清除目标上的字符。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果目标视图在 &lt;code&gt;ScrollView&lt;/code&gt; 内， 执行 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/action/ViewActions.html#scrollTo()&#34;&gt;&lt;code&gt;ViewActions.scrollTo()&lt;/code&gt;&lt;/a&gt; 操作会先将目标视图展示到屏幕上，再执行其他操作。如果视图已经被显示在界面上。则 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/action/ViewActions.html#scrollTo()&#34;&gt;&lt;code&gt;ViewActions.scrollTo()&lt;/code&gt;&lt;/a&gt; 操作不会执行任何动作。&lt;/p&gt;
&lt;h5 id=&#34;使用-espresso-intents-隔离测试-activity&#34;&gt;使用 Espresso Intents 隔离测试 Activity&lt;/h5&gt;
&lt;p&gt;&lt;a href=&#34;https://google.github.io/android-testing-support-library/docs/espresso/intents/index.html&#34;&gt;Espresso Intents&lt;/a&gt; 可以验证并保持应用发出的 Intent 。 使用Espresso Intents ，开发者可以拦截并保持发出的 Intent 和返回的结果，并发送他们到要测试的组件，从而达到隔离测试 app、activity、 或 service 的目的。&lt;/p&gt;
&lt;p&gt;要使用 Espresso Intents 做测试， 需要添加如下内容到应用的 &lt;code&gt;build.gradle&lt;/code&gt; 文件：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;dependencies &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Other dependencies ...
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; androidTestCompile &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;com.android.support.test.espresso:espresso-intents:2.2.2&amp;#39;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;要测试一个 Intent ，你需要创建一个类似于 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/rule/ActivityTestRule.html&#34;&gt;ActivityTestRule&lt;/a&gt; 的 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/intent/rule/IntentsTestRule.html&#34;&gt;IntentsTestRule&lt;/a&gt; 类实例。&lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/intent/rule/IntentsTestRule.html&#34;&gt;IntentsTestRule&lt;/a&gt; 会在每个测试前初始化 Espresso Intents ， 终止 activity ， 并在每次测试完成后释放 Espresso Intents 。&lt;/p&gt;
&lt;p&gt;如下测试类代码片段展示了如何测试 intent ，它测试了 &lt;a href=&#34;https://developer.android.com/training/basics/firstapp/index.html&#34;&gt;Building Your First App&lt;/a&gt; 教程中创建的 activity 和intent。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Large&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;@RunWith&lt;/span&gt;(AndroidJUnit4.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; SimpleIntentTest {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; MESSAGE &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;This is a test&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; PACKAGE_NAME &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.example.myfirstapp&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/* Instantiate an IntentsTestRule object. */&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;@Rule&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;IntentsTestRule&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;MainActivity&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;mIntentsRule&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; IntentsTestRule&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;(MainActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; verifyMessageSentToMessageActivity() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Types a message into a EditText element.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; onView(withId(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;edit_message&lt;/span&gt;))
.&lt;span style=&#34;color:#309&#34;&gt;perform&lt;/span&gt;(typeText(MESSAGE), closeSoftKeyboard());
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Clicks a button to send the message to another
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// activity through an explicit intent.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; onView(withId(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;send_message&lt;/span&gt;)).&lt;span style=&#34;color:#309&#34;&gt;perform&lt;/span&gt;(click());
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Verifies that the DisplayMessageActivity received an intent
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// with the correct package name and message.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; intended(allOf(
hasComponent(hasShortClassName(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;.DisplayMessageActivity&amp;#34;&lt;/span&gt;)),
toPackage(PACKAGE_NAME),
hasExtra(MainActivity.&lt;span style=&#34;color:#309&#34;&gt;EXTRA_MESSAGE&lt;/span&gt;, MESSAGE)));
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;更多关于 Espresso Intents 的内容， 请阅读 &lt;a href=&#34;https://google.github.io/android-testing-support-library/docs/espresso/intents/index.html&#34;&gt;Espresso Intents documentation on the Android Testing Support Library 网站&lt;/a&gt; 。你也可以下载并阅读 &lt;a href=&#34;https://github.com/googlesamples/android-testing/tree/master/ui/espresso/IntentsBasicSample&#34;&gt;IntentsBasicSample&lt;/a&gt; 和 &lt;a href=&#34;https://github.com/googlesamples/android-testing/tree/master/ui/espresso/IntentsAdvancedSample&#34;&gt;IntentsAdvancedSample&lt;/a&gt; 代码示例。&lt;/p&gt;
&lt;h5 id=&#34;使用-espresso-web-测试-webview&#34;&gt;使用 Espresso Web 测试 WebView&lt;/h5&gt;
&lt;p&gt;Espresso Web 可以用来测试 Activity 内的 &lt;code&gt;WebView&lt;/code&gt; 组件。它使用 &lt;a href=&#34;http://docs.seleniumhq.org/docs/03_webdriver.jsp&#34;&gt;WebDriver API&lt;/a&gt; 来检查和控制 &lt;code&gt;WebView&lt;/code&gt; 的行为。&lt;/p&gt;
&lt;p&gt;开始使用 Espresso Web 前，你需要添加如下依赖到 app 的 build.gradle 文件：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;dependencies &lt;span style=&#34;color:#555&#34;&gt;{&lt;/span&gt;
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Other dependencies ...
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; androidTestCompile &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;com.android.support.test.espresso:espresso-web:2.2.2&amp;#39;&lt;/span&gt;
&lt;span style=&#34;color:#555&#34;&gt;}&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;使用 Espresso Web 创建测试， 实例化 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/rule/ActivityTestRule.html&#34;&gt;ActivityTestRule&lt;/a&gt; 对象来测试 activity 时，开发者需要启用 &lt;code&gt;WebView&lt;/code&gt; 上的 JavaScript 支持。在测试中， 开发者可以选择展示在 &lt;code&gt;WebView&lt;/code&gt; 上的 HTML 元素，然后模拟用户交互行为。例如可以在一个 text box 中输入字符然后点击一个按钮。当操作执行完成后，开发者可以验证网页上的结果是不是符合预期。&lt;/p&gt;
&lt;p&gt;在下面的代码中，测试了一个 ID 为 &amp;lsquo;webview&amp;rsquo; 的 &lt;code&gt;WebView&lt;/code&gt; 。 测试单元&lt;code&gt;verifyValidInputYieldsSuccesfulSubmission()&lt;/code&gt; 选择了网页上的一个 &lt;code&gt;&amp;lt;input&amp;gt;&lt;/code&gt; 元素，输入一些字符，然后检查出现在另一个元素上的字符串。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@LargeTest&lt;/span&gt;
&lt;span style=&#34;color:#99f&#34;&gt;@RunWith&lt;/span&gt;(AndroidJUnit4.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; WebViewActivityTest {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; MACCHIATO &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Macchiato&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; DOPPIO &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Doppio&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#99f&#34;&gt;@Rule&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;ActivityTestRule&lt;/span&gt; mActivityRule &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ActivityTestRule(WebViewActivity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;,
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/* Initial touch mode */&lt;/span&gt;, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;false&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;/* launch activity */&lt;/span&gt;) {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; afterActivityLaunched() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Enable JavaScript.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; onWebView().&lt;span style=&#34;color:#309&#34;&gt;forceJavascriptEnabled&lt;/span&gt;();
}
}
&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; typeTextInInput_clickButton_SubmitsForm() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Lazily launch the Activity with a custom start Intent per test
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; mActivityRule.&lt;span style=&#34;color:#309&#34;&gt;launchActivity&lt;/span&gt;(withWebFormIntent());
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Selects the WebView in your layout.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// If you have multiple WebViews you can also use a
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// matcher to select a given WebView, onWebView(withId(R.id.web_view)).
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; onWebView()
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Find the input element by ID
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; .&lt;span style=&#34;color:#309&#34;&gt;withElement&lt;/span&gt;(findElement(Locator.&lt;span style=&#34;color:#309&#34;&gt;ID&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;text_input&amp;#34;&lt;/span&gt;))
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Clear previous input
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; .&lt;span style=&#34;color:#309&#34;&gt;perform&lt;/span&gt;(clearElement())
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Enter text into the input element
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; .&lt;span style=&#34;color:#309&#34;&gt;perform&lt;/span&gt;(DriverAtoms.&lt;span style=&#34;color:#309&#34;&gt;webKeys&lt;/span&gt;(MACCHIATO))
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Find the submit button
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; .&lt;span style=&#34;color:#309&#34;&gt;withElement&lt;/span&gt;(findElement(Locator.&lt;span style=&#34;color:#309&#34;&gt;ID&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;submitBtn&amp;#34;&lt;/span&gt;))
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Simulate a click via JavaScript
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; .&lt;span style=&#34;color:#309&#34;&gt;perform&lt;/span&gt;(webClick())
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Find the response element by ID
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; .&lt;span style=&#34;color:#309&#34;&gt;withElement&lt;/span&gt;(findElement(Locator.&lt;span style=&#34;color:#309&#34;&gt;ID&lt;/span&gt;, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;response&amp;#34;&lt;/span&gt;))
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Verify that the response page contains the entered text
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; .&lt;span style=&#34;color:#309&#34;&gt;check&lt;/span&gt;(webMatches(getText(), containsString(MACCHIATO)));
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;要学习更多关于 Espresso Web 的内容，请阅读 &lt;a href=&#34;https://google.github.io/android-testing-support-library/docs/espresso/web/index.html&#34;&gt;Espresso Web documentation on the Android Testing Support Library 网站&lt;/a&gt;。 你也可以下载并阅读 &lt;a href=&#34;https://github.com/googlesamples/android-testing/tree/master/ui/espresso/WebBasicSample&#34;&gt;Espresso Web 示例代码&lt;/a&gt; 。&lt;/p&gt;
&lt;h5 id=&#34;验证结果&#34;&gt;验证结果&lt;/h5&gt;
&lt;p&gt;调用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/ViewInteraction.html#check(android.support.test.espresso.ViewAssertion)&#34;&gt;&lt;code&gt;ViewInteraction.check()&lt;/code&gt;&lt;/a&gt; 或 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/DataInteraction.html#check(android.support.test.espresso.ViewAssertion)&#34;&gt;&lt;code&gt;DataInteraction.check()&lt;/code&gt;&lt;/a&gt; 方法来断言用户界面符合期望的状态。 开发者必须将 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/ViewAssertion.html&#34;&gt;&lt;code&gt;ViewAssertion&lt;/code&gt;&lt;/a&gt; 对象作为参数传入。 如果断言失败，Espresso 会抛出一个 &lt;code&gt;AssertionFailedError&lt;/code&gt; 错误。&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/assertion/ViewAssertions.html&#34;&gt;&lt;code&gt;ViewAssertions&lt;/code&gt;&lt;/a&gt; 类提供了一些列一般场景断言的帮助方法。可以使用的断言包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/assertion/ViewAssertions.html#doesNotExist()&#34;&gt;&lt;code&gt;doesNotExist&lt;/code&gt;&lt;/a&gt;: 断言当前视图界面不存在指定的视图。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/assertion/ViewAssertions.html#matches(org.hamcrest.Matcher)&#34;&gt;&lt;code&gt;matches&lt;/code&gt;&lt;/a&gt;: 断言指定的视图存在于当前的视图层且它的状态匹配给定的 Hamcrest 匹配器。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/espresso/assertion/ViewAssertions.html#selectedDescendantsMatch(org.hamcrest.Matcher, org.hamcrest.Matcher)&#34;&gt;&lt;code&gt;selectedDescendentsMatch&lt;/code&gt;&lt;/a&gt;： 断言父视图中存在指定的子视图，且它的状态匹配给定的 Hamcrest 匹配器。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如下代码片段展示了如何验证上文中输入字符到 &lt;code&gt;EditText&lt;/code&gt; 成员后显示的字符串：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; testChangeText_sameActivity() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Type text and then press the button.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; ...
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Check that the text was changed.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; onView(withId(R.&lt;span style=&#34;color:#309&#34;&gt;id&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;textToBeChanged&lt;/span&gt;))
.&lt;span style=&#34;color:#309&#34;&gt;check&lt;/span&gt;(matches(withText(STRING_TO_BE_TYPED)));
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h5 id=&#34;在设备或模拟器运行-espresso-测试&#34;&gt;在设备或模拟器运行 Espresso 测试&lt;/h5&gt;
&lt;p&gt;开发者可以从 Android Studio 或命令行运行 Espresso 测试。请确保已按照上文内容指定 &lt;code&gt;AndroidJUnitRunner&lt;/code&gt; 作为工程默认的 instrumentation runner 。要运行 Espresso 测试，请参考上文 &lt;code&gt;运行 Instrumented 单元测试&lt;/code&gt;。&lt;/p&gt;
&lt;h4 id=&#34;3-2-跨应用-ui-测试-ui-automator&#34;&gt;3.2. 跨应用 UI 测试 - UI Automator&lt;/h4&gt;
&lt;p&gt;一个支持跨应用的 UI 测试可以让开发者验证应用在与其他应用或与系统交互时行为正常。例如，一个消息应用让用户输入一些文本，启动 Android 联系人选择器，然后用户可以选择消息的接收人，然后再返回到原应用控制，让用户提交信息。&lt;/p&gt;
&lt;p&gt;本小结将说明如何使用 Android 测试支持库提供的 UI Automator 测试框架来编写此类 UI 测试。UI Automator API 允许开发者与设备上的可见元素交互，而不管哪个 Activity 正持有焦点。测试可以通过方便的描述方法查找 UI 组件，如组件显示的文本或组件的内容描述。UI Automator 测试可以运行在 Android 4.3 (API 18) 及更高本版的设备上。&lt;/p&gt;
&lt;p&gt;UI Automator 测试框架是一个基于 instrumentation 的 API 且运行在 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html&#34;&gt;&lt;code&gt;AndroidJUnitRunner&lt;/code&gt;&lt;/a&gt; 测试执行者上。&lt;/p&gt;
&lt;h5 id=&#34;配置-ui-automator&#34;&gt;配置 UI Automator&lt;/h5&gt;
&lt;p&gt;在开始编写你的 UI 测试前，请先确保已经正确配置了工程依赖及测试代码存放路径，如上文开始测试内容。&lt;/p&gt;
&lt;p&gt;在工程的 app 模块 &lt;code&gt;build.gradle&lt;/code&gt; 文件中，你需要配置 UI Automator 的库依赖。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;dependencies {
...
androidTestCompile &lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&amp;#39;&lt;/span&gt;com.&lt;span style=&#34;color:#309&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;support&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;test&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;uiautomator&lt;/span&gt;&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;uiautomator&lt;span style=&#34;color:#555&#34;&gt;-&lt;/span&gt;v18&lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt;2.&lt;span style=&#34;color:#309&#34;&gt;1&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;1&lt;/span&gt;&lt;span style=&#34;color:#a00;background-color:#faa&#34;&gt;&amp;#39;&lt;/span&gt;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;编写 UI Automator 测试前，开发者需要先检查目标 app 的 UI 组件并确保它们可以被访问。下面两个小节将介绍这些优化的要点。&lt;/p&gt;
&lt;h5 id=&#34;检查设备上的-ui&#34;&gt;检查设备上的 UI&lt;/h5&gt;
&lt;p&gt;在开始设计的测试前，开发者需要检查设备上的 UI 组件是可见的。为了确保 UI Automator 测试可以访问这些组件，请检查这些组件有可见的文字标签，&lt;a href=&#34;http://developer.android.com/reference/android/view/View.html#attr_android:contentDescription&#34;&gt;&lt;code&gt;android:contentDescription&lt;/code&gt;&lt;/a&gt; 值。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;uiautomatorviewer&lt;/code&gt; 工具提供了一个便利的可视化界面检查工具来查看在设备前台显示的组件的属性。有了这些信息就可以创建更高细粒度的 UI Automator 测试。例如，开发者可以创建一个匹配特定可见属性的 UI 选择器。&lt;/p&gt;
&lt;p&gt;按照以下步骤运行 &lt;code&gt;uiautomatorviewer&lt;/code&gt; 工具：&lt;/p&gt;
&lt;p&gt;1. 在物理设备上运行目标 app。&lt;/p&gt;
&lt;p&gt;2. 将设备与开发计算机连接。&lt;/p&gt;
&lt;p&gt;3. 打开终端窗口并进入 &lt;code&gt;&amp;lt;android-sdk&amp;gt;/tools/&lt;/code&gt; 目录。&lt;/p&gt;
&lt;p&gt;4. 运行命令 &lt;code&gt;uiautomatorviewer&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;要查看应用的 UI 属性，请按照如下步骤：&lt;/p&gt;
&lt;p&gt;1. 在 &lt;code&gt;uiautomatorviewer&lt;/code&gt; 界面，点击 &lt;strong&gt;Device Screenshot&lt;/strong&gt; 按钮。&lt;/p&gt;
&lt;p&gt;2. 在 &lt;code&gt;uiautomatorviewer&lt;/code&gt; 工具里，移动鼠标到快照左侧面板来查看 UI 组件，组件属性将在右下面板显示，布局层次将在右上面板显示。&lt;/p&gt;
&lt;p&gt;3. 可选的，点击 &lt;strong&gt;Toggle NAF Nodes&lt;/strong&gt; 按钮来查看不可访问的 UI 组件，这些组件可能只会显示组件的部分可用信息。&lt;/p&gt;
&lt;h5 id=&#34;确保-activity-可被访问&#34;&gt;确保 Activity 可被访问&lt;/h5&gt;
&lt;p&gt;UI Automator 测试框架在实现了 Android 可访问性功能的应用上表现的更好。当开发者使用一个视图元素 View 或者一个继承了 View 的子类时，就不需要实现可访问性支持，因为 SDK 或支持库已经在这些类里实现了此功能。&lt;/p&gt;
&lt;p&gt;有些应用会使用自定义个 UI 元素来提供一个更丰富的用户体验。这些元素不会提供自动的可访问性支持。如果开发者的应用包含了非 SDK 或支持库 View 子类的实例，请确保已经按照如下步骤实现了可访问性功能：&lt;/p&gt;
&lt;p&gt;1. 创建一个继承自 &lt;code&gt;ExploreByTouchHelper&lt;/code&gt; 的实现类。&lt;/p&gt;
&lt;p&gt;2. 通过调用 &lt;a href=&#34;https://developer.android.com/reference/android/support/v4/view/ViewCompat.html#setAccessibilityDelegate(android.view.View, android.support.v4.view.AccessibilityDelegateCompat)&#34;&gt;setAccessibilityDelegate()&lt;/a&gt; 方法将自定义的 UI 元素实例关联。&lt;/p&gt;
&lt;p&gt;要学习更多关于自定义视图增加可访问性的功能，请阅读 &lt;a href=&#34;https://developer.android.com/guide/topics/ui/accessibility/custom-views.html&#34;&gt;Building Accessible Custom Views&lt;/a&gt; 。要学习更多关于 Android 可访问性的最佳实践，请阅读 &lt;a href=&#34;https://developer.android.com/guide/topics/ui/accessibility/apps.html&#34;&gt;Making Apps More Accessible&lt;/a&gt; 。&lt;/p&gt;
&lt;h5 id=&#34;创建-ui-automator-测试&#34;&gt;创建 UI Automator 测试&lt;/h5&gt;
&lt;p&gt;UI Automator 测试类的编写应该和 JUnit 4 测试类一致。要学习更多关于创建 JUnit 4 测试类和使用 Junit4 断言及注解，请阅读上文的 创建 Instrumented 单元测试类。&lt;/p&gt;
&lt;p&gt;在类的声明部分前增加 &lt;code&gt;@RunWith(AndroidJUnit4.class)&lt;/code&gt; 注解。开发者同样需要指定由 Android 测试支持库提供的 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html&#34;&gt;&lt;code&gt;AndroidJUnitRunner&lt;/code&gt;&lt;/a&gt; 类为默认的测试运行者。&lt;/p&gt;
&lt;p&gt;请遵循如下变成模型来编写 UI Automator 测试类：&lt;/p&gt;
&lt;p&gt;1. 通过 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiDevice.html#getInstance(android.app.Instrumentation)&#34;&gt;&lt;code&gt;getInstance()&lt;/code&gt;&lt;/a&gt; 方法获取测试设备的 UIDevice 对象，同时传入一个 &lt;code&gt;Instrumentation&lt;/code&gt; 对象参数。&lt;/p&gt;
&lt;p&gt;2. 通过 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiDevice.html#findObject(android.support.test.uiautomator.UiSelector)&#34;&gt;&lt;code&gt;findObject()&lt;/code&gt;&lt;/a&gt; 方法获取一个 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html&#34;&gt;&lt;code&gt;UiObject&lt;/code&gt;&lt;/a&gt; 对象来访问设备上显示的 UI 组件。&lt;/p&gt;
&lt;p&gt;3. 通过 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html&#34;&gt;&lt;code&gt;UiObject&lt;/code&gt;&lt;/a&gt; 的方法如 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#performMultiPointerGesture(android.view.MotionEvent.PointerCoords[]...)&#34;&gt;&lt;code&gt;performMultiPointerGesture()&lt;/code&gt;&lt;/a&gt; (模拟多点手势) 或 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#setText(java.lang.String)&#34;&gt;&lt;code&gt;setText()&lt;/code&gt;&lt;/a&gt; (编辑文本) 模拟一个特定的用户交互行为。如果有必要，可以重复使用 API 重复创建步骤 2 和步骤 3 中的操作来实现更复杂的用户交互行为。&lt;/p&gt;
&lt;p&gt;4. 执行用户交互行为后，检查 UI 反映结果是否符合预期的状态或行为。&lt;/p&gt;
&lt;p&gt;这些步骤将在下面几小节详细描述。&lt;/p&gt;
&lt;h5 id=&#34;访问-ui-组件-1&#34;&gt;访问 UI 组件&lt;/h5&gt;
&lt;p&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiDevice.html&#34;&gt;&lt;code&gt;UiDevice&lt;/code&gt;&lt;/a&gt; 对象是开发者访问和操作设备状态的主要方式。在测试中，开发者可以调用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiDevice.html&#34;&gt;&lt;code&gt;UiDevice&lt;/code&gt;&lt;/a&gt; 方法来检查不同的属性状态，例如当前的屏幕方向或显示大小。测试可以使用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiDevice.html&#34;&gt;&lt;code&gt;UiDevice&lt;/code&gt;&lt;/a&gt; 对象执行设备级别的操作，如强制设备屏幕转向，按下 D-pad 硬件按钮，按下 Home 或 菜单按钮。&lt;/p&gt;
&lt;p&gt;从设备的主屏幕开始测试是个很好的做法。从主屏幕开始，开发者可以调用 UI Automator API 提供的方法来选择特定的 UI 组件并交互。如下代码片段展示了如何获取 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiDevice.html&#34;&gt;&lt;code&gt;UiDevice&lt;/code&gt;&lt;/a&gt; 实例并模拟按下 Home 按钮。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;org&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;junit&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Before&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;support&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;test&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;runner&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;AndroidJUnit4&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;support&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;test&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;uiautomator&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;UiDevice&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;support&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;test&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;uiautomator&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;By&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;import&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;android&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;support&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;test&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;uiautomator&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;Until&lt;/span&gt;;
...
&lt;span style=&#34;color:#99f&#34;&gt;@RunWith&lt;/span&gt;(AndroidJUnit4.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;)
&lt;span style=&#34;color:#99f&#34;&gt;@SdkSuppress&lt;/span&gt;(minSdkVersion &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 18)
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;class&lt;/span&gt; ChangeTextBehaviorTest {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; BASIC_SAMPLE_PACKAGE
&lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.example.android.testing.uiautomator.BasicSample&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; LAUNCH_TIMEOUT &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; 5000;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; STRING_TO_BE_TYPED &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;UiAutomator&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;UiDevice&lt;/span&gt; mDevice;
&lt;span style=&#34;color:#99f&#34;&gt;@Before&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; startMainActivityFromHomeScreen() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Initialize UiDevice instance
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; mDevice &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; UiDevice.&lt;span style=&#34;color:#309&#34;&gt;getInstance&lt;/span&gt;(InstrumentationRegistry.&lt;span style=&#34;color:#309&#34;&gt;getInstrumentation&lt;/span&gt;());
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Start from the home screen
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; mDevice.&lt;span style=&#34;color:#309&#34;&gt;pressHome&lt;/span&gt;();
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Wait for launcher
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; launcherPackage &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mDevice.&lt;span style=&#34;color:#309&#34;&gt;getLauncherPackageName&lt;/span&gt;();
assertThat(launcherPackage, notNullValue());
mDevice.&lt;span style=&#34;color:#309&#34;&gt;wait&lt;/span&gt;(Until.&lt;span style=&#34;color:#309&#34;&gt;hasObject&lt;/span&gt;(By.&lt;span style=&#34;color:#309&#34;&gt;pkg&lt;/span&gt;(launcherPackage).&lt;span style=&#34;color:#309&#34;&gt;depth&lt;/span&gt;(0)),
LAUNCH_TIMEOUT);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Launch the app
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; InstrumentationRegistry.&lt;span style=&#34;color:#309&#34;&gt;getContext&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Intent&lt;/span&gt; intent &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; context.&lt;span style=&#34;color:#309&#34;&gt;getPackageManager&lt;/span&gt;()
.&lt;span style=&#34;color:#309&#34;&gt;getLaunchIntentForPackage&lt;/span&gt;(BASIC_SAMPLE_PACKAGE);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Clear out any previous instances
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; intent.&lt;span style=&#34;color:#309&#34;&gt;addFlags&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;FLAG_ACTIVITY_CLEAR_TASK&lt;/span&gt;);
context.&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(intent);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Wait for the app to appear
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; mDevice.&lt;span style=&#34;color:#309&#34;&gt;wait&lt;/span&gt;(Until.&lt;span style=&#34;color:#309&#34;&gt;hasObject&lt;/span&gt;(By.&lt;span style=&#34;color:#309&#34;&gt;pkg&lt;/span&gt;(BASIC_SAMPLE_PACKAGE).&lt;span style=&#34;color:#309&#34;&gt;depth&lt;/span&gt;(0)),
LAUNCH_TIMEOUT);
}
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;在示例代码中， &lt;code&gt;@SdkSuppress(minSdkVersion = 18)&lt;/code&gt; 声明确保了测试只会运行在 Android 4.3 (API 18) 或更高的版本上，正如 UI Automator 框架所要求的那样。&lt;/p&gt;
&lt;p&gt;使用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiDevice.html#findObject(android.support.test.uiautomator.UiSelector)&#34;&gt;&lt;code&gt;findObject()&lt;/code&gt;&lt;/a&gt; 方法获取一个展现在屏幕上，匹配了给定选择器的 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html&#34;&gt;&lt;code&gt;UiObject&lt;/code&gt;&lt;/a&gt; 对象。如果需要，可以在代码中重复利用已经创建的 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html&#34;&gt;&lt;code&gt;UiObject&lt;/code&gt;&lt;/a&gt; 实例。注意 UI Automator 测试框架会在每次使用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html&#34;&gt;&lt;code&gt;UiObject&lt;/code&gt;&lt;/a&gt; 点击或查询属性时在当前界面重新查找匹配。&lt;/p&gt;
&lt;p&gt;如下代码展示了如何使用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html&#34;&gt;&lt;code&gt;UiObject&lt;/code&gt;&lt;/a&gt; 实例来操作应用中的 Cancel 按钮和 OK 按钮。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;UiObject &lt;span style=&#34;color:#c0f&#34;&gt;cancelButton&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mDevice.&lt;span style=&#34;color:#309&#34;&gt;findObject&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;text&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Cancel&amp;#34;&lt;/span&gt;))
.&lt;span style=&#34;color:#309&#34;&gt;className&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.widget.Button&amp;#34;&lt;/span&gt;));
UiObject &lt;span style=&#34;color:#c0f&#34;&gt;okButton&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mDevice.&lt;span style=&#34;color:#309&#34;&gt;findObject&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;text&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;OK&amp;#34;&lt;/span&gt;))
.&lt;span style=&#34;color:#309&#34;&gt;className&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.widget.Button&amp;#34;&lt;/span&gt;));
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Simulate a user-click on the OK button, if found.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt;(okButton.&lt;span style=&#34;color:#309&#34;&gt;exists&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;&amp;amp;&amp;amp;&lt;/span&gt; okButton.&lt;span style=&#34;color:#309&#34;&gt;isEnabled&lt;/span&gt;()) {
okButton.&lt;span style=&#34;color:#309&#34;&gt;click&lt;/span&gt;();
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;指定选择器&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;请使用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiSelector.html&#34;&gt;&lt;code&gt;UiSelector&lt;/code&gt;&lt;/a&gt; 类来访问特定的 UI 组件。如果找到了多个视图元素，布局层次中的第一个匹配元素将被返回为 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html&#34;&gt;&lt;code&gt;UiObject&lt;/code&gt;&lt;/a&gt; 。构造 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiSelector.html&#34;&gt;&lt;code&gt;UiSelector&lt;/code&gt;&lt;/a&gt; 时，可以将多个属性串联来提炼查找。如果没有找到匹配的元素，将抛出 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObjectNotFoundException.html&#34;&gt;&lt;code&gt;UiAutomatorObjectNotFoundException&lt;/code&gt;&lt;/a&gt; 异常。&lt;/p&gt;
&lt;p&gt;可以使用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiSelector.html#childSelector(android.support.test.uiautomator.UiSelector)&#34;&gt;&lt;code&gt;childSelector()&lt;/code&gt;&lt;/a&gt; 方法来嵌套多个 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiSelector.html&#34;&gt;&lt;code&gt;UiSelector&lt;/code&gt;&lt;/a&gt; 实例。例如下面代码展示了一个测试，在当前显示的视图中，找到第一个 ListView ，然后在这个 ListView 中查找匹配文本属性 &lt;code&gt;Apps&lt;/code&gt; 的视图元素。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;UiObject &lt;span style=&#34;color:#c0f&#34;&gt;appItem&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiObject(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;className&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.widget.ListView&amp;#34;&lt;/span&gt;)
.&lt;span style=&#34;color:#309&#34;&gt;instance&lt;/span&gt;(0)
.&lt;span style=&#34;color:#309&#34;&gt;childSelector&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;text&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Apps&amp;#34;&lt;/span&gt;)));&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;作为最佳实践，开发者应该使用资源 ID 来指定一个选择器，而不是使用文本值或者内容描述。不是所有的视图元素都含有文本内容。文本选择器很脆弱，可能会在 UI 发生很小变化时导致测试失败。同样，文本选择器可能在多语言环境下失效，它们可能匹配不到翻译后的字符串。&lt;/p&gt;
&lt;p&gt;在选择器中指定对象状态可能会很有用。例如可以通过 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/By.html#checked(boolean)&#34;&gt;&lt;code&gt;checked()&lt;/code&gt;&lt;/a&gt; 方法传入参数 &lt;code&gt;true&lt;/code&gt; 选择列表中所有的已选择元素，然后可以取消这些元素的选择。&lt;/p&gt;
&lt;h5 id=&#34;执行用户操作&#34;&gt;执行用户操作&lt;/h5&gt;
&lt;p&gt;当获取到 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html&#34;&gt;&lt;code&gt;UiObject&lt;/code&gt;&lt;/a&gt; 对象后，开发者可以通过调用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html&#34;&gt;&lt;code&gt;UiObject&lt;/code&gt;&lt;/a&gt; 类中的方法来执行 UI 组件的用户交互行为。可以执行的操作包括：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#click()&#34;&gt;&lt;code&gt;click()&lt;/code&gt;&lt;/a&gt; : 点击可见 UI 与元素边界的中心位置。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#dragTo(int, int, int)&#34;&gt;&lt;code&gt;dragTo()&lt;/code&gt;&lt;/a&gt; : 拖动对象到任意位置。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#setText(java.lang.String)&#34;&gt;&lt;code&gt;setText()&lt;/code&gt;&lt;/a&gt; : 清除内容并设置可编辑区域的文字；类似的 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#clearTextField()&#34;&gt;&lt;code&gt;clearTextField()&lt;/code&gt;&lt;/a&gt; 方法清除可编辑区域的文字。&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#swipeUp(int)&#34;&gt;&lt;code&gt;swipeUp()&lt;/code&gt;&lt;/a&gt; : 在 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html&#34;&gt;&lt;code&gt;UiObject&lt;/code&gt;&lt;/a&gt; 对象上执行向上滑动操作。 类似的 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#swipeDown(int)&#34;&gt;&lt;code&gt;swipeDown()&lt;/code&gt;&lt;/a&gt;, &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#swipeLeft(int)&#34;&gt;&lt;code&gt;swipeLeft()&lt;/code&gt;&lt;/a&gt;, 和 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiObject.html#swipeRight(int)&#34;&gt;&lt;code&gt;swipeRight()&lt;/code&gt;&lt;/a&gt; 会执行相应的操作。&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;UI Automator 测试框架允许开发者通过 &lt;code&gt;getContext()&lt;/code&gt; 方法获得一个 &lt;code&gt;Context&lt;/code&gt; 对象来发送 &lt;code&gt;Intent&lt;/code&gt; 或启动 &lt;code&gt;Activity&lt;/code&gt; ， 不依赖 shell 命令。&lt;/p&gt;
&lt;p&gt;下面代码片段展示了测试中如何发送一个Intent 来启动应用。当只对测试计算器应用感兴趣时，此方法非常有用，这个途径并不关心谁是启动器。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; setUp() {
...
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Launch a simple calculator app
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; getInstrumentation().&lt;span style=&#34;color:#309&#34;&gt;getContext&lt;/span&gt;();
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; context.&lt;span style=&#34;color:#309&#34;&gt;getPackageManager&lt;/span&gt;()
.&lt;span style=&#34;color:#309&#34;&gt;getLaunchIntentForPackage&lt;/span&gt;(CALC_PACKAGE);
intent.&lt;span style=&#34;color:#309&#34;&gt;addFlags&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;FLAG_ACTIVITY_CLEAR_TASK&lt;/span&gt;);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Clear out any previous instances
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; context.&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(intent);
mDevice.&lt;span style=&#34;color:#309&#34;&gt;wait&lt;/span&gt;(Until.&lt;span style=&#34;color:#309&#34;&gt;hasObject&lt;/span&gt;(By.&lt;span style=&#34;color:#309&#34;&gt;pkg&lt;/span&gt;(CALC_PACKAGE).&lt;span style=&#34;color:#309&#34;&gt;depth&lt;/span&gt;(0)), TIMEOUT);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;&lt;strong&gt;在集合上执行操作&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;如果要模拟一个集合上（如音乐专辑里的歌曲或收件箱里的邮件）的用户操作，可以使用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiCollection.html&#34;&gt;&lt;code&gt;UiCollection&lt;/code&gt;&lt;/a&gt; 类。 要创建一个 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiCollection.html&#34;&gt;&lt;code&gt;UiCollection&lt;/code&gt;&lt;/a&gt; 对象，指定一个 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiSelector.html&#34;&gt;&lt;code&gt;UiSelector&lt;/code&gt;&lt;/a&gt; 来查找一个 UI 包装器或其他 UI 元素的包装器，如一个包含子 UI 元素的布局视图。&lt;/p&gt;
&lt;p&gt;如下代码展示了如何构造一个 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiCollection.html&#34;&gt;&lt;code&gt;UiCollection&lt;/code&gt;&lt;/a&gt; ：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;UiCollection &lt;span style=&#34;color:#c0f&#34;&gt;videos&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiCollection(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;className&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.widget.FrameLayout&amp;#34;&lt;/span&gt;));
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Retrieve the number of videos in this collection:
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;&lt;span style=&#34;color:#078;font-weight:bold&#34;&gt;int&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;count&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; videos.&lt;span style=&#34;color:#309&#34;&gt;getChildCount&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;className&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.widget.LinearLayout&amp;#34;&lt;/span&gt;));
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Find a specific video and simulate a user-click on it
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;UiObject &lt;span style=&#34;color:#c0f&#34;&gt;video&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; videos.&lt;span style=&#34;color:#309&#34;&gt;getChildByText&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;className&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.widget.LinearLayout&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Cute Baby Laughing&amp;#34;&lt;/span&gt;);
video.&lt;span style=&#34;color:#309&#34;&gt;click&lt;/span&gt;();
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Simulate selecting a checkbox that is associated with the video
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;UiObject &lt;span style=&#34;color:#c0f&#34;&gt;checkBox&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; video.&lt;span style=&#34;color:#309&#34;&gt;getChild&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;className&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.widget.Checkbox&amp;#34;&lt;/span&gt;));
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt;(&lt;span style=&#34;color:#555&#34;&gt;!&lt;/span&gt;checkBox.&lt;span style=&#34;color:#309&#34;&gt;isSelected&lt;/span&gt;()) checkbox.&lt;span style=&#34;color:#309&#34;&gt;click&lt;/span&gt;();&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h5 id=&#34;在可滚动视图上执行操作&#34;&gt;在可滚动视图上执行操作&lt;/h5&gt;
&lt;p&gt;使用 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/uiautomator/UiScrollable.html&#34;&gt;&lt;code&gt;UiScrollable&lt;/code&gt;&lt;/a&gt; 模拟垂直或水平滚动显示。UI 元素位于屏幕外时，需要滚动以将其置于视图中。如下的代码片段展示了模拟向下滚动设置菜单，然后点击一个 &amp;ldquo;About tablet&amp;rdquo; 选项：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;UiScrollable &lt;span style=&#34;color:#c0f&#34;&gt;settingsItem&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiScrollable(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;className&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.widget.ListView&amp;#34;&lt;/span&gt;));
UiObject &lt;span style=&#34;color:#c0f&#34;&gt;about&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; settingsItem.&lt;span style=&#34;color:#309&#34;&gt;getChildByText&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;className&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.widget.LinearLayout&amp;#34;&lt;/span&gt;), &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;About tablet&amp;#34;&lt;/span&gt;);
about.&lt;span style=&#34;color:#309&#34;&gt;click&lt;/span&gt;();&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h5 id=&#34;验证结果-1&#34;&gt;验证结果&lt;/h5&gt;
&lt;p&gt;&lt;code&gt;InstrumentationTestCase&lt;/code&gt; 类继承自 &lt;code&gt;TestCase&lt;/code&gt;，所以可以使用 JUnit &lt;a href=&#34;http://junit.org/javadoc/latest/org/junit/Assert.html&#34;&gt;&lt;code&gt;Assert&lt;/code&gt;&lt;/a&gt; 方法来测试 UI 组件返回了期望的结果。下面的代码片段展示了如何定位计算器应用中的多个按钮，然后按顺序点击，最后确认显示了正确的结果：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;static&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;final&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;String&lt;/span&gt; CALC_PACKAGE &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.myexample.calc&amp;#34;&lt;/span&gt;;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; testTwoPlusThreeEqualsFive() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Enter an equation: 2 + 3 = ?
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; mDevice.&lt;span style=&#34;color:#309&#34;&gt;findObject&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;packageName&lt;/span&gt;(CALC_PACKAGE).&lt;span style=&#34;color:#309&#34;&gt;resourceId&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;two&amp;#34;&lt;/span&gt;)).&lt;span style=&#34;color:#309&#34;&gt;click&lt;/span&gt;();
mDevice.&lt;span style=&#34;color:#309&#34;&gt;findObject&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;packageName&lt;/span&gt;(CALC_PACKAGE).&lt;span style=&#34;color:#309&#34;&gt;resourceId&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;plus&amp;#34;&lt;/span&gt;)).&lt;span style=&#34;color:#309&#34;&gt;click&lt;/span&gt;();
mDevice.&lt;span style=&#34;color:#309&#34;&gt;findObject&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;packageName&lt;/span&gt;(CALC_PACKAGE).&lt;span style=&#34;color:#309&#34;&gt;resourceId&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;three&amp;#34;&lt;/span&gt;)).&lt;span style=&#34;color:#309&#34;&gt;click&lt;/span&gt;();
mDevice.&lt;span style=&#34;color:#309&#34;&gt;findObject&lt;/span&gt;(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; UiSelector()
.&lt;span style=&#34;color:#309&#34;&gt;packageName&lt;/span&gt;(CALC_PACKAGE).&lt;span style=&#34;color:#309&#34;&gt;resourceId&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;equals&amp;#34;&lt;/span&gt;)).&lt;span style=&#34;color:#309&#34;&gt;click&lt;/span&gt;();
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Verify the result = 5
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; UiObject &lt;span style=&#34;color:#c0f&#34;&gt;result&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mDevice.&lt;span style=&#34;color:#309&#34;&gt;findObject&lt;/span&gt;(By.&lt;span style=&#34;color:#309&#34;&gt;res&lt;/span&gt;(CALC_PACKAGE, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;result&amp;#34;&lt;/span&gt;));
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;5&amp;#34;&lt;/span&gt;, result.&lt;span style=&#34;color:#309&#34;&gt;getText&lt;/span&gt;());
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h5 id=&#34;在设备或模拟器运行-ui-automator-测试&#34;&gt;在设备或模拟器运行 UI Automator 测试&lt;/h5&gt;
&lt;p&gt;开发者可以从 Android Studio 或命令行运行 UI Automator 测试。请确保已指定 &lt;code&gt;AndroidJUnitRunner&lt;/code&gt; 作为工程默认的 instrumentation runner 。要运行 UI Automator 测试，请参考上文 运行 Instrumented 单元测试。&lt;/p&gt;
&lt;h3 id=&#34;4-应用组件集成测试&#34;&gt;4. 应用组件集成测试&lt;/h3&gt;
&lt;p&gt;本小节介绍如何编写用户不直接参与交互的组件测试，如 Service 和 Content provider。&lt;/p&gt;
&lt;p&gt;参考 &lt;a href=&#34;https://developer.android.com/training/testing/integration-testing/index.html&#34;&gt;Testing App Compontent Integrations&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;如果应用使用了用户不会直接参与交互的组件，例如 Service 和 Content Provider，则应当验证应用内的这些组件行文正确。&lt;/p&gt;
&lt;p&gt;当开发这些组件时，编写验证这些组件功能是否正确的集成测试是个好习惯。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; Android 没有提供独立 BroadcastReceiver 的测试类。要验证 BroadcastReceiver 响应，需要测试发送出 Intent 对象的组件。另外，可以通过 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/InstrumentationRegistry.html#getContext()&#34;&gt;&lt;code&gt;InstrumentationRegistry.getTargetContext()&lt;/code&gt;&lt;/a&gt; 方法创建一个 BroadcastReceiver 的实例，然后测试 BroadcastReceiver 的方法 ( 通常是 onReceive() 方法 ) 。&lt;/p&gt;
&lt;p&gt;这一节将介绍如何使用 Android 平台提供的 API 和工具编写自动化集成测试。&lt;/p&gt;
&lt;h4 id=&#34;4-1-测试-service&#34;&gt;4.1. 测试 Service&lt;/h4&gt;
&lt;p&gt;如果实现了一个本地 Service 作为应用的一个组件，那么开发者应当通过测试确保它按照预期的行为运行。可以创建 &lt;a href=&#34;https://developer.android.com/training/testing/unit-testing/instrumented-unit-tests.html&#34;&gt;instrumented 单元测试&lt;/a&gt; 来验证 Service 中的行为正确。例如，服务储存并返回了有效的数据同时数据操作正确。&lt;/p&gt;
&lt;p&gt;Android 测试支持库提供了隔离测试 Service 对象的 API， &lt;a href=&#34;https://developer.android.com/reference/android/support/test/rule/ServiceTestRule.html&#34;&gt;ServiceTestRule&lt;/a&gt; 类是一个用来在测试运行前启动Service 并在测试结束后完成 Service 的 JUnit 4 规则。通过这个测试规则，可以确保在每次测试运行前，已建立到服务的连接。&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;注意：&lt;/strong&gt; ServiceTestRule 类不提供对 IntentService 对象测试的支持。如果需要测试一个 IntentService 对象，请将它的逻辑实现分离到另一个独立的类里，然后创建类的单元测试。&lt;/p&gt;
&lt;h5 id=&#34;配置测试环境-2&#34;&gt;配置测试环境&lt;/h5&gt;
&lt;p&gt;请参考上文的配置测试环境内容。&lt;/p&gt;
&lt;h5 id=&#34;创建一个-service-的集成测试&#34;&gt;创建一个 Service 的集成测试&lt;/h5&gt;
&lt;p&gt;Service 的集成测试应该被写作 JUnit 4 测试类的格式。要创建一个 Service 的测试，请在测试类的声明前增加 &lt;code&gt;@RunWith(AndroidJUnit4.class)&lt;/code&gt; 注解。同时，如上文内容所述，需要指定 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html&#34;&gt;&lt;code&gt;AndroidJUnitRunner&lt;/code&gt;&lt;/a&gt; 类为默认的测试运行者。&lt;/p&gt;
&lt;p&gt;然后通过 &lt;code&gt;@Rule&lt;/code&gt; 注解，创建一个 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/rule/ServiceTestRule.html&#34;&gt;ServiceTestRule&lt;/a&gt; 实例：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Rule&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;final&lt;/span&gt; ServiceTestRule &lt;span style=&#34;color:#c0f&#34;&gt;mServiceRule&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ServiceTestRule();&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;如下代码片段展示了如何实现一个 service 的集成测试。测试方法 &lt;code&gt;testWithBoundService&lt;/code&gt; 验证了应用是否成功绑定到一个本地服务，同时验证了服务接口行为是否正确。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; testWithBoundService() &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;throws&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;TimeoutException&lt;/span&gt; {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Create the service Intent.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; Intent &lt;span style=&#34;color:#c0f&#34;&gt;serviceIntent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(InstrumentationRegistry.&lt;span style=&#34;color:#309&#34;&gt;getTargetContext&lt;/span&gt;(),
LocalService.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Data can be passed to the service via the Intent.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; serviceIntent.&lt;span style=&#34;color:#309&#34;&gt;putExtra&lt;/span&gt;(LocalService.&lt;span style=&#34;color:#309&#34;&gt;SEED_KEY&lt;/span&gt;, 42L);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Bind the service and grab a reference to the binder.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; IBinder &lt;span style=&#34;color:#c0f&#34;&gt;binder&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mServiceRule.&lt;span style=&#34;color:#309&#34;&gt;bindService&lt;/span&gt;(serviceIntent);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Get the reference to the service, or you can call
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; &lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// public methods on the binder directly.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; LocalService &lt;span style=&#34;color:#c0f&#34;&gt;service&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt;
((LocalService.&lt;span style=&#34;color:#309&#34;&gt;LocalBinder&lt;/span&gt;) binder).&lt;span style=&#34;color:#309&#34;&gt;getService&lt;/span&gt;();
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// Verify that the service is working correctly.
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; assertThat(service.&lt;span style=&#34;color:#309&#34;&gt;getRandomInt&lt;/span&gt;(), is(any(Integer.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;)));
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h5 id=&#34;运行-service-集成测试&#34;&gt;运行 Service 集成测试&lt;/h5&gt;
&lt;p&gt;开发者可以从 Android Studio 或命令行运行 Service 集成测试。请确保已指定 &lt;code&gt;AndroidJUnitRunner&lt;/code&gt; 作为工程默认的 instrumentation runner 。要运行 Service 集成测试，请参考上文 运行 Instrumented 单元测试。&lt;/p&gt;
&lt;h4 id=&#34;4-2-测试-content-provider&#34;&gt;4.2. 测试 Content Provider&lt;/h4&gt;
&lt;p&gt;如果实现了一个 content provider 来保存或获取数据，或向其他应用提供可访问的数据，开发者应当编写测试来确保 content provider 的行为正确无误。这一小节将介绍如何编写 content provider 测试。&lt;/p&gt;
&lt;h5 id=&#34;创建-content-provider-集成测试&#34;&gt;创建 Content Provider 集成测试&lt;/h5&gt;
&lt;p&gt;在 Android 中，应用把 content provider 看作提供数据表的数据 API ，而看不到它们的内部实现。一个内容提供者可能有很多公共常量，但是很少有 public 方法或 public 变量。因为这个原因，需要编写只基于 provider 公开成员的测试。&lt;/p&gt;
&lt;p&gt;Content Provider 允许访问实际的用户数据，因此对 content provider 的测试应该在隔离环境，这同样意味着测试不应该修改实际的用户数据。例如，应该避免由于之前的测试遗留数据导致当前测试失败。同样的，测试应该避免增加或删除 provider 的实际数据。&lt;/p&gt;
&lt;p&gt;为了将 content provider 隔离，需要使用 &lt;code&gt;ProviderTestCase2&lt;/code&gt; 类。这个类允许开发者使用 Android mock 对象类，如 &lt;code&gt;IsolatedContext&lt;/code&gt; 和 &lt;code&gt;MockContentResolver&lt;/code&gt; 来访问文件和数据库信息而不影响真实用户数据。&lt;/p&gt;
&lt;p&gt;Content Provider 的集成测试应该编写为 JUnit 4 测试类，更多关于创建 JUnit 4 测试类和使用 JUnit 4 断言，请阅读上文中创建本地单元测试类的内容。&lt;/p&gt;
&lt;p&gt;要创建 content provider 的集成测试，请按照如下几个步骤：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;p&gt;创建一个继承自 &lt;code&gt;ProviderTestCase2&lt;/code&gt; 的子类。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;类声明部分前添加 &lt;code&gt;@RunWith(AndroidJUnit4.class)&lt;/code&gt; 注解。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;指定 &lt;a href=&#34;https://developer.android.com/reference/android/support/test/runner/AndroidJUnitRunner.html&#34;&gt;&lt;code&gt;AndroidJUnitRunner&lt;/code&gt;&lt;/a&gt; 类为默认的测试执行者。&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;从 &lt;code&gt;InstrumentationRegistry&lt;/code&gt; 设置 &lt;code&gt;Context&lt;/code&gt; 对象，如下所示：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;protected&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; setUp() &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;throws&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Exception&lt;/span&gt; {
setContext(InstrumentationRegistry.&lt;span style=&#34;color:#309&#34;&gt;getTargetContext&lt;/span&gt;());
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;super&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;setUp&lt;/span&gt;();
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h5 id=&#34;providertestcase2-是如何工作的&#34;&gt;ProviderTestCase2 是如何工作的&lt;/h5&gt;
&lt;p&gt;通过 &lt;code&gt;ProviderTestCase2&lt;/code&gt; 的子类测试 provider，这个类继承自 AndroidTestCase，所以它提供了 JUnit 测试框架和用来测试应用的 Android 特定方法。这个类的最重要的一个功能是在它初始化时创建了一个独立的测试环境。&lt;/p&gt;
&lt;p&gt;初始化过程是在 ProviderTestCase2 的构造器中完成的，ProviderTestCase2 构造器创建了一个 IsolatedContext 对象，以允许文件和数据库操作与 Android 系统隔离。文件和数据库操作会被放在设备或模拟器的一个有特殊前缀的独立目录中。&lt;/p&gt;
&lt;p&gt;之后构造器会创建一个 MockContentResolver 来作为测试的 resolver。最终，构造器创建一个 provider 实例用来测试。这是一个普通的 ContentProvider 对象，同时它从 IsolatedContext 中获取所有的环境信息。由此 provider 被限制在独立的测试环境中。&lt;/p&gt;
&lt;p&gt;Content Provider 的集成测试和 instrumented 单元测试一致，要运行 integration 测试，请参考上文运行 integration 测试的内容。&lt;/p&gt;
&lt;h5 id=&#34;测试什么&#34;&gt;测试什么&lt;/h5&gt;
&lt;p&gt;如下是几条测试 content provider 的具体指导：&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;使用 Resolver 方法测试： 即使开发者可以在 &lt;code&gt;ProviderTestCase2&lt;/code&gt; 中实例化一个 provider 对象，也应该使用一个 resolver 对象及适当的 URI 测试 provider。使用 resolver 可以确保开发者像常规应用交互那样测试 provider 。&lt;/li&gt;
&lt;li&gt;像合约一样测试一个公开的 provider，如果开发者期望 provider 公开的被其他应用访问，则需要将它作为一个合约测试，一些如何做的例子如下：
&lt;ul&gt;
&lt;li&gt;测试 provider 暴露的常量。例如， 检查 provider 数据表的栏目名常量，这些数据库字段名总是公开的常量。&lt;/li&gt;
&lt;li&gt;测试所有 provider 提供的 URI。provider 可能提供多个 URI，每个 URI 代表着不同的数据。&lt;/li&gt;
&lt;li&gt;测试无效 URI ：开发者的单元测试应该故意调用一个无效的 URI，来检查是否报错。 一个好的 Provider 设计会为无效 URI 抛出 &lt;code&gt;IllegalArgumentException&lt;/code&gt; 异常。&lt;/li&gt;
&lt;/ul&gt;&lt;/li&gt;
&lt;li&gt;测试标准的 Provider 交互： 大多数 Provider 提供六个方法： &lt;code&gt;query()&lt;/code&gt;, &lt;code&gt;insert()&lt;/code&gt;, &lt;code&gt;delete()&lt;/code&gt;, &lt;code&gt;update()&lt;/code&gt;, &lt;code&gt;getType()&lt;/code&gt;, and &lt;code&gt;onCreate()&lt;/code&gt;。 开发者的测试应该验证所有的这些方法可以工作。 这些方法在 &lt;a href=&#34;https://developer.android.com/guide/topics/providers/content-providers.html&#34;&gt;Content Providers&lt;/a&gt; 中有更多描述。&lt;/li&gt;
&lt;li&gt;测试业务逻辑： 如果 content provider 实现了业务逻辑，则开发者需要测试它。&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id=&#34;5-测试-ui-性能&#34;&gt;5. 测试 UI 性能&lt;/h3&gt;
&lt;p&gt;UI 性能测试不需要开发人员编写代码，请参考 &lt;a href=&#34;https://developer.android.com/training/testing/performance.html&#34;&gt;Testing Display Performance&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;四-代码覆盖率&#34;&gt;四. 代码覆盖率&lt;/h2&gt;
&lt;p&gt;代码覆盖率即 &lt;a href=&#34;https://zh.wikipedia.org/zh-hant/%E4%BB%A3%E7%A2%BC%E8%A6%86%E8%93%8B%E7%8E%87&#34;&gt;Code Coverage&lt;/a&gt; 是编写的测试代码完整度的衡量标准。一个完整的工程应该有代码覆盖率报告。&lt;/p&gt;
&lt;p&gt;请参考 &lt;a href=&#34;https://github.com/codecov/example-android&#34;&gt;https://github.com/codecov/example-android&lt;/a&gt;&lt;/p&gt;
&lt;h2 id=&#34;五-参考资料&#34;&gt;五. 参考资料&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://developer.android.com/training/testing/index.html&#34;&gt;Testing Apps on Android&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://developer.android.com/training/testing/ui-testing/uiautomator-testing.html&#34;&gt;Testing UI for Multiple Apps&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/googlesamples/android-architecture&#34;&gt;Android Architecture Blueprints&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/xdtianyu/CallerInfo/blob/master/app/src/androidTest/java/org/xdty/callerinfo/MainActivityTest.java&#34;&gt;MainActivityTest.java&lt;/a&gt;&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;2017-08-01&lt;/p&gt;</description></item><item><title>Mockito 单元测试实例</title><link>https://busy.im/post/android-mockito-testing/</link><pubDate>Fri, 05 Oct 2018 11:54:49 +0800</pubDate><guid>https://busy.im/post/android-mockito-testing/</guid><description>
&lt;p&gt;&lt;code&gt;Mockito&lt;/code&gt; 是一款优秀的 &lt;code&gt;Java&lt;/code&gt; 单元测试库，本文介绍如何使用 &lt;code&gt;Mockito&lt;/code&gt; 来编写 &lt;code&gt;Android&lt;/code&gt; 本地单元测试。&lt;/p&gt;
&lt;h3 id=&#34;mockito-库接入&#34;&gt;mockito 库接入&lt;/h3&gt;
&lt;p&gt;添加 mockito 支持是很简单的，只需要在 &lt;code&gt;build.gradle&lt;/code&gt; 中加入如下库依赖即可&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-groovy&#34; data-lang=&#34;groovy&#34;&gt;testImplementation &lt;span style=&#34;color:#c30&#34;&gt;&amp;#39;org.mockito:mockito-core:2.19.0&amp;#39;&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;代码实例&#34;&gt;代码实例&lt;/h3&gt;
&lt;p&gt;首先我们先来看一段日历主界面 &lt;code&gt;EventAdapter&lt;/code&gt; 中的代码，这段代码是点击天气后的业务逻辑。&lt;/p&gt;
&lt;p&gt;这段代码主要有三个逻辑：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;根据配置拉起天气应用&lt;/li&gt;
&lt;li&gt;没有天气应用时拉起市场安装应用&lt;/li&gt;
&lt;li&gt;没有天气应用时根据配置拉起网页&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;这段代码包含了 &lt;code&gt;EventAdpater&lt;/code&gt; 的成员 &lt;code&gt;mWeatherController&lt;/code&gt;，通过它获取天气的配置；&lt;code&gt;Context&lt;/code&gt; 是需要传入的上下文，另外两个参数是没有使用的预留参数，可以忽视。这不是一个静态的全局类，又依赖于 &lt;code&gt;WeatherController&lt;/code&gt; 和 &lt;code&gt;Context&lt;/code&gt; 要怎么测试呢？&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;int&lt;/span&gt; performClick(Context &lt;span style=&#34;color:#c0f&#34;&gt;context&lt;/span&gt;, WeatherView &lt;span style=&#34;color:#c0f&#34;&gt;weatherView&lt;/span&gt;, Weather &lt;span style=&#34;color:#c0f&#34;&gt;weather&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;try&lt;/span&gt; {
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;WeatherConf&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;weatherConfList&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mWeatherController.&lt;span style=&#34;color:#309&#34;&gt;loadWeatherConf&lt;/span&gt;();
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;for&lt;/span&gt; (WeatherConf &lt;span style=&#34;color:#c0f&#34;&gt;conf&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;:&lt;/span&gt; weatherConfList) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;try&lt;/span&gt; {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 拉起应用
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent();
intent.&lt;span style=&#34;color:#309&#34;&gt;setClassName&lt;/span&gt;(conf.&lt;span style=&#34;color:#309&#34;&gt;getPackageName&lt;/span&gt;(), conf.&lt;span style=&#34;color:#309&#34;&gt;getActivityName&lt;/span&gt;());
context.&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(intent);
Action.&lt;span style=&#34;color:#309&#34;&gt;WEATHER_CLICK&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;put&lt;/span&gt;(Attribute.&lt;span style=&#34;color:#309&#34;&gt;PACKAGE&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;with&lt;/span&gt;(conf.&lt;span style=&#34;color:#309&#34;&gt;getPackageName&lt;/span&gt;()))
.&lt;span style=&#34;color:#309&#34;&gt;anchor&lt;/span&gt;(context);
Log.&lt;span style=&#34;color:#309&#34;&gt;d&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;onWeatherViewClicked: &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; conf.&lt;span style=&#34;color:#309&#34;&gt;getPackageName&lt;/span&gt;());
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; WeatherEvent.&lt;span style=&#34;color:#309&#34;&gt;CLICK&lt;/span&gt;;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (Exception &lt;span style=&#34;color:#555&#34;&gt;|&lt;/span&gt; Error &lt;span style=&#34;color:#c0f&#34;&gt;e&lt;/span&gt;) {
e.&lt;span style=&#34;color:#309&#34;&gt;printStackTrace&lt;/span&gt;();
}
}
Log.&lt;span style=&#34;color:#309&#34;&gt;d&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;onWeatherViewClicked: no package installed -&amp;gt; &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; weatherConfList);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (weatherConfList.&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; 0) {
WeatherConf &lt;span style=&#34;color:#c0f&#34;&gt;weatherConf&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; weatherConfList.&lt;span style=&#34;color:#309&#34;&gt;get&lt;/span&gt;(0);
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;if&lt;/span&gt; (weatherConf.&lt;span style=&#34;color:#309&#34;&gt;getWebUrl&lt;/span&gt;() &lt;span style=&#34;color:#555&#34;&gt;!=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;) {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 拉起网页
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(Intent.&lt;span style=&#34;color:#309&#34;&gt;ACTION_VIEW&lt;/span&gt;)
.&lt;span style=&#34;color:#309&#34;&gt;setData&lt;/span&gt;(Uri.&lt;span style=&#34;color:#309&#34;&gt;parse&lt;/span&gt;(weatherConf.&lt;span style=&#34;color:#309&#34;&gt;getWebUrl&lt;/span&gt;()));
context.&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(intent);
Action.&lt;span style=&#34;color:#309&#34;&gt;WEATHER_INSTALL&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;put&lt;/span&gt;(Attribute.&lt;span style=&#34;color:#309&#34;&gt;PACKAGE&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;with&lt;/span&gt;(weatherConf.&lt;span style=&#34;color:#309&#34;&gt;getWebUrl&lt;/span&gt;()))
.&lt;span style=&#34;color:#309&#34;&gt;anchor&lt;/span&gt;(context);
Log.&lt;span style=&#34;color:#309&#34;&gt;d&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;onWeatherViewClicked: visit url &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; weatherConf.&lt;span style=&#34;color:#309&#34;&gt;getWebUrl&lt;/span&gt;());
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; WeatherEvent.&lt;span style=&#34;color:#309&#34;&gt;WEB_URL&lt;/span&gt;;
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;else&lt;/span&gt; {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// 拉起应用市场安装应用
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; Intent &lt;span style=&#34;color:#c0f&#34;&gt;goToMarket&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Intent(Intent.&lt;span style=&#34;color:#309&#34;&gt;ACTION_VIEW&lt;/span&gt;)
.&lt;span style=&#34;color:#309&#34;&gt;setData&lt;/span&gt;(Uri.&lt;span style=&#34;color:#309&#34;&gt;parse&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;market://details?id=&amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt;
weatherConf.&lt;span style=&#34;color:#309&#34;&gt;getPackageName&lt;/span&gt;()));
context.&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(goToMarket);
Action.&lt;span style=&#34;color:#309&#34;&gt;WEATHER_INSTALL&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;put&lt;/span&gt;(Attribute.&lt;span style=&#34;color:#309&#34;&gt;PACKAGE&lt;/span&gt;.&lt;span style=&#34;color:#309&#34;&gt;with&lt;/span&gt;(weatherConf.&lt;span style=&#34;color:#309&#34;&gt;getPackageName&lt;/span&gt;()))
.&lt;span style=&#34;color:#309&#34;&gt;anchor&lt;/span&gt;(context);
Log.&lt;span style=&#34;color:#309&#34;&gt;d&lt;/span&gt;(TAG, &lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;onWeatherViewClicked: install &amp;#34;&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;+&lt;/span&gt; weatherConf.&lt;span style=&#34;color:#309&#34;&gt;getPackageName&lt;/span&gt;());
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; WeatherEvent.&lt;span style=&#34;color:#309&#34;&gt;INSTALL&lt;/span&gt;;
}
}
} &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;catch&lt;/span&gt; (Exception &lt;span style=&#34;color:#555&#34;&gt;|&lt;/span&gt; Error &lt;span style=&#34;color:#c0f&#34;&gt;e&lt;/span&gt;) {
e.&lt;span style=&#34;color:#309&#34;&gt;printStackTrace&lt;/span&gt;();
}
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; WeatherEvent.&lt;span style=&#34;color:#309&#34;&gt;NONE&lt;/span&gt;;
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;测试分析&#34;&gt;测试分析&lt;/h3&gt;
&lt;p&gt;通过普通的 &lt;code&gt;Junit&lt;/code&gt; 单元测试是不能测试这个方法的，我们不能 &lt;code&gt;new&lt;/code&gt; 一个 &lt;code&gt;Context&lt;/code&gt; 出来，我们也不知道 &lt;code&gt;new&lt;/code&gt; 一个 &lt;code&gt;WeatherController&lt;/code&gt; 对象会有什么结果，也许这个类有严重的耦合和依赖关系，创建一个陌生的对象是不可预期的。 这时我们就需要使用 &lt;code&gt;mockito&lt;/code&gt; 来模拟这些依赖关系并在这些依赖类的方法被调用时做预期处理和检查断言。&lt;/p&gt;
&lt;p&gt;我们有怎样的输入？阅读代码我们可以看到在执行过程中，&lt;code&gt;mWeatherController.loadWeatherConf()&lt;/code&gt; 被执行，&lt;code&gt;WeatherConf&lt;/code&gt; 是一个简单的数据模块类，可以轻松的 &lt;code&gt;new&lt;/code&gt; 出来，我们需要在此方法被调用时返回这个对象的列表；我们需要输入一个模拟的上下文。整理的期望如下：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;模拟上下文 &lt;code&gt;Context&lt;/code&gt; 对象，&lt;code&gt;WeatherView&lt;/code&gt; 和 &lt;code&gt;Weather&lt;/code&gt; 对象无关紧要&lt;/li&gt;
&lt;li&gt;模拟 &lt;code&gt;mWeatherController&lt;/code&gt; 对象，并准备期望的数据&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;要覆盖到所有的逻辑分支，我们需要多种情况的输入和预期：&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;空的配置列表！这是一个边缘场景 &amp;ndash;&amp;gt; 不做任何事&lt;/li&gt;
&lt;li&gt;有效的配置列表，含有应用包名和主界面名，同时应用存在 &amp;ndash;&amp;gt; 拉起应用&lt;/li&gt;
&lt;li&gt;有效的配置列表，含有应用包名和主界面名，同时应用不存在 &amp;ndash;&amp;gt; 拉起应用商店安装应用&lt;/li&gt;
&lt;li&gt;有效的配置列表，含有应用包名、主界面名和拉起网页地址，同时应用不存在 &amp;ndash;&amp;gt; 拉起网页&lt;/li&gt;
&lt;/ol&gt;
&lt;h3 id=&#34;测试代码&#34;&gt;测试代码&lt;/h3&gt;
&lt;h4 id=&#34;准备工作&#34;&gt;准备工作&lt;/h4&gt;
&lt;p&gt;首先我们需要模拟依赖类，我们通过 &lt;code&gt;mock&lt;/code&gt; 方法，将 &lt;code&gt;EventAdapter&lt;/code&gt; 的依赖类都模拟出来，然后再创建一个 &lt;code&gt;EventAdapter&lt;/code&gt; 的对象。因为每个单元测试都是独立无关的，而又都需要这些预置条件，我们可以将这部分代码抽出来放在 &lt;code&gt;@Before&lt;/code&gt; 中作为公共部分，每次测试时都会重新走 &lt;code&gt;@Before&lt;/code&gt; 方法，所以不需要担心数据被共享或混乱。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;Context&lt;/span&gt; mContext;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;AdController&lt;/span&gt; mAdController;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;FeedController&lt;/span&gt; mFeedController;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;WeatherController&lt;/span&gt; mWeatherController;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;private&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;EventAdapter&lt;/span&gt; mEventAdapter;
&lt;span style=&#34;color:#99f&#34;&gt;@Before&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; setup() {
mContext &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mock(Activity.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
mAdController &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mock(AdController.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
mFeedController &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mock(FeedController.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
mWeatherController &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mock(WeatherController.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
mEventAdapter &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; EventAdapter(mAdController, mFeedController,
mWeatherController);
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;第一个场景&#34;&gt;第一个场景&lt;/h4&gt;
&lt;p&gt;空的配置列表！这是一个边缘场景 &amp;ndash;&amp;gt; 不做任何事&lt;/p&gt;
&lt;p&gt;我们先模拟实际操作，添加一个 &lt;code&gt;Weather&lt;/code&gt; 对象到 &lt;code&gt;EventAdapter&lt;/code&gt; 中，然后断言只含有一个 &lt;code&gt;Weather&lt;/code&gt; 对象。之后我们再检查执行 &lt;code&gt;performClick()&lt;/code&gt; 方法执行后的返回值，我们期望 &lt;code&gt;WeatherEvent.NONE&lt;/code&gt; 的结果。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; testWeatherViewClickWithNoneConfig() {
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Weather&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;weathers&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
Weather &lt;span style=&#34;color:#c0f&#34;&gt;weather&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Weather();
weathers.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(weather);
mEventAdapter.&lt;span style=&#34;color:#309&#34;&gt;reloadWeather&lt;/span&gt;(weathers);
assertEquals(1, mEventAdapter.&lt;span style=&#34;color:#309&#34;&gt;getWeather&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;());
WeatherView &lt;span style=&#34;color:#c0f&#34;&gt;weatherView&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mock(WeatherView.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
assertEquals(WeatherEvent.&lt;span style=&#34;color:#309&#34;&gt;NONE&lt;/span&gt;, mEventAdapter.&lt;span style=&#34;color:#309&#34;&gt;performClick&lt;/span&gt;(mContext, weatherView, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;));
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;第二个场景&#34;&gt;第二个场景&lt;/h4&gt;
&lt;p&gt;有效的配置列表，含有应用包名和主界面名，同时应用存在 &amp;ndash;&amp;gt; 拉起应用&lt;/p&gt;
&lt;p&gt;我们需要创建一个 &lt;code&gt;WeatherConf&lt;/code&gt; 对象，并设定预期的字段；然后我们需要设定，在 &lt;code&gt;WeatherController.loadWeatherConf()&lt;/code&gt; 方法调用时会返回刚才创建的对象。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;when(mWeatherController.loadWeatherConf()).thenReturn(weatherConfList);&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;之后，我们执行 &lt;code&gt;performClick()&lt;/code&gt; 方法，并断言期望的返回值为 &lt;code&gt;WeatherEvent.CLICK&lt;/code&gt; 。&lt;/p&gt;
&lt;p&gt;&lt;code&gt;assertEquals(WeatherEvent.CLICK, mEventAdapter.performClick(mContext, weatherView, null));&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;这个预期可能已经足够了，但是我们期望得到更细节的预期：真的拉起了应用吗？&lt;/p&gt;
&lt;p&gt;最后，我们检查 &lt;code&gt;Context.startActivity(Intent intent)&lt;/code&gt; 方法是否被执行，同时检查它的 &lt;code&gt;Intent&lt;/code&gt; 参数是否是我们预设的字段。&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// check startActivity() is called and check argument value
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;ArgumentCaptor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style=&#34;color:#309&#34;&gt;forClass&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
verify(mContext).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(argument.&lt;span style=&#34;color:#309&#34;&gt;capture&lt;/span&gt;());
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { cmp=com.example/.activity.MainActivity }&amp;#34;&lt;/span&gt;, argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这个测试场景的完整代码如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; testWeatherViewClickWithInstalledPackageConfig() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// add weather item
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Weather&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;weathers&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
Weather &lt;span style=&#34;color:#c0f&#34;&gt;weather&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Weather();
weathers.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(weather);
mEventAdapter.&lt;span style=&#34;color:#309&#34;&gt;reloadWeather&lt;/span&gt;(weathers);
assertEquals(1, mEventAdapter.&lt;span style=&#34;color:#309&#34;&gt;getWeather&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;());
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// mock WeatherView
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; WeatherView &lt;span style=&#34;color:#c0f&#34;&gt;weatherView&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mock(WeatherView.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;WeatherConf&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;weatherConfList&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
WeatherConf &lt;span style=&#34;color:#c0f&#34;&gt;weatherConf&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; WeatherConf();
weatherConf.&lt;span style=&#34;color:#309&#34;&gt;setActivityName&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.example.activity.MainActivity&amp;#34;&lt;/span&gt;);
weatherConf.&lt;span style=&#34;color:#309&#34;&gt;setPackageName&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.example&amp;#34;&lt;/span&gt;);
weatherConfList.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(weatherConf);
when(mWeatherController.&lt;span style=&#34;color:#309&#34;&gt;loadWeatherConf&lt;/span&gt;()).&lt;span style=&#34;color:#309&#34;&gt;thenReturn&lt;/span&gt;(weatherConfList);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// check performClick() return value
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; assertEquals(WeatherEvent.&lt;span style=&#34;color:#309&#34;&gt;CLICK&lt;/span&gt;, mEventAdapter.&lt;span style=&#34;color:#309&#34;&gt;performClick&lt;/span&gt;(mContext, weatherView, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;));
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// check startActivity() is called and check argument value
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; ArgumentCaptor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style=&#34;color:#309&#34;&gt;forClass&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
verify(mContext).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(argument.&lt;span style=&#34;color:#309&#34;&gt;capture&lt;/span&gt;());
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { cmp=com.example/.activity.MainActivity }&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;第三个场景&#34;&gt;第三个场景&lt;/h4&gt;
&lt;p&gt;有效的配置列表，含有应用包名和主界面名，同时应用不存在 &amp;ndash;&amp;gt; 拉起应用商店安装应用&lt;/p&gt;
&lt;p&gt;这个场景和第二个场景类似，相同点不在赘述，只是我们需要在拉起应用时抛出一个运行时异常，然后才会走到拉起应用商店的代码：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// throw an no package found exception when startActivity() is called
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;doThrow(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; RuntimeException(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;no package found&amp;#34;&lt;/span&gt;)).&lt;span style=&#34;color:#309&#34;&gt;when&lt;/span&gt;(mContext).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(argThat(
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArgumentMatcher&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt;() {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;boolean&lt;/span&gt; matches(Intent &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; argument.&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { cmp=com.example/.activity.MainActivity }&amp;#34;&lt;/span&gt;);
}
}));&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这个场景的完整测试代码如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; testWeatherViewClickWithNoInstalledPackageConfig() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// add weather item
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Weather&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;weathers&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
Weather &lt;span style=&#34;color:#c0f&#34;&gt;weather&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Weather();
weathers.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(weather);
mEventAdapter.&lt;span style=&#34;color:#309&#34;&gt;reloadWeather&lt;/span&gt;(weathers);
assertEquals(1, mEventAdapter.&lt;span style=&#34;color:#309&#34;&gt;getWeather&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;());
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// mock WeatherView
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; WeatherView &lt;span style=&#34;color:#c0f&#34;&gt;weatherView&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mock(WeatherView.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;WeatherConf&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;weatherConfList&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
WeatherConf &lt;span style=&#34;color:#c0f&#34;&gt;weatherConf&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; WeatherConf();
weatherConf.&lt;span style=&#34;color:#309&#34;&gt;setActivityName&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.example.activity.MainActivity&amp;#34;&lt;/span&gt;);
weatherConf.&lt;span style=&#34;color:#309&#34;&gt;setPackageName&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.example&amp;#34;&lt;/span&gt;);
weatherConfList.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(weatherConf);
when(mWeatherController.&lt;span style=&#34;color:#309&#34;&gt;loadWeatherConf&lt;/span&gt;()).&lt;span style=&#34;color:#309&#34;&gt;thenReturn&lt;/span&gt;(weatherConfList);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// throw an no package found exception when startActivity() is called
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; doThrow(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; RuntimeException(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;no package found&amp;#34;&lt;/span&gt;)).&lt;span style=&#34;color:#309&#34;&gt;when&lt;/span&gt;(mContext).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(argThat(
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArgumentMatcher&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt;() {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;boolean&lt;/span&gt; matches(Intent &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; argument.&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { cmp=com.example/.activity.MainActivity }&amp;#34;&lt;/span&gt;);
}
}));
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// check performClick() return value
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; assertEquals(WeatherEvent.&lt;span style=&#34;color:#309&#34;&gt;INSTALL&lt;/span&gt;, mEventAdapter.&lt;span style=&#34;color:#309&#34;&gt;performClick&lt;/span&gt;(mContext, weatherView, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;));
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// check startActivity() is called and check argument value is start app market
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; ArgumentCaptor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style=&#34;color:#309&#34;&gt;forClass&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
verify(mContext, times(2)).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(argument.&lt;span style=&#34;color:#309&#34;&gt;capture&lt;/span&gt;());
assertEquals(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { act=android.intent.action.VIEW dat=market://details?id=com.example }&amp;#34;&lt;/span&gt;,
argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id=&#34;第四个场景&#34;&gt;第四个场景&lt;/h4&gt;
&lt;p&gt;有效的配置列表，含有应用包名、主界面名和拉起网页地址，同时应用不存在 &amp;ndash;&amp;gt; 拉起网页&lt;/p&gt;
&lt;p&gt;这个场景和前两个场景类似，只是输入的配置字段多了一个拉起链接，我们期望拉起的网页和我们输入的字段相同：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// check startActivity() is called and check argument value is start web view intent
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt;ArgumentCaptor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style=&#34;color:#309&#34;&gt;forClass&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
verify(mContext, times(2)).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(argument.&lt;span style=&#34;color:#309&#34;&gt;capture&lt;/span&gt;());
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;();
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.intent.action.VIEW&amp;#34;&lt;/span&gt;, intent.&lt;span style=&#34;color:#309&#34;&gt;getAction&lt;/span&gt;());
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://www.example.com/weather.html&amp;#34;&lt;/span&gt;, intent.&lt;span style=&#34;color:#309&#34;&gt;getData&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;这个场景的完整测试代码如下：&lt;/p&gt;
&lt;div class=&#34;highlight&#34;&gt;&lt;pre style=&#34;background-color:#f0f3f3;-moz-tab-size:4;-o-tab-size:4;tab-size:4&#34;&gt;&lt;code class=&#34;language-java&#34; data-lang=&#34;java&#34;&gt;&lt;span style=&#34;color:#99f&#34;&gt;@Test&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;void&lt;/span&gt; testWeatherViewClickWithNoInstalledPackageAndWithUrlConfig() {
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// add weather item
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Weather&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;weathers&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
Weather &lt;span style=&#34;color:#c0f&#34;&gt;weather&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; Weather();
weathers.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(weather);
mEventAdapter.&lt;span style=&#34;color:#309&#34;&gt;reloadWeather&lt;/span&gt;(weathers);
assertEquals(1, mEventAdapter.&lt;span style=&#34;color:#309&#34;&gt;getWeather&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;size&lt;/span&gt;());
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// mock WeatherView
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; WeatherView &lt;span style=&#34;color:#c0f&#34;&gt;weatherView&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; mock(WeatherView.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
List&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;WeatherConf&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;weatherConfList&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArrayList&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&amp;gt;&lt;/span&gt;();
WeatherConf &lt;span style=&#34;color:#c0f&#34;&gt;weatherConf&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; WeatherConf();
weatherConf.&lt;span style=&#34;color:#309&#34;&gt;setActivityName&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.example.activity.MainActivity&amp;#34;&lt;/span&gt;);
weatherConf.&lt;span style=&#34;color:#309&#34;&gt;setPackageName&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;com.example&amp;#34;&lt;/span&gt;);
weatherConf.&lt;span style=&#34;color:#309&#34;&gt;setWebUrl&lt;/span&gt;(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://www.example.com/weather.html&amp;#34;&lt;/span&gt;);
weatherConfList.&lt;span style=&#34;color:#309&#34;&gt;add&lt;/span&gt;(weatherConf);
when(mWeatherController.&lt;span style=&#34;color:#309&#34;&gt;loadWeatherConf&lt;/span&gt;()).&lt;span style=&#34;color:#309&#34;&gt;thenReturn&lt;/span&gt;(weatherConfList);
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// throw an no package found exception when startActivity() is called
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; doThrow(&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; RuntimeException(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;no package found&amp;#34;&lt;/span&gt;)).&lt;span style=&#34;color:#309&#34;&gt;when&lt;/span&gt;(mContext).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(argThat(
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;new&lt;/span&gt; ArgumentMatcher&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt;() {
&lt;span style=&#34;color:#99f&#34;&gt;@Override&lt;/span&gt;
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;public&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;boolean&lt;/span&gt; matches(Intent &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt;) {
&lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;return&lt;/span&gt; argument.&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;equals&lt;/span&gt;(
&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;Intent { cmp=com.example/.activity.MainActivity }&amp;#34;&lt;/span&gt;);
}
}));
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// check performClick() return value
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; assertEquals(WeatherEvent.&lt;span style=&#34;color:#309&#34;&gt;WEB_URL&lt;/span&gt;, mEventAdapter.&lt;span style=&#34;color:#309&#34;&gt;performClick&lt;/span&gt;(mContext, weatherView, &lt;span style=&#34;color:#069;font-weight:bold&#34;&gt;null&lt;/span&gt;));
&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;// check startActivity() is called and check argument value is start web view intent
&lt;/span&gt;&lt;span style=&#34;color:#09f;font-style:italic&#34;&gt;&lt;/span&gt; ArgumentCaptor&lt;span style=&#34;color:#555&#34;&gt;&amp;lt;&lt;/span&gt;Intent&lt;span style=&#34;color:#555&#34;&gt;&amp;gt;&lt;/span&gt; &lt;span style=&#34;color:#c0f&#34;&gt;argument&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; ArgumentCaptor.&lt;span style=&#34;color:#309&#34;&gt;forClass&lt;/span&gt;(Intent.&lt;span style=&#34;color:#309&#34;&gt;class&lt;/span&gt;);
verify(mContext, times(2)).&lt;span style=&#34;color:#309&#34;&gt;startActivity&lt;/span&gt;(argument.&lt;span style=&#34;color:#309&#34;&gt;capture&lt;/span&gt;());
Intent &lt;span style=&#34;color:#c0f&#34;&gt;intent&lt;/span&gt; &lt;span style=&#34;color:#555&#34;&gt;=&lt;/span&gt; argument.&lt;span style=&#34;color:#309&#34;&gt;getValue&lt;/span&gt;();
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;android.intent.action.VIEW&amp;#34;&lt;/span&gt;, intent.&lt;span style=&#34;color:#309&#34;&gt;getAction&lt;/span&gt;());
assertEquals(&lt;span style=&#34;color:#c30&#34;&gt;&amp;#34;https://www.example.com/weather.html&amp;#34;&lt;/span&gt;, intent.&lt;span style=&#34;color:#309&#34;&gt;getData&lt;/span&gt;().&lt;span style=&#34;color:#309&#34;&gt;toString&lt;/span&gt;());
}&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h3 id=&#34;总结&#34;&gt;总结&lt;/h3&gt;
&lt;p&gt;运行覆盖率检查，我们可以看到这个方法的业务逻辑已经被完全覆盖到了，可以说明这些测试代码是必要的。&lt;/p&gt;
&lt;p&gt;从代码示例中也可以看出 &lt;code&gt;mockito&lt;/code&gt; 的使用是很简单和强大的，可以帮助我们快速的开发测试代码，断言预期。同时通过这些测试，可以让我们明晰代码的依赖关系，避免编写出来耦合太多或依赖混乱的代码。&lt;/p&gt;</description></item><item><title>如何编写出不可测试的代码</title><link>https://busy.im/post/untestable-code/</link><pubDate>Fri, 05 Oct 2018 11:50:23 +0800</pubDate><guid>https://busy.im/post/untestable-code/</guid><description>
&lt;p&gt;这份指南列举了一些可以帮助你写出不可测试代码的原理或方法。或者，避免这些技术点可以帮助你写出可测试的代码。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;创建你自己的依赖&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要传入对象实例，在方法中间执行 &lt;code&gt;new&lt;/code&gt; 操作来实例化对象。这是一个邪恶的手段，因为无论何时，你在代码块中实例化一个对象然后使用它，任何想要测试这块代码的人都被强制使用这个你实例化的具体对象。他们不能注入一个假的或者其他 &lt;code&gt;mock&lt;/code&gt; 对象来简化这个行为或 对你做了什么做出断言。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;重型构造器&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;创建做很多工作的构造方法，在构造函数中执行的工作越多，在测试夹具中创建对象就越困难。另外，如果你的构造器能够构建出其他比较难构建的对象，那就更好了！你应该期望每个构造方法的依赖传递性都是巨大的，巨大到难以测试。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;依赖于特定的类&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;和特定的类做绑定 - 在任何可能的地方都避免使用接口。（接口可以让别人替换你使用的特定类为他们自己创建的类，他们的类可以实现接口或抽象类里的约定，想要写出好测试的家伙可能会这样做来测试你的代码 - 不要让这些家伙得逞！）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;条件障碍&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;写冗长的 &lt;code&gt;if&lt;/code&gt; 分支和 &lt;code&gt;switch&lt;/code&gt; 声明，并总是对此感觉良好。这样做可以增加测试时需要覆盖的可能执行路径。这个条件圈越复杂，测试越困难！当有人建议使用多态替代条件时，嘲笑他们对测试的体贴。是条件分支即深又宽：如果你不能保持至少5个条件深度，你就是在喂养可测试代码给那些 &lt;code&gt;TDD&lt;/code&gt; 狂热者。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;依赖大型上下文对象&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;传递巨大的上下文对象（或难以构造的小型对象），这将降低方法的清晰度。&lt;code&gt;myMethod(Context ctx)&lt;/code&gt; 不如 &lt;code&gt;myMethod(User user, Label lable)&lt;/code&gt; 清晰。为了进行测试，需要创建，填充和传递上下文对象。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用静态&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;静态，静态无处不在！它们在可测试性方面创造了巨大的危机。他们不能被模拟，同时是一个可以任意获取到的方法， OO (面向对象) 狂热者会说静态方法是其中一个参数应该拥有该方法的标志。但你是3v1L！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用更多的静态&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;静态是一个非常强大的工具，可以使被感染 TDD 的工程师们屈服。静态方法不能被子类重写（有时编写一个子类重写方法是一个测试技术点）。当你使用静态方法时，它们不能被 mock 库模拟（掣肘亲测试工程师的另一个好办法）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用全局标志&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么要明确调用一个方法？ 就像 &lt;a href=&#34;https://en.wikipedia.org/wiki/L._Ron_Hubbard&#34;&gt;L Ron Hubbard&lt;/a&gt; 那样，使用 &amp;ldquo;mind over matter&amp;rdquo; （精神高于物质） 格言来在你的一段代码中设置一个标记，以便在你的应用程序的完全不同的部分产生影响（当你在不同的线程中同时这样做时，它会更有趣！）。测试人员会疯狂地试图弄清楚为什么突然之间有一个条件是 &lt;code&gt;true&lt;/code&gt;，突然又变为 &lt;code&gt;false&lt;/code&gt;。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;到处使用单实例&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;为什么要显而易见的将依赖传递进来而不是使用单实例来隐藏这个依赖？建立一个需要单实例的测试时很难的，当所有测试都依赖于彼此的状态时，TDD 工程师们将会被刺痛到怀疑人生。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;要防守 - 他们为了获取你的代码出击！&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;对传入方法、构造器和中间方法的参数状态采取防卫断言。如果有人能传入一个 &lt;code&gt;null&lt;/code&gt;，那就说明你已经放松警惕。你看，有一些测试怪胎在那想要实例化你的对象，或者在测试中调用你的方法并传入 &lt;code&gt;null&lt;/code&gt;！要积极防止这种情况发生： 用铁拳捍卫你的代码！（同时铭记：如果获取到了你的代码，那他们就不是偏执狂了）&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在任何可能的地方使用基本类型&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每次需要一个值时，与其使用 &lt;code&gt;曲奇饼对象&lt;/code&gt;，不如将基本类型传入后再做你需要的所有解析。基本类型需要人们解析和传递才能得到想要的数据，这使工作变得更艰难 &amp;ndash; 而对象是可以伪造的（喘口气）同时也可以为空，并且可以封装状态（谁才会想要这样做？）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;查找你想要找的任何东西&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;通过随意的查找你可以确保你的对象处于霸主地位，它知道其他任何东西在哪。这样可以让测试开发困难一些，因为他必须模仿环境，以便你的代码能够掌握它所需要的东西。不要害怕你需要获取多少对象，查找的越多，测试越难伪造这些对象。如果你是 &lt;code&gt;InvoiceTaxCalculator&lt;/code&gt; 类，随意做这样的调用 &lt;code&gt;invoiceTaxCalculator.getUser().getDbManager().getCaRateTables().getSalesTaxRate()&lt;/code&gt;。当那些测试维尼告诉你关于 &amp;ldquo;&lt;a href=&#34;http://misko.hevery.com/2008/07/08/how-to-think-about-the-new-operator/&#34;&gt;依赖注入&lt;/a&gt;&amp;ldquo;、&amp;rdquo;&lt;a href=&#34;http://misko.hevery.com/2008/07/18/breaking-the-law-of-demeter-is-like-looking-for-a-needle-in-the-haystack/&#34;&gt;得墨忒耳定律&lt;/a&gt;&amp;rdquo; 或者不要查找别的东西时，请捂住你的耳朵。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;使用静态初始化&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;加载类时，做尽可能多地工作。当想要写测试的疯子们发现加载你的类会导致如网络或文件访问这些令人讨厌的东西时会变得抓狂。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;将外部系统依赖和逻辑代码直接挂钩（耦合）&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;如果你的产品依赖于外部系统如数据库、文件系统或网络，请确保你的业务逻辑编码时引用了尽可能多的这些底层系统实现细节。这样可以阻止其他人不按照你的本意使用你的代码，（如运行2毫秒的小测试而不是5分钟）。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;混合对象生命周期&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;长生命周期的对象引用短生命周期的对象。这使其他人感到困惑，因为长寿命对象在它不再有效或活着之后仍然引用它，这样做特别阴险，不仅是糟糕的设计，而且是邪恶的且难以测试。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;副作用是个好帮手&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;你最好的选择是在您的方法中执行大量副作用生成操作，对于设置器来说尤其如此。副作用越不明显越好，特殊和看似不合理的副作用对单元测试特别有用。使用未初始化或初始化为无效状态的成员字段可以再为副作用锦上添花，一旦你实现这一点，请确保在设置器方法调用时访问这些未经初始化的成员，来产生段错误(&lt;code&gt;SEGV&lt;/code&gt;)或空指针(&lt;code&gt;NPE&lt;/code&gt;)。为什么要这么做？干净的、可读的代码然后才是可以测试的代码，这就是为什么！副作用方法是专门写给那些智力弱者的，他们认为方法名程应该表明这个方法该干什么。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;创建实用工具类 Utility 和函数/方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;例如：你有一个要传递的 &lt;code&gt;URL&lt;/code&gt; 为内容的 &lt;code&gt;String&lt;/code&gt;（遵守尽可能使用基本类型的原则），创建另一个含有静态方法的类比如 &lt;code&gt;isValidUrl(String url)&lt;/code&gt;，不要让面向对象规则告诉你要将这个方法修改为一个 &lt;code&gt;URL&lt;/code&gt; 对象。如果你的静态工具方法能够同时调用外部服务那就更好了！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;创建 Manager 和 Controller&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;到处使用这些 &lt;code&gt;Manager&lt;/code&gt; 和 &lt;code&gt;Controller&lt;/code&gt; 来干预其他对象的职责，不要理睬想将这些职责被移除到其他独立的对象的想法。看看这个你完全不知道要干什么的 &lt;code&gt;SomeObjectManager&lt;/code&gt; 类。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;在对象中做复杂的创建工作&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;每当有人建议你使用工厂来实例化对象时，你要知道你比他们更聪明。你一定比他们更聪明，因为你的类可以有多个职责而且有上千行代码。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;对 if 和 switch 分支亮绿灯&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;不要对嵌套的 &lt;code&gt;if&lt;/code&gt; 分支感到肮脏，这样写 &amp;ldquo;可读性更高&amp;rdquo;。面向对象思想的牛仔们想要用多态来串联对象，对这些面向对象的家伙说不。当你嵌套条件分支时，你唯一需要做的是从顶到底阅读代码。就像一个伟大的小说，一个简单的线性散文代码。使用 &lt;code&gt;OO-overbard&lt;/code&gt; 范例，就像是一个恐怖的 &lt;code&gt;choose-your-own-adventure&lt;/code&gt; 孩子的书，你需要经常在多个类、模式戏法和太多的复杂的概念中翻转。就只使用 &lt;code&gt;if&lt;/code&gt; 就对了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Utils, Utils, Utils!&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;代码异味？没门 - 代码香水！按你的期望将尽可能多的 &lt;code&gt;util&lt;/code&gt; 和 &lt;code&gt;helper&lt;/code&gt; 类乱扔，这些东西是很有帮助的，而且当你把它们粘在某个地方，其他人也可以使用。这就是代码复用，同时对每个人都好，对吧？当心，面向对象规则会说这些方法逻辑数序某些对象，是这些对象的职责。算了吧，你要务实地破坏他们的意愿。毕竟你有一个可以推送的产品！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;每当你需要逃避某些事情时，请使用“重构”&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&amp;ldquo;重构&amp;rdquo;是一个测试驱动、面向对象的家伙们喜欢用的词。因此，如果你想做一些有意义的事情，涉及新的功能，没有测试，只要告诉他们你是“重构”。这可以每次骗到他们。他们认为你需要在重构之前对所有内容进行测试，并且在这之前永远不应该添加新的功能。无视他们的喧哗，按照自己的方式做事吧！&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;final 方法&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;总是使用 &lt;code&gt;final&lt;/code&gt; 类和方法。他们不能为了测试而重载。(-; 不过不要担心没将变量或值对象（除了 setter）写为 &lt;code&gt;final&lt;/code&gt;- 让你的对象状态可以被任何人任何东西改写，保证状态毫无意义，这样做可以让事情变得太容易了。&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;指定特定的类型&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;尽可能多的使用 &lt;code&gt;instanceof&lt;/code&gt;，这可以使伪造对象变得头疼，这样可以告诉他们你掌控着被允许的对象。&lt;/p&gt;
&lt;h3 id=&#34;参考&#34;&gt;参考&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://testing.googleblog.com/2008/07/how-to-write-3v1l-untestable-code.html&#34; title=&#34;How to Write 3v1L, Untestable Code&#34;&gt;How to Write 3v1L, Untestable Code&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;附&#34;&gt;附&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;http://misko.hevery.com/2008/07/30/top-10-things-which-make-your-code-hard-to-test/&#34;&gt;Top 10 things which make your code hard to test&lt;/a&gt;&lt;/p&gt;</description></item><item><title>MS-DOS 在 GITHUB 上托管源码</title><link>https://busy.im/post/ms-dos/</link><pubDate>Sun, 30 Sep 2018 23:54:13 +0800</pubDate><guid>https://busy.im/post/ms-dos/</guid><description>
&lt;p&gt;为了更便于查找和下载，&lt;a href=&#34;https://zh.wikipedia.org/wiki/MS-DOS&#34;&gt;MS-DOS&lt;/a&gt; 即微软磁盘操作系统，在经历了将近 40 年历史后，将早期代码托管在了 Github 上开源。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/msdos-logo_250x250.png&#34; alt=&#34;enter image description here&#34; /&gt;&lt;/p&gt;
&lt;p&gt;这并不是 MS-DOS 第一次开源，早在 2014 年，微软就已经提供 MS-DOS 2.0 版本的源码公开下载。&lt;/p&gt;
&lt;h3 id=&#34;开源项目地址&#34;&gt;开源项目地址&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/Microsoft/MS-DOS&#34;&gt;https://github.com/Microsoft/MS-DOS&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Android 屏幕控制工具 scrcpy</title><link>https://busy.im/post/android-scrcpy/</link><pubDate>Sun, 30 Sep 2018 23:51:07 +0800</pubDate><guid>https://busy.im/post/android-scrcpy/</guid><description>
&lt;p&gt;scrcpy 是 &lt;a href=&#34;https://github.com/Genymobile&#34;&gt;Genymobile&lt;/a&gt; 出品的一款优秀 Android 屏幕控制软件，可以通过 &lt;code&gt;adb&lt;/code&gt; 连接或 &lt;code&gt;adb over TCP/IP&lt;/code&gt; 控制手机屏幕。&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;https://busy.im/img/screenshot-debian-600.jpg&#34; alt=&#34;&#34; /&gt;&lt;/p&gt;
&lt;p&gt;支持 Linux, Windows, Mac OS 平台，使用 &lt;code&gt;Apache2&lt;/code&gt; 协议，开源免费 。&lt;/p&gt;
&lt;p&gt;同时也支持键盘输入、复制电脑剪切板和拖放 &lt;code&gt;APK&lt;/code&gt; 文件安装应用。&lt;/p&gt;
&lt;h3 id=&#34;快捷键&#34;&gt;快捷键&lt;/h3&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;th&gt;Shortcut&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;切换到全屏模式&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;f&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调整窗口到 1:1 (pixel-perfect)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;g&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;调整窗口以移除黑边&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;x&lt;/code&gt; | &lt;em&gt;双击¹&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;点击 &lt;code&gt;HOME&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;h&lt;/code&gt; | &lt;em&gt;鼠标中键&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;点击 &lt;code&gt;BACK&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;b&lt;/code&gt; | &lt;em&gt;鼠标右键²&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;点击 &lt;code&gt;APP_SWITCH&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;s&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;点击 &lt;code&gt;MENU&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;m&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;点击 &lt;code&gt;VOLUME_UP&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;↑&lt;/code&gt; &lt;em&gt;(上键)&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;点击 &lt;code&gt;VOLUME_DOWN&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;↓&lt;/code&gt; &lt;em&gt;(下键)&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;点击 &lt;code&gt;POWER&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;p&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;点亮屏幕&lt;/td&gt;
&lt;td&gt;&lt;em&gt;鼠标右键²&lt;/em&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;复制计算机剪贴板到设备&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;v&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;启用或禁用 FPS 计数器 (在标准输出)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Ctrl&lt;/code&gt;+&lt;code&gt;i&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;从计算机安装 APK&lt;/td&gt;
&lt;td&gt;拖放 APK 文件到窗口上&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;em&gt;¹在黑边上双击来移除它们。&lt;/em&gt;&lt;br /&gt;
&lt;em&gt;²灭屏状态下点击右键会点亮屏幕，否则触发返回键。&lt;/em&gt;&lt;/p&gt;
&lt;h3 id=&#34;同类对比&#34;&gt;同类对比&lt;/h3&gt;
&lt;p&gt;scrcpy 类似于 &lt;a href=&#34;https://www.vysor.io/&#34;&gt;vysor&lt;/a&gt;，后者是以 &lt;code&gt;chrome&lt;/code&gt; 应用形式运行的，同时闭源收费。相较于 vysor pro 版，scrcpy 暂不支持截屏、录屏分享屏幕等功能。对于普通用户和开发者用户，scrcpy 应该是更好的选择。&lt;/p&gt;
&lt;h3 id=&#34;下载地址&#34;&gt;下载地址&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/Genymobile/scrcpy/releases&#34;&gt;https://github.com/Genymobile/scrcpy/releases&lt;/a&gt;&lt;/p&gt;
&lt;h3 id=&#34;开源项目地址&#34;&gt;开源项目地址&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://github.com/Genymobile/scrcpy&#34;&gt;https://github.com/Genymobile/scrcpy&lt;/a&gt;&lt;/p&gt;</description></item><item><title>Archive</title><link>https://busy.im/archive/</link><pubDate>Sun, 30 Sep 2018 23:19:40 +0800</pubDate><guid>https://busy.im/archive/</guid><description/></item><item><title>关于</title><link>https://busy.im/about/</link><pubDate>Sat, 29 Sep 2018 22:11:50 +0800</pubDate><guid>https://busy.im/about/</guid><description>
&lt;p&gt;&lt;a href=&#34;https://busy.im&#34;&gt;Busy.Im&lt;/a&gt; - Too much to learn, too busy to live? You can be more productive!&lt;/p&gt;
&lt;h2 id=&#34;creator&#34;&gt;Creator&lt;/h2&gt;
&lt;h3 id=&#34;ty&#34;&gt;ty&lt;/h3&gt;
&lt;p&gt;一个兴趣使然的软件工程师&lt;/p&gt;
&lt;h3 id=&#34;技能&#34;&gt;技能&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Java/Android&lt;/li&gt;
&lt;li&gt;Linux/Archlinux/Ubuntu/Centos&lt;/li&gt;
&lt;li&gt;Shell/Python/C++/C/Javascript&lt;/li&gt;
&lt;li&gt;System Admin/Backend/Gitlab/Docker&lt;/li&gt;
&lt;li&gt;Hosting/Dedicated Server/KVM/VPS/Seedbox&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;github&#34;&gt;Github&lt;/h2&gt;
&lt;h3 id=&#34;开源项目&#34;&gt;开源项目&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/xdtianyu/CallerInfo&#34;&gt;CallerInfo&lt;/a&gt; - 一个获取号码归属地和其他信息（诈骗、骚扰等）的开源 Android 应用&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/xdtianyu/scripts&#34;&gt;Scripts&lt;/a&gt; - Shell 脚本集合：backup/ddns/le-dns/lets-encrypt 等&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/xdtianyu/PhoneNumber&#34;&gt;PhoneNumber&lt;/a&gt; - 一个获取号码归属地和其他信息（诈骗、骚扰等）的开源库&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/xdtianyu/SourceAnalysis&#34;&gt;SourceAnalysis&lt;/a&gt; - 源码分析文档&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/xdtianyu/ColorPicker&#34;&gt;ColorPicker&lt;/a&gt; - 一个颜色选择库&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/xdtianyu/docker-auto-builds&#34;&gt;docker-auto-builds&lt;/a&gt; 常用服务 docker 化部署&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/xdtianyu/Certs&#34;&gt;Certs&lt;/a&gt; - 利用 Gitlab ci 自动更新和部署 ssl 证书&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/xdtianyu/CallerBackend&#34;&gt;CallerBackend&lt;/a&gt; - CallerInfo 后台数据服务&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/xdtianyu/LanCamera&#34;&gt;LanCamera&lt;/a&gt; - 局域网摄像头监控观看应用&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/xdtianyu/hugo-clean-theme&#34;&gt;hugo-clean-theme&lt;/a&gt; - 一个 hugo 博客的 clean 主题&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id=&#34;contacts&#34;&gt;Contacts&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href=&#34;https://github.com/xdtianyu&#34;&gt;Github: xdtianyu&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://busy.im&#34;&gt;Blog: Busy.Im&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href=&#34;https://www.xdty.org&#34;&gt;Blog: 天宇空间&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description></item></channel></rss>