PackageManagerService包扫描和apk解析过程

        前面学习到了PackageManagerService构造函数的前四步,第一步创建全局设置对象mSettings,协助PMS保存系统中安装APP包名,权限,四大组件等相关信息的存储,并且创建packages.xml和packages.list文件保存所有apk的权限和信息等等。然后第二步通过HandlerThread线程创建了PackageHandler的实例mHandler来处理消息,然后第三步是通过mSettings调用readLPw()方法,读取和解析配置更新packages.xml文件,这一步会将packages.xml文件中的package解析出来以PackageSetting对象的形式放进mPackages集合中,然后是第四步,遍历了前面解析出来的所有PackageSetting对象,然后去检查是否有被误删的apk,或者是已经卸载的apk,有的话就从mSettings中移除相应的PackageSetting对象。然后接下来就是我们需要重点讨论的包扫描过程了。

final File privilegedAppDir = new File(Environment.getRootDirectory(), "priv-app");
            scanDirTracedLI(privilegedAppDir,
                    mDefParseFlags
                    | PackageParser.PARSE_IS_SYSTEM_DIR,
                    scanFlags
                    | SCAN_AS_SYSTEM
                    | SCAN_AS_PRIVILEGED,
                    0);
            // Collect ordinary system packages.
            final File systemAppDir = new File(Environment.getRootDirectory(), "app");
            scanDirTracedLI(systemAppDir,
                    mDefParseFlags
                    | PackageParser.PARSE_IS_SYSTEM_DIR,
                    scanFlags
                    | SCAN_AS_SYSTEM,
                    0);

         首先是调用scanDirTracedLI()扫描/system/目录下的app和priv-app目录,然后还有其他的目录,像framewotk/、vendor/、odm/、product/等。在scanDirTracedLI()方法中,调用scanDirLI()方法进行扫描。

