本文为菜鸟窝作者蒋志碧的连载。“从 0 开始开发一款直播 APP ”系列来聊聊时下最火的直播 APP,如何完整的实现一个类"腾讯直播"的商业化项目
视频地址:http://www.cniao5.com/course/10121
【从 0 开始开发一款直播 APP】4.1 网络封装之 Okhttp -- 基础回顾
【从 0 开始开发一款直播 APP】4.2 网络封装之 OkHttp -- GET,POST,前后端交互
【从 0 开始开发一款直播 APP】4.3 网络封装之 OkHttp -- 封装 GET,POST FORM,POST JSON
【从 0 开始开发一款直播 APP】4.4 网络封装之 OkHttp -- 网络请求实现直播登录
封装 OkHttp 框架,主要功能有:
GET 请求
POST FORM
POST JSON
封装之前,再次熟悉一下 OkHttp 请求网络的大致步骤,让封装的时候思路清晰一点。
//1、初始化 OkHttpClient 对象
private OkHttpClient mHttpClient = new OkHttpClient();
//2、构造Request
//2.1 构造RequestBody
FormEncodingBuilder builder = new FormEncodingBuilder();
RequestBody requestBody = builder.add("key", "value").build();
final Request request = new Request
.Builder()
.post(requestBody)
.url(url)
.build();
//3、将 Request 封装成 call
final Call call = mHttpClient.newCall(request);
//4、执行 call
call.enqueue(new Callback() {
//请求失败调用
@Override
public void onFailure(Request request, IOException e) {
}
//请求成功调用
@Override
public void onResponse(Response response) throws IOException {
}
});
OkHttp 请求基本步骤就是这几步,看起来很简单,要封装自然也要考虑到这里面的方方面面,根据自己的业务需求进行相应的封装。封装是基于鸿洋大神的 OkHttpUtils,站在巨人的肩膀上,我们封装起来会更加简单。
1、OkHttpClient 实例化
声明一个私有的 OkHttpClient 对象,对其进行构建并设置超时信息,而 AsyncHttp 类则是我们要进行网络访问对外公开的类,OkHttpClient 官方建议我们设置成单例模式。
public class AsyncHttp {
private static AsyncHttp mInstance;
//初始化操作,设置超时时间
private OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(20 * 1000, TimeUnit.MILLISECONDS)
.readTimeout(20 * 1000, TimeUnit.MILLISECONDS)
.build();
//私有构造函数,初始化 OkHttpClient 对象
private AsyncHttp() {
OkHttpUtils.initClient(okHttpClient);
}
//单例模式
public static AsyncHttp instance() {
if (mInstance == null) {
synchronized (OkHttpUtils.class) {
if (mInstance == null) {
mInstance = new AsyncHttp();
}
}
}
return mInstance;
}
}
2、构造 GET 请求
get 请求官方给我们的是没有参数的,不满足我们需求,根据我们的需求将其进行封装。GET 请求需要 Request 对象传参,还需要回调 CallBack,因此需要添加 Request 基类和 CallBack 基类。
public void get(IRequest request, IHttpListener listener) {}
IRequest 是 Request 的封装类,IHttpListener 是回调 CallBack 的监听。
2.1、封装 IRequest
我们知道官方提供的 Request 类是对请求信息进行封装,包括请求地址 url,请求方法 method,请求头 head,请求体 RequestBody,取消 http 请求的标志 tag。IRequest 是一个抽象类,请求信息封装在 RequestParams 类里面,IRequest 对外提供了一些方法,获取请求参数,获取/设置唯一标识,获取 Url,获取解析的数据类型。
//IDontObfuscate 实现 Serializable 的一个抽象类
public abstract class IRequest extends IDontObfuscate {
//测试登录的 API
public static final String HOST_PUBLIC = "http://live.demo.cniao5.com/Api/";
public static final String HOST_DEBUG = "http://192.168.31.92:8094/Api/";
//判断上面两个 API 使用哪一个。表示猜不透直播老师的心啊,不知道为什么要这样
private boolean DEBUG = false;
//Request 请求信息封装类
protected RequestParams mParams = new RequestParams();
//请求网络直播登录的唯一标识
public int mRequestId = 0;
protected int mDraw = 0;
public IRequest() {
}
/**
* 接口请求参数
*/
public RequestParams getParams() {
return mParams;
}
/**
* 设置接口请求唯一标识
*/
public void setRequestId(int requestId) {
mRequestId = requestId;
}
/**
* 返回请求接口唯一标识
*/
public int getRequestId() {
return mRequestId;
}
/**
* 当前接口的url地址
*/
public abstract String getUrl();
/**
* 获取解析类型
*/
public abstract Type getParserType();
/**
* 返回服务器接口地址
*/
protected String getHost() {
return DEBUG ? HOST_DEBUG : HOST_PUBLIC;
}
@Override
public String toString() {
return "IRequest [DEBUG=" + DEBUG
+ ", mParams=" + mParams + ", mRequestId=" + mRequestId
+ ", mDraw=" + mDraw + "]";
}
//是否缓存
public boolean isCache() {
return false;
}
}
RequestParams 请求参数封装
参数封装类,顾名思义就是对参数的保存和移除,该类适用于 GET 和 POST 参数,包括 url 参数封装,stream 参数封装,file 参数封装,file 数组参数封装,url 对象参数封装。全部都是存储在 ConcurrentHashMap 对象中,通过键值对进行存储,put 方法有很多重载,对不同形式的参数采取不同的添加方式。
public class RequestParams implements Serializable{
//二进制流数据
public final static String APPLICATION_OCTET_STREAM = "application/octet-stream";
//JSON数据格式
public final static String APPLICATION_JSON = "application/json";
protected final static String LOG_TAG = "RequestParams";
//存储 url 参数
protected final ConcurrentHashMap<String, String> urlParams = new ConcurrentHashMap<String, String>();
//存储 stream 参数
protected final ConcurrentHashMap<String, StreamWrapper> streamParams = new ConcurrentHashMap<String, StreamWrapper>();
//存储 file 参数
protected final ConcurrentHashMap<String, FileWrapper> fileParams = new ConcurrentHashMap<String, FileWrapper>();
//存储 fileArray 参数
protected final ConcurrentHashMap<String, List<FileWrapper>> fileArrayParams = new ConcurrentHashMap<String, List<FileWrapper>>();
//存储 url 对象参数
protected final ConcurrentHashMap<String, Object> urlParamsWithObjects = new ConcurrentHashMap<String, Object>();
//是否重复
protected boolean isRepeatable;
//标志 multipart/form-data:需要在表单中进行文件上传时,就需要使用该格式
protected boolean forceMultipartEntity = false;
//用户 json 数据流
protected boolean useJsonStreamer;
//自动关闭输入流标志
protected boolean autoCloseInputStreams;
//保存上传有效载荷所需的时间
protected String elapsedFieldInJsonStreamer = "_elapsed";
//指定编码格式
protected String contentEncoding = "utf-8";
private Gson mGson = new Gson();
/**
* 构造一个空的 RequestParams 实例
*/
public RequestParams() {
this((Map<String, String>) null);
}
//获取 urlParams
public String getUrlParams(String key){
if(!TextUtils.isEmpty(key)&&urlParams.containsKey(key)){
return urlParams.get(key);
}
return "";
}
/**
* 构造一个新的RequestParams实例,该实例包含指定map中的键/值字符串参数。
*/
public RequestParams(Map<String, String> source) {
if (source != null) {
for (Map.Entry<String, String> entry : source.entrySet()) {
put(entry.getKey(), entry.getValue());
}
}
}
/**
* 构造一个新的RequestParams实例,并使用单个初始键/值填充
*/
public RequestParams(final String key, final String value) {
this(new HashMap<String, String>() {{
put(key, value);
}});
}
/**
* 构造一个新的RequestParams实例,并用多个初始键/值填充
*/
public RequestParams(Object... keysAndValues) {
int len = keysAndValues.length;
if (len % 2 != 0)
throw new IllegalArgumentException("Supplied arguments must be even");
for (int i = 0; i < len; i += 2) {
String key = String.valueOf(keysAndValues[i]);
String val = String.valueOf(keysAndValues[i + 1]);
put(key, val);
}
}
public void setContentEncoding(final String encoding) {
if (encoding != null) {
this.contentEncoding = encoding;
} else {
Log.d(LOG_TAG, "setContentEncoding called with null attribute");
}
}
/**
* 如果设置为true,即使没有文件或流被发送,也会将Content-Type头部强制为“multipart/form-data”
* multipart/form-data:需要在表单中进行文件上传时,就需要使用该格式
*/
public void setForceMultipartEntityContentType(boolean force) {
this.forceMultipartEntity = force;
}
/**
* 添加 string 类型字符串到 urlParams
*/
public void put(String key, String value) {
if (key != null && value != null) {
urlParams.put(key, value);
}
}
/**
* 将自定义提供的文件内容类型和文件名称添加到 fileArrayParams
*/
public void put(String key, File files[]) throws FileNotFoundException {
put(key, files, null, null);
}
/**
* 将自定义提供的文件内容类型和文件名称添加到 fileArrayParams
*/
public void put(String key, File files[], String contentType, String customFileName) throws FileNotFoundException {
if (key != null) {
List<FileWrapper> fileWrappers = new ArrayList<FileWrapper>();
for (File file : files) {
if (file == null || !file.exists()) {
throw new FileNotFoundException();
}
fileWrappers.add(new FileWrapper(file, contentType, customFileName));
}
fileArrayParams.put(key, fileWrappers);
}
}
/**
* 将自定义提供的文件内容类型和文件名添加到 fileParams
*/
public void put(String key, File file) throws FileNotFoundException {
put(key, file, null, null);
}
/**
* 将自定义提供的文件内容类型和文件名添加到 fileParams
*/
public void put(String key, String customFileName, File file) throws FileNotFoundException {
put(key, file, null, customFileName);
}
/**
* 将自定义提供的文件内容类型和文件名添加到 fileParams
*/
public void put(String key, File file, String contentType) throws FileNotFoundException {
put(key, file, contentType, null);
}
/**
* 将自定义提供的文件内容类型和文件名添加到 fileParams
*/
public void put(String key, File file, String contentType, String customFileName) throws FileNotFoundException {
if (file == null || !file.exists()) {
throw new FileNotFoundException();
}
if (key != null) {
fileParams.put(key, new FileWrapper(file, contentType, customFileName));
}
}
/**
* 添加 InputStream 流到 streamParams
*/
public void put(String key, InputStream stream) {
put(key, stream, null);
}
/**
* 添加 InputStream 流到 streamParams
*/
public void put(String key, InputStream stream, String name) {
put(key, stream, name, null);
}
/**
* 添加 InputStream 流到 streamParams
*/
public void put(String key, InputStream stream, String name, String contentType) {
put(key, stream, name, contentType, autoCloseInputStreams);
}
/**
* 添加 InputStream 流到 streamParams
*/
public void put(String key, InputStream stream, String name, String contentType, boolean autoClose) {
if (key != null && stream != null) {
streamParams.put(key, StreamWrapper.newInstance(stream, name, contentType, autoClose));
}
}
/**
* 添加 Object 对象到 urlParamsWithObjects
*/
public void put(String key, Object value) {
if (key != null && value != null) {
urlParamsWithObjects.put(key, value);
}
}
/**
* 添加 int 类型字符串到 urlParams
*/
public void put(String key, int value) {
if (key != null) {
urlParams.put(key, String.valueOf(value));
}
}
/**
* 添加 long 类型字符串到 urlParams
*/
public void put(String key, long value) {
if (key != null) {
urlParams.put(key, String.valueOf(value));
}
}
/**
* 添加 String 类型字符串到 urlParamsWithObjects
*/
public void add(String key, String value) {
if (key != null && value != null) {
Object params = urlParamsWithObjects.get(key);
if (params == null) {
// 向后兼容,这将导致“k = v1&k = v2&k = v3”
params = new HashSet<String>();
this.put(key, params);
}
if (params instanceof List) {
((List<Object>) params).add(value);
} else if (params instanceof Set) {
((Set<Object>) params).add(value);
}
}
}
/**
* 从 request 中移除某个字段
*/
public void remove(String key) {
urlParams.remove(key);
streamParams.remove(key);
fileParams.remove(key);
urlParamsWithObjects.remove(key);
fileArrayParams.remove(key);
}
/**
* 监测字段是否被定义
*/
public boolean has(String key) {
return urlParams.get(key) != null ||
streamParams.get(key) != null ||
fileParams.get(key) != null ||
urlParamsWithObjects.get(key) != null ||
fileArrayParams.get(key) != null;
}
public void setHttpEntityIsRepeatable(boolean flag) {
this.isRepeatable = flag;
}
public void setUseJsonStreamer(boolean flag) {
this.useJsonStreamer = flag;
}
/**
* 通过流上传 JSON 对象时设置一个附加字段,以保存上载有效载荷所需的时间(以毫秒为单位)。 默认情况下,此字段设置为“_elapsed”。
* 要禁用此功能,请将此方法调用为null作为字段值。
*/
public void setElapsedFieldInJsonStreamer(String value) {
this.elapsedFieldInJsonStreamer = value;
}
/**
* 设置全局标志,用于确定在成功上传时是否自动关闭输入流。
*/
public void setAutoCloseInputStreams(boolean flag) {
autoCloseInputStreams = flag;
}
/**
* http get builder 参数封装构造类
*/
public GetBuilder getGetBuilder() {
GetBuilder getBuilder = new GetBuilder();
//添加参数 url 后面
for (ConcurrentHashMap.Entry<String, String> entry : urlParams.entrySet()) {
Log.i("log", "getBuilder value:" + entry.getValue());
getBuilder.addParams(entry.getKey(), entry.getValue());
}
return getBuilder;
}
/**
* post form 表单
*/
public PostFormBuilder getPostFormBuilder() {
PostFormBuilder postFormBuilder = new PostFormBuilder();
// post 请求添加参数 url后面添加
for (ConcurrentHashMap.Entry<String, String> entry : urlParams.entrySet()) {
Log.i("log", "getBuilder value:" + entry.getValue());
postFormBuilder.addParams(entry.getKey(), entry.getValue());
}
//post 请求添加参数 file 文件参数
for (ConcurrentHashMap.Entry<String, FileWrapper> entry : fileParams.entrySet()) {
postFormBuilder.addFile(entry.getKey(), entry.getValue().file.getAbsolutePath(), entry.getValue().file);
}
//文件对象列表存储相关信息
//public PostFormBuilder addFile(String name, String filename, File file)
//{
// files.add(new FileInput(name, filename, file));
// return this;
//}
return postFormBuilder;
}
// post String 字符串
public PostStringBuilder getPostJsonBuilder() {
PostStringBuilder postFormBuilder = new PostStringBuilder();
//将对象转成 json 字符串传递给 PostStringBuilder
postFormBuilder.content(mGson.toJson(urlParams));
return postFormBuilder;
}
//文件包装类
public static class FileWrapper implements Serializable {
public final File file;
public final String contentType;
public final String customFileName;
public FileWrapper(File file, String contentType, String customFileName) {
this.file = file;
this.contentType = contentType;
this.customFileName = customFileName;
}
}
//输入流包装类
public static class StreamWrapper {
public final InputStream inputStream;//输入流
public final String name;//文件名
public final String contentType;//contentType 媒体类型
public final boolean autoClose;//是否自动关闭
public StreamWrapper(InputStream inputStream, String name, String contentType, boolean autoClose) {
this.inputStream = inputStream;
this.name = name;
this.contentType = contentType;
this.autoClose = autoClose;
}
//输入流包装类 APPLICATION_OCTET_STREAM:二进制流数据标志
static StreamWrapper newInstance(InputStream inputStream, String name, String contentType, boolean autoClose) {
return new StreamWrapper(
inputStream,
name,
contentType == null ? APPLICATION_OCTET_STREAM : contentType,
autoClose);
}
}
}
2.2、IHttpListener 接口
在 OkHttp 的 CallBack 类中,封装了 onSuccess() 和 onFailure() 方法,我们按照类似需求进行添加相应的方法。
onStart() 方法用于网络请求之前的一些操作,onSuccess() 网络请求成功的回调,onFailure() 网络请求成功的回调。
public interface IHttpListener {
//请求网络之前回调(对话框提示信息等) requestId:网络请求唯一标识
void onStart(int requestId);
//请求网络成功回调 requestId:网络请求唯一标识,response:Response 对象
void onSuccess(int requestId, Response response);
//请求网络失败回调 requestId:网络请求唯一标识,httpStatus:状态码,error:错误信息
void onFailure(int requestId, int httpStatus, Throwable error);
}
两个重要的参数算是封装完了,下面开始 GET 请求方法实现。
public void get(IRequest request, IHttpListener listener) {
LogDebugUtil.e(TAG, "post: url=" + request.getUrl());
if (request != null) {
//请求参数构造,其实就是构造 Request 对象,Request 采用 build 模式,这里也一样
//RequestParams params = request.getParams();
//GetBuilder 来自 OkHttpUtils
//GetBuilder getBuilder = params.getGetBuilder();
//RequestCall 和 Call 对象类似,就是用来执行网络请求的
//RequestCall call = getBuilder.url(request.getUrl())
// .id(request.getRequestId()).build();
//执行 call,ResponseCallback是自定义 CallBack,listener:回调监听,request.getParserType():请求类型解析 ResponseCallback(IHttpListener httpListener, Type parserType)
//call.execute(new ResponseCallback(listener,request.getParserType()));
//链式调用,上面是分析调用过程
request.getParams()
.getGetBuilder()
.url(request.getUrl())
.id(request.getRequestId())
.build()
.execute(new ResponseCallback(listener, request.getParserType()));
} else {
throw new RuntimeException("Request param is null");
}
}
3、CallBack 封装
看到 鸿洋大神的 github 上对于自定义 CallBack 写了示例,根据这个示例,我们来封装一个自己的 CallBack。
先定义一个 Bean,针对下面这个 json 串进行封装,这个是登录的请求信息。
{ "status": 0,
"msg": "请求成功",
"data": {
"id": "1", "nickname": "pzf“
}
}
创建一个 Response 对象,声明需要解析的数据
//IDontObfuscate 是一个实现了 Serializable 接口的序列化抽象类,上面已经讲过了
public class Response<T> extends IDontObfuscate {
public int status;//状态码
public String msg;//状态信息
public T data;//对象
@Override
public String toString() {
return "Response [code=" + status + ", msg=" + msg + ", data=" + data
+ "]";
}
}
ResponseCallback — 自定义 Callback
//ResponseCallback 继承自 OkHttpUtils 框架中的 Callback,Response 是一个 bean 对象,上面讲过
class ResponseCallback extends Callback<Response> {
private IHttpListener mHttpListener;//callback 回调监听
private Type mParserType;//数据转换类型
public ResponseCallback(IHttpListener httpListener, Type parserType) {
mHttpListener = httpListener;
mParserType = parserType;
}
//请求网络之前回调
@Override
public void onBefore(Request request, int id) {
if (mHttpListener != null) {
mHttpListener.onStart(id);
}
}
@Override
public Response parseNetworkResponse(okhttp3.Response response, int id) throws Exception {
LogDebugUtil.e(TAG, "parseNetworkResponse: ");
Response responseData = null;
if (mHttpListener != null && response != null) {
if (response.isSuccessful()) {
String content = response.body().string();
if (content != null) {
try {
LogDebugUtil.e(TAG, "onSuccess: " + content);
//解析数据,返回解析之后的数据
responseData = mGson.fromJson(content,mParserType);
} catch (JsonSyntaxException e) {
onError(null,e,id);
}
}
} else {
onError(null, new Exception("net error"),id);
}
}
return responseData;
}
//请求错误回调
@Override
public void onError(Call call, Exception e, int id) {
if (mHttpListener != null) {
if (!call.isCanceled()) {
mHttpListener.onFailure(id, 0, e);
}
}
}
//请求成功回调
@Override
public void onResponse(Response response, int id) {
if (mHttpListener != null) {
mHttpListener.onSuccess(id, response);
}
}
}
OkHttpUtils 中 Callback 源码
public abstract class Callback<T>{
/**
* UI Thread 请求网络之前调用
*/
public void onBefore(Request request, int id){
}
/**
* UI Thread 请求网络之后调用
*/
public void onAfter(int id){
}
/**
* UI Thread 更新进度条
*/
public void inProgress(float progress, long total , int id){
}
/**
* 如果在 parseNetworkResponse 中解析响应代码,则应该使此方法返回 true。
*/
public boolean validateReponse(Response response, int id)
{
return response.isSuccessful();
}
/**
* Thread Pool Thread 根据示例可以看出,该方法是解析数据的,T 是数据源
*/
public abstract T parseNetworkResponse(Response response, int id) throws Exception;
public abstract void onError(Call call, Exception e, int id);
public abstract void onResponse(T response, int id);
public static Callback CALLBACK_DEFAULT = new Callback(){
@Override
public Object parseNetworkResponse(Response response, int id) throws Exception{
return null;
}
@Override
public void onError(Call call, Exception e, int id){
}
@Override
public void onResponse(Object response, int id){
}
};
}
到此,GET 请求封装完成。
4、构造 POST 请求
post 请求这里封装两个,Form 表单和 Json 字符串的 Post 提交封装。
4.1、Post Form
post 和 get 类似,之前 RequestParams 请求参数封装类已经构造完成,这里可以直接使用。
public void postForm(IRequest request, IHttpListener listener) {
LogDebugUtil.e(TAG, "postForm: url=" + request.getUrl());
if (request != null) {
//请求参数构造,其实就是构造 Request 对象,Request 采用 build 模式,这里也一样
//RequestParams params = request.getParams();
//PostFormBuilder 来自 OkhttpUtils
//PostFormBuilder postFormBuilder = params.getPostFormBuilder();
//RequestCall 和 Call 对象类似,就是用来执行网络请求的
//RequestCall call = postFormBuilder.url(request.getUrl())
// .id(request.getRequestId()).build();
//执行 call,ResponseCallback 是自定义 CallBack,listener:回调监听,request.getParserType():请求类型解析 ResponseCallback(IHttpListener httpListener, Type parserType)
//call.execute(new ResponseCallback(listener,request.getParserType()));
//这里采用链式调用,上面的是解析
request.getParams()
.getPostFormBuilder()
.url(request.getUrl())
.id(request.getRequestId())
.build()
.execute(new ResponseCallback(listener, request.getParserType()));
} else {
throw new RuntimeException("Request param is null");
}
}
4.2、Post Json
public void postJson(IRequest request, IHttpListener listener) {
LogDebugUtil.e(TAG, "postForm: url=" + request.getUrl());
if (request != null) {
//请求参数构造,其实就是构造 Request 对象,Request 采用 build 模式,这里也一样
//RequestParams params = request.getParams();
//PostStringBuilder 来自 OkhttpUtils
//PostStringBuilder postJsonBuilder = params.getPostJsonBuilder();
//RequestCall 和 Call 对象类似,就是用来执行网络请求的
//RequestCall call = postJsonBuilder.url(request.getUrl())
// .id(request.getRequestId()).build();
//执行 call,ResponseCallback 是自定义 CallBack,listener:回调监听,request.getParserType():请求类型解析 ResponseCallback(IHttpListener httpListener, Type parserType)
//call.execute(new ResponseCallback(listener,request.getParserType()));
//这里采用链式调用,上面的是解析
request.getParams()
.getPostJsonBuilder()
.url(request.getUrl())
.id(request.getRequestId())
.build().execute(new ResponseCallback(listener, request.getParserType()));
} else {
throw new RuntimeException("Request param is null");
}
}
5、其它方法和类的封装
使用 Call.cancel() 可以立即停止掉一个正在执行的 call。如果一个线程正在写请求或者读响应,将会引发 IOException。当 call 没有必要的时候,使用这个 api 可以节约网络资源。例如当用户离开一个应用时。不管同步还是异步的 call 都可以取消。
你可以通过 tags 来同时取消多个请求。当你构建一请求时,使用 RequestBuilder.tag(tag) 来分配一个标签。之后你就可以用 OkHttpClient.cancel(tag) 来取消所有带有这个 tag 的 call。
cancelRequest 根据 tag 取消 call,StringCallback 为 String 类 Callback。
//取消 call
public void cancelRequest(String tag) {
if (tag != null) {
OkHttpUtils.getInstance().cancelTag(tag);
}
}
//StringCallback 封装,解析 String 类型数据
abstract class StringCallback extends Callback<String> {
@Override
public String parseNetworkResponse(okhttp3.Response response, int id) throws IOException {
return response.body().string();
}
}
6、总结
OkHttp 封装步骤整理
1、OkHttpClient 实例化,所有请求都是在此基础上进行的。
2、封装 Request,包括参数的传递和 url 的构造
3、封装 Callback,将数据源作为范型传递进去,根据具体需求进行解析,重写抽象方法,onSuccess()、onFailure()、onStart() 等。
140套Android优秀开源项目源码,领取地址:http://mp.weixin.qq.com/s/afPGHqfdiApALZqHsXbw-A