1. 程式人生 > >Android原始碼解析Window系列第(一)篇---Window的基本認識和Activity的Window建立過程

Android原始碼解析Window系列第(一)篇---Window的基本認識和Activity的Window建立過程

您可能聽說過View ,ViewManager,Window,PhoneWindow,WindowManager,WindowManagerService,可是你知道這幾個類是什麼關係,幹嘛用的。概括的來說,View是放在Window中的,Window是一個抽象類,它的具體實現是PhoneWindow,PhoneWindow還有個內部類DecorView,WindowManager是一個interface,繼承自ViewManager,它是外界訪問Window的入口,,提供了add/remove/updata的方法操作View,WindowManager與WindowManagerSerice是個跨程序的過程,WindowManagerService的職責是對系統中的所有視窗進行管理。如果您不太清楚,建議往下看,否則就不要看了。

1、Window的型別

Android系統的Window有很多種,大體上來說,Framework定義了三種視窗型別;

  • 應用程式Window
    所謂應用視窗指的就是該視窗對應一個Activity,因此,要建立應用視窗就必須在Activity中完成了。本節後面會分析Activity對應的Window的建立過程。

  • 子Window
    所謂的子Window,是說這個Window必須要有一個父窗體,比如PopWindow,Dialog。

  • 系統Window
    常見的系統Window有哪些呢?比如在手機電量低的時候,會有一個提示電量低的Window,我們輸入文字的時候,會彈出輸入法Window,還有搜尋條Window,來電顯示Window,Toast對應的Window,可以總結出來,系統Window是獨立與我們的應用程式的,對於應用程式而言,我們理論上是無法建立系統Window,因為沒有許可權,這個許可權只有系統程序有。
    這就是Framework定義了三種視窗型別,這三種類型定義在WindowManager的內部類LayoutParams中,WindowManager講這三種類型 進行了細化,把每一種型別都用一個int常量來表示,這些常量代表視窗所在的層,WindowManagerService在進行視窗疊加的時候,會按照常量的大小分配不同的層,常量值越大,代表位置越靠上面,**所以我們可以猜想一下,應用程式Window的層值常量要小於子Window的層值常量,子Window的層值常量要小於系統Window的層值常量。**Window的層級關係如下所示。

Android系統Window層級
實際上應用程式的Window的層級範圍是1~99,子Window的層級範圍是1000~1999,系統Window的層級範圍是2000~2999,這些值對應著WindowManager.LayoutParams的type引數,如果我們想視窗處在上面,那麼只要採用層級比較大的type就行了。OK,到此我們對Window有了一個初步的認識。

2、怎麼去描述一個Window

上面說了Window分為三種,用Window的type區分,在搞清楚Window的建立之前,我們需要知道怎麼去描述一個Window,我們就把Window當做一個實體類,給我的感覺,它必須要下面幾個欄位。

width:描述視窗的寬度
height:描述視窗的高度
type:這是哪一種型別的Window

實際上WindowManager.LayoutParams對Window有很詳細的定義。

