OkHttp与Retrofit上传文件详解

Http上传原理

Http上传需要用到multipart/form-data请求方式,Http协议原始方法不支持multipart/form-data请求,那这个请求自然就是由原始的请求方法拼装而成,具体规则如下:

1、multipart/form-data的本质上还是Post请求
2、multipart/form-data与post方法的不同之处:请求头,请求体。
3、multipart/form-data的请求头必须包含一个特殊的头信息:Content-Type,且其值也必须规定为multipart/form-data,同时还需要规定一个内容分割符用于分割请求体中的多个post的内容,如文件内容和文本内容自然需要分割开来,不然接收方就无法正常解析和还原这个文件了。

4、multipart/form-data的请求体也是一个字符串,不过和post的请求体不同的是它的构造方式,post是简单的name=value值连接,而multipart/form-data则是添加了分隔符等内容的构造体。

抓包结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Request URL:https://your_base_url/open/qiniu/image
Request Method:POST
Status Code:200 OK

Request Headers
Accept-Encoding:gzip
Connection:Keep-Alive
Content-Length:117276
Content-Type:multipart/form-data; boundary=ed67c97e-2000-47de-9033-77aeb8df43d9
Host:your_base_url
token:794d5240-de2a-465b-9a5a-66f71f567acd
User-Agent:Dalvik/2.1.0 (Linux; U; Android 5.1.1; vivo X7 Build/LMY47V) app_name/1.5.0

Request Payload
--ed67c97e-2000-47de-9033-77aeb8df43d9
Content-Disposition: form-data; name="file"; filename="coin.jpg"
Content-Type: image/jpg
Content-Length: 117075
--ed67c97e-2000-47de-9033-77aeb8df43d9--

可以看到Request Headers中包含了Accept-Encoding、Content-Length、Content-Type、Host、User-Agent等参数,OkHttp会自动生成boundary,无需手动编写规则;Request Payload中包含了具体的上传内容,Content-Disposition包含了上传文件名以及part参数key值,Content-Type指明了上传文件的后缀格式。

1
2
> User-Agent一般都会重写,以其可以包含系统信息和用户自定义的信息,系统信息可以通过System.getProperty("http.agent")获得,然后再拼接上app独有的信息即可
>

OkHttp上传单个文件

先直接看代码吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
public static final String MULTIPART_FORM_DATA = "image/jpg";		// 指明要上传的文件格式
public static void okHttpUpload(String partName, String path, final UploadCallback callback){
File file = new File(path); // 需要上传的文件
RequestBody requestFile = // 根据文件格式封装文件
RequestBody.create(MediaType.parse(MULTIPART_FORM_DATA), file);

// 初始化请求体对象,设置Content-Type以及文件数据流
RequestBody requestBody = new MultipartBody.Builder()
.setType(MultipartBody.FORM) // multipart/form-data
.addFormDataPart(partName, file.getName(), requestFile)
.build();

// 封装OkHttp请求对象,初始化请求参数
Request request = new Request.Builder()
.url(UPLOAD_URL) // 上传url地址
.post(requestBody) // post请求体
.build();

final okhttp3.OkHttpClient.Builder httpBuilder = new OkHttpClient.Builder();
OkHttpClient okHttpClient = httpBuilder
.connectTimeout(100, TimeUnit.SECONDS) // 设置请求超时时间
.writeTimeout(150, TimeUnit.SECONDS)
.build();
// 发起异步网络请求
okHttpClient.newCall(request).enqueue(new Callback() {
@Override
public void onResponse(Call call, okhttp3.Response response) throws IOException {
if (callback != null){
callback.onResponse(call, response);
}
}
@Override
public void onFailure(Call call, IOException e) {
if (callback != null){
callback.onFailure(call, e);
}
}
});
}

