Picasso,Glide,Fresco以及UIL使用經(jīng)驗的整理

這么久以來雖然經(jīng)常用到一些圖庫,但是自己從來沒有真正整理過我們使用過的這些東西有什么不同點,我們?yōu)槭裁匆x擇這個圖庫作為項目的加載圖片的框架(由于本人資歷尚淺,經(jīng)驗也不豐富,所以從沒有試過自己做一個圖片加載庫),今天突發(fā)奇想,也算是讓自己思路更清晰一點,來寫一些關(guān)于這些圖庫的異同之處.
作為一個應(yīng)用豐富的App,圖片加載是必不可少的,甚至對于圖片的質(zhì)量和數(shù)量都是要求很高的,然而對于圖片的加載是一個耗時操作,在UI線程為了避免ANR異常不能做耗時操作,是一個Android開發(fā)人員的基本常識,所以異步加載圖片是必然的,這就讓我們面臨了一個怎么選擇圖片加載框架的問題?市面上比較成熟的圖庫有: Universal-Image-Loader(UIL)、Picasso猜憎、Glide娩怎、Fresco。其實任何一個圖片加載框架都可以當(dāng)做一個普通的下載文件流程胰柑,一般都包含這么幾個步驟:初始化配置->構(gòu)造請求->執(zhí)行請求->處理請求結(jié)果峦树。

1.Universal-Image-Loader(UIL)

這是一個很早就出現(xiàn)的圖片加載框架了,作者是nostra13旦事,UIL使用很方便魁巩,而且自帶多種緩存策略,如最大尺寸先刪除姐浮、時間最久刪除 等谷遂,使用它,基本上不需要考慮太多的問題卖鲤。另外肾扰,UIL還支持圖片下載進(jìn)度的監(jiān)聽畴嘶,如果你有特殊需求,則可以在 圖片開始下載前集晚、剛開始下載等各個時間段來做一些額外的事情窗悯,非常方便。而且UIL可以在View滾動的過程中暫停圖片的加載偷拔,有利于提升界面的流暢度蒋院。但由于作者在兩年前宣布不再維護(hù)這個項目了,也就是說這個項目將不再更新,所以如果你將開發(fā)一個新項目,本人不推薦你使用此框架,因為市面上還有其它跟它一樣強(qiáng)大甚至更好的圖庫可以使用,但如果你現(xiàn)在的項目是一個老牌項目,那也沒必要著急更換它,因為它還是很強(qiáng)大的,沒有到跟不上時代的地步,到了真需要更換的時候再換也來得及

2.Picasso

這是一個來自于開源界名氣很大的Square公司開發(fā)的,總體來看,它屬于輕量級別的圖片加載庫莲绰,但它也有著一些自己的特色欺旧。比如,很特別的擁有統(tǒng)計功能蛤签,可以知道使用了多少內(nèi)存辞友、緩存命中如何,另外它本身沒有什么緩存策略震肮,而是依賴所用的網(wǎng)絡(luò)庫的緩存策略——其實就是依賴了OkHttp称龙。Picasso使用起來也是比較簡單的,不過對于新項目來說戳晌,也不是很推薦茵瀑,原因就在于,Glide比它更優(yōu)秀躬厌,而且使用起來幾乎 是一樣的……

緩存路徑:

這里講一下關(guān)于Picasso緩存路徑:

data/data/your package name/cache/picasso-cache/(默認(rèn)路徑)

有時候根據(jù)需求,我們需要更改其緩存路徑,這里我們分析一下啊,Picasso 底層其實是使用OkHttp去下載圖片马昨,同時在設(shè)置Picasso的時候,有一個.downloader(Downloader downloader)方法扛施,我們可以傳遞進(jìn)去一個OkHttpDownloader( OkHttpClient client).

Picasso picasso = new Picasso.Builder(Context)
                .downloader(new OkHttpDownloader(client))
                .build();

看到這里我們應(yīng)該想到,如果給OkHttpClient設(shè)置Cache是不是就可以改變緩存路徑呢?只需要給OkHttpClient設(shè)置.cache(new Cache(file, maxSize))就可以實現(xiàn)修改緩存路徑了鸿捧。代碼就是:

File file = new File("緩存文件所在路徑");
        if (!file.exists()) {
            file.mkdirs();
        }
        long maxSize = Runtime.getRuntime().maxMemory() / 8;   //設(shè)置圖片緩存大小為運(yùn)行時緩存的八分之一
        OkHttpClient client = new OkHttpClient.Builder()
                .cache(new Cache(file, maxSize))
                .build();
        Picasso picasso = new Picasso.Builder(this)
                .downloader(new OkHttpDownloader(client))
                .build();

這里需要注意的就是:當(dāng)把OkHttp升級到OkHttp3時,給downloader設(shè)置OkHttpDownloader()并不支持OkHttp3.如果想使用OkHttp3,需要使用 OkHttp3Downloader來替代OkHttpDownloader,OkHttp3Downloader庫是jakewharton專為為了能使用OkHttp3作為下載器而寫的,使用姿勢也很簡單:在Module dependencies添加依賴:

compile 'com.jakewharton.picasso:picasso2-okhttp3-downloader:1.1.0'

然后上面的代碼改成:

    Picasso picasso = new Picasso.Builder(this)
                .downloader(new OkHttp3Downloader(client))    //注意此處替換為 OkHttp3Downloader
                .build();
關(guān)于Picasso實例化對象(兩種方式)
  • Picasso.with(context)
    此方法提供默認(rèn)方式疙渣,生成單例的Picasso對象.
  • new Picasso.Builder(context).build()
    此方式提供自定義線程池匙奴、緩存、下載器等方法.
關(guān)于Picasso源碼簡單分析
  • Picasso.with(context),在源碼中我們很容易就可以看出這是在構(gòu)造一個單例的對象,而且是通過new Builder(context).build()建造者模式構(gòu)建.
    1.通過Builder(context)往下看就會發(fā)現(xiàn) this.context = context.getApplicationContext();得到的是全局的上下文,這是為了讓Picasso下載器同步應(yīng)用的生命周期的,然后我們的重點就可以放在build()上了.
    2.在build()方法里面我們會發(fā)現(xiàn)有六個方法:
