美文网首页Android开发Android技术知识Android开发
Android源码设计模式(二)-- 应用最广的模式:单例模式

Android源码设计模式(二)-- 应用最广的模式:单例模式

作者: 随时学丫 | 来源:发表于2017-11-03 09:46 被阅读65次

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

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

一、单例模式的定义

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

二、单例模式的使用场景

确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如创建一个对象需要消耗的资源过多,如要访问 IO 和数据库等资源。

三、单例模式的 UML 类图

单例模式 UML.png

角色介绍

  • Client : 高层客户端。
  • Singleton : 单例类。

单例模式主要有以下几个关键点:

  1. 构造函数不对外开发,一般为 private。
  2. 通过一个静态方法或者枚举返回单例类对象。
  3. 确保单例对象有且只有一个,尤其是在多线程的情况下。
  4. 确保单例类在反序列化时不会重新构建对象。

四、七种单例模式

4.1 懒汉,线程不安全

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

这种写法是 lazy loading 很明显,致命的是多线程下不能正常工作。

4.2 懒汉,线程安全

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

这种写法在多线程中能很好的工作,看起来也具备很好的 lazy loading,但是效率很低,90% 情况下不需要同步。

4.3 饿汉

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

这种方式基于 classloader 机制避免了多线程同步问题,不过 instance 在类装载时就实例化,虽然导致类加载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法,但是也不能确定有其他方式(或者其他静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

4.4 饿汉,变种

public class Singleton {  
    private Singleton instance = null;  
    static {  
        instance = new Singleton();  
    }  
    private Singleton (){}  
    public static Singleton getInstance() {  
        return this.instance;  
    }  
} 

表面上看起来差别很大,其实和第三种饿汉方式差不多,都在在类初始化时实例化 instance。

4.5 静态内部类

public class Singleton {  
    private static class SingletonHolder {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
        return SingletonHolder.INSTANCE;  
    }  
} 

这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第三种和第四种方式不同非是:第三种和第四种是只要Singleton 类被加载,那么 instance 就被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被加载,instance 不一定被初始化,因为 SingletonHolder 类没有被主动使用,只有显示通过 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比第三和第四种方式就显得很合理。

4.6 枚举

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

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

4.7 双重校验锁

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
          if (singleton == null) {  
              singleton = new Singleton();  
          }  
        }  
    }  
    return singleton;  
    }  
}

第二种方式的升级版,俗称 双重检查锁定。在 JDK1.5 之后,双重检查锁定才能够正常达到单例效果。

双重检查加锁

  • 既可以实现线程安全,又能使性能不受很大影响

优点:资源利用率高,第一次执行 getInstance 时单例对象才会被实例化,效率高。

缺点:第一次加载时反应稍慢,由于 Java 内存模型的原因偶尔失败。

​ 在高并发环境下也有一定缺陷,虽然发生概率较小。

什么是双重检查加锁?

并不是每次进入 getInstance 方法都需要同步,而是先不同步,进入方法过后,先检查实例是否存在,如果不存在才进入下面的同步代码块,这是第一重检查。进入同步代码块后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次,从而减少了多次在同步情况下进行判断所浪费的时间。

  • 双重检查加锁机制会在实现的时候使用一个关键字 volatile,意思是:被 volatile 修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确处理该变量。
  • 由于 volatile 关键字可能会屏蔽掉虚拟机一些必要的代码优化,所以运行效率并不是很高,一般建议,没有特别需要,不要使用。也就是说,虽然可以使用 “双重检查加锁” 机制来实现线程安全的单例,但是并不建议大量采用,可以根据情况来选用。

4.8 使用容器实现单例模式

注册到容器, 根据 key 获取对象.一般都会有多种相同属性类型的对象会注册到一个 map 中

