【凱子哥帶你學(xué)Framework】Activity界面顯示全解析(下)

咱們接著上篇繼續(xù)講,上篇沒看的請(qǐng)戳:【凱子哥帶你學(xué)Framework】Activity界面顯示全解析(上)

如何驗(yàn)證上一個(gè)問題

首先,說明一下運(yùn)行條件:

 //主題
name="AppTheme" parent="@android:style/Theme.Holo.Light.NoActionBar"

//編譯版本
android {
    compileSdkVersion 19
    buildToolsVersion '19.1.0'

    defaultConfig {
        applicationId "com.socks.uitestapp"
        minSdkVersion 15
        targetSdkVersion 19
        versionCode 1
        versionName "1.0"
    }
}

dependencies {
    compile fileTree(include: ['*.jar'], dir: 'libs')
    compile 'com.android.support:appcompat-v7:19.1.0'
}

//Activity代碼
public class MainActivity extends Activity {

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

//activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    android:text="Hello World!"
    android:textSize="20sp" />

OK,咱們的軟件已經(jīng)準(zhǔn)備好了,采用的是最簡(jiǎn)單的布局,界面效果如下:

下面用Hierarchy看一下樹狀結(jié)構(gòu):

第一層,就是上面的DecorView,里面有一個(gè)線性布局,上面的是ViewStub,下面就是id為content的ViewGroup,是一個(gè)FrameLayout。而我們通過setContentView()設(shè)置的布局,就是TextView了。

能不能在源碼里面找到源文件呢?當(dāng)然可以,這個(gè)布局就是screen_simple.xml

frameworks/base/core/res/res/layout/screen_simple.xml

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

所以,即使你不調(diào)用setContentView(),在一個(gè)空Activity上面,也是有布局的。而且肯定有一個(gè)DecorView,一個(gè)id為content的FrameLayout。

你可以采用下面的方式獲取到DecorView,但是你不能獲取到一個(gè)DecorView實(shí)例,只能獲取到ViewGroup。

下面貼上這個(gè)圖,你就可以看明白了(轉(zhuǎn)自 工匠若水)

ViewGroup view = (ViewGroup) getWindow().getDecorView();

我們通過setContentView()設(shè)置的界面,為什么在onResume()之后才對(duì)用戶可見呢?

有開發(fā)經(jīng)驗(yàn)的朋友應(yīng)該知道,我們的界面元素在onResume()之后才對(duì)用戶是可見的,這是為啥呢?

那我們就追蹤一下,onResume()是什么時(shí)候調(diào)用的,然后看看做了什么操作就Ok了。

這一下,我們又要從ActivityThread開始說起了,不熟悉的快去看前一篇文章《Activity啟動(dòng)過程全解析》](http://blog.csdn.net/zhaokaiqiang1992/article/details/49428287)。

話說,前文說到,我們想要開啟一個(gè)Activity的時(shí)候,ActivityThread的handleLaunchActivity()會(huì)在Handler中被調(diào)用

private void handleLaunchActivity(ActivityClientRecord r, Intent customIntent) {

    //就是在這里調(diào)用了Activity.attach()呀,接著調(diào)用了Activity.onCreate()和Activity.onStart()生命周期,但是由于只是初始化了mDecor,添加了布局文件,還沒有把
    //mDecor添加到負(fù)責(zé)UI顯示的PhoneWindow中,所以這時(shí)候?qū)τ脩魜碚f,是不可見的
    Activity a = performLaunchActivity(r, customIntent);

    ......
    
    if (a != null) {
    //這里面執(zhí)行了Activity.onResume()
    handleResumeActivity(r.token, false, r.isForward,
                    !r.activity.mFinished && !r.startsNotResumed);
    
    if (!r.activity.mFinished && r.startsNotResumed) {
        try {
                    r.activity.mCalled = false;
                    //執(zhí)行Activity.onPause()
                    mInstrumentation.callActivityOnPause(r.activity);
                    }
        }
    }
}

所以說,ActivityThread.handleLaunchActivity執(zhí)行完之后,Activity的生命周期已經(jīng)執(zhí)行了4個(gè)(onCreate、onStart()、onResume、onPause())。

下面咱們重點(diǎn)看下handleResumeActivity()做了什么

final void handleResumeActivity(IBinder token,
            boolean clearHide, boolean isForward, boolean reallyResume) {
            
            //這個(gè)時(shí)候,Activity.onResume()已經(jīng)調(diào)用了,但是現(xiàn)在界面還是不可見的
            ActivityClientRecord r = performResumeActivity(token, clearHide);
            
            if (r != null) {
                final Activity a = r.activity;
                  if (r.window == null && !a.mFinished && willBeVisible) {
                r.window = r.activity.getWindow();
                View decor = r.window.getDecorView();
                //decor對(duì)用戶不可見
                decor.setVisibility(View.INVISIBLE);
                ViewManager wm = a.getWindowManager();
                WindowManager.LayoutParams l = r.window.getAttributes();
                a.mDecor = decor;
                //這里記住這個(gè)WindowManager.LayoutParams的type為TYPE_BASE_APPLICATION,后面介紹Window的時(shí)候會(huì)見到
                l.type = WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
               
                if (a.mVisibleFromClient) {
                    a.mWindowAdded = true;
                    //終于被添加進(jìn)WindowManager了,但是這個(gè)時(shí)候,還是不可見的
                    wm.addView(decor, l);
                }
                
                if (!r.activity.mFinished && willBeVisible
                    && r.activity.mDecor != null && !r.hideForNow) {
                     //在這里,執(zhí)行了重要的操作!
                     if (r.activity.mVisibleFromClient) {
                            r.activity.makeVisible();
                        }
                    }
            }

從上面的分析中我們知道,其實(shí)在onResume()執(zhí)行之后,界面還是不可見的,當(dāng)我們執(zhí)行了Activity.makeVisible()方法之后,界面才對(duì)我們是可見的


if (!mWindowAdded) {
            ViewManager wm = getWindowManager();
            wm.addView(mDecor, getWindow().getAttributes());
            mWindowAdded = true;
        }
        mDecor.setVisibility(View.VISIBLE);

OK,其實(shí)講到了這里,關(guān)于Activity中的界面顯示應(yīng)該算是告一段落了,我們知道了Activity的生命周期方法的調(diào)用時(shí)機(jī),還知道了一個(gè)最簡(jiǎn)單的Activity的界面的構(gòu)成,并了解了Window、PhoneWindow、DecorView、WindowManager的存在。

但是我還是感覺不過癮,因?yàn)樯厦嬷皇窃诹鞒躺洗篌w上過了一遍,對(duì)于Window、WindowManager的深入了解還不夠,所以下面就開始講解Window、WindowManager等相關(guān)類的稍微高級(jí)點(diǎn)的知識(shí)。

前面看累了的朋友,可以上個(gè)廁所,泡個(gè)咖啡,休息下繼續(xù)往下看。

ViewManager、WindowManager、WindowManagerImpl、WindowManagerGlobal到底都是些什么玩意?

WindowManager其實(shí)是一個(gè)接口,和Window一樣,起作用的是它的實(shí)現(xiàn)類

public interface WindowManager extends ViewManager {

     //對(duì)這個(gè)異常熟悉么?當(dāng)你往已經(jīng)銷毀的Activity中添加Dialog的時(shí)候,就會(huì)拋這個(gè)異常
     public static class BadTokenException extends RuntimeException {
            public BadTokenException() {
        }

        public BadTokenException(String name) {
            super(name);
        }
    }
     
     //其實(shí)WindowManager里面80%的代碼是用來描述這個(gè)內(nèi)部靜態(tài)類的
      public static class LayoutParams extends ViewGroup.LayoutParams
            implements Parcelable {
            }
}

WindowManager繼承自ViewManager這個(gè)接口,從注釋和方法我們可以知道,這個(gè)就是用來描述可以對(duì)Activity中的子View進(jìn)行添加和移除能力的接口。

/** Interface to let you add and remove child views to an Activity. To get an instance
  * of this class, call {@link android.content.Context#getSystemService(java.lang.String) Context.getSystemService()}.
  */
public interface ViewManager
{
    public void addView(View view, ViewGroup.LayoutParams params);
        public void updateViewLayout(View view, ViewGroup.LayoutParams params);
        public void removeView(View view);
}

那么我們?cè)谑褂肳indowManager的時(shí)候,到底是在使用哪個(gè)類呢?

是WindowManagerImpl。

public final class WindowManagerImpl implements WindowManager {}

怎么知道的呢?那我們還要從Activity.attach()說起

話說,在attach()里面完成了mWindowManager的初始化

 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, IVoiceInteractor voiceInteractor) {
        
            mWindow.setWindowManager(
                (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
                mToken, mComponent.flattenToString(),
                (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
            
            mWindowManager = mWindow.getWindowManager();
        
        }
        

那我們只好看下(WindowManager)context.getSystemService(Context.WINDOW_SERVICE)是什么玩意了。

這里要說明的是,context是一個(gè)ContextImpl對(duì)象,這里先記住就好,以后再細(xì)說。

class ContextImpl extends Context {

 //靜態(tài)代碼塊,完成各種系統(tǒng)服務(wù)的注冊(cè)
 static {
    
    ......
    
     registerService(WINDOW_SERVICE, new ServiceFetcher() {
                Display mDefaultDisplay;
                public Object getService(ContextImpl ctx) {
                    Display display = ctx.mDisplay;
                    if (display == null) {
                        if (mDefaultDisplay == null) {
                            DisplayManager dm = (DisplayManager)ctx.getOuterContext().
                                    getSystemService(Context.DISPLAY_SERVICE);
                            mDefaultDisplay = dm.getDisplay(Display.DEFAULT_DISPLAY);
                        }
                        display = mDefaultDisplay;
                    }
                    //沒騙你吧
                    return new WindowManagerImpl(display);
                }});
    ......
 }

@Override
    public Object getSystemService(String name) {
        ServiceFetcher fetcher = SYSTEM_SERVICE_MAP.get(name);
        return fetcher == null ? null : fetcher.getService(this);
    }
}

要注意的是,這里返回的WindowManagerImpl對(duì)象,最終并不是和我們的Window關(guān)聯(lián)的,而且這個(gè)方法是有可能返回null的,所以在Window.setWindowManager()的時(shí)候,進(jìn)行了處理

 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);
        }
        //設(shè)置parentWindow,創(chuàng)建真正關(guān)聯(lián)的WindowManagerImpl對(duì)象
        mWindowManager = ((WindowManagerImpl)wm).createLocalWindowManager(this);
    }
    
    public final class WindowManagerImpl implements WindowManager {
        
        //最終調(diào)用的這個(gè)構(gòu)造
        private WindowManagerImpl(Display display, Window parentWindow) {
            mDisplay = display;
            mParentWindow = parentWindow;
        }

    public WindowManagerImpl createLocalWindowManager(Window parentWindow) {
            return new WindowManagerImpl(mDisplay, parentWindow);
        }
    }
 