// 调用文件上传方法,需要传入requestBody的key值,本地文件路径以及请求回调方法
UploadWrapper.okHttpUpload("file", mImagePath, new UploadWrapper.UploadCallback() {
@Override
public void onResponse(Call call, final okhttp3.Response response) {
try {
final String result = response.body().string();
JSONObject jsonObject = new JSONObject(result);
mImageUrl = jsonObject.getJSONObject("data").getString("url");
showImage();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onFailure(Call arg0, IOException e) {
e.printStackTrace();
}
});

代码中的注释已经很清楚了,RequestBody分为两部分,第一部分是封装文件,封装时需要指明文件格式,常见的文件格式有.txt,.jpg,.png等等,如果是上传图片,则MediaType为image/jpg,这里的jpg可以换成png等其他图片格式,另一部分是封装整个请求体,如果有多个文件要上传或者多个post请求key-value,则可以统一封装到RequestBody中,此时还需要指明请求Content-Type,即multipart/form-data,文件请求体可以通过addFormDataPart方法进行封装,最后将请求体传入OkHttp请求中即可。

OkHttp已经帮我们预先处理了很多工作,例如boundary不需要我们手动指定,请求内容的传递也只需要调用OkHttp提供的api接口即可,无需关心上文抓包中的数据格式,如果是采用Android原生的HttpURLConnection实现文件上传,那么所有的这些细节就必须都要考虑。

Retrofit实现文件上传

由于Retrofit底层本质上还是通过OkHttp实现的,所以基本原理和OkHttp也很想,只不过Retrofit又对OkHttp进行了一次封装,使其更直观更好用,如果对OkHttp还不是很了解的,请参考我之前的一篇文章Retrofit用法详解,这里就直接讨论文件上传相关的实现。

先来看代码吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
showProgressBar();
Observable.just("")
.subscribeOn(Schedulers.computation()) // 切换至计算线程
.map(new Func1<String, String>() {
@Override
public String call(String s) {
// 图片压缩
mImagePath = BitmapExtKt.compressImageFileByQualityAndSize(Uri.parse(mImagePath).toString(),
getCacheDir().getPath() + File.separator + "coin.jpg", 1000, 0, 0);
return mImagePath;
}
})
.subscribeOn(Schedulers.io()) // 切换至IO线程
.flatMap(new Func1<String, Observable<UploadResultEntity>>() {
@Override
public Observable<UploadResultEntity> call(String s) {
// 封装请求体
MultipartBody.Part body = UploadWrapper.prepareFilePart("file", mImagePath);
// 具体的文件上传请求
return mCoinService.uploadFile(body)
.compose(new DefaultTransformer<Response<CommonResponse<UploadResultEntity>>,
CommonResponse<UploadResultEntity>>(mActivity))
.map(new Func1<CommonResponse<UploadResultEntity>, UploadResultEntity>() {
@Override
public UploadResultEntity call(CommonResponse<UploadResultEntity> response) {
return response.data;
}
});
}
})
.observeOn(AndroidSchedulers.mainThread()) // 切换至Android主线程
.subscribe(new Subscriber<UploadResultEntity>() {
@Override
public void onCompleted() {
dismissProgressBar();
}
@Override
public void onError(Throwable e) {
dismissProgressBar();
e.printStackTrace();
}
@Override
public void onNext(UploadResultEntity uploadResultEntity) {
mImageUrl = uploadResultEntity.url;
showImage();
}
});

// 封装请求体,可以看到这里和OkHttp的请求体封装基本上是一样的
@NonNull
public static MultipartBody.Part prepareFilePart(String partName, String path) {
File file = new File(path);
RequestBody requestFile =
RequestBody.create(MediaType.parse(UploadWrapper.MULTIPART_FORM_DATA), file);
return MultipartBody.Part.createFormData(partName, file.getName(), requestFile);
}

// post请求定义,通过@Multipart指定multipart/form-data格式,@Part指定具体的请求体
@Multipart
@POST("/open/qiniu/image")
Observable<Response<CommonResponse<UploadResultEntity>>> uploadFile(@Part MultipartBody.Part file);

这里用到了RxJava,如果对RxJava不是很熟悉,可以参考我之前的一篇文章RxJava与Retrofit实战总结,这里就不过多讨论了。重点看Http请求的代码

1
2
3
@Multipart
@POST("/open/qiniu/image")
Observable<Response<CommonResponse<UploadResultEntity>>> uploadFile(@Part MultipartBody.Part file);

Http定义在Retrofit中显得很简单直观,通过注解的方式即可指定请求方式,url,请求参数以及返回值等等,请求参数也可以通过不同的注解完成封装,详细地可以参考Retrofit用法详解,对于上传请求,需要通过指定注解@Multipart,请求参数是以@Part的方式传递的。然后我们再来看一下请求体的具体封装方法

1
2
3
4
5
6
7
@NonNull
public static MultipartBody.Part prepareFilePart(String partName, String path) {
File file = new File(path);
RequestBody requestFile =
RequestBody.create(MediaType.parse(UploadWrapper.MULTIPART_FORM_DATA), file);
return MultipartBody.Part.createFormData(partName, file.getName(), requestFile);
}

可以看到基本上和OkHttp的封装方式是一样的,只不过api接口名称不一样而已,图片封装格式以及需要传递的参数个数和参数类型也基本上一样,所以就不再过多解释了。

多文件上传利用Retrofit也很简单,大部分都是一样的,只是需要传递更多的参数,参考代码如下:

1
2
3
4
5
6
7
// 上传多个文件
@Multipart
@POST("upload")
Call<ResponseBody> uploadMultipleFiles(
@Part("description") RequestBody description,
@Part MultipartBody.Part file1,
@Part MultipartBody.Part file2);

如果大家觉得写得还不错,欢迎关注我的微信公众号:蓝田大营

蓝田大营

RxJava与Retrofit实战总结

一、ReactiveX简单介绍

Rx是一个使用可观察数据流进行异步编程的编程接口,ReactiveX结合了观察者模式、迭代器模式和函数式编程的精华。Rx提供了一系列的操作符,你可以使用它们来过滤(filter)、选择(select)、变换(transform)、结合(combine)和组合(compose)多个Observable,这些操作符让执行和复合变得非常高效。

在ReactiveX中,一个观察者(Observer)订阅一个可观察对象(Observable)。观察者对Observable发射的数据或数据序列作出响应。这种模式可以极大地简化并发操作,因为它创建了一个处于待命状态的观察者哨兵,在未来某个时刻响应Observable的通知,不需要阻塞等待Observable发射数据。

上图取自ReactiveX官方文档,上面一排图标代表被观察对象产生的事件,横向的箭头代表时间线,有6个事件依次发射,经过中间的转换和处理得到了下面的图标,即处理结果,有些事件发射、处理和接收都成功,而有些事件因为各种原因导致失败,这些情况都会在相应的回调方法中呈现。

Subscribe方法用于将观察者连接到Observable,你的观察者需要实现以下方法的一个子集:

  • onNext(T item)

    Observable调用这个方法发射数据,方法的参数就是Observable发射的数据,这个方法可能会被调用多次,取决于你的实现。

  • onError(Exception ex)

    当Observable遇到错误或者无法返回期望的数据时会调用这个方法,这个调用会终止Observable,后续不会再调用onNext和onCompleted,onError方法的参数是抛出的异常。

  • onComplete

    正常终止,如果没有遇到错误,Observable在最后一次调用onNext之后调用此方法。

根据Observable协议的定义,onNext可能会被调用零次或者很多次,最后会有一次onCompleted或onError调用(不会同时),传递数据给onNext通常被称作发射,onCompleted和onError被称作通知。

二、RxJava与RxAndroid的配合使用

RxJava是 ReactiveX 在JVM上的一个实现,ReactiveX使用Observable序列组合异步和基于事件的程序。RxJava提供了5种调度器,分别是:

  • .io()

    这个调度器时用于I/O操作。它基于根据需要,增长或缩减来自适应的线程池。由于它专用于I/O操作,所以并不是RxJava的默认方法;正确的使用它是由开发者决定的。重点需要注意的是线程池是无限制的,大量的I/O调度操作将创建许多个线程并占用内存。

  • .computation()

    这个是计算工作默认的调度器,它与I/O操作无关。它也是许多RxJava方法的默认调度器:buffer(), debounce(), delay() , interval(), sample(), skip()。

  • .immediate()

    这个调度器允许你立即在当前线程执行你指定的工作。它是timeout(),timeInterval(),以及timestamp()方法默认的调度器。

  • .newThread()

    这个调度器为指定任务启动一个新的线程。

  • .trampoline()

    当我们想在当前线程执行一个任务时,并不是立即,我们可以用.trampoline()将它入队。这个调度器将会处理它的队列并且按序运行队列中每一个任务。它是repeat()和retry()方法默认的调度器。

RxAndroid模块包含RxJava的Android特定的绑定代码。它给RxJava添加了一些类,用于帮助在Android应用中编写响应式(reactive)的组件。它提供了一个可以在给定的Android Handler上调度Observable的调度器 Scheduler,特别是在UI主线程上 AndroidSchedulers.mainThread()。

上面提到了几种线程调度器,可以让开发者在不同的线程执行不同的事件,那么如何指定和切换线程呢?RxJava提供了两个方法subscribeOn和observeOn,前者可以指定Observable事件产生和变换处理的线程,后者可以指定订阅者所在的线程,假如事件是一个耗时任务,完全可以通过subscribeOn指定为计算线程或者子线程,然后再通过observeOn切换Android主线程,即可在订阅者回调方法中操作UI视图。

看一个具体的例子吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Observable.just("")
.subscribeOn(Schedulers.newThread())
.map(s -> {

for (PerformanceEntity.Components components : performanceEntity.components) {
PerformanceSubmitEntity.Data submitData = new PerformanceSubmitEntity.Data();
submitData.componentId = components.id;
for (PerformanceEntity.Data data : performanceEntity.data) {
if (data.componentId == components.id) {
transferData(submitData, data);
break;
}

}
submitDataList.add(submitData);
}

return null;
})
.observeOn(AndroidSchedulers.mainThread())
.subscribe(o -> {

initPerformanceView();
});

在渲染视图之前需要处理大量的数据,那么就可以将Observable的线程切换至子线程,等负责的业务逻辑完成后再切换至UI线程,然后在subscribe中完成视图的渲染。这样即可以保证UI线程不会大量复杂的计算,也可以不用Handler、AsycTask等复杂的操作,代码清晰度也相对较高。

三、RxJava与Retrofit的结合

在Android开发中,网络请求往往是最耗时的,也是情况最复杂的,如果直接在UI线程进行网络请求,编译时就会报错,及时编译报错,运行时系统也会报ANR错误,所以网络请求必须在子线程完成。而网络请求结果一般又需要操作UI视图,所以返回结果的回调有必须在主线程,那么上面提到的方法就可以完美解决这个问题。

网络请求的返回结果一般都是json格式,如果返回值为原始的字符串,那么就需要调用放每次都要进行json至JavaBean的转换,有的时候我们只关心返回结果中的一部分数据,那么就还需要在返回结果中再次对数据进行筛选,还有情况是我们只关心满足一定条件的数据,那么这些需求如果用RxJava来实现就变得异常简单。有关Retrofit详细用法,以及如何将返回结果切换成RxJava处理模式,可以阅读Retrofit用法详解这篇文章,下面直接看示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
showProgressBar();
messageService.messageList(msgType, PAGE_INDEX, PAGE_SIZE)
.compose(new DefaultTransformer<>(getActivity()))
.map(messageListResponse -> messageListResponse.data)
.flatMap(messageListEntity -> Observable.from(messageListEntity))
.filter(messageEntity.id > 10)
.subscribe(new Subscriber<MessageEntity>() {
@Override
public void onCompleted() {
dismissProgressBar();
refreshLayout.setRefreshing(false);
}
@Override
public void onError(Throwable e) {
dismissProgressBar();
refreshLayout.setRefreshing(false);
}
@Override
public void onNext(MessageEntity messageEntity) {
updateView(messageEntity);
}
});
  • DefaultTransformer主要实现线程的切换以及错误消息的统一处理,一般来说服务端接口返回的数据格式都比较固定,会有code、message、data,其中code只是正确或者错误类型,message返回正确或者错误日志,data则是业务数据的存放地,那么就可以在DefaultTransformer中统一判断code值,如果是请求出错,那么直接将异常抛给onError回调方法。
  • map中实现的功能就是上文提到的调用方只关心data中的数据,而不关心code和message,那么就可以通过map方法直接将data中的数据传递给订阅者。
  • 如果我们想将返回值List逐条处理,一般的做法就是直接for循环,这里用到了flatMap,就是将messageListEntity通过Observable的from方法重新拆分成更细粒度的MessageEntity,而订阅者得到也就是MessageEntity。
  • filter方法就是通过布尔表达式筛选出符合条件的数据,上述例子中就是将id值大于10的MessageEntity筛选出来。

有关Observable的compose、map、from、flatMap、filter等方法可以参考ReactiveX官方教程,中文环境下可以参考给Android开发者的RxJava详解

欢迎大家关注我的微信公众号蓝田大营,下面是微信公众号二维码:

微信公众号蓝田大营二维码

Android Studio引入jar,aar,so文件的正确姿势

目前网上很多关于如何引入这三种文件的文章,但是良莠不齐,特写一篇文章以作记录。

  1. 引入jar包

    将jar包拷贝至app(module)->libs目录下,然后在app目录下的build.gradle文件中添加

    1
    2
    3
    4
    dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile files('libs/TalkingData_Analytics_Android_SDK.jar')
    }
  2. 引入aar文件

    同样的,将aar文件放入app(module)->libs目录下,然后在app目录下的build.gradle文件中添加

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    repositories {
    flatDir {
    dirs 'libs' //this way we can find the .aar file in libs folder
    }
    }

    ...

    dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    compile(name: 'openapi2-release', ext: 'aar')
    }
  3. 引入so文件

    同样的,将so文件放入app(module)->libs目录下,然后在app目录下的build.gradle文件中添加

    1
    2
    3
    4
    5
    sourceSets {
    main {
    jniLibs.srcDirs = ['libs']
    }
    }

Retrofit用法详解

一、简介

Retrofit是Square公司开发的一款针对Android网络请求的框架,Retrofit2底层基于OkHttp实现的,OkHttp现在已经得到Google官方认可,大量的app都采用OkHttp做网络请求,其源码详见OkHttp Github

本文全部是在Retrofit2.0+版本基础上论述,所用例子全部来自豆瓣Api

首先先来看一个完整Get请求是如何实现:

  1. 创建业务请求接口,具体代码如下:

    1
    2
    3
    4
    5
    6
    public interface BlueService {
    @GET("book/search")
    Call<BookSearchResponse> getSearchBooks(@Query("q") String name,
    @Query("tag") String tag, @Query("start") int start,
    @Query("count") int count);
    }

    这里需要稍作说明,@GET注解就表示get请求,@Query表示请求参数,将会以key=value的方式拼接在url后面

  2. 需要创建一个Retrofit的示例,并完成相应的配置

    1
    2
    3
    4
    5
    6
    Retrofit retrofit = new Retrofit.Builder()
    .baseUrl("https://api.douban.com/v2/")
    .addConverterFactory(GsonConverterFactory.create())
    .build();

    BlueService service = retrofit.create(BlueService.class);

    这里的baseUrl就是网络请求URL相对固定的地址,一般包括请求协议(如Http)、域名或IP地址、端口号等,当然还会有很多其他的配置,下文会详细介绍。还有addConverterFactory方法表示需要用什么转换器来解析返回值,GsonConverterFactory.create()表示调用Gson库来解析json返回值,具体的下文还会做详细介绍。

  3. 调用请求方法,并得到Call实例

    1
    Call<BookSearchResponse> call = mBlueService.getSearchBooks("小王子", "", 0, 3);

    Call其实在Retrofit中就是行使网络请求并处理返回值的类,调用的时候会把需要拼接的参数传递进去,此处最后得到的url完整地址为

    https://api.douban.com/v2/book/search?q=%E5%B0%8F%E7%8E%8B%E5%AD%90&tag=&start=0&count=3

  4. 使用Call实例完成同步或异步请求

    • 同步请求

      1
      BookSearchResponse response = call.execute().body();

      这里需要注意的是网络请求一定要在子线程中完成,不能直接在UI线程执行,不然会crash

    • 异步请求

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      call.enqueue(new Callback<BookSearchResponse>() {
      @Override
      public void onResponse(Call<BookSearchResponse> call, Response<BookSearchResponse> response) {
      asyncText.setText("异步请求结果: " + response.body().books.get(0).altTitle);
      }
      @Override
      public void onFailure(Call<BookSearchResponse> call, Throwable t) {

      }
      });

