picasso框架

picasso 是一个强大的图片加载缓存框架

1.首先看下picasso 如何使用:

Picasso.with(this)
.load("url")
.placeholder(R.drawable.leak_canary_icon)
.error(R.drawable.leak_canary_icon)
.resize(480,800)
.centerCrop()
.rotate(360)
.priority(Picasso.Priority.HIGH)
.tag("picasso listview")
.memoryPolicy(MemoryPolicy.NO_CACHE)
.networkPolicy(NetworkPolicy.NO_CACHE)
.into(imageView);

Picasso和Glide相似都是使用链式调用 我们可以把Picasso类视为一个管理类,管理图片的加载,转换,缓存的工作.
.with()获取Picasso的单例,参数是上下文.
.load()方法完成加载图片的方法.参数是url或file路径
.resize()方法 参数单位像素 因为在项目中要考虑网络带宽,手机的内存使用,下载速度等情况.结合考虑.这时候有一种场景服务器给我们的图片的宽和高与我们实际的imageview的宽和高是不一致的.服务器给我们一些奇奇怪怪的图片,这时我们就要调用resize方法.对图片的宽高进行设置.
.centerCrop() 将整个图片充满imageview边界 裁减掉多余部分
.rotate() 对图片加载进行旋转设置.旋转点坐标0.0
.priority(Picasso.Priority.HIGH) 设置图片加载优先级 不是100%保证 因为picasso内部只是向高的优先级靠拢.但并不会保证
.tag() Picasso 允许我们为每个请求设置一个tag 为什么用到tag呢 比如listview 快速滑动这样的场景,如果不去设置响应事件就会加载所有的图片,浪费性能,造成体验不好.UI卡顿,而我们为每一个加载事件做标记tag,监听响应事件.做出处理操作.

mlsitview.setOnScrollStateChanged(AbsListView view,int scrollState){
final Picasso picasso = Picasso.with(MainActivity.this);
if(scrollState == SCROLL_STATE_IDLE){//停止状态加载图片
picasso.resumeTag("picasso listview");
}else{
picasso.pauseTag("picasso listview");
}
}

.memoryPolicy(MemoryPolicy.NO_CACHE) picasso的缓存策略 内存缓存
.networkPolicy(NetworkPolicy.NO_CACHE)文件缓存

2.picasso源码分析

1)with方法:内存缓存Lrucache和线程池调度

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

.build()方法,生成Picasso对象,返回该对象实例,按照需求配置下载器,缓存,线程池,转换器等.

 public Picasso build() {
      Context context = this.context;

      if (downloader == null) {
        downloader = Utils.createDefaultDownloader(context);//下载器
      }
      if (cache == null) {
        cache = new LruCache(context);//缓存
      }
      if (service == null) {
        service = new PicassoExecutorService();//线程池
      }
      if (transformer == null) {
        transformer = RequestTransformer.IDENTITY;//转换器
      }

      Stats stats = new Stats(cache);

      Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);

      return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
          defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
    }

.build()方法.-->downloader = Utils.createDefaultDownloader(context);
-->

static Downloader createDefaultDownloader(Context context) {
    try {
      Class.forName("com.squareup.okhttp.OkHttpClient");//通过反射查找是否引用了okhttp,如果引用了采用okhttp进行网络连接
      return OkHttpLoaderCreator.create(context);
    } catch (ClassNotFoundException ignored) {
    }
    return new UrlConnectionDownloader(context);//没有引用,采用httpurlConnection进行网络请求
  }

继续看build方法中

 if (cache == null) {
        cache = new LruCache(context);//lruCache主要采用linkedhashmap 将图片添加到链表尾部//( final LinkedHashMap<String, Bitmap> map;)
      }
      if (service == null) {
        service = new PicassoExecutorService();
      }

接着来看PicassoExecutorService
PicassoExecutorService 是一个线程并发池
PicassoExecutorService extends ThreadPoolExecutor
ThreadPoolExecutor是java并发库中的内容 无论是图片库还是网络库都离不开这个线程池

public ThreadPoolExecutor(int corePoolSize, //核心线程数目
                              int maximumPoolSize,//线程池中允许的最大线程数
                              long keepAliveTime,//线程池中线程数目比核心线程数目多的时候,超过这个时间,就会将多余的线程回收
                              TimeUnit unit,//时间单位
                              BlockingQueue<Runnable> workQueue,//阻塞队列 线程安全队列
                              ThreadFactory threadFactory,//线程工厂 产生线程
                              RejectedExecutionHandler handler) {}//当线程池 由于线程数目和队列导致任务阻塞时 ,线程池的处理方式

