blob: 3f9d32b83f2e9e514d3e31b60a82133eee4574d0 [file] [log] [blame]
<html devsite><head>
<title>APK 缓存</title>
<meta name="project_path" value="/_project.yaml"/>
<meta name="book_path" value="/_book.yaml"/>
</head>
<body>
<!--
Copyright 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<p>
本文档介绍了如何设计 APK 缓存解决方案,以在支持 A/B 分区的设备上快速安装预加载的应用。
</p>
<p>
原始设备制造商 (OEM) 可以将预加载应用和热门应用放置在 APK 缓存中(对于<a href="/devices/tech/ota/ab_updates">采用 A/B 分区</a>的新设备而言,这种缓存会存储在通常为空的 B 分区中),而且这样不会影响面向用户的任何数据空间。新设备或最近恢复出厂设置的设备上有 APK 缓存时,用户基本上可以立即开始使用,而无需从 Google Play 下载 APK 文件。
</p>
<h2 id="use-cases">用例</h2>
<ul>
<li>将预加载应用存储在 B 分区,以实现更快捷的设置
</li><li>将热门应用存储在 B 分区,以实现更快速的恢复
</li></ul>
<h2 id="prerequisites">前提条件</h2>
<p>
要使用此功能,设备需要满足以下条件:
</p>
<ul>
<li>安装了 Android 8.1 (O MR1) 版本
</li><li>实现了 A/B 分区</li>
</ul>
<p>
您只能在首次启动期间复制预加载内容。这是因为,在支持 A/B 系统更新的设备上,B 分区不会真正存储系统映像文件,而是存储预加载内容(例如零售演示模式资源、OAT 文件和 APK 缓存)。将资源复制到 /data 分区(此操作在首次启动期间完成)后,<a href="/devices/tech/ota/">无线 (OTA) 更新</a>就会将 B 分区用于下载系统映像的已更新版本。
</p>
<p>
因此,APK 缓存无法通过 OTA 进行更新;只能在出厂时预加载到设备上。恢复出厂设置只会影响 /data 分区。系统的 B 分区中仍会有预加载内容,直到系统下载 OTA 映像为止。恢复出厂设置后,系统会再次进行首次启动。这意味着,如果将 OTA 映像下载到 B 分区,然后将设备恢复出厂设置,那么 APK 缓存将无法使用。
</p>
<h2 id="implementation">实现</h2>
<h3 id="approach-1-content-on-system_other-partition">方法 1. system_other 分区上的内容</h3>
<p>
<strong>优点</strong>:预加载内容不会在恢复出厂设置后丢失,系统会在重新启动后从 B 分区复制这些内容。
</p>
<p>
<strong>缺点</strong>:B 分区上需要有可用空间。在恢复出厂设置后进行启动时,需要额外时间来复制预加载内容。
</p>
<p>
为了在首次启动期间复制预加载内容,系统会调用 <code>/system/bin/preloads_copy.sh</code> 中的脚本。系统会通过单个参数(<code>system_b</code> 分区的只读装载点的路径)调用该脚本:
</p>
<p>
要实现此功能,请完成以下特定于设备的更改。以下是 Marlin 设备的示例:</p>
<ol>
<li>将执行复制操作的脚本添加到 <code>device-common.mk</code> 文件(在本例中是 <code>device/google/marlin/device-common.mk</code>),如下所示:
<pre class="devsite-click-to-copy">
# Script that copies preloads directory from system_other to data partition
PRODUCT_COPY_FILES += \
device/google/marlin/preloads_copy.sh:system/bin/preloads_copy.sh
</pre>
在以下位置查找示例脚本源代码:<a href="https://android.googlesource.com/device/google/marlin/+/master/preloads_copy.sh">device/google/marlin/preloads_copy.sh</a>
</li>
<li>修改 <code>init.common.rc</code> 文件,以让其创建必要的<code> /data/preloads</code> 目录和子目录:
<pre class="devsite-click-to-copy">
<code class="devsite-terminal">mkdir /data/preloads 0775 system system</code>
<code class="devsite-terminal">mkdir /data/preloads/media 0775 system system</code>
<code class="devsite-terminal">mkdir /data/preloads/demo 0775 system system</code>
</pre>
在以下位置查找示例 <code>init</code> 文件源代码:<a href="https://android.googlesource.com/device/google/marlin/+/master/init.common.rc">device/google/marlin/init.common.rc</a>
</li>
<li>在文件 <code>preloads_copy.te</code> 中定义新的 SELinux 域:
<pre class="devsite-click-to-copy">
type preloads_copy, domain, coredomain;
type preloads_copy_exec, exec_type, vendor_file_type, file_type;
init_daemon_domain(preloads_copy)
allow preloads_copy shell_exec:file rx_file_perms;
allow preloads_copy toolbox_exec:file rx_file_perms;
allow preloads_copy preloads_data_file:dir create_dir_perms;
allow preloads_copy preloads_data_file:file create_file_perms;
allow preloads_copy preloads_media_file:dir create_dir_perms;
allow preloads_copy preloads_media_file:file create_file_perms;
# Allow to copy from /postinstall
allow preloads_copy system_file:dir r_dir_perms;
</pre>
在以下位置查找示例 SELinux 域文件:<a href="https://android.googlesource.com/device/google/marlin/+/master/sepolicy/preloads_copy.te">/device/google/marlin/+/master/sepolicy/preloads_copy.te</a>
</li>
<li>在新的 <code><device>/sepolicy/file_contexts</device></code> 文件中注册该域:
<pre class="devsite-click-to-copy">
/system/bin/preloads_copy\.sh u:object_r:preloads_copy_exec:s0
</pre>
在以下位置查找示例 SELinux 上下文描述文件:<a href="https://android.googlesource.com/device/google/marlin/+/master/sepolicy/preloads_copy.te">device/google/marlin/sepolicy/preloads_copy.te</a>
</li>
<li>在编译时,您必须将具有预加载内容的目录复制到 <code>system_other</code> 分区:
<pre class="devsite-click-to-copy">
# Copy contents of preloads directory to system_other partition
PRODUCT_COPY_FILES += \
$(call find-copy-subdir-files,*,vendor/google_devices/marlin/preloads,system_other/preloads)
</pre>
这是 Makefile 中的更改示例,完成这项更改后,就可以将 APK 缓存资源从供应商的 Git 代码库(在本例中是 vendor/google_devices/marlin/preloads)复制到 system_other 分区上的相应位置;稍后,在设备首次启动时,APK 缓存资源便会复制到 /data/preloads。此脚本会在编译时运行,以准备 system_other 映像。它希望将预加载内容放置到 vendor/google_devices/marlin/preloads 中。OEM 可以自由选择实际的代码库名称/路径。
</li>
<li>APK 缓存位于 <code>/data/preloads/file_cache</code>,布局如下:
<pre>
/data/preloads/file_cache/
app.package.name.1/
file1
fileN
app.package.name.N/
</pre>
这是设备上的最终目录结构。只要最终文件结构与上述结构相同,OEM 就可以自由选择任何实现方法。
</li>
</ol>
<h3 id="approach-2-content-on-user-data-image">方法 2. 在设备出厂时刷写的用户数据映像上的内容</h3>
<p>
此替代方式假设预加载内容已包含在 <code>/data</code> 分区上的 <code>/data/preloads</code> 目录中。
</p>
<p>
<strong>优点</strong>:开箱即用,无需进行设备自定义即可在首次启动时复制文件。预加载内容已位于 <code>/data</code> 分区。
</p>
<p>
<strong>缺点</strong>:预加载内容会在恢复出厂设置后丢失。虽然这对部分 OEM 来说是可以接受的,但对于在完成质量控制检查后要对设备恢复出厂设置的 OEM 来说,这种方法并不总是行得通。
</p>
<p>
将一种新的 @SystemApi 方法 <code>getPreloadsFileCache()</code> 添加到了 <code>android.content.Context</code>。该方法会返回预加载缓存中某个应用专属目录的绝对路径。
</p>
<p>
添加了新方法 <code>IPackageManager.deletePreloadsFileCache</code>,它允许删除预加载目录以回收所有空间。此方法只能由具有 SYSTEM_UID 的应用(即系统服务器或设置)进行调用。
</p>
<h2 id="app-preparation">应用准备</h2>
<p>
只有特权应用才可以访问预加载缓存目录。要获得访问权限,应用必须安装在<code> /system/priv-app</code> 目录中。
</p>
<h2 id="validation">验证</h2>
<ul>
<li>首次启动后,设备的 <code>/data/preloads/file_cache</code> 目录中应该包含相关内容。
</li><li>如果设备的存储空间不足,则必须删除 <code>file_cache/</code> 目录中的内容。</li>
</ul>
<p>使用示例 <a href="https://android.googlesource.com/platform/development/+/master/samples/apkcachetest/">ApkCacheTest</a> 应用测试 APK 缓存。</p>
<ol>
<li>通过在根目录下运行以下命令来编译应用:<pre class="devsite-click-to-copy">
<code class="devsite-terminal">make ApkCacheTest</code>
</pre>
</li>
<li>将应用安装为特权应用(请注意,只有特权应用可以访问 APK 缓存)。这需要一台已取得 root 权限的设备:
<pre class="devsite-click-to-copy">
<code class="devsite-terminal">adb root &amp;&amp; adb remount</code>
<code class="devsite-terminal">adb shell mkdir /system/priv-app/ApkCacheTest</code>
<code class="devsite-terminal">adb push $ANDROID_PRODUCT_OUT/data/app/ApkCacheTest/ApkCacheTest.apk /system/priv-app/ApkCacheTest/</code>
<code class="devsite-terminal">adb shell stop &amp;&amp; adb shell start</code>
</pre>
</li>
<li>如果需要,模拟文件缓存目录及其内容(此操作也需要 Root 权限):
<pre class="devsite-click-to-copy">
<code class="devsite-terminal">adb shell mkdir -p /data/preloads/file_cache/com.android.apkcachetest</code>
<code class="devsite-terminal">adb shell restorecon -r /data/preloads</code>
<code class="devsite-terminal">adb shell "echo "Test File" &gt; /data/preloads/file_cache/com.android.apkcachetest/test.txt"</code>
</pre>
</li>
<li>测试应用。安装应用并创建测试 <code>file_cache</code> 目录之后,打开 ApkCacheTest 应用。该应用中应该会显示一个 <code>test.txt</code> 文件及其内容。请参见以下屏幕截图,了解这些结果在界面上的显示方式。
<p><img src="/devices/tech/perf/images/apk_cache_test_results.png"/></p>
<figcaption><strong>图 1.</strong> ApkCacheTest 结果</figcaption>
</li>
</ol>
</body></html>