二、如何使用

首先需要在build.gradle文件中引入需要的第三包,配置如下:

1
2
3
compile 'com.squareup.retrofit2:retrofit:2.1.0'
compile 'com.squareup.retrofit2:converter-gson:2.1.0'
compile 'com.squareup.retrofit2:adapter-rxjava:2.1.0'

引入完第三包接下来就可以使用Retrofit来进行网络请求了。接下来会对不同的请求方式做进一步的说明。

Get方法

1. @Query

Get方法请求参数都会以key=value的方式拼接在url后面,Retrofit提供了两种方式设置请求参数。第一种就是像上文提到的直接在interface中添加@Query注解,还有一种方式是通过Interceptor实现,直接看如何通过Interceptor实现请求参数的添加。

1
2
3
4
5
6
7
8
9
10
11
public class CustomInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
Request request = chain.request();
HttpUrl httpUrl = request.url().newBuilder()
.addQueryParameter("token", "tokenValue")
.build();
request = request.newBuilder().url(httpUrl).build();
return chain.proceed(request);
}
}

addQueryParameter就是添加请求参数的具体代码,这种方式比较适用于所有的请求都需要添加的参数,一般现在的网络请求都会添加token作为用户标识,那么这种方式就比较适合。

创建完成自定义的Interceptor后,还需要在Retrofit创建client处完成添加

1
addInterceptor(new CustomInterceptor())
2. @QueryMap

如果Query参数比较多,那么可以通过@QueryMap方式将所有的参数集成在一个Map统一传递,还以上文中的get请求方法为例

1
2
3
4
public interface BlueService {
@GET("book/search")
Call<BookSearchResponse> getSearchBooks(@QueryMap Map<String, String> options);
}

调用的时候将所有的参数集合在统一的map中即可

1
2
3
4
5
6
Map<String, String> options = new HashMap<>();
map.put("q", "小王子");
map.put("tag", null);
map.put("start", "0");
map.put("count", "3");
Call<BookSearchResponse> call = mBlueService.getSearchBooks(options);
3. Query集合

假如你需要添加相同Key值,但是value却有多个的情况,一种方式是添加多个@Query参数,还有一种简便的方式是将所有的value放置在列表中,然后在同一个@Query下完成添加,实例代码如下:

1
2
3
4
public interface BlueService {
@GET("book/search")
Call<BookSearchResponse> getSearchBooks(@Query("q") List<String> name);
}

最后得到的url地址为

1
https://api.douban.com/v2/book/search?q=leadership&q=beyond%20feelings
4. Query非必填

如果请求参数为非必填,也就是说即使不传该参数,服务端也可以正常解析,那么如何实现呢?其实也很简单,请求方法定义处还是需要完整的Query注解,某次请求如果不需要传该参数的话,只需填充null即可。

针对文章开头提到的get的请求,加入按以下方式调用

1
Call<BookSearchResponse> call = mBlueService.getSearchBooks("小王子", null, 0, 3);

那么得到的url地址为

1
https://api.douban.com/v2/book/search?q=%E5%B0%8F%E7%8E%8B%E5%AD%90&start=0&count=3
5. @Path

如果请求的相对地址也是需要调用方传递,那么可以使用@Path注解,示例代码如下:

1
2
@GET("book/{id}")
Call<BookResponse> getBook(@Path("id") String id);

业务方想要在地址后面拼接书籍id,那么通过Path注解可以在具体的调用场景中动态传递,具体的调用方式如下:

1
Call<BookResponse> call = mBlueService.getBook("1003078");

此时的url地址为

1
https://api.douban.com/v2/book/1003078

@Path可以用于任何请求方式,包括Post,Put,Delete等等

Post请求

1. @field

Post请求需要把请求参数放置在请求体中,而非拼接在url后面,先来看一个简单的例子

1
2
3
4
@FormUrlEncoded
@POST("book/reviews")
Call<String> addReviews(@Field("book") String bookId, @Field("title") String title,
@Field("content") String content, @Field("rating") String rating);

这里有几点需要说明的

  • @FormUrlEncoded将会自动将请求参数的类型调整为application/x-www-form-urlencoded,假如content传递的参数为Good Luck,那么最后得到的请求体就是

    1
    content=Good+Luck

    FormUrlEncoded不能用于Get请求

  • @Field注解将每一个请求参数都存放至请求体中,还可以添加encoded参数,该参数为boolean型,具体的用法为

    1
    @Field(value = "book", encoded = true) String book

    encoded参数为true的话,key-value-pair将会被编码,即将中文和特殊字符进行编码转换

2. @FieldMap

上述Post请求有4个请求参数,假如说有更多的请求参数,那么通过一个一个的参数传递就显得很麻烦而且容易出错,这个时候就可以用FieldMap

1
2
3
@FormUrlEncoded
@POST("book/reviews")
Call<String> addReviews(@FieldMap Map<String, String> fields);
3. @Body

如果Post请求参数有多个,那么统一封装到类中应该会更好,这样维护起来会非常方便

1
2
3
4
5
6
7
8
9
10
@FormUrlEncoded
@POST("book/reviews")
Call<String> addReviews(@Body Reviews reviews);

public class Reviews {
public String book;
public String title;
public String content;
public String rating;
}

其他请求方式

除了Get和Post请求,Http请求还包括Put,Delete等等,用法和Post相似,所以就不再单独介绍了。

上传

上传因为需要用到Multipart,所以需要单独拿出来介绍,先看一个具体上传的例子

首先还是需要新建一个interface用于定义上传方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public interface FileUploadService {  
// 上传单个文件
@Multipart
@POST("upload")
Call<ResponseBody> uploadFile(
@Part("description") RequestBody description,
@Part MultipartBody.Part file);

// 上传多个文件
@Multipart
@POST("upload")
Call<ResponseBody> uploadMultipleFiles(
@Part("description") RequestBody description,
@Part MultipartBody.Part file1,
@Part MultipartBody.Part file2);
}

接下来我们还需要在Activity和Fragment中实现两个工具方法,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static final String MULTIPART_FORM_DATA = "multipart/form-data";

@NonNull
private RequestBody createPartFromString(String descriptionString) {
return RequestBody.create(
MediaType.parse(MULTIPART_FORM_DATA), descriptionString);
}

@NonNull
private MultipartBody.Part prepareFilePart(String partName, Uri fileUri) {
File file = FileUtils.getFile(this, fileUri);

// 为file建立RequestBody实例
RequestBody requestFile =
RequestBody.create(MediaType.parse(MULTIPART_FORM_DATA), file);

// MultipartBody.Part借助文件名完成最终的上传
return MultipartBody.Part.createFormData(partName, file.getName(), requestFile);
}

好了,接下来就是最终的上传文件代码了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Uri file1Uri = ... // 从文件选择器或者摄像头中获取 
Uri file2Uri = ...

// 创建上传的service实例
FileUploadService service =
ServiceGenerator.createService(FileUploadService.class);

// 创建文件的part (photo, video, ...)
MultipartBody.Part body1 = prepareFilePart("video", file1Uri);
MultipartBody.Part body2 = prepareFilePart("thumbnail", file2Uri);

// 添加其他的part
RequestBody description = createPartFromString("hello, this is description speaking");

// 最后执行异步请求操作
Call<ResponseBody> call = service.uploadMultipleFiles(description, body1, body2);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call,
Response<ResponseBody> response) {
Log.v("Upload", "success");
}
@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
Log.e("Upload error:", t.getMessage());
}
});

三、其他必须知道的事项

1. 添加自定义的header

Retrofit提供了两个方式定义Http请求头参数:静态方法和动态方法,静态方法不能随不同的请求进行变化,头部信息在初始化的时候就固定了。而动态方法则必须为每个请求都要单独设置。

  • 静态方法

    1
    2
    3
    4
    5
    6
    7
    public interface BlueService {
    @Headers("Cache-Control: max-age=640000")
    @GET("book/search")
    Call<BookSearchResponse> getSearchBooks(@Query("q") String name,
    @Query("tag") String tag, @Query("start") int start,
    @Query("count") int count);
    }

    当然你想添加多个header参数也是可以的,写法也很简单

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public interface BlueService {
    @Headers({
    "Accept: application/vnd.yourapi.v1.full+json",
    "User-Agent: Your-App-Name"
    })
    @GET("book/search")
    Call<BookSearchResponse> getSearchBooks(@Query("q") String name,
    @Query("tag") String tag, @Query("start") int start,
    @Query("count") int count);
    }

    此外也可以通过Interceptor来定义静态请求头

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    public class RequestInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
    Request original = chain.request();
    Request request = original.newBuilder()
    .header("User-Agent", "Your-App-Name")
    .header("Accept", "application/vnd.yourapi.v1.full+json")
    .method(original.method(), original.body())
    .build();
    return chain.proceed(request);
    }
    }

    添加header参数Request提供了两个方法,一个是header(key, value),另一个是.addHeader(key, value),两者的区别是,header()如果有重名的将会覆盖,而addHeader()允许相同key值的header存在

    然后在OkHttp创建Client实例时,添加RequestInterceptor即可

    1
    2
    3
    4
    5
    6
    private static OkHttpClient getNewClient(){
    return new OkHttpClient.Builder()
    .addInterceptor(new RequestInterceptor())
    .connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
    .build();
    }
  • 动态方法

    1
    2
    3
    4
    5
    6
    7
    public interface BlueService {
    @GET("book/search")
    Call<BookSearchResponse> getSearchBooks(
    @Header("Content-Range") String contentRange,
    @Query("q") String name, @Query("tag") String tag,
    @Query("start") int start, @Query("count") int count);
    }

