Android 11 PackageManagerService源码分析(二):Packages.xml详解

2024-08-25 13:08

本文主要是介绍Android 11 PackageManagerService源码分析(二):Packages.xml详解,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

1、开篇

在上一篇文章中提到Settings类会在PackageManagerService启动过程中对packages.xml等一些列xml文件进行解析。那么有以下问题:

  1. 这些文件记录了什么内容?
  2. 为什么需要这些文件?

让我们一起通过阅读源码解决这些问题吧。

2、packages.xml文件详解

要在真机上拿到packages.xml殊为不易,所以我这里是在模拟器上通过adb命令拉取了一份:

adb pull /data/system/packages.xml

文件内容精简后如下:

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<packages><version sdkVersion="24" databaseVersion="3" fingerprint="Android/sdk_phone_x86/generic_x86:7.0/NYC/4174735:userdebug/test-keys" /><version volumeUuid="primary_physical" sdkVersion="24" databaseVersion="3" fingerprint="Android/sdk_phone_x86/generic_x86:7.0/NYC/4174735:userdebug/test-keys" /><permission-trees /><permissions><item name="android.permission.REAL_GET_TASKS" package="android" protection="18" /><item name="android.permission.ACCESS_CACHE_FILESYSTEM" package="android" protection="18" />...</permissions><package name="com.android.providers.media" codePath="/system/priv-app/MediaProvider" nativeLibraryPath="/system/priv-app/MediaProvider/lib" primaryCpuAbi="x86" publicFlags="944291397" privateFlags="8" ft="15d38697a58" it="15d38697a58" ut="15d38697a58" version="800" sharedUserId="10010"><sigs count="1"><cert index="2" key="308..." /></sigs><perms><item name="android.permission.ACCESS_CACHE_FILESYSTEM" granted="true" flags="0" />...</perms><proper-signing-keyset identifier="4" /></package>...<shared-user name="com.android.emergency.uid" userId="10011"><sigs count="1"><cert index="1" /></sigs><perms><item name="android.permission.MANAGE_USERS" granted="true" flags="0" /></perms></shared-user><keyset-settings version="1"><keys><public-key identifier="1" value="MIIBIDAN..." /><public-key identifier="2" value="MIIBI..." /><public-key identifier="3" value="MIIBI..." /><public-key identifier="4" value="MIIBID..." /></keys><keysets><keyset identifier="1"><key-id identifier="1" /></keyset><keyset identifier="2"><key-id identifier="2" /></keyset><keyset identifier="3"><key-id identifier="3" /></keyset><keyset identifier="4"><key-id identifier="4" /></keyset></keysets><lastIssuedKeyId value="4" /><lastIssuedKeySetId value="4" /></keyset-settings>
</packages>

可以看到,packages.xml主要记录了以下几方面的信息:

  1. 权限信息,permission-trees标签和permissions标签。这里记录的是系统里所有的权限条目
  2. 安装的App信息,包括系统App和用户自行安装的App,package和updated-package标签。其中package标签用户记录一般App的信息;而updated-package通常用于被用户手动升级了的系统App,比如说手机自带了计算器App,这个自带的App的版本是1.0,而厂商又在它的应用商店发布了计算器2.0,当我们在应用商店更新成2.0版本的时候,在系统分区和用户安装分区会各存在一个计算器App。这也是为什么我们在系统应用管理界面删除更新后的计算器2.0后,手机上还是会有计算器1.0版本,而不是彻底消失的原因。
    package标签的属性记录了包名、代码路径、版本等各项信息。另外,package还会有一些子标签,sigs记录App的签名信息,perms记录App的权限信息,注意这里的权限是manifest中声明的权限而不是已经授予的权限。
  3. 共享用户信息,shared-user标签。一般来说一个Android系统会为每一个App分配一个user id,但是我们也可以在manifest元素中指定android:sharedUserId属性,使多个App具有同样的user id从而可以共享数据等等。但是除了系统App,Android强烈建议我们不要使用它,并且在未来的版本可能会被移除,所以了解即可。
  4. 签名信息,keyset-settings标签,记录的是应用签名的公钥。

