Retrofit是Square公司基于restful风格推出的网络框架封装,截止目前github已经有了37.2kstart,可见他的受欢迎程度非常高,Retrofit基于Okhttp封装,具有非常强大的解耦特点,高度的灵活解耦导致使用起来不够简洁,下面对Retrofit进行一次二次的封装,在使用上更加简洁。封装之后具有一下特点:

  • 支持reftofit的单例模式配置,一次配置多处使用。
  • 支持动态切换baseUrl不影响原有的baseUrl。
  • 支持通用格式的网络请求,返回对象或者字符串。
  • 支持快捷的请求方式返回字符串格式的数据。
  • 支持大文件下载和进度回调以及动态取消。
  • 支持单文件和多文件上传。

一、导入依赖库

implementation 'com.squareup.retrofit2:retrofit:2.9.0'
implementation 'com.squareup.retrofit2:converter-gson:2.9.0'
implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
implementation 'com.google.code.gson:gson:2.8.6'

二、reftofit的创建

Retrofit的创建方式如下:

 Retrofit retrofit = new Retrofit.Builder().baseUrl(baseUrl).addConverterFactory(GsonConverterFactory.create()).build();

1、addConverterFactory

用于添加Retrofit返回数据的支持格式,通常有两个返回字符串格式或者返回一个对象,这块需要添加对应的依赖:

  • 返回字符串:.addConverterFactory(GsonConverterFactory.create())
  • 返回对象:.addConverterFactory(ScalarsConverterFactory.create())

二者不能同时添加,同时添加只有后添加的生效,需要依据需求或者项目配置。

2、baseUrl

用于设置请求的协议IP和端口,在配置时候需要确定,但是有些时候有些接口baseUrl会变化,这时候需要单独配置了,原始的retrofit不支持动态配置,目前市面上的解决方案是两种,一种是重新创建一个Retrofit设置baseUrl,另一种是直接在拦截器里面处理,通过Api接口动态配置header,获取对应的baseUrl,使其生效,如果没有配置就使用默认的。

3、Retrofit对象的封装

基于上面两点对retrofit进行封装,命名为RetrofitManager,该管理类中有一个统一的baseUrl配置,需要提前配置好,在请求中直接使用该默认的配置,如果有需要切换的baseUrl,额外创建一个Retrofit,为了避免每次调用创建,把他放在一个Map集合中,每次获取Retrofit根据baseUrl从集合中读取,如果没有则创建,这样可以避免频繁的创建不同baseUrl的Retrofit。另外换需要对于返回数据是String或者为对象的Retrofit的支持。

    /*** 返回全局对象** 解析为对象*/public Retrofit getRetrofit() {checkBaseUrl();Retrofit retrofit = retrofitMap.get(baseUrl);if (retrofit == null) {retrofit = new Retrofit.Builder().baseUrl(baseUrl).addConverterFactory(GsonConverterFactory.create()).build();retrofitMap.put(baseUrl, retrofit);}return retrofit;}/*** 返回局部对象** 解析为对象*/public Retrofit getRetrofit(String baseUrl) {checkBaseUrl(baseUrl);Retrofit retrofit = retrofitMap.get(baseUrl);if (retrofit == null) {retrofit = new Retrofit.Builder().baseUrl(baseUrl).addConverterFactory(GsonConverterFactory.create()).build();retrofitMap.put(baseUrl, retrofit);}return retrofit;}/***  返回全局对象**  解析为字符串*/public Retrofit getStringRetrofit() {checkBaseUrl();Retrofit retrofit = retrofitMap.get(baseUrl);if (retrofit == null) {retrofit = new Retrofit.Builder().baseUrl(baseUrl).addConverterFactory(ScalarsConverterFactory.create()).build();retrofitMap.put(baseUrl, retrofit);}return retrofit;}/*** 返回局部对象** 解析为字符串*/public Retrofit getStringRetrofit(String baseUrl) {checkBaseUrl();Retrofit retrofit = retrofitMap.get(baseUrl);if (retrofit == null) {retrofit = new Retrofit.Builder().baseUrl(baseUrl).addConverterFactory(ScalarsConverterFactory.create()).build();retrofitMap.put(baseUrl, retrofit);}return retrofit;}/*** 设置全局的url* @param baseUrl* @return*/public  RetrofitManager setBaseUrl(String baseUrl) {RetrofitManager.baseUrl = baseUrl;return this;}