1.如果线程池中的线程数目少于corePoolSize , 这时候线程池是会重新创建线程的,直到线程数目达到corePoolSize.
2.如果线程池中线程数目大于或者等于corePoolSize,但是工作队列workQueue没有满, 这时新的任务还是会放进队列中的,按照先进先出的原则来进行执行.
3.如果线程池中的线程数目大于等于corePoolSize,并且工作队列workQueue满了,但是总线程数目小于maximumPoolSzie 这时候还是可以直接创建线程 来处理添加的任务
4.如果工作队列满了,并且线程池中线程的数目达到了最大数目maximumPoolSize 这时就会由RejectedExecutionHandler来处理
默认的处理方式就是 丢弃到任务,同时抛出异常
继续看
非常重要的 Dispatcher 是来管理非常重要的线程之间的切换.
dispatcher如何完成线程切换
首先我们进入Dispatcher这个类,Dispatcher类中首先开启线程DispatcherThread

final DispatcherThread dispatcherThread;
this.dispatcherThread = new DispatcherThread();
this.dispatcherThread.start();

首先DispatcherThread extends HandlerThread

static class DispatcherThread extends HandlerThread {
    DispatcherThread() {
      super(Utils.THREAD_PREFIX + DISPATCHER_THREAD_NAME, THREAD_PRIORITY_BACKGROUND);
    }
  }

所以先分析下HandlerThread
HandlerThread extends Thread 注释中这样写到他会开启一个新的线程,线程内部会有一个Looper对象,通过这个Looper对象可以去循环遍历我们的消息队列.
看下run()方法
run方法的作用就是完成Looper的创建 通过Looper的循环方法构造一个循环的线程.HandlerThread也要调用start方法,完成线程的运行的.

@Override
    public void run() {
        mTid = Process.myTid();
        Looper.prepare();//完成Looper对象的创建
        synchronized (this) {//通过同步锁机制锁住当前对象
            mLooper = Looper.myLooper();//mLooper在我们的同步代码块中
            notifyAll();//唤醒等待线程 和notifyAll()同步出现的就是wait()方法.
        }
        Process.setThreadPriority(mPriority);//设置线程优先级
        onLooperPrepared();//内部是个空实现 留给我们自己去重写
        Looper.loop();//完成整个的线程消息启动工作
        mTid = -1;
    }

//我们回到同步代码块中 我们为什么要通过同步代码块中的方法去唤醒等待线程呢?有在哪里去wait的操作呢?
在getLooper方法中

 public Looper getLooper() {
        if (!isAlive()) {
            return null;
        }
        
        // If the thread has been started, wait until the looper has been created.
        synchronized (this) {//这里同样用同步锁机制锁住当前对象
            while (isAlive() && mLooper == null) {//判断线程存货且不为空的情况下
                try {
                    wait();//线程等待
                } catch (InterruptedException e) {
                }
            }
        }
        return mLooper;
    }

继续看Dispatcher构造方法中的其他参数

Dispatcher(Context context, ExecutorService service, Handler mainThreadHandler,
      Downloader downloader, Cache cache, Stats stats) {
    this.dispatcherThread = new DispatcherThread();
    this.dispatcherThread.start();
    Utils.flushStackLocalLeaks(dispatcherThread.getLooper());
    this.context = context;
    this.service = service;
    this.hunterMap = new LinkedHashMap<String, BitmapHunter>();
    this.failedActions = new WeakHashMap<Object, Action>();
    this.pausedActions = new WeakHashMap<Object, Action>();
    this.pausedTags = new HashSet<Object>();
    this.handler = new DispatcherHandler(dispatcherThread.getLooper(), this);//dispatherThread线程内部的handler(因为dispatcherThread.getLooper()参数)
    this.downloader = downloader;
    this.mainThreadHandler = mainThreadHandler;//主线程的handler
    this.cache = cache;
    this.stats = stats;
    this.batch = new ArrayList<BitmapHunter>(4);
    this.airplaneMode = Utils.isAirplaneModeOn(this.context);
    this.scansNetworkChanges = hasPermission(context, Manifest.permission.ACCESS_NETWORK_STATE);
    this.receiver = new NetworkBroadcastReceiver(this);//广播接收者,主要用于监听网络变化的操作的,这也是比较重要的
    receiver.register();
  }

