Android应用启动,Activity加载及UI绘制流程详解

1.Android程序流程

           众所周知,我们的java程序想要开启需要依赖于main方法,也就是我们的程序入口(主线程)进入,但是在我们日常开发android程序的过程当中我们并没有发现main方法的存在,那么android当中的是如何开始运行的?

        熟悉的朋友们可能都知道在android当中存在一个叫做ActivityThread的类,这个类代表的是android当中的主线程,而在这个类当中我们看到了比较熟悉的main方法,那么现在是否可以认为我们的android在打开app时是首先调用的是当前这个类的main,也就是此处为我们的启动点:

public static void main(String[] args) {
    Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "ActivityThreadMain");
    这里省略掉部分代码...............
    Looper.prepareMainLooper();//这里开启主线程的Looper创建

    ActivityThread thread = new ActivityThread();//主函数再次开启一个Activity线程
    thread.attach(false);

    if (sMainThreadHandler == null) {
        sMainThreadHandler = thread.getHandler();
    }
    if (false) {
        Looper.myLooper().setMessageLogging(new
                LogPrinter(Log.DEBUG, "ActivityThread"));
    }
    // End of event ActivityThreadMain.
    Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
    Looper.loop();//这里开启主线程的消息轮训

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

OK,我们开始进入thread.attach(false)这个方法中查看做了什么:

private void attach(boolean system) {
    sCurrentActivityThread = this;
    mSystemThread = system;
    if (!system) {
    这里省略掉部分代码...............
        RuntimeInit.setApplicationObject(mAppThread.asBinder());
        //这个IActivityManager是Activity管理接口,
        final IActivityManager mgr = ActivityManager.getService();那么这个mgr得到的到底是什么?继续看下一步
        try {
        //这里调用attachApplication()这个方法
            mgr.attachApplication(mAppThread);
        } catch (RemoteException ex) {
            throw ex.rethrowFromSystemServer();
        }
       
    } else {
       这里省略掉部分代码...............
    }

    // add dropbox logging to libcore
    DropBox.setReporter(new DropBoxReporter());

  
}

进入ActivityManager中查看getService()这个方法:

/**
 * @hide
 */
public static IActivityManager getService() {
    return IActivityManagerSingleton.get();
}

private static final Singleton<IActivityManager> IActivityManagerSingleton =
        new Singleton<IActivityManager>() {
            @Override
            protected IActivityManager create() {
                final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);
                final IActivityManager am = IActivityManager.Stub.asInterface(b);
                return am;
            }
        };

在这个当中,里面调用了的系统的ActivityManagerService这个服务,并且给出了一个Binder接口

了解Binder机制的小伙伴看到这里应该很眼熟,在图中的create方法中进行了一个跨进程的相关操作

1:获得系统的IBinder的实现类
2:将系统(服务端)的Binder对象转换成客户端可以使用的接口类对象


也就是说getService这个方法所返回的对象其实就是和系统进行跨进程通信的,从对象的类名称可知是Activity的管理类。mgr通过attachApplication方法和mAppThread进行了绑定,mAppThread的类是ApplicationThread,父类为IApplicationThread.Stub,和Binder中的Stub的作用一样,用来接收系统(服务端)发过来的跨进程消息。这也是为什么我们讲Activity是跨进程访问的原因。

attachApplication方法另外一个作用是在这里的作用其实实际上是ActivityThread通过attach获取到后,然后将applciationThread将其关联,把activity相关信息存储在applciationThread里面,apllicationThread的类为activity的各种状态做了相对应的准备工作。如下为attachApplication方法的具体源码:

这个时候我们需要关注,ApplicationThread(ApplicationThread这个类是在ActivityThread中的内部类)当中做了什么?

当我们打开ApplicationThread中我们会看到一堆的schedle方法,这些方法的名称其实就可以给我们表明,代表的是在执行Activity的某种状态时调用的计划执行方法,这时我们会看到一个scheduleLaunchActivity方法,表示计划加载时调用的。这里我们发现了一个很有意思的事情:

@Override
public final void scheduleLaunchActivity(Intent intent, IBinder token, int ident,
        ActivityInfo info, Configuration curConfig, Configuration overrideConfig,
        CompatibilityInfo compatInfo, String referrer, IVoiceInteractor voiceInteractor,
        int procState, Bundle state, PersistableBundle persistentState,
        List<ResultInfo> pendingResults, List<ReferrerIntent> pendingNewIntents,
        boolean notResumed, boolean isForward, ProfilerInfo profilerInfo) {
    updateProcessState(procState, false);
    ActivityClientRecord r = new ActivityClientRecord();//这个对象就是我们的Activity
    r.token = token;
    r.ident = ident;
    r.intent = intent;
    //...一系列的赋值操作
    updatePendingConfiguration(curConfig);

    sendMessage(H.LAUNCH_ACTIVITY, r);//将当前我们创建的Activity发送了出去
}