public interface WindowManager extends ViewManager {
    ...........
    public static class LayoutParams extends ViewGroup.LayoutParams
            implements Parcelable {

        //視窗的起點座標
        public int x;
        public int y;

        //以下定義都是描述視窗的型別
        public int type;
        //第一個應用視窗
        public static final int FIRST_APPLICATION_WINDOW = 1;
        //所有程式視窗的base視窗,其他應用程式視窗都顯示在它上面
        public static final int TYPE_BASE_APPLICATION   = 1;
        //所有Activity的視窗
        public static final int TYPE_APPLICATION        = 2;
        //目標應用視窗未啟動之前的那個視窗
        public static final int TYPE_APPLICATION_STARTING = 3;
        //最後一個應用視窗
        public static final int LAST_APPLICATION_WINDOW = 99;

        //第一個子視窗
        public static final int FIRST_SUB_WINDOW        = 1000;
        // 面板視窗,顯示於宿主視窗的上層
        public static final int TYPE_APPLICATION_PANEL  = FIRST_SUB_WINDOW;
        // 媒體視窗(例如視訊),顯示於宿主視窗下層
        public static final int TYPE_APPLICATION_MEDIA  = FIRST_SUB_WINDOW+1;
        // 應用程式視窗的子面板,顯示於所有面板視窗的上層
        public static final int TYPE_APPLICATION_SUB_PANEL = FIRST_SUB_WINDOW+2;
        //對話方塊視窗
        public static final int TYPE_APPLICATION_ATTACHED_DIALOG = FIRST_SUB_WINDOW+3;
        //
        public static final int TYPE_APPLICATION_MEDIA_OVERLAY  = FIRST_SUB_WINDOW+4;
        //最後一個子視窗
        public static final int LAST_SUB_WINDOW         = 1999;

        //系統視窗,非應用程式建立
        public static final int FIRST_SYSTEM_WINDOW     = 2000;
        //狀態列,只能有一個狀態列,位於螢幕頂端,其他視窗都位於它下方
        public static final int TYPE_STATUS_BAR         = FIRST_SYSTEM_WINDOW;
        //搜尋欄,只能有一個搜尋欄,位於螢幕上方
        public static final int TYPE_SEARCH_BAR         = FIRST_SYSTEM_WINDOW+1;
        //電話視窗,它用於電話互動(特別是呼入),置於所有應用程式之上,狀態列之下
        public static final int TYPE_PHONE              = FIRST_SYSTEM_WINDOW+2;
        //系統警告提示視窗,出現在應用程式視窗之上
        public static final int TYPE_SYSTEM_ALERT       = FIRST_SYSTEM_WINDOW+3;
        //鎖屏視窗
        public static final int TYPE_KEYGUARD           = FIRST_SYSTEM_WINDOW+4;
        //資訊視窗,用於顯示Toast
        public static final int TYPE_TOAST              = FIRST_SYSTEM_WINDOW+5;
        //系統頂層視窗,顯示在其他一切內容之上,此視窗不能獲得輸入焦點,否則影響鎖屏
        public static final int TYPE_SYSTEM_OVERLAY     = FIRST_SYSTEM_WINDOW+6;
        //電話優先,當鎖屏時顯示,此視窗不能獲得輸入焦點,否則影響鎖屏
        public static final int TYPE_PRIORITY_PHONE     = FIRST_SYSTEM_WINDOW+7;
        //系統對話方塊視窗
        public static final int TYPE_SYSTEM_DIALOG      = FIRST_SYSTEM_WINDOW+8;
        //鎖屏時顯示的對話方塊
        public static final int TYPE_KEYGUARD_DIALOG    = FIRST_SYSTEM_WINDOW+9;
        //系統內部錯誤提示,顯示在任何視窗之上
        public static final int TYPE_SYSTEM_ERROR       = FIRST_SYSTEM_WINDOW+10;
        //內部輸入法視窗,顯示於普通UI之上,應用程式可重新佈局以免被此視窗覆蓋
        public static final int TYPE_INPUT_METHOD       = FIRST_SYSTEM_WINDOW+11;
        //內部輸入法對話方塊,顯示於當前輸入法視窗之上
        public static final int TYPE_INPUT_METHOD_DIALOG= FIRST_SYSTEM_WINDOW+12;
        //牆紙視窗
        public static final int TYPE_WALLPAPER          = FIRST_SYSTEM_WINDOW+13;
        //狀態列的滑動面板
        public static final int TYPE_STATUS_BAR_PANEL   = FIRST_SYSTEM_WINDOW+14;
        //安全系統覆蓋視窗,這些窗戶必須不帶輸入焦點,否則會干擾鍵盤
        public static final int TYPE_SECURE_SYSTEM_OVERLAY = FIRST_SYSTEM_WINDOW+15;
        //最後一個系統視窗
        public static final int LAST_SYSTEM_WINDOW      = 2999;

        ........

        //視窗特徵標記
        public int flags;
        //當該window對使用者可見的時候,允許鎖屏
        public static final int FLAG_ALLOW_LOCK_WHILE_SCREEN_ON     = 0x00000001;
        //視窗後面的所有內容都變暗
        public static final int FLAG_DIM_BEHIND        = 0x00000002;
        //Flag:視窗後面的所有內容都變模糊
        public static final int FLAG_BLUR_BEHIND        = 0x00000004;
        //視窗不能獲得焦點
        public static final int FLAG_NOT_FOCUSABLE      = 0x00000008;
        //視窗不接受觸控式螢幕事件
        public static final int FLAG_NOT_TOUCHABLE      = 0x00000010;
        //即使在該window在可獲得焦點情況下,允許該視窗之外的點選事件傳遞到當前視窗後面的的視窗去
        public static final int FLAG_NOT_TOUCH_MODAL    = 0x00000020;
        //當手機處於睡眠狀態時,如果螢幕被按下,那麼該window將第一個收到觸控事件
        public static final int FLAG_TOUCHABLE_WHEN_WAKING = 0x00000040;
        //當該window對使用者可見時,螢幕出於常亮狀態
        public static final int FLAG_KEEP_SCREEN_ON     = 0x00000080;
        //:讓window佔滿整個手機螢幕,不留任何邊界
        public static final int FLAG_LAYOUT_IN_SCREEN   = 0x00000100;
        //允許視窗超出整個手機螢幕
        public static final int FLAG_LAYOUT_NO_LIMITS   = 0x00000200;
        //window全屏顯示
        public static final int FLAG_FULLSCREEN      = 0x00000400;
        //恢復window非全屏顯示
        public static final int FLAG_FORCE_NOT_FULLSCREEN   = 0x00000800;
        //開啟視窗抖動
        public static final int FLAG_DITHER             = 0x00001000;
        //安全內容視窗,該視窗顯示時不允許截圖
        public static final int FLAG_SECURE             = 0x00002000;


        //鎖屏時顯示該視窗
        public static final int FLAG_SHOW_WHEN_LOCKED = 0x00080000;
        //系統的牆紙顯示在該視窗之後
        public static final int FLAG_SHOW_WALLPAPER = 0x00100000;
        //當window被顯示的時候,系統將把它當做一個使用者活動事件,以點亮手機螢幕
        public static final int FLAG_TURN_SCREEN_ON = 0x00200000;
        //該視窗顯示,消失鍵盤
        public static final int FLAG_DISMISS_KEYGUARD = 0x00400000;
        //當該window在可以接受觸控式螢幕情況下,讓因在該window之外,而傳送到後面的window的觸控式螢幕可以支援split touch
        public static final int FLAG_SPLIT_TOUCH = 0x00800000;
        //對該window進行硬體加速,該flag必須在Activity或Dialog的Content View之前進行設定
        public static final int FLAG_HARDWARE_ACCELERATED = 0x01000000;
        //讓window佔滿整個手機螢幕,不留任何邊界
        public static final int FLAG_LAYOUT_IN_OVERSCAN = 0x02000000;
        //透明狀態列
        public static final int FLAG_TRANSLUCENT_STATUS = 0x04000000;
        //透明導航欄
        public static final int FLAG_TRANSLUCENT_NAVIGATION = 0x08000000;


        ..........
        //軟輸入法模式
        public int softInputMode;

        //用於描述軟鍵盤顯示規則的bite的mask
        public static final int SOFT_INPUT_MASK_STATE = 0x0f;
        //沒有軟鍵盤顯示的約定規則
        public static final int SOFT_INPUT_STATE_UNSPECIFIED = 0;
        //可見性狀態softInputMode,請不要改變軟輸入區域的狀態
        public static final int SOFT_INPUT_STATE_UNCHANGED = 1;
        //使用者導航(navigate)到你的視窗時隱藏軟鍵盤
        public static final int SOFT_INPUT_STATE_HIDDEN = 2;
        //總是隱藏軟鍵盤
        public static final int SOFT_INPUT_STATE_ALWAYS_HIDDEN = 3;
        //使用者導航(navigate)到你的視窗時顯示軟鍵盤
        public static final int SOFT_INPUT_STATE_VISIBLE = 4;
        //總是顯示軟鍵盤
        public static final int SOFT_INPUT_STATE_ALWAYS_VISIBLE = 5;
        //顯示軟鍵盤時用於表示window調整方式的bite的mask
        public static final int SOFT_INPUT_MASK_ADJUST = 0xf0;
        //不指定顯示軟體盤時,window的調整方式
        public static final int SOFT_INPUT_ADJUST_UNSPECIFIED = 0x00;
        //當顯示軟鍵盤時,調整window內的控制元件大小以便顯示軟鍵盤
        public static final int SOFT_INPUT_ADJUST_RESIZE = 0x10;
        //當顯示軟鍵盤時,調整window的空白區域來顯示軟鍵盤,即使調整空白區域,軟鍵盤還是有可能遮擋一些有內容區域,這時使用者就只有退出軟鍵盤才能看到這些被遮擋區域並進行
        public static final int SOFT_INPUT_ADJUST_PAN = 0x20;
        //當顯示軟鍵盤時,不調整window的佈局
        public static final int SOFT_INPUT_ADJUST_NOTHING = 0x30;
        //使用者導航(navigate)到了你的window
        public static final int SOFT_INPUT_IS_FORWARD_NAVIGATION = 0x100;


        //視窗的對齊方式
        public int gravity;

        //期望的點陣圖格式,預設為不透明,參考android.graphics.PixelFormat
        public int format;
        //視窗所使用的動畫設定,它必須是一個系統資源而不是應用程式資源,因為視窗管理器不能訪問應用程式
        public int windowAnimations;
        //整個視窗的半透明值,1.0表示不透明,0.0表示全透明
        public float alpha = 1.0f;
        //當FLAG_DIM_BEHIND設定後生效,該變數指示後面的視窗變暗的程度,1.0表示完全不透明,0.0表示沒有變暗
        public float dimAmount = 1.0f;

        public static final float BRIGHTNESS_OVERRIDE_NONE = -1.0f;
        public static final float BRIGHTNESS_OVERRIDE_OFF = 0.0f;
        public static final float BRIGHTNESS_OVERRIDE_FULL = 1.0f;
        public float screenBrightness = BRIGHTNESS_OVERRIDE_NONE;
        //用來覆蓋使用者設定的螢幕亮度,表示應用使用者設定的螢幕亮度,從0到1調整亮度從暗到最亮發生變化
        public float buttonBrightness = BRIGHTNESS_OVERRIDE_NONE;

        public static final int ROTATION_ANIMATION_ROTATE = 0;
        public static final int ROTATION_ANIMATION_CROSSFADE = 1;
        public static final int ROTATION_ANIMATION_JUMPCUT = 2;
        //螢幕旋轉動畫
        public int rotationAnimation = ROTATION_ANIMATION_ROTATE;

        //視窗的標示符
        public IBinder token = null;
        //此視窗所在應用的包名
        public String packageName = null;
        //視窗螢幕方向
        public int screenOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED;

        //控制status bar是否可見,兩種賦值  View#STATUS_BAR_VISIBLE;View#STATUS_BAR_HIDDEN
        public int systemUiVisibility;

        ......

    }
}

