网址:http://www.2cto.com/kf/201404/290996.html

 

最近在学习Android 4.4上面的WifiDisplay(Miracast)相关的模块,这里先从WifiDisplay用到的各个Service讲起,然后再从WifiDisplaySettings里面讲解打开wfd的流程。首先看下面的主要几个Service的架构图:

相关Service的启动

图中主要有以下几个模块,DisplayManagerService、MediaRouterService、WifiDisplayAdapter和WifiDisplayController。其中:

DisplayManagerService用于管理系统显示设备的生命周期,包含物理屏幕、虚拟屏幕、wifi display等,它用一组DiaplayAdapter来管理这些显示设备。

MediaRouterService用于管理各个应用程序的多媒体播放的行为。

MediaRouter用于和MediaRouterService交互一起管理多媒体的播放行为,并维护当前已经配对上的remote display设备,包括Wifi diplay、蓝牙A2DP设备、chromecast设备。

WifiDisplayAdapter是用于DisplayManagerService管理Wifi display显示的adapter。

WifiDisplayController用于控制扫描wifi display设备、连接、断开等操作。

 

先来顺着上面的架构图看各个Service的启动。首先来看DisplayManagerService,在SystemServer中先创建一个DisplayManagerService对象,然后调用systemReady方法:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public DisplayManagerService(Context context, Handler mainHandler) {
    mContext = context;
    mHeadless = SystemProperties.get(SYSTEM_HEADLESS).equals("1");
 
    mHandler = new DisplayManagerHandler(mainHandler.getLooper());
    mUiHandler = UiThread.getHandler();
    mDisplayAdapterListener = new DisplayAdapterListener();
    mSingleDisplayDemoMode = SystemProperties.getBoolean("persist.demo.singledisplay", false);
 
    mHandler.sendEmptyMessage(MSG_REGISTER_DEFAULT_DISPLAY_ADAPTER);
}
 
public void systemReady(boolean safeMode, boolean onlyCore) {
    synchronized (mSyncRoot) {
        mSafeMode = safeMode;
        mOnlyCore = onlyCore;
    }
 
    mHandler.sendEmptyMessage(MSG_REGISTER_ADDITIONAL_DISPLAY_ADAPTERS);
}

在DisplayManagerService的构造函数中,首先获取SYSTEM_HEADLESS属性,用于表明系统是否支持headless模式,默认为0。然后创建一个DisplayManagerHandler用于处理DisplayManagerService中的消息,mSigleDisplayDemoMode用于开发模式中。然后给自己发送MSG_REGISTER_DEFAULT_DISPLAY_ADAPTER,我们到DisplayManagerHandler看如何处理这个消息:

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private final class DisplayManagerHandler extends Handler {
    public DisplayManagerHandler(Looper looper) {
        super(looper, null, true /*async*/);
    }
 
    @Override
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_REGISTER_DEFAULT_DISPLAY_ADAPTER:
                registerDefaultDisplayAdapter();
                break;
 
            case MSG_REGISTER_ADDITIONAL_DISPLAY_ADAPTERS:
                registerAdditionalDisplayAdapters();
                break;


处理MSG_REGISTER_DEFAULT_DISPLAY_ADAPTER消息就是调用registerDefaultDisplayAdapter来注册一个默认的DiaplayAdapter,DisplayManagerService维护一组DiaplayAdapter,用于管理这些显示设备。默认的DiaplayAdapter就是系统的物理屏幕,通过Surface flinger来控制输出。

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void registerDefaultDisplayAdapter() {
    // Register default display adapter.
    synchronized (mSyncRoot) {
        if (mHeadless) {
            registerDisplayAdapterLocked(new HeadlessDisplayAdapter(
                    mSyncRoot, mContext, mHandler, mDisplayAdapterListener));
        } else {
            registerDisplayAdapterLocked(new LocalDisplayAdapter(
                    mSyncRoot, mContext, mHandler, mDisplayAdapterListener));
        }
    }
}
 
private void registerDisplayAdapterLocked(DisplayAdapter adapter) {
    mDisplayAdapters.add(adapter);
    adapter.registerLocked();
}


管理surface finger的知识就不讲解了。接着来看systemReady函数中会发送MSG_REGISTER_ADDITIONAL_DISPLAY_ADAPTERS,这里就会调用registerAdditionalDisplayAdapters来注册其它的显示设备:

 

 

1
2
3
4
5
6
7
8
9
private void registerAdditionalDisplayAdapters() {
    synchronized (mSyncRoot) {
        if (shouldRegisterNonEssentialDisplayAdaptersLocked()) {
            registerOverlayDisplayAdapterLocked();
            registerWifiDisplayAdapterLocked();
            registerVirtualDisplayAdapterLocked();
        }
    }
}


这里主要注册三种DisplayAdapter,一种是OverlayDiaplayAdapter用于开发模式用;一种是WifiDisplayAdapter用于wifi display,也是我们接下来要讲的;还有一种是虚拟显示。接下来只看registerWifiDisplayAdapterLocked:

 

 

1
2
3
4
5
6
7
8
9
10
private void registerWifiDisplayAdapterLocked() {
    if (mContext.getResources().getBoolean(
            com.android.internal.R.bool.config_enableWifiDisplay)
            || SystemProperties.getInt(FORCE_WIFI_DISPLAY_ENABLE, -1) == 1) {
        mWifiDisplayAdapter = new WifiDisplayAdapter(
                mSyncRoot, mContext, mHandler, mDisplayAdapterListener,
                mPersistentDataStore);
        registerDisplayAdapterLocked(mWifiDisplayAdapter);
    }
}


这里会创建WifiDisplayAdapter对象,我们到它的构造函数中去分析,并调用registerDisplayAdapterLocked添加到mDisplayAdapter中,这里会回调WifiDisplayAdapter的registerLocked方法:

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public WifiDisplayAdapter(DisplayManagerService.SyncRoot syncRoot,
        Context context, Handler handler, Listener listener,
        PersistentDataStore persistentDataStore) {
    super(syncRoot, context, handler, listener, TAG);
    mHandler = new WifiDisplayHandler(handler.getLooper());
    mPersistentDataStore = persistentDataStore;
    mSupportsProtectedBuffers = context.getResources().getBoolean(
            com.android.internal.R.bool.config_wifiDisplaySupportsProtectedBuffers);
    mNotificationManager = (NotificationManager)context.getSystemService(
            Context.NOTIFICATION_SERVICE);
}
 
public void registerLocked() {
    super.registerLocked();
 
    updateRememberedDisplaysLocked();
 
    getHandler().post(new Runnable() {
        @Override
        public void run() {
            mDisplayController = new WifiDisplayController(
                    getContext(), getHandler(), mWifiDisplayListener);
 
            getContext().registerReceiverAsUser(mBroadcastReceiver, UserHandle.ALL,
                    new IntentFilter(ACTION_DISCONNECT), null, mHandler);
        }
    });
}


PersistentDateStore用于持久性存储连过的wifi display设备,用于在WifiDisplaySettings中显示前面已经连接过的设备列表。SupportsProtectedBuffer与gralloc显示相关。在registerLocked通过updateRememberedDisplaysLocked去加载/data/system/display-manager-state.xml中保存过的列表,并记录在mRememberedDisplays中。接着实例化一个WifiDisplayController对象,同时注册对ACTION_DISCONNECT的receiver。接着到WifiDisplayController去分析,注意WifiDisplayController最后一个参数用于回调通知WifiDisplayAdapter相关状态的改变,比如wifi display打开/关闭、wifi display连接/断开等。

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
public WifiDisplayController(Context context, Handler handler, Listener listener) {
    mContext = context;
    mHandler = handler;
    mListener = listener;
 
    mWifiP2pManager = (WifiP2pManager)context.getSystemService(Context.WIFI_P2P_SERVICE);
    mWifiP2pChannel = mWifiP2pManager.initialize(context, handler.getLooper(), null);
 
    IntentFilter intentFilter = new IntentFilter();
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_PEERS_CHANGED_ACTION);
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);
    intentFilter.addAction(WifiP2pManager.WIFI_P2P_THIS_DEVICE_CHANGED_ACTION);
    context.registerReceiver(mWifiP2pReceiver, intentFilter, null, mHandler);
 
    ContentObserver settingsObserver = new ContentObserver(mHandler) {
        @Override
        public void onChange(boolean selfChange, Uri uri) {
            updateSettings();
        }
    };
 
    final ContentResolver resolver = mContext.getContentResolver();
    resolver.registerContentObserver(Settings.Global.getUriFor(
            Settings.Global.WIFI_DISPLAY_ON), false, settingsObserver);
    resolver.registerContentObserver(Settings.Global.getUriFor(
            Settings.Global.WIFI_DISPLAY_CERTIFICATION_ON), false, settingsObserver);
    resolver.registerContentObserver(Settings.Global.getUriFor(
            Settings.Global.WIFI_DISPLAY_WPS_CONFIG), false, settingsObserver);
    updateSettings();
}

 

