零基础快速搭建rxjava框架

基本概念

定义

RxJava 是一个 基于事件流、实现异步操作的库

原理

角色 作用 类比
被观察者(Observable) 产生事件 顾客
观察者(Observer) 接收事件,并给出响应动作 厨房
订阅(Subscribe) 连接 被观察者 & 观察者 服务员
事件(Event) 被观察者 & 观察者 沟通的载体 菜式

即RxJava原理可总结为:被观察者 (Observable) 通过 订阅(Subscribe) 按顺序发送事件 给观察者 (Observer), 观察者(Observer) 按顺序接收事件 & 作出对应的响应动作。具体如下图:

(图片和表格来自Carson_Ho博客)

集成

在build中添加

    implementation 'io.reactivex.rxjava2:rxjava:2.2.0'implementation 'io.reactivex.rxjava2:rxandroid:2.0.0'

最简单的形式

    /*** 最基础的订阅*/private void test1() {Observable.create(new ObservableOnSubscribe<Integer>() {@Overridepublic void subscribe(ObservableEmitter<Integer> emitter) throws Exception {emitter.onNext(1);emitter.onNext(2);emitter.onNext(3);emitter.onComplete();}}).subscribe(new Observer<Integer>() {@Overridepublic void onSubscribe(Disposable d) {Log.d("qin","开始采用subscribe连接");}@Overridepublic void onNext(Integer integer) {Log.d("qin","对Next事件"+integer+"做出响应");}@Overridepublic void onError(Throwable e) {Log.d("qin","对error事件作出响应");}@Overridepublic void onComplete() {Log.d("qin","对onComplete事件做出响应");}});}
  1. 在这个函数中首先通过Observable.create创建了一个被观察者,然后被观察者发送了四个event事件,包含三个onNext和一个onComplete事件。
  2. 之后通过new Observer
  3. 最后通过subscribe将二者连接到了一起。

常用基本操作符

fromArray和fromIterable

作用

直接发送数组(fromArray)和列表(fromIterable)

使用场景

可以实现数组或者列表的便利,可以替代for循环

实例代码:
 Observable.fromArray(new Integer[]{0,1,2,3,4}).subscribe(new Observer<Integer>() {@Overridepublic void onSubscribe(Disposable d) {Log.d("qin","开始采用subscribe连接");}@Overridepublic void onNext(Integer integer) {Log.d("qin","对Next事件"+integer+"做出响应");}@Overridepublic void onError(Throwable e) {Log.d("qin","对error事件作出响应");}@Overridepublic void onComplete() {Log.d("qin","对onComplete事件做出响应");}});ArrayList<Integer> list=new ArrayList();for (int i=0;i<10;i++){list.add(i);}Observable.fromIterable(list).subscribe(new Observer<Integer>() {@Overridepublic void onSubscribe(Disposable d) {Log.d("qin","开始采用subscribe连接");}@Overridepublic void onNext(Integer integer) {Log.d("qin","对Next事件"+integer+"做出响应");}@Overridepublic void onError(Throwable e) {Log.d("qin","对error事件作出响应");}@Overridepublic void onComplete() {Log.d("qin","对onComplete事件做出响应");}});

interval

作用

定时发送事件,类型timer

使用场景

可以替代timer等定时任务,例如每隔一段时间去重新拉取数据

实例代码:
  /**参数1 = 第1次延迟时间;参数2 = 间隔时间数字;参数3 = 时间单位;*/Observable.interval(3,1, TimeUnit.SECONDS).subscribe(new Observer<Long>() {@Overridepublic void onSubscribe(Disposable d) {Log.d("qin","开始采用subscribe连接");}@Overridepublic void onNext(Long integer) {Log.d("qin","对Next事件"+integer+"做出响应");}@Overridepublic void onError(Throwable e) {Log.d("qin","对error事件作出响应");}@Overridepublic void onComplete() {Log.d("qin","对onComplete事件做出响应");}});

map和flatmap

作用

将获取到的事件转换成新的事件(map)或者转换成新的观察者(flatmap)

使用场景
  1. 数据类型转换,例如将罗马数字1234转换成中文数字一二三四。或者从json中取出特定的数据。
  2. 可以将两个观察者串在一起。例如我在第一个接口中获取了用户信息,但是只包含userid用户姓名需要第二个接口去根据userid去查。就可以使用flatmap去实现串联操作
  3. 可以结合上面的interval操作符,实现定时拉取数据
