插件式换肤框架搭建 - setContentView源码阅读

1. 概述


内涵段子架构第一阶段已经更新完了,后面我们主要是以google源码为主,今天我带大家来看一下setContentView的源码,请先看一下如果继承自Activity去打印一个TextView与继承自AppCompatActivity去打印一个TextView分别是这样的:

继承自Activity:  
android.widget.TextView{ac5cd17 V.ED..... ......ID 0,0-0,0 #7f0b002c app:id/text_view}

继承自AppCompatActivity:  
android.support.v7.widget.AppCompatTextView{392562b V.ED..... ......ID 0,0-0,0 #7f0b0055 app:id/text_view}

谁能告诉我这到底是怎么啦?我布局里面明明是TextView为什么继承自AppCompatActivity就变成了AppCompatTextView,那么接下来我们就来看一下源码到底是怎么把我的TextView给拐走的。

所有分享大纲:2017Android进阶之路与你同行

视频讲解地址:https://pan.baidu.com/s/1qYl2AOO

2. Activity的setContentView源码阅读


2.1 很多人都问过我怎么看源码,我只想说怎么看?当然是坐着点进去看啊!

    public void setContentView(@LayoutRes int layoutResID) {
        // 获取Window 调用window的setContentView方法,发现是抽象类,所以需要找具体的实现类PhoneWindow
        getWindow().setContentView(layoutResID);
    }

    // PhoneWindow 中的 setContentView方法
    @Override
    public void setContentView(int layoutResID) {
        // 如果mContentParent 等于空,调用installDecor();
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        // 把我们自己的布局layoutId加入到mContentParent,我们set进来的布局原来是放在这里面的Soga
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }

2.2 installDecor(),这个之前已经带大家看过一遍了,不过没办法再进来看看吧:

    // This is the top-level view of the window, containing the window decor.
    // 看到这解释木有?
    private DecorView mDecor;

    private void installDecor() {    
        if (mDecor == null) {
           // 先去创建一个  DecorView 
           mDecor = generateDecor(-1);
        }
        // ......
        // 省略调一些代码,看着晕,不过这也太省了。
        if (mContentParent == null) {
           mContentParent = generateLayout(mDecor);
        }
    }
    
    // generateDecor 方法
    protected DecorView generateDecor(int featureId) {
        // 就是new一个DecorView ,DecorView extends FrameLayout 不同版本的源码有稍微的区别,
        // 低版本DecorView 是PhoneWindow的内部类,高版本是一个单独的类,不过这不影响。
        return new DecorView(context, featureId, this, getAttributes());
    }

    protected ViewGroup generateLayout(DecorView decor) {
        // Inflate the window decor.
        // 我看你到底怎么啦
        int layoutResource;
        // 都是一些判断,发现 layoutResource = 系统的一个资源文件,
        if(){}else if(){}else if(){
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;
            // System.out.println("Simple!");
        }
        
        mDecor.startChanging();
        // 把布局解析加载到  DecorView 而加载的布局是一个系统提供的布局,不同版本不一样
        // 某些源码是 addView() 其实是一样的
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        //  ID_ANDROID_CONTENT 是 android.R.id.content,这个View是从DecorView里面去找的,
        //  也就是    从系统的layoutResource里面找一个id是android.R.id.content的一个FrameLayout
        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        
        // 返回
        return contentParent;
    }

其实看源码一定要带着最初的出发点来看,要不然里面太多了根本找不到方向,如果带着思想来看那么就算跑偏了也可以从新再回来,我目前就是想弄清楚我们的 setContentView() 系统到底把我们的布局加到哪里去了。我先用文字总结一下,然后去画一张图:

  • Activity里面设置setContentView(),我们的布局显示主要是通过PhoneWindow,PhoneWindow获取实例化一个DecorView。
  • 实例化DecorView,然后做一系列的判断然后去解析系统的资源layoutId文件,至于解析哪一个资源文件会做判断比如有没有头部等等,把它解析加载到DecorView,资源layout里面有一个View的id是android.R.id.content。
  • 我们自己通过setContentView设置的布局id其实是解析到mParentContent里面的,也就是那个id叫做android.R.id.content的FarmeLayout,好了就这么多了。

3. AppCompatActivity的setContentView

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        // 跟我在网上看的完全不一样
        getDelegate().setContentView(layoutResID);
    }
   

    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }
    
    // window 还是那个window ,留意一下就行 , 不同的版本返回 AppCompatDelegateImpl,但是都是相互继承
    // 最终继承都是继承  AppCompatDelegateImplV9 有的版本V7有的V9 好麻烦 嗨!
    private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        final int sdk = Build.VERSION.SDK_INT;
        if (BuildCompat.isAtLeastN()) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (sdk >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (sdk >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (sdk >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV9(context, window, callback);
        }
    }

    // 下面其实就没啥好看的了,一个一个点进去,仔细看看就好了。与Activity没啥区别了
    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

    private void ensureSubDecor() {
        mSubDecor = createSubDecor();
    }

4. AppCompatViewInflater源码分析


看到这里还是不知道为什么我的TextView变成了AppCompatTextView,找啊找啊就找了这么个方法:

    @Override
    public void installViewFactory() {
        LayoutInflater layoutInflater = LayoutInflater.from(mContext);
        if (layoutInflater.getFactory() == null) {
            // 把LayoutInflater 的 Factory设置为了this,也就说待会创建View就会走自己的onCreateView方法
            // 如果看不懂还需要看一下 LayoutInflater 的源码,我们的LayoutInflater.from(mContext)其实是一个单例
            // 如果设置了Factory那么每次创建View都会先执行Factory的onCreateView方法
            LayoutInflaterCompat.setFactory(layoutInflater, this);
        } else {
            if (!(LayoutInflaterCompat.getFactory(layoutInflater)
                    instanceof AppCompatDelegateImplV7)) {
                Log.i(TAG, "The Activity's LayoutInflater already has a Factory installed"
                        + " so we can not install AppCompat's");
            }
        }
    }

    @Override
    public View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs) {
        // 看一下是不是 5.0 ,5.0 都自带什么效果我就不说了
        final boolean isPre21 = Build.VERSION.SDK_INT < 21;

        if (mAppCompatViewInflater == null) {
            mAppCompatViewInflater = new AppCompatViewInflater();
        }

        // We only want the View to inherit its context if we're running pre-v21
        final boolean inheritContext = isPre21 && shouldInheritContext((ViewParent) parent);
        // 通过 AppCompatViewInflater 去创建View
        return mAppCompatViewInflater.createView(parent, name, context, attrs, inheritContext,
                isPre21, /* Only read android:theme pre-L (L+ handles this anyway) */
                true, /* Read read app:theme as a fallback at all times for legacy reasons */
                VectorEnabledTintResources.shouldBeUsed() /* Only tint wrap the context if enabled */
        );
    }