提取幾個重要的引數
- width:描述視窗的寬度,該變數是父類ViewGroup.LayoutParams的成員變數。
- height:描述視窗的高度,該變數同樣是父類ViewGroup.LayoutParams的成員變數。
- x:描述視窗的起點X軸的座標。
- y:描述視窗起點Y軸的座標。
- type:視窗的型別,分為三個大型別:應用視窗,子視窗,系統視窗。
- flag:視窗特徵標記,比如是否全屏,是否隱藏標題欄等。
- gravity:視窗的對齊方式,居中還是置頂或者置底等等。

Window是一個是一個抽象的概念,千萬不要認為我們所看到的就是Window,我們平時所看到的是檢視,每一個Window都對應著一個View,View和Window通過ViewRootImpl來建立聯絡。有了View,Window的存在意義在哪裡呢,因為View不能單獨存在,它必須依附著Window,所以有檢視的地方就有Window,比如Activity,一個Dialog,一個PopWindow,一個選單,一個Toast等等。

3、Window的建立過程與顯示過程

通過上面我們知道檢視和Window的關係,那麼有一個問題,是先有檢視,還是先有Window。這個答案只有在原始碼中找了。應用程式的入口類是ActivityThread,在ActivityThread中有performLaunchActivity來啟動Activity,這個performLaunchActivity方法內部會建立一個Activity。