private void scanDirLI(File scanDir, int parseFlags, int scanFlags, long currentTime) {
// 1、获取目录下的文件集合,即所有的apk列表,然后逐个解析
final File[] files = scanDir.listFiles(); 
	...
// 2、创建ParallelPackageParser对象
	try (ParallelPackageParser parallelPackageParser = new ParallelPackageParser(
        	mSeparateProcesses, mOnlyCore, mMetrics, mCacheDir,
        	mParallelPackageParserCallback)) { 
    	int fileCount = 0;
   		 for (File file : files) {
//3、遍历files集合,使用parallelPackageParser提交每个apk文件
        		parallelPackageParser.submit(file, parseFlags); 
       		 fileCount++;
   		 }
//4、取出每个File的扫描结果,执行scan
    	for (; fileCount > 0; fileCount--) {
        		ParallelPackageParser.ParseResult parseResult = parallelPackageParser.take();
       		 scanPackageChildLI(parseResult.pkg, parseFlags, scanFlags,currentTime, null); 
		...
  		}
...
}

scanDirLI()方法主要功能如下:
        1.遍历文件目录下的所有文件;
        2.创建ParallelPackageParser对象,调用submit()方法提交请求,执行解析每个apk文件;
        3.调用parallelPackageParser.take()逐个取出每个解析的结果;
        到这里我们知道PMS是对多个目录中所有apk文件进行解析,在之前的版本中会直接创建PackageParser对象执行解析,在Androip P版本中引入ParallelPackageParser类,使用线程池和队列执行程序的解析;
ParallelPackageParser:内部使用线程池和队列执行文件目录中apk扫描解析 

// 1、保存请求结果的队列,阻塞线程
private final BlockingQueue<ParseResult> mQueue = new ArrayBlockingQueue<>(QUEUE_CAPACITY); 
// 2、创建线程池
private final ExecutorService mService = ConcurrentUtils.newFixedThreadPool(MAX_THREADS,
        "package-parsing-thread", Process.THREAD_PRIORITY_FOREGROUND); 
 // 3、线程池提交任务       
public void submit(File scanFile, int parseFlags) {
    mService.submit(() -> {
        ParseResult pr = new ParseResult();
        try {
            PackageParser pp = new PackageParser();
            pp.setSeparateProcesses(mSeparateProcesses);
            pp.setOnlyCoreApps(mOnlyCore);
            pp.setDisplayMetrics(mMetrics);
            pp.setCacheDir(mCacheDir);
            pp.setCallback(mPackageParserCallback);
            pr.scanFile = scanFile;
            pr.pkg = parsePackage(pp, scanFile, parseFlags); // 执行文件的解析扫描,将结果封装在ParseResult中
        }
//4、将解析的结果添加到队列中
        try {
            mQueue.put(pr); // 
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            mInterruptedInThread = Thread.currentThread().getName();
        }
    });
}
public ParseResult take() {
    try {
        return mQueue.take(); // 从队列中取出解析结果
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new IllegalStateException(e);
    }
}

        ParallelPackageParser中利用线程池处理并发问题,执行多个apk文件的解析,并使用阻塞队列的方式同步线程的数据,在submit提交的任务run()方法中,创建了PackageParser对象并调用parserPackage()方法解析apk,之后将解析的结果封装在ParseResult中,最后添加到mQueue队列中,PMS中在依次调用take()方法从mQueue队列中获取执行的结果; 

 

public Package parsePackage(File packageFile, int flags, boolean useCaches)
        throws PackageParserException {
 // 1、从缓存中获取解析结果
    Package parsed = useCaches ? getCachedResult(packageFile, flags) : null;
    if (parsed != null) {
        return parsed;
    }
// 2、判断是文件还是文件夹,分别执行不同的方法;
    if (packageFile.isDirectory()) { 
        parsed = parseClusterPackage(packageFile, flags); // 执行文件夹解析
    } else {
        parsed = parseMonolithicPackage(packageFile, flags); // 执行单个apk文件解析,单个安装apk文件时执行
    }
 3、缓存解析的结果parser
    cacheResult(packageFile, flags, parsed); //
    return parsed;
}

         parserPackage()方法中会根据传入的文件的类型夹执行不同的方法,如果是文件夹,执行parseClusterPackage(),是单个apk则执行 parseMonolithicPackage()。另外ParserPackage类中还有一个包解析的方法parsePackageLite(),此方法只对apk做轻量级解析,然后返回一个含有apk少量信息的PackageLite对象。刚刚看到PackageParser中parserPackage()方法调用parseMonolithicPackage()解析apk;

public Package parseMonolithicPackage(File apkFile, int flags) throws PackageParserException {
//1.获取apk的少量信息
    final PackageLite lite = parseMonolithicPackageLite(apkFile, flags);
// 2、使用AssetManager加载资源文件
    final SplitAssetLoader assetLoader = new DefaultSplitAssetLoader(lite, flags); 
    try {
// 3.执行parseBaseAPk
        final Package pkg = parseBaseApk(apkFile, assetLoader.getBaseAssetManager(), flags); 
        pkg.setCodePath(apkFile.getCanonicalPath());
        pkg.setUse32bitAbi(lite.use32bitAbi);
        return pkg;
    } 
}

        parseMonolithicPackage()中先调用parseMonolithicPackageLite()获取apk的少量信息,其中parseApkLite()将File先简单的解析以下,这里的解析只是获取注册清单中的基础信息,并将信息保存在ApkLite对象中,然后将ApkLite和文件路径封装在PackageLite对象中;

private static PackageLite parseMonolithicPackageLite(File packageFile, int flags) throws PackageParserException {
        final ApkLite baseApk = parseApkLite(packageFile, flags);
        final String packagePath = packageFile.getAbsolutePath();
    
        return new PackageLite(packagePath, baseApk, null, null, null, null, null, null);
    }

//先轻量级解析apk的基本信息
 private static ApkLite parseApkLite(String codePath, XmlPullParser parser, AttributeSet attrs,SigningDetails signingDetails)
            throws IOException, XmlPullParserException, PackageParserException {
        final Pair<String, String> packageSplit = parsePackageSplitNames(parser, attrs);
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            final String attr = attrs.getAttributeName(i);
            if (attr.equals("installLocation")) {
                installLocation = attrs.getAttributeIntValue(i,
                        PARSE_DEFAULT_INSTALL_LOCATION);
            } else if (attr.equals("versionCode")) {
                versionCode = attrs.getAttributeIntValue(i, 0);
            } else if (attr.equals("versionCodeMajor")) {
                versionCodeMajor = attrs.getAttributeIntValue(i, 0);
            } else if (attr.equals("revisionCode")) {
                revisionCode = attrs.getAttributeIntValue(i, 0);
            } 
            .....
        }
......
        return new ApkLite(codePath, packageSplit.first, packageSplit.second, isFeatureSplit,
                configForSplit, usesSplitName, versionCode, versionCodeMajor, revisionCode,
                installLocation, verifiers, signingDetails, coreApp, debuggable,
                multiArch, use32bitAbi, extractNativeLibs, isolatedSplits);
    }

        接着回到parseMonolithicPackage()方法中,
