Android inflate方法总结

前言

inflater.inflate(R.layout.layout_inflate_test,null);
inflater.inflate(R.layout.layout_inflate_test, root,false);
inflater.inflate(R.layout.layout_inflate_test, root,true);

看到上面这几个方法是不是非常眼熟,基本上做过Android开发的人都会调用过inflate方法,可是你真的了解inflate方法吗?各个参数都是什么含义?传递不同参数会产生什么效果?以前的观点是钥匙就是用来开锁的,椅子就是用来坐的,能用不就行了,管它原理是什么。这种观点对于一个新手还好说,刨根问底确实有点难度,但是随着时间的推移,你总会不断遇到一些相同的问题。如果你不早点把原理搞清楚,那么你就像路过没有灯光的胡同,会在相同的地方跌倒一次又一次。扯远了哈,其实网上对inflate方法太多的总结和分析,我在这里主要是自己记录总结,当然能帮到有需要的人更好。

我先提供三个链接:分析都挺好的
1,郭神分析:http://blog.csdn.net/guolin_blog/article/details/12921889
2, http://blog.csdn.net/u012702547/article/details/52628453
3, http://blog.csdn.net/l540675759/article/details/78080656

其实博主我,之前没写这篇博客的时候,只会一直用,然后都不知道LayoutInflater的加载原理,每次直接
LayoutInflater.from(context).inflate(R.layout.activity_test, root, false);
//不行就这样,反正有一种能实现我要的效果
LayoutInflater.from(context).inflate(R.layout.activity_test, null);
反正总有一种方式适合我。

上面摘录自第三篇博客,我深有共鸣。我以前也是这样,虽然闭着眼睛,凭着经验也可以迈过一些坑。但是如果你是一个有追求的想让自己的title加上高级两个字的工程师,你就要去看源码,去了解有疑问的地方的原理,不然你去大厂面试的时候深深体会到书到用时方恨少,胸中无墨,哑口无言的真正含义。

分析

首先

放源码之前先要知道inflate方法是干嘛的,看返回是一个View,就知道这个方法是要根据布局id把这个布局加载成一个View并返回的。

源码分析

   /**
     * ...
     */
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
        return inflate(resource, root, root != null);
    }

    /**
     * ...
     */
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root) {
        return inflate(parser, root, root != null);
    }

    /**
     * Inflate a new view hierarchy from the specified xml resource. Throws
     * {@link InflateException} if there is an error.
     *
     * @param resource ID for an XML layout resource to load (e.g.,
     *        <code>R.layout.main_page</code>)
     * @param root Optional view to be the parent of the generated hierarchy (if
     *        <em>attachToRoot</em> is true), or else simply an object that
     *        provides a set of LayoutParams values for root of the returned
     *        hierarchy (if <em>attachToRoot</em> is false.)
     * @param attachToRoot Whether the inflated hierarchy should be attached to
     *        the root parameter? If false, root is only used to create the
     *        correct subclass of LayoutParams for the root view in the XML.
     * @return The root View of the inflated hierarchy. If root was supplied and
     *         attachToRoot is true, this is root; otherwise it is the root of
     *         the inflated XML file.
     */
    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) + ")");
        }

        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

    /**
     * Inflate a new view hierarchy from the specified XML node. Throws
     * {@link InflateException} if there is an error.
     * <p>
     * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
     * reasons, view inflation relies heavily on pre-processing of XML files
     * that is done at build time. Therefore, it is not currently possible to
     * use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
     *
     * @param parser XML dom node containing the description of the view
     *        hierarchy.
     * @param root Optional view to be the parent of the generated hierarchy (if
     *        <em>attachToRoot</em> is true), or else simply an object that
     *        provides a set of LayoutParams values for root of the returned
     *        hierarchy (if <em>attachToRoot</em> is false.)
     * @param attachToRoot Whether the inflated hierarchy should be attached to
     *        the root parameter? If false, root is only used to create the
     *        correct subclass of LayoutParams for the root view in the XML.
     * @return The root View of the inflated hierarchy. If root was supplied and
     *         attachToRoot is true, this is root; otherwise it is the root of
     *         the inflated XML file.
     */
    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");

            final Context inflaterContext = mContext;
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context) mConstructorArgs[0];
            mConstructorArgs[0] = inflaterContext;
            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, inflaterContext, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    final View temp = createViewFromTag(root, name, inflaterContext, 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 against its context.
                    rInflateChildren(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) {
                final InflateException ie = new InflateException(e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } catch (Exception e) {
                final InflateException ie = new InflateException(parser.getPositionDescription()
                        + ": " + e.getMessage(), e);
                ie.setStackTrace(EMPTY_STACK_TRACE);
                throw ie;
            } finally {
                // Don't retain static reference on context.
                mConstructorArgs[0] = lastContext;
                mConstructorArgs[1] = null;

                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            return result;
        }
    }

源码有点长,不要被吓到,下面一一拆解
看源码一共有四个inflate方法,这四个又分为两类:

首参数为布局id,@LayoutRes int resource ,也是我们经常用的;
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot)