public class Singleton {  
    private static Map<String, Object> objMap = new HashMap<String, Object>();
    private Singleton (){}  
    public static void registerService(String key, Singleton instance) {
        if (!objMap.containsKey(key) ) {
            objMap.put(key, instance) ;
        }
    }
    public static Object getService(String key) {
        return objMap.get(key);
    } 
}

在程序的初始,将多种单例类型注入到一个统一的管理类中,在使用时根据 key 获取对应类型的对象。这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一接口进行获取操作,降低了用户的使用成本,也对用户隐藏了具体实现,降低了耦合度。

总结

  1. 如果单例由不同的类装载器装入,那便可能存在多个单例类的实例。假定不是远端存取,假如一些 servlet 容器对每个 servlet 使用完全不同的类装载,这样的话如果有两个 servlet 访问一个单例类,他们都会有各自的实例。
  2. 如果 Singleton 实现了 java.io.Serializable 接口,那么这个类的实例就可能被序列化和复原。不管怎么样,如果你序列化一个单例类的对象,接下来复原多个那个对象,那就会有多个单例类的实例。

对第一个问题的修复

private static Class getClass(String classname) throws ClassNotFoundException {     
      ClassLoader classLoader = Thread.currentThread().getContextClassLoader();     
      if(classLoader == null)     
         classLoader = Singleton.class.getClassLoader();     
      return (classLoader.loadClass(classname));     
   }     
}  

对第二个问题的修复

public class Singleton implements java.io.Serializable {     
   public static Singleton INSTANCE = new Singleton();     
   protected Singleton() {     
   }     
   private Object readResolve() {     
      return INSTANCE;     
   }    
}

一般来说,我会使用第三种(饿汉)和第五种(静态内部类),而且在 JVM 层实现了线程安全(如果不是多个类加载器环境),一般情况下,我会使用第三种方式,只有在明确实现 lazy loading 效果时才会使用第五种方式,另外,如果涉及到反序列化创建对象时,我会试着用枚举的方式实现单例,不过,我会一直保证我的程序的线程安全的,而且我永远不会使用第一种和第二种方式。如果没有其他特殊需求,我会使用第七种方式,毕竟,JDK 1.5 已经没有双重检查锁定的问题了。

一般来说,第一种不算单例,第四种和第三种就是一种。所以说,一般单例都是 5 种写法。

懒汉,饿汉,双重校验锁,枚举和静态内部类。

应用场景

  1. Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
  2. windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
  3. 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  4. 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  5. Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
  6. 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
  7. 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
  8. 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
  9. HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例。

五、Android 源码中的单例模式

5.1 Context 获取系统级别的服务

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

@Override
public View getView(int position, View convertView, ViewGroup parent)   
    View itemView = null;
    if (convertView == null) {
        itemView = LayoutInflater.from(mContext).inflate(mLayoutId, null);
        // 其他代码
    } else {
        itemView = convertView;
    }
    // 获取Holder
    // 初始化每项的数据
    return itemView;
}