当走到这里我们会发现最终我们调用的是Handler的消息通信机制,也就是说,在这里我们可以总结一下,当Activity状态改变时,都会有对应的一个消息发送出去,而接收这里,我能发现通过发送时不同的状态,这边调用了不同的handlerXXXActivity方法。

在ActivityThread类中有一个Handler类名为H,该类就是用来处理不同的发送消息,以便改变Activity状态等操作的,如下:

public void handleMessage(Message msg) {
    if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));
    switch (msg.what) {
        case LAUNCH_ACTIVITY: {
            Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, "activityStart");
            final ActivityClientRecord r = (ActivityClientRecord) msg.obj;
            r.packageInfo = getPackageInfoNoCheck(
                    r.activityInfo.applicationInfo, r.compatInfo);
            handleLaunchActivity(r, null, "LAUNCH_ACTIVITY");//进行Activity启动的处理
            Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);
        } break;
        case PAUSE_ACTIVITY: {
         // .....
        } break;
        //....等等一系列的判断处理操作
    }
    Object obj = msg.obj;
    if (obj instanceof SomeArgs) {
        ((SomeArgs) obj).recycle();
    }
    if (DEBUG_MESSAGES) Slog.v(TAG, "<<< done: " + codeToString(msg.what));
}

进入handleLaunchActivity方法中发现有个调用了performLaunchActivity方法:

ClassLoader cl = appContext.getClassLoader();
activity = this.mInstrumentation.newActivity(cl, component.getClassName(), r.intent);//创建了一个Activity对象

继续往下看,mInstrumentation.callActivityOnCreate(...)看到这里,我们貌似发现了Activity的生命周期的调用痕迹,那么其实到此为止,我门可以得出一个结论,Application运行的过程当中,对于Activity的操作,状态转变,其实实际上是通过Handler消息机制来完成的,Application当中只管去发, 由消息机制负责调用,因为在main方法当中我门的Looper轮训器是一直在进行轮训的。而当我们在加载Activity的时候,当中调用了一个performLaunchActivity()方法,在这个中间我发现了我们onCreate的调用痕迹如下:

private Activity performLaunchActivity(ActivityThread.ActivityClientRecord r, Intent customIntent) {
    ActivityInfo aInfo = r.activityInfo;
    if(r.packageInfo == null) {
        r.packageInfo = this.getPackageInfo((ApplicationInfo)aInfo.applicationInfo, r.compatInfo, 1);
    }

   //.............
 
            if(r.isPersistable()) {
                this.mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
            } else {
                this.mInstrumentation.callActivityOnCreate(activity, r.state);
            }

   //..........
}

那么这个mInstrumentation是什么呢?

每一个ActivityThread对象都有一个Instrumentation mInstrumentation;成员变量。mInstrumentation的初始化在ActivityThread,handleBindApplication函数中在callActivityOnCreate,callApplicationOnCreate,newActivity等基本上在application和activity的所有生命周期调用中,都会先调用instrumentation的相应方法。并且针对应用内的所有activity都生效。Instrumentation为程序员提供了一个强大的能力,有更多的可能性进入android app框架执行流程。

也就是说,到目前为止我们能够明白,整个Application加载Activity的整套流程是怎么回事,那么接下来我们需要关注的是,在onCreate当中我们所写的setContentView到底干了什么

2.setContentView

        在onCreate当中我们往往会使用setContentView去进行设置我们自己的布局文件或者view,那么在这当中他到底是怎么做的?getWindow()方法获取的就是Window对象,Window类上有一句注释,大概的意思是:这个Window的实现类有且只有一个实现类,那就是PhoneWindow。所以说,setContentView方法实际上市PhoneWindow来调用的,查看源码通过观察源码:

public void setContentView(@LayoutRes int layoutResID) {
    getWindow().setContentView(layoutResID);//这个getWindow()方法获取的就是Window对象,而只有一个实现类就是PhoneWindow类。
    initWindowDecorActionBar();
}

这个时候通过一系列线索我找到了最终的位置PhoneWindow类,进入phoneWindow中找到setContentView方法如下:

public void setContentView(int layoutResID) {
    if(this.mContentParent == null) {
        this.installDecor();
    } else if(!this.hasFeature(12)) {
        this.mContentParent.removeAllViews();
    }

    if(this.hasFeature(12)) {
        Scene newScene = Scene.getSceneForLayout(this.mContentParent, layoutResID, this.getContext());
        this.transitionTo(newScene);
    } else {
        this.mLayoutInflater.inflate(layoutResID, this.mContentParent);
    }

    this.mContentParent.requestApplyInsets();
   //...
}

