前言:大家都知道,手机关机以后,就是一个冰冷的砖头,只能用来做防身的利器,但是开机后,点击桌面上的任何一个图片,都能开启一个APP,这说明在开机过程中,系统把已经安装好的APP加载到内存中,这到底是怎么做的?

所以我们可以推断,在安卓系统中肯定存在这么一块区域,用于存放已经安装的APP的信息,在开机的时候,通过系统扫描,这块区域,把对应的内容加载到内存中去。

其次,我们知道了在Android系统中存在这样一块区域,在开机的的时候,加载这块区域的信息,从而实现加载在内存中去。那么我们继续反推断,那这块区域的信息,是怎么来的?

应该在安装这个APK的时候,把这个APK的信息写入到该区域的。这样就可以实现了在安卓系统一次安装后,在删除APK文件后,还可以运行APP了。

该篇文章是讲述apk的安装流程,想要了解apk打包、app启动 流程的同学可以点击下面的传送门:

apk打包流程

app启动流程详解

apk信息存储在哪?

回到apk的安装话题上来,上面说的Android区域其实就是:“/data目录”下的system目录,这个目录用来保存很多系统文件。主要工作是创建了5个位于目录/data/system的File对象,分别是:

  • packages.xml:记录了系统中所有安装的应用信息,包括基本信息、签名和权限。
  • pakcages-back.xml:packages.xml文件的备份。
  • pakcages-stoped.xml:记录系统中被强制停止的运行的应用信息,系统在强制停止某个应用的时候,会将应用的信息记录在该文件中。
  • pakcages-stoped-backup.xml:pakcages-stoped.xml文件的备份。
  • packages.list:保存普通应用的数据目录和uid等信息。

这5个文件中pakcages-back.xml和pakcages-stoped-backup.xml是备份文件。当Android对文件packages.xml和pakcages-stoped.xml写之前,会先把它们备份,如果写文件成功了,再把备份文件删除。如果写的时候,系统出问题了,重启后在需要读取这两个文件时,如果发现备份文件存在,会使用备份文件的内容,因为源文件可能已经损坏了。其中packages.xmlPackageManagerServcie启动时,需要用到的文件。

为了更直观的看一下,下面copy一张手机Root后,在/data/system目录下 截图如下:

把packages.xml导出来,因为文件内容太大,所以展示局部内容如下:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<packages><version sdkVersion="23" databaseVersion="3" fingerprint="google/angler/angler:6.0.1/MTC20L/3230295:user/release-keys" /><version volumeUuid="primary_physical" sdkVersion="23" databaseVersion="23" fingerprint="google/angler/angler:6.0.1/MTC19T/2741993:user/release-keys" /><permission-trees><item name="com.google.android.googleapps.permission.GOOGLE_AUTH" package="com.google.android.gsf" /></permission-trees><permissions><item name="android.permission.REAL_GET_TASKS" package="android" protection="18" /><item name="android.permission.REMOTE_AUDIO_PLAYBACK" package="android" protection="2" />.....<item name="com.android.voicemail.permission.ADD_VOICEMAIL" package="android" protection="1" /></permissions><package name="com.google.android.youtube" codePath="/system/app/YouTube" nativeLibraryPath="/system/app/YouTube/lib" primaryCpuAbi="arm64-v8a" publicFlags="945307205" privateFlags="0" ft="11e9134c000" it="11e9134c000" ut="11e9134c000" version="107560144" userId="10075"><sigs count="1"><cert index="0" key="30820252308201bb02044934987e300d06092a864886f70d01010405003070310b3009060355040613025553310b3009060355040813024341311630140603550407130d4d6f756e7461696e205669657731143012060355040a130b476f6f676c652c20496e6331143012060355040b130b476f6f676c652c20496e633110300e06035504031307556e6b6e6f776e301e170d3038313230323032303735385a170d3336303431393032303735385a3070310b3009060355040613025553310b3009060355040813024341311630140603550407130d4d6f756e7461696e205669657731143012060355040a130b476f6f676c652c20496e6331143012060355040b130b476f6f676c652c20496e633110300e06035504031307556e6b6e6f776e30819f300d06092a864886f70d010101050003818d00308189028181009f48031990f9b14726384e0453d18f8c0bbf8dc77b2504a4b1207c4c6c44babc00adc6610fa6b6ab2da80e33f2eef16b26a3f6b85b9afaca909ffbbeb3f4c94f7e8122a798e0eba75ced3dd229fa7365f41516415aa9c1617dd583ce19bae8a0bbd885fc17a9b4bd2640805121aadb9377deb40013381418882ec52282fc580d0203010001300d06092a864886f70d0101040500038181004086669ed631da4384ddd061d226e073b98cc4b99df8b5e4be9e3cbe97501e83df1c6fa959c0ce605c4fd2ac6d1c84cede20476cbab19be8f2203aff7717ad652d8fcc890708d1216da84457592649e0e9d3c4bb4cf58da19db1d4fc41bcb9584f64e65f410d0529fd5b68838c141d0a9bd1db1191cb2a0df790ea0cb12db3a4" /></sigs><perms><item name="com.google.android.c2dm.permission.RECEIVE" granted="true" flags="0" /><item name="android.permission.USE_CREDENTIALS" granted="true" flags="0" /><item name="com.google.android.providers.gsf.permission.READ_GSERVICES" granted="true" flags="0" /><item name="com.google.android.youtube.permission.C2D_MESSAGE" granted="true" flags="0" /><item name="android.permission.MANAGE_ACCOUNTS" granted="true" flags="0" /><item name="android.permission.NFC" granted="true" flags="0" /><item name="android.permission.CHANGE_NETWORK_STATE" granted="true" flags="0" /><item name="android.permission.RECEIVE_BOOT_COMPLETED" granted="true" flags="0" /><item name="com.google.android.gms.permission.AD_ID_NOTIFICATION" granted="true" flags="0" /><item name="android.permission.INTERNET" granted="true" flags="0" /><item name="android.permission.ACCESS_NETWORK_STATE" granted="true" flags="0" /><item name="android.permission.VIBRATE" granted="true" flags="0" /><item name="android.permission.ACCESS_WIFI_STATE" granted="true" flags="0" /><item name="android.permission.WAKE_LOCK" granted="true" flags="0" /></perms><proper-signing-keyset identifier="11" /><domain-verification packageName="com.google.android.youtube" status="0"><domain name="youtu.be" /><domain name="m.youtube.com" /><domain name="youtube.com" /><domain name="www.youtube.com" /></domain-verification></package>

