Android中的单例模式-深入理解LayoutInflater

单例模式介绍

许多时候整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为。

使用场景:

  1. 如在一个应用中,应该只有一个ImageLoader实例,这个ImageLoader中又含有线程池、缓存系统、网络请求等,很消耗资源,因此,没有理由让它构造多个实例。这就是单例模式的使用场景。
  2. 某种类型的对象应该有且只有一个。如果制造出多个这样的实例,可能导致:程序行为异常、资源使用过量、结果不一致等问题。

定义: 确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。

单例模式的优点:可以减少系统内存开支,减少系统性能开销,避免对资源的多重占用、同时操作。

单例模式的缺点:扩展很困难,容易引发内存泄露,测试困难,一定程度上违背了单一职责原则,进程被杀时可能有状态不一致问题。

简单示例

public class Singleton {
    private static final Singleton instance = new Singleton();
    private Singleton(){}

    public static Singleton getInstance() {
        return instance;
    }
}

Singleton类在外部是不能通过new的形式构造对象的,只能通过Singleton.getInstance()方法来获取。
而这个instance对象是静态对象,并且在声明的时候就已经初始化了,这就保证了Singleton对象的唯一性。
这个实现的核心在于将CEO类的构造方法私有化,使得外部程序不能通过构造函数来构造Singleton对象,而Singleton类通过一个静态方法返回一个静态对象。
这只是单例模式中的一种写法,叫做饿汉式。它一开始就初始化了单例,同时它还不是线程安全的。

懒汉式单例

public class Singleton {
    private static Singleton instance;
    private Singleton(){}

    public static synchronized Singleton getInstance() {
        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

在getInstance()方法中添加了synchronized关键字,也就是getInstance()是一个同步的方法,保证了在多线程情况下单例对象的唯一性。
但是在instance已经被初始化之后,每次调用getInstance方法都会进行同步,这样就会消耗不必要的资源。这也是懒汉式单例存在的问题。

Double CheckLock实现单例

Double CheckLock实现单例模式的优点是既能够在需要时才初始化单例,又能够保证线程的安全。

public class Singleton {
    private volatile static Singleton sInstance = null;
    private Singleton() {}

    public static Singleton getInstance() {
        if(sInstance == null) {
            synchronized(Singleton.class) {
                if(sInstance == null) {
                    sInstance = new Singleton();
                }
            }
        }
        return sInstance;
    }
}

这里对sInstance进行了两次判空。第一次主要是为了避免不必要的同步,第二次是为了在null的情况下创建实例。
优点:资源利用率高,第一次执行getInstance时单例对象才会被实例化。
缺点:第一次加载时反应稍慢,在高并发环境下也有可能失败;同时需要在JDK1.5以上版本使用。

静态内部类单例

public class Singleton {
    private Singleton() {}
    public static Singleton getInstance() {
        return SingletonHolder.sInstance;
    }
    // 静态内部类
    private static class SingletonHolder {
        private static final Singleton sInstance = new Singleton();
    }
}

这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。

枚举

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}

这种方式是《Effective Java》提倡的方式,它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,不过,个人认为由于1.5中才加入enum特性,用这种方式写不免让人感觉生疏,在实际工作中,我也很少看见有人这么写过。

Android中的单例模式

在Android系统中,我们经常会通过Context获取系统级别的服务,如WindowManagerService、ActivityManagerService等,我们比较常用的是一个LayoutInflater类,这些服务会在合适的时候以单例的形式注册在系统中,在我们需要的时候就通过Context的getSystemService(String name)方法来获取。
这里我们就以LayoutInflater为例来进行说明。通常我们使用LayoutInflater比较多的地方是ListView/RecyclerView中:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
    View view = LayoutInflater.from(mContext).inflate(R.layout.recycler_view, parent, false);
    return new RecyclerView.ViewHolder(view);
}

思考:
每次都通过这个方法来获取LayoutInflater对象,不会造成资源浪费吗? 这个系统级的服务是什么时候注册的呢? infalte方法是怎么构建出View对象呢?
我们这里就直接点击from进入源码一看究竟:

// #LayoutInflater.java
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;
}

可以看到from(Context)其实内部是调用的context.getSystemService(String key)方法。
所以我们就只需要查看Context中的具体实现方法就可以了,大家都知道Context类其实是一个抽象类,它的具体实现都依托于ContextImpl。
注: Context完全解析

// ContextImpl.java
@Override
public String getSystemServiceName(Class<?> serviceClass) {
    return SystemServiceRegistry.getSystemServiceName(serviceClass);
}