回到picasso的build()方法中
Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);
HANDLER是picasso内部定义的
static final Handler HANDLER = new Handler(Looper.getMainLooper()){//getMainLooper是主线程
}//定义为静态内部类,就是静态内部类不持有外部类的引用

3.NetworkRequestHandler处理图片请求与回调

继续看build()方法
build()方法最后 还是返回一个picasso对象
return new Picasso(context, dispatcher, cache, listener, transformer, requestHandlers, stats,
defaultBitmapConfig, indicatorsEnabled, loggingEnabled);
我们进入这个方法看一下

  Picasso(Context context, Dispatcher dispatcher, Cache cache, Listener listener,
      RequestTransformer requestTransformer, List<RequestHandler> extraRequestHandlers, Stats stats,
      Bitmap.Config defaultBitmapConfig, boolean indicatorsEnabled, boolean loggingEnabled) {
  this.context = context;
    this.dispatcher = dispatcher;
    this.cache = cache;
    this.listener = listener;
    this.requestTransformer = requestTransformer;
    this.defaultBitmapConfig = defaultBitmapConfig;

    int builtInHandlers = 7; // Adjust this as internal handlers are added or removed.
    int extraCount = (extraRequestHandlers != null ? extraRequestHandlers.size() : 0);
    List<RequestHandler> allRequestHandlers =
        new ArrayList<RequestHandler>(builtInHandlers + extraCount);

    // ResourceRequestHandler needs to be the first in the list to avoid
    // forcing other RequestHandlers to perform null checks on request.uri
    // to cover the (request.resourceId != 0) case.
    allRequestHandlers.add(new ResourceRequestHandler(context));
    if (extraRequestHandlers != null) {
      allRequestHandlers.addAll(extraRequestHandlers);//将extraRequestHandlers集合加入到allRequesthandlers集合中,看下
allRequestHandler这个ArrayList集合.

    }
    allRequestHandlers.add(new ContactsPhotoRequestHandler(context));
    allRequestHandlers.add(new MediaStoreRequestHandler(context));
    allRequestHandlers.add(new ContentStreamRequestHandler(context));//集合中加入处理contentprovider中图片的handler
    allRequestHandlers.add(new AssetRequestHandler(context));//处理asset中图片
    allRequestHandlers.add(new FileRequestHandler(context));//处理file中图片
    allRequestHandlers.add(new NetworkRequestHandler(dispatcher.downloader, stats));//处理网络请求图片的handler
    requestHandlers = Collections.unmodifiableList(allRequestHandlers);

    this.stats = stats;
    this.targetToAction = new WeakHashMap<Object, Action>();
    this.targetToDeferredRequestCreator = new WeakHashMap<ImageView, DeferredRequestCreator>();
    this.indicatorsEnabled = indicatorsEnabled;
    this.loggingEnabled = loggingEnabled;
    this.referenceQueue = new ReferenceQueue<Object>();
    this.cleanupThread = new CleanupThread(referenceQueue, HANDLER);
    this.cleanupThread.start();
}

我们主要看下处理网络请求图片的handler:NetworkRequestHandler.主要看其中的load()方法:

@Override public Result load(Request request, int networkPolicy) throws IOException {
    Response response = downloader.load(request.uri, request.networkPolicy);
    if (response == null) {
      return null;
    }

    Picasso.LoadedFrom loadedFrom = response.cached ? DISK : NETWORK;

    Bitmap bitmap = response.getBitmap();
    if (bitmap != null) {
      return new Result(bitmap, loadedFrom);
    }

    InputStream is = response.getInputStream();
    if (is == null) {
      return null;
    }
    // Sometimes response content length is zero when requests are being replayed. Haven't found
    // root cause to this but retrying the request seems safe to do so.
    if (loadedFrom == DISK && response.getContentLength() == 0) {
      Utils.closeQuietly(is);
      throw new ContentLengthException("Received response with 0 content-length header.");
    }
    if (loadedFrom == NETWORK && response.getContentLength() > 0) {//请求响应长度>0
      stats.dispatchDownloadFinished(response.getContentLength());//stats派发响应完成结果
    }
    return new Result(is, loadedFrom);
  }