对上面的标签的含义,下面做一个简单的介绍:

<package>表示包信息,下面我们就来解释下标签<package>中的属性:

  • name表示应用的包名。
  • codePath表示的是apk文件的路径。
  • nativeLibraryPath表示应用的native库的存储路径。
  • flags是指应用的属性,如FLAG_SYSTEM、FLAG_PERSISTENT等。
  • it表示应用安装的时间。
  • ut表示应用最后一次修改的时间。
  • version表示应用的版本号。
  • userId表示所属于的id。

<sign>表示应用的签名,下面我们就来解释下 标签<sign>中的属性:

  • count表示标签中包含有多少个证书
  • cert表示具体的证书的值

<perms>表示应用声明使用的权限,每一个子标签代表一项权限。

在上面知道了apk的信息在系统的存储的位置和方式,有人就会有新的疑问:系统是怎么获取到apk的信息的?接下来就是要梳理apk的安装流程了,看了apk安装流程的解析,你就会知道apk的信息是怎么被系统获取到的了。

从源码角度分析apk安装过程

先介绍一下Apk安装的四种方式:

1. 系统应用安装:没有安装界面,在开机时自动完成。
2. 网络下载应用安装:  没有安装界面,在应用市场完成。
3. ADB命令安装:  没有安装界面,通过命令直接安装。
4. 外部设备安装:  有安装界面,通过SD卡等外部设备安装,由packageInstaller处理安装逻辑。

接下来介绍一下APK安装涉及到的几个常用目录:

1.system/app : 系统自带的应用程序,获得root权限才能删除。

2.data/app : 用户程序安装目录,安装时会把apk文件复制到此目录下。

3.data/data : 存放应用程序的数据。

4.data/dalvik-cache : 将apk中的dex文件安装到该目录下(dex文件是dalvik虚拟机的可执行文件,大小约为原始apk的四分之一)。

还有APK安装的预备知识点也说一下吧:

(1)PackageManagerService是由SystemServer启动,PMS负责应用的安装、卸载、权限检查等工作;

(2)在/system/app和/data/app目录下的apk文件,PMS在启动过程中,都会扫描安装;

(3)每次开机时,PMS都会在构造函数中对指定目录下的apk进行扫描,没有安装的apk就会触发安装;

在梳理源码之前先大概说一下apk安装的四大步骤:

(1)拷贝apk到指定的目录:默认情况下,用户安装的apk首先会拷贝到/data/app下,用户有访问/data/app目录的权限,但系统出厂的apk文件会被放到/system分区下,包括/system/app,/system/vendor/app,以及/system/priv-app等,该分区需要root权限的用户才能访问。
(2)加载apk、拷贝文件、创建应用的数据目录:
为了加快APP的启动速度,apk在安装的时候,会首先将APP的可执行文件(dex)拷贝到/data/dalvik-cache目录下,缓存起来。再在/data/data/目录下创建应用程序的数据目录(以应用包名命令),用来存放应用的数据库、xml文件、cache、二进制的so动态库等。
(3)解析apk的AndroidManifest.xml文件:
在安装apk的过程中,会解析apk的AndroidManifest.xml文件,将apk的权限、应用包名、apk的安装位置、版本、userID等重要信息保存在/data/system/packages.xml文件中。这些操作都是在PackageManagerService中完成
的。
(4)显示icon图标:
应用程序经过PMS中的逻辑处理后,相当于已经注册好了,如果想要在Android桌面上看到icon图标,则需要Launcher将系统中已经安装的程序展现在桌面上。

概念说的差不多了,接下来就对不同的安装方式做源码的过程梳理。

源码解析系统应用安装

系统在创建PackageManagerService实例时,会在PMS的构造函数中开始执行安装应用程序的逻辑。

在PMS的构造函数中做了如下几点重要操作:

1.创建Settings对象,添加shareUserId。

    mSettings = new Settings(mPackages);mSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM_UID,ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);mSettings.addSharedUserLPw("android.uid.phone", RADIO_UID,ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);mSettings.addSharedUserLPw("android.uid.log", LOG_UID,ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);mSettings.addSharedUserLPw("android.uid.nfc", NFC_UID,ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);mSettings.addSharedUserLPw("android.uid.bluetooth", BLUETOOTH_UID,ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);mSettings.addSharedUserLPw("android.uid.shell", SHELL_UID,ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);

Settings是Android的包的全局管理者,用于协助PackageManagerService保存所有的安装包信息,同时用于存储系统执行过程中的一些设置,PackageManagerService和Settings之间的类图关系如下:

2.通过PackageManagerService构造函数参数获取应用安装器Installer。

mInstaller = installer;