// SystemServiceRegistry.java
final class SystemServiceRegistry {
    // Service registry information.
    // This information is never changed once static initialization has completed.
    private static final HashMap<Class<?>, String> SYSTEM_SERVICE_NAMES = new HashMap<Class<?>, String>();
    // Not instantiable.
    private SystemServiceRegistry() { }
    static {
      registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
                    new CachedServiceFetcher<LayoutInflater>() {
                @Override
                public LayoutInflater createService(ContextImpl ctx) {
                    return new PhoneLayoutInflater(ctx.getOuterContext());
                }});
      // 省略其他注册代码          
      ...
    }

    /**
     * Statically registers a system service with the context.
     * This method must be called during static initialization only.
     */
    private static <T> void registerService(String serviceName, Class<T> serviceClass,
            ServiceFetcher<T> serviceFetcher) {
        SYSTEM_SERVICE_NAMES.put(serviceClass, serviceName);
        SYSTEM_SERVICE_FETCHERS.put(serviceName, serviceFetcher);
    }

    /**
    * Gets the name of the system-level service that is represented by the specified class.
    */
    public static String getSystemServiceName(Class<?> serviceClass) {
       return SYSTEM_SERVICE_NAMES.get(serviceClass);
    }
}

我们可以看到在SystemServiceRegistry类中,通过一个HashMap保存了系统级别的一些服务, 这些服务是在该类中通过static代码块来进行注册的。
这样就避免了每次获取的时候重复创建对象了。 这种单例模式也叫做:容器的单例模式

深入理解LayoutInflater

接下来我们挖掘一下LayoutInflater的具体作用。通过查看LayoutInflater的代码可以知道,它是一个抽象类,同时在上面的registerService中,我们也发现,实际上是new PhoneLayoutInflater,同时PhoneLayoutInflater是继承了LayoutInflater的。所以我们就先看看PhoneLayoutInflater

public class PhoneLayoutInflater extends LayoutInflater {
    private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit.",
        "android.app."
    };

    /** Override onCreateView to instantiate names that correspond to the
        widgets known to the Widget factory. If we don't find a match,
        call through to our super class.
    */
    @Override protected View onCreateView(String name, AttributeSet attrs) throws ClassNotFoundException {
        // 在View名字的前面添加前缀来构造View的完整路径
        // 例如类名是TextView,那么TextView完全的路径就是android.widget.TextView
        for (String prefix : sClassPrefixList) {
            try {
                View view = createView(name, prefix, attrs);
                if (view != null) {
                    return view;
                }
            } catch (ClassNotFoundException e) {
                // In this case we want to let the base class take a crack
                // at it.
            }
        }

        return super.onCreateView(name, attrs);
    }
}

其实PhoneLayoutInflater就是覆盖了LayoutInflater的onCreateView方法,该方法就是在传递进来的View名字前面加上"android.widget"。最后根据类的完整路径来构造对应的View对象。
具体是一个什么样的构造流程呢? 以Activyt的setContentView为例:

// Activity.java
public void setContentView(View view) {
    getWindow().setContentView(view);
    initWindowDecorActionBar();
}

Window是一个抽象类,它的具体实现类是PhoneWindow

// PhoneWindow.java
public void setContentView(int layoutResID) {
    // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
    // decor, when theme attributes and the like are crystalized. Do not check the feature
    // before this happens.
    // 1.当mContentParent为空的时候,先构建DecorView,并将DecorView包裹在mContentParent中
    if (mContentParent == null) {
        installDecor();
    } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        mContentParent.removeAllViews();
    }

    if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
        final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                getContext());
        transitionTo(newScene);
    } else {
        // 2.解析layoutResID
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
}
// LayoutInflater.java
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root) {
    // 如果root不为空,则会从resource布局解析到view,并添加到root中
    return inflate(resource, root, root != null);
}

// 参数一: 要解析的布局layoutResID
// 参数二: 要解析布局的父视图
// 参数三: 是否将要解析的视图添加到父视图中
public View inflate(@LayoutRes int resource, @Nullable ViewGroup root, boolean attachToRoot) {
    final Resources res = getContext().getResources();
    // 获取xml资源解析器
    final XmlResourceParser parser = res.getLayout(resource);
    try {
        return inflate(parser, root, attachToRoot);
    } finally {
        parser.close();
    }
}    

// 参数一: XML资源解析器
// 参数二: 要解析布局的父视图
// 参数三: 是否将要解析的视图添加到父视图中
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];
        // Context对象
        mConstructorArgs[0] = inflaterContext;
        // 父视图
        View result = root;

        try {
            // Look for the root node.
            int type;
            // 1. 找到root元素
            while ((type = parser.next()) != XmlPullParser.START_TAG &&
                    type != XmlPullParser.END_DOCUMENT) {
                // Empty
            }

            final String name = parser.getName();
            // 2. 如果是merge标签
            if (TAG_MERGE.equals(name)) {
                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                // 如果不是merge标签,就直接解析
                // 通过xml的tag来解析layout跟视图
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                ViewGroup.LayoutParams params = null;

                if (root != null) {
                    // Create layout params that match root, if supplied
                    // 生成布局参数
                    params = root.generateLayoutParams(attrs);
                    // 如果不添加到父视图,那么给temp设置布局参数
                    if (!attachToRoot) {
                        // Set the layout params for temp if we are not
                        // attaching. (If we are, we use addView, below)
                        temp.setLayoutParams(params);
                    }
                }
                // Inflate all children under temp against its context.
                // 解析temp下的所有子View
                rInflateChildren(parser, temp, attrs, true);


                // We are supposed to attach all the views we found (int temp)
                // to root. Do that now.
                // 如果Root不为空,且要添加到父视图中
                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;
                }
            }
        }
        return result;
    }
}