首参数为Parser,XmlPullParser parse ,我开发中好像没有用到过。
public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)

源码中inflate(R.layout.layout_inflate_test,null)其实调用的是inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot),inflate(XmlPullParser parser, @Nullable ViewGroup root)调用的是inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)。

final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }

第一类其实调用的也是第四个方法。那就着重看一下第四个方法inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot)。
开始分析:

final Context inflaterContext = mContext;
final AttributeSet attrs = Xml.asAttributeSet(parser);
Context lastContext = (Context) mConstructorArgs[0];
mConstructorArgs[0] = inflaterContext;
View result = root;

看开始的几句代码,先不管前面的那几句定义,最后一句话View result = root;那么这个result基本就是作为返回值了,看这个方法最后return result;好吧果然是的。result = root,也就是将第二个参数ViewGroup root返回了,但是使用的inflate方法的时候我们有可能传递的是null也有可能不是null,继续往下看

// 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 (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, inflaterContext, attrs, false);
}

如果根节点的标签是merge,如果root为null或者attachToRoot为false会直接抛异常,也就是当根标签为merge的时候必须使用inflater.inflate(R.layout.layout_inflate_test, root,true);这种形式,不然会报错,你可以自己试验一下。实验结果:


329DC4C2-A7AF-4B70-807C.png

继续往下看:rInflate(parser, root, inflaterContext, attrs, false);

/**
     * Recursive method used to descend down the xml hierarchy and instantiate
     * views, instantiate their children, and then call onFinishInflate().
     * <p>
     * <strong>Note:</strong> Default visibility so the BridgeInflater can
     * override it.
     */
    void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

        final int depth = parser.getDepth();
        int type;
        boolean pendingRequestFocus = false;

        while (((type = parser.next()) != XmlPullParser.END_TAG ||
                parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

            if (type != XmlPullParser.START_TAG) {
                continue;
            }

            final String name = parser.getName();

            if (TAG_REQUEST_FOCUS.equals(name)) {
                pendingRequestFocus = true;
                consumeChildElements(parser);
            } else if (TAG_TAG.equals(name)) {
                parseViewTag(parser, parent, attrs);
            } else if (TAG_INCLUDE.equals(name)) {
                if (parser.getDepth() == 0) {
                    throw new InflateException("<include /> cannot be the root element");
                }
                parseInclude(parser, context, parent, attrs);
            } else if (TAG_MERGE.equals(name)) {
                throw new InflateException("<merge /> must be the root element");
            } else {
                final View view = createViewFromTag(parent, name, context, attrs);
                final ViewGroup viewGroup = (ViewGroup) parent;
                final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                rInflateChildren(parser, view, attrs, true);
                viewGroup.addView(view, params);
            }
        }

        if (pendingRequestFocus) {
            parent.restoreDefaultFocus();
        }

        if (finishInflate) {
            parent.onFinishInflate();
        }
    }

