android源码(M)-jni实现app卸载统计(一)

9/7/2016 5:04:12 PM

android源码(M)-jni实现app卸载统计(一)

本章涉及的源代码文件名及位置

AndroidRunTime.cpp (framework/base/core/jni/AndroidRunTime.cpp)
JNIHelp.c (libnativehelper/JNIHelp.c)
UninstallerActivity.java (packages/apps/PackageInstaller/.../UninstallerActivity.java)
UninstallAppProgress.java (packages/apps/UninstallAppProgress.java/.../UninstallAppProgress.java)
PackageManagerService.java (frameworks/base/services/core/.../pm/PackageManagerService.java)
installed.cpp (frameworks/native/cmds/installd/installed.cpp)

本章将介绍android app卸载事件统计,并将其开源至github和oschina上。源码的下载可参考上一篇博客:android源码-下载与管理,关于jni学习可参看深入理解JNI技术。

1.1 android应用卸载简介

对于一款互联网产品来说,往往不可避免的会面临用户流失的问题。一定范围内的用户流失率是可以被接受的,因为通过各种渠道接触到产品的用户并不一定都是我们的目标用户。但当流失率高于我们的预期值或者流失率突然变高时,我们就需要思考用户为什么会离开?还会有更多的用户继续流失吗?已经流失的用户还有没有可能被挽回呢?这时候就急需要进行流失用户研究来回答这些问题。

用户流失最严重的莫过于用户对产品已经失去了兴趣,对产品进行卸载行为,当用户从存储空间上删除程序文件和文件夹以及从注册表删除相关数据的操作,释放原来占用的存储空间并使其软件不再存在于系统中。这个流程我们该如何检测呢?

1.1.1 应用卸载源码分析

在APK的卸载过程,需要经过4个过程:

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

  2. 删除本地代码-code、library和resource等信息

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

  4. 更新Settings中的package信息

注意:这里没有对应用创建在内存设备(MTP挂载)下的目录文件夹进行链接删除,也就意味着垃圾清除软件可以清除或修改存储在内存设备目录或文件夹

我们从android->settings开始,当在Settings中的应用管理页面找到一个已经安装了的应用程序,并点击卸载,就会发送一个广播,传递Intent信息,如下所示。

act=android.intent.action.PACKAGE_REMOVED
dat=package:com.nd.assistance flg=0x4000010 
cmp=com.xx.test/.PackageActionsReceiver

当系统应用UninstallerActivity收到当前卸载广播的时候,UninstallerActivity最后会启动UninstallAppProgress的initView方法,并调用如下卸载函数:

[--> UninstallAppProgress.java]

packageManager.deletePackageAsUser(mAppInfo.packageName, observer,
	mUser.getIdentifier(), 
	mAllUsers ? PackageManager.DELETE_ALL_USERS : 0);
  • 参数1:要卸载应用的包名
  • 参数2:观察者,用来监测卸载过程中的事件,譬如卸载完成。
  • 参数3:多用户模式的时候需要删除所有用户对当前app存储的内存信息,目前是0

UninstallAppProgress中调用了PackageManagerService中的deletePackageAsUser(package, observer, 0, 0)方法,进入到PackageManagerService类中,deletePackageAsUser调用deletePackage方法。

[--> PackageManagerService.java]

@Override
public void deletePackageAsUser(String packageName, IPackageDeleteObserver observer, int userId,
        int flags) {
    deletePackage(packageName, new LegacyPackageDeleteObserver(observer).getBinder(), userId,
            flags);
}

@Override
public void deletePackage(final String packageName,
        final IPackageDeleteObserver2 observer, final int userId, final int flags) {

    ......

	// 判断是否为不能卸载的应用,譬如系统应用
    if (isUserRestricted(userId, UserManager.DISALLOW_UNINSTALL_APPS)) {
        
        return;
    }

	......        

    // 移除包可能需要一定的时间,需要开启一个线程
    mHandler.post(new Runnable() {
        public void run() {
            mHandler.removeCallbacks(this);
			
			// 真正的删除操作
            final int returnCode = deletePackageX(packageName, userId, flags);
            if (observer != null) {
            	observer.onPackageDeleted(packageName, returnCode, null);
            } //end if
        } //end run
    });
}