(源代碼)
    (第一個方法)  if (downloader == null) {
        downloader = Utils.createDefaultDownloader(context);  //創(chuàng)建一個默認(rèn)的下載器.
            1.其中Downloader是一個用于從網(wǎng)絡(luò)上加載圖片的接口妄荔,需要實現(xiàn)load和shutdown方法泼菌。load用于加載圖片,shutdown用于關(guān)閉一些操作.
            2.Picasso的線程池是經(jīng)過優(yōu)化過的啦租,可以根據(jù)當(dāng)前設(shè)備網(wǎng)絡(luò)狀況設(shè)置其ThreadCount哗伯。
在網(wǎng)絡(luò)良好的條件下,線程池持有較多線程篷角,保證下載速度夠快焊刹。在網(wǎng)絡(luò)較差的條件下(2G網(wǎng)絡(luò)等),線程池減少持有線程,保證帶寬不會被多個連接阻塞虐块。
      }

    (第二個方法)  if (cache == null) {
        cache = new LruCache(context);  //初始化緩存,創(chuàng)建內(nèi)存緩存
      }

     (第三個方法)  if (service == null) {
        service = new PicassoExecutorService();  //初始化線程池
           1.默認(rèn)啟動了3個核心線程俩滥,采用了PriorityBlockingQueue優(yōu)先級阻塞隊列,也就是說Picasso支持優(yōu)先級調(diào)度.(對網(wǎng)絡(luò)狀態(tài)進(jìn)行線程的優(yōu)化)
      }

     (第四個方法)  if (transformer == null) {
        transformer = RequestTransformer.IDENTITY;  // 初始化轉(zhuǎn)換器,請求的前置處理贺奠,在請求發(fā)出去之前執(zhí)行霜旧,類似于攔截器
          1.默認(rèn)RequestTransformer.IDENTITY表示不作處理.
      }

     (第五個方法)  Stats stats = new Stats(cache);  //狀態(tài)控制類,統(tǒng)計一些狀態(tài)信息,用來發(fā)送各種消息儡率,例如查找圖片緩存的命中率挂据,下載是否完成等

    (第六個方法)   Dispatcher dispatcher = new Dispatcher(context, service, HANDLER, downloader, cache, stats);   //最后創(chuàng)建調(diào)度器,用來分發(fā)任務(wù)
1.從build方法中可以看出,大多數(shù)參數(shù)直接傳進(jìn)了這個類的構(gòu)造方法中喉悴,可見這個類不容小覷棱貌。
       Dispatcher主要是來調(diào)度任務(wù)的玖媚,比如提交任務(wù)箕肃,取消任務(wù),暫停加載今魔,恢復(fù)加載勺像,重試,加載完成错森,監(jiān)聽網(wǎng)絡(luò)等等吟宦。
       同樣,里面也用了一個HandlerThread和Handler來分發(fā)任務(wù)涩维。通過一系列的dispatchXXX殃姓,由Handler發(fā)送消息,Handler接收消息后瓦阐,通過performXXX來調(diào)度任務(wù)蜗侈。
  • 關(guān)于Picasso中任務(wù)調(diào)度器Dispatcher的簡單分析:
    翻看源碼我們會看到其Dispatcher類的構(gòu)造方法有六個參數(shù),這里主要分析其中兩個,一個ExecutorService,另一個就是Handler,
    第一,我們首先講一下Handler
public DispatcherHandler(Looper looper, Dispatcher dispatcher) {
      super(looper);
      this.dispatcher = dispatcher;
    }

    @Override public void handleMessage(final Message msg) {
      switch (msg.what) {
        case REQUEST_SUBMIT: {      //提交請求
          Action action = (Action) msg.obj;
          dispatcher.performSubmit(action);
          break;
        }
        case REQUEST_CANCEL: {        //取消請求
          Action action = (Action) msg.obj;
          dispatcher.performCancel(action);
          break;
        }
        case TAG_PAUSE: {          //暫停請求
          Object tag = msg.obj;
          dispatcher.performPauseTag(tag);
          break;
        }
        case TAG_RESUME: {      //恢復(fù)請求
          Object tag = msg.obj;
          dispatcher.performResumeTag(tag);
          break;
        }
        case HUNTER_COMPLETE: {         //捕獲完成
          BitmapHunter hunter = (BitmapHunter) msg.obj;
          dispatcher.performComplete(hunter);
          break;
        }
        case HUNTER_RETRY: {          //重試
          BitmapHunter hunter = (BitmapHunter) msg.obj;
          dispatcher.performRetry(hunter);
          break;
        }
        case HUNTER_DECODE_FAILED: {         //解碼失敗
          BitmapHunter hunter = (BitmapHunter) msg.obj;
          dispatcher.performError(hunter, false);
          break;
        }
    }
  }

第二,關(guān)于ExecutorService

  if (service instanceof PicassoExecutorService) {
      ((PicassoExecutorService) service).adjustThreadCount(info);
    }