这个方法的意思是:递归方法进入xml层次结构并实例化视图,实例化它们的子项,然后调用onFinishInflate()。这个方法的内容可以先不看,知道它的作用就行了,就是把这个布局里面的各个子项实例化。举个例子一个完整的快递肯定是大盒子包小盒子再包,有的包了好几层最后才是你的商品。你想要的肯定不是最外层的那个空盒子,你需要的是一个完整的快递。这个方法就是用来把一个或者好几个商品用纸盒子一层层包起来组成一个可以运输的快递的,上面的root就是最外面的那个盒子。

else {
    // Temp is the root view that was found in the xml
    final View temp = createViewFromTag(root, name, inflaterContext, 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 against its context.
    rInflateChildren(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;
    }
}

刚才哪个merge是特殊情况,一般常见的是else里面的情况

// Temp is the root view that was found in the xml
final View temp = createViewFromTag(root, name, inflaterContext, attrs);

生成一个root view,也就是根据根目录的标签生成的view。这里有个需要注意的地方,最后一个参数attrs,也就是说这个根视图view的一些属性还是会被添加上去例如背景颜色等属性

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);
        }
    }

考点来了,如果root != null,创建LayoutParams,params = root.generateLayoutParams(attrs);这个参数attrs来自上面的final AttributeSet attrs = Xml.asAttributeSet(parser);Xml.asAttributeSet(parser)这句代码我也不是很懂,但是有时候可以通过具体现象或返回值推算某一句代码的作用。它返回一个AttributeSet,AttributeSet是view的布局属性集合,所以这里的作用就是把我们传入的布局的属性拿到。然后后面根据这些属性创建LayoutParams。看下面

/**
     * Returns a new set of layout parameters based on the supplied attributes set.
     * 根据提供的属性集返回一个新的LayoutParams
     * @param attrs the attributes to build the layout parameters from
     *
     * @return an instance of {@link android.view.ViewGroup.LayoutParams} or one
     *         of its descendants
     */
    public LayoutParams generateLayoutParams(AttributeSet attrs) {
        return new LayoutParams(getContext(), attrs);
    }

...

/**
     * Creates a new set of layout parameters. The values are extracted from
     * the supplied attributes set and context. The XML attributes mapped
     * to this set of layout parameters are:
     *
     * <ul>
     *   <li><code>layout_width</code>: the width, either an exact value,
     *   {@link #WRAP_CONTENT}, or {@link #FILL_PARENT} (replaced by
     *   {@link #MATCH_PARENT} in API Level 8)</li>
     *   <li><code>layout_height</code>: the height, either an exact value,
     *   {@link #WRAP_CONTENT}, or {@link #FILL_PARENT} (replaced by
     *   {@link #MATCH_PARENT} in API Level 8)</li>
     * </ul>
     *
     * @param c the application environment
     * @param attrs the set of attributes from which to extract the layout
     *              parameters' values
     */
    public LayoutParams(Context c, AttributeSet attrs) {
        TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.ViewGroup_Layout);
        setBaseAttributes(a,
                R.styleable.ViewGroup_Layout_layout_width,
                R.styleable.ViewGroup_Layout_layout_height);
        a.recycle();
    }

现在有了LayoutParams,如果attachToRoot为false时执行temp.setLayoutParams(params);将上面得到的LayoutParams给temp--也就是我们上面根据布局根目录标签创建的的那个View设置上。布局根目录一般都是LinearLayout,RelativeLayout等这些ViewGroup,当然也可以是view
再往下看

if (DEBUG) {
    System.out.println("-----> start inflating children");
}

// Inflate all children under temp against its context.
rInflateChildren(parser, temp, attrs, true);