private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
        ...
        Activity activity = null;
        try {
          //通過反射機制建立一個Activity 
            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) {
          ...
        }

        try {
            Application app = r.packageInfo.makeApplication(false, mInstrumentation);
            if (activity != null) {
                Context appContext = createBaseContextForActivity(r, activity);
                CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
                Configuration config = new Configuration(mCompatConfiguration);
                //這個裡面建立了Window物件
                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);

               }

              activity.mCalled = false;
               if (r.isPersistable()) {
                    //呼叫Activity的onCreate方法
                   mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
               } else {
                   mInstrumentation.callActivityOnCreate(activity, r.state);
               }

            mActivities.put(r.token, r);
            ...
        }  catch (Exception e) {
         ...
        }
        return activity;
    }

如果activity不為null,就會呼叫attach,在attach方法中通過PolicyManager建立了Window物件,並且給Window設定了回撥介面。

mWindow = PolicyManager.makeNewWindow(this);
mWindow.setCallback(this);/設定回撥函式,使得Activity可以處理一些事件  

PolicyManager的實現類是Policy

public Window makeNewWindow(Context context) { 
      return new PhoneWindow(context);
 }

這樣Window就創建出來了,所以先有檢視,後有Window,檢視依賴Window存在,再說一說檢視(Activity)為Window設定的回撥介面。

    /**
     * API from a Window back to its caller.  This allows the client to
     * intercept key dispatching, panels and menus, etc.
     */
    public interface Callback {

        public boolean dispatchKeyEvent(KeyEvent event);

        public boolean dispatchKeyShortcutEvent(KeyEvent event);

        public boolean dispatchTouchEvent(MotionEvent event);

        public boolean dispatchTrackballEvent(MotionEvent event);

        public boolean dispatchGenericMotionEvent(MotionEvent event);

        public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);

        public View onCreatePanelView(int featureId);

        public boolean onCreatePanelMenu(int featureId, Menu menu);

        public boolean onPreparePanel(int featureId, View view, Menu menu);

        public boolean onMenuOpened(int featureId, Menu menu);

        public boolean onMenuItemSelected(int featureId, MenuItem item);

        public void onWindowAttributesChanged(WindowManager.LayoutParams attrs);

        public void onContentChanged();

        public void onWindowFocusChanged(boolean hasFocus);

        public void onAttachedToWindow();

        public void onDetachedFromWindow();

        public void onPanelClosed(int featureId, Menu menu);

        public boolean onSearchRequested();

        public ActionMode onWindowStartingActionMode(ActionMode.Callback callback);

        public void onActionModeStarted(ActionMode mode);

        public void onActionModeFinished(ActionMode mode);
    }

