添加系统级res资源包

//

刚做完自定义res资源包的配置,这里做一下关于在配置过程中出现的问题和解决方法作一下记录。

资源的引用格式为:

@包名:资源类型/资源名

以framework资源为例:

@android:style/Theme.Holo.Light

这次需要配置与framework同级的资源包,以包名为"custemer"为例,配置完成后资源引用:

@custemer:style/Theme.Holo.Light
一、新建自定义资源包
  1. 在framework/base/core/下新建名为“res_custemer”文件夹,结构如下:
  2. 编写Android.mk/Android.bp文件
LOCAL_PATH := $(call my-dir)
include $(CLEAR_VARS)

# include apicheck.mk later, we need the build pass to prepare the first version
# include $(LOCAL_PATH)/apicheck.mk
LOCAL_PACKAGE_NAME := custemer-res
LOCAL_CERTIFICATE := platform
LOCAL_AAPT_FLAGS := -x3

# Tell aapt to build resource in utf16(the ROM will be enlarged),
# in order to save RAM size for string cache table
ifeq (yes,strip$(MTK_GMO_RAM_OPTIMIZE))
LOCAL_AAPT_FLAGS += --utf16
endif


LOCAL_NO_CUSTEMERRES := true
LOCAL_MODULE_TAGS := optional
# Install this alongside the libraries.
LOCAL_MODULE_PATH := $(TARGET_OUT_JAVA_LIBRARIES)
# Create package-export.apk, which other packages can use to get
# PRODUCT-agnostic resource data like IDs and type definitions.
LOCAL_EXPORT_PACKAGE_RESOURCES := true
include $(BUILD_PACKAGE)

# define a global intermediate target that other module may depend on.
.PHONY: gome-res-package-target
gome-res-package-target: $(LOCAL_BUILT_MODULE)

"LOCAL_AAPT_FLAGS := -x3 " value 的定义需要与 "frameworks\base\libs\androidfw\ResourceTypes.cpp"中 所定义的resource ID 一致

//
// Copyright (C) 2008 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//

android_app {
    name: "xxx-res", 
    certificate: "platform",
    platform_apis: true,

    // Soong special-cases framework-res to install this alongside
    // the libraries at /system/framework/framework-res.apk.

    // Generate private symbols into the com.android.internal.R class
    // so they are not accessible to 3rd party apps.
    aaptflags: [
        // Framework doesn't need versioning since it IS the platform.
        "--no-auto-version",

        // Allow overlay to add resource
        "--auto-add-overlay",

        // Add for xxx
        "--package-id 0x0a",

        "--private-symbols",
        "com.xxx.internal",
    ],

    owner: "xxx",

    // Create package-export.apk, which other packages can use to get
    // PRODUCT-agnostic resource data like IDs and type definitions.
    export_package_resources: true,

    defaults: ["getxxxResManifest"],
}

bootstrap_go_package {
    name: "soong-getxxxResManifest",
    pkgPath: "android/soong/getxxxResManifest",
    deps: [
        "blueprint",
        "blueprint-pathtools",
        "soong",
        "soong-android",
        "soong-cc",
        "soong-genrule",
	"soong-apex",
    ],
    srcs: [
        "getxxxResManifest.go",
    ],
    pluginFor: ["soong_build"],
}

getSunmiResManifest {
    name: "getxxxResManifest",
}
  1. 编写AndroidManifest.xml文件
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="custemer" coreApp="true" android:sharedUserId="android.uid.system"
    android:sharedUserLabel="@null">

    <eat-comment />
    <protected-broadcast android:name="android.intent.action.SCREEN_OFF" />

    <permission android:name="android.permission.ADVANCED_WIDGET_API"
            android:protectionLevel="normal" />
    
    <application android:process="system"
                 android:persistent="true"
                 android:hasCode="false"
                 android:label="@null"
                 android:allowClearUserData="false"
                 android:killAfterRestore="false"
                 android:icon="@null">

    </application>

</manifest>

"custemer" 为资源包名

二、添加编译依赖关系

framework_custemer_res_package_export :=
framework_custemer_res_package_export_deps :=