2. 网络请求日志

调试网络请求的时候经常需要关注一下请求参数和返回值,以便判断和定位问题出在哪里,Retrofit官方提供了一个很方便查看日志的Interceptor,你可以控制你需要的打印信息类型,使用方法也很简单。

首先需要在build.gradle文件中引入logging-interceptor

1
compile 'com.squareup.okhttp3:logging-interceptor:3.4.1'

同上文提到的CustomInterceptor和RequestInterceptor一样,添加到OkHttpClient创建处即可,完整的示例代码如下:

1
2
3
4
5
6
7
8
9
private static OkHttpClient getNewClient(){
HttpLoggingInterceptor logging = new HttpLoggingInterceptor();
logging.setLevel(HttpLoggingInterceptor.Level.BODY);
return new OkHttpClient.Builder()
.addInterceptor(new CustomInterceptor())
.addInterceptor(logging)
.connectTimeout(DEFAULT_TIMEOUT, TimeUnit.SECONDS)
.build();
}

HttpLoggingInterceptor提供了4中控制打印信息类型的等级,分别是NONE,BASIC,HEADERS,BODY,接下来分别来说一下相应的打印信息类型。

  • NONE

    没有任何日志信息

  • Basic

    打印请求类型,URL,请求体大小,返回值状态以及返回值的大小

    1
    2
    D/HttpLoggingInterceptor$Logger: --> POST /upload HTTP/1.1 (277-byte body)  
    D/HttpLoggingInterceptor$Logger: <-- HTTP/1.1 200 OK (543ms, -1-byte body)
  • Headers

    打印返回请求和返回值的头部信息,请求类型,URL以及返回值状态码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    <-- 200 OK https://api.douban.com/v2/book/search?q=%E5%B0%8F%E7%8E%8B%E5%AD%90&start=0&count=3&token=tokenValue (3787ms)
    D/OkHttp: Date: Sat, 06 Aug 2016 14:26:03 GMT
    D/OkHttp: Content-Type: application/json; charset=utf-8
    D/OkHttp: Transfer-Encoding: chunked
    D/OkHttp: Connection: keep-alive
    D/OkHttp: Keep-Alive: timeout=30
    D/OkHttp: Vary: Accept-Encoding
    D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
    D/OkHttp: Pragma: no-cache
    D/OkHttp: Cache-Control: must-revalidate, no-cache, private
    D/OkHttp: Set-Cookie: bid=D6UtQR5N9I4; Expires=Sun, 06-Aug-17 14:26:03 GMT; Domain=.douban.com; Path=/
    D/OkHttp: X-DOUBAN-NEWBID: D6UtQR5N9I4
    D/OkHttp: X-DAE-Node: dis17
    D/OkHttp: X-DAE-App: book
    D/OkHttp: Server: dae
    D/OkHttp: <-- END HTTP
  • Body

    打印请求和返回值的头部和body信息

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    <-- 200 OK https://api.douban.com/v2/book/search?q=%E5%B0%8F%E7%8E%8B%E5%AD%90&tag=&start=0&count=3&token=tokenValue (3583ms)
    D/OkHttp: Connection: keep-alive
    D/OkHttp: Date: Sat, 06 Aug 2016 14:29:11 GMT
    D/OkHttp: Keep-Alive: timeout=30
    D/OkHttp: Content-Type: application/json; charset=utf-8
    D/OkHttp: Vary: Accept-Encoding
    D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
    D/OkHttp: Transfer-Encoding: chunked
    D/OkHttp: Pragma: no-cache
    D/OkHttp: Connection: keep-alive
    D/OkHttp: Cache-Control: must-revalidate, no-cache, private
    D/OkHttp: Keep-Alive: timeout=30
    D/OkHttp: Set-Cookie: bid=ESnahto1_Os; Expires=Sun, 06-Aug-17 14:29:11 GMT; Domain=.douban.com; Path=/
    D/OkHttp: Vary: Accept-Encoding
    D/OkHttp: X-DOUBAN-NEWBID: ESnahto1_Os
    D/OkHttp: Expires: Sun, 1 Jan 2006 01:00:00 GMT
    D/OkHttp: X-DAE-Node: dis5
    D/OkHttp: Pragma: no-cache
    D/OkHttp: X-DAE-App: book
    D/OkHttp: Cache-Control: must-revalidate, no-cache, private
    D/OkHttp: Server: dae
    D/OkHttp: Set-Cookie: bid=5qefVyUZ3KU; Expires=Sun, 06-Aug-17 14:29:11 GMT; Domain=.douban.com; Path=/
    D/OkHttp: X-DOUBAN-NEWBID: 5qefVyUZ3KU
    D/OkHttp: X-DAE-Node: dis17
    D/OkHttp: X-DAE-App: book
    D/OkHttp: Server: dae
    D/OkHttp: {"count":3,"start":0,"total":778,"books":[{"rating":{"max":10,"numRaters":202900,"average":"9.0","min":0},"subtitle":"","author":["[法] 圣埃克苏佩里"],"pubdate":"2003-8","tags":[{"count":49322,"name":"小王子","title":"小王子"},{"count":41381,"name":"童话","title":"童话"},{"count":19773,"name":"圣埃克苏佩里","title":"圣埃克苏佩里"}
    D/OkHttp: <-- END HTTP (13758-byte body)

3. 为某个请求设置完整的URL

​ 假如说你的某一个请求不是以base_url开头该怎么办呢?别着急,办法很简单,看下面这个例子你就懂了

1
2
3
4
5
6
7
8
9
10
11
public interface BlueService {  
@GET
public Call<ResponseBody> profilePicture(@Url String url);
}

Retrofit retrofit = Retrofit.Builder()
.baseUrl("https://your.api.url/");
.build();

BlueService service = retrofit.create(BlueService.class);
service.profilePicture("https://s3.amazon.com/profile-picture/path");

​ 直接用@Url注解的方式传递完整的url地址即可。

4. 取消请求

Call提供了cancel方法可以取消请求,前提是该请求还没有执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
String fileUrl = "http://futurestud.io/test.mp4";  
Call<ResponseBody> call =
downloadService.downloadFileWithDynamicUrlSync(fileUrl);
call.enqueue(new Callback<ResponseBody>() {
@Override
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
Log.d(TAG, "request success");
}

@Override
public void onFailure(Call<ResponseBody> call, Throwable t) {
if (call.isCanceled()) {
Log.e(TAG, "request was cancelled");
} else {
Log.e(TAG, "other larger issue, i.e. no network connection?");
}
}
});
}

// 触发某个动作,例如用户点击了取消请求的按钮
call.cancel();
}

四、结语

关于Retrofit常用的方法基本上已经介绍完了,有些请求由于工作保密性的原因,所以就没有放出来,但是基本的方法和操作都是有的,仿照文中提到的代码就可以实现你想要的功能。参考了国外的一则系列教程和liangfei的一篇文章图解 Retrofit - ServiceMethod,由于本人能力有限,有错误或者表述不准确的地方还望多多留言指正。

Retrofit源码分析

Retrofit是一个在Android和Java中类型安全Http客户端框架,可以通过注解的方式声明请求方法、参数和URL路径,此外还支持mutlipart请求和文件上传。Retrofit2.0是基于OkHttp实现的,底层的网络请求由OkHttp完成,下文的分析中会详细提及两者的关联。

一. 简单用法

首先需要定义一个通用的基础类ServiceGenerator,ServiceGenerator是整个框架的核心,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class ServiceGenerator {
public static final String API_BASE_URL = "http://your.api-base.url";

private static OkHttpClient.Builder httpClient = new OkHttpClient.Builder();

private static Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create());

public static <S> S createService(Class<S> serviceClass) {
Retrofit retrofit = builder.client(httpClient.build()).build();
return retrofit.create(serviceClass);
}
}

在ServiceGenerator中定义了BASE_URL,后续所有的业务请求只需要提供相对路径即可,GsonConverterFactory表示网络请求返回值通过Gson解析,OkHttpClient.Builder可以配置几乎所有的网络请求相关的参数,具体可以参照OkHttp的用法和源码,最后一行代码retrofit.create(serviceClass)很重要,会在下文的源码分析中重点提及。

以请求Github仓库的贡献者列表为例,此时需要先定义一个接口,并且声明具体的业务请求方法,代码如下:

1
2
3
4
5
6
7
public interface GitHubClient {  
@GET("/repos/{owner}/{repo}/contributors")
Call<List<Contributor>> contributors(
@Path("owner") String owner,
@Path("repo") String repo
);
}

@GET表示网络请求方法为get请求,URL路径中的{ower}{repo}是由具体的调用方通过参数传递的方式提供的,@Path为URL占位符标识,Contributor为贡献者的JavaBean。

接口和请求方法都定义好了,接下来是具体调用和返回值处理,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String... args) {  
// Create a very simple REST adapter which points the GitHub API endpoint.
GitHubClient client = ServiceGenerator.createService(GitHubClient.class);

// Fetch and print a list of the contributors to this library.
Call<List<Contributor>> call =
client.contributors("fs_opensource", "android-boilerplate");

try {
List<Contributor> contributors = call.execute().body();
} catch (IOException e) {
// handle errors
}

for (Contributor contributor : contributors) {
System.out.println(
contributor.login + " (" + contributor.contributions + ")");
}
}