所以說,每一個(gè)Activity都有一個(gè)PhoneWindow成員變量,并且也都有一個(gè)WindowManagerImpl,而且,PhoneWindow和WindowManagerImpl在Activity.attach()的時(shí)候進(jìn)行了關(guān)聯(lián)。

插一張類圖(轉(zhuǎn)自工匠若水

知道了這些,那下面的操作就可以直接看WindowManagerImpl了。

其實(shí)WindowManagerImpl這個(gè)類也沒有什么看頭,為啥這么說呢?因?yàn)樗鋵?shí)是代理模式中的代理。是誰的代理呢?是WindowManagerGlobal。

public final class WindowManagerImpl implements WindowManager {
    private final WindowManagerGlobal mGlobal = WindowManagerGlobal.getInstance();
    private final Display mDisplay;
    private final Window mParentWindow;

    @Override
    public void addView(View view, ViewGroup.LayoutParams params) {
        mGlobal.addView(view, params, mDisplay, mParentWindow);
    }

    @Override
    public void updateViewLayout(View view, ViewGroup.LayoutParams params) {
        mGlobal.updateViewLayout(view, params);
    }

    @Override
    public void removeView(View view) {
        mGlobal.removeView(view, false);
    }

    @Override
    public void removeViewImmediate(View view) {
        mGlobal.removeView(view, true);
    }

}

