Rxjava, RxAndroid, Retrofit 等库的使用

RxJava 是他们的基础组件,RxAndroid, Retrofit, RxCache等等都要用到它。javascript

 

RxJava的基本用法:css

关于 unSubscribe() 的调用问题:html

There is no need to unsubscribe in onCompleted. Take a look at The Observable Contractjava

When an Observable issues an OnError or OnComplete notification to its observers, this ends the subscription. Observers do not need to issue an Unsubscribe notification to end subscriptions that are ended by the Observable in this way.react

On the other hand, you definitely should unsubscribe in onDestroy in order to prevent memory leaks.android

上面的话可知,若是你调用过 subscriber.OnError() 或者 subscriber.OnComplete(),则就不须要调用unSubscribe()了,由于它会自动调用unSubscribe()。 但强烈建议在界面消失的 onDestroy() 里调用 unSubscribe()来手动的将 subscription 注销掉,由于有些时候界面退出时,subscriber.OnError 或者 subscriber.OnComplete还没来得及执行。git

 

 

一个例子

RxJava的强大之处,在于它提供了很是丰富且功能强悍的操做符,经过使用和组合这些操做符,你几乎能完成全部你想要完成的任务,举个例子以下:github

如今有一个需求:app启动时显示一张图片(通常是app的logo),也就是咱们所说的欢迎页,2-3秒后自动跳转到主页面。这不就是几乎每一个app都有的启动页需求吗?几乎不用思考,代码以下:sql

 @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);

        ImageView view = (ImageView) findViewById(R.id.iv_welcome);
        view.setImageResource(R.drawable.welcome);
        Handler handler = new Handler();
        handler.postDelayed(new Runnable() {
            @Override
            public void run() {
                startActivity(new Intent(WelcomeActivity.this, MainActivity.class));
                finish();
            }
        },2000);
    }

使用RxJava的代码实现以下:json

protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_welcome);

        ImageView view = (ImageView) findViewById(R.id.iv_welcome);
        view.setImageResource(R.drawable.welcome);
        Observable.timer(2, TimeUnit.SECONDS, AndroidSchedulers.mainThread()).map(l->{
            startActivity(new Intent(this, MainActivity.class));
            finish();
            return null;
        }).subscribe();
    }

操做符分类

经过上面的例子,你们应该看到了RxJava操做符的威力,下面我按类别把经常使用操做符分别介绍,其实不少内容都是来自于ReactiveX的官方网站,英文比较好的朋友能够参考(http://reactivex.io/)。
按照官方的分类,操做符大体分为如下几种:

  • Creating            Observables(Observable的建立操做符),好比:Observable.create()、Observable.just()、Observable.from()等等;
  • Transforming      Observables(Observable的转换操做符),好比:observable.map()、observable.flatMap()、observable.buffer()等等;
  • Filtering         Observables(Observable的过滤操做符),好比:observable.filter()、observable.sample()、observable.take()等等;
  • Combining         Observables(Observable的组合操做符),好比:observable.join()、observable.merge()、observable.combineLatest()等等;
  • Error Handling Operators(Observable的错误处理操做符),好比:observable.onErrorResumeNext()、observable.retry()等等;
  • Observable Utility Operators(Observable的功能性操做符),好比:observable.subscribeOn()、observable.observeOn()、observable.delay()等等;
  • Conditional and Boolean Operators(Observable的条件操做符),好比:observable.amb()、observable.contains()、observable.skipUntil()等等;
  • Mathematical and Aggregate Operators(Observable数学运算及聚合操做符),好比:observable.count()、observable.reduce()、observable.concat()等等;
  • 其余如observable.toList()、observable.connect()、observable.publish()等等;

defer操做符

defer操做符是直到有订阅者订阅时,才经过Observable的工厂方法建立Observable并执行,defer操做符可以保证Observable的状态是最新的,其流程实例以下:

下面经过比较defer操做符和just操做符的运行结果做比较:

  i=10;
        Observable justObservable = Observable.just(i);
        i=12;
        Observable deferObservable = Observable.defer(new Func0<Observable<Object>>() {
            @Override
            public Observable<Object> call() {
                return Observable.just(i);
            }
        });
        i=15;

        justObservable.subscribe(new Subscriber() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(Object o) {
                System.out.println("just result:" + o.toString());
            }
        });

        deferObservable.subscribe(new Subscriber() {
            @Override
            public void onCompleted() {

            }

            @Override
            public void onError(Throwable e) {

            }

            @Override
            public void onNext(Object o) {
                System.out.println("defer result:" + o.toString());
            }
        });
   }