通常我们使用 LayoutInflater.from(Context) 来获取LayoutInflater服务, 下面我们看 LayoutInflater.from(Context) 的实现。

    /**
     * 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;
    }

可以看到 from(Context) 函数内部调用的是 Context 类的 getSystemService(String key) 方法,我们跟踪到Context 类看到, 该类是抽象类。

public abstract class Context {
    // 省略
}

使用的 getView 中使用的 Context 对象的具体实现类是什么呢 ?其实在 Application,Activity, Service 中都会存在一个 Context 对象,即 Context (数量) = Activity 数 + Service 数 + 1。而 ListView 通常都是显示在 Activity 中,那么我们就以 Activity 中的 Context 来分析。

我们知道,一个 Activity 的入口是 ActivityThread 的 main 函数。在该 main 函数中创建一个新的 ActivityThread 对象,并且启动消息循环 ( UI 线程),创建新的 Activity、新的 Context 对象,然后将该 Context 对象传递给 Activity。下面我们看看 ActivityThread 源码。

 public static void main(String[] args) {
        SamplingProfilerIntegration.start();

        // CloseGuard defaults to true and can be quite spammy.  We
        // disable it here, but selectively enable it later (via
        // StrictMode) on debug builds, but using DropBox, not logs.
        CloseGuard.setEnabled(false);

        Environment.initForCurrentUser();

        // Set the reporter for event logging in libcore
        EventLogger.setReporter(new EventLoggingReporter());
        Process.setArgV0("<pre-initialized>");
        // 主线程消息循环
        Looper.prepareMainLooper();
        // 创建ActivityThread对象
        ActivityThread thread = new ActivityThread();
        thread.attach(false);

        if (sMainThreadHandler == null) {
            sMainThreadHandler = thread.getHandler();
        }

        AsyncTask.init();

        if (false) {
            Looper.myLooper().setMessageLogging(new
                    LogPrinter(Log.DEBUG, "ActivityThread"));
        }

        Looper.loop();

        throw new RuntimeException("Main thread loop unexpectedly exited");
    }

    private void attach(boolean system) {
        sThreadLocal.set(this);
        mSystemThread = system;
        if (!system) {
            ViewRootImpl.addFirstDrawHandler(new Runnable() {
                public void run() {
                    ensureJitEnabled();
                }
            });
            android.ddm.DdmHandleAppName.setAppName("<pre-initialized>",
                                                    UserHandle.myUserId());
            RuntimeInit.setApplicationObject(mAppThread.asBinder());
            IActivityManager mgr = ActivityManagerNative.getDefault();
            try {
                mgr.attachApplication(mAppThread);
            } catch (RemoteException ex) {
                // Ignore
            }
        } else {
               // 省略
        }
}

在 main 方法中,我们创建一个 ActivityThread 对象后,调用了其 attach 函数,并且参数为 false。在 attach 函数中, 参数为 false 的情况下, 会通过 Binder 机制与 ActivityManagerService 通信,并且最终调用handleLaunchActivity 函数 ( 具体分析请参考老罗的博客 : Activity的启动流程),我们看看该函数的实现 。

    private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        // 代码省略
        Activity a = performLaunchActivity(r, customIntent);
        // 代码省略
    }
    
     private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        // 代码省略
        Activity activity = null;
        try {
            java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
            activity = mInstrumentation.newActivity(         // 1 : 创建Activity
                    cl, component.getClassName(), r.intent);
         // 代码省略
        } catch (Exception e) {
         // 省略
        }
        try {
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);

            if (activity != null) {
                // 2 : 获取Context对象
                Context appContext = createBaseContextForActivity(r, activity); 
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);
                // 3: 将appContext等对象attach到activity中
                activity.attach(appContext, this, getInstrumentation(), r.token,
                        r.ident, app, r.intent, r.activityInfo, title, r.parent,
                        r.embeddedID, r.lastNonConfigurationInstances, config);

                // 代码省略
                // 4 : 调用Activity的onCreate方法
                mInstrumentation.callActivityOnCreate(activity, r.state);
                // 代码省略
        } catch (SuperNotCalledException e) {
            throw e;
        } catch (Exception e) {
            // 代码省略
        }
        return activity;
    }


    private Context createBaseContextForActivity(ActivityClientRecord r,
            final Activity activity) {
        // 5 : 创建Context对象, 可以看到实现类是ContextImpl
        ContextImpl appContext = new ContextImpl();           
        appContext.init(r.packageInfo, r.token, this);
        appContext.setOuterContext(activity);
        // 代码省略
        return baseContext;
    }
    

通过上面 1~5 的代码分析可以知道, Context 的实现类为 ComtextImpl 类。我们继续跟踪到 ContextImpl 类。

class ContextImpl extends Context {
    // 代码省略
    /**
     * Override this class when the system service constructor needs a
     * ContextImpl.  Else, use StaticServiceFetcher below.
     */
     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) {
                    for (int i = 0; i < sNextPerContextServiceCacheIndex; i++) {
                        cache.add(null);
                    }
                } else {
                    service = cache.get(mContextCacheIndex);
                    if (service != null) {
                        return service;
                    }
                }
                service = createService(ctx);
                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");
        }
    }

    // 1 : service容器
    private static final HashMap<String, ServiceFetcher> SYSTEM_SERVICE_MAP =
            new HashMap<String, ServiceFetcher>();

    private static int sNextPerContextServiceCacheIndex = 0;
    // 2: 注册服务器
    private static void registerService(String serviceName, ServiceFetcher fetcher) {
        if (!(fetcher instanceof StaticServiceFetcher)) {
            fetcher.mContextCacheIndex = sNextPerContextServiceCacheIndex++;
        }
        SYSTEM_SERVICE_MAP.put(serviceName, fetcher);
    }

    // 3: 静态语句块, 第一次加载该类时执行 ( 只执行一次, 保证实例的唯一性. )
    static {
        //  代码省略
        // 注册Activity Servicer
        registerService(ACTIVITY_SERVICE, new ServiceFetcher() {
                public Object createService(ContextImpl ctx) {
                    return new ActivityManager(ctx.getOuterContext(), ctx.mMainThread.getHandler());
                }});

        // 注册LayoutInflater service
        registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {
                public Object createService(ContextImpl ctx) {
                    return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());
                }});
        // 代码省略
    }

    // 4: 根据key获取对应的服务, 
    @Override
    public Object getSystemService(String name) {
        // 根据name来获取服务
        ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
        return fetcher == null ? null : fetcher.getService(this);
    }

    // 代码省略
}