这个时候我们会看到他做了两个事情,一个是installDecor,另一个是inflate,这两个后一个不难猜出他是在进行布局文件的解析, 前面的我们认为她是在初始化某个东西:

private void installDecor() {
    this.mForceDecorInstall = false;
    if(this.mDecor == null) {
        this.mDecor = this.generateDecor(-1);
        this.mDecor.setDescendantFocusability(262144);
        this.mDecor.setIsRootNamespace(true);
        if(!this.mInvalidatePanelMenuPosted && this.mInvalidatePanelMenuFeatures != 0) {
            this.mDecor.postOnAnimation(this.mInvalidatePanelMenuRunnable);
        }
    } else {
        this.mDecor.setWindow(this);
    }

    if(this.mContentParent == null) {
        this.mContentParent = this.generateLayout(this.mDecor);
        this.mDecor.makeOptionalFitsSystemWindows();
        DecorContentParent decorContentParent = (DecorContentParent)this.mDecor.findViewById(16908823);
       //.....
    }

}

进来之后发现他初始化了两个东西,一个叫做mDecor,一个叫做mContentParent

// This is the top-level view of the window, containing the window decor.
private DecorView mDecor;

// This is the view in which the window contents are placed. It is either
// mDecor itself, or a child of mDecor where the contents go.
ViewGroup mContentParent;

我们看到了,1:mDecor是一个DecorView

                     2:mContentParent是一个ViewGroup

透过注释的翻译,其实我们就能很明确知道这两个是用来干嘛的

// This is the view in which the window contents are placed. It is either(这是窗口内容放置的视图)

// mDecor itself, or a child of mDecor where the contents go.(它要么是mDecor本身,要么是mDecor的子类的内容。)

//This is the top-level view of the window, containing the window decor.(这是在窗口当中的顶层View,包含窗口的decor)

一个代表的是顶层view,一个用来装他下面的视图内容

在接着往下看的时候,我门发现,generateLayout方法当中,发现了在此处进行了大量的requestFeature的调用,也就是说,我们的requestFeature设置其实是在setContentView方法当中就开始了, 这也是为什么我们自己要去getWindow.requestFeature时必须在setContent之前的原因:

protected ViewGroup generateLayout(DecorView decor) {
    // Apply data from current theme.

    TypedArray a = getWindowStyle();

    if (false) {
        System.out.println("From style:");
        String s = "Attrs:";
        for (int i = 0; i < R.styleable.Window.length; i++) {
            s = s + " " + Integer.toHexString(R.styleable.Window[i]) + "="
                    + a.getString(i);
        }
        System.out.println(s);
    }

    mIsFloating = a.getBoolean(R.styleable.Window_windowIsFloating, false);
    int flagsToUpdate = (FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR)
            & (~getForcedWindowFlags());
    if (mIsFloating) {
        setLayout(WRAP_CONTENT, WRAP_CONTENT);
        setFlags(0, flagsToUpdate);
    } else {
        setFlags(FLAG_LAYOUT_IN_SCREEN|FLAG_LAYOUT_INSET_DECOR, flagsToUpdate);
    }

    if (a.getBoolean(R.styleable.Window_windowNoTitle, false)) {
        requestFeature(FEATURE_NO_TITLE);
    } else if (a.getBoolean(R.styleable.Window_windowActionBar, false)) {
        // Don't allow an action bar if there is no title.
        requestFeature(FEATURE_ACTION_BAR);
    }

    
}

然后在下面我门会发现在做了一件事情,

// Inflate the window decor.

int layoutResource;
int features = getLocalFeatures();
// System.out.println("Features: 0x" + Integer.toHexString(features));
if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
    layoutResource = R.layout.screen_swipe_dismiss;
    setCloseOnSwipeEnabled(true);
} else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
    if (mIsFloating) {
        TypedValue res = new TypedValue();
        getContext().getTheme().resolveAttribute(
                R.attr.dialogTitleIconsDecorLayout, res, true);
        layoutResource = res.resourceId;
    } else {
        layoutResource = R.layout.screen_title_icons;
    }
    // XXX Remove this once action bar supports these features.
    removeFeature(FEATURE_ACTION_BAR);
    // System.out.println("Title Icons!");
} else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
        && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
    //.....一系列判断赋值操作
} else {
    // Embedded, so no decoration is needed.
    layoutResource = R.layout.screen_simple;//该布局就是下面的xml布局文件,也是Activity出事化后最初的界面
    // System.out.println("Simple!");
}