ifneq ($(LOCAL_PACKAGE_NAME), mediatek-res)
    framework_custemer_res_package_export += \
        $(call intermediates-dir-for,APPS,mediatek-res,,COMMON)/package-export.apk
    framework_custemer_res_package_export_deps += \
        $(call intermediates-dir-for,APPS,mediatek-res,,COMMON)/src/R.stamp

    ifneq ($(LOCAL_PACKAGE_NAME),custemer-res)
        framework_custemer_res_package_export += \
            $(call intermediates-dir-for,APPS,gome-res,,COMMON)/package-export.apk
        framework_custemer_res_package_export_deps += \
            $(call intermediates-dir-for,APPS,gome-res,,COMMON)/src/R.stamp
    endif
endif

  # FIXME: Cannot set in Android.mk due to base_rules.mk
  #ifneq ($(LOCAL_PACKAGE_NAME),mediatek-res)
  #   LOCAL_RES_LIBRARIES += mediatek-res
  #endif
....
$(filter-out $(framework_gome_res_package_export),$(resource_export_package)): $(framework_custemer_res_package_export_deps )
$(R_file_stamp): $(framework_custemer_res_package_export_deps )

$(resource_export_package) $(R_file_stamp) $(LOCAL_BUILT_MODULE): $(all_library_res_package_export_deps)
$(LOCAL_INTERMEDIATE_TARGETS): \
    PRIVATE_AAPT_INCLUDES := $(all_library_res_package_exports) $(framework_custemer_res_package_export )

将 mediatek-res与自定义资源模块名定义在一起,否则编译时会出现circle dependence的错误;
将 framework_gome_res_package_export 添加进 PRIVATE_AAPT_INCLUDES 中,否则应用运行会报找不到资源的错误。

三、将资源路径添加进framework编译

路径: frameworks/base/Android.mk

framework_res_source_path := APPS/framework-res_intermediates/src
# M:add mediatek resource path
mediatek-res-source-path := APPS/mediatek-res_intermediates/src
gome_custemer_source_path := APPS/custemer-res_intermediates/src
...
LOCAL_INTERMEDIATE_SOURCES := \
            $(framework_res_source_path)/android/R.java \
            $(framework_res_source_path)/android/Manifest.java \
            $(framework_res_source_path)/com/android/internal/R.java
# M:add mediatek resource R.java into framework,@{
LOCAL_INTERMEDIATE_SOURCES += \
            $(mediatek-res-source-path)/com/mediatek/internal/R.java \
            $(mediatek-res-source-path)/com/mediatek/R.java \
            $(mediatek-res-source-path)/com/mediatek/Manifest.java
# @}

# M:add custemer resource R.java into framework,@{
LOCAL_INTERMEDIATE_SOURCES += \
            $(custemer_res_source_path)/com/gome/internal/R.java \
            $(custemer_res_source_path)/com/gome/R.java \
            $(custemer_res_source_path)/com/gome/Manifest.java
# @}


LOCAL_MODULE := framework

# Make sure that R.java and Manifest.java are built before we build
# the source for this library.
framework_res_R_stamp := \
    $(call intermediates-dir-for,APPS,framework-res,,COMMON)/src/R.stamp
$(full_classes_compiled_jar): $(framework_res_R_stamp)
$(built_dex_intermediate): $(framework_res_R_stamp)
# M:add mediatek resource dependes framework->mediatek_res->framework_res,@{
mediatek_res_R_stamp := \
    $(call intermediates-dir-for,APPS,mediatek-res,,COMMON)/src/R.stamp
$(full_classes_compiled_jar): $(mediatek_res_R_stamp)
$(built_dex_intermediate): $(mediatek_res_R_stamp)
custemer_res_R_stamp := \
    $(call intermediates-dir-for,APPS,custemer-res,,COMMON)/src/R.stamp
$(full_classes_compiled_jar): $(custemer_res_R_stamp)
$(built_dex_intermediate): $(custemer_res_R_stamp)
# @}
四、修改 PackageManagerService