实例代码:
        Observable.create(new ObservableOnSubscribe<Integer>() {@Overridepublic void subscribe(ObservableEmitter<Integer> emitter) throws Exception {emitter.onNext(1);emitter.onNext(2);emitter.onNext(3);}}).map(new Function<Integer, String>() {@Overridepublic String apply(Integer integer) throws Exception {return "使用 Map变换操作符 将事件" + integer + "的参数从 整型" + integer + " 变换成 字符串类型" + integer;}}).subscribe(new Consumer<String>() {@Overridepublic void accept(String s) throws Exception {Log.d("qin", s);}});Observable.create(new ObservableOnSubscribe<Integer>() {@Overridepublic void subscribe(ObservableEmitter<Integer> emitter) throws Exception {emitter.onNext(1);emitter.onNext(2);emitter.onNext(3);}}).flatMap(new Function<Integer, ObservableSource<String>>() {@Overridepublic ObservableSource<String> apply(Integer integer) throws Exception {final List<String> list = new ArrayList<>();for (int i = 0; i < 3; i++) {list.add("我是事件" + integer + "拆分后的子事件" + i);}return Observable.fromIterable(list);}}).subscribe(new Consumer<String>() {@Overridepublic void accept(String s) throws Exception {Log.d("qin", s);}});

zip

作用

合并 多个被观察者(Observable)发送的事件,生成一个新的事件序列(即组合过后的事件序列),并最终发送

使用场景
  1. 两个接口没有关联关系,但是都需要调用。那么就可以使用zip来进行并发操作。例如我拥有用户的id,需要查询用户的姓名(第一个接口)和用户的家庭住址(第二个接口)。按照原来的程序逻辑我们需要先查询用户姓名,成功以后再查询用户地址,最后显示。如果使用zip,我们就可以同时创建两个被观察者一个获取姓名一个获取地址,并发操作,最后再zip中进行合并,在观察者中显示。
实例代码:
  Observable<Integer> observable1 = Observable.create(new ObservableOnSubscribe<Integer>() {@Overridepublic void subscribe(ObservableEmitter<Integer> emitter) throws Exception {Log.d(TAG, "被观察者发送了事件1");emitter.onNext(1);Thread.sleep(1000);Log.d(TAG, "被观察者发送了事件2");emitter.onNext(2);Thread.sleep(1000);Log.d(TAG, "被观察者发送了事件3");emitter.onNext(3);Thread.sleep(1000);emitter.onComplete();}}).subscribeOn(Schedulers.io());Observable<String> observable2 = Observable.create(new ObservableOnSubscribe<String>() {@Overridepublic void subscribe(ObservableEmitter<String> emitter) throws Exception {Log.d(TAG, "被观察者2发送了事件1");emitter.onNext("A");Thread.sleep(1000);Log.d(TAG, "被观察者2发送了事件2");emitter.onNext("B");Thread.sleep(1000);Log.d(TAG, "被观察者2发送了事件3");emitter.onNext("C");Thread.sleep(1000);Log.d(TAG, "被观察者2发送了事件4");emitter.onNext("D");Thread.sleep(1000);emitter.onComplete();}}).subscribeOn(Schedulers.newThread());Observable.zip(observable1, observable2, new BiFunction<Integer, String, String>() {@Overridepublic String apply(Integer integer, String s) throws Exception {return integer + s;}}).subscribe(new Observer<String>() {@Overridepublic void onSubscribe(Disposable d) {Log.d(TAG, "onSubscribe");}@Overridepublic void onNext(String value) {Log.d(TAG, "最终接收到的事件 =  " + value);}@Overridepublic void onError(Throwable e) {Log.d(TAG, "onError");}@Overridepublic void onComplete() {Log.d(TAG, "onComplete");}});

subscribeOn和observeOn

作用

subscribeOn :指定Observable自身在哪个调度器上执行,也就是指定被观察者运行的线程

observeOn :指定一个观察者在哪个调度器上观察这个Observable,也就是指定观察者运行的线程

使用场景
  1. 在程序中,我们经常需要在非主线程进行网络操作,在主线程更新ui,就可以使用这两个操作符
