资源访问机制之资源定义与解析流程

转载请注明出处:http://blog.csdn.net/droyon/article/details/22429191

本文主要介绍一下问题:

1、android资源相关的简要介绍。2、xml文件,在android中的加载、解析过程。3、以layout文件为例,介绍layout文件是如何一步一步的被andrioid加载、解析、生成View树。

--------------------------------------------------------------------------------------------------------------------------------------------

Android的资源类型包括:drawable、layout、string、color、menu、animation等。这些资源我们会定义在xml文件中,在具体使用时,我们加载xml,但是xml里定义的属性和赋值是如何被android解析的,这是本文要介绍的重点:

xml文件样例:

<view xmlns:android="http://schemas.android.com/apk/res/android"
      class="com.android.internal.widget.ActionBarView$HomeView"
      android:layout_width="wrap_content"
      android:layout_height="match_parent"
      android:background="?android:attr/actionBarItemBackground" >
    <ImageView android:id="@android:id/up"
               android:src="?android:attr/homeAsUpIndicator"
               android:layout_gravity="center_vertical|left"
               android:visibility="gone"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:layout_marginRight="-8dip" />
    <ImageView android:id="@android:id/home"
               android:layout_width="wrap_content"
               android:layout_height="wrap_content"
               android:layout_marginRight="8dip"
               android:layout_marginTop="@android:dimen/action_bar_icon_vertical_padding"
               android:layout_marginBottom="@android:dimen/action_bar_icon_vertical_padding"
               android:layout_gravity="center"
               android:adjustViewBounds="true"
               android:scaleType="fitCenter" />
</view>
一、我们为什么能够使用android:layout_width这样进行view的定义?

原因是我们在framework/base/core/res/res/values/attr.xml文件中定义了如下代码:

<declare-styleable name="ViewGroup_Layout">
        <!-- Specifies the basic width of the view.  This is a required attribute
             for any view inside of a containing layout manager.  Its value may
             be a dimension (such as "12dip") for a constant width or one of
             the special constants. -->
        <attr name="layout_width" format="dimension">
            <!-- The view should be as big as its parent (minus padding).
                 This constant is deprecated starting from API Level 8 and
                 is replaced by {@code match_parent}. -->
            <enum name="fill_parent" value="-1" />
            <!-- The view should be as big as its parent (minus padding).
                 Introduced in API Level 8. -->
            <enum name="match_parent" value="-1" />
            <!-- The view should be only big enough to enclose its content (plus padding). -->
            <enum name="wrap_content" value="-2" />
        </attr>

        <!-- Specifies the basic height of the view.  This is a required attribute
             for any view inside of a containing layout manager.  Its value may
             be a dimension (such as "12dip") for a constant height or one of
             the special constants. -->
        <attr name="layout_height" format="dimension">
我们看到layout_width下方定义了enum属性,就是这些属性的定义,将我们的fill_parent以及wrap_content属性转化为-1、-2等值。

二、上面提到了属性的定义,什么是属性?属性定义和引用的区别?

android:layout_width="wrap_content"
如上代码,layout_width是属性,wrap_content就是属性对应的值。

在framework/base/core/res/res/values/attr.xml文件中:

定义的形式:

<attr name="layout_width" format="dimension">
使用的形式:

<declare-styleable name="ViewGroup_MarginLayout">
        <attr name="layout_width" />
我们可以看到,属性的声明和使用,不同在于声明的地方使用format,一个属性可以使用多次,但仅能定义/声明一次。

三、styleable和style的区别?

从定义上来说:

style是样式,是一系列属性的集合。style是android将某一类属性集中定义[赋值]的一种表现形式。

styleable翻译为可样式化的,styleable是android将某一类属性集中访问的一种表现形式。

从使用上来说:

使用style

<application
        android:theme="@style/AppTheme" >
使用styleable:

TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View,
                defStyle, 0);