其实在讲数据库优化我们已经看过一次AppCompatViewInflater的源码了,创建View都是用的反射,只不过做了缓存和优化而已,我们写代码其实可以仿照源码来,给我们很好的思路。

public final View createView(View parent, final String name, @NonNull Context context,
            @NonNull AttributeSet attrs, boolean inheritContext,
            boolean readAndroidTheme, boolean readAppTheme, boolean wrapContext) {

        View view = null;
        // 果真找到你了,哈哈 ,做了替换
        // We need to 'inject' our tint aware Views in place of the standard framework versions
        switch (name) {
            case "TextView":
                view = new AppCompatTextView(context, attrs);
                break;
            case "ImageView":
                view = new AppCompatImageView(context, attrs);
                break;
            case "Button":
                view = new AppCompatButton(context, attrs);
                break;
            case "EditText":
                view = new AppCompatEditText(context, attrs);
                break;
            // .........
        }

        if (view == null && originalContext != context) {
            // If the original context does not equal our themed context, then we need to manually
            // inflate it using the name so that android:theme takes effect.
            view = createViewFromTag(context, name, attrs);
        }
        return view;
    }

private View createView(Context context, String name, String prefix)
            throws ClassNotFoundException, InflateException {
        // 先从构造缓存里面获取
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        try {
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                Class<? extends View> clazz = context.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                // 利用反射创建一个构造函数
                constructor = clazz.getConstructor(sConstructorSignature);
                sConstructorMap.put(name, constructor);
            }
            constructor.setAccessible(true);
            // 利用反射创建View的实例
            return constructor.newInstance(mConstructorArgs);
        } catch (Exception e) {
            // We do not want to catch these, lets return null and let the actual LayoutInflater
            // try
            return null;
        }
}