这里主要注册WifiP2pReceiver用于接收处理WIFI_P2P_STATE_CHANGED_ACTION、WIFI_P2P_PEERS_CHANGED_ACTION、WIFI_P2P_CONNECTION_CHANGED_ACTION、WIFI_P2P_THIS_DEVICE_CHANGED_ACTION消息,然后注册ContentObserver来监控Settings.Global这个数据库里面的WIFI_DISPLAY_ON、WIFI_DISPLAY_CERTIFICATION_ON和WIFI_DISPLAY_WPS_CONFIG,这里比较重要,我们后面会看到在WifiDisplaySettings里面enable wifi display的时候,就会走到这个地方来。接着调用updateSettings来处理默认是否打开Wifi display,这里默认是关闭的,我们后面再来分析这一块。

 

接着来看MediaRouterService和MediaRouter,MediaRouter通过AIDL调用MediaRouterService的实现来完成一些工作。在SystemServer启动MediaRouterService的时候,主要创建一个MediaRouterService,然后调用它的systemRunning方法,代码如下:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public MediaRouterService(Context context) {
    mContext = context;
    Watchdog.getInstance().addMonitor(this);
}
 
public void systemRunning() {
    IntentFilter filter = new IntentFilter(Intent.ACTION_USER_SWITCHED);
    mContext.registerReceiver(new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(Intent.ACTION_USER_SWITCHED)) {
                switchUser();
            }
        }
    }, filter);
 
    switchUser();
}


上面的方法比较简单,主要就是接收ACTION_USER_SWITCHED,这是关于多用户切换的操作。MediaRouterService的工作比较少,主要都是MediaRouter通过AIDL调用完成,接下来去看MediaRouter的部分,在Android官方文档中有说明MediaRouter的调用方法:

 

A MediaRouter is retrieved through Context.getSystemService() of aContext.MEDIA_ROUTER_SERVICE. 这样系统是实例化一个MediaRouter对象并返回,下面来看它的构造函数:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public MediaRouter(Context context) {
    synchronized (Static.class) {
        if (sStatic == null) {
            final Context appContext = context.getApplicationContext();
            sStatic = new Static(appContext);
            sStatic.startMonitoringRoutes(appContext);
        }
    }
}
 
    Static(Context appContext) {
        mAppContext = appContext;
        mResources = Resources.getSystem();
        mHandler = new Handler(appContext.getMainLooper());
 
        IBinder b = ServiceManager.getService(Context.AUDIO_SERVICE);
        mAudioService = IAudioService.Stub.asInterface(b);
 
        mDisplayService = (DisplayManager) appContext.getSystemService(Context.DISPLAY_SERVICE);
 
        mMediaRouterService = IMediaRouterService.Stub.asInterface(
                ServiceManager.getService(Context.MEDIA_ROUTER_SERVICE));
 
        mSystemCategory = new RouteCategory(
                com.android.internal.R.string.default_audio_route_category_name,
                ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO, false);
        mSystemCategory.mIsSystem = true;
 
        mCanConfigureWifiDisplays = appContext.checkPermission(
                Manifest.permission.CONFIGURE_WIFI_DISPLAY,
                Process.myPid(), Process.myUid()) == PackageManager.PERMISSION_GRANTED;
    }


