全方位了解8.0系统下的Handler
/ 今日科技快讯 /
8月11日,有网友分享称,日前小米电视推送固件更新,取消了开机广告。开机广告取消后,电视启动过程中屏幕出现整体为蓝色的静态壁纸,屏幕中部偏下位置是一个伸开双臂仰望星空的人。知乎认证为小米产品运营经理发文称,没有看到小米电视广告,很有可能是你今天看广告的次数已达到了频率限制,或者今天没有广告排期。
/ 作者简介 /
本篇文章来自MxsQ的投稿,分享了他对Handler的理解,相信会对大家有所帮助!同时也感谢作者贡献的精彩文章。
MxsQ的博客地址:
https://www.jianshu.com/u/9cf1f31e1d09
/ 前言 /
Handler不管是作为一种消息机制,还是作为切换线程的手段,在Android中都有充足的应用场景。在不了解Handler原理的情况下,仅知道上层API如何发送消息,如何处理消息,加之了解一些Handler容易造成的问题以及应对策略,实际上也不会造成什么大问题。
Handler的戏份比上面所描述的要重,因为一个APP的运行过程,是不断接受消息以及处理消息的过程。比如Activity,从启动、创建、生命周期回调、销毁,都是借由Handler发送消息来驱动完成。从一个APK的安装,到一个View的更新,都离不开Handler的帮助。
如果对于以下问题有疑问,那这篇文章可能有借鉴价值:
- Handler如何保证运行在目标线程
- Handler容易造成内存泄漏的原因
- loop()为什么不会阻塞,CPU为什么不会忙等
- MessageQueue如何存储
- Message如何缓存
- 什么是线程空闲消息
- 线程如何使用Handler机制
note: 本文源码版本为8.0
/ Handler 如何运行 /
Handler角色分配
Handler中存在四种角色。
Handler
Handler用来向Looper发送消息,在Looper处理到对应的消息时,Handler在对消息进行具体的处理。上层关键API为handleMessage(),由子类自行实现处理逻辑。
Looper
Looper运行在目标线程里,不断从消息队列MessageQueue读取消息,分配给Handler处理。Looper起到连接的作用,将来自不同渠道的消息,聚集在目标线程里处理。也因此Looper需要确保线程唯一。
MessageQueue
存储消息对象Message,当Looper向MessageQueue获取消息,或Handler向其插入数据时,决定消息如何提取、如何存储。不仅如此,MessageQueue还维护与Native端的连接,也是解决Looper.loop() 阻塞问题的 Java 端的控制器。
Message
Message包含具体的消息数据,在成员变量target中保存了用来发送此消息的Handler引用。因此在消息获得这行时机时,能知道具体由哪一个Handler处理。此外静态成员变量sPool,则维护了消息缓存池以复用。
/ 运行过程 /
首先,需要构建消息对象。获取消息对象从Handler.obtainMessage()系列方法可以获取Message,这一系列的函数提供了相应对应于Message对象关键成员变量对应的函数参数,而无论使用哪一个方法获取,最终通过Message.obtain()获取具体的Message对象。
// 缓存池 private static Message sPool; // 缓存池当前容量 private static int sPoolSize = 0; // 下一节点 Message next; public static Message obtain() { // 确保同步 synchronized (sPoolSync) { if (sPool != null) { // 缓存池不为空 Message m = sPool; // 缓存池指向下一个Message节点 sPool = m.next; // 从缓存池拿到的Message对象与缓存断开连接 m.next = null; m.flags = 0; // clear in-use flag // 缓存池大小减一 sPoolSize--; return m; } } // 缓存吃没有可用对象,返回新的Message() return new Message(); }
Message成员变量中存在类型为Message的next,可以看出Message为链表结构,而上面代码从缓存池里获取消息对象的过程可以用下图描述:
创建出消息之后,通过Handler将消息发送到消息队列,发送方法有很多,不一一陈列。发送发上有两种:
- 将Message对象发送到
- 发送Runnable,通过getPostMessage()将Runnable包装在Message里,表现为成员变量callback
private static Message getPostMessage(Runnable r) { // 获取Message Message m = Message.obtain(); // 记住Runnale,等消息获得执行时回调 m.callback = r; return m; }
不管哪种方式发送,最终消息队列MessageQueue只知接受到了消息对象Message。而将消息加入到消息队列,最终通过enqueueMessage()加入。
private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { // Message.target 记住 Handler 以明确是由哪一个Handler来处理这个消息的 msg.target = this; if (mAsynchronous) { msg.setAsynchronous(true); } // 消息入队 return queue.enqueueMessage(msg, uptimeMillis); }
在将消息加入消息队列时,有时需要提供延迟信息delayTime,以期未来多久后执行,这个值存于 uptimeMillis。
之后,等待Looper轮询从消息队列中读取消息进行处理。见Looper.loop()。
public static void loop() { // 拿到Looper final Looper me = myLooper(); if (me == null) { // 没调用prepare初始化Looper,报错 throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread."); } // 拿到消息队列 final MessageQueue queue = me.mQueue; ...... for (;;) { // 从消息队列取出下一个信息 Message msg = queue.next(); if (msg == null) { // 消息为空,返回 return; } ....... try { // 分发消息到Handler msg.target.dispatchMessage(msg); end = (slowDispatchThresholdMs == 0) ? 0 : SystemClock.uptimeMillis(); } // 消息回收,放入缓存池 msg.recycleUnchecked(); }
Looper从MessageQueue里取出Message,Message.target则是具体的Hander,Handler.dispatchMessage()将触发具体分配逻辑。此后,将Message回收,放入缓存池。
public void dispatchMessage(Message msg) { if (msg.callback != null) { // 这个情况说明了次消息为Runnable,触发Runnable.run() handleCallback(msg); } else { if (mCallback != null) { // 指定了Handler的mCallback if (mCallback.handleMessage(msg)) { return; } } // 普通消息处理 handleMessage(msg); } }
Handler分配消息分三种情况:
- 可以通过Handler发送Runnable消息到消息队列,因此handleCallback()处理这种情况
- 可以给Handler设置Callback,当分配消息给Handler时,Callback可以优先处理此消息,如果Callback.handleMessage()返回了true,不再执行Handler.handleMessage()
- Handler.handleMessage()处理具体逻辑
回收则是通过Message.recycleUnchecked()。
void recycleUnchecked() { // 这里是将Message各种属性重置操作 ...... synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) { // 缓存池还能装下,回收到缓存池 // 下面操作将此Message加入到缓存池头部 next = sPool; sPool = this; sPoolSize++; } } }
通过上面的分析,Handler的运行如下图。
- Handler 从缓存池获取Message,发送到MessageQueue
- Looper不断从MessageQueue读取消息,通过Message.target.dispatchMessage()触发Handler处理逻辑
- 回收Message到缓存池
目前来看,可以算是了解了Handler的运行机制,但是对于解答开篇提出的问题,捉襟见肘,需要深入Handler。
/ Java端与Native端建立连接 /
实际上,不仅仅是Java端存在Handler机制,在Native端同样存在Handler机制。他们通过MessageQueue建立了连接。
一般来说,Looper通过prepare()进行初始化。
private static void prepare(boolean quitAllowed) { // 保证Looper在线程唯一 if (sThreadLocal.get() != null) { throw new RuntimeException("Only one Looper may be created per thread"); } // 将Looper放入ThreadLocal sThreadLocal.set(new Looper(quitAllowed)); }
在实例化Looper时,需要确保Looper在线程里是唯一的。Handler知道自己的具体Looper对象,而Looper运行在具体的线程里并在此线程里处理消息。这也是为什么Looper能达到切换线程的目的。Looper线程唯一需要ThreadLocal来确保,ThreadLocal的原理,简单来说Thread里有类型为ThreadLocalMap的成员threadLocals,通过ThreadLocal能将相应对象放入threadLocals里通过K/V存储,如此能保证变量在线程范围内存储,其中Key为ThreadLocal< T > 。
private Looper(boolean quitAllowed) { // 初始化MessageQueue mQueue = new MessageQueue(quitAllowed); // 记住当前线程 mThread = Thread.currentThread(); } MessageQueue(boolean quitAllowed) { mQuitAllowed = quitAllowed; // 与Native建立连接 mPtr = nativeInit(); }
在MessageQueue创建时,通过native方法nativeInit()与Native端建立了连接,mPtr为long型变量,存储一个地址。方法实现文件位于frameworks/base/core/jni/android_os_MessageQueue.cpp。
static jlong android_os_MessageQueue_nativeInit(JNIEnv* env, jclass clazz) { NativeMessageQueue* nativeMessageQueue = new NativeMessageQueue(); if (!nativeMessageQueue) { jniThrowRuntimeException(env, "Unable to allocate native queue"); return 0; } nativeMessageQueue->incStrong(env); // 返回给Java层的mPtr, NativeMessageQueue地址值 return reinterpret_cast<jlong>(nativeMessageQueue);} NativeMessageQueue::NativeMessageQueue() : mPollEnv(NULL), mPollObj(NULL), mExceptionObj(NULL) { mLooper = Looper::getForThread(); // 检查Looper 是否创建 if (mLooper == NULL) { mLooper = new Looper(false); // 确保Looper唯一 Looper::setForThread(mLooper); }}
在Native端创建了NativeMessageQueue,同样也创建了Native端的Looper。在创建NativeMessageQueue后,将它的地址值返回给了Java层MessageQueue.mPtr。实际上,Native端Looper实例化时做了更多事情。Nativ端Looper文件位于system/core/libutils/Looper.cpp。
Looper::Looper(bool allowNonCallbacks) : mAllowNonCallbacks(allowNonCallbacks), mSendingMessage(false), mPolling(false), mEpollFd(-1), mEpollRebuildRequired(false), mNextRequestSeq(0), mResponseIndex(0), mNextMessageUptime(LLONG_MAX) { // 添加到epoll的文件描述符,线程唤醒事件的fd mWakeEventFd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); LOG_ALWAYS_FATAL_IF(mWakeEventFd < 0, "Could not make wake event fd: %s", strerror(errno)); AutoMutex _l(mLock); rebuildEpollLocked();} void Looper::rebuildEpollLocked() { ..... // Allocate the new epoll instance and register the wake pipe. // 创建epolle实例,并注册wake管道 mEpollFd = epoll_create(EPOLL_SIZE_HINT); LOG_ALWAYS_FATAL_IF(mEpollFd < 0, "Could not create epoll instance: %s", strerror(errno)); struct epoll_event eventItem; // 清空,把未使用的数据区域进行置0操作 memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union // 监听可读事件 eventItem.events = EPOLLIN; // 设置作为唤醒评判的fd eventItem.data.fd = mWakeEventFd; // 将唤醒事件(mWakeEventFd)添加到epoll实例,意为放置一个唤醒机制 int result = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, mWakeEventFd, & eventItem); LOG_ALWAYS_FATAL_IF(result != 0, "Could not add wake event fd to epoll instance: %s", strerror(errno)); // 添加各种事件的fd到epoll实例,如键盘、传感器输入等 for (size_t i = 0; i < mRequests.size(); i++) { const Request& request = mRequests.valueAt(i); struct epoll_event eventItem; request.initEventItem(&eventItem); int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, request.fd, & eventItem); if (epollResult < 0) { ALOGE("Error adding epoll events for fd %d while rebuilding epoll set: %s", request.fd, strerror(errno)); } }}
初次见面,上面的代码让人迷糊,因为缺少了关键的知识点,epoll机制。
如何理解epoll机制?
文件、socket、pipe(管道)等可以进行I/O操作的对象可以视为流。既然是I/O操作,则有read端读入数据,有write端写入数据。但是两端并不知道对方进行操作的时机。而epoll则能观察到哪个流发生了了I/O事件,并进行通知。
这个过程,就好比你在等快递,但你不知道快递什么时候来,那这时你可以去睡觉,因为你知道快递送来时一定会打个电话叫醒你,让你拿快递,接着做你想的事情。epoll有效地降低了CPU的使用,在线程空间时令其休眠,等有事件到来时再讲它唤醒。
在知道了epoll之后,再来看上面的代码,就可以理解了。在Native端创建Looper时,会创建用来唤醒线程的fd —— mWakeEventFd,创建epoll实例并注册管道,清空管道数据,监听可读事件。当有数据写入mWakeEventFd描述的文件时,epoll能监听到此事件,并通知将目标线程唤醒。
在Java端MessageQueue.mPrt存储了Native端NativeMassageQueue的地址,可以利用NativeMassageQueue享用此机制。
/ 发送数据的具体过程 /
之前说过,Handler发送消息时,最终通过MessageQueue.enqueueMessage向消息队列中插入消息,下面为具体代码。
boolean enqueueMessage(Message msg, long when) { ...... synchronized (this) { ...... // 记录消息处理的时间 msg.when = when; Message p = mMessages; // 唤醒线程的标志位 boolean needWake; if (p == null || when == 0 || when < p.when) { // 这里三种情况: // 1、目标消息队列是空队列 // 2、插入的消息处理时间等于0 // 3、插入的消息处理时间小于保存在消息队列头的消息处理时间 // 这三种情况都插入列表头 msg.next = p; mMessages = msg; // mBlocked 表示当前线程是否睡眠 needWake = mBlocked; } else { // 这里则说明消息处理时间大于消息列表头的处理时间,因此需要找到合适的插入位置 needWake = mBlocked && p.target == null && msg.isAsynchronous(); Message prev; // 这里的循环是找到消息的插入位置 for (;;) { prev = p; p = p.next; // 到链表尾,或处理时间早于p的时间 if (p == null || when < p.when) { break; } if (needWake && p.isAsynchronous()) { // 如果插入的消息在目标队列中间,是不需要检查改变线程唤醒状态的 needWake = false; } } // 插入到消息队列 msg.next = p; prev.next = msg; } if (needWake) { // 唤醒线程 nativeWake(mPtr); } } return true; }
消息队列里的消息也是以链表形式存储,存储顺序则按照处理的时间顺序。那么在向消息队列里插入数据时,存在四种情况:
- 目标消息队列是空队列
- 插入的消息处理时间等于0
- 插入的消息处理时间小于保存在消息队列头的消息处理时间
- 插入的消息处理时间大于消息队列头的消息处理时间
前三种情况,将消息插入消息队列头的位置,在这种情况下,因为不能保证当前消息是否达到了可以处理的状态,且如果此时线程是睡眠的,则需要调用nativeWake()将其线程唤醒。后一种情况,则需要找到消息的插入位置,因不影响线程状态而需要改变线程状态。插入消息如图。
mPtr保存了NativeMessageQueue的地址,所以Native可以知道具体操作的NativeMessageQueue,当前用它来唤醒线程,实际调用链为MessageQueue.cpp.nativeWake()到MessageQueue.cpp.wake()到Looper.cpp.wake()。
void Looper::wake() {#if DEBUG_POLL_AND_WAKE ALOGD("%p ~ wake", this);#endif uint64_t inc = 1; // 向管道写入一个新数据,这样管道因为发生了IO事件被唤醒 ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd, &inc, sizeof(uint64_t))); if (nWrite != sizeof(uint64_t)) { if (errno != EAGAIN) { LOG_ALWAYS_FATAL("Could not write wake signal to fd %d: %s", mWakeEventFd, strerror(errno)); } }}
实现也简单,向mWakeEventFd文件里写入一个数据,根据epoll机制监听到此次I/O事件,将线程唤醒。
/ 消息读取 /
Looper不断从MessageQueue读取消息进行处理,通过MessageQueue.next()进行读取。
Message next() { final long ptr = mPtr; if (ptr == 0) { // 获取NativeMessageQueue地址失败,无法正常使用epoll机制 return null; } // 用来保存注册到消息队列中的空闲消息处理器(IdleHandler)的个数 int pendingIdleHandlerCount = -1; // 如果这个变量等于0,表示即便消息队列中没有新的消息需要处理,当前 // 线程也不要进入睡眠等待状态。如果值等于-1,那么就表示当消息队列中没有新的消息 // 需要处理时,当前线程需要无限地处于休眠等待状态,直到它被其它线程唤醒为止 int nextPollTimeoutMillis = 0; for (;;) { ...... // 检查当前线程的消息队列中是否有新的消息需要处理,尝试进入休眠 nativePollOnce(ptr, nextPollTimeoutMillis); synchronized (this) { // 当前时间 final long now = SystemClock.uptimeMillis(); Message prevMsg = null; // mMessages 表示当前线程需要处理的消息 Message msg = mMessages; if (msg != null && msg.target == null) { // 找到有效的Message do { prevMsg = msg; msg = msg.next; } while (msg != null && !msg.isAsynchronous()); } if (msg != null) { /** * 检查当前时间和消息要被处理的时间,如果小于当前时间,说明马上要进行处理 */ if (now < msg.when) { // 还没达到下一个消息需要被处理的时间,计算需要休眠的时间 nextPollTimeoutMillis = (int) Math.min(msg.when - now, Integer.MAX_VALUE); } else { // 有消息需要处理 // 不要进入休眠 mBlocked = false; if (prevMsg != null) { prevMsg.next = msg.next; } else { // 指向下一个需要处理的消息 mMessages = msg.next; } msg.next = null; if (DEBUG) Log.v(TAG, "Returning message: " + msg); msg.markInUse(); return msg; } } else { // 没有更多消息,休眠时间无限 nextPollTimeoutMillis = -1; } ...... if (pendingIdleHandlerCount < 0 && (mMessages == null || now < mMessages.when)) { // 获取IdleHandler数 pendingIdleHandlerCount = mIdleHandlers.size(); } if (pendingIdleHandlerCount <= 0) { // 没有IdleHandler需要处理,可直接进入休眠 mBlocked = true; continue; } if (mPendingIdleHandlers == null) { mPendingIdleHandlers = new IdleHandler[Math.max(pendingIdleHandlerCount, 4)]; } mPendingIdleHandlers = mIdleHandlers.toArray(mPendingIdleHandlers); } // 如果没有更多要进行处理的消息,在休眠之前,发送线程空闲消息给已注册到消息队列中的IdleHandler对象来处理 for (int i = 0; i < pendingIdleHandlerCount; i++) { final IdleHandler idler = mPendingIdleHandlers[i]; mPendingIdleHandlers[i] = null; // release the reference to the handler boolean keep = false; try { // 处理对应逻辑,并由自己决定是否保持激活状态 keep = idler.queueIdle(); } catch (Throwable t) { Log.wtf(TAG, "IdleHandler threw exception", t); } if (!keep) { // 不需要存活,移除 synchronized (this) { mIdleHandlers.remove(idler); } } } // 重置IdleHandler数量 pendingIdleHandlerCount = 0; /** * 这里置0,表示下一次循环不能马上进入休眠状态,因为IdleHandler在处理事件的时间里, * 有可能有新的消息发送来过来,需要重新检查。 */ nextPollTimeoutMillis = 0; } }
分为两种情况处理:
取到消息Message时
需要查看当前时间是否达到了Message处理的时间,如果达到了则返回,改变mMessages指向下一消息。如果没达到,则计算要达到处理的时间,还需要休眠多久,并进行休眠。
没有更多Message时
当消息队列里没有消息时,则会检查是否有IdleHandler需要处理。在Handler机制里,允许添加一些事件,在线程空闲时进行处理,表现为IdleHandler,可以通过MessageQueue.addIdleHandler添加。当有IdleHandler需要处理,则在IdleHandler处理完后,线程不能马上进入休眠状态,在此期间可能已有新消息加入消息队列,需要重新做检查。如果没有IdleHandler,则可以进入休眠。
线程休眠调用链为NativeMessageQueue.nativePollOnce()到NativeMessageQueue.pollOnce()到Looper.pollOnce()到Looper.pollInner()。
int Looper::pollInner(int timeoutMillis) {...... // 这个是用来监听实例化时创建的epoll实例的文件描述符的IO读写事件 struct epoll_event eventItems[EPOLL_MAX_EVENTS]; // 如果没有事件,进入休眠,timeoutMillis为休眠事件 int eventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS, timeoutMillis); ...... /** * 检测是哪一个文件描述符发生了IO读写事件 */ for (int i = 0; i < eventCount; i++) { int fd = eventItems[i].data.fd; uint32_t epollEvents = eventItems[i].events; if (fd == mWakeEventFd) { if (epollEvents & EPOLLIN) { // 如果文件描述符为mWakeEventFd,并且读写事件类型为EPOLLIN,说明 // 当前线程所关联的一个管道被写入了一个新的数据 // 唤醒 awoken(); } } ...... }}
Java层提供了线程休眠时间timeoutMillis,通过epoll_wait()让线程进行休眠。当线程被唤醒后,查看文件描述符,如果为mWakeEventFd并且为I/O事件,则说明当当前线程所关联的一个管道被写入了一个新的数据,通过awoken()处理。当前线程已是唤醒状态,awoken()则是将管道中的数据读出达到清理目的,但并不关心数据什么。核心目的是唤醒线程。
/ 总结 /
Handler机制更具体的原理如图:
- Looper通过prepare()创建,借助ThreadLocal保证线程唯一,如果没有进行prepare(),调用Loop()会抛出异常
- Looper在实例化时创建MessageQueue,MessageQueue与NativeMessageQueue建立连接,NativeMessageQueue存储地址存于MessageQueue.mPtr。Native端也建立了Handler机制,使用epoll机制。Java端借由NativeMessageQueue能达到使用epoll机制的目的
- 从Message缓存里获取Message,缓存为链表存储,从头出取出,并且Message在回收时也是插入头部。如果存缓存里取不到,则新建
- Handler向MessageQueue插入消息,如果消息插入消息队列头部,需要唤醒线程;如果插入消息队列中,无需改变线程状态
- Looper.loop() 不断从消息队列获取消息,消息队列获取消息时会出现两种情况。如果取到消息,但没达到处理时间,则让线程休眠;如果没有更多消息,则在处理IdleHandler事后,在考虑让线程进入休眠
- Message达到了可处理状态,则有Handler处理,处理时考虑三种情况,消息内容为Runnable时、设置了Handle.Callback时、普通消息时,对应调用为Message.callback.run() 、 Callback.handleMessage()、Handler.handleMessage()
- 从Handler机制里,epoll可以简单理解为,当Handler机制没有消息要处理时,让线程进入休眠,当Handler机制有消息要处理时,将线程唤起。通过Native端监听mWakeEventFd的I/O事件实现
/ 答疑 /
这里对文章一开始的问题进行回答。
Handler如何保证运行在目标线程
Looper在实例化时通过ThreadLocal保证线程唯一。Looper运行在目标线程中,接收Handler发送的消息并进行处理。Message创建时与具体的Handler进行了关联,因此能知道由哪一个Handler进行相应。
Handler容易造成内存泄漏的原因
Message.target存有Handler的引用,以知道自身由哪一个Handler来处理。因此,当Handler为非静态内部类、或持有关键对象的其它表现形式时(如Activity常表现为Context),就引用了其它外部对象。当Message得不到处理时,被Handler持有的外部对象会一直处于内存泄漏状态。
loop()为什么不会阻塞,CPU为什么不会忙等
通过epoll机制监听文件I/O时间,在有Message需要处理时,写入数据以唤醒线程;在没有Message要处理时,让线程进入休眠状态。
MessageQueue如何存储
以链表存储,MessageQueue.mMessages指向头节点。
Message如何缓存
以链表缓存,取出时从头部取出,回收时插入头部。
什么是线程空闲消息
Handler提供的一种机制,允许添加IdleHandler事件。并在没有更多Message要处理,要进入休眠前,让IdleHandler做具体事情,也就是线程空间时处理的事件。
子线程如何使用Handler机制
只要保证在子线程先执行Looper.prepare()再使用Looper.Loop()即可,但实际应用场景不多。顺便提一句,主线程初始化Looper操作在ActivityTread.main()里触发,简单了解即可。
推荐阅读:好奇一键登录是怎么实现的吗?进来了解一下?
动画代码太丑,用Kotlin DSL来拯救!
由Android官方团队带你学习Android Studio的布局编辑器
欢迎关注我的公众号学习技术或投稿
长按上图,识别图中二维码即可关注
全方位了解8.0系统下的Handler相关推荐
- 461在全志r16平台tinav3.0系统下使用地磁计QMC5883L
461在全志r16平台tinav3.0系统下使用地磁计QMC5883L 2018/9/7 14:08 版本:V1.0 开发板:SC3817R SDK:tina v3.0 (基本确认全志tina v3. ...
- android9.0系统下,如何保活
android9.0系统下,讨论如何延长APP退到后台的保活/复活时间 一.7.0及以上不存在真正意义的保活. 二.盘点目前在9.0上,可能有效的"白色手段"保活手段(这里不讨论黑 ...
- android设置静态i,关于Android4.0系统下如何设置wlan静态ip的设置(转)
该楼层疑似违规已被系统折叠 隐藏此楼查看此楼 估计有不少人都以为Android4.0系统下如何设置wlan静态ip地址吧,其实不是Android4.0的问题,只是设置上有点区别而已,下面重新教大家如何 ...
- 在WinCE 6.0系统下实现USB功能定制
USB的广泛应用就不用多说了,相信目前的各个领域的嵌入式产品中,很少有不用USB的.USB是主从结构的,分为USB Host和USB Slave,从USB1.0,USB1.1到现在的USB2.0,基于 ...
- Centos7.0系统下Rsync+sersync实现多文件数据实时增量同步
前言: 一.为什么要用Rsync+sersync架构? 1.sersync是基于Inotify开发的,类似于Inotify-tools的工具 2.sersync可以记录下被监听目录中发生变化的(包括增 ...
- android9 apk自动安装功能,如何在Android7.0、8.0、9.0系统下通过Intent安装apk
public static void installApk(Context context, String apkPath) { if (context == null || TextUtils.is ...
- CentOS 6.0 系统 LAMP(Apache+MySQL+PHP)安装步骤
转自:http://down.chinaz.com/server/201109/1064_1.htm 先来解释一下,什么是 LAMP.正如标题所言,LAMP 实际上就是 Linux.Apache.My ...
- 如何在Linux系统下的IntelliJ IDEA 2018.3.5下载与安装以及激活教程
如何在Linux系统下的IntelliJ IDEA 2018.3.5下载与安装以及激活教程 作者:张国军_Suger 开发工具与关键技术:VMware Workstation Pro.Linux系统( ...
- OPPO Find5 X909T ColorOS1.0系统精简笔记
OPPO Find5 X909T ColorOS1.0系统精简笔记 挂载 system 分区 在ColorOS 1.0 系统下,进入官方Recovery后即可使用 ADB 连接手机,adb shell ...
- Ubuntu 16.04系统下CUDA8.0配置Caffe教程
由于最近安装了Ubuntu 16.04,本文教程的特点是不需要降级gcc的版本,毕竟cuda8.0已经支持gcc5以上(默认不支持,实际支持). 本文是在参考caffe官网教程以及http://www ...
最新文章
- 清华、北大教授同台激辩:脑科学是否真的能启发AI?
- 【Android 逆向】Dalvik 函数抽取加壳 ( 类加载流程分析 | native 函数查询 | dalvik_system_DexFile.cpp#defineClassNative 函数 )
- 【项目实战课】基于Pytorch的SRGAN图像超分辨实战
- list::splice函数的用法与参数解释
- 同事说rar压缩有风险,让我用zip压缩文件
- div动态消失的动画效果
- Android笔记 消息机制handler+http之 网络图片浏览器demo
- 图片配置文件设置 索尼a7s2_16组Sony索尼系列相机Slog2和Slog3常用Vlog灰片视频电影LTUS调色预设...
- Mysql中各种常见数据库存储引擎对比
- 29.2. Ubuntu
- 解决PHP不打印任何东西浏览器输出字符串
- APP版本号命名规范及原则
- ShowAPI识别验证码
- Linux用户家目录与根目录
- Qt系列文章之 QAbstractItemModel(下)
- 备份微信聊天记录到电脑上,并且可以随时导回
- EasyLearn--JAVA实现32个经典算法设计(二):集束算法
- ​LeetCode刷题实战375:猜数字大小 II
- string.h头文件
- JESD204协议理解
热门文章
- 万彩动画大师-零基础视频教程-笔记
- Spring Bean的作用域
- 基于导频的信道估计实现
- 孙过庭草书:《景福殿赋》(图像古昔,以当箴规),韵味十足!
- java升职之路_初级Java程序员的发展及晋升之路
- Laser Safety
- 京瓷计算机无法打印机驱动程序,京瓷6525打印机驱动(修复京瓷6525打印机连接故障)V1.0 免费版...
- MATLAB结构模态分析
- 安卓使用富文本编辑器html5,Android富文本编辑器,图文详细
- NMF非负矩阵分解算法(Non-negative Matrix Factorization)