Android实战——RxJava2解锁图片三级缓存框架

RxJava2解锁图片三级缓存框架


本篇文章包括以下内容

  • 前言
  • 图片三级缓存的介绍
  • 框架结构目录的介绍
  • 构建项目整体框架
  • 实现图片三级缓存
  • 演示效果
  • 源码下载
  • 结语

前言

RxJava2作为如今那么主流的技术,不学习学习都不行了,本篇文章需要你有RxJava2的基础,如果需要对RxJava2学习的同学,可以关注我的博客,查看Android实战——RxJava2+Retrofit+RxBinding解锁各种新姿势 。项目代码实现模仿Picasso,大伙们可以看下最后的代码效果,那么废话不多说,Hensen老师开车啦

RxImageLoader.with(TextImageLoaderActivity.this).load(url).into(iv);

图片三级缓存的介绍

图片的三级缓存很多同学可能已经掌握了,很多同学可能也听说过,那么这里就简单的来回顾一下你们学习的三级缓存机制是否正确吧

首先,这个图就是表示三级缓存机制的所有,其三级分别是(按先后顺序)

  1. 内存缓存(一级):如果内存存在我们的缓存信息,直接用它
  2. 文件缓存(二级):如果内存不存在我们的缓存信息,那么就查看是否有我们的缓存文件,如果有,直接使用它。同时,将其缓存到内存中
  3. 网络缓存(三级):如果文件不存在缓存文件,直接从网络上下载,直接使用它。同时,将其缓存到文件和内存中

那么我们在框架中我们需要做的事情有哪些呢?

  1. 内存缓存:采用Google自带的LruCache进行缓存
  2. 文件缓存:采用Github上没有被Google收录却被Google认证的DiskLruCache
  3. 网络缓存:通过io流的Stream进行文件的读写

如果对于LruCache和DiskCache不懂的同学,可以查看郭霖大神的博客,里面有很详细的讲解,Lru指的是近期最少使用算法

框架结构目录的介绍

框架的结构看似复杂,其实内容不多,实现起来也不难

下面对框架结构目录进行介绍,图片上显示了目录结构之间的关系

  1. TextImageLoaderActivity:是我们的测试界面
  2. ImageBean:使用RxJava的onNext传递的Bean对象
  3. CacheObservable:三级缓存的父类
  4. DiskCacheObservable:文件缓存Observable
  5. MemoryCacheObservable:内存缓存Observable
  6. NetworkCacheObservable:网络缓存Observable
  7. RequestCreator:对三级缓存进行统一管理
  8. RxImageLoader:使用RequestCreator管理类进行缓存机制的调用
  9. DiskCacheUtils:文件缓存的工具类

构建项目整体框架

1、准备工作

导入我们需要的依赖库

//rxjava
compile "io.reactivex.rxjava2:rxjava:2.0.8"
compile ‘io.reactivex.rxjava2:rxandroid:2.0.1‘
//disklrucache
compile ‘com.jakewharton:disklrucache:2.0.2‘

2、Bean对象的创建

我们以key、value的形式来创建该Bean对象

public class ImageBean {
    private String url;
    private Bitmap bitmap;
    public ImageBean(Bitmap bitmap, String url) {
        this.bitmap = bitmap;
        this.url = url;
    }
    public Bitmap getBitmap() {
        return bitmap;
    }
    public void setBitmap(Bitmap bitmap) {
        this.bitmap = bitmap;
    }
    public String getUrl() {
        return url;
    }
    public void setUrl(String url) {
        this.url = url;
    }
}

3、缓存类的创建

public abstract class CacheObservable {

    /**
     * 获取缓存数据
     * @param url
     * @return
     */
    public Observable<ImageBean> getImage(final String url) {
        return Observable.create(new ObservableOnSubscribe<ImageBean>() {
            @Override
            public void subscribe(@NonNull ObservableEmitter<ImageBean> e) throws Exception {
                if (!e.isDisposed()) {
                    ImageBean image = getDataFromCache(url);
                    e.onNext(image);
                    e.onComplete();
                }
            }
        }).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());
    }

    /**
     * 取出缓存数据
     * @param url
     * @return
     */
    public abstract ImageBean getDataFromCache(String url);

    /**
     * 缓存数据
     * @param image
     */
    public abstract void putDataToCache(ImageBean image);
}

