單例模式的幾種實現與在Android原始碼中的應用

sun_xin發表於2017-12-14

更多的關於設計模式與原始碼的內容都在我的github

單例模式

核心原理: 將建構函式私有化,並且通過靜態方法獲取一個唯一的例項,在這個過程中必須保證執行緒安全、防止反序列化導致重新生成例項物件等問題。

  • UML
    單例設計模式

單例模式實現的幾種方式

餓漢式

/**
 * 餓漢式
 */
public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton(){}

    public static Singleton getInstance() {
        return instance;
    }
}
複製程式碼

懶漢式

/**
 * 懶漢式
 */
public class Singleton {

    private static Singleton instance;

    private Singleton(){}

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

複製程式碼

Double Check Lock(DCL)實現單例模式

/**
 * Double Check Lock(DCL) 雙重鎖校驗
 * 優點:資源利用率高,但是由於Java記憶體模型的問題偶爾會出現DCL失效問題
 */
public class Singleton {

    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        //避免不必要的同步
        if (instance == null) {
            synchronized (Singleton.class) {
                //在null的情況下建立例項
                if (instance == null) {
                    /**
                     * 1.給Singleton的例項分配記憶體
                     * 2.呼叫Singleton()的建構函式
                     * 3.將instance物件指向分配的空間
                     *
                     * 因為JVM編譯器物件允許處理器亂序執行,所以這三步順序不一定
                     */
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}
複製程式碼

靜態內部類單例模式

/**
 * 靜態內部類單例模式(推薦使用)
 * 能夠保證執行緒安全,物件的唯一性,延遲了單例的例項化。
 */
public class Singleton {

    private Singleton(){}

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

    /**
     * 靜態內部類
     */
    private static class SingletonHolder{
        private static final Singleton instance = new Singleton();
    }

}
複製程式碼

列舉單例

/**
 * 列舉單例
 *  寫法簡單,預設列舉例項的建立是執行緒安全的。
 *  在上述的幾種單例模式的實現中,在反序列化的情況下會出現重新建立物件的情況。
 */
public enum SingletonEnum {
    INSTANCE;
    public void doSomething(){
        System.out.println("do sth");
    }
}

/**
 * 防止單例物件被反序列化,重寫反序列化的一個鉤子函式readResolve()
 */
public final class Singleton implements Serializable{

    public static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }

    /**
     * 反序列化操作中可以讓那個開發人員控制物件的函式
     * @return
     * @throws ObjectStreamException
     */
    private Object readResolve() throws ObjectStreamException{
        return INSTANCE;
    }

}

複製程式碼

容器實現單例模式

/**
 * 使用容器實現單例模式
 */
public class SingletonManager {
    private static Map<String,Object> objMap = new HashMap<String,Object>();

    private SingletonManager(){}

    public static void registerService(String key,Object instance){
        if (!objMap.containsKey(key)){
            objMap.put(key,instance);
        }
    }

    public static Object getInstance(String key){
        return objMap.get(key);
    }

}
複製程式碼

單例模式在Android原始碼中的應用**(LayoutInflater)**

  1. 通過LayoutInflater.from(context)來獲取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;
}
複製程式碼
  1. 來看看context.getSystemService是怎麼工作的,context的實現類是ContextImpl類,點進去看一下
@Override
public Object getSystemService(String name) {
    return SystemServiceRegistry.getSystemService(this, name);
}
複製程式碼
  1. 進入到SystemServiceRegistry類中
/**
 * Gets a system service from a given context.
 */
public static Object getSystemService(ContextImpl ctx, String name) {
    ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
    return fetcher != null ? fetcher.getService(ctx) : null;
}
複製程式碼

看到這裡感覺好像是我們上面用到的第五種單例模式,使用容器實現。看一下果然是

private static final HashMap<String, ServiceFetcher<?>> SYSTEM_SERVICE_FETCHERS =
        new HashMap<String, ServiceFetcher<?>>();
複製程式碼

使用map通過鍵值對的方式儲存系統服務。在呼叫registerService的時候注入。

/**
 * 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);
}
複製程式碼
  1. 我們可以再看看這些系統服務都是在什麼時候註冊的
static {
  
registerService(Context.LAYOUT_INFLATER_SERVICE, LayoutInflater.class,
        new CachedServiceFetcher<LayoutInflater>() {
    @Override
    public LayoutInflater createService(ContextImpl ctx) {
        return new PhoneLayoutInflater(ctx.getOuterContext());
    }});
}
複製程式碼

是在一個靜態的程式碼塊中進行註冊服務,第一次載入該類的時候執行,並且只執行一次,保證例項的唯一性。

從這個過程中可以看出,系統將服務以鍵值對的形式儲存在HashMap中,使用者使用時只需要獲取具體的服務物件,第一次獲取時,呼叫getSystemService來獲取具體的物件,在第一次呼叫時,會呼叫registerService通過map將物件快取在一個列表中,下次再用時直接從容器中取出來就可以。避免重複建立物件,從而達到單例的效果。減少了資源消耗。

  1. 接下來,我們繼續深入研究一下LayoutInflater的原始碼實現,我們知道LayoutInflater是一個抽象類,具體的實現肯定都在它的子類,在註冊服務的時候可以之後它的子類就是PhoneLayoutInflater.
/**
 * @hide
 */
public class PhoneLayoutInflater extends LayoutInflater {
   //內建View型別的字首,拼接出完整路徑 andorid.widget.TextView
    private static final String[] sClassPrefixList = {
        "android.widget.",
        "android.webkit.",
        "android.app."
    };

