从事系统开发工作的朋友都知道,手机厂商因为各种原因会在系统中预装一些第三方应用来作为手机的卖点或者额外的收入,但是随着Android 系统日益强大和完善,对一些应用的安全性和稳定性的要求也越来越高。下面就对在预装三方apk的过程中遇到的一些问题做点总结,有错误或者没讲到的地方欢迎大家指正和提建议。

首先,我们先来了解下日常预装三方应用涉及到的安装路径,因预装的需求不同导致部分apk的预装路径也有所差别,主要有以下几个路径:
1.system/app/ :该目录下存放的是一些系统级的应用,该目录下的应用能获取到比较高的权限,应用不可卸载,如Phone、Contacts等

2.system/priv-app/ :该目录是从Android 4.4开始出现的目录,它存放的是一些系统核心应用,能获取到比system/app/下应用更高的权限,应用不可卸载,如:Setting、SystemUI等。

3.vendor/app/ :该目录存放制造商的一些应用,应用不可卸载。

4.data/app/ :该目录下存放的一些第三方应用,应用可卸载。

接下来,我们把预装应用总体分为四个部分:

一、预装有源码的应用
我们可以在package/app/下或者vendor下预装带有源码的应用,这里以vendor下预装为例:

(1).在vendor下新建preload文件夹,在preload下创建APK的文件夹,以DemoApp为例,将APK的Source code 拷贝至DemoApp下,删除/bin 和/gen目录。

(2).在DemoApp中创建Android.mk文件,并添加如下内容:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_PACKAGE_NAME := DemoApp
LOCAL_MODULE_TAGS := optional
LOCAL_CERTIFICATE := platform
LOCAL_PRIVILEGED_MODULE := true
LOCAL_SRC_FILES := $(call all-subdir-java-files)
include $(BUILD_PACKAGE)


(3).在device/制造商/项目名/项目名.mk文件中做如下配置

PRODUCT_PACKAGES += DemoApp

(4).重新编译整个工程

注:LOCAL_CERTIFICATE 表示apk的签名方式,有platform 和PRESIGNED,platform 表示apk使用系统签名,PRESIGNED表示使用apk原来的签名。LOCAL_PRIVILEGED_MODULE := true表示apk将预装到system/priv-app/下

二、预装无源码并且不可卸载的应用
(1).在vendor下新建preloadApk文件夹,以Demo.apk为例,将Demo.apk拷贝至preloadApk下.

(2).新建Android.mk文件,添加如下内容:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
LOCAL_MODULE := Demo
 LOCAL_MODULE_TAGS := optional
 LOCAL_SRC_FILES := $(LOCAL_MODULE).apk
 LOCAL_MODULE_CLASS := APPS
 LOCAL_MODULE_SUFFIX := $(COMMON_ANDROID_PACKAGE_SUFFIX)
 LOCAL_CERTIFICATE := PRESIGNEDpriv-app
LOCAL_PRIVILEGED_MODULE := true
include $(BUILD_PREBUILT)


(3).在device/制造商/项目名/项目名.mk文件中做如下配置

PRODUCT_PACKAGES += DemoApp

(4).重新编译整个工程

三、预装无源码并且可卸载的应用
将apk预装成可卸载的方法有很多种,这里介绍其中一种:

(1).在vendor下新建preloadApk文件夹,以Demo.apk为例,将Demo.apk拷贝至preloadApk下.

(2).新建Android.mk文件,添加如下内容:

LOCAL_PATH:= $(call my-dir)

include $(CLEAR_VARS)
LOCAL_MODULE := Demo
 $(shell mkdir -p (LOCAL_MODULE))
 $(shell cp (LOCAL_MODULE).apk (LOCAL_MODULE)/$(LOCAL_MODULE).apk)


这里我们采用的方法是先通过shell命令的方式临时将apk文件拷贝至系统中一个路径下,这里放在vendor/preloadApp下,

(3).
接着需要在PackageManagerService.java添加如下功能函数:

我们先看下installApps()这个函数,该函数的作用是判断我们上面在Android.mk文件中的shell命令是否把apk文件成功拷贝至我们存放在系统中的路径/vendor/preloadApp/下

private void installApps(){
 //判断vendor/preloadApp 目录是否存在
 File preloadAppDir = new File(“/vendor/preloadApp/”);
 if(!preloadAppDir.exists()){
 Log.e(TAG,“/vendor/preloadApp is not exist”);
 return;
 }
 //从vendor/preloadApp中获取apk文件
 String apkDirFilesNames[] = preloadAppDir.list();
 if(apkDirFilesNames == null){
 Log.e(TAG,“apk file name is null”);
 return;
 }
 //复制apk文件到 /data/app
 for(int i = 0; i < apkDirFilesNames.length; i++){
 File srcFileDir = new File(“/vendor/preloadApp/”, apkDirFilesNames[i]);
 String srcFileNames[] = srcFileDir.list();
 for(int j = 0; j < srcFileNames.length; j++) {
 File srcFile = new File(srcFileDir, srcFileNames[j]);
 File destFileDir = new File(“/data/app/”,apkDirFilesNames[i]);
 preparePreloadAppDir(destFileDir);
 boolean installResult = copyPreloadApp(srcFile, destFileDir);
 if(!installResult){
 Log.d(TAG,“install failed”);
 return;
 }
 }
 }
 }


