OkHttp的理解和使用
- OkHttp
- 1、什么是OkHttp
- 2、OkHttp的作用
- 3、Okhttp的基本使用
- 3.1、Http請求和回應的組成
- 3.2、OkHttp請求和回應的組成
- 3.3、GET請求同步方法
- 3.4、GET請求異步方法
- 3.5、post請求方法
- 3.6、POST請求傳遞引數的方法總結
- 3.6.1、Post方式提交String
- 3.6.2、Post方式提交 `流`
- 3.6.3、Post方式提交檔案
- 3.6.4、Post方式提交表單
- 3.7、POST其他用法
- 3.7.1、提取回應頭
- 3.7.2、使用Gson來決議JSON回應
- 3.7.3、回應快取
- 3.8、綜合實體
- 參考
OkHttp

1、什么是OkHttp
1、網路請求發展
歷史上Http請求庫優缺點
HttpURLConnection—>Apache HTTP Client—>Volley—->okHttp
2、專案開源地址
https://github.com/square/okhttp
3、OkHttp是什么
- OKhttp是一個網路請求開源專案,Android網路請求輕量級框架,支持檔案上傳與下載,支持https,
2、OkHttp的作用
OkHttp是一個高效的HTTP庫:
- 支持HTTP/2, HTTP/2通過使用多路復用技術在一個單獨的TCP連接上支持并發, 通過在一個連接上一次性發送多個請求來發送或接收資料
- 如果HTTP/2不可用, 連接池復用技術也可以極大減少延時
- 支持GZIP, 可以壓縮下載體積
- 回應快取可以直接避免重復請求
- 會從很多常用的連接問題中自動恢復
- 如果您的服務器配置了多個IP地址, 當第一個IP連接失敗的時候, OkHttp會自動嘗試下一個IP OkHttp還處理了代理服務器問題和SSL握手失敗問題
優勢
- 使用 OkHttp無需重寫您程式中的網路代碼,OkHttp實作了幾乎和java.net.HttpURLConnection一樣的API,如果您用了 Apache HttpClient,則OkHttp也提供了一個對應的okhttp-apache 模塊
3、Okhttp的基本使用
Okhttp的基本使用,從以下五方面講解:
- 1.Get請求(同步和異步)
- 2.POST請求表單(key-value)
- 3.POST請求提交(JSON/String/檔案等)
- 4.檔案下載
- 5.請求超時設定
加入build.gradle
compile 'com.squareup.okhttp3:okhttp:3.6.0'
3.1、Http請求和回應的組成
http請求

所以一個類別庫要完成一個http請求, 需要包含 請求方法, 請求地址, 請求協議, 請求頭, 請求體這五部分. 這些都在okhttp3.Request的類中有體現, 這個類正是代表http請求的類. 看下圖:

其中HttpUrl類代表請求地址, String method代表請求方法, Headers代表請求頭, RequestBody代表請求體. Object tag這個是用來取消http請求的標志, 這個我們先不管.
http回應
回應組成圖:

可以看到大體由應答首行, 應答頭, 應答體構成. 但是應答首行表達的資訊過多, HTTP/1.1表示訪問協議, 200是回應碼, OK是描述狀態的訊息.
根據單一職責, 我們不應該把這么多內容用一個應答首行來表示. 這樣的話, 我們的回應就應該由訪問協議, 回應碼, 描述資訊, 回應頭, 回應體來組成.
3.2、OkHttp請求和回應的組成
OkHttp請求
構造一個http請求, 并查看請求具體內容:
final Request request = new Request.Builder().url("https://github.com/").build();
我們看下在記憶體中, 這個請求是什么樣子的, 是否如我們上文所說和請求方法, 請求地址, 請求頭, 請求體一一對應.

OkHttp回應
OkHttp庫怎么表示一個回應:

可以看到Response類里面有Protocol代表請求協議, int code代表回應碼, String message代表描述資訊, Headers代表回應頭, ResponseBody代表回應體. 當然除此之外, 還有Request代表持有的請求, Handshake代表SSL/TLS握手協議驗證時的資訊, 這些額外資訊我們暫時不問.
有了剛才說的OkHttp回應的類組成, 我們看下OkHttp請求后回應在記憶體中的內容:
final Request request = new Request.Builder().url("https://github.com/").build();
Response response = client.newCall(request).execute();