路径:frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java

   ApplicationInfo mAndroidApplication;
    /// M: [CIP] Add application info for mediatek-res.apk
   ApplicationInfo mMediatekApplication;
   ApplicationInfo mCustemerApplication;

   private PackageParser.Package scanPackageDirtyLI(PackageParser.Package pkg,
            final int policyFlags, final int scanFlags, long currentTime, UserHandle user)
            throws PackageManagerException {    
             /** M: [CIP] skip duplicated mediatek-res.apk @{ */
        /// This will replace original resources with CIP resources
        if (pkg.packageName.equals("com.mediatek")) {
            synchronized (mPackages) {
                if (mMediatekApplication != null) {
                    Slog.w(TAG, "*************************************************");
                    Slog.w(TAG, "Core mediatek package being redefined.  Skipping.");
                    Slog.w(TAG, " file=" + scanFile);
                    Slog.w(TAG, "*************************************************");
                    throw new PackageManagerException(INSTALL_FAILED_DUPLICATE_PACKAGE,
                            "Core android package being redefined.  Skipping.");
                }
                mMediatekApplication = pkg.applicationInfo;
            }
        }
        /** @} */
        if (pkg.packageName.equals("custemer")) {
            synchronized (mPackages) {
                if (mCustemerApplication != null) {
                    Slog.w(TAG, "*************************************************");
                    Slog.w(TAG, "Core gome package being redefined.  Skipping.");
                    Slog.w(TAG, " file=" + scanFile);
                    Slog.w(TAG, "*************************************************");
                    throw new PackageManagerException(INSTALL_FAILED_DUPLICATE_PACKAGE,
                            "Core android package being redefined.  Skipping.");
                }
                mCustemerApplication = pkg.applicationInfo;
            }
        }
  }

别的版本frameworks\base\services/core/java/com/android/server/pm/InstallPackageHelper.java