其中i是类的成员变量,运行结果以下:
just result:10
defer result:15

能够看到,just操做符是在建立Observable就进行了赋值操做,而defer是在订阅者订阅时才建立Observable,此时才进行真正的赋值操做

timer操做符

timer操做符是建立一串连续的数字,产生这些数字的时间间隔是必定的;这里有两种状况:

一种是隔一段时间产生一个数字,而后就结束,能够理解为延迟产生数字,其流程实例以下:

一种是每隔一段时间就产生一个数字,没有结束符,也就是是能够产生无限个连续的数字,其流程实例以下:

timer操做符默认状况下是运行在一个新线程上的,固然你能够经过传入参数来修改其运行的线程。
下面是调用例子:

//每隔两秒产生一个数字
        Observable.timer(2, 2, TimeUnit.SECONDS).subscribe(new Subscriber<Long>() {
            @Override
            public void onCompleted() {
                System.out.println("Sequence complete.");
            }

            @Override
            public void onError(Throwable e) {
                System.out.println("error:" + e.getMessage());
            }

            @Override
            public void onNext(Long aLong) {
                System.out.println("Next:" + aLong.toString());
            }
        });

运行结果以下:
Next:0
Next:1
Next:2
Next:3
……

interval操做符

interval操做符是每隔一段时间就产生一个数字,这些数字从0开始,一次递增1直至无穷大;interval操做符的实现效果跟上面的timer操做符的第二种情形同样。如下是流程实例:

interval操做符默认状况下是运行在一个新线程上的,固然你能够经过传入参数来修改其运行的线程。

调用例子就不列出了,基本跟上面timer的调用例子同样。

repeat/repeatWhen操做符

repeat操做符是对某一个Observable,重复产生屡次结果,其流程实例以下:

 

 

 

 

 

 

 

Retrofit用法详解

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

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

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

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

    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的示例,并完成相应的配置

    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实例

    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实例完成同步或异步请求

    • 同步请求

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

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

    • 异步请求

      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) { } }); 

2、如何使用

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

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实现请求参数的添 加。

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处完成添加

addInterceptor(new CustomInterceptor())

2. @QueryMap

若是Query参数比较多,那么能够经过@QueryMap方式将全部的参数集成在一个Map统一传递,还以上文中的get请求方法为例

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

调用的时候将全部的参数集合在统一的map中便可

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下完成添加,实例代码以下:

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

最后获得的url地址为

https://api.douban.com/v2/book/search?q=leadership&q=beyond%20feelings 

4. Query非必填

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

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

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

那么获得的url地址为

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

5. @Path

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

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

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

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

此时的url地址为

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

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

Post请求

1. @field

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

@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,那么最后获得的请求体就是

    content=Good+Luck 

    FormUrlEncoded不能用于Get请求

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

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

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

2. @FieldMap

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

@FormUrlEncoded
@POST("book/reviews")
Call<String> addReviews(@FieldMap Map<String, String> fields); 

3. @Body

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

@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用于定义上传方法

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中实现两个工具方法,代码以下:

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); } 

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

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()); } }); 

3、其余必须知道的事项

1. 添加自定义的header

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

  • 静态方法

    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参数也是能够的,写法也很简单

    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来定义静态请求头

    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便可

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

    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

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

同上文提到的CustomInterceptor和RequestInterceptor同样,添加到OkHttpClient建立处便可,完整的示例代码以下:

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,请求体大小,返回值状态以及返回值的大小

    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以及返回值状态码

    <-- 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信息

    <-- 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开头该怎么办呢?别着急,办法很简单,看下面这个例子你就懂了

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方法能够取消请求,前提是该请求尚未执行

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(); } 

4、结语

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

相关文章
相关标签/搜索