if (DEBUG) {
    System.out.println("-----> done inflating children");
}
/**
     * Recursive method used to inflate internal (non-root) children. This
     * method calls through to {@link #rInflate} using the parent context as
     * the inflation context.
     * <strong>Note:</strong> Default visibility so the BridgeInflater can
     * call it.
     */
    final void rInflateChildren(XmlPullParser parser, View parent, AttributeSet attrs,
            boolean finishInflate) throws XmlPullParserException, IOException {
        rInflate(parser, parent, parent.getContext(), attrs, finishInflate);
    }

跟刚才哪个merge标签的情况一样打包快递,把子布局子view都一层层组装起来,装到temp里。继续

// 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);
}

又到考点了,假如root不为空,并且attachToRoot为true,那么root就把生成的temp装到自己里面addView,后面还有参数params。temp会被添加到root的最后,并且params设置给temp。

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

最后一个考点,如果root为空,那么attachToRoot就不用看了,attachToRoot为true或者false都没有意义, 直接result = temp。注意这里temp是没有被设置刚才的LayoutParams的,而LayoutParams是用来设置位置、高、宽等信息,也就意味着temp的这些属性是全新的。

结论

但是使用的时候我们更关心的是各个参数传递给我们带来的影响和效果。那么通过看源码我们得到什么样的结论呢?
先把郭神的结论贴出来:

  1. 如果root为null,attachToRoot将失去作用,设置任何值都没有意义。
  2. 如果root不为null,attachToRoot设为true,则会给加载的布局文件的指定一个父布局,即root。
  3. 如果root不为null,attachToRoot设为false,则会将布局文件最外层的所有layout属性进行设置,当该view被添加到父view当中时,这些layout属性会自动生效。
  4. 在不设置attachToRoot参数的情况下,如果root不为null,attachToRoot参数默认为true。

关于上面还有一些补充说明,如果root不为null,布局文件最外层的layout关于LayoutParams设置的属性和其他属性都会被保留下来,attachToRoot设为true,则会给加载的布局文件的指定一个父布局,我们不需要自己在addView,否则会报错;attachToRoot设为false,需要我们自己addView,root为null时,被加载的布局LayoutParams的属性会被改变,但是其它属性例如背景颜色什么的会被保留。

参考链接

http://blog.csdn.net/guolin_blog/article/details/12921889 http://blog.csdn.net/u012702547/article/details/52628453 http://blog.csdn.net/l540675759/article/details/78080656
https://www.cnblogs.com/coding-way/p/5257579.html
http://blog.csdn.net/jaysong2012/article/details/41117339
http://www.cnblogs.com/xiaoweiz/p/3788332.html
http://www.jianshu.com/p/07fcd2517dbc

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,530评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,403评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,120评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,770评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,758评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,649评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,021评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,675评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,931评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,751评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,410评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,004评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,969评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,042评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,493评论 2 343

推荐阅读更多精彩内容

  • 一、适用场景 ListViewListview是一个很重要的组件,它以列表的形式根据数据的长自适应展示具体内容,用...
    Geeks_Liu阅读 10,635评论 1 28
  • RecyclerView Item 布局宽高无效问题探究 前言 这个问题很早之前就碰到过,后来通过google找到...
    TinyMen阅读 421评论 0 0
  • 原文地址:http://www.jianshu.com/p/de7f651170be 一、简述 LayoutInf...
    AFinalStone阅读 4,019评论 1 7
  • 有段时间没写博客了,感觉都有些生疏了呢。最近繁忙的工作终于告一段落,又有时间写文章了,接下来还会继续坚持每一周篇的...
    justin_pan阅读 549评论 0 2
  • 不止一次,我挣扎在自己的世界里 不止一次,我认为这世上的一切都是虚假 或许我正活在自己的梦里 在幼年时的某次熟睡 ...
    梅千延阅读 198评论 0 0