Android nativePollOnce函數(shù)解析
nativePollOnce的實(shí)現(xiàn)函數(shù)是android_os_MessageQueue_nativePollOnce,代碼如下:
android_os_MessageQueue.cpp
static void android_os_MessageQueue_nativePollOnce(JNIEnv*env, jobject obj,
jintptr, jint timeoutMillis)
NativeMessageQueue*nativeMessageQueue =
reinterpret_cast<NativeMessageQueue*>(ptr);
//取出NativeMessageQueue對(duì)象,并調(diào)用它的pollOnce
nativeMessageQueue->pollOnce(timeoutMillis);
}
//分析pollOnce函數(shù)
void NativeMessageQueue::pollOnce(inttimeoutMillis) {
mLooper->pollOnce(timeoutMillis); //重任傳遞到Looper的pollOnce函數(shù)
}
Looper的pollOnce函數(shù)如下:
Looper.cpp
inline int pollOnce(int timeoutMillis) {
return pollOnce(timeoutMillis, NULL, NULL, NULL);
}
上面的函數(shù)將調(diào)用另外一個(gè)有4個(gè)參數(shù)的pollOnce函數(shù),這個(gè)函數(shù)的原型如下:
int pollOnce(int timeoutMillis, int* outFd, int*outEvents, void** outData)
其中:
- timeOutMillis參數(shù)為超時(shí)等待時(shí)間。如果為-1,則表示無(wú)限等待,直到有事件發(fā)生為止。如果值為0,則無(wú)需等待立即返回。
- outFd用來(lái)存儲(chǔ)發(fā)生事件的那個(gè)文件描述符。
- outEvents用來(lái)存儲(chǔ)在該文件描述符[[1]上發(fā)生了哪些事件,目前支持可讀、可寫、錯(cuò)誤和中斷4個(gè)事件。這4個(gè)事件其實(shí)是從epoll事件轉(zhuǎn)化而來(lái)。后面我們會(huì)介紹大名鼎鼎的epoll。
- outData用于存儲(chǔ)上下文數(shù)據(jù),這個(gè)上下文數(shù)據(jù)是由用戶在添加監(jiān)聽句柄時(shí)傳遞的,它的作用和pthread_create函數(shù)最后一個(gè)參數(shù)param一樣,用來(lái)傳遞用戶自定義的數(shù)據(jù)。
另外,pollOnce函數(shù)的返回值也具有特殊的意義,具體如下:
- 當(dāng)返回值為ALOOPER_POLL_WAKE時(shí),表示這次返回是由wake函數(shù)觸發(fā)的,也就是管道寫端的那次寫事件觸發(fā)的。
- 返回值為ALOOPER_POLL_TIMEOUT表示等待超時(shí)。
- 返回值為ALOOPER_POLL_ERROR,表示等待過程中發(fā)生錯(cuò)誤。
返回值為ALOOPER_POLL_CALLBACK,表示某個(gè)被監(jiān)聽的句柄因某種原因被觸發(fā)。這時(shí),outFd參數(shù)用于存儲(chǔ)發(fā)生事件的文件句柄,outEvents用于存儲(chǔ)所發(fā)生的事件。
上面這些知識(shí)是和epoll息息相關(guān)的。
提示查看Looper的代碼會(huì)發(fā)現(xiàn),Looper采用了編譯選項(xiàng)(即#if和#else)來(lái)控制是否使用epoll作為I/O復(fù)用的控制中樞。鑒于現(xiàn)在大多數(shù)系統(tǒng)都支持epoll,這里僅討論使用epoll的情況。
1.epoll基礎(chǔ)知識(shí)介紹
epoll機(jī)制提供了Linux平臺(tái)上最高效的I/O復(fù)用機(jī)制,因此有必要介紹一下它的基礎(chǔ)知識(shí)。
從調(diào)用方法上看,epoll的用法和select/poll非常類似,其主要作用就是I/O復(fù)用,即在一個(gè)地方等待多個(gè)文件句柄的I/O事件。
下面通過一個(gè)簡(jiǎn)單例子來(lái)分析epoll的工作流程。
epoll工作流程分析案例
/*
使用epoll前,需要先通過epoll_create函數(shù)創(chuàng)建一個(gè)epoll句柄。
下面一行代碼中的10表示該epoll句柄初次創(chuàng)建時(shí)候分配能容納10個(gè)fd相關(guān)信息的緩存。
對(duì)于2.6.8版本以后的內(nèi)核,該值沒有實(shí)際作用,這里可以忽略。其實(shí)這個(gè)值的主要目的是
確定分配一塊多大的緩存?,F(xiàn)在的內(nèi)核都支持動(dòng)態(tài)拓展這塊緩存,所以該值就沒有意義了
*/
int epollHandle = epoll_create(10);
/*
得到epoll句柄后,下一步就是通過epoll_ctl把需要監(jiān)聽的文件句柄加入到epoll句柄中。
除了指定文件句柄本身的fd值外,同時(shí)還需要指定在該fd上等待什么事件。epoll支持四類事件,
分別是EPOLLIN(句柄可讀)、EPOLLOUT(句柄可寫),EPOLLERR(句柄錯(cuò)誤)、EPOLLHUP(句柄斷)。
epoll定義了一個(gè)結(jié)構(gòu)體struct epoll_event來(lái)表達(dá)監(jiān)聽句柄的訴求。
假設(shè)現(xiàn)在有一個(gè)監(jiān)聽端的socket句柄listener,要把它加入到epoll句柄中。
*/
structepoll_event listenEvent; //先定義一個(gè)event
/*
EPOLLIN表示可讀事件,EPOLLOUT表示可寫事件,另外還有EPOLLERR,EPOLLHUP表示
系統(tǒng)默認(rèn)會(huì)將EPOLLERR加入到事件集合中
*/
listenEvent.events= EPOLLIN;//指定該句柄的可讀事件
//epoll_event中有一個(gè)聯(lián)合體叫data,用來(lái)存儲(chǔ)上下文數(shù)據(jù),本例的上下文數(shù)據(jù)就是句柄自己
listenEvent.data.fd= listenEvent;
/*
EPOLL_CTL_ADD將監(jiān)聽fd和監(jiān)聽事件加入到epoll句柄的等待隊(duì)列中;
EPOLL_CTL_DEL將監(jiān)聽fd從epoll句柄中移除;
EPOLL_CTL_MOD修改監(jiān)聽fd的監(jiān)聽事件,例如本來(lái)只等待可讀事件,現(xiàn)在需要同時(shí)等待
可寫事件,那么修改listenEvent.events 為EPOLLIN|EPOLLOUT后,再傳給epoll句柄
*/
epoll_ctl(epollHandle,EPOLL_CTL_ADD,listener,&listenEvent);
/*
當(dāng)把所有感興趣的fd都加入到epoll句柄后,就可以開始坐等感興趣的事情發(fā)生了。
為了接收所發(fā)生的事情,先定義一個(gè)epoll_event數(shù)組
*/
struct epoll_eventresultEvents[10];
inttimeout = -1;
while(1)
{
/*
調(diào)用epoll_wait用于等待事件,其中timeout可以指定一個(gè)超時(shí)時(shí)間,
resultEvents用于接收發(fā)生的事件,10為該數(shù)組的大小。
epoll_wait函數(shù)的返回值有如下含義:
nfds大于0表示所監(jiān)聽的句柄上有事件發(fā)生;
nfds等于0表示等待超時(shí);
nfds小于0表示等待過程中發(fā)生了錯(cuò)誤
*/
int nfds= epoll_wait(epollHandle, resultEvents, 10, timeout);
if(nfds== -1)
{
// epoll_wait發(fā)生了錯(cuò)誤
}
elseif(nfds == 0)
{
//發(fā)生超時(shí),期間沒有發(fā)生任何事件
}
else
{
//resultEvents用于返回那些發(fā)生了事件的信息
for(int i = 0; i < nfds; i++)
{
struct epoll_event & event =resultEvents[i];
if(event & EPOLLIN)
{
/*
收到可讀事件。到底是哪個(gè)文件句柄發(fā)生該事件呢?可通過event.data這個(gè)聯(lián)合體取得
之前傳遞給epoll的上下文數(shù)據(jù),該上下文信息可用于判斷到底是誰(shuí)發(fā)生了事件。
*/
}
.......//其他處理
}
}
}
epoll整體使用流程如上面代碼所示,基本和select/poll類似,不過作為L(zhǎng)inux平臺(tái)最高效的I/O復(fù)用機(jī)制,這里有些內(nèi)容供讀者參考,
epoll的效率為什么會(huì)比select高?其中一個(gè)原因是調(diào)用方法。每次調(diào)用select時(shí),都需要把感興趣的事件復(fù)制到內(nèi)核中,而epoll只在epll_ctl進(jìn)行加入的時(shí)候復(fù)制一次。另外,epoll內(nèi)部用于保存事件的數(shù)據(jù)結(jié)構(gòu)使用的是紅黑樹,查找速度很快。而select采用數(shù)組保存信息,不但一次能等待的句柄個(gè)數(shù)有限,并且查找起來(lái)速度很慢。當(dāng)然,在只等待少量文件句柄時(shí),select和epoll效率相差不是很多,但筆者還是推薦使用epoll。
epoll等待的事件有兩種觸發(fā)條件,一個(gè)是水平觸發(fā)(EPOLLLEVEL),另外一個(gè)是邊緣觸發(fā)(EPOLLET,ET為Edge Trigger之意),這兩種觸發(fā)條件的區(qū)別非常重要。讀者可通過man epoll查閱系統(tǒng)提供的更為詳細(xì)的epoll機(jī)制。
最后,關(guān)于pipe,還想提出一個(gè)小問題供讀者思考討論:
為什么Android中使用pipe作為線程間通訊的方式?對(duì)于pipe的寫端寫入的數(shù)據(jù),讀端都不感興趣,只是為了簡(jiǎn)單的喚醒。POSIX不是也有線程間同步函數(shù)嗎?為什么要用pipe呢?
關(guān)于這個(gè)問題的答案,可參見筆者一篇博文“隨筆之如何實(shí)現(xiàn)一個(gè)線程池”。
2. pollOnce函數(shù)分析
下面分析帶4個(gè)參數(shù)的pollOnce函數(shù),代碼如下:
Looper.cpp
int Looper::pollOnce(int timeoutMillis, int*outFd, int* outEvents,
void** outData) {
intresult = 0;
for (;;){ //一個(gè)無(wú)限循環(huán)
//mResponses是一個(gè)Vector,這里首先需要處理response
while (mResponseIndex < mResponses.size()) {
const Response& response = mResponses.itemAt(mResponseIndex++);
ALooper_callbackFunc callback = response.request.callback;
if (!callback) {//首先處理那些沒有callback的Response
int ident = response.request.ident; //ident是這個(gè)Response的id
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
......
if (outFd != NULL) *outFd = fd;
if (outEvents != NULL) *outEvents = events;
if (outData != NULL) *outData = data;
//實(shí)際上,對(duì)于沒有callback的Response,pollOnce只是返回它的
//ident,并沒有實(shí)際做什么處理。因?yàn)闆]有callback,所以系統(tǒng)也不知道如何處理
return ident;
}
}
if(result != 0) {
if (outFd != NULL) *outFd = 0;
if (outEvents != NULL) *outEvents = NULL;
if (outData != NULL) *outData = NULL;
return result;
}
//調(diào)用pollInner函數(shù)。注意,它在for循環(huán)內(nèi)部
result = pollInner(timeoutMillis);
}
}
初看上面的代碼,可能會(huì)讓人有些丈二和尚摸不著頭腦。但是把pollInner函數(shù)分析完畢,大家就會(huì)明白很多。pollInner函數(shù)非常長(zhǎng),把用于調(diào)試和統(tǒng)計(jì)的代碼去掉,結(jié)果如下:
Looper.cpp
int Looper::pollInner(int timeoutMillis) {
if(timeoutMillis != 0 && mNextMessageUptime != LLONG_MAX) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
......//根據(jù)Native Message的信息計(jì)算此次需要等待的時(shí)間
timeoutMillis= messageTimeoutMillis;
}
intresult = ALOOPER_POLL_WAKE;
mResponses.clear();
mResponseIndex = 0;
#ifdef LOOPER_USES_EPOLL //我們只討論使用epoll進(jìn)行I/O復(fù)用的方式
structepoll_event eventItems[EPOLL_MAX_EVENTS];
//調(diào)用epoll_wait,等待感興趣的事件或超時(shí)發(fā)生
inteventCount = epoll_wait(mEpollFd, eventItems, EPOLL_MAX_EVENTS,
timeoutMillis);
#else
......//使用別的方式進(jìn)行I/O復(fù)用
#endif
//從epoll_wait返回,這時(shí)候一定發(fā)生了什么事情
mLock.lock();
if(eventCount < 0) { //返回值小于零,表示發(fā)生錯(cuò)誤
if(errno == EINTR) {
goto Done;
}
//設(shè)置result為ALLOPER_POLL_ERROR,并跳轉(zhuǎn)到Done
result = ALOOPER_POLL_ERROR;
gotoDone;
}
//eventCount為零,表示發(fā)生超時(shí),因此直接跳轉(zhuǎn)到Done
if(eventCount == 0) {
result = ALOOPER_POLL_TIMEOUT;
gotoDone;
}
#ifdef LOOPER_USES_EPOLL
//根據(jù)epoll的用法,此時(shí)的eventCount表示發(fā)生事件的個(gè)數(shù)
for (inti = 0; i < eventCount; i++) {
intfd = eventItems[i].data.fd;
uint32_t epollEvents = eventItems[i].events;
/*
之前通過pipe函數(shù)創(chuàng)建過兩個(gè)fd,這里根據(jù)fd知道是管道讀端有可讀事件。
讀者還記得對(duì)nativeWake函數(shù)的分析嗎?在那里我們向管道寫端寫了一個(gè)”W”字符,這樣
就能觸發(fā)管道讀端從epoll_wait函數(shù)返回了
*/
if(fd == mWakeReadPipeFd) {
if (epollEvents & EPOLLIN) {
//awoken函數(shù)直接讀取并清空管道數(shù)據(jù),讀者可自行研究該函數(shù)
awoken();
}
......
}else {
/*
mRequests和前面的mResponse相對(duì)應(yīng),它也是一個(gè)KeyedVector,其中存儲(chǔ)了
fd和對(duì)應(yīng)的Request結(jié)構(gòu)體,該結(jié)構(gòu)體封裝了和監(jiān)控文件句柄相關(guān)的一些上下文信息,
例如回調(diào)函數(shù)等。我們?cè)诤竺娴男」?jié)會(huì)再次介紹該結(jié)構(gòu)體
*/
ssize_t requestIndex = mRequests.indexOfKey(fd);
if (requestIndex >= 0) {
int events = 0;
//將epoll返回的事件轉(zhuǎn)換成上層LOOPER使用的事件
if (epollEvents & EPOLLIN) events |= ALOOPER_EVENT_INPUT;
if (epollEvents & EPOLLOUT) events |= ALOOPER_EVENT_OUTPUT;
if (epollEvents & EPOLLERR) events |= ALOOPER_EVENT_ERROR;
if (epollEvents & EPOLLHUP) events |= ALOOPER_EVENT_HANGUP;
//每處理一個(gè)Request,就相應(yīng)構(gòu)造一個(gè)Response
pushResponse(events, mRequests.valueAt(requestIndex));
}
......
}
}
Done: ;
#else
......
#endif
//除了處理Request外,還處理Native的Message
mNextMessageUptime = LLONG_MAX;
while(mMessageEnvelopes.size() != 0) {
nsecs_t now = systemTime(SYSTEM_TIME_MONOTONIC);
const MessageEnvelope& messageEnvelope =mMessageEnvelopes.itemAt(0);
if(messageEnvelope.uptime <= now) {
{
sp<MessageHandler> handler = messageEnvelope.handler;
Message message = messageEnvelope.message;
mMessageEnvelopes.removeAt(0);
mSendingMessage = true;
mLock.unlock();
//調(diào)用Native的handler處理Native的Message
//從這里也可看出NativeMessage和Java層的Message沒有什么關(guān)系
handler->handleMessage(message);
}
mLock.lock();
mSendingMessage = false;
result = ALOOPER_POLL_CALLBACK;
}else {
mNextMessageUptime = messageEnvelope.uptime;
break;
}
}
mLock.unlock();
//處理那些帶回調(diào)函數(shù)的Response
for(size_t i = 0; i < mResponses.size(); i++) {
const Response& response = mResponses.itemAt(i);
ALooper_callbackFunc callback = response.request.callback;
if(callback) {//有了回調(diào)函數(shù),就能知道如何處理所發(fā)生的事情了
int fd = response.request.fd;
int events = response.events;
void* data = response.request.data;
//調(diào)用回調(diào)函數(shù)處理所發(fā)生的事件
int callbackResult = callback(fd, events, data);
if (callbackResult == 0) {
//callback函數(shù)的返回值很重要,如果為0,表明不需要再次監(jiān)視該文件句柄
removeFd(fd);
}
result = ALOOPER_POLL_CALLBACK;
}
}
returnresult;
}
看完代碼了,是否還有點(diǎn)模糊?那么,回顧一下pollInner函數(shù)的幾個(gè)關(guān)鍵點(diǎn):
- 首先需要計(jì)算一下真正需要等待的時(shí)間。
- 調(diào)用epoll_wait函數(shù)等待。
- epoll_wait函數(shù)返回,這時(shí)候可能有三種情況:
發(fā)生錯(cuò)誤,則跳轉(zhuǎn)到Done處。
超時(shí),這時(shí)候也跳轉(zhuǎn)到Done處。
epoll_wait監(jiān)測(cè)到某些文件句柄上有事件發(fā)生。
- 假設(shè)epoll_wait因?yàn)槲募浔惺录祷?,此時(shí)需要根據(jù)文件句柄來(lái)分別處理:
如果是管道讀這一端有事情,則認(rèn)為是控制命令,可以直接讀取管道中的數(shù)據(jù)。
如果是其他FD發(fā)生事件,則根據(jù)Request構(gòu)造Response,并push到Response數(shù)組中。
- 真正開始處理事件是在有Done標(biāo)志的位置。
首先處理Native的Message。調(diào)用Native Handler的handleMessage處理該Message。
處理Response數(shù)組中那些帶有callback的事件。
上面的處理流程還是比較清晰的,但還是有個(gè)一個(gè)攔路虎,那就是mRequests,下面就來(lái)清剿這個(gè)攔路虎。
3.添加監(jiān)控請(qǐng)求
添加監(jiān)控請(qǐng)求其實(shí)就是調(diào)用epoll_ctl增加文件句柄。下面通過從Native的Activity找到的一個(gè)例子來(lái)分析mRequests。
android_app_NativeActivity.cpp
static jint
loadNativeCode_native(JNIEnv* env, jobject clazz,jstring path,
jstring funcName,jobject messageQueue,
jstring internalDataDir, jstring obbDir,
jstring externalDataDir, int sdkVersion,
jobject jAssetMgr, jbyteArraysavedState)
{
......
/*
調(diào)用Looper的addFd函數(shù)。第一個(gè)參數(shù)表示監(jiān)聽的fd;第二個(gè)參數(shù)0表示ident;
第三個(gè)參數(shù)表示需要監(jiān)聽的事件,這里為只監(jiān)聽可讀事件;第四個(gè)參數(shù)為回調(diào)函數(shù),當(dāng)該fd發(fā)生
指定事件時(shí),looper將回調(diào)該函數(shù);第五個(gè)參數(shù)code為回調(diào)函數(shù)的參數(shù)
*/
code->looper->addFd(code->mainWorkRead,0,
ALOOPER_EVENT_INPUT,mainWorkCallback, code);
......
}
Looper的addFd代碼如下所示:
Looper.cpp
int Looper::addFd(int fd, int ident, int events,
ALooper_callbackFunccallback, void* data) {
if (!callback) {
//判斷該Looper是否支持不帶回調(diào)函數(shù)的文件句柄添加。一般不支持,因?yàn)闆]有回調(diào)函數(shù)
//Looper也不知道如何處理該文件句柄上發(fā)生的事情
if(! mAllowNonCallbacks) {
return -1;
}
......
}
#ifdefLOOPER_USES_EPOLL
intepollEvents = 0;
//將用戶的事件轉(zhuǎn)換成epoll使用的值
if(events & ALOOPER_EVENT_INPUT) epollEvents |= EPOLLIN;
if(events & ALOOPER_EVENT_OUTPUT) epollEvents |= EPOLLOUT;
{
AutoMutex _l(mLock);
Request request; //創(chuàng)建一個(gè)Request對(duì)象
request.fd = fd; //保存fd
request.ident = ident; //保存id
request.callback = callback; //保存callback
request.data = data; //保存用戶自定義數(shù)據(jù)
struct epoll_event eventItem;
memset(& eventItem, 0, sizeof(epoll_event));
eventItem.events = epollEvents;
eventItem.data.fd = fd;
//判斷該Request是否已經(jīng)存在,mRequests以fd作為key值
ssize_t requestIndex = mRequests.indexOfKey(fd);
if(requestIndex < 0) {
//如果是新的文件句柄,則需要為epoll增加該fd
int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_ADD, fd, & eventItem);
......
//保存Request到mRequests鍵值數(shù)組
mRequests.add(fd, request);
}else {
//如果之前加過,那么就修改該監(jiān)聽句柄的一些信息
int epollResult = epoll_ctl(mEpollFd, EPOLL_CTL_MOD, fd, &eventItem);
......
mRequests.replaceValueAt(requestIndex, request);
}
}
#else
......
#endif
return1;
}
4.處理監(jiān)控請(qǐng)求
我們發(fā)現(xiàn)在pollInner函數(shù)中,當(dāng)某個(gè)監(jiān)控fd上發(fā)生事件后,就會(huì)把對(duì)應(yīng)的Request取出來(lái)調(diào)用。
pushResponse(events, mRequests.itemAt(i));
此函數(shù)如下:
Looper.cpp
void Looper::pushResponse(int events, constRequest& request) {
Responseresponse;
response.events = events;
response.request = request; //其實(shí)很簡(jiǎn)單,就是保存所發(fā)生的事情和對(duì)應(yīng)的Request
mResponses.push(response); //然后保存到mResponse數(shù)組
}
根據(jù)前面的知識(shí)可知,并不是單獨(dú)處理Request,而是需要先收集Request,等到Native Message消息處理完之后再做處理。這表明,在處理邏輯上,Native Message的優(yōu)先級(jí)高于監(jiān)控FD的優(yōu)先級(jí)。
下面我們來(lái)了解如何添加Native的Message。
5. Native的sendMessage
Android 2.2中只有Java層才可以通過sendMessage往MessageQueue中添加消息,從4.0開始,Native層也支持sendMessage了[2]。sendMessage的代碼如下:
Looper.cpp
void Looper::sendMessage(constsp<MessageHandler>& handler,
constMessage& message) {
//Native的sendMessage函數(shù)必須同時(shí)傳遞一個(gè)Handler
nsecs_tnow = systemTime(SYSTEM_TIME_MONOTONIC);
sendMessageAtTime(now, handler, message); //調(diào)用sendMessageAtTime
}
void Looper::sendMessageAtTime(nsecs_t uptime,
const sp<MessageHandler>& handler,
const Message& message) {
size_t i= 0;
{ //acquire lock
AutoMutex _l(mLock);
size_t messageCount = mMessageEnvelopes.size();
//按時(shí)間排序,將消息插入到正確的位置上
while (i < messageCount &&
uptime >= mMessageEnvelopes.itemAt(i).uptime) {
i += 1;
}
MessageEnvelope messageEnvelope(uptime, handler, message);
mMessageEnvelopes.insertAt(messageEnvelope, i, 1);
//mSendingMessage和Java層中的那個(gè)mBlocked一樣,是一個(gè)小小的優(yōu)化措施
if(mSendingMessage) {
return;
}
}
//喚醒epoll_wait,讓它處理消息
if (i ==0) {
wake();
}
}
1.注意,以后文件描述符也會(huì)簡(jiǎn)寫為文件句柄。 ↩︎
2.我們這里略過了Android2.2到Android 4.0之間幾個(gè)版本中的代碼變化。 ↩︎
以上就是Android nativePollOnce函數(shù)解析的詳細(xì)內(nèi)容,更多關(guān)于Android nativePollOnce函數(shù)的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Android ListView里控件添加監(jiān)聽方法的實(shí)例詳解
這篇文章主要介紹了Android ListView里控件添加監(jiān)聽方法的實(shí)例詳解的相關(guān)資料,這里提供實(shí)例幫助大家學(xué)習(xí)理解這部分內(nèi)容,需要的朋友可以參考下2017-08-08
超實(shí)用的Android手勢(shì)鎖制作實(shí)例教程
這篇文章主要介紹了一個(gè)超實(shí)用的Android手勢(shì)鎖制作實(shí)例教程,普通的圓環(huán)形圖標(biāo)變換,在App和系統(tǒng)的鎖屏界面中都可以調(diào)用,需要的朋友可以參考下2016-04-04
Android編程實(shí)現(xiàn)滑動(dòng)按鈕功能詳解
這篇文章主要介紹了Android編程實(shí)現(xiàn)滑動(dòng)按鈕功能,結(jié)合實(shí)例形式較為詳細(xì)的分析了Android實(shí)現(xiàn)滑動(dòng)按鈕的功能、布局及相關(guān)注意事項(xiàng),需要的朋友可以參考下2017-02-02
Android中SQLite數(shù)據(jù)庫(kù)知識(shí)點(diǎn)總結(jié)
在本篇文章里小編給大家分享了關(guān)于Android中SQLite數(shù)據(jù)庫(kù)知識(shí)點(diǎn)總結(jié),有需要的朋友們跟著學(xué)習(xí)下。2019-02-02
Android獲取手機(jī)系統(tǒng)版本等信息的方法
這篇文章主要介紹了Android獲取手機(jī)系統(tǒng)版本等信息的方法,涉及Android獲取手機(jī)版本中各種常見信息的技巧,非常具有實(shí)用價(jià)值,需要的朋友可以參考下2015-04-04
Android?Camera+SurfaceView自動(dòng)聚焦防止變形拉伸
這篇文章主要為大家介紹了Android自定義相機(jī)Camera+SurfaceView實(shí)現(xiàn)自動(dòng)聚焦防止變形拉伸詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
Android跳轉(zhuǎn)到通訊錄獲取用戶名稱和手機(jī)號(hào)碼的實(shí)現(xiàn)思路
這篇文章主要介紹了Android跳轉(zhuǎn)到通訊錄獲取用戶名稱和手機(jī)號(hào)碼的實(shí)現(xiàn)思路,當(dāng)用戶點(diǎn)擊跳轉(zhuǎn)到通訊錄界面 并取通訊錄姓名和手機(jī)號(hào)碼 ,實(shí)現(xiàn)代碼簡(jiǎn)單易懂,非常不錯(cuò)感興趣的朋友一起看看吧2016-10-10
android 自定義圓角button效果的實(shí)例代碼(自定義view Demo)
這篇文章主要介紹了android 自定義圓角button(自定義View Demo),本文給大家介紹的非常詳細(xì),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2019-12-12

