本篇博客分析PackageInstaller源码目的是分析Android权限机制,Android App的权限在应用被安装时,用户选择授予或者拒绝。所以,分析Android权限机制源码的第一步分析应用程序安装时的行为。
  此次阅读源码旨在解决的问题:Android权限是一次性授予的,即用户在同意安装后,App就获得了申请的权限。那这个过程是怎样的,即:用户点击同意——>App获得权限,经理了怎样的调用过程。

一、源码分析追踪过程

  Android源码中,负责应用程序安装的是PackageInstaller.apk。其源码路径/packages/apps/PackageInstaller。从Manifest文件可以看到,PackageInstallersActivity是其入口Activity,也是主要负责安装apk的Activity。所以首先分析PackageInstallersActivity。
  这次分析过程是基于源码调试的,便于追踪代码执行流程。首先利用一个Intent进行测试,am start -n com.android.packageinstaller/com.android.packageinstaller.PackageInstallerActivity -d file:///data/local/tmp/qqmusic.apk,是从手机中的文件来安装apk。

<activity android:name=".PackageInstallerActivity" android:configChanges="orientation|keyboardHidden|screenSize" android:excludeFromRecents="true"><intent-filter><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:mimeType="application/vnd.android.package-archive"/></intent-filter><intent-filter><action android:name="android.intent.action.INSTALL_PACKAGE"/><category android:name="android.intent.category.DEFAULT"/><data android:scheme="file"/><data android:scheme="package"/></intent-filter><intent-filter><action android:name="android.content.pm.action.CONFIRM_PERMISSIONS"/><category android:name="android.intent.category.DEFAULT"/></intent-filter>
</activity>

  以下就是源码分析过程。
  进入onCreate()函数后,首先从Intent获取相关数据,然后判断用户是否允许安装非官方应用。

protected void onCreate(Bundle icicle) {super.onCreate(icicle);``````final boolean unknownSourcesAllowedByAdmin = isUnknownSourcesAllowedByAdmin();final boolean unknownSourcesAllowedByUser = isUnknownSourcesEnabled();```````}

  接着的一段代码也是关于从第三方应用市场(含本地)安装的一些安全设定工作。

protected void onCreate(Bundle icicle) {super.onCreate(icicle);``````final boolean unknownSourcesAllowedByAdmin = isUnknownSourcesAllowedByAdmin();final boolean unknownSourcesAllowedByUser = isUnknownSourcesEnabled();boolean requestFromUnknownSource = isInstallRequestFromUnknownSource(intent);mInstallFlowAnalytics = new InstallFlowAnalytics();mInstallFlowAnalytics.setContext(this);mInstallFlowAnalytics.setStartTimestampMillis(SystemClock.elapsedRealtime());mInstallFlowAnalytics.setInstallsFromUnknownSourcesPermitted(unknownSourcesAllowedByAdmin&& unknownSourcesAllowedByUser);mInstallFlowAnalytics.setInstallRequestFromUnknownSource(requestFromUnknownSource);mInstallFlowAnalytics.setVerifyAppsEnabled(isVerifyAppsEnabled());mInstallFlowAnalytics.setAppVerifierInstalled(isAppVerifierInstalled());mInstallFlowAnalytics.setPackageUri(mPackageURI.toString());```````}

  接着,从data中的取出scheme,并根据是都是file,package,和没有data进入不同的分支,测试程序是利用file安装程序,所以进入的是else分支。

protected void onCreate(Bundle icicle) {super.onCreate(icicle);`````````if (scheme != null && !"file".equals(scheme) && !"package".equals(scheme)) {Log.w(TAG, "Unsupported scheme " + scheme);setPmResult(PackageManager.INSTALL_FAILED_INVALID_URI);mInstallFlowAnalytics.setFlowFinished(InstallFlowAnalytics.RESULT_FAILED_UNSUPPORTED_SCHEME);finish();return;}final PackageUtil.AppSnippet as;if ("package".equals(mPackageURI.getScheme())) {mInstallFlowAnalytics.setFileUri(false);try {mPkgInfo = mPm.getPackageInfo(mPackageURI.getSchemeSpecificPart(),PackageManager.GET_PERMISSIONS | PackageManager.GET_UNINSTALLED_PACKAGES);} catch (NameNotFoundException e) {}if (mPkgInfo == null) {Log.w(TAG, "Requested package " + mPackageURI.getScheme()+ " not available. Discontinuing installation");showDialogInner(DLG_PACKAGE_ERROR);setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);mInstallFlowAnalytics.setPackageInfoObtained();mInstallFlowAnalytics.setFlowFinished(InstallFlowAnalytics.RESULT_FAILED_PACKAGE_MISSING);return;}as = new PackageUtil.AppSnippet(mPm.getApplicationLabel(mPkgInfo.applicationInfo),mPm.getApplicationIcon(mPkgInfo.applicationInfo));} else {mInstallFlowAnalytics.setFileUri(true);final File sourceFile = new File(mPackageURI.getPath());PackageParser.Package parsed = PackageUtil.getPackageInfo(sourceFile);// Check for parse errorsif (parsed == null) {Log.w(TAG, "Parse error when parsing manifest. Discontinuing installation");showDialogInner(DLG_PACKAGE_ERROR);setPmResult(PackageManager.INSTALL_FAILED_INVALID_APK);mInstallFlowAnalytics.setPackageInfoObtained();mInstallFlowAnalytics.setFlowFinished(InstallFlowAnalytics.RESULT_FAILED_TO_GET_PACKAGE_INFO);return;}mPkgInfo = PackageParser.generatePackageInfo(parsed, null,PackageManager.GET_PERMISSIONS, 0, 0, null,new PackageUserState());mPkgDigest = parsed.manifestDigest;as = PackageUtil.getAppSnippet(this, mPkgInfo.applicationInfo, sourceFile);}mInstallFlowAnalytics.setPackageInfoObtained();```````
}

  在else分支里面,判断能否正确解析apk文件,变量parsed是Package实例,包含了解析apk的结果,解析过程的关键API是(这个解析过程的API值得具体分析):

 PackageParser.Package parsed = PackageUtil.getPackageInfo(sourceFile);

  Package包含了解析apk文件的结果,具体可以查看Package类文件,(在单步调试这一步时候需要等待很长时间)。这个API基本对apk文件的所有重要信息都做了分析,其实应该就是分析manifest文件,包括apk中的Activity,service等四大组件,以及自定义权限和申请的权限。