//在adjustThreadCount(info)方法里面就是下面這段代碼,很明可以看出,這段代碼是根據(jù)網(wǎng)絡(luò)狀態(tài)信息info來決定線程池個數(shù)的,默認(rèn)是3條線程
 if (info == null || !info.isConnectedOrConnecting()) {
      setThreadCount(DEFAULT_THREAD_COUNT);
      return;
    }
    switch (info.getType()) {
      case ConnectivityManager.TYPE_WIFI:   //wife狀態(tài)下
      case ConnectivityManager.TYPE_WIMAX:  //802·16無線城域網(wǎng),類似于wife
      case ConnectivityManager.TYPE_ETHERNET:  //以太網(wǎng)數(shù)據(jù)連接
        setThreadCount(4);
        break;
      case ConnectivityManager.TYPE_MOBILE:
        switch (info.getSubtype()) {
          case TelephonyManager.NETWORK_TYPE_LTE:        // 4G狀態(tài)下
          case TelephonyManager.NETWORK_TYPE_HSPAP:
          case TelephonyManager.NETWORK_TYPE_EHRPD:
            setThreadCount(3);
            break;
          case TelephonyManager.NETWORK_TYPE_UMTS:       // 3G狀態(tài)下
          case TelephonyManager.NETWORK_TYPE_CDMA:
          case TelephonyManager.NETWORK_TYPE_EVDO_0:
          case TelephonyManager.NETWORK_TYPE_EVDO_A:
          case TelephonyManager.NETWORK_TYPE_EVDO_B:
            setThreadCount(2);
            break;
          case TelephonyManager.NETWORK_TYPE_GPRS:      // 2G狀態(tài)下
          case TelephonyManager.NETWORK_TYPE_EDGE:
            setThreadCount(1);
            break;
          default:
            setThreadCount(DEFAULT_THREAD_COUNT);  
        }
        break;
      default:
        setThreadCount(DEFAULT_THREAD_COUNT);    //默認(rèn)狀態(tài)下是3條
  • 接下來我們講一下Picasso中的load("image src url")方法,源碼中l(wèi)oad()方法有四個
(源代碼如下)
//通過uri參數(shù)獲得RequestCreator對象
public RequestCreator load(Uri uri) {
    return new RequestCreator(this, uri, 0);
  }
//通過請求路徑path,獲取其uri參數(shù)獲得RequestCreator對象
 public RequestCreator load(String path) {
    if (path == null) {
      return new RequestCreator(this, null, 0);
    }
    if (path.trim().length() == 0) {
      throw new IllegalArgumentException("Path must not be empty.");
    }
    return load(Uri.parse(path));
  }
//通過文件file獲得其uri參數(shù),從而獲取RequestCreator對象
public RequestCreator load(File file) {
    if (file == null) {
      return new RequestCreator(this, null, 0);
    }
    return load(Uri.fromFile(file));
  }
//通過指定的請求id來獲取RequestCreator對象
 public RequestCreator load(int resourceId) {
    if (resourceId == 0) {
      throw new IllegalArgumentException("Resource ID must not be zero.");
    }
    return new RequestCreator(this, null, resourceId);
  }
(從上面的四個方法可以看出,其實又可以分為兩類:uri和resourceId。uri又分為file和net睡蟋。)

從上面的代碼可以看出load()方法最終都是需要得到RequestCreator對象,那RequestCreator又有什么作用呢?
RequestCreator是用來配置加載參數(shù)的踏幻。RequestCreator有兩個功能

  1. 配置加載參數(shù)。
    包括placeHolder與error圖片戳杀,加載圖片的大小该面、旋轉(zhuǎn)、居中等屬性信卡。
  2. 執(zhí)行加載隔缀。
    通過調(diào)用into(object)方法進(jìn)行加載。
  • 說完load()方法,接下來得說說into()方法了
    into()可以說是Picasso中比較復(fù)雜的方法,方法有五個,方法體也比較長,代碼我這里就不貼了,大家可以自行翻看,這個方法在RequestCreator里面
    通過源碼分析,其邏輯還是比較清晰的,這里總結(jié)一下:
  1. into會檢查當(dāng)前是否是在主線程上執(zhí)行傍菇。
 long started = System.nanoTime();
    checkMain();
  1. 如果我們沒有提供一個圖片資源并且有設(shè)置placeholder蚕泽,那么就會把我們設(shè)置的placeholder顯示出來,并中斷執(zhí)行。(下面的代碼)
   Drawable drawable =
        placeholderResId != 0 ? picasso.context.getResources().getDrawable(placeholderResId)
            : placeholderDrawable;

    if (!data.hasImage()) {
      picasso.cancelRequest(target);
      target.onPrepareLoad(drawable);
      return;
    }
  1. defered屬性我們一般情況下不需要關(guān)注须妻,只有當(dāng)我們調(diào)用了RequestCreator的fit方法時defered才為true仔蝌,但我們幾乎不會這樣做。
if (deferred) {
      throw new IllegalStateException("Fit cannot be used with get.");
    }
  1. 接下來就是創(chuàng)建了一個Request對象荒吏,我們在前面做得一些設(shè)置都會被封裝到這個Request對象里面敛惊。
Request finalData = createRequest(started);
   String key = createKey(finalData, new StringBuilder());
  1. 檢查我們要顯示的圖片是否可以直接在緩存中獲取,如果有就直接顯示出來好了绰更。
  if (!skipMemoryCache) {
      Bitmap bitmap = picasso.quickMemoryCacheCheck(requestKey);
      if (bitmap != null) {
        picasso.cancelRequest(target);
        target.onBitmapLoaded(bitmap, MEMORY);
        return;
      }
    }
  1. 緩存沒命中瞧挤,那就只能費點事把源圖片down下來了。這個過程是異步的儡湾,并且通過一個Action來完成請求前后的銜接工作特恬。
 Action action =
        new TargetAction(picasso, target, request, skipMemoryCache, errorResId, errorDrawable,
            requestKey);
    picasso.enqueueAndSubmit(action);    //異步提交請求action

3.Glide

Google官方推薦圖庫,在許多Android的原生應(yīng)用中都采用了Glide來加載圖片徐钠。其實Glide與Picasso的使用姿勢是驚人的相似的

 Picasso.with(context).load("image src url").into(ImageView);
 Glide.with(context).load("image src url").into(ImageView);(當(dāng)然這只是它們常用的)

從某種程度上說癌刽,Glide可以看作是Picasso的增強(qiáng)版,所以它有著自己獨特的優(yōu)勢,Glide不僅支持常見的jpg和png格式,還能顯示gif動畫尝丐,甚至是視頻显拜,或者說它已經(jīng)不僅僅是一個普通的圖片加載庫了,而是一個多媒體庫爹袁。另外一個優(yōu)勢是远荠,Glide在內(nèi)存方面的表現(xiàn)相當(dāng)出色,首先它的圖片默認(rèn)格式是RGB565失息,要比Picasso默認(rèn)的ARGB8888節(jié)省更多內(nèi)存譬淳,而且它緩存的不是原始圖片,而是緩存了圖片的實際大小——比如加載的圖片是 19201080的大小盹兢,而在你的App中邻梆,顯示該圖片的ImageView控件大小只有1280720,那么Glide就會很聰明的自動緩存 1280*720大小的圖片蛤迎。

關(guān)于Glide的源碼簡單解析

在Glide中我們經(jīng)常用的一種姿勢就是:

 Glide.with(context).load("image src url").into(ImageView);

那么這里解析也是從這段代碼開始

1.關(guān)于with(context)方法

在源碼中這個方法是靜態(tài)的,重載方法有五個,

   //第一個方法傳入一個上下文,根據(jù)上下文的所屬生命周期來確定需要獲取對象的生命周期
   public static RequestManager with(Context context) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(context);
    }
  //第二個方法傳入一個activity
    public static RequestManager with(Activity activity) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(activity);
    }
  //第三個方法傳入一個FragmentActivity
   public static RequestManager with(FragmentActivity activity) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(activity);
    }
   //第四個方法傳入一個app.Fragment
  @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    public static RequestManager with(android.app.Fragment fragment) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(fragment);
    }
   //第五個方法傳入一個V4兼容包下的Fragment
   public static RequestManager with(Fragment fragment) {
        RequestManagerRetriever retriever = RequestManagerRetriever.get();
        return retriever.get(fragment);
    }