接着看看preparePreloadAppDir函数,这个函数是由installApps函数调用,创建apk文件存放所需要的目标文件夹,并且赋予相关权限。

private boolean preparePreloadAppDir(File destFileDir) {
 //判断destFileDir是否存在
 if (destFileDir.exists()) {
 Log.e(TAG,"dir already exists: " + destFileDir);
 return true;
 }
 try {
 Os.mkdir(destFileDir.getAbsolutePath(), 0755);
 Os.chmod(destFileDir.getAbsolutePath(), 0755);
 } catch (Exception e) {
 // This purposefully throws if directory already exists
 Log.e(TAG,"Failed to prepare session dir: " + destFileDir + e);
 return false;
 }
 if (!SELinux.restorecon(destFileDir)) {
 Log.e(TAG,"Failed to restorecon session dir: " + destFileDir);
 return false;
 }
 return true;
 }


接下来看下copyPreloadApp函数,该函数也是由installApps函数调用,其作用是将apk文件从/vendor/preloadApp/ 下拷贝至 /data/app/appname/ 下

private boolean copyPreloadApp(File srcFile, File destDir) {
 //检查目录是否存在
 if (srcFile == null || destDir == null || !srcFile.exists()) {
 Log.e(TAG, “invalid arguments”);
 return false;
 }
 try{
 // 删除所有.tmp文件
 for (File file : destDir.listFiles()) {
 if (file.getName().endsWith(“.tmp”)) {
 file.delete();
 }
 }
 final File tmpFile = File.createTempFile(“inherit”, “.tmp”, destDir);
 if (!FileUtils.copyFile(srcFile, tmpFile)) {
 Log.e(TAG, "Failed to copy " + srcFile + " to " + tmpFile);
 return false;
 }
 try {
 Os.chmod(tmpFile.getAbsolutePath(), 0644);
 } catch (Exception e) {
 Log.e(TAG, "Failed to chmod " + tmpFile);
 return false;
 }
 final File toFile = new File(destDir, srcFile.getName());
 Log.e(TAG, "Renaming " + tmpFile + " to " + toFile);
 if (!tmpFile.renameTo(toFile)) {
 Log.e(TAG, "Failed to rename " + tmpFile + " to " + toFile);
 return false;
 }
 } catch (Exception e) {
 Log.e(TAG, "Failed to copy " + e);
 return false;
 }
 return true;
 }


最后在PackageManagerService构造函数中扫描非系统应用的部分添加如下判断:

if (!mOnlyCore) {
 EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_DATA_SCAN_START,
 SystemClock.uptimeMillis());
 //判断为系统第一次启动才做扫描安装操作:
 if(isFirstBoot()){
 try{
 installApps();
 } catch (Exception e) {
 Log.e(TAG,“Failed to installApps” + e);
 }
 }


(4).重新编译整个工程

到此为止,Android 系统预装第三方应用大致为这几类,下面总结下预装三方应用遇到的问题:

四、Signature Scheme v2签名方式APK预装失败
Android 7.0 引入一项新的应用签名方案 APK Signature Scheme v2,它能提供更快的应用安装时间和更多针对未授权 APK 文件更改的保护。在默认情况下,Android Studio 2.2 和 Android Plugin for Gradle 2.2 会使用 APK Signature Scheme v2 和传统签名方案来签署您的应用。

在Android7.0以及之后的版本上预置APK时,如果APK是采用的Signature Scheme v2签名,采用原有的预置应用方式预置APK会失败,经过BUILD_PREBUILT后的apk与原apk是有差异的。Android编译系统会用zipalign对APK进行字节对齐等操作,v2 签名将验证归档中每个文件的已压缩文件内容,如有任何自定义任务篡改 APK 文件或对其进行后处理(无论以任何方式),那么v2 签名会有作废的风险

在预装apk的时候因为各个合作公司的应用签名方式都有可能不同,那么我们怎么知道要预装的第三方apk是V1还是V2签名呢?
好在google给我们提供了工具让我们验证,我们打开SDK所在目录,SDK/build-tools/27.0.1/lib 目录下有个apksigner.jar,我们可以用这个工具来验证apk是否为V2签名。
方法为:
1.将apk文件拷贝至SDK/build-tools/27.0.1/lib目录下
2.打开命令行窗口输入java -jar apksigner.jar verify -v MeiTuan.apk ,接着会出现如下信息,V2为true表示apk做了V2签名。

java -jar apksigner.jar verify -v MeiTuan.apk
 Verifies
 Verified using v1 scheme (JAR signing): true
 Verified using v2 scheme (APK Signature Scheme v2): true


那么到这种问题我们应该怎么解决呢?

解决方案
在预置APK build的时候不让其走编译流程,在APK的 Android.mk加入下面的Shell拷贝脚本:

LOCAL_PATH:= $(call my-dir)
include $(CLEAR_VARS)
 LOCAL_MODULE := DemoApp
 $(shell cp (LOCAL_MODULE).apk (LOCAL_MODULE)/$(LOCAL_MODULE).apk)

好了,以上就是关于Android 系统预装第三方应用的部分总结,后续有问题继续总结到这里,同时欢迎各位同行指正错误和交流技术,祝大家生活愉快!