赞
踩
该系列文章总纲链接:专题分纲目录 Android Framework 包管理子系统
导图是不断迭代的,这里主要关注➕ PkgMS安装应用部分。主要是三个步骤:从执行adb install 到复制文件,再到装载文件,最后到安装成功。
PackagerInstallerService特殊说明:PackagerInstallerService(PkgMIS)是android5.0中新加入的服务,一般通过PackagerInstallerService来分配一个SessinId,这个系统唯一的ID代表一次安装过程,如果一个应用的安装被中断了,比如设备重启,那么也可以通过这个ID来继续安装。Session的创建如下所示:
- public int createSession(SessionParams params, String installerPackageName, int userId) {
- try {
- return createSessionInternal(params, installerPackageName, userId);
- } catch (IOException e) {
- throw ExceptionUtils.wrap(e);
- }
- }
该方法会返回个系统唯一值作为SessionID,如果希望再次使用Session,可以通过接口openSession打开,代码实现如下:
- public IPackageInstallerSession openSession(int sessionId) {
- try {
- return openSessionInternal(sessionId);
- } catch (IOException e) {
- throw ExceptionUtils.wrap(e);
- }
- }
这里将返回一个IPackageInstallerSession(PackageInstallerSession的IBinder对象),在PackagerManagerService中用变量mSession来存所有的PackageInstallerSession对象。这些PackageInstallerSession对象保存了应用安装的相关数据,比如安装路径、安装进度等。。。
1 安装应用程序(从 adb install 到PkgMS)
一般我们会使用adb install 来安装应用,因此这里就从adb install开始分析,adb命令,install参数。直接看adb命令 处理install参数的代码,如下所示:
- int adb_commandline(int argc, char **argv)
- {
- char buf[4096];
- //...
- if (!strcmp(argv[0], "install")) {
- if (argc < 2) return usage();
- return install_app(ttype, serial, argc, argv);
- }
- //...
- if (!strcmp(argv[0], "uninstall")) {
- if (argc < 2) return usage();
- return uninstall_app(ttype, serial, argc, argv);
- }
- //...
- usage();
- return 1;
- }
这里继续分析install_app实现,代码如下:
- int install_app(transport_type transport, char* serial, int argc, char** argv)
- {
- //这里需要设置复制目标的目录,如果安装在内部存储中,则目标目录为/data/local/tmp。
- static const char *const DATA_DEST = "/data/local/tmp/%s";
- //如果安装在SD卡上,则目标目录为/sdcard/tmp。
- static const char *const SD_DEST = "/sdcard/tmp/%s";
- //...
- for (i = 1; i < argc; i++) {
- if (!strcmp(argv[i], "-s")) {
- where = SD_DEST; //-s参数指明该APK安装到SD卡上
- }
- }
- //...
- char* apk_file = argv[last_apk];
- char apk_dest[PATH_MAX];
- snprintf(apk_dest, sizeof apk_dest, where, get_basename(apk_file));
- //将此APK传送到手机的目标路径
- int err = do_sync_push(apk_file, apk_dest, 0 /* no show progress */);
- if (err) {
- goto cleanup_apk;
- } else {
- argv[last_apk] = apk_dest; /* destination name, not source location */
- }
- //执行pm命令
- pm_command(transport, serial, argc, argv);
-
- cleanup_apk:
- //手机中执行shell rm 命令,删除刚才传送过去的目标APK文件
- delete_file(transport, serial, apk_dest);
- return err;
- }
这里关注pm_command方法,代码如下:
- static int pm_command(transport_type transport, char* serial,
- int argc, char** argv)
- {
- char buf[4096];
-
- snprintf(buf, sizeof(buf), "shell:pm");
-
- while(argc-- > 0) {
- char *quoted = escape_arg(*argv++);
- strncat(buf, " ", sizeof(buf) - 1);
- strncat(buf, quoted, sizeof(buf) - 1);
- free(quoted);
- }
-
- send_shellcommand(transport, serial, buf);
- return 0;
- }
这里调用了send_shellcommand 发送"shell:pm install 参数"给手机端的adbd,这里继续分析pm命令,pm是脚本文件,构成如下:
- # Script to start "pm" on the device,which has a very rudimentary
- # shell.
- #
-
- base=/system
- export CLASSPATH=$base/frameworks/pm.jar
- exec app_process $base/bincom.android.commands.pm.Pm "$@"
在编译system镜像时,Android.mk中会将该脚本复制到system/bin目录下。从pm脚本的内容来看,它就是通过app_process执行pm.jar包的main函数。pm的main函数实现如下:
- public static void main(String[] args) {
- int exitCode = 1;
- exitCode = new Pm().run(args);
- //...
- System.exit(exitCode);
- }
这里的run方法实现如下:
- public int run(String[] args) throws IOException, RemoteException {
- //...
- //获取UserM 和 PkgMS的binder客户端
- mUm = IUserManager.Stub.asInterface(ServiceManager.getService("user"));
- mPm = IPackageManager.Stub.asInterface(ServiceManager.getService("package"));
- //...
- mInstaller = mPm.getPackageInstaller();
- //...
- //处理其他命令,这里仅考虑install的处理
- if ("install".equals(op)) {
- return runInstall();
- }
- //...
- }
这里仅仅关注install部分相关代码,runInstall代码实现如下:
- private int runInstall() {
- int installFlags = 0;
- //...
- while ((opt=nextOption()) != null) {
- if (opt.equals("-l")) {
- installFlags |= PackageManager.INSTALL_FORWARD_LOCK;
- } else if (opt.equals("-r")) {
- installFlags |= PackageManager.INSTALL_REPLACE_EXISTING;
- //...各种参数解析
- } else {
- System.err.println("Error: Unknown option: " + opt);
- return 1;
- }
- }
- //...
- //获取Verification Package的文件位置
- final String verificationFilePath = nextArg();
- if (verificationFilePath != null) {
- System.err.println("\tver: " + verificationFilePath);
- verificationURI = Uri.fromFile(new File(verificationFilePath));
- } else {
- verificationURI = null;
- }
- //创建PackageInstallObserver,用于接收PkgMS的安装结果
- LocalPackageInstallObserver obs = new LocalPackageInstallObserver();
- try {
- VerificationParams verificationParams = new VerificationParams(verificationURI,
- originatingURI, referrerURI, VerificationParams.NO_UID, null);
- //这里调用PkgMS来安装应用程序
- mPm.installPackageAsUser(apkFilePath, obs.getBinder(), installFlags,
- installerPackageName, verificationParams, abi, userId);
-
- synchronized (obs) {
- while (!obs.finished) {
- try {
- obs.wait();//等待安装结果
- } catch (InterruptedException e) {
- //...
- }
- }
- //...
- }
- } catch (RemoteException e) {
- //...
- }
- }
这里调用了mPm.installPackageAsUser方法来处理“安装”,从 adb install 到PkgMS到此分析结束,接下来从这里开始便正式进入PkgMS来安装应用程序。
同时这里介绍另一种调用方式,给指定用户安装应用,安装应用程序也可以通过调用installPackage来实现,代码如下:
- public void installPackage(String originPath, IPackageInstallObserver2 observer,
- int installFlags, String installerPackageName, VerificationParams verificationParams,
- String packageAbiOverride) {
- installPackageAsUser(originPath, observer, installFlags, installerPackageName, verificationParams,
- packageAbiOverride, UserHandle.getCallingUserId());
- }
这里实际上也是调用了installPackageAsUser方法。因此后面的安装应用程序步骤会从installPackageAsUser开始分析。
2 安装应用程序(复制文件)
2.1 流程分析
从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 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 {
- installFlags &= ~PackageManager.INSTALL_FROM_ADB;
- installFlags &= ~PackageManager.INSTALL_ALL_USERS;
- }
-
- UserHandle user;
- //如果有INSTALL_ALL_USERS标志,则给所有用户安装
- 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);
- //保存参数到InstallParams对象
- final Message msg = mHandler.obtainMessage(INIT_COPY);
- msg.obj = new InstallParams(origin, observer, installFlags,
- installerPackageName, verificationParams, user, packageAbiOverride);
- mHandler.sendMessage(msg);//发送消息
- }
这里最后把调用的参数Installarams对象中,然后发送INIT_COPY消息。mHandler被绑定到另外一个工作线程(借助ThreadHandler对象的Looper)中,所以该INIT_COPY消息也将在那个工作线程中进行处理。接下来看handleMessage如何处理消息,代码如下:
- public void handleMessage(Message msg) {
- try {
- doHandleMessage(msg);
- } finally {
- Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
- }
- }
继续分析doHandleMessage,这里只关注INIT_COPY相关处理,代码如下:
- case INIT_COPY: {
- HandlerParams params = (HandlerParams) msg.obj;
- int idx = mPendingInstalls.size();
- if (!mBound) {
- //绑定defaultContainerService服务
- if (!connectToService()) {
- //错误处理
- } else {
- //连接成功,把安装信息保存到mPendingInstalls
- mPendingInstalls.add(idx, params);
- }
- } else {
- //插入安装信息
- mPendingInstalls.add(idx, params);
- //如果安装请求队列之前的状态为空,则表明要启动安装
- if (idx == 0) {
- mHandler.sendEmptyMessage(MCS_BOUND);
- }
- }
- break;
- }
这里有一个前提,成功启动了DefaultContainerService(简称DCS)且idx为零,所以这是PkgMS首次处理安装请求,即将要处理MCS_BOUND消息。继续分析MCS_BOUND的处理,代码如下:
- case 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()) {//执行安装
- if (mPendingInstalls.size() > 0) {
- //工作完成,删除第一项
- mPendingInstalls.remove(0);
- }
- if (mPendingInstalls.size() == 0) {
- if (mBound) {
- //安装请求都处理完了,则需要和Service断绝联系,
- //通过发送MSC_UNB消息处理断交请求
- removeMessages(MCS_UNBIND);
- Message ubmsg = obtainMessage(MCS_UNBIND);
- sendMessageDelayed(ubmsg, 10000);
- }
- } else {
- //还有未处理的请求,则继续发送MCS_BOUND
- mHandler.sendEmptyMessage(MCS_BOUND);
- }
- }
- }
- }
- break;
- }
这里先分析MCS_UNBIND的处理流程,代码如下所示:
- case MCS_UNBIND: {
- if (mPendingInstalls.size() == 0 && mPendingVerification.size() == 0) {
- if (mBound) {
- disconnectService();
- }
- } else if (mPendingInstalls.size() > 0) {
- mHandler.sendEmptyMessage(MCS_BOUND);
- }
- break;
- }
可以看到,如果没有请求则与服务断开联系,只要还有请求,就会继续发送MCS_BOUND消息。接下来继续分析startCopy,代码实现如下:
- final boolean startCopy() {
- boolean res;
- try {
- if (++mRetries > MAX_RETRIES) {
- mHandler.sendEmptyMessage(MCS_GIVE_UP);
- handleServiceError();
- return false;
- } else {
- handleStartCopy();//调用派生类InstallParams的handleStartCopy函数
- res = true;
- }
- } catch (RemoteException e) {
- mHandler.sendEmptyMessage(MCS_RECONNECT);
- res = false;
- }
- //调用派生类InstallParams的handleReturnCode,返回处理结果
- handleReturnCode();
- return res;
- }
这里继续分析关键方法InstallParams类的handleStartCopy,代码如下:
- public void handleStartCopy() throws RemoteException {
- //...
- if (onInt && onSd) {
- // Check if both bits are set
- ret = PackageManager.INSTALL_FAILED_INVALID_INSTALL_LOCATION;
- } else {
- pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath, installFlags,
- packageAbiOverride);
- if (!origin.staged && pkgLite.recommendedInstallLocation
- == PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE) {
- //如果安装空间不够,释放cache空间
- final StorageManager storage = StorageManager.from(mContext);
- final long lowThreshold = storage.getStorageLowBytes(
- Environment.getDataDirectory());
-
- final long sizeBytes = mContainerService.calculateInstalledSize(
- origin.resolvedPath, isForwardLocked(), packageAbiOverride);
-
- if (mInstaller.freeCache(sizeBytes + lowThreshold) >= 0) {
- pkgLite = mContainerService.getMinimalPackageInfo(origin.resolvedPath,
- installFlags, packageAbiOverride);
- }
- if (pkgLite.recommendedInstallLocation
- == PackageHelper.RECOMMEND_FAILED_INVALID_URI) {
- pkgLite.recommendedInstallLocation
- = PackageHelper.RECOMMEND_FAILED_INSUFFICIENT_STORAGE;
- }
- }
- }
- //...
- //创建InstallArgs对象,如果应用要求安装在SD卡或forward lock的应用
- //则创建AsecInstallArgs,否则创建FileInstallArgs
- final InstallArgs args = createInstallArgs(this);
- mArgs = args;
-
- if (ret == PackageManager.INSTALL_SUCCEEDED) {
- //...
- final int requiredUid = mRequiredVerifierPackage == null ? -1
- : getPackageUid(mRequiredVerifierPackage, userIdentifier);
- if (!origin.existing && requiredUid != -1
- && isVerificationEnabled(userIdentifier, installFlags)) {
- //...校验相关
- } else {//无需校验则直接处理
- ret = args.copyApk(mContainerService, true);
- }
- }
- mRet = ret;
- }
这里关注无需校验环节的copyApk(依赖于args的类,这里只从FileInstallArgs类开始分析)方法实现,代码如下:
- int copyApk(IMediaContainerService imcs, boolean temp) throws RemoteException {
- if (origin.staged) {
- codeFile = origin.file;
- resourceFile = origin.file;
- return PackageManager.INSTALL_SUCCEEDED;
- }
-
- try {
- //在data/app下生成临时文件
- final File tempDir = mInstallerService.allocateInternalStageDirLegacy();
- codeFile = tempDir;
- resourceFile = tempDir;
- } catch (IOException e) {
- return PackageManager.INSTALL_FAILED_INSUFFICIENT_STORAGE;
- }
-
- final IParcelFileDescriptorFactory target = new IParcelFileDescriptorFactory.Stub() {
- //为临时文件创建文件描述符ParcelFileDescriptor,能够通过Binder传递
- @Override
- public 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) {
- return ret;
- }
-
- //安装系统自带的native动态库
- 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) {
- ret = PackageManager.INSTALL_FAILED_INTERNAL_ERROR;
- } finally {
- IoUtils.closeQuietly(handle);
- }
- return ret;
- }
至此,安装的第一部份工作完成,应用安装到/data/app目录下。
2.2 关键类说明
HandlerParams和InstallArgs介绍,关系如下所示:
HandlerParams和InstallArgs均为抽象类。HandlerParams有三个子类:InstallParams、MoveParams和MeasureParams:
对于InstallParams来说,它还有两个伴儿,即InstallArgs的派生类FileInstallArgs和AsecInstallArgs。FileInstallArgs针对的是安装在内部存储的APK,而AsecInstallArgs针对的是那些安装在SD卡上的APK。(整个流程的分析,包括后面,都是针对FileInstallArgs来做,AsecInstallArgs分析方式类似)
3 应用安装第二阶段:装载应用
本阶段是将应用格式转换成oat格式,为应用创建数据目录,最后把应用的信息装载到PkgMS的数据结构中,在前面startCopy函数中最后调用了handleReturnCode方法,代码如下:
- void handleReturnCode() {
- 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);//防止重复调用
- PackageInstalledInfo res = new PackageInstalledInfo();
- res.returnCode = currentStatus;
- res.uid = -1;
- res.pkg = null;
- res.removedInfo = new PackageRemovedInfo();
- //如果应用安装成功,则装载安装的应用
- if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
- //InstallArgs的doPreInstall函数
- args.doPreInstall(res.returnCode);
- synchronized (mInstallLock) {
- //关键函数,进行APK安装
- installPackageLI(args, res);
- }
- //FileInstallArgs的doPostInstall函数
- args.doPostInstall(res.returnCode, res.uid);
- }
- //执行备份相关代码(通过BackupManagerService来备份处理)...
- if (!doRestore) {
- //发送POST_INSTALL消息
- Message msg = mHandler.obtainMessage(POST_INSTALL, token, 0);
- mHandler.sendMessage(msg);
- }
- }
- });
- }
该方法总结如下:
接下来将针对{步骤2}的installPackageLI和{步骤4}的handler消息处理进行详细分析。
@1 installPackageLI实现
- private void installPackageLI(InstallArgs args, PackageInstalledInfo res) {
- //...
- final int parseFlags = mDefParseFlags | PackageParser.PARSE_CHATTY
- | (forwardLocked ? PackageParser.PARSE_FORWARD_LOCK : 0)
- | (onSd ? PackageParser.PARSE_ON_SDCARD : 0);
- //创建apk文件的解析器
- PackageParser pp = new PackageParser();
- pp.setSeparateProcesses(mSeparateProcesses);
- pp.setDisplayMetrics(mMetrics);
-
- final PackageParser.Package pkg;
- try {
- //解析apk文件
- pkg = pp.parsePackage(tmpPackageFile, parseFlags);
- } catch (PackageParserException e) {
- res.setError("Failed parse during installPackageLI", e);
- return;
- }
- //收集应用的签名
- try {
- pp.collectCertificates(pkg, parseFlags);
- pp.collectManifestDigest(pkg);
- } catch (PackageParserException e) {
- res.setError("Failed collect during installPackageLI", e);
- return;
- }
- //...
- pp = null;
- String oldCodePath = null;
- boolean systemApp = false;
- synchronized (mPackages) {
- //升级判定及相关处理
- int N = pkg.permissions.size();
- //permission相关处理
- }
-
- //有系统标志的应用要求装在sd卡上,报错退出
- if (systemApp && onSd) {
- res.setError(INSTALL_FAILED_INVALID_INSTALL_LOCATION,
- "Cannot install updates to system apps on sdcard");
- return;
- }
-
- //如果更名失败,则退出
- if (!args.doRename(res.returnCode, pkg, oldCodePath)) {
- res.setError(INSTALL_FAILED_INSUFFICIENT_STORAGE, "Failed rename");
- return;
- }
-
- 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);
- }
- synchronized (mPackages) {
- final PackageSetting ps = mSettings.mPackages.get(pkgName);
- if (ps != null) {
- res.newUsers = ps.queryInstalledUsers(sUserManager.getUserIds(), true);
- }
- }
- }
installPackageLI首先解析了安装的应用文件,得到解析的结果后,主要是判断新安装应用是否和应用系统中已安装应用构成升级关系,是则调用replacePackageLI来处理,否则就用installNewPackageLI来处理。
@2 POST_INSTALL消息处理
相关代码如下:
- case POST_INSTALL: {
- PostInstallData data = mRunningInstalls.get(msg.arg1);
- mRunningInstalls.delete(msg.arg1);
- boolean deleteOld = false;
-
- if (data != null) {
- InstallArgs args = data.args;
- PackageInstalledInfo res = data.res;
-
- if (res.returnCode == PackageManager.INSTALL_SUCCEEDED) {
- res.removedInfo.sendBroadcast(false, true, false);
- Bundle extras = new Bundle(1);
- extras.putInt(Intent.EXTRA_UID, res.uid);
- //...
- //发送广播ACTION_PACKAGE_ADDED
- sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
- res.pkg.applicationInfo.packageName,
- extras, null, null, firstUsers);
- final boolean update = res.removedInfo.removedPackage != null;
- if (update) {
- extras.putBoolean(Intent.EXTRA_REPLACING, true);
- }
- sendPackageBroadcast(Intent.ACTION_PACKAGE_ADDED,
- res.pkg.applicationInfo.packageName,
- extras, null, null, updateUsers);
- if (update) {
- //如果是升级包,则发送更多的广播
- sendPackageBroadcast(Intent.ACTION_PACKAGE_REPLACED,
- res.pkg.applicationInfo.packageName,
- extras, null, null, updateUsers);
- sendPackageBroadcast(Intent.ACTION_MY_PACKAGE_REPLACED,
- null, null,
- res.pkg.applicationInfo.packageName, null, updateUsers);
-
- //如果是forward lock的应用,或者安装在sd卡上的应用,发送广播
- if (isForwardLocked(res.pkg) || isExternal(res.pkg)) {
- int[] uidArray = new int[] { res.pkg.applicationInfo.uid };
- ArrayList<String> pkgList = new ArrayList<String>(1);
- pkgList.add(res.pkg.applicationInfo.packageName);
- sendResourcesChangedBroadcast(true, true,
- pkgList,uidArray, null);
- }
- }
- if (res.removedInfo.args != null) {
- deleteOld = true;
- }
- }
- //...
- if (args.observer != null) {
- try {//通知调用者 安装的结果
- Bundle extras = extrasForInstallResult(res);
- args.observer.onPackageInstalled(res.name, res.returnCode,
- res.returnMsg, extras);
- } catch (RemoteException e) {
- Slog.i(TAG, "Observer no longer exists.");
- }
- }
- }
- } break;
整个消息处理主要是发送广播,应用安装成功后通知其他的应用开始处理,比如Launcher中需要添加的图标等。。。最后通过回调函数通知 安装者 安装的结果。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。