这里我们的三级缓存只要继承至该类,实现存入缓存和取出缓存的操作就可以了

public class DiskCacheObservable extends CacheObservable {
    @Override
    public ImageBean getDataFromCache(String url) {
        return null;
    }
    @Override
    public void putDataToCache(final ImageBean image) {

    }
}
public class MemoryCacheObservableextends CacheObservable {
    @Override
    public ImageBean getDataFromCache(String url) {
        return null;
    }
    @Override
    public void putDataToCache(final ImageBean image) {

    }
}
public class NetworkCacheObservable extends CacheObservable {
    @Override
    public ImageBean getDataFromCache(String url) {
        return null;
    }
    @Override
    public void putDataToCache(final ImageBean image) {

    }
}

这里也是我们最后一步所要实现的逻辑功能,这里我们先把框框搭建好

4、管理缓存类创建

public class RequestCreator {
    public MemoryCacheObservable memoryCacheObservable;
    public DiskCacheObservable diskCacheObservable;
    public NetworkCacheObservable networkCacheObservable;

    public RequestCreator(Context context) {
        memoryCacheObservable = new MemoryCacheObservable();
        diskCacheObservable = new DiskCacheObservable();
        networkCacheObservable = new NetworkCacheObservable();
    }

    public Observable<ImageBean> getImageFromMemory(String url) {
        return memoryCacheObservable.getImage(url);
    }

    public Observable<ImageBean> getImageFromDisk(String url) {
        return diskCacheObservable.getImage(url);
    }

    public Observable<ImageBean> getImageFromNetwork(String url) {
        return networkCacheObservable.getImage(url);
    }
}

5、模拟Picasso源码,使用构造者模式创建我们的RxImageLoader

public class RxImageLoader {

    static RxImageLoader singleton;
    private String mUrl;
    private RequestCreator requestCreator;

    //防止用户可以创建该对象
    private RxImageLoader(Builder builder) {
        requestCreator = new RequestCreator(builder.mContext);
    }

    public static RxImageLoader with(Context context) {
        if (singleton == null) {
            synchronized (RxImageLoader.class) {
                if (singleton == null) {
                    singleton = new Builder(context).build();
                }
            }
        }
        return singleton;
    }

    public RxImageLoader load(String url) {
        this.mUrl = url;
        return singleton;
    }

    public void into(final ImageView imageView) {
        Observable
                .concat(
                        requestCreator.getImageFromMemory(mUrl),
                        requestCreator.getImageFromDisk(mUrl),
                        requestCreator.getImageFromNetwork(mUrl)
                )
                .first(new ImageBean(null,mUrl)).toObservable()
                .subscribe(new Observer<ImageBean>() {
                    @Override
                    public void onSubscribe(Disposable d) {

                    }

                    @Override
                    public void onNext(ImageBean imageBean) {
                        if (imageBean.getBitmap() != null) {
                            imageView.setImageBitmap(imageBean.getBitmap());
                        }
                    }

                    @Override
                    public void onError(Throwable e) {
                        e.printStackTrace();
                    }

                    @Override
                    public void onComplete() {
                        Log.e("onComplete", "onComplete");
                    }
                });
    }

    public static class Builder {

        private Context mContext;

        public Builder(Context mContext) {
            this.mContext = mContext;
        }

        public RxImageLoader build() {
            return new RxImageLoader(this);
        }
    }
}

上面代码做了哪些事

  1. 采用双判空的单例模式
  2. 可采用链式编程方式
  3. 使用RxJava的concat和first方法
  4. concat方法表示将缓存机制Observable进行有序的连接,按顺序读取内存缓存,文件缓存,网络缓存
  5. first方法表示判断,如果IamgeBean中的bitmap为空,那么跳过此次连接,例如,requestCreator.getImageFromMemory(mUrl)获取的bitmap为空,那么直接跳过这次concat连接,进行requestCreator.getImageFromDisk(mUrl)操作,直到bitmap不为空则程序继续往下执行,断开concat的连接