从 ContextImpl 类的部分代码中可以看到,在虚拟机第一次加载该类时会注册各种服务,其中就包含了LayoutInflater Service, 将这些服务以键值对的形式存储在一个 HashMap 中,用户使用时只需要根据 key 来获取到对应的服务,从而达到单例的效果。这种模式就是上文中提到的“单例模式的实现方式 8”。系统核心服务以单例形式存在,减少了资源消耗。

5.2 无名英雄 -- 深入理解 LayoutInflater

LayoutInflater 在开发中扮演着重要的角色,LayoutInflater 是一个抽象类,具体代码如下:

public abstract class LayoutInflater {
}

在加载 ContextImpl 时会通过如下代码将 LayoutInflater 的 ServiceFetcher 注入到容器中。

// 注册LayoutInflater service
        registerService(LAYOUT_INFLATER_SERVICE, new ServiceFetcher() {
                public Object createService(ContextImpl ctx) {
                    return PolicyManager.makeNewLayoutInflater(ctx.getOuterContext());
                }});

这里调用了 PolicyManager.makeNewLayoutInflater(ctx.getOuterContext()) 方法,继续往下看:

public final class PolicyManager {
  // Policy实现类
  private static final String POLICY_IMPL_CLASS_NAME =
    "com.android.internal.policy.impl.Policy";
  private static final IPolicy sPolicy;
  static {
    // 通过反射构造Policy对象
    try {
      Class policyClass = Class.forName(POLICY_IMPL_CLASS_NAME);
      sPolicy = (IPolicy)policyClass.newInstance();
    }
    // catch 代码段
  }
  private PolicyManager() {}
  // 这里就是创建PhoneWindow对象的地方
  public static Window makeNewWindow(Context context) {
    return sPolicy.makeNewWindow(context);
  }
  // 通过sPolicy创建LayoutInflater
  public static LayoutInflater makeNewLayoutInflater(Context context) {
    return sPolicy.makeNewLayoutInflater(context);
  }
}

PolicyManager 中通过反射构造了 Policy 实现类,这个类实现了 IPolicy 接口,通过这种形式将具体的 Policy 类对外进行隐藏实现。PolicyManager 实际上是一个代理类,具体的功能通过 sPolicy 对象进行实现,我们看看sPolicy 对应的 Policy 类,也就是 com.android.internal.policy.impl.Policy:

public class Policy implements IPolicy {
  // 代码省略
  // 创建PhoneWindow, 这就是Activity中Window的具体实现类
  public Window makeNewWindow(Context context) {
    return new PhoneWindow(context);
  }
  // 创建LayoutInflater,具体类为PhoneLayoutInflater,这才是我们要关注的地方
  public LayoutInflater makeNewLayoutInflater(Context context) {
    return new PhoneLayoutInflater(context);
  }
}

此时,己经很清楚了,真正 LayoutInflater 的实现类就是 PhoneLayoutInflater。我们继续深入看看PhoneLayoutInflater 的源代码:

public class PhoneLayoutInflater extends LayoutInflater {
  // 内置View类型的前缀, 如TextView的完整路径是android.widget.TextView
  private static final String[] sClassPrefixList = {
    "android.widget.",
    "android.webkit."
  };
  // 代码省略
  @Override 
    protected View onCreateView(String name,AttributeSet attrs)throws ClassNot FoundException {
    // 在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) {
        // 省略
      }
    }
    return super.onCreateView(name, attrs);
  }
}

代码不多,核心的程序语句就是覆写了 LayoutInflater 的 onCreateView 方法,该方法就是在传递进来的 View 名字前面加上 “android.widget.” 或者 “android.webkit.” 前缀用以得到该内置 View 类(如 TextView、Button 等都在android.widget包下)的完整路径。最后,根据类的完整路径来构造对应的 View 对象。
具体是一个怎样的流程呢?以 Activity 的 setContentView 为例,先来看看这个函数的实现:

public void setContentView(View view) {
  getWindow().setContentView(view);
  initActionBar();
}

Activity 的 setContentView 方法实际上调用的是 Window 的 setContentView,而 Window 是一个抽象类,上文提到 Window 的具体实现类是 PhoneWindow,我们看看 PhoneWindow 中对应的方法:

@Override
  public void setContentView(int layoutResID) {
    // 1.当mContentParent为空时先构建DecorView
    // 并且将DecorView包裹到mContentParent中
    if (mContentParent == null) {
      installDecor();
    } else {
      mContentParent.removeAllViews();
    }
    // 2.解析layoutResID
    mLayoutInflater.inflate(layoutResID, mContentParent);
    // 代码省略
  }

在分析之前,我们来看看一个 Window 的 View 层级图。


View 层级图.png

我们看到 mDecor 中会加载一个系统定义好的布局,这个布局中又包裹了 mContentParent,而这个 mContentParent 就是我们设置的布局,并将添加到 parent 区域。在 PhoneWindow 的 setContentView 方法中也验证了这点,首先会构建 mContentParent 这个对象,然后通过 LayoutInflater 的 inflate 函数将指定布局的视图添加到 mContentParent 中。那么就先来看看 inflate 方法:

public View inflate(int resource, ViewGroup root) {
          // root不为空, 则会从resource布局解析到View, 并添加到root中
          return inflate(resource, root, root != null);
    }
    public View inflate(int resource, ViewGroup root, boolean attachToRoot) {
          // 获取xml资源解析器
        XmlResourceParser parser = getContext().getResources().getLayout(resource);
        try {
              return inflate(parser, root, attachToRoot);
        } finally {
              parser.close();
          }
        }
    // 参数1为xml解析器, 参数2为要解析布局的父视图, 参数3为是否将要解析的视图添加到父视图中
    public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
          synchronized (mConstructorArgs) {
              final AttributeSet attrs = Xml.asAttributeSet(parser);
              Context lastContext = (Context)mConstructorArgs[0];
              // Context对象
              mConstructorArgs[0] = mContext;
              // 存储父视图
              View result = root;
              try {
                  // Look for the root node.
                  int type;
                  // 找到root元素
                  while ((type = parser.next()) != XmlPullParser.START_TAG &&
                        type != XmlPullParser.END_DOCUMENT) {
                      // Empty
                  }
                  // 代码省略
                  final String name = parser.getName();
                  // 1.解析merge标签
                  if (TAG_MERGE.equals(name)) {
                      rInflate(parser, root, attrs, false);
                  } else {
                      // 2.不是merge标签那么直接解析布局中的视图
                      View temp;
                      if (TAG_1995.equals(name)) {
                        temp = new BlinkLayout(mContext, attrs);
                      } else {
                        // 3.这里就是通过xml的tag来解析layout根视图
                        // name就是要解析的视图的类名, 如RelativeLayout
                        temp = createViewFromTag(root, name, attrs);
                      }
                      ViewGroup.LayoutParams params = null;
                      if (root != null) {
                        // 生成布局参数
                        params = root.generateLayoutParams(attrs);
                        // 如果attachToRoot为false,那么将给temp设置布局参数
                        if (!attachToRoot) {
                            temp.setLayoutParams(params);
                        }
                    }
                      // 解析temp视图下的所有子View
                    rInflate(parser, temp, attrs, true);
                    // 如果Root不为空, 且attachToRoot为true, 那么将temp添加到父视图中
                    if (root != null && attachToRoot) {
                        root.addView(temp, params);
                    }
                    // 如果root为空或者attachToRoot为false,那么返回的结果就是temp
                    if (root == null || !attachToRoot) {
                        result = temp;
                    }
                  }
              }
              // 省略catch, finaly代码
              return result;
          }

上述的 inflate 方法中,主要有下面几步:
(1)解析 xml 中的根标签(第一个元素);
(2)如果根标签是 merge,那么调用 rInflate 进行解析,rInflate 会将 merge 标签下的所有子 View 直接添加到根标签中;
(3)如果标签是普通元素,那么运行到代码 3,调用 createViewFromTag 对该元素进行解析;
(4)调用 rInflate 解析 temp 根元素下的所有子 View,并且将这些子 View 都添加到 temp 下;
(5)返回解析到的根视图。

我们先从简单的地方理解,即解析单个元素的 createViewFromTag,看看如下代码:

View createViewFromTag(View parent, String name, AttributeSet attrs) {
  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, mContext, attrs);
    else if (mFactory != null) view = mFactory.onCreateView(name, mContext, attrs);
    else view = null;
    // 代码省略
    // 2.没有Factory的情况下通过onCreateView或者createView创建View
    if (view == null) {
      // 3.内置View控件的解析
      if (-1 == name.indexOf('.')) {
        view = onCreateView(parent, name, attrs);
      } else {
        // 4.自定义控件的解析
        view = createView(name, null, attrs);
      }
    }
    return view;
  }
  // 省略catch块
}

本程序重点就在代码 2,以及以后的代码,createViewFromTag 会将该元素的 parent 及名字传递过来。当这个tag 的名字中没有包含“.”(在名字中查找“.”返回-1)时,LayoutInflater 会认为这是一个内置的 View,例如,我们在 xml 中声明一个内置 View 时大概是这样的:

<TextView
          android:id="@+id/my_textview "
          android:layout_width="60dp"
          android:layout_height="60dp" /> 

这里的 TextView 就是 xml 元素的名字,因此,在执行 infate 时就会调用代码 3 处的 onCreateView 来解析这个TextView 标签。当我们自定义 View 时,在 xml 中必须写 View 的完整路径,例如:

<com.dp.custom.MyView
         android:id="@+id/my_custom_view "
         android:layout_width="fill_parent"
         android:layout_height="fill_parent" />             

