PMS安装程序

PMS负责扫描系统中特定的目录,找到里面的apk文件,然后对这些文件进行解析得到相关信息,然后完成安装的过程。
PMS安装APP,其实也就是解析APP配置文件AndroidManifest.xml的过程。
解析apk文件的核心类是PackageParser,这个类负责从apk文件中解析各种标签以及权限等信息,并将这些信息注册进PMS。
以下就以4.2.2_r1版本的PackageParser为例来看看PackageParser是如何解析apk文件的。
PackageParser的核心方法是parsePackage()

public Package parsePackage(File sourceFile, String destCodePath,
        DisplayMetrics metrics, int flags) {
    mParseError = PackageManager.INSTALL_SUCCEEDED;

    /** 首先对apk文件进行检查,如果出错则打印相应的错误日志并返回null. **/
    mArchiveSourcePath = sourceFile.getPath();
    if (!sourceFile.isFile()) {
        Slog.w(TAG, "Skipping dir: " + mArchiveSourcePath);
        mParseError = PackageManager.INSTALL_PARSE_FAILED_NOT_APK;
        return null;
    }
    if (!isPackageFilename(sourceFile.getName())
            && (flags&PARSE_MUST_BE_APK) != 0) {
        if ((flags&PARSE_IS_SYSTEM) == 0) {
            // We expect to have non-.apk files in the system dir,
            // so don't warn about them.
            Slog.w(TAG, "Skipping non-package file: " + mArchiveSourcePath);
        }
        mParseError = PackageManager.INSTALL_PARSE_FAILED_NOT_APK;
        return null;
    }

    if (DEBUG_JAR)
        Slog.d(TAG, "Scanning package: " + mArchiveSourcePath);

    /** 这里则是尝试读取AndroidManifest.xml文件和资源文件以及通过apk来初始化AssetManager. 
        如果出错,则打印相应的日志并记录错误同时返回空值;否则将调用另一个parsePackage方法继续解析apk. 
        另一个方法的解析贴在本方法的下方。 **/
    XmlResourceParser parser = null;
    AssetManager assmgr = null;
    Resources res = null;
    boolean assetError = true;
    try {
        assmgr = new AssetManager();
        int cookie = assmgr.addAssetPath(mArchiveSourcePath);
        if (cookie != 0) {
            res = new Resources(assmgr, metrics, null);
            assmgr.setConfiguration(0, 0, null, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
                    Build.VERSION.RESOURCES_SDK_INT);
            parser = assmgr.openXmlResourceParser(cookie, ANDROID_MANIFEST_FILENAME);
            assetError = false;
        } else {
            Slog.w(TAG, "Failed adding asset path:"+mArchiveSourcePath);
        }
    } catch (Exception e) {
        Slog.w(TAG, "Unable to read AndroidManifest.xml of "
                + mArchiveSourcePath, e);
    }
    if (assetError) {
        if (assmgr != null) assmgr.close();
        mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_MANIFEST;
        return null;
    }
    String[] errorText = new String[1];
    Package pkg = null;
    Exception errorException = null;
    try {
        // XXXX todo: need to figure out correct configuration.
        pkg = parsePackage(res, parser, flags, errorText);
    } catch (Exception e) {
        errorException = e;
        mParseError = PackageManager.INSTALL_PARSE_FAILED_UNEXPECTED_EXCEPTION;
    }


    if (pkg == null) {
        // If we are only parsing core apps, then a null with INSTALL_SUCCEEDED
        // just means to skip this app so don't make a fuss about it.
        if (!mOnlyCoreApps || mParseError != PackageManager.INSTALL_SUCCEEDED) {
            if (errorException != null) {
                Slog.w(TAG, mArchiveSourcePath, errorException);
            } else {
                Slog.w(TAG, mArchiveSourcePath + " (at "
                        + parser.getPositionDescription()
                        + "): " + errorText[0]);
            }
            if (mParseError == PackageManager.INSTALL_SUCCEEDED) {
                mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
            }
        }
        parser.close();
        assmgr.close();
        return null;
    }

    parser.close();
    assmgr.close();

    // Set code and resource paths
    pkg.mPath = destCodePath;
    pkg.mScanPath = mArchiveSourcePath;
    //pkg.applicationInfo.sourceDir = destCodePath;
    //pkg.applicationInfo.publicSourceDir = destRes;
    pkg.mSignatures = null;

    return pkg;
}

