常用的app中,许多都带有了换肤功用,换肤是为了换资源文件,也便是res下边的资源。

<ImageView
    android:layout_width="match_parent"
    android:layout_height="?actionBarSize"
    android:background="@drawable/toolbar" />
<Button
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:onClick="skinSelect"
    android:text="挑选皮肤"
    android:textColor="?colorAccent" />

咱们换肤,比如像上边的imageView和Button,首要便是要替换他的布景或许Color,这就需求了解资源的加载流程了。

资源的加载流程终究都会走到xml解析:

public void setContentView:(int resId) {
    ensureSubDecor();
    ViewGroup contentParent = mSubDecor.findViewById(android.R.id.content);
    contentParent.removeAllViews();
    LayoutInflater.from(mContext).inflate(resId, contentParent);
    mAppCompatWindowCallback.getWrapped().onContentChanged();
}
// 递归解析资源
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    if (DEBUG) {
        Log.d(TAG, "INFLATING from resource: "" + res.getResourceName(resource) + "" ("
              + Integer.toHexString(resource) + ")");
    }
    View view = tryInflatePrecompiled(resource, res, root, attachToRoot);
    if (view != null) {
        return view;
    }
    XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}

终究都会走到这个办法里面android.view.LayoutInflater#createViewFromTag(android.view.View, java.lang.String, android.content.Context, android.util.AttributeSet, boolean):

@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
        boolean ignoreThemeAttr) {
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }
    // Apply a theme wrapper, if allowed and one is specified.
    if (!ignoreThemeAttr) {
        final TypedArray ta = context.obtainStyledAttributes(attrs, ATTRS_THEME);
        final int themeResId = ta.getResourceId(0, 0);
        if (themeResId != 0) {
            context = new ContextThemeWrapper(context, themeResId);
        }
        ta.recycle();
    }
    try {
        // 这儿测验创立View,咱们换肤需求着重重视这儿。这是由于tryCreateView运用了mFactory2来创立View,咱们能够经过这儿来完成自己的逻辑。
        View view = tryCreateView(parent, name, context, attrs);
        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(context, parent, name, attrs);
                } else {
                    view = createView(context, name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }
        return view;
    } catch (InflateException e) {
        throw e;
    } catch (ClassNotFoundException e) {
        final InflateException ie = new InflateException(
                getParserStateDescription(context, attrs)
                + ": Error inflating class " + name, e);
        ie.setStackTrace(EMPTY_STACK_TRACE);
        throw ie;
    } catch (Exception e) {
        final InflateException ie = new InflateException(
                getParserStateDescription(context, attrs)
                + ": Error inflating class " + name, e);
        ie.setStackTrace(EMPTY_STACK_TRACE);
        throw ie;
    }
}
@UnsupportedAppUsage(trackingBug = 122360734)
@Nullable
public final View tryCreateView(@Nullable View parent, @NonNull String name,
    @NonNull Context context,
    @NonNull AttributeSet attrs) {
    if (name.equals(TAG_1995)) {
        // Let's party like it's 1995!
        return new BlinkLayout(context, attrs);
    }
    View view;
    // 重点重视mFactory2。
    if (mFactory2 != null) {
        view = mFactory2.onCreateView(parent, name, context, attrs);
    } else if (mFactory != null) {
        view = mFactory.onCreateView(name, context, attrs);
    } else {
        view = null;
    }
    if (view == null && mPrivateFactory != null) {
        view = mPrivateFactory.onCreateView(parent, name, context, attrs);
    }
    return view;
}

mFactory2是何时创立的呢?在Activity的onCreate的时分,会调用这个办法:

@Override
public void installViewFactory() {
    LayoutInflater layoutInflater = LayoutInflater.from(mContext);
    if (layoutInflater.getFactory() == null) {
        LayoutInflaterCompat.setFactory2(layoutInflater, this);
    } else {
        if (!(layoutInflater.getFactory2() instanceof AppCompatDelegateImpl)) {
            Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                    + " so we can not install AppCompat's");
        }
    }
}

之后体系在创立View的时分就会走这段代码了。

@Override
public View createView(View parent, final String name, @NonNull Context context,
        @NonNull AttributeSet attrs) {
    // 省略无关代码。。。。。
    return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
            IS_PRE_LOLLIPOP, /* Only read android:theme pre-L (L+ handles this anyway) */
            true, /* Read read app:theme as a fallback at all times for legacy reasons */
            VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
    );
}

咱们能够看到,终究走到了androidx.appcompat.app.AppCompatViewInflater#createView(android.view.View, java.lang.String, android.content.Context, android.util.AttributeSet, boolean, boolean, boolean, boolean) 这儿面会依据不同的View来完成对应的实例化,这也是为什么咱们在xml中声明的TextView,会在初始化之后变成 AppCompatTextView。

