Android View中Background載入的時間分析

陳哈哈的部落格發表於2015-03-03

對大多數Android的開發者來說,最經常的操作莫過於對介面進行佈局,View中背景圖片的載入是最經常做的。但是我們很少關注這個過程,這篇文章主要解析view中背景圖片載入的流程。瞭解view中背景圖片的載入(資源的載入)可以讓我們對資源載入的過程進行一些優化,另外當需要進行整個應用的換膚時,也可以更得心應手。

View圖片的載入,我們最常見的就是通過在XML檔案當中進行drawable的設定,然後讓Android系統幫我們完成,或者手動寫程式碼載入成Bitmap,然後載入到View上。這篇文章主要分析Android在什麼時候以及怎麼幫我們完成背景圖片的載入的,那麼我們就從Activity.setContentView還是LayoutInflater.inflate(…)方法開始分析。

不管是從Activity.setContentView(…)還是LayoutInflater.inflate(…)方法進行View的初始化,最終都會到達LayoutInflater.inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot)這個方法中。在這裡我們主要關注View的背景圖片載入,對於XML如何解析和載入就放過了。

public View inflate(XmlPullParser parser, ViewGroup root, boolean attachToRoot) {
        synchronized (mConstructorArgs) {
            final AttributeSet attrs = Xml.asAttributeSet(parser);
            Context lastContext = (Context)mConstructorArgs[0];
            mConstructorArgs[0] = mContext;
            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, attrs, false);
                } else {
                    // Temp is the root view that was found in the xml
                    View temp;
                    if (TAG_1995.equals(name)) {
                        temp = new BlinkLayout(mContext, attrs);
                    } else {
                        temp = createViewFromTag(root, name, 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
                    rInflate(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;
                    }
                }

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

            return result;
        }
    }

上面這麼長一串程式碼,其實思路很清晰,就是針對XML檔案進行解析,然後根據XML解析出的每一個節點進行View的初始化,緊接著將View的Layout引數設定到View上,然後將View新增到它的父控制元件上。
為了瞭解View是怎麼被載入出來的,我們只需要瞭解

temp = createViewFromTag(root, name, attrs);

跟進去看看。