3.获取SystemConfig实例,读取“/system/etc/permissions/*.xml”资源文件,从资源文件中获取mGlobalsGids(Group-ids)、mSystemPermissions(系统权限)、mAvailableFeatures(系统支持的features)属性。

SystemConfig systemConfig = SystemConfig.getInstance();
mGlobalGids = systemConfig.getGlobalGids();
mSystemPermissions = systemConfig.getSystemPermissions();
mAvailableFeatures = systemConfig.getAvailableFeatures();

4.创建系统消息处理线程。

mHandlerThread = new ServiceThread(TAG,Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
mHandlerThread.start();
mHandler = new PackageHandler(mHandlerThread.getLooper());
Watchdog.getInstance().addThread(mHandler, WATCHDOG_TIMEOUT);

5.执行com.android.server.pm.Settings中的readLPw方法,读取安装包中的信息,并解析成对应的数据结构。

    File dataDir = Environment.getDataDirectory();mAppDataDir = new File(dataDir, "data");mAppInstallDir = new File(dataDir, "app");mAppLib32InstallDir = new File(dataDir, "app-lib");mAsecInternalPath = new File(dataDir, "app-asec").getPath();mUserAppDataDir = new File(dataDir, "user");mDrmAppPrivateInstallDir = new File(dataDir, "app-private");sUserManager = new UserManagerService(context, this,mInstallLock, mPackages);// Propagate permission configuration in to package manager.ArrayMap<String, SystemConfig.PermissionEntry> permConfig= systemConfig.getPermissions();for (int i=0; i<permConfig.size(); i++) {SystemConfig.PermissionEntry perm = permConfig.valueAt(i);BasePermission bp = mSettings.mPermissions.get(perm.name);if (bp == null) {bp = new BasePermission(perm.name, "android", BasePermission.TYPE_BUILTIN);mSettings.mPermissions.put(perm.name, bp);}if (perm.gids != null) {bp.setGids(perm.gids, perm.perUser);}}ArrayMap<String, String> libConfig = systemConfig.getSharedLibraries();for (int i=0; i<libConfig.size(); i++) {mSharedLibraries.put(libConfig.keyAt(i),new SharedLibraryEntry(libConfig.valueAt(i), null));}mFoundPolicyFile = SELinuxMMAC.readInstallPolicy();mRestoredSettings = mSettings.readLPw(this, sUserManager.getUsers(false),mSdkVersion, mOnlyCore);

其中,读取的重要文件就是之前已经说过的保存apk信息的那几个:packages.xml、packages-backup.xml、packages.list、packages-stopped.xml、packages-stopped-backup.xml。并且这几个目录在创建Settings对象时就已经被封装成了对应的File文件。

6.执行PMS中的scanDirLI方法扫描系统安装目录和非系统apk信息。

    File vendorOverlayDir = new File(VENDOR_OVERLAY_DIR);scanDirLI(vendorOverlayDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags | SCAN_TRUSTED_OVERLAY, 0);// Find base frameworks (resource packages without code).scanDirLI(frameworkDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR| PackageParser.PARSE_IS_PRIVILEGED,scanFlags | SCAN_NO_DEX, 0);// Collected privileged system packages.final File privilegedAppDir = new File(Environment.getRootDirectory(), "priv-app");scanDirLI(privilegedAppDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR| PackageParser.PARSE_IS_PRIVILEGED, scanFlags, 0);// Collect ordinary system packages.final File systemAppDir = new File(Environment.getRootDirectory(), "app");scanDirLI(systemAppDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);// Collect all vendor packages.File vendorAppDir = new File("/vendor/app");try {vendorAppDir = vendorAppDir.getCanonicalFile();} catch (IOException e) {// failed to look up canonical path, continue with original one}scanDirLI(vendorAppDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);// Collect all OEM packages.final File oemAppDir = new File(Environment.getOemDirectory(), "app");scanDirLI(oemAppDir, PackageParser.PARSE_IS_SYSTEM| PackageParser.PARSE_IS_SYSTEM_DIR, scanFlags, 0);

其中,系统安装目录有:

  • /system/framework 系统库;

  • /system/app 默认的系统应用;

  • /vendor/app 厂商定制的应用;

非系统apk信息目录有:

  • /data/app/;

  • /system/priv-app/;

  • /data/app-private/;

到此,PMS构造函数中主要的逻辑操作就介绍完了,接下来继续探究扫描安装过程:

(1)深入到PackageManagerService—>scanDirLI方法中:

private void scanDirLI(File dir, int parseFlags, int scanFlags, long currentTime) {final File[] files = dir.listFiles();if (ArrayUtils.isEmpty(files)) {Log.d(TAG, "No files in app dir " + dir);return;}if (DEBUG_PACKAGE_SCANNING) {Log.d(TAG, "Scanning app dir " + dir + " scanFlags=" + scanFlags+ " flags=0x" + Integer.toHexString(parseFlags));}for (File file : files) {final boolean isPackage = (isApkFile(file) || file.isDirectory())&& !PackageInstallerService.isStageName(file.getName());if (!isPackage) {// Ignore entries which are not packagescontinue;}try {scanPackageLI(file, parseFlags | PackageParser.PARSE_MUST_BE_APK,scanFlags, currentTime, null);//注释1} catch (PackageManagerException e) {Slog.w(TAG, "Failed to parse " + file + ": " + e.getMessage());// Delete invalid userdata appsif ((parseFlags & PackageParser.PARSE_IS_SYSTEM) == 0 &&e.error == PackageManager.INSTALL_FAILED_INVALID_APK) {logCriticalInfo(Log.WARN, "Deleting invalid package at " + file);if (file.isDirectory()) {mInstaller.rmPackageDir(file.getAbsolutePath());} else {file.delete();}}}}
}

在注释1处,对于目录中的每一个文件,如果是已apk作为后缀名的,就会调用PackageManagerService—>scanPackageLI方法进行扫描解析。

(2)深入到PackageManagerService—>scanPackageLI方法中:

private PackageParser.Package scanPackageLI(File scanFile, int parseFlags, int scanFlags,long currentTime, UserHandle user) throws PackageManagerException {if (DEBUG_INSTALL) Slog.d(TAG, "Parsing: " + scanFile);parseFlags |= mDefParseFlags;PackageParser pp = new PackageParser();//注释1pp.setSeparateProcesses(mSeparateProcesses);pp.setOnlyCoreApps(mOnlyCore);pp.setDisplayMetrics(mMetrics);if ((scanFlags & SCAN_TRUSTED_OVERLAY) != 0) {parseFlags |= PackageParser.PARSE_TRUSTED_OVERLAY;}final PackageParser.Package pkg;try {pkg = pp.parsePackage(scanFile, parseFlags);//注释2} catch (PackageParserException e) {throw PackageManagerException.from(e);}......省略......        // readersynchronized (mPackages) {//注释3// Look to see if we already know about this package.String oldName = mSettings.mRenamedPackages.get(pkg.packageName);if (pkg.mOriginalPackages != null && pkg.mOriginalPackages.contains(oldName)) {// This package has been renamed to its original name.  Let's// use that.ps = mSettings.peekPackageLPr(oldName);}// If there was no original package, see one for the real package name.if (ps == null) {ps = mSettings.peekPackageLPr(pkg.packageName);}// Check to see if this package could be hiding/updating a system// package.  Must look for it either under the original or real// package name depending on our state.updatedPkg = mSettings.getDisabledSystemPkgLPr(ps != null ? ps.name : pkg.packageName);if (DEBUG_INSTALL && updatedPkg != null) Slog.d(TAG, "updatedPkg = " + updatedPkg);}......省略......       PackageParser.Package scannedPkg = scanPackageLI(pkg, parseFlags, scanFlags| SCAN_UPDATE_SIGNATURE, currentTime, user);//注释4/** If the system app should be overridden by a previously installed* data, hide the system app now and let the /data/app scan pick it up* again.*/if (shouldHideSystemApp) {synchronized (mPackages) {mSettings.disableSystemPackageLPw(pkg.packageName);}}return scannedPkg;
}

