Android 启动时应用的安装解析过程《一》

应用对于Android系统来说至关重要,系统会有几个时机对APP进行解析,一个是APK安装的时候会进行解析,还有一个就是系统在重启之后会进行解析,这里就简单的记录一下重启的时候APK的解析过程。

一、SystemServer

系统在启动之后从内核层启动第一个用户控件Init进程,再通过Init进程启动系统中第一个java 进程zygote,随之zygote fock出SystemServer,SystemServer再开始启动一些列系统服务,其中就包括PackageMangerService,在startBootstrapServices函数中:

public final void startBootstrapServices(TimingsTraceAndSlog timingsTraceAndSlog) {
.....
    // 先调用了PackageManagerService的main函数
 	Pair<PackageManagerService, IPackageManager> main = PackageManagerService.main(context, installer, domainVerificationService, z, this.mOnlyCore);
 	// main 函数创建了PackageManagerService 和IPackageManager 的实例
    this.mPackageManagerService = (PackageManagerService) main.first;
    IPackageManager iPackageManager = (IPackageManager) main.second;
    Watchdog.getInstance().resumeWatchingCurrentThread("packagemanagermain");
    // 该方法将在 BaseDexClassLoader 中安装一个报告器,同时还将强制报告系统服务器已加载的任何 dex 文件
    SystemServerDexLoadReporter.configureSystemServerDexReporter(iPackageManager);
    this.mFirstBoot = this.mPackageManagerService.isFirstBoot();
    this.mPackageManager = this.mSystemContext.getPackageManager();

二、PackageManagerService

在SystemServer中是先调用PackageManagerService的main函数来初始化它自己的,来看看它做了什么:

public static Pair<PackageManagerService, IPackageManager> main(Context context,
            Installer installer, @NonNull DomainVerificationService domainVerificationService,
            boolean factoryTest, boolean onlyCore) {
		PackageManagerServiceCompilerMapping.checkProperties();
        final TimingsTraceAndSlog t = new TimingsTraceAndSlog(TAG + "Timing",
                Trace.TRACE_TAG_PACKAGE_MANAGER);
        t.traceBegin("create package manager");
        final PackageManagerTracedLock lock = new PackageManagerTracedLock();
        final Object installLock = new Object();
        // 创建自带handler的线程
        HandlerThread backgroundThread = new ServiceThread("PackageManagerBg",
                Process.THREAD_PRIORITY_BACKGROUND, true /*allowIo*/);
        backgroundThread.start();
        Handler backgroundHandler = new Handler(backgroundThread.getLooper());
        // 创建PackageManagerServiceInjector 用来持有管理其他一系列重要的类实例
		PackageManagerServiceInjector injector = new PackageManagerServiceInjector(
                context, lock, installer, installLock, new PackageAbiHelperImpl(),
                backgroundHandler,
                SYSTEM_PARTITIONS,
                // 解析compnent的
                (i, pm) -> new ComponentResolver(i.getUserManagerService(), pm.mUserNeedsBadging),
                // 权限管理服务
                (i, pm) -> PermissionManagerService.create(context,
                        i.getSystemConfig().getAvailableFeatures()),
                // 用户管理服务
                (i, pm) -> new UserManagerService(context, pm,
                        new UserDataPreparer(installer, installLock, context, onlyCore),
                        lock),
               // 系统数据库,用来访问系统数据库的
                (i, pm) -> new Settings(Environment.getDataDirectory(),
                        RuntimePermissionsPersistence.createInstance(),
                        i.getPermissionManagerServiceInternal(),
                        domainVerificationService, backgroundHandler, lock),
                (i, pm) -> AppsFilterImpl.create(i,
                        i.getLocalService(PackageManagerInternal.class)),
                 // compat服务用来管理一些系统特性
                (i, pm) -> (PlatformCompat) ServiceManager.getService("platform_compat"),
                // 系统配置服务,预置的系统的权限、硬件feature还有其他的一些配置在这里面解析
                (i, pm) -> SystemConfig.getInstance(),
                // 在软件包上运行 dexopt 命令的辅助类。
                (i, pm) -> new PackageDexOptimizer(i.getInstaller(), i.getInstallLock(),
                        i.getContext(), "*dexopt*"),
                // dex 行为管理类会保存所有包的dex位置
                (i, pm) -> new DexManager(i.getContext(), i.getPackageDexOptimizer(),
                        i.getInstaller(), i.getInstallLock()),
                (i, pm) -> new ArtManagerService(i.getContext(), i.getInstaller(),
                        i.getInstallLock()),
                (i, pm) -> ApexManager.getInstance(),
                (i, pm) -> new ViewCompiler(i.getInstallLock(), i.getInstaller()),
                (i, pm) -> (IncrementalManager)
                        i.getContext().getSystemService(Context.INCREMENTAL_SERVICE),
                (i, pm) -> new DefaultAppProvider(() -> context.getSystemService(RoleManager.class),
                        () -> LocalServices.getService(UserManagerInternal.class)),
                (i, pm) -> new DisplayMetrics(),
                // 包解析类
                (i, pm) -> new PackageParser2(pm.mSeparateProcesses, pm.mOnlyCore,
                        i.getDisplayMetrics(), pm.mCacheDir,
                        pm.mPackageParserCallback) /* scanningCachingPackageParserProducer */,
                 (i, pm) -> new PackageParser2(pm.mSeparateProcesses, pm.mOnlyCore,
                        i.getDisplayMetrics(), null,
                        pm.mPackageParserCallback) /* scanningPackageParserProducer */,
                (i, pm) -> new PackageParser2(pm.mSeparateProcesses, false, i.getDisplayMetrics(),
                        null, pm.mPackageParserCallback) /* preparingPackageParserProducer */,
                // Prepare a supplier of package parser for the staging manager to parse apex file
                // during the staging installation.
                // 包安装服务
                (i, pm) -> new PackageInstallerService(
                        i.getContext(), pm, i::getScanningPackageParser),
                (i, pm, cn) -> new InstantAppResolverConnection(
                        i.getContext(), cn, Intent.ACTION_RESOLVE_INSTANT_APP_PACKAGE),
                (i, pm) -> new ModuleInfoProvider(i.getContext()),
                (i, pm) -> LegacyPermissionManagerService.create(i.getContext()),
                (i, pm) -> domainVerificationService,
                (i, pm) -> {
                    HandlerThread thread = new ServiceThread(TAG,
                            Process.THREAD_PRIORITY_DEFAULT, true /*allowIo*/);
                    thread.start();
                    return new PackageHandler(thread.getLooper(), pm);
                },
                 new DefaultSystemWrapper(),
                LocalServices::getService,
                context::getSystemService,
                (i, pm) -> new BackgroundDexOptService(i.getContext(), i.getDexManager(), pm),
                (i, pm) -> IBackupManager.Stub.asInterface(ServiceManager.getService(
                        Context.BACKUP_SERVICE)),
                (i, pm) -> new SharedLibrariesImpl(pm, i));
                if (Build.VERSION.SDK_INT <= 0) {
            Slog.w(TAG, "**** ro.build.version.sdk not set!");
        }
        // 初始化PackageManagerService
        PackageManagerService m = new PackageManagerService(injector, onlyCore, factoryTest, PackagePartitions.FINGERPRINT, Build.IS_ENG, Build.IS_USERDEBUG, Build.VERSION.SDK_INT, Build.VERSION.INCREMENTAL);
        t.traceEnd(); // "create package manager"
        final CompatChange.ChangeListener selinuxChangeListener = packageName -> {
            synchronized (m.mInstallLock) {
                final Computer snapshot = m.snapshotComputer();
                final PackageStateInternal packageState =
                        snapshot.getPackageStateInternal(packageName);
                if (packageState == null) {
                    Slog.e(TAG, "Failed to find package setting " + packageName);
                    return;
                }
                 AndroidPackage pkg = packageState.getPkg();
                SharedUserApi sharedUser = snapshot.getSharedUser(
                        packageState.getSharedUserAppId());
                String oldSeInfo = AndroidPackageUtils.getSeInfo(pkg, packageState);
                
                if (pkg == null) {
                    Slog.e(TAG, "Failed to find package " + packageName);
                    return;
                }
                final String newSeInfo = SELinuxMMAC.getSeInfo(pkg, sharedUser,
                        m.mInjector.getCompatibility());
                
                if (!newSeInfo.equals(oldSeInfo)) {
                    Slog.i(TAG, "Updating seInfo for package " + packageName + " from: "
                            + oldSeInfo + " to: " + newSeInfo);
                    m.commitPackageStateMutation(null, packageName,
                            state -> state.setOverrideSeInfo(newSeInfo));
                    m.mAppDataHelper.prepareAppDataAfterInstallLIF(pkg);
                }
            }
        };
		//监听selinux的一些变化
		injector.getCompatibility().registerListener(SELinuxMMAC.SELINUX_LATEST_CHANGES,
                selinuxChangeListener);
        injector.getCompatibility().registerListener(SELinuxMMAC.SELINUX_R_CHANGES,
                selinuxChangeListener);
        // ota完或者第一次开机为所有用户安装其被
        m.installAllowlistedSystemPackages();
        // 初始化 IPackageManagerImpl 
        IPackageManagerImpl iPackageManager = m.new IPackageManagerImpl();
        ServiceManager.addService("package", iPackageManager);
        // 初始化PackageManagerNative 
        final PackageManagerNative pmn = new PackageManagerNative(m);
        ServiceManager.addService("package_native", pmn);
        LocalManagerRegistry.addManager(PackageManagerLocal.class, m.new PackageManagerLocalImpl());
        return Pair.create(m, iPackageManager);
    }

简单的介绍了一下main,下面重头戏在PackageManagerService的初始化过程,它总共有两个构造函数,注意一个只做了赋值,我们这里调用的不是这个构造函数而是另一个

public PackageManagerService(PackageManagerServiceInjector injector, boolean onlyCore,
            boolean factoryTest, final String buildFingerprint, final boolean isEngBuild,
            final boolean isUserDebugBuild, final int sdkVersion, final String incrementalVersion) {
	mIsEngBuild = isEngBuild;
	......
	// 这里部分主要是给PackageMangerService的一些属性赋值
	......
	// CHECKSTYLE:ON IndentationCheck
    t.traceEnd();
	t.traceBegin("addSharedUsers");
	// 注册各种UID
    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);
    mSettings.addSharedUserLPw("android.uid.log", LOG_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.nfc", NFC_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.bluetooth", BLUETOOTH_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.shell", SHELL_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.se", SE_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.networkstack", NETWORKSTACK_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    mSettings.addSharedUserLPw("android.uid.uwb", UWB_UID,
                ApplicationInfo.FLAG_SYSTEM, ApplicationInfo.PRIVATE_FLAG_PRIVILEGED);
    t.traceEnd();
    ......
    // 又是一堆初始化赋值
    ......
    // 从systemconfig获取对于机器来说可用的硬件feature,可以在这里之前对feature进行修改
    t.traceBegin("get system config");
    SystemConfig systemConfig = injector.getSystemConfig();
    mAvailableFeatures = systemConfig.getAvailableFeatures();
    t.traceEnd();
    ......
    // 这里是读取在mac_permissions.xml下面配置的一些se策略可以有效的限制一些应用的权限
    SELinuxMMAC.readInstallPolicy();
    ......
    final VersionInfo ver = mSettings.getInternalVersion();
    // 指纹信息是否有更新
    mIsUpgrade =
            !buildFingerprint.equals(ver.fingerprint);
    if (mIsUpgrade) {
           PackageManagerServiceUtils.logCriticalInfo(Log.INFO, "Upgrading from "
                        + ver.fingerprint + " to " + PackagePartitions.FINGERPRINT);
   }
  
   mInitAppsHelper = new InitAppsHelper(this, mApexManager, mInstallPackageHelper,
                mInjector.getSystemPartitions());

    // when upgrading from pre-M, promote system app permissions from install to runtime
    // 是否从M升上来
    mPromoteSystemApps =
                    mIsUpgrade && ver.sdkVersion <= Build.VERSION_CODES.LOLLIPOP_MR1;

   // When upgrading from pre-N, we need to handle package extraction like first boot,
   // as there is no profiling data available.
   // 是否从N 升上来
   mIsPreNUpgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.N;
   // 是否从n mr升上来
   mIsPreNMR1Upgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.N_MR1;
  // 是否从Q升上来
   mIsPreQUpgrade = mIsUpgrade && ver.sdkVersion < Build.VERSION_CODES.Q;
   ......
   // 根据不同的版本类型创建缓存目录,如果已存在则不创建
   mCacheDir = PackageManagerServiceUtils.preparePackageParserCache(
                    mIsEngBuild, mIsUserDebugBuild, mIncrementalVersion);

   final int[] userIds = mUserManager.getUserIds();
   PackageParser2 packageParser = mInjector.getScanningCachingPackageParser();
   // 初始化安装系统级APP
   mOverlayConfig = mInitAppsHelper.initSystemApps(packageParser, packageSettings, userIds,
                    startTime);
   // 初始化安装非系统级APP
   mInitAppsHelper.initNonSystemApps(packageParser, userIds, startTime);
   packageParser.close();
   ......
}