/*
     * 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;
            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('.')) {
                    view = onCreateView(parent, name, attrs);
                } else {
                    view = createView(name, null, attrs);
                }
            }

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

上面程式碼的重點在於try…Catch裡的內容。try包起來的東西就是對View進行初始化,注意到上面程式碼中有幾個Factory,這些Factory可以在View進行初始化,也就是說其實我們可以在這裡干預View的初始化。從上面程式碼我們可以知道,如果我們自定義了一個Factory,那麼當前要初始化的View會優先被我們自定義的Factory初始化,而不通過系統預設的Factory初始化。那麼如果我們要自定義Factory,應該在哪裡定義呢?容易想到,Factory必須要趕在資源載入前自定義完成,所以我們應該在onCreate(…)的this.setContentView(…)之前設定LayoutInflater.Factory。

getLayoutInflater().setFactory(factory);

接下來我們看到上面函式裡面的

if (-1 == name.indexOf('.')) {
        view = onCreateView(parent, name, attrs);
    } else {
        view = createView(name, null, attrs);
    }

這段函式就是對View進行初始化,有兩種情況,一種是系統自帶的View,它在

if (-1 == name.indexOf('.'))

這裡面進行初始化,因為如果是系統自帶的View,傳入的那麼一般不帶系統的字首”android.view.”。另一個分支初始化的是我們自定義的View。我們跟進onCreateView看看。

protected View onCreateView(String name, AttributeSet attrs)
            throws ClassNotFoundException {
        return createView(name, "android.view.", attrs);
    }

    public final View createView(String name, String prefix, AttributeSet attrs)
            throws ClassNotFoundException, InflateException {
        Constructor<? extends View> constructor = sConstructorMap.get(name);
        Class<? extends View> clazz = null;

        try {
            if (constructor == null) {
                // Class not found in the cache, see if it's real, and try to add it
                clazz = mContext.getClassLoader().loadClass(
                        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);
                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;
            args[1] = attrs;

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

從onCreateView(…)中我們知道,其實createViewFromTag(…)中對View的初始化最終都是通過createView(…)這個函式進行初始化的,不同只在於系統控制元件需要通過onCreateView(…)加上字首,以便類載入器(ClassLoader)正確地通過類所在的包初始化這個類。createView(…)這個函式的思路很清晰,不看catch裡面的內容,try裡面開頭的兩個分支就是用來將所要用的類建構函式提取出來,Android系統會對使用過的類建構函式進行快取,因為像TextView這些常用的控制元件可能會被使用很多次。接下來,就是通過類建構函式對View進行初始化了。我們注意到傳入建構函式的mConstructorArgs是一個包含兩個元素的陣列。

final Object[] mConstructorArgs = new Object[2];

那麼我們就很清楚了,它就是呼叫系統控制元件中對應兩個引數的建構函式。為了方便,我們就從最基礎的View進行分析。

public View(Context context, AttributeSet attrs) {
        this(context, attrs, 0);
    }

    public View(Context context, AttributeSet attrs, int defStyle) {
	    this(context);

	    TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.View,
	            defStyle, 0);

	    Drawable background = null;

	    int leftPadding = -1;
	    int topPadding = -1;
	    int rightPadding = -1;
	    int bottomPadding = -1;
	    int startPadding = UNDEFINED_PADDING;
	    int endPadding = UNDEFINED_PADDING;

	    int padding = -1;

	    int viewFlagValues = 0;
	    int viewFlagMasks = 0;

	    boolean setScrollContainer = false;

	    int x = 0;
	    int y = 0;

	    float tx = 0;
	    float ty = 0;
	    float rotation = 0;
	    float rotationX = 0;
	    float rotationY = 0;
	    float sx = 1f;
	    float sy = 1f;
	    boolean transformSet = false;

	    int scrollbarStyle = SCROLLBARS_INSIDE_OVERLAY;
	    int overScrollMode = mOverScrollMode;
	    boolean initializeScrollbars = false;

	    boolean leftPaddingDefined = false;
	    boolean rightPaddingDefined = false;
	    boolean startPaddingDefined = false;
	    boolean endPaddingDefined = false;

	    final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;

	    final int N = a.getIndexCount();
		    for (int i = 0; i < N; i++) {
		        int attr = a.getIndex(i);
		        switch (attr) {
		            case com.android.internal.R.styleable.View_background:
		                background = a.getDrawable(attr);
		                break;
		            case com.android.internal.R.styleable.View_padding:
		                padding = a.getDimensionPixelSize(attr, -1);
		                mUserPaddingLeftInitial = padding;
		                mUserPaddingRightInitial = padding;
		                leftPaddingDefined = true;
		                rightPaddingDefined = true;
		                break;

			//省略一大串無關的函式
	}

由於我們只關注View中的背景圖是怎麼載入的,注意這個函式其實就是遍歷AttributeSet attrs這個東西,然後對View的各個屬性進行初始化。我們直接進入

background = a.getDrawable(attr);

這裡看看(TypedArray.getDrawable)。

public Drawable getDrawable(int index) {
        final TypedValue value = mValue;
        if (getValueAt(index*AssetManager.STYLE_NUM_ENTRIES, value)) {
            if (false) {
                System.out.println("******************************************************************");
                System.out.println("Got drawable resource: type="
                                   + value.type
                                   + " str=" + value.string
                                   + " int=0x" + Integer.toHexString(value.data)
                                   + " cookie=" + value.assetCookie);
                System.out.println("******************************************************************");
            }
            return mResources.loadDrawable(value, value.resourceId);
        }
        return null;
    }

我們發現它呼叫mResources.loadDrawable(…),進去看看。

/*package*/ Drawable loadDrawable(TypedValue value, int id)
            throws NotFoundException {

        if (TRACE_FOR_PRELOAD) {
            // Log only framework resources
            if ((id >>> 24) == 0x1) {
                final String name = getResourceName(id);
                if (name != null) android.util.Log.d("PreloadDrawable", name);
            }
        }

        boolean isColorDrawable = false;
        if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
                value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
            isColorDrawable = true;
        }
        final long key = isColorDrawable ? value.data :
                (((long) value.assetCookie) << 32) | value.data;

        Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key);

        if (dr != null) {
            return dr;
        }

        Drawable.ConstantState cs = isColorDrawable
                ? sPreloadedColorDrawables.get(key)
                : (sPreloadedDensity == mConfiguration.densityDpi
                        ? sPreloadedDrawables.get(key) : null);
        if (cs != null) {
            dr = cs.newDrawable(this);
        } else {
            if (isColorDrawable) {
                dr = new ColorDrawable(value.data);
            }

            if (dr == null) {
                if (value.string == null) {
                    throw new NotFoundException(
                            "Resource is not a Drawable (color or path): " + value);
                }

                String file = value.string.toString();

                if (TRACE_FOR_MISS_PRELOAD) {
                    // Log only framework resources
                    if ((id >>> 24) == 0x1) {
                        final String name = getResourceName(id);
                        if (name != null) android.util.Log.d(TAG, "Loading framework drawable #"
                                + Integer.toHexString(id) + ": " + name
                                + " at " + file);
                    }
                }

                if (DEBUG_LOAD) Log.v(TAG, "Loading drawable for cookie "
                        + value.assetCookie + ": " + file);

                if (file.endsWith(".xml")) {
                    try {
                        XmlResourceParser rp = loadXmlResourceParser(
                                file, id, value.assetCookie, "drawable");
                        dr = Drawable.createFromXml(this, rp);
                        rp.close();
                    } catch (Exception e) {
                        NotFoundException rnf = new NotFoundException(
                            "File " + file + " from drawable resource ID #0x"
                            + Integer.toHexString(id));
                        rnf.initCause(e);
                        throw rnf;
                    }

                } else {
                    try {
                        InputStream is = mAssets.openNonAsset(
                                value.assetCookie, file, AssetManager.ACCESS_STREAMING);
        //                System.out.println("Opened file " + file + ": " + is);
                        dr = Drawable.createFromResourceStream(this, value, is,
                                file, null);
                        is.close();
        //                System.out.println("Created stream: " + dr);
                    } catch (Exception e) {
                        NotFoundException rnf = new NotFoundException(
                            "File " + file + " from drawable resource ID #0x"
                            + Integer.toHexString(id));
                        rnf.initCause(e);
                        throw rnf;
                    }
                }
            }
        }

        if (dr != null) {
            dr.setChangingConfigurations(value.changingConfigurations);
            cs = dr.getConstantState();
            if (cs != null) {
                if (mPreloading) {
                    if (verifyPreloadConfig(value, "drawable")) {
                        if (isColorDrawable) {
                            sPreloadedColorDrawables.put(key, cs);
                        } else {
                            sPreloadedDrawables.put(key, cs);
                        }
                    }
                } else {
                    synchronized (mTmpValue) {
                        //Log.i(TAG, "Saving cached drawable @ #" +
                        //        Integer.toHexString(key.intValue())
                        //        + " in " + this + ": " + cs);
                        if (isColorDrawable) {
                            mColorDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs));
                        } else {
                            mDrawableCache.put(key, new WeakReference<Drawable.ConstantState>(cs));
                        }
                    }
                }
            }
        }

        return dr;
    }

就是這個函式了,所有View的背景的載入都在這裡了。這個函式的邏輯就比較複雜了,大體說來就是根據背景的型別(純顏色、定義在XML檔案中的,或者是一張靜態的背景),如果快取裡面有,就直接用快取裡的。

總結一下,經過上面的分析,我們知道了,Android就是在Activity.setContentView(…)中為我們進行資原始檔的載入,精確到具體的函式的話,資原始檔的載入就是在每一個被初始化的View的建構函式中進行載入的。

相關文章