public final View createView(@Nullable View parent, @NonNull final String name,
        @NonNull Context context,
        @NonNull AttributeSet attrs, boolean inheritContext,
        boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {
    final Context originalContext = context;
    // We can emulate Lollipop's android:theme attribute propagating down the view hierarchy
    // by using the parent's context
    if (inheritContext && parent != null) {
        context = parent.getContext();
    }
    if (readAndroidTheme || readAppTheme) {
        // We then apply the theme on the context, if specified
        context = themifyContext(context, attrs, readAndroidTheme, readAppTheme);
    }
    if (wrapContext) {
        context = TintContextWrapper.wrap(context);
    }
    View view = null;
    // We need to 'inject' our tint aware Views in place of the standard framework versions
    switch (name) {
        case "TextView":
            view = createTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageView":
            view = createImageView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "Button":
            view = createButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "EditText":
            view = createEditText(context, attrs);
            verifyNotNull(view, name);
            break;
        case "Spinner":
            view = createSpinner(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ImageButton":
            view = createImageButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "CheckBox":
            view = createCheckBox(context, attrs);
            verifyNotNull(view, name);
            break;
        case "RadioButton":
            view = createRadioButton(context, attrs);
            verifyNotNull(view, name);
            break;
        case "CheckedTextView":
            view = createCheckedTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "AutoCompleteTextView":
            view = createAutoCompleteTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "MultiAutoCompleteTextView":
            view = createMultiAutoCompleteTextView(context, attrs);
            verifyNotNull(view, name);
            break;
        case "RatingBar":
            view = createRatingBar(context, attrs);
            verifyNotNull(view, name);
            break;
        case "SeekBar":
            view = createSeekBar(context, attrs);
            verifyNotNull(view, name);
            break;
        case "ToggleButton":
            view = createToggleButton(context, attrs);
            verifyNotNull(view, name);
            break;
        default:
            // The fallback that allows extending class to take over view inflation
            // for other tags. Note that we don't check that the result is not-null.
            // That allows the custom inflater path to fall back on the default one
            // later in this method.
            view = createView(context, name, attrs);
    }
    if (view == null && originalContext != context) {
        // If the original context does not equal our themed context, then we need to manually
        // inflate it using the name so that android:theme takes effect.
        view = createViewFromTag(context, name, attrs);
    }
    if (view != null) {
        // If we have created a view, check its android:onClick
        checkOnClickListener(view, attrs);
        backportAccessibilityAttributes(context, view, attrs);
    }
    return view;
}

已然谷歌能够替换掉TextView为AppCompatTextView,是不是咱们也能够完成相似的作用?便是把TextView换成咱们的TextView。

LayoutInflater.from(this).setFactory2(new LayoutInflater.Factory2() {
    @Nullable
    @Override
    public View onCreateView(@Nullable View parent, @NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        // 完成了TextView被Button替换的作用。
        if (TextUtils.equals(name, "TextView")) {
            Button button = new Button(context);
            button.setText("AAAA");
            return button;
        }
        return null;
    }
    @Nullable
    @Override
    public View onCreateView(@NonNull String name, @NonNull Context context, @NonNull AttributeSet attrs) {
        return null;
    }
});

可是需求留意,Factory2只能设置一次,上边的逻辑需求在onCreate的super之前调用。或许经过反射修正mFactorySet的值。

public void setFactory2(Factory2 factory) {
    // mFactorySet有值就会抛出反常。
    if (mFactorySet) {
        throw new IllegalStateException("A factory has already been set on this LayoutInflater");
    }
    if (factory == null) {
        throw new NullPointerException("Given factory can not be null");
    }
    mFactorySet = true;
    if (mFactory == null) {
        mFactory = mFactory2 = factory;
    } else {
        mFactory = mFactory2 = new FactoryMerger(factory, factory, mFactory, mFactory2);
    }
}

Activity发动进程中资源的加载流程

performLaunchActivity

–>createBaseContextForActivity(r)

–> ContextImpl.createActivityContext()

–> context.setResources

–> createResources

–> ResourceImpl impl = findOrCreateResourcesImplForKeyLocked

–> impl = createResourcesImpl(key)

–> assets = createAssetManager(key);

–> builder.addApkAssets(loadApkAssets(key.mResDir, false)

AssetManager 加载资源–>资源途径–>默许传入的资源途径key.mResDir(app下面的res,咱们能够反射改成皮肤包的资源途径—-Resources AssetManager皮肤包的)

不能改变原有的资源加载。独自创立一个AssetManager–>专门加载皮肤包的资源,hide的api是能够直接反射的。

换肤的整体思路

(1)知道xml的View怎样解析?

(2)怎么阻拦体系的创立流程(setFactory2()完成,这样咱们就能自己控制对创立的View的操作了);

(3)阻拦后怎样做?重写体系创立进程的代码(仿制即可);

(4)搜集View以及对应的特点(一切的页面都需求);

(5)拿到皮肤包(也便是apk),进行替换;

(6)怎么运用?运用插件化技能,只用插件的resource。体系的资源是经过Resource和AssetManager进行的,当然,Resource终究也是走的AssetManager.

咱们选用换肤的终究的办法:

—>体系的资源怎么加载?Resource、AssetManager

—>经过Hook技能,创立一个AssetManager。不能用同一个,不影响原流程,由于会形成资源ID抵触,专门加载皮肤包的资源。

—>经过反射addAssetPath 放入皮肤包的途径,从而得到 加载皮肤包资源AssetManager

—>经过app的资源ID—>找到app的资源Name—>皮肤包的资源ID(为什么要这么做?由于咱们换肤的时分给的资源的名肯定是相同的,可是这两个资源的ID在编译之后,在app和皮肤包里面一般是不同的,所以咱们需求经过这种办法来获取到皮肤包的资源ID然后设置给app)

AssetManager 加载资源–>资源途径–>默许传入的资源途径key.mResDir(app下面的res,改成皮肤包的资源途径—-Resources AssetManager皮肤包的)

代码完成

首先咱们需求自界说一个 SkinLayoutInflaterFactory 来阻拦创立View的流程,由于咱们需求记载每一个View的特点,以便后续换肤的时分来对特点进行修正。这儿之所以还需求完成Observer,是由于咱们换肤之后,需求运用观察者形式告诉一切的界面进行更新。因而每一个界面都应该注册一个观察者。

public class SkinLayoutInflaterFactory implements LayoutInflater.Factory2, Observer {

因而,咱们需求界说一个ApplicationActivityLifecycle,用来在Activity创立的时分,将每一个观察者都传递进去,也便是每一个SkinLayoutInflaterFactory,这样每一个界面都适当于是观察者。

public class ApplicationActivityLifecycle implements Application.ActivityLifecycleCallbacks {

那么被观察者是谁呢?咱们界说一个被观察者,在加载新皮肤之后,会告诉一切的观察者,也便是一切的页面,去更新。

public class SkinManager extends Observable {
    private volatile static SkinManager instance;
    /**
     * Activity生命周期回调
     */
    private ApplicationActivityLifecycle skinActivityLifecycle;
    private Application mContext;
    /**
     * 初始化 必须在Application中先进行初始化
     */
    public static void init(Application application) {
        if (instance == null) {
            synchronized (SkinManager.class) {
                if (instance == null) {
                    instance = new SkinManager(application);
                }
            }
        }
    }
    private SkinManager(Application application) {
        mContext = application;
        // 同享首选项 用于记载当前运用的皮肤
        SkinPreference.init(application);
        // 资源管理类 用于从 app/皮肤 中加载资源
        SkinResources.init(application);
        // 注册Activity生命周期,并设置被观察者
        skinActivityLifecycle = new ApplicationActivityLifecycle(this);
        application.registerActivityLifecycleCallbacks(skinActivityLifecycle);
        // 加载上次运用保存的皮肤
        loadSkin(SkinPreference.getInstance().getSkin());
    }
    public static SkinManager getInstance() {
        return instance;
    }
    /**
     * 记载皮肤并运用
     *
     * @param skinPath 皮肤途径 假如为空则运用默许皮肤
     */
    public void loadSkin(String skinPath) {
        if (TextUtils.isEmpty(skinPath)) {
            // 还原默许皮肤
            SkinPreference.getInstance().reset();
            SkinResources.getInstance().reset();
        } else {
            try {
                // 反射创立AssetManager 与 Resource
                AssetManager assetManager = AssetManager.class.newInstance();
                // 资源途径设置 目录或压缩包
                Method addAssetPath = assetManager.getClass().getMethod("addAssetPath", String.class);
                addAssetPath.invoke(assetManager, skinPath);
                // 宿主app的 resources;
                Resources appResource = mContext.getResources();
                // 依据当前的设备显示器信息 与 配置(横竖屏、语言等) 创立Resources
                Resources skinResource = new Resources(assetManager, appResource.getDisplayMetrics(), appResource.getConfiguration());
                // 获取外部Apk(皮肤包) 包名
                PackageManager mPm = mContext.getPackageManager();
                PackageInfo info = mPm.getPackageArchiveInfo(skinPath, PackageManager.GET_ACTIVITIES);
                String packageName = info.packageName;
                SkinResources.getInstance().applySkin(skinResource, packageName);
                // 记载途径
                SkinPreference.getInstance().setSkin(skinPath);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        // 告诉采集的View 更新皮肤
        // 被观察者改变 告诉一切观察者
        setChanged();
        notifyObservers(null);
    }
}

别的,咱们还需求一个类,用来解析一切的View的特点解析,同时也供给更新特点的办法,以便确定咱们对哪些特点来进行替换,一般的,咱们不需求对一切的特点都进行替换:

/**
 * 这儿面放了一切要换肤的view所对应的特点
 */
public class SkinAttribute {
    /**
     * 只要这些特点,咱们才需求进行替换,其他的不必。
     */
    private static final List<String> mAttributes = new ArrayList<>();
    static {
        mAttributes.add("background");
        mAttributes.add("src");
        mAttributes.add("textColor");
        mAttributes.add("drawableLeft");
        mAttributes.add("drawableTop");
        mAttributes.add("drawableRight");
        mAttributes.add("drawableBottom");
    }
    /**
     * 记载换肤需求操作的View与特点信息
     */
    private final List<SkinView> mSkinViews = new ArrayList<>();
    /**
     * 记载下一个VIEW身上哪几个特点需求换肤textColor/src,每一个页面的view都需求搜集。
     *
     * @param view
     * @param attrs
     */
    public void look(View view, AttributeSet attrs) {
        List<SkinPair> mSkinPars = new ArrayList<>();
        for (int i = 0; i < attrs.getAttributeCount(); i++) {
            //获得特点名  textColor/background
            String attributeName = attrs.getAttributeName(i);
            if (mAttributes.contains(attributeName)) {
                // 获取特点值
                String attributeValue = attrs.getAttributeValue(i);
                // 比如color 以#最初表明写死的颜色 不行用于换肤
                if (attributeValue.startsWith("#")) {
                    continue;
                }
                int resId;
                // 以 ?最初的表明运用 特点
                if (attributeValue.startsWith("?")) {
                    int attrId = Integer.parseInt(attributeValue.substring(1));
                    resId = SkinThemeUtils.getResId(view.getContext(), new int[]{attrId})[0];
                } else {
                    // 正常以 @ 最初
                    resId = Integer.parseInt(attributeValue.substring(1));
                }
                // 保存特点名以及对应的ID,每一个特点都有对应的ID,为后边映射皮肤包ID做准备。
                SkinPair skinPair = new SkinPair(attributeName, resId);
                mSkinPars.add(skinPair);
            }
        }
        if (!mSkinPars.isEmpty() || view instanceof SkinViewSupport) {
            SkinView skinView = new SkinView(view, mSkinPars);
            // 假如挑选过皮肤 ,调用 一次 applySkin 加载皮肤的资源
            skinView.applySkin();
            mSkinViews.add(skinView);
        }
    }
    /*
       对一切的view中的一切的特点进行皮肤修正
     */
    public void applySkin() {
        // 第一层循环,找到一切的View
        for (SkinView mSkinView : mSkinViews) {
            mSkinView.applySkin();
        }
    }
    static class SkinView {
        View view;
        //这个View的能被 换肤的特点与它对应的id 集合
        List<SkinPair> skinPairs;
        public SkinView(View view, List<SkinPair> skinPairs) {
            this.view = view;
            this.skinPairs = skinPairs;
        }
        /**
         * 对一个View中的一切的特点进行修正
         */
        public void applySkin() {
            applySkinSupport();
            // 第二层循环,找到一切的view对应的特点,然后进行设置,完成真实的作用更改。
            for (SkinPair skinPair : skinPairs) {
                Drawable left = null, top = null, right = null, bottom = null;
                switch (skinPair.attributeName) {
                    case "background":
                        // 这一行就完成了:经过app的资源ID--->找到app的资源Name--->皮肤包的资源ID
                        Object background = SkinResources.getInstance().getBackground(skinPair.resId);
                        //布景可能是 @color 也可能是 @drawable
                        if (background instanceof Integer) {
                            view.setBackgroundColor((int) background);
                        } else {
                            ViewCompat.setBackground(view, (Drawable) background);
                        }
                        break;
                    case "src":
                        // 这一行就完成了:经过app的资源ID--->找到app的资源Name--->皮肤包的资源ID
                        background = SkinResources.getInstance().getBackground(skinPair.resId);
                        if (background instanceof Integer) {
                            ((ImageView) view).setImageDrawable(new ColorDrawable((Integer)
                                    background));
                        } else {
                            ((ImageView) view).setImageDrawable((Drawable) background);
                        }
                        break;
                    case "textColor":
                        ((TextView) view).setTextColor(SkinResources.getInstance().getColorStateList(skinPair.resId));
                        break;
                    case "drawableLeft":
                        left = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableTop":
                        top = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableRight":
                        right = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    case "drawableBottom":
                        bottom = SkinResources.getInstance().getDrawable(skinPair.resId);
                        break;
                    default:
                        break;
                }
                if (null != left || null != right || null != top || null != bottom) {
                    ((TextView) view).setCompoundDrawablesWithIntrinsicBounds(left, top, right, bottom);
                }
            }
        }
        private void applySkinSupport() {
            if (view instanceof SkinViewSupport) {
                ((SkinViewSupport) view).applySkin();
            }
        }
    }
    static class SkinPair {
        /**
         * 特点名
         */
        String attributeName;
        /**
         * 对应的资源id
         */
        int resId;
        public SkinPair(String attributeName, int resId) {
            this.attributeName = attributeName;
            this.resId = resId;
        }
    }
}

总结

(1)经过自界说 LayoutInflater.Factory2来完成可控制的View创立进程,让咱们能够对一切的创立的View,搜集他们需求换肤的特点,以及特点对应的ID;

(2)界说观察者与被观察者,观察者便是每一个页面,被观察者便是换肤类。当换肤类触发了换肤之后,会经过状态分发让一切页面更新,假如还没有发动的Activiyt是否就不能更新了呢?当然不会,由于咱们的能够直接调用 SkinManager.getInstance().loadSkin(skinPkg),来完成未发动的页面的更新。

(3)经过app的资源ID—>找到app的资源Name—>皮肤包的资源ID(为什么要这么做?由于咱们换肤的时分给的资源的名肯定是相同的,可是这两个资源的ID在编译之后,在app和皮肤包里面一般是不同的,所以咱们需求经过这种办法来获取到皮肤包的资源ID然后设置给app),这也是为什么咱们在最初解析的时分需求运用下边的代码:

static class SkinPair {
    /**
     * 特点名
     */
    String attributeName;
    /**
     * 对应的资源id
     */
    int resId;
    public SkinPair(String attributeName, int resId) {
        this.attributeName = attributeName;
        this.resId = resId;
    }
}

测验源码:github.com/xingchaozha…

几个问题

1、自界说View的制作流程:

Android完成动态换肤
1、View几个结构办法的区别?

public class CustomView extends View  {
 /**
     * 一般在直接new一个view的时分运用
     * @param context
     */
    public CustomView(Context context) {
        super(context);
    }
    /**
     * 一般在layout文件中运用的时分回调用,关于它的特点(包括自界说特点)都会在attrs中传递进来。
     * @param context
     * @param attrs
     */
    public BigView(Context context, @Nullable AttributeSet attrs) {
        super(context, attrs);
       // <com.example.MyView
       //     android:layout_width="wrap_content"
       //     android:layout_height="wrap_content"
       //     custom:myAttribute="value" />
        //假如在这儿这样写就会调用到三个参数的结构办法
        //this(context, attrs,0);
    }
    /**
     * @param context
     * @param attrs
     * @param defStyleAttr 默许的style,指的是当期application或许activity所用的theme中默许的style,
     *                     且只要清晰调用的时分才会收效,
     *                     如 this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
     *                     留意:即便在view中运用了style这个特点,也不会调用这个结构办法,所以这个结构办法
     *                     也不考虑。
     */
    public CustomView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        //this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
    }
   /**
     * android5.0今后的api才有
     * 假如第三个参数为0或许没有界说defStyleAttr时,第四个参数才起作用,它是style的引证.
     * 不同于`defStyleAttr`,`defStyleAttr`是在当前主题中查找样式,
     * 而`defStyleRes`直接引证一个清晰的样式资源。这个结构办法供给了更多的灵活性来处理视图的样式。
     * @param context
     * @param attrs
     * @param defStyleAttr
     * @param defStyleRes
     */
    @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
    public BigView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
    }
}

3、 咱们自界说View的时分,在运用的时分需求加上全途径,为什么体系内置的LinearLayout,Relativelayout这些不必呢?请看这儿:

protected View onCreateView(String name, AttributeSet attrs)
        throws ClassNotFoundException {
    return createView(name, "android.view.", attrs);
}

由于他们在创立的时分把前缀加上去了。

4、 换肤的进程中,比如Color中直接经过#FF00000,这种办法, 是无法替换的,由于这种只要ID,而没有name。无法映射到皮肤包中。