系统考虑到应用在卸载的过程中需要一定的时间,开启了线程执行deletePackageX函数卸载应用,并将最后的卸载结果返回然后通过observer(观察者)通知给监测的应用程序。这里进入deletePackageX函数。

[--> PackageManagerService.java]

private int deletePackageX(String packageName, int userId, int flags) {
    final PackageRemovedInfo info = new PackageRemovedInfo();
    final boolean res;

    ......

    synchronized (mInstallLock) {
        res = deletePackageLI(packageName, removeForUser,
                true, allUsers, perUserInstalled,
                flags | REMOVE_CHATTY, info, true);

    }

    if (res) {
        info.sendBroadcast(true, systemUpdate, removedForAllUsers);

        ......
    }
    // Force a gc here.
    Runtime.getRuntime().gc();
    
	......

    return res ? PackageManager.DELETE_SUCCEEDED : PackageManager.DELETE_FAILED_INTERNAL_ERROR;
}

deletePackageX作为应用卸载的重要函数,注意承担了:

  • 调用卸载函数deletePackageLI,完成应用卸载,并返回结果至res中。
  • 根据返回结果,通过PackageRemovedInfo结构,对外发出广播,也就是我们最前面提到的Intent
  • 主动调用gc释放内存,并将结果返回

接下来我们进入到deletePackageLI函数中: 函数传入参数为:

  • packageName 包名
  • user 为应用拥有者身份,当前为单用户
  • deleteCodeAndResources 删除代码和资源,当前为true
  • allUserHandles 所有用户的handler,当前为null
  • perUserInstalled 所有用户安装情况
  • flags 为0|(1<<16)
  • outInfo 移除信息,当前为new PackageRemovedInfo()
  • writeSettings 是否写入设置中,当前为true

[--> PackageManagerService.java]

private boolean deletePackageLI(String packageName, UserHandle user,
        boolean deleteCodeAndResources, int[] allUserHandles, boolean[] perUserInstalled,
        int flags, PackageRemovedInfo outInfo,
        boolean writeSettings) {
	
	.......

    boolean ret = false;
    if (isSystemApp(ps)) {
        if (DEBUG_REMOVE) Slog.d(TAG, "Removing system package:" + ps.name);
        // When an updated system application is deleted we delete the existing resources as well and
        // fall back to existing code in system partition
        ret = deleteSystemPackageLI(ps, allUserHandles, perUserInstalled,
                flags, outInfo, writeSettings);
    } else {
        if (DEBUG_REMOVE) Slog.d(TAG, "Removing non-system package:" + ps.name);
        // Kill application pre-emptively especially for apps on sd.
        killApplication(packageName, ps.appId, "uninstall pkg");
        ret = deleteInstalledPackageLI(ps, deleteCodeAndResources, flags,
                allUserHandles, perUserInstalled,
                outInfo, writeSettings);
    }

    return ret;
}

通过isSystemApp()函数判断是不是系统应用采用不同的卸载流程,如果isSystemApp()函数返回true则调用deleteSystemPackageLI()函数实现系统应用的卸载,如果是非系统应用,主要分为两个步骤:

  1. 通过killApplication()函数“杀死”应用。
  2. 通过deleteInstalledPackageLI()清除应用在磁盘和内存上的数据。

[--> PackageManagerService.java]

private boolean deleteInstalledPackageLI(PackageSetting ps,
        boolean deleteCodeAndResources, int flags,
        int[] allUserHandles, boolean[] perUserInstalled,
        PackageRemovedInfo outInfo, boolean writeSettings) {
	
    ......

    // 1.删除内部结构上的package数据,删除data/data/下数据
    removePackageDataLI(ps, allUserHandles, perUserInstalled, outInfo, flags, writeSettings);

    // 2.删除应用代码,删除应用资源
    if (deleteCodeAndResources && (outInfo != null)) {
        outInfo.args = createInstallArgsForExisting(packageFlagsToInstallFlags(ps),
                ps.codePathString, ps.resourcePathString, getAppDexInstructionSets(ps));
    }
    return true;
}