通过ServiceGenerator生成了GitubClient的对象实例,然后就可以调用之前定义好的网络请求,可以看到返回值只要传入事先定义好的JavaBean,Retrofit就会通过上文配置的Gson进行转换,无需手动解析json,client.contributors方法带有两个参数就是上文中@Path占位符需要的参数,返回值为Call类型的实例对象,这个Call类其实就是OkHttp的静态代理,OkHttp也有Call类,两者接口定义有很多相似之处,可以实现无缝对接。call.execute执行同步请求,线程会一直处于阻塞状态,直到请求成功或失败,返回值可以通过body方法获取。如果是异步请求的话,其他都一样,只是调用Call的enqueue方法,返回值通过回调的方式传递给调用方,代码如下:

1
2
3
4
5
6
7
8
9
call.enqueue(new Callback(){
@Override public void onFailure(Call call, IOException e) {
e.printStackTrace();
}

@Override public void onResponse(Call call, Response response) throws IOException {

}
});

本文重点不是讲解Retrofit的用法,只是为了方便下面的源码分析。

二、源码分析

上文提到了ServiceGenerator最后一行代码return retrofit.create(serviceClass)很重要,也是理解Retrofit最核心的部分,那么就从这里入手来看看create方法究竟做了什么,从这个方法也可以看出参数是我们之前定义好的接口,返回值是接口的实例对象,并且可以直接调用网络请求,源码如下。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public <T> T create(final Class<T> service) {
Utils.validateServiceInterface(service);
if (validateEagerly) {
eagerlyValidateMethods(service);
}
return (T) Proxy.newProxyInstance(service.getClassLoader(), new Class<?>[] { service },
new InvocationHandler() {
private final Platform platform = Platform.get();

@Override public Object invoke(Object proxy, Method method, Object... args)
throws Throwable {
// If the method is a method from Object then defer to normal invocation.
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
if (platform.isDefaultMethod(method)) {
return platform.invokeDefaultMethod(method, service, proxy, args);
}
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);
}
});
}

可以看到该方法主要利用了Java的动态代理(关于动态代理可以参考Java详解相关的书籍和文档,这里就不过多解释了),InvocationHandle的invoke方法会在service接口每个方法被调用的时候传入invoke方法体中的代码,重点看最后3行代码。

1
2
3
ServiceMethod serviceMethod = loadServiceMethod(method);
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);
return serviceMethod.callAdapter.adapt(okHttpCall);

1. ServiceMethod

这里有一个新出现的类ServiceMethod,这个类包含了网络请求的大部分参数,我们先来看
loadServiceMethod方法,跟踪进去:

1
2
3
4
5
6
7
8
9
10
11
ServiceMethod loadServiceMethod(Method method) {
ServiceMethod result;
synchronized (serviceMethodCache) {
result = serviceMethodCache.get(method);
if (result == null) {
result = new ServiceMethod.Builder(this, method).build();
serviceMethodCache.put(method, result);
}
}
return result;
}

我们只看核心代码result = new ServiceMethod.Builder(this, method).build(),这段代码是将网络请求方法method分拆解析,然后在ServiceMethod内部完成处理,跟踪进去:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public ServiceMethod build() {
callAdapter = createCallAdapter();
responseType = callAdapter.responseType();

···

responseConverter = createResponseConverter();

for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);
}

···

return new ServiceMethod<>(this);
}

代码比较多,只挑重要的来看,刚才也提到过ServiceMethod的build方法主要是对请求参数进行分拆解析,callAdapter = createCallAdapter()获取返回值的处理方式,Retofit支持目前比较流行的Call Adapter,分别是

  • RxJava Observable & Single - com.squareup.retrofit2:adapter-rxjava
  • Guava ListenableFuture - com.squareup.retrofit2:adapter-guava
  • Java 8 CompleteableFuture - com.squareup.retrofit2:adapter-java8

具体用法是在Retrofit初始化时通过addCallAdapterFactory()方法配置,如果是RxJava,只需传参RxJavaCallAdapterFactory.create()即可。

createResponseConverter()方法主要是获取返回值Converter,Retrofit初始化时配置addConverterFactory(GsonConverterFactory.create())就表示通过Gson解析返回值。

然后是循环获取Annotation参数并处理,看一下parseMethodAnnotation方法的具体代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private void parseMethodAnnotation(Annotation annotation) {
if (annotation instanceof DELETE) {
parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);
} else if (annotation instanceof GET) {
parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
} else if (annotation instanceof HEAD) {
parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false);
if (!Void.class.equals(responseType)) {
throw methodError("HEAD method must use Void as response type.");
}
} else if (annotation instanceof PATCH) {
parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true);
} else if (annotation instanceof POST) {
parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
} else if (annotation instanceof PUT) {
parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
} else if (annotation instanceof OPTIONS) {
parseHttpMethodAndPath("OPTIONS", ((OPTIONS) annotation).value(), false);
} else if (annotation instanceof HTTP) {
HTTP http = (HTTP) annotation;
parseHttpMethodAndPath(http.method(), http.path(), http.hasBody());
} else if (annotation instanceof retrofit2.http.Headers) {
String[] headersToParse = ((retrofit2.http.Headers) annotation).value();
if (headersToParse.length == 0) {
throw methodError("@Headers annotation is empty.");
}
headers = parseHeaders(headersToParse);
} else if (annotation instanceof Multipart) {
if (isFormEncoded) {
throw methodError("Only one encoding annotation is allowed.");
}
isMultipart = true;
} else if (annotation instanceof FormUrlEncoded) {
if (isMultipart) {
throw methodError("Only one encoding annotation is allowed.");
}
isFormEncoded = true;
}
}

通过解析Annotation参数可以获得请求方法和请求参数,就是在这里分类拼装的,这个方法基本上囊括了Http请求的所有请求方式,除了常见的get,post,put,delete还有header,patch,options,multipart等等,这里主要是获取业务请求的请求方式、url、请求参数。

至此基本上就分析完了Retrofit对request的解析。

2. OkHttpCall

回到Retrofit的create方法倒数第二行代码

1
OkHttpCall okHttpCall = new OkHttpCall<>(serviceMethod, args);

OkHttpCall实现了Retrofit的Call接口,Call接口定义了和OkHttp库的Call接口相同的方法,所以OkHttpCall就是OkHttp的静态代理,请求url和参数经过解析后直接透传至OkHttp的Call接口,可以看一下OkHttpCall的方法列表。

OkHttpCall方法列表

可以看到execte就是执行同步请求,enqueue执行异步请求,还可以取消请求,Retrofit还有一个特性是提供了clone方法,如果你想利用之前的请求方法,可以直接拷贝一份,参数配置完全一样。

3. CallAdapter

回到Retrofit的create方法,最后一行代码

1
return serviceMethod.callAdapter.adapt(okHttpCall)

之前配置好的CallAdapter开始适配返回值处理方式了,注意到这里网络请求还没有开始执行,获取到的OkHttpCall也只是参数配置完成,真正的网络请求需要手动调用call.execte()或者call.enqueue(),如果是配置了RxJava处理方式,那么网络请求是在subscribe时才执行的。

上文围绕Retrofit的create方法分析了整个网络请求从参数解析到执行网络请求的全过程,牵扯到OkHttp和RxJava相关的处理没有详细展开,因为这两个不是本文的重点,关于OkHttp会在接下来的文章中详细阐述和分析。

三、结语

Retrofit提供了一种清晰简单的请求定义方式,通过简单的注解即可完成复杂的参数定义,网络请求直接封装了目前Android最流行的OkHttp框架,如果再结合RxJava一起使用,网络请求代码将会变得更优雅清晰,关于Retrofit的详细用法会在下一篇文章中详细阐述。

Android权限最佳实践和代码实例分析

一、 官方推荐的权限最佳实践

如果没有节制地频繁请求权限很容易使用户反感,如果用户发现app需要大量的敏感权限,很可能会拒绝使用甚至直接卸载。以下几点可以有效地提升用户的使用体验。

1. 考虑使用Intent

在很多情况下,你可以有两种选择实现你的操作,一种是直接app中请求比如摄像头这样的权限,然后调用摄像头APIs去控制摄像头并获取照片。这种方式可以使你对摄像头有全部的控制权,并且可以自定义相关的UI。

然而,如果你不需要完全控制,那么你只需要使用ACTION_IMAGE_CAPTURE intent来请求图片。当你发送这个intent,系统会自动询问用户打开哪个照相app(加入手机上安装不止一个照相app)。用户从选中的照相app中选择照片后,你的app就会从onActivityResult()方法中得到需要的照片。

类似的,如果你需要打电话,访问用户的通讯录等等,你也可以发送相应的intent,或者直接请求相应的权限。以下是两种方式的优缺点:

如果自己申请权限:

  • 你将可以完全控制想要的权限,但是同时也增加了复杂度,例如你要设计相应的UI界面
  • 一旦用户同意了你的权限申请,不管是在使用时还是安装阶段(根据用户手机的系统版本),你将可以一直使用该权限。但是一旦用户拒绝了你的权限申请(或者随后撤回了权限),你的app将无法使用相关的权限和功能

如果使用Intent:

  • 首先你不需要自己设计UI界面,拥有该权限的app会提供UI,那么同时也意味着你将不能控制用户的使用体验,用户将会被一个你甚至完全不知道的app所影响。
  • 如果用户有不止一个相关的app,系统将会提示用户做出选择。如果用户不勾选默认的操作,那么每次调用该权限的时候都会弹出提示框。

2. 不要同时申请大量的权限

