• Android中将xml布局文件转化为View树的过程分析(下)-- LayoutInflater源码分析


      在Android开发中为了inflate一个布局文件,大体有2种方式,如下所示:

        // 1. get a instance of LayoutInflater, then do whatever you want
        LayoutInflater inflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            
        // 2. you're in some View class, then just call View's static inflate method 
        View.inflate(context, R.layout.xxx_xml, someViewGroup/null);

    我们来看看这2种方式的具体源码:

        <!-- View.java -->
        /**
         * Inflate a view from an XML resource.  This convenience method wraps the {@link
         * LayoutInflater} class, which provides a full range of options for view inflation.
         *
         * @param context The Context object for your activity or application.
         * @param resource The resource ID to inflate
         * @param root A view group that will be the parent.  Used to properly inflate the
         * layout_* parameters.
         * @see LayoutInflater
         */
        public static View inflate(Context context, int resource, ViewGroup root) {
            LayoutInflater factory = LayoutInflater.from(context);
            return factory.inflate(resource, root);
        }
    
        <!-- LayoutInflater.java -->
        /**
         * Obtains the LayoutInflater from the given context.
         */
        public static LayoutInflater from(Context context) {
            LayoutInflater LayoutInflater =
                    (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            if (LayoutInflater == null) {
                throw new AssertionError("LayoutInflater not found.");
            }
            return LayoutInflater;
        }

    现在我们看到实质上都是方法1中的做法,View.inflate只是个helper方法而已(少敲几行代码)。那么我们就先来看看

    Context.getSystemService的具体实现,这里我们直接去ContextImpl.java文件中的相关代码:

        /**
         * Override this class when the system service constructor needs a
         * ContextImpl.  Else, use StaticServiceFetcher below.
         */
        /*package*/ static class ServiceFetcher {
            int mContextCacheIndex = -1;
    
            /**
             * Main entrypoint; only override if you don't need caching.
             */
            public Object getService(ContextImpl ctx) {
                ArrayList<Object> cache = ctx.mServiceCache;
                Object service;
                synchronized (cache) {
                    if (cache.size() == 0) {
                        // Initialize the cache vector on first access.
                        // At this point sNextPerContextServiceCacheIndex
                        // is the number of potential services that are
                        // cached per-Context.
                        for (int i = 0; i < sNextPerContextServiceCacheIndex; i++) {
                            cache.add(null);
                        }
                    } else {
                        service = cache.get(mContextCacheIndex); // 先从cache中找,
                        if (service != null) { // 如果已经存在了直接返回
                            return service;
                        }
                    }
                    service = createService(ctx); // 否则创建并加入到cache中,只会调用1次
                    cache.set(mContextCacheIndex, service);
                    return service;
                }
            }
    
            /**
             * Override this to create a new per-Context instance of the
             * service.  getService() will handle locking and caching.
             */
            public Object createService(ContextImpl ctx) {
                throw new RuntimeException("Not implemented");
            }
        }
    
        /**
         * Override this class for services to be cached process-wide.
         */
        abstract static class StaticServiceFetcher extends ServiceFetcher {
            private Object mCachedInstance;
    
            @Override
            public final Object getService(ContextImpl unused) {
                synchronized (StaticServiceFetcher.this) {
                    Object service = mCachedInstance;
                    if (service != null) {
                        return service;
                    }
                    return mCachedInstance = createStaticService();
                }
            }
    
            public abstract Object createStaticService(); // 它不需要ContextImpl参数
        }
    
        private static final HashMap<String, ServiceFetcher> SYSTEM_SERVICE_MAP =
                new HashMap<String, ServiceFetcher>(); // 全局system service的map
    
        private static int sNextPerContextServiceCacheIndex = 0;
        private static void registerService(String serviceName, ServiceFetcher fetcher) {
            if (!(fetcher instanceof StaticServiceFetcher)) {
                fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
            }
            SYSTEM_SERVICE_MAP.put(serviceName, fetcher); // 放到全局的静态map中
        }
    // 还有很多registerService的调用,这里都省略了,我们现在只关心LAYOUT_INFLATER_SERVICE registerService(LAYOUT_INFLATER_SERVICE,
    new ServiceFetcher() { public Object createService(ContextImpl ctx) { // 我们前一篇文章中提到过会new一个 return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext()); // PhoneLayoutInflater的对象返回 }});

      到这里我们就清楚了Context.getSystemService方法的具体实现了,接下来我们将注意力转移到LayoutInflater类。关键代码如下:

        /**
         * Obtains the LayoutInflater from the given context.
         */
        public static LayoutInflater from(Context context) {
            LayoutInflater LayoutInflater =
                    (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            if (LayoutInflater == null) {
                throw new AssertionError("LayoutInflater not found.");
            }
            return LayoutInflater;
        }
    
        /**
         * Inflate a new view hierarchy from the specified xml resource. Throws
         * {@link InflateException} if there is an error.
         * 
         * @param resource ID for an XML layout resource to load (e.g.,
         *        <code>R.layout.main_page</code>)
         * @param root Optional view to be the parent of the generated hierarchy.
         * @return The root View of the inflated hierarchy. If root was supplied,
         *         this is the root View; otherwise it is the root of the inflated
         *         XML file.
         */
        public View inflate(int resource, ViewGroup root) { // 实际上调用3个参数的版本,从这里我们可以看出客户端代码
            return inflate(resource, root, root != null); // 没必要这样写(root!= null):inflate(resource, root, true);
        }
    
        /**
         * Inflate a new view hierarchy from the specified xml node. Throws
         * {@link InflateException} if there is an error. *
         * <p>
         * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
         * reasons, view inflation relies heavily on pre-processing of XML files
         * that is done at build time. Therefore, it is not currently possible to
         * use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
         * 
         * @param parser XML dom node containing the description of the view
         *        hierarchy.
         * @param root Optional view to be the parent of the generated hierarchy.
         * @return The root View of the inflated hierarchy. If root was supplied,
         *         this is the root View; otherwise it is the root of the inflated
         *         XML file.
         */
        public View inflate(XmlPullParser parser, ViewGroup root) { // 不太常用,我们一般使用layout文件的版本,但实质都一样
            return inflate(parser, root, root != null);             // 下面的代码中inflate一个include tag时调用了此版本
        }
    
        /**
         * Inflate a new view hierarchy from the specified xml resource. Throws
         * {@link InflateException} if there is an error.
         * 
         * @param resource ID for an XML layout resource to load (e.g.,
         *        <code>R.layout.main_page</code>)
         * @param root Optional view to be the parent of the generated hierarchy (if
         *        <em>attachToRoot</em> is true), or else simply an object that
         *        provides a set of LayoutParams values for root of the returned
         *        hierarchy (if <em>attachToRoot</em> is false.)
         * @param attachToRoot Whether the inflated hierarchy should be attached to
         *        the root parameter? If false, root is only used to create the
         *        correct subclass of LayoutParams for the root view in the XML.
         * @return The root View of the inflated hierarchy. If root was supplied and
         *         attachToRoot is true, this is root; otherwise it is the root of
         *         the inflated XML file.
         */
        public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
            if (DEBUG) System.out.println("INFLATING from resource: " + resource);
            XmlResourceParser parser = getContext().getResources().getLayout(resource);
            try {
                return inflate(parser, root, attachToRoot);
            } finally {
                parser.close();
            }
        }
    
        /**
         * Inflate a new view hierarchy from the specified XML node. Throws
         * {@link InflateException} if there is an error.
         * <p>
         * <em><strong>Important</strong></em>&nbsp;&nbsp;&nbsp;For performance
         * reasons, view inflation relies heavily on pre-processing of XML files
         * that is done at build time. Therefore, it is not currently possible to
         * use LayoutInflater with an XmlPullParser over a plain XML file at runtime.
         * 
         * @param parser XML dom node containing the description of the view
         *        hierarchy.
         * @param root Optional view to be the parent of the generated hierarchy (if
         *        <em>attachToRoot</em> is true), or else simply an object that
         *        provides a set of LayoutParams values for root of the returned
         *        hierarchy (if <em>attachToRoot</em> is false.)
         * @param attachToRoot Whether the inflated hierarchy should be attached to
         *        the root parameter? If false, root is only used to create the
         *        correct subclass of LayoutParams for the root view in the XML.
         * @return The root View of the inflated hierarchy. If root was supplied and
         *         attachToRoot is true, this is root; otherwise it is the root of
         *         the inflated XML file.
         */
        public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) { // 这是最终调用的版本
            synchronized (mConstructorArgs) { // 进入同步块
                Trace.traceBegin(Trace.TRACE_TAG_VIEW, "inflate");
    
                final AttributeSet attrs = Xml.asAttributeSet(parser);
                Context lastContext = (Context)mConstructorArgs[0];
                mConstructorArgs[0] = mContext;
                View result = root; // 此方法最后的返回值,初始化为传入的root
    
                try {
                    // Look for the root node.
                    int type;
                    while ((type = parser.next()) != XmlPullParser.START_TAG &&
                            type != XmlPullParser.END_DOCUMENT) {
                        // Empty
                    }
                    // 能走到这里,说明type是START_TAG 或 END_DOCUMENT
                    if (type != XmlPullParser.START_TAG) { // 如果一开始就是END_DOCUMENT,那说明xml文件有问题
                        throw new InflateException(parser.getPositionDescription()
                                + ": No start tag found!");
                    }
                    // 能到这里,那type一定是START_TAG,也就是xml文件里的root node
                    final String name = parser.getName(); // 获得当前start tag的name
                    
                    if (DEBUG) {
                        System.out.println("**************************");
                        System.out.println("Creating root view: "
                                + name);
                        System.out.println("**************************");
                    }
    
                    if (TAG_MERGE.equals(name)) { // 处理merge tag的情况
                        if (root == null || !attachToRoot) { // root必须非空且attachToRoot为true,否则抛异常结束
                            throw new InflateException("<merge /> can be used only with a valid "
                                    + "ViewGroup root and attachToRoot=true"); // 因为merge的xml并不代表某个具体的view,只是将它
                        }                                                 // 包起来的其他xml的内容加到某个上层ViewGroup中
    
                        rInflate(parser, root, attrs, false); // 递归的inflate
                    } else {
                        // Temp is the root view that was found in the xml
                        View temp; // xml文件中的root view
                        if (TAG_1995.equals(name)) {
                            temp = new BlinkLayout(mContext, attrs);
                        } else {
                            temp = createViewFromTag(root, name, attrs); // 根据tag节点创建view对象
                        }
    
                        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); // 根据root生成合适的LayoutParams实例
                            if (!attachToRoot) { // 如果不attach的话就调用view的setLayoutParams方法
                                // 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
                        rInflate(parser, temp, attrs, true); // 递归inflate剩下的所有children
                        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非空且指定了要attachToRoot
                            root.addView(temp, params); // 将xml文件的root view 加到用户提供的root里
                        }
    
                        // Decide whether to return the root that was passed in or the
                        // top view found in xml.
                        if (root == null || !attachToRoot) {
                            result = temp; // 否则我们将返回xml里发现的root view:temp,而不是方法中传递进来的root对象
                        }
                    }
    
                } catch (XmlPullParserException e) {
                    InflateException ex = new InflateException(e.getMessage());
                    ex.initCause(e);
                    throw ex;
                } catch (IOException e) {
                    InflateException ex = new InflateException(
                            parser.getPositionDescription()
                            + ": " + e.getMessage());
                    ex.initCause(e);
                    throw ex;
                } finally {
                    // Don't retain static reference on context.
                    mConstructorArgs[0] = lastContext;
                    mConstructorArgs[1] = null;
                }
    
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
    
                return result; // 返回参数root或xml文件里的root view
            }
        }

    接下来我们看看inflate各种不同节点的方法:

        /**
         * Recursive method used to descend down the xml hierarchy and instantiate
         * views, instantiate their children, and then call onFinishInflate().
         */
        void rInflate(XmlPullParser parser, View parent, final AttributeSet attrs,
                boolean finishInflate) throws XmlPullParserException, IOException { // 深度优先inflate,所有才能保证你在onFinish
                                                                     // Inflate()里可以通过findViewById找到已经创建完毕的孩子view
            final int depth = parser.getDepth();
            int type;
    
            while (((type = parser.next()) != XmlPullParser.END_TAG ||
                    parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {
    
                if (type != XmlPullParser.START_TAG) {
                    continue;
                }
                // 确保是一个START_TAG node
                final String name = parser.getName(); // 拿到tagName
                
                if (TAG_REQUEST_FOCUS.equals(name)) { // 处理REQUEST_FOCUS tag
                    parseRequestFocus(parser, parent);
                } else if (TAG_INCLUDE.equals(name)) { // 处理include tag
                    if (parser.getDepth() == 0) { // include节点不能是根节点,否则就抛异常了。。。
                        throw new InflateException("<include /> cannot be the root element");
                    }
                    parseInclude(parser, parent, attrs);
                } else if (TAG_MERGE.equals(name)) { // merge节点必须是xml文件里的根节点,也就是说到这里的时候不应该再出现merge节点了
                    throw new InflateException("<merge /> must be the root element");
                } else if (TAG_1995.equals(name)) {
                    final View view = new BlinkLayout(mContext, attrs);
                    final ViewGroup viewGroup = (ViewGroup) parent;
                    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                    rInflate(parser, view, attrs, true);
                    viewGroup.addView(view, params);                
                } else { // 一般情况,各种Android view、widget或用户自定义的view节点
                    final View view = createViewFromTag(parent, name, attrs);
                    final ViewGroup viewGroup = (ViewGroup) parent;
                    final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
                    rInflate(parser, view, attrs, true);
                    viewGroup.addView(view, params);
                }
            }
    
            if (finishInflate) parent.onFinishInflate(); // parent的所有孩子节点都inflate完毕的时候,调用onFinishInflate回调
        }
    
        private void parseRequestFocus(XmlPullParser parser, View parent)
                throws XmlPullParserException, IOException {
            int type;
            parent.requestFocus();
            final int currentDepth = parser.getDepth();
            while (((type = parser.next()) != XmlPullParser.END_TAG || // 忽略此节点剩下的所有内容,直到下一个新的START_TAG
                    parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }
        }
    
        private void parseInclude(XmlPullParser parser, View parent, AttributeSet attrs)
                throws XmlPullParserException, IOException {
    
            int type;
    
            if (parent instanceof ViewGroup) {
                final int layout = attrs.getAttributeResourceValue(null, "layout", 0); // include节点中必须指定layout属性的值
                if (layout == 0) {
                    final String value = attrs.getAttributeValue(null, "layout");
                    if (value == null) {
                        throw new InflateException("You must specifiy a layout in the"
                                + " include tag: <include layout="@layout/layoutID" />");
                    } else {
                        throw new InflateException("You must specifiy a valid layout "
                                + "reference. The layout ID " + value + " is not valid.");
                    }
                } else {
                    final XmlResourceParser childParser =
                            getContext().getResources().getLayout(layout);
    
                    try {
                        final AttributeSet childAttrs = Xml.asAttributeSet(childParser);
    
                        while ((type = childParser.next()) != XmlPullParser.START_TAG &&
                                type != XmlPullParser.END_DOCUMENT) {
                            // Empty.
                        }
    
                        if (type != XmlPullParser.START_TAG) {
                            throw new InflateException(childParser.getPositionDescription() +
                                    ": No start tag found!");
                        }
    
                        final String childName = childParser.getName();
    
                        if (TAG_MERGE.equals(childName)) { // 处理include xml里包含merge的情况
                            // Inflate all children.
                            rInflate(childParser, parent, childAttrs, false);
                        } else { // 处理一般的include layout文件,创建此xml文件的root view
                            final View view = createViewFromTag(parent, childName, childAttrs);
                            final ViewGroup group = (ViewGroup) parent;
    
                            // We try to load the layout params set in the <include /> tag. If
                            // they don't exist, we will rely on the layout params set in the
                            // included XML file.
                            // During a layoutparams generation, a runtime exception is thrown
                            // if either layout_width or layout_height is missing. We catch
                            // this exception and set localParams accordingly: true means we
                            // successfully loaded layout params from the <include /> tag,
                            // false means we need to rely on the included layout params.
                            ViewGroup.LayoutParams params = null;
                            try {
                                params = group.generateLayoutParams(attrs);
                            } catch (RuntimeException e) {
                                params = group.generateLayoutParams(childAttrs);
                            } finally {
                                if (params != null) {
                                    view.setLayoutParams(params); // 设置其layoutParams
                                }
                            }
    
                            // Inflate all children.
                            rInflate(childParser, view, childAttrs, true); // 递归inflate剩下的节点
    
                            // Attempt to override the included layout's android:id with the
                            // one set on the <include /> tag itself.
                            TypedArray a = mContext.obtainStyledAttributes(attrs,
                                com.android.internal.R.styleable.View, 0, 0);
                            int id = a.getResourceId(com.android.internal.R.styleable.View_id, View.NO_ID);
                            // While we're at it, let's try to override android:visibility.
                            int visibility = a.getInt(com.android.internal.R.styleable.View_visibility, -1);
                            a.recycle();
    
                            if (id != View.NO_ID) {
                                view.setId(id); // override id,如果include节点提供了
                            }
    
                            switch (visibility) { // 同样的,override visibility,如果include节点提供了
                                case 0:
                                    view.setVisibility(View.VISIBLE);
                                    break;
                                case 1:
                                    view.setVisibility(View.INVISIBLE);
                                    break;
                                case 2:
                                    view.setVisibility(View.GONE);
                                    break;
                            }
    
                            group.addView(view); // 将include的xml文件里的root view加到上层group中 
                        }
                    } finally {
                        childParser.close();
                    }
                }
            } else { // include节点必须是某个ViewGroup的子节点
                throw new InflateException("<include /> can only be used inside of a ViewGroup");
            }
    
            final int currentDepth = parser.getDepth();
            while (((type = parser.next()) != XmlPullParser.END_TAG || // skip掉include节点剩下的内容
                    parser.getDepth() > currentDepth) && type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }
        }

      最后我们看看根据节点创建对应View的相关方法:

    /*
         * default visibility so the BridgeInflater can override it.
         */
        View createViewFromTag(View parent, String name, AttributeSet attrs) {
            if (name.equals("view")) {
                name = attrs.getAttributeValue(null, "class");
            }
    
            if (DEBUG) System.out.println("******** Creating view: " + name);
    
            try {
                View view;
    // 这里我们忽略掉了各种factory的onCreateView,有兴趣的读者可自行研究
    if (mFactory2 != null) view = mFactory2.onCreateView(parent, name, mContext, attrs); else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs); else view = null; if (view == null && mPrivateFactory != null) { view = mPrivateFactory.onCreateView(parent, name, mContext, attrs); } if (view == null) { if (-1 == name.indexOf('.')) { // 创建android.view.*里的任何view,如TextView,ImageView等等 view = onCreateView(parent, name, attrs); // 其子类PhoneLayoutInflater override了此方法用来 } else { // 创建android.widget.*/android.webkit.*里的任何对象 view = createView(name, null, attrs); // 创建用户自定义的各种View(如com.xiaoweiz.browser.MyCustomView) } } if (DEBUG) System.out.println("Created view is: " + view); return view; } catch (InflateException e) { throw e; } catch (ClassNotFoundException e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name); ie.initCause(e); throw ie; } catch (Exception e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + name); ie.initCause(e); throw ie; } } /** * Low-level function for instantiating a view by name. This attempts to * instantiate a view class of the given <var>name</var> found in this * LayoutInflater's ClassLoader. * * <p> * There are two things that can happen in an error case: either the * exception describing the error will be thrown, or a null will be * returned. You must deal with both possibilities -- the former will happen * the first time createView() is called for a class of a particular name, * the latter every time there-after for that class name. * * @param name The full name of the class to be instantiated. * @param attrs The XML attributes supplied for this instance. * * @return View The newly instantiated view, or null. */ public final View createView(String name, String prefix, AttributeSet attrs) // 用户自定义的view不需要prefix,因为 throws ClassNotFoundException, InflateException { // name中已经有所有需要的信息了;系统的prefix则是android.view. Constructor<? extends View> constructor = sConstructorMap.get(name); // 或android.widget. 或 android.webkit. Class<? extends View> clazz = null; try { Trace.traceBegin(Trace.TRACE_TAG_VIEW, name); if (constructor == null) { // Class not found in the cache, see if it's real, and try to add it clazz = mContext.getClassLoader().loadClass( // 加载class文件 prefix != null ? (prefix + name) : name).asSubclass(View.class); if (mFilter != null && clazz != null) { boolean allowed = mFilter.onLoadClass(clazz); if (!allowed) { failNotAllowed(name, prefix, attrs); } } constructor = clazz.getConstructor(mConstructorSignature); // 拿到此类型的ctor sConstructorMap.put(name, constructor); } else { // If we have a filter, apply it to cached constructor if (mFilter != null) { // Have we seen this name before? Boolean allowedState = mFilterMap.get(name); if (allowedState == null) { // New class -- remember whether it is allowed 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) { failNotAllowed(name, prefix, attrs); } } else if (allowedState.equals(Boolean.FALSE)) { failNotAllowed(name, prefix, attrs); } } } Object[] args = mConstructorArgs; // 需要2个参数Context,AttributeSet的版本,所以如果你不打算动态inflate args[1] = attrs; // 你的view,则没必要提供此版本的ctor。 final View view = constructor.newInstance(args); // new一个View(可能是其子类)的对象,可能为null if (view instanceof ViewStub) { // ViewStub的特殊处理 // always use ourselves when inflating ViewStub later final ViewStub viewStub = (ViewStub) view; viewStub.setLayoutInflater(this); } return view; } catch (NoSuchMethodException e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + (prefix != null ? (prefix + name) : name)); ie.initCause(e); throw ie; } catch (ClassCastException e) { // If loaded class is not a View subclass InflateException ie = new InflateException(attrs.getPositionDescription() + ": Class is not a View " + (prefix != null ? (prefix + name) : name)); ie.initCause(e); throw ie; } catch (ClassNotFoundException e) { // If loadClass fails, we should propagate the exception. throw e; } catch (Exception e) { InflateException ie = new InflateException(attrs.getPositionDescription() + ": Error inflating class " + (clazz == null ? "<unknown>" : clazz.getName())); ie.initCause(e); throw ie; } finally { Trace.traceEnd(Trace.TRACE_TAG_VIEW); } }

      至此我们已经将LayoutInflater.inflate的关键代码分析完毕了。

  • 相关阅读:
    NET开发中的事务处理大比拼
    gridview 导出Excel
    项目 心得
    设计模式Prototype原型模式
    设计模式Memento 备忘录模式
    设计模式Chain Of Responsibility 职责链模式
    设计模式Composite组合模式
    .NET常用功能和代码[总结与收藏]
    SQL SERVER函数——表值函数的处理
    设计模式学习笔记 简单工厂模式
  • 原文地址:https://www.cnblogs.com/xiaoweiz/p/3788332.html
Copyright © 2020-2023  润新知