從上面的代碼中可以看出來,WindowManagerImpl里面對(duì)ViewManager接口內(nèi)方法的實(shí)現(xiàn),都是通過代理WindowManagerGlobal的方法實(shí)現(xiàn)的,所以重點(diǎn)轉(zhuǎn)移到了WindowManagerGlobal這個(gè)類。

還記得前面我們的DecorView被添加到了WindowManager嗎?

wm.addView(decor, l);

其實(shí)最終調(diào)用的是WindowManagerGlobal.addView();

 public final class WindowManagerGlobal {
 
    private static IWindowManager sWindowManagerService;
        private static IWindowSession sWindowSession;
 
    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>();
 
    //WindowManagerGlobal是單例模式
    private static WindowManagerGlobal sDefaultWindowManager;
    
    public static WindowManagerGlobal getInstance() {
        synchronized (WindowManagerGlobal.class) {
            if (sDefaultWindowManager == null) {
                sDefaultWindowManager = new WindowManagerGlobal();
            }
            return sDefaultWindowManager;
        }
        }
        
    public void addView(View view, ViewGroup.LayoutParams params,
            Display display, Window parentWindow) {
            
              final WindowManager.LayoutParams wparams = (WindowManager.LayoutParams)params;
             ......
                synchronized (mLock) {
            
                ViewRootImpl root;
             
                root = new ViewRootImpl(view.getContext(), display);
                view.setLayoutParams(wparams);
             
              mViews.add(view);
                mRoots.add(root);
                mParams.add(wparams);
            }
             ......
            
             try {
             //注意下這個(gè)方法,因?yàn)橄旅娼榻BViewRootImpl的時(shí)候會(huì)用到
                root.setView(view, wparams, panelParentView);
            }catch (RuntimeException e) {
            }
            
            }
 }

我們看到,WindowManagerGlobal是單例模式,所以在一個(gè)App里面只會(huì)有一個(gè)WindowManagerGlobal實(shí)例。在WindowManagerGlobal里面維護(hù)了三個(gè)集合,分別存放添加進(jìn)來的View(實(shí)際上就是DecorView),布局參數(shù)params,和剛剛實(shí)例化的ViewRootImpl對(duì)象,WindowManagerGlobal到底干嘛的呢?

其實(shí),WindowManagerGlobal是和WindowManagerService(即WMS)通信的。

還記得在上一篇文章中我們介紹ActivityThread和AMS之間的IBinder通信的嗎?是的,這里也是IBinder通信。


 public static IWindowSession getWindowSession() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowSession == null) {
                try {
                        InputMethodManager imm = InputMethodManager.getInstance();
                        IWindowManager windowManager = getWindowManagerService();
                        sWindowSession = windowManager.openSession(
                         
                            ......
                            
                     } catch (RemoteException e) {
                    Log.e(TAG, "Failed to open window session", e);
                }
            }
            return sWindowSession;
        }
    }

 public static IWindowManager getWindowManagerService() {
        synchronized (WindowManagerGlobal.class) {
            if (sWindowManagerService == null) {
                  //ServiceManager是用來管理系統(tǒng)服務(wù)的,比如AMS、WMS等,這里就獲取到了WMS的客戶端代理對(duì)象
                sWindowManagerService = IWindowManager.Stub.asInterface(
                        ServiceManager.getService("window"));
            }
            return sWindowManagerService;
        }
    }

首先通過上面的方法獲取到IBinder對(duì)象,然后轉(zhuǎn)化成了WMS在本地的代理對(duì)象IWindowManager,然后通過openSession()初始化了sWindowSession對(duì)象。這個(gè)對(duì)象是干什么的呢?

“Session“是會(huì)話的意思,這個(gè)類就是為了實(shí)現(xiàn)與WMS的會(huì)話的,誰和WMS的對(duì)話呢?WindowManagerGlobal類內(nèi)部并沒有用這個(gè)類呀!

是ViewRootImpl與WMS的對(duì)話。

ViewRootImpl是什么?有什么作用?ViewRootImpl如何與WMS通信

你還記得么?在前面將WindowManagerGlobal.addView()的時(shí)候,實(shí)例化了一個(gè)ViewRootImpl,然后添加到了一個(gè)集合里面,咱們先看下ViewRootImpl的構(gòu)造函數(shù)吧

public final class ViewRootImpl implements ViewParent,
        View.AttachInfo.Callbacks, HardwareRenderer.HardwareDrawCallbacks {
           
       public ViewRootImpl(Context context, Display display) { 
        
            mContext = context;
            //獲取WindowSession
            mWindowSession = WindowManagerGlobal.getWindowSession();
            mDisplay = display;
            
            ......
            
            mWindow = new W(this);
            //默認(rèn)不可見
            mViewVisibility = View.GONE;
            //這個(gè)數(shù)值就是屏幕寬度的dp總數(shù)
            mDensity = context.getResources().getDisplayMetrics().densityDpi;
            mChoreographer = Choreographer.getInstance();
            mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
        }
 
}

在這個(gè)構(gòu)造方法里面,主要是完成了各種參數(shù)的初始化,并且最關(guān)鍵的,獲取到了前面介紹的WindowSession,那么你可能好奇了,這個(gè)ViewRootImpl到底有什么作用呢?

ViewRootImpl負(fù)責(zé)管理視圖樹和與WMS交互,與WMS交互是通過WindowSession。而且ViewRootImpl也負(fù)責(zé)UI界面的布局與渲染,負(fù)責(zé)把一些事件分發(fā)至Activity,以便Activity可以截獲事件。大多數(shù)情況下,它管理Activity頂層視圖DecorView,它相當(dāng)于MVC模型中的Controller。

