Android以太网框架情景分析之NetworkFactory与NetworkAgent深入分析

Android网络框架分析系列文章目录:

Android P适配以太网功能开发指南
Android以太网框架情景分析之启动简介
Android以太网框架情景分析之EthernetServiceImpl和NetworkManagementService交互深入分析
Android以太网框架情景分析之NetworkManagementService和netd交互深入分析二
Android以太网框架情景分析之NetworkManagementService和netd交互深入分析一
Android以太网框架情景分析之NetworkFactory与NetworkAgent深入分析
AsyncChannel原理分析以及实操演练


引言

  在前面的章节中Android以太网框架情景分析之启动简介我们介绍了以太网框架的整体启动流程,然后说到了EthernetNetworkFactory类是Ethernet的核心管理类,几乎包含了Ethernet所有的网络管理操作,这其中就包括各种网络的注册网络以及和ConnectifyService服务的交互(主要是通知网络状态的变化,评分机制的变化)。那么本章节将会重点来讲解EthernetNetworkFactory,而这其中的重重之重是NetworkFactory与NetworkAgent和ConnectifyService的通信机制的建立。

  本篇章演示的源码是在Android 7.1 msm8953平台上,其中涉及的源码路径如下所示:


frameworks/base/services/java/com/android/server/---ConnectivityService.java---INativeDaemonConnectorCallbacks.java---NativeDaemonConnector.java---NetworkManagementService.java---SystemServer.java---SystemServiceManager.javaframeworks/base/core/java/android/net/---ConnectivityManager.java---EthernetManager.java---IConnectivityManager.aidl---IEthernetManager.aidl---LinkProperties.java---NetworkPolicy.java---NetworkAgent.java---NetworkFactory.java---NetworkInfo.java---ProxyInfo.javaframeworks/opt/net/ethernet/java/com/android/server/ethernet/---EthernetConfigStore.java---EthernetNetworkFactory.java---EthernetServiceImpl.java---EthernetService.java

  在正式开始本篇的介绍前,还是附上祖传的以太网框架相关类调用关系图,这样能让大伙先在整体流程上有个概括,这样在分析代码走神的时候还可以回来看看,自己到那里了,将要到那里去。


一. EthernetNetworkFactory的初始化

  在前面我们扯了一大推的理论知识和前期准备,从现在开始就是开始真正的源码分析了,在前面的篇章中我们分析到了EthernetNetworkFactory,那么我们就接着上篇继续分析了,开干,各位小伙伴们系好安全带,开始发车了。

1.1 EthernetNetworkFactory.EthernetNetworkFactory

  分析Java中的一个类,一般从构造方法开始,这里我们也不能免俗也从构造方法开始,这里的该构造函数比较简单,主要就是初始化一些成员变量信息,这里不是我们重点关注的。

class EthernetNetworkFactory {EthernetNetworkFactory(RemoteCallbackList<IEthernetServiceListener> listeners) {mNetworkInfo = new NetworkInfo(ConnectivityManager.TYPE_ETHERNET, 0, NETWORK_TYPE, "");//设置网络基本信息,如网络类型,网络别名mLinkProperties = new LinkProperties();initNetworkCapabilities();mListeners = listeners;}
}}

1.2 EthernetNetworkFactory.start

class EthernetNetworkFactory {/*** Begin monitoring connectivity*/public synchronized void start(Context context, Handler target) {// The services we use.IBinder b = ServiceManager.getService(Context.NETWORKMANAGEMENT_SERVICE);mNMService = INetworkManagementService.Stub.asInterface(b);//获取NetworkManagementService服务端代理,在前期知识储备中我们有讲过NetworkManagementService主要用于和Netd通信mEthernetManager = (EthernetManager) context.getSystemService(Context.ETHERNET_SERVICE);//获取EthernetManager服务端代理// Interface match regex.mIfaceMatch = context.getResources().getString(com.android.internal.R.string.config_ethernet_iface_regex);// Create and register our NetworkFactory.mFactory = new LocalNetworkFactory(NETWORK_TYPE, context, target.getLooper());//这个是重点,注意第三个参数,这个会在章节二重点介绍mFactory.setCapabilityFilter(mNetworkCapabilities);mFactory.setScoreFilter(-1); // this set high when we have an ifacemFactory.register();//向ConnectivityService注册自己mContext = context;// Start tracking interface change events.mInterfaceObserver = new InterfaceObserver();try {mNMService.registerObserver(mInterfaceObserver);} catch (RemoteException e) {Log.e(TAG, "Could not register InterfaceObserver " + e);}// If an Ethernet interface is already connected, start tracking that.// Otherwise, the first Ethernet interface to appear will be tracked.try {final String[] ifaces = mNMService.listInterfaces();for (String iface : ifaces) {synchronized(this) {if (maybeTrackInterface(iface)) {// We have our interface. Track it.// Note: if the interface already has link (e.g., if we// crashed and got restarted while it was running),// we need to fake a link up notification so we start// configuring it. Since we're already holding the lock,// any real link up/down notification will only arrive// after we've done this.if (mNMService.getInterfaceConfig(iface).hasFlag("running")) {updateInterfaceState(iface, true);}break;}}}} catch (RemoteException|IllegalStateException e) {Log.e(TAG, "Could not get list of interfaces " + e);}}

  这里可以看到start方法接收一个Handler对象作为参数,对于该参数我们回溯一下是从EthernetServiceImpl.start()中传入的,它新建了一个HandlerThread对象,并传入作为参数,这个Handler对象很重要,因为它会有多重的传递,串联起了以太网的网络管理。并且在代码中创建了一个LocalNetworkFactory对象,而我们的LocalNetworkFactory继承自NetworkFactory。


二. NetworkFactory网络工厂类详解及注册流程分析