===============
  else分支后,设置程序apk以获取,这个API不重要。接着就是设置Activity界面,注意下面的最后一行代码:

protected void onCreate(Bundle icicle) {````````setContentView(R.layout.install_start);mInstallConfirm = findViewById(R.id.install_confirm_panel);mInstallConfirm.setVisibility(View.INVISIBLE);PackageUtil.initSnippetForNewApp(this, as, R.id.app_snippet);mOriginatingUid = getOriginatingUid(intent);```````final boolean isManagedProfile = mUserManager.isManagedProfile();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();}
}

  最后进入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();}

  这个函数内部就是对应用的包名做一些处理,以及判断是否是升级,是否是系统应用等,这些和权限机制基本没有关系。判断完毕之后,进入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);}}});boolean permVisible = false;mScrollView = null;mOkCanInstall = false;int msg = 0;if (mPkgInfo != null) {AppSecurityPermissions perms = new AppSecurityPermissions(this, mPkgInfo);final int NP = perms.getPermissionCount(AppSecurityPermissions.WHICH_PERSONAL);final int ND = perms.getPermissionCount(AppSecurityPermissions.WHICH_DEVICE);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 =(perms.getPermissionCount(AppSecurityPermissions.WHICH_NEW) > 0);mInstallFlowAnalytics.setNewPermissionsFound(newPermissionsFound);if (newPermissionsFound) {permVisible = true;mScrollView.addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_NEW));} else {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 (NP > 0 || ND > 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);}if (NP > 0) {((ViewGroup)root.findViewById(R.id.privacylist)).addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_PERSONAL));} else {root.findViewById(R.id.privacylist).setVisibility(View.GONE);}if (ND > 0) {((ViewGroup)root.findViewById(R.id.devicelist)).addView(perms.getPermissionsView(AppSecurityPermissions.WHICH_DEVICE));} else {root.findViewById(R.id.devicelist).setVisibility(View.GONE);}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);mCancel = (Button)findViewById(R.id.cancel_button);mOk.setOnClickListener(this);mCancel.setOnClickListener(this);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;}});}}

  从文件的134行起和权限有,进入AppSecurityPermissions(Context context, PackageInfo info)构造函数内部查看具体逻辑,AppSecurityPermissions(Context context, PackageInfo info)构造函数所在位置:/frameworks/base/core/java/android/widget/AppSecurityPermissions.java;

public AppSecurityPermissions(Context context, PackageInfo info) {this(context);Set<MyPermissionInfo> permSet = new HashSet<MyPermissionInfo>();if(info == null) {return;}mPackageName = info.packageName;// Convert to a PackageInfoPackageInfo installedPkgInfo = null;// Get requested permissionsif (info.requestedPermissions != null) {try {installedPkgInfo = mPm.getPackageInfo(info.packageName,PackageManager.GET_PERMISSIONS);} catch (NameNotFoundException e) {}extractPerms(info, permSet, installedPkgInfo);}// Get permissions related to  shared user if anyif (info.sharedUserId != null) {int sharedUid;try {sharedUid = mPm.getUidForSharedUser(info.sharedUserId);getAllUsedPermissions(sharedUid, permSet);} catch (NameNotFoundException e) {Log.w(TAG, "Couldn't retrieve shared user id for: " + info.packageName);}}// Retrieve list of permissionsmPermsList.addAll(permSet);setPermissions(mPermsList);}

  此函数调用了extractPerms(info, permSet, installedPkgInfo)函数,这个函数也在AppSecurityPermissions.java文件内部,查看函数定义。

private void extractPerms(PackageInfo info, Set<MyPermissionInfo> permSet, PackageInfo installedPkgInfo) {``````myPerm.mNew = newPerm;permSet.add(myPerm);````
}
private final Map<String, MyPermissionGroupInfo> mPermGroups= new HashMap<String, MyPermissionGroupInfo>();

  在AppSecurityPermissions类中定义了mPermGroups全局变量,以permissionGroup为键的键值对。extractPerms(PackageInfo info,Set《AppSecurityPermissions.MyPermissionInfo> permSet,PackageInfo installedPkgInfo)函数就是从第一个参数PackageInfo中获取的权限信息保存到第二个参数。

public AppSecurityPermissions(Context context, PackageInfo info) {```` if (info.sharedUserId != null) {                       int sharedUid;try {sharedUid = mPm.getUidForSharedUser(info.sharedUserId);getAllUsedPermissions(sharedUid, permSet);} catch (NameNotFoundException e) {Log.w(TAG, "Couldn't retrieve shared user id for: " + info.packageName);}  ````
}

  在这个if分支里面,处理了shareUid的情况,即getAllUsedPermissions(sharedUid, permSet)函数,结果也存储在第二参数即permSet参数。AppSecurityPermissions类中有一个变量,即mPermsList变量,用于存储从apk中分析得到的permission信息。

  private final PermissionGroupInfoComparator mPermGroupComparator = new PermissionGroupInfoComparator();private final PermissionInfoComparator mPermComparator = new PermissionInfoComparator();private final List<MyPermissionInfo> mPermsList = new ArrayList<MyPermissionInfo>();private final CharSequence mNewPermPrefix;private String mPackageName;    

  安装流程至此,所有和权限相关的就是从apk中提取权限,然后把提取出的权限存放在AppSecurityPermissions类的mPermsList变量变量中,最后显示出来。最后,界面上“下一步”和“取消”按钮分别对应于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);} else {// 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);newIntent.putExtra(InstallAppProgress.EXTRA_MANIFEST_DIGEST, mPkgDigest);newIntent.putExtra(InstallAppProgress.EXTRA_INSTALL_FLOW_ANALYTICS, mInstallFlowAnalytics);String installerPackageName = getIntent().getStringExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME);if (mOriginatingURI != null) {newIntent.putExtra(Intent.EXTRA_ORIGINATING_URI, mOriginatingURI);}if (mReferrerURI != null) {newIntent.putExtra(Intent.EXTRA_REFERRER, mReferrerURI);}if (mOriginatingUid != VerificationParams.NO_UID) {newIntent.putExtra(Intent.EXTRA_ORIGINATING_UID, mOriginatingUid);}if (installerPackageName != null) {newIntent.putExtra(Intent.EXTRA_INSTALLER_PACKAGE_NAME,installerPackageName);}if (getIntent().getBooleanExtra(Intent.EXTRA_RETURN_RESULT, false)) {newIntent.putExtra(Intent.EXTRA_RETURN_RESULT, true);newIntent.addFlags(Intent.FLAG_ACTIVITY_FORWARD_RESULT);}if(localLOGV) Log.i(TAG, "downloaded app uri="+mPackageURI);startActivity(newIntent);}finish();} 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();}
}

  继续分析前,把上面的函数调用流程总结一下,见下图。

Created with Raphaël 2.1.0PackageInstallerActivity中onCreate()PackageInstallerActivity中initiateInstall()PackageInstallerActivity中startInstallConfirm()perms = new AppSecurityPermissions(this, mPkgInfo);perms结构中的mPermsList保存App权限信息启动InstallAppProcess这个Activity

  把AppSecurityPermissions实例化过程也总结一下。

Created with Raphaël 2.1.0AppSecurityPermissions()extractPerms(PackageInfo, Set<MyPermissionInfo> ,PackageInfo)permission存到第二个参数回到AppSecurityPermissions将permission存到mPermsList成员。

  点击同意后,就是一个startActivity的操作,也就是打开InstallAppProcess.java这个Activity。在Oncreate()函数中调用了PackageManager的pm.installPackageWithVerificationAndEncryption(mPackageURI, observer, installFlags, installerPackageName, verificationParams, null)函数。PackageManager是抽象内,其方法实现在子类:/frameworks/base/core/java/android/app/ApplicationPackageManager.java

public void onCreate(Bundle icicle) {super.onCreate(icicle);```initView();
}
public void initView() {setContentView(R.layout.op_progress);```pm.installPackageWithVerificationAndEncryption(mPackageURI,observer, installFlags,installerPackageName, verificationParams, null);
}
    @Overridepublic void installPackageWithVerificationAndEncryption(Uri packageURI,PackageInstallObserver observer, int flags, String installerPackageName,VerificationParams verificationParams, ContainerEncryptionParams encryptionParams) {installCommon(packageURI, observer, flags, installerPackageName, verificationParams,encryptionParams);}

  这个函数调用了自身文件内定义的installCommon(Uri packageURI, PackageInstallObserver observer, int flags, String installerPackageName,VerificationParams verificationParams, ContainerEncryptionParams encryptionParams)函数。

private void installCommon(Uri packageURI,PackageInstallObserver observer, int flags, String installerPackageName,VerificationParams verificationParams, ContainerEncryptionParams encryptionParams) {if (!"file".equals(packageURI.getScheme())) {throw new UnsupportedOperationException("Only file:// URIs are supported");}if (encryptionParams != null) {throw new UnsupportedOperationException("ContainerEncryptionParams not supported");}final String originPath = packageURI.getPath();try {mPM.installPackage(originPath, observer.getBinder(), flags, installerPackageName,verificationParams, null);} catch (RemoteException ignored) {}
}

  而这个函数的关键是调用了installPackage函数,继续追中installPackag()函数,发现并不能跟踪到。原来,ApplicationPackageManager中的mPM成员变量一个IPackageManager变量,而IPackageManager是通过AIDL定义的类,就是有程序生成的。IPackageManager.java文件在编译过程中是经aidl工具处理IPackageManager.aidl后得到,最终的文件位置在Android源码/out/target。IPackageManager.aidl文件所在路径:\frameworks\base\core\java\android\content\pm\IPackageManager.aidl。
所以实际调用了PackageManager.java的installPackage()函数,所在路径:\frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java。

public void installPackage(String originPath, IPackageInstallObserver2 observer,int installFlags, String installerPackageName, VerificationParams verificationParams,String packageAbiOverride) {installPackageAsUser(originPath, observer, installFlags, installerPackageName, verificationParams,packageAbiOverride, UserHandle.getCallingUserId());
}@Override
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 int callingUid = Binder.getCallingUid();enforceCrossUserPermission(callingUid, userId, true, true, "installPackageAsUser");if (isUserRestricted(userId, UserManager.DISALLOW_INSTALL_APPS)) {try {if (observer != null) {observer.onPackageInstalled("", INSTALL_FAILED_USER_RESTRICTED, null, null);}} catch (RemoteException re) {}return;}if ((callingUid == Process.SHELL_UID) || (callingUid == Process.ROOT_UID)) {installFlags |= PackageManager.INSTALL_FROM_ADB;} else {// Caller holds INSTALL_PACKAGES permission, so we're less strict// about installerPackageName.installFlags &= ~PackageManager.INSTALL_FROM_ADB;installFlags &= ~PackageManager.INSTALL_ALL_USERS;}UserHandle user;if ((installFlags & PackageManager.INSTALL_ALL_USERS) != 0) {user = UserHandle.ALL;} else {user = new UserHandle(userId);}verificationParams.setInstallerUid(callingUid);final File originFile = new File(originPath);final OriginInfo origin = OriginInfo.fromUntrustedFile(originFile);final Message msg = mHandler.obtainMessage(INIT_COPY);msg.obj = new InstallParams(origin, observer, installFlags,installerPackageName, verificationParams, user, packageAbiOverride);mHandler.sendMessage(msg);
}

  installPackage()函数调用了installPackageAsUser()函数,在这个函数中,首先调用mContext.enforceCallingOrSelfPermission()检查调用者是否有安装apk的权限,即判断否申请了android.Manifest.permission.INSTALL_PACKAGES权限。接着判断调用者是否是受限制的app,即只有root,shell以及拥有系统签名的app才有执行权限。

  由于安装程序是一个耗时的过程,所以函数使用了Handler消息传递机制,PackageManagerService中定义了PackageHandler的Handler类。Handler首先处理installPackageAsUser()方法发出的INIT_COPY消息,接着跳转到MCS_BOUND消息的处理。在MCS_BOUND消息处理中调用了startCopy()函数,接着调用handleStartCopy()函数,最后调用copyApk()函数,这个函数完成apk的拷贝以及所需文件夹子的创建。handlerStartCopy()函数调用完成之后,调用了handleReturnCode()方法。

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();res = true;}} catch (RemoteException e) {if (DEBUG_INSTALL) Slog.i(TAG, "Posting install MCS_RECONNECT");mHandler.sendEmptyMessage(MCS_RECONNECT);res = false;}handleReturnCode();return res;
}
public void handleStartCopy() throws RemoteException {``````    final InstallArgs args = createInstallArgs(this);`````ret = args.copyApk(mContainerService, true);
}  
int copyApk(IMediaContainerService imcs, boolean temp) throws RemoteException {if (origin.staged) {Slog.d(TAG, origin.file + " already staged; skipping copy");codeFile = origin.file;resourceFile = origin.file;return PackageManager.INSTALL_SUCCEEDED;}try {final File tempDir = mInstallerService.allocateInternalStageDirLegacy();codeFile = tempDir;resourceFile = tempDir;} catch (IOException e) {Slog.w(TAG, "Failed to create copy file: " + e);return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;}final IParcelFileDescriptorFactory target = new IParcelFileDescriptorFactory.Stub() {@Overridepublic ParcelFileDescriptor open(String name, int mode) throws RemoteException {if (!FileUtils.isValidExtFilename(name)) {throw new IllegalArgumentException("Invalid filename: " + name);}try {final File file = new File(codeFile, name);final FileDescriptor fd = Os.open(file.getAbsolutePath(),O_RDWR | O_CREAT, 0644);Os.chmod(file.getAbsolutePath(), 0644);return new ParcelFileDescriptor(fd);} catch (ErrnoException e) {throw new RemoteException("Failed to open: " + e.getMessage());}}};int ret = PackageManager.INSTALL_SUCCEEDED;ret = imcs.copyPackage(origin.file.getAbsolutePath(), target);if (ret != PackageManager.INSTALL_SUCCEEDED) {Slog.e(TAG, "Failed to copy package");return ret;}final File libraryRoot = new File(codeFile, LIB_DIR_NAME);NativeLibraryHelper.Handle handle = null;try {handle = NativeLibraryHelper.Handle.create(codeFile);ret = NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libraryRoot,abiOverride);} catch (IOException e) {Slog.e(TAG, "Copying native libraries failed", e);ret = PackageManager.INSTALL_FAILED_INTERNAL_ERROR;} finally {IoUtils.closeQuietly(handle);}return ret;
}

  这个函数中调用了copyPackage()函数,这个函数也是调用了系统服务,这个方法在/frameworks/base/core/java/com/android/internal/app/IMediaContainerService.aidl中定义,在/frameworks/base/packages/DefaultContainerService/src/com/android/defcontainer/DefaultContainerService.java中实现:

/*** Copy package to the target location.** @param packagePath absolute path to the package to be copied. Can be*            a single monolithic APK file or a cluster directory*            containing one or more APKs.* @return returns status code according to those in*         {@link PackageManager}*/
@Override
public int copyPackage(String packagePath, IParcelFileDescriptorFactory target) {if (packagePath == null || target == null) {return PackageManager.INSTALL_FAILED_INVALID_URI;}PackageLite pkg = null;try {final File packageFile = new File(packagePath);pkg = PackageParser.parsePackageLite(packageFile, 0);return copyPackageInner(pkg, target);} catch (PackageParserException | IOException | RemoteException e) {Slog.w(TAG, "Failed to copy package at " + packagePath + ": " + e);return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;}
}

  查看copyPackageInner()函数:

private int copyPackageInner(PackageLite pkg, IParcelFileDescriptorFactory target)throws IOException, RemoteException {copyFile(pkg.baseCodePath, target, "base.apk");if (!ArrayUtils.isEmpty(pkg.splitNames)) {for (int i = 0; i < pkg.splitNames.length; i++) {copyFile(pkg.splitCodePaths[i], target, "split_" + pkg.splitNames[i] + ".apk");}}return PackageManager.INSTALL_SUCCEEDED;
}

  copyFile()函数:

private void copyFile(String sourcePath, IParcelFileDescriptorFactory target, String targetName)throws IOException, RemoteException {Slog.d(TAG, "Copying " + sourcePath + " to " + targetName);InputStream in = null;OutputStream out = null;try {in = new FileInputStream(sourcePath);out = new ParcelFileDescriptor.AutoCloseOutputStream(target.open(targetName, ParcelFileDescriptor.MODE_READ_WRITE));Streams.copy(in, out);} finally {IoUtils.closeQuietly(out);IoUtils.closeQuietly(in);}
}

  所以copyPackage()就是把apk文件拷贝到/data/app/packageName/base.apk.而NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libraryRoot,abiOverride);就是将apk中的so文件拷贝到/data/app/packageName/lib目录下。

  至此,startCopy()方法漫长的函数调用链追踪完毕,我们在回顾一下这个方法的调用流程。
startCopy()——>handleStartCopy()——>copyApk();copyApk()分别调用了copyPackage()就是把apk文件拷贝到/data/app/packageName/base.apk,以及NativeLibraryHelper.copyNativeBinariesWithOverride(handle, libraryRoot,abiOverride);就是将apk中的so文件拷贝到/data/app/packageName/lib目录下,我们还是以流程图的形式总结一下。

Created with Raphaël 2.1.0InstallAppProcess中的OnCreate()ApplicationPackageManager中的installPackageWithVerificationAndEncryption()ApplicationPackageManager中的installCommon()PackageManagerService中的installPackage()PackageManagerService中的installPackageAsUser()启动Handler机制发送INIT_COPY消息MCS_BOUND消息处理中调用startCopy()调用handleStartCopy()和handleReturnCode()
Created with Raphaël 2.1.0PackageManagerService中的handleStartCopy()PackageManagerService.FileInstallArgs中的copyApk()copyPackage()把apk拷贝到/data/app/pkgName-1/下 NativeLibraryHelper.copyNativeBinariesWithOverride()把so拷贝到/data/app/pkgName-1/lib/下

  回到startCopy()函数,在handleStartCopy()调用完成后,还调用了一个重要的方法handleReturnCode()。继续查看handleReturnCode()源码。