DefaultSplitAssetLoader:内部使用AssestManager加载apk文件的资源,并缓存AssestManager对象信息,主要针对split APK;
parseBaseApk():解析每个apk文件,parseBaseApk(File apkFile, AssetManager assets, int flags)中从apkFile对象中获取apk文件路径,然后使用assmgr加载apk文件中的资源,从文件中读取注册清单文件,然后调用重载的parseBaseApk()解析注册清单;

private Package parseBaseApk(File apkFile, AssetManager assets, int flags)
           throws PackageParserException {// 参数:apk文件File、apk文件路径destCodePath
// 1、获取apk文件路径
           final String apkPath = apkFile.getAbsolutePath(); 
           mArchiveSourcePath = sourceFile.getPath(); 
// 2、将apk的路径添加到AssetManager中加载资源
           int cookie = assmgr.findCookieForPath(mArchiveSourcePath); 
 // 3、解析xml注册清单文件
        	 parser = assmgr.openXmlResourceParser(cookie, "AndroidManifest.xml");
// 4、创建Resource对象
           Resources res = new Resources(assmgr, metrics, null); 
           pkg = parseBaseApk(res, parser, flags, errorText); // 解析parser中的信息,保存在Package对象中
          pkg.setVolumeUuid(volumeUuid);
          pkg.setApplicationVolumeUuid(volumeUuid);
          pkg.setBaseCodePath(apkPath);
          pkg.setSigningDetails(SigningDetails.UNKNOWN);
          return pkg;
}

parseBaseApk(String apkPath, Resources res, XmlResourceParser parser, int flags,
            String[] outError):从Parser对象中获取解析的信息,保存在Package对象中,parseBaseApk()中从parser中提取注册清单中的基础信息,并封装保存在Pakage对象中,然后调用parseBaseApkCommon()方法继续解析清单文件中内容。