  我们知道各个具有网络连接的对象(WIFI,BT,PHONE)都需要向ConnectivityService注册自己,并把自己所提供的网络的分值告诉ConnectivityService。而Android为了ConnectivityService便于统一管理,每一个具备提供网络服务的对象都需要创建一个NetworkFactory的子类对象,并利用该对象注册自己,以及提供自己的分值。并且我们可以通过搜索发现确实WIFI/BT/PHONE/ETHERNET都有实现了NetworkFactory工厂类。如下所示,而我们的LocalNetworkFactory的也在此行列中,那先让我们看看NetworkFactory究竟是如何定义的。

XXX/frameworks$ grep -nr "extends NetworkFactory"  ./
./base/services/tests/servicestests/src/com/android/server/ConnectivityServiceTest.java:430:    private static class MockNetworkFactory extends NetworkFactory {./opt/telephony/src/java/com/android/internal/telephony/PhoneSwitcher.java:222:    private static class PhoneSwitcherNetworkRequestListener extends NetworkFactory {./opt/telephony/src/java/com/android/internal/telephony/dataconnection/TelephonyNetworkFactory.java:42:public class TelephonyNetworkFactory extends NetworkFactory {./opt/net/ethernet/java/com/android/server/ethernet/EthernetNetworkFactory.java:127:    private class LocalNetworkFactory extends NetworkFactor {./opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java:4083:    private class WifiNetworkFactory extends NetworkFactory {./opt/net/wifi/service/java/com/android/server/wifi/WifiStateMachine.java:4116:    private class UntrustedWifiNetworkFactory extends NetworkFactory {XXX/packages$ grep -nr "extends NetworkFactory"  ./
./apps/Bluetooth/src/com/android/bluetooth/pan/BluetoothTetheringNetworkFactory.java:49:public class BluetoothTetheringNetworkFactory extends NetworkFactory {

  NetworkFactory网络注册的时序图如下所示:

2.1 NetworkFactory类简介

  好吗,前面空洞的介绍了NetworkFactory的功能,下面得实打实的来点干货分析分析了,不然没有料不是!

/*** A NetworkFactory is an entity that creates NetworkAgent objects.* The bearers register with ConnectivityService using {@link #register} and* their factory will start receiving scored NetworkRequests.  NetworkRequests* can be filtered 3 ways: by NetworkCapabilities, by score and more complexly by* overridden function.  All of these can be dynamic - changing NetworkCapabilities* or score forces re-evaluation of all current requests.** If any requests pass the filter some overrideable functions will be called.* If the bearer only cares about very simple start/stopNetwork callbacks, those* functions can be overridden.  If the bearer needs more interaction, it can* override addNetworkRequest and removeNetworkRequest which will give it each* request that passes their current filters.* @hide**/
public class NetworkFactory extends Handler {public NetworkFactory(Looper looper, Context context, String logTag,NetworkCapabilities filter) {super(looper);LOG_TAG = logTag;mContext = context;mCapabilityFilter = filter;} //将当前网络注册到ConnectivityService public void register() {if (DBG) log("Registering NetworkFactory");if (mMessenger == null) {mMessenger = new Messenger(this);ConnectivityManager.from(mContext).registerNetworkFactory(mMessenger, LOG_TAG);}}//处理网络请求,用于打开或者释放当前连接,一般在有新的网络接入时会触发此处protected void handleAddRequest(NetworkRequest request, int score) {NetworkRequestInfo n = mNetworkRequests.get(request.requestId);if (n == null) {if (DBG) log("got request " + request + " with score " + score);n = new NetworkRequestInfo(request, score);mNetworkRequests.put(n.request.requestId, n);} else {if (VDBG) log("new score " + score + " for exisiting request " + request);n.score = score;}if (VDBG) log("  my score=" + mScore + ", my filter=" + mCapabilityFilter);evalRequest(n);}//被子类实现protected void startNetwork() { }protected void stopNetwork() { }//更新当前网络的分值public void setScoreFilter(int score) {sendMessage(obtainMessage(CMD_SET_SCORE, score, 0));}
}

  可以看到,这里的NetworkFactory 可以分为如下三个知识点:

  • 这里我们可以看到NetworkFactory 继承于Handler,从该类的字面理解我们可以把它当作一个工厂类,对相关网络的请求和终止操作都通过该对象进行处理。并且从注释可知NetworkFactory与NetworkAgent对象有密切关系,至于为什么说它们有密切的关系,这个先不点破,大伙可以跟着代码分析来自行体会。
  • 而且可知NetworkFactory会通过register()方法向ConnectivityService进行注册。
  • 其中它的方法start/stopNetwork()是提供的回调函数 ,我们可以通过这两个函数来请求或终止网络:

2.2 NetworkFactory注册到ConnectivityService服务

  既然这里提到了注册,通常注册到目的一般是想通过注册到目的端,然后当目的端的状态发生变化的时候回调发起端的相关方法或者接口,而我们这里的注册也不能免俗!通过后面的分析你会分析原来也是依照这个套路来进行的。

 //NetworkFactory.javapublic void register() {if (DBG) log("Registering NetworkFactory");if (mMessenger == null) {mMessenger = new Messenger(this);//注意这里的是Messenger,其参数为NetworkFactory本身,且Messenger是可序列的所以可以跨进程Binder传递ConnectivityManager.from(mContext).registerNetworkFactory(mMessenger, LOG_TAG);//}}

  在正式开始分析前,有必要认识一下Messenger类,这个可不是Message,不是,不是!如果小伙伴们对Messenger和AsyncChannel不熟悉,强烈建议小伙伴们抽出一点时间参见AsyncChannel原理分析以及实操演练的篇章,里面有具体讲解了Messenger和AsyncChannle,如果不搞懂上述两个内容,后续的相关知识点小伙伴可能会一头雾水了!这里我们可以用一句话来概括Messenger主要是为了实现AsyncChannel通信而封装的一个类,而AsyncChannle又是为了实现Handler跨进程或者相同进程通信而封装的一个类。

  而我们在前面的篇章知道,我们在创建LocalNetworkFactory对象时,我们给它指定了一个特定线程的Looper对象,而这个Looper得最终来源是EthernetServiceImpl的start方法,如下所示:

 //EthernetServiceImpl.javapublic void start() {Log.i(TAG, "Starting Ethernet service");HandlerThread handlerThread = new HandlerThread("EthernetServiceThread");handlerThread.start();mHandler = new Handler(handlerThread.getLooper());//根本来源mTracker.start(mContext, mHandler);mStarted.set(true);}

  结合前面的两点,我们再来捋一捋!这里我们的registerNetworkFactory方法参数Messenger对象是一个可以跨进程传递的实例对象,你可以认为它代表一个Handler对象,既然是Handler对象,那么我们可以像使用Handler对象一样,使用Messenger对象来发送消息了,而此处该引用指向mNetworkFactory这个Handler对象,它绑定的Looper属于一个特定的线程,它在EthernetServiceImpl中创建。

  然后们接着继续分析registerNetworkFactory方法,如下所示:

 //ConnectivityManager.javapublic static ConnectivityManager from(Context context) {return (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);}public void registerNetworkFactory(Messenger messenger, String name) {try {mService.registerNetworkFactory(messenger, name);//详见章节2.3} catch (RemoteException e) {throw e.rethrowFromSystemServer();}}

一看就是老套路,获取CONNECTIVITY_SERVICE服务,这个服务就是我们的ConnectivityService服务,然后通过Binder调用ConnectivityService服务的registerNetworkFactory方法,对于Binder不熟悉的小伙伴们可以看看Android Binder指南让你对Android的Binder有一个比较深入的认识和了解。

2.3 ConnectivityService.registerNetworkFactory

  历经重重险阻,万水千山的终于来到了ConnectivityService的世界,让我们紧跟源码继续分析!

 //ConnectivityService.javaprivate static class NetworkFactoryInfo {public final String name;public final Messenger messenger;public final AsyncChannel asyncChannel;public NetworkFactoryInfo(String name, Messenger messenger, AsyncChannel asyncChannel) {this.name = name;//要注册的网络名称,譬如Ethernet,wifi,bt等this.messenger = messenger;//this.asyncChannel = asyncChannel;}}public void registerNetworkFactory(Messenger messenger, String name) {enforceConnectivityInternalPermission();//权限检测忽略NetworkFactoryInfo nfi = new NetworkFactoryInfo(name, messenger, new AsyncChannel());//内部类,对相关的信息进行封装mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_FACTORY, nfi));//InternalHandler对象用于处理内部事务}

  这里的NetworkFactoryInfo 是一个ConnectivityService的内部类,用来封装一些变量信息,而我们这里用它保存了将要注册的网络信息名称(Ethernet)以及特定的Messenger对象messenger,并且将新创建的匿名AsyncChannel通道类对象也保存到其中了。

  接着通过InternalHandler对象mHandler向自己发送了一条信息EVENT_REGISTER_NETWORK_FACTORY并且携带了NetworkFactoryInfo相关信息,这里的InternalHandler是ConnectivityService的内部Handler主要用于其内部一些事务的处理流程,我们直接直捣黄龙看其对EVENT_REGISTER_NETWORK_FACTORY的处理流程,如下所示:

 //ConnectivityService.javaprivate class InternalHandler extends Handler {public InternalHandler(Looper looper) {super(looper);}@Overridepublic void handleMessage(Message msg) {switch (msg.what) {......case EVENT_REGISTER_NETWORK_FACTORY: {handleRegisterNetworkFactory((NetworkFactoryInfo)msg.obj);//详见章节2.4break;......}}}}

2.4 ConnectivityService.handleRegisterNetworkFactory

 //ConnectivityService.javaprivate void handleRegisterNetworkFactory(NetworkFactoryInfo nfi) {if (DBG) log("Got NetworkFactory Messenger for " + nfi.name);mNetworkFactoryInfos.put(nfi.messenger, nfi);nfi.asyncChannel.connect(mContext, mTrackerHandler, nfi.messenger);//是不是有种似曾识相的感觉}

  这里的mNetworkFactoryInfos是一个HashMap对象,在此处将前面创建的NetworkFactoryInfo对象nfi的参数Messenger位key,以NetworkFactoryInfo对象nfi为value添加到mNetworkFactoryInfos中。接着调用前面创建的匿名AsyncChannel对象建立连接,对于这个知识点还是建议大伙抽出一定的时间参阅博客AsyncChannel原理分析以及实操演练,这里我还是简单的分析下调用connect的后续流程,如下所示:

 //AsyncChannel.javapublic void connect(Context srcContext, Handler srcHandler, Messenger dstMessenger) {    connected(srcContext, srcHandler, dstMessenger);    replyHalfConnected(STATUS_SUCCESSFUL);}public void connected(Context srcContext, Handler srcHandler, Messenger dstMessenger) {mSrcContext = srcContext;mSrcHandler = srcHandler;mSrcMessenger = new Messenger(mSrcHandler);mDstMessenger = dstMessenger;linkToDeathMonitor();}private void replyHalfConnected(int status) {Message msg = mSrcHandler.obtainMessage(CMD_CHANNEL_HALF_CONNECTED);msg.arg1 = status;msg.obj = this;msg.replyTo = mDstMessenger;if (!linkToDeathMonitor()) {msg.arg1 = STATUS_BINDING_UNSUCCESSFUL;}mSrcHandler.sendMessage(msg);}

  通过上面的connect流程简要分析,我们可以得到如下两点结论:

  • 这里通过调用connect方法带入参数,对AsyncChannel中的变量进行相关的初始化,初始化之后mSrcHandler指向了ConnectivityService中的NetworkStateTrackerHandler对象mTrackerHandler,它主要负责接收网络模块发送的消息,并进行网络更新;并且这里的mSrcMessenger表示了对该Handler对象的一个引用;最后的mDstMessenger 指向了EthernetNetworkFactory中的LocalNetworkFactory对象,该对象也是一个Handler。在这里要特别关注Handler和Messenger的来源和出处,不是说自古英雄不问出处吗,看来有时候古人还是欺我啊。

  • 最后通过mSrcHandler发送了CMD_CHANNEL_HALF_CONNECTED的相关指令,将会在ConnectivityService中对象中的mTrackerHandler被处理,处理逻辑如下:

 //ConnectivityService.javaprivate class NetworkStateTrackerHandler extends Handler {public NetworkStateTrackerHandler(Looper looper) {super(looper);}private boolean maybeHandleAsyncChannelMessage(Message msg) {switch (msg.what) {default:return false;case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: {handleAsyncChannelHalfConnect(msg);//此处处理,详见章节2.5break;}case AsyncChannel.CMD_CHANNEL_DISCONNECT: {NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);if (nai != null) nai.asyncChannel.disconnect();break;}case AsyncChannel.CMD_CHANNEL_DISCONNECTED: {handleAsyncChannelDisconnected(msg);break;}}return true;}

2.5 ConnectivityService.handleAsyncChannelHalfConnect

 //ConnectivityService.javaprivate void handleAsyncChannelHalfConnect(Message msg) {AsyncChannel ac = (AsyncChannel) msg.obj;if (mNetworkFactoryInfos.containsKey(msg.replyTo)) {//又见到老熟人了mNetworkFactoryInfosif (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {if (VDBG) log("NetworkFactory connected");// A network factory has connected.  Send it all current NetworkRequests.for (NetworkRequestInfo nri : mNetworkRequests.values()) {if (nri.request.isListen()) continue;NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);ac.sendMessage(android.net.NetworkFactory.CMD_REQUEST_NETWORK,(nai != null ? nai.getCurrentScore() : 0), 0, nri.request);//详见章节2.6}} else {loge("Error connecting NetworkFactory");mNetworkFactoryInfos.remove(msg.obj);}} else if (mNetworkAgentInfos.containsKey(msg.replyTo)) {//NetworkAgent注册的时候会走这个分支......}}

  还记得大明湖畔的夏雨荷吗!错了,还记得2.4章节的mNetworkFactoryInfos吗,在该章节里面我们会将NetworkFactoryInfo添加到其中,而此时我们会从其中取出来进行匹配,既然我们已经添加了当然能匹配到了,所以最终会调用到如下的逻辑流程:

             AsyncChannel ac = (AsyncChannel) msg.obj;for (NetworkRequestInfo nri : mNetworkRequests.values()) {if (nri.request.isListen()) continue;NetworkAgentInfo nai = mNetworkForRequestId.get(nri.request.requestId);ac.sendMessage(android.net.NetworkFactory.CMD_REQUEST_NETWORK,(nai != null ? nai.getCurrentScore() : 0), 0, nri.request);//详见章节2.6}

这里最终会调用到AsyncChannel对象ac的mDstMessenger发送CMD_REQUEST_NETWORK消息,而通过前面的分析我们知道mDstMessenger是指向了EthernetNetworkFactory中的LocalNetworkFactory,从字面理解该请求可以理解为向LocalNetworkFactory请求一个网络, 而此时还处于初始化阶段还没有为以太网创建NetworkRequest请求,所以mNetworkForRequestId.get取出的内容为空,从而sendMessage发送的第二个参数arg1为为0这个很重要。

2.6 NetworkFactory.handleAddRequest

  我找遍LocalNetworkFactory也没有看到Handler的处理消息的方法handleMessage,好吗它的处理方法在其父类LocalNetworkFactory中实现的,那我们就来看看它是怎么处理CMD_REQUEST_NETWORK消息的:

 //NetworkFactory.javapublic void handleMessage(Message msg) {switch (msg.what) {case CMD_REQUEST_NETWORK: {handleAddRequest((NetworkRequest)msg.obj, msg.arg1);//处理请求break;}case CMD_CANCEL_REQUEST: {handleRemoveRequest((NetworkRequest) msg.obj);break;}case CMD_SET_SCORE: {handleSetScore(msg.arg1);break;}case CMD_SET_FILTER: {handleSetFilter((NetworkCapabilities) msg.obj);break;}}}protected void handleAddRequest(NetworkRequest request, int score) {NetworkRequestInfo n = mNetworkRequests.get(request.requestId);if (n == null) {if (DBG) log("got request " + request + " with score " + score);n = new NetworkRequestInfo(request, score);mNetworkRequests.put(n.request.requestId, n);} else {if (VDBG) log("new score " + score + " for exisiting request " + request);n.score = score;}if (VDBG) log("  my score=" + mScore + ", my filter=" + mCapabilityFilter);evalRequest(n);//详见章节2.7}

  在这里由于是第一次接收到CMD_REQUEST_NETWORK的请求,因此NetworkRequestInfo 对象n为null,所以将会在NetworkFactory中创建NetworkRequestInfo的对象并且将其存储到列表mNetworkRequests中供后续使用,然后调用方法evalRequest进入网络评价过程。

2.7 NetworkFactory.evalRequest

 //NetworkFactory.javaprivate void evalRequest(NetworkRequestInfo n) {if (VDBG) log("evalRequest");if (n.requested == false && n.score < mScore &&n.request.networkCapabilities.satisfiedByNetworkCapabilities(mCapabilityFilter) && acceptRequest(n.request, n.score)) {if (VDBG) log("  needNetworkFor");needNetworkFor(n.request, n.score);//详见章节2.8n.requested = true;} else if (n.requested == true &&(n.score > mScore || n.request.networkCapabilities.satisfiedByNetworkCapabilities(mCapabilityFilter) == false || acceptRequest(n.request, n.score) == false)) {if (VDBG) log("  releaseNetworkFor");releaseNetworkFor(n.request);n.requested = false;} else {if (VDBG) log("  done");}}

  该逻辑就是整个网络注册系统最关键的地方,其逻辑分为如下两条线路:

  • 如果NetworkRequestInfo没有被requested过,并且其分值(n.score)小于当前NetworkFactory自己的分值(mScore),那么就说明,当前NetworkFactory所处的网络优先级高于其他网络的优先级,就会触发当前NetworkFactory所在网络的needNetworkFor()流程,也就是连接建立流程,并将标记NetworkRequestInfo.requested=true。
  • 当NetworkRequestInfo被requested过(也就是当前网络被needNetworkFor过),此时如果再次收到请求,并且携带的新score大于当前NetworkFactory所处网络的mScore,那么就说明当前NetworkFactory所在网络优先级已经不是最高,需要将其releaseNetworkFor掉,并标记NetworkRequestInfo.requested=false。

  对于初始化流程来说,由于NetworkRequestInfo是刚才在handleAddRequest新创建的,所以其requested状态必然为false,而且我们前面提到,ConnectivityService发送CMD_REQUEST_NETWORK时携带的分值参数为0,并且对于以太网网络来说,其mScore=70(至于这个70在什么地方赋值的,这个先忽略,后续的篇章中会讲到此处),因此此时的判定状态将会是:n.requested=false 且n.score < mScore,那么在这种环境下,对于以太网网络环境初始化过程来说,将会满足第一个if判断,进入needNetworkFor流程,也就是触发以太网网络的建立。

  这里插播一个知识点,假如客户提出网络共存需求,即有客户提出,也就是机器中所有的物理传输介质(WiFi、Mobil、Ethernet)都必须可以同时可以进行网络通信工作,不能互斥。通过设置不同主机IP地址使用不同的网络类型,设置规则发送给Netd,而实现需求。而在这里我们可见,同一时间只能存在一种网络通信方式,即优先级。也就是WiFi、Mobil、以太网他们之间是互斥的,同一时间只有一种通信方式在线提供服务。那要怎么操作呢?

  其实这里就涉及到了evalRequest的评分机制了,要想WiFi、Mobil、Ethernet网络共存,我们可以通过修改评分回调的逻辑实现共存,也就是当进入评分互斥的逻辑时不调用网络释放接口,就能实现网络共存不互斥的需求。也就是通过控制传入的score值大小,可以是设置为40,最终走到needNetworkFor的这个逻辑中去就能实现网络共存。

2.8 NetworkFactory.evalRequest

 //NetworkFactory.javaprotected void needNetworkFor(NetworkRequest networkRequest, int score) {if (++mRefCount == 1) startNetwork();}//EthernetNetworkFactory.javaprivate class LocalNetworkFactory extends NetworkFactory {protected void startNetwork() {onRequestNetwork();//详见章节2.9}}

  兜兜转转最后走到了EthernetNetworkFactory中的onRequestNetwork来处理Ethernet的连接操作,包括对静态IP和DHCP的处理。到这里onRequestNetwork处理完成,一个NetworkFactory对象的注册过程就结束了。

2.9 EthernetNetworkFactory.onRequestNetwork

 //EthernetNetworkFactory.javapublic void onRequestNetwork() {synchronized(EthernetNetworkFactory.this) {if (mIpProvisioningThread != null) {return;}}final Thread ipProvisioningThread = new Thread(new Runnable() {public void run() {if (DBG) {Log.d(TAG, String.format("starting ipProvisioningThread(%s): mNetworkInfo=%s",mIface, mNetworkInfo));}LinkProperties linkProperties;IpConfiguration config = mEthernetManager.getConfiguration();if (config.getIpAssignment() == IpAssignment.STATIC) {if (!setStaticIpAddress(config.getStaticIpConfiguration())) {// We've already logged an error.mIpProvisioningThread = null;return;}if (PAX_ETHERNET) {StaticStatus = true;}linkProperties = config.getStaticIpConfiguration().toLinkProperties(mIface);if (config.getProxySettings() == ProxySettings.STATIC ||config.getProxySettings() == ProxySettings.PAC) {linkProperties.setHttpProxy(config.getHttpProxy());} } else {mNetworkInfo.setDetailedState(DetailedState.OBTAINING_IPADDR, null, mHwAddr);WaitForProvisioningCallback ipmCallback = new WaitForProvisioningCallback() {@Overridepublic void onLinkPropertiesChange(LinkProperties newLp) {synchronized(EthernetNetworkFactory.this) {if (mNetworkAgent != null && mNetworkInfo.isConnected()) {mLinkProperties = newLp;mNetworkAgent.sendLinkProperties(newLp);}}}};synchronized(EthernetNetworkFactory.this) {stopIpManagerLocked();mIpManager = new IpManager(mContext, mIface, ipmCallback);if (config.getProxySettings() == ProxySettings.STATIC ||config.getProxySettings() == ProxySettings.PAC) {mIpManager.setHttpProxy(config.getHttpProxy());}final String tcpBufferSizes = mContext.getResources().getString(com.android.internal.R.string.config_ethernet_tcp_buffers);if (!TextUtils.isEmpty(tcpBufferSizes)) {mIpManager.setTcpBufferSizes(tcpBufferSizes);}final ProvisioningConfiguration provisioningConfiguration =mIpManager.buildProvisioningConfiguration().withProvisioningTimeoutMs(0).build();mIpManager.startProvisioning(provisioningConfiguration);}linkProperties = ipmCallback.waitForProvisioning();if (linkProperties == null ) {Log.e(TAG, "IP provisioning error");// set our score lower than any network could go// so we get dropped.mFactory.setScoreFilter(-1);synchronized(EthernetNetworkFactory.this) {stopIpManagerLocked();}mIpProvisioningThread = null;return;}}synchronized(EthernetNetworkFactory.this) {if (mNetworkAgent != null) {Log.e(TAG, "Already have a NetworkAgent - aborting new request");stopIpManagerLocked();mIpProvisioningThread = null;return;}mLinkProperties = linkProperties;mNetworkInfo.setIsAvailable(true);mNetworkInfo.setDetailedState(DetailedState.CONNECTED, null, mHwAddr);// Create our NetworkAgent.mNetworkAgent = new NetworkAgent(mFactory.getLooper(), mContext,NETWORK_TYPE, mNetworkInfo, mNetworkCapabilities, mLinkProperties,NETWORK_SCORE) {public void unwanted() {synchronized(EthernetNetworkFactory.this) {if (this == mNetworkAgent) {stopIpManagerLocked();mLinkProperties.clear();mNetworkInfo.setDetailedState(DetailedState.DISCONNECTED, null,mHwAddr);updateAgent();mNetworkAgent = null;try {mNMService.clearInterfaceAddresses(mIface);} catch (Exception e) {Log.e(TAG, "Failed to clear addresses or disable ipv6" + e);}} else {Log.d(TAG, "Ignoring unwanted as we have a more modern " +"instance");}}};};mIpProvisioningThread = null;}if (DBG) {Log.d(TAG, String.format("exiting ipProvisioningThread(%s): mNetworkInfo=%s",mIface, mNetworkInfo));}}});synchronized(EthernetNetworkFactory.this) {if (mIpProvisioningThread == null) {mIpProvisioningThread = ipProvisioningThread;mIpProvisioningThread.start();}}}

  尼玛这个方法的逻辑代码也太多了,我们牛逼的谷歌工程师就不知道拆分拆分吗,这一头看不都尾巴的代码,这个各位在分析的时候可以不要硬啃,我们这里只用关心NetworkAgent。通过前面的分析我们可知NetworkFactory将自己注册到ConnectivityService,然后在EthernetNetworkFactory中快速响应ConnectivityService的网络请求,但是这种通道请求时单向的,只能由ConnectivityService流向NetworkFactory而不能反过来。那此时就一个问题了假如我们的EthernetNetworkFactory需要和ConnectivityService进行双向通信呢,这就轮到了我们的NetworkAgent上场了。


三. NetworkAgent网络代理类详解及注册流程分析

  通过我们第二章节的分析可知NetworkFactory可以被看做是ConnectivityService向链路网络(以太网,蓝牙,WIFI)请求的统一接口.,那么NetworkAgent网络代理则可以认为是是ConnectivityService和链路网络管理者(如EthernetNetworkFactory)之间的双向信使,在NetworkFactory和ConnectivityService建立连接并调用onRequestNetwork之后通过NetworkAgent,EthernetNetworkFactory可以向ConnectivityService执行如下操作:

  • 更新网络状态 NetworkInfo(断开、连接中、已连接等)
  • 更新链路配置 LinkProperties(本机网口、IP、DNS、路由信息等)
  • 更新网络能力 NetworkCapabilities(信号强度、是否收费等)

ConnectivityService可以向EthernetNetworkFactory执行如下操作,但是遗憾的是EthernetNetworkFactory都放任没有处理:

  • 更新网络有效性(即NetworkMonitor的网络检测结果)
  • 禁止自动连接
  • 由于网络不可上网等原因主动断开网络

到这里我们应该知道了,NetworkAgent提供了ConnectivityService和EthernetNetworkFactory之间双向通信的能力。原理类似NetworkFactory,也是使用了AsyncChannel和Messenger,其时序图如下红色框标记所示:

3.1 NetWorkFactory和NetworkAgent之间的关系究竟是啥

  还记得在章节2.1的时候留下的一个小小疑问点吗!我们说NetWorkFactory和NetworkAgent之间存在着紧密的联系,那这个紧密联系是什么呢!通过从前面的代码分析我们可以知道NetworkAgent是被NetworkFactory创建的(通过前面的时序图也可以看到),这里的创建并不是说在NetworkFactory内部创建NetworkAgent,而是说,在NetworkFactory这个环境就绪之后,网络提供者才可以创建NetworkAgent。并且在一个NetworkFactory中可以创建不同的NetworkAgent,他们拥有不同的Capabilities等参数而他们之间还有一个区别就是NetworkFactory是在系统初始化时就被创建,而NetworkAgent是在真正接入网络时才会创建。

  我们可以用运营商之间的关系来比喻他们的关系。
   NetworkFactory相当于不同的运营商,比如中国电信、铁通、移动,他们具备联通互联网的能力,当用户入网时就决定了自己的运营商(即完成NetworkFactory初始化)。但同时在每个运营商内部又创建各个不同的接入点,比如对于中国电信来说,还分为上海电信、河北电信等,只有当用户打开电脑真正上网的时候,才会被分配具体的接入点(即完成NetworkAgent初始化)。
  也就是说,同一个NetworkFactory可以在不同的时刻根据需要创建不同的NetworkAgent,比如使用数据上网时,会根据当前的需要(发送MMS还是IMS,或者单纯上网)来创建不同参数的NetworkAgent(不同的APN参数)对象,然后将其注册到ConnectivityService中,而同理在以太网络环境中也是如此可能当前联网是DHCP动态获取或者静态设置IP或者是以太网络代码,也会来创建不同参数的NetworkAgent然后注册到ConnectivityService中。

3.2 NetworkAgent类简介

  前面从理论阶段介绍了NetworkAgent的基本情况,现在得现场相亲熟悉熟悉了,媒婆介绍得吹得再好还是得现场感受下不!下面得上点干货来点实际的东西了。翠花上酸菜!

public abstract class NetworkAgent extends Handler {public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni,NetworkCapabilities nc, LinkProperties lp, int score, NetworkMisc misc) {super(looper);LOG_TAG = logTag;mContext = context;if (ni == null || nc == null || lp == null) {throw new IllegalArgumentException();}if (VDBG) log("Registering NetworkAgent");ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);netId = cm.registerNetworkAgent(new Messenger(this), new NetworkInfo(ni),new LinkProperties(lp), new NetworkCapabilities(nc), score, misc);//注册到ConnectivityService中}@Overridepublic void handleMessage(Message msg) {}abstract protected void unwanted();//虚方法
}

  这里NetworkAgent 的是Hnadler的一个子类,通常Handler吗大多情况是用来处理消息的这里也不例外。而它的unwanted()是一个虚方法,必须在子类中被实现,而在章节二的后面我们在初始化NetworkAgent 的时候实现了它,当ConnectivityService不再需要当前网络连接时调用它(看来ConnectivityService也是个负心汉啊)。除了清除掉网络配置信息,还会把mNetworkAgent置为null。从代码中可以看出mNetworkAgent是否为null标志了当前是否该类型的网络连接正在使用,如果有则不会处理新的网络请求。

 //EthernetNetworkFactory.javamNetworkAgent = new NetworkAgent(mFactory.getLooper(), mContext,NETWORK_TYPE, mNetworkInfo, mNetworkCapabilities, mLinkProperties,NETWORK_SCORE) {public void unwanted() {synchronized(EthernetNetworkFactory.this) {if (this == mNetworkAgent) {stopIpManagerLocked();mLinkProperties.clear();mNetworkInfo.setDetailedState(DetailedState.DISCONNECTED, null,mHwAddr);updateAgent();mNetworkAgent = null;try {mNMService.clearInterfaceAddresses(mIface);} catch (Exception e) {Log.e(TAG, "Failed to clear addresses or disable ipv6" + e);}} else {Log.d(TAG, "Ignoring unwanted as we have a more modern " +"instance");}}};};

3.3 NetworkAgent注册到ConnectivityService

  前面我们在口口声声的说NetworkAgent注册到ConnectivityService中,可是却怎么也找不到和NetworkFactory类似的register的方法,好吗NetworkAgent没有专门提供注册接口,而是直接在构造方法中注册了,具体逻辑如下:

    public NetworkAgent(Looper looper, Context context, String logTag, NetworkInfo ni,NetworkCapabilities nc, LinkProperties lp, int score, NetworkMisc misc) {super(looper);LOG_TAG = logTag;mContext = context;if (ni == null || nc == null || lp == null) {throw new IllegalArgumentException();}if (VDBG) log("Registering NetworkAgent");ConnectivityManager cm = (ConnectivityManager)mContext.getSystemService(Context.CONNECTIVITY_SERVICE);netId = cm.registerNetworkAgent(new Messenger(this), new NetworkInfo(ni),new LinkProperties(lp), new NetworkCapabilities(nc), score, misc);}

  这个注册方法还是老套路和前面NetworkFactory的注册类似,其中的第一个参数是Messenge对象,这个也是实现双向通信的关键!这里最终通过Binder调用到了ConnectivityService中。

3.4 ConnectivityService.registerNetworkAgent

  历经重重险阻,万水千山的终于来到了ConnectivityService的世界,让我们紧跟源码继续分析!

    public int registerNetworkAgent(Messenger messenger, NetworkInfo networkInfo,LinkProperties linkProperties, NetworkCapabilities networkCapabilities,int currentScore, NetworkMisc networkMisc) {enforceConnectivityInternalPermission();// TODO: Instead of passing mDefaultRequest, provide an API to determine whether a Network// satisfies mDefaultRequest.final NetworkAgentInfo nai = new NetworkAgentInfo(messenger, new AsyncChannel(),new Network(reserveNetId()), new NetworkInfo(networkInfo), new LinkProperties(linkProperties), new NetworkCapabilities(networkCapabilities), currentScore,mContext, mTrackerHandler, new NetworkMisc(networkMisc), mDefaultRequest, this);//通过传递过来的参数,初始化NetworkAgentInfo对象naisynchronized (this) {nai.networkMonitor.systemReady = mSystemReady;}addValidationLogs(nai.networkMonitor.getValidationLogs(), nai.network,networkInfo.getExtraInfo());if (DBG) log("registerNetworkAgent " + nai);mHandler.sendMessage(mHandler.obtainMessage(EVENT_REGISTER_NETWORK_AGENT, nai));return nai.network.netId;}

  这里的传入的参数Messenger对象messenger是前面NetworkAgent的引用,这个也是实现NetworkAgent和ConnectivityService双向跨进程通信的关键,然后在ConnectivityService的内部创建了一个新的对象NetworkAgentInfo,该对象中保留了传递进来的一系列参数,包括NetworkAgent的Messenger对象、NetworkInfo、NetworkCapabilities、score以及创建了一个用于通讯的AsyncChannel通道。
   然后就把当前创建的NetworkAgentInfo对象放入EVENT_REGISTER_NETWORK_AGENT消息中,发送给Handler处理:

//ConnectivityService.javaprivate class InternalHandler extends Handler {public void handleMessage(Message msg) {NetworkInfo info;switch (msg.what) {case EVENT_REGISTER_NETWORK_AGENT: {handleRegisterNetworkAgent((NetworkAgentInfo)msg.obj);//详见章节3.5break;}}}

3.5 ConnectivityService.handleRegisterNetworkAgent

 //ConnectivityService.javaprivate void handleRegisterNetworkAgent(NetworkAgentInfo na) {if (VDBG) log("Got NetworkAgent Messenger");mNetworkAgentInfos.put(na.messenger, na);//将NetworkAgentInfo保存到mNetworkAgentInfos列表中synchronized (mNetworkForNetId) {mNetworkForNetId.put(na.network.netId, na);}//发送单向通道连接请求na.asyncChannel.connect(mContext, mTrackerHandler, na.messenger);NetworkInfo networkInfo = na.networkInfo;na.networkInfo = null;updateNetworkInfo(na, networkInfo);//更新最新的NetworkInfo信息}

  在该阶段,ConnectivityService主要干了三件事情:

  • 将新要注册的NetworkAgentInfo信息保存到HashMap列表mNetworkAgentInfos中
  • 利用NetworkAgentInfo对象na的AsyncChannle工具类向NetWrokAgent发起单向通道连接
  • 更新最新的NetworkAgentInfo状态

  如果对AsyncChannel还有不清楚的小伙伴,墙烈建议参见篇章AsyncChannel看这就对了,此时我们可以看到AsyncChannel通过connect方法发起的单向通道连接,按照AsyncChannel通信的逻辑将会在mTrackerHandler收到CMD_CHANNEL_HALF_CONNECTED的消息,历史和其的相似啊和NetWorkFactory注册的过程:

 //ConnectivityService.javaprivate class NetworkStateTrackerHandler extends Handler {public NetworkStateTrackerHandler(Looper looper) {super(looper);}private boolean maybeHandleAsyncChannelMessage(Message msg) {switch (msg.what) {default:return false;case AsyncChannel.CMD_CHANNEL_HALF_CONNECTED: {handleAsyncChannelHalfConnect(msg);break;}case AsyncChannel.CMD_CHANNEL_DISCONNECT: {NetworkAgentInfo nai = mNetworkAgentInfos.get(msg.replyTo);if (nai != null) nai.asyncChannel.disconnect();break;}case AsyncChannel.CMD_CHANNEL_DISCONNECTED: {handleAsyncChannelDisconnected(msg);break;}}return true;}
 //ConnectivityService.javaprivate void handleAsyncChannelHalfConnect(Message msg) {AsyncChannel ac = (AsyncChannel) msg.obj;if (mNetworkFactoryInfos.containsKey(msg.replyTo)) {//NetWorkFactory注册走的是此通道......} else if (mNetworkAgentInfos.containsKey(msg.replyTo)) {//NetworkAgent注册走此通道if (msg.arg1 == AsyncChannel.STATUS_SUCCESSFUL) {if (VDBG) log("NetworkAgent connected");// A network agent has requested a connection.  Establish the connection.mNetworkAgentInfos.get(msg.replyTo).asyncChannel.sendMessage(AsyncChannel.CMD_CHANNEL_FULL_CONNECTION);} else {......}}}

  因为此时发起的是NetworkAgent注册流程所以会在第二个分支,因为我们前面已经将要注册的NetWorkAgent信息存储到了mNetworkAgentInfos中。这里我们可以看到走入该分支以后,此时ConnectivityService与NetworkAgent之间单向通道建立完成后,然后又通过sendMessage发起了双向通道的请求,此时在NetworkAgent端,将会收到CMD_CHANNEL_FULL_CONNECTION的消息。

3.6 NetWorkAgent.handleMessage

 //NetworkAgent.javapublic void handleMessage(Message msg) {switch (msg.what) {case AsyncChannel.CMD_CHANNEL_FULL_CONNECTION: {if (mAsyncChannel != null) {} else {AsyncChannel ac = new AsyncChannel();ac.connected(null, this, msg.replyTo);//告知ConnectivityService双向连接成功ac.replyToMessage(msg, AsyncChannel.CMD_CHANNEL_FULLY_CONNECTED, AsyncChannel.STATUS_SUCCESSFUL);synchronized (mPreConnectedQueue) {mAsyncChannel = ac;for (Message m : mPreConnectedQueue) {//如果有缓存消息,则发送出去ac.sendMessage(m);}mPreConnectedQueue.clear();}}break;}}}

  至此ConnectivityService和NetWrokAgent的双向通道建立完成,随后通过AsyncChannel发送CMD_CHANNEL_FULLY_CONNECTED消息,表示整个AsyncChannel的连接成功,可以进行通信了。同时,还会遍历mPreConnectedQueue集合,这个集合中保存了当mAsyncChannel为null时的所有与更新网络信息相关的message,通过ac.sendMessage()向CS发送所有的message进行状态更新(要注意,ac对象的mSrcHanlder为当前NetworkAgent,mDstMessenger指向NetworkStateTrackerHandler)。

3.7 NetWorkAgent通知ConnectivityService网络状态的变化

  还记得在章节三的开端说的我们可以通过NetworkAgent向ConnectivityService报告网络的变化,通知它进行网络状态的更新吗。而在EthernetNetworkFactory中通过updateAgen完成此项工作(至于怎么触发updateAgen这个本篇不予讨论,这个就牵涉到其它的知识点了),逻辑如下:

 //EthernetNetworkFactory.javapublic void updateAgent() {synchronized (EthernetNetworkFactory.this) {if (mNetworkAgent == null) return;if (DBG) {Log.i(TAG, "Updating mNetworkAgent with: " +mNetworkCapabilities + ", " +mNetworkInfo + ", " +mLinkProperties);}mNetworkAgent.sendNetworkCapabilities(mNetworkCapabilities);mNetworkAgent.sendNetworkInfo(mNetworkInfo);mNetworkAgent.sendLinkProperties(mLinkProperties);// never set the network score below 0.mNetworkAgent.sendNetworkScore(mLinkUp? NETWORK_SCORE : 0);}}

  我们这里以sendNetworkScore为例说明:

 //NetWorkAgent.javapublic void sendNetworkScore(int score) {if (score < 0) {throw new IllegalArgumentException("Score must be >= 0");}queueOrSendMessage(EVENT_NETWORK_SCORE_CHANGED, new Integer(score));}private void queueOrSendMessage(Message msg) {synchronized (mPreConnectedQueue) {if (mAsyncChannel != null) {mAsyncChannel.sendMessage(msg);} else {mPreConnectedQueue.add(msg);}}}

  这个应该又是老套路了吗,通过已经建立的AsyncChannel连接向ConnectifyService发送消息,并附带需要更新的NetworkInfo对象,这样ConnectivityService中NetworkStateTrackerHandler就可以收到消息,并进行网络状态更新了。


结语

  修行至此,恭喜读者你已经对EthernetNetworkFactory类的功能了然于心了,但是这个还只是个开始,关于Android以太网框架的情景分析还有涉及到和netd的交互逻辑,具体详见博客Android网络框架情景分析之NetworkManagementService和Netd交互深入分析一。


写在最后

  各位读者看官朋友们,Android以太网框架情景分析之NetworkFactory与NetworkAgent深入分析到这里结束了,希望能吸引你,激发你的学习欲望和斗志。在最后麻烦读者朋友们如果本篇对你有帮助,关注和点赞一下,当然如果有错误和不足的地方也可以拍砖。

特别鸣谢如下博客:
https://blog.csdn.net/u010961631/article/details/48971651
https://blog.csdn.net/qq_14978113/article/details/89182253

Android以太网框架情景分析之NetworkFactory与NetworkAgent深入分析相关推荐

  1. Android以太网框架情景分析之启动简介

            Android以太网框架情景分析之启动简介 Android网络框架分析系列文章目录: Android P适配以太网功能开发指南 Android以太网框架情景分析之启动简介 Androi ...

  2. Android以太网框架情景分析之EthernetServiceImpl和NetworkManagementService交互深入分析

    EthernetServiceImpl和NetworkManagementService交互深入分析 Android网络框架分析系列文章目录: Android P适配以太网功能开发指南 Android ...

  3. 老罗的《Android系统源代码情景分析》翻了10遍还看不懂?因为你用错了

    最近老朽又把罗升阳老师的<Android系统源代码情景分析>拿出来啃了一番. 为什么要加个"又"呢?因为从老罗的第一版开始到迄今为止尚未更新的第三版为止,每年有学习冲动 ...

  4. 《Android系统源代码情景分析》一书勘误

    在大家的支持和鼓励下,<Android系统源代码情景分析>一书得以出版了,老罗在此首先谢过大家了.本书的内容来源于博客的文章,经过大半年的整理之后,形成了初稿.在正式出版之前,又经过了三次 ...

  5. Android系统源代码情景分析:基础知识

    老罗(罗升阳)发表在的InfoQ上的好文,最新在学习Android,转载一下,方便学习. 老罗的CSDN blog链接:http://blog.csdn.net/Luoshengyang/ 原文链接: ...

  6. Android系统源代码情景分析-0714学习

    目录 8.3开发Android硬件访问服务 硬件访问服务通过硬件抽象层模块来为应用程序提供硬件读写操作.硬件抽象层模块是用C++语言开发的,而应用程序框架层中的硬件访问服务是Java开发的,因此,硬件 ...

  7. android MotionEvent.ACTION_CANCEL情景分析

    今天在温习ViewGroup的dispatchTouchEvent理解这篇文章的时候,偶然间发现了MotionEvent.ACTION_CANCEL使用的情景,温故而知新,说的就是这了. 还是直入主题 ...

  8. 罗升阳 android系统源代码情景分析,Android系统源代码情景分析

    领取成功 您已领取成功! 您可以进入Android/iOS/Kindle平台的多看阅读客户端,刷新个人中心的已购列表,即可下载图书,享受精品阅读时光啦! - | 回复不要太快哦~ 回复内容不能为空哦 ...

  9. 《Android系统源代码情景分析》一书正在连载中

    进击的程序员:http://0xcc0xcd.com,PC版和移动版同时进行,感谢大家支持!

最新文章

  1. 彻底理解大数据 HDFS 分布式文件系统,这篇就够了
  2. python使用matplotlib可视化线图(line plot)、使用arrow函数在matplotlib可视化图像中添加箭头(drawing arrows in matplotlib)
  3. UnicodeDecodeError: ‘utf-8‘ codec can‘t decode bytes in position 708-709: invalid continuation byte
  4. 成功解决ModuleNotFoundError: No module named engine
  5. mysql5.7.19设置_MySQL5.7.19安装配置
  6. 从根本上解决 Infopath 2010 重复表的序号问题
  7. mysql 表列表_一个MySQL表中的多列表显示
  8. 今日恐慌与贪婪指数为77 贪婪程度有所缓解
  9. window.event对象详尽解析
  10. LDAP实现企业异构平台的统一认证
  11. 基于高程的地面点云信息提取
  12. 轻量级日志收集转发 | fluent-bit外部插件详解(三)
  13. C语言 三角函数用法
  14. 计算机ipv6无法连接,ipv6无网络访问权限实测解决教程
  15. html用于定义表格行的标签,HTML表格标签
  16. 最新可用的快速FLV转MP4方法,解决转换后无声音及视频不流畅问题
  17. 微信JSAPI之V3版本支付踩坑
  18. 工控物联网案例-如何利用LTE-658 4G DTU实现水文水利远程物联网监控
  19. 【良心推荐两款软件】eDiary记笔记软件+截图识别文字软件
  20. 想要运营公众号?公众号形象定位有哪些?

热门文章

  1. Virtual script not found, may missing <script lang=“ts“> / “allowJs“: true / jsconfig.json. 报错解决
  2. Android——Fragment懒加载
  3. 教你不用装抖音APP,也照样可以在手机上看抖音!
  4. 伊朗上下5000年简史
  5. HTML5期末大作业:艺术品商城网站设计——艺购艺术品商城网页(1页) HTML+CSS+JavaScript 学生DW网页 使用html+css实现一个静态页面(含源码)
  6. VMware将虚机导出为ova镜像
  7. sew 31系列服务器报警12,SEW变频器MOVITRAC-31C系列故障代码详表
  8. 【正点原子Linux连载】 第十九章 CAN Bus 摘自【正点原子】I.MX6U嵌入式Qt开发指南V1.0.2
  9. PHP把表单中的数据写入到EXCEL
  10. Java项目管理工具:maven配置详解