這幾個方法不長,邏輯也很清晰,都是通過一個RequestManagerRetriever的靜態(tài)get()方法得到一個RequestManagerRetriever對象确虱,其實這個靜態(tài)get()方法就是一個單例的具體實現(xiàn),然后再調(diào)用RequestManagerRetriever的實例get()方法,去獲取RequestManager對象替裆。這里需要注意的是實例get()方法中傳入的參數(shù)類型,不同的參數(shù)類型對應(yīng)不同的生命周期,下面代碼就是具體的實現(xiàn)
(源代碼)

private RequestManager getApplicationManager(Context context) {
        // Either an application context or we're on a background thread.
        if (applicationManager == null) {
            synchronized (this) {
                if (applicationManager == null) {
                    // Normally pause/resume is taken care of by the fragment we add to the fragment or activity.
                    // However, in this case since the manager attached to the application will not receive lifecycle
                    // events, we must force the manager to start resumed using ApplicationLifecycle.
                    applicationManager = new RequestManager(context.getApplicationContext(),
                            new ApplicationLifecycle(), new EmptyRequestManagerTreeNode());
                }
            }
        }
        return applicationManager;
    }
    //第一種,傳入全局的上下文,根據(jù)不同類型的上下文執(zhí)行不同的方法
    public RequestManager get(Context context) {
        if (context == null) {
            throw new IllegalArgumentException("You cannot start a load on a null Context");
        } else if (Util.isOnMainThread() && !(context instanceof Application)) {
            if (context instanceof FragmentActivity) {
                return get((FragmentActivity) context);
            } else if (context instanceof Activity) {
                return get((Activity) context);
            } else if (context instanceof ContextWrapper) {
                return get(((ContextWrapper) context).getBaseContext());
            }
        }

        return getApplicationManager(context);
    }
    //第二種,傳入一個FragmentActivity,根據(jù)情況的不同調(diào)用的方法也不同,這里需要注意一下if (Util.isOnBackgroundThread())這種情形表示在子線程執(zhí)行Glide加載圖片,最終會執(zhí)行最上面那個方法
   public RequestManager get(FragmentActivity activity) {
        if (Util.isOnBackgroundThread()) {
            return get(activity.getApplicationContext());
        } else {
            assertNotDestroyed(activity);
            FragmentManager fm = activity.getSupportFragmentManager();  
            return supportFragmentGet(activity, fm);
        }
    }
     //第三種,傳入一個Fragment,也是根據(jù)情況的不同調(diào)用的方法也不同,注意 if (Util.isOnBackgroundThread())這種情形表示在子線程執(zhí)行Glide加載圖片,最終也會執(zhí)行最上面的方法
    public RequestManager get(Fragment fragment) {
        if (fragment.getActivity() == null) {
            throw new IllegalArgumentException("You cannot start a load on a fragment before it is attached");
        }
        if (Util.isOnBackgroundThread()) {
            return get(fragment.getActivity().getApplicationContext());
        } else {
            FragmentManager fm = fragment.getChildFragmentManager();
            return supportFragmentGet(fragment.getActivity(), fm);
        }
    }

  @TargetApi(Build.VERSION_CODES.HONEYCOMB)
    RequestManager fragmentGet(Context context, android.app.FragmentManager fm) {
        RequestManagerFragment current = getRequestManagerFragment(fm); //獲取隱藏的app包下的Fragment
        RequestManager requestManager = current.getRequestManager();
        if (requestManager == null) {
            requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode());
            current.setRequestManager(requestManager);
        }
        return requestManager;
    }

    RequestManager supportFragmentGet(Context context, FragmentManager fm) {
        SupportRequestManagerFragment current = getSupportRequestManagerFragment(fm); //獲取隱藏的V4包下的Fragment
        RequestManager requestManager = current.getRequestManager();
        if (requestManager == null) {
            requestManager = new RequestManager(context, current.getLifecycle(), current.getRequestManagerTreeNode());
            current.setRequestManager(requestManager);
        }
        return requestManager;
    }