如果用户使用的Android 6.0(API为23)及以上版本,用户需要在使用app时选择是否允许使用某权限。如果你一次性向用户申请大量的权限,用户会很反感甚至直接退出app。所以,你应该在只有用到某权限时才询问用户。

在有些情况下,你可能需要不止一个权限。你应该在启动app时请求权限,例如,你要做一个照片相关的app,你需要申请摄像头权限。当用户第一次打开app时,他们不会惊讶app询问使用摄像头的权限。同时app也需要分享照片给通讯录中好友,你不应该在app首次启动时申请READ_CONTACTS权限,而是当用户分享照片再去申请。

3. 解释为什么你需要权限

当你调用requestPermissions()方法时,系统会自动弹出权限对话框展示相应的权限描述,但是不会显示申请的原因。这会在某种程度上给用户造成困惑,所以在调用requestPermissions()前解释一下申请的原因会比较合适。

例如,一个图片应用想要地址服务以给图片标出地理标签,一个普通用户可能不明白为什么照片需要地理信息,甚至困惑app为什么要申请地址权限。因此,在调用requestPermissions()前告诉用户申请的原因就显得很有必要了。

至于如何在代码中实现显示申请原因下面的代码分析中会提及。

4. 测试权限

从Android 6.0(API为23)开始,用户可以在任意时刻同意和拒绝权限,而不是像之前版本安装时做一次决定。在Android 6.0之前,你可以假定app所有在manifest文件声明的权限是已经通过了。但是在Android 6.0及更高版本,你不能再有这样的假定。

以下这些建议将会在Android 6.0及更高版本帮你识别权限相关的代码问题:

  • 确认你的app当前需要的权限和相关的代码
  • 测试用户可以通过权限保护服务
  • 测试各种同意和拒绝的权限组合,例如,一个照相app可能会在manifest文件中罗列CAMERA, READ_CONTACTS, and ACCESS_FINE_LOCATION权限,你应该测试每个权限同意或拒绝,并且保证app可以有相应的处理。
  • 在命令行中使用adb工具管理权限:
  1. 按组罗列权限和状态:$ adb shell pm list permissions -d -g

  2. 同意和拒绝一个或者多个权限:

$ adb shell pm [grant | revoke]

例如:

打开READ_CONTACTS权限

1
adb shell pm grant com.name.app android.permission.READ_CONTACTS

关闭READ_CONTACTS权限

1
adb shell pm revoke com.name.app android.permission.READ_CONTACTS

二、 权限代码分析

以申请摄像头为例:

首先需要判断是否要申请摄像头权限:

1
2
3
4
5
6
7
8
9
10
11
public void showCamera(View view) {
// 检查摄像头权限是否已经有效
if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
// 摄像头权限还未得到用户的同意
requestCameraPermission();
} else {
// 摄像头权限以及有效,显示摄像头预览界面
showCameraPreview();
}
}

正如代码中注释那样,需要先判断摄像头权限是否有限,如果权限还未有效,需要调用申请权限方法,如果已经有效,则直接显示摄像头预览界面。

打开摄像头预览界面不是本文的重点,所以相关的代码就不关注了。那么接下来看一下requestCameraPermission方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
private void requestCameraPermission() {
// 摄像头权限已经被拒绝
if (ActivityCompat.shouldShowRequestPermissionRationale(this,
Manifest.permission.CAMERA)) {
// 如果用户已经拒绝劝降,那么提供额外的权限说明
Snackbar.make(mLayout, R.string.permission_camera_rationale,
Snackbar.LENGTH_INDEFINITE)
.setAction(R.string.ok, new View.OnClickListener() {
@Override
public void onClick(View view) {
ActivityCompat.requestPermissions(MainActivity.this,
new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA);
}
})
.show();
} else {
// 摄像头还没有被拒绝,直接申请
ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.CAMERA},
REQUEST_CAMERA);
}
}

上述代码先是判断权限是否已经被拒绝,如果被拒绝则通过Snackbar展示权限申请的原因,如果用户同意将会再次申请权限。权限申请的结果是在onRequestPermissionsResult返回的,下面来看这部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Callback received when a permissions request has been completed.
*/

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults)
{

if (requestCode == REQUEST_CAMERA) {
// BEGIN_INCLUDE(permission_result)
// 收到摄像头权限申请的结果
// 检查摄像头权限是否已经通过
if (grantResults.length == 1 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
// 摄像头权限已经申请成功,可以展示摄像预览界面了
Snackbar.make(mLayout, R.string.permision_available_camera,
Snackbar.LENGTH_SHORT).show();
showCameraPreview();
} else {
// 摄像头权限申请失败
Snackbar.make(mLayout, R.string.permissions_not_granted,
Snackbar.LENGTH_SHORT).show();
}
} else {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
}
}

三、第三方SDK:EasyPermissions

Github上有一个比较火且简单易懂的第三方SDK可以简化权限管理,EasyPermissions可以详见github地址。接下来会大致介绍一下其用法,最后会发现其实和系统提供的权限管理很相似,理解起来也很简单。

1. 简单用法

首先需要在build.gradle文件中引入包,操作如下:

1
2
3
dependencies {
compile 'pub.devrel:easypermissions:0.1.7'
}

还以申请摄像头权限为例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class MainActivity extends AppCompatActivity
implements EasyPermissions.PermissionCallbacks {

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
}
@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);

// 调用EasyPermissions的onRequestPermissionsResult方法,参数和系统方法保持一致,然后就不要关心具体的权限申请代码了
EasyPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
@Override
public void onPermissionsGranted(int requestCode, List<String> list) {
// 此处表示权限申请已经成功,可以使用该权限完成app的相应的操作了
// ...
}
@Override
public void onPermissionsDenied(int requestCode, List<String> list) {
// 此处表示权限申请被用户拒绝了,此处可以通过弹框等方式展示申请该权限的原因,以使用户允许使用该权限
// ...
}
}

首先需要在Activity实现EasyPermissions.PermissionCallbacks接口,该接口提供了onPermissionsGranted和onPermissionsDenied两个方法,也即权限申请成功和失败的回调方法,而EasyPermissions.PermissionCallbacks又实现了ActivityCompat.OnRequestPermissionsResultCallback,该接口提供了onRequestPermissionsResult方法,相当于EasyPermissions将系统的权限申请结果回调方法又进行了二次封装,同时提供了权限申请成功和失败的回调方法。

同时触发摄像头权限申请方法如下:

1
2
3
4
5
6
7
8
9
10
11
@AfterPermissionGranted(RC_CAMERA_PERM)
public void cameraTask() {
if (EasyPermissions.hasPermissions(this, Manifest.permission.CAMERA)) {
// 已经有摄像头权限了,可以使用该权限完成app的相应的操作了
Toast.makeText(this, "TODO: Camera things", Toast.LENGTH_LONG).show();
} else {
// app还没有使用摄像头的权限,调用该方法进行申请,同时给出了相应的说明文案,提高用户同意的可能性
EasyPermissions.requestPermissions(this, getString(R.string.rationale_camera),
RC_CAMERA_PERM, Manifest.permission.CAMERA);
}
}

此处会先调用EasyPermissions.hasPermissions方法判断是否允许使用该权限,如果返回值为ture表示已经申请成功过该权限,则直接使用即可,如果返回值为false表示还没有申请过该权限,那么可以通过EasyPermissions.requestPermissions方法进行申请,同时给出申请原因文案。

通过查看EasyPermissions.hasPermissions的源码,可以看到该方法可以接收多个参数,即可以同时检查多个权限。

AfterPermissionGranted注解是可选的,如果有该注解的话,那么当request值对应的权限申请通过的话会自动调用该方法。

需要特别说明的是,当用户在系统弹出的权限申请对话框中拒绝权限并且勾选不再询问,那么下次系统讲不会自动尝试申请,但是可以在onPermissionsDenied方法中通过弹框的方式解释app需要该权限的理由,如果用户同意的话会再次尝试请求。

2. 源码分析

首先来看EasyPermissions.hasPermissions方法,可以看到先是有一个版本检查,因为Android 6.0之前是不需要在运行时检查权限的,然后就是调用系统提供的ContextCompat.checkSelfPermission方法,所以这个方法好理解的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public static boolean hasPermissions(Context context, String... perms) {
// Always return true for SDK < M, let the system deal with the permissions
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
Log.w(TAG, "hasPermissions: API version < M, returning true by default");
return true;
}
for (String perm : perms) {
boolean hasPerm = (ContextCompat.checkSelfPermission(context, perm) ==
PackageManager.PERMISSION_GRANTED);
if (!hasPerm) {
return false;
}
}
return true;
}

然后是权限申请requestPermissions方法,首先是通过checkCallingObjectSuitability判断版本号和是否是Acticity或Fragment调用,然后是根据入参拼接权限申请解释文案并通过对话框显示给用户,如果用户点击同意将会调用系统提供的requestPermissions方法,如果用户点击取消,只直接返回权限申请拒绝的回调方法onPermissionsDenied。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
 public static void requestPermissions(final Object object, String rationale,
@StringRes int positiveButton,
@StringRes int negativeButton,
final int requestCode, final String... perms)
{


checkCallingObjectSuitability(object);
final PermissionCallbacks callbacks = (PermissionCallbacks) object;

boolean shouldShowRationale = false;
for (String perm : perms) {
shouldShowRationale =
shouldShowRationale || shouldShowRequestPermissionRationale(object, perm);
}

if (shouldShowRationale) { // permission has ever denied
Activity activity = getActivity(object);
if (null == activity) {
return;
}

AlertDialog dialog = new AlertDialog.Builder(activity)
.setMessage(rationale)
.setPositiveButton(positiveButton, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
executePermissionsRequest(object, perms, requestCode);
}
})
.setNegativeButton(negativeButton, new DialogInterface.OnClickListener() {
@Override
public void onClick(DialogInterface dialog, int which) {
// act as if the permissions were denied
callbacks.onPermissionsDenied(requestCode, Arrays.asList(perms));
}
}).create();
dialog.show();
} else {
executePermissionsRequest(object, perms, requestCode);
}
}