6、Activity加载图片

我们简单的使用一个按钮加载图片就好了

public class TextImageLoaderActivity extends AppCompatActivity {

    ImageView iv;
    Button bt;
    String url;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_text_image_loader);
        iv = (ImageView) findViewById(R.id.iv);
        bt = (Button) findViewById(R.id.bt);
        url = "http://img2.baa.bitautotech.com/usergroup/editor_pic/2017/3/22/694494c2f3544226ae911bf86b4e2bcc.png";

        bt.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                RxImageLoader.with(TextImageLoaderActivity.this).load(url).into(iv);
            }
        });
    }
}

实现图片三级缓存

做好了框架的框框,下面就是对具体的DiskCacheObservable、MemoryCacheObservable、NetworkCacheObservable进行对应的方法实现就可以了

1、内存缓存

内存缓存最简单了,只要放入到LruCache即可

public class MemoryCacheObservable extends CacheObservable {

    private int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);
    private int cacheSize = maxMemory / 4;
    private LruCache<String, Bitmap> mLruCache = new LruCache<String, Bitmap>(cacheSize) {
        @Override
        protected int sizeOf(String key, Bitmap bitmap) {
            return bitmap.getRowBytes() * bitmap.getHeight() / 1024;
        }
    };

    @Override
    public ImageBean getDataFromCache(String url) {
        Log.e("getDataFromCache", "getDataFromMemoryCache");
        Bitmap bitmap = mLruCache.get(url);
        return new ImageBean(bitmap, url);
    }

    @Override
    public void putDataToCache(ImageBean image) {
        mLruCache.put(image.getUrl(), image.getBitmap());
    }
}

2、文件缓存

文件缓存涉及DiskLruCache的使用、文件下载和文件名用MD5加密

public class DiskCacheObservable extends CacheObservable {

    private Context mContext;
    private DiskLruCache mDiskLruCache;
    private final int maxSize = 10 * 1024 * 1024;

    public DiskCacheObservable(Context mContext) {
        this.mContext = mContext;
        initDiskLruCache();
    }

    @Override
    public ImageBean getDataFromCache(String url) {
        Log.e("getDataFromCache","getDataFromDiskCache");
        Bitmap bitmap = getDataFromDiskLruCache(url);
        return new ImageBean(bitmap, url);
    }

    @Override
    public void putDataToCache(final ImageBean image) {
        //由于网络读取需要在子线程中执行
        Observable.create(new ObservableOnSubscribe<ImageBean>() {
            @Override
            public void subscribe(@NonNull ObservableEmitter<ImageBean> e) throws Exception {
                putDataToDiskLruCache(image);
            }
        }).subscribeOn(Schedulers.io()).subscribe();
    }