其實通過上面的代碼看下來,邏輯上還是比較清晰的,RequestManagerRetriever類中看似有很多個get()方法的重載校辩,什么Context參數(shù),Activity參數(shù)辆童,F(xiàn)ragment參數(shù)等等宜咒,實際上只有兩種情況而已,即傳入Application類型的參數(shù)把鉴,和傳入非Application類型的參數(shù)故黑。

  1. 先看傳入Application參數(shù)的情況儿咱。如果在Glide.with()方法中傳入的是一個Application對象,那么這里就會調(diào)用帶有Context參數(shù)的get()方法重載场晶,然后會調(diào)用getApplicationManager()方法來獲取一個RequestManager對象混埠。Application對象的生命周期即應(yīng)用程序的生命周期,因此Glide并不需要做什么特殊的處理诗轻,它自動就是和應(yīng)用程序的生命周期是同步的钳宪,如果應(yīng)用程序關(guān)閉的話,Glide的加載也會同時終止扳炬。
  2. 再看傳入非Application參數(shù)的情況吏颖。不管你在Glide.with()方法中傳入的是Activity、FragmentActivity恨樟、v4包下的Fragment半醉、還是app包下的Fragment,最終的流程都是一樣的劝术,那就是會向當(dāng)前的Activity當(dāng)中添加一個隱藏的Fragment缩多。具體添加的邏輯是在上述代碼都有注釋說明,分別對應(yīng)的app包和v4包下的兩種Fragment的情況夯尽。那么這里為什么要添加一個隱藏的Fragment呢瞧壮?因為Glide需要知道加載的生命周期登馒。比如說:如果你在某個Activity上正在加載著一張圖片匙握,結(jié)果圖片還沒加載出來,Activity就被用戶關(guān)掉了陈轿,但是如果圖片請求還在繼續(xù),當(dāng)請求的數(shù)據(jù)回來之后沒有界面可以進(jìn)行渲染,這就會造成內(nèi)存泄漏,所以這種情況下肯定是需要取消Glide的網(wǎng)絡(luò)的請求的圈纺。可是Glide并沒有辦法知道Activity的生命周期麦射,于是Glide就使用了添加隱藏Fragment的技巧蛾娶,因為Fragment的生命周期和Activity是同步的,如果Activity被銷毀了潜秋,F(xiàn)ragment是可以監(jiān)聽到的蛔琅,這樣Glide就可以捕獲這個事件并停止網(wǎng)絡(luò)請求了。這里需要注意的一點就是:如果我們是在非主線程當(dāng)中使用的Glide峻呛,那么不管你是傳入的Activity還是Fragment罗售,都會被強(qiáng)制當(dāng)成Application來處理
    總體來說钩述,第一個with()方法其實就是為了得到一個RequestManager對象而已寨躁,然后Glide會根據(jù)我們傳入with()方法的參數(shù)來確定圖片加載的生命周期,接下來我們就分析一下load("image src url")這個方法
2.關(guān)于load("image src url")方法

通過上面的with(context)方法返回的都是RequestManager對象,那么load()方法肯定在RequestManager這個類里面,我們知道Glide是支持圖片URL字符串牙勘、圖片本地路徑等等加載形式的,load重載的方法有很多,我們常用的一般都是load(String string),關(guān)于URL字符串加載形式,下面我們分析一下這種情形:
(源代碼:)

    /**
     * Returns a request builder to load the given {@link java.lang.String}.
     * signature.
     *
     * @see #fromString()
     * @see #load(Object)
     *
     * @param string A file path, or a uri or url handled by {@link com.bumptech.glide.load.model.UriLoader}.
     */
    public DrawableTypeRequest<String> load(String string) {
        return (DrawableTypeRequest<String>) fromString().load(string);
    }

    / * @see #from(Class)
     * @see #load(String)
     */
    public DrawableTypeRequest<String> fromString() {
        return loadGeneric(String.class);
    }

    private <T> DrawableTypeRequest<T> loadGeneric(Class<T> modelClass){
        ModelLoader<T, InputStream> streamModelLoader = Glide.buildStreamModelLoader(modelClass, context);
        ModelLoader<T, ParcelFileDescriptor> fileDescriptorModelLoader =
                Glide.buildFileDescriptorModelLoader(modelClass, context);
        if (modelClass != null && streamModelLoader == null && fileDescriptorModelLoader == null) {
            throw new IllegalArgumentException("Unknown type " + modelClass + ". You must provide a Model of a type for"
                    + " which there is a registered ModelLoader, if you are using a custom model, you must first call"
                    + " Glide#register with a ModelLoaderFactory for your custom model class");
        }
   //傳入StreamStringLoader對象,獲取DrawableTypeRequest對象
        return optionsApplier.apply(
                new DrawableTypeRequest<T>(modelClass, streamModelLoader, fileDescriptorModelLoader, context,
                        glide, requestTracker, lifecycle, optionsApplier));
    }

RequestManager類的代碼是非常多的职恳,關(guān)于load(String string)簡化之后比較重要的方法就只剩下上述代碼中的這三個方法。

  1. 先來看load()方法,這個方法中的邏輯是非常簡單的放钦,只有一行代碼色徘,就是先調(diào)用了fromString()方法再調(diào)用load()方法操禀,然后把傳入的圖片URL地址傳進(jìn)去贺氓。而fromString()方法也極為簡單,就是調(diào)用了loadGeneric()方法床蜘,并且指定參數(shù)為String.class.

  2. 執(zhí)行l(wèi)oadGeneric()方法時辙培,分別調(diào)用了Glide.buildStreamModelLoader()方法和Glide.buildFileDescriptorModelLoader()方法來獲得ModelLoader對象。ModelLoader對象是用于加載圖片的邢锯,而我們給load()方法傳入不同類型的參數(shù)扬蕊,這里也會得到不同的ModelLoader對象。由于我們剛才傳入的參數(shù)是String.class丹擎,因此最終得到的是StreamStringLoader對象尾抑,它是實現(xiàn)了ModelLoader接口的。

  3. 最后可以看到蒂培,loadGeneric()方法是要返回一個DrawableTypeRequest對象的再愈,因此在loadGeneric()方法的最后又new了一個DrawableTypeRequest對象,然后把剛才獲得的ModelLoader對象(StreamStringLoader對象)护戳,還有其他的一些參數(shù)傳進(jìn)去翎冲。
    這里就可以看到如果得到一個DrawableTypeRequest對象,那這里面肯定是有所作為的(源碼如下:)

/**
     * Attempts to always load the resource as a {@link android.graphics.Bitmap}, even if it could actually be animated.
     *
     * @return A new request builder for loading a {@link android.graphics.Bitmap}
     */
    public BitmapTypeRequest<ModelType> asBitmap() {
        return optionsApplier.apply(new BitmapTypeRequest<ModelType>(this, streamModelLoader,
                fileDescriptorModelLoader, optionsApplier));
    }

 public GifTypeRequest<ModelType> asGif() {
        return optionsApplier.apply(new GifTypeRequest<ModelType>(this, streamModelLoader, optionsApplier));
    }