实例代码:
 Observable.create(new ObservableOnSubscribe<Integer>() {@Overridepublic void subscribe(ObservableEmitter<Integer> emitter) throws Exception {Log.d("qin", "运行的线程是" + Thread.currentThread().getName());Thread.sleep(2000);emitter.onNext(1);emitter.onComplete();}}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()).subscribe(new Observer<Integer>() {@Overridepublic void onSubscribe(Disposable d) {}@Overridepublic void onNext(Integer value) {Log.d("qin", "运行的线程是" + Thread.currentThread().getName());Log.d(TAG, "onNext: " + value);}@Overridepublic void onError(Throwable e) {Log.d(TAG, "对Error事件作出响应");}// 接收合并事件后,统一展示@Overridepublic void onComplete() {Log.d(TAG, "获取数据完成");Log.d(TAG, result);}});

compose

作用

可以将一种类型的Observable转换成另一种类型的Observable

使用场景
  1. 可以将一组操作符重用于多个数据流中。例如,因为希望在工作线程中处理数据,然后在主线程中处理结果,所以我会频繁使用subscribeOn()和observeOn(),但是这是非常繁琐的,我们就可以使用compose来处理
实例代码:
//我们可以把上个例子的代码变为Observable.create(new ObservableOnSubscribe<Integer>() {@Overridepublic void subscribe(ObservableEmitter<Integer> emitter) throws Exception {Log.d("qin", "运行的线程是" + Thread.currentThread().getName());Thread.sleep(2000);emitter.onNext(1);emitter.onComplete();}}).compose(rxSchedulerHelper()).subscribe(new Observer<Object>() {@Overridepublic void onSubscribe(Disposable d) {}@Overridepublic void onNext(Object value) {Log.d("qin", "运行的线程是" + Thread.currentThread().getName());Log.d(TAG, "onNext: " + value);}@Overridepublic void onError(Throwable e) {Log.d(TAG, "对Error事件作出响应");}// 接收合并事件后,统一展示@Overridepublic void onComplete() {Log.d(TAG, "获取数据完成");Log.d(TAG, result);}});/*** 统一线程处理* @param <T> 指定的泛型类型* @return ObservableTransformer*/public static <T> ObservableTransformer<T, T> rxSchedulerHelper() {return new ObservableTransformer<T, T>() {@Overridepublic ObservableSource<T> apply(Observable<T> upstream) {return upstream.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());}};}

retrofit和rxjava的结合

依赖包

如果让retrofit支持rxjava的调用需要依赖下面两个包

// 衔接 Retrofit & RxJava
// 此处一定要注意使用RxJava2的版本compile 'com.jakewharton.retrofit:retrofit2-rxjava2-adapter:1.0.0'// 支持Gson解析compile 'com.squareup.retrofit2:converter-gson:2.1.0'

最基本网络访问

首先是新建retrofit,这里封装了一个方法。

    /*** 新建需要进行网络访问retrofit*/public <T> T getRx(Class<T> service) {String baseUrl =  HttpURLConstant.getURL(0);LogUtil.i("provideRetrofit()  retrofit will new Retrofit.Builder() baseUrl:" + baseUrl);retrofit = new Retrofit.Builder() //每次请求都要求新建.baseUrl(baseUrl).addConverterFactory(ScalarsConverterFactory.create()).addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(RxJava2CallAdapterFactory.create()).client(AppModule.provideOkHttpClient()).build();return retrofit.create(service);}

可以看到与不使用rxjava的retrofit相比多了一句" .addCallAdapterFactory(RxJava2CallAdapterFactory.create())" 这句是必须的,否则rxjava会创建报错

接下来我们需要编写retrofit的访问方法

//不使用rxjava的时候retrofit的写法@POST(HttpURLConstant.URL_CHECK_REGISTED)Call<String> checkRegisted(@Body Map<String,Object> jsonString);
//使用rxjava的时候retrofit的写法@POST(HttpURLConstant.URL_CHECK_REGISTED)Observable<String> checkObservableRegisted(@Body Map<String,Object> jsonString);

很明显就可以发现,二者最大的不同就是返回的类型由call变成了Observable。这样联系上面的知识,我们就可以知道,我们获取了retrofit的observable之后,就可以去订阅,获取访问结果.

所以,最后我们获取到retrofit的Observable的完整的方法是

    /*** 校验是否已注册** @param phoneNum* @param* @return*/public Observable<String> checkRegistedObservableString(String phoneNum) {Map<String, Object> obj = new HashMap<>();if (!TextUtils.isEmpty(phoneNum)) {obj.put("telephone", phoneNum);}Observable observable = getRx(HttpTalkForRetrofit.UserApi.class).checkRegistedObservableString(obj);return observable;}

然后我们在调用的地方这样写

 new HttpManagerUser().checkRegistedObservableString(phone).compose(RxUtils.rxSchedulerHelper()).subscribe(new ResourceObserver<String>() {@Overridepublic void onNext(String value) {Log.d("qin","value"+value);}@Overridepublic void onError(Throwable e) {e.printStackTrace();}@Overridepublic void onComplete() {}});

这样我们就可以在onNext中获取访问的结果,如果访问出错那么就会onError的回调。这样我们就完成了一个最基本的网络访问的请求。

对网络返回结果进行过滤

我们在网络请求的时候经常会出现,服务端返回的数据虽然走的是正确的接口,但是里面的数据是没有的。所以我们希望滤掉这样的信息,只保留获取成功的数据,在我们的项目中也就是state=0的数据。

如果要实现这样的功能,那么我们就需要和服务端约定一个基类。例如在我们现在的工程中基类如下

public class HttpResponseBase<T> {private int status;private String description;private int total;private T data;/**这里是get,set方法,就不列出来了**/
}

然后在例子中,data的类是

public class CheckUserExistsBean {/*** registered : true*/private boolean registered;public boolean isRegistered() {return registered;}public void setRegistered(boolean registered) {this.registered = registered;}
}

接下来,我们就可以改造之前的获取retrofit的Observable的方法。由string改成具体的bean。

    /*** 校验是否已注册** @param phoneNum* @param* @return*/public Observable<HttpResponseBase<CheckUserExistsBean>> checkObservableRegisted(String phoneNum) {Map<String, Object> obj = new HashMap<>();if (!TextUtils.isEmpty(phoneNum)) {obj.put("telephone", phoneNum);}Observable observable = getRx(HttpTalkForRetrofit.UserApi.class).checkObservableRegisted(obj);return observable;}@POST(HttpURLConstant.URL_CHECK_REGISTED)Observable<HttpResponseBase<CheckUserExistsBean>> checkObservableRegisted(@Body Map<String,Object> jsonString);

这样,只要接口返回的数据正确,我们就可以直接获取到对应bean。而不是string。接下来,我们只需要在最后的观察者收到数据直接拦截state不为0的数据即可,这里我们就可以使用前面讲过flatmap方法。所以调用接口方法可以改成这样

  new HttpManagerUser().checkObservableRegisted(phone).compose(RxUtils.rxSchedulerHelper()).flatMap(new Function<HttpResponseBase<CheckUserExistsBean>, ObservableSource<CheckUserExistsBean>>() {@Overridepublic ObservableSource<CheckUserExistsBean> apply(HttpResponseBase<CheckUserExistsBean> httpResponseBase) throws Exception {if(httpResponseBase.getStatus() == 0) {return  Observable.create(new ObservableOnSubscribe<CheckUserExistsBean>() {@Overridepublic void subscribe(ObservableEmitter<CheckUserExistsBean> emitter) throws Exception {try {emitter.onNext(httpResponseBase.getData());emitter.onComplete();} catch (Exception e) {emitter.onError(e);}}});} else {return Observable.error(new ServerException(httpResponseBase.getStatus(),httpResponseBase.getDescription()));}}}).subscribe(new ResourceObserver<CheckUserExistsBean>() {@Overridepublic void onNext(CheckUserExistsBean value) {}@Overridepublic void onError(Throwable e) {}@Overridepublic void onComplete() {}});

但是这样功能虽然实现了,很明显代码很臃肿,而且不能复用,每个接口都需要写这么一大坨。所以这时候compose就派上用处了。我们可以把代码拆分成这样

       new HttpManagerUser().checkObservableRegisted(phone).compose(RxUtils.rxSchedulerHelper()).compose(RxUtils.handleResult()).subscribe(new ResourceObserver<CheckUserExistsBean>() {@Overridepublic void onNext(CheckUserExistsBean value) {}@Overridepublic void onError(Throwable e) {}@Overridepublic void onComplete() {}});//RxUtils中/*** 统一返回结果处理* @param <T> 指定的泛型类型* @return ObservableTransformer*/public static <T> ObservableTransformer<HttpResponseBase<T>, T> handleResult() {ObservableTransformer observableTransformer= new ObservableTransformer<HttpResponseBase<T>, T>() {@Overridepublic Observable apply(Observable<HttpResponseBase<T>> httpResponseObservable) {return httpResponseObservable.flatMap(new Function<HttpResponseBase<T>, Observable<T>>() {@Overridepublic Observable<T> apply(HttpResponseBase<T> httpResponseBase) throws Exception {if(httpResponseBase.getStatus() == 0) {return createData(httpResponseBase.getData());} else {return Observable.error(new ServerException(httpResponseBase.getStatus(),httpResponseBase.getDescription()));}}});}};return observableTransformer;}/*** 得到 Observable* @param <T> 指定的泛型类型* @return Observable*/private static <T> Observable<T> createData(final T t) {return Observable.create(emitter -> {try {emitter.onNext(t);emitter.onComplete();} catch (Exception e) {emitter.onError(e);}});}

这样我们就成功的筛选出了stata为0的数据,并且无论哪个接口只需要添加一句

.compose(RxUtils.handleResult())

就可以实现数据的过滤

对网络访问错误进行处理

同样的,如果网络访问出错,例如网络错误,服务器返回了错误的数据,而且在上面我们把stata不为0的数据也抛到错误中了,如果我们分别在每个错误的回调中处理,这样的处理方法不仅低效,而且臃肿了。所以和上面一样,我们也使用compose来进行统一的处理。把网络访问的错误信息直接转换成用户可以理解的。
在RxUtils中添加错误的处理方法

    /*** 统一返回结果处理* @param <T> 指定的泛型类型* @return ObservableTransformer*/public static <T> ObservableTransformer<T,T> handleError() {ObservableTransformer observableTransformer= new ObservableTransformer<T, T>() {@Overridepublic ObservableSource<T> apply(Observable<T> upstream) {return upstream.onErrorResumeNext(new Function<Throwable, ObservableSource<? extends T>>() {@Overridepublic ObservableSource<? extends T> apply(Throwable throwable) throws Exception {return Observable.error(ExceptionEngine.handleException(throwable));}});}};return observableTransformer;}

新建一个错误处理类ExceptionEngine(这里仅仅用作例子,需要根据实际情况更改)

public class ExceptionEngine {public static final int UNKNOWN = 1000;public static ApiException handleException(Throwable e){ApiException ex;if (e instanceof HttpException){             //HTTP错误org.xutils.ex.HttpException httpException = (org.xutils.ex.HttpException) e;ex = new ApiException(e, httpException.getCode());ex.message=ErrorManager.getToastErrorMsg(httpException.getCode());return ex;} else if (e instanceof ServerException){    //服务器返回的错误ServerException resultException = (ServerException) e;ex = new ApiException(resultException, resultException.code);ex.message = ErrorManager.getToastErrorMsg(resultException.code);return ex;} else if (e instanceof JsonParseException|| e instanceof JSONException|| e instanceof ParseException){ex = new ApiException(e, HttpListener.ERROR_EXCEPTION);
//            ex.message = "解析错误";            //均视为解析错误ex.message = ErrorManager.getToastErrorMsg(HttpListener.ERROR_EXCEPTION);return ex;}else if(e instanceof ConnectException){ex = new ApiException(e, HttpListener.ERROR_GATEWAY_TIMEOUT);
//            ex.message = "连接失败";  //均视为网络错误ex.message = ErrorManager.getToastErrorMsg(HttpListener.ERROR_GATEWAY_TIMEOUT);return ex;}else {ex = new ApiException(e, UNKNOWN);
//            ex.message = "未知错误";          //未知错误ex.message = ErrorManager.getToastErrorMsg(UNKNOWN);return ex;}}
}

同时新建一种错误类型ApiException

public class ApiException extends Exception {public int code;public String message;public ApiException(Throwable throwable, int code) {super(throwable);this.code = code;}
}

好了现在万事具备,我们只需要在我们的获取数据方法中使用这个方法就行了

                   new HttpManagerUser().checkObservableRegisted(phone).compose(RxUtils.rxSchedulerHelper()).compose(RxUtils.handleResult()).compose(RxUtils.handleError()).subscribeWith(new ResourceObserver<CheckUserExistsBean>() {@Overridepublic void onNext(CheckUserExistsBean value) {if (value.isRegistered()) {//如果是已经注册的手机号view.toVelidateActivity();} else {//如果是新用户view.toRegisterctivity();}}@Overridepublic void onError(Throwable e) {showErrorMsg(e);}@Overridepublic void onComplete() {view.hindDialog();}})

这样我们就基本上完成了比较简单和完善的rxjava访问的框架了

posted @ 2018-08-29 09:21 蓝冷然 阅读(...) 评论(...) 编辑 收藏

零基础快速搭建rxjava框架相关推荐

  1. 零基础快速搭建私人影音媒体平台

    目录 1. 前言 2. Jellyfin服务网站搭建 2.1. Jellyfin下载和安装 2.2. Jellyfin网页测试 3.本地网页发布 3.1 cpolar的安装和注册 3.2 Cpolar ...

  2. 零基础快速搭建K歌应用

    点击观看大咖分享 玩法开天辟地,体验不留缝隙.K歌不遗余力,应用解决效益.总是羡慕别人家的"歌房"苦叹自家"茅草房"消除不了回音和混音?这次就将带你实战K歌功能 ...

  3. 零基础快速学习Java技术的方法整理

    在学习java技术这条道路上,有很多都是零基础学员,他们对于java的学习有着很多的不解,不知怎么学习也不知道如何下手,其实Java编程涉及到的知识点还是非常多的,我们需要制定java学习路线图这样才 ...

  4. 零基础快速开发全栈后台管理系统(Vue3+ElementPlus+Koa2)—项目概述篇(一)

    零基础快速开发全栈后台管理系统(Vue3+ElementPlus+Koa2)-项目概述篇(一) 一.项目开发总体框架 二.项目开发流程 三.项目技术选型

  5. 零基础快速入门web学习路线(含视频教程)

    下面小编专门为广大web学习爱好者汇总了一条完整的自学线路:零基础快速入门web学习路线(含视频教程)(绝对纯干货)适合初学者的最新WEB前端学习路线汇总! 在当下来说web前端开发工程师可谓是高福利 ...

  6. 【Python零基础快速入门系列 | 03】AI数据容器底层核心之Python列表

    • 这是机器未来的第7篇文章 原文首发地址:https://blog.csdn.net/RobotFutures/article/details/124957520 <Python零基础快速入门 ...

  7. 【Python零基础快速入门系列 | 07】浪漫的数据容器:成双成对之字典

    这是机器未来的第11篇文章 原文首发链接:https://blog.csdn.net/RobotFutures/article/details/125038890 <Python零基础快速入门系 ...

  8. 零基础快速打造一个属于自己的微信聊天工具

    " 零基础快速打造一个属于自己的微信聊天工具" 打开微信,我们可以和别人进行聊天,发送消息.非常方便,那微信是怎么来的呢​?这个本质的问题让人突发奇想,我们能不能做一个属于自己的微 ...

  9. 零基础快速入门SpringBoot2.0教程 (三)

    一.SpringBoot Starter讲解 简介:介绍什么是SpringBoot Starter和主要作用 1.官网地址:https://docs.spring.io/spring-boot/doc ...

最新文章

  1. 神了,无意中发现一位1500道的2021LeetCode算法刷题pdf笔记
  2. linux性能调优原创翻译系列
  3. 崩溃重启_semi-sync插件崩溃导致MySQL重启的故障分析-爱可生
  4. python控制台输出到文件_Python print 立即打印内容到重定向的文件
  5. 用until编写一段shell程序,计算1~10的平方和
  6. Zookeeper架构及FastLeaderElection机制
  7. JavaScript基本类型值和引用类型值的复制问题
  8. nlp基础—10.结巴分词的应用及底层原理剖析
  9. bdm导入mysql_MySQL数据库导入教程
  10. Go 设计模式(Go patterns)
  11. VMware workstation 16 pro下载、安装(官网)
  12. js 大地坐标转经纬度
  13. 怎么查看微信收藏功能的剩余可用空间
  14. Asio Threads and Asio
  15. 华为薪资等级结构表_2018华为等级工资表一览
  16. mysql group_concat去重_mysql GROUP_CONCAT 函数 将相同的键的多个单元格合并到一个单元格...
  17. php date函数 在哪里,PHP date函数
  18. 微信小程序用echarts引入中国地图
  19. 两台ubantu搭linux集群,如何使用运行Ubuntu 14.04的firewire在两台Linux...
  20. leetcode_885. 螺旋矩阵 III

热门文章

  1. yara 源码学习(三)  扫描部分
  2. 使用CAS框架快速实现单点登录
  3. 掌握Revit中的标高的绘制和修改学习记录
  4. 默默前行的livego--基于go语言的rtmp直播服务器
  5. 用svg画一个微信订阅号的图标
  6. IBM推出蓝色基因二代产品蓝色基因/P(转)
  7. Barrier与多线程
  8. 【转自知乎】软件实施工程师技能要求
  9. CodeForces - 699A Launch of Collider 粒子对撞机 基础题
  10. C_教程_MATLAB远程桌面无法使用的问题