另外一个parsePackage()方法,该方法为私有方法,在上面个公有的parsePackage()方法中调用:

private Package parsePackage(
    Resources res, XmlResourceParser parser, int flags, String[] outError)
    throws XmlPullParserException, IOException {
    /** 由于XmlResourceParser实现了AttributeSet接口,因此这里直接当作AttributeSet来使用 **/
    AttributeSet attrs = parser;

    mParseInstrumentationArgs = null;
    mParseActivityArgs = null;
    mParseServiceArgs = null;
    mParseProviderArgs = null;
    
    /** 先检查包名,如果包名有问题则直接失败返回 **/
    String pkgName = parsePackageName(parser, attrs, flags, outError);
    if (pkgName == null) {
        mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_PACKAGE_NAME;
        return null;
    }
    int type;

    /** coreApp 同加密机制有关 **/
    if (mOnlyCoreApps) {
        boolean core = attrs.getAttributeBooleanValue(null, "coreApp", false);
        if (!core) {
            mParseError = PackageManager.INSTALL_SUCCEEDED;
            return null;
        }
    }

    final Package pkg = new Package(pkgName);
    boolean foundApp = false;
    
    /** 获取版本号、版本名和shareUserId,并检查shareUserId是否正确 **/
    TypedArray sa = res.obtainAttributes(attrs,
            com.android.internal.R.styleable.AndroidManifest);
    pkg.mVersionCode = sa.getInteger(
            com.android.internal.R.styleable.AndroidManifest_versionCode, 0);
    pkg.mVersionName = sa.getNonConfigurationString(
            com.android.internal.R.styleable.AndroidManifest_versionName, 0);
    if (pkg.mVersionName != null) {
        pkg.mVersionName = pkg.mVersionName.intern();
    }
    String str = sa.getNonConfigurationString(
            com.android.internal.R.styleable.AndroidManifest_sharedUserId, 0);
    if (str != null && str.length() > 0) {
        String nameError = validateName(str, true);
        if (nameError != null && !"android".equals(pkgName)) {
            outError[0] = "<manifest> specifies bad sharedUserId name \""
                + str + "\": " + nameError;
            mParseError = PackageManager.INSTALL_PARSE_FAILED_BAD_SHARED_USER_ID;
            return null;
        }
        pkg.mSharedUserId = str.intern();
        pkg.mSharedUserLabel = sa.getResourceId(
                com.android.internal.R.styleable.AndroidManifest_sharedUserLabel, 0);
    }
    sa.recycle();

    pkg.installLocation = sa.getInteger(
            com.android.internal.R.styleable.AndroidManifest_installLocation,
            PARSE_DEFAULT_INSTALL_LOCATION);
    pkg.applicationInfo.installLocation = pkg.installLocation;

    /* Set the global "forward lock" flag */
    if ((flags & PARSE_FORWARD_LOCK) != 0) {
        pkg.applicationInfo.flags |= ApplicationInfo.FLAG_FORWARD_LOCK;
    }

    /* Set the global "on SD card" flag */
    if ((flags & PARSE_ON_SDCARD) != 0) {
        pkg.applicationInfo.flags |= ApplicationInfo.FLAG_EXTERNAL_STORAGE;
    }

    // Resource boolean are -1, so 1 means we don't know the value.
    int supportsSmallScreens = 1;
    int supportsNormalScreens = 1;
    int supportsLargeScreens = 1;
    int supportsXLargeScreens = 1;
    int resizeable = 1;
    int anyDensity = 1;
    
    int outerDepth = parser.getDepth();
    /** 开始迭代处理各种标签 
        对于大多数Android开发者来说,这里最重要的是parseApplication.
        这里会将Application标签下的各种信息解析出来,并注册进PMS. 
        迭代检查完所有预定义的标签之后如果没有问题,则将记录了apk信息的Package对象返回。**/
    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("application")) {
            if (foundApp) {
                if (RIGID_PARSER) {
                    outError[0] = "<manifest> has more than one <application>";
                    mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
                    return null;
                } else {
                    Slog.w(TAG, "<manifest> has more than one <application>");
                    XmlUtils.skipCurrentTag(parser);
                    continue;
                }
            }

            foundApp = true;
            if (!parseApplication(pkg, res, parser, attrs, flags, outError)) {
                return null;
            }
        } else if (tagName.equals("permission-group")) {
            if (parsePermissionGroup(pkg, flags, res, parser, attrs, outError) == null) {
                return null;
            }
        } else if (tagName.equals("permission")) {
            if (parsePermission(pkg, res, parser, attrs, outError) == null) {
                return null;
            }
        } else if (tagName.equals("permission-tree")) {
            if (parsePermissionTree(pkg, res, parser, attrs, outError) == null) {
                return null;
            }
        } else if (tagName.equals("uses-permission")) {
            sa = res.obtainAttributes(attrs,
                    com.android.internal.R.styleable.AndroidManifestUsesPermission);

            // Note: don't allow this value to be a reference to a resource
            // that may change.
            String name = sa.getNonResourceString(
                    com.android.internal.R.styleable.AndroidManifestUsesPermission_name);
            /* Not supporting optional permissions yet.
            boolean required = sa.getBoolean(
                    com.android.internal.R.styleable.AndroidManifestUsesPermission_required, true);
            */

            sa.recycle();

            if (name != null && !pkg.requestedPermissions.contains(name)) {
                pkg.requestedPermissions.add(name.intern());
                pkg.requestedPermissionsRequired.add(Boolean.TRUE);
            }

            XmlUtils.skipCurrentTag(parser);

        } else if (tagName.equals("uses-configuration")) {
            ConfigurationInfo cPref = new ConfigurationInfo();
            sa = res.obtainAttributes(attrs,
                    com.android.internal.R.styleable.AndroidManifestUsesConfiguration);
            cPref.reqTouchScreen = sa.getInt(
                    com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqTouchScreen,
                    Configuration.TOUCHSCREEN_UNDEFINED);
            cPref.reqKeyboardType = sa.getInt(
                    com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqKeyboardType,
                    Configuration.KEYBOARD_UNDEFINED);
            if (sa.getBoolean(
                    com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqHardKeyboard,
                    false)) {
                cPref.reqInputFeatures |= ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD;
            }
            cPref.reqNavigation = sa.getInt(
                    com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqNavigation,
                    Configuration.NAVIGATION_UNDEFINED);
            if (sa.getBoolean(
                    com.android.internal.R.styleable.AndroidManifestUsesConfiguration_reqFiveWayNav,
                    false)) {
                cPref.reqInputFeatures |= ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV;
            }
            sa.recycle();
            pkg.configPreferences.add(cPref);

            XmlUtils.skipCurrentTag(parser);

        } else if (tagName.equals("uses-feature")) {
            FeatureInfo fi = new FeatureInfo();
            sa = res.obtainAttributes(attrs,
                    com.android.internal.R.styleable.AndroidManifestUsesFeature);
            // Note: don't allow this value to be a reference to a resource
            // that may change.
            fi.name = sa.getNonResourceString(
                    com.android.internal.R.styleable.AndroidManifestUsesFeature_name);
            if (fi.name == null) {
                fi.reqGlEsVersion = sa.getInt(
                        com.android.internal.R.styleable.AndroidManifestUsesFeature_glEsVersion,
                        FeatureInfo.GL_ES_VERSION_UNDEFINED);
            }
            if (sa.getBoolean(
                    com.android.internal.R.styleable.AndroidManifestUsesFeature_required,
                    true)) {
                fi.flags |= FeatureInfo.FLAG_REQUIRED;
            }
            sa.recycle();
            if (pkg.reqFeatures == null) {
                pkg.reqFeatures = new ArrayList<FeatureInfo>();
            }
            pkg.reqFeatures.add(fi);
            
            if (fi.name == null) {
                ConfigurationInfo cPref = new ConfigurationInfo();
                cPref.reqGlEsVersion = fi.reqGlEsVersion;
                pkg.configPreferences.add(cPref);
            }

            XmlUtils.skipCurrentTag(parser);

        } else if (tagName.equals("uses-sdk")) {
            if (SDK_VERSION > 0) {
                sa = res.obtainAttributes(attrs,
                        com.android.internal.R.styleable.AndroidManifestUsesSdk);

                int minVers = 0;
                String minCode = null;
                int targetVers = 0;
                String targetCode = null;
                
                TypedValue val = sa.peekValue(
                        com.android.internal.R.styleable.AndroidManifestUsesSdk_minSdkVersion);
                if (val != null) {
                    if (val.type == TypedValue.TYPE_STRING && val.string != null) {
                        targetCode = minCode = val.string.toString();
                    } else {
                        // If it's not a string, it's an integer.
                        targetVers = minVers = val.data;
                    }
                }
                
                val = sa.peekValue(
                        com.android.internal.R.styleable.AndroidManifestUsesSdk_targetSdkVersion);
                if (val != null) {
                    if (val.type == TypedValue.TYPE_STRING && val.string != null) {
                        targetCode = minCode = val.string.toString();
                    } else {
                        // If it's not a string, it's an integer.
                        targetVers = val.data;
                    }
                }
                
                sa.recycle();

                if (minCode != null) {
                    if (!minCode.equals(SDK_CODENAME)) {
                        if (SDK_CODENAME != null) {
                            outError[0] = "Requires development platform " + minCode
                                    + " (current platform is " + SDK_CODENAME + ")";
                        } else {
                            outError[0] = "Requires development platform " + minCode
                                    + " but this is a release platform.";
                        }
                        mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK;
                        return null;
                    }
                } else if (minVers > SDK_VERSION) {
                    outError[0] = "Requires newer sdk version #" + minVers
                            + " (current version is #" + SDK_VERSION + ")";
                    mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK;
                    return null;
                }
                
                if (targetCode != null) {
                    if (!targetCode.equals(SDK_CODENAME)) {
                        if (SDK_CODENAME != null) {
                            outError[0] = "Requires development platform " + targetCode
                                    + " (current platform is " + SDK_CODENAME + ")";
                        } else {
                            outError[0] = "Requires development platform " + targetCode
                                    + " but this is a release platform.";
                        }
                        mParseError = PackageManager.INSTALL_FAILED_OLDER_SDK;
                        return null;
                    }
                    // If the code matches, it definitely targets this SDK.
                    pkg.applicationInfo.targetSdkVersion
                            = android.os.Build.VERSION_CODES.CUR_DEVELOPMENT;
                } else {
                    pkg.applicationInfo.targetSdkVersion = targetVers;
                }
            }

            XmlUtils.skipCurrentTag(parser);

        } else if (tagName.equals("supports-screens")) {
            sa = res.obtainAttributes(attrs,
                    com.android.internal.R.styleable.AndroidManifestSupportsScreens);

            pkg.applicationInfo.requiresSmallestWidthDp = sa.getInteger(
                    com.android.internal.R.styleable.AndroidManifestSupportsScreens_requiresSmallestWidthDp,
                    0);
            pkg.applicationInfo.compatibleWidthLimitDp = sa.getInteger(
                    com.android.internal.R.styleable.AndroidManifestSupportsScreens_compatibleWidthLimitDp,
                    0);
            pkg.applicationInfo.largestWidthLimitDp = sa.getInteger(
                    com.android.internal.R.styleable.AndroidManifestSupportsScreens_largestWidthLimitDp,
                    0);

            // This is a trick to get a boolean and still able to detect
            // if a value was actually set.
            supportsSmallScreens = sa.getInteger(
                    com.android.internal.R.styleable.AndroidManifestSupportsScreens_smallScreens,
                    supportsSmallScreens);
            supportsNormalScreens = sa.getInteger(
                    com.android.internal.R.styleable.AndroidManifestSupportsScreens_normalScreens,
                    supportsNormalScreens);
            supportsLargeScreens = sa.getInteger(
                    com.android.internal.R.styleable.AndroidManifestSupportsScreens_largeScreens,
                    supportsLargeScreens);
            supportsXLargeScreens = sa.getInteger(
                    com.android.internal.R.styleable.AndroidManifestSupportsScreens_xlargeScreens,
                    supportsXLargeScreens);
            resizeable = sa.getInteger(
                    com.android.internal.R.styleable.AndroidManifestSupportsScreens_resizeable,
                    resizeable);
            anyDensity = sa.getInteger(
                    com.android.internal.R.styleable.AndroidManifestSupportsScreens_anyDensity,
                    anyDensity);

            sa.recycle();
            
            XmlUtils.skipCurrentTag(parser);
            
        } else if (tagName.equals("protected-broadcast")) {
            sa = res.obtainAttributes(attrs,
                    com.android.internal.R.styleable.AndroidManifestProtectedBroadcast);

            // Note: don't allow this value to be a reference to a resource
            // that may change.
            String name = sa.getNonResourceString(
                    com.android.internal.R.styleable.AndroidManifestProtectedBroadcast_name);

            sa.recycle();

            if (name != null && (flags&PARSE_IS_SYSTEM) != 0) {
                if (pkg.protectedBroadcasts == null) {
                    pkg.protectedBroadcasts = new ArrayList<String>();
                }
                if (!pkg.protectedBroadcasts.contains(name)) {
                    pkg.protectedBroadcasts.add(name.intern());
                }
            }

            XmlUtils.skipCurrentTag(parser);
            
        } else if (tagName.equals("instrumentation")) {
            if (parseInstrumentation(pkg, res, parser, attrs, outError) == null) {
                return null;
            }
            
        } else if (tagName.equals("original-package")) {
            sa = res.obtainAttributes(attrs,
                    com.android.internal.R.styleable.AndroidManifestOriginalPackage);

            String orig =sa.getNonConfigurationString(
                    com.android.internal.R.styleable.AndroidManifestOriginalPackage_name, 0);
            if (!pkg.packageName.equals(orig)) {
                if (pkg.mOriginalPackages == null) {
                    pkg.mOriginalPackages = new ArrayList<String>();
                    pkg.mRealPackage = pkg.packageName;
                }
                pkg.mOriginalPackages.add(orig);
            }

            sa.recycle();

            XmlUtils.skipCurrentTag(parser);
            
        } else if (tagName.equals("adopt-permissions")) {
            sa = res.obtainAttributes(attrs,
                    com.android.internal.R.styleable.AndroidManifestOriginalPackage);

            String name = sa.getNonConfigurationString(
                    com.android.internal.R.styleable.AndroidManifestOriginalPackage_name, 0);

            sa.recycle();

            if (name != null) {
                if (pkg.mAdoptPermissions == null) {
                    pkg.mAdoptPermissions = new ArrayList<String>();
                }
                pkg.mAdoptPermissions.add(name);
            }

            XmlUtils.skipCurrentTag(parser);
            
        } else if (tagName.equals("uses-gl-texture")) {
            // Just skip this tag
            XmlUtils.skipCurrentTag(parser);
            continue;
            
        } else if (tagName.equals("compatible-screens")) {
            // Just skip this tag
            XmlUtils.skipCurrentTag(parser);
            continue;
            
        } else if (tagName.equals("eat-comment")) {
            // Just skip this tag
            XmlUtils.skipCurrentTag(parser);
            continue;
            
        } else if (RIGID_PARSER) {
            outError[0] = "Bad element under <manifest>: "
                + parser.getName();
            mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_MALFORMED;
            return null;

        } else {
            Slog.w(TAG, "Unknown element under <manifest>: " + parser.getName()
                    + " at " + mArchiveSourcePath + " "
                    + parser.getPositionDescription());
            XmlUtils.skipCurrentTag(parser);
            continue;
        }
    }

    if (!foundApp && pkg.instrumentation.size() == 0) {
        outError[0] = "<manifest> does not contain an <application> or <instrumentation>";
        mParseError = PackageManager.INSTALL_PARSE_FAILED_MANIFEST_EMPTY;
    }

    final int NP = PackageParser.NEW_PERMISSIONS.length;
    StringBuilder implicitPerms = null;
    for (int ip=0; ip<NP; ip++) {
        final PackageParser.NewPermissionInfo npi
                = PackageParser.NEW_PERMISSIONS[ip];
        if (pkg.applicationInfo.targetSdkVersion >= npi.sdkVersion) {
            break;
        }
        if (!pkg.requestedPermissions.contains(npi.name)) {
            if (implicitPerms == null) {
                implicitPerms = new StringBuilder(128);
                implicitPerms.append(pkg.packageName);
                implicitPerms.append(": compat added ");
            } else {
                implicitPerms.append(' ');
            }
            implicitPerms.append(npi.name);
            pkg.requestedPermissions.add(npi.name);
            pkg.requestedPermissionsRequired.add(Boolean.TRUE);
        }
    }
    if (implicitPerms != null) {
        Slog.i(TAG, implicitPerms.toString());
    }

    final int NS = PackageParser.SPLIT_PERMISSIONS.length;
    for (int is=0; is<NS; is++) {
        final PackageParser.SplitPermissionInfo spi
                = PackageParser.SPLIT_PERMISSIONS[is];
        if (pkg.applicationInfo.targetSdkVersion >= spi.targetSdk
                || !pkg.requestedPermissions.contains(spi.rootPerm)) {
            continue;
        }
        for (int in=0; in<spi.newPerms.length; in++) {
            final String perm = spi.newPerms[in];
            if (!pkg.requestedPermissions.contains(perm)) {
                pkg.requestedPermissions.add(perm);
                pkg.requestedPermissionsRequired.add(Boolean.TRUE);
            }
        }
    }

    if (supportsSmallScreens < 0 || (supportsSmallScreens > 0
            && pkg.applicationInfo.targetSdkVersion
                    >= android.os.Build.VERSION_CODES.DONUT)) {
        pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_SMALL_SCREENS;
    }
    if (supportsNormalScreens != 0) {
        pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_NORMAL_SCREENS;
    }
    if (supportsLargeScreens < 0 || (supportsLargeScreens > 0
            && pkg.applicationInfo.targetSdkVersion
                    >= android.os.Build.VERSION_CODES.DONUT)) {
        pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_LARGE_SCREENS;
    }
    if (supportsXLargeScreens < 0 || (supportsXLargeScreens > 0
            && pkg.applicationInfo.targetSdkVersion
                    >= android.os.Build.VERSION_CODES.GINGERBREAD)) {
        pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_XLARGE_SCREENS;
    }
    if (resizeable < 0 || (resizeable > 0
            && pkg.applicationInfo.targetSdkVersion
                    >= android.os.Build.VERSION_CODES.DONUT)) {
        pkg.applicationInfo.flags |= ApplicationInfo.FLAG_RESIZEABLE_FOR_SCREENS;
    }
    if (anyDensity < 0 || (anyDensity > 0
            && pkg.applicationInfo.targetSdkVersion
                    >= android.os.Build.VERSION_CODES.DONUT)) {
        pkg.applicationInfo.flags |= ApplicationInfo.FLAG_SUPPORTS_SCREEN_DENSITIES;
    }

    return pkg;
}

在解析以上方法中调用的类似parseApplication()、parsePermission() …之类的方法就不贴出来了,他们都是从属于parsePackage(),负责解析相应标签的。
而从源码中也可以看到,整个PackageParser做的工作就是从apk中AndroidManifest.xml里各个属性和标签的内容

    原文作者:wusp
    原文地址: https://www.jianshu.com/p/858d5af0ca25
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