源码分析篇 - Android绘制流程(一)窗口启动流程分析

Activity、View、Window之间的关系可以用以下的简要UML关系图表示,在这里贴出来,比较能够帮组后面流程分析部分的阅读。

源码分析篇 - Android绘制流程(一)窗口启动流程分析

一、Activity的启动流程

  在startActivity()后,经过一些逻辑流程会通知到ActivityManagerService(后面以AMS简称),AMS接收到启动acitivty的请求后,会通过跨进程通信调用AcitivtyThread.handleLauncherActivity()方法,我们从这里开始分析,首先来看handleLauncherActivity()方法。

   private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent, String reason) {
     
...// Initialize before creating the activity
WindowManagerGlobal.initialize(); Activity a = performLaunchActivity(r, customIntent);

      if (a != null) {
        r.createdConfig = new Configuration(mConfiguration);
        reportSizeConfigurations(r);
        Bundle oldState = r.state;

        //该方法会调用到Activity的onResume()方法
        handleResumeActivity(r.token, false, r.isForward,
              !r.activity.mFinished && !r.startsNotResumed, r.lastProcessedSeq, reason);

        ...

      }

        ...
}

  这里重点关注三个方法(加粗的地方),首先来看WindowManagerGlobal.initialize(),WindowManagerGlobal是单例模式的,一个进程内只有一个,这里调用该类的初始化方法,后续我们再对该类的作用和相关方法进行分析;第三个是在创建好Activity后调用Acitivty的onResume()方法。这里我们来看需关注的第二个方法performLaunchActivity(),代码如下。

    private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
     ...

     //通过反射方式创建Activity
Activity activity = null;
try {
java.lang.ClassLoader cl = r.packageInfo.getClassLoader();
activity = mInstrumentation.newActivity(
cl, component.getClassName(), r.intent);
StrictMode.incrementExpectedActivityCount(activity.getClass());
r.intent.setExtrasClassLoader(cl);
r.intent.prepareToEnterProcess();
if (r.state != null) {
r.state.setClassLoader(cl);
}
} catch (Exception e) {
if (!mInstrumentation.onException(activity, e)) {
throw new RuntimeException(
"Unable to instantiate activity " + component
+ ": " + e.toString(), e);
}
} try {
...
       if (activity != null) {
Context appContext = createBaseContextForActivity(r, activity);
CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
Configuration config = new Configuration(mCompatConfiguration);
if (r.overrideConfig != null) {
config.updateFrom(r.overrideConfig);
}
if (DEBUG_CONFIGURATION) Slog.v(TAG, "Launching activity "
+ r.activityInfo.name + " with config " + config);
Window window = null;
if (r.mPendingRemoveWindow != null && r.mPreserveWindow) {
window = r.mPendingRemoveWindow;
r.mPendingRemoveWindow = null;
r.mPendingRemoveWindowManager = null
;
}

activity.attach(appContext, this, getInstrumentation(), r.token,
r.ident, app, r.intent, r.activityInfo, title, r.parent,
r.embeddedID, r.lastNonConfigurationInstances, config,
r.referrer, r.voiceInteractor, window);

          ...           //调用acitivity的onCreate()方法

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

          ...
       }
            return activity;
}

  这个方法主要是读取Acitivity这里利用反射创建出ActivityClientRecord所要求的Activity对象,然后调用了acitivity.attach()方法。注意attach()传入的参数有很多,在performLacunchActivity()方法流程中,调用attach()方前,我们省略掉的步骤基本都在为这些参数做准备,attach()方法的作用其实就是将这些参数配置到新创建的Activity对象中;而在attach之后则会回调到acitivity的onCreate()方法。我们进入Activity.java类详细来看下attach方法。

  此外,在attach之前会初始化一个Window对象,Window.java是一个抽象类,代表了一个矩形不可见的容器,主要负责加载显示界面,每个Activity都会对应了一个Window对象。如果ActivityClientRecord.mPendingRevomeWindow变量中已经保存了一个Window对象,则会在后面的attach方法中被使用,具体使用的场景会在后面中介绍。

    final void attach(Context context, ActivityThread aThread,
Instrumentation instr, IBinder token, int ident,
Application application, Intent intent, ActivityInfo info,
CharSequence title, Activity parent, String id,
NonConfigurationInstances lastNonConfigurationInstances,
Configuration config, String referrer, IVoiceInteractor voiceInteractor,
Window window) {
attachBaseContext(context); mFragments.attachHost(null /*parent*/);

mWindow = new PhoneWindow(this, window);//(1)
mWindow.setWindowControllerCallback(this);
mWindow.setCallback(this);
mWindow.setOnWindowDismissedCallback(this
);
mWindow.getLayoutInflater().setPrivateFactory(this);
if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
mWindow.setSoftInputMode(info.softInputMode);
}
if (info.uiOptions != 0) {
mWindow.setUiOptions(info.uiOptions);
}
     ... //初始化Acitity相关属性 mWindow.setWindowManager(
(WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
mToken, mComponent.flattenToString(),
(info.flags
& ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);//(2)
if (mParent != null) {
mWindow.setContainer(mParent.getWindow());
}
mWindowManager = mWindow.getWindowManager();
mCurrentConfig = config;
}

  重点关注初始化window对象的操作,首先创建了PhoneWindow对象为activity的mWindow变量,在创建时传入了上一个activity对应的window对象,之后又将这个acitivity设置为window对象的回调。Activity中很多操作view相关的方法,例如setContentView()、findViewById()、getLayoutInflater()等,实际上都是直接调用到PhoneWindow里面的相关方法。创建完acitivty对应的PhoneWindow之后便会调用setWindowManager()方法。首先来看PhonewWindow构造方法。

  public PhoneWindow(Context context, Window preservedWindow) {
this(context);
// Only main activity windows use decor context, all the other windows depend on whatever
// context that was given to them.
mUseDecorContext = true;if (preservedWindow != null) { //快速重启activity机制
mDecor = (DecorView) preservedWindow.getDecorView();
mElevation = preservedWindow.getElevation();
mLoadElevation = false;
mForceDecorInstall = true;
// If we're preserving window, carry over the app token from the preserved
// window, as we'll be skipping the addView in handleResumeActivity(), and
// the token will not be updated as for a new window.
getAttributes().token = preservedWindow.getAttributes().token;
}
// Even though the device doesn't support picture-in-picture mode,
// an user can force using it through developer options.
boolean forceResizable = Settings.Global.getInt(context.getContentResolver(),
DEVELOPMENT_FORCE_RESIZABLE_ACTIVITIES, 0) != 0;
mSupportsPictureInPicture = forceResizable || context.getPackageManager().hasSystemFeature(
PackageManager.FEATURE_PICTURE_IN_PICTURE);
}

  首先要关注的就是preserviedWindow参数,这个参数就是上一段中提到的mPendingRevomeWindow变量,这个参数在什么时候会不为空呢?其实这里的逻辑是用来快速重启acitivity的,比如你的一个activity已经启动了,但是主题换了或者configuration变了,这里只需要重新加载资源和view,没必重新再执行DecorView的创建工作。

  另一个要关注的就是mDecor变量,这个变量是DecorView类型的,如果这里没有初始化的话,则会在调用setContentView方法中new一个DecorView对象出来。DecorView对象继承自FrameLayout,所以他本质上还是一个view,只是对FrameLayout做了一定的包装,例如添加了一些与window需要调用的方法setWindowBackground()、setWindowFrame()等。我们知道,acitivty界面的view是呈树状结构的,而mDecor变量在这里作为activity的界面的根view存在。这三个点关系就比如,PhoneWindow是一块手机电子屏,DecorView就是电子屏要显示的内容,Activity就是手机电子屏安装位置。

  再来看创建PhonewWindow之后调用的setWindowManager()方法的逻辑,这段代码是在PhonewWindow.java的父类Window.java中代码如下。

    public void setWindowManager(WindowManager wm, IBinder appToken, String appName,
boolean hardwareAccelerated) {
mAppToken = appToken;
mAppName = appName;
mHardwareAccelerated = hardwareAccelerated
|| SystemProperties.getBoolean(PROPERTY_HARDWARE_UI, false);
if (wm == null) {
wm =
(WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
}

mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
}

  对于wWindowManager变量,实际上这里是创建了一个WindowManagerImpl对象。首先是这种首先获取系统服务的代理到wm上,然后强制转换为WindowManagerImpl调用createLocalWindowManager(),在createLocalWindowManager()实际是执行了一个new WindowManagerImpl()到方法来创建。关于这部分代码看了很久很困惑的一个点,就是为啥要弄个这么复杂的逻辑,直接把上面加粗的代码改为new WindowManagerImpl(...),这养会有什么区别吗?如果有大虾看到这里,希望能帮我解答。

  在WindowManager中保存了对于单例对象WindowManagerGloble的引用,即mGlobal变量。此外,WindowManager.java实现了WindowManager又,而WindowManager继承自ViewManager接口,ViewManager接口方法如下方代码。

public interface ViewManager
{
/**
* Assign the passed LayoutParams to the passed View and add the view to the window.
* <p>Throws {@link android.view.WindowManager.BadTokenException} for certain programming
* errors, such as adding a second view to a window without removing the first view.
* <p>Throws {@link android.view.WindowManager.InvalidDisplayException} if the window is on a
* secondary {@link Display} and the specified display can't be found
* (see {@link android.app.Presentation}).
* @param view The view to be added to this window.
* @param params The LayoutParams to assign to view.
*/
public void addView(View view, ViewGroup.LayoutParams params);
public void updateViewLayout(View view, ViewGroup.LayoutParams params);
public void removeView(View view);
}

  在WindowManager对于addView()、updateViewLayout()和removeView()的实现,都是调用到mGlobal变量对应的addView()、updateViewLayout()和removeView()方法来实现的。这里我们

  

这样我们分析完activity以及对应的window对象的创建,回到performLauncerActivity()方法中Activity a = performLaunchActivity(r, customIntent)这一步骤,之后便回调activity方法的onCreate(),在onCreate()的setContentView方法会初始化DecorView,并根据传入参数加载布局,详细步骤在下一节介绍。

  再回到最初的handlerLaunchActivity()方法中,通过调用performLauncerActivity()创建出一个Acitivty对象后,如果创建成功则执行handleResumeActivity(),便执行到了Acitivity的onResume()方法,即是完成了acitivty的启动。

 

二、setContentView()流程

  首先,我们一般在onCreate()里调用setContentView()的方法。

    @override
protected void onCreate(Bundle savedInstanceState) {
  super.onCreate(savedInstanceState);
  setContentView(R.layout.activity_main);
}

  这里实际调用到到地方是Acitivity.java类中的setContentView()方法,如下。

    public void setContentView(@LayoutRes int layoutResID) {
  getWindow().setContentView(layoutResID);
  initWindowDecorActionBar();
}

  这里getWindow()返回的是Acitivity.java类中的mWindow变量,就是Activity创建时一起创建的PhoneWindow对象,进入到

    @Override
public void setContentView(int layoutResID) {
if (mContentParent == null) {
installDecor();
} else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
mContentParent.removeAllViews();//如果多次调用setContentView则会执行removeAllView操作
} if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) { //过渡动画机制相关
final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
getContext());
transitionTo(newScene);
} else {
mLayoutInflater.inflate(layoutResID, mContentParent);
}
mContentParent.requestApplyInsets();
final Callback cb = getCallback();
if (cb != null && !isDestroyed()) {
cb.onContentChanged();
}
mContentParentExplicitlySet = true;
}

  代码里涉及到FEATURE_CONTENT_TRANSITIONS的属性,这里是Android的过渡动画相关机制,这里我们不再展开详述。一般的Acitivty启动时,会进入mContentParent为null的逻辑,首先调用的是installDecor()方法,完成DecorView的创建工作;之后调用mLayoutInflater.inflate()方法将我们传入的资源文件转换为view树,装载到mContentParent中。首先来看installDecor()代码。

    private void installDecor() {
mForceDecorInstall = false;
if (mDecor == null) { //创建DecorView
mDecor
= generateDecor(-1);
mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
mDecor.setIsRootNamespace(true);
if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
}
} else {
mDecor.setWindow(this);
}
if (mContentParent == null) {
mContentParent = generateLayout(mDecor); ...
}

  在这个方法又两个主要步骤,首先是使用generateDecor()方法创建了DecorView对象,generateDecor()方法比较简单,主要就是执行new DecorView(context, featureId, this, getAttributes())方法,这里不再贴出代码;重点来看generateLayout()方法,这个方法生成的mContentParent是作为来我们后续加载加载的用户的布局的父布局存在的。

   protected ViewGroup generateLayout(DecorView decor) {
// Apply data from current theme.
     //获取当前主题的相关属性
TypedArray a = getWindowStyle(); ... //一大段的根据获取到到主题属性,解析保存到PhonwWindow的相关参数的变量中 int layoutResource;
int features = getLocalFeatures();

     ... //一大段根据PhoneWindow的设定好的属性(features和mIsFloating)的判断,为layoutResource进行赋值,
//值可以为R.layout.screen_custom_title、R.layout.screen_action_bar等 mDecor.startChanging();
     //将layoutRsourece值对应的布局文件加载到DecorView中
mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

     //在加载给DecorView的布局文件中有一块id为ID_ANDROID_CONTENT的区域是用于用户显示自己布局的,也是setContextView传入的布局显示的地方
     //这块区域会以ViewGroup的形式赋值给mContentParent变量,这个ViewGroup即是用户布局的父布局节点
ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);

     ... //继续一大段的属性配置 mDecor.finishChanging(); return contentParent;
}

  generateLayout方法实际就是解析出主题的相关属性,根据不同的主题样式的属性值选择不同的布局文件设置到DecorView中(DecorView本事就是FrameLayout)。在view的树状结构下,DecorView即是整个Window显示的视图的根节点,在DecorView的子节点中又有一块id为ID_ANDROID_CONTENT的区域有一块区域作为mContentParent变量用于加载用户的布局,与mContentParent平级的视图有ActionBar视图和Title的视图。总结来说,installDecor()方法实质就是产生mDecor和mContentParent对象。在installDecor之后,会执行到mLayoutInflater.inflate(layoutResID, mContentParent)方法将用户传入的布局转化为view再加入到mContentParent上。这样就完成了setContentView()流程。

