Android中的LayoutInflater分析(三)

PS:本文系转载文章,阅读原文可读性会更好,文章末尾有原文链接

ps:本篇文章是基于 Android Api 26 来分析的

目录

1、LayoutInflater 创建 View 过程

 1、1 LayoutInflater 的 createViewFromTag 方法分析

 1、2 创建 View 时不可忽视的耗时

 1、3 自定义一个 LayoutInflater.Factory



1、LayoutInflater 创建 View 过程

 1、1 LayoutInflater 的 createViewFromTag 方法分析


我们接着Android中的LayoutInflater分析(二)这篇文章继续分析,我们看回Android中的LayoutInflater分析(二)这篇文章中给出的 LayoutInflater 的 createViewFromTag 方法;

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,

                       boolean ignoreThemeAttr) {
    ......
    try {
        View view;
        
        //41、
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
            
            //42、
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }
        
        //43、
        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        if (view == null) {
            ......
            try {
                if (-1 == name.indexOf('.')) {
                    
                    //44、
                    view = onCreateView(parent, name, attrs);
                } else {
                    
                    //45、
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }
        return view;
    } catch (InflateException e) {
      ......
    } catch (ClassNotFoundException e) {
      ......
    } catch (Exception e) {
      ......
    }

}

在Android中的LayoutInflater分析(二)这篇文章中,我们写的界面类不是继承于 Activity,而是继承于 AppCompatActivity,所以会走注释41 的代码;如果我们写的界面类是继承于 Activity,那么注释41、42、43 的代码不会执行,而会执行注释44 或者注释45 的代码;注释44 的代码表示创建的是系统 View,例如 ;注释45 的代码表示创建的是自定义的 View,例如 ,该方法 [createView(String name, String prefix, AttributeSet attrs) 方法] 的第二个参数表示标签的前缀,为 null 是因为不需要添加前缀, 标签的前缀是 com.xr. ,所以不需要再添加前缀了;注释45 的代码调用的是 LayoutInflater 的 createView(String name, String prefix, AttributeSet attrs) 方法,注释44 的代码也最终还是调用到 LayoutInflater 的 createView(String name, String prefix, AttributeSet attrs) 方法,所以我们跟踪注释44 的代码就好,看一下 LayoutInflater 的 onCreateView(View parent, String name, AttributeSet attrs) 方法;

图片

看注释53 的代码,这里调用的是 LayoutInflater 的 onCreateView(String name, AttributeSet attrs) 方法吗?显然不是,从Android中的LayoutInflater分析(一)这篇文章可以知道,Activity、Service 和 Application 作为环境上下文拿到的 LayoutInflater 其实是 PhoneLayoutInflater,所以我们看的是 PhoneLayoutInflater 的 onCreateView(String name, AttributeSet attrs) 方法

图片

看注释54 的代码,也就是通过 LayoutInflater 的 createView(String name, String prefix, AttributeSet attrs) 方法创建 View,该方法最终通过反射的机制创建 View;我们看看注释54 的代码外围的 for 循环里的 sClassPrefixList 是什么,且看它的定义;

图片

哦原来是存放 View 前缀的数组,也就是说我们大多数用的系统 View 都是放在 android.widget 、 android.webkit 、 android.app 这3个包下面;那这里有的人可能就有疑问了,假设我在 xml 文件中写的系统 View 是 SurfaceView,SurfaceView 是在 android.view 包下面,为什么能创建成功,而且注释54 的代码最终通过反射机制创建 View,应该会有找不到类的异常信息出现啊;假设我们创建的是 SurfaceView,会尝试循环 sClassPrefixList 数组里前缀并进行拼接,如果通过反射机制创建的 View 时,发现类找不到,本应该抛出类找不到的异常,但是注释54 的代码外围有一个 try catch 语句,只是捕获并没有抛出异常,所以不影响程序往下走;当 sClassPrefixList 循环完了,发现创建的 View 还是空的,就会走注释55 的代码,也就是调用 LayoutInflater 的 onCreateView(String name, AttributeSet attrs) 方法;

图片

看注释56 的代码,LayoutInflater 的 onCreateView(String name, AttributeSet attrs) 方法还是会调用到 LayoutInflater 的 createView(String name, String prefix, AttributeSet attrs) 方法,又回到了通过反射机制创建 View 的方法,这回终于明白 SurfaceView 在 android.view 包下通过反射机制也能创建成功了吧。

好,我们往下看通过反射机制创建 View 的细节,也就是 LayoutInflater 的 createView(String name, String prefix, AttributeSet attrs) 方法;

public final View createView(String name, String prefix, AttributeSet attrs)

        throws ClassNotFoundException, InflateException {

    //57、
    Constructor constructor = sConstructorMap.get(name);

    //58、
    if (constructor != null && !verifyClassLoader(constructor)) {
        constructor = null;

        //59、
        sConstructorMap.remove(name);
    }
    ......
    try {
        ......
        if (constructor == null) {
            // Class not found in the cache, see if it's real, and try to add it
            //60、
            clazz = mContext.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);

            //61、
            if (mFilter != null && clazz != null) {
                boolean allowed = mFilter.onLoadClass(clazz);
                if (!allowed) {
                    failNotAllowed(name, prefix, attrs);
                }
            }
            ......
            //62、
            sConstructorMap.put(name, constructor);
        } else {
            // If we have a filter, apply it to cached constructor
            //63、
            if (mFilter != null) {
                // Have we seen this name before?
                Boolean allowedState = mFilterMap.get(name);
                if (allowedState == null) {
                    // New class -- remember whether it is allowed
                    //64、
                    clazz = mContext.getClassLoader().loadClass(
                            prefix != null ? (prefix + name) : name).asSubclass(View.class);

                    boolean allowed = clazz != null && mFilter.onLoadClass(clazz);
                    mFilterMap.put(name, allowed);
                    if (!allowed) {

                        //65、
                        failNotAllowed(name, prefix, attrs);
                    }
                } else if (allowedState.equals(Boolean.FALSE)) {
                    failNotAllowed(name, prefix, attrs);
                }
            }
        }
        ......
        //66、
        final View view = constructor.newInstance(args);
        ......
        return view;

    } catch (NoSuchMethodException e) {
        ......
    } catch (ClassCastException e) {
        ......
    } catch (ClassNotFoundException e) {
        ......
    } catch (Exception e) {
        ......
    } finally {
        ......
    }

}