inflate过程:

  1. 解析xml中的根标签(第一个元素)
  2. 如果根标签是merge,那么调用rInflate进行解析,将merge标签下的所有子View直接添加到根标签下
  3. 如果根标签是普通元素,那么调用createViewFromTag对该元素进行解析, 注意布局参数
  4. 调用rInflate解析temp元素下的所有子View,并将这些子View都添加到temp下
  5. 返回解析到的根视图

先理解解析单个元素的createViewFromTag:

View createViewFromTag(View parent, String name, Context context, AttributeSet attrs,
            boolean ignoreThemeAttr) {
    if (name.equals("view")) {
        name = attrs.getAttributeValue(null, "class");
    }


    try {
        View view;
        // 1.用户可以通过设置LayoutInflater的factory来自行解析View,默认这些factory都为空
        if (mFactory2 != null) {
            view = mFactory2.onCreateView(parent, name, context, attrs);
        } else if (mFactory != null) {
            view = mFactory.onCreateView(name, context, attrs);
        } else {
            view = null;
        }

        if (view == null && mPrivateFactory != null) {
            view = mPrivateFactory.onCreateView(parent, name, context, attrs);
        }

        // 2. 没有factory的情况下,通过onCreateView或者createView方法来创建View
        if (view == null) {
            final Object lastContext = mConstructorArgs[0];
            mConstructorArgs[0] = context;
            try {
                // name 就是xml解析出来的标签
                if (-1 == name.indexOf('.')) {
                  // 3.内置View控件的解析, 比如<TextView ... />
                    view = onCreateView(parent, name, attrs);
                } else {
                  // 4.自定义View的解析  比如 <com.whyalwaysmea.myview ... />
                    view = createView(name, null, attrs);
                }
            } finally {
                mConstructorArgs[0] = lastContext;
            }
        }
        return view;
    }
}

这里重点关注步骤3和步骤4, 一个是解析标签中没有带'.'的控件,一个是解析带了'.'的控件。
我们回到之前的PhoneLayoutInflater中,看到onCreateView其实就是在View前面加上了前缀,然后调用了creatView方法。所以onCreateView方法只是为了开发者可以方便的写控件的标签名而已

// 根据完整路径的类名通过反射机制构造View对象
public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
    // 1. 获取缓存的构造函数         
    Constructor<? extends View> constructor = sConstructorMap.get(name);
    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
            // 2.如果prefix不为空,那么构造完整的View路径,并加载该类
            // prefix在onCreateView中会有,
            clazz = mContext.getClassLoader().loadClass(
                    prefix != null ? (prefix + name) : name).asSubclass(View.class);

            // 3.获取构造函数
            constructor = clazz.getConstructor(mConstructorSignature);
            constructor.setAccessible(true);
            // 4.缓存构造函数
            sConstructorMap.put(name, constructor);
        } else {
            // If we have a filter, apply it to cached constructor

        }

        Object[] args = mConstructorArgs;
        args[1] = attrs;
        // 5. 通过反射构造View
        final View view = constructor.newInstance(args);
        if (view instanceof ViewStub) {
            // Use the same context when inflating ViewStub later.
            final ViewStub viewStub = (ViewStub) view;
            viewStub.setLayoutInflater(cloneInContext((Context) args[0]));
        }
        return view;

    }
}

createView步骤:

  1. 如果有前缀,那么就构造出完整的路径
  2. 通过反射获取该类的构造函数并且缓存起来
  3. 通过构造函数来创建View并返回
    我们的窗口中是一棵视图树,LayoutInflater需要解析完这棵树,这个功能就交给了rInflate方法。
void rInflate(XmlPullParser parser, View parent, Context context,
            AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {
    // 1.获取树的深度,
    final int depth = parser.getDepth();
    int type;
    // 2.挨个元素解析
    while (((type = parser.next()) != XmlPullParser.END_TAG ||
            parser.getDepth() > depth) && type != XmlPullParser.END_DOCUMENT) {

        if (type != XmlPullParser.START_TAG) {
            continue;
        }

        final String name = parser.getName();

        if (TAG_REQUEST_FOCUS.equals(name)) {
            parseRequestFocus(parser, parent);
        } else if (TAG_TAG.equals(name)) {
            parseViewTag(parser, parent, attrs);
        } else if (TAG_INCLUDE.equals(name)) {
            // include标签,不能为根标签
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            // merge标签必须为根标签
            throw new InflateException("<merge /> must be the root element");
        } else {
            // 3.根据元素名进行解析
            final View view = createViewFromTag(parent, name, context, attrs);
            final ViewGroup viewGroup = (ViewGroup) parent;
            final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams(attrs);
            // 递归调用,深度优先遍历
            rInflateChildren(parser, view, attrs, true);
            // 将解析到的View添加到它的parent中
            viewGroup.addView(view, params);
        }
    }
}

这样整个LayoutInflater.infalte流程就走完了。

相关链接

Android LayoutInflater原理分析,带你一步步深入了解View(一)
Android LayoutInflater深度解析 给你带来全新的认识

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

推荐阅读更多精彩内容