前言

  通过前面篇章Android以太网框架情景分析之NetworkManagementService和netd交互深入分析一我想小伙伴已经对Netd有了一个比较深层次的认知了,Netd一方面接收处理内核上报的网络状态信息然后将相关指令发送给上层,另外一方面接收上层传递下来的指令执行对应的命令。尼玛!这里多次提到的上层究竟是个啥玩意,其实就是我们本章节要讲到的NetworkManagementService服务(该服务的内容非常多,这里我们只重点关注其怎么和Netd交互的)。好吗,不多说了直接开干!

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

system/netd/server/---CommandListener.cpp---CommandListener.h---main.cpp---NetlinkHandler.cpp---netd.rc---NetlinkManager.cpp---DnsProxyListener.cpp---MDnsSdListener.cpp---FwmarkServer.cpp...NetdNativeService.cpp...system/core/libsysutils/src/---FrameworkListener.cpp---NetlinkListener.cpp---FrameworkCommand.cpp---NetlinkEvent.cpp---SocketListener.cpp...frameworks/base/services/java/com/android/server/SystemServer.java
frameworks/base/services/core/java/com/android/server/NetworkManagementService.java
frameworks/opt/net/ethernet/java/com/android/server/ethernet/EthernetNetworkFactory.java
frameworks/opt/net/ethernet/java/com/android/server/ethernet/EthernetService.java
frameworks/opt/net/ethernet/java/com/android/server/ethernet/EthernetServiceImpl.java
frameworks/opt/net/ethernet/java/com/android/server/ethernet/EthernetConfigStore.java
frameworks/base/services//core/java/com/android/server/ConnectivityService.java
frameworks/base/core/java/android/net/INetworkManagementEventObserver.aidl

一. NetworkManagementService和Netd交互深入分析

  在正式开始分析前,还是把祖传的Netd和NetworkManagementService交互的架构图奉上,镇场子!同时也为了后续的描述更加简洁,后续统一将NetworkManagementService简称为NMService。

1.1 NetworkManagementService的启动

  地球人都知道NMService服务属于system_server进程,而通过博客Android 系统启动之SystemServer大揭秘下知道,system_server进程会在其启动的startOtherServices阶段启动一系列相关的服务,而这些服务则包括我们今天的主角NMService。其关键代码如下所示:

//SystemServer.java
NetworkManagementService networkManagement = null;
private void startOtherServices() {traceBeginAndSlog("StartNetworkManagementService");try {networkManagement = NetworkManagementService.create(context);//创建NetworkManagementService对象ServiceManager.addService(Context.NETWORKMANAGEMENT_SERVICE, networkManagement);//将NetworkManagementService添加到ServiceManager中,供第三方开发者使用} catch (Throwable e) {reportWtf("starting NetworkManagement Service", e);}Trace.traceEnd(Trace.TRACE_TAG_SYSTEM_SERVER);

  接着继续分析NMService的create方法:

   //NetworkManagementService.javaprivate NetworkManagementService(Context context, String socket) {mContext = context;mFgHandler = new Handler(FgThread.get().getLooper());PowerManager.WakeLock wl = null; //pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, NETD_TAG);/*NativeDaemonConnector是Java Framework中一个特别的类,它用于连接指定的socket,并发送和接收socket数据。此处,"netd"参数代表目标socket。NetdCallbackReceiver为具体的socket连接及消息处理对象。1.当Netd连接成功后,NetdCallbackReceiver的onDaemonConnected函数将被调用。2.当收到来自Netd的数据后,NetdCallbackReceiver的onEvent函数将被调用。NativeDaemonConnector代码比较简单,感兴趣的读者不妨自行阅读。*/mConnector = new NativeDaemonConnector(new NetdCallbackReceiver(), socket, 10, NETD_TAG, 160, wl,FgThread.get().getLooper());//NativeDaemonConnector// 创建一个线程,其Runnable对象就是mConnectormThread = new Thread(mConnector, NETD_TAG);mDaemonHandler = new Handler(FgThread.get().getLooper());Watchdog.getInstance().addMonitor(this);//看来我们的NetworkManagementService服务很重要,都必须使用看门狗服务来管理}static NetworkManagementService create(Context context, String socket)throws InterruptedException {final NetworkManagementService service = new NetworkManagementService(context, socket);//直接创建对象final CountDownLatch connectedSignal = service.mConnectedSignal;if (DBG) Slog.d(TAG, "Creating NetworkManagementService");service.mThread.start();//这个会在1.2中具体讲解,if (DBG) Slog.d(TAG, "Awaiting socket connection");connectedSignal.await();if (DBG) Slog.d(TAG, "Connected");service.connectNativeNetdService();return service;}private static final String NETD_SERVICE_NAME = "netd";public static NetworkManagementService create(Context context) throws InterruptedException {return create(context, NETD_SERVICE_NAME);//注意这里的NETD_SERVICE_NAME是"netd"和Netd进程中的"/dev/socket/netd"是对应的}

  可以看到NMService的静态方法create主要干了两件事:

  • 创建NMService的实例对象并返回

  • 构建NativeDaemonConnector实例对象mConnector,并启动

1.2 NetworkManagementService如何接收Netd传递的消息

  NetworkManagementService也启动OK了,我们要直奔主题了NetworkManagementService是如何接收Netd传递的消息呢,这个就要轮到我们的NativeDaemonConnector的登场了,NativeDaemonConnector在和Netd的交互中起到了丰功至伟的作用。基本全程都是它在操盘!至于为啥说它是丰功至伟,详解章节1.3。

1.3 NativeDaemonConnector处理Netd的消息

//NativeDaemonConnector.java
final class NativeDaemonConnector implements Runnable, Handler.Callback, Watchdog.Monitor {...NativeDaemonConnector(INativeDaemonConnectorCallbacks callbacks, String socket,int responseQueueSize, String logTag, int maxLogSize, PowerManager.WakeLock wl,Looper looper) {mCallbacks = callbacks;//这个回调接口很重要mSocket = socket;//这里传入的参数是“netd”mResponseQueue = new ResponseQueue(responseQueueSize);mWakeLock = wl;if (mWakeLock != null) {mWakeLock.setReferenceCounted(true);}mLooper = looper;mSequenceNumber = new AtomicInteger(0);TAG = logTag != null ? logTag : "NativeDaemonConnector";mLocalLog = new LocalLog(maxLogSize);}...
}

  可以看到NativeDaemonConnector实现了Runnable,而在前面章节1.1中以NativeDaemonConnector的实例为参数构建了mThread 线程,然后调用了其start方法,那么接着肯定的执行了NativeDaemonConnector的run方法。那还能咋样呢,接着分析继续干呗。

  //NativeDaemonConnector.javapublic void run() {mCallbackHandler = new Handler(mLooper, this);//构建Handler对象,并且该Handler对象的Callback是NativeDaemonConnector自己(因为它有实现了Handler.Callback接口),while (true) {try {listenToSocket();//开始循环执行,详见章节1.3.1} catch (Exception e) {loge("Error in NativeDaemonConnector: " + e);SystemClock.sleep(5000);}}}

1.3.1 NativeDaemonConnector.listenToSocket

  在正式开始分析listenToSocket源码之前,如果各位小伙伴对LocalSocket还不是很了解的可以先看看Android Framework层和Native层通过LocalSocket实现通信。

    //NativeDaemonConnector.javaprivate void listenToSocket() throws IOException {LocalSocket socket = null;try {socket = new LocalSocket();//LocalSocketAddress address = determineSocketAddress();socket.connect(address);//和Netd的“netd”建立连接InputStream inputStream = socket.getInputStream();synchronized (mDaemonLock) {mOutputStream = socket.getOutputStream();}mCallbacks.onDaemonConnected();//通知回调接口,和Netd的"netd"的LocalSocket通信通道已经建立成功了FileDescriptor[] fdList = null;byte[] buffer = new byte[BUFFER_SIZE];int start = 0;while (true) {int count = inputStream.read(buffer, start, BUFFER_SIZE - start);//读取Netd发送过来的内容if (count < 0) {loge("got " + count + " reading with start = " + start);break;}fdList = socket.getAncillaryFileDescriptors();// Add our starting point to the count and reset the start.count += start;start = 0;for (int i = 0; i < count; i++) {if (buffer[i] == 0) {// Note - do not log this raw message since it may contain// sensitive datafinal String rawEvent = new String(buffer, start, i - start, StandardCharsets.UTF_8);boolean releaseWl = false;try {final NativeDaemonEvent event =NativeDaemonEvent.parseRawEvent(rawEvent, fdList);//将Netd发送过来的数据封装成NativeDaemonEvent对象log("RCV <- {" + event + "}");if (event.isClassUnsolicited()) {// TODO: migrate to sending NativeDaemonEvent instancesif (mCallbacks.onCheckHoldWakeLock(event.getCode())&& mWakeLock != null) {mWakeLock.acquire();releaseWl = true;}Message msg = mCallbackHandler.obtainMessage(event.getCode(), uptimeMillisInt(), 0, event.getRawEvent());if (mCallbackHandler.sendMessage(msg)) {//通过Handler发送消息,然后在该Handler对应handleMessage中进行相关的处理,而我们的NativeDaemonConnector实现了Handler.Callback的详见章节1.2.3releaseWl = false;}} else {mResponseQueue.add(event.getCmdNumber(), event);}} catch (IllegalArgumentException e) {log("Problem parsing message " + e);} finally {if (releaseWl) {mWakeLock.release();}}start = i + 1;}}if (start == 0) {log("RCV incomplete");}// We should end at the amount we read. If not, compact then// buffer and read again.if (start != count) {final int remaining = BUFFER_SIZE - start;System.arraycopy(buffer, start, buffer, 0, remaining);start = remaining;} else {start = 0;}}} catch (IOException ex) {loge("Communications error: " + ex);throw ex;} finally {synchronized (mDaemonLock) {if (mOutputStream != null) {try {loge("closing stream for " + mSocket);mOutputStream.close();} catch (IOException e) {loge("Failed closing output stream: " + e);}mOutputStream = null;}}try {if (socket != null) {socket.close();}} catch (IOException ex) {loge("Failed closing socket: " + ex);}}}

1.3.2 NativeDaemonConnector.handleMessage

    //NativeDaemonConnector.javapublic boolean handleMessage(Message msg) {final String event = (String) msg.obj;final int start = uptimeMillisInt();final int sent = msg.arg1;try {if (!mCallbacks.onEvent(msg.what, event, NativeDaemonEvent.unescapeArgs(event))) {//通知回调接口,可以通过onEvent进行处理了,详见章节1.4log(String.format("Unhandled event '%s'", event));}} catch (Exception e) {loge("Error handling '" + event + "': " + e);} finally {if (mCallbacks.onCheckHoldWakeLock(msg.what) && mWakeLock != null) {mWakeLock.release();}final int end = uptimeMillisInt();if (start > sent && start - sent > WARN_EXECUTE_DELAY_MS) {loge(String.format("NDC event {%s} processed too late: %dms", event, start - sent));}if (end > start && end - start > WARN_EXECUTE_DELAY_MS) {loge(String.format("NDC event {%s} took too long: %dms", event, end - start));}}return true;}
  • 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

  通过前面的章节我们知道NativeDaemonConnector实现了Handler.Callback接口,所以前面的Hander发送的消息最终由其处理,而通过代码看到最终在handleMessage方法中会调用回调接口的mCallbacks.onEvent方法进行处理。而我们这里的mCallbacks就是在NMService中构建NativeDaemonConnector时传递过来的回调接口类NetdCallbackReceiver。

1.4 NativeDaemonConnector接口回调类

  通过前面章节分析可知,NativeDaemonConnector接收到的Netd传递的消息最后经由NetdCallbackReceiver回调类的的onEvent方法进行处理,而在该方法中根据不同的消息,调用对应的notifyXXXX方法,而notifyXXXX方法最后通知mObservers实观察者感兴趣的内容。而这里我们以以太网络接口更改为例来说明。

//NetworkManagementService.javaprivate class NetdCallbackReceiver implements INativeDaemonConnectorCallbacks {@Overridepublic void onDaemonConnected() {...}@Overridepublic boolean onCheckHoldWakeLock(int code) {return code == NetdResponseCode.InterfaceClassActivity;}@Overridepublic boolean onEvent(int code, String raw, String[] cooked) {String errorMessage = String.format("Invalid event from daemon (%s)", raw);switch (code) {case NetdResponseCode.InterfaceChange:/** a network interface change occured* Format: "NNN Iface added <name>"*         "NNN Iface removed <name>"*         "NNN Iface changed <name> <up/down>"*         "NNN Iface linkstatus <name> <up/down>"*/if (cooked.length < 4 || !cooked[1].equals("Iface")) {throw new IllegalStateException(errorMessage);}if (cooked[2].equals("added")) {notifyInterfaceAdded(cooked[3]);//网络接口添加,譬如以太网模块的加载return true;} else if (cooked[2].equals("removed")) {notifyInterfaceRemoved(cooked[3]);return true;} else if (cooked[2].equals("changed") && cooked.length == 5) {notifyInterfaceStatusChanged(cooked[3], cooked[4].equals("up"));return true;} else if (cooked[2].equals("linkstate") && cooked.length == 5) {notifyInterfaceLinkStateChanged(cooked[3], cooked[4].equals("up"));return true;}throw new IllegalStateException(errorMessage);// break;default: break;}return false;}}private void notifyInterfaceAdded(String iface) {final int length = mObservers.beginBroadcast();try {for (int i = 0; i < length; i++) {try {mObservers.getBroadcastItem(i).interfaceAdded(iface);//这里的mObservers即注册的观察者,有可能是WIFI,Ethernet,Phone等,而关于观察者的注册这个会在以太网络和NetworkManagementService交互中讲到,小解请看1.4.1} catch (RemoteException | RuntimeException e) {}}} finally {mObservers.finishBroadcast();}}

1.4.1 NetworkManagementService.registerObserver

    //NetworkManagementService.javaprivate final RemoteCallbackList<INetworkManagementEventObserver> mObservers =new RemoteCallbackList<INetworkManagementEventObserver>();//关于INetworkManagementEventObserver的详见1.4.2@Overridepublic void registerObserver(INetworkManagementEventObserver observer) {mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);mObservers.register(observer);}@Overridepublic void unregisterObserver(INetworkManagementEventObserver observer) {mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);mObservers.unregister(observer);}

  这里我们可以看到mObservers通过registerObserver注入的,其中我们的以太网也属于这个行列,至于怎么注册的这个我们这里先不分析,后面会有准们章节分析。这里我们只需要知道WIFI,Ethernet,Phone都是观察者会注册如此,当有感兴趣的事情发生时候立马响应!

1.4.2 INetworkManagementEventObserver

//INetworkManagementEventObserver.aidl
interface INetworkManagementEventObserver {/*** Interface configuration status has changed.** @param iface The interface.* @param up True if the interface has been enabled.*/void interfaceStatusChanged(String iface, boolean up);/*** Interface physical-layer link state has changed.  For Ethernet,* this method is invoked when the cable is plugged in or unplugged.** @param iface The interface.* @param up  True if the physical link-layer connection signal is valid.*/void interfaceLinkStateChanged(String iface, boolean up);/*** An interface has been added to the system** @param iface The interface.*/void interfaceAdded(String iface);/*** An interface has been removed from the system** @param iface The interface.*/void interfaceRemoved(String iface);/*** An interface address has been added or updated.** @param iface The interface.* @param address The address.*/void addressUpdated(String iface, in LinkAddress address);/*** An interface address has been removed.** @param iface The interface.* @param address The address.*/void addressRemoved(String iface, in LinkAddress address);/*** A networking quota limit has been reached. The quota might not* be specific to an interface.** @param limitName The name of the limit that triggered.* @param iface The interface on which the limit was detected.*/void limitReached(String limitName, String iface);/*** Interface data activity status is changed.** @param iface The interface.* @param active  True if the interface is actively transmitting data, false if it is idle.* @param tsNanos Elapsed realtime in nanos when the state of the network interface changed.*/void interfaceClassDataActivityChanged(String label, boolean active, long tsNanos);/*** Message is received from network interface.** @param message The message*/void interfaceMessageRecevied(String message);/*** Information about available DNS servers has been received.** @param iface The interface on which the information was received.* @param lifetime The time in seconds for which the DNS servers may be used.* @param servers The IP addresses of the DNS servers.*/void interfaceDnsServerInfo(String iface, long lifetime, in String[] servers);/*** A route has been added or updated.*/void routeUpdated(in RouteInfo route);/*** A route has been removed.*/void routeRemoved(in RouteInfo route);}

  这里可以看到INetworkManagementEventObserver是一个aidl类型的接口类,既然是aidl保证了其可以通过Binder跨进程传输数据,其源码路径为frameworks/base//core/java/android/net/INetworkManagementEventObserver.aidl。可以看到它的接口方法主要是用于通知各种网络状态的改变!

1.5 NetworkManagementService如何接收Netd传递的消息小结

  通过前面章节的分析,我们对于NMService如何接收Netd传递的消息大伙应该心中已经有谱了,但是我们还是趁热打铁小结一波:

  • 当Netd进程接收到内核上报消息然后就要传递给上层,这里的上层就是NMService了,然后我们NMService借助NativeDaemonConnector工具类封装的LocalSocket完成了和Netd进程的消息传递

  • 消息传递到NMService之后,NMService还是要击鼓传花继续传递,然后NMService通过INetworkManagementEventObserver回调类将消息传递给已经注册的观察者门

  整个流程已将分析OK了,如果小伙伴还是有什么疑问或者有不清晰的地方,请参照下面的时序图在撸一篇:

1.6 NetworkManagementService如何向Netd传递消息

  通过前面的章节我们解决了一个世纪大难题即NMService如何接收Netd传递过来的消息,而我们知道NMService和Netd的交互是双向的,那么我们下面即将要下一个高峰冲锋了即NMService如何向Netd传递消息。而这其中也离不开NativeDaemonConnector的功劳了,都是它都是它来显示的!而NMService最终向Netd传递消息的终极接口如下:

   //NetworkManagementService.javapublic void setInterfaceConfig(String iface, InterfaceConfiguration cfg) {mContext.enforceCallingOrSelfPermission(CONNECTIVITY_INTERNAL, TAG);LinkAddress linkAddr = cfg.getLinkAddress();if (linkAddr == null || linkAddr.getAddress() == null) {throw new IllegalStateException("Null LinkAddress given");}final Command cmd = new Command("interface", "setcfg", iface,linkAddr.getAddress().getHostAddress(),linkAddr.getPrefixLength());for (String flag : cfg.getFlags()) {cmd.appendArg(flag);}try {mConnector.execute(cmd);//详见章节1.7} catch (NativeDaemonConnectorException e) {throw e.rethrowAsParcelableException();}}

即将传递过来的消息经过层层封装成Command 对象,最后借助NativeDaemonConnector工具类对象mConnector的execute方法将消息传递给Netd进程处理。

1.7 NativeDaemonConnector如何向Netd传递消息

  不来虚的直接上代码!

    public NativeDaemonEvent execute(long timeoutMs, String cmd, Object... args)throws NativeDaemonConnectorException {final NativeDaemonEvent[] events = executeForList(timeoutMs, cmd, args);if (events.length != 1) {throw new NativeDaemonConnectorException("Expected exactly one response, but received " + events.length);}return events[0];}public NativeDaemonEvent[] executeForList(long timeoutMs, String cmd, Object... args)throws NativeDaemonConnectorException {if (mWarnIfHeld != null && Thread.holdsLock(mWarnIfHeld)) {Slog.wtf(TAG, "Calling thread " + Thread.currentThread().getName() + " is holding 0x"+ Integer.toHexString(System.identityHashCode(mWarnIfHeld)), new Throwable());}final long startTime = SystemClock.elapsedRealtime();final ArrayList<NativeDaemonEvent> events = Lists.newArrayList();final StringBuilder rawBuilder = new StringBuilder();final StringBuilder logBuilder = new StringBuilder();final int sequenceNumber = mSequenceNumber.incrementAndGet();makeCommand(rawBuilder, logBuilder, sequenceNumber, cmd, args);final String rawCmd = rawBuilder.toString();final String logCmd = logBuilder.toString();log("SND -> {" + logCmd + "}");synchronized (mDaemonLock) {if (mOutputStream == null) {throw new NativeDaemonConnectorException("missing output stream");} else {try {mOutputStream.write(rawCmd.getBytes(StandardCharsets.UTF_8));} catch (IOException e) {throw new NativeDaemonConnectorException("problem sending command", e);}}}NativeDaemonEvent event = null;do {event = mResponseQueue.remove(sequenceNumber, timeoutMs, logCmd);if (event == null) {loge("timed-out waiting for response to " + logCmd);throw new NativeDaemonTimeoutException(logCmd, event);}if (VDBG) log("RMV <- {" + event + "}");events.add(event);} while (event.isClassContinue());final long endTime = SystemClock.elapsedRealtime();if (endTime - startTime > WARN_EXECUTE_DELAY_MS) {loge("NDC Command {" + logCmd + "} took too long (" + (endTime - startTime) + "ms)");}if (event.isClassClientError()) {throw new NativeDaemonArgumentException(logCmd, event);}if (event.isClassServerError()) {throw new NativeDaemonFailureException(logCmd, event);}return events.toArray(new NativeDaemonEvent[events.size()]);}

  这里可以看到最后会调用到NativeDaemonConnector的executeForList方法,这里里面的处理就比较简单了借助前面已经互通的LocalSocket将相关的命令发送给Netd进程进行相关的处理。整个的传递消息时序图如下所示:

小结

  NMService和Netd的交互过程分析完成,至此Android以太网框架情景分析之NetworkManagementService和Netd三者交互逻辑分析三分之二已经完成,还剩下最后的一小部分即Ethernet端如何借助NMService和Netd进行交互的,这个会在后面的篇章中讲述到。好了,万分不舍,也只能是下期见了!希望各位能多多点赞,或者评论,拍砖亦可!

  关于Ethernet端如何借助NMService和Netd进行交互的,详见博客EthernetServiceImpl和NetworkManagementService交互深入分析。

https://blog.csdn.net/tkwxty/article/details/107762121

文章来源https://blog.csdn.net/tkwxty/article/details/107814419?utm_medium=distribute.pc_category.none-task-blog-hot-2.nonecase&depth_1-utm_source=distribute.pc_category.none-task-blog-hot-2.nonecase&request_id=

关注我获取更多知识或者投稿

Android以太网框架情景分析之NetworkManagementService和netd交互深入分析二相关推荐

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

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

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

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

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

    Android以太网框架情景分析之NetworkFactory与NetworkAgent深入分析 Android网络框架分析系列文章目录: Android P适配以太网功能开发指南 Android以太 ...

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

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

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

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

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

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

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

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

  8. android MotionEvent.ACTION_CANCEL情景分析

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

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

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

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

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

最新文章

  1. android 电量控件,Android实现显示电量的控件代码
  2. 说说身边产品的用户体验
  3. JZOJ 5399. 【NOIP2017提高A组模拟10.7】Confess
  4. ORACLE TEXT LEXER PREFERENCE(四)
  5. mysql注入攻击实_三十三、MySQL基础系列笔记之MySQL安全问题与SQL注入攻击
  6. php数据表格的重载,layui数据表格实现重载数据表格功能(搜索功能)
  7. 关于__int128高精度运算
  8. 王道计算机组成原理 物理层整理 超详细版
  9. 配置dhcp服务器和dhcp中继
  10. 免费的3D GIS 软件,特点与应用领域介绍
  11. Activiti7工作流+Springboot快速入门
  12. 发动机冒黑烟_发动机冒黑烟常见的24个原因和解决方法!
  13. 基因结构图的0_TBtools | 只有序列,怎么做基因结构图?
  14. javascript书签工具
  15. 知识图谱和行业领域的结合产物-KGB知识图谱介绍
  16. 淘宝订单自动确认收货的N种实现,秒杀面试官
  17. TikZ示例——Venn图绘制(机器学习有关概念的关系)
  18. Python开发指南[1]之程序员计时小时钟(附源码)
  19. 化繁为简,我用”知晓推送”开发微信小程序订阅消息
  20. 软件需求工程 高校教学平台 测试报告

热门文章

  1. Vbox安装增强功能
  2. 运行metamascara时出现的一些错误
  3. 手机写java_手机怎么写java
  4. java从键盘输入若干学生的成绩_初学Java3:数组-从键盘录入若干学生的成绩,计算平均值,最大值,最小值...
  5. 欧空局:SNAP and the Sentinel Toolboxes下载网址
  6. python初学者-计算小于100的最大素数
  7. 液化石油气(LPG)的全球与中国市场2022-2028年:技术、参与者、趋势、市场规模及占有率研究报告
  8. cad图纸问号怎么转换文字_打开cad2016图纸字体和符号显示问号怎么办?
  9. 各地大厂名单(一二线城市知名公司)
  10. VB中关于Array函数与Split函数