注释57 的代码表示从 HashMap 中根据 name 拿出一个具体 View 的构造器,比如说 TextView 的构造器;注释58 的代码表示如果构造器不为空且不是同一个构造器,那么就执行注释59 的代码;注释60、64 的代码表示通过反射机制创建对应的具体 View 的 Class 对象,比如 TextView 对应的 Class 是 TextView.class;注释61、63 的 mFilter 不为空可以起到拦截是否被允许创建该视图类的对象的作用;注释66 的代码表示通过构造器并传入参数真正实现创建具体的 View 对象,args 是长度为2的数组,所以调用的是具体的 View 的2个参数的构造方法;注释65 的代码表示 !allowed 为 true 时,那么就不允许创建该 View 的类对象,直接抛出异常,不信的话,我们可以看看 LayoutInflater 的 failNotAllowed(String name, String prefix, AttributeSet attrs) 方法;

private void failNotAllowed(String name, String prefix, AttributeSet attrs) {
    throw new InflateException(attrs.getPositionDescription()
            + ": Class not allowed to be inflated "+ (prefix != null ? (prefix + name) : name));
}

看到了没,只有抛出异常的一行代码。

1、2 创建 View 时不可忽视的耗时

如果界面类继承的是 Activity,而 LayoutInflater 在 View 对象的创建过程中使用了大量反射,如果某个布局界面内容比较复杂,这过程耗时是不可忽视的;一些情况下可能是某个 View 的创建过程需要执行 4 次,比如前面提到的 SurfaceView,因为系统默认遍历规则依次为 android.weight、android.webkit 和 android.app,但是由于 SurfaceView 属于 android.view 目录下,所以需要第 4 次进行反射创建对应的 Class 才可以正确加载,这个效率会有点慢;界面类直接继承 Activity 并用 PhoneLayoutInflater 对 View 进行创建的过程中简单粗暴,这就给我们留下了很多优化的空间。