WindowSession是ViewRootImpl獲取之后,主動(dòng)和WMS通信的,但是我們?cè)谇懊娴奈恼轮溃蛻舳撕头?wù)器需要互相持有對(duì)方的代理引用,才能實(shí)現(xiàn)雙向通信,那么WMS是怎么得到ViewRootImpl的通信代理的呢?

是在ViewRootImpl.setView()的時(shí)候。

還記得不?在上面介紹WindowManagerGlobal.addView()的時(shí)候,我還重點(diǎn)說了下,在這個(gè)方法的try代碼塊中,調(diào)用了ViewRootImpl.setView(),下面咱們看下這個(gè)方法干嘛了:

 public void setView(View view, WindowManager.LayoutParams attrs, View panelParentView) {
        synchronized (this) {
        
             if (mView == null) {
                 mView = view;
                 int res;
                 requestLayout();
                
                    try {
                    res = mWindowSession.addToDisplay(mWindow, mSeq, mWindowAttributes,
                            getHostVisibility(), mDisplay.getDisplayId(),
                            mAttachInfo.mContentInsets, mInputChannel);
                        }catch (RemoteException e) {
                                  throw new RuntimeException("Adding window failed", e);
                        } finally {
                        
                     }   
                    }
                }
        }

為了突出重點(diǎn),我簡(jiǎn)化了很多代碼,從上面可以看出來,是mWindowSession.addToDisplay()這個(gè)方法把mWindow傳遞給我WMS,WMS就持有了當(dāng)前ViewRootlmpl的代理,就可以調(diào)用W對(duì)象讓ViewRootlmpl做一些事情了。

這樣,雙方都有了對(duì)方的接口,WMS中的Session注冊(cè)到WindowManagerGlobal的成員WindowSession中,ViewRootImpl::W注冊(cè)到WindowState中的成員mClient中。前者是為了App改變View結(jié)構(gòu)時(shí)請(qǐng)求WMS為其更新布局。后者代表了App端的一個(gè)添加到WMS中的View,每一個(gè)像這樣通過WindowManager接口中addView()添加的窗口都有一個(gè)對(duì)應(yīng)的ViewRootImpl,也有一個(gè)相應(yīng)的ViewRootImpl::W。它可以理解為是ViewRootImpl中暴露給WMS的接口,這樣WMS可以通過這個(gè)接口和App端通信。

另外源碼中很多地方采用了這種將接口隱藏為內(nèi)部類的方式,這樣可以實(shí)現(xiàn)六大設(shè)計(jì)原則之一——接口最小原則。

從什么時(shí)候開始繪制整個(gè)Activity的View樹的?

注意前面代碼中的requestLayout();因?yàn)檫@個(gè)方法執(zhí)行之后,我們的ViewRootImpl才開始繪制整個(gè)View樹!

@Override
    public void requestLayout() {
        if (!mHandlingLayoutInLayoutRequest) {
            checkThread();
            mLayoutRequested = true;
           
            scheduleTraversals();
        }
    }
void scheduleTraversals() {
        if (!mTraversalScheduled) {
            mTraversalScheduled = true;
            //暫停UI線程消息隊(duì)列對(duì)同步消息的處理
            mTraversalBarrier = mHandler.getLooper().postSyncBarrier();
            //向Choreographer注冊(cè)一個(gè)類型為CALLBACK_TRAVERSAL的回調(diào),用于處理UI繪制
            mChoreographer.postCallback(
                    Choreographer.CALLBACK_TRAVERSAL, mTraversalRunnable, null);
            if (!mUnbufferedInputDispatch) {
                scheduleConsumeBatchedInput();
            }
           notifyRendererOfFramePending();
        }
    }

“Choreographer就是一個(gè)消息處理器,根據(jù)vsync 信號(hào) 來計(jì)算frame“

解釋起來比較麻煩,我們暫時(shí)不展開討論,你只要知道,當(dāng)回調(diào)被觸發(fā)之后,mTraversalRunnable對(duì)象的run()就會(huì)被調(diào)用

 final class TraversalRunnable implements Runnable {
        @Override
        public void run() {
            doTraversal();
        }
    }

doTraversal()中最關(guān)鍵的,就是調(diào)用了performTraversals(),然后就開始mesure,layout,draw了,這里面的具體邏輯本篇文章不講,因?yàn)橹攸c(diǎn)是Activity的界面顯示流程,這一塊屬于View的,找時(shí)間單獨(dú)拿出來說

 void doTraversal() {
        if (mTraversalScheduled) {
            mTraversalScheduled = false;
            mHandler.getLooper().removeSyncBarrier(mTraversalBarrier);

            if (mProfile) {
                Debug.startMethodTracing("ViewAncestor");
            }

            Trace.traceBegin(Trace.TRACE_TAG_VIEW, "performTraversals");
            try {
                performTraversals();
            } finally {
                Trace.traceEnd(Trace.TRACE_TAG_VIEW);
            }

            if (mProfile) {
                Debug.stopMethodTracing();
                mProfile = false;
            }
        }
    }

來回倒騰了這么多,終于看見界面了,讓我哭會(huì) T^T

Window的類型有幾種?分別在什么情況下會(huì)使用到哪一種?

Window的類型是根據(jù)WindowManager.LayoutParams的type屬性相關(guān)的,根據(jù)類型可以分為三類:

  • 取值在FIRST_APPLICATION_WINDOW與LAST_APPLICATION_WINDOW之間(1-99),是常用的頂層應(yīng)用程序窗口,須將token設(shè)置成Activity的token,比如前面開啟Window的時(shí)候設(shè)置的類型即為TYPE_APPLICATION
  • 在FIRST_SUB_WINDOW和LAST_SUB_WINDOW(1000-1999)之間,與頂層窗口相關(guān)聯(lián),需將token設(shè)置成它所附著宿主窗口的token,比如PopupWindow就是TYPE_APPLICATION_PANEL
  • 取值在FIRST_SYSTEM_WINDOW和LAST_SYSTEM_WINDOW(2000-2999)之間,不能用于應(yīng)用程序,使用時(shí)需要有特殊權(quán)限,它是特定的系統(tǒng)功能才能使用,比如Toast就是TYPE_TOAST=2005,所以不需要特殊權(quán)限

