android 应用卸载流程分析

参考了其他大神的一些方法,

1.简介:

Android的每个应用程序,都有自己的可控的目录。在Setting/Application info里面,可以看到每个应用程序,都有Clear data和Clear cache选项。具体这些目录在哪里呢?用adb连接上设备。如果是连接真实设备,需要有设备的root权限。
在/data/data目录下,每个应用程序都有自己的目录,目录名就是应用程序在AndroidManifest.xml文件中定义的包。每个应用程序的代码,对自己的目录是有绝对的控制权限的。在每个目录下,一般有如下几个子目录:
   databases : 存放数据库
   cache : 存放缓存数据
   files : 存放应用程序自己控制的文件
   lib : 存放使用的包
应用安装的流程及路径 
应用安装涉及到如下几个目录:
   system/app 系统自带的应用程序,无法删除
   data/app用户程序安装的目录,有删除权限。安装时把apk文件复制到此目录。
   data/data存放应用程序的数据
   Data/dalvik-cache将apk中的dex文件安装到dalvik-cache目录下(dex文件是dalvik虚拟机的可执行文件,其大小约为原始apk文件大小的四分之一)
安装过程:复制APK安装包到data/app目录下,解压并扫描安装包,把dex文件(Dalvik字节码)保存到dalvik-cache目录,并在data/data目录下创建对应的应用数据目录。


卸载过程:删除安装过程中在上述三个目录下创建的文件及目录。主要步骤如下:

1.从PMS的内部结构上删除acitivity、service、provider等信息

2.删除code、library和resource等信息

3.调用installd删除/data/data/packageName以及/data/dalvik-cache下面的文件

4.更新Settings中的package信息

2.具体分析

当我们在Settings中的应用页面找到一个安装了的应用程序,并点击卸载后,就会发送一个Intent给UninstallerActivity,在UninstallerActivity最后会启动UninstallAppProgress的initView方法,并调用如下卸载函数:

[java]  view plain  copy
  1. getPackageManager().deletePackage(mAppInfo.packageName, observer,  
  2.         mAllUsers ? PackageManager.DELETE_ALL_USERS : 0);  

上面的mAllUsers默认是false。getPackageManager()函数的实现在ContextImpl.java,它最后会调用到ApplicantPackageManger.java的deletePackage方法:

[java]  view plain  copy
  1. public void deletePackage(String packageName, IPackageDeleteObserver observer, int flags) {  
  2.     try {  
  3.         mPM.deletePackageAsUser(packageName, observer, UserHandle.myUserId(), flags);  
  4.     } catch (RemoteException e) {  
  5.         // Should never happen!  
  6.     }  
  7. }  

通过Binder调用,我们来看PMS中的deletePackageAsUser方法:

[java]  view plain  copy
  1. public void deletePackageAsUser(final String packageName,  
  2.                                 final IPackageDeleteObserver observer,  
  3.                                 final int userId, final int flags) {  
  4.     mContext.enforceCallingOrSelfPermission(  
  5.             android.Manifest.permission.DELETE_PACKAGES, null);  
  6.     final int uid = Binder.getCallingUid();  
  7.     if (isUserRestricted(userId, UserManager.DISALLOW_UNINSTALL_APPS)) {  
  8.         try {  
  9.             observer.packageDeleted(packageName, PackageManager.DELETE_FAILED_USER_RESTRICTED);  
  10.         } catch (RemoteException re) {  
  11.         }  
  12.         return;  
  13.     }  
  14.   
  15.     mHandler.post(new Runnable() {  
  16.         public void run() {  
  17.             mHandler.removeCallbacks(this);  
  18.             final int returnCode = deletePackageX(packageName, userId, flags);  
  19.             if (observer != null) {  
  20.                 try {  
  21.                     observer.packageDeleted(packageName, returnCode);  
  22.                 } catch (RemoteException e) {  
  23.                     Log.i(TAG, "Observer no longer exists.");  
  24.                 } //end catch  
  25.             } //end if  
  26.         } //end run  
  27.     });  
  28. }  

在deletePackageAsUser方法中,首先做权限检查,然后就调用deletePackageX方法去执行卸载任务:

[java]  view plain  copy
  1. private int deletePackageX(String packageName, int userId, int flags) {  
  2.     final PackageRemovedInfo info = new PackageRemovedInfo();  
  3.     final boolean res;  
  4.   
  5.     boolean removedForAllUsers = false;  
  6.     boolean systemUpdate = false;  
  7.   
  8.     int[] allUsers;  
  9.     boolean[] perUserInstalled;  
  10.     synchronized (mPackages) {  
  11.         PackageSetting ps = mSettings.mPackages.get(packageName);  
  12.         allUsers = sUserManager.getUserIds();  
  13.         perUserInstalled = new boolean[allUsers.length];  
  14.         for (int i = 0; i < allUsers.length; i++) {  
  15.             perUserInstalled[i] = ps != null ? ps.getInstalled(allUsers[i]) : false;  
  16.         }  
  17.     }  
  18.   
  19.     synchronized (mInstallLock) {  
  20.         res = deletePackageLI(packageName,  
  21.                 (flags & PackageManager.DELETE_ALL_USERS) != 0  
  22.                         ? UserHandle.ALL : new UserHandle(userId),  
  23.                 true, allUsers, perUserInstalled,  
  24.                 flags | REMOVE_CHATTY, info, true);  
  25.         systemUpdate = info.isRemovedPackageSystemUpdate;  
  26.         if (res && !systemUpdate && mPackages.get(packageName) == null) {  
  27.             removedForAllUsers = true;  
  28.         }  
  29.         if (DEBUG_REMOVE) Slog.d(TAG, "delete res: systemUpdate=" + systemUpdate  
  30.                 + " removedForAllUsers=" + removedForAllUsers);  
  31.     }  
  32.   
  33.     if (res) {  
  34.         info.sendBroadcast(true, systemUpdate, removedForAllUsers);  
  35.   
  36.     }  
  37.     Runtime.getRuntime().gc();  
  38.     if (info.args != null) {  
  39.         synchronized (mInstallLock) {  
  40.             info.args.doPostDeleteLI(true);  
  41.         }  
  42.     }  
  43.   
  44.     return res ? PackageManager.DELETE_SUCCEEDED : PackageManager.DELETE_FAILED_INTERNAL_ERROR;  
  45. }  

deletePackageX在这里我们只考虑当前只有一个user的情况,来看deletePackageLI的实现:

[java]  view plain  copy
  1. private boolean deletePackageLI(String packageName, UserHandle user,  
  2.         boolean deleteCodeAndResources, int[] allUserHandles, boolean[] perUserInstalled,  
  3.         int flags, PackageRemovedInfo outInfo,  
  4.         boolean writeSettings) {  
  5.     PackageSetting ps;  
  6.     boolean dataOnly = false;  
  7.     int removeUser = -1;  
  8.     int appId = -1;  
  9.     synchronized (mPackages) {  
  10.         ps = mSettings.mPackages.get(packageName);  
  11.         if (ps == null) {  
  12.             Slog.w(TAG, "Package named '" + packageName + "' doesn't exist.");  
  13.             return false;  
  14.         }  
  15.         if ((!isSystemApp(ps) || (flags&PackageManager.DELETE_SYSTEM_APP) != 0) && user != null  
  16.                 && user.getIdentifier() != UserHandle.USER_ALL) {  
  17.             ps.setUserState(user.getIdentifier(),  
  18.                     COMPONENT_ENABLED_STATE_DEFAULT,  
  19.                     false//installed  
  20.                     true,  //stopped  
  21.                     true,  //notLaunched  
  22.                     false//blocked  
  23.                     nullnullnull);  
  24.             if (!isSystemApp(ps)) {  
  25.                 if (ps.isAnyInstalled(sUserManager.getUserIds())) {  
  26.                      
  27.             } else {  
  28.                 removeUser = user.getIdentifier();  
  29.                 appId = ps.appId;  
  30.                 mSettings.writePackageRestrictionsLPr(removeUser);  
  31.             }  
  32.         }  
  33.     }  
  34.   
  35.     boolean ret = false;  
  36.     mSettings.mKeySetManager.removeAppKeySetData(packageName);  
  37.     if (isSystemApp(ps)) {  
  38.         ret = deleteSystemPackageLI(ps, allUserHandles, perUserInstalled,  
  39.                 flags, outInfo, writeSettings);  
  40.     } else {  
  41.         // Kill application pre-emptively especially for apps on sd.  
  42.         killApplication(packageName, ps.appId, "uninstall pkg");  
  43.         ret = deleteInstalledPackageLI(ps, deleteCodeAndResources, flags,  
  44.                 allUserHandles, perUserInstalled,  
  45.                 outInfo, writeSettings);  
  46.     }  
  47.   
  48.     return ret;  
  49. }  

在deletePackageLI函数中根据是否是systemApp调用不同的流程,如果是systemApp,则调用deleteSystemPackageLI完成卸载;如果非systemApp,则调用deleteInstalledPackageLI完成卸载,当然在卸载之前,首先会调用AMS的killApplication方法先让这个APP停止运行。我们主要介绍非systemApp的卸载过程,来看deleteInstalledPackageLI方法的实现:

[java]  view plain  copy
  1. private boolean deleteInstalledPackageLI(PackageSetting ps,  
  2.         boolean deleteCodeAndResources, int flags,  
  3.         int[] allUserHandles, boolean[] perUserInstalled,  
  4.         PackageRemovedInfo outInfo, boolean writeSettings) {  
  5.     if (outInfo != null) {  
  6.         outInfo.uid = ps.appId;  
  7.     }  
  8.   
  9.     removePackageDataLI(ps, allUserHandles, perUserInstalled, outInfo, flags, writeSettings);  
  10.   
  11.     if (deleteCodeAndResources && (outInfo != null)) {  
  12.         outInfo.args = createInstallArgs(packageFlagsToInstallFlags(ps), ps.codePathString,  
  13.                 ps.resourcePathString, ps.nativeLibraryPathString);  
  14.     }  
  15.     return true;  
  16. }  

在deleteInstalledPackageLI方法中,分为两步去卸载应用:第一步删除/data/data下面的数据目录,并从PMS的内部数据结构上清除当前卸载的package信息;第二步就删除code和resource文件。我们先来看第一步:

[java]  view plain  copy
  1. private void removePackageDataLI(PackageSetting ps,  
  2.         int[] allUserHandles, boolean[] perUserInstalled,  
  3.         PackageRemovedInfo outInfo, int flags, boolean writeSettings) {  
  4.     String packageName = ps.name;  
  5.     removePackageLI(ps, (flags&REMOVE_CHATTY) != 0);  
  6.     final PackageSetting deletedPs;  
  7.   
  8.     synchronized (mPackages) {  
  9.         deletedPs = mSettings.mPackages.get(packageName);  
  10.         if (outInfo != null) {  
  11.             outInfo.removedPackage = packageName;  
  12.             outInfo.removedUsers = deletedPs != null  
  13.                     ? deletedPs.queryInstalledUsers(sUserManager.getUserIds(), true)  
  14.                     : null;  
  15.         }  
  16.     }  
  17.     if ((flags&PackageManager.DELETE_KEEP_DATA) == 0) {  
  18.         removeDataDirsLI(packageName);  
  19.         schedulePackageCleaning(packageName, UserHandle.USER_ALL, true);  
  20.     }  

removePackageDataLI用于删除应用的/data/data数据目录,并且从PMS内部数据结构里面清除package的信息。首先调用removePackageLI从PMS内部的数据结构上删除要卸载的package信息:

[java]  view plain  copy
  1. void removePackageLI(PackageSetting ps, boolean chatty) {  
  2.     synchronized (mPackages) {  
  3.         mPackages.remove(ps.name);  
  4.         if (ps.codePathString != null) {  
  5.             mAppDirs.remove(ps.codePathString);  
  6.         }  
  7.   
  8.         final PackageParser.Package pkg = ps.pkg;  
  9.         if (pkg != null) {  
  10.             cleanPackageDataStructuresLILPw(pkg, chatty);  
  11.         }  
  12.     }  
  13. }  

cleanPackageDataStructuresLILPw用于将package的providers、services、receivers、activities等信息去PMS的全局数据结构上移除,这部分代码比较简单。如果没有设置DELETE_KEEP_DATA这个flag,就会首先调用removeDataDirsLI去删除/data/data下面的目录:

[java]  view plain  copy
  1. private int removeDataDirsLI(String packageName) {  
  2.     int[] users = sUserManager.getUserIds();  
  3.     int res = 0;  
  4.     for (int user : users) {  
  5.         int resInner = mInstaller.remove(packageName, user);  
  6.         if (resInner < 0) {  
  7.             res = resInner;  
  8.         }  
  9.     }  
  10.   
  11.     final File nativeLibraryFile = new File(mAppLibInstallDir, packageName);  
  12.     NativeLibraryHelper.removeNativeBinariesFromDirLI(nativeLibraryFile);  
  13.     if (!nativeLibraryFile.delete()) {  
  14.         Slog.w(TAG, "Couldn't delete native library directory " + nativeLibraryFile.getPath());  
  15.     }  
  16.   
  17.     return res;  
  18. }  

这里首先调用installd的remove方法去删除/data/data下面的目录。然后去删除/data/app-lib下面的应用程序的library信息,但因为这里的nativeLibraryFile为/data/app-lib/packageName,和前面介绍的APK安装过程中的目录/data/app-lib/packageName-num不一样,所以实际上,这里并没有真正的去删除library目录。先来看installd的remove方法:

[cpp]  view plain  copy
  1. static int do_remove(char **arg, char reply[REPLY_MAX])  
  2. {  
  3.     return uninstall(arg[0], atoi(arg[1])); /* pkgname, userid */  
  4. }  
  5.   
  6. int uninstall(const char *pkgname, userid_t userid)  
  7. {  
  8.     char pkgdir[PKG_PATH_MAX];  
  9.   
  10.     if (create_pkg_path(pkgdir, pkgname, PKG_DIR_POSTFIX, userid))  
  11.         return -1;  
  12.   
  13.     return delete_dir_contents(pkgdir, 1, NULL);  
  14. }  
  15.   
  16. int delete_dir_contents(const char *pathname,  
  17.                         int also_delete_dir,  
  18.                         const char *ignore)  
  19. {  
  20.     int res = 0;  
  21.     DIR *d;  
  22.   
  23.     d = opendir(pathname);  
  24.     if (d == NULL) {  
  25.         ALOGE("Couldn't opendir %s: %s\n", pathname, strerror(errno));  
  26.         return -errno;  
  27.     }  
  28.     res = _delete_dir_contents(d, ignore);  
  29.     closedir(d);  
  30.     if (also_delete_dir) {  
  31.         if (rmdir(pathname)) {  
  32.             ALOGE("Couldn't rmdir %s: %s\n", pathname, strerror(errno));  
  33.             res = -1;  
  34.         }  
  35.     }  
  36.     return res;  
  37. }  

create_pkg_path方法构造/data/data/packageName的文件路径名,然后调用delete_dir_contents来删除文件内容以及目录,前面介绍过,/data/data/packageName的文件其实都是符号链接,所以_delete_dir_contents的实现中都是调用unlinkat去删除这些符号链接。回到removePackageDataLI中,接着调用schedulePackageCleaning来安排清理动作:

[java]  view plain  copy
  1. void schedulePackageCleaning(String packageName, int userId, boolean andCode) {  
  2.     mHandler.sendMessage(mHandler.obtainMessage(START_CLEANING_PACKAGE,  
  3.             userId, andCode ? 1 : 0, packageName));  
  4. }  

这里向PackageHandler发送START_CLEANING_PACKAGE消息,PMS会调用ContainService的函数去删除/storage/sdcard0/Android/data和/storage/sdcard0/Android/media下面与package相关的文件,有兴趣可以去看一下这部分的code。接着来看removePackageDataLI方法:

[java]  view plain  copy
  1. synchronized (mPackages) {  
  2.     if (deletedPs != null) {  
  3.         if ((flags&PackageManager.DELETE_KEEP_DATA) == 0) {  
  4.             if (outInfo != null) {  
  5.                 outInfo.removedAppId = mSettings.removePackageLPw(packageName);  
  6.             }  
  7.             if (deletedPs != null) {  
  8.                 updatePermissionsLPw(deletedPs.name, null0);  
  9.                 if (deletedPs.sharedUser != null) {  
  10.                     // remove permissions associated with package  
  11.                     mSettings.updateSharedUserPermsLPw(deletedPs, mGlobalGids);  
  12.                 }  
  13.             }  
  14.             clearPackagePreferredActivitiesLPw(deletedPs.name, UserHandle.USER_ALL);  
  15.         }  
  16.     }  
  17.   
  18.     if (writeSettings) {  
  19.         mSettings.writeLPr();  
  20.     }  
  21. }  
  22. if (outInfo != null) {  
  23.     removeKeystoreDataIfNeeded(UserHandle.USER_ALL, outInfo.removedAppId);  
  24. }  

这里首先从Settings中删除PackageSettings的信息:

[java]  view plain  copy
  1. int removePackageLPw(String name) {  
  2.     final PackageSetting p = mPackages.get(name);  
  3.     if (p != null) {  
  4.         mPackages.remove(name);  
  5.         if (p.sharedUser != null) {  
  6.             p.sharedUser.removePackage(p);  
  7.             if (p.sharedUser.packages.size() == 0) {  
  8.                 mSharedUsers.remove(p.sharedUser.name);  
  9.                 removeUserIdLPw(p.sharedUser.userId);  
  10.                 return p.sharedUser.userId;  
  11.             }  
  12.         } else {  
  13.             removeUserIdLPw(p.appId);  
  14.             return p.appId;  
  15.         }  
  16.     }  
  17.     return -1;  
  18. }  

removePackageLPw首先从mPackages这个map中删除PackageSettings信息,如果不存在sharedUser,则从mUserIds这个数组中删除对应的Package UID信息;如果存在sharedUser,则首先检查这个sharedUser是否所有的package都已经被卸载了,如果都被卸载了,这个sharedUser也就可以删除。然后removePackageDataLI调用updatePermissionsLPw去检查mPermissionTrees和mPermissions两个数组中的权限是否是被删除的Package提供,如果有,则删除。Settings的updateSharedUserPermsLPw方法用于清除sharedUser不用的gid信息,防止权限泄露:

[java]  view plain  copy
  1. void updateSharedUserPermsLPw(PackageSetting deletedPs, int[] globalGids) {  
  2.     SharedUserSetting sus = deletedPs.sharedUser;  
  3.   
  4.     for (String eachPerm : deletedPs.pkg.requestedPermissions) {  
  5.         boolean used = false;  
  6.         if (!sus.grantedPermissions.contains(eachPerm)) {  
  7.             continue;  
  8.         }  
  9.         for (PackageSetting pkg:sus.packages) {  
  10.             if (pkg.pkg != null &&  
  11.                     !pkg.pkg.packageName.equals(deletedPs.pkg.packageName) &&  
  12.                     pkg.pkg.requestedPermissions.contains(eachPerm)) {  
  13.                 used = true;  
  14.                 break;  
  15.             }  
  16.         }  
  17.         if (!used) {  
  18.             sus.grantedPermissions.remove(eachPerm);  
  19.         }  
  20.     }  
  21.     int newGids[] = globalGids;  
  22.     for (String eachPerm : sus.grantedPermissions) {  
  23.         BasePermission bp = mPermissions.get(eachPerm);  
  24.         if (bp != null) {  
  25.             newGids = PackageManagerService.appendInts(newGids, bp.gids);  
  26.         }  
  27.     }  
  28.     sus.gids = newGids;  
  29. }  

循环的从要被卸载的Package所在的sharedUser组中找被申请的权限是否还被同一组的其它package使用,如果没有使用者,就从sharedUser的grantedPermissions删除。clearPackagePreferredActivitiesLPw与AMS相关,我们留到以后再来介绍。在removePackageDataLI方法最好调用Settings.writeLPr()方法将改动的信息写到Package.xml中。到这里,我们前面所说的deleteInstalledPackageLI方法中的第一步已经完成,来看第二部分:

[java]  view plain  copy
  1.     if (deleteCodeAndResources && (outInfo != null)) {  
  2.         outInfo.args = createInstallArgs(packageFlagsToInstallFlags(ps), ps.codePathString,  
  3.                 ps.resourcePathString, ps.nativeLibraryPathString);  
  4.     }  
  5.   
  6. private InstallArgs createInstallArgs(int flags, String fullCodePath, String fullResourcePath,  
  7.         String nativeLibraryPath) {  
  8.     final boolean isInAsec;  
  9.     if (installOnSd(flags)) {  
  10.         isInAsec = true;  
  11.     } else if (installForwardLocked(flags)  
  12.             && !fullCodePath.startsWith(mDrmAppPrivateInstallDir.getAbsolutePath())) {  
  13.         isInAsec = true;  
  14.     } else {  
  15.         isInAsec = false;  
  16.     }  
  17.   
  18.     if (isInAsec) {  
  19.         return new AsecInstallArgs(fullCodePath, fullResourcePath, nativeLibraryPath,  
  20.                 installOnSd(flags), installForwardLocked(flags));  
  21.     } else {  
  22.         return new FileInstallArgs(fullCodePath, fullResourcePath, nativeLibraryPath);  
  23.     }  
  24. }  

这里根据安装目录的不同,分别构造FileInstallArgs和AsecInstallArgs来完成code和resource资源的清除。这里我们主要介绍卸载内部存储空间上面的APK,来看FileInstallArgs的doPostDeleteLI方法:

[java]  view plain  copy
  1. boolean doPostDeleteLI(boolean delete) {  
  2.     cleanUpResourcesLI();  
  3.     return true;  
  4. }  
  5.   
  6. void cleanUpResourcesLI() {  
  7.     String sourceDir = getCodePath();  
  8.     if (cleanUp()) {  
  9.         int retCode = mInstaller.rmdex(sourceDir);  
  10.         if (retCode < 0) {  
  11.             Slog.w(TAG, "Couldn't remove dex file for package: "  
  12.                     +  " at location "  
  13.                     + sourceDir + ", retcode=" + retCode);  
  14.             // we don't consider this to be a failure of the core package deletion  
  15.         }  
  16.     }  
  17. }  

cleanUpResourcesLI方法中首先调用cleanUp方法去删除code、resource以及library文件:

[java]  view plain  copy
  1. private boolean cleanUp() {  
  2.     boolean ret = true;  
  3.     String sourceDir = getCodePath();  
  4.     String publicSourceDir = getResourcePath();  
  5.     if (sourceDir != null) {  
  6.         File sourceFile = new File(sourceDir);  
  7.         if (!sourceFile.exists()) {  
  8.             Slog.w(TAG, "Package source " + sourceDir + " does not exist.");  
  9.             ret = false;  
  10.         }  
  11.   
  12.         sourceFile.delete();  
  13.     }  
  14.     if (publicSourceDir != null && !publicSourceDir.equals(sourceDir)) {  
  15.         final File publicSourceFile = new File(publicSourceDir);  
  16.         if (!publicSourceFile.exists()) {  
  17.             Slog.w(TAG, "Package public source " + publicSourceFile + " does not exist.");  
  18.         }  
  19.         if (publicSourceFile.exists()) {  
  20.             publicSourceFile.delete();  
  21.         }  
  22.     }  
  23.   
  24.     if (libraryPath != null) {  
  25.         File nativeLibraryFile = new File(libraryPath);  
  26.         NativeLibraryHelper.removeNativeBinariesFromDirLI(nativeLibraryFile);  
  27.         if (!nativeLibraryFile.delete()) {  
  28.             Slog.w(TAG, "Couldn't delete native library directory " + libraryPath);  
  29.         }  
  30.     }  
  31.   
  32.     return ret;  
  33. }  

然后cleanUpResourcesLI调用installd的rmdex方法去删除存在/data/dalvik-cache文件:

[cpp]  view plain  copy
  1. static int do_rm_dex(char **arg, char reply[REPLY_MAX])  
  2. {  
  3.     return rm_dex(arg[0]); /* pkgname */  
  4. }  
  5.   
  6. int rm_dex(const char *path)  
  7. {  
  8.     char dex_path[PKG_PATH_MAX];  
  9.   
  10.     if (validate_apk_path(path)) return -1;  
  11.     if (create_cache_path(dex_path, path)) return -1;  
  12.   
  13.     ALOGV("unlink %s\n", dex_path);  
  14.     if (unlink(dex_path) < 0) {  
  15.         ALOGE("Couldn't unlink %s: %s\n", dex_path, strerror(errno));  
  16.         return -1;  
  17.     } else {  
  18.         return 0;  
  19.     }  
  20. }  

create_cache_path依据path构造/data/dalvik-cache下的文件目录,调用unlink去删除文件。到这里卸载APK的deletePackageAsUser函数就已经分析完了。这时会通过observer把卸载结果返回给UninstallAppProgress。




  • 3
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值