這里就可以看到我們經(jīng)常使用的 asBitmap() 和 asGif(),這兩個方法分別是用于強(qiáng)制指定加載靜態(tài)圖片動態(tài)圖片,而從源碼中可以看出,它們分別又創(chuàng)建了一個BitmapTypeRequest和GifTypeRequest媳荒,如果沒有進(jìn)行強(qiáng)制指定的話抗悍,那默認(rèn)就是使用DrawableTypeRequest

上面的fromString()方法會返回一個DrawableTypeRequest對象钳枕,接下來會調(diào)用這個對象的load()方法缴渊,把圖片的URL地址傳進(jìn)去。通過對DrawableTypeRequest類的查看,沒有找到load()方法,所以在DrawableTypeRequest的父類DrawableRequestBuilder中可以看到load()方法.

 @Override
    public DrawableRequestBuilder<ModelType> load(ModelType model) {
        super.load(model);
        return this;
    }

 /**
     * {@inheritDoc}
     *
     * <p>
     *     Note - If no transformation is set for this load, a default transformation will be applied based on the
     *     value returned from {@link android.widget.ImageView#getScaleType()}. To avoid this default transformation,
     *     use {@link #dontTransform()}.
     * </p>
     *
     * @param view {@inheritDoc}
     * @return {@inheritDoc}
     */
    @Override
    public Target<GlideDrawable> into(ImageView view) {
        return super.into(view);
    }

DrawableRequestBuilder中有很多個方法鱼炒,這些方法其實就是Glide絕大多數(shù)的API了衔沼。通過源碼,我們可以看到load()和into()這兩個方法了,所以我們總結(jié)一下:最終load()方法返回的其實就是一個DrawableTypeRequest對象。那么接下來分析into()方法中的邏輯昔瞧。

3.關(guān)于into()方法的分析

從上面的代碼我們可以看到DrawableRequestBuilder類里面的into()方法只有

return super.into(view); 

這說明into()真正的實現(xiàn)邏輯是在DrawableRequestBuilder的父類GenericRequestBuilder,所以into()方法需要在這個類進(jìn)行分析

    /**
     * Sets the {@link ImageView} the resource will be loaded into, cancels any existing loads into the view, and frees
     * any resources Glide may have previously loaded into the view so they may be reused.
     *
     * @see Glide#clear(android.view.View)
     *
     * @param view The view to cancel previous loads for and load the new resource into.
     * @return The {@link com.bumptech.glide.request.target.Target} used to wrap the given {@link ImageView}.
     */
    public Target<TranscodeType> into(ImageView view) {
        Util.assertMainThread();
        if (view == null) {
            throw new IllegalArgumentException("You must pass in a non null View");
        }

        if (!isTransformationSet && view.getScaleType() != null) {
            switch (view.getScaleType()) {
                case CENTER_CROP:
                    applyCenterCrop();
                    break;
                case FIT_CENTER:
                case FIT_START:
                case FIT_END:
                    applyFitCenter();
                    break;
                //$CASES-OMITTED$
                default:
                    // Do nothing.
            }
        }

        return into(glide.buildImageViewTarget(view, transcodeClass));
    }

在上面代碼中into(ImageView view)方法里面最后一行代碼先是調(diào)用glide.buildImageViewTarget()方法,這個方法會構(gòu)建出一個Target對象指蚁,Target對象則是用來最終展示圖片用的,如果我們跟進(jìn)去的話會看到如下代碼:

<R> Target<R> buildImageViewTarget(ImageView imageView, Class<R> transcodedClass) {
        return imageViewTargetFactory.buildTarget(imageView, transcodedClass);
    }

這里可以看到會通過imageViewTargetFactory調(diào)用buildTarget(imageView, transcodedClass)這個方法,如果繼續(xù)查看源碼,會發(fā)現(xiàn)在buildTarget()方法中會根據(jù)傳入的class參數(shù)來構(gòu)建不同的Target對象硬爆。這個class參數(shù)其實基本上只有兩種情況欣舵,如果你在使用Glide加載圖片的時候調(diào)用了asBitmap()方法,那么這里就會構(gòu)建出BitmapImageViewTarget對象缀磕,否則的話構(gòu)建的都是GlideDrawableImageViewTarget對象缘圈。

這里glide.buildImageViewTarget(view, transcodeClass),我們得到一個GlideDrawableImageViewTarget對象,然后回到

    return into(glide.buildImageViewTarget(view, transcodeClass));

我們可以看到into(GlideDrawableImageViewTarget對象)的源碼

 /**
     * Set the target the resource will be loaded into.
     *
     * @see Glide#clear(com.bumptech.glide.request.target.Target)
     *
     * @param target The target to load the resource into.
     * @return The given target.
     */
    public <Y extends Target<TranscodeType>> Y into(Y target) {
        Util.assertMainThread();
        if (target == null) {
            throw new IllegalArgumentException("You must pass in a non null Target");
        }
        if (!isModelSet) {
            throw new IllegalArgumentException("You must first set a model (try #load())");
        }

        Request previous = target.getRequest();

        if (previous != null) {
            previous.clear();
            requestTracker.removeRequest(previous);
            previous.recycle();
        }

        Request request = buildRequest(target); //調(diào)用buildRequest()方法構(gòu)建出了一個Request對象
        target.setRequest(request);
        lifecycle.addListener(target);
        requestTracker.runRequest(request);  //執(zhí)行Request對象

        return target;
    }

上面代碼構(gòu)建出來的Request對象是用來發(fā)出加載圖片請求的劣光,它是Glide中非常關(guān)鍵的一個組件.這就是我們經(jīng)常用的with(),load(),into()最表面的意思,里面還有很多很多內(nèi)容沒有寫出來,我也是參考郭霖大神的文章以及自己的拙見才寫這么點東西的,也算是做了一次筆記吧!

關(guān)于Glide優(yōu)化

  1. 配置使用Volley和OkHttp來加載圖片
    Volley和OkHttp是項目中使用最廣泛的兩個網(wǎng)絡(luò)庫,也是兩個相對來說速度比較快的糟把,Glide默認(rèn)使用的是HttpUrlConnection的方式請求網(wǎng)絡(luò)绢涡,其效率是比較低的,可以使用Volley或者OkHttp(和項目使用的網(wǎng)絡(luò)請求庫一致)作為Glide的網(wǎng)絡(luò)請求方式.