mDecor.startChanging();
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
if (contentParent == null) {
    throw new RuntimeException("Window couldn't find content container view");
}

再次进入onResourcesLoaded方法中去:

void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
    mStackId = getStackId();
    if (mBackdropFrameRenderer != null) {
        loadBackgroundDrawablesIfNeeded();
        mBackdropFrameRenderer.onResourcesLoaded(
                this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
                mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
                getCurrentColor(mNavigationColorViewState));
    }

    mDecorCaptionView = createDecorCaptionView(inflater);
    final View root = inflater.inflate(layoutResource, null);
    if (mDecorCaptionView != null) {
        if (mDecorCaptionView.getParent() == null) {
            addView(mDecorCaptionView,
                    new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mDecorCaptionView.addView(root,
                new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
    } else {

        // Put it below the color views.
        addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
    mContentRoot = (ViewGroup) root;
    initializeElevation();
}

当前这里竟然在加载布局文件,并且生成了一个view, 但是好像貌似不是我门自己的,所以我们需要去探寻他到底加载了一个什么布局?点击R.layout.screen_simple进入

This is an optimized layout for a screen, with the minimum set of features
enabled.
-->

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fitsSystemWindows="true"
    android:orientation="vertical">
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
         android:id="@android:id/content"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
         android:foregroundInsidePadding="false"
         android:foregroundGravity="fill_horizontal|top"
         android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

这是我找到了一个比较有意思的组件,在这个上面我看到了一句这样的注释

//This is an optimized layout for a screen, with the minimum set of features enabled.

这是一个屏幕的优化布局,具有最小的特征集启用。通过注释和一些资料分析, 得到了一个比较坑的结果。从网络上查找得出如下图:

这是DecorView默认的一个渲染,然后我门自己的布局都是渲染到她的FrameLayout上的。

那么在这里我门现在能够明白,installDector其实实际上是在初始化两个视图容器,然后加载系统的R资源及特征,产生了一个基本布局,那么接着回到之前我门关注的另外一个方法mLayoutInflater.inflate(layoutResID, mContentParent),这个方法就比较好理解了,

/**
 * 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(@LayoutRes int resource, @Nullable ViewGroup root) {
    return inflate(resource, root, root != null);
}

这这段注释上面我门就可以得到一个信息

//Inflate a new view hierarchy from the specified xml resource.(从指定的视图当中获取试图的层次结构,意思就是,现在在加载自己的资源)而具体流程就不贴代码了给各位上一张图

那么在这里我门就能够明白,setContentView其实做了两件比较核心的事情,就是加载环境配置,和自己的布局,那么接下来我门需要考虑的事情就是,他到底怎么画到界面上的

3.UI是如何绘制的?

    通过前面两个章节,我门了解到,程序对于activity生命周期的调用,以及我们的视图资源的由来。这是我门需要找到的是我门的绘制起点在哪?

在ActivityThread启动时, 我发现在加载handleLaunchActivity方法调用performLaunchActivity方法之后又调用了一个handleResumeActivity在这里我发现了绘制流程的开始

final void handleResumeActivity(IBinder token, boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
    ActivityThread.ActivityClientRecord r = (ActivityThread.ActivityClientRecord)this.mActivities.get(token);
    if(checkAndUpdateLifecycleSeq(seq, r, "resumeActivity")) {
        //......
            WindowManager wm;
            if(r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                decor.setVisibility(4);
                wm = a.getWindowManager();
                LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                l.type = 1;
                l.softInputMode |= forwardBit;
                if(r.mPreserveWindow) {
                    a.mWindowAdded = true;
                    r.mPreserveWindow = false;
                    ViewRootImpl impl = decor.getViewRootImpl();
                    if(impl != null) {
                        impl.notifyChildRebuilt();
                    }
                }

                if(a.mVisibleFromClient) {
                    if(!a.mWindowAdded) {
                        a.mWindowAdded = true;
                        wm.addView(decor, l);
                    } else {
                        a.onWindowAttributesChanged(l);
                    }
                }
            } else if(!willBeVisible) {
                r.hideForNow = true;
            }
    }
}

通过前面的流程我门知道,onCreate之行完成之后,所有资源交给WindowManager保管

在这里,将我们的VIew交给了WindowManager,此处调用了addView,继续进入WindowManagerGlobal中的addView方法中:

public void addView(View view, ViewGroup.LayoutParams params,
        Display display, Window parentWindow) {
    if (view == null) {
        throw new IllegalArgumentException("view must not be null");
    }
  //....一系列判断
    ViewRootImpl root;
    View panelParentView = null;

    synchronized (mLock) {
        // Start watching for system property changes.
        if (mSystemPropertyUpdater == null) {
            mSystemPropertyUpdater = new Runnable() {
                @Override public void run() {
                    synchronized (mLock) {
                        for (int i = mRoots.size() - 1; i >= 0; --i) {
                            mRoots.get(i).loadSystemProperties();
                        }
                    }
                }
            };
            SystemProperties.addChangeCallback(mSystemPropertyUpdater);
        }

        // If this is a panel window, then find the window it is being
        // attached to for future reference.
        if (wparams.type >= WindowManager.LayoutParams.FIRST_SUB_WINDOW &&
                wparams.type <= WindowManager.LayoutParams.LAST_SUB_WINDOW) {
            final int count = mViews.size();
            for (int i = 0; i < count; i++) {
                if (mRoots.get(i).mWindow.asBinder() == wparams.token) {
                    panelParentView = mViews.get(i);
                }
            }
        }

        root = new ViewRootImpl(view.getContext(), display);

        view.setLayoutParams(wparams);

        mViews.add(view);
        mRoots.add(root);
        mParams.add(wparams);

        // do this last because it fires off messages to start doing things
        try {
            root.setView(view, wparams, panelParentView);
        } catch (RuntimeException e) {
            // BadTokenException or InvalidDisplayException, clean up.
            if (index >= 0) {
                removeViewLocked(index, true);
            }
            throw e;
        }
    }
}
private final ArrayList<View> mViews = new ArrayList<View>();
private final ArrayList<ViewRootImpl> mRoots = new ArrayList<ViewRootImpl>();
private final ArrayList<WindowManager.LayoutParams> mParams =
        new ArrayList<WindowManager.LayoutParams>();
private final ArraySet<View> mDyingViews = new ArraySet<View>();

进入addView之后我们发现了一段这样的代码,他将视图,和参数还有我门的一个ViewRoot对象都用了容器去装在了起来,那么在此处我门可以得出,是将所有的相关对象保存起来

mViews保存的是View对象,DecorView

mRoots保存和顶层View关联的ViewRootImpl对象

mParams保存的是创建顶层View的layout参数。

而WindowManagerGlobal类也负责和WMS通信

而在此时,有一句关键代码root.setView,这里是将我们的参数,和视图同时交给了ViewRoot,那么这个时候我们来看下ViewRoot当中的setView干了什么?终于在ViewRootImpl类中的setView方法中让我发现了让我明白的一步

view.assignParent(this);//this就是ViewRoot

在这里我门会看到view.assignParent的设置是this, 那么也就是说在view当中parent其实实际上是ViewRoot

那么在setContentView当中调用了一个setLayoutParams()是调用的ViewRoot的,而在ViewRoot当中发现了setLayoutParams和preformLayout对requestLayout方法的调用。在requestLayout当中发现了对scheduleTraversals方法的调用而scheduleTraversals当中调用了doTraversal的访问,最终访问到了performTraversals(),而在这个里面,我发现了整体的绘制流程的调用。当前里面依次是用了:

 // Ask host how big it wants to be
performMeasure(childWidthMeasureSpec, childHeightMeasureSpec);

// Implementation of weights from WindowManager.LayoutParams
// We just grow the dimensions as needed and re-measure if
// needs be
int width = host.getMeasuredWidth();
int height = host.getMeasuredHeight();
boolean measureAgain = false;

final boolean didLayout = layoutRequested && (!mStopped || mReportNextDraw);
boolean triggerGlobalLayoutListener = didLayout
        || mAttachInfo.mRecomputeGlobalAttributes;
if (didLayout) {
    performLayout(lp, mWidth, mHeight);
boolean cancelDraw = mAttachInfo.mTreeObserver.dispatchOnPreDraw() || !isViewVisible;

if (!cancelDraw && !newSurface) {
    if (mPendingTransitions != null && mPendingTransitions.size() > 0) {
        for (int i = 0; i < mPendingTransitions.size(); ++i) {
            mPendingTransitions.get(i).startChangingAnimations();
        }
        mPendingTransitions.clear();
    }

    performDraw();
}

UI绘制先回去测量布局,然后在进行布局的摆放,当所有的布局测量摆放完毕之后,进行绘制。至此整体UI绘制过程我们就已经非常清楚了。 我门可以根据这种绘制的流程来操作自己的自定义组件。

发布了116 篇原创文章 · 获赞 165 · 访问量 17万+

猜你喜欢

转载自blog.csdn.net/qq_42618969/article/details/104946388