下面是所有的Type說明

//WindowType:開始應(yīng)用程序窗口
        public static final int FIRST_APPLICATION_WINDOW = 1;
        //WindowType:所有程序窗口的base窗口,其他應(yīng)用程序窗口都顯示在它上面
        public static final int TYPE_BASE_APPLICATION  = 1;
        //WindowType:普通應(yīng)用程序窗口,token必須設(shè)置為Activity的token來指定窗口屬于誰
        public static final int TYPE_APPLICATION        = 2;
        //WindowType:應(yīng)用程序啟動(dòng)時(shí)所顯示的窗口,應(yīng)用自己不要使用這種類型,它被系統(tǒng)用來顯示一些信息,直到應(yīng)用程序可以開啟自己的窗口為止
        public static final int TYPE_APPLICATION_STARTING = 3;
        //WindowType:結(jié)束應(yīng)用程序窗口
        public static final int LAST_APPLICATION_WINDOW = 99;

        //WindowType:SubWindows子窗口,子窗口的Z序和坐標(biāo)空間都依賴于他們的宿主窗口
        public static final int FIRST_SUB_WINDOW        = 1000;
        //WindowType: 面板窗口,顯示于宿主窗口的上層
        public static final int TYPE_APPLICATION_PANEL  = FIRST_SUB_WINDOW;
        //WindowType:媒體窗口(例如視頻),顯示于宿主窗口下層
        public static final int TYPE_APPLICATION_MEDIA  = FIRST_SUB_WINDOW+1;
        //WindowType:應(yīng)用程序窗口的子面板,顯示于所有面板窗口的上層
        public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
        //WindowType:對(duì)話框,類似于面板窗口,繪制類似于頂層窗口,而不是宿主的子窗口
        public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
        //WindowType:媒體信息,顯示在媒體層和程序窗口之間,需要實(shí)現(xiàn)半透明效果
        public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW+4;
        //WindowType:子窗口結(jié)束
        public static final int LAST_SUB_WINDOW        = 1999;

        //WindowType:系統(tǒng)窗口,非應(yīng)用程序創(chuàng)建
        public static final int FIRST_SYSTEM_WINDOW    = 2000;
        //WindowType:狀態(tài)欄,只能有一個(gè)狀態(tài)欄,位于屏幕頂端,其他窗口都位于它下方
        public static final int TYPE_STATUS_BAR        = FIRST_SYSTEM_WINDOW;
        //WindowType:搜索欄,只能有一個(gè)搜索欄,位于屏幕上方
        public static final int TYPE_SEARCH_BAR        = FIRST_SYSTEM_WINDOW+1;
        //WindowType:電話窗口,它用于電話交互(特別是呼入),置于所有應(yīng)用程序之上,狀態(tài)欄之下
        public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
        //WindowType:系統(tǒng)提示,出現(xiàn)在應(yīng)用程序窗口之上
        public static final int TYPE_SYSTEM_ALERT      = FIRST_SYSTEM_WINDOW+3;
        //WindowType:鎖屏窗口
        public static final int TYPE_KEYGUARD          = FIRST_SYSTEM_WINDOW+4;
        //WindowType:信息窗口,用于顯示Toast
        public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
        //WindowType:系統(tǒng)頂層窗口,顯示在其他一切內(nèi)容之上,此窗口不能獲得輸入焦點(diǎn),否則影響鎖屏
        public static final int TYPE_SYSTEM_OVERLAY    = FIRST_SYSTEM_WINDOW+6;
        //WindowType:電話優(yōu)先,當(dāng)鎖屏?xí)r顯示,此窗口不能獲得輸入焦點(diǎn),否則影響鎖屏
        public static final int TYPE_PRIORITY_PHONE    = FIRST_SYSTEM_WINDOW+7;
        //WindowType:系統(tǒng)對(duì)話框
        public static final int TYPE_SYSTEM_DIALOG      = FIRST_SYSTEM_WINDOW+8;
        //WindowType:鎖屏?xí)r顯示的對(duì)話框
        public static final int TYPE_KEYGUARD_DIALOG    = FIRST_SYSTEM_WINDOW+9;
        //WindowType:系統(tǒng)內(nèi)部錯(cuò)誤提示,顯示于所有內(nèi)容之上
        public static final int TYPE_SYSTEM_ERROR      = FIRST_SYSTEM_WINDOW+10;
        //WindowType:內(nèi)部輸入法窗口,顯示于普通UI之上,應(yīng)用程序可重新布局以免被此窗口覆蓋
        public static final int TYPE_INPUT_METHOD      = FIRST_SYSTEM_WINDOW+11;
        //WindowType:內(nèi)部輸入法對(duì)話框,顯示于當(dāng)前輸入法窗口之上
        public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
        //WindowType:墻紙窗口
        public static final int TYPE_WALLPAPER          = FIRST_SYSTEM_WINDOW+13;
        //WindowType:狀態(tài)欄的滑動(dòng)面板
        public static final int TYPE_STATUS_BAR_PANEL  = FIRST_SYSTEM_WINDOW+14;
        //WindowType:安全系統(tǒng)覆蓋窗口,這些窗戶必須不帶輸入焦點(diǎn),否則會(huì)干擾鍵盤
        public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
        //WindowType:拖放偽窗口,只有一個(gè)阻力層(最多),它被放置在所有其他窗口上面
        public static final int TYPE_DRAG              = FIRST_SYSTEM_WINDOW+16;
        //WindowType:狀態(tài)欄下拉面板
        public static final int TYPE_STATUS_BAR_SUB_PANEL = FIRST_SYSTEM_WINDOW+17;
        //WindowType:鼠標(biāo)指針
        public static final int TYPE_POINTER = FIRST_SYSTEM_WINDOW+18;
        //WindowType:導(dǎo)航欄(有別于狀態(tài)欄時(shí))
        public static final int TYPE_NAVIGATION_BAR = FIRST_SYSTEM_WINDOW+19;
        //WindowType:音量級(jí)別的覆蓋對(duì)話框,顯示當(dāng)用戶更改系統(tǒng)音量大小
        public static final int TYPE_VOLUME_OVERLAY = FIRST_SYSTEM_WINDOW+20;
        //WindowType:起機(jī)進(jìn)度框,在一切之上
        public static final int TYPE_BOOT_PROGRESS = FIRST_SYSTEM_WINDOW+21;
        //WindowType:假窗,消費(fèi)導(dǎo)航欄隱藏時(shí)觸摸事件
        public static final int TYPE_HIDDEN_NAV_CONSUMER = FIRST_SYSTEM_WINDOW+22;
        //WindowType:夢(mèng)想(屏保)窗口,略高于鍵盤
        public static final int TYPE_DREAM = FIRST_SYSTEM_WINDOW+23;
        //WindowType:導(dǎo)航欄面板(不同于狀態(tài)欄的導(dǎo)航欄)
        public static final int TYPE_NAVIGATION_BAR_PANEL = FIRST_SYSTEM_WINDOW+24;
        //WindowType:universe背后真正的窗戶
        public static final int TYPE_UNIVERSE_BACKGROUND = FIRST_SYSTEM_WINDOW+25;
        //WindowType:顯示窗口覆蓋,用于模擬輔助顯示設(shè)備
        public static final int TYPE_DISPLAY_OVERLAY = FIRST_SYSTEM_WINDOW+26;
        //WindowType:放大窗口覆蓋,用于突出顯示的放大部分可訪問性放大時(shí)啟用
        public static final int TYPE_MAGNIFICATION_OVERLAY = FIRST_SYSTEM_WINDOW+27;
        //WindowType:......
        public static final int TYPE_KEYGUARD_SCRIM          = FIRST_SYSTEM_WINDOW+29;
        public static final int TYPE_PRIVATE_PRESENTATION = FIRST_SYSTEM_WINDOW+30;
        public static final int TYPE_VOICE_INTERACTION = FIRST_SYSTEM_WINDOW+31;
        public static final int TYPE_ACCESSIBILITY_OVERLAY = FIRST_SYSTEM_WINDOW+32;
        //WindowType:系統(tǒng)窗口結(jié)束
        public static final int LAST_SYSTEM_WINDOW      = 2999;