void 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);}
}
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);}args.doPostInstall(res.returnCode, res.uid);}// A restore should be performed at this point if (a) the install// succeeded, (b) the operation is not an update, and (c) the new// package has not opted out of backup participation.final boolean update = res.removedInfo.removedPackage != null;final int flags = (res.pkg == null) ? 0 : res.pkg.applicationInfo.flags;boolean doRestore = !update&& ((flags & ApplicationInfo.FLAG_ALLOW_BACKUP) != 0);// Set up the post-install work request bookkeeping.  This will be used// and cleaned up by the post-install event handling regardless of whether// there's a restore pass performed.  Token values are >= 1.int token;if (mNextInstallToken < 0) mNextInstallToken = 1;token = mNextInstallToken++;PostInstallData data = new PostInstallData(args, res);mRunningInstalls.put(token, data);if (DEBUG_INSTALL) Log.v(TAG, "+ starting restore round-trip " + token);if (res.returnCode == PackageManager.INSTALL_SUCCEEDED && doRestore) {// Pass responsibility to the Backup Manager.  It will perform a// restore if appropriate, then pass responsibility back to the// Package Manager to run the post-install observer callbacks// and broadcasts.IBackupManager bm = IBackupManager.Stub.asInterface(ServiceManager.getService(Context.BACKUP_SERVICE));if (bm != null) {if (DEBUG_INSTALL) Log.v(TAG, "token " + token+ " to BM for possible restore");try {if (bm.isBackupServiceActive(UserHandle.USER_OWNER)) {bm.restoreAtInstall(res.pkg.applicationInfo.packageName, token);} else {doRestore = false;}} catch (RemoteException e) {// can't happen; the backup manager is local} catch (Exception e) {Slog.e(TAG, "Exception trying to enqueue restore", e);doRestore = false;}} else {Slog.e(TAG, "Backup Manager not found!");doRestore = false;}}if (!doRestore) {// No restore possible, or the Backup Manager was mysteriously not// available -- just fire the post-install work request directly.if (DEBUG_INSTALL) Log.v(TAG, "No restore - queue post-install for " + token);Message msg = mHandler.obtainMessage(POST_INSTALL, token, 0);mHandler.sendMessage(msg);}}});
}
Created with Raphaël 2.1.0handleReturnCode()processPendingInstall(final InstallArgs args, final int currentStatus) void installPackageLI(InstallArgs args, PackageInstalledInfo res)

  installPackageLI()方法的代码太长,所以还是分解分析。首先还是通过PackageParser来获得Package实例,collectCertificates()函数调用链中,完成了App安装过程中(非升级App)对App签名的检验工作,具体可参看另一篇博客Android App签名(证书)校验过程源码分析。collectManifestDigest()函数负责计算Manifest.xml的文件摘要,并且存在了Package的manifestDigest成员下。

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {````try {pp.collectCertificates(pkg, parseFlags);pp.collectManifestDigest(pkg);} catch (PackageParserException e) {res.setError("Failed collect during installPackageLI", e);return;}````
}

  接着,安装程序比较apk中manifest.xml文件的摘要值:将从apk文件中提提取的manifest计算摘要和之前installer解析到的进行比较,若不匹配,则抛出异常。

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {````if (args.manifestDigest != null) {if (DEBUG_INSTALL) {final String parsedManifest = pkg.manifestDigest == null ? "null": pkg.manifestDigest.toString();Slog.d(TAG, "Comparing manifests: " + args.manifestDigest.toString() + " vs. "+ parsedManifest);}if (!args.manifestDigest.equals(pkg.manifestDigest)) {res.setError(INSTALL_FAILED_PACKAGE_CHANGED, "Manifest digest changed");return;}} else if (DEBUG_INSTALL) {final String parsedManifest = pkg.manifestDigest == null? "null" : pkg.manifestDigest.toString();Slog.d(TAG, "manifestDigest was not present, but parser got: " + parsedManifest);}````
}

  接下来,是对App升级或者使用了shareUserId属性时的证书判断。

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {````if (!ps.keySetData.isUsingUpgradeKeySets() || ps.sharedUser != null) {try {verifySignaturesLP(ps, pkg);} catch (PackageManagerException e) {res.setError(e.error, e.getMessage());return;}}````
}

  上面if中的!ps.keySetData.isUsingUpgradeKeySets()给出了不是采用Upgrade keysets升级的情况,是不是采用Upgrade keysets升级时根据PackageKeySetData中的long[] mUpgradeKeySets成员是否空或者长度是否为零决定的。关于Upgrade keysets,后面可能会给出说明。对于不是Upgrade keysets升级或者shareUid情况的,调用了verifySignaturesLP(PackageSetting pkgSetting, PackageParser.Package pkg)函数来检验签名,若不匹配,抛出PackageManagerException异常。

