OKHttp使用詳解
OkHttp 是一套處理 HTTP 網(wǎng)絡(luò)請(qǐng)求的依賴(lài)庫(kù),由 Square 公司設(shè)計(jì)研發(fā)并開(kāi)源,目前可以在 Java 和 Kotlin 中使用。對(duì)于 Android App 來(lái)說(shuō),OkHttp 現(xiàn)在幾乎已經(jīng)占據(jù)了所有的網(wǎng)絡(luò)請(qǐng)求操作,RetroFit + OkHttp 實(shí)現(xiàn)網(wǎng)絡(luò)請(qǐng)求似乎成了一種標(biāo)配。因此它也是每一個(gè) Android 開(kāi)發(fā)工程師的必備技能,了解其內(nèi)部實(shí)現(xiàn)原理可以更好地進(jìn)行功能擴(kuò)展、封裝以及優(yōu)化。
網(wǎng)絡(luò)請(qǐng)求流程分析
先看下 OkHttp 的基本使用:
除了直接 new OkHttpClient 之外,還可以使用內(nèi)部工廠類(lèi) Builder 來(lái)設(shè)置 OkHttpClient。如下所示:
請(qǐng)求操作的起點(diǎn)從 OkHttpClient.newCall().enqueue() 方法開(kāi)始:
newCall
RealCall.enqueue
調(diào)用 Dispatcher 的入隊(duì)方法,執(zhí)行一個(gè)異步網(wǎng)絡(luò)請(qǐng)求的操作。
可以看出,最終請(qǐng)求操作是委托給 Dispatcher的enqueue 方法內(nèi)實(shí)現(xiàn)的。
Dispatcher 是 OkHttpClient 的調(diào)度器,是一種門(mén)戶(hù)模式。主要用來(lái)實(shí)現(xiàn)執(zhí)行、取消異步請(qǐng)求操作。本質(zhì)上是內(nèi)部維護(hù)了一個(gè)線程池去執(zhí)行異步操作,
并且在 Dispatcher 內(nèi)部根據(jù)一定的策略,保證最大并發(fā)個(gè)數(shù)、同一 host 主機(jī)允許執(zhí)行請(qǐng)求的線程個(gè)數(shù)等。
Dispatcher的enqueue 方法的具體實(shí)現(xiàn)如下:
可以看出,實(shí)際上就是使用線程池執(zhí)行了一個(gè) AsyncCall,而 AsyncCall 實(shí)現(xiàn)了 Runnable 接口,因此整個(gè)操作會(huì)在一個(gè)子線程(非 UI 線程)中執(zhí)行。
繼續(xù)查看 AsyncCall 中的 run 方法如下:
在 run 方法中執(zhí)行了另一個(gè) execute 方法,而真正獲取請(qǐng)求結(jié)果的方法是在 getResponseWithInterceptorChain 方法中,從名字也能看出其內(nèi)部是一個(gè)攔截器的調(diào)用鏈,具體代碼如下:
每一個(gè)攔截器的作用如下。
- BridgeInterceptor:主要對(duì) Request 中的 Head 設(shè)置默認(rèn)值,比如 Content-Type、Keep-Alive、Cookie 等。
- CacheInterceptor:負(fù)責(zé) HTTP 請(qǐng)求的緩存處理。
- ConnectInterceptor:負(fù)責(zé)建立與服務(wù)器地址之間的連接,也就是 TCP 鏈接。
- CallServerInterceptor:負(fù)責(zé)向服務(wù)器發(fā)送請(qǐng)求,并從服務(wù)器拿到遠(yuǎn)端數(shù)據(jù)結(jié)果。
- 在添加上述幾個(gè)攔截器之前,會(huì)調(diào)用 client.interceptors 將開(kāi)發(fā)人員設(shè)置的攔截器添加到列表當(dāng)中。
對(duì)于 Request 的 Head 以及 TCP 鏈接,我們能控制修改的成分不是很多。
總結(jié)
1.Okhttp是對(duì)Socket的封裝。有三個(gè)主要的類(lèi),Request,Response,Call
2.默認(rèn)使用new OkHttpClient() 創(chuàng)建初client對(duì)象。如果需要初始化網(wǎng)絡(luò)請(qǐng)求的參數(shù),如timeout,interceptor等,可以創(chuàng)建Builder,通過(guò)builder.build() 創(chuàng)建初client對(duì)象。
3.使用new Request.Builder().url().builder()創(chuàng)建初requst對(duì)象。
4.通過(guò)client.newCall()創(chuàng)建出call對(duì)象,同步使用call.excute(), 異步使用call,enqueue(). 這里Call是個(gè)接口,具體的實(shí)現(xiàn)在RealCall這個(gè)實(shí)現(xiàn)類(lèi)里面。
Okhttp的高效體現(xiàn)在,okhttp內(nèi)有個(gè)Dispatcher類(lèi),是okhttp內(nèi)部維護(hù)的一個(gè)線程池,對(duì)最大連接數(shù),host最大訪問(wèn)量做了初始定義。維護(hù)3個(gè)隊(duì)列及1個(gè)線程池
readyAsyncCalls
待訪問(wèn)請(qǐng)求隊(duì)列,里面存儲(chǔ)準(zhǔn)備執(zhí)行的請(qǐng)求。
runningAsyncCalls
異步請(qǐng)求隊(duì)列,里面存儲(chǔ)正在執(zhí)行,包含已經(jīng)取消但是還沒(méi)有結(jié)束的請(qǐng)求。
runningSyncCalls
同步請(qǐng)求隊(duì)列,正在執(zhí)行的請(qǐng)求,包含已經(jīng)取消但是還沒(méi)有結(jié)束的請(qǐng)求。
ExecutorService
線程池,最小0,最大Max的線程池
在執(zhí)行call.excute()的時(shí)候,調(diào)用到realcall類(lèi)里的excute方法,這個(gè)是同步方法,在方法的第一行就加了鎖,判斷executed標(biāo)記,如果是true就拋出異常,保證一個(gè)請(qǐng)求只被執(zhí)行一次。false的話繼續(xù)向下執(zhí)行。調(diào)用client.dispatcher.excute()進(jìn)入到dispatcher類(lèi)中,向runningSyncCalls隊(duì)列中添加當(dāng)前這個(gè)請(qǐng)求。執(zhí)行結(jié)束會(huì)調(diào)用finished方法
如果是異步操作,會(huì)創(chuàng)建一個(gè)RealCall.AsyncCall對(duì)象,AsyncCall繼承的NamedRunnable接口,NamedRunnable是個(gè)runnable。進(jìn)入到Dispatcher的enqueue()方法中,首先判斷線程池中線程的數(shù)據(jù),host的訪問(wèn)量,如果都沒(méi)有達(dá)到那么加入到runningAsyncCalls中,并執(zhí)行。否則加入到readyAsyncCalls隊(duì)列中。
finished方法,如果是異步操作,promoteCall方法,promoteCalls()中用迭代器遍歷readyAsyncCalls 然后加入到runningAsyncCalls
RealConnection
真正的連接操作類(lèi),對(duì)soket封裝,http1/http2的選擇,ssl協(xié)議等等信息。一個(gè)recalconnection就是一次鏈接
ConnectionPool
鏈接池,管理http1/http2的連接,同一個(gè)address共享一個(gè)connection,實(shí)現(xiàn)鏈接的復(fù)用。
StreamAlloction
保存了鏈接信息,address,HttpCodec,realconnection,connectionpool等信息
常見(jiàn)問(wèn)題:
OKHttp有哪些攔截器,分別起什么作用?
OKHTTP
的攔截器是把所有的攔截器放到一個(gè)list里,然后每次依次執(zhí)行攔截器,并且在每個(gè)攔截器分成三部分:
- 預(yù)處理攔截器內(nèi)容
- 通過(guò)
proceed
方法把請(qǐng)求交給下一個(gè)攔截器 - 下一個(gè)攔截器處理完成并返回,后續(xù)處理工作。
這樣依次下去就形成了一個(gè)鏈?zhǔn)秸{(diào)用,看看源碼,具體有哪些攔截器:
Response getResponseWithInterceptorChain() throws IOException { // Build a full stack of interceptors. List<Interceptor> interceptors = new ArrayList<>(); interceptors.addAll(client.interceptors()); interceptors.add(retryAndFollowUpInterceptor); interceptors.add(new BridgeInterceptor(client.cookieJar())); interceptors.add(new CacheInterceptor(client.internalCache())); interceptors.add(new ConnectInterceptor(client)); if (!forWebSocket) { interceptors.addAll(client.networkInterceptors()); } interceptors.add(new CallServerInterceptor(forWebSocket)); Interceptor.Chain chain = new RealInterceptorChain( interceptors, null, null, null, 0, originalRequest); return chain.proceed(originalRequest); }
根據(jù)源碼可知,一共七個(gè)攔截器:
addInterceptor(Interceptor)
,這是由開(kāi)發(fā)者設(shè)置的,會(huì)按照開(kāi)發(fā)者的要求,在所有的攔截器處理之前進(jìn)行最早的攔截處理,比如一些公共參數(shù),Header都可以在這里添加。RetryAndFollowUpInterceptor
,這里會(huì)對(duì)連接做一些初始化工作,以及請(qǐng)求失敗的充實(shí)工作,重定向的后續(xù)請(qǐng)求工作。跟他的名字一樣,就是做重試工作還有一些連接跟蹤工作。BridgeInterceptor
,這里會(huì)為用戶(hù)構(gòu)建一個(gè)能夠進(jìn)行網(wǎng)絡(luò)訪問(wèn)的請(qǐng)求,同時(shí)后續(xù)工作將網(wǎng)絡(luò)請(qǐng)求回來(lái)的響應(yīng)Response轉(zhuǎn)化為用戶(hù)可用的Response,比如添加文件類(lèi)型,content-length計(jì)算添加,gzip解包。CacheInterceptor
,這里主要是處理cache相關(guān)處理,會(huì)根據(jù)OkHttpClient對(duì)象的配置以及緩存策略對(duì)請(qǐng)求值進(jìn)行緩存,而且如果本地有了可?的Cache,就可以在沒(méi)有網(wǎng)絡(luò)交互的情況下就返回緩存結(jié)果。ConnectInterceptor
,這里主要就是負(fù)責(zé)建立連接了,會(huì)建立TCP連接或者TLS連接,以及負(fù)責(zé)編碼解碼的HttpCodecnetworkInterceptors
,這里也是開(kāi)發(fā)者自己設(shè)置的,所以本質(zhì)上和第一個(gè)攔截器差不多,但是由于位置不同,所以用處也不同。這個(gè)位置添加的攔截器可以看到請(qǐng)求和響應(yīng)的數(shù)據(jù)了,所以可以做一些網(wǎng)絡(luò)調(diào)試。CallServerInterceptor
,這里就是進(jìn)行網(wǎng)絡(luò)數(shù)據(jù)的請(qǐng)求和響應(yīng)了,也就是實(shí)際的網(wǎng)絡(luò)I/O操作,通過(guò)socket讀寫(xiě)數(shù)據(jù)。
OkHttp怎么實(shí)現(xiàn)連接池
為什么需要連接池?
頻繁的進(jìn)行建立Sokcet
連接和斷開(kāi)Socket
是非常消耗網(wǎng)絡(luò)資源和浪費(fèi)時(shí)間的,所以HTTP中的keepalive
連接對(duì)于降低延遲和提升速度有非常重要的作用。keepalive機(jī)制
是什么呢?也就是可以在一次TCP連接中可以持續(xù)發(fā)送多份數(shù)據(jù)而不會(huì)斷開(kāi)連接。所以連接的多次使用,也就是復(fù)用就變得格外重要了,而復(fù)用連接就需要對(duì)連接進(jìn)行管理,于是就有了連接池的概念。
OkHttp中使用ConectionPool
實(shí)現(xiàn)連接池,默認(rèn)支持5個(gè)并發(fā)KeepAlive
,默認(rèn)鏈路生命為5分鐘。
怎么實(shí)現(xiàn)的?
1)首先,ConectionPool
中維護(hù)了一個(gè)雙端隊(duì)列Deque
,也就是兩端都可以進(jìn)出的隊(duì)列,用來(lái)存儲(chǔ)連接。
2)然后在ConnectInterceptor
,也就是負(fù)責(zé)建立連接的攔截器中,首先會(huì)找可用連接,也就是從連接池中去獲取連接,具體的就是會(huì)調(diào)用到ConectionPool
的get方法。
RealConnection get(Address address, StreamAllocation streamAllocation, Route route) { assert (Thread.holdsLock(this)); for (RealConnection connection : connections) { if (connection.isEligible(address, route)) { streamAllocation.acquire(connection, true); return connection; } } return null; }
也就是遍歷了雙端隊(duì)列,如果連接有效,就會(huì)調(diào)用acquire方法計(jì)數(shù)并返回這個(gè)連接。
3)如果沒(méi)找到可用連接,就會(huì)創(chuàng)建新連接,并會(huì)把這個(gè)建立的連接加入到雙端隊(duì)列中,同時(shí)開(kāi)始運(yùn)行線程池中的線程,其實(shí)就是調(diào)用了ConectionPool
的put方法。
public final class ConnectionPool { void put(RealConnection connection) { if (!cleanupRunning) { //沒(méi)有連接的時(shí)候調(diào)用 cleanupRunning = true; executor.execute(cleanupRunnable); } connections.add(connection); } }
其實(shí)這個(gè)線程池中只有一個(gè)線程,是用來(lái)清理連接的,也就是上述的cleanupRunnable
private final Runnable cleanupRunnable = new Runnable() { @Override public void run() { while (true) { //執(zhí)行清理,并返回下次需要清理的時(shí)間。 long waitNanos = cleanup(System.nanoTime()); if (waitNanos == -1) return; if (waitNanos > 0) { long waitMillis = waitNanos / 1000000L; waitNanos -= (waitMillis * 1000000L); synchronized (ConnectionPool.this) { //在timeout時(shí)間內(nèi)釋放鎖 try { ConnectionPool.this.wait(waitMillis, (int) waitNanos); } catch (InterruptedException ignored) { } } } } } };
這個(gè)runnable
會(huì)不停的調(diào)用cleanup方法清理線程池,并返回下一次清理的時(shí)間間隔,然后進(jìn)入wait等待。
怎么清理的呢?看看源碼:
long cleanup(long now) { synchronized (this) { //遍歷連接 for (Iterator<RealConnection> i = connections.iterator(); i.hasNext(); ) { RealConnection connection = i.next(); //檢查連接是否是空閑狀態(tài), //不是,則inUseConnectionCount + 1 //是 ,則idleConnectionCount + 1 if (pruneAndGetAllocationCount(connection, now) > 0) { inUseConnectionCount++; continue; } idleConnectionCount++; // If the connection is ready to be evicted, we're done. long idleDurationNs = now - connection.idleAtNanos; if (idleDurationNs > longestIdleDurationNs) { longestIdleDurationNs = idleDurationNs; longestIdleConnection = connection; } } //如果超過(guò)keepAliveDurationNs或maxIdleConnections, //從雙端隊(duì)列connections中移除 if (longestIdleDurationNs >= this.keepAliveDurationNs || idleConnectionCount > this.maxIdleConnections) { connections.remove(longestIdleConnection); } else if (idleConnectionCount > 0) { //如果空閑連接次數(shù)>0,返回將要到期的時(shí)間 // A connection will be ready to evict soon. return keepAliveDurationNs - longestIdleDurationNs; } else if (inUseConnectionCount > 0) { // 連接依然在使用中,返回保持連接的周期5分鐘 return keepAliveDurationNs; } else { // No connections, idle or in use. cleanupRunning = false; return -1; } } closeQuietly(longestIdleConnection.socket()); // Cleanup again immediately. return 0; }
也就是當(dāng)如果空閑連接maxIdleConnections
超過(guò)5個(gè)或者keepalive時(shí)間大于5分鐘,則將該連接清理掉。
4)這里有個(gè)問(wèn)題,怎樣屬于空閑連接?
其實(shí)就是有關(guān)剛才說(shuō)到的一個(gè)方法acquire
計(jì)數(shù)方法:
public void acquire(RealConnection connection, boolean reportedAcquired) { assert (Thread.holdsLock(connectionPool)); if (this.connection != null) throw new IllegalStateException(); this.connection = connection; this.reportedAcquired = reportedAcquired; connection.allocations.add(new StreamAllocationReference(this, callStackTrace)); }
在RealConnection
中,有一個(gè)StreamAllocation
虛引用列表allocations
。每創(chuàng)建一個(gè)連接,就會(huì)把連接對(duì)應(yīng)的StreamAllocationReference
添加進(jìn)該列表中,如果連接關(guān)閉以后就將該對(duì)象移除。
其實(shí)可以這樣理解,在上層反復(fù)調(diào)用acquire和release函數(shù),來(lái)增加或減少connection.allocations所維持的集合的大小,到最后如果size大于0,則代表RealConnection還在使用連接,如果size等于0,那就說(shuō)明已經(jīng)處于空閑狀態(tài)了
5)連接池的工作就這么多,并不復(fù)雜,主要就是管理雙端隊(duì)列Deque<RealConnection>
,可以用的連接就直接用,然后定期清理連接,同時(shí)通過(guò)對(duì)StreamAllocation
的引用計(jì)數(shù)實(shí)現(xiàn)自動(dòng)回收。
OkHttp里面用到了什么設(shè)計(jì)模式
責(zé)任鏈模式
這個(gè)不要太明顯,可以說(shuō)是okhttp的精髓所在了,主要體現(xiàn)就是攔截器的使用,具體代碼可以看看上述的攔截器介紹。
建造者模式
在Okhttp中,建造者模式也是用的挺多的,主要用處是將對(duì)象的創(chuàng)建與表示相分離,用Builder組裝各項(xiàng)配置。
比如Request:
public class Request { public static class Builder { @Nullable HttpUrl url; String method; Headers.Builder headers; @Nullable RequestBody body; public Request build() { return new Request(this); } } }
工廠模式
工廠模式和建造者模式類(lèi)似,區(qū)別就在于工廠模式側(cè)重點(diǎn)在于對(duì)象的生成過(guò)程,而建造者模式主要是側(cè)重對(duì)象的各個(gè)參數(shù)配置。
例子有CacheInterceptor攔截器中又個(gè)CacheStrategy對(duì)象:
CacheStrategy strategy = new CacheStrategy.Factory(now, chain.request(), cacheCandidate).get(); public Factory(long nowMillis, Request request, Response cacheResponse) { this.nowMillis = nowMillis; this.request = request; this.cacheResponse = cacheResponse; if (cacheResponse != null) { this.sentRequestMillis = cacheResponse.sentRequestAtMillis(); this.receivedResponseMillis = cacheResponse.receivedResponseAtMillis(); Headers headers = cacheResponse.headers(); for (int i = 0, size = headers.size(); i < size; i++) { String fieldName = headers.name(i); String value = headers.value(i); if ("Date".equalsIgnoreCase(fieldName)) { servedDate = HttpDate.parse(value); servedDateString = value; } else if ("Expires".equalsIgnoreCase(fieldName)) { expires = HttpDate.parse(value); } else if ("Last-Modified".equalsIgnoreCase(fieldName)) { lastModified = HttpDate.parse(value); lastModifiedString = value; } else if ("ETag".equalsIgnoreCase(fieldName)) { etag = value; } else if ("Age".equalsIgnoreCase(fieldName)) { ageSeconds = HttpHeaders.parseSeconds(value, -1); } } } }
觀察者模式
之前我寫(xiě)過(guò)一篇文章,是關(guān)于Okhttp中websocket的使用,由于webSocket屬于長(zhǎng)連接,所以需要進(jìn)行監(jiān)聽(tīng),這里是用到了觀察者模式:
final WebSocketListener listener; @Override public void onReadMessage(String text) throws IOException { listener.onMessage(this, text); }
單例模式
這個(gè)就不舉例了,每個(gè)項(xiàng)目都會(huì)有
OkHttp中為什么使用構(gòu)建者模式?
使用多個(gè)簡(jiǎn)單的對(duì)象一步一步構(gòu)建成一個(gè)復(fù)雜的對(duì)象;
- 優(yōu)點(diǎn): 當(dāng)內(nèi)部數(shù)據(jù)過(guò)于復(fù)雜的時(shí)候,可以非常方便的構(gòu)建出我們想要的對(duì)象,并且不是所有的參數(shù)我們都需要進(jìn)行傳遞;
- 缺點(diǎn): 代碼會(huì)有冗余
怎么設(shè)計(jì)一個(gè)自己的網(wǎng)絡(luò)訪問(wèn)框架,為什么這么設(shè)計(jì)?
我目前還沒(méi)有正式設(shè)計(jì)過(guò)網(wǎng)絡(luò)訪問(wèn)框架,
是我自己設(shè)計(jì)的話,我會(huì)從以下兩個(gè)方面考慮
- 先參考現(xiàn)有的框架,找一個(gè)比較合適的框架作為啟動(dòng)點(diǎn),比如說(shuō),基于上面講到的
okhttp
的優(yōu)點(diǎn),選擇okhttp
的源碼進(jìn)行閱讀,并且將主線的流程抽取出,為什么這么做,因?yàn)?code>okhttp里面雖然涉及到了很多的內(nèi)容,但是我們用到的內(nèi)容并不是特別多;保證先能運(yùn)行起來(lái)一個(gè)基本的框架; - 考慮拓展,有了基本框架之后,我會(huì)按照我目前在項(xiàng)目中遇到的一些需求或者網(wǎng)路方面的問(wèn)題,看看能不能基于我這個(gè)框架進(jìn)行優(yōu)化,比如服務(wù)器它設(shè)置的緩存策略,
我應(yīng)該如何去編寫(xiě)客戶(hù)端的緩存策略去對(duì)應(yīng)服務(wù)器的,還比如說(shuō),可能剛剛?cè)ソ⒒镜目蚣軙r(shí),不會(huì)考慮HTTPS
的問(wèn)題,那么也會(huì)基于后來(lái)都要求https
,進(jìn)行拓展;
為什么要基于Okhttp
,就是因?yàn)樗腔赟ocket,從我個(gè)人角度講,如果能更底層的深入了解相關(guān)知識(shí),這對(duì)我未來(lái)的技術(shù)有很大的幫助;
如何考慮app的安全性?
1:使用https協(xié)議進(jìn)行交互
2:數(shù)據(jù)交互時(shí),根據(jù)業(yè)務(wù)分出哪些是敏感信息,凡是敏感信息使用對(duì)稱(chēng)加密方式,如果是類(lèi)似密碼的,則使用不可逆的加密方式;md5
3:考慮跟錢(qián)相關(guān),或者同等重要的數(shù)據(jù)接口,需要做多重驗(yàn)證,比如:前端加密請(qǐng)求參數(shù),合并請(qǐng)求參數(shù)生成MD5碼,服務(wù)器端做多重認(rèn)證,最好能對(duì)比本地?cái)?shù)據(jù)庫(kù)或者緩存之類(lèi)的信息;
4:混淆,
5: app加固,dex文件進(jìn)行加密,這種方式,可以通過(guò)”內(nèi)存下載“,不安全,也只是為了增加破解難度;
6:將加密算法,一些核心數(shù)據(jù)添加到so文件中;
到此這篇關(guān)于OKHttp詳解的文章就介紹到這了,更多相關(guān)OKHttp詳解內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Activity isFinishing()判斷Activity的狀態(tài)實(shí)例
下面小編就為大家分享一篇Activity isFinishing()判斷Activity的狀態(tài)實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-03-03RecyclerView實(shí)現(xiàn)常見(jiàn)的列表菜單
這篇文章主要為大家詳細(xì)介紹了用RecyclerView實(shí)現(xiàn)移動(dòng)應(yīng)用中常見(jiàn)的列表菜單,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2016-12-12Android package屬性、package name和Application ID三者的聯(lián)系及區(qū)別
這篇文章主要介紹了Android package屬性、package name和Application ID三者的聯(lián)系及區(qū)別的相關(guān)資料,需要的朋友可以參考下2016-12-12Android簡(jiǎn)單的利用MediaRecorder進(jìn)行錄音的實(shí)例代碼
MediaRecorder可以進(jìn)行簡(jiǎn)單的錄音,由于操作簡(jiǎn)單所以可以用來(lái)進(jìn)行基本的錄音。下面提供一個(gè)簡(jiǎn)單的例子,記得在Mainfest文件中添加權(quán)限2013-08-08Android手機(jī)獲取root權(quán)限并實(shí)現(xiàn)關(guān)機(jī)重啟功能的方法
這篇文章主要介紹了Android手機(jī)獲取root權(quán)限并實(shí)現(xiàn)關(guān)機(jī)重啟功能的方法,是Android程序設(shè)計(jì)中非常重要的技巧,需要的朋友可以參考下2014-08-08Android 使用XML做動(dòng)畫(huà)UI的深入解析
在Android應(yīng)用程序,使用動(dòng)畫(huà)效果,能帶給用戶(hù)更好的感覺(jué)。做動(dòng)畫(huà)可以通過(guò)XML或Android代碼。本教程中,介紹使用XML來(lái)做動(dòng)畫(huà)。在這里,介紹基本的動(dòng)畫(huà),如淡入,淡出,旋轉(zhuǎn)等,需要的朋友可以參考下2013-07-07