為什么使用PopWindow的時(shí)候,不設(shè)置背景就不能觸發(fā)事件?

我們?cè)谑褂肞opupWindow的時(shí)候,會(huì)發(fā)現(xiàn)如果不給PopupWindow設(shè)置背景,那么就不能觸發(fā)點(diǎn)擊返回事件,有人認(rèn)為這個(gè)是BUG,其實(shí)并不是的。

我們以下面的方法為例,其實(shí)所有的顯示方法都有下面的流程:

public void showAtLocation(IBinder token, int gravity, int x, int y) {
        if (isShowing() || mContentView == null) {
            return;
        }

        mIsShowing = true;
        mIsDropdown = false;

        WindowManager.LayoutParams p = createPopupLayout(token);
        p.windowAnimations = computeAnimationResource();
        
        //在這里會(huì)根據(jù)不同的設(shè)置,配置不同的LayoutParams屬性
        preparePopup(p);
        if (gravity == Gravity.NO_GRAVITY) {
            gravity = Gravity.TOP | Gravity.START;
        }
        p.gravity = gravity;
        p.x = x;
        p.y = y;
        if (mHeightMode < 0) p.height = mLastHeight = mHeightMode;
        if (mWidthMode < 0) p.width = mLastWidth = mWidthMode;
        invokePopup(p);
    }

我們重點(diǎn)看下preparePopup()


private void preparePopup(WindowManager.LayoutParams p) {
         //根據(jù)背景的設(shè)置情況進(jìn)行不同的配置
        if (mBackground != null) {
            final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
            int height = ViewGroup.LayoutParams.MATCH_PARENT;
           
           //如果設(shè)置了背景,就用一個(gè)PopupViewContainer對(duì)象來包裹之前的mContentView,并設(shè)置背景后
            PopupViewContainer popupViewContainer = new PopupViewContainer(mContext);
            PopupViewContainer.LayoutParams listParams = new PopupViewContainer.LayoutParams(
                    ViewGroup.LayoutParams.MATCH_PARENT, height
            );
            popupViewContainer.setBackground(mBackground);
            popupViewContainer.addView(mContentView, listParams);

            mPopupView = popupViewContainer;
        } else {
            mPopupView = mContentView;
        }
    }

為啥包了一層PopupViewContainer,就可以處理按鈕點(diǎn)擊事件了?因?yàn)镻opupWindow沒有相關(guān)事件回調(diào),也沒有重寫按鍵和觸摸方法,所以接收不到對(duì)應(yīng)的信號(hào)

public class PopupWindow {}

而PopupViewContainer則可以,因?yàn)樗貙懥讼嚓P(guān)方法

private class PopupViewContainer extends FrameLayout {