MediaRouter中主要通过Static对象来实现其大多数的方法,Static就是一个单例模式,先看Static的构造函数,也可以通过上面的图看到,MediaRouter包含DisplayManager对象和MediaRouterService的BpBinder引用,MediaRouter还持有AudioService的BpBind,用于控制audio数据的输出设备,例如可以用于蓝牙A2DP中使用。接着看Static的startMonitoringRoutes方法:

 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
void startMonitoringRoutes(Context appContext) {
    mDefaultAudioVideo = new RouteInfo(mSystemCategory);
    mDefaultAudioVideo.mNameResId = com.android.internal.R.string.default_audio_route_name;
    mDefaultAudioVideo.mSupportedTypes = ROUTE_TYPE_LIVE_AUDIO | ROUTE_TYPE_LIVE_VIDEO;
    mDefaultAudioVideo.updatePresentationDisplay();
    addRouteStatic(mDefaultAudioVideo);
 
    // This will select the active wifi display route if there is one.
    updateWifiDisplayStatus(mDisplayService.getWifiDisplayStatus());
 
    appContext.registerReceiver(new WifiDisplayStatusChangedReceiver(),
            new IntentFilter(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED));
    appContext.registerReceiver(new VolumeChangeReceiver(),
            new IntentFilter(AudioManager.VOLUME_CHANGED_ACTION));
 
    mDisplayService.registerDisplayListener(this, mHandler);
 
 
    // Bind to the media router service.
    rebindAsUser(UserHandle.myUserId());
 
    // Select the default route if the above didn\'t sync us up
    // appropriately with relevant system state.
    if (mSelectedRoute == null) {
        selectDefaultRouteStatic();
    }
}

 

首先注册系统中默认的AudioVideo输出设备,如果有处于活动状态的wifi display连接,就记录下当前处于活动连接的设备,默认为空。上面会注册两个broadcastReceiver,一个用于接收ACTION_WIFI_DISPLAY_STATUS_CHANGED,另一个接收VOLUME_CHANGED_ACTION,我们主要看前面接收ACTION_WIFI_DISPLAY_STATUS_CHANGED的receiver,如下:

 

1
2
3
4
5
6
7
8
static class WifiDisplayStatusChangedReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getAction().equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) {
            updateWifiDisplayStatus((WifiDisplayStatus) intent.getParcelableExtra(
                    DisplayManager.EXTRA_WIFI_DISPLAY_STATUS));
        }
    }

上面接收ACTION_WIFI_DISPLAY_STATUS_CHANGED,从Intent里面取出WifiDisplayStatus对象,WifiDisplayStatus内部的变量如下:

 

 

mFeatureState 表明现在wifi display是关闭还是打开状态
mScanState 表现现在wifi display是否在scanning状态
mActiveDisplayState 表明现在wifi display是在连接还是无连接状态
mActiveDisplay 处于正在连接或者连接中的WifiDisplay对象
mDisplays 扫描到的WifiDisplay对象数组
mSessionInfo 用于过Miracast认证时用

 

 

然后向DisplayManager注册一个回调函数,当有显示设备增加、删除或者改变的时候,就会有相应的回调函数来通知Static对象。接着绑定MediaRouterService:

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void rebindAsUser(int userId) {
    if (mCurrentUserId != userId || userId < 0 || mClient == null) {
 
        mCurrentUserId = userId;
 
        try {
            Client client = new Client();
            mMediaRouterService.registerClientAsUser(client,
                    mAppContext.getPackageName(), userId);
            mClient = client;
        } catch (RemoteException ex) {
            Log.e(TAG, "Unable to register media router client.", ex);
        }
 
        publishClientDiscoveryRequest();
        publishClientSelectedRoute(false);
        updateClientState();
    }
}

 

 

Enable WifiDisplay

当用户进入WifiDisplaySettings界面,会调用其对应的onCreate和onStart方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public void onCreate(Bundle icicle) {
    super.onCreate(icicle);
 
    final Context context = getActivity();
    mRouter = (MediaRouter)context.getSystemService(Context.MEDIA_ROUTER_SERVICE);
    mDisplayManager = (DisplayManager)context.getSystemService(Context.DISPLAY_SERVICE);
    mWifiP2pManager = (WifiP2pManager)context.getSystemService(Context.WIFI_P2P_SERVICE);
    mWifiP2pChannel = mWifiP2pManager.initialize(context, Looper.getMainLooper(), null);
 
    addPreferencesFromResource(R.xml.wifi_display_settings);
    setHasOptionsMenu(true);
}
 
public void onStart() {
    super.onStart();
    mStarted = true;
 
    final Context context = getActivity();
    IntentFilter filter = new IntentFilter();
    filter.addAction(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED);
    context.registerReceiver(mReceiver, filter);
 
    getContentResolver().registerContentObserver(Settings.Global.getUriFor(
            Settings.Global.WIFI_DISPLAY_ON), false, mSettingsObserver);
    getContentResolver().registerContentObserver(Settings.Global.getUriFor(
            Settings.Global.WIFI_DISPLAY_CERTIFICATION_ON), false, mSettingsObserver);
    getContentResolver().registerContentObserver(Settings.Global.getUriFor(
            Settings.Global.WIFI_DISPLAY_WPS_CONFIG), false, mSettingsObserver);
 
    mRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mRouterCallback,
            MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN);
 
    update(CHANGE_ALL);
}


