源码分析初级《篇一》 为何建议使用LayoutInflater.from而不用View.inflate

转载文章请注明出处:道龙的博客

我们不管是在ListView、RecyclerView、甚至自定义布局的时候,都会通过View.inflate(......);方法加载布局,其实这是偷懒的方式,有些时候,通过这种偷懒的方式反而带来意想不到的bug。比如空指针异常,非法状态异常。接下来就通过源码角度,分析为何不建议使用这种方式。

伪代码示例:

public class MyStaggedRecyclerAdapter extends RecyclerView.Adapter {

    private List list;
    private List heights;

    public MyStaggedRecyclerAdapter(List list) {
        // TODO Auto-generated constructor stub
        this.list = list;

        heights = new ArrayList();
        for (int i = 0; i < list.size(); i++) {
            heights.add((int) (200 + Math.random() * 50));
        }

    }

    class MyViewHolder extends RecyclerView.ViewHolder {
        TextView tv;

        public MyViewHolder(View view) {
            super(view);
            tv = (TextView) view.findViewById(android.R.id.text1);

        }

    }

    @Override
    public int getItemCount() {
        // TODO Auto-generated method stub
        return list.size();
    }

    @Override
    public void onBindViewHolder(MyViewHolder holder, int position) {
        //绑定数据
        LayoutParams params = holder.tv.getLayoutParams();
        params.height = heights.get(position);
        holder.tv.setBackgroundColor(Color.rgb(100, (int) (Math.random() * 255), (int) (Math.random() * 255)));
        holder.tv.setLayoutParams(params);
        holder.tv.setText(list.get(position));

    }

    @Override
    public MyViewHolder onCreateViewHolder(ViewGroup viewGroup, int arg1) {
        // 创建ViewHolder
        MyViewHolder holder = new MyViewHolder(View.inflate(viewGroup.getContext(), android.R.layout.simle_list_item_1, null));
        //MyViewHolder holder = new MyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(android.R.layout.simple_list_item_1, viewGroup, false));

        return holder;
    }
}

这里的代码非常简单,是为了给RecycleView设置流式布局的适配器类。流式布局需要给孩子设置宽高,这里通过动态随机给item孩子设置高度的方式,这样展示流式布局显得更加高大上一点~   首先,在onCreateViewHolder方法中,先通过以往偷懒的方式加载item孩子布局的。运行程序,我们会发现报错:

NullPointException

经过log日志,或者debug,锁定到LayoutParams为空,接下来设置宽高,也就没法执行了。看看为空情况,眼见为实:



这里就很头疼了,我按照标准写法写的,为什么为空呢?这里,其实存在着不小的坑。对于这个坑,需要通过源码角度理解。进入源码:

public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
    LayoutInflater factory = LayoutInflater.from(context);
    return factory.inflate(resource, root);
}
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}
我们发现,通过调用View.inflate本质上调用的是LayoutInflater的inflate(resource, root, root != null);方法。由于传入的根布局root为nulll,因此这里本质上为inflate(resource, null, false);。继续跟进源码
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三个参数含义前两个很明显,第三个看参数名称也很明显,含义是:是否绑定传入的RootView?我们本质上传入的是false,所以这个值以后都为fallse了。方法里面是通过xml序列化器,对自定义传入的res布局文件预进行解析。具体的解析,继续跟进源码:
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(" 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;
    }
}
在这个方法里面,就是对传入的item布局进行全面的解析了。看一下红色位置标注处,params = root.generateLayoutParams(attrs);

哈哈,终于把你揪出来来了。我们传入的root为null,更不可能去生成该item的params了,因此,刚开始获取params为null很明显了了。

同样的我们加载的布局他压根就没有父亲,这样他的layout_width和layout_height等属性都需要有父亲才有效。没有params为null也就没有什么可以疑问的了。

我们先不直接使用LayoutInflater.from,我们想到,既然root为空,那么我传入一个root让其不是空,不就解决了这个bug吗?做如下修改:

MyViewHolder holder = new MyViewHolder(View.inflate(viewGroup.getContext(), android.R.layout.simple_list_item_1, viewGroup));
还是报错:
java.lang.IllegalStateException:
The specified child already has a parent. You must call removeView() on the child's parent first.

错误情况也很明朗,因为,我们指定的孩子view已经有了父容器了(RecyclerView会默认成为父亲),我们传入的root不能直接成为父亲。你必须先让原来孩子的父亲把孩子去掉,才能再找一个父亲。这尼玛真实一言不合就换父亲啊~~

我们还是要看看为何报这个错误:

首先:由于我们传入了root,也就不为空,最初默认调用还是默认还是调用的:
inflate(resource, root, root != null);
inflate(resource, root, true);

接着定位最终调用源码绿色位置:

看源码就知道:多做了一个事情就是
 if (root != null && attachToRoot) {
            root.addView(temp, params);
        }

由于RecyclerView/ListView会自动将child添加到它里面去成为父亲,并最终一起添加到viewGroup(成为爷爷)。可以找出错误的原因有两个:1、给布局添加父亲,只能存在一个父亲。2、(这个解释有点牵强但是可以增加理解)孩子自己就想添加到viewGroup,想直接把爷爷当做父亲,这伦理上也说不过去啊,难免自作多情!这个时候,就会报非法异常了。

那么最后看解决办法:

解决办法1,还是使用View.inflate()

        MyViewHolder holder = new MyViewHolder(View.inflate(viewGroup.getContext(), R.layout.listview_item, null));

布局使用自己定义的布局:

xml version="1.0" encoding="utf-8"?>
xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="match_parent"
              android:layout_height="match_parent">

            android:gravity="center"
        android:textSize="20sp"
        android:id="@+id/tv"
        android:layout_width="match_parent"
        android:layout_height="match_parent"/>

这样的,我们给TextView制定了父布局,那么通过最初的方式就能完成任务。

解决方式2:

通过建议的方式,使用

        MyViewHolder holder = new MyViewHolder(LayoutInflater.from(viewGroup.getContext()).inflate(R.layout.listview_item, viewGroup, false));
使用这种方式,无论加载什么布局,都能完成View的加载,当然,这也是本文所说的结论,如果想要加载布局,不想带来可能存在的“危机”,就使用这种方式加载布局吧!

其他解决方式:

可以通过ViewTreeObserver.addGlob..Listener()方法,设置页面布局绘制完毕监听器,在里面进行获取组件的宽高一定不会报错。至于使用方式,自定百度~


对于这一块的更具体、详细的源码,等笔者能力提高了,再进入两万行的代码里带大家遨游一番吧~


觉得有作用,就点个赞价加个关注呗~~






你可能感兴趣的:(Android)