for (int i = 0; i < N; i++) {
            int attr = a.getIndex(i);
            switch (attr) {
                case com.android.internal.R.styleable.View_background:
                    background = a.getDrawable(attr);
                    break;
                case com.android.internal.R.styleable.View_padding:
                    padding = a.getDimensionPixelSize(attr, -1);
                    break;
styleable在编译时会在R.java文件中增加一个int[]数组,aapt为每一包含在styleable中的attr分配了一个指定的id值。


//----------------------------------------------------------------------------------------------------------

四、layout资源文件是如何被加载与解析的?里面定义了ImageView、View等节点。

我们在activity中通过setContentView使用layout文件,代码如下:

1、/android-4.0-mr1/frameworks/base/core/java/android/app/Activity.java

public void setContentView(int layoutResID) {
        getWindow().setContentView(layoutResID);
        initActionBar();
    }
我们看getWindow().setContentView(),这里的window是phoneWindow

2、path /android-4.0-mr1/frameworks/base/policy/src/com/android/internal/policy/impl/PhoneWindow.java

@Override
    public void setContentView(int layoutResID) {
        if (mContentParent == null) {
            installDecor();
        } else {
            mContentParent.removeAllViews();
        }
        mLayoutInflater.inflate(layoutResID, mContentParent);
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
    }
这其中关键代码为:mLayoutInflater.inflate。我们继续跟踪:

3、path /android-4.0-mr1/frameworks/base/core/java/android/view/LayoutInflater.java

public View inflate(int resource, ViewGroup root) {
        return inflate(resource, root, root != null);
    }
最终会执行到下面的代码:

public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        if (DEBUG) System.out.println("INFLATING from resource: " + resource);
        XmlResourceParser parser = getContext().getResources().getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
在这段代码中,构建了XmlResourceParser对象。对象创建过程如下:

3.1、调用Resouces.java中的getLayout方法:

/android-4.0-mr1/frameworks/base/core/java/android/content/res/Resources.java

public XmlResourceParser getLayout(int id) throws NotFoundException {
        return loadXmlResourceParser(id, "layout");
    }
接着调用loadXmlResouceParser

3.2、

/*package*/ XmlResourceParser loadXmlResourceParser(int id, String type)
            throws NotFoundException {
        synchronized (mTmpValue) {
            TypedValue value = mTmpValue;
            getValue(id, value, true);
            if (value.type == TypedValue.TYPE_STRING) {
                return loadXmlResourceParser(value.string.toString(), id,
                        value.assetCookie, type);
            }
            throw new NotFoundException(
                    "Resource ID #0x" + Integer.toHexString(id) + " type #0x"
                    + Integer.toHexString(value.type) + " is not valid");
        }
    }
这个方法中,首先调用getValue方法,这里传递进去一个TypeValue对象(value),layout对应的文件的内容会被加载到这个对象中 。getValue方法第一个参数为layout文件的id值,第二个参数是TypeValue对象,第三个对象是true,表明此资源文件非文件,仅仅是一个引用。

getValue方法内容如下:

public void getValue(int id, TypedValue outValue, boolean resolveRefs)
            throws NotFoundException {
        boolean found = mAssets.getResourceValue(id, 0, outValue, resolveRefs);
        if (found) {
            return;
        }
        throw new NotFoundException("Resource ID #0x"
                                    + Integer.toHexString(id));
    }
调用mAsserts对象的getResouceValue方法,如果found为false,抛出NotFoundException异常。

path /android-4.0-mr1/frameworks/base/core/java/android/content/res/AssetManager.java

/*package*/ final boolean getResourceValue(int ident,
                                               int density,
                                               TypedValue outValue,
                                               boolean resolveRefs)
    {
        int block = loadResourceValue(ident, (short) density, outValue, resolveRefs);
        if (block >= 0) {
            if (outValue.type != TypedValue.TYPE_STRING) {
                return true;
            }
            outValue.string = mStringBlocks[block].get(outValue.data);
            return true;
        }
        return false;
    }
在此方法中调用loadResouceValue方法:此方法的实现时通过JNI实现的,实现代码如下:

path /android-4.0-mr1/frameworks/base/core/jni/android_util_AssetManager.cpp

static jint android_content_AssetManager_loadResourceValue(JNIEnv* env, jobject clazz,
                                                           jint ident,
                                                           jshort density,
                                                           jobject outValue,
                                                           jboolean resolve)
{
    AssetManager* am = assetManagerForJavaObject(env, clazz);
    if (am == NULL) {
        return 0;
    }
    const ResTable& res(am->getResources());

    Res_value value;
    ResTable_config config;
    uint32_t typeSpecFlags;
    ssize_t block = res.getResource(ident, &value, false, density, &typeSpecFlags, &config);
#if THROW_ON_BAD_ID
    if (block == BAD_INDEX) {
        jniThrowException(env, "java/lang/IllegalStateException", "Bad resource!");
        return 0;
    }
#endif
    uint32_t ref = ident;
    if (resolve) {
        block = res.resolveReference(&value, block, &ref);
#if THROW_ON_BAD_ID
        if (block == BAD_INDEX) {
            jniThrowException(env, "java/lang/IllegalStateException", "Bad resource!");
            return 0;
        }
#endif
    }
    return block >= 0 ? copyValue(env, outValue, &res, value, ref, block, typeSpecFlags, &config) : block;
}
首先通过如下代码进行加载解析二进制layout文件:

ssize_t block = res.getResource(ident, &value, false, density, &typeSpecFlags, &config);
接着根据resolver这个bool变量,进行下一步加载。(resolver代表ident是否为引用,引用到另一个文件,如果为false,则表明ident为文件本身)
if (resolve) {
        block = res.resolveReference(&value, block, &ref);
最后根据block是否>0,来决定是否将二进制layout拷贝到value中。如果layotu文件存在,则block>0。

层层返回,就可以接着执行loadXmlResouceParse。

if (value.type == TypedValue.TYPE_STRING) {
                return loadXmlResourceParser(value.string.toString(), id,
                        value.assetCookie, type);
            }

代码如下:

/*package*/ XmlResourceParser loadXmlResourceParser(String file, int id,
            int assetCookie, String type) throws NotFoundException {
        if (id != 0) {
            try {
                // These may be compiled...
                synchronized (mCachedXmlBlockIds) {
                    // First see if this block is in our cache.
                    final int num = mCachedXmlBlockIds.length;
                    for (int i=0; i<num; i++) {
                        if (mCachedXmlBlockIds[i] == id) {
                            //System.out.println("**** REUSING XML BLOCK!  id="
                            //                   + id + ", index=" + i);
                            return mCachedXmlBlocks[i].newParser();
                        }
                    }

                    // Not in the cache, create a new block and put it at
                    // the next slot in the cache.
                    XmlBlock block = mAssets.openXmlBlockAsset(
                            assetCookie, file);
                    if (block != null) {
                        int pos = mLastCachedXmlBlockIndex+1;
                        if (pos >= num) pos = 0;
                        mLastCachedXmlBlockIndex = pos;
                        XmlBlock oldBlock = mCachedXmlBlocks[pos];
                        if (oldBlock != null) {
                            oldBlock.close();
                        }
                        mCachedXmlBlockIds[pos] = id;
                        mCachedXmlBlocks[pos] = block;
                        //System.out.println("**** CACHING NEW XML BLOCK!  id="
                        //                   + id + ", index=" + pos);
                        return block.newParser();
                    }
                }
            } catch (Exception e) {
                NotFoundException rnf = new NotFoundException(
                        "File " + file + " from xml type " + type + " resource ID #0x"
                        + Integer.toHexString(id));
                rnf.initCause(e);
                throw rnf;
            }
        }

        throw new NotFoundException(
                "File " + file + " from xml type " + type + " resource ID #0x"
                + Integer.toHexString(id));
    }
3.3、这段代码的作用就是根据上一步执行的结果得到真实的layout文件,解析layot文件,得到键值对。调用XmlBlock block = mAssets.openXmlBlockAsset(assetCookie, file);,得到XmlBlock对象,并将对象加入到Cache中(mCachedXmlBlocks[pos] = block;)。最后调用block.newParser得到XmlResourceParser对象。

详细流程如下:

/*package*/ final XmlBlock openXmlBlockAsset(int cookie, String fileName)
        throws IOException {
        synchronized (this) {
            if (!mOpen) {
                throw new RuntimeException("Assetmanager has been closed");
            }
            int xmlBlock = openXmlAssetNative(cookie, fileName);
            if (xmlBlock != 0) {
                XmlBlock res = new XmlBlock(this, xmlBlock);
                incRefsLocked(res.hashCode());
                return res;
            }
        }
        throw new FileNotFoundException("Asset XML file: " + fileName);
    }
调用openXmlAssetNative方法:

static jint android_content_AssetManager_openXmlAssetNative(JNIEnv* env, jobject clazz,
                                                         jint cookie,
                                                         jstring fileName)
{
    AssetManager* am = assetManagerForJavaObject(env, clazz);
    if (am == NULL) {
        return 0;
    }

    LOGV("openXmlAsset in %p (Java object %p)\n", am, clazz);

    ScopedUtfChars fileName8(env, fileName);
    if (fileName8.c_str() == NULL) {
        return 0;
    }

    Asset* a = cookie
        ? am->openNonAsset((void*)cookie, fileName8.c_str(), Asset::ACCESS_BUFFER)
        : am->openNonAsset(fileName8.c_str(), Asset::ACCESS_BUFFER);

    if (a == NULL) {
        jniThrowException(env, "java/io/FileNotFoundException", fileName8.c_str());
        return 0;
    }

    ResXMLTree* block = new ResXMLTree();
    status_t err = block->setTo(a->getBuffer(true), a->getLength(), true);
    a->close();
    delete a;

    if (err != NO_ERROR) {
        jniThrowException(env, "java/io/FileNotFoundException", "Corrupt XML binary file");
        return 0;
    }

    return (jint)block;
}
path /android-4.0-mr1/frameworks/base/core/java/android/content/res/XmlBlock.java
public XmlResourceParser newParser() {
        synchronized (this) {
            if (mNative != 0) {
                return new Parser(nativeCreateParseState(mNative), this);
            }
            return null;
        }
    }
path /android-4.0-mr1/frameworks/base/core/jni/android_util_XmlBlock.cpp

static jint android_content_XmlBlock_nativeCreateParseState(JNIEnv* env, jobject clazz,
                                                          jint token)
{
    ResXMLTree* osb = (ResXMLTree*)token;
    if (osb == NULL) {
        jniThrowNullPointerException(env, NULL);
        return 0;
    }

    ResXMLParser* st = new ResXMLParser(*osb);
    if (st == NULL) {
        jniThrowException(env, "java/lang/OutOfMemoryError", NULL);
        return 0;
    }

    st->restart();

    return (jint)st;
}

3.4、得到了XmlResouceParse对象,那么就可以继续往下执行了。接下来就是继续执行inflate方法。

public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
        if (DEBUG) System.out.println("INFLATING from resource: " + resource);
        XmlResourceParser parser = getContext().getResources().getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }
在3.2,3.3中,我们从二进制layout文件id得到了layout真实文件,并从layout真实文件中加载出键值对以及各种属性。那么此处就是将得到的view、IamgeView等tag解析并且构建View对象树。

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context)mConstructorArgs[0];
            mConstructorArgs[0] = mContext;
            View result = root;

            try {
                // Look for the root node.
                int type;
                while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }

                if (type != XmlPullParser.START_TAG) {
                    throw new InflateException(parser.getPositionDescription()
                            + ": No start tag found!");
                }

                final String name = parser.getName();
                
                if (DEBUG) {
                    System.out.println("**************************");
                    System.out.println("Creating root view: "
                            + name);
                    System.out.println("**************************");
                }

                if (TAG_MERGE.equals(name)) {
                    if (root == null || !attachToRoot) {
                        throw new InflateException("<merge /> can be used only with a valid "
                                + "ViewGroup root and attachToRoot=true");
                    }

                    rInflate(parser, root, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    View temp;
                    if (TAG_1995.equals(name)) {
                        temp = new BlinkLayout(mContext, attrs);
                    } else {
                        temp = createViewFromTag(root, name, attrs);
                    }

                    ViewGroup.LayoutParams params = null;

                    if (root != null) {
                        if (DEBUG) {
                            System.out.println("Creating params from root: " +
                                    root);
                        }
                        // Create layout params that match root, if supplied
                        params = root.generateLayoutParams(attrs);
                        if (!attachToRoot) {
                            // Set the layout params for temp if we are not
                            // attaching. (If we are, we use addView, below)
                            temp.setLayoutParams(params);
                        }
                    }

                    if (DEBUG) {
                        System.out.println("-----> start inflating children");
                    }
                    // Inflate all children under temp
                    rInflate(parser, temp, attrs, true);
                    if (DEBUG) {
                        System.out.println("-----> done inflating children");
                    }

                    // We are supposed to attach all the views we found (int temp)
                    // to root. Do that now.
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                }

            } catch (XmlPullParserException e) {
                InflateException ex = new InflateException(e.getMessage());
                ex.initCause(e);
                throw ex;
            } catch (IOException e) {
                InflateException ex = new InflateException(
                        parser.getPositionDescription()
                        + ": " + e.getMessage());
                ex.initCause(e);
                throw ex;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;
            }

            return result;
        }
    }