1.1.2 删除内部结构和data数据

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

[--> PackageManagerService.java]

private void removePackageDataLI(PackageSetting ps,
        int[] allUserHandles, boolean[] perUserInstalled,
        PackageRemovedInfo outInfo, int flags, boolean writeSettings) {
    String packageName = ps.name;
    
	// 1.内部结构(内存)上清除应用信息		
    removePackageLI(ps, (flags&REMOVE_CHATTY) != 0);
	
    // Retrieve object to delete permissions for shared user later on
    final PackageSetting deletedPs;
    // reader
    synchronized (mPackages) {
		
        deletedPs = mSettings.mPackages.get(packageName);
        
		......
    }
    if ((flags&PackageManager.DELETE_KEEP_DATA) == 0) {
		// 2.清除data/data上的数据,JNI操作
        removeDataDirsLI(ps.volumeUuid, packageName);
		// 3.执行包清除操作
        schedulePackageCleaning(packageName, UserHandle.USER_ALL, true);
    }
    // writer
    synchronized (mPackages) {
        if (deletedPs != null) {
            if ((flags&PackageManager.DELETE_KEEP_DATA) == 0) {
                clearIntentFilterVerificationsLPw(deletedPs.name, UserHandle.USER_ALL);
                clearDefaultBrowserIfNeeded(packageName);
                if (outInfo != null) {
                    mSettings.mKeySetManagerService.removeAppKeySetDataLPw(packageName);
					// 4.更新设置
                    outInfo.removedAppId = mSettings.removePackageLPw(packageName);
                }
				// 5.更新权限
                updatePermissionsLPw(deletedPs.name, null, 0);
                
				......
				
                clearPackagePreferredActivitiesLPw(deletedPs.name, UserHandle.USER_ALL);
            }
            // make sure to preserve per-user disabled state if this removal was just
            // a downgrade of a system app to the factory package
            if (allUserHandles != null && perUserInstalled != null) {
                if (DEBUG_REMOVE) {
                    Slog.d(TAG, "Propagating install state across downgrade");
                }
                for (int i = 0; i < allUserHandles.length; i++) {
                    if (DEBUG_REMOVE) {
                        Slog.d(TAG, "    user " + allUserHandles[i]
                                + " => " + perUserInstalled[i]);
                    }
                    ps.setInstalled(perUserInstalled[i], allUserHandles[i]);
                }
            }
        }
        // can downgrade to reader
        if (writeSettings) {
            // Save settings now
            mSettings.writeLPr();
        }
    }
    if (outInfo != null) {
        // A user ID was deleted here. Go through all users and remove it
        // from KeyStore.
        removeKeystoreDataIfNeeded(UserHandle.USER_ALL, outInfo.removedAppId);
    }
}

removePackageDataLI用于删除应用的/data/data数据目录,并且从PMS内部数据结构里面清除package的信息。大致经过五个阶段:

  • 第一阶段,调用removePackageLI从PMS内部的数据结构上删除要卸载的package信息,调用removePackageLI函数。
  • 第二阶段,如果没有设置DELETE_KEEP_DATA这个flag,就会去删除/data/data下面的目录,调用调用removeDataDirsLI函数。
  • 第三阶段,调用ContainService的函数去删除/storage/sdcard0/Android/data和/storage/sdcard0/Android/media下面与package相关的文件,调用schedulePackageCleaning函数。
  • 第四阶段,从Settings中删除PackageSettings的信息,调用mSettings.removePackageLPw()函数。
  • 第五阶段,更新权限信息,调用updatePermissionsLPw()函数。

第一阶段:调用removePackageLI从PMS内部的数据结构上删除要卸载的package信息。

[--> PackageManagerService.java]

void removePackageLI(PackageSetting ps, boolean chatty) {

    // writer
    synchronized (mPackages) {
        mPackages.remove(ps.name);
        final PackageParser.Package pkg = ps.pkg;
        if (pkg != null) {
            cleanPackageDataStructuresLILPw(pkg, chatty);
        }
    }
}