看下stats的dispatchDownloadFinished方法:

void dispatchDownloadFinished(long size) {
    handler.sendMessage(handler.obtainMessage(DOWNLOAD_FINISHED, size));
  }

发现了熟悉的sendMessage()方法
其中的handler是StatsHandler 看其中handleMessage()方法:

 @Override public void handleMessage(final Message msg) {
      switch (msg.what) {
        case CACHE_HIT:
          stats.performCacheHit();
          break;
        case CACHE_MISS:
          stats.performCacheMiss();
          break;
        case BITMAP_DECODE_FINISHED:
          stats.performBitmapDecoded(msg.arg1);
          break;
        case BITMAP_TRANSFORMED_FINISHED:
          stats.performBitmapTransformed(msg.arg1);
          break;
        case DOWNLOAD_FINISHED:
          stats.performDownloadFinished((Long) msg.obj);

          break;
        default:
          Picasso.HANDLER.post(new Runnable() {
            @Override public void run() {
              throw new AssertionError("Unhandled stats message." + msg.what);
            }
          });
      }
    }

Stats中的performDownloadFinished()方法

  void performDownloadFinished(Long size) {
    downloadCount++;
    totalDownloadSize += size;
    averageDownloadSize = getAverage(downloadCount, totalDownloadSize);
  }

4.picasso源码load方法

load 方法return load(Uri.parse(path)).
该函数有四种重载方法,其中Uri,String,File最终都转化为Uri进行请求,而int则是app内部的资源访问。
load()函数返回RequestCreator对象,

 public RequestCreator load(Uri uri) {
    return new RequestCreator(this, uri, 0);
  }

再看RequestCreator();
RequestCreator的成员变量里有一个重要的对象是Request.Builder(其有一个内部类Builder),RequestCreator里很多函数都是桥 接到该Builder。

  RequestCreator(Picasso picasso, Uri uri, int resourceId) {
    if (picasso.shutdown) {
      throw new IllegalStateException(
          "Picasso instance already shut down. Cannot submit new requests.");
    }
    this.picasso = picasso;
    this.data = new Request.Builder(uri, resourceId, picasso.defaultBitmapConfig);
    }

看最后一行是创建了 Request.Builder对象,通过Request.Builder对象我们可以配置更多对象.

picasso源码into方法:Action&BitmapHunter

with()方法完成了Picasso对象的创建,内部通过单例的形式进行构建,这样就保证了在app使用Picasso对象的时候,只会存在一个
picasso对象,同时他也是我们整个Picasso加载的入口.同时with()方法还做了一些基础的配置工作.比如说Dispatcher这个类,在picasso框架中主要完成线程的切换工作,其内部实现原理是handler.
load()方法主要完成了RequsetCreate()方法的创建.同时获取到了我们要加载的一些资源的路径
into()方法完成图片的加载工作.

DeferredRequestCreator类
当我们创建请求的时候,我们新建一个图片加载请求,但是我们还不能获取到当imageview的宽高,这时我们会创建DeferredRequestCreator,通过这个类会我们的imageview的target去进行监听,直到获取到当前imageview的宽高,这时候我们重新执行我们的请求创建.所以删除请求的时候,同时我们还应该删除DeferredRequestCreator对target的监听事件.

public void into(Target target) {
    long started = System.nanoTime();
//线程检查
    checkMain();

    if (target == null) {
      throw new IllegalArgumentException("Target must not be null.");
    }
    if (deferred) {
      throw new IllegalStateException("Fit cannot be used with a Target.");
    }
//没设置url以及resId则取消请求
    if (!data.hasImage()) {
      picasso.cancelRequest(target);//取消请求
      target.onPrepareLoad(setPlaceholder ? getPlaceholderDrawable() : null);//取消后设置占位符.
      return;
    }

    Request request = createRequest(started);
    String requestKey = createKey(request);

    if (shouldReadFromMemoryCache(memoryPolicy)) {
      Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
      if (bitmap != null) {
        picasso.cancelRequest(target);
        target.onBitmapLoaded(bitmap, MEMORY);
        return;
      }
    }

    target.onPrepareLoad(setPlaceholder ? getPlaceholderDrawable() : null);

    Action action =
        new TargetAction(picasso, target, request, memoryPolicy, networkPolicy, errorDrawable,
            requestKey, tag, errorResId);
//提交请求,Picasso内部维护了一个map,key是imageView,value是Action
//提交时做判断,如果当前imageView已经在任务队列里了。判断当前任务与之前的任务是否相同,
//如果不相同则取消之前的任务并将新的key-value加入到map
    picasso.enqueueAndSubmit(action);
}