解析过程:

1、解析开始,确认非空。

while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                    // Empty
                }
2、得到tag的名字。

final String name = parser.getName();
3、根据tag的name创建View

temp = createViewFromTag(root, name, attrs);
创建过程如下:

View createViewFromTag(View parent, String name, AttributeSet attrs) {
        if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }

        if (DEBUG) System.out.println("******** Creating view: " + name);

        try {
            View view;
            if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs);
            else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);
            else view = null;

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);
            }
            
            if (view == null) {
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            }

            if (DEBUG) System.out.println("Created view is: " + view);
            return view;

        } catch (InflateException e) {
            throw e;

        } catch (ClassNotFoundException e) {
            InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name);
            ie.initCause(e);
            throw ie;

        } catch (Exception e) {
            InflateException ie = new InflateException(attrs.getPositionDescription()
                    + ": Error inflating class " + name);
            ie.initCause(e);
            throw ie;
        }
    }
创建tag的name指定的view的过程解析:

3.1、判断tag的name是否为view,如果为view,得到view指示的class。

if (name.equals("view")) {
            name = attrs.getAttributeValue(null, "class");
        }
由于我们的xml文件以view开头,故而,此处name为:class="com.android.internal.widget.ActionBarView$HomeView"

<view xmlns:android="http://schemas.android.com/apk/res/android"
      class="com.android.internal.widget.ActionBarView$HomeView"
      android:layout_width="wrap_content"
      android:layout_height="match_parent"
      android:background="?android:attr/actionBarItemBackground" >