packages.xml也不止以上这些标签,具体可以参考Settings类对packages.xml的解析:

boolean readLPw(@NonNull List<UserInfo> users) {FileInputStream str = null;if (mBackupSettingsFilename.exists()) {try {str = new FileInputStream(mBackupSettingsFilename);mReadMessages.append("Reading from backup settings file\n");PackageManagerService.reportSettingsProblem(Log.INFO,"Need to read from backup settings file");if (mSettingsFilename.exists()) {// 两者都存在的时候,说明上次更新packages.xml时发生了异常Slog.w(PackageManagerService.TAG, "Cleaning up settings file "+ mSettingsFilename);mSettingsFilename.delete();}} catch (java.io.IOException e) {// We'll try for the normal settings file.}}mPendingPackages.clear();mPastSignatures.clear();mKeySetRefs.clear();mInstallerPackages.clear();try {if (str == null) {if (!mSettingsFilename.exists()) {mReadMessages.append("No settings file found\n");PackageManagerService.reportSettingsProblem(Log.INFO,"No settings file; creating initial state");// It's enough to just touch version details to create them// with default valuesfindOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent();findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent();return false;}str = new FileInputStream(mSettingsFilename);}XmlPullParser parser = Xml.newPullParser();parser.setInput(str, StandardCharsets.UTF_8.name());int type;while ((type = parser.next()) != XmlPullParser.START_TAG&& type != XmlPullParser.END_DOCUMENT) {;}if (type != XmlPullParser.START_TAG) {mReadMessages.append("No start tag found in settings file\n");PackageManagerService.reportSettingsProblem(Log.WARN,"No start tag found in package manager settings");Slog.wtf(PackageManagerService.TAG,"No start tag found in package manager settings");return false;}int outerDepth = parser.getDepth();while ((type = parser.next()) != XmlPullParser.END_DOCUMENT&& (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {continue;}String tagName = parser.getName();if (tagName.equals("package")) {readPackageLPw(parser);} else if (tagName.equals("permissions")) {mPermissions.readPermissions(parser);} else if (tagName.equals("permission-trees")) {mPermissions.readPermissionTrees(parser);} else if (tagName.equals("shared-user")) {readSharedUserLPw(parser);} else if (tagName.equals("preferred-packages")) {// 不再使用了,所以不做任何操作} else if (tagName.equals("preferred-activities")) {// Upgrading from old single-user implementation;// these are the preferred activities for user 0.readPreferredActivitiesLPw(parser, 0);} else if (tagName.equals(TAG_PERSISTENT_PREFERRED_ACTIVITIES)) {// TODO: check whether this is okay! as it is very// similar to how preferred-activities are treatedreadPersistentPreferredActivitiesLPw(parser, 0);} else if (tagName.equals(TAG_CROSS_PROFILE_INTENT_FILTERS)) {// TODO: check whether this is okay! as it is very// similar to how preferred-activities are treatedreadCrossProfileIntentFiltersLPw(parser, 0);} else if (tagName.equals(TAG_DEFAULT_BROWSER)) {readDefaultAppsLPw(parser, 0);} else if (tagName.equals("updated-package")) {// 注意这里,updated-package记录的package视为disabledreadDisabledSysPackageLPw(parser);} else if (tagName.equals("renamed-package")) {String nname = parser.getAttributeValue(null, "new");String oname = parser.getAttributeValue(null, "old");if (nname != null && oname != null) {mRenamedPackages.put(nname, oname);}} else if (tagName.equals("restored-ivi")) {readRestoredIntentFilterVerifications(parser);} else if (tagName.equals("last-platform-version")) {// Upgrade from older XML schemafinal VersionInfo internal = findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL);final VersionInfo external = findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL);internal.sdkVersion = XmlUtils.readIntAttribute(parser, "internal", 0);external.sdkVersion = XmlUtils.readIntAttribute(parser, "external", 0);internal.fingerprint = external.fingerprint =XmlUtils.readStringAttribute(parser, "fingerprint");} else if (tagName.equals("database-version")) {// Upgrade from older XML schemafinal VersionInfo internal = findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL);final VersionInfo external = findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL);internal.databaseVersion = XmlUtils.readIntAttribute(parser, "internal", 0);external.databaseVersion = XmlUtils.readIntAttribute(parser, "external", 0);} else if (tagName.equals("verifier")) {final String deviceIdentity = parser.getAttributeValue(null, "device");try {mVerifierDeviceIdentity = VerifierDeviceIdentity.parse(deviceIdentity);} catch (IllegalArgumentException e) {Slog.w(PackageManagerService.TAG, "Discard invalid verifier device id: "+ e.getMessage());}} else if (TAG_READ_EXTERNAL_STORAGE.equals(tagName)) {final String enforcement = parser.getAttributeValue(null, ATTR_ENFORCEMENT);mReadExternalStorageEnforced ="1".equals(enforcement) ? Boolean.TRUE : Boolean.FALSE;} else if (tagName.equals("keyset-settings")) {mKeySetManagerService.readKeySetsLPw(parser, mKeySetRefs);} else if (TAG_VERSION.equals(tagName)) {final String volumeUuid = XmlUtils.readStringAttribute(parser,ATTR_VOLUME_UUID);final VersionInfo ver = findOrCreateVersion(volumeUuid);ver.sdkVersion = XmlUtils.readIntAttribute(parser, ATTR_SDK_VERSION);ver.databaseVersion = XmlUtils.readIntAttribute(parser, ATTR_DATABASE_VERSION);ver.fingerprint = XmlUtils.readStringAttribute(parser, ATTR_FINGERPRINT);} else {Slog.w(PackageManagerService.TAG, "Unknown element under <packages>: "+ parser.getName());XmlUtils.skipCurrentTag(parser);}}str.close();} catch (XmlPullParserException e) {...} catch (java.io.IOException e) {...}...return true;
}

3、Packages.xml的作用

在上篇文章中我们可以看到,packages.xml文件最终被解析和保存到了Settings的mPackages属性里了。来看一下PMS的构造方法里,它都发挥了什么作用吧

public PackageManagerService(Injector injector, boolean onlyCore, boolean factoryTest) {...mSettings = injector.getSettings();...t.traceBegin("addSharedUsers");// 创建一些列系统shared user idmSettings.addSharedUserLPw("android.uid.system", Process.SYSTEM_UID,ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);mSettings.addSharedUserLPw("android.uid.phone", RADIO_UID,ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);...// CHECKSTYLE:OFF IndentationChecksynchronized (mInstallLock) {// writersynchronized (mLock) {...// 读取packages.xml或packages-backup.xml,两者的格式一样,上次更新packages.xml出现异常的时候才会出现packages-backup.xmlt.traceBegin("read user settings");mFirstBoot = !mSettings.readLPw(mInjector.getUserManagerInternal().getUsers(false));t.traceEnd();// 清除代码路径不存在的packagefinal int packageSettingCount = mSettings.mPackages.size();for (int i = packageSettingCount - 1; i >= 0; i--) {PackageSetting ps = mSettings.mPackages.valueAt(i);if (!isExternal(ps) && (ps.codePath == null || !ps.codePath.exists())&& mSettings.getDisabledSystemPkgLPr(ps.name) != null) {mSettings.mPackages.removeAt(i);mSettings.enableSystemPackageLPw(ps.name);}}if (!mOnlyCore && mFirstBoot) {requestCopyPreoptedFiles();}...// Save the names of pre-existing packages prior to scanning, so we can determine// which system packages are completely new due to an upgrade.// 在扫描之前保存预先存在的App的名称,以便确定哪些系统App由于升级完全新加的if (isDeviceUpgrading()) {mExistingPackages = new ArraySet<>(mSettings.mPackages.size());for (PackageSetting ps : mSettings.mPackages.values()) {mExistingPackages.add(ps.name);}}// 扫描系统App,这里代码省略...// Prune any system packages that no longer exist.final List<String> possiblyDeletedUpdatedSystemApps = new ArrayList<>();// Stub packages must either be replaced with full versions in the /data// partition or be disabled.final List<String> stubSystemApps = new ArrayList<>();if (!mOnlyCore) {...final Iterator<PackageSetting> psit = mSettings.mPackages.values().iterator();while (psit.hasNext()) {PackageSetting ps = psit.next();// 非系统App跳过if ((ps.pkgFlags & ApplicationInfo.FLAG_SYSTEM) == 0) {continue;}final AndroidPackage scannedPkg = mPackages.get(ps.name);if (scannedPkg != null) { // packages.xml和即时扫描的结果都存在这个package// 如果系统App扫描到了,而且是disabled状态,也就是被记录在updated-package标签里,把这个记录删除,以期使用用户安装的版本// 后面如果没有找到用户安装的版本,会恢复系统自带的版本if (mSettings.isDisabledSystemPackageLPr(ps.name)) {logCriticalInfo(Log.WARN,"Expecting better updated system app for " + ps.name+ "; removing system app.  Last known"+ " codePath=" + ps.codePathString+ ", versionCode=" + ps.versionCode+ "; scanned versionCode=" + scannedPkg.getLongVersionCode());removePackageLI(scannedPkg, true);mExpectingBetter.put(ps.name, ps.codePath);}continue;}// packages.xml中存在的package如果没有被扫描到执行接下来的代码if (!mSettings.isDisabledSystemPackageLPr(ps.name)) { // 不是disabled状态说明用户没有手动更新过,直接删除psit.remove();logCriticalInfo(Log.WARN, "System package " + ps.name+ " no longer exists; it's data will be wiped");// Assume package is truly gone and wipe residual permissions.mPermissionManager.updatePermissions(ps.name, null);// 真正删除代码和数据的操作会在后面执行} else {// 在disabled list里,判断代码路径是不是还存在,存在的话可能是升级的时候改了包名,不存在则可能删除了final PackageSetting disabledPs =mSettings.getDisabledSystemPkgLPr(ps.name);if (disabledPs.codePath == null || !disabledPs.codePath.exists()|| disabledPs.pkg == null) {possiblyDeletedUpdatedSystemApps.add(ps.name);} else {// 加到mExpectingBetter,以便后续扫描到对应的升级版本的时候继续保持系统版本disabled,而使用用户版本,没有扫描到则再处理是删除还是保留mExpectingBetter.put(disabledPs.name, disabledPs.codePath);}}}}final int cachedSystemApps = PackageCacher.sCachedPackageReadCount.get();// 移除那些没有package关联的shared user idmSettings.pruneSharedUsersLPw();...// 扫描用户安装的Appif (!mOnlyCore) {EventLog.writeEvent(EventLogTags.BOOT_PROGRESS_PMS_DATA_SCAN_START,SystemClock.uptimeMillis());scanDirTracedLI(sAppInstallDir, 0, scanFlags | SCAN_REQUIRE_KNOWN, 0,packageParser, executorService);}packageParser.close();List<Runnable> unfinishedTasks = executorService.shutdownNow();if (!unfinishedTasks.isEmpty()) {throw new IllegalStateException("Not all tasks finished before calling close: "+ unfinishedTasks);}if (!mOnlyCore) {// Remove disable package settings for updated system apps that were// removed via an OTA. If the update is no longer present, remove the// app completely. Otherwise, revoke their system privileges.// 系统升级中移除了App,如果App还存在于用户区(用户手动安装过新版本),剥夺App的系统级权限,否则完全删除for (int i = possiblyDeletedUpdatedSystemApps.size() - 1; i >= 0; --i) {final String packageName = possiblyDeletedUpdatedSystemApps.get(i);final AndroidPackage pkg = mPackages.get(packageName);final String msg;// remove from the disabled system list; do this first so any future// scans of this package are performed without this statemSettings.removeDisabledSystemPackageLPw(packageName);if (pkg == null) {// 这里仍然没找到扫描结果,直接删除msg = "Updated system package " + packageName+ " no longer exists; removing its data";// 真正删除代码和数据的操作会在后面执行} else {// 扫描到了,剥夺系统级权限msg = "Updated system package " + packageName+ " no longer exists; rescanning package on data";// NOTE: We don't do anything special if a stub is removed from the// system image. But, if we were [like removing the uncompressed// version from the /data partition], this is where it'd be done.// remove the package from the system and re-scan it without any// special privileges// 先删除,后重新扫描removePackageLI(pkg, true);try {final File codePath = new File(pkg.getCodePath());// 重新扫描scanPackageTracedLI(codePath, 0, scanFlags, 0, null);} catch (PackageManagerException e) {Slog.e(TAG, "Failed to parse updated, ex-system package: "+ e.getMessage());}}// 最终确认结果final PackageSetting ps = mSettings.mPackages.get(packageName);if (ps != null && mPackages.get(packageName) == null) {removePackageDataLIF(ps, null, null, 0, false);}logCriticalInfo(Log.WARN, msg);}/** Make sure all system apps that we expected to appear on* the userdata partition actually showed up. If they never* appeared, crawl back and revive the system version.*/// 确保应该在用户区出现的系统App存在,不存在则使用系统区的版本for (int i = 0; i < mExpectingBetter.size(); i++) {final String packageName = mExpectingBetter.keyAt(i);if (!mPackages.containsKey(packageName)) {final File scanFile = mExpectingBetter.valueAt(i);logCriticalInfo(Log.WARN, "Expected better " + packageName+ " but never showed up; reverting to system");@ParseFlags int reparseFlags = 0;@ScanFlags int rescanFlags = 0;for (int i1 = mDirsToScanAsSystem.size() - 1; i1 >= 0; i1--) {final ScanPartition partition = mDirsToScanAsSystem.get(i1);if (partition.containsPrivApp(scanFile)) {reparseFlags = systemParseFlags;rescanFlags = systemScanFlags | SCAN_AS_PRIVILEGED| partition.scanFlag;break;}if (partition.containsApp(scanFile)) {reparseFlags = systemParseFlags;rescanFlags = systemScanFlags | partition.scanFlag;break;}}if (rescanFlags == 0) {Slog.e(TAG, "Ignoring unexpected fallback path " + scanFile);continue;}mSettings.enableSystemPackageLPw(packageName);try {scanPackageTracedLI(scanFile, reparseFlags, rescanFlags, 0, null);} catch (PackageManagerException e) {Slog.e(TAG, "Failed to parse original system package: "+ e.getMessage());}}}...}mExpectingBetter.clear();...for (SharedUserSetting setting : mSettings.getAllSharedUsersLPw()) {// NOTE: We ignore potential failures here during a system scan (like// the rest of the commands above) because there's precious little we// can do about it. A settings error is reported, though.final List<String> changedAbiCodePath =applyAdjustedAbiToSharedUser(setting, null /*scannedPackage*/,mInjector.getAbiHelper().getAdjustedAbiForSharedUser(setting.packages, null /*scannedPackage*/));if (changedAbiCodePath != null && changedAbiCodePath.size() > 0) {for (int i = changedAbiCodePath.size() - 1; i >= 0; --i) {final String codePathString = changedAbiCodePath.get(i);try {mInstaller.rmdex(codePathString,getDexCodeInstructionSet(getPreferredInstructionSet()));} catch (InstallerException ignored) {}}}// Adjust seInfo to ensure apps which share a sharedUserId are placed in the same// SELinux domain.setting.fixSeInfoLocked();setting.updateProcesses();}// Now that we know all the packages we are keeping,// read and update their last usage times.mPackageUsage.read(mSettings.mPackages);...t.traceBegin("write settings");mSettings.writeLPr();...mSettings.setPermissionControllerVersion(getPackageInfo(mRequiredPermissionControllerPackage, 0,UserHandle.USER_SYSTEM).getLongVersionCode());...} // synchronized (mLock)} // synchronized (mInstallLock)...
}

这里删除了非常多的代码,只列出了关键性的。可以看到packages.xml的主要作用存储上一次启动时扫描和更新的结果,和本次启动扫描的结果进行比较,判断哪些该更新,哪些该删除。这就是它的主要作用。

这篇关于Android 11 PackageManagerService源码分析(二):Packages.xml详解的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!


原文地址:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.chinasem.cn/article/1105635

相关文章

使用Python将JSON,XML和YAML数据写入Excel文件

《使用Python将JSON,XML和YAML数据写入Excel文件》JSON、XML和YAML作为主流结构化数据格式,因其层次化表达能力和跨平台兼容性,已成为系统间数据交换的通用载体,本文将介绍如何... 目录如何使用python写入数据到Excel工作表用Python导入jsON数据到Excel工作表用

MySQL中的交叉连接、自然连接和内连接查询详解

《MySQL中的交叉连接、自然连接和内连接查询详解》:本文主要介绍MySQL中的交叉连接、自然连接和内连接查询,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、引入二、交php叉连接(cross join)三、自然连接(naturalandroid join)四

Go 语言中的select语句详解及工作原理

《Go语言中的select语句详解及工作原理》在Go语言中,select语句是用于处理多个通道(channel)操作的一种控制结构,它类似于switch语句,本文给大家介绍Go语言中的select语... 目录Go 语言中的 select 是做什么的基本功能语法工作原理示例示例 1:监听多个通道示例 2:带

mysql的基础语句和外键查询及其语句详解(推荐)

《mysql的基础语句和外键查询及其语句详解(推荐)》:本文主要介绍mysql的基础语句和外键查询及其语句详解(推荐),本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋... 目录一、mysql 基础语句1. 数据库操作 创建数据库2. 表操作 创建表3. CRUD 操作二、外键

Spring Boot项目部署命令java -jar的各种参数及作用详解

《SpringBoot项目部署命令java-jar的各种参数及作用详解》:本文主要介绍SpringBoot项目部署命令java-jar的各种参数及作用的相关资料,包括设置内存大小、垃圾回收... 目录前言一、基础命令结构二、常见的 Java 命令参数1. 设置内存大小2. 配置垃圾回收器3. 配置线程栈大小

鸿蒙中@State的原理使用详解(HarmonyOS 5)

《鸿蒙中@State的原理使用详解(HarmonyOS5)》@State是HarmonyOSArkTS框架中用于管理组件状态的核心装饰器,其核心作用是实现数据驱动UI的响应式编程模式,本文给大家介绍... 目录一、@State在鸿蒙中是做什么的?二、@Spythontate的基本原理1. 依赖关系的收集2.

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

在Android平台上实现消息推送功能

《在Android平台上实现消息推送功能》随着移动互联网应用的飞速发展,消息推送已成为移动应用中不可或缺的功能,在Android平台上,实现消息推送涉及到服务端的消息发送、客户端的消息接收、通知渠道(... 目录一、项目概述二、相关知识介绍2.1 消息推送的基本原理2.2 Firebase Cloud Me

Redis实现延迟任务的三种方法详解

《Redis实现延迟任务的三种方法详解》延迟任务(DelayedTask)是指在未来的某个时间点,执行相应的任务,本文为大家整理了三种常见的实现方法,感兴趣的小伙伴可以参考一下... 目录1.前言2.Redis如何实现延迟任务3.代码实现3.1. 过期键通知事件实现3.2. 使用ZSet实现延迟任务3.3

C语言函数递归实际应用举例详解

《C语言函数递归实际应用举例详解》程序调用自身的编程技巧称为递归,递归做为一种算法在程序设计语言中广泛应用,:本文主要介绍C语言函数递归实际应用举例的相关资料,文中通过代码介绍的非常详细,需要的朋... 目录前言一、递归的概念与思想二、递归的限制条件 三、递归的实际应用举例(一)求 n 的阶乘(二)顺序打印