这样在使用中如果有全局统一的baseUrl,则设置:

  • Retrofit retrofit = RetrofitManager.getInstance().setBaseUrl("http://127.0.0.1:3000/").getStringRetrofit();
  • Retrofit retrofit = RetrofitManager.getInstance().setBaseUrl("http://127.0.0.1:3000/").getRetrofit();

如果是需要设置其他的baseUrl,则设置:

  • Retrofit retrofit = RetrofitManager.getInstance().getRetrofit("http://www.weather.com.cn/");
  • Retrofit retrofit = RetrofitManager.getInstance().getStringRetrofit("http://www.weather.com.cn/");

三、Retrofit的通用的请求

    LoginApi loginApi = retrofit.create(LoginApi.class);Call<ResponseBody> call = loginApi.getName();call.enqueue(new Callback<ResponseBody>() {@Overridepublic void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {}@Overridepublic void onFailure(Call<ResponseBody> call, Throwable t) {}});

Retrofit的请求如上,支持get/post/delete/patch/options等方式的请求,配置方式灵活多变,高度解耦,为了兼容各个请求和内容回调,和请求参数配置的解耦,既支持返回为String有支持返回为对象Call这个对象不能直接处理,需要用户配置,否则就是去了通用性,同时能够兼顾请求的简洁性,仅对回调做了处理,回调如下:

 public interface ResponseCallback<T> {void onSuccess(T t);void onFailure(Throwable t);}

封装为如下,T表示用户希望返回的数据类型和Call中的泛型保持一致:

 public static <T> void executeAsync(Call<T> call, ResponseCallback<T> callback) {call.enqueue(new Callback<T>() {@Overridepublic void onResponse(Call<T> call, Response<T> response) {if (response.isSuccessful() && callback != null) {callback.onSuccess(response.body());}}@Overridepublic void onFailure(Call<T> call, Throwable t) {if (callback != null) {callback.onFailure(t);}}});}

使用封装后的内容如下:

     LoginApi loginApi = retrofit.create(LoginApi.class);Call<ResponseBody> call = loginApi.getName();HttpUtils.executeAsync(call, new ResponseCallback<ResponseBody>() {@Overridepublic void onSuccess(ResponseBody responseBody) {}@Overridepublic void onFailure(Throwable t) {}});

四、get和post请求

除了通用的网络请求外,很多时候是get或者post请求,需要返回的格式是字符串的数据,这时候需要对通用的请求进一步封装,返回String格式:

 public interface StringCallback extends ResponseCallback<String> {}

