美文网首页Android技术知识Android开发Android开发经验谈
Android源码设计模式(三)-- 自由扩展你的项目:Buil

Android源码设计模式(三)-- 自由扩展你的项目:Buil

作者: 随时学丫 | 来源:发表于2018-07-11 16:10 被阅读38次

    Android源码设计模式(一) -- 面向对象的六大原则
    Android源码设计模式(二)-- 应用最广的模式:单例模式
    Android源码设计模式(三)-- 自由扩展你的项目:Builder 模式
    Android源码设计模式(四)-- 时势造英雄:策略模式
    Android源码设计模式(五)-- 使编程更有灵活性:责任链模式
    Android源码设计模式(六)— 编程好帮手:代理模式
    Android源码设计模式(七)— 解决、解耦的钥匙 — 观察者模式

    简书 MD 语法不识别 [TOC] ,也不会根据标题行(#) 来插入目录,作为每次看资料喜欢先看目录把握总体的我来说,很不习惯,查找跳转也不方便,所以,如果看到文章没有目录预览的,请安装脚本:简书目录脚本地址

    一、Builder 模式的定义

    将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

    二、Builder 模式的使用场景

    1. 相同的方法,不同的执行顺序,产生不同的事件结果时;
    2. 多个部件或零件,都可以装配到一个对象中,但是产生的运行结果又不相同时;
    3. 产品类非常复杂,或者产品类中的调用顺序不同产生了不同的效能,这个时候使用建造者模式非常合适;
    4. 当初始化一个对象特别复杂,如参数多,且很多参数都具有默认值时。

    三、Builder 模式的 UML 类图

    builder-uml.png

    角色介绍

    • Product 产品类 : 产品的抽象类。
    • Builder:抽象 Builder 类, 规范产品的组建,一般是由子类实现具体的组件过程。
    • ConcreteBuilder : 具体的 Builder 类。
    • Director : 统一组装过程(可省略)。

    四、Builder 模式的简单实现

    简单实现的介绍

    电脑的组装过程较为复杂,步骤繁多,但是顺序却是不固定的,为了易于理解,我们把计算机组装过程简化为构建主机,设置操作系统,设置显示器 3 个部分,然后通过 Director 和具体的 Builder 来构建计算机对象。下面我们以组装电脑为例来演示一下简单且经典的builder 模式。

    package com.dp.example.builder;
    /**
     * Computer产品抽象类, 为了例子简单, 只列出这几个属性
     * @author mrsimple
     */
    public abstract class Computer {
    
        protected int mCpuCore = 1;
        protected int mRamSize = 0;
        protected String mOs = "Dos";
    
        protected Computer() {
    
        }
        // 设置CPU核心数
        public abstract void setCPU(int core);
        // 设置内存
        public abstract void setRAM(int gb);
        // 设置操作系统
        public abstract void setOs(String os);
        @Override
        public String toString() {
            return "Computer [mCpuCore=" + mCpuCore + ", mRamSize=" + mRamSize
                    + ", mOs=" + mOs + "]";
        }
    }
    
    package com.dp.example.builder;
    /**
     * Apple电脑
     */
    public class AppleComputer extends Computer {
    
        protected AppleComputer() {
        }
        @Override
        public void setCPU(int core) {
            mCpuCore = core;
        }
        @Override
        public void setRAM(int gb) {
            mRamSize = gb;
        }
        @Override
        public void setOs(String os) {
            mOs = os;
        }
    }
    
    package com.dp.example.builder;
    /**
     * builder抽象类
     *
     */
    public abstract class Builder {
        // 设置CPU核心数
        public abstract void buildCPU(int core);
        // 设置内存
        public abstract void buildRAM(int gb);
        // 设置操作系统
        public abstract void buildOs(String os);
        // 创建Computer
        public abstract Computer create();
    }
    
    package com.dp.example.builder;
    public class ApplePCBuilder extends Builder {
        private Computer mApplePc = new AppleComputer();
        @Override
        public void buildCPU(int core) {
            mApplePc.setCPU(core);
        }
        @Override
        public void buildRAM(int gb) {
            mApplePc.setRAM(gb);
        }
        @Override
        public void buildOs(String os) {
            mApplePc.setOs(os);
        }
        @Override
        public Computer create() {
            return mApplePc;
        }
    }
    
    package com.dp.example.builder;
    public class Director {
        Builder mBuilder = null;
        /**
         * @param builder
         */
        public Director(Builder builder) {
            mBuilder = builder;
        }
        /**
         * 构建对象
         * @param cpu
         * @param ram
         * @param os
         */
        public void construct(int cpu, int ram, String os) {
            mBuilder.buildCPU(cpu);
            mBuilder.buildRAM(ram);
            mBuilder.buildOs(os);
        }
    }
    
    /**
     * 经典实现较为繁琐
     * @author mrsimple
     */
    public class Test {
        public static void main(String[] args) {
            // 构建器
            Builder builder = new ApplePCBuilder();
            // Director
            Director pcDirector = new Director(builder);
            // 封装构建过程, 4核, 内存2GB, Mac系统
            pcDirector.construct(4, 2, "Mac OS X 10.9.1");
            // 构建电脑, 输出相关信息
            System.out.println("Computer Info : " + builder.create().toString());
        }
    }
    

    通过 Builder 来构建产品对象, 而 Director 封装了构建复杂产品对象对象的过程,不对外隐藏构建细节。Builder 和 Director 一起将复杂对象的构建和表示分离,使得同样的构建过程可以创建不同的对象。

    Director 在实际开发中经常被省略,而是直接用 Builder 来进行对象的组装,这个 Builder 通常为链式调用,它的关键点是每个 setter 方法都返回自身,也就是 return this,这样就使得 setter 方法可以链式调用。

    new Builder().setA("A").setB("B").build();
    

    通过这种方式不仅去除了 Director 角色,整个结构更加简单,也能对 Product 对象的组装过程有更加精细的控制。

    省去 Director 角色的 Builder,我之前写过一篇 PopupWindow 的封装,就是采用 Builder 模式。MPopupWindow

    五、 Android 源码中的 Builder 模式实现

    在 Android 源码中,我们最常用到的 Builder 模式就是 AlertDialog.Builder, 使用该 Builder 来构建复杂的AlertDialog 对象。简单示例如下 :

        //显示基本的AlertDialog  
        private void showDialog(Context context) {  
            AlertDialog.Builder builder = new AlertDialog.Builder(context);  
            builder.setIcon(R.drawable.icon);  
            builder.setTitle("Title");  
            builder.setMessage("Message");  
            builder.setPositiveButton("Button1",  
                    new DialogInterface.OnClickListener() {  
                        public void onClick(DialogInterface dialog, int whichButton) {  
                            setTitle("点击了对话框上的Button1");  
                        }  
                    });  
            builder.setNeutralButton("Button2",  
                    new DialogInterface.OnClickListener() {  
                        public void onClick(DialogInterface dialog, int whichButton) {  
                            setTitle("点击了对话框上的Button2");  
                        }  
                    });  
            builder.setNegativeButton("Button3",  
                    new DialogInterface.OnClickListener() {  
                        public void onClick(DialogInterface dialog, int whichButton) {  
                            setTitle("点击了对话框上的Button3");  
                        }  
                    });  
            builder.create().show();  // 构建AlertDialog, 并且显示
        } 
    
    result.png

    下面我们看看 AlertDialog 的相关源码 :

    // AlertDialog
    public class AlertDialog extends Dialog implements DialogInterface {
        // Controller, 接受Builder成员变量P中的各个参数
        private AlertController mAlert;
    
        // 构造函数
        protected AlertDialog(Context context, int theme) {
            this(context, theme, true);
        }
    
        // 4 : 构造AlertDialog
        AlertDialog(Context context, int theme, boolean createContextWrapper) {
            super(context, resolveDialogTheme(context, theme), createContextWrapper);
            mWindow.alwaysReadCloseOnTouchAttr();
            mAlert = new AlertController(getContext(), this, getWindow());
        }
    
        // 实际上调用的是mAlert的setTitle方法
        @Override
        public void setTitle(CharSequence title) {
            super.setTitle(title);
            mAlert.setTitle(title);
        }
    
        // 实际上调用的是mAlert的setCustomTitle方法
        public void setCustomTitle(View customTitleView) {
            mAlert.setCustomTitle(customTitleView);
        }
        
        public void setMessage(CharSequence message) {
            mAlert.setMessage(message);
        }
    
        // AlertDialog其他的代码省略
        
        // ************  Builder为AlertDialog的内部类   *******************
        public static class Builder {
            // 1 : 存储AlertDialog的各个参数, 例如title, message, icon等.
            private final AlertController.AlertParams P;
            // 属性省略
            
            /**
             * Constructor using a context for this builder and the {@link AlertDialog} it creates.
             */
            public Builder(Context context) {
                this(context, resolveDialogTheme(context, 0));
            }
    
    
            public Builder(Context context, int theme) {
                P = new AlertController.AlertParams(new ContextThemeWrapper(
                        context, resolveDialogTheme(context, theme)));
                mTheme = theme;
            }
            
            // Builder的其他代码省略 ......
    
            // 2 : 设置各种参数
            public Builder setTitle(CharSequence title) {
                P.mTitle = title;
                return this;
            }
            
            
            public Builder setMessage(CharSequence message) {
                P.mMessage = message;
                return this;
            }
    
            public Builder setIcon(int iconId) {
                P.mIconId = iconId;
                return this;
            }
            
            public Builder setPositiveButton(CharSequence text, final OnClickListener listener) {
                P.mPositiveButtonText = text;
                P.mPositiveButtonListener = listener;
                return this;
            }
            
            
            public Builder setView(View view) {
                P.mView = view;
                P.mViewSpacingSpecified = false;
                return this;
            }
            
            // 3 : 构建AlertDialog, 传递参数
            public AlertDialog create() {
                // 调用new AlertDialog构造对象, 并且将参数传递个体AlertDialog 
                final AlertDialog dialog = new AlertDialog(P.mContext, mTheme, false);
                // 5 : 将P中的参数应用的dialog中的mAlert对象中
                P.apply(dialog.mAlert);
                dialog.setCancelable(P.mCancelable);
                if (P.mCancelable) {
                    dialog.setCanceledOnTouchOutside(true);
                }
                dialog.setOnCancelListener(P.mOnCancelListener);
                if (P.mOnKeyListener != null) {
                    dialog.setOnKeyListener(P.mOnKeyListener);
                }
                return dialog;
            }
        }
        
    }
    

    可以看到,通过 Builder 来设置 AlertDialog 中的 title, message, button 等参数, 这些参数都存储在类型为AlertController.AlertParams 的成员变量 P 中,AlertController.AlertParams 中包含了与之对应的成员变量。在调用 Builder 类的 create 函数时才创建 AlertDialog, 并且将 Builder 成员变量 P 中保存的参数应用到 AlertDialog的 mAlert 对象中,即 P.apply(dialog.mAlert) 代码段。我们看看 apply 函数的实现 :

            public void apply(AlertController dialog) {
                if (mCustomTitleView != null) {
                    dialog.setCustomTitle(mCustomTitleView);
                } else {
                    if (mTitle != null) {
                        dialog.setTitle(mTitle);
                    }
                    if (mIcon != null) {
                        dialog.setIcon(mIcon);
                    }
                    if (mIconId >= 0) {
                        dialog.setIcon(mIconId);
                    }
                    if (mIconAttrId > 0) {
                        dialog.setIcon(dialog.getIconAttributeResId(mIconAttrId));
                    }
                }
                if (mMessage != null) {
                    dialog.setMessage(mMessage);
                }
                if (mPositiveButtonText != null) {
                    dialog.setButton(DialogInterface.BUTTON_POSITIVE, mPositiveButtonText,
                            mPositiveButtonListener, null);
                }
                if (mNegativeButtonText != null) {
                    dialog.setButton(DialogInterface.BUTTON_NEGATIVE, mNegativeButtonText,
                            mNegativeButtonListener, null);
                }
                if (mNeutralButtonText != null) {
                    dialog.setButton(DialogInterface.BUTTON_NEUTRAL, mNeutralButtonText,
                            mNeutralButtonListener, null);
                }
                if (mForceInverseBackground) {
                    dialog.setInverseBackgroundForced(true);
                }
                // For a list, the client can either supply an array of items or an
                // adapter or a cursor
                if ((mItems != null) || (mCursor != null) || (mAdapter != null)) {
                    createListView(dialog);
                }
                if (mView != null) {
                    if (mViewSpacingSpecified) {
                        dialog.setView(mView, mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight,
                                mViewSpacingBottom);
                    } else {
                        dialog.setView(mView);
                    }
                }
            }
    

    在 apply 函数中,只是将 AlertParams 参数设置到 AlertController 中。例如,将标题设置到 Dialog 对应的标题识图中,将 Message 设置到内容视图中等。当我们获取到 AlertDialog 对象后,通过 show 函数就可以显示这个对话框。 AlertDialog 中相关代码如下:

    //显示Dialog
    public void show() {
            //已经是显示状态,则return
            if (mShowing) {
                if (mDecor != null) {
                    if (mWindow.hasFeature(Window.FEATURE_ACTION_BAR)) {
                        mWindow.invalidatePanelMenu(Window.FEATURE_ACTION_BAR);
                    }
                    mDecor.setVisibility(View.VISIBLE);
                }
                return;
            }
            mCanceled = false;
            //1、onCreate调用
            if (!mCreated) {
                dispatchOnCreate(null);
            }
            //2、onStart
            onStart();
            //3、获取DecorView
            mDecor = mWindow.getDecorView();
            //省略  
            //4、获取布局参数
            WindowManager.LayoutParams l = mWindow.getAttributes();
            if ((l.softInputMode
                    & WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION) == 0) {
                WindowManager.LayoutParams nl = new WindowManager.LayoutParams();
                nl.copyFrom(l);
                nl.softInputMode |=
                        WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION;
                l = nl;
            }
            try {  
                //5、将mDecor添加到mWindowManager中
                mWindowManager.addView(mDecor, l);
                mShowing = true;
                //发送一个显示Dialog的消息
                sendShowMessage();
            } finally {
            }
        }
    

    在 show 函数中主要做了如下几个事情:

    1. 通过 dispatchOnCreate 函数来调用 AlertDialog 中的 onCreate 函数。
    2. 调用 AlertDialog 的 onStart 函数。
    3. 最后将 Dialog 的 DecorView 添加到 WindowManager 中。

    很明显,这就是一系列的典型的生命周期函数。按照惯例,AlertDialog 的内容视图构建按理应该在 onCreate 函数中,我们来看看是不是:

    public class AlertDialog extends Dialog implements DialogInterface {
       @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            //调用了AlertController的installContent方法
            mAlert.installContent();
        }
    }
    

    在 onCreate 函数中主要调用了 AlertController 的 installContent 方法,Dialog 中的 onCreate 函数只是一个空实现而已,可以忽略他。那么 AlertDialog 的内容视图必然就在 installContent 函数中。

    public void installContent() {
            /* We use a custom title so never request a window title */
            mWindow.requestFeature(Window.FEATURE_NO_TITLE);
            int contentView = selectContentView();
            mWindow.setContentView(contentView);
            setupView();
            setupDecor();
        }
        private int selectContentView() {
            if (mButtonPanelSideLayout == 0) {
                return mAlertDialogLayout;
            }
            if (mButtonPanelLayoutHint == AlertDialog.LAYOUT_HINT_SIDE) {
                return mButtonPanelSideLayout;
            }
            // TODO: use layout hint side for long messages/lists
            return mAlertDialogLayout;
        }
    

    installContent 函数的代码很少,但极为重要,它调用了 Window 对象的 setContentView,这个 setContentView 就与 Activity 中的一模一样,实际上 Activity 最终也是调用 Window 对象的 setContentView 函数。因此,这里就是设置 AlertDialog 的内容布局,这个布局就是 mAlertDialogLayout 的值。这个值在AlertController 的构造函数中进行了初始化,具体代码如下:

    public AlertController(Context context, DialogInterface di, Window window) {
            mContext = context;
            mDialogInterface = di;
            mWindow = window;
            mHandler = new ButtonHandler(di);
            final TypedArray a = context.obtainStyledAttributes(null,
                    R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
            //AlertController的布局id,也就是alert_dialog.xml布局
            mAlertDialogLayout = a.getResourceId(
                    R.styleable.AlertDialog_layout, R.layout.alert_dialog);
            mButtonPanelSideLayout = a.getResourceId(
                    R.styleable.AlertDialog_buttonPanelSideLayout, 0);
            mListLayout = a.getResourceId(
                    R.styleable.AlertDialog_listLayout, R.layout.select_dialog);
            //省略
            a.recycle();
    }
    

    从 AlertController 的构造函数中可以看到,AlertDialog 的布局资源就是 select_dialog.xml 这个布局,我们直接大致看他的结构如下:

    AlertController 结构

    当通过 Builder 对象的 setTitle、setMessage 等方法设置具体内容时,就是将这些内容填充到对应的视图中。而AlertDialog 也允许你通过 setView 传入内容视图,这个内容视图就是替换掉图中的内容视图(蓝色区域),AlertDialog 预留了一个 costomPanel 区域用来显示用户自定义的内容视图。我们来看看 setupView 函数:

    private void setupView() {
            final View parentPanel = mWindow.findViewById(R.id.parentPanel);
            final View defaultTopPanel = parentPanel.findViewById(R.id.topPanel);
            //1、获取内容区域
            final View defaultContentPanel = parentPanel.findViewById(R.id.contentPanel);
            //1、获取按钮
            final View defaultButtonPanel = parentPanel.findViewById(R.id.buttonPanel);
            // Install custom content before setting up the title or buttons so
            // that we can handle panel overrides.
            final ViewGroup customPanel = (ViewGroup) parentPanel.findViewById(R.id.customPanel);
            //自定义内容视图区域
            setupCustomContent(customPanel);
            final View customTopPanel = customPanel.findViewById(R.id.topPanel);
            final View customContentPanel = customPanel.findViewById(R.id.contentPanel);
            final View customButtonPanel = customPanel.findViewById(R.id.buttonPanel);
            // Resolve the correct panels and remove the defaults, if needed.
            final ViewGroup topPanel = resolvePanel(customTopPanel, defaultTopPanel);
            final ViewGroup contentPanel = resolvePanel(customContentPanel, defaultContentPanel);
            final ViewGroup buttonPanel = resolvePanel(customButtonPanel, defaultButtonPanel);
            //2、初始化内容
            setupContent(contentPanel);
            setupButtons(buttonPanel);
            //2、初始化标题
            setupTitle(topPanel);
            //自定义视图可见性
            final boolean hasCustomPanel = customPanel != null
                    && customPanel.getVisibility() != View.GONE;
            final boolean hasTopPanel = topPanel != null
                    && topPanel.getVisibility() != View.GONE;
            final boolean hasButtonPanel = buttonPanel != null
                    && buttonPanel.getVisibility() != View.GONE;
            // Only display the text spacer if we don't have buttons.
            if (!hasButtonPanel) {
                if (contentPanel != null) {
                    final View spacer = contentPanel.findViewById(R.id.textSpacerNoButtons);
                    if (spacer != null) {
                        spacer.setVisibility(View.VISIBLE);
                    }
                }
                mWindow.setCloseOnTouchOutsideIfNotSet(true);
            }
            if (hasTopPanel) {
                // Only clip scrolling content to padding if we have a title.
                if (mScrollView != null) {
                    mScrollView.setClipToPadding(true);
                }
                // Only show the divider if we have a title.
                final View divider;
                if (mMessage != null || mListView != null || hasCustomPanel) {
                    divider = topPanel.findViewById(R.id.titleDivider);
                } else {
                    divider = topPanel.findViewById(R.id.titleDividerTop);
                }
                if (divider != null) {
                    divider.setVisibility(View.VISIBLE);
                }
            }
            // Update scroll indicators as needed.
            if (!hasCustomPanel) {
                final View content = mListView != null ? mListView : mScrollView;
                if (content != null) {
                    final int indicators = (hasTopPanel ? View.SCROLL_INDICATOR_TOP : 0)
                            | (hasButtonPanel ? View.SCROLL_INDICATOR_BOTTOM : 0);
                    content.setScrollIndicators(indicators,
                            View.SCROLL_INDICATOR_TOP | View.SCROLL_INDICATOR_BOTTOM);
                }
            }
            final TypedArray a = mContext.obtainStyledAttributes(
                    null, R.styleable.AlertDialog, R.attr.alertDialogStyle, 0);
            //设置背景
            setBackground(a, topPanel, contentPanel, customPanel, buttonPanel,
                    hasTopPanel, hasCustomPanel, hasButtonPanel);
            a.recycle();
        }
        //自定义内容视图区域
        private void setupCustomContent(ViewGroup customPanel) {
            final View customView;
            //如果用户设置了内容视图,那么将它显示在customPanel的custom布局里面
            if (mView != null) {
                customView = mView;
            } else if (mViewLayoutResId != 0) {
                final LayoutInflater inflater = LayoutInflater.from(mContext);
                customView = inflater.inflate(mViewLayoutResId, customPanel, false);
            } else {
                customView = null;
            }
            final boolean hasCustomView = customView != null;
            if (!hasCustomView || !canTextInput(customView)) {
                mWindow.setFlags(WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM,
                        WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM);
            }
            if (hasCustomView) {
                final FrameLayout custom = (FrameLayout) mWindow.findViewById(R.id.custom);
                //显示用户设置的视图
                custom.addView(customView, new LayoutParams(MATCH_PARENT, MATCH_PARENT));
                if (mViewSpacingSpecified) {
                    custom.setPadding(
                            mViewSpacingLeft, mViewSpacingTop, mViewSpacingRight, mViewSpacingBottom);
                }
                if (mListView != null) {
                    ((LinearLayout.LayoutParams) customPanel.getLayoutParams()).weight = 0;
                }
            } else {
                customPanel.setVisibility(View.GONE);
            }
        }
    
    

    这个 setupView 顾名思义就是初始化 AlertDialog 布局中的各个部分,如标题区域、按钮区域、内容区域等,在该函数调用之后整个 Dialog 的视图内容全部设置完毕。而这些各区域的视图都属于 mAlertDialogLayout 布局中的子元素,Window 对象又关联了 mAlertDialogLayout 的整个布局树,当调用完 setupView 之后整个视图树的数据都填充完毕。当用户调用了 show 函数时,WindosManager 会将 Window 对象的 DecorView (也就是 mAlertDialogLayout 对应的视图),添加到用户的窗口上,并且显示出来。至此,整个 Dialog 就会出现在用户的视野中了。

    AlertDialog
    • Builder 内部类
    • onCreate() 方法调用 AlertController 类 installContent() 方法设置内容布局
    AlertDialog.Builder --- Builder、ConcreteBuilder、Dirctor角色
    • 初始化 AlertController.AlertParams 对象 p 储存数据 (Title、message、icon 等)。

    • set 方法将数据储存至 AlertController.AlertParams 对象 p。

    • create() 方法储存将 AlertController.AlertParams 对象 p 数据应用至 AlertController 对象 dialog.mAlert。

    • show() 方法调用 AlertDialog 父类 Dialog 的 show() 方法加载布局。

    AlertController
    • AlertParams内部类
    • 接收数据(Title、message、icon等)
    • installContent()方法调用Window对象的setContentView函数设置AlertDialog的内容布局,并调用setupView()方法初始化布局
    • setupView()初始化布局
    AlertController.AlertParams
    • 用于储存数据(Title、message、icon等)
    • apply()方法将数据传递给AlertDialog类实例化的AlertController对象

    在 AlertDialog 的 Builder 模式中并没有看到 Director 角色的出现(其实在很多场景中,Android 并没有按照《设计模式: 可复用面向对象软件的基础》一书中描述的经典模式来实现,而是做了些修改,使得更易于使用)。这里的AlertDialog.Builder 同时扮演了 builder、ConcreteBuilder、Dirctor 的角色,简化了 Builder 模式的设计。

    六、总结

    优点与缺点

    优点

    • 良好的封装性, 使用建造者模式可以使客户端不必知道产品内部组成的细节;
    • 建造者独立,容易扩展;
    • 在对象创建过程中会使用到系统中的一些其它对象,这些对象在产品对象的创建过程中不易得到。

    缺点

    • 会产生多余的 Builder 对象以及 Director 对象,消耗内存;
    • 对象的构建过程暴露。

    相关文章

      网友评论

        本文标题:Android源码设计模式(三)-- 自由扩展你的项目:Buil

        本文链接:https://www.haomeiwen.com/subject/gaihpftx.html