    @Override
        public boolean dispatchKeyEvent(KeyEvent event) {
            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
                if (getKeyDispatcherState() == null) {
                    return super.dispatchKeyEvent(event);
                }

                if (event.getAction() == KeyEvent.ACTION_DOWN
                        && event.getRepeatCount() == 0) {
                    KeyEvent.DispatcherState state = getKeyDispatcherState();
                    if (state != null) {
                        state.startTracking(event, this);
                    }
                    return true;
                } else if (event.getAction() == KeyEvent.ACTION_UP) {
                    //back鍵消失
                    KeyEvent.DispatcherState state = getKeyDispatcherState();
                    if (state != null && state.isTracking(event) && !event.isCanceled()) {
                        dismiss();
                        return true;
                    }
                }
                return super.dispatchKeyEvent(event);
            } else {
                return super.dispatchKeyEvent(event);
            }
        }

        @Override
        public boolean dispatchTouchEvent(MotionEvent ev) {
            if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
                return true;
            }
            return super.dispatchTouchEvent(ev);
        }

        @Override
        public boolean onTouchEvent(MotionEvent event) {
            final int x = (int) event.getX();
            final int y = (int) event.getY();
            //觸摸在外面就消失
            if ((event.getAction() == MotionEvent.ACTION_DOWN)
                    && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
                dismiss();
                return true;
            } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
                dismiss();
                return true;
            } else {
                return super.onTouchEvent(event);
            }
        }
}

在Activity中使用Dialog的時(shí)候,為什么有時(shí)候會(huì)報(bào)錯(cuò)“Unable to add window -- token is not valid; is your activity running?”?

這種情況一般發(fā)生在什么時(shí)候?一般發(fā)生在Activity進(jìn)入后臺(tái),Dialog沒有主動(dòng)Dismiss掉,然后從后臺(tái)再次進(jìn)入App的時(shí)候。

為什么會(huì)這樣呢?

還記得前面說過吧,子窗口類型的Window,比如Dialog,想要顯示的話,比如保證appToken與Activity保持一致,而當(dāng)Activity銷毀,再次回來的時(shí)候,Dialog試圖重新創(chuàng)建,調(diào)用ViewRootImp的setView()的時(shí)候就會(huì)出問題,所以記得在Activity不可見的時(shí)候,主動(dòng)Dismiss掉Dialog。

if (res < WindowManagerGlobal.ADD_OKAY) {

    switch (res) {
                        case WindowManagerGlobal.ADD_BAD_APP_TOKEN:
                        case WindowManagerGlobal.ADD_BAD_SUBWINDOW_TOKEN:
                            throw new WindowManager.BadTokenException(
                                "Unable to add window -- token " + attrs.token
                                + " is not valid; is your activity running?");
                        case WindowManagerGlobal.ADD_NOT_APP_TOKEN:
                            throw new WindowManager.BadTokenException(
                                "Unable to add window -- token " + attrs.token
                                + " is not for an application");
                        case WindowManagerGlobal.ADD_APP_EXITING:
                            throw new WindowManager.BadTokenException(
                                "Unable to add window -- app for token " + attrs.token
                                + " is exiting");
                        case WindowManagerGlobal.ADD_DUPLICATE_ADD:
                            throw new WindowManager.BadTokenException(
                                "Unable to add window -- window " + mWindow
                                + " has already been added");
                        case WindowManagerGlobal.ADD_STARTING_NOT_NEEDED:
                            // Silently ignore -- we would have just removed it
                            // right away, anyway.
                            return;
                        case WindowManagerGlobal.ADD_MULTIPLE_SINGLETON:
                            throw new WindowManager.BadTokenException(
                                "Unable to add window " + mWindow +
                                " -- another window of this type already exists");
                        case WindowManagerGlobal.ADD_PERMISSION_DENIED:
                            throw new WindowManager.BadTokenException(
                                "Unable to add window " + mWindow +
                                " -- permission denied for this window type");
                        case WindowManagerGlobal.ADD_INVALID_DISPLAY:
                            throw new WindowManager.InvalidDisplayException(
                                "Unable to add window " + mWindow +
                                " -- the specified display can not be found");
                    }
                    throw new RuntimeException(
                        "Unable to add window -- unknown error code " + res);
                }
      }

為什么Toast需要由系統(tǒng)統(tǒng)一控制,在子線程中為什么不能顯示Toast?

首先Toast也屬于窗口系統(tǒng),但是并不是屬于App的,是由系統(tǒng)同一控制的。
關(guān)于這一塊不想說太多,具體實(shí)現(xiàn)機(jī)制請(qǐng)參考后面的文章。

為了看下面的內(nèi)容,你需要知道以下幾件事情:

  1. Toast的顯示是由系統(tǒng)Toast服務(wù)控制的,與系統(tǒng)之間的通信方式是Binder
  2. 整個(gè)Toast系統(tǒng)會(huì)維持最多50個(gè)Toast的隊(duì)列,依次顯示
  3. 負(fù)責(zé)現(xiàn)實(shí)工作的是Toast的內(nèi)部類TN,它負(fù)責(zé)最終的顯示與隱藏操作
  4. 負(fù)責(zé)給系統(tǒng)Toast服務(wù)發(fā)送內(nèi)容的是INotificationManager的實(shí)現(xiàn)類,它負(fù)責(zé)在Toast.show()里面把TN對(duì)象傳遞給系統(tǒng)消息服務(wù),service.enqueueToast(pkg, tn, mDuration);這樣Toast服務(wù)就持有客戶端的代理,可以通過TN來控制每個(gè)Toast的顯示與隱藏。

再來張圖(轉(zhuǎn)自工匠若水

ok,現(xiàn)在假如你知道上面這些啦,那么我們下面就看為什么在子線程使用Toast.show()會(huì)提示

"No Looper; Looper.prepare() wasn't called on this thread."

原因很簡(jiǎn)單,因?yàn)門N在操作Toast的時(shí)候,是通過Handler做的

@Override
        public void show() {
            if (localLOGV) Log.v(TAG, "SHOW: " + this);
            mHandler.post(mShow);
        }

        @Override
        public void hide() {
            if (localLOGV) Log.v(TAG, "HIDE: " + this);
            mHandler.post(mHide);
        }

所以說,TN初始化的線程必須為主線程,在子線程中使用Handler,由于沒有消息隊(duì)列,就會(huì)造成這個(gè)問題。

結(jié)語