可以从上面PackageManagerService的初始化可以看到,机器开机的时候安装APP的任务是交给InitAppsHelper这个类来做的,两个函数一个initSystemApps一个initNonSystemApps,分别是初始化系统app和非系统app,两个函数都是对系统中的应用进行安装解析,区别就是针对系统app还是非系统app,我们先从系统app来看看。

三、InitAppsHelper

public OverlayConfig initSystemApps(PackageParser2 packageParser,
            WatchedArrayMap<String, PackageSetting> packageSettings,
            int[] userIds, long startTime) 

initSystemApps有四个参数,看下其中比较重要的两个参数怎么来的

1、PackageParser2

第一个参数实在PackageManagerService初始化的时候从mInjector获取的,而这里的PackageParser2是在PackaManagerService的main函数初始化的

PackageParser2 packageParser = mInjector.getScanningCachingPackageParser();

2、WatchedArrayMap<String, PackageSetting>

首先这个参数是通过PackageMangerService的成员变量mSettings获取:

final WatchedArrayMap<String, PackageSetting> packageSettings =
                mSettings.getPackagesLocked();

Settings.getPackagesLocked

 WatchedArrayMap<String, PackageSetting> getPackagesLocked() {
        return mPackages;
    }

Settings.addPackageLPw 中添加数据

 PackageSetting addPackageLPw(String name, String realName, File codePath, int uid, int pkgFlags,
                                 int pkgPrivateFlags, @NonNull UUID domainSetId) {
        PackageSetting p = mPackages.get(name);
        if (p != null) {
            if (p.getAppId() == uid) {
                return p;
            }
            PackageManagerService.reportSettingsProblem(Log.ERROR,
                    "Adding duplicate package, keeping first: " + name);
            return null;
        }
        p = new PackageSetting(name, realName, codePath, pkgFlags, pkgPrivateFlags, domainSetId)
                .setAppId(uid);
        if (mAppIds.registerExistingAppId(uid, p, name)) {
            mPackages.put(name, p);
            return p;
        }
        return null;
    }