private Package parseBaseApk(String apkPath,Resources res, XmlResourceParser parser, int flags, String[] outError){
//1、从parser中解析出“package”设置的包名pkgName
Pair<String, String> packageSplit = parsePackageSplitNames(parser, parser); 
pkgName = packageSplit.first;
splitName = packageSplit.second;
// 2、创建Package对象,保存apk的包名
Package pkg = new Package(pkgName); 
//从Resource中获取各种信息,并保存在Package的属性中
TypedArray sa = res.obtainAttributes(parser,
                com.android.internal.R.styleable.AndroidManifest);
 pkg.mVersionCode = sa.getInteger(…...
pkg.mVersionName = sa.getNonConfigurationString(…...
 pkg.mSharedUserId = str.intern(…...);
pkg.mSharedUserLabel = sa.getResourceId(…...
pkg.installLocation = sa.getInteger(…...
pkg.applicationInfo.installLocation = pkg.installLocation;
// 3、调用parseBaseApkCommon()继续解析文件
return parseBaseApkCommon(pkg, null, res, parser, flags, outError); 
}
//parseBaseApkCommon()从Parser对象中解析数据信息
private Package parseBaseApkCommon(Package pkg, Set<String> acceptedTags, Resources res,
            XmlResourceParser parser, int flags, String[] outError) throws XmlPullParserException,
            IOException {
...
		int outerDepth = parser.getDepth(); // 获取parser的深度
		while ((type=parser.next()) != parser.END_DOCUMENT. // 1、循环解析parser对象
             && (type != parser.END_TAG || parser.getDepth() > outerDepth)) {
 		if (tagName.equals("application")) {
		if (!parseBaseApplication(pkg, res, parser, attrs, flags, outError)) { // 2、解析application标签
                    return null;
		}else if (tagName.equals("permission")) { // 3、解析权限标签
                if (parsePermission(pkg, res, parser, attrs, outError) == null) {
	                    return null;
                }
} else if (tagName.equals("uses-feature")) { // 4、解析使用的user-feature标签,并保存在Package的集合中
 FeatureInfo fi = new FeatureInfo();
    …….
pkg.reqFeatures.add(fi);
}else if (tagName.equals("uses-sdk")) { // 解析user-sdk标签
}else if (tagName.equals("supports-screens")) { // 解析support-screens标签
}
}
}

        parseBaseApkCommon()中主要负责解析清单文件中的各种标签信息,其中最主要的就是解析标签下的四大组件的信息,在遇到applicaiton标签时直接调用了parseBaseApplication()执行解析;

private boolean parseBaseApplication(Package owner, Resources res,
            XmlResourceParser parser, int flags, String[] outError) throws XmlPullParserException, IOException {
String tagName = parser.getName();
final ApplicationInfo ai = owner.applicationInfo;
ai.theme = sa.getResourceId(
        com.android.internal.R.styleable.AndroidManifestApplication_theme, 0);
ai.descriptionRes = sa.getResourceId(
        com.android.internal.R.styleable.AndroidManifestApplication_description, 0);
ai.maxAspectRatio = sa.getFloat(R.styleable.AndroidManifestApplication_maxAspectRatio, 0);

// 分别解析四大组件,将解析结果保存在Package对应的集合中
           if (tagName.equals("activity")) { 
                Activity a = parseActivity(owner, res, parser, attrs, flags, outError, false);
                owner.activities.add(a);
            } else if (tagName.equals("receiver")) {
               Activity a = parseActivity(owner, res, parser, attrs, flags, outError, true);
                owner.receivers.add(a);
            } else if (tagName.equals("service")) {
                Service s = parseService(owner, res, parser, attrs, flags, outError);
                owner.services.add(s);
            } else if (tagName.equals("provider")) {
                Provider p = parseProvider(owner, res, parser, attrs, flags, outError);
                owner.providers.add(p);
            }

清单文件解析共分两部分:

1.解析出application标签下设置的name类名、icon、theme、targetSdk、processName等属性标签并保存在ApplicationInfo对象中
2.循环解析activity、receiver、service、provider四个标签,并将信息到保存在Package中对应的集合中

接下来看四大组件是如何解析保存的
1.parserActivity():解析Activity和Receiver标签并返回Activity对象封装所有属性;

private Activity parseActivity(Package owner,...)
     throws XmlPullParserException, IOException {
    TypedArray sa = res.obtainAttributes(parser, R.styleable.AndroidManifestActivity);
/ 1、判断为activity或receiver
    cachedArgs.mActivityArgs.tag = receiver ? "<receiver>" : "<activity>"; /
    cachedArgs.mActivityArgs.sa = sa;
    cachedArgs.mActivityArgs.flags = flags;
// 2、创建Activity实例,并初始化系列属性
    Activity a = new Activity(cachedArgs.mActivityArgs, new ActivityInfo()); 
    a.info.theme = sa.getResourceId(R.styleable.AndroidManifestActivity_theme, 0);
    a.info.taskAffinity = buildTaskAffinityName(owner.applicationInfo.packageName,
        owner.applicationInfo.taskAffinity, str, outError);
    a.info.launchMode = ...
// 3、解析intent-filter
    if (parser.getName().equals("intent-filter")) { 
        ActivityIntentInfo intent = new ActivityIntentInfo(a);
        if (!parseIntent(res, parser, true /*allowGlobs*/, true /*allowAutoVerify*/,
            intent, outError)) {
            return null;
        }
        a.order = Math.max(intent.getOrder(), a.order);
        a.intents.add(intent); // 将intent设置到Activity
    } 
    return a;
}

        在解析Activity和Receiver标签时,当标签设置intent-filter时则创建一个ActivityIntentInfo对象,并调用parseIntent()将intent-filter标签下的信息解析到ActivityIntentInfo中,并将ActivityIntentInfo对象保存在a.intents的集合中,简单的说一个intent-filter对应一个ActivityIntentInfo对象,一个Activity和Receiver可以包好多个intent-filter;


1.1 parseIntent():解析每个intent-filter标签下的action、name等属性值,并将所有的属性值保存在outInfo对象中,这里的outInfo是ActivityIntentInfo对象;

private Activity parseActivity(Package owner,...)
     throws XmlPullParserException, IOException {
...
while ((type = parser.next()) != XmlPullParser.END_DOCUMENT
        && (type != XmlPullParser.END_TAG || parser.getDepth() > outerDepth)) {
    String nodeName = parser.getName(); // 1、获取节点名称
    if (nodeName.equals("action")) { //2、处理action节点
    String value = parser.getAttributeValue( // 3、获取action节点的name属性,并保存在outInfo属性中
        ANDROID_RESOURCES, "name");
    outInfo.addAction(value); //
    } else if (nodeName.equals("category")) {
        String value = parser.getAttributeValue( // 4、获取category属性,保存在outInfo属性中
            ANDROID_RESOURCES, "name");
        outInfo.addCategory(value); // 添加到Category集合中
    } else if (nodeName.equals("data")) {
        sa = res.obtainAttributes(parser,
        com.android.internal.R.styleable.AndroidManifestData); // 解析parser为sa属性
        String str = sa.getNonConfigurationString(
        com.android.internal.R.styleable.AndroidManifestData_mimeType, 0);
        outInfo.addDataType(str); // 获取并保存mimeType中
        str = sa.getNonConfigurationString(
        com.android.internal.R.styleable.AndroidManifestData_scheme, 0);
        outInfo.addDataScheme(str); // 获取并保存scheme中
        String host = sa.getNonConfigurationString(
        com.android.internal.R.styleable.AndroidManifestData_host, 0); // 
        String port = sa.getNonConfigurationString(
        com.android.internal.R.styleable.AndroidManifestData_port, 0);
        outInfo.addDataAuthority(host, port); // 获取并保存host、port属性
        outInfo.hasDefault = outInfo.hasCategory(Intent.CATEGORY_DEFAULT); // 设置Default属性
}

1.2 ActivityIntentInfo 继承 IntentInfo ,IntentInfo继承IntentFilter属性,所以outInfo保存的数据都保存在IntentFilter中,在IntentFilter中有标签对应的集合,如actions、mDataSchemes等,所以遍历的所有intent-filter中设置的数据都保存在其中;

public final static class ActivityIntentInfo extends IntentInfo {}
public static abstract class IntentInfo extends IntentFilter {}
public final void addAction(String action) {
    if (!mActions.contains(action)) {
        mActions.add(action.intern()); // 在Intent-Filter中保存action
    }
}
public final void addCategory(String category) {
    if (mCategories == null) mCategories = new ArrayList<String>();
    if (!mCategories.contains(category)) {
        mCategories.add(category.intern()); // 
    }
}
public final void addDataScheme(String scheme) {
    if (mDataSchemes == null) mDataSchemes = new ArrayList<String>();
    if (!mDataSchemes.contains(scheme)) {
        mDataSchemes.add(scheme.intern()); // 
    }
}

2.parserService():解析Service标签并返回Service对象,对应service标签下的信息保存在ServiceIntentInfo对象中,ServiceIntentInfo的作用和保存和ActivityIntentInfo一样。

private Service parseService(Package owner, Resources res,
            XmlResourceParser parser, int flags, String[] outError,
            CachedComponentArgs cachedArgs)
            throws XmlPullParserException, IOException {
    TypedArray sa = res.obtainAttributes(parser,
        com.android.internal.R.styleable.AndroidManifestService);
    cachedArgs.mServiceArgs.sa = sa;
    cachedArgs.mServiceArgs.flags = flags;
    Service s = new Service(cachedArgs.mServiceArgs, new ServiceInfo()); // 创建Service对象
    if (parser.getName().equals("intent-filter")) { // 解析intent-filter节点
        ServiceIntentInfo intent = new ServiceIntentInfo(s);
        if (!parseIntent(res, parser, true /*allowGlobs*/, false /*allowAutoVerify*/,
            intent, outError)) {
        return null;
        }
        s.order = Math.max(intent.getOrder(), s.order);
        s.intents.add(intent);
    } else if (parser.getName().equals("meta-data")) { // 解析meta-data数据,保存在bundle中
        if ((s.metaData=parseMetaData(res, parser, s.metaData,
            outError)) == null) {
        return null;
    }
}

3.parserProvider():解析provider标签并返回provider对象 

 private Provider parseProvider(Package owner, Resources res,
            XmlResourceParser parser, int flags, String[] outError,
            CachedComponentArgs cachedArgs)
            throws XmlPullParserException, IOException {
    TypedArray sa = res.obtainAttributes(parser,
        com.android.internal.R.styleable.AndroidManifestProvider);
    cachedArgs.mProviderArgs.tag = "<provider>";
    cachedArgs.mProviderArgs.sa = sa;
    cachedArgs.mProviderArgs.flags = flags;

    Provider p = new Provider(cachedArgs.mProviderArgs, new ProviderInfo()); // 创建Provider对象
    p.info.exported = sa.getBoolean(
        com.android.internal.R.styleable.AndroidManifestProvider_exported,
        providerExportedDefault);
    String permission = sa.getNonConfigurationString(
        com.android.internal.R.styleable.AndroidManifestProvider_permission, 0);
    p.info.multiprocess = sa.getBoolean(
        com.android.internal.R.styleable.AndroidManifestProvider_multiprocess,
        false);
    p.info.authority = cpname.intern();
    if (!parseProviderTags( // 调用parseProvider解析provider下的标签
        res, parser, visibleToEphemeral, p, outError)) {
        return null;
    }
    return p;
}

在解析provider标签时,创建ProviderInfo对象保存设置的属性信息,如export、permission等,然后调用parseProviderTags解析provider标签中使用的其他标签信息。
3.1 parseProviderTags():

private boolean parseProviderTags(Resources res, XmlResourceParser parser,
        boolean visibleToEphemeral, Provider outInfo, String[] outError){
    int outerDepth = parser.getDepth();
    int type;
    while ((type=parser.next()) != XmlPullParser.END_DOCUMENT
           && (type != XmlPullParser.END_TAG
                   || parser.getDepth() > outerDepth)) {
        if (parser.getName().equals("intent-filter")) {
            ProviderIntentInfo intent = new ProviderIntentInfo(outInfo);
            if (!parseIntent(res, parser, true /*allowGlobs*/, false /*allowAutoVerify*/,
                intent, outError)) {
                return false;
            }
            outInfo.order = Math.max(intent.getOrder(), outInfo.order);
            outInfo.intents.add(intent);
        } 
        if (parser.getName().equals("meta-data")) {
            if ((outInfo.metaData=parseMetaData(res, parser,
                outInfo.metaData, outError)) == null) {
                return false;
            }
        } 
        if (parser.getName().equals("grant-uri-permission")) {
            String str = sa.getNonConfigurationString(
            com.android.internal.R.styleable.AndroidManifestGrantUriPermission_path, 0);
            PatternMatcher   pa = new PatternMatcher(str, PatternMatcher.PATTERN_LITERAL); // 创建PatterMatcher匹配权限
            PatternMatcher[] newp = new PatternMatcher[N+1];
            newp[N] = pa;
            outInfo.info.uriPermissionPatterns = newp; // 保存临时权限数组
        }
        if (parser.getName().equals("path-permission")) { // 保存路径权限
            newp[N] = pa;
            outInfo.info.pathPermissions = newp;
        }
    }
}

 

主要解析如下:

1.对于intent-filter标签,调用parserIntent()解析保存在ProviderIntentInfo对象中,并添加到intent集合中
2.解析设置的meta-data值
3.解析grant-uri-permission和path-permission等权限的匹配状况保存在Provider对象中的数组中;


到此apk文件已经解析完成,相应的文件和属性都封装在Package对象中,并都保存在PMS属性集合mPackages集合中。

原文链接:https://blog.csdn.net/Alexwll/article/details/102777742

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值