最后是onRequestPermissionsResult方法,该方法接收系统的权限申请结果方法,并做统一的处理。可以看到也是先通过checkCallingObjectSuitability判断版本号和是否是Acticity或Fragment调用,然后根据不同权限申请结果分别放置到通过和拒绝列表,可以看到如果拒绝列表不为空直接返回申请失败的回调,当成功列表不为空调用之前包含AfterPermissionGranted注解的方法,完成后续的业务动作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public static void onRequestPermissionsResult(int requestCode, String[] permissions,
int[] grantResults, Object object)
{


checkCallingObjectSuitability(object);
PermissionCallbacks callbacks = (PermissionCallbacks) object;

// Make a collection of granted and denied permissions from the request.
ArrayList<String> granted = new ArrayList<>();
ArrayList<String> denied = new ArrayList<>();
for (int i = 0; i < permissions.length; i++) {
String perm = permissions[i];
if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
granted.add(perm);
} else {
denied.add(perm);
}
}

// Report granted permissions, if any.
if (!granted.isEmpty()) {
// Notify callbacks
callbacks.onPermissionsGranted(requestCode, granted);
}

// Report denied permissions, if any.
if (!denied.isEmpty()) {
callbacks.onPermissionsDenied(requestCode, denied);
}

// If 100% successful, call annotated methods
if (!granted.isEmpty() && denied.isEmpty()) {
runAnnotatedMethods(object, requestCode);
}
}

结语

至此算是讲完了Android权限管理最佳实践的所有内容,第一部分是官方的操作建议,第二部分是系统提供的权限管理方案,第三部分是Github上比较成熟的SDK的简单用法和源码分析,希望对各位读者有帮助。

Android M 权限管理详解

声明权限

每一个Android APP都是运行在一个受限的沙盒中,如果一个app想要使用外部的资源或信息,就必须申请相应的权限,通常我们会在App Manifest文件中声明我们需要的所有权限。

根据权限的敏感程度,系统会自动申请一些敏感程度低的权限,而敏感程度高的就必须要显式的申请。例如,如果你的app需要打开闪光灯的权限,系统会自动帮你获取,但是如果你的app要访问通讯录,那么系统就会询问用户是否允许app使用该权限。根据不同的系统版本,用户在Android 5.1及以下的版本安装时提示申请权限,而在Android 6.0及以上的版本运行时申请权限。

判断你需要哪些权限

一般如果app想要使用非app内部信息,或者有影响设备或其他app的操作时就需要申请权限。例如,如果app需要访问网络,使用手机摄像头,或者开关和关闭Wi-Fi,那么就需要申请相应的权限。以下是一般和危险的权限列表:

Android一般和危险权限列表

你的app只需要申请你直接操作的权限,如果调用其他app的操作或信息则不需要申请权限,因为其他app自己会处理。例如,如果你的app需要访问用户的通讯录,那么你需要申请READ_CONTACTS权限。但是如果你使用intent去访问手机上的其他通讯录app,则不需要申请权限,但是通讯录app就必须要申请READ_CONTACTS权限。

添加权限到Manifest文件

如果你要申请某个权限,需要在app manifest文件中的根节点下创建一个子节点,同时将相应的权限放入子节点中间。例如,你需要申请发送SMS信息的权限,那么你需要在app manifest文件下完成以下代码:

1
2
3
4
5
6
7
8
9
10
11
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.snazzyapp">


<uses-permission android:name="android.permission.SEND_SMS"/>


<application ...>
...
</application>

</manifest>

系统会根据你申请权限的敏感度做相应的操作,如果申请的权限不影响用户的隐私,则系统会自动帮你获取到该权限,如果涉及用户的隐私,那么系统就会询问用户是否允许使用该权限。

运行时申请权限

从Android 6.0(API level 23)开始,申请敏感度高的权限需要在运行时完成,而非安装时,这样可以简化app的安装流程,因为用户不需要在安装和升级app时申请权限。同时也增加了用户对app功能的控制权,例如用户可以控制允许访问摄像头而不允许访问当前地址,用户可以在系统设置中任意控制app的权限。

系统权限被分为了两种类型,一般权限和危险权限:

  • 一般权限不会直接威胁用户的隐私,如果你在app manifest文件中申明一般权限,系统会自动帮你完成申请
  • 危险权限允许app访问用户的私密信息,如果你在app manifest文件中申明危险权限,那么系统将会询问用户是否允许app使用该权限。

在所有的Android版本中,你的app一般都需要申请一般和危险权限,那么不同的系统版本会有不同的处理:

  • 手机系统版本为Android 5.1及以下,或者app的目标SDK为22及以下:如果你在manifest文件中申请了危险权限,那么在安装阶段需要申请这些权限,如果安装时用户拒绝了某些权限,那么系统就不会安装该app
  • 手机系统版本为Android 6.0及以上,或者app的目标SDK为23及以上:如果你在manifest文件中申请了危险权限,那么在app运行到需要这些权限时就必须显式地申请该权限,如果安装时用户拒绝了某些权限,那么app就无法使用该权限

从Android 6.0(API level 23)开始,用户可以系统设置里任意控制app的权限

检查权限

如果你的app需要危险权限,你必须每次在用到该权限时都要检查是否允许使用,用户可以随时打开或者关闭权限,因此即使昨天你可以正常访问手机摄像头,今天你也仍要申请。

为了检查是否拥有某个权限,需要调用ContextCompat.checkSelfPermission()方法,例如,下面一段代码就是检查是否有访问日历权限:

1
2
3
// 假定thisActivity就是当前的activity
int permissionCheck = ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.WRITE_CALENDAR);

如果app有这个权限,该方法返回PackageManager.PERMISSION_GRANTED, 并且app可以操作相应的权限。如果app没有该权限,则该方法返回PERMISSION_DENIED,并且app不得不再次询问用户是否允许拥有该权限。

申请权限

如果你的app在manifest文件中申请一个危险权限,你必须询问用户申请该权限,为此Android提供了几个方法,调用这些方法可以唤起标准的Dialog,且不可自定义。

如果你的app没有拥有某个权限,那么必须调用requestPermissions()中的一个方法。当你要申请某个权限,需要传递一个整型的request值给指定的权限,当用户在对话框中允许或者拒绝后,系统将会返回申请结果,同时返回相同的request值。

下面的代码展示了检查是否拥有访问用户通讯录的权限,并申请该权限:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 此处thisActivity就是当前的activity
if (ContextCompat.checkSelfPermission(thisActivity,
Manifest.permission.READ_CONTACTS)
!= PackageManager.PERMISSION_GRANTED) {

// Should we show an explanation?
if (ActivityCompat.shouldShowRequestPermissionRationale(thisActivity,
Manifest.permission.READ_CONTACTS)) {

// 显示需要该权限的理由,此处等待用户的反馈,当用户看过后在尝试申请权限

} else {

// 此处已经获得了权限,因此无需再解释.

ActivityCompat.requestPermissions(thisActivity,
new String[]{Manifest.permission.READ_CONTACTS},
MY_PERMISSIONS_REQUEST_READ_CONTACTS);

// MY_PERMISSIONS_REQUEST_READ_CONTACTS是自定义的整型常数,
// 回调方法将会据此返回相应的结果。
}
}

当调用equestPermissions()方法时,系统会为用户展示一个标准的对话框,你无法对其进行修改和自定义。如果你需要向用户展示说明信息,那么你应该在调用之前完成。

处理权限申请结果

当你的app申请权限,系统会为用户显示一个对话框,当用户做出判断后,系统就会回调onRequestPermissionsResult()方法,你必须重载该方法以判断用户是否允许使用权限。回调方法将会返回之前自定义的request整型值,例如如果app申请访问通讯录权限,将会有如下回调方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@Override
public void onRequestPermissionsResult(int requestCode,
String permissions[], int[] grantResults)
{

switch (requestCode) {
case MY_PERMISSIONS_REQUEST_READ_CONTACTS: {
// 如果申请被拒绝,那么结果数组将为空。
if (grantResults.length > 0
&& grantResults[0] == PackageManager.PERMISSION_GRANTED) {

// 申请权限通过,你将可以实现具体的业务逻辑

} else {

// 权限被拒绝,相关的功能也要有所处理。
}
return;
}

// other 'case' lines to check for other
// permissions this app might request
}
}

系统提供的对话框展示了你需要访问的权限组,而不是一个个具体的权限。例如,如果你申请READ_CONTACTS权限,系统对话框只会说app需要访问设备的通讯录。用户只需要同意其中的一个权限即可。如果app需要访问权限组的其他权限(在app manifest文件列举),系统会自动帮你完成申请。申请成功的结果是系统通过调用app重写的onRequestPermissionsResult()回调方法并且返回PERMISSION_GRANTED。

你仍然需要明确地请求每一个你需要的权限,即使用户已经在权限组中同意了其他权限。此外,权限组中的权限在未来的Android版本中有可能会改变。你的代码不应该依赖所需要的权限是否在同一个权限组。

例如,假定你在app manifest文件中列举了READ_CONTACTS和WRITE_CONTACTS,如果你申请READ_CONTACTS并且用户同意允许使用该权限,那么当你需要WRITE_CONTACTS权限时,系统就不会弹框提示用户选择而是直接同意使用。

如果用户拒绝了你的权限申请,你的app应该做好足够的应对。例如,你需要显示一个对话框解释某个操作是因为没有权限所以才无法正常使用。

如果你申请的权限已经被用户拒绝,在申请权限之前,你可以先调用ActivityCompat#shouldShowRequestPermissionRationale(Activity,

  • String)方法展示额外的说明文案。