3.3、GET請求同步方法
同步GET的意思是一直等待http請求, 直到回傳了回應. 在這之間會阻塞行程, 所以通過get不能在Android的主執行緒中執行, 否則會報錯.
對于同步請求在請求時需要開啟子執行緒,請求成功后需要跳轉到UI執行緒修改UI,
public void getDatasync(){
new Thread(new Runnable() {
@Override
public void run() {
try {
OkHttpClient client = new OkHttpClient();//創建OkHttpClient物件
Request request = new Request.Builder()
.url("http://www.baidu.com")//請求介面,如果需要傳參拼接到介面后面,
.build();//創建Request 物件
Response response = null;
response = client.newCall(request).execute();//得到Response 物件
if (response.isSuccessful()) {
Log.d("kwwl","response.code()=="+response.code());
Log.d("kwwl","response.message()=="+response.message());
Log.d("kwwl","res=="+response.body().string());
//此時的代碼執行在子執行緒,修改UI的操作請使用handler跳轉到UI執行緒,
}
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
此時列印結果如下:
response.code()==200;
response.message()OK;
res{“code”:200,“message”:success};
OkHttpClient實作了Call.Factory介面, 是Call的工廠類, Call負責發送執行請求和讀取回應.
Request代表Http請求, 通過Request.Builder輔助類來構建.
client.newCall(request)通過傳入一個http request, 回傳一個Call呼叫. 然后執行execute()方法, 同步獲得Response代表Http請求的回應. response.body()是ResponseBody類, 代表回應體
注意事項:
1,Response.code是http回應行中的code,如果訪問成功則回傳200.這個不是服務器設定的,而是http協議中自帶的,res中的code才是服務器設定的,注意二者的區別,
2,response.body().string()本質是輸入流的讀操作,所以它還是網路請求的一部分,所以這行代碼必須放在子執行緒,
3,response.body().string()只能呼叫一次,在第一次時有回傳值,第二次再呼叫時將會回傳null,原因是:response.body().string()的本質是輸入流的讀操作,必須有服務器的輸出流的寫操作時客戶端的讀操作才能得到資料,而服務器的寫操作只執行一次,所以客戶端的讀操作也只能執行一次,第二次將回傳null,
4、回應體的string()方法對于小檔案來說十分方便高效. 但是如果回應體太大(超過1MB), 應避免使用 string()方法, 因為它會將把整個檔案加載到記憶體中.
5、對于超過1MB的回應body, 應使用流的方式來處理回應body. 這和我們處理xml檔案的邏輯是一致的, 小檔案可以載入記憶體樹狀決議, 大檔案就必須流式決議.
注解:
responseBody.string()獲得字串的表達形式, 或responseBody.bytes()獲得位元組陣列的表達形式, 這兩種形式都會把檔案加入到記憶體. 也可以通過responseBody.charStream()和responseBody.byteStream()回傳流來處理.
3.4、GET請求異步方法
異步GET是指在另外的作業執行緒中執行http請求, 請求時不會阻塞當前的執行緒, 所以可以在Android主執行緒中使用.
這種方式不用再次開啟子執行緒,但回呼方法是執行在子執行緒中,所以在更新UI時還要跳轉到UI執行緒中,
下面是在一個作業執行緒中下載檔案, 當回應可讀時回呼Callback介面. 當回應頭準備好后, 就會呼叫Callback介面, 所以讀取回應體時可能會阻塞. OkHttp現階段不提供異步api來接收回應體,
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
client.newCall(request).enqueue(new Callback() {
@Override public void onFailure(Request request, Throwable throwable) {
throwable.printStackTrace();
}
@Override public void onResponse(Response response) throws IOException {
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Headers responseHeaders = response.headers();
for (int i = 0; i < responseHeaders.size(); i++) {
System.out.println(responseHeaders.name(i) + ": " + responseHeaders.value(i));
}
System.out.println(response.body().string());
}
});
}
異步請求的列印結果與注意事項與同步請求時相同,最大的不同點就是異步請求不需要開啟子執行緒,enqueue方法會自動將網路請求部分放入子執行緒中執行,
注意事項:
- 1,回呼介面的
onFailure方法和onResponse執行在子執行緒, - 2,
response.body().string()方法也必須放在子執行緒中,當執行這行代碼得到結果后,再跳轉到UI執行緒修改UI,
3.5、post請求方法
Post請求也分同步和異步兩種方式,同步與異步的區別和get方法類似,所以此時只講解post異步請求的使用方法,
private void postDataWithParame() {
OkHttpClient client = new OkHttpClient();//創建OkHttpClient物件,
FormBody.Builder formBody = new FormBody.Builder();//創建表單請求體
formBody.add("username","zhangsan");//傳遞鍵值對引數
Request request = new Request.Builder()//創建Request 物件,
.url("http://www.baidu.com")
.post(formBody.build())//傳遞請求體
.build();
client.newCall(request).enqueue(new Callback() {,,,});//回呼方法的使用與get異步請求相同,此時略,
}
看完代碼我們會發現:post請求中并沒有設定請求方式為POST,回憶在get請求中也沒有設定請求方式為GET,那么是怎么區分請求方式的呢?重點是Request.Builder類的post方法,在Request.Builder物件創建最初默認是get請求,所以在get請求中不需要設定請求方式,當呼叫post方法時把請求方式修改為POST,所以此時為POST請求,
3.6、POST請求傳遞引數的方法總結
3.6.1、Post方式提交String
下面是使用HTTP POST提交請求到服務. 這個例子提交了一個markdown檔案到web服務, 以HTML方式渲染markdown. 因為整個請求體都在記憶體中, 因此避免使用此api提交大檔案(大于1MB).
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
String postBody = ""
+ "Releases\n"
+ "--------\n"
+ "\n"
+ " * _1.0_ May 6, 2013\n"
+ " * _1.1_ June 15, 2013\n"
+ " * _1.2_ August 11, 2013\n";
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, postBody))
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
3.6.2、Post方式提交 流
以流的方式POST提交請求體. 請求體的內容由流寫入產生. 這個例子是流直接寫入Okio的BufferedSink. 你的程式可能會使用OutputStream, 你可以使用BufferedSink.outputStream()來獲取. OkHttp的底層對流和位元組的操作都是基于Okio庫, Okio庫也是Square開發的另一個IO庫, 填補I/O和NIO的空缺, 目的是提供簡單便于使用的介面來操作IO.
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
RequestBody requestBody = new RequestBody() {
@Override public MediaType contentType() {
return MEDIA_TYPE_MARKDOWN;
}
@Override public void writeTo(BufferedSink sink) throws IOException {
sink.writeUtf8("Numbers\n");
sink.writeUtf8("-------\n");
for (int i = 2; i <= 997; i++) {
sink.writeUtf8(String.format(" * %s = %s\n", i, factor(i)));
}
}
private String factor(int n) {
for (int i = 2; i < n; i++) {
int x = n / i;
if (x * i == n) return factor(x) + " × " + i;
}
return Integer.toString(n);
}
};
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(requestBody)
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
3.6.3、Post方式提交檔案
public static final MediaType MEDIA_TYPE_MARKDOWN
= MediaType.parse("text/x-markdown; charset=utf-8");
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
File file = new File("README.md");
Request request = new Request.Builder()
.url("https://api.github.com/markdown/raw")
.post(RequestBody.create(MEDIA_TYPE_MARKDOWN, file))
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
3.6.4、Post方式提交表單
使用FormEncodingBuilder來構建和HTML標簽相同效果的請求體. 鍵值對將使用一種HTML兼容形式的URL編碼來進行編碼.
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
RequestBody formBody = new FormBody.Builder()
.add("search", "Jurassic Park")
.build();
Request request = new Request.Builder()
.url("https://en.wikipedia.org/w/index.php")
.post(formBody)
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println(response.body().string());
}
3.7、POST其他用法
3.7.1、提取回應頭
典型的HTTP頭像是一個Map<String, String> : 每個欄位都有一個或沒有值. 但是一些頭允許多個值, 像Guava的Multimap
例如:
HTTP回應里面提供的Vary回應頭, 就是多值的. OkHttp的api試圖讓這些情況都適用.
- 當寫請求頭的時候, 使用header(name, value)可以設定唯一的name、value. 如果已經有值, 舊的將被移除,然后添加新的. 使用addHeader(name, value)可以添加多值(添加, 不移除已有的).
- 當讀取回應頭時, 使用header(name)回傳最后出現的name、value. 通常情況這也是唯一的name、value.如果沒有值, 那么header(name)將回傳null. 如果想讀取欄位對應的所有值,使用headers(name)會回傳一個list.
為了獲取所有的Header, Headers類支持按index訪問.
private final OkHttpClient client = new OkHttpClient();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/repos/square/okhttp/issues")
.header("User-Agent", "OkHttp Headers.java")
.addHeader("Accept", "application/json; q=0.5")
.addHeader("Accept", "application/vnd.github.v3+json")
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
System.out.println("Server: " + response.header("Server"));
System.out.println("Date: " + response.header("Date"));
System.out.println("Vary: " + response.headers("Vary"));
}
3.7.2、使用Gson來決議JSON回應
Gson是一個在JSON和Java物件之間轉換非常方便的api庫. 這里我們用Gson來決議Github API的JSON回應.
注意: ResponseBody.charStream()使用回應頭Content-Type指定的字符集來決議回應體. 默認是UTF-8.
private final OkHttpClient client = new OkHttpClient();
private final Gson gson = new Gson();
public void run() throws Exception {
Request request = new Request.Builder()
.url("https://api.github.com/gists/c2a7c39532239ff261be")
.build();
Response response = client.newCall(request).execute();
if (!response.isSuccessful()) throw new IOException("Unexpected code " + response);
Gist gist = gson.fromJson(response.body().charStream(), Gist.class);
for (Map.Entry<String, GistFile> entry : gist.files.entrySet()) {
System.out.println(entry.getKey());
System.out.println(entry.getValue().content);
}
}
static class Gist {
Map<String, GistFile> files;
}
static class GistFile {
String content;
}
3.7.3、回應快取
為了快取回應, 你需要一個你可以讀寫的快取目錄, 和快取大小的限制. 這個快取目錄應該是私有的, 不信任的程式應不能讀取快取內容.
一個快取目錄同時擁有多個快取訪問是錯誤的. 大多數程式只需要呼叫一次new OkHttp(), 在第一次呼叫時配置好快取, 然后其他地方只需要呼叫這個實體就可以了. 否則兩個快取示例互相干擾, 破壞回應快取, 而且有可能會導致程式崩潰.
回應快取使用HTTP頭作為配置. 你可以在請求頭中添加Cache-Control: max-stale=3600 , OkHttp快取會支持. 你的服務通過回應頭確定回應快取多長時間, 例如使用Cache-Control: max-age=9600.
private final OkHttpClient client;
public CacheResponse(File cacheDirectory) throws Exception {
int cacheSize = 10 * 1024 * 1024; // 10 MiB
Cache cache = new Cache(cacheDirectory, cacheSize);
client = new OkHttpClient();
client.setCache(cache);
}
public void run() throws Exception {
Request request = new Request.Builder()
.url("http://publicobject.com/helloworld.txt")
.build();
Response response1 = client.newCall(request).execute();
if (!response1.isSuccessful()) throw new IOException("Unexpected code " + response1);
String response1Body = response1.body().string();
System.out.println("Response 1 response: " + response1);
System.out.println("Response 1 cache response: " + response1.cacheResponse());
System.out.println("Response 1 network response: " + response1.networkResponse());
Response response2 = client.newCall(request).execute();
if (!response2.isSuccessful()) throw new IOException("Unexpected code " + response2);
String response2Body = response2.body().string();
System.out.println("Response 2 response: " + response2);
System.out.println("Response 2 cache response: " + response2.cacheResponse());
System.out.println("Response 2 network response: " + response2.networkResponse());
System.out.println("Response 2 equals Response 1? " + response1Body.equals(response2Body));
}
如果需要阻值response使用快取, 使用CacheControl.FORCE_NETWORK. 如果需要阻值response使用網路, 使用CacheControl.FORCE_CACHE.
警告
如果你使用FORCE_CACHE, 但是response要求使用網路, OkHttp將會回傳一個504 Unsatisfiable Request回應.
3.8、綜合實體
參考鏈接
activity_main
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_horizontal"
android:orientation="horizontal">
<Button
android:id="@+id/syncGet"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="同步請求"/>
<Button
android:id="@+id/asyncget"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="異步請求"/>
<Button
android:id="@+id/post"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="表單提交"/>
<Button
android:id="@+id/fileDownload"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="檔案下載"/>
</LinearLayout>
<TextView
android:id="@+id/tv_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"/>
</LinearLayout>
MainActivity
package com.example.okhttpdemo;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.TimeUnit;
import okhttp3.Call;
import okhttp3.Callback;
import okhttp3.FormBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private Button syncGet;
private Button asyncget;
private Button post;
private Button fileDownload;
private TextView tvtext;
private String result;
private static OkHttpClient client = new OkHttpClient();
/**
* 在這里直接設定連接超時,靜態方法內,在構造方法被呼叫前就已經初始話了
*/
static {
client.newBuilder().connectTimeout(10, TimeUnit.SECONDS);
client.newBuilder().readTimeout(10, TimeUnit.SECONDS);
client.newBuilder().writeTimeout(10, TimeUnit.SECONDS);
}
private Request request;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
initialize();
initListener();
}
/**
* 事件監聽
*/
private void initListener() {
syncGet.setOnClickListener(this);
asyncget.setOnClickListener(this);
post.setOnClickListener(this);
fileDownload.setOnClickListener(this);
}
/**
* 初始化布局控制元件
*/
private void initialize() {
syncGet = (Button) findViewById(R.id.syncGet);
asyncget = (Button) findViewById(R.id.asyncget);
post = (Button) findViewById(R.id.post);
tvtext = (TextView) findViewById(R.id.tv_text);
fileDownload = (Button) findViewById(R.id.fileDownload);
}
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.syncGet:
initSyncData();
break;
case R.id.asyncget:
initAsyncGet();
break;
case R.id.post:
initPost();
break;
case R.id.fileDownload:
downLoadFile();
break;
default:
break;
}
}
/**
* get請求同步方法
*/
private void initSyncData() {
new Thread(new Runnable() {
@Override
public void run() {
try {
request = new Request.Builder().url(Contants.SYNC_URL).build();
Response response = client.newCall(request).execute();
result = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
tvtext.setText(result);
Log.d("MainActivity", "hello");
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
}).start();
}
/**
* 異步請求
*/
private void initAsyncGet() {
new Thread(new Runnable() {
@Override
public void run() {
request = new Request.Builder().url(Contants.ASYNC_URL).build();
client.newCall(request).enqueue(new Callback() {
/**
* @param call 是一個介面, 是一個準備好的可以執行的request
* 可以取消,對位一個請求物件,只能單個請求
* @param e
*/
@Override
public void onFailure(Call call, IOException e) {
Log.d("MainActivity", "請求失敗");
}
/**
*
* @param call
* @param response 是一個回應請求
* @throws IOException
*/
@Override
public void onResponse(Call call, Response response) throws IOException {
/**
* 通過拿到response這個回應請求,然后通過body().string(),拿到請求到的資料
* 這里最好用string() 而不要用toString()
* toString()每個類都有的,是把物件轉換為字串
* string()是把流轉為字串
*/
result = response.body().string();
runOnUiThread(new Runnable() {
@Override
public void run() {
tvtext.setText(result);
}
});
}
});
}
}).start();
}
/**
* 表單提交
*/
private void initPost() {
String url = "http://112.124.22.238:8081/course_api/banner/query";
FormBody formBody = new FormBody.Builder()
.add("type", "1")
.build();
request = new Request.Builder().url(url)
.post(formBody).build();
new Thread(new Runnable() {
@Override
public void run() {
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, final Response response) throws IOException {
runOnUiThread(new Runnable() {
@Override
public void run() {
tvtext.setText("提交成功");
}
});
}
});
}
}).start();
}
/**
* 檔案下載地址
*/
private void downLoadFile() {
String url = "http://www.0551fangchan.com/images/keupload/20120917171535_49309.jpg";
request = new Request.Builder().url(url).build();
OkHttpClient client = new OkHttpClient();
client.newCall(request).enqueue(new Callback() {
@Override
public void onFailure(Call call, IOException e) {
}
@Override
public void onResponse(Call call, Response response) throws IOException {
//把請求成功的response轉為位元組流
InputStream inputStream = response.body().byteStream();
/**
* 在這里要加上權限 在mainfests檔案中
* <uses-permission android:name="android.permission.INTERNET"/>
* <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
*/
//在這里用到了檔案輸出流
FileOutputStream fileOutputStream = new FileOutputStream(new File("/sdcard/logo.jpg"));
//定義一個位元組陣列
byte[] buffer = new byte[2048];
int len = 0;
while ((len = inputStream.read(buffer)) != -1) {
//寫出到檔案
fileOutputStream.write(buffer, 0, len);
}
//關閉輸出流
fileOutputStream.flush();
Log.d("wuyinlei", "檔案下載成功...");
}
});
}
}
參考
1、https://blog.csdn.net/fightingXia/article/details/70947701
2、https://blog.csdn.net/chenzujie/article/details/46994073
3、https://blog.csdn.net/weixin_30700099/article/details/95962192
4、https://www.jianshu.com/p/5a12ae6d741a
5、https://www.jianshu.com/p/ca8a982a116b
6、https://wuyinlei.blog.csdn.net/article/details/50579564
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/287753.html
標籤:其他
上一篇:添加widget失敗