此时,就会调用代码注释 4 的 createView 来解析该 View。为什么要这么处理,它们之间又有什么不同呢?
在上文的 PhoneLayoutInflater 中我们知道,PhoneLayoutInflater 覆写了 onCreateView 方法,也就是代码 3 处的 onCreateView,该方法就是在 View 标签名的前面设置一个 “android.widget.” 前缀,然后再传递给 createView 进行解析。也就是说内置 View 和自定义 View 最终都调用了 createView 进行解析,只是 Google 为了让开发者在 xml 中更方便定义 View,只写 View 名称而不需要写完整的路径。在 LayoutInflater 解析时如果遇到只写类名的 View,那么认为是内置的 View 控件,在 onCreateView 方法中会将 “android.widget.” 前缀传递给 createView 方法,最后,在 createView 中构造 View 的完整路径来进行解析。如果是自定义控件,那么必须写完整的路径,此时调用 createView 且前缀为 null 进行解析。
关于 createView 的解释己经有很多,我们还是看下面的代码吧:

// 根据完整路径的类名通过反射机制构造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 {
      // 2.没有缓存构造函数
      if (constructor == null) {
        // 如果prefix不为空, 那么构造完整的View路径, 并且加载该类
        clazz = mContext.getClassLoader().loadClass(
          prefix != null ? (prefix + name) : name).asSubclass(View.class);
        // 代码省略
        // 3.从Class对象中获取构造函数
        constructor = clazz.getConstructor(mConstructorSignature);
        // 4.将构造函数存入缓存中
        sConstructorMap.put(name, constructor);
      } else {
        // 代码省
                // 代码省略
      }
      Object[] args = mConstructorArgs;
      args[1] = attrs;
      // 5.通过反射构造View
      final View view = constructor.newInstance(args);
      if (view instanceof ViewStub) {
        // always use ourselves when inflating ViewStub later
        final ViewStub viewStub = (ViewStub) view;
        viewStub.setLayoutInflater(this);
      }
      return view;
    }
    // 省略各种catch、finaly代码
  }

createView 相对比较简单,如果有前缀,那么构造 View 的完整路径,并且将该类加载到虚拟机中,然后获取该类的构造函数并且缓存起来,再通过构造函数来创建该 View 的对象,最后将 View 对象返回,这就是解析单个View 的过程。而我们的窗口中是一棵视图树,LayoutInflater 需要解析完这棵树,这个功能就交给了 rInflate 方法,具体代码如下:

void rInflate(XmlPullParser parser, View parent, final 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_INCLUDE.equals(name)) {  // 解析include标签
      parseInclude(parser, parent, attrs);
    } else if (TAG_MERGE.equals(name)) { // 解析merge标签, 抛出异常, 因为merge标签
      //必须为根视图
      throw new InflateException("<merge /> must be the root element");
    } else if (TAG_1995.equals(name)) {// 闪烁视图,这里可以不用管
      // 代码省略
    } else {
      // 3.根据元素名进行解析
      final View view = createViewFromTag(parent, name, attrs);
      final ViewGroup viewGroup = (ViewGroup) parent;
      final ViewGroup.LayoutParams params = viewGroup.generateLayoutParams (attrs);
      // 递归调用进行解析, 也就是深度优先遍历
      rInflate(parser, view, attrs, true);
      // 将解析到的View添加到viewGroup中, 也就是它的parent
      viewGroup.addView(view, params);
    }
  }
  if (finishInflate) parent.onFinishInflate();
}

rInflate 通过深度优先遍历来构造视图树,每解析到一个 View 元素就会递归调用 rInflate,直到这条路径下的最后一个元素,然后再回溯过来将每个 View 元素添加到它们的 parent 中。通过 rInflate 的解析之后,整棵视图树就构建完毕。当调用了 Activity 的 onResume 之后,我们通过 setContentView 设置的内容就会出现在我们的视野中。

六、总结

优点与缺点

优点
  • 由于单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁地创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就非常明显。
  • 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后用永久驻留内存的方式来解决;
  • 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。
  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
缺点
  • 单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。

参考:

http://www.blogjava.net/kenzhh/archive/2013/03/15/357824.html

《Android源码设计模式解析与实战》

相关文章

网友评论

    本文标题:Android源码设计模式(二)-- 应用最广的模式:单例模式

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