RequestCreator最重要的一个方法是into(),在into()方法调用之前所调用的一切配置函数都只是把配置信息储 存起来,而没有做网络请求,当调用into()函数后才开始网络请求。into()函数有五个重载方法,用于把请求结果存 放或显示到指定位置。

submit()函数:用于把RequestCreator提交上来的Action添加到队列里,该函数其实是把提交任务交给Dispatcher

image

而Dispatcher最后则通过handler把任务切换到Dispatcher所在的线程(后台线程,因为要进行网络访问).

RequestCreator最重要的一个方法是into(),在into()方法调用之前所调用的一切配置函数都只是把配置信息储 存起来,而没有做网络请求,当调用into()函数后才开始网络请求。into()函数有五个重载方法,用于把请求结果存 放或显示到指定位置。

image

这里分析into到ImageView里,该函数首先会查看缓存里是否有请求的Bitmap,如果有那最好,都不用进行网络请求, 直接把Bitmap显示到ImageView里。如果缓存里没有,则会把请求加入到请求队列里,之后进行网络请求。

到这里,又把控制权交给Picasso类。

image

底层分析:
Action 类:是一个request请求类包装类,最终把包装好的类,交给线程还执行.(一个没有Set的Bean,包含各种动作信息,如网络请求策略,内存策略,请求配置等。)
BitmapHuntyer 就是一个runnable,是一个开启子线程的工具.是一个Runnable的子类,用来进行Bitmap的获取(网络,硬盘,内存等),处理(角度,大小等),然后执行分发器(dispatcher)的回调处理

picasso源码into方法:线程池&PicassoFutureTask

Stats:对请求整个过程的一个记录,如命中(hit)缓存的次数,不命中(miss)缓存的次数,Bitmap下载解析次数, 下载完成次数等。
ImageViewAction:Action的子类,内部有complete()和error()函数,用于把请求结果显示到ImageView上
其实Picasso里用来在指定目标上显示结果都是通过PicassoDrawable类来实现的。

再次 强调下最重要的类

RequestHandler

//抽象类,由不同的子类来实现不同来源的图片的获取与加载,比如:
//AssetRequestHandler:加载asset里的图片
//FileRequestHandler:加载硬盘里的图片
//ResourceRequestHandler:加载资源图片
//NetworkRequestHandler:加载网络图片

BitmapHunter

//是一个Runnable的子类,用来进行Bitmap的获取(网络,硬盘,内存等),处理(角度,大小等),
//然后执行分发器(dispatcher)的回调处理

PicassoDrawable

//实现了引入图片渐变功能和debug状态的标识的Drawable,用来在最后bitmap转换成PicassoDrawable
//然后设置给ImageView,根据图片的来源可以在图片的左上角显示一个不同颜色的三角形色块
MEMORY(Color.GREEN) //内存加载
DISK(Color.BLUE) //本地加载
NETWORK(Color.RED) //网络加载

DeferredRequestCreator

//ViewTreeObserver.OnPreDrawListener的实现类,即将绘制视图树时执行的回调函数。
//这时所有的视图都测量完成并确定了框架。 客户端可以使用该方法来调整滚动边框,
//甚至可以在绘制之前请求新的布局,这里用来实现当开发者修改图片尺寸时的逻辑

Action

//Action代表了一个具体的加载任务,主要用于图片加载后的结果回调,有两个抽象方法,complete和error,
//并保存了每个请求的各种信息(),具体的实现类如下
//GetAction:同步执行请求时使用。
//FetchAction:当不需要ImageView来安置bitmap时的异步请求,通常用来预热缓存
//RemoteViewsAction:用来更新远程图片(notification等)的抽象类。
//TargetAction:一般在View(不只是ImageView)或者ViewHolder中用来加载图片,需要实现Target接口
//ImageViewAction:最常用的Action,主要用来给ImageView加载图片

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,117评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,328评论 1 293
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,839评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,007评论 0 206
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,384评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,629评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,880评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,593评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,313评论 1 243
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,575评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,066评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,392评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,052评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,082评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,844评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,662评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,575评论 2 270

推荐阅读更多精彩内容