private void assertPackageIsValid(AndroidPackage pkg,
            final @ParsingPackageUtils.ParseFlags int parseFlags,
            final @PackageManagerService.ScanFlags int scanFlags)
            throws PackageManagerException {
        if ((parseFlags & ParsingPackageUtils.PARSE_ENFORCE_CODE) != 0) {
            ScanPackageUtils.assertCodePolicy(pkg);
        }

        if (pkg.getPath() == null) {
            // Bail out. The resource and code paths haven't been set.
            throw new PackageManagerException(INSTALL_FAILED_INVALID_APK,
                    "Code and resource paths haven't been set correctly");
        }

        // Check that there is an APEX package with the same name only during install/first boot
        // after OTA.
        final boolean isUserInstall = (scanFlags & SCAN_BOOTING) == 0;
        final boolean isFirstBootOrUpgrade = (scanFlags & SCAN_FIRST_BOOT_OR_UPGRADE) != 0;
        // It is allowed to install a new APEX with the same name. But there shouldn't be
        // conflicting names between APK and APEX.
        final boolean installApex = (scanFlags & SCAN_AS_APEX) != 0;
        if ((isUserInstall || isFirstBootOrUpgrade)
                && mPm.snapshotComputer().isApexPackage(pkg.getPackageName())
                && !installApex) {
            throw new PackageManagerException(INSTALL_FAILED_DUPLICATE_PACKAGE,
                    pkg.getPackageName()
                            + " is an APEX package and can't be installed as an APK.");
        }

        // Make sure we're not adding any bogus keyset info
        final KeySetManagerService ksms = mPm.mSettings.getKeySetManagerService();
        ksms.assertScannedPackageValid(pkg);

        synchronized (mPm.mLock) {
            // The special "android" package can only be defined once
            if (pkg.getPackageName().equals("android")) {
                if (mPm.getCoreAndroidApplication() != null) {
                    Slog.w(TAG, "*************************************************");
                    Slog.w(TAG, "Core android package being redefined.  Skipping.");
                    Slog.w(TAG, " codePath=" + pkg.getPath());
                    Slog.w(TAG, "*************************************************");
                    throw new PackageManagerException(INSTALL_FAILED_DUPLICATE_PACKAGE,
                            "Core android package being redefined.  Skipping.");
                }
            }
            
            if (pkg.getPackageName().equals("com.xxx")) {
                if (mPm.getxxxApplication() != null) {
                    Slog.w(TAG, "*************************************************");
                    Slog.w(TAG, "Core android package being redefined.  Skipping.");
                    Slog.w(TAG, " codePath=" + pkg.getPath());
                    Slog.w(TAG, "*************************************************");
                    throw new PackageManagerException(INSTALL_FAILED_DUPLICATE_PACKAGE,
                            "Core android package being redefined.  Skipping.");
                }
                //mPm.mxxxApplication = pkg.toAppInfoWithoutState();
            }

            /// M: [CIP] skip duplicated mediatek-res.apk
            mPm.sPmsExt.checkMtkResPkg(pkg);

            // A package name must be unique; don't allow duplicates
            if ((scanFlags & SCAN_NEW_INSTALL) == 0
                    && mPm.mPackages.containsKey(pkg.getPackageName())) {
                throw new PackageManagerException(INSTALL_FAILED_DUPLICATE_PACKAGE,
                        "Application package " + pkg.getPackageName()
                                + " already installed.  Skipping duplicate.");
            }

            if (pkg.isStaticSharedLibrary()) {
                // Static libs have a synthetic package name containing the version
                // but we still want the base name to be unique.
                if ((scanFlags & SCAN_NEW_INSTALL) == 0
                        && mPm.mPackages.containsKey(pkg.getManifestPackageName())) {
                    throw PackageManagerException.ofInternalError(
                            "Duplicate static shared lib provider package",
                            PackageManagerException.INTERNAL_ERROR_DUP_STATIC_SHARED_LIB_PROVIDER);
                }
                ScanPackageUtils.assertStaticSharedLibraryIsValid(pkg, scanFlags);
                assertStaticSharedLibraryVersionCodeIsValid(pkg);
            }

            // If we're only installing presumed-existing packages, require that the
            // scanned APK is both already known and at the path previously established
            // for it.  Previously unknown packages we pick up normally, but if we have an
            // a priori expectation about this package's install presence, enforce it.
            // With a singular exception for new system packages. When an OTA contains
            // a new system package, we allow the codepath to change from a system location
            // to the user-installed location. If we don't allow this change, any newer,
            // user-installed version of the application will be ignored.
            if ((scanFlags & SCAN_REQUIRE_KNOWN) != 0) {
                if (mPm.isExpectingBetter(pkg.getPackageName())) {
                    Slog.w(TAG, "Relax SCAN_REQUIRE_KNOWN requirement for package "
                            + pkg.getPackageName());
                } else {
                    PackageSetting known = mPm.mSettings.getPackageLPr(pkg.getPackageName());
                    if (known != null) {
                        if (DEBUG_PACKAGE_SCANNING) {
                            Log.d(TAG, "Examining " + pkg.getPath()
                                    + " and requiring known path " + known.getPathString());
                        }
                        if (!pkg.getPath().equals(known.getPathString())) {
                            throw new PackageManagerException(INSTALL_FAILED_PACKAGE_CHANGED,
                                    "Application package " + pkg.getPackageName()
                                            + " found at " + pkg.getPath()
                                            + " but expected at " + known.getPathString()
                                            + "; ignoring.");
                        }
                    } else {
                        throw new PackageManagerException(INSTALL_FAILED_INVALID_INSTALL_LOCATION,
                                "Application package " + pkg.getPackageName()
                                        + " not found; ignoring.");
                    }
                }
            }

            // Verify that this new package doesn't have any content providers
            // that conflict with existing packages.  Only do this if the
            // package isn't already installed, since we don't want to break
            // things that are installed.
            if ((scanFlags & SCAN_NEW_INSTALL) != 0) {
                mPm.mComponentResolver.assertProvidersNotDefined(pkg);
            }

            // If this package has defined explicit processes, then ensure that these are
            // the only processes used by its components.
            ScanPackageUtils.assertProcessesAreValid(pkg);

            // Verify that packages sharing a user with a privileged app are marked as privileged.
            assertPackageWithSharedUserIdIsPrivileged(pkg);

            // Apply policies specific for runtime resource overlays (RROs).
            if (pkg.getOverlayTarget() != null) {
                assertOverlayIsValid(pkg, parseFlags, scanFlags);
            }

            // Ensure the package is signed with at least the minimum signature scheme version
            // required for its target SDK.
            ScanPackageUtils.assertMinSignatureSchemeIsValid(pkg, parseFlags);
        }
    }