Activity實現了這個回撥介面,當Window的狀態發生變化的時候,就會回撥Activity中實現的這些介面,有些回撥介面我們還是熟悉的,dispatchTouchEvent,onAttachedToWindow,onDetachedFromWindow等。

下面分析view是如何附屬到window上的,通過上面可以看到,在attach之後就要執行callActivityOnCreate,在onCreate中我們會呼叫setContentView方法。

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

getWindow獲取了Window物件,Window的具體實現類是PhoneWindow,所以要看PhoneWindow的setContentView方法。

 public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
          //第一步,構建DecroView
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
          //第二步,將View新增到mContentParent中
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
          //第三步,回撥Activity的onContentChanged方法,通知檢視發生了改變
            cb.onContentChanged();
        }
    }

這裡涉及到一個mContentParent變數,他是一個DecorView的一部分,DecorView是PhoneWindow的一個內部類,我先介紹一下關於DecorView的知識。

DecorView

DecorView是Activity的頂級VIew,DecorView繼承自FrameLayout,在DecorView中有上下兩個部分,上面是標題欄,下面是內容欄,我們通過PhoneWindow的setContentView所設定的佈局檔案是加到內容欄(mContentParent)裡面的,View層的事件都是先經過DecorView在傳遞給我們的View的。

OK在回到setContentView的原始碼分析,我們可以得到Activity的Window建立需要三步。