清除mPackages中的包信息,调用cleanPackageDataStructuresLILPw()函数清除具体数据结构,这里不再介绍内部结构清除数据过程。

第二阶段:删除/data/data下面的目录

[--> PackageManagerService.java]

private int removeDataDirsLI(String volumeUuid, String packageName) {
    int[] users = sUserManager.getUserIds();
    int res = 0;
    for (int user : users) {
        int resInner = mInstaller.remove(volumeUuid, packageName, user);
        if (resInner < 0) {
            res = resInner;
        }
    }

    return res;
}

这里获取当前所有用户,遍历所有用户,调用Installer的remove方法去删除/data/data下面的目录。先来看Installer的remove方法:

[--> installed.cpp]

static int do_remove(char **arg, char reply[REPLY_MAX] __unused)
{
    return uninstall(parse_null(arg[0]), arg[1], atoi(arg[2])); /* uuid, pkgname, userid */
}

int uninstall(const char *uuid, const char *pkgname, userid_t userid)
{
    std::string _pkgdir(create_data_user_package_path(uuid, userid, pkgname));
    const char* pkgdir = _pkgdir.c_str();

    remove_profile_file(pkgname);

    /* delete contents AND directory, no exceptions */
    return delete_dir_contents(pkgdir, 1, NULL);
}
int delete_dir_contents(const char *pathname,
                        int also_delete_dir,
                        int (*exclusion_predicate)(const char*, const int))
{
    int res = 0;
    DIR *d;

    d = opendir(pathname);
    if (d == NULL) {
        ALOGE("Couldn't opendir %s: %s\n", pathname, strerror(errno));
        return -errno;
    }
    res = _delete_dir_contents(d, exclusion_predicate);
    closedir(d);
    if (also_delete_dir) {
        if (rmdir(pathname)) {
            ALOGE("Couldn't rmdir %s: %s\n", pathname, strerror(errno));
            res = -1;
        }
    }
    return res;
}

底层在接收到"remove"请求后,调用do_remove()函数,uninstall()函数通过create_data_user_package_path()函数拼接地址,如data/data/packageName的文件路径名,然后调用delete_dir_contents来删除文件内容以及目录,/data/data/packageName的文件其实都是符号链接,所以_delete_dir_contents的实现中都是调用unlinkat去删除这些符号链接。

第三阶段:执行包清除操作

schedulePackageCleaning(packageName, UserHandle.USER_ALL, true);向mHandler发送了一个消息START_CLEANING_PACKAGE,来看一下该消息的handler处理,

[--> PackageManagerService.java]

case START_CLEANING_PACKAGE: {
    Process.setThreadPriority(Process.THREAD_PRIORITY_DEFAULT);
    final String packageName = (String)msg.obj;
    final int userId = msg.arg1;
    final boolean andCode = msg.arg2 != 0;
    synchronized (mPackages) {
        if (userId == UserHandle.USER_ALL) {
            int[] users = sUserManager.getUserIds();
            for (int user : users) {
                mSettings.addPackageToCleanLPw(
                        new PackageCleanItem(user, packageName, andCode));
            }
        } else {
            mSettings.addPackageToCleanLPw(
                    new PackageCleanItem(userId, packageName, andCode));
        }
    }
    Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
    startCleaningPackages();
} break;

当前的userId == UserHandle.USER_ALL条件成立,通过mSettings调用addPackageToCleanLPw()函数将将要删除的包名添加到Settings中,接下来看startCleaningPackages()函数,PMS会调用ContainService的函数去删除/storage/sdcard0/Android/data和/storage/sdcard0/Android/media下面与package相关的文件,清除app挂载设备的信息。

第四阶段,从Settings中删除PackageSettings的信息

该阶段通过调用mSettings.removePackageLPw(packageName)函数去清除设置,来看一下具体信息:

[--> PackageManagerService.java]

int removePackageLPw(String name) {
    final PackageSetting p = mPackages.get(name);
    if (p != null) {
        mPackages.remove(name);
        if (p.sharedUser != null) {
            p.sharedUser.removePackage(p);
            if (p.sharedUser.packages.size() == 0) {
                mSharedUsers.remove(p.sharedUser.name);
                removeUserIdLPw(p.sharedUser.userId);
                return p.sharedUser.userId;
            }
        } else {
            removeUserIdLPw(p.appId);
            return p.appId;
        }
    }
    return -1;
}