对于get和post请求,分为两大类,没有参数的请求和有参数的请求,进一步缩小了请求范围。统一一个StringApi:

 public interface StringApi {//带参数的通用get请求@GET()Call<String> executeGet(@Url String url, @QueryMap Map<String, String> maps);//不带参数的通用get请求@GET()Call<String> executeGet(@Url String url);//不带参数的通用post请求@POST()Call<String> executePost( @Url String url);//带参数的通用post请求@POST()@FormUrlEncodedCall<String> executePost( @Url String url, @FieldMap Map<String, String> maps);}

对于call.enqueue的结果统一出来回调

 public class RetrofitStringCallback implements Callback<String> {private StringCallback callback;public RetrofitStringCallback(StringCallback callback) {this.callback = callback;}@Overridepublic void onResponse(Call<String> call, Response<String> response) {if (response.isSuccessful()) {callback.onSuccess(response.body());} else {try {Throwable throwable = new Throwable(response.errorBody().string());callback.onFailure(throwable);} catch (IOException e) {e.printStackTrace();}}}@Overridepublic void onFailure(Call<String> call, Throwable t) {callback.onFailure(t);}}

具体请求封装如下:

 /*** get:无参数*/public static void get(String url, StringCallback callback) {Retrofit retrofit = RetrofitManager.getInstance().getStringRetrofit();get(retrofit, url, callback);}public static void get(Retrofit retrofit, String url, StringCallback callback) {StringApi baseApi = retrofit.create(StringApi.class);Call<String> call = baseApi.executeGet(url);call.enqueue(new RetrofitStringCallback(callback));}/*** get:有参数*/public static void get(String url, Map<String, String> map, StringCallback callback) {Retrofit retrofit = RetrofitManager.getInstance().getStringRetrofit();get(retrofit, url, map, callback);}public static void get(Retrofit retrofit, String url, Map<String, String> map, StringCallback callback) {StringApi baseApi = retrofit.create(StringApi.class);Call<String> call = baseApi.executeGet(url, map);call.enqueue(new RetrofitStringCallback(callback));}/*** post:无参数*/public static void post(String url, StringCallback callback) {Retrofit retrofit = RetrofitManager.getInstance().getStringRetrofit();post(retrofit, url, callback);}public static void post(Retrofit retrofit, String url, StringCallback callback) {StringApi baseApi = retrofit.create(StringApi.class);Call<String> call = baseApi.executePost(url);call.enqueue(new RetrofitStringCallback(callback));}/*** post有参数*/public static void post(String url, Map<String, String> map, StringCallback callback) {Retrofit retrofit = RetrofitManager.getInstance().getStringRetrofit();post(retrofit, url, map, callback);}public static void post(Retrofit retrofit, String url, Map<String, String> map, StringCallback callback) {StringApi baseApi = retrofit.create(StringApi.class);Call<String> call = baseApi.executePost(url, map);call.enqueue(new RetrofitStringCallback(callback));}

这样,如果是get或者post请求就变的很简单:

    get("aoi/weather", new StringCallback() {@Overridepublic void onSuccess(String s) {Log.d(TAG, "onSuccess: " + s);}@Overridepublic void onFailure(Throwable t) {Log.d(TAG, "onSuccess: " + t);}});

四、文件下载

1、回调定义

public interface DownLoadListener {void onStart();void progress(int progress, float currentSize, float totalSize);void onFinish(String path);void onFailure(String msg);void onCancel(String tag);}

实现类:

public abstract class DownLoadCallback implements DownLoadListener {private String folder;private String fileName;private String tag;public DownLoadCallback(String folder, String fileName) {this.folder = folder;this.fileName = fileName;}public DownLoadCallback(String folder, String fileName, String tag) {this.folder = folder;this.fileName = fileName;this.tag = tag;}public void onStart() {}public void progress(int progress, float currentSize, float totalSize) {}@Overridepublic void onCancel(String tag) {}public String getFolder() {return folder;}public String getFileName() {return fileName;}public String getTag() {return tag;}
}

tag用于标记那是那个请求,用于请求的取消和请求的回调标记,如果没有这个需求,则可以不用。

2、DownLoadApi

和get/post的请求一样,文件下载也定义两个接口,一个是有参数的下载,另一个是无参数的下载,返回格式必须是ResponseBody,用于处理下载结果,如果是大文件的话,需要加上@Streaming,因为Retrofit默认下载中回一次性把内容下载到内存中才回调结果,加上@Streaming之后会以流的方式读取写入,否则进度回调不到。

 public interface DownLoadApi {@Streaming@GET()Call<ResponseBody> downLoadApi(@Url String url);@Streaming@GET()Call<ResponseBody> downLoadApi( @Url String url, @QueryMap Map<String, String> maps);}

3、封装下载

文件下载是比较耗时间的任务,需要放在子线程中执行,最好放在个线程池executor中,下面直接看下载入口:

    /*** 文件下载*/public void executeDownLoadAsync(Retrofit retrofit, String downloadUrl, DownLoadCallback callback) {DownLoadApi baseApi = retrofit.create(DownLoadApi.class);Call<ResponseBody> call = baseApi.downLoadApi(downloadUrl);setDownLoadTag(callback.getTag(), call);executor.execute(new DownloadRunnable(call, callback, executor, callMap));}public void executeDownLoadAsync(Retrofit retrofit, String downloadUrl, Map<String, String> map, DownLoadCallback callback) {DownLoadApi baseApi = retrofit.create(DownLoadApi.class);Call<ResponseBody> call = baseApi.downLoadApi(downloadUrl, map);setDownLoadTag(callback.getTag(), call);executor.execute(new DownloadRunnable(call, callback, executor, callMap));}/*** 设置取消下载回调*/private void setDownLoadTag(String tag, Call<ResponseBody> call) {if (TextUtils.isEmpty(tag)) {return;}Call put = callMap.get(tag);if (put == null) {callMap.put(tag, call);}}

4、下载任务

具体的下载任务放在了DownloadRunnable里面,实现了Runnable接口,run方法中直接发起请求:

 call.enqueue(new Callback<ResponseBody>() {@Overridepublic void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {if (callback == null) {return;}if (response.isSuccessful()) {callback.onStart();executor.execute(() -> writeResponseBodyToDisk(response.body()));} else {callback.onFailure(response.message());}}@Overridepublic void onFailure(Call<ResponseBody> call, Throwable t) {callback.onFailure(t.getMessage());}});

因为回调是在主线程的,所以需要开辟一块子线程进行磁盘的写入,调用executor.execute只写入:

     private void writeResponseBodyToDisk(ResponseBody body) {try {File file = new File(callback.getFolder() + File.separator + callback.getFileName());if (!file.getParentFile().exists()) {file.getParentFile().mkdir();}if (file.exists()) {file.delete();}if (!file.createNewFile()) {handler.post(() -> callback.onFailure("file path is not exit"));return;}InputStream inputStream = null;OutputStream outputStream = null;ProgerssRun progerssRun = new ProgerssRun(callback, file.getAbsolutePath());try {byte[] fileReader = new byte[1024 * 4];long fileSize = body.contentLength();long downLoadSize = 0;inputStream = body.byteStream();outputStream = new FileOutputStream(file);handler.postDelayed(progerssRun, 300);while (true) {int read = inputStream.read(fileReader);if (read == -1) {break;}outputStream.write(fileReader, 0, read);downLoadSize += read;//回调结果保留几位小数DecimalFormat df = new DecimalFormat("0.000");df.setRoundingMode(RoundingMode.HALF_UP);//将K转为M,保留3位小数返回float currentSize = Float.parseFloat(df.format(downLoadSize / (1024 * 1024f)));float totalSize = Float.parseFloat(df.format(fileSize / (1024 * 1024f)));int progress = (int) (downLoadSize * 100 / fileSize);progerssRun.setProgress(progress, currentSize, totalSize);}outputStream.flush();} catch (IOException e) {handler.post(() -> {handler.removeCallbacks(progerssRun);callback.onCancel(callback.getTag());});} finally {if (inputStream != null) {inputStream.close();}if (outputStream != null) {outputStream.close();}if (!TextUtils.isEmpty(callback.getTag()) && callMap.get(callback.getTag()) != null) {callMap.remove(callback.getTag());}}} catch (IOException e) {handler.post(() -> {callback.onFailure(e.getMessage());});}}

上面的handler是主线程的handler,将结果回调给主线程,便于数据的设置,最后关于进度回调的类:

   public static class ProgerssRun implements Runnable {private int progress;private float currentSize;private float totalSize;private DownLoadCallback callback;private String filePath;public ProgerssRun(DownLoadCallback callback, String filePath) {this.callback = callback;this.filePath = filePath;}public void setProgress(int progress, float currentSize, float totalSize) {this.progress = progress;this.currentSize = currentSize;this.totalSize = totalSize;}@Overridepublic void run() {if (currentSize == totalSize) {callback.progress(progress, currentSize, totalSize);handler.removeCallbacks(this);handler.post(() -> callback.onFinish(filePath));} else {callback.progress(progress, currentSize, totalSize);//延迟300毫秒回调handler.postDelayed(this, 300);}}}

这样就完成了文件的下载,用法如下:

    downLoadFile(retrofit, url, new DownLoadCallback(folder, fileFile, tag) {@Overridepublic void onFinish(String path) {}@Overridepublic void onFailure(String msg) {}@Overridepublic void progress(int progress, float currentSize, float totalSize) {}@Overridepublic void onCancel(String tag) {}});

5、取消下载

在请求的时候设置有一个tag,如果需要取消下载的需求,根据tag进行取消,如果不需要请求可以不用设置:

 /*** 取消指定的请求** @param tag*/public void cancel(String tag) {if (TextUtils.isEmpty(tag)) {return;}Call call = callMap.get(tag);if (call != null) {callMap.remove(tag);if (!call.isCanceled()) {call.cancel();}}}/*** 取消所有的请求*/public void cancelAll() {Iterator it = callMap.entrySet().iterator();while (it.hasNext()) {Map.Entry entry = (Map.Entry) it.next();Call<ResponseBody> call = (Call<ResponseBody>) entry.getValue();call.cancel();}}

六、文件上传

总结了一下常用的文件上传的Api格式,具体需要那个,直接调用:

 public interface UpLoadApi {/*** 上传字符串*/@FormUrlEncoded@POST()Call<String> uploadStringApi(@Url String url, @FieldMap Map<String, String> map);@POST()Call<String> uploadStringApi(@Url String url, @Body RequestBody body);/*** 上传单个文件*/@Multipart@POST()Call<String> uploadFileApi(@Url String url, @Part MultipartBody.Part body);/*** 上传多个文件*/@Multipart@POST()Call<String> uploadFilesApi(@Url String url, @PartMap Map<String, RequestBody> map);@Multipart@POST()Call<String> uploadFilesApi(@Url String url, @Part List<MultipartBody.Part> parts);/*** 文件字符串混合传递*/@Multipart@POST()Call<String> uploadFileStringApi(@Url String url, @Part("body") RequestBody body, @Part MultipartBody.Part file);/*** 通用的上传:这种方式上传的时候,不能再接口上加上@Multipart的注解,否者会报错*/@POST()Call<String> uploadApi(@Url String url,@Body RequestBody body);}

1、单个文件上传

    /*** 上传文件单个* * path:文件路径* action:服务器接收的字段,和服务器对应*/public static void upLoadFile(String url, String path, String action, StringCallback callback) {Retrofit retrofit = RetrofitManager.getInstance().getStringRetrofit();upLoadFile(retrofit, url, path, action, callback);}public static void upLoadFile(Retrofit retrofit, String url, String path, String action, StringCallback callback) {UpLoadApi upLoadApi = retrofit.create(UpLoadApi.class);File file = new File(path);RequestBody body = RequestBody.create(MediaType.parse("multipart/form-data"), file);MultipartBody.Part part = MultipartBody.Part.createFormData(action, file.getName(), body);Call<String> call = upLoadApi.uploadFileApi(url, part);call.enqueue(new RetrofitStringCallback(callback));}

使用:

 upLoadFile("upload", "/storage/emulated/0/DCIM/Screenshots/Screenshot_2020_1225_122945.png", "logo", new StringCallback() {@Overridepublic void onSuccess(String s) {Log.d(TAG, "onSuccess: " + s);}@Overridepublic void onFailure(Throwable t) {Log.d(TAG, "onFailure: " + t.getMessage());}});

2、多个文件上传

    /*** 上传多个文件* listPath:文件路径集合* action:服务器接收的字段,和服务器对应*/public static void upLoadFiles(String url, List<String> listPath, String action, StringCallback callback) {Retrofit retrofit = RetrofitManager.getInstance().getStringRetrofit();upLoadFiles(retrofit, url, listPath, action, callback);}public static void upLoadFiles(Retrofit retrofit, String url, List<String> listPath, String action, StringCallback callback) {Map<String, RequestBody> map = new HashMap<>();for (int i = 0; i < listPath.size(); i++) {File file = new File(listPath.get(i));RequestBody body = RequestBody.create(MediaType.parse("image/*"), file);map.put("" + action + "\"; filename=\"" + file.getName(), body);}UpLoadApi upLoadApi = retrofit.create(UpLoadApi.class);Call<String> call = upLoadApi.uploadFilesApi(url, map);call.enqueue(new RetrofitStringCallback(callback));}

使用:

     ArrayList<String> pathList = new ArrayList<>();pathList.add("/storage/emulated/0/DCIM/Screenshots/Screenshot_2020_1225_122945.png");pathList.add("/storage/emulated/0/DCIM/Screenshots/Screenshot_2020_1225_181054.png");HttpUtils.upLoadFiles("form", pathList, "logo", new StringCallback() {@Overridepublic void onSuccess(String s) {Log.d(TAG, "onSuccess: " + s);}@Overridepublic void onFailure(Throwable t) {Log.d(TAG, "onFailure: " + t.getMessage());}});

有什么改进方案,欢迎提出

Retrofit的封装相关推荐

  1. retrofit 简单封装 支持多域名

    retrofit 简单封装 依赖库 implementation 'com.squareup.retrofit2:retrofit:2.4.0' implementation 'com.squareu ...

  2. Android MVP开发模式及Retrofit + RxJava封装

    代码已上传到Github,因为接口都是模拟无法进行测试,明白大概的逻辑就行了! 欢迎浏览我的博客--https://pushy.site 1. MVP模式 1.1 介绍 如果熟悉MVP模式架构的话,对 ...

  3. Android肝帝战纪之网络请求框架封装(Retrofit的封装)

    网络请求框架封装(OkHttp3+Retrofit+loading的封装) Retrofit的Github链接 点此链接到Github AVLoadingIndicatorView的Github链接( ...

  4. retrofit框架学习(二)----retrofit封装

    retrofit 的封装 前言 上一篇文章的链接 http://blog.csdn.net/qq_26296197/article/details/78011188 1 上一篇文章讲到Retrofit ...

  5. 浅谈Retrofit封装-让框架更加简洁易用

    尊重他人的劳动成果,转载请标明出处:http://blog.csdn.net/gengqiquan/article/details/52329259, 本文出自:[gengqiquan的博客] 不知不 ...

  6. MVVM+Retrofit+Kotlin网络框架封装

    上篇文章讲了MVVM入门,网络请求部分非常简单和原始,本篇则是上一篇的进阶,主要讲解如何在vm中使用协程结合Retrofit进行网络框架的封装. GitHub完整版:https://github.co ...

  7. Android OkHttp+Retrofit+Rxjava+Hilt 的网络请求封装

    今天给大家简单的封装一个现在比较流行的网络请求框架 第一步是导入我们所需要的依赖还需要在android {}闭包下添加一个 buildFeatures{viewBinding true } imple ...

  8. Retrofit 最简单的快速入门及自己封装

    简单介绍及官方文档的坑 官方文档 http://square.github.io/retrofit/ Retrofit是Square公司开发的一款针对Android网络请求的框架,Retrofit2底 ...

  9. android retrofit入门,Android开发 retrofit入门讲解

    前言 retrofit基于okhttp封装的网络请求框架,网络请求的工作本质上是 OkHttp 完成,而 retrofit 仅负责网络请求接口的封装.如果你不了解OKhttp建议你还是先了解它在来学习 ...

  10. RxJava+Retrofit+Mvp实现购物车(没有结算页面)

    先给大家展示一下效果图 框架结构: 1.项目框架:MVP,图片加载用Fresco,网络请求用OKhttp+Retrofit实现(自己封装,加单例模式), 2.完成购物车数据添加(如果接口无数据,可用接 ...

最新文章

  1. 艾伟:FCKeditor 配置、扩展
  2. jQuery使用手册
  3. 事件监听 || v-on参数
  4. 浙大计算机专业分数线,被浙江大学提前批的冷门专业录取,后悔没选计算机专业:可惜分数...
  5. 目标检测(R-CNN、Fast R-CNN、Fater R-CNN)
  6. springboot mongo查询固定字段_你真的会用索引么?[Mongo]
  7. 2021 年 6 月程序员工资统计,反作弊算法工程师太可怕了。。
  8. C++/C--unordered_map常见用法详解
  9. redis快照持久化和aof日志持久化
  10. UI设计素材|卡券界面设计
  11. Windows Server 2012之ISCSI目标服务器群集
  12. 【微信开发】-- 发送模板消息
  13. ie8 不支持 position:fixed 的简单解决办法
  14. MYSQL中使索引失效的情況
  15. c语言程序流程图怎么写,C语言课程设计————写下流程图! 谢谢
  16. lcd开机流程图_LCD1602程序代码及显示流程图
  17. python 批量修改文件夹和子文件夹的名称
  18. WMI服务不存在或标记为删除解决方案
  19. Spring+SpringMVC+MongoDB案例
  20. 图片识别——感知哈希算法

热门文章

  1. 百度K站之前兆与解决方案的另类分析
  2. Windows server 2012修改输入法
  3. 解决运行uiautomatorviewer时报错-Djava.ext.dirs=/usr/local/android-sdk-
  4. 爬虫爬取东方财富网的股票走势图
  5. RHEL8.x-RedHat-Podman
  6. oppoa9处理器怎么样_oppo a9是什么处理器
  7. qq显示下线通知什么意思_qq最近登录设备显示其他设备,但我手机没有下线通知,怎么回事...
  8. java中的radix_int radix()
  9. 商城超卖问题的几种解决方案
  10. Wireshark基础知识(一)