5. LayoutInflater源码分析


LayoutInflater的源码我们分三个步骤去看相对来说会更加的系统:
  4. 1 如何获取LayoutInflater?
  4. 2 如何使用LayoutInflater?
  4. 3 布局的View是如何被实例化的?

先来看看我们平时都是怎么去获取LayoutInflater的,这个我们其实并不陌生LayoutInflater.from(context):

    /**
    * Obtains the LayoutInflater from the given context.
    */
    // 是一个静态的方法
    public static LayoutInflater from(Context context) {
        // 通过context获取系统的服务
        LayoutInflater LayoutInflater =
                // context.getSystemService()是一个抽象类,所以我们必须找到实现类ContextImpl
                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
        if (LayoutInflater == null) {
            throw new AssertionError("LayoutInflater not found.");
        }
        return LayoutInflater;
    }

   
    // ContextImpl 里面的实现方法
    @Override
    public Object getSystemService(String name) {
        return SystemServiceRegistry.getSystemService(this, name);
    }

    /**
     * Gets a system service from a given context.
     */
    // SystemServiceRegistry 里面的getSystemService方法
    public static Object getSystemService(ContextImpl ctx, String name) {
        ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
        return fetcher != null ? fetcher.getService(ctx) : null;
    }
    
    // 这是一个静态的HashMap集合
    private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
            new HashMap<String, ServiceFetcher<?>>();
    
    // 静态的代码块中
    static{
         // 注册LayoutInflater服务
         registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                new CachedServiceFetcher<LayoutInflater>() {
            @Override
            public LayoutInflater createService(ContextImpl ctx) {
                return new PhoneLayoutInflater(ctx.getOuterContext());
            }});
         }
         // 注册很多的其他服务......
    }

接下来大致的整理一下获取LayoutInflater的思路,通过Context的实现类ContextImpl获取的,最终是通过SystemServiceRegistry.getSystemService()方法,而SYSTEM_SERVICE_FETCHERS是一个静态的HashMap,初始化是在静态代码块中通过registerService注册了很多服务。所以到目前为止我们有两个思想对于我们后面插件化的皮肤框架有很大的关系,第一LayoutInflater其实是一个系统的服务,第二每次通过LayoutInflater.form(context)是一个静态的单例类无论在哪里获取都是同一个对象。接下来我们来看一下加载布局的三种方式:

1.View.inflate(context,layoutId,parent);
2.LayoutInflater.from(context).inflate(layoutId,parent);
3.LayoutInflater.from(context).inflate(layoutId,parent,attachToRoot);

1.View.inflate(context,layoutId,parent);

    // 其实就是调用的  LayoutInflater.from(context).inflate(layoutId,parent);
    public static View inflate(Context context, @LayoutRes int resource, ViewGroup root) {
        LayoutInflater factory = LayoutInflater.from(context);
        return factory.inflate(resource, root);
    }

2.LayoutInflater.from(context).inflate(layoutId,parent);

    // 其实就是调用的 LayoutInflater.from(context).inflate(layoutId,parent,attachToRoot);
    public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
         return inflate(resource, root, root != null);
    }