五、预置自定义资源包

目录:device/mediatek/common/device.mk

# for mediatek-res
PRODUCT_PACKAGES += mediatek-res

# for custemer-res
PRODUCT_PACKAGES += custemer-res
六、修改资源管理器 Assetmanager.cpp AssetManager.java

目录:frameworks\base\core\java\android\content\res\AssetManager.java

public final class AssetManager implements AutoCloseable {
    private static final String TAG = "AssetManager";
    private static final boolean DEBUG_REFS = false;

    private static final String FRAMEWORK_APK_PATH = "/system/framework/framework-res.apk";

    private static final String MY_FRAMEWORK_APK_PATH = "/system/app/my-res/my-res.apk";
@GuardedBy("sSync")
    @VisibleForTesting
    public static void createSystemAssetsInZygoteLocked(boolean reinitialize,
            String frameworkPath) {
        if (sSystem != null && !reinitialize) {
            return;
        }

        try {
            final ArrayList<ApkAssets> apkAssets = new ArrayList<>();
            apkAssets.add(ApkAssets.loadFromPath(frameworkPath, ApkAssets.PROPERTY_SYSTEM));

            final String[] systemIdmapPaths =
                    OverlayConfig.getZygoteInstance().createImmutableFrameworkIdmapsInZygote();
            for (String idmapPath : systemIdmapPaths) {
                apkAssets.add(ApkAssets.loadOverlayFromPath(idmapPath, ApkAssets.PROPERTY_SYSTEM));
            }
            apkAssets.add(ApkAssets.loadFromPath(MY_FRAMEWORK_APK_PATH, ApkAssets.PROPERTY_SYSTEM));

            ///M: for load mediatek-res
            apkAssets.add(ApkAssets.loadFromPath(MEDIATEK_APK_PATH, ApkAssets.PROPERTY_SYSTEM /*system*/));

            sSystemApkAssetsSet = new ArraySet<>(apkAssets);
            sSystemApkAssets = apkAssets.toArray(new ApkAssets[apkAssets.size()]);
            if (sSystem == null) {
                sSystem = new AssetManager(true /*sentinel*/);
            }
            sSystem.setApkAssets(sSystemApkAssets, false /*invalidateCaches*/);
        } catch (IOException e) {
            throw new IllegalStateException("Failed to create system AssetManager", e);
        }
    }

目录:framework/base/libs/androidfw/AssetManager.cpp

///M:add the resource path
static const char* kMediatekAssets = "/vendor/framework/mediatek-res/mediatek-res.apk";
///M:add the resource path
static const char* kCustemerAssets = "framework/custemer-res/custemer-res.apk";

bool AssetManager::addDefaultAssets()
{
    const char* root = getenv("ANDROID_ROOT");
    LOG_ALWAYS_FATAL_IF(root == NULL, "ANDROID_ROOT not set");

    ...
    if((fp= fopen(pathCip,"w+"))!= NULL) {
       .....
       return isOK1;
    } else {
       //ALOGD("AssetManager-->addDefaultAssets CIP path not exsit!");
       String8 path(root);
       path.appendPath(kSystemAssets);
       ///M:add the new resource path into default path,so all the app can reference,@{
       bool isOK1 = addAssetPath(path, NULL, false /* appAsLib */, true /* isSystemAsset */);
       String8 path2(kMediatekAssets);
       bool isOK2 = addAssetPath(path2, NULL, false, false);

       String8 path3(root);
       path3.appendPath(kCustemerAssets);
       bool isOK3 =addAssetPath(path3, NULL, false, false);
       if(!isOK3){
            ALOGW("AssetManager-->addDefaultAssets isok3 is false");
       }else{
            ALOGW("AssetManager-->addDefaultAssets isok3 is true");
       }

将自定义资源apk添加进系统资源管理器,当应用使用自定义资源id查找资源时,会通过AssetManager来查找。如果这里写错会导致会先各种问题。以字符串为例,当应用通过R.strings.xxx来查找字符串时,由于AssetManager无法返回正确的资源,默认会返回资源的id值。

六、修改 ResourceTypes.cpp,添加对应的package id
#define APP_PACKAGE_ID      0x7f
#define SYS_MTK_PACKAGE_ID  0x08 /// M: 0x08 is mediatek-res.apk resource ID
#define SYS_CUSTEMER_PACKAGE_ID  0x03 // 0x03 is gome-res.apk resource ID
#define SYS_PACKAGE_ID      0x01

bool ResTable::stringToValue(Res_value* outValue, String16* outString,
                             const char16_t* s, size_t len,
                             bool preserveSpaces, bool coerceType,
                             uint32_t attrID,
                             const String16* defType,
                             const String16* defPackage,
                             Accessor* accessor,
                             void* accessorCookie,
                             uint32_t attrType,
                             bool enforcePrivate) const
{
    if (*s == '@') {
        ....
      
    } else {
        if (rid != 0) {
                uint32_t packageId = Res_GETPACKAGE(rid) + 1;
                if (packageId != APP_PACKAGE_ID && packageId != SYS_PACKAGE_ID && packageId != SYS_MTK_PACKAGE_ID && packageId != SYS_CUSTEMER_PACKAGE_ID) {
                    outValue->dataType = Res_value::TYPE_DYNAMIC_REFERENCE;
                }
                outValue->data = rid;
                return true;
            }

            if (accessor) {
                uint32_t rid = accessor->getCustomResourceWithCreation(package, type, name,
                                                                       createIfNotFound);
                if (rid != 0) {
                    if (kDebugTableNoisy) {
                        ALOGI("Pckg %s:%s/%s: 0x%08x\n",
                                String8(package).string(), String8(type).string(),
                                String8(name).string(), rid);
                    }
                    uint32_t packageId = Res_GETPACKAGE(rid) + 1;
                    if (packageId == 0x00) {
                        outValue->data = rid;
                        outValue->dataType = Res_value::TYPE_DYNAMIC_REFERENCE;
                        return true;
                    } else if (packageId == APP_PACKAGE_ID || packageId == SYS_PACKAGE_ID || packageId == SYS_MTK_PACKAGE_ID || packageId == SYS_CUSTEMER_PACKAGE_ID) {
                        // We accept packageId's generated as 0x01 in order to support
                        // building the android system resources
                        outValue->data = rid;
                        return true;
                    }
                }
            }
    }

DynamicRefTable::DynamicRefTable(uint8_t packageId, bool appAsLib)
    : mAssignedPackageId(packageId)
    , mAppAsLib(appAsLib)
{
    memset(mLookupTable, 0, sizeof(mLookupTable));

    // Reserved package ids
    mLookupTable[APP_PACKAGE_ID] = APP_PACKAGE_ID;
    mLookupTable[SYS_PACKAGE_ID] = SYS_PACKAGE_ID;
    mLookupTable[SYS_MTK_PACKAGE_ID] = SYS_MTK_PACKAGE_ID;
    mLookupTable[SYS_CUSTEMER_PACKAGE_ID] = SYS_CUSTEMER_PACKAGE_ID;
}

}

SYS_CUSTEMER_PACKAGE_ID 需要对应与自定义模块mk文件的 LOCAL_AAPT_FLAGS

七、将资源apk路径添加进白名单

路径:frameworks/base/core/jni

static const char* kPathWhitelist[] = {
  "/dev/null",
  "/dev/socket/zygote",
  "/dev/socket/zygote_secondary",
  "/system/etc/event-log-tags",
  "/sys/kernel/debug/tracing/trace_marker",
  "/system/framework/framework-res.apk",
  "/dev/urandom",
  "/dev/ion",
  "/dev/dri/renderD129", // Fixes b/31172436
  "/proc/ged", // MTK add
  "/system/vendor/framework/mediatek-res/mediatek-res.apk", // MTK add
  "/system/framework/custemer-res/custemer-res.apk" /* XXX add */
};

修改的目的是为了通过开机的检查,否则无法开机。

关于如何使用,就不作记录了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xiaowang_lj

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值