Gradle配置

//使用volley
dependencies {
    compile 'com.github.bumptech.glide:volley-integration:1.4.0@aar'
    //compile 'com.mcxiaoke.volley:library:1.0.8'
}
//使用okhttp
dependencies {
    compile 'com.github.bumptech.glide:okhttp-integration:1.4.0@aar'
    //compile 'com.squareup.okhttp:okhttp:2.2.0'
}

當(dāng)在Library庫中使用aar的時候遣疯,Library中的GlideModule會自動合并 到主項目中mainfest文件中雄可,當(dāng)使用jar包導(dǎo)入時,需要手動去合并Library合并GlideModule或者使用自己配置的GlideModule缠犀。

Maven配置

//使用volley
<dependency>
    <groupId>com.github.bumptech.glide</groupId>
    <artifactId>volley-integration</artifactId>
    <version>1.4.0</version>
    <type>aar</type>
</dependency>
<dependency>
    <groupId>com.mcxiaoke.volley</groupId>
    <artifactId>library</artifactId>
    <version>1.0.8</version>
    <type>aar</type>
</dependency>

//使用okhttp
<dependency>
    <groupId>com.github.bumptech.glide</groupId>
    <artifactId>okhttp-integration</artifactId>
    <version>1.4.0</version>
    <type>aar</type>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp</groupId>
    <artifactId>okhttp</artifactId>
    <version>2.2.0</version>
    <type>jar</type>
</dependency>

jar的形式引入

如果通過Maven数苫,Ant 或者其它系統(tǒng)工具來構(gòu)建的話,是不支持manifest 文件合并的辨液,你必須手動在AndroidManifest.xml添加GlideModule metadata 屬性虐急。

//使用volley
<meta-data
    android:name="com.bumptech.glide.integration.volley.VolleyGlideModule"
    android:value="GlideModule" />
//使用okhttp
<meta-data
    android:name="com.bumptech.glide.integration.okhttp.OkHttpGlideModule"
    android:value="GlideModule" />

//添加混淆配置
-keep class com.bumptech.glide.integration.volley.VolleyGlideModule
-keep class com.bumptech.glide.integration.okhttp.OkHttpGlideModule

其實如果我們想使用OkHttp3作為Glide網(wǎng)絡(luò)請求方式,可以自行查看相關(guān)文檔.

4.Fresco

這個號稱是Android平臺上目前最為強(qiáng)大的圖片加載庫,由Facebook公司開發(fā)滔迈。與Glide一樣止吁,F(xiàn)resco也是支持gif動畫顯示,而且在內(nèi)存方面的表現(xiàn)更加優(yōu)秀燎悍。由于將圖片放在Ashmem(匿名共享內(nèi)存)中敬惦,大大降低了App的內(nèi)存占用(因為Ashmem沒有被統(tǒng)計到App的內(nèi)存使用里),再加上各種級別優(yōu)化谈山,使得Fresco基本上告別了OOM俄删,而且Fresco的圖片直接顯示為ARGB8888這種最高質(zhì)量級別,即使是在這種高質(zhì)量的情況下依然保證了比其他庫更少的內(nèi)存占用勾哩,這就是Fresco最吸引人的地方抗蠢。而且類似于進(jìn)度監(jiān)聽举哟、緩存策略等思劳,也是非常完善的,總之作為一個圖片加載庫妨猩,F(xiàn)resco在功能和性能方面已經(jīng)趨于完美了潜叛。

Picasso,Glide,Fresco的區(qū)別

  • Picasso :和Square的網(wǎng)絡(luò)庫一起能發(fā)揮最大作用,因為Picasso可以選擇將網(wǎng)絡(luò)請求的緩存部分交給了okhttp實現(xiàn)壶硅。使用4.0+系統(tǒng)上的HTTP緩存來代替磁盤緩存.
    Picasso 底層是使用OkHttp去下載圖片,所以Picasso底層網(wǎng)絡(luò)協(xié)議為Http.

  • Glide:模仿了Picasso的API威兜,而且在它的基礎(chǔ)上加了很多的擴(kuò)展(比如gif等支持),Glide默認(rèn)的Bitmap格式是RGB_565庐椒,比Picasso默認(rèn)的ARGB_8888格式的內(nèi)存開銷要小一半椒舵;Picasso緩存的是全尺寸的(只緩存一種),而Glide緩存的是跟ImageView尺寸相同的(即5656和128128是兩個緩存) 约谈。
    Glide 底層默認(rèn)使用的是HttpUrlConnection的方式請求網(wǎng)絡(luò),所以Glide的底層網(wǎng)絡(luò)協(xié)議也為Http.

  • Fresco:最大的優(yōu)勢在于5.0以下(最低2.3)的bitmap加載笔宿。在5.0以下系統(tǒng)犁钟,F(xiàn)resco將圖片放到一個特別的內(nèi)存區(qū)域(Ashmem區(qū),這個區(qū)域沒有被統(tǒng)計到App的內(nèi)存使用里)。當(dāng)然泼橘,在圖片不顯示的時候涝动,占用的內(nèi)存會自動被釋放。這會使得APP更加流暢炬灭,減少因圖片內(nèi)存占用而引發(fā)的OOM醋粟。為什么說是5.0以下,因為在5.0以后系統(tǒng)默認(rèn)就是存儲在Ashmem區(qū)了重归。
    Image pipeline 默認(rèn)使用HttpURLConnection米愿。應(yīng)用可以根據(jù)自己需求使用不同的網(wǎng)絡(luò)庫。Fresco的Image Pipeline負(fù)責(zé)圖片的獲取和管理鼻吮。圖片可以來自遠(yuǎn)程服務(wù)器吗货,本地文件,或者Content Provider狈网,本地資源宙搬。壓縮后的文件緩存在本地存儲中,Bitmap數(shù)據(jù)緩存在內(nèi)存中.

    功能性 Fresco > Glide > Picasso
    包大小 Fresco > Glide > Picasso

主要功能:

共有的功能:根據(jù)content生命周期進(jìn)行圖片加載或暫停和恢復(fù)拓哺,緩存圖片到本地勇垛。
加載圖片格式及大小:

  • Picasso:下載全尺寸圖片,load全尺寸圖片到imageview上士鸥,圖片使用ARGB-8888格式闲孤。
  • Glide:包含Picasso功能,默認(rèn)下載不同圖片至本地烤礁,load 對應(yīng)imageview尺寸的圖片讼积,圖片使用ARGB-565格式。
    可加載gif脚仔、縮略圖勤众、視頻靜態(tài)圖片、轉(zhuǎn)換字節(jié)數(shù)組鲤脏、顯示動畫们颜。
  • Fresco:結(jié)合Picasso、Glide優(yōu)點猎醇,更適用于加載大量圖片窥突。另支持漸進(jìn)式顯示圖片、WebP格式圖片硫嘶。

對圖片轉(zhuǎn)換

  1. Picasso: picasso-transformations,:結(jié)合picasso,支持將圖片轉(zhuǎn)換為其他形狀后顯示阻问。
  2. Glide: glide-transformations:結(jié)合glide,支持將圖片轉(zhuǎn)換為其他形狀后顯示。
  3. Fresco: android-gpuimage:支持將圖片變幻為各種濾鏡效果沦疾。

總結(jié):

  • Picasso所能實現(xiàn)的功能称近,Glide都能做贡蓖,無非是所需的設(shè)置不同。但是Picasso體積比起Glide小太多,如果項目中網(wǎng)絡(luò)請求本身用的就是okhttp或者retrofit(本質(zhì)還是okhttp)煌茬,那么建議用Picasso斥铺,體積會小很多(Square全家桶的干活)。
  • Glide的好處是大型的圖片流坛善,比如gif晾蜘、Video,如果你們是做美拍眠屎、愛拍這種視頻類應(yīng)用剔交,建議使用。
  • Fresco在5.0以下的內(nèi)存優(yōu)化非常好改衩,代價就是體積也非常的大岖常,按體積算Fresco>Glide>Picasso不過在使用起來也有些不便(小建議:它只能用內(nèi)置的一個ImageView來實現(xiàn)這些功能,用起來比較麻煩葫督,我們通常是根據(jù)Fresco自己改改竭鞍,直接使用他的Bitmap層).

該如何選擇圖片加載庫?

如果你手中的項目比較老舊橄镜,而且代碼量較大偎快,你又沒什么時間去大改,那么繼續(xù)維持當(dāng)前的選擇是比較穩(wěn)妥的辦法洽胶。如果是新上馬的項目晒夹,那么UIL由于不再維護(hù)、Picasso基本被Glide全方位超越姊氓,我推薦使用Glide或Fresco丐怯。如果你的App里,圖片特別多翔横,而且都是很大读跷、質(zhì)量很高的圖 片,而且你不太在乎App的體積(可能性不大)棕孙,那么Fresco就是很好的選擇了舔亭,而Glide相比較Fresco,Glide要輕量一些,而且是Google官方推薦蟀俊,所以在多數(shù)時候,會是開發(fā)者的首選订雾。話說回來肢预,如果你對這些圖庫都不滿意,那可以自己寫一個,如果可以的話!!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市洼哎,隨后出現(xiàn)的幾起案子烫映,更是在濱河造成了極大的恐慌沼本,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,372評論 6 498
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件正塌,死亡現(xiàn)場離奇詭異鳍置,居然都是意外死亡憔狞,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,368評論 3 392
  • 文/潘曉璐 我一進(jìn)店門辫红,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人祝辣,你說我怎么就攤上這事贴妻。” “怎么了蝙斜?”我有些...
    開封第一講書人閱讀 162,415評論 0 353
  • 文/不壞的土叔 我叫張陵名惩,是天一觀的道長。 經(jīng)常有香客問我孕荠,道長娩鹉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,157評論 1 292
  • 正文 為了忘掉前任稚伍,我火速辦了婚禮底循,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘槐瑞。我一直安慰自己熙涤,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 67,171評論 6 388
  • 文/花漫 我一把揭開白布困檩。 她就那樣靜靜地躺著祠挫,像睡著了一般。 火紅的嫁衣襯著肌膚如雪悼沿。 梳的紋絲不亂的頭發(fā)上等舔,一...
    開封第一講書人閱讀 51,125評論 1 297
  • 那天,我揣著相機(jī)與錄音糟趾,去河邊找鬼慌植。 笑死,一個胖子當(dāng)著我的面吹牛义郑,可吹牛的內(nèi)容都是我干的蝶柿。 我是一名探鬼主播,決...
    沈念sama閱讀 40,028評論 3 417
  • 文/蒼蘭香墨 我猛地睜開眼非驮,長吁一口氣:“原來是場噩夢啊……” “哼交汤!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起劫笙,我...
    開封第一講書人閱讀 38,887評論 0 274
  • 序言:老撾萬榮一對情侶失蹤芙扎,失蹤者是張志新(化名)和其女友劉穎星岗,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體戒洼,經(jīng)...
    沈念sama閱讀 45,310評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡俏橘,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,533評論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了圈浇。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片寥掐。...
    茶點故事閱讀 39,690評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖汉额,靈堂內(nèi)的尸體忽然破棺而出曹仗,到底是詐尸還是另有隱情,我是刑警寧澤蠕搜,帶...
    沈念sama閱讀 35,411評論 5 343
  • 正文 年R本政府宣布怎茫,位于F島的核電站,受9級特大地震影響妓灌,放射性物質(zhì)發(fā)生泄漏轨蛤。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,004評論 3 325
  • 文/蒙蒙 一虫埂、第九天 我趴在偏房一處隱蔽的房頂上張望祥山。 院中可真熱鬧,春花似錦掉伏、人聲如沸缝呕。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,659評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽供常。三九已至,卻和暖如春鸡捐,著一層夾襖步出監(jiān)牢的瞬間栈暇,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,812評論 1 268
  • 我被黑心中介騙來泰國打工箍镜, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留源祈,地道東北人。 一個月前我還...
    沈念sama閱讀 47,693評論 2 368
  • 正文 我出身青樓色迂,卻偏偏與公主長得像香缺,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子脚草,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,577評論 2 353

推薦閱讀更多精彩內(nèi)容