Settings.readSettingsLPw,这个函数中对应用的一些配置信息进行解析,这些信息主要是存在/data/system目录下面

 boolean readSettingsLPw(@NonNull Computer computer, @NonNull List<UserInfo> users,
            ArrayMap<String, Long> originalFirstInstallTimes) {
        mPendingPackages.clear();
        mInstallerPackages.clear();
        originalFirstInstallTimes.clear();

        ArrayMap<Long, Integer> keySetRefs = new ArrayMap<>();
        ArrayList<Signature> readSignatures = new ArrayList<>();
      	// 获取一系列配置文件,文件位于/data/system/下面,在setings初始化的时候被赋值
      	/**
      	mSystemDir = new File(dataDir, "system");
        mSystemDir.mkdirs();
		 mSettingsFilename = new File(mSystemDir, "packages.xml");
         mSettingsReserveCopyFilename = new File(mSystemDir, "packages.xml.reservecopy");
         mPreviousSettingsFilename = new File(mSystemDir, "packages-backup.xml");
         下面就是对这些配置文件进行解析
		*/
        try (ResilientAtomicFile atomicFile = getSettingsFile()) {
            FileInputStream str = null;
            try {
                str = atomicFile.openRead();
                if (str == null) {
                    // Not necessary, but will avoid wtf-s in the "finally" section.
                    findOrCreateVersion(StorageManager.UUID_PRIVATE_INTERNAL).forceCurrent();
                    findOrCreateVersion(StorageManager.UUID_PRIMARY_PHYSICAL).forceCurrent();
                    return false;
                }
                final TypedXmlPullParser parser = Xml.resolvePullParser(str);

                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG
                        && type != XmlPullParser.END_DOCUMENT) {
                    // nothing
                }

                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, readSignatures, keySetRefs, users,
                                originalFirstInstallTimes);
                    } 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, readSignatures, users);
                    } else if (tagName.equals("preferred-packages")) {
                        // no longer used.
                    } 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")) {
                        readDisabledSysPackageLPw(parser, users);
                    } 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("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 = parser.getAttributeInt(null, "internal", 0);
                        external.sdkVersion = parser.getAttributeInt(null, "external", 0);
                        internal.buildFingerprint = external.buildFingerprint =
                                XmlUtils.readStringAttribute(parser, "buildFingerprint");
                        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 = parser.getAttributeInt(null, "internal", 0);
                        external.databaseVersion = parser.getAttributeInt(null, "external", 0);

                    } else if (tagName.equals("verifier")) {
                        final String deviceIdentity = parser.getAttributeValue(null, "device");
                        mVerifierDeviceIdentity = VerifierDeviceIdentity.parse(deviceIdentity);
                    } else if (TAG_READ_EXTERNAL_STORAGE.equals(tagName)) {
                        // No longer used.
                    } else if (tagName.equals("keyset-settings")) {
                        mKeySetManagerService.readKeySetsLPw(parser, keySetRefs);
                    } else if (TAG_VERSION.equals(tagName)) {
                        final String volumeUuid = XmlUtils.readStringAttribute(parser,
                                ATTR_VOLUME_UUID);
                        final VersionInfo ver = findOrCreateVersion(volumeUuid);
                        ver.sdkVersion = parser.getAttributeInt(null, ATTR_SDK_VERSION);
                        ver.databaseVersion = parser.getAttributeInt(null, ATTR_DATABASE_VERSION);
                        ver.buildFingerprint = XmlUtils.readStringAttribute(parser,
                                ATTR_BUILD_FINGERPRINT);
                        ver.fingerprint = XmlUtils.readStringAttribute(parser, ATTR_FINGERPRINT);
                    } else if (tagName.equals(
                            DomainVerificationPersistence.TAG_DOMAIN_VERIFICATIONS)) {
                        mDomainVerificationManager.readSettings(computer, parser);
                    } else if (tagName.equals(
                            DomainVerificationLegacySettings.TAG_DOMAIN_VERIFICATIONS_LEGACY)) {
                        mDomainVerificationManager.readLegacySettings(parser);
                    } else {
                        Slog.w(PackageManagerService.TAG, "Unknown element under <packages>: "
                                + parser.getName());
                        XmlUtils.skipCurrentTag(parser);
                    }
                }

                str.close();
            } catch (IOException | XmlPullParserException | ArrayIndexOutOfBoundsException e) {
                // Remove corrupted file and retry.
                atomicFile.failRead(str, e);

                // Ignore the result to not mark this as a "first boot".
                readSettingsLPw(computer, users, originalFirstInstallTimes);
            }
        }

        return true;
    }

从上面的函数可以看出Settings这个类的主要职责就是存储修改解析应用的配置信息,这里涉及的东西比较多,不再赘述有兴趣可以自行查阅相关源码。

            mFirstBoot = !mSettings.readLPw(computer,
                    mInjector.getUserManagerInternal().getUsers(
                    /* excludePartial= */ true,
                    /* excludeDying= */ false,
                    /* excludePreCreated= */ false));

这里是Settings解析的开始在PackageMangerService的构造函数中,这是第二个参数的由来,接着第三个四个参数就比较简单分别是uid和时间戳。

篇幅有限下一篇展开细说

  • 8
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值