3.2、 是否指定了ViewFactory用户指定创建view的规则。android允许设定ViewFactory,从ViewFactory中用户可以设定特定tag所对应的View文件。

Preference的加载方式也是这样的,但据我所查,android指示定义,但未使用。android提供了良好的View实例化对象机制。

if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs);
            else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);
            else view = null;

            if (view == null && mPrivateFactory != null) {
                view = mPrivateFactory.onCreateView(parent, name, mContext, attrs);
            }
3.3、根据View的名字进行View的实例化。

if (view == null) {
                if (-1 == name.indexOf('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            }
如果tag的名字不含有“.”,那么执行onCreateView。如果含有“.”,那么就认为此处的tag的name对应的View为自定义View。这两行代码最终会殊途同归,最终都会通过反射进行View对象的创建。只不过,如果不含有“.”,例如此处的ImageView,不含有“.”,那么就实例化andriod.view.ImageView这个class对象,如果是自定义View,那么就省去了拼接操作,直接去实例化相应的对象。例如:<com.android.myView   />这样一个xml标记,就直接去实例化com.android.myView这个View。

 protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return createView(name, "android.view.", attrs);
    }
反射实例化View对象:

clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                
                if (mFilter != null && clazz != null) {
                    boolean allowed = mFilter.onLoadClass(clazz);
                    if (!allowed) {
                        failNotAllowed(name, prefix, attrs);
                    }
                }
                constructor = clazz.getConstructor(mConstructorSignature);
Object[] args = mConstructorArgs;
            args[1] = attrs;
            return constructor.newInstance(args);

3.4、将创建的View对象返回。

 return view;

以上就是View的实例化创建过程。


4、上面是解析得到tag,并根据tag的name进行反射操作,得到View的实例化对象。接着上面说,第4步操作:

上面得到的tag为一级tag,如果我们的tag为LinearLayout,那么下面还会有子对象。我们的第4步就是实例化子View对象。

rInflate(parser, temp, attrs, true);
if (TAG_REQUEST_FOCUS.equals(name)) {
                parseRequestFocus(parser, parent);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else if (TAG_1995.equals(name)) {
                final View view = new BlinkLayout(mContext, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflate(parser, view, attrs, true);
                viewGroup.addView(view, params);                
            } else {
                final View view = createViewFromTag(parent, name, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflate(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
创建过程不再重复,和之前的创建View的过程类似。这个过程会遍历加载,会加载include属性指定的layout文件,还会加载requestFocus属性等。

5、应用ViewGroup.paras参数,如果root根View不为空,将创建的View树加到root根View中,如果root根View为null,则新创建的View为根View。

 ViewGroup.LayoutParams params = null;
params = root.generateLayoutParams(attrs);
 if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }

                    // Decide whether to return the root that was passed in or the
                    // top view found in xml.
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }

6、返回View树的根View。

return result;

以上就是layout文件加载,解析的大体过程。

上面是个人对layout以及资源加载的一些总结,欢迎大家交流。


  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

hailushijie

您的鼓励是我创作最大的动力

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

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

打赏作者

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

抵扣说明:

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

余额充值