How does Android install APK? What does it do and what information does it save when it is installed? What is the use of stored information?
In this article, let’s discuss the apK installation process of Android system with the above questions.
Let’s start by tracing back to the calling code of the APK installation:
Intent intent = new Intent(Intent.ACTION_VIEW); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); Uri apkUri = FileProvider. GetUriForFile (context, "your package name. FileProvider," file). intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); intent.setDataAndType(apkUri, "application/vnd.android.package-archive"); startActivity(intent)Copy the code
The code content is very simple, Start a Action for Intent. ACTION_VIEW, Flag for Intent. FLAG_GRANT_READ_URI_PERMISSION, Type of application/VND. Android. The package – Ac archive Tivity.
Where is this Activity?
Android before 10, you can be in/packages/apps/PackageInstaller AndroidManifest. Find it in the XML:
<activity android:name=".InstallStart" android:exported="true" android:excludeFromRecents="true"> <intent-filter android:priority="1"> <action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.INSTALL_PACKAGE" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="file" /> <data android:scheme="content" /> <data android:mimeType="application/vnd.android.package-archive" /> </intent-filter> <intent-filter android:priority="1"> <action android:name="android.intent.action.INSTALL_PACKAGE" /> <category android:name="android.intent.category.DEFAULT" /> <data android:scheme="file" /> <data android:scheme="package" /> <data android:scheme="content" /> </intent-filter> <intent-filter android:priority="1"> <action android:name="android.content.pm.action.CONFIRM_PERMISSIONS" /> <category android:name="android.intent.category.DEFAULT" /> </intent-filter> </activity>Copy the code
However, Google has removed the PackageInstaller app from the Android 10 source code, This means that there is a certain risk of using the previous installation method (PS: Domestic mobile systems in Android 10 added their own PackageInstaller app). Android 10 recommends using a class called PackageInstaller to install and uninstall APK. You can be in/samples/ApiDemos/SRC/com/example/android/apis/content/InstallApkSessionApi found in Java example by use of the Api:
Code 1
PackageInstaller packageInstaller = getPackageManager().getPackageInstaller();
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
int sessionId = packageInstaller.createSession(params);
session = packageInstaller.openSession(sessionId);
addApkToInstallSession("HelloActivity.apk", session);
// Create an install status receiver.
Context context = InstallApkSessionApi.this;
Intent intent = new Intent(context, InstallApkSessionApi.class);
intent.setAction(PACKAGE_INSTALLED_ACTION);
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent, 0);
IntentSender statusReceiver = pendingIntent.getIntentSender();
// Commit the session (this will start the installation workflow).
session.commit(statusReceiver);
Copy the code
This article, we discuss the use of PackageInstaller class to install Apk, using the PackageInstaller APP to install Apk way to leave you to explore the reader.
PackageInstaller
PackageInstaller (PackageInstaller) :
Offers the ability to install, upgrade, and remove applications on the device. This includes support for apps packaged either as a single "monolithic" APK, or apps packaged as multiple "split" APKs. An app is delivered for installation through a `PackageInstaller.Session`, which any app can create. Once the session is created, the installer can stream one or more APKs into place until it decides to either commit or destroy the session. Committing may require user intervention to complete the installation, unless the caller falls into one of the following categories, in which case the installation will complete automatically. - the device owner - the affiliated profile owner Sessions can install brand new apps, upgrade existing apps, or add new splits into an existing app. Apps packaged as multiple split APKs always consist of a single "base" APK (with a `null` split name) and zero or more "split" APKs (with unique split names). Any subset of these APKs can be installed together, as long as the following constraints are met: - All APKs must have the exact same package name, version code, and signing certificates. - All APKs must have unique split names. - All installations must contain a single base APK.Copy the code
I will do the translation work, because the last paragraph is about the installation of disassembly and subcontracting, this article does not discuss the installation of disassembly and subcontracting, so it directly skipped:
- Provides the ability to install, upgrade, and delete applications on the device, either as a package or as a subpackage.
- The application passes
PackageInstaller.Session
Any application can create this Session. After a Session is created, the Installer can stream one or more APKs to the appropriate location until it decides to commit or destroy the Session. Commit may require user intervention to complete the installation, unless the caller belongs to the device owner or file owner, in which case the installation will complete automatically. - Sessions can install entirely new applications, upgrade existing applications, or add new splashes to existing applications.
Next, we’ll explore it in conjunction with the PackageInstallApi call Code from Code 1 above.
PackageInstaller.SessionParams
PackageInstaller.SessionParams params = new PackageInstaller.SessionParams(
PackageInstaller.SessionParams.MODE_FULL_INSTALL);
Copy the code
SessionParams has the following two parameters of interest:
mode
Type | Description |
---|---|
MODE_FULL_INSTALL | The installed Apk completely replaces the existing Apk of the target program |
MODE_INHERIT_EXISTING | Inherit any existing APKS of the target application, unless they have been explicitly overridden by the session (based on split names) |
installLocation
Type | Description |
---|---|
INSTALL_LOCATION_AUTO | Let the system determine the ideal installation location. |
INSTALL_LOCATION_INTERNAL_ONLY | The default value is explicitly required to be installed only on the phone’s internal storage. |
INSTALL_LOCATION_PREFER_EXTERNAL | Prefer to install on an SD card. There is no guarantee that the system will comply with this requirement. If external storage is unavailable or too full, applications may be installed on internal storage. |
Let’s go back to PackageInstaller and read on.
packageInstaller.createSession
public int createSession(@NonNull SessionParams params) throws IOException { try { final String installerPackage; If (params. InstallerPackageName = = null) {/ / mInstallerPackageName here refers to install the apk application package name, InstallerPackage = mInstallerPackageName; } else { installerPackage = params.installerPackageName; Minstaller.createsession (Params, installerPackage, mUserId);} // Call the createSession method in PackageInstallerService. } catch (RuntimeException e) { ExceptionUtils.maybeUnwrapIOException(e); throw e; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); }}Copy the code
. As you can see, packageInstaller createSession method finally will transfer actual call to the service side PackageInstallerService to perform.
PackageInstallerService.createSession
@Override public int createSession(SessionParams params, String installerPackageName, int userId) { try { return createSessionInternal(params, installerPackageName, userId); } catch (IOException e) { throw ExceptionUtils.wrap(e); }}Copy the code
createSessionInternal
private int createSessionInternal(SessionParams params, String installerPackageName, int userId)
throws IOException {
final int callingUid = Binder.getCallingUid();
mPermissionManager.enforceCrossUserPermission(
callingUid, userId, true, true, "createSession");
//检查此用户是否被禁止安装程序
if (mPm.isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) {
throw new SecurityException("User restriction prevents installing");
}
//如果安装来自adb 或者 root用户,param里增加 INSTALL_FROM_ADB 的FLAG
if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) {
params.installFlags |= PackageManager.INSTALL_FROM_ADB;
} else {
// 只有具有INSTALL_PACKAGES权限的APP才允许以非调用者的身份设置为安装器
if (mContext.checkCallingOrSelfPermission(Manifest.permission.INSTALL_PACKAGES) !=
PackageManager.PERMISSION_GRANTED) {
mAppOps.checkPackage(callingUid, installerPackageName);
}
//移除以下三个FLAG
params.installFlags &= ~PackageManager.INSTALL_FROM_ADB;
params.installFlags &= ~PackageManager.INSTALL_ALL_USERS;
params.installFlags &= ~PackageManager.INSTALL_ALLOW_TEST;
//如果APP已存在,则替换它
params.installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
if ((params.installFlags & PackageManager.INSTALL_VIRTUAL_PRELOAD) != 0
&& !mPm.isCallerVerifier(callingUid)) {
params.installFlags &= ~PackageManager.INSTALL_VIRTUAL_PRELOAD;
}
}
if (Build.IS_DEBUGGABLE || isDowngradeAllowedForCaller(callingUid)) {
//允许降级
params.installFlags |= PackageManager.INSTALL_ALLOW_DOWNGRADE;
} else {
//不允许降级,不允许向低版本升级
params.installFlags &= ~PackageManager.INSTALL_ALLOW_DOWNGRADE;
params.installFlags &= ~PackageManager.INSTALL_REQUEST_DOWNGRADE;
}
...
if (!params.isMultiPackage) {
//安装时,只有系统组件可以绕过运行时权限。
if ((params.installFlags & PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS) != 0
&& mContext.checkCallingOrSelfPermission(Manifest.permission
.INSTALL_GRANT_RUNTIME_PERMISSIONS) == PackageManager.PERMISSION_DENIED) {
throw new SecurityException("You need the "
+ "android.permission.INSTALL_GRANT_RUNTIME_PERMISSIONS permission "
+ "to use the PackageManager.INSTALL_GRANT_RUNTIME_PERMISSIONS flag");
}
//如果AppIcon过大,则对它进行调整
if (params.appIcon != null) {
final ActivityManager am = (ActivityManager) mContext.getSystemService(
Context.ACTIVITY_SERVICE);
final int iconSize = am.getLauncherLargeIconSize();
if ((params.appIcon.getWidth() > iconSize * 2)
|| (params.appIcon.getHeight() > iconSize * 2)) {
params.appIcon = Bitmap.createScaledBitmap(params.appIcon, iconSize, iconSize,
true);
}
}
switch (params.mode) {
case SessionParams.MODE_FULL_INSTALL:
case SessionParams.MODE_INHERIT_EXISTING:
break;
default:
throw new IllegalArgumentException("Invalid install mode: " + params.mode);
}
// If caller requested explicit location, sanity check it, otherwise
// resolve the best internal or adopted location.
if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
if (!PackageHelper.fitsOnInternal(mContext, params)) {
throw new IOException("No suitable internal storage available");
}
} else if ((params.installFlags & PackageManager.INSTALL_FORCE_VOLUME_UUID) != 0) {
// For now, installs to adopted media are treated as internal from
// an install flag point-of-view.
params.installFlags |= PackageManager.INSTALL_INTERNAL;
} else {
params.installFlags |= PackageManager.INSTALL_INTERNAL;
// Resolve best location for install, based on combination of
// requested install flags, delta size, and manifest settings.
final long ident = Binder.clearCallingIdentity();
try {
//根据给定的installLocation计算安装需要的大小,选择要安装应用程
//序的实际卷。只考虑内部卷和专用卷,并且更偏向将现有包保留在其当前卷上。
//volumeUuid 指的是 安装所在的卷的fsUuid,如果安装在内部存储上,则返回null
params.volumeUuid = PackageHelper.resolveInstallVolume(mContext, params);
} finally {
Binder.restoreCallingIdentity(ident);
}
}
}
final int sessionId;
final PackageInstallerSession session;
synchronized (mSessions) {
// 检查安装程序是否正常运行,打开的Installer Session不能超过1024个
final int activeCount = getSessionCount(mSessions, callingUid);
if (activeCount >= MAX_ACTIVE_SESSIONS) {
throw new IllegalStateException(
"Too many active sessions for UID " + callingUid);
}
//历史Session不能超过1048576个
final int historicalCount = mHistoricalSessionsByInstaller.get(callingUid);
if (historicalCount >= MAX_HISTORICAL_SESSIONS) {
throw new IllegalStateException(
"Too many historical sessions for UID " + callingUid);
}
//随机生成一个不超过Integer.MAX_VALUE的sessionId,并保存在mAllocatedSessions中
sessionId = allocateSessionIdLocked();
}
final long createdMillis = System.currentTimeMillis();
// We're staging to exactly one location
File stageDir = null;
String stageCid = null;
if (!params.isMultiPackage) {
if ((params.installFlags & PackageManager.INSTALL_INTERNAL) != 0) {
//复制路径为/mnt/expand/${volumeUuid}/app-staging
stageDir = buildSessionDir(sessionId, params);
} else {
stageCid = buildExternalStageCid(sessionId);
}
}
//存储了安装相关的所有信息
session = new PackageInstallerSession(mInternalCallback, mContext, mPm, this,
mInstallThread.getLooper(), mStagingManager, sessionId, userId,
installerPackageName, callingUid, params, createdMillis, stageDir, stageCid, false,
false, false, null, SessionInfo.INVALID_ID, false, false, false,
SessionInfo.STAGED_SESSION_NO_ERROR, "");
//将sessionId和session绑定起来
synchronized (mSessions) {
mSessions.put(sessionId, session);
}
if (params.isStaged) {
//处理分阶段安装会话,即仅在重新启动后才需要安装的会话。
mStagingManager.createSession(session);
}
if ((session.params.installFlags & PackageManager.INSTALL_DRY_RUN) == 0) {
//回调通知Session已创建成功
mCallbacks.notifySessionCreated(session.sessionId, session.userId);
}
//异步将session信息写入install_sessions.xml文件中
writeSessionsAsync();
return sessionId;
}
Copy the code
CreateSessionInternal initializes the apK installation information and environment, creates a sessionId, and binds the installation Session to the sessionId.
packageInstaller.openSession
public @NonNull Session openSession(int sessionId) throws IOException { try { try { return new Session(mInstaller.openSession(sessionId)); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); } } catch (RuntimeException e) { ExceptionUtils.maybeUnwrapIOException(e); throw e; }}Copy the code
We continue to trace the openSession method in PackageInstallerService:
PackageInstallerService.openSession
public IPackageInstallerSession openSession(int sessionId) { try { return openSessionInternal(sessionId); } catch (IOException e) { throw ExceptionUtils.wrap(e); }}Copy the code
openSessionInternal
private IPackageInstallerSession openSessionInternal(int sessionId) throws IOException { synchronized (mSessions) { final PackageInstallerSession session = mSessions.get(sessionId); if (session == null || ! isCallingUidOwner(session)) { throw new SecurityException("Caller has no access to session " + sessionId); } // If StageDir is not empty, create this folder // StageDir is the temporary location for writing client data session.open(); return session; }}Copy the code
As you can see, the openSessionInternal method simply looks up the PackageInstallerSession object created by createSession in the previous phase based on the sessionId and returns it.
The installation code next calls addApkToInstallSession(“HelloActivity.apk”, session); , the detailed implementation of this method is as follows:
private void addApkToInstallSession(String assetName, PackageInstaller.Session session) throws IOException { // It's recommended to pass the file size to openWrite(). Otherwise installation may fail // if the disk is almost full. try (OutputStream packageInSession = session.openWrite("package", 0, -1); InputStream is = getAssets().open(assetName)) { byte[] buffer = new byte[16384]; int n; while ((n = is.read(buffer)) >= 0) { packageInSession.write(buffer, 0, n); }}}Copy the code
session.openWrite
The main function of the openWrite method is to open a stream and write the APK file to the Session. The returned stream will begin writing data at the requested offset in the file that can be used to restore partially written files. If a valid file length is specified, the system preallocates the underlying disk space to optimize the location on the disk. It is highly recommended to provide valid file lengths when known.
You can write data to the returned stream, or you can call fsync(OutputStream) as needed to make sure the bytes are persisted to disk and then close when finished. All streams must be closed before calling COMMIT (IntentSender).
The source code for openWrite is as follows:
public @NonNull OutputStream openWrite(@NonNull String name, long offsetBytes, long lengthBytes) throws IOException { try { if (ENABLE_REVOCABLE_FD) { return new ParcelFileDescriptor.AutoCloseOutputStream( mSession.openWrite(name, offsetBytes, lengthBytes)); } else {//ENABLE_REVOCABLE_FD Default false Final ParcelFileDescriptor clientSocket = msession. openWrite(name, offsetBytes, lengthBytes); return new FileBridge.FileBridgeOutputStream(clientSocket); } } catch (RuntimeException e) { ExceptionUtils.maybeUnwrapIOException(e); throw e; } catch (RemoteException e) { throw e.rethrowFromSystemServer(); }}Copy the code
In this method we will focus on the sentence msession. openWrite(name, offsetBytes, lengthBytes), which means that the openWrite method in the PackageInstallerSession class will continue to process.
PackageInstallerSession.openWrite
@Override public ParcelFileDescriptor openWrite(String name, long offsetBytes, long lengthBytes) { try { return doWriteInternal(name, offsetBytes, lengthBytes, null); } catch (IOException e) { throw ExceptionUtils.wrap(e); }}Copy the code
Moving on to the doWriteInternal method:
doWriteInternal
private ParcelFileDescriptor doWriteInternal(String name, long offsetBytes, long lengthBytes, Parameter Description ParcelFileDescriptor incomingFd) throws IOException {// Quickly check whether the status is normal and assign yourself a Pipe. // Then we do a lot of disk allocation outside of the lock, but this open Pipe will prevent any attempts to install convert final RevocableFileDescriptor fd; final FileBridge bridge; final File stageDir; synchronized (mLock) { if (PackageInstaller.ENABLE_REVOCABLE_FD) { fd = new RevocableFileDescriptor(); bridge = null; mFds.add(fd); } else {fd = null; bridge = new FileBridge(); mBridges.add(bridge); } // Parse the actual location where the staging data should be written. stageDir = resolveStageDirLocked(); } try {// First use the name provided by the installer; We'll rename if (! FileUtils.isValidExtFilename(name)) { //Check if given filename is valid for an ext4 filesystem. throw new IllegalArgumentException("Invalid name: " + name); } final File target; final long identity = Binder.clearCallingIdentity(); try { target = new File(stageDir, name); } finally { Binder.restoreCallingIdentity(identity); } // TODO: This should delegate to DCS so the system process avoids // holding open FDs into containers. // Open the file and set permissions to 644 final FileDescriptor targetFd = Os.open(target.getAbsolutePath(), O_CREAT | O_WRONLY, 0644); Os.chmod(target.getAbsolutePath(), 0644); If (stageDir! = null && lengthBytes > 0) { mContext.getSystemService(StorageManager.class).allocateBytes(targetFd, lengthBytes, PackageHelper.translateAllocateFlags(params.installFlags)); } if (offsetBytes > 0) { Os.lseek(targetFd, offsetBytes, OsConstants.SEEK_SET); } if (incomingFd ! // If session.write is called, incomingFd is not null, Switch (binder.getCallinguid ()) {case Android.os.process. SHELL_UID: case Android.os.process. ROOT_UID: case android.os.Process.SYSTEM_UID: break; default: throw new SecurityException( "Reverse mode only supported from shell or system"); } // In "reverse" mode, we're streaming data ourselves from the // incoming FD, which means we never have to hand out our // sensitive internal FD. We still rely on a "bridge" being // inserted above to hold the session active. try { final Int64Ref last = new Int64Ref(0); / / writing data to the temporary files FileUtils) copy (incomingFd. GetFileDescriptor (), targetFd, lengthBytes, null, Runnable: : run, (long progress) -> { if (params.sizeBytes > 0) { final long delta = progress - last.value; last.value = progress; addClientProgress((float) delta / (float) params.sizeBytes); }}); } finally { IoUtils.closeQuietly(targetFd); IoUtils.closeQuietly(incomingFd); File bridge synchronized (mLock) {if (PackageInstaller.ENABLE_REVOCABLE_FD) {mfds.remove (fd); } else { bridge.forceClose(); mBridges.remove(bridge); } } } return null; } bridge.setTargetFile(targetFd); bridge.start(); return new ParcelFileDescriptor(bridge.getClientSocket()); } catch (ErrnoException e) { throw e.rethrowAsIOException(); }}Copy the code
The doWriteInternal method initializes the disk storage space (if size is specified) of the APK files to be installed and creates a FileBridge object that can write files across processes. File output streams are provided to the application layer to write installed APK data through the ParcelFileDescriptor class wrapper.
Note that the subsequent session.write method also calls doWriteInternal, except that incomingFd is not null. This means that doWriteInternal writes data to the file corresponding to the incomingFd file descriptor, which is also the file we created when we called the openWrite method.
At this point, we are done copying the Apk we want to install.
session.commit
Now it’s our last step to commit the Installer Session and actually perform the Apk installation. Let’s look at the commit method:
public void commit(@NonNull IntentSender statusReceiver) { try { mSession.commit(statusReceiver, false); } catch (RemoteException e) { throw e.rethrowFromSystemServer(); }}Copy the code
Again, it delegates this method to the PackageInstallerSession class. Note that the commit method has an IntentSender parameter, which is used to inform users of changes in the Session state during installation, such as installation confirmation, installation success, installation failure, etc.
PackageInstallerSession.commit
public void commit(@NonNull IntentSender statusReceiver, boolean forTransfer) {
if (hasParentSessionId()) {
throw new IllegalStateException(
"Session " + sessionId + " is a child of multi-package session "
+ mParentSessionId + " and may not be committed directly.");
}
if (!markAsCommitted(statusReceiver, forTransfer)) {
return;
}
...
mHandler.obtainMessage(MSG_COMMIT).sendToTarget();
}
Copy the code
The COMMIT method ends up sending an MSG_COMMIT message to the mHandler, where the handlerCommit method is actually executed.
handlerCommit
private void handleCommit() { ... // Return the List of subsessions, or null List<PackageInstallerSession> childSessions = getChildSessions(); try { synchronized (mLock) { commitNonStagedLocked(childSessions); } } catch (PackageManagerException e) { final String completeMsg = ExceptionUtils.getCompleteMessage(e); Slog.e(TAG, "Commit of session " + sessionId + " failed: " + completeMsg); destroyInternal(); dispatchSessionFinished(e.error, completeMsg, null); }}Copy the code
commitNonStagedLocked
private void commitNonStagedLocked(List<PackageInstallerSession> childSessions) throws PackageManagerException { / / return a ActiveInstallSession object final PackageManagerService. ActiveInstallSession committingSession = makeSessionActiveLocked(); if (committingSession == null) { return; } if (isMultiPackage()) { List<PackageManagerService.ActiveInstallSession> activeChildSessions = new ArrayList<>(childSessions.size()); boolean success = true; PackageManagerException failure = null; for (int i = 0; i < childSessions.size(); ++i) { final PackageInstallerSession session = childSessions.get(i); try { final PackageManagerService.ActiveInstallSession activeSession = session.makeSessionActiveLocked(); if (activeSession ! = null) { activeChildSessions.add(activeSession); } } catch (PackageManagerException e) { failure = e; success = false; } } if (! success) { try { mRemoteObserver.onPackageInstalled( null, failure.error, failure.getLocalizedMessage(), null); } catch (RemoteException ignored) { } return; } mPm.installStage(activeChildSessions); } else {// We usually perform the installation through this branch mpm. installStage(committingSession); }}Copy the code
At the end of the commitNonStagedLocked methods, the PMS installStage method is called, so that the code logic goes into the PMS and the PMS parses the contents of the APK file to be installed to complete the actual installation.
In fact, PackageInstaller does all the pre-installation work, maintains an installation session, manages installation parameters, and provides the ability to temporarily copy installation packages to specific paths, but the actual installation depends on the PMS.
In the next section, we’ll look at how PMS parses APK and what it does during APK installation.