三、handleResumeActivity()流程

  在文章开头贴出的第一段AcitityThread.handleLauncherActivity()方法的代码中,执行完performLaunchAcitity()创建好Acitivity后,便会执行到handleResumeActivity()方法,该方法代码如下。

   final void handleResumeActivity(IBinder token,
boolean clearHide, boolean isForward, boolean reallyResume, int seq, String reason) {
...// TODO Push resumeArgs into the activity for consideration
// 该方法执行过程中会调用到Acitity的onResume()方法,返回的ActivityClientRecord对象描述的即是创建好的Activity
     r = performResumeActivity(token, clearHide, reason); if (r != null) {
final Activity a = r.activity;//返回之前创建的Acitivty if (localLOGV) Slog.v(
TAG, "Resume " + r + " started activity: " +
a.mStartedActivity + ", hideForNow: " + r.hideForNow
+ ", finished: " + a.mFinished); final int forwardBit = isForward ?
WindowManager.LayoutParams.SOFT_INPUT_IS_FORWARD_NAVIGATION : 0; // If the window hasn't yet been added to the window manager,
// and this guy didn't finish itself or start another activity,
// then go ahead and add the window.
       // 判断该Acitivity是否可见,mStartedAcitity记录的是一个Activity是否还处于启动状态
       // 如果还处于启动状态则mStartedAcitity为true,表示该activity还未启动好,则该Activity还不可见

       boolean willBeVisible = !a.mStartedActivity;
       // 如果启动的组建不是全屏的,mStartedActivity也会是true,此时依然需要willBeVisible为true以下的if逻辑就是针对这种情况的校正
if (!willBeVisible) {
try {
willBeVisible = ActivityManagerNative.getDefault().willActivityBeVisible(
a.getActivityToken());
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
if (r.window == null && !a.mFinished && willBeVisible) {
r.window = r.activity.getWindow();
View decor = r.window.getDecorView();
decor.setVisibility(View.INVISIBLE);
ViewManager wm = a.getWindowManager();
WindowManager.LayoutParams l = r.window.getAttributes();
a.mDecor = decor;
l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
l.softInputMode |= forwardBit;
          //PreserverWindow,一般指主题换了或者configuration变了情况下的Acitity快速重启机制
if (r.mPreserveWindow) {
a.mWindowAdded = true;
r.mPreserveWindow = false;
// Normally the ViewRoot sets up callbacks with the Activity
// in addView->ViewRootImpl#setView. If we are instead reusing
// the decor view we have to notify the view root that the
// callbacks may have changed.
ViewRootImpl impl = decor.getViewRootImpl();
if (impl != null) {
impl.notifyChildRebuilt();
}
}
if (a.mVisibleFromClient && !a.mWindowAdded) {
a.mWindowAdded = true;
            //调用了WindowManagerImpl的addView方法
wm.addView(decor, l);

}   ...
}

  重点来看wm.addView()方法,该方法中的decor参数为Acitity对应的Window中的视图DecorView,wm为在创建PhoneWindow是创建的WindowManagerImpl对象,该对象的addView方法实际调用到到是单例对象WindowManagerGlobal的addView方法(前文有提到)。在看addView代码前,我先来看看WindowManagerGlobal对象成员变量。

    private static WindowManagerGlobal sDefaultWindowManager;
private static IWindowManager sWindowManagerService;
private static IWindowSession sWindowSession; private final Object mLock = new Object(); 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>();

  三个成员变量mViews、mRoots和mParams分别是类型为View、ViewRootImpl和WindowManager.LayoutParams的数组。这里有这样的逻辑关系,每个View都对应着唯一的一个ViewRootImpl和WindowManager.LayoutRarams,即是1:1:1的关系。这三个数组长度始终保持一致,并且在同一个位置上存放的是互相关联的View、ViewRootImpl和WindowManager.LayoutParams对象。此外还有一个成员变量mDyView,保存的则是已经不需要但还未被系统会收到View。

  View与LayoutParams比较好理解,那ViewRootImpl对象的作用是什么呢?首先WindowManagerImpl是作为管理类,就像主管一样,根据Acitity和Window的调用请求,找到合适的做事的人;DecorView本身是FrameworkLayout,本事是一个View,所表示的是一种静态的结构;所以这里就需要一个真正做事的人,那就是ViewRootImpl类的工作。总结来讲ViewRootImpl的功能如下

1. 完成了绘制过程。在ViewRootImpl类中,实现了perfromMeasure()、performDraw()、performLayout()等绘制相关的方法。

2. 与系统服务进行交互,例如与AcitityManagerSerivice,DisplayService、AudioService等进行通信,保证了Acitity相关功能等正常运转。

3. 触屏事件等分发逻辑的实现

  接下来我们进入WindowManagerGlobal.addView()方法的代码。

    public void addView(View view, ViewGroup.LayoutParams params,
Display display, Window parentWindow) {
... ViewRootImpl root;
View panelParentView = null; synchronized (mLock) {        ...
            // If this is a panel window, then find the window it is being
// attached to for future reference.
       // 如果当前添加的是一个子视图,则还需要找他他的父视图
       //这里我们分析的是添加DecorView的逻辑,没有父视图,故不会走到这里,panelParentView为null
       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);

       //保存互相对应的View、ViewRootImpl、WindowManager.LayoutParams到数组中
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;
}
}
}

  关注代码中加粗的两个方法,首先会创建一个ViewRootImpl对象,然后调用ViewRootImpl.setView方法,其中panelParentView在addView参数为DecorView是为null。进入ViewRootImpl.setView()代码。 

   public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
synchronized (this) {
if (mView == null) {
          //初始化成员变量mView、mWindowAttraibutes
          //mAttachInfo是View类的一个内部类AttachInfo类的对象
//该类的主要作用就是储存一组当View attach给它的父Window的时候Activity各种属性的信息
mView = view;
mAttachInfo.mDisplayState = mDisplay.getState();
mDisplayManager.registerDisplayListener(mDisplayListener, mHandler); mViewLayoutDirectionInitial = mView.getRawLayoutDirection();
mFallbackEventHandler.setView(view);
mWindowAttributes.copyFrom(attrs);
... //继续初始化一些变量,包含针对panelParentView不为null时的父窗口的一些处理

          mAdded = true;
// Schedule the first layout -before- adding to the window
// manager, to make sure we do the relayout before receiving
// any other events from the system.
          // 这里调用异步刷新请求,最终会调用performTraversals方法来完成View的绘制
          requestLayout();
if ((mWindowAttributes.inputFeatures
& WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL) == 0) {
mInputChannel = new InputChannel();
}
mForceDecorViewVisibility = (mWindowAttributes.privateFlags
& PRIVATE_FLAG_FORCE_DECOR_VIEW_VISIBILITY) != 0;
try {
mOrigWindowType = mWindowAttributes.type;
mAttachInfo.mRecomputeGlobalAttributes = true;
collectViewAttributes();
res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
getHostVisibility(), mDisplay.getDisplayId(),
mAttachInfo.mContentInsets, mAttachInfo.mStableInsets,
mAttachInfo.mOutsets, mInputChannel);

} catch (RemoteException e) {
mAdded = false;
mView = null;
mAttachInfo.mRootView = null;
mInputChannel = null;
mFallbackEventHandler.setView(null);
unscheduleTraversals();
setAccessibilityFocus(null, null);
throw new RuntimeException("Adding window failed", e);
} finally {
if (restore) {
attrs.restore();
}
} ...
}
}
}

  相关变量初始化完成后,便会将mAdded设置为true,表示ViewRootImpl与setView传入的View参数已经做好了关联。之后便会调用requestLayout()方法来请求一次异步刷新,该方法后来又会调用到performTraversals()方法来完成view到绘制工作。注意到这里虽然完成了绘制的工作,但是我们创建Activity的源头是AMS中发起的,我们从一开始创建Acitivity到相对应的Window、DecorView这一大套对象时,还并未与AMS进程进行反馈。所以之后便会调用mWindowSession.addToDisplay()方法会执行IPC的跨进程通信,最终调用到AMS中的addWindow方法来在系统进程中执行相关加载Window的操作。

 
上一篇:ORACLE抽象数据类型


下一篇:threejs- z-fighting 问题(模型的重叠部位便不停的闪烁起来。这便是Z-Fighting问题)