    public void initDiskLruCache() {
        File cacheDir = DiskCacheUtils.getDiskCacheDir(mContext, "our_cache");
        if (!cacheDir.exists()) {
            cacheDir.mkdirs();
        }
        int versionCode = DiskCacheUtils.getAppVersion(mContext);
        try {
            //这里需要注意参数二:缓存版本号,只要不同版本号,缓存都会被清除,重新使用新的
            mDiskLruCache = DiskLruCache.open(cacheDir, versionCode, 1, maxSize);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 获取文件缓存
     * @param url
     * @return
     */
    private Bitmap getDataFromDiskLruCache(String url) {
        Bitmap bitmap = null;
        FileDescriptor fileDescriptor = null;
        FileInputStream fileInputStream = null;
        try {
            final String key = DiskCacheUtils.getMD5String(url);
            DiskLruCache.Snapshot snapshot = mDiskLruCache.get(key);
            if (snapshot != null) {
                fileInputStream = (FileInputStream) snapshot.getInputStream(0);
                fileDescriptor = fileInputStream.getFD();
            }
            if (fileDescriptor != null) {
                bitmap = BitmapFactory.decodeStream(fileInputStream);
            }
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return bitmap;
    }

    /**
     * 缓存文件数据
     * @param imageBean
     */
    private void putDataToDiskLruCache(ImageBean imageBean) {
        try {
            String key = DiskCacheUtils.getMD5String(imageBean.getUrl());
            DiskLruCache.Editor editor = mDiskLruCache.edit(key);
            if (editor != null) {
                OutputStream outputStream = editor.newOutputStream(0);
                boolean isSuccess = downloadUrlToStream(imageBean.getUrl(), outputStream);
                if (isSuccess) {
                    editor.commit();
                } else {
                    editor.abort();
                }
                mDiskLruCache.flush();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 下载文件
     * @param urlString
     * @param outputStream
     * @return
     */
    private boolean downloadUrlToStream(String urlString, OutputStream outputStream) {
        HttpURLConnection urlConnection = null;
        BufferedOutputStream out = null;
        BufferedInputStream in = null;
        try {
            final URL url = new URL(urlString);
            urlConnection = (HttpURLConnection) url.openConnection();
            in = new BufferedInputStream(urlConnection.getInputStream(), 8 * 1024);
            out = new BufferedOutputStream(outputStream, 8 * 1024);
            int b;
            while ((b = in.read()) != -1) {
                out.write(b);
            }
            return true;
        } catch (final IOException e) {
            e.printStackTrace();
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            try {
                if (out != null) {
                    out.close();
                }
                if (in != null) {
                    in.close();
                }
            } catch (final IOException e) {
                e.printStackTrace();
            }
        }
        return false;
    }

}

这里用到一个DiskLruCacheUtils

public class DiskCacheUtils {

    /**
     * 获取缓存路径
     * @param context
     * @param uniqueName
     * @return
     */
    public static File getDiskCacheDir(Context context, String uniqueName) {
        String cachePath;
        if (Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())
                || !Environment.isExternalStorageRemovable()) {
            cachePath = context.getExternalCacheDir().getPath();
        } else {
            cachePath = context.getCacheDir().getPath();
        }
        return new File(cachePath + File.separator + uniqueName);
    }

    /**
     * 获取App版本号
     * @param context
     * @return
     */
    public static int getAppVersion(Context context) {
        try {
            PackageInfo info = context.getPackageManager().getPackageInfo(context.getPackageName(), 0);
            return info.versionCode;
        } catch (PackageManager.NameNotFoundException e) {
            e.printStackTrace();
        }
        return 1;
    }

    /**
     * 获取加密后的MD5
     * @param key
     * @return
     */
    public static String getMD5String(String key) {
        String cacheKey;
        try {
            final MessageDigest mDigest = MessageDigest.getInstance("MD5");
            mDigest.update(key.getBytes());
            cacheKey = bytesToHexString(mDigest.digest());
        } catch (NoSuchAlgorithmException e) {
            cacheKey = String.valueOf(key.hashCode());
        }
        return cacheKey;
    }

    private static String bytesToHexString(byte[] bytes) {
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < bytes.length; i++) {
            String hex = Integer.toHexString(0xFF & bytes[i]);
            if (hex.length() == 1) {
                sb.append(‘0‘);
            }
            sb.append(hex);
        }
        return sb.toString();
    }

}

3、网络缓存

网络缓存只需要下载文件,不需要实现缓存数据的方法即可

public class NetworkCacheObservable extends CacheObservable {
    @Override
    public ImageBean getDataFromCache(String url) {
        Log.e("getDataFromCache", "getDataFromNetworkCache");
        Bitmap bitmap = downloadImage(url);
        return new ImageBean(bitmap, url);
    }

    @Override
    public void putDataToCache(ImageBean image) {

    }

    /**
     * 下载文件
     * @param url
     * @return
     */
    public Bitmap downloadImage(String url) {
        Bitmap bitmap = null;
        InputStream inputStream = null;
        try {
            URL imageUrl = new URL(url);
            URLConnection urlConnection = (HttpURLConnection) imageUrl.openConnection();
            inputStream = urlConnection.getInputStream();
            bitmap = BitmapFactory.decodeStream(inputStream);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (inputStream != null) {
                try {
                    inputStream.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        return bitmap;
    }
}

4、管理缓存

获取三级缓存逻辑实现完之后,就应该对管理我们的缓存,进行对应的缓存操作

public class RequestCreator {

    public MemoryCacheObservable memoryCacheObservable;
    public DiskCacheObservable diskCacheObservable;
    public NetworkCacheObservable networkCacheObservable;

    public RequestCreator(Context context) {
        memoryCacheObservable = new MemoryCacheObservable();
        diskCacheObservable = new DiskCacheObservable(context);
        networkCacheObservable = new NetworkCacheObservable();
    }

    public Observable<ImageBean> getImageFromMemory(String url) {
        return memoryCacheObservable.getImage(url)
                .filter(new Predicate<ImageBean>() {
                    @Override
                    public boolean test(@NonNull ImageBean imageBean) throws Exception {
                        Bitmap bitmap = imageBean.getBitmap();
                        return bitmap != null;
                    }
                });
    }

    public Observable<ImageBean> getImageFromDisk(String url) {

        return diskCacheObservable.getImage(url)
                .filter(new Predicate<ImageBean>() {
                    @Override
                    public boolean test(@NonNull ImageBean imageBean) throws Exception {
                        Bitmap bitmap = imageBean.getBitmap();
                        return bitmap != null;
                    }
                }).doOnNext(new Consumer<ImageBean>() {
                    @Override
                    public void accept(@NonNull ImageBean imageBean) throws Exception {
                        //缓存内存
                        memoryCacheObservable.putDataToCache(imageBean);
                    }
                });
    }

    public Observable<ImageBean> getImageFromNetwork(String url) {
        return networkCacheObservable.getImage(url)
                .filter(new Predicate<ImageBean>() {
                    @Override
                    public boolean test(@NonNull ImageBean imageBean) throws Exception {
                        Bitmap bitmap = imageBean.getBitmap();
                        return bitmap != null;
                    }
                })
                .doOnNext(new Consumer<ImageBean>() {
                    @Override
                    public void accept(@NonNull ImageBean imageBean) throws Exception {
                        //缓存文件和内存
                        diskCacheObservable.putDataToCache(imageBean);
                        memoryCacheObservable.putDataToCache(imageBean);
                    }
                });
    }
}

演示效果

1、首次运行程序,没有任何缓存,当我们连续点击2次按钮时

可以看到其运行的顺序

  1. 第一次点击按钮可以先从内存和文件获取图片,发现没有,再从网络获取图片
  2. 第二次点击按钮从内存获取

2、第二次运行程序,有了文件缓存,当我们连续点击2次按钮时

可以看到其运行的顺序

  1. 第一次点击按钮先从内存获取,发现没有,再从文件获取图片
  2. 第二次点击按钮从内存获取

源码下载

源码下载

结语

各位同学可以下载源码进行阅读,最好自己手写一遍,你会更深刻体会到RxJava的好处和掌握图片的三级缓存机制,如果看不懂的同学不要气馁,多看几遍就会了。喜欢我的朋友可以关注我的博客,一定会有你想要学习的知识

时间: 2024-10-27 07:38:06

Android实战——RxJava2解锁图片三级缓存框架的相关文章

Android图片三级缓存策略

1.简介 Android缓存原理都是一样,可以自己封装. 三级缓存: 1.内存缓存:缓存在内存中,基于LRU(least recently used )算法,机器重启消失. 2.本地缓存.缓存在本地中.一般键值对形式.(url,filepath) 3.网络缓存.从网络加载资源,然后缓存在内存.本地中. 2.实现步骤 2.1 内存缓存: [java] view plain copypublic class MemoryCacheUtils { private LruCache<String,Bit

Android 图片三级缓存加载框架原理解析与代码实现

本文主要介绍三级缓存的原理解析与实现方式.以前一直觉得三级缓存图片加载是一个很难理解的东西,但是自己看了一下午再试着写了一遍之后感觉还是只要沉下心思考还时很容易熟悉掌握的. 所谓三级缓存:首先是内存-文件(外存)-网络三级缓存机制. 首先: 框架需要一个接入方法NGImageloadHelper.java: /** * 图片加载框架使用帮助类 * Created by nangua on 2016/7/8. */ public class NGImageloadHelper { /** * 处理

Android中常见的图片加载框架

图片加载涉及到图片的缓存.图片的处理.图片的显示等.而随着市面上手机设备的硬件水平飞速发展,对图片的显示要求越来越高,稍微处理不好就会造成内存溢出等问题.很多软件厂家的通用做法就是借用第三方的框架进行图片加载. 开源框架的源码还是挺复杂的,但使用较为简单.大部分框架其实都差不多,配置稍微麻烦点,但是使用时一般只需要一行,显示方法一般会提供多个重载方法,支持不同需要.这样会减少很不必要的麻烦.同时,第三方框架的使用较为方便,这大大的减少了工作量.提高了开发效率.本文主要介绍四种常用的图片加载框架,

属性动画与图片三级缓存

属性动画 动画: UI渐变, 变量值的变化 ObjectAnimator : ofInt("backgroundColor",start,end); ValueAnimator: for(int i = start; i< end; i++) { a = i; } ValueAnimator animation=ValueAnimator.ofInt(start,end); animation.setDuration(DURATION); animation.addUpdateL

图片三级缓存的原理

三级缓存的概念: 内存-->硬盘-->网络 由内存.硬盘.网络缓存形成. 关于三级缓存用到的技术: Android高效加载大图.多图解决方案.有效避免程序OOM使用的核心技术就是LruCache. LruCache只是管理了内存中图片的存储与释放,如果图片从内存中被移除的话,那么又需要从网络上重新加载一次图片,这显然非常耗时.对此,Google又提供了一套硬盘缓存的解决方案:DiskLruCache(非Google官方编写,但获得官方认证. 用法和流程: 当每次加载图片的时候都优先去内存加载图

图片三级缓存的流程

三级缓存的内容: 1. 从内存中获取图片,有,加载显示 2. 如果内存中没有,从本地获取图片,有加载显示,并且将图片缓存到内存,为下一次显示准备 3. 如果本地也没有,从网络下载图片,下载完成,显示图片,通过缓存到内存,保存到本地文件中,为下一次显示准备 在内存中获取图片有两种方式 第一种:软引用的方式(不太常用了) 强引用: user = new UserInfo(), 不会轻易被系统回收 软引用: SoftReference<Bitmap>, 当内存不足的时候,系统会回收软引用 弱引用:

Android异步批量下载图片并缓存

前言 本文引自:http://www.xycoding.com/articles/2014/07/29/android-async-images-download/,作者不详 ImagesDownLoad源码下载:DEMO 接触android开发不久,近段时间需实现一个批量下载图片并显示的小功能.在网上搜索了一圈,发现国内外网上异步加载的例子太多太杂,要么是加载大图decode时报OOM异常,要么内存急剧上升不稳定.所以在前辈们的基础上,做了一些优化,特共享出来,欢迎大家指正.这里主要参见了以下

android开源项目:图片下载缓存库picasso

picasso是Square公司开源的一个Android图形缓存库,地址http://square.github.io/picasso/,可以实现图片下载和缓存功能. picasso有如下特性: 在adapter中回收和取消当前的下载: 使用最少的内存完成复杂的图形转换操作: 自动的内存和硬盘缓存: 图形转换操作,如变换大小,旋转等,提供了接口来让用户可以自定义转换操作: 加载载网络或本地资源: 可以转换为自己需要的request(Square公司开源的另一个网络支持库:retrofit支持转化

Android实战——RxJava2+Retrofit+RxBinding解锁各种新姿势

RxJava2+Retrofit+RxBinding解锁各种新姿势 本篇文章内容包含以下内容 前言 RxJava2的基本介绍 RxJava2观察者模式的介绍 RxJava2观察者模式的使用 RxJava2的基本使用 模拟发送验证码 RxJava2与Retrofit的使用 模拟用户登陆获取用户数据 合并本地与服务器购物车列表 RxJava2与RxBinding的使用 优化搜索请求 优化点击请求 源码下载 结语 前言 作为主流的第三方框架Rx系列,不学习也不行啊,对于初学者来说,可能RxJava看起