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 values
                findOrCreateVersion(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 treated
                readPersistentPreferredActivitiesLPw(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 treated
                readCrossProfileIntentFiltersLPw(parser, 0);
            } else if (tagName.equals(TAG_DEFAULT_BROWSER)) {
                readDefaultAppsLPw(parser, 0);
            } else if (tagName.equals("updated-package")) {
                // 注意这里,updated-package记录的package视为disabled
                readDisabledSysPackageLPw(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 schema
                final 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 schema
                final 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 id
    mSettings.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 IndentationCheck
    synchronized (mInstallLock) {
    // writer
    synchronized (mLock) {
        ...

        // 读取packages.xml或packages-backup.xml,两者的格式一样,上次更新packages.xml出现异常的时候才会出现packages-backup.xml
        t.traceBegin("read user settings");
        mFirstBoot = !mSettings.readLPw(mInjector.getUserManagerInternal().getUsers(false));
        t.traceEnd();

        
        // 清除代码路径不存在的package
        final 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 id
        mSettings.pruneSharedUsersLPw();
        ...

        // 扫描用户安装的App
        if (!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 state
                mSettings.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的主要作用存储上一次启动时扫描和更新的结果,和本次启动扫描的结果进行比较,判断哪些该更新,哪些该删除。这就是它的主要作用。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Android PackageManagerserviceAndroid 系统中的一个服务,它负责管理应用程序包。PackageManagerservice 提供了许多功能,包括安装、卸载、更新和管理应用程序的权限等。下面对其功能进行详细解释: 1. 安装应用程序:PackageManagerservice 能够将应用程序的 APK 文件安装到设备中。当用户从 Google Play 商店或其他渠道下载应用程序时,PackageManagerservice 将应用程序文件正确安装到手机的存储区域中。 2. 卸载应用程序:PackageManagerservice 可以卸载设备上的应用程序。当用户要卸载一个应用程序时,PackageManagerservice 会检查该应用程序的包名及其相关文件,并将其从设备中删除。 3. 更新应用程序:PackageManagerservice 能够更新已经安装在设备上的应用程序。当用户从应用商店下载一个已经安装的应用程序的更新时,PackageManagerservice 将下载的新版本进行安装,以替换旧版本。 4. 管理应用程序权限:PackageManagerservice 配合 Android 系统的权限系统,控制应用程序的权限。用户给予应用程序某些权限后,PackageManagerservice 会验证和授权应用程序的权限使用,确保应用程序的安全性和隐私保护。 除此之外,PackageManagerservice 还负责处理应用程序的签名验证、计算应用程序的版本号和版本名、管理应用程序的组件(如活动、服务等),以及处理应用程序之间的相互调用等。 总之,Android PackageManagerserviceAndroid 系统中的一个重要服务,负责管理应用程序的安装、卸载、更新和权限管理等功能。它的存在确保了 Android 应用生态的安全和稳定运行。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值