3.LayoutInflater.from(context).inflate(layoutId,parent,attachToRoot); 其实最终都是调用的该方法,我们关键是要弄清楚这个参数的概念,尤其是attachToRoot:

    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) + ")");
        }
        // 获取一个 XmlResourceParser 解析器,这个应该并不陌生,就是待会需要去解析我们的layoutId.xml文件
        // 这个到后面的插件化架构再去详细讲解
        final XmlResourceParser parser = res.getLayout(resource);
        try {
            return inflate(parser, root, attachToRoot);
        } finally {
            parser.close();
        }
    }

    public View inflate(XmlPullParser parser, @Nullable ViewGroup root, boolean attachToRoot) {
    synchronized (mConstructorArgs) {
        ......
        //保存传进来的这个view
        View result = root;

        try {
            // Look for the root node.
            int type;
            //在这里找到root标签
            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!");
            }
            //获取这个root标签的名字
            final String name = parser.getName();
             ......

            //判断是否merge标签
            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");
                }
                //这里直接加载页面,忽略merge标签,直接传root进rInflate进行加载子view
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                //通过标签来获取view
                //先获取加载资源文件中的根view
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                //布局参数          
                ViewGroup.LayoutParams params = null;

                //关键代码A
                if (root != null) {
                    params = root.generateLayoutParams(attrs);
                    if (!attachToRoot) {
                        //temp设置布局参数
                        temp.setLayoutParams(params);
                    }
                }
                  ......
                //关键代码B
                //在这里,先获取到了temp,再把temp当做root传进去rInflateChildren
                //进行加载temp后面的子view
                rInflateChildren(parser, temp, attrs, true);
                  ......

                 //关键代码C
                if (root != null && attachToRoot) {
                    //把view添加到root中并设置布局参数
                    root.addView(temp, params);
                }

                //关键代码D
                if (root == null || !attachToRoot) {
                    result = temp;
                }
            }

        } catch (XmlPullParserException e) {
            ......
        } catch (Exception e) {
            ......
        } finally {
            ......
        }

        return result;
       }
    }
    // 创建View
    View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
        // ......
        try {
            // 创建我们的View
            View view;
            if (mFactory2 != null) {
                // 先通过mFactory2 创建,其实在 AppCompatActivity里面会走这个方法,也就会去替换某些控件
                // 所以我们就 看到了上面的内容
                view = mFactory2.onCreateView(parent, name, context, attrs);
            } else if (mFactory != null) {
                // 走mFactory 
                view = mFactory.onCreateView(name, context, attrs);
            } else {
                view = null;
            }
            // ......省略
            if (view == null) {
                final Object lastContext = mConstructorArgs[0];
                mConstructorArgs[0] = context;
                try {
                    // 判断是不是自定义View,自定义View在布局文件中com.hc.BannerView是个全类名,
                    // 而系统的View在布局文件中不是全类名 TextView
                    if (-1 == name.indexOf('.')) {
                        view = onCreateView(parent, name, attrs);
                    } else {
                        view = createView(name, null, attrs);
                    }
                } finally {
                    mConstructorArgs[0] = lastContext;
                }
            }

            return view;
        } catch (InflateException e) {
            // ........
        }
    }
    // 创建View
    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        // 做一些反射的性能优化

        try {
            // 先从缓存中拿,这是没拿到的情况
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                // 加载 clazz
                clazz = mContext.getClassLoader().loadClass(
                        prefix != null ? (prefix + name) : name).asSubclass(View.class);
                
                // 创建View的构造函数
                constructor = clazz.getConstructor(mConstructorSignature);
                constructor.setAccessible(true);
                // 加入缓存集合集合
                sConstructorMap.put(name, constructor);
            } else {
                
            }
            // 通过反射创建View
            final View view = constructor.newInstance(args);
            return view;

        } catch (NoSuchMethodException e) {
            // ......省略部分代码
        }
    }

这里有两个思想比较重要第一个View的创建是通过当前View的全类名反射实例化的View,第二个View的创建首先会走mFactory2,然后会走mFactory,只要不为空先会去执行Factory的onCreateView方法,最后才会走系统的LayoutInflater里面的createView()方法,所以我们完全可以自己去实例化View,这对于我们的插件化换肤很有帮助。
  基于插件式换肤框架搭建 - 资源加载源码分析插件式换肤框架搭建 - setContentView源码阅读这两篇文章我们完全可以自己动手搭建一套换肤框架了,我们下期再见。

所有分享大纲:2017Android进阶之路与你同行

视频讲解地址:https://pan.baidu.com/s/1qYl2AOO

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

推荐阅读更多精彩内容