    /**
     * Instead of instantiating directly, you should retrieve an instance
     * through {@link Context#getSystemService}
     *
     * @param context The Context in which in which to find resources and other
     *                application-specific things.
     *
     * @see Context#getSystemService
     */
    public PhoneLayoutInflater(Context context) {
        super(context);
    }

    protected PhoneLayoutInflater(LayoutInflater original, Context newContext) {
        super(original, newContext);
    }

    /** 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 {
        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.
            }
        }
     //核心語句,根據我完整View的的路徑名來構造出View物件
        return super.onCreateView(name, attrs);
    }

    public LayoutInflater cloneInContext(Context newContext) {
        return new PhoneLayoutInflater(this, newContext);
    }
}
複製程式碼

從上述程式碼中我們可以看出 為什麼當我們自定義View的時候需要把全類名寫上。

  1. 看一下一個View的構建流程,以ActivitysetContentView為
/**
 * Set the activity content from a layout resource.  The resource will be
 * inflated, adding all top-level views to the activity.
 *
 * @param layoutResID Resource ID to be inflated.
 *
 * @see #setContentView(android.view.View)
 * @see #setContentView(android.view.View, android.view.ViewGroup.LayoutParams)
 */
public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);
    initWindowDecorActionBar();
}
複製程式碼

實際上是呼叫WindowsetContentView,window是一個抽象類,子類是PhoneWindow,具體來看下

@Override
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.
//如果為空,安裝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 {
    //通過佈局id和mContentParent渲染布局,解析xml檔案
        mLayoutInflater.inflate(layoutResID, mContentParent);
    }
    mContentParent.requestApplyInsets();
    final Callback cb = getCallback();
    if (cb != null && !isDestroyed()) {
        cb.onContentChanged();
    }
    mContentParentExplicitlySet = true;
}
複製程式碼

看一下infate方法,主要就是解析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];
        mConstructorArgs[0] = inflaterContext;
        View result = root;

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

            if (type != XmlPullParser.START_TAG) {
                throw new InflateException(parser.getPositionDescription()
                        + ": No start tag found!");
            }

            final String name = parser.getName();
            
            if (DEBUG) {
                System.out.println("**************************");
                System.out.println("Creating root view: "
                        + name);
                System.out.println("**************************");
            }

            if (TAG_MERGE.equals(name)) {
                if (root == null || !attachToRoot) {
                    throw new InflateException("<merge /> can be used only with a valid "
                            + "ViewGroup root and attachToRoot=true");
                }

                rInflate(parser, root, inflaterContext, attrs, false);
            } else {
                // Temp is the root view that was found in the xml
                final View temp = createViewFromTag(root, name, inflaterContext, attrs);

                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);
                    if (!attachToRoot) {
                        // 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 against its context.
                rInflateChildren(parser, temp, attrs, true);

                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.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;
    }
}
複製程式碼

rInflate通過深度優先遍歷來構造檢視樹,每解析一個View元素就會遞迴呼叫rInflte,會將每個View元素新增到parent中,最後最後,通過setContentView設定的內容就會出現在螢幕上。

void rInflate(XmlPullParser parser, View parent, Context context,
        AttributeSet attrs, boolean finishInflate) throws XmlPullParserException, IOException {

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

        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)) {
            if (parser.getDepth() == 0) {
                throw new InflateException("<include /> cannot be the root element");
            }
            parseInclude(parser, context, parent, attrs);
        } else if (TAG_MERGE.equals(name)) {
            throw new InflateException("<merge /> must be the root element");
        } else {
            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);
            viewGroup.addView(view, params);
        }
    }

    if (finishInflate) {
        parent.onFinishInflate();
    }
}
複製程式碼

小結:單例模式是運用最多的設計模式之一,在客戶端沒有高併發的情況下,選擇那種實現方式並沒有太大的影響,但出於效率考慮,推薦使用DCL和靜態內部類的實現方式。

相關文章