removePackageLPw首先从mPackages这个map中删除PackageSettings信息,如果不存在sharedUser,则从mUserIds这个数组中删除对应的Package UID信息;如果存在sharedUser,则首先检查这个sharedUser是否所有的package都已经被卸载了,如果都被卸载了,这个sharedUser也就可以删除,调用removeUserIdLPw()方法。

第五阶段,更新权限信息

removePackageDataLI调用updatePermissionsLPw去检查mPermissionTrees和mPermissions两个数组中的权限是否是被删除的Package提供,如果有,则删除。

[--> PackageManagerService.java]

private void updatePermissionsLPw(String changingPkg, PackageParser.Package pkgInfo,
        int flags) {
    final String volumeUuid = (pkgInfo != null) ? getVolumeUuidForPackage(pkgInfo) : null;
    updatePermissionsLPw(changingPkg, pkgInfo, volumeUuid, flags);
}

private void updatePermissionsLPw(String changingPkg,
        PackageParser.Package pkgInfo, String replaceVolumeUuid, int flags) {
    // Make sure there are no dangling permission trees.
    Iterator<BasePermission> it = mSettings.mPermissionTrees.values().iterator();
    while (it.hasNext()) {
        
		......
	
    }

    // Make sure all dynamic permissions have been assigned to a package,
    // and make sure there are no dangling permissions.
    it = mSettings.mPermissions.values().iterator();
    while (it.hasNext()) {
        
		......
		
    }

    // 更新所有包权限,特别是保护系统应用的权限
    if ((flags&UPDATE_PERMISSIONS_ALL) != 0) {
        for (PackageParser.Package pkg : mPackages.values()) {
            if (pkg != pkgInfo) {
                // Only replace for packages on requested volume
                final String volumeUuid = getVolumeUuidForPackage(pkg);
                final boolean replace = ((flags & UPDATE_PERMISSIONS_REPLACE_ALL) != 0)
                        && Objects.equals(replaceVolumeUuid, volumeUuid);
                grantPermissionsLPw(pkg, replace, changingPkg);
            }
        }
    }

    if (pkgInfo != null) {
        // Only replace for packages on requested volume
        final String volumeUuid = getVolumeUuidForPackage(pkgInfo);
        final boolean replace = ((flags & UPDATE_PERMISSIONS_REPLACE_PKG) != 0)
                && Objects.equals(replaceVolumeUuid, volumeUuid);
        grantPermissionsLPw(pkgInfo, replace, changingPkg);
    }
}

1.1.3 删除资源文件

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

  • installFlags 安装标志,如PackageManager.INSTALL_EXTERNAL
  • codePath 代码路径
  • resourcePath 资源路径
  • instructionSets appDex指令集

[--> PackageManagerService.java]

private InstallArgs createInstallArgsForExisting(int installFlags, String codePath,
        String resourcePath, String[] instructionSets) {
    final boolean isInAsec;
    if (installOnExternalAsec(installFlags)) {
        /* Apps on SD card are always in ASEC containers. */
        isInAsec = true;
    } else if (installForwardLocked(installFlags)
            && !codePath.startsWith(mDrmAppPrivateInstallDir.getAbsolutePath())) {
        /*
         * Forward-locked apps are only in ASEC containers if they're the
         * new style
         */
        isInAsec = true;
    } else {
        isInAsec = false;
    }

    if (isInAsec) {
        return new AsecInstallArgs(codePath, instructionSets,
                installOnExternalAsec(installFlags), installForwardLocked(installFlags));
    } else {
        return new FileInstallArgs(codePath, resourcePath, instructionSets);
    }
}

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

[--> PackageManagerService.java]