1、3 自定义一个 LayoutInflater.Factory

我们知道,如果我们写的界面类继承的是 Activity,解析 xml 布局文件的标签就会走注释44 或者注释45 的代码,就会用到反射机制创建 View,这样的创建 View 就会得简单粗暴;从Android中的AppCompatActivity的偷梁换柱之UI偷换这一篇文章可以知道,当我们写的界面类直接继承于 AppCompatActivity 时,那么就会走这篇文章注释41 的代码,mFactory2 就不为空,mFactory2 本质上是 LayoutInflater.Factory2 接口, AppCompatActivity 通过 AppCompatDelegate 的具体实现类设置好 mFactory2 的值,mFactory2 的 onCreateView(View parent, String name, Context context, AttributeSet attrs) 方法调用到 AppCompatDelegate 具体实现类的 onCreateView(View parent, String name, Context context, AttributeSet attrs)方法,AppCompatDelegate 具体实现类最终会调用到 AppCompatViewInflater 的 createView(View parent, final String name, @NonNull Context context,@NonNull AttributeSet att-rs,boolean inheritContext,boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) 方法(简称 A 方法),A 方法优先根据标签名字直接 new 一个具体的 View,这就优化的具体 View 的创建过程;其本质是 AppCompatActivity 间接的设置了 LayoutInflater.Factory2,LayoutInflater.Factory2 再间接用代理 AppCompatViewInflater 创建具体 View。

那如果我的界面类直接继承的是 Activity,我也想优化具体 View 的创建过程怎么办?我们就自定义一个 LayoutInflater.Factory 并在 Activity 的子类设置 PhoneLayoutInflater 的 mFactory 属性的值,这样不仅优化创建具体 View 的过程,还可以自己写 “自定义的系统 View ” 呢。好,我们现在写一个 demo 测试一下;

(1)自定义一个 View :

public class CustomTextView extends AppCompatTextView {

public CustomTextView(Context context) {
    super(context);
}

public CustomTextView(Context context, AttributeSet attrs) {
    super(context, attrs);
}

public CustomTextView(Context context, AttributeSet attrs, int defStyleAttr) {
    super(context, attrs, defStyleAttr);
}

}

(2)写一个 LayoutInflater.Factory 的实现类 MyFactory :

public class MyFactory implements LayoutInflater.Factory {

@Override
public View onCreateView(String name, Context context, AttributeSet attrs) {
    if (name.equals("CustomTextView")) {
        return new CustomTextView(context,attrs);
    }
    return null;
}

}

(3)写一个 Activity,名叫 MainActivity :

public class MainActivity extends Activity {

TextView mTv;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);
    mTv = findViewById(R.id.tv);
    Log.d("MainActivity","TextView----" + mTv);
}

@Override
public Object getSystemService(@NonNull String name) {
    if (name.equals(Context.LAYOUT_INFLATER_SERVICE)) {
        LayoutInflater inflater = (LayoutInflater)super.getSystemService(name);
        if (inflater.getFactory() == null) {
            inflater.setFactory(new MyFactory());
        }
        return inflater;
    }
    return super.getSystemService(name);
}

}

(4)MainActivity 对应的 xml 布局文件 activity_main.xml :


xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
>


程序运行后的界面如下所示:

图片

日志打印如下所示:

02-12 13:43:54.309 20564-20564/com.epbox.userrecycle.myapplication D/MainActivity: TextView----com.epbox.userrecycle.myapplication.CustomTextView{bf6a0a9 V.ED..... ......ID 0,0-0,0 #7f07007b app:id/tv}

从日志可以看出,打印的确实是自定义的 View;从 activity_main.xml 布局文件可以看出,我们写的标签确实是自定义的 View 而且是不写前缀的,也就是不写包名出来,这就实现了 “自定义的系统 View ”。

你可能感兴趣的:(androidjava)