在注释1处:创建一个PackageParser实例。

在注释2处:调用PackageParser的parsePackage函数来对apk安装包的AndroidManifest.xml文件扫描和提取证书信息,然后构建一个PackageParser.Package对象,并将其返回。

在注释3处:将解析返回的PackageParser对象中的信息保存到PMS中。

在注释4处:调用另一个重载的scanPackageLI方法来构建一个PackageSetting对象,这个对象保存的信息最后会通过writeLPr写入到/data/system/packages.xml文件中去。

(3)根据(2)中注释2深入到PackageParser—>parsePackage方法中:

public Package parsePackage(File packageFile, int flags) throws PackageParserException {if (packageFile.isDirectory()) {return parseClusterPackage(packageFile, flags);} else {return parseMonolithicPackage(packageFile, flags);}
}

这里是根据packageFile是否是目录分别调用parseClusterPackage和parseMonolithicPackage去解析。

其中parseClusterPackage方法如下:

private Package parseClusterPackage(File packageDir, int flags) throws PackageParserException {final PackageLite lite = parseClusterPackageLite(packageDir, 0);if (mOnlyCoreApps && !lite.coreApp) {throw new PackageParserException(INSTALL_PARSE_FAILED_MANIFEST_MALFORMED,"Not a coreApp: " + packageDir);}final AssetManager assets = new AssetManager();try {...省略...final File baseApk = new File(lite.baseCodePath);final Package pkg = parseBaseApk(baseApk, assets, flags);//注释1if (pkg == null) {throw new PackageParserException(INSTALL_PARSE_FAILED_NOT_APK,"Failed to parse base APK: " + baseApk);}...省略...pkg.codePath = packageDir.getAbsolutePath();return pkg;} finally {IoUtils.closeQuietly(assets);}
}

parseMonolithicPackage方法如下:

public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {...省略...final AssetManager assets = new AssetManager();try {final Package pkg = parseBaseApk(apkFile, assets, flags);//注释2pkg.codePath = apkFile.getAbsolutePath();return pkg;} finally {IoUtils.closeQuietly(assets);}
}

从parseClusterPackage的注释1和parseMonolithicPackage的注释2可以看出都是调用parseBaseApk去解析。

往下继续探究parseBaseApk方法:

 private Package parseBaseApk(File apkFile, AssetManager assets, int flags)throws PackageParserException {...省略...try {res = new Resources(assets, mMetrics, null);assets.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,Build.VERSION.RESOURCES_SDK_INT);parser = assets.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);final String[] outError = new String[1];final Package pkg = parseBaseApk(res, parser, flags, outError);//注释1if (pkg == null) {throw new PackageParserException(mParseError,apkPath + " (at " + parser.getPositionDescription() + "): " + outError[0]);}...省略...} catch (PackageParserException e) {throw e;} catch (Exception e) {throw new PackageParserException(INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION,"Failed to read manifest from " + apkPath, e);} finally {IoUtils.closeQuietly(parser);}
}

在注释1处可以看到是调用另一个重载的parseBaseApk方法对apk进行解析。

(4) 先回到(2)中注释4处深入到PackageManagerService—>另一个scanPackageLI方法中:

private PackageParser.Package scanPackageLI(PackageParser.Package pkg, int parseFlags,int scanFlags, long currentTime, UserHandle user) throws PackageManagerException {boolean success = false;try {final PackageParser.Package res = scanPackageDirtyLI(pkg, parseFlags, scanFlags,currentTime, user);//注释1success = true;return res;} finally {if (!success && (scanFlags & SCAN_DELETE_DATA_ON_FAILURES) != 0) {removeDataDirsLI(pkg.volumeUuid, pkg.packageName);}}
}

接着由注释1继续深入PackageManagerService—>scanPackageDirtyLI方法中:

private PackageParser.Package scanPackageDirtyLI(PackageParser.Package pkg, int parseFlags,int scanFlags, long currentTime, UserHandle user) throws PackageManagerException {final File scanFile = new File(pkg.codePath);......省略......// And now re-install the app.ret = createDataDirsLI(pkg.volumeUuid, pkgName, pkg.applicationInfo.uid,HasSystemUidErrors = true;//注释1......省略......//invoke installer to do the actual installationint ret = createDataDirsLI(pkg.volumeUuid, pkgName, pkg.applicationInfo.uid,pkg.applicationInfo.seinfo);//注释2if (ret < 0) {// Error from installerthrow new PackageManagerException(INSTALL_FAILED_INSUFFICIENT_STORAGE,"Unable to create data dirs [errorCode=" + ret + "]");}......省略......
}

从注释1和注释2处可以看出调用了PMS中的createDataDirsLI方法,给installed发送消息,为应用程序创建对应的数据目录,如果目录已经存在,也会重新创建一遍。

继续深入到PMS的createDataDirsLI方法中:

private int createDataDirsLI(String volumeUuid, String packageName, int uid, String seinfo) {int[] users = sUserManager.getUserIds();int res = mInstaller.install(volumeUuid, packageName, uid, uid, seinfo);//注释1if (res < 0) {return res;}for (int user : users) {if (user != 0) {res = mInstaller.createUserData(volumeUuid, packageName,//注释2UserHandle.getUid(user, uid), user, seinfo);if (res < 0) {return res;}}}return res;
}

从上面代码可知最后是调用mInstaller.install()函数完成APK安装。到此为止,系统应用的安装流程差不多就完成了。

网络下载应用安装

在网络应用下载完成后,会自动调用PackageManagerService中的installPackage方法:

 @Override
public void installPackage(String originPath, IPackageInstallObserver2 observer,int installFlags, String installerPackageName, VerificationParams verificationParams,String packageAbiOverride) {installPackageAsUser(originPath, observer, installFlags, installerPackageName,verificationParams, packageAbiOverride, UserHandle.getCallingUserId());
}

在上面的代码里可以看到主要是调用了PMS—>installPackageAsUser方法:

public void installPackageAsUser(String originPath, IPackageInstallObserver2 observer,int installFlags, String installerPackageName, VerificationParams verificationParams,String packageAbiOverride, int userId) {mContext.enforceCallingOrSelfPermission(android.Manifest.permission.INSTALL_PACKAGES, null);......省略......final Message msg = mHandler.obtainMessage(INIT_COPY);msg.obj = new InstallParams(origin, null, observer, params.installFlags,installerPackageName, params.volumeUuid, verifParams, user, params.abiOverride,params.grantedRuntimePermissions);mHandler.sendMessage(msg);
}

这里主要是获取用户安装位置,将InstallParams对象封装在Message里,然后发一个Handler消息。

接下来深入到处理INIT_COPY的地方:PMS—>doHandleMessage方法中:

void doHandleMessage(Message msg) {switch (msg.what) {case INIT_COPY: {......省略......                    if (!mBound) {// If this is the only one pending we might// have to bind to the service again.if (!connectToService()) {Slog.e(TAG, "Failed to bind to media container service");params.serviceError();return;} else {// Once we bind to the service, the first// pending request will be processed.mPendingInstalls.add(idx, params);}} else {mPendingInstalls.add(idx, params);// Already bound to the service. Just make// sure we trigger off processing the first request.if (idx == 0) {mHandler.sendEmptyMessage(MCS_BOUND);}}break;}case MCS_BOUND: {if (DEBUG_INSTALL) Slog.i(TAG, "mcs_bound");if (msg.obj != null) {mContainerService = (IMediaContainerService) msg.obj;}if (mContainerService == null) {......省略......} else if (mPendingInstalls.size() > 0) {HandlerParams params = mPendingInstalls.get(0);if (params != null) {if (params.startCopy()) {//注释1// We are done...  look for more work or to// go idle.if (DEBUG_SD_INSTALL) Log.i(TAG,"Checking for more work or unbind...");// Delete pending installif (mPendingInstalls.size() > 0) {mPendingInstalls.remove(0);}if (mPendingInstalls.size() == 0) {if (mBound) {if (DEBUG_SD_INSTALL) Log.i(TAG,"Posting delayed MCS_UNBIND");removeMessages(MCS_UNBIND);Message ubmsg = obtainMessage(MCS_UNBIND);// Unbind after a little delay, to avoid// continual thrashing.sendMessageDelayed(ubmsg, 10000);}} else {// There are more pending requests in queue.// Just post MCS_BOUND message to trigger processing// of next pending install.if (DEBUG_SD_INSTALL) Log.i(TAG,"Posting MCS_BOUND for next work");mHandler.sendEmptyMessage(MCS_BOUND);}}}} else {// Should never happen ideally.Slog.w(TAG, "Empty queue");}break;}

上面的逻辑是:如果msg.what是INIT_COPY:则连接DefaultContainerService服务,将我们要安装的信息方法HandlerParams的mPendingInstalls中,然后再发送MCS_BOUND消息。

如果msg.what是MCS_BOUND:则通过HandlerParams params = mPendingInstalls.get(0)获取要安装包的信息,然后清除包信息,如果还有其他包,则继续发MCS_BOUND消息,一直循环,直到安装包都安装完。

然后再调用PackageManagerService.HandlerParams—>startCopy方法:

final boolean startCopy() {boolean res;try {if (DEBUG_INSTALL) Slog.i(TAG, "startCopy " + mUser + ": " + this);if (++mRetries > MAX_RETRIES) {Slog.w(TAG, "Failed to invoke remote methods on default container service. Giving up");mHandler.sendEmptyMessage(MCS_GIVE_UP);handleServiceError();return false;} else {handleStartCopy();//注释1res = true;}} catch (RemoteException e) {if (DEBUG_INSTALL) Slog.i(TAG, "Posting install MCS_RECONNECT");mHandler.sendEmptyMessage(MCS_RECONNECT);res = false;}handleReturnCode();//注释2return res;}

其中startCopy中有两个重要的方法handleStartCopy和handleReturnCode。handleStartCopy中会检查应用是否能安装成功,如果不能安装成功,则会返回failed的CODE;返回res标识,判断是否安装成功。handleReturnCode方法有两处重载了,一个是删除时调用的,一个是安装时调用的,下面列出安装的地方:

@Overridevoid handleReturnCode() {// If mArgs is null, then MCS couldn't be reached. When it// reconnects, it will try again to install. At that point, this// will succeed.if (mArgs != null) {processPendingInstall(mArgs, mRet);}}

可以看到调用了processPendingInstall方法:

    private void processPendingInstall(final InstallArgs args, final int currentStatus) {// Queue up an async operation since the package installation may take a little while.mHandler.post(new Runnable() {public void run() {mHandler.removeCallbacks(this);// Result object to be returnedPackageInstalledInfo res = new PackageInstalledInfo();res.returnCode = currentStatus;res.uid = -1;res.pkg = null;res.removedInfo = new PackageRemovedInfo();if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {args.doPreInstall(res.returnCode);synchronized (mInstallLock) {installPackageLI(args, res);//注释1}args.doPostInstall(res.returnCode, res.uid);}......省略......
}

从注释1处可以看到,调用的是installPackageLI方法:

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {......省略......if (replace) {replacePackageLI(pkg, parseFlags, scanFlags | SCAN_REPLACING, args.user,installerPackageName, volumeUuid, res);} else {installNewPackageLI(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES,args.user, installerPackageName, volumeUuid, res);}......省略......
}

如果是替换安装,则走replacePackageLI的逻辑。追踪replacePackageLI的逻辑流程:

  • replacePackageLI—>如果是系统应用则调用replaceSystemPackageLI—>再调用PackageParser.Package的scanPackageLI方法。
  • replacePackageLI—>如果是非系统应用则调用replaceNonSystemPackageLI—>再调用PackageParser.Package的scanPackageLI方法。

如果是第一次安装,则走installNewPackageLI。追踪installNewPackageLI的逻辑流程:

到了调用PackageParser.Package的scanPackageLI方法就到了系统应用安装过程中的“继续探究扫描安装过程”逻辑,后面的安装逻辑就与系统应用安装的逻辑一样了。

通过命令安装应用

命令安装应用的入口是:framework/base/cmds/pm/src/com/android/commands/pm/Pm.java

应用安装的时候就会调用runInstall()方法:

  • installNewPackageLI—>再调用PackageParser.Package的scanPackageLI方法。
private int runInstall() {......省略......try {VerificationParams verificationParams = new VerificationParams(verificationURI,originatingURI, referrerURI, VerificationParams.NO_UID, null);mPm.installPackageAsUser(apkFilePath, obs.getBinder(), installFlags,installerPackageName, verificationParams, abi, userId);//注释1......省略......            } catch (RemoteException e) {System.err.println(e.toString());System.err.println(PM_NOT_RUNNING_ERR);return 1;}
}

从注释1处可以看到安装逻辑调用的是PackageManagerService—>installPackageAsUser方法。后面的调用逻辑就与“网络下载应用安装”的逻辑一样了。

通过外部设备安装应用

通过外部设备安装应用,调用的是Android内部的PackageInstaller去完成的,其本身也是一个apk。

代码位置在:/packages/apps/PackageInstaller/。 用于显示安装apk,但其最终的本质还是调用PackageManagerService去完成安装的。

当点击文件管理器中的apk时,文件管理器就会启动PackageInstaller中的PackageInstallerActivity,并将apk的信息通过intent传递给PackageInstallerActivity。

(1)深入到PackageInstallerActivity—>onCreate方法:

@Override
protected void onCreate(Bundle icicle) {super.onCreate(icicle);mPm = getPackageManager();mInstaller = mPm.getPackageInstaller();//注释1mUserManager = (UserManager) getSystemService(Context.USER_SERVICE);final Intent intent = getIntent();//注释2if (PackageInstaller.ACTION_CONFIRM_PERMISSIONS.equals(intent.getAction())) {final int sessionId = intent.getIntExtra(PackageInstaller.EXTRA_SESSION_ID, -1);final PackageInstaller.SessionInfo info = mInstaller.getSessionInfo(sessionId);if (info == null || !info.sealed || info.resolvedBaseCodePath == null) {Log.w(TAG, "Session " + mSessionId + " in funky state; ignoring");finish();return;}mSessionId = sessionId;mPackageURI = Uri.fromFile(new File(info.resolvedBaseCodePath));mOriginatingURI = null;mReferrerURI = null;} else {mSessionId = -1;mPackageURI = intent.getData();mOriginatingURI = intent.getParcelableExtra(Intent.EXTRA_ORIGINATING_URI);mReferrerURI = intent.getParcelableExtra(Intent.EXTRA_REFERRER);}final boolean unknownSourcesAllowedByAdmin = isUnknownSourcesAllowedByAdmin();final boolean unknownSourcesAllowedByUser = isUnknownSourcesEnabled();//注释3boolean requestFromUnknownSource = isInstallRequestFromUnknownSource(intent);......省略.....// Block the install attempt on the Unknown Sources setting if necessary.if (!requestFromUnknownSource) {initiateInstall();//注释4return;}......省略.....if (!unknownSourcesAllowedByAdmin|| (!unknownSourcesAllowedByUser && isManagedProfile)) {showDialogInner(DLG_ADMIN_RESTRICTS_UNKNOWN_SOURCES);mInstallFlowAnalytics.setFlowFinished(InstallFlowAnalytics.RESULT_BLOCKED_BY_UNKNOWN_SOURCES_SETTING);} else if (!unknownSourcesAllowedByUser) {// Ask user to enable setting firstshowDialogInner(DLG_UNKNOWN_SOURCES);mInstallFlowAnalytics.setFlowFinished(InstallFlowAnalytics.RESULT_BLOCKED_BY_UNKNOWN_SOURCES_SETTING);} else {initiateInstall();//注释5}
}

注释1:获取PackageInstaller对象;

注释2:PackageInstaller检查权限;

注释3:PackageInstaller检查是否开启未知来源;

注释4、注释5:调用initiateInstall方法;

(2)继续深入到PackageInstallerActivity—>initiateInstall方法:

private void initiateInstall() {String pkgName = mPkgInfo.packageName;// Check if there is already a package on the device with this name// but it has been renamed to something else.String[] oldName = mPm.canonicalToCurrentPackageNames(new String[] { pkgName });if (oldName != null && oldName.length > 0 && oldName[0] != null) {pkgName = oldName[0];mPkgInfo.packageName = pkgName;mPkgInfo.applicationInfo.packageName = pkgName;}// Check if package is already installed. display confirmation dialog if replacing pkgtry {// This is a little convoluted because we want to get all uninstalled// apps, but this may include apps with just data, and if it is just// data we still want to count it as "installed".mAppInfo = mPm.getApplicationInfo(pkgName,PackageManager.GET_UNINSTALLED_PACKAGES);if ((mAppInfo.flags&ApplicationInfo.FLAG_INSTALLED) == 0) {mAppInfo = null;}} catch (NameNotFoundException e) {mAppInfo = null;}mInstallFlowAnalytics.setReplace(mAppInfo != null);mInstallFlowAnalytics.setSystemApp((mAppInfo != null) && ((mAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0));startInstallConfirm();//调用
}

initiateInstall方法主要负责检查是否已经安装过,是否是系统应用等。然后继续调用了startInstallConfirm方法。

(3) 接着深入到PackageInstallerActivity—>startInstallConfirm方法:

private void startInstallConfirm() {TabHost tabHost = (TabHost)findViewById(android.R.id.tabhost);tabHost.setup();ViewPager viewPager = (ViewPager)findViewById(R.id.pager);TabsAdapter adapter = new TabsAdapter(this, tabHost, viewPager);adapter.setOnTabChangedListener(new TabHost.OnTabChangeListener() {@Overridepublic void onTabChanged(String tabId) {if (TAB_ID_ALL.equals(tabId)) {mInstallFlowAnalytics.setAllPermissionsDisplayed(true);} else if (TAB_ID_NEW.equals(tabId)) {mInstallFlowAnalytics.setNewPermissionsDisplayed(true);}}});// If the app supports runtime permissions the new permissions will// be requested at runtime, hence we do not show them at install.boolean supportsRuntimePermissions = mPkgInfo.applicationInfo.targetSdkVersion>= Build.VERSION_CODES.M;boolean permVisible = false;mScrollView = null;mOkCanInstall = false;int msg = 0;AppSecurityPermissions perms = new AppSecurityPermissions(this, mPkgInfo);final int N = perms.getPermissionCount(AppSecurityPermissions.WHICH_ALL);if (mAppInfo != null) {msg = (mAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0? R.string.install_confirm_question_update_system: R.string.install_confirm_question_update;mScrollView = new CaffeinatedScrollView(this);mScrollView.setFillViewport(true);boolean newPermissionsFound = false;if (!supportsRuntimePermissions) {newPermissionsFound =(perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0);mInstallFlowAnalytics.setNewPermissionsFound(newPermissionsFound);if (newPermissionsFound) {permVisible = true;mScrollView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_NEW));}}if (!supportsRuntimePermissions && !newPermissionsFound) {LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);TextView label = (TextView)inflater.inflate(R.layout.label, null);label.setText(R.string.no_new_perms);mScrollView.addView(label);}adapter.addTab(tabHost.newTabSpec(TAB_ID_NEW).setIndicator(getText(R.string.newPerms)), mScrollView);} else  {findViewById(R.id.tabscontainer).setVisibility(View.GONE);findViewById(R.id.divider).setVisibility(View.VISIBLE);}if (!supportsRuntimePermissions && N > 0) {permVisible = true;LayoutInflater inflater = (LayoutInflater)getSystemService(Context.LAYOUT_INFLATER_SERVICE);View root = inflater.inflate(R.layout.permissions_list, null);if (mScrollView == null) {mScrollView = (CaffeinatedScrollView)root.findViewById(R.id.scrollview);}((ViewGroup)root.findViewById(R.id.permission_list)).addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_ALL));adapter.addTab(tabHost.newTabSpec(TAB_ID_ALL).setIndicator(getText(R.string.allPerms)), root);}mInstallFlowAnalytics.setPermissionsDisplayed(permVisible);if (!permVisible) {if (mAppInfo != null) {// This is an update to an application, but there are no// permissions at all.msg = (mAppInfo.flags & ApplicationInfo.FLAG_SYSTEM) != 0? R.string.install_confirm_question_update_system_no_perms: R.string.install_confirm_question_update_no_perms;} else {// This is a new application with no permissions.msg = R.string.install_confirm_question_no_perms;}tabHost.setVisibility(View.GONE);mInstallFlowAnalytics.setAllPermissionsDisplayed(false);mInstallFlowAnalytics.setNewPermissionsDisplayed(false);findViewById(R.id.filler).setVisibility(View.VISIBLE);findViewById(R.id.divider).setVisibility(View.GONE);mScrollView = null;}if (msg != 0) {((TextView)findViewById(R.id.install_confirm_question)).setText(msg);}mInstallConfirm.setVisibility(View.VISIBLE);mOk = (Button)findViewById(R.id.ok_button);mOk.requestFocus();mCancel = (Button)findViewById(R.id.cancel_button);mOk.setOnClickListener(this);mCancel.setOnClickListener(this);
//add by wangqi begin{@TabWidget tabWidget = tabHost.getTabWidget();int childCount = tabWidget.getChildCount();if (childCount == 2) {final View left = tabWidget.getChildAt(0);final View right = tabWidget.getChildAt(1);left.setId(tabWidget.getId() + 1);right.setId(tabWidget.getId() + 2);right.setNextFocusLeftId(left.getId());right.setOnKeyListener(new View.OnKeyListener() {@Overridepublic boolean onKey(View v, int keyCode, KeyEvent event) {if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT && event.getAction() == KeyEvent.ACTION_DOWN) {left.requestFocus();}return true;}});}
// end @}if (mScrollView == null) {// There is nothing to scroll view, so the ok button is immediately// set to install.//mOk.setText(R.string.install);mOkCanInstall = true;} else {mScrollView.setFullScrollAction(new Runnable() {@Overridepublic void run() {//mOk.setText(R.string.install);mOkCanInstall = true;}});}
}

从上面代码可以看到startInstallConfirm主要负责界面初始化,显示权限信息等。

界面初始化完成后,安装界面就会呈现在用户面前,如果用户想要安装这个应用程序,可以直接点击确认按钮,此时就会调用PackageInstallerActivity中的onClick方法:

public void onClick(View v) {if (v == mOk) {if (mOkCanInstall || mScrollView == null) {mInstallFlowAnalytics.setInstallButtonClicked();if (mSessionId != -1) {mInstaller.setPermissionsResult(mSessionId, true);// We're only confirming permissions, so we don't really know how the// story ends; assume success.mInstallFlowAnalytics.setFlowFinishedWithPackageManagerResult(PackageManager.INSTALL_SUCCEEDED);finish();} else {startInstall();//注释1}} else {mScrollView.pageScroll(View.FOCUS_DOWN);}} else if(v == mCancel) {// Cancel and finishsetResult(RESULT_CANCELED);if (mSessionId != -1) {mInstaller.setPermissionsResult(mSessionId, false);}mInstallFlowAnalytics.setFlowFinished(InstallFlowAnalytics.RESULT_CANCELLED_BY_USER);finish();}
}

onClick方法中分别会对取消和确定按钮做处理,如果是确定按钮,就会调用注释1处的startInstall方法。

(4) 下面到PackageInstallerActivity—>startInstall方法:

private void startInstall() {// Start subactivity to actually install the applicationIntent newIntent = new Intent();newIntent.putExtra(PackageUtil.INTENT_ATTR_APPLICATION_INFO,mPkgInfo.applicationInfo);newIntent.setData(mPackageURI);newIntent.setClass(this, InstallAppProgress.class);//跳转的ActivitynewIntent.putExtra(InstallAppProgress.EXTRA_MANIFEST_DIGEST, mPkgDigest);newIntent.putExtra(InstallAppProgress.EXTRA_INSTALL_FLOW_ANALYTICS, mInstallFlowAnalytics);String installerPackageName = getIntent().getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME);......省略.....if(localLOGV) Log.i(TAG, "downloaded app uri="+mPackageURI);startActivity(newIntent);//注释1finish();
}

startInstall方法启动后,将会跳转到注释1处的InstallAppProgress界面,并关掉当前的PackageInstallerActivity。

(5) 接着深入到InstallAppProgress.java文件中:

当InstallAppProgress.java启动时,会先调用InstallAppProgress.java—>onCreate方法:

@Override
public void onCreate(Bundle icicle) {......省略.....initView();
}
public void initView() {......省略.....if ("package".equals(mPackageURI.getScheme())) {try {pm.installExistingPackage(mAppInfo.packageName);observer.packageInstalled(mAppInfo.packageName,PackageManager.INSTALL_SUCCEEDED);} catch (PackageManager.NameNotFoundException e) {observer.packageInstalled(mAppInfo.packageName,PackageManager.INSTALL_FAILED_INVALID_APK);}} else {pm.installPackageWithVerificationAndEncryption(mPackageURI, observer, installFlags,installerPackageName, verificationParams, null);}
}

可以看到有两条安装逻辑,我这里只探索else中的逻辑。else中实际上是调用了ApplicationPackageManager的installPackageWithVerificationAndEncryption方法来安装。

(6) 再到ApplicationPackageManager—>installPackageWithVerificationAndEncryption方法:

@Override
public void installPackageWithVerificationAndEncryption(Uri packageURI,PackageInstallObserver observer, int flags, String installerPackageName,VerificationParams verificationParams, ContainerEncryptionParams encryptionParams) {installCommon(packageURI, observer, flags, installerPackageName, verificationParams,encryptionParams);//注释1
}

注意installPackageWithVerificationAndEncryption方法上有个Override,说明继承于父类的PackageManager中。

从注释1中可以看到调用的是installCommon方法:

private void installCommon(Uri packageURI,PackageInstallObserver observer, int flags, String installerPackageName,VerificationParams verificationParams, ContainerEncryptionParams encryptionParams) {......省略.....try {mPM.installPackage(originPath, observer.getBinder(), flags, installerPackageName,verificationParams, null);//注释1} catch (RemoteException ignored) {}
}

从注释1中可以看到调用的是PMS中的installPackage方法,到这里后续的逻辑就与前面的“网络下载应用安装”中的逻辑一样了。

结论:看到这里咱们就可以对apk的安装过程有清晰的感知了,在四种安装方式中每种安装方式的入口部分是特殊的流程,但是后面的流程都是一致的。

并且咱们对之前的疑惑也算是有了答案:apk的信息就是在安装的过程中通过对apk解析得到其信息并保存在对应的文件中的。

总结APK的安装流程如下:

复制APK安装包到/data/app目录下,解压缩并扫描安装包,向资源管理器注入APK资源,解析AndroidManifest文件,并在/data/data目录下创建对应的应用数据目录,然后针对Dalvik/ART环境优化dex文件,保存到dalvik-cache目录,将AndroidManifest文件解析出的组件、权限注册到PackageManagerService并发送广播。

see you

Android中APK安装流程解析相关推荐

  1. 从源码角度解析Android中APK安装过程

    从源码角度解析Android中APK的安装过程 1. Android中APK简介 Android应用Apk的安装有如下四种方式: 1.1 系统应用安装 没有安装界面,在开机时自动完成 1.2 网络下载 ...

  2. 【转】Android中APK安装过程及原理解析

    应用安装是智能机的主要特点,即用户可以把各种应用(如游戏等)安装到手机上,并可以对其进行卸载等管理操作.APK是Android Package的缩写,即Android安装包.APK是类似Symbian ...

  3. Android中APK安装过程及原理解析

    来自华为内部资料 应用安装是智能机的主要特点,即用户可以把各种应用(如游戏等)安装到手机上,并可以对其进行卸载等管理操作.APK是Android Package的缩写,即android安装包.APK是 ...

  4. Android中APK打包流程

    aapt: android application package tool,SDK (Android打包流程图) #Android打包流程 1.通过aapt工具将 资源文件(res).清单文件(An ...

  5. 【Android】提取Android中已安装app的apk

    提取Android中已安装的apk,这个需求很多时候都会遇到.比如从google play上安装过apk后,如何提取出来给别人用? 本文1. 先介绍转载的apk提取方法并 2. 验证提取出来的apk和 ...

  6. Android-Multidex安装流程解析

    Android-Multidex安装流程解析 关于为什么需要引入Multidex支持以及如何配置Multidex可参考官网,本篇不做阐述,本篇着重分析Multidex1.0.2源码进行分析 大家都知道 ...

  7. Android中apk加固完善篇之内存加载dex方案实现原理(不落地方式加载)

    一.前言 时隔半年,困扰的问题始终是需要解决的,之前也算是没时间弄,今天因为有人在此提起这个问题,那么就不能不解决了,这里写一篇文章记录一下吧.那么是什么问题呢? 就是关于之前的一个话题:Androi ...

  8. Android 系统 (79)---Android应用程序安装过程解析

    Android应用程序安装过程解析 Android应用程序安装过程解析 1.程序安装的4大步骤 (1) 拷贝apk文件到指定目录 在Android系统中,apk安装文件是会被保存起来的,默认情况下,用 ...

  9. Android应用程序安装过程解析(源码解析)

    Android应用程序安装过程解析 1.程序安装的4大步骤 (1) 拷贝apk文件到指定目录 在Android系统中,apk安装文件是会被保存起来的,默认情况下,用户安装的apk首先会被拷贝到 /da ...

最新文章

  1. html中引入ifrim视频,Making Of Rallypoint - Outer Rim
  2. java 搜索机制_Java爬虫搜索原理实现
  3. 炼油机出来的什么油_办一个废轮胎炼油设备厂有哪些注意事项
  4. poj3069 Saruman's Army(贪心)
  5. 解决Linux下DNS配置重启失效问题
  6. Java中获取资源文件路径
  7. Flex(try-catch-finally)机制
  8. java intent 传递集合对象_Android系列之Intent传递对象的几种实例方法
  9. android 巧用动画使您app风骚起来
  10. 国家气象数据中心网站
  11. Unity学习日志_全局光照GI系统简介
  12. Android aar包的so和jniLibs中的so库冲突
  13. 数学基础(1)~ 概率论基础知识
  14. 116. 飞行员兄弟 Java题解
  15. 数学建模c语言必背知识,【计算机网络论文】数学建模计算机知识的应用(共4296字)...
  16. Matlab:向饼图添加图例
  17. returnT java
  18. 消费者行为分析模型,用AISAS模型拆解营销方式
  19. Windows+IIS+PHP——PHP安装与环境配置
  20. VM虚拟机去虚拟化教程(二)如何实现多开虚拟机 每个窗口独立硬件信息

热门文章

  1. 使用静态分析,帮助实现GDPR的“设计安全”和“设计隐私”
  2. HDLBits—Lemmings3
  3. Mean-Shift算法
  4. win10计算机拒绝访问,Win10文件访问被拒绝如何解决?
  5. vue-router 是什么?它有哪些组件
  6. 【转载】 恢复百度云同步盘本地误删的文件(2篇)
  7. 2022年北京大数据技能大赛“隐私计算”赛道初赛完结!12强出炉
  8. 美国造出最大电视 4K电视262英寸超大屏
  9. 全国程序员高考卷,开始答题!
  10. ROS Qt环境的搭建及基础知识介绍