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

来源:互联网 发布:php 会员下载系统源码 编辑:程序博客网 时间:2024/06/06 15:45


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

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

伪代码示例:

public class MyStaggedRecyclerAdapter extends RecyclerView.Adapter<MyStaggedRecyclerAdapter.MyViewHolder> {    private List<String> list;    private List<Integer> heights;    public MyStaggedRecyclerAdapter(List<String> list) {        // TODO Auto-generated constructor stub        this.list = list;        heights = new ArrayList<Integer>();        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("<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;    }}
在这个方法里面,就是对传入的item布局进行全面的解析了。看一下红色位置标注处,params = root.generateLayoutParams(attrs);

哈哈,终于把你揪出来来了。我们传入的root为null,更不可能去生成该item的params了,因此,刚开始获取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已经有了父容器了,我们传入的root不能直接成为父亲。你必须先让原来孩子的父亲把孩子去掉,才能再找一个父亲。这尼玛真实一言不合就换父亲啊~~

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

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

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

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

由于RecyclerView/ListView会自动将child添加到它里面去成为父亲,并最终一起添加到viewGroup(成为爷爷)。孩子自己就想添加到viewGroup,想直接把爷爷当做父亲,这伦理上也说不过去啊,难免自作多情!这个时候,就会报非法异常了。

那么最后看解决办法:

解决办法1:

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

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

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"              android:orientation="vertical"              android:layout_width="match_parent"              android:layout_height="match_parent">    <TextView        android:gravity="center"        android:textSize="20sp"        android:id="@+id/tv"        android:layout_width="match_parent"        android:layout_height="match_parent"/></LinearLayout>
这样的,我们给TextView制定了父布局,通过最初的方式就能完成任务。

解决方式2:

通过建议的方式,使用

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

其他解决方式:

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


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


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






2 1