首先注册对ACTION_WIFI_DISPLAY_STATUS_CHANGED的receiver,这个broadcast会在WifiDisplayAdapter里面当wifi display的状态发送改变时发送,包括扫描到新的设备、开始连接、连接成功、断开等消息都会被这个receiver接收到,后面我们会来分析这个receiver干了什么,然后在onStart中想MediaRouter对象注册一个callback函数,用于获取系统中remote display的相关回调信息。然后类似WifiDisplayController一样,注册一些对数据库改变的ContentObserver。接着来看MediaRouter.addCallback的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
public void addCallback(int types, Callback cb, int flags) {
    CallbackInfo info;
    int index = findCallbackInfo(cb);
    if (index >= 0) {
        info = sStatic.mCallbacks.get(index);
        info.type |= types;
        info.flags |= flags;
    } else {
        info = new CallbackInfo(cb, types, flags, this);
        sStatic.mCallbacks.add(info);
    }
    sStatic.updateDiscoveryRequest();
}


Static的mCallbacks是一个CopyOnWriteArrayList数组,记录所有注册到MediaRouter中的回调函数。如果已经向MediaRouter注册过这个callback,则更新相关的type和flag;如果没有注册,则新建一个CallbackInfo对象并添加到mCallbacks数组中。然后调用Static的updateDiscoveryRequest去更新是否需要发送Discovery request请求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
void updateDiscoveryRequest() {
            final int count = mCallbacks.size();
            for (int i = 0; i < count; i++) {
                CallbackInfo cbi = mCallbacks.get(i);
                if ((cbi.flags & (CALLBACK_FLAG_PERFORM_ACTIVE_SCAN
                        | CALLBACK_FLAG_REQUEST_DISCOVERY)) != 0) {
                    // Discovery explicitly requested.
                    routeTypes |= cbi.type;
                } else if ((cbi.flags & CALLBACK_FLAG_PASSIVE_DISCOVERY) != 0) {
                    // Discovery only passively requested.
                    passiveRouteTypes |= cbi.type;
                } else {
                    // Legacy case since applications don\'t specify the discovery flag.
                    // Unfortunately we just have to assume they always need discovery
                    // whenever they have a callback registered.
                    routeTypes |= cbi.type;
                }
                if ((cbi.flags & CALLBACK_FLAG_PERFORM_ACTIVE_SCAN) != 0) {
                    activeScan = true;
                    if ((cbi.type & ROUTE_TYPE_REMOTE_DISPLAY) != 0) {
                        activeScanWifiDisplay = true;
                    }
                }
            }
            if (routeTypes != 0 || activeScan) {
                // If someone else requests discovery then enable the passive listeners.
                // This is used by the MediaRouteButton and MediaRouteActionProvider since
                // they don\'t receive lifecycle callbacks from the Activity.
                routeTypes |= passiveRouteTypes;
            }
 
            // Update wifi display scanning.
            // TODO: All of this should be managed by the media router service.
            if (mCanConfigureWifiDisplays) {
                if (mSelectedRoute != null
                        && mSelectedRoute.matchesTypes(ROUTE_TYPE_REMOTE_DISPLAY)) {
                    // Don\'t scan while already connected to a remote display since
                    // it may interfere with the ongoing transmission.
                    activeScanWifiDisplay = false;
                }
                if (activeScanWifiDisplay) {
                    if (!mActivelyScanningWifiDisplays) {
                        mActivelyScanningWifiDisplays = true;
                        mDisplayService.startWifiDisplayScan();
                    }
                } else {
                    if (mActivelyScanningWifiDisplays) {
                        mActivelyScanningWifiDisplays = false;
                        mDisplayService.stopWifiDisplayScan();
                    }
                }
            }
        }


这个函数体比较长,主要通过注册的一系列的callback类型来决定是否要进行wifiDisplay scan的动作,根据在WifiDisplaySettings里面注册callback的方法: mRouter.addCallback(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY, mRouterCallback,
MediaRouter.CALLBACK_FLAG_PERFORM_ACTIVE_SCAN),上面函数中的activeScanWifiDisplay会为true,接着会调用DisplayManagerService中的startWifiDisplayScan,如下图。 这里会通过WifiDisplayAdapter调用到WifiDisplayController的updateScanState动作,我们到updateScanState中去分析:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void updateScanState() {
    if (mScanRequested && mWfdEnabled && mDesiredDevice == null) {
        if (!mDiscoverPeersInProgress) {
            Slog.i(TAG, "Starting Wifi display scan.");
            mDiscoverPeersInProgress = true;
            handleScanStarted();
            tryDiscoverPeers();
        }
    } else {
        if (mDiscoverPeersInProgress) {
            // Cancel automatic retry right away.
            mHandler.removeCallbacks(mDiscoverPeers);
 
            if (mDesiredDevice == null || mDesiredDevice == mConnectedDevice) {
                Slog.i(TAG, "Stopping Wifi display scan.");
                mDiscoverPeersInProgress = false;
                stopPeerDiscovery();
                handleScanFinished();
            }
        }
    }
}


当初次进入到WifiDisplaySettings中,并没有去optionMenu中enable wifi display时,上面code中的mWfdEnabled为false,所以会跳出前面的if语句;后面的else语句中mDiscoverPeersInProgress也为false,因为这个变量只有在scan时才会被置为true。 
接着来分析当用户点击了optionMenu中enable wifi display后的流程,先看WifiDisplaySettings的代码:

1
2
3
4
5
6
7
public boolean onOptionsItemSelected(MenuItem item) {
    switch (item.getItemId()) {
        case MENU_ID_ENABLE_WIFI_DISPLAY:
            mWifiDisplayOnSetting = !item.isChecked();
            item.setChecked(mWifiDisplayOnSetting);
            Settings.Global.putInt(getContentResolver(),
                    Settings.Global.WIFI_DISPLAY_ON, mWifiDisplayOnSetting ? 1 : 0);


这里首先改变OptionMenu的状态,并置mWifiDisplayOnSetting为上次MenuItem相反的状态,然后改变Settings.Global数据库中WIFI_DISPLAY_ON的指为1。前面我们介绍过,在WifiDisplaySettings和WifiDisplayController都有注册ContentObserver来监控这个值的变化。其中WifiDisplaySettings在监控到这个值的变化后,主要是调用MediaRouter和DisplayManager的方法去获取系统中已经扫描到的remote display设备,并更新到listview列表上,显然这时候还没有开始scan,所以listview列表为空。接着看WifiDisplayController处理ContentOberver的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void updateSettings() {
    final ContentResolver resolver = mContext.getContentResolver();
    mWifiDisplayOnSetting = Settings.Global.getInt(resolver,
            Settings.Global.WIFI_DISPLAY_ON, 0) != 0;
    mWifiDisplayCertMode = Settings.Global.getInt(resolver,
            Settings.Global.WIFI_DISPLAY_CERTIFICATION_ON, 0) != 0;
 
    mWifiDisplayWpsConfig = WpsInfo.INVALID;
    if (mWifiDisplayCertMode) {
        mWifiDisplayWpsConfig = Settings.Global.getInt(resolver,
              Settings.Global.WIFI_DISPLAY_WPS_CONFIG, WpsInfo.INVALID);
    }
 
    updateWfdEnableState();
}


这里主要置mWifiDisplayOnSetting为true,然后就调用updateWfdEnableState去更新wfd的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
private void updateWfdEnableState() {
    if (mWifiDisplayOnSetting && mWifiP2pEnabled) {
        // WFD should be enabled.
        if (!mWfdEnabled && !mWfdEnabling) {
            mWfdEnabling = true;
 
            WifiP2pWfdInfo wfdInfo = new WifiP2pWfdInfo();
            wfdInfo.setWfdEnabled(true);
            wfdInfo.setDeviceType(WifiP2pWfdInfo.WFD_SOURCE);
            wfdInfo.setSessionAvailable(true);
            wfdInfo.setControlPort(DEFAULT_CONTROL_PORT);
            wfdInfo.setMaxThroughput(MAX_THROUGHPUT);
            mWifiP2pManager.setWFDInfo(mWifiP2pChannel, wfdInfo, new ActionListener() {
                @Override
                public void onSuccess() {
                    if (DEBUG) {
                        Slog.d(TAG, "Successfully set WFD info.");
                    }
                    if (mWfdEnabling) {
                        mWfdEnabling = false;
                        mWfdEnabled = true;
                        reportFeatureState();
                        updateScanState();
                    }
                }
 
                @Override
                public void onFailure(int reason) {
                    if (DEBUG) {
                        Slog.d(TAG, "Failed to set WFD info with reason " + reason + ".");
                    }
                    mWfdEnabling = false;
                }
            });
        }


首先调用WifiP2pMananger的setWFDInfo把与wifi display相关的信息设置到wpa_supplicant,这些信息包括enable状态、device type(指为source还是sink)、session available(当前可否连接)、control port(用于rtsp连接)、maxThroughput(吞吐量),这些信息最终会随着P2P的IE信息在扫描阶段被对方知道。接着会调用reportFeatureState来通知WifiDisplayAdapter相应状态的变化,这里我们先看一下下面的流程图来了解一下WifiDisplaySettings、MediaRouter、DisplayMananger、WifiDisplayAdapter、WifiDisplayController是如何相互通知信息的,这其中有简单的callback,也有发送/接收broadcast,如下图: 

通过上面的图我们可以看到实线部分是调用关系,虚线部分是回调关系。接着我们来看reportFeatureState的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
private void reportFeatureState() {
    final int featureState = computeFeatureState();
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            mListener.onFeatureStateChanged(featureState);
        }
    });
}
 
private int computeFeatureState() {
    if (!mWifiP2pEnabled) {
        return WifiDisplayStatus.FEATURE_STATE_DISABLED;
    }
    return mWifiDisplayOnSetting ? WifiDisplayStatus.FEATURE_STATE_ON :
            WifiDisplayStatus.FEATURE_STATE_OFF;
}


直接回调WifiDisplayListener的onFeatureStateChanged,从上面的图我们可以看着WifiDisplayListener会由WifiDisplayAdapter注册的,去看这部分的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
    public void onFeatureStateChanged(int featureState) {
        synchronized (getSyncRoot()) {
            if (mFeatureState != featureState) {
                mFeatureState = featureState;
                scheduleStatusChangedBroadcastLocked();
            }
        }
    }
 
private void scheduleStatusChangedBroadcastLocked() {
    mCurrentStatus = null;
    if (!mPendingStatusChangeBroadcast) {
        mPendingStatusChangeBroadcast = true;
        mHandler.sendEmptyMessage(MSG_SEND_STATUS_CHANGE_BROADCAST);
    }
}


这里最后通过WifiDisplayHandler的sendEmptyMessage的方法实现,目的是不要卡住了WifiDisplayController后面代码的执行,来看WifiDisplayHandler如何处理MSG_SEND_STATUS_CHANGE_BROADCAST:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    public void handleMessage(Message msg) {
        switch (msg.what) {
            case MSG_SEND_STATUS_CHANGE_BROADCAST:
                handleSendStatusChangeBroadcast();
                break;
 
            case MSG_UPDATE_NOTIFICATION:
                handleUpdateNotification();
                break;
        }
 
private void handleSendStatusChangeBroadcast() {
    final Intent intent;
    synchronized (getSyncRoot()) {
        if (!mPendingStatusChangeBroadcast) {
            return;
        }
 
        mPendingStatusChangeBroadcast = false;
        intent = new Intent(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED);
        intent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
        intent.putExtra(DisplayManager.EXTRA_WIFI_DISPLAY_STATUS,
                getWifiDisplayStatusLocked());
    }
 
    // Send protected broadcast about wifi display status to registered receivers.
    getContext().sendBroadcastAsUser(intent, UserHandle.ALL);
}


上面的代码都比较简单,在getWifiDisplayStatusLocked中会根据WifiDisplayAdapter中的变量mFeatureState、mScanState、mActiveDisplayState、mActiveDisplay、mDisplays、mSessionInfo去构造一个WifiDisplayStatus对象,在前面我们介绍过这几个变量的含义了,当然这几个变量会从WifiDisplayListener的各个callback分别去改变自己的值。接着我们到MediaRouter中去看如何处理这个broadcastReceiver,前面我们已经讲过了,WifiDisplayStatusChangedReceiver会接收这个broadcast,然后调用updateWifiDisplayStatus来更新状态,我们稍后来看这部分的实现。回到WifiDisplayController的updateWfdEnableState方法中,接着会调用updateScanState方法开始扫描WifiDisplay设备:

1
2
3
4
5
6
7
8
9
private void updateScanState() {
    if (mScanRequested && mWfdEnabled && mDesiredDevice == null) {
        if (!mDiscoverPeersInProgress) {
            Slog.i(TAG, "Starting Wifi display scan.");
            mDiscoverPeersInProgress = true;
            handleScanStarted();
            tryDiscoverPeers();
        }
    }


handleScanStarted用于通知WifiDisplayAdapter扫描开始了,当然WifiDisplayAdapter也会发broadcast给MediaRouter。接着会调用tryDiscoverPeers:

1
2
3
4
5
6
7
8
9
10
11
12
13
private void tryDiscoverPeers() {
    mWifiP2pManager.discoverPeers(mWifiP2pChannel, new ActionListener() {
        @Override
        public void onSuccess() {
            if (DEBUG) {
                Slog.d(TAG, "Discover peers succeeded.  Requesting peers now.");
            }
            if (mDiscoverPeersInProgress) {
                requestPeers();
            }
        }
    mHandler.postDelayed(mDiscoverPeers, DISCOVER_PEERS_INTERVAL_MILLIS);
}


这里调用WifiP2pManager的discoverPeers去扫描所有的p2p设备,比较重要是后面有发一个delay message,表示每间隔10秒就去发一下P2P_FIND。当然下了P2P_FIND命令后,并不能马上获取到对方设备,但因为我们前面有讲过在/data/system/display-manager-state.xml有保存过前面连接过的设备列表,所以这里会马上调用requestPeers去获取设备列表。当然在WifiDisplayController也会注册对WIFI_P2P_PEERS_CHANGED_ACTION的receiver,最终还是会调用reqeustPeers去获取所有扫描到的设备列表,下面来看这个函数的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
private void requestPeers() {
    mWifiP2pManager.requestPeers(mWifiP2pChannel, new PeerListListener() {
        @Override
        public void onPeersAvailable(WifiP2pDeviceList peers) {
            if (DEBUG) {
                Slog.d(TAG, "Received list of peers.");
            }
 
            mAvailableWifiDisplayPeers.clear();
            for (WifiP2pDevice device : peers.getDeviceList()) {
                if (DEBUG) {
                    Slog.d(TAG, "  " + describeWifiP2pDevice(device));
                }
 
                if (isWifiDisplay(device)) {
                    mAvailableWifiDisplayPeers.add(device);
                }
            }
 
            if (mDiscoverPeersInProgress) {
                handleScanResults();
            }
        }
    });
}


首先从扫描的设备列表中过滤掉不能做wifi display的设备,主要从三个方面过滤,一是纯粹的P2P设备,不会待用WfdInfo;第二是带有WfdInfo,但是暂时没有被enable;三是只能是PrimarySinkDevice,看起来Android还不支持SecondSink。并将过滤掉剩下的设备加入到mAvailableWifiDisplayPeers列表中,接着调用handleScanResults来组装WifiDisplay列表数组并notify给WifiDisplayAdapter:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private void handleScanResults() {
    final int count = mAvailableWifiDisplayPeers.size();
    final WifiDisplay[] displays = WifiDisplay.CREATOR.newArray(count);
    for (int i = 0; i < count; i++) {
        WifiP2pDevice device = mAvailableWifiDisplayPeers.get(i);
        displays[i] = createWifiDisplay(device);
        updateDesiredDevice(device);
    }
 
    mHandler.post(new Runnable() {
        @Override
        public void run() {
            mListener.onScanResults(displays);
        }
    });
}


这里首先根据mAvailableWifiDisplayPeers的数目创建一个WifiDisplay数组,然后一个个构造WifiDisplay对象,WifiDiplay对象包含以下几个变量:

mDeviceAddress 设备的Mac地址
mDeviceName 设备的名字
mDeviceAlias 设备的别名,一般为NULL
mIsAvailable 是否可用状态
mCanConnect WfdInfo中的SessionAvailable是否为1
mIsRemembered 是否被记录的


接着调用updateDesiredDevice用于判断扫描到的这个设备是否是现在正在连接或者连接上的设备,如果是,则更新它的一些信息,以后在连接Wifi display的时候再来分析这一块。接着就会向WifiDisplayAdapter回调onScanResults,回调函数中带有已经扫描到的wifi display设备列表(如果有):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
        public void onScanResults(WifiDisplay[] availableDisplays) {
            synchronized (getSyncRoot()) {
                availableDisplays = mPersistentDataStore.applyWifiDisplayAliases(
                        availableDisplays);
 
                boolean changed = !Arrays.equals(mAvailableDisplays, availableDisplays);
 
                // Check whether any of the available displays changed canConnect status.
                for (int i = 0; !changed && i<br>
这里首先调用PersistentDateStore的applyWifiDisplayAliases方法去判断扫描到的设备中有没有以前连接过并记录下来的wifi display设备,比较方法是比较两者的MAC地址,如果在PersistentDateStore中找到,再比较两者的别名(Alias),如果不相同则更新results列表,细节的代码可以看applyWifiDisplayAlias中的实现。
<pre class="brush:java;">    public WifiDisplay[] applyWifiDisplayAliases(WifiDisplay[] displays) {
        WifiDisplay[] results = displays;
        if (results != null) {
            int count = displays.length;
            for (int i = 0; i < count; i++) {
                WifiDisplay result = applyWifiDisplayAlias(displays[i]);
                if (result != displays[i]) {
                    if (results == displays) {
                        results = new WifiDisplay[count];
                        System.arraycopy(displays, 0, results, 0, count);
                    }
                    results[i] = result;
                }
            }
        }
        return results;
    }</pre><br>
回到上面的onScanResults中,接着判断刚扫描到的设备列表(availableDisplays)和之前存储的设备列表(mAvailableDisplays)之间有没有变化,可以数组内容以及是否可连两个方面检查。如果有变化,则把刚扫描到的设备列表(availableDisplays)赋值给存储的设备列表(mAvailableDisplays)。接下来调用fixRememberedDisplayNamesFromAvailableDisplaysLocked来更新PersistentDateStore中存储的已经连接过的wifi
 display设备,更新的条件是设备的MAC地址一样,但设备的DeviceName和DeviceAlias有变化,这是就要更新到PersistentDateStore中,代码如下:
<pre class="brush:java;">    private void fixRememberedDisplayNamesFromAvailableDisplaysLocked() {
        boolean changed = false;
        for (int i = 0; i < mRememberedDisplays.length; i++) {
            WifiDisplay rememberedDisplay = mRememberedDisplays[i];
            WifiDisplay availableDisplay = findAvailableDisplayLocked(
                    rememberedDisplay.getDeviceAddress());
            if (availableDisplay != null && !rememberedDisplay.equals(availableDisplay)) {
                mRememberedDisplays[i] = availableDisplay;
                changed |= mPersistentDataStore.rememberWifiDisplay(availableDisplay);
            }
        }
        if (changed) {
            mPersistentDataStore.saveIfNeeded();
        }
    }</pre>如果扫描到的设备列表中有wifi display设备的名字或者别名发生了变化,就会调用到PersistentDataStore.saveIfNeeded方法把数据写到/data/system/display-manager-state.xml中。
<br>
 
回到onScanResults中,接下来会调用updateDisplaysLocked来更新返回给MediaRouter的设备列表信息,在这里会把扫描到的设备以及之前存储下来的设备做一次合并,共同保存到mDisplays数组中,后面在发送broadcast的时候,就会把mDisplays保存到WifiDisplayStatus对象中,并在broadcast带上这个对象。
<pre class="brush:java;">    private void updateDisplaysLocked() {
        List<wifidisplay> displays = new ArrayList<wifidisplay>(
                mAvailableDisplays.length + mRememberedDisplays.length);
        boolean[] remembered = new boolean[mAvailableDisplays.length];
        for (WifiDisplay d : mRememberedDisplays) {
            boolean available = false;
            for (int i = 0; i < mAvailableDisplays.length; i++) {
                if (d.equals(mAvailableDisplays[i])) {
                    remembered[i] = available = true;
                    break;
                }
            }
            if (!available) {
                displays.add(new WifiDisplay(d.getDeviceAddress(), d.getDeviceName(),
                        d.getDeviceAlias(), false, false, true));
            }
        }
        for (int i = 0; i < mAvailableDisplays.length; i++) {
            WifiDisplay d = mAvailableDisplays[i];
            displays.add(new WifiDisplay(d.getDeviceAddress(), d.getDeviceName(),
                    d.getDeviceAlias(), true, d.canConnect(), remembered[i]));
        }
        mDisplays = displays.toArray(WifiDisplay.EMPTY_ARRAY);
    }</wifidisplay></wifidisplay></pre><br>
上面的实现中先从mRememberedDisplays逐个添加wifi display设备到displays数组中,如果在mAvailableDisplays有相同的设备,则不添加到displays数组;后面再把mAvailableDisplays所有元素添加到displays数组,并全部赋值给mDisplays数组。
<br>
 
再回到onScanResults中,就会调用scheduleStatusChangedBroadcastLocked向WifiDisplayHandler发送MSG_SEND_STATUS_CHANGE_BROADCAST消息,这个我们在前面已经讲过了,然后会发送broadcast,并带上一个WifiDisplayStatus对象。现在我们再到MediaRouter和WifiDisplaySettings中看如何处理这个broadcast,先来看MediaRouter如何解析WifiDisplayStatus对象。updateWifiDisplayStatus的实现如下:<br>
<pre class="brush:java;">    static void updateWifiDisplayStatus(WifiDisplayStatus status) {
        WifiDisplay[] displays;
        WifiDisplay activeDisplay;
        if (status.getFeatureState() == WifiDisplayStatus.FEATURE_STATE_ON) {
            displays = status.getDisplays();
            activeDisplay = status.getActiveDisplay();
        } else {
            displays = WifiDisplay.EMPTY_ARRAY;
            activeDisplay = null;
        }
        String activeDisplayAddress = activeDisplay != null ?
                activeDisplay.getDeviceAddress() : null;
 
        // Add or update routes.
        for (int i = 0; i < displays.length; i++) {
            final WifiDisplay d = displays[i];
            if (shouldShowWifiDisplay(d, activeDisplay)) {
                RouteInfo route = findWifiDisplayRoute(d);
                if (route == null) {
                    route = makeWifiDisplayRoute(d, status);
                    addRouteStatic(route);
                } else {
                    String address = d.getDeviceAddress();
                    boolean disconnected = !address.equals(activeDisplayAddress)
                            && address.equals(sStatic.mPreviousActiveWifiDisplayAddress);
                    updateWifiDisplayRoute(route, d, status, disconnected);
                }
                if (d.equals(activeDisplay)) {
                    selectRouteStatic(route.getSupportedTypes(), route, false);
                }
            }
        }
 
        // Remove stale routes.
        for (int i = sStatic.mRoutes.size(); i-- > 0; ) {
            RouteInfo route = sStatic.mRoutes.get(i);
            if (route.mDeviceAddress != null) {
                WifiDisplay d = findWifiDisplay(displays, route.mDeviceAddress);
                if (d == null || !shouldShowWifiDisplay(d, activeDisplay)) {
                    removeRouteStatic(route);
                }
            }
        }
 
        sStatic.mPreviousActiveWifiDisplayAddress = activeDisplayAddress;
    }</pre>
<br>
 
上面的代码中,首先从WifiDisplayStatus取出已经扫描到的WifiDisplay设备数组和当前处于连接状态的WifiDisplay设备,然后shouldShowWifiDisplay用于过滤是否将这个wifi display设备加入到mRoutes数组中,判断条件是这个设备已经连过并且有保存在PersistentDateStore或者这个设备就是当前正在连接中的设备,对于其它的设备并没有加入到mRoutes中,这里就有个疑问了,其它没连过的设备将在哪里加入呢? 我们后面分析WifiDisplaySettings再来看这部分。如果在mRoutes没有找到相同的wifi
 display设备,就会把这个设备加入到mRoutes中,并通知WifiDisplaySettings相应的变化;如果在mRoutes存在相同的wifi display设备,则检查它的名字或者状态(available、canConnect)有没有变化,如果有变化,则通知WifiDisplaySettings相应的改变。selectRouteStatic用于更新是否默认的router并dispatch相应的回调消息。最后会从mRoutes踢出有错误的wifi display设备。
<br>
 
我的一些简单理解:MediaRouter只保存已经配对上的remote display设备,包括Wifi diplay、蓝牙A2DP设备、chromecast设备等,用于提供给其它应用程序使用,比如youtube可以直接chromecast,当我们前面有成功和一个chromecast设备配对过后,youtube应用就可以从MediaRouter对象中获取到当前已经配对的chromecast设备信息,并可以把youtube的视频推送到chromecast上面播放;再举个例子,百度视频应用可以访问MediaRouter中的wifi
 display设备,当我们设备中有已经连接或已经保存的wifi display设备时,就可以很方便的从直接百度视频上面直接开始wifi display,而不需要用户再去Settings里面扫描连接。
<br>
 
再来看WifiDisplaySettings中如何处理MSG_SEND_STATUS_CHANGE_BROADCAST:
<pre class="brush:java;">    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            String action = intent.getAction();
            if (action.equals(DisplayManager.ACTION_WIFI_DISPLAY_STATUS_CHANGED)) {
                scheduleUpdate(CHANGE_WIFI_DISPLAY_STATUS);
            }
        }
    };</pre><br>
从MediaRouter中的callback消息也会进入到scheduleUpdate中,只是后面的参数不一样,通过callback进来的参数是CHANGE_ROUTES,而broadcast进来的参数是CHANGE_WIFI_DISPLAY_STATUS,来看scheduleUpdate,最终实现是mUpdateRunnable<strong>中:</strong>
<pre class="brush:java;">    private void update(int changes) {
        boolean invalidateOptions = false;
 
        // Update wifi display state.
        if ((changes & CHANGE_WIFI_DISPLAY_STATUS) != 0) {
            mWifiDisplayStatus = mDisplayManager.getWifiDisplayStatus();
 
            // The wifi display feature state may have changed.
            invalidateOptions = true;
        }
 
        // Rebuild the routes.
        final PreferenceScreen preferenceScreen = getPreferenceScreen();
        preferenceScreen.removeAll();
 
        // Add all known remote display routes.
        final int routeCount = mRouter.getRouteCount();
        for (int i = 0; i < routeCount; i++) {
            MediaRouter.RouteInfo route = mRouter.getRouteAt(i);
            if (route.matchesTypes(MediaRouter.ROUTE_TYPE_REMOTE_DISPLAY)) {
                preferenceScreen.addPreference(createRoutePreference(route));
            }
        }
 
        // Additional features for wifi display routes.
        if (mWifiDisplayStatus != null
                && mWifiDisplayStatus.getFeatureState() == WifiDisplayStatus.FEATURE_STATE_ON) {
            // Add all unpaired wifi displays.
            for (WifiDisplay display : mWifiDisplayStatus.getDisplays()) {
                if (!display.isRemembered() && display.isAvailable()
                        && !display.equals(mWifiDisplayStatus.getActiveDisplay())) {
                    preferenceScreen.addPreference(new UnpairedWifiDisplayPreference(
                            getActivity(), display));
                }
            }
        }
 
    }</pre><br>
上面的代码比较简单,一个是从MediaRouter中获取mRoutes数组中存着的remote display设备;一个是从broadcast中的WifiDisplayStatus对象中获取mDisplay数组,两者相互合并构建整个listview展现给用户。至此,wifi display的扫描流程就介绍完了,下面是整体的流程图:

版权声明:本文为senior-engineer原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/senior-engineer/p/4971396.html