- 1、 如果沒有DecorView,在installDecor中建立DecorView。

- 2、將View新增到decorview中的mContentParent中。

- 3、回撥Activity的onContentChanged介面。

先看看第一步,installDecor的原始碼

...
  if (mDecor == null) {
       mDecor = generateDecor();
        ...
  }
 ...

installDecor中呼叫了generateDecor,繼續看

 protected DecorView generateDecor() {
        return new DecorView(getContext(), -1);
    }

直接給new一個DecorView,有了DecorView之後,就可以載入具體的佈局檔案到DecorView中了,具體的佈局檔案和系統和主題有關係。

  if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);
  }
  View in = mLayoutInflater.inflate(layoutResource, null);
  decor.addView(in, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
   mContentRoot = (ViewGroup) in;

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

在看第二步,將View新增到decorview中的mContentParent中。

  mLayoutInflater.inflate(layoutResID, mContentParent);

直接將Activity檢視加到DecorView的mContentParent中,最後一步,回撥Activity的onContentChanged介面。在Activity中尋找onContentChanged方法,它是個空實現,我們可以在子Activity中處理。

public void onContentChanged() {}

到此DecorView被建立完畢,我們一開始從Thread中的handleLaunchActivity方法開始分析,首先載入Activity的位元組碼檔案,利用反射的方式建立一個Activity物件,呼叫Activity物件的attach方法,在attach方法中,建立系統需要的Window併為設定回撥,這個回撥定義在Window之中,由Activity實現,當Window的狀態發生變化的時候,就會回撥Activity實現的這些回撥方法。呼叫attach方法之後,Window被建立完成,這時候需要關聯我們的檢視,在handleLaunchActivity中的attach執行之後就要執行handleLaunchActivity中的callActivityOnCreate,在onCreate中我們會呼叫setContentView方法。通過setContentView,建立了Activity的頂級View—DecorView,DecorView的內容欄(mContentParent)用來顯示我們的佈局。這個是我們上面分析得到了一個大致流程,走到這裡,這只是新增的過程,還要有一個顯示的過程,顯示的過程就要呼叫handleLaunchActivity中的handleResumeActivity方法了。最後會呼叫makeVisible方法。

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

這裡面首先拿到WindowManager物件,用tWindowManager 的父介面ViewManager接收,ViewManager可以
最後呼叫 mDecor.setVisibility(View.VISIBLE)設定mDecor可見。到此,我們終於明白一個Activity是怎麼顯示在我們的面前了。
參考連結:
http://blog.csdn.net/feiduclear_up/article/details/49201357