private void verifySignaturesLP(PackageSetting pkgSetting, PackageParser.Package pkg)throws PackageManagerException {if (pkgSetting.signatures.mSignatures != null) {// Already existing package. Make sure signatures matchboolean match = compareSignatures(pkgSetting.signatures.mSignatures, pkg.mSignatures)== PackageManager.SIGNATURE_MATCH;if (!match) {match = compareSignaturesCompat(pkgSetting.signatures, pkg)== PackageManager.SIGNATURE_MATCH;}if (!match) {match = compareSignaturesRecover(pkgSetting.signatures, pkg)== PackageManager.SIGNATURE_MATCH;}if (!match) {throw new PackageManagerException(INSTALL_FAILED_UPDATE_INCOMPATIBLE, "Package "+ pkg.packageName + " signatures do not match the "+ "previously installed version; ignoring!");}}// Check for shared user signaturesif (pkgSetting.sharedUser != null && pkgSetting.sharedUser.signatures.mSignatures != null) {// Already existing package. Make sure signatures matchboolean match = compareSignatures(pkgSetting.sharedUser.signatures.mSignatures,pkg.mSignatures) == PackageManager.SIGNATURE_MATCH;if (!match) {match = compareSignaturesCompat(pkgSetting.sharedUser.signatures, pkg)== PackageManager.SIGNATURE_MATCH;}if (!match) {match = compareSignaturesRecover(pkgSetting.sharedUser.signatures, pkg)== PackageManager.SIGNATURE_MATCH;}if (!match) {throw new PackageManagerException(INSTALL_FAILED_SHARED_USER_INCOMPATIBLE,"Package " + pkg.packageName+ " has no signatures that match those in shared user "+ pkgSetting.sharedUser.name + "; ignoring!");}}
}

  校验过程主要调用了三个函数,其实这三个函数都是比较旧的apk中的Signatures[]和新的apk中的Signatures[]是否相同。但这里有几个不明确的点,就是如何从apk中或得Signatures[],这个以后单独分析。

  在installPackageLI()函数中,接着是对采用Upgrade keysets升级时检验签名的处理,即调用checkUpgradeKeySetLP()检验新的apk中是都全部包含老的apk中的公钥信息,若不是,则抛出异常。checkUpgradeKeySetLP()用到了证书公钥信息,这解决了一点点在PackageInstaller源码分析中提出疑问。

private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {````else {if (!checkUpgradeKeySetLP(ps, pkg)) {res.setError(INSTALL_FAILED_UPDATE_INCOMPATIBLE, "Package "+ pkg.packageName + " upgrade keys do not match the "+ "previously installed version");return;}}````
}
private boolean checkUpgradeKeySetLP(PackageSetting oldPS, PackageParser.Package newPkg) {// Upgrade keysets are being used.  Determine if new package has a superset of the// required keys.long[] upgradeKeySets = oldPS.keySetData.getUpgradeKeySets();KeySetManagerService ksms = mSettings.mKeySetManagerService;for (int i = 0; i < upgradeKeySets.length; i++) {Set<PublicKey> upgradeSet = ksms.getPublicKeysFromKeySetLPr(upgradeKeySets[i]);if (newPkg.mSigningKeys.containsAll(upgradeSet)) {return true;}}return false;
}

  接下来,处理安装的apk申明的权限,分别对apk为系统apk申明系统权限和非系统apk申明系统权限做了处理。

private boolean checkUpgradeKeySetLP(PackageSetting oldPS, PackageParser.Package newPkg) {`````// Check whether the newly-scanned package wants to define an already-defined permint N = pkg.permissions.size();for (int i = N-1; i >= 0; i--) {PackageParser.Permission perm = pkg.permissions.get(i);BasePermission bp = mSettings.mPermissions.get(perm.info.name);if (bp != null) {// If the defining package is signed with our cert, it's okay.  This// also includes the "updating the same package" case, of course.// "updating same package" could also involve key-rotation.final boolean sigsOk;if (!bp.sourcePackage.equals(pkg.packageName)|| !(bp.packageSetting instanceof PackageSetting)|| !bp.packageSetting.keySetData.isUsingUpgradeKeySets()|| ((PackageSetting) bp.packageSetting).sharedUser != null) {sigsOk = compareSignatures(bp.packageSetting.signatures.mSignatures,pkg.mSignatures) == PackageManager.SIGNATURE_MATCH;} else {sigsOk = checkUpgradeKeySetLP((PackageSetting) bp.packageSetting, pkg);}if (!sigsOk) {// If the owning package is the system itself, we log but allow// install to proceed; we fail the install on all other permission// redefinitions.if (!bp.sourcePackage.equals("android")) {res.setError(INSTALL_FAILED_DUPLICATE_PERMISSION, "Package "+ pkg.packageName + " attempting to redeclare permission "+ perm.info.name + " already owned by " + bp.sourcePackage);res.origPermission = perm.info.name;res.origPackage = bp.sourcePackage;return;} else {Slog.w(TAG, "Package " + pkg.packageName+ " attempting to redeclare system permission "+ perm.info.name + "; ignoring new declaration");pkg.permissions.remove(i);}}}}````
}

  最后,针对升级App还是安装新App分别交由不同的函数执行,升级App调用replacePackageLI()函数,安装新App调用installNewPackageLI()。

private boolean checkUpgradeKeySetLP(PackageSetting oldPS, PackageParser.Package newPkg) {````if (replace) {replacePackageLI(pkg, parseFlags, scanFlags | SCAN_REPLACING, args.user,installerPackageName, res);} else {installNewPackageLI(pkg, parseFlags, scanFlags | SCAN_DELETE_DATA_ON_FAILURES,args.user, installerPackageName, res);}````
}

  在分析这两个函数之前,还是对installPackageLI()函数做个总结,我们看看在这个函数中都做了哪些事。

  • 首先,调用了collectCertificates()函数对apk的签名做了校验,确保apk没有被非法修改(修改其中的文件);
  • 接着,调用collectManifestDigest()函数计算了Manifest.xml的文件摘要,并且存在了Package的manifestDigest成员下;
  • 接着,将从apk文件中提提取的manifest计算摘要和之前installer解析到的进行比较,若不匹配,则抛出异常。
  • 然后,对App升级或者使用了共享属性sharedUid的情况校验待安装的App的证书是否匹配,并就升级方式做了具体处理,这里有两个问题,就是升级过程中的Upgrade keysets升级方式和Signature[]的获得不明朗。
  • 最后,就到了根据是升级App还是安装新App,调用不同的函数进行处理环节。

  鉴于不想本篇博文篇幅过长,对App升级还是安装新App过程分析放到PackageInstaller源码分析(二)。下面总结一下。

二、总结

  PackageInstaller负责App的安装工作,根据既有知识,Android权限是在App安装时候一次性授予的,所以在App安装过程要对权限处理。原本分析PackageInstaller源码,旨在分析权限授予过程。由于App安装过程中牵扯很多的安全策略,就一并分析了,包括对apk证书的校验等。

  下面对前面的分析做个梳理。

  • PackageInstallerActivity的onCreate();
  • 根据从Intent中获取的Data的scheme的不同,调用不同的处理逻辑安装App,这篇是从File安装的;
  • 解析App文件到Package对象;
  • initInstall()函数获取apk中的权限信息;
  • onClick()监听用户点击同意安装按钮;
  • 启动InstallAppProcess Activity;
  • onCrete()函数调用了installPackageWithVerificationAndEncryption()函数;
  • 继续调用installPackage()函数;
  • 通过Handler机制启动了startCopy()函数;
  • startCopy()调用了启动了handlerStartCopy()将apk拷贝到/data/app、pkg-name/和把.so文件拷贝到/data/app/pkg-name/lib/的handlerReturnCode()函数;
  • handlerReturnCode调用了installPackageLI()函数;
  • 在installPackageLI()中,首先调用了collectCertificates()函数对apk的签名做了校验,确保apk没有被非法修改(修改其中的文件);
  • 接着,调用collectManifestDigest()函数计算了Manifest.xml的文件摘要,并且存在了Package的manifestDigest成员下;
  • 接着,将从apk文件中提提取的manifest计算摘要和之前installer解析到的进行比较,若不匹配,则抛出异常。
  • 然后,对App升级或者使用了共享属性sharedUid的情况校验待安装的App的证书是否匹配,并就升级方式做了具体处理,这里有两个问题,就是升级过程中的Upgrade keysets升级方式和Signature[]的获得不明朗。
  • 最后,就到了根据是升级App还是安装新App,调用不同的函数进行处理环节。

  PackageInstaller源码分析(一)暂时处理到这,这里还有几个模糊的地方,前面分析也提到了。一是升级过程中的Upgrade keysets升级方式,二是Signature[]的结构和怎么获得的不够明朗。后续分析即关于是升级App还是安装新App的处理见PackageInstaller源码分析(二)。

PackageInstaller源码分析(一)相关推荐

  1. Android 系统(78)---《android framework常用api源码分析》之 app应用安装流程

    <android framework常用api源码分析>之 app应用安装流程 <android framework常用api源码分析>android生态在中国已经发展非常庞大 ...

  2. android源码分析

    01_Android系统概述 02_Android系统的开发综述 03_Android的Linux内核与驱动程序 04_Android的底层库和程序 05_Android的JAVA虚拟机和JAVA环境 ...

  3. Android 源码分析

    查看源码版本号: build\core\version_defaults.mk //搜索该文件中的 PLATFORM_VERSION值 frameworks 目录 (核心框架--java及C++语言) ...

  4. 【Golang源码分析】Go Web常用程序包gorilla/mux的使用与源码简析

    目录[阅读时间:约10分钟] 一.概述 二.对比: gorilla/mux与net/http DefaultServeMux 三.简单使用 四.源码简析 1.NewRouter函数 2.HandleF ...

  5. SpringBoot-web开发(四): SpringMVC的拓展、接管(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) SpringBoot-web开发(二): 页面和图标定制(源码分析) SpringBo ...

  6. SpringBoot-web开发(二): 页面和图标定制(源码分析)

    [SpringBoot-web系列]前文: SpringBoot-web开发(一): 静态资源的导入(源码分析) 目录 一.首页 1. 源码分析 2. 访问首页测试 二.动态页面 1. 动态资源目录t ...

  7. SpringBoot-web开发(一): 静态资源的导入(源码分析)

    目录 方式一:通过WebJars 1. 什么是webjars? 2. webjars的使用 3. webjars结构 4. 解析源码 5. 测试访问 方式二:放入静态资源目录 1. 源码分析 2. 测 ...

  8. Yolov3Yolov4网络结构与源码分析

    Yolov3&Yolov4网络结构与源码分析 从2018年Yolov3年提出的两年后,在原作者声名放弃更新Yolo算法后,俄罗斯的Alexey大神扛起了Yolov4的大旗. 文章目录 论文汇总 ...

  9. ViewGroup的Touch事件分发(源码分析)

    Android中Touch事件的分发又分为View和ViewGroup的事件分发,View的touch事件分发相对比较简单,可参考 View的Touch事件分发(一.初步了解) View的Touch事 ...

最新文章

  1. 刨根问底: Kafka 到底会不会丢数据?
  2. [Azure] Azure 中国服务使用注意事项及兼容版存储访问工具
  3. Tomcat7.0.26的连接数控制bug的问题排查
  4. Linux打印指定的行范围
  5. php 从第几开始截取,php如何实现截取前几个字符
  6. 二十八种未授权访问漏洞合集(暂时最全)
  7. 容器编排技术 -- 在Azure上使用CoreOS和Weave的 Kubernetes
  8. C语言:学生信息管理程序
  9. pytorch搭建TextRNN与使用案例
  10. 360浏览器升级_360安全卫士下载|360安全卫士 12.0 最新版
  11. Mybatis_day2_Mybatis的CRUD操作
  12. excel VB代码
  13. 138.复制带随机指针的链表
  14. OTC场外交易平台源码/虚拟场外交易源码
  15. 保护您的眼睛:电脑背景色设置(XP WIN 7)
  16. 前端:运用js制作一个万年历程序
  17. 什么是互联网产品策划、什么是运营策划(经典收藏)
  18. halcon 将数据保存到excel_halcon保存数据到excel表格-怎样把图像里面的数据提取到excel表格里面去?...
  19. 文件拷贝命令至服务器,远程服务器拷贝文件命令
  20. CouchDB与MongoDB对比

热门文章

  1. 2019全国职业院校“网络空间安全”MS17-010安全自制题
  2. Excel如何解密工作表保护
  3. 你知道如何写一个框架吗?详细步骤放送(上)
  4. 信号去噪,基于Sage-Husa自适应卡尔曼滤波器实现海浪磁场噪声抑制及海浪磁场噪声的产生附Matlab代码
  5. POSTMAN从入门到精通系列(十):团队协作
  6. Elasticsearch系列-Elasticsearch入门教程
  7. css如何让两个div上下排列_深入了解CSS层叠上下层
  8. 闪灵数据恢复软件2.5版上线,可制作U盘启动盘
  9. 闪灵s-cms 5.0 20220328版 去广告 破解过程 思路
  10. 唤醒屏幕 ,解锁屏幕(Android)