void cleanUpResourcesLI() {
    // Try enumerating all code paths before deleting
    List<String> allCodePaths = Collections.EMPTY_LIST;
    if (codeFile != null && codeFile.exists()) {
        try {
            final PackageLite pkg = PackageParser.parsePackageLite(codeFile, 0);
            allCodePaths = pkg.getAllCodePaths();
        } catch (PackageParserException e) {
            // Ignored; we tried our best
        }
    }

    cleanUp();
    removeDexFiles(allCodePaths, instructionSets);
}

cleanUpResourcesLI()函数中中主要完成两个工作:

  • 首先调用cleanUp()函数去删除code、resource以及library文件。
  • 然后调用removeDexFiles()函数去删除对应的dex文件。

[--> PackageManagerService.java]

private boolean cleanUp() {
    if (codeFile == null || !codeFile.exists()) {
        return false;
    }

    if (codeFile.isDirectory()) {
        mInstaller.rmPackageDir(codeFile.getAbsolutePath());
    } else {
		// 删除代码
        codeFile.delete();
    }

    if (resourceFile != null && !FileUtils.contains(codeFile, resourceFile)) {
		// 删除资源
        resourceFile.delete();
    }

    return true;
}
private void removeDexFiles(List<String> allCodePaths, String[] instructionSets) {
    if (!allCodePaths.isEmpty()) {
        if (instructionSets == null) {
            throw new IllegalStateException("instructionSet == null");
        }
        String[] dexCodeInstructionSets = getDexCodeInstructionSets(instructionSets);
        for (String codePath : allCodePaths) {
            for (String dexCodeInstructionSet : dexCodeInstructionSets) {
				// 移除dex,/data/dalvik-cache文件
                int retCode = mInstaller.rmdex(codePath, dexCodeInstructionSet);
                if (retCode < 0) {
                    Slog.w(TAG, "Couldn't remove dex file for package: "
                            + " at location " + codePath + ", retcode=" + retCode);
                    // we don't consider this to be a failure of the core package deletion
                }
            }
        }
    }
}

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

1.1.4 卸载过程总结

通过对android应用源码级卸载应用过程的分析,梳理出对应的每一步做了什么工作,经历了什么过程,更新了什么信息。若是想对卸载的细节有更深入的了解还需仔细品读卸载的每一句代码的真正含意以及造成的影响。我们来看一下系统结构图:

系统结构图

从系统结构图中可以看出分析的主线,当UninstallAppProgress.java类调用deletePackageAsUser()函数后即开始了我们的卸载之旅。首先通过deletePackage()方法开启线程,进行卸载应用。这里根据流程图的层级一一进行说明:

  1. deletePackageX()函数层级,调用deletePackageLI()函数,然后发送卸载广播,最后通过OutInfo进行代码和资源文件的真正删除。
  2. deletePackageLI()函数层级,通过killApplication()函数停止运行的应用,调用deleteInstalledPackageLI()函数。
  3. deleteInstalledPackageLI()函数层级,通过removePackageDataLI()函数删除内部结构和数据,通过deleteCodeAndResources()函数删除资源和代码。
  4. removePackageDataLI()函数层级,分为五个阶段,具体可以参考文中介绍,createInstallArgsForExisting()函数层级为删除代码和资源,其实只是创建了FileInstallArgs结构,具体删除操作由deletePackageX()函数层级完成。

到此卸载流程介绍完毕。

1.2 JNI实现应用监听自身卸载

作为被卸载的应用自然是无法收到系统广播的,那么只能通过开启新的进程来监测文件变化或删除情况,若是发现应用被卸载,将记录上传至服务端即可将进行exit()掉。这里我们选择卸载监听/data/data/packagename/ 这个目录,因为在卸载的时候卸载程序会删除监听目录的,而fork出来的守护进程当发现自身应用的目录被卸载程序删除了也就是卸载了,这个时候调用 inotify_handle这个函数,然后调用am命令启动浏览器(也可以不使用),调用自己需要调用界面。当然,在调用am指令以后,记得自身守护进程的使命也完成了,需要exit()退出一下。

这里下篇讲。

参考

csdn-Android PackageManagerService分析三:卸载APK

Linux inotify功能及实现原理

Linux inotify

转载于:https://my.oschina.net/feiyangxiaomi/blog/746563

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值