Native層消息機(jī)制深入探究實(shí)例解析
引言
在分析底層源碼時(shí),時(shí)不時(shí)會(huì)碰到 Looper::wake() 或者 Looper::pollOnce() 這樣的代碼,之前大概知道是 Native 層的消息循環(huán)機(jī)制。為了以后我也能夠使用它,我決定還是徹底分析一遍源碼。
本文只涉及一個(gè)文件,路徑如下
system/core/libutils/Looper.cpp
Looper的創(chuàng)建
在 Java 層,有一個(gè)線程的子類 HandlerThread,它可以創(chuàng)建一個(gè)線程,并且使用消息機(jī)制,其中的關(guān)鍵兩步是 Looper::prepare() 和 Looper::loop()。
Looper::prepare() 會(huì)創(chuàng)建一個(gè)與線程綁定的 Looper 對(duì)象,這個(gè)綁定是通過(guò) ThreadLocal實(shí)現(xiàn)的,而 Looper::loop() 會(huì)讓線程進(jìn)入無(wú)限循環(huán)來(lái)處理消息。
在 Native 層,也有一個(gè) Looper 類,可以通過(guò) Looper::prepare() 來(lái)創(chuàng)建 Looper 對(duì)象,代碼如下
sp<Looper> Looper::prepare(int opts) {
// opts決定Looper::addFd()的參數(shù)callback是否可以為空
bool allowNonCallbacks = opts & PREPARE_ALLOW_NON_CALLBACKS;
// 通過(guò)pthread_getspecific()獲取線程本地存儲(chǔ)中的Looper對(duì)象
sp<Looper> looper = Looper::getForThread();
if (looper == nullptr) {
looper = new Looper(allowNonCallbacks);
// 通過(guò)pthread_setspecific()把Looper對(duì)象保存到線程本地存儲(chǔ)中
Looper::setForThread(looper);
}
return looper;
}
Native 層的線程在首次調(diào)用 Looper::prepare() 時(shí),會(huì)創(chuàng)建 Looper 對(duì)象,并通過(guò) pthread_setspecific() 把它保存到線程本地存儲(chǔ)中,后面再獲取 Looper 對(duì)象時(shí),通過(guò) pthread_getspecific() 從線程本地存儲(chǔ)中獲取。
這不就是 Java 的 ThreadLocal 的功能嗎?
現(xiàn)在讓我們來(lái)看下 Looper 對(duì)象的創(chuàng)建過(guò)程
Looper::Looper(bool allowNonCallbacks)
: mAllowNonCallbacks(allowNonCallbacks),
mSendingMessage(false),
mPolling(false),
mEpollRebuildRequired(false),
mNextRequestSeq(0),
mResponseIndex(0),
mNextMessageUptime(LLONG_MAX) {
// 1. 創(chuàng)建 eventfd 對(duì)象,用于喚醒 Looper
mWakeEventFd.reset(eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC));
AutoMutex _l(mLock);
// 2. 創(chuàng)建 epoll 對(duì)象,并監(jiān)聽(tīng)剛才創(chuàng)建的 eventfd 的輸入事件
rebuildEpollLocked();
}
首先創(chuàng)建了一個(gè)eventfd 對(duì)象,由 mWakeEventFd 代表,它用于喚醒 Looper 。如何喚醒呢? 繼續(xù)往下看。
然后調(diào)用 rebuildEpollLocked() 創(chuàng)建一個(gè) epoll 對(duì)象,并監(jiān)聽(tīng)剛才創(chuàng)建的 mWakeEventFd 的 I/O 事件,代碼如下
void Looper::rebuildEpollLocked() {
// ...
// 創(chuàng)建epoll對(duì)象
mEpollFd.reset(epoll_create1(EPOLL_CLOEXEC));
// 監(jiān)聽(tīng) mWakeEventFd 輸入事件
struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event)); // zero out unused members of data field union
eventItem.events = EPOLLIN;
eventItem.data.fd = mWakeEventFd.get();
int result = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, mWakeEventFd.get(), &eventItem);
for (size_t i = 0; i < mRequests.size(); i++) {
// ...
}
}
epoll 對(duì)象監(jiān)聽(tīng)了 mWakeEventFd 的可讀事件。在后面的分析中,我們將看到,當(dāng) Looper 開(kāi)始輪詢時(shí),會(huì)調(diào)用 epoll_wait() 阻塞地等待事件,那么有人向 mWakeEventFd 寫(xiě)入數(shù)據(jù)時(shí),epoll_wait() 將返回,那么 Looper 就被喚醒。
發(fā)送消息與監(jiān)聽(tīng)請(qǐng)求
Native 層的 Looper 可以處理兩種類型的事件,一種是消息( Message ),另一種是請(qǐng)求( Request )。下面我們來(lái)看看如何向 Looper 發(fā)送消息,如何讓 Looper 監(jiān)聽(tīng)請(qǐng)求。
發(fā)送消息
通過(guò) Looper::sendMessageXXX() 這一類函數(shù),可以向 Looper 發(fā)送消息
void Looper::sendMessageAtTime(nsecs_t uptime, const sp<MessageHandler>& handler,
const Message& message) {
size_t i = 0;
{ // acquire lock
// 通過(guò)鎖,可以保存 mMessageEnvelopes 的線程安全
AutoMutex _l(mLock);
// 獲取消息要插入的位置
size_t messageCount = mMessageEnvelopes.size();
while (i < messageCount && uptime >= mMessageEnvelopes.itemAt(i).uptime) {
i += 1;
}
// 1. 把信息保存到 mMessageEnvelopes 中
MessageEnvelope messageEnvelope(uptime, handler, message);
mMessageEnvelopes.insertAt(messageEnvelope, i, 1);
// mSendingMessage 表明 Looper 正在處理消息,因此不用喚醒Looper
if (mSendingMessage) {
return;
}
} // release lock
// 2. 如有必要,就喚醒Looper
if (i == 0) {
wake();
}
}
當(dāng) Looper 接收到消息時(shí),它會(huì)把消息保存到 mMessageEnvelopes 容器中,并且如果有必要,那么會(huì)調(diào)用 Looper::wake() 喚醒 Looper 來(lái)處理消息。
前面我們大概地說(shuō)明了下如何通過(guò) mWakeEventFd 這個(gè) eventfd 對(duì)象喚醒 Looper,現(xiàn)在讓我們來(lái)看下喚醒是如何實(shí)現(xiàn)的
void Looper::wake() {
uint64_t inc = 1;
// 向 mWakeFd 中寫(xiě)入數(shù)據(jù)
// TEMP_FAILURE_RETRY 是一個(gè)重試機(jī)制,確保不會(huì)因?yàn)橄到y(tǒng)中斷而導(dǎo)致數(shù)據(jù)沒(méi)有寫(xiě)入
ssize_t nWrite = TEMP_FAILURE_RETRY(write(mWakeEventFd.get(), &inc, sizeof(uint64_t)));
// 確保寫(xiě)入的是 unsigned 64-bit 的數(shù)據(jù)
if (nWrite != sizeof(uint64_t)) {
if (errno != EAGAIN) {
LOG_ALWAYS_FATAL("Could not write wake signal to fd %d (returned %zd): %s",
mWakeEventFd.get(), nWrite, strerror(errno));
}
}
}
原來(lái)就是向 mWakeEventFd 寫(xiě)數(shù)據(jù)。在后面我們將看到,當(dāng) Looper 進(jìn)入輪詢時(shí), 當(dāng)epoll_wait() 檢測(cè)到 mWakeEventFd 有數(shù)據(jù)可讀時(shí),就會(huì)從阻塞中醒來(lái),從而達(dá)到 mWakeEventFd 喚醒 Looper 的目的。
監(jiān)聽(tīng)請(qǐng)求
現(xiàn)在我們來(lái)看下如何讓 Looper 監(jiān)聽(tīng)請(qǐng)求,它是通過(guò) Looper::addFd() 實(shí)現(xiàn)的
int Looper::addFd(int fd, int ident, int events, const sp<LooperCallback>& callback, void* data) {
// 1. 確認(rèn) ident 的值
if (!callback.get()) { // 回調(diào)為空
if (! mAllowNonCallbacks) { // mAllowNonCallbacks是在創(chuàng)建Looper時(shí)初始化的
ALOGE("Invalid attempt to set NULL callback but not allowed for this looper.");
return -1;
}
if (ident < 0) { // 回調(diào)為空,ident的值不能小于0
ALOGE("Invalid attempt to set NULL callback with ident < 0.");
return -1;
}
} else { // 回調(diào)不為空
// POLL_CALLBACK值為-2,表示請(qǐng)求通過(guò)回調(diào)處理請(qǐng)求
ident = POLL_CALLBACK;
}
{ // acquire lock
AutoMutex _l(mLock);
// 2. 包裝成一個(gè) Request
Request request;
request.fd = fd;
request.ident = ident;
request.events = events;
request.seq = mNextRequestSeq++;
request.callback = callback;
request.data = data;
if (mNextRequestSeq == -1) mNextRequestSeq = 0; // reserve sequence number -1
struct epoll_event eventItem;
request.initEventItem(&eventItem);
// 3. 用 epoll 對(duì)象監(jiān)聽(tīng) fd 的事件,并把請(qǐng)求保存到 mRequests 中
ssize_t requestIndex = mRequests.indexOfKey(fd);
if (requestIndex < 0) {
int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_ADD, fd, &eventItem);
if (epollResult < 0) {
ALOGE("Error adding epoll events for fd %d: %s", fd, strerror(errno));
return -1;
}
mRequests.add(fd, request);
} else {
int epollResult = epoll_ctl(mEpollFd.get(), EPOLL_CTL_MOD, fd, &eventItem);
if (epollResult < 0) {
// ...
}
mRequests.replaceValueAt(requestIndex, request);
}
} // release lock
return 1;
}
Looper::addFd() 的實(shí)質(zhì)就是用 epoll 對(duì)象監(jiān)聽(tīng)指定 fd 的 I/O 事件。為何我把這一過(guò)程稱之為 監(jiān)聽(tīng)請(qǐng)求 呢 ? 因?yàn)檫@個(gè)函數(shù)把它的參數(shù)包裝成一個(gè) Request 對(duì)象,并保存到 mRequests 容器 中。
Looper 進(jìn)行輪詢時(shí),epoll_wait() 會(huì)阻塞,當(dāng) fd 指向的文件有 I/O 事件時(shí),epoll_wait() 將會(huì)獲取到 fd 的可讀事件,因此 Looper 會(huì)被喚醒。
當(dāng) Looper 檢測(cè)到有請(qǐng)求到來(lái)時(shí),一般是通過(guò)回調(diào)處理的,也就是這里的參數(shù) callback。當(dāng)然,也可以不設(shè)置回調(diào),當(dāng)有請(qǐng)求到來(lái)時(shí),交給外部的調(diào)用者去處理,我們將會(huì)在后面看到。
根據(jù)以上知識(shí),我們來(lái)理解 Looper::addFd() 的第一步。當(dāng) callback 參數(shù)為空時(shí),ident 必須大于或等于0,否則為 POLL_CALLBACK,注意,這的值是 -2。 因此呢,當(dāng)我們檢測(cè)到一個(gè)請(qǐng)求的 ident 大于或等于0時(shí),這個(gè)請(qǐng)求肯定不是通過(guò)回調(diào)處理的。這一點(diǎn)非常重要,我們將會(huì)在后面用到。
Looper 處理消息或請(qǐng)求
Native 層的 Looper 是通過(guò) Looper::pollOnce() 或 Looper::pollAll() 來(lái)統(tǒng)一處理消息和請(qǐng)求的,我們挑 Looper::pollOnce() 這個(gè)函數(shù)來(lái)分析下
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result = 0;
for (;;) { // 無(wú)限循環(huán)
// 1. 處理那些不是通過(guò)callback處理的請(qǐng)求
while (mResponseIndex < mResponses.size()) {
// ...
}
// 2. 處理pollInner()輪詢的結(jié)果
if (result != 0) {
// ...
}
// 3. epoll 等待并處理事件(如果有事件到來(lái))
result = pollInner(timeoutMillis);
}
}
當(dāng)首次調(diào)用 Looper::pollOnce() 時(shí),第一步和第二步肯定不會(huì)發(fā)生,那么我們先來(lái)看下第三步,這個(gè)函數(shù)比較長(zhǎng),我們一步步解析
int Looper::pollInner(int timeoutMillis) {
// 省略計(jì)算 timeoutMillis 的代碼
int result = POLL_WAKE;
mResponses.clear();
mResponseIndex = 0;
mPolling = true;
struct epoll_event eventItems[EPOLL_MAX_EVENTS];
// 1. 等待事件
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// ...
}
第一步,通過(guò) epoll_wait() 阻塞地等待它監(jiān)聽(tīng)的 fd 的 I/O 就緒, 此時(shí) Looper 進(jìn)入休眠。
那么怎么喚醒 Looper 呢? 根據(jù)前面的分析,epoll 對(duì)象監(jiān)聽(tīng)了 mWakeEventFd 以及 通過(guò) Looper::addFd() 添加的 fd。 那么向這些被監(jiān)聽(tīng)的 fd 寫(xiě)入數(shù)據(jù),就可以喚醒 Looper。例如,Looper::wake() 就是通過(guò)向 mWakeEventFd 寫(xiě)入數(shù)據(jù)來(lái)喚醒 Looper。
那么現(xiàn)在我們來(lái)看下 Looper 被喚醒后的處理流程
int Looper::pollInner(int timeoutMillis) {
// ...
// 1. 等待I/O就緒
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// ...
// 2. 處理 epoll 事件
for (int i = 0; i < eventCount; i++) {
int fd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
if (fd == mWakeEventFd.get()) {
// 2.1 處理被消息喚醒的情況
if (epollEvents & EPOLLIN) {
// 清理eventfd中的數(shù)據(jù)
awoken();
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on wake event fd.", epollEvents);
}
} else {
// 2.2 處理被請(qǐng)求喚醒的情況
ssize_t requestIndex = mRequests.indexOfKey(fd);
if (requestIndex >= 0) {
// 根據(jù)epoll觸發(fā)的事件類型,填充events相應(yīng)的位
int events = 0;
if (epollEvents & EPOLLIN) events |= EVENT_INPUT;
if (epollEvents & EPOLLOUT) events |= EVENT_OUTPUT;
if (epollEvents & EPOLLERR) events |= EVENT_ERROR;
if (epollEvents & EPOLLHUP) events |= EVENT_HANGUP;
// 創(chuàng)建Response對(duì)象,保存兩個(gè)參數(shù),然后把Response對(duì)象保存到mResponses中
pushResponse(events, mRequests.valueAt(requestIndex));
} else {
ALOGW("Ignoring unexpected epoll events 0x%x on fd %d that is "
"no longer registered.", epollEvents, fd);
}
}
}
}
當(dāng) mWakeEventFd 的 I/O 就緒,就會(huì)走到2.1步,之后會(huì)讀取 mWakeEventFd 中的數(shù)據(jù),讀取的數(shù)據(jù)并沒(méi)有什么用,只是清理數(shù)據(jù)而已。而這一步,大部分情況 是由于消息的到來(lái),而極少情況是并不是因?yàn)橄⒌牡絹?lái),而是因?yàn)榫€程有緊急事情需要處理,所以必須要喚醒。
當(dāng)通過(guò)Looper::addFd() 添加的 fd 就緒時(shí),就會(huì)走到 2.2 步,這一步一定是因?yàn)檎?qǐng)求到來(lái)了。它會(huì)創(chuàng)建 Reponse 對(duì)象,并保存,代碼如下
void Looper::pushResponse(int events, const Request& request) {
Response response;
response.events = events;
response.request = request;
mResponses.push(response);
}
這里我們要注意下,mResponses 容器表示待處理請(qǐng)求的集合,這些請(qǐng)求會(huì)在后面處理,讓我們接著往下看。
int Looper::pollInner(int timeoutMillis) {
// ...
// 1. 等待I/O就緒
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// ...
// 2. 處理 epoll 事件
for (int i = 0; i < eventCount; i++) {
// ...
}
Done: ;
mNextMessageUptime = LLONG_MAX;
// 3. 處理消息
while (mMessageEnvelopes.size() != 0) { // 循環(huán)處理消息
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
// 3.1 取出隊(duì)頭消息
const MessageEnvelope& messageEnvelope = mMessageEnvelopes.itemAt(0);
if (messageEnvelope.uptime <= now) {
// 3.2 隊(duì)頭消息處理的時(shí)間點(diǎn)小于當(dāng)前時(shí)間點(diǎn),表示要立即處理消息
{
// 獲取handler
sp<MessageHandler> handler = messageEnvelope.handler;
Message message = messageEnvelope.message;
mMessageEnvelopes.removeAt(0);
mSendingMessage = true;
mLock.unlock();
// 消息交給handler處理
handler->handleMessage(message);
} // release handler
mLock.lock();
mSendingMessage = false;
// POLL_CALLBACK 表示消息被回調(diào)處理
result = POLL_CALLBACK;
} else {
// 3.2 隊(duì)頭的消息處理的時(shí)間點(diǎn)大于當(dāng)前時(shí)間,表示還沒(méi)有到處理的時(shí)間點(diǎn),就退出處理消息的循環(huán)
// mNextMessageUptime 表示下一個(gè)消息要處理的時(shí)間點(diǎn),當(dāng)通過(guò)break退出循環(huán)后,
// 在外層的下一次循調(diào)用pollInner()時(shí),會(huì)通過(guò) mNextMessageUptime 計(jì)算 epoll_wait 的超時(shí)時(shí)間
mNextMessageUptime = messageEnvelope.uptime;
break;
}
}
// Release lock.
mLock.unlock();
}
根據(jù)前面分析的消息發(fā)送的過(guò)程,消息保存在 mMessageEnvelopes 中。那么這里的第三步,很明顯是在處理消息。通過(guò)循環(huán),不斷取出消息,然后把消息的 messageEnvelope.uptime 與當(dāng)前時(shí)間進(jìn)行比較,如果小于當(dāng)前時(shí)間,就證明要立馬處理消息了,否則這些消息只能在下一次輪詢中再處理。
處理完了消息,現(xiàn)在來(lái)處理請(qǐng)求
int Looper::pollInner(int timeoutMillis) {
// ...
// 1. 等待事件
int eventCount = epoll_wait(mEpollFd.get(), eventItems, EPOLL_MAX_EVENTS, timeoutMillis);
// ...
// 2. 處理 epoll 事件
for (int i = 0; i < eventCount; i++) {
// ...
}
Done: ;
// 3. 處理消息
while (mMessageEnvelopes.size() != 0) { // 循環(huán)處理消息
// ...
}
// 4. 循環(huán)處理請(qǐng)求
for (size_t i = 0; i < mResponses.size(); i++) {
Response& response = mResponses.editItemAt(i);
// 檢測(cè)請(qǐng)求是否通過(guò)回調(diào)處理
if (response.request.ident == POLL_CALLBACK) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
int callbackResult = response.request.callback->handleEvent(fd, events, data);
if (callbackResult == 0) {
removeFd(fd, response.request.seq);
}
response.request.callback.clear();
// 表明消息被回調(diào)處理了
result = POLL_CALLBACK;
}
}
// 返回結(jié)果
return result;
}
剛剛我們還提到,mResponse 中保存了待處理的請(qǐng)求?,F(xiàn)在通過(guò)循環(huán),不斷取出請(qǐng)求來(lái)處理。處理請(qǐng)求有一個(gè)條件,那就是請(qǐng)求必須有回調(diào),否則不處理。 再回顧前面分析 監(jiān)聽(tīng)請(qǐng)求 的代碼,當(dāng)Looper::addFd() 的參數(shù) callback 不為空時(shí),Request.ident 的值為 POLL_CALLBACK,表明請(qǐng)求需要通過(guò)回調(diào)處理。
Looper::pollInner() 函數(shù)分析完畢,現(xiàn)在再回到 Looper::pollOnce()
int Looper::pollOnce(int timeoutMillis, int* outFd, int* outEvents, void** outData) {
int result = 0;
for (;;) { // 無(wú)限循環(huán)
// 1. 處理那些不是通過(guò)callback處理的請(qǐng)求
while (mResponseIndex < mResponses.size()) {
const Response& response = mResponses.itemAt(mResponseIndex++);
int ident = response.request.ident;
// 從Looper::addFd()分析可知,只有當(dāng)callback為空的情況下,ident的值>=0,否則為POLL_CALLBACK(-2)
// 因此,這里處理的是那些沒(méi)有通過(guò)callback處理的請(qǐng)求
if (ident >= 0) {
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
// 因?yàn)長(zhǎng)ooper無(wú)法通過(guò)callback處理,所以把這些元數(shù)據(jù)交給調(diào)用者處理
if (outFd != nullptr) *outFd = fd;
if (outEvents != nullptr) *outEvents = events;
if (outData != nullptr) *outData = data;
// 注意,這里返回的值大于0
return ident;
}
}
// 2. 處理pollInner()輪詢的結(jié)果
// result的值有很多種,但是都為負(fù)數(shù)(注意,上面處理那些不是通過(guò)callback處理的請(qǐng)求,返回正值)
// 1. POLL_WAKE(-1): 表示epoll_wait()是被eventfd喚醒的
// 1. POLL_ERROR(-4): 表示epoll_wait()出錯(cuò)
// 2. POLL_TIMEOUT(-3) : 表示epoll_wait()超時(shí)
// 3. POLL_CALLBACK(-2) : 表示消息或請(qǐng)求是通過(guò)回調(diào)處理的
if (result != 0) {
// 消息或事件無(wú)論是否被callback處理,這些傳入的參數(shù)都沒(méi)有意義,因此清空
if (outFd != nullptr) *outFd = 0;
if (outEvents != nullptr) *outEvents = 0;
if (outData != nullptr) *outData = nullptr;
// 注意,返回的是負(fù)值
return result;
}
// 3. epoll 等待并處理事件(如果有事件到來(lái))
result = pollInner(timeoutMillis);
}
}
從整體看,當(dāng) pollInner() 返回后,就會(huì)調(diào)用第一步和第二步來(lái)處理結(jié)果。
首先來(lái)看第一步,根據(jù)前面 監(jiān)聽(tīng)請(qǐng)求 的分析,當(dāng) Looper::addFd() 的參數(shù) callback 為空時(shí),Request.ident 的值才大于等于0。Looper::pollInner 只通過(guò)回調(diào)來(lái)處理請(qǐng)求,而對(duì)于那些沒(méi)有回調(diào)的請(qǐng)求呢?那就是在這里處理。而處理的方式是直接把元數(shù)據(jù)返回給調(diào)用者,那么意思就很明顯了,讓調(diào)用者自己處理。
再來(lái)看第二步,直接返回 Looper::pollInner() 的結(jié)果,并把參數(shù)清0。因?yàn)闊o(wú)論返回的什么結(jié)果,這些參數(shù)都沒(méi)有意義了,這一點(diǎn)請(qǐng)大家自己體會(huì)。
關(guān)于第一步和第二步,還有一點(diǎn)需要關(guān)注,第一步的返回值是正值,而第二步返回值是負(fù)值。
結(jié)束
本文對(duì) Native 的 Looper 的主要函數(shù)進(jìn)行分析,揭開(kāi)了 Native 層消息機(jī)制的核心,但是目前我并不能給一個(gè)很好例子來(lái)理解本文的內(nèi)容。需要大家在分析 Native 層源碼時(shí)慢慢體會(huì)。
可能有人會(huì)問(wèn),你為何不以 Java 層的消息機(jī)制為例來(lái)引出 Native 層的消息機(jī)制呢? 因?yàn)檫@樣廢話太多。
以上就是Native層消息機(jī)制深入探究實(shí)例解析的詳細(xì)內(nèi)容,更多關(guān)于Native層消息機(jī)制的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
- React Native自定義Android的SSL證書(shū)鏈校驗(yàn)
- Android性能優(yōu)化之plt?hook與native線程監(jiān)控詳解
- Android WebView開(kāi)發(fā)之WebView與Native交互
- Android nativePollOnce函數(shù)解析
- 詳解Flutter 調(diào)用 Android Native 的方法
- android中使用react-native設(shè)置應(yīng)用啟動(dòng)頁(yè)過(guò)程詳解
- Android Native 內(nèi)存泄漏系統(tǒng)化解決方案
- Android使用google breakpad捕獲分析native cash
相關(guān)文章
Android中VideoView音視頻開(kāi)發(fā)的實(shí)現(xiàn)
VideoView是一個(gè)用于播放視頻的視圖組件,可以方便地在應(yīng)用程序中播放本地或網(wǎng)絡(luò)上的視頻文件,本文主要介紹了Android中VideoView音視頻開(kāi)發(fā)的實(shí)現(xiàn),具有一定的 參考價(jià)值,感興趣的可以了解一下2025-03-03
詳解Android中Application設(shè)置全局變量以及傳值
這篇文章主要介紹了詳解Android中Application設(shè)置全局變量以及傳值的相關(guān)資料,希望通過(guò)本文大家能夠理解掌握這部分內(nèi)容,需要的朋友可以參考下2017-09-09
Flutter實(shí)現(xiàn)PopupMenu彈出式菜單按鈕詳解
這篇文章主要介紹了Flutter實(shí)現(xiàn)PopupMenu彈出式菜單按鈕,PopupMenuButton是一個(gè)用于創(chuàng)建彈出菜單的小部件,當(dāng)用戶點(diǎn)擊觸發(fā)按鈕時(shí),PopupMenuButton會(huì)在屏幕上方或下方彈出一個(gè)菜單,感興趣想要詳細(xì)了解可以參考下文2023-05-05
Android編程之高效開(kāi)發(fā)App的10個(gè)建議
這篇文章主要介紹了Android編程之高效開(kāi)發(fā)App的10個(gè)建議,較為詳細(xì)的分析了Android開(kāi)發(fā)中的常見(jiàn)問(wèn)題與注意事項(xiàng),具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-10-10
Android屬性動(dòng)畫(huà)特點(diǎn)詳解
這篇文章主要為大家詳細(xì)介紹了Android屬性動(dòng)畫(huà)特點(diǎn),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-11-11
Android實(shí)時(shí)文件夾創(chuàng)建方法
這篇文章主要介紹了Android實(shí)時(shí)文件夾創(chuàng)建方法,涉及基于Activity實(shí)現(xiàn)文件實(shí)時(shí)查詢的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-09-09
Android開(kāi)發(fā)注解排列組合出啟動(dòng)任務(wù)ksp
這篇文章主要為大家介紹了Android開(kāi)發(fā)注解排列組合出啟動(dòng)任務(wù)ksp示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-06-06
Android實(shí)現(xiàn)底部狀態(tài)欄切換的兩種方式
這篇文章主要介紹了Android實(shí)現(xiàn)底部狀態(tài)欄切換功能,在文中給大家提到了兩種實(shí)現(xiàn)方式,本文分步驟給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-06-06