当系统询问用户是否允许使用某个权限时,用户可以选择勾选不再提示,那么,当app之后再次调用requestPermissions()申请权限时,系统将会直接拒绝申请。系统将会回调onRequestPermissionsResult()方法并返回PERMISSION_DENIED,这种情况同样适用用户已经明确拒绝过请求的情况。也就是说当你调用requestPermissions()时,你不应该假定和用户的交互一定会出现。

下一篇文章将会介绍Android权限最佳实践以及github热门权限SDK的代码分析。

成功远不止是努力——《褚时健传》读后感

衡量一个人是否成功,要看他在谷底反弹的高度。 —— 巴顿将军

尽管最近工作比较忙,但还是抽时间看完了周桦的《褚时健传》,这本书详尽地讲述了褚时健传奇的一生,从1927年褚时健降生,到2015年该书完稿,横跨88年。期间在中国大地上发生了太多太多的事情,让每一个身在其中的人都不能完全置身事外,只能被历史的洪流裹胁着前进,这或许是每一代人都逃脱不了的宿命。

他的人生几经起落:早年丧父,辍学、烤酒、种地,以此帮母亲谋生;青年,重新求学却遭遇战争,扛过枪打过仗;解放后没能逃脱“右派”的命运,却能埋头搞生产,所在糖厂成为当地条件最好的地方;年过半百,接手玉溪卷烟厂,将其打造成亚洲第一、世界第五的集团企业,褚时健也成为“亚洲烟王”。然而,巅峰跌落……

当他再次进入公众视野时,已成橙王,他种植的褚橙凭借口感香甜成为一橙难求的稀缺水果。拜访、学习甚至膜拜的创业者和企业家蜂拥哀牢山……

我其实不是想复述褚时健的一生,因为书中有很多精彩的细节,感兴趣的读者可以自行去阅读。这本书给我的最大思考是在建国后到改革开放的一二十年中,面对中国纷繁复杂的政治经济环境,褚时健何以做出那么多令同时代人惊叹的成就?有人说他努力,但我觉得不全是,自古以来,华夏大地上就不乏勤奋努力之人,但是能像褚时健这样连续取得世人惊叹成就的确实乏善可陈,所以褚时健身上到底有着哪些世人所不拥有的性格呢?

这段时间也在看李笑来推荐的《原则》,作者是雷.达里奥,书中他提到了自己信奉的最基本原则就是精准理解现实,然后在联想到褚时健一生的处事原则,真的是一语惊醒梦中人。这句话说起来容易,做起来真的很难,试想即使我们真的能精准理解我们所处的社会环境和现实,又有多少人能提出建设性且可执行的方案,更别说一点一点地付诸行动,但是褚时健却做到了。

烤酒

在父亲去世后,褚时健为了帮助母亲减轻负担,主动辍学在家烤酒。烤酒在玉溪当地有着悠久的历史,各项工艺流程都很成熟,只要按着前人的经验基本上没有太大问题,但是想提高单位重量苞谷的出酒量却是很难。传统烤酒分几个步骤:泡苞谷、蒸包谷、放酒曲发酵、蒸馏、接酒。蒸苞谷需要保证锅里一直有水,灶里一直有柴火,稍出差错就前功尽弃,因此需要人守着灶火,褚时健为了不通宵但又防止睡过头,他就仔细估算一锅水从开始煮烧到烧干的时间,然后就靠墙浅睡,灶上稍有动静立马就醒,从此褚时健就形成了自己的习惯,而他也从来没有烤糊过苞谷,这个连村里同样烤酒的大人都做不到。发酵的过程最重要,出酒量和酒精度的高低全在这一环节上,三伯家的师傅教了褚时健怎么发酵,但在褚时健看来,这事可以做得更好。师傅提醒他发酵时要关门,他琢磨着这应该是温度的问题,他发现夏天和冬天的发酵情况不一样,靠近灶边的发酵箱发酵程度总是好一些,于是他在远离灶台的发酵箱旁边放上装有柴火的破铁盆,结果出酒量立马得到提升。在烤酒的过程中褚时健随时记录原料重量,所需柴火重量,然后核算各种成本和利润。到集市上售酒更是时刻调整酒量,而且每次都保证少那么一点点,营造一种大家抢着买酒的氛围,在酒所剩无几的时候,褚时健会根据酒的成色及时下调价格,保证不存货,同时让利顾客。

啰啰嗦嗦说了这么多烤酒的事情,其实是想表达褚时健在对待烤酒这件小事上的认真,他在实践中对整个烤酒工艺有很深的理解,每个环节和流程都会通过观察总结一点一点地改善和优化,最后累积的效果往往是令人惊叹的。

经营玉溪卷烟厂

文革后褚时健被调任到玉溪卷烟厂担任厂长,受计划经济和”大锅饭”的影响,当时的民众普遍磨洋工,偌大的工厂每年都亏损,为了调动工人的生产积极性,褚时健对消极怠工的中层领导毫无情面,从各个方面鼓励那些愿意脚踏实地做事的人。基建科长不服从褚时健为职工建居民房的命令,立马撤职,给大营街有口皆碑的建筑队。褚时健通过串货的方式给职工提供了大量的猪肉、家电等生活物资,烟厂职工的生活水平得到了很大的提升,这是历任厂长许诺但惟有褚时健做到的善举。为了将工厂扭亏为盈,褚时健严格把控质量,不惜重金引进德国先进的生成设备,而当时的外汇指标都是政府严格控制的,褚时健便以利税游说政府,并辅以和广东串货的方式,实现改善生产设备的目的。除了设备,原料也是烟质量好坏的关键,但是全国的烟叶种植和销售都是国家烟草部门控制并分配的,因此褚时健想要染指烟草种植,其阻力可想而知。但是褚时健并不放弃,而是找一些种植烟草的小地方,和当地政府谈判,而当地政府也想发展本地的经济,因此双方一拍即合。褚时健加大对烟农的补贴,同时严格控制烟叶的种植,并请农业科技人员指导烟农种植。而在工厂内部,实行奖惩制度,这在当时大锅饭的环境下绝对是破天荒的制度。通过这一系列制度、设备、原料、技术的改革,玉溪卷烟厂生成的红塔山和红梅市场占有率连连提升,而且零售价也在不断提升,利润自然逐年递增。

我们有的时候做事会抱怨阻力多,资源少,可是相比褚时健当时所处的社会,我想我们还是幸运的多吧,褚时健不是只顾生产的政治白痴,他深谙政治,但是却又和政治保持一定的距离,为了推动玉溪卷烟厂的发展,他脚踏实地,遇到问题解决问题,从不抱怨,在很多人看来不可能的事情,在他手里都一一实现了。

褚橙

经历过牢狱之灾和丧女之痛,高龄的褚时健依然保持着做事的热情和认真,出狱后他在哀牢山承包了2400亩土地开始种植冰糖橙,周遭人都不理解,一个年逾古稀的老人为什么还要大规模种植果树,但是褚时健认真的做事态度还是逐渐打消了所有人的疑虑。从最开始的使用机械化翻动土地,到工业化的种植果树,褚时健一点一滴地经营着这片果园。身为果树种植门外汉的褚时健,通过阅读农业种植书籍和大量的一线实践,其种植的橙子不断地受到市场的欢迎。为了改善口感,褚时健创造性地在有机肥中加入烟梗,弥补了之前肥料中缺失的钾元素。为了提高产果率,褚时健硬是要求农户将每亩140颗果树砍至80颗,这在那些有经验的农户看来简直就是不可思议。为了保证果树生长所需的水,褚时健不惜投入重金从附近的河流铺设水管,他总是说水源投入再多都是值得的。为了保证冰糖橙的利润,褚时健拒绝到菜市场售卖,在其外孙女和外孙女婿的努力下,他们的果园公司在全国铺设了大量的直销网点,随着这几年电子商务的发展,褚橙作为生鲜爆品在北上广等大城市迅速打开市场,一开始就出现供不应求的局面。褚时健说,随着云南开始更多人种植冰糖橙,褚橙唯一保持市场占有率的条件就是质量,也就是橙子的口感,因此他以及手下的员工严格把控橙子的质量。

21世纪初的十几年是互联网和房地产发展的井喷时期,大量的创业公司应运而生,与之相随的是泡沫和虚假也层出不穷,真正沉下心来做实事的少之又少,而褚橙的出现给了国人更多的思考,褚橙的亩产量和质量是澳大利亚和南美洲都望尘莫及的。前段时间国人去日本买马桶盖的情景是不是可以从褚橙身上得到一些借鉴呢。

结语

吴晓波在《大败局》中讲述了众多上个世纪八九十年代的企业和企业家失败的案例,其中国企和乡镇企业产权改革是众多失败案例中最重要的失败原因,而褚时健也未能幸免,这是他们这代人的悲剧,褚时健常说改革是需要牺牲的,这是何等的豁达与平静。人们常说当代的中国缺少工匠精神,试问褚时健种植冰糖橙谁敢说没有工匠精神吗?但是像褚时健这样认真做事情的恐怕确实不多。之前吴晓波频道有一期节目谈到了工匠精神,他认为,短缺经济很难造就工匠精神,因为处于物质短缺的社会,人们不会为工匠精神买单,这点我很赞同,中国在经历了30年的高速发展,已经从短缺经济慢慢过渡到富余经济,我相信未来中国的工匠精神一定会重新回来。

每个渴望成长的现代年轻人都在努力拼搏,褚时健身上的认真执着精神或许可以给我们带来一些启发,我们需要精确理解我们的社会和环境,更要有勇气和耐心提出我们的建议和方案,并不断的践行。成功没有捷径可寻,褚时健在过往的成功中所付出的艰辛和汗水是我们身在大城市按时上下班的年轻人所无法想象的,但是他老人家对待事情的认真是我们可以学习的。成功远不止努力那么简单,方向、细节、勇气等等都是需要的,这就是我从褚时健身上学到的。