上面寫了這么多,你可能看了前面忘了后面,下面,凱子哥給你總結(jié)一下,這篇文章到底講了什么東西:

  • 每個(gè)Activity,都至少有一個(gè)Window,這個(gè)Window實(shí)際類型為PhoneWindow,當(dāng)Activity中有子窗口,比如Dialog的時(shí)候,就會(huì)出現(xiàn)多個(gè)Window。Activity的Window是我們控制的,狀態(tài)欄和導(dǎo)航欄的Window由系統(tǒng)控制。
  • 在DecorView的里面,一定有一個(gè)id為content的FraneLayout的布局容器,咱們自己定義的xml布局都放在這里面。
  • Activity的Window里面有一個(gè)DecorView,它使繼承自FrameLayout的一個(gè)自定義控件,作為整個(gè)View層的容器,及View樹的根節(jié)點(diǎn)。
  • Window是虛擬的概念,DecorView才是看得見,摸得著的東西,Activity.setContentView()實(shí)際調(diào)用的是PhoneWindow.setContentView(),在這里面實(shí)現(xiàn)了DecorView的初始化和id為content的FraneLayout的布局容器的初始化,并且會(huì)根據(jù)主題等配置,選擇不同的xml文件。而且在Activity.setContentView()之后,Window的一些特征位將被鎖定。
  • Activity.findViewById()實(shí)際上調(diào)用的是DecorView的findviewById(),這個(gè)方法在View中定義,但是是final的,實(shí)際起作用的是在ViewGroup中被重寫的findViewTraversal()方法。
  • Activity的mWindow成員變量是在attach()的時(shí)候被初始化的,attach()是Activity被通過反射手段實(shí)例化之后調(diào)用的第一個(gè)方法,在這之后生命周期方法才會(huì)依次調(diào)用
  • 在onResume()剛執(zhí)行之后,界面還是不可見的,只有執(zhí)行完Activity.makeVisible(),DecorView才對(duì)用戶可見
  • ViewManager這個(gè)接口里面就三個(gè)接口,添加、移除和更新,實(shí)現(xiàn)這個(gè)接口的有WindowManager和ViewGroup,但是他們兩個(gè)面向的對(duì)象是不一樣的,WindowManager實(shí)現(xiàn)的是對(duì)Window的操作,而ViewGroup則是對(duì)View的增、刪、更新操作。
  • WindowManagerImpl是WindowManager的實(shí)現(xiàn)類,但是他就是一個(gè)代理類,代理的是WindowManagerGlobal,WindowManagerGlobal一個(gè)App里面就有一個(gè),因?yàn)樗菃卫?,它里面管理了App中所有打開的DecorView,ContentView和PhoneWindow的布局參數(shù)WindowManager.LayoutParams,而且WindowManagerGlobal這個(gè)類是和WMS通信用的,是通過IWindowSession對(duì)象完成這個(gè)工作的,而IWindowSession一個(gè)App只有一個(gè),但是每個(gè)ViewRootImpl都持有對(duì)IWindowSession的引用,所以ViewRootImpl可以和WMS喊話,但是WMS怎么和ViewRootImpl喊話呢?是通過ViewRootImpl::W這個(gè)內(nèi)部類實(shí)現(xiàn)的,而且源碼中很多地方采用了這種將接口隱藏為內(nèi)部類的方式,這樣可以實(shí)現(xiàn)六大設(shè)計(jì)原則之一——接口最小原則,這樣ViewRootImpl和WMS就互相持有對(duì)方的代理,就可以互相交流了
  • ViewRootImpl這個(gè)類每個(gè)Activity都有一個(gè),它負(fù)責(zé)和WMS通信,同時(shí)相應(yīng)WMS的指揮,還負(fù)責(zé)View界面的測(cè)量、布局和繪制工作,所以當(dāng)你調(diào)用View.invalidate()和View.requestLayout()的時(shí)候,都會(huì)把事件傳遞到ViewRootImpl,然后ViewRootImpl計(jì)算出需要重繪的區(qū)域,告訴WMS,WMS再通知其他服務(wù)完成繪制和動(dòng)畫等效果,當(dāng)然,這是后話,咱們以后再說。
  • Window分為三種,子窗口,應(yīng)用窗口和系統(tǒng)窗口,子窗口必須依附于一個(gè)上下文,就是Activity,因?yàn)樗枰狝ctivity的appToken,子窗口和Activity的WindowManager是一個(gè)的,都是根據(jù)appToken獲取的,描述一個(gè)Window屬于哪種類型,是根據(jù)LayoutParam.type決定的,不同類型有不同的取值范圍,系統(tǒng)類的的Window需要特殊權(quán)限,當(dāng)然Toast比較特殊,不需要權(quán)限
  • PopupWindow使用的時(shí)候,如果想觸發(fā)按鍵和觸摸事件,需要添加一個(gè)背景,代碼中會(huì)根據(jù)是否設(shè)置背景進(jìn)行不同的邏輯判斷
  • Dialog在Activity不可見的時(shí)候,要主動(dòng)dismiss掉,否則會(huì)因?yàn)閍ppToken為空crash
  • Toast屬于系統(tǒng)窗口,由系統(tǒng)服務(wù)NotificationManagerService統(tǒng)一調(diào)度,NotificationManagerService中維持著一個(gè)集合ArrayList<ToastRecord>,最多存放50個(gè)Toast,但是NotificationManagerService只負(fù)責(zé)管理Toast,具體的現(xiàn)實(shí)工作由Toast::TN來實(shí)現(xiàn)

最后來一張Android的窗口管理框架(轉(zhuǎn)自ariesjzj

OK,關(guān)于Activity的界面顯示就說到這里吧,本篇文章大部分的內(nèi)容來自于閱讀下面參考文章之后的總結(jié)和思考,想了解更詳細(xì)的可以研究下。

下次再見,拜拜~

參考文章


尊重原創(chuàng),轉(zhuǎn)載請(qǐng)注明:From 凱子哥(<a >http://blog.csdn.net/zhaokaiqiang1992</a>) 侵權(quán)必究!

關(guān)注我的微博,可以獲得更多精彩內(nèi)容:http://weibo.com/zhaokaiqiang1992

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容