編輯:關於Android編程
然後直接進入正題。
看完上面這篇文章,主要理解的幾個點:
外部通過構造Request,初始化OkHttpClient,並由兩者共同構造出Call。 訪問網絡通過Call,Call支持兩種模式:同步和異步。同步使用execute,該方法會立即返回一個response,該response中包含結果。異步使用enqueue,也要傳入callback來接受請求結果。 OkHttp還支持攔截器,攔截器分兩種,請求前攔截和網絡返回前攔截。 OkHttp還支持host檢查、證書檢查等等。並且還支持http1.0、http1.1、http2.0、SPDY協議。先給一張關系圖,嘔心瀝血之作。(這圖請放大看)
vcfrx/O1xMfpv/ajqMbkyrXNrLK90uyyvcO7x/ix8MCyo6mjrLj5vt3V4tXFzbzAtL2yveLSu7j2x+vH87XEwfezzKGjPC9wPg0KPHA+z8jAtL+0z8JSZXF1ZXN0o6zV4rj2wODA78Pmuty88rWlo6zWu8rHvMfCvMHL0rvQqbv5sb61xLLOyv2jrMD9yOd1cmyhom1ldGhvZKGiaGVhZGVyoaJyZXF1ZXN0Qm9kebXI0MXPoqGjPC9wPg0KPHA+1Nm/tM/CT2tIdHRwQ2xpZW501eK49sDgo6zV4rj2wO/D5tKyuty88rWlo6zDu9PQyrLDtLi01NO1xMLfvK2jrNa7ysfSu7j21tDXqtW+oaPU2tXiuPbA78PmvMfCvMHL0rvQqcirvta1xLarzvejqLWxyLtPa0h0dHBDbGllbnTV4rj20rLKx8irvtbOqNK7tcSjrLWrsrvKx7WlwP2jrMTjv8nS1LS0vai24Lj2T2tIdHRwQ2xpZW50o6yyu82stcTH68fz1Nqyu82stcRPa0h0dHBDbGllbnTW0KOsubLP7bXE18rUtNKysrvSu9H5o6zA/cjnz8LD5szhtb21xNK70KnXytS0o6mjrMD9yOc8Y29kZT5MaXN0PGludGVyY2VwdG9yPqOosPzAqMfrx/PHsLrNt7W72MewtcTAub3Yo6mjrENhY2hlz+C52LXE0rvQqcDgoaJDb25uZWN0aW9uUG9vbKOsu7nT0NK70Kmwssirt73D5rXEwOCjrMD9yOdTb2NrZXRGYWN0b3J5oaJTb2NrZXRTU0xGYWN0b3J5oaJUcnVzdFJvb3RJbmRleKOo0enWpMrpo6mhokhvc3RuYW1lVmVyaWZpZXKhokNlcnRpZmljYXRlUGlubmVytciho0Nvbm5lY3Rpb25Qb29s1eK49rrc1tjSqqOsv7TD+9fWvs3WqrXA1eLKx8GsvdOz2KOs1vfSqtPDwLS05rfFtbHHsLSm09rBrL3T17TMrLXEuPe49mNvbm5lY3Rpb26jrLrNz9+zzLPYysfSu7j2uMXE7qGjtbHQ6NKqzfjC58GsvdPKsaOsu+HPyLTTuMNwb29s1tDIobXD0ru49mNvbm5lY3Rpb26jrNXiuPbJ1Lrzu+G8zND4veLKzaOs1N3Ksc/I1eLDtMDtveKho0Nvbm5lY3Rpb25Qb29s1rvT0NTaT2tIdHRwQ2xpZW50wO/D5rP1yry7r6OssqLH0sirvtbS/dPDtb1jb25uZWN0aW9uUG9vbLa8tNNPa0h0dHBDbGllbnTW0NL908OhozwvaW50ZXJjZXB0b3I+PC9jb2RlPjwvcD4NCjxwPjxjb2RlPrv5sb7By73izerV4rj2wODWrrrzo6zAtL+0z8LI57rOubnU7LP2Y2FsbKOstffTw7XEysdPa0h0dHBDbGllbnQubmV3Q2FsbCguLim3vbeoo7o8L2NvZGU+PC9wPg0KPHByZSBjbGFzcz0="brush:java;">
@Override public Call newCall(Request request) {
return new RealCall(this, request);
}
該方法返回的是RealCall對象,RealCall是接下來需要介紹的一個類,這個類是繼承自Call。該類記錄了初始傳進來的request,還有HttpEngine(這裡暫時理解為網絡訪問的入口,通過HttpEngine來建立連接並返回數據)。剛剛提到了RealCall同步執行execute會立即返回一個response:
@Override public Response execute() throws IOException {
synchronized (this) {
if (executed) throw new IllegalStateException("Already Executed");
executed = true;
}
try {
client.dispatcher().executed(this);
Response result = getResponseWithInterceptorChain(false);
if (result == null) throw new IOException("Canceled");
return result;
} finally {
client.dispatcher().finished(this);
}
}
看Response result = getResponseWithInterceptorChain(false);這句,這裡需要說明下剛剛提到的攔截器,攔截器分為請求前和返回前攔截,所以這裡是請求前攔截。看下getResponseWithInterceptorChain(..)這個函數:
private Response getResponseWithInterceptorChain(boolean forWebSocket) throws IOException {
Interceptor.Chain chain = new ApplicationInterceptorChain(0, originalRequest, forWebSocket);
return chain.proceed(originalRequest);
}
這個函數很簡單,創建了一個攔截器鏈,來處理這個請求。
這裡先解釋下攔截器Interceptor和攔截器鏈Chain:
public interface Interceptor {
Response intercept(Chain chain) throws IOException;
interface Chain {
Request request();
Response proceed(Request request) throws IOException;
Connection connection();
}
}
攔截器的接口很簡單,只有一個intercept方法,用來處理該攔截器想要如何處理請求。Chain的接口也很簡單,request方法該鏈中被處理的請求。proceed也很簡單,返回當前被處理後的response。至於最後個方法,暫時不清楚。所以Chain的作用就是讓request從頭至尾經歷攔截器的Intercept方法。
在上面方法中,創建的是ApplicationInterceptorChain這個鏈,讓我們看下這個鏈proceed的具體實現:
@Override public Response proceed(Request request) throws IOException {
// If there's another interceptor in the chain, call that.
if (index < client.interceptors().size()) {
Interceptor.Chain chain = new ApplicationInterceptorChain(index + 1, request, forWebSocket);
Interceptor interceptor = client.interceptors().get(index);
Response interceptedResponse = interceptor.intercept(chain);
if (interceptedResponse == null) {
throw new NullPointerException("application interceptor " + interceptor
+ " returned null");
}
return interceptedResponse;
}
// No more interceptors. Do HTTP.
return getResponse(request, forWebSocket);
}
這個方法中就是遍歷client中所有的intercepts(請求前攔截),這裡要注意的是client中的攔截器是每個request都要經歷的。當走完所有的攔截器後,就去調用getResponse訪問網絡。看下getResponse中具體的實現:
Response getResponse(Request request, boolean forWebSocket) throws IOException {
// Copy body metadata to the appropriate request headers.
RequestBody body = request.body();
if (body != null) {
Request.Builder requestBuilder = request.newBuilder();
MediaType contentType = body.contentType();
if (contentType != null) {
requestBuilder.header("Content-Type", contentType.toString());
}
long contentLength = body.contentLength();
if (contentLength != -1) {
requestBuilder.header("Content-Length", Long.toString(contentLength));
requestBuilder.removeHeader("Transfer-Encoding");
} else {
requestBuilder.header("Transfer-Encoding", "chunked");
requestBuilder.removeHeader("Content-Length");
}
request = requestBuilder.build();
}
// Create the initial HTTP engine. Retries and redirects need new engine for each attempt.
engine = new HttpEngine(client, request, false, false, forWebSocket, null, null, null);
int followUpCount = 0;
while (true) {
if (canceled) {
engine.releaseStreamAllocation();
throw new IOException("Canceled");
}
boolean releaseConnection = true;
try {
engine.sendRequest();
engine.readResponse();
releaseConnection = false;
} catch (RequestException e) {
// The attempt to interpret the request failed. Give up.
throw e.getCause();
} catch (RouteException e) {
// The attempt to connect via a route failed. The request will not have been sent.
HttpEngine retryEngine = engine.recover(e.getLastConnectException(), null);
if (retryEngine != null) {
releaseConnection = false;
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
throw e.getLastConnectException();
} catch (IOException e) {
// An attempt to communicate with a server failed. The request may have been sent.
HttpEngine retryEngine = engine.recover(e, null);
if (retryEngine != null) {
releaseConnection = false;
engine = retryEngine;
continue;
}
// Give up; recovery is not possible.
throw e;
} finally {
// We're throwing an unchecked exception. Release any resources.
if (releaseConnection) {
StreamAllocation streamAllocation = engine.close();
streamAllocation.release();
}
}
Response response = engine.getResponse();
Request followUp = engine.followUpRequest();
if (followUp == null) {
if (!forWebSocket) {
engine.releaseStreamAllocation();
}
return response;
}
StreamAllocation streamAllocation = engine.close();
if (++followUpCount > MAX_FOLLOW_UPS) {
streamAllocation.release();
throw new ProtocolException("Too many follow-up requests: " + followUpCount);
}
if (!engine.sameConnection(followUp.url())) {
streamAllocation.release();
streamAllocation = null;
}
request = followUp;
engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,
response);
}
}
該方法中首先是根據request中的參數重新構建一個request,在request的頭部中加入了一些參數。然後就根據HttpEngine來訪問網絡了。
首先根據request和OkHttpClient創建了一個HttpEngine(當然還有其他的參數,稍後解釋),然後就進入了一個while(true)的循環。這個循環中先判斷該request是不是被cancel了。然後就調用了HttpEngine的sendRequest方法(後面講解,這理解為就是發送請求),然後在調用HttpEngine的readResponse方法來讀取訪問結果。如果訪問時發生了錯誤,例如RouteException、IOException,則會嘗試重連,調用HttpEngine的recover方法,重新生成一個HttpEngine來訪問,然後重新進入剛剛的循環。如果訪問成功,則調用getResponse方法來獲取訪問結果,調用followUpRequest來獲取重定向請求,這裡需要了解下重定向的概念。
一個簡單的重定向例子就是一個網站,登陸的用戶分為兩種:管理員和游客,同一個url,對於不同身份的人來說訪問到的頁面可能不一樣,這裡就是用了重定向功能,例如該網站的某個頁面,如果管理員訪問是長某某樣子(網頁A),游客訪問是長某某樣子(網頁B),這兩個網頁對應的url也不同,但是初始訪問的url都是一樣的,這是通過服務器對url和用戶解析,返回一個新的url讓用戶去訪問。不知道這麼講大家懂了沒。。。一個簡單的請求,重定向可以多次,不同的浏覽器支持不同的次數。OkHttp框架中重定向最大次數由HttpEngine.MAX_FOLLOW_UPS決定:
回到正題,剛剛講到如果訪問成功,則調用getResponse方法來獲取訪問結果,調用followUpRequest來獲取重定向請求。這裡要了解的是,如果服務器發現要重定向,此次response還是返回200(代表成功),在response中還會包含下個重定向的url等信息。所以拿到response前需要檢測是否需要重定向,如果要重定向還要檢測是否超過最大值,因為剛剛說到這是在一個while(true)的循環裡,重定向可能多次。如果發現followUpRequest為空,則說明response已拿到,直接返回。若不為空,則先檢測是否超過最大值,若不超過,則新生成一個httpEngine來訪問。
上面這段代碼基本講解完畢,但是在代碼中,還看到一個概念StreamAllocation,將這個詞分開來看,stream和allocation。解釋下stream,是流的概念,和我們平時用InputStream和outputStream是一樣的概念,我不知道該怎麼解釋http中的stream,就暫且把stream看成是一個請求在http中輸入輸出的流,一個stream代表一個請求。allocation比較簡單,就是分配的概念,合起來看的話就是記錄一個stream分配。StreamAllocation是一個比較重要的概念,首先先要了解下http版本。
http版本從最初的1.0版本,到後續的1.1版本,再到後續Google推出了SPDY,後來在推出了2.0版本,http協議越來越完善,下面幾篇資料是講述http協議的一個發展以及優化的過程。
HTTP1.0和HTTP1.1的區別 ———- http://blog.csdn.net/elifefly/article/details/3964766/
HTTP2.0的講解 ———- http://www.open-open.com/lib/view/open1455796649605.html
HOL排頭阻塞(head of line blocking) – http://baike.baidu.com/link?url=o2BnfuFRd-5jaIlcfAPMolg8nU-U4FUcjlK3jBA9PqI8iffnw3Q7nqx4EzNnjp9BKQDrTWuHFhLGquwxCH1DIa
socket講解————–http://blog.csdn.net/semillon/article/details/7515926
上面這幾篇文章一定要看,特別是http2.0的講解和socket講解這篇文章,閱讀完該篇文章才能理解後續的講解以及OkHttp設計的一個思想。
看完上述幾篇文章,主要的一個核心思想就是http2.0和http1.0/1.1的主要區別,而OkHttp也是根據2.0和1.0/1.1作為區分,實現了兩種連接機制。2.0解決了老版本(後續成1.0和1.1為老版本)最重要的兩個問題:連接無法復用和head of line blocking問題。2.0使用了多路復用的技術,多個stream可以共用一個tcp連接,每個tcp連接都是通過一個socket來完成的,socket對應一個host和一個port,如果有多個stream(也就是多個request)都是連接到同一個host和port上,那麼他們就可以共同使用一個socket。這樣做的好處是減少TCP的一個三次握手的時間。 那在OkHttp裡,記錄一次連接的是RealConnection,這個就是負責連接的,在這個類中用socket來連接,還有HandShake來處理握手。
這裡要說明下,OkHttp和HttpUrlConnection以及Volley的區別。雖然這三個都是可以用來訪問網絡的,但是還是不同的。我們最熟悉的肯定是HttpUrlConnection,這是google官方提供的用來訪問網絡,但是HttpUrlConnection實現的比較簡單,只支持1.0/1.1,並沒有上面講的多路復用,如果碰到app大量網絡請求的時候,性能比較差,而且HttpUrlConnection底層也是用Socket來實現的。而Volley是一個開源庫,它只是封裝了訪問網絡的一些操作,但是底層還是使用HttpUrlConnection。但是OkHttp不同,在看源碼之前,一直以為OkHttp和Volley一樣,是用HttpUrlConnection,但是找了半天代碼,都沒看到HttpUrlConnection和InputStream(OutputStream),倒是反而看到socket和sink(source)這些,後來才搞明白,原來OkHttp像HttpUrlConnection一樣,實現了一個網絡連接的過程。所以按照層級來說,OkHttp和HttpUrlConnection是一級的,用socket實現了網絡連接,只是OkHttp更強大,而Volley只是一個引用了HttpUrlConnection,它並不關心網絡連接過程,只是封裝了請求的過程而已。剛剛提到HttpUrlConnection在IO方面用到的是InputStream和OutputStream,但是OkHttp用的是sink和source,這兩個是在Okio這個開源庫裡的,sink相當於outputStream,source相當於是inputStream。sink和source比InputStream和OutputStream更加強大,單拿sink舉例,他的子類有BufferedSink(支持緩沖)、GzipSink(支持Gzip壓縮)、ForwardingSink和InflaterSink(後面這兩者服務於GzipSink),source對應的也有,具體的可以自行上網查找。
剛剛我們講到多個相同host和port的stream可以共同使用一個socket,而RealConnection就是處理連接的,那也就是說一個RealConnection上可能有很多個Stream,所以在RealConnection上記錄了
List
再回到剛剛那個函數的最後一點代碼:
if (!engine.sameConnection(followUp.url())) {
streamAllocation.release();
streamAllocation = null;
}
request = followUp;
engine = new HttpEngine(client, request, false, false, forWebSocket, streamAllocation, null,response);
當發現需要重定向的時候,就會執行這段代碼,首先先檢測重定向的url和剛剛的請求是不是同一個Connection,看下sameConnection函數:
public boolean sameConnection(HttpUrl followUp){
HttpUrl url = userRequest.url();
return url.host().equals(followUp.host())
&& url.port() == followUp.port()
&& url.scheme().equals(followUp.scheme());
}
這函數很簡單,只是看下這兩個url的host、port、scheme是不是一樣。如果發現不一樣,就釋放HttpEngine原來的streamAllocation,並置空,如果發現一樣,則重用剛剛的stream。HttpEngine的構造函數裡面會判斷傳入的StreamAllocation是不是為空,若為空則創建一個根據request,並傳入ConnectionPool,創建一個streamAllocation,並且從ConnectionPool中取出Connection,並將該Connection記錄到StreamAllocation中,如果沒有可用的RealConnection,就創建個新的,然後再放到ConnectionPool中。
講到這裡,應該還是比較清楚的,目前已經講清楚幾個類了:OkHttpClient、RealCall、StreamAllocation、RealConnection、ConnectionPool。
接下來看下HttpEngine。這個類還是比較復雜的,剛剛在RealCall中只是看到調用了HttpEngine的一些函數,大致明白這些函數的意義,那現在繼續看看這些函數內部實現。
先看下sendRequest函數:
public void sendRequest() throws RequestException, RouteException, IOException {
if (cacheStrategy != null) return; // Already sent.
if (httpStream != null) throw new IllegalStateException();
Request request = networkRequest(userRequest);
InternalCache responseCache = Internal.instance.internalCache(client);
Response cacheCandidate = responseCache != null
? responseCache.get(request)
: null;
long now = System.currentTimeMillis();
cacheStrategy = new CacheStrategy.Factory(now, request, cacheCandidate).get();
networkRequest = cacheStrategy.networkRequest;
cacheResponse = cacheStrategy.cacheResponse;
if (responseCache != null) {
responseCache.trackResponse(cacheStrategy);
}
if (cacheCandidate != null && cacheResponse == null) {
closeQuietly(cacheCandidate.body()); // The cache candidate wasn't applicable. Close it.
}
// If we're forbidden from using the network and the cache is insufficient, fail.
if (networkRequest == null && cacheResponse == null) {
userResponse = new Response.Builder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.protocol(Protocol.HTTP_1_1)
.code(504)
.message("Unsatisfiable Request (only-if-cached)")
.body(EMPTY_BODY)
.build();
return;
}
// If we don't need the network, we're done.
if (networkRequest == null) {
userResponse = cacheResponse.newBuilder()
.request(userRequest)
.priorResponse(stripBody(priorResponse))
.cacheResponse(stripBody(cacheResponse))
.build();
userResponse = unzip(userResponse);
return;
}
// We need the network to satisfy this request. Possibly for validating a conditional GET.
boolean success = false;
try {
httpStream = connect();
httpStream.setHttpEngine(this);
if (writeRequestHeadersEagerly()) {
long contentLength = OkHeaders.contentLength(request);
if (bufferRequestBody) {
if (contentLength > Integer.MAX_VALUE) {
throw new IllegalStateException("Use setFixedLengthStreamingMode() or "
+ "setChunkedStreamingMode() for requests larger than 2 GiB.");
}
if (contentLength != -1) {
// Buffer a request body of a known length.
httpStream.writeRequestHeaders(networkRequest);
requestBodyOut = new RetryableSink((int) contentLength);
} else {
// Buffer a request body of an unknown length. Don't write request headers until the
// entire body is ready; otherwise we can't set the Content-Length header correctly.
requestBodyOut = new RetryableSink();
}
} else {
httpStream.writeRequestHeaders(networkRequest);
requestBodyOut = httpStream.createRequestBody(networkRequest, contentLength);
}
}
success = true;
} finally {
// If we're crashing on I/O or otherwise, don't leak the cache body.
if (!success && cacheCandidate != null) {
closeQuietly(cacheCandidate.body());
}
}
}
代碼還是很長的,先來看下第五行,先是調用了一個networkRequest返回了一個request,這個函數就是對原來我們外部傳進去的request做了一個封裝,封裝成一個真正訪問網絡請求的request。在HttpEngine中有兩個request,一個叫userRequest,一個是networkRequest,第一個是外部傳入的,未經OkHttp修改的,第二個是根據userRequest封裝的一個request,用來訪問網絡。接下來就是獲取InternalCache,Internal.instance是一個單例,該單例初始化是在OkHttpClient裡面,可以看到調用Internal.instance.internalCache(client)函數只是調用了client(OkHttpClient)的internalCache,這個函數是返回了我們傳入OkHttpClient的cache,如果你創建OkHttpClient的時候,沒有傳入,那麼這裡就會返回空。
得到了InternalCache後,嘗試根據request去獲取response,當然可能為空。接下來就到CacheStrategy了,CacheStrategy是一個決定訪問網絡還是訪問緩存的類。CacheStrategy.Factory是工廠類,通過傳入request和剛剛internalCache中獲取到的response,去get一個CacheStrategy,Factory.get函數主要是對request和response做了一個檢查,會根據response合不合法、是否過期、request的參數來判斷,最後返回一個CacheStrategy。CacheStrategy很簡單,裡面有兩個變量,一個是networkRequest,一個是cacheResponse(這個變量和傳入Factory的response可能是不一樣的哦!)。返回到剛剛的HttpEngine.sendRequest函數中看第17行,如果InternalCache不為空,就調用trackResponse,這個函數很簡單,就是記錄下緩存讀取命中率這些數據。然後如果從InternalCache中response不為空,但是cacheStrategy的response為空,則說明剛剛InternalCache取出來的response無效,則需要關掉。如果CacheStrategy的networkRequest不為空,則說明需要進行網絡訪問,如果cacheResponse不為空,則說明訪問緩存足以。根據這個理論,下面一直到第47行的代碼不成問題,就不解釋了。接下來看網絡訪問的部分。
網絡訪問主要是通過HttpStream來實現,HttpStream這是一個接口,裡面定義了一些函數,這些函數是負責讀取網絡數據、輸出數據等。實現該接口的有兩個類,一個是Http2xStream,一個是Http1xStream。第一個是專門負責Http2.0版本的,第二個是負責Http1.x版本的,兩者內部實現機制不一樣。Http2xStream是通過一個FramedConnection,至於對FramedConnection的理解,可以看前面關於Http2.0講解的文章,看完那個你應該比較能夠大概清楚的理解他了,我就不解釋了,這裡我也沒深入去看。而Http1xStream則是通過sink和source來實現的,這個前面講過了,具體自己看吧,不解釋了。
再次返回代碼,connect函數返回了一個HttpStream,看下具體實現:
private HttpStream connect() throws RouteException, RequestException, IOException {
boolean doExtensiveHealthChecks = !networkRequest.method().equals("GET");
return streamAllocation.newStream(client.connectTimeoutMillis(),
client.readTimeoutMillis(), client.writeTimeoutMillis(),
client.retryOnConnectionFailure(), doExtensiveHealthChecks);
}
主要還是調用了stream.newStream,接下來的源碼就不看了,實在太多,感覺這篇文章可以繞地球三圈了。
newStream的函數裡面,首先就是去創建一個RealConnection,然後根據返回的RealConnection裡的參數去判斷,如果RealConnection.framedConnection不為空,則返回Http2xStream(這裡面包含connection的framedConnection),如果為空,則返回Http1xStream(這裡包含connection的sink和source),所以這和我前面講到的一樣,RealConnection只是負責連接到服務器,具體的數據傳輸讀取還是通過HttpStream。
那streamAllocation如何創建一個RealConnection呢,這個前面講過,先看看StreamAllocation自己是否已經有一個connection,如果有則返回,若沒有繼續。根據StreamAllocation裡面的address去connectionPool裡面去取Connection,如果發現有則返回,如果沒有就新創建一個,創建之後放入connectionPool中,並連接!!這點很重要,放入連接池的或者已經記錄在StreamAllocation中的都是已連接的。
創建完HttpStream後就是數據流的進行了,後面應該就可以自己看懂了,不解釋了。
如果一個stream結束了,需要release,並且關閉sink和source、framedConnection,還要釋放RealConnection的list的一個,總之就是收尾工作要做好。然後HttpEngine的readResponse也就不解釋了,對了在這裡還有結果返回前的一個攔截,和之前攔截差不多一個原理,也不解釋了。
還有就是Route和RouteDatabase,這兩個類的意思就是記住當前已經走過的彎路,後續不要再走了。
新手弄第三方類庫也許會很納悶,有時弄幾天都不行。那就讓我帶大家10分鐘做一個簡單的短信驗證吧!1.首先上Mob官網注冊賬號:http://www.mob.com/2.下載
一 簡介 SQLite是一個輕量的、跨平台的、開源的數據庫引擎,它的讀寫效率、資源消耗總量、延遲時間和整體簡單性上具有的優越性,使其成為移動平台數
谷歌官方提供了apktool可以逆向已經發布出去的APK應用,即反編譯已經打包成功的APK文件,使用它可以將其反編譯成非常接近打包前的原始格式,對於APK來說,可以具體的
ToggleButton的狀態只能是選中和未選中,並且需要為不同的狀態設置不同的顯示文本。以下案例為ToggleButton的用法目錄結構main.xml布局文件復制代碼