本文GitHub專案地址 : 統一的圖片載入架構
前言
我們目前的專案對於圖片載入的需求很大,一直以來,我們使用的是Glide作為圖片載入的底層包,為了節省圖片佔用的空間,希望使用webp的格式來展示動圖。由於Glide不支援Animated WebP(即WebP動圖) 格式,所以我們需要把底層的Glide替換為Fresco。相信用過這兩個包的同學都知道,兩者的差異還是比較大的,想要保證在不大量修改程式碼,也不入侵業務程式碼的前提下遷移到Fresco上,是個值得思考的問題。
由於之前已經把專案的圖片載入模組重構整合了一次,把圖片載入和業務程式碼有效分離開,因此這次遷移也算有一個較好的基礎。至於整合的具體思路以及為什麼整合這些模組的意義,我在《封裝並實現統一的圖片載入架構》文章裡面已經講了很多,沒看過那篇文章的同學建議看看,因為接下來就是在這篇文章的基礎上去分析如何遷移到Fresco上的問題。
替換Glide遇到的難題
我們都知道,Glide是使用了ImageView來載入圖片的,而實際專案中還會有我們自定義的ImageView,比如CircleImageView等,而Fresco則是通過DraweeView來載入圖片的,這是一個非常嚴重的問題。
所以我們需要考慮的是在專案遷移到Fresco上的時候,到底是想辦法沿用ImageView來載入圖片,還是想辦法使用SimpleDraweeView來載入圖片,如果是前者,那麼我們應該怎麼做?假如是後者,要怎樣才能保證Fresco的程式碼不會大量入侵到業務程式碼中,同時又能相容原來專案的介面呢?
Tips : 對於Fresco自己的圖片載入容器DraweeView,目前暫時繼承自ImageView,但是官方表示未來會直接繼承自View,我們開發時常常使用DraweeView的子類SimpleDraweeView來載入圖片。
解決的思路
方案一:沿用ImageView載入
- 思路
- 尋找一種辦法從Fresco中獲取圖片資料,然後使用ImageView來載入。而正好,Fresco有一個Image pipeline 作為圖片載入的控制核心,它負責判斷圖片快取,並且能從各種渠道解碼,變換,返回bitmap,然後快取起來。也就是說,我們可以直接通過這個模組去獲取快取的bitmap,把它載入到ImageView上,這樣的話,就不需要使用Fresco的DraweeView了。
優點
在我看官方文件的時候,暗喜了良久。因為如果可以這麼做的話,那麼這就是最簡單的直接的遷移到Fresco上的方法。
- 缺陷
- 我發現如果使用Image Pipline去獲取Bitmap來載入圖片,那麼也就必須放棄Fresco特有的Drawee各種效果配置。不僅如此,Fresco對自己返回的bitmap的生命週期控制十分嚴格,以至於我們在使用時必須十分小心,而且一旦不再使用必須釋放引用,換言之,我們必須手動控制整個圖片載入的快取釋放策略,稍有不慎就會報錯。由於我們的專案是一個直播App,許多複雜的業務都涉及到圖片載入模組的使用,我無法保證能在那麼多複雜得場景中控制好Bitmap的生命週期。
- 結論
- 這種策略看起來最簡單,實際上需要付出的代價極大,基本上是無法承受的。Pass。
方案二 :直接替換(使用SimpleDraweeView)
- 思路:
- 既然我們自己控制圖片載入和記憶體回收太過吃力,就只能把這些工作交給Fresco,最直接的方法就是,把xml,Java檔案中的涉及到ImageView以及自定義的ImageView的引用貼上複製成SimpleDraweeView就行了(而且Fresco雖然有自己的DraweeView,但是也都繼承自ImageView,這也會給我們帶來一些便利)。當然,需要修改的還會有很多。當第一個思路行不通的時候,這個思路可以說是很容易想到的,無非就是統一替換的問題,簡單粗暴,不會有太多難題。
缺陷:
- 但是,能最先想到的未必是最好的方案,首先,xml,Java檔案中大量出現Fresco的引用,是個比較糟糕的情況。會破壞我們的之前對於圖片載入框架的一些封裝,以後再修改框架,或者替換Fresco就很被動。(想一想,萬一以後Glide以後支援了Webp格式,而我們又對Fresco包的大小很不爽,那麼就免不了要替換掉Fresco了)
結論:
- 難度很低,工作量較大,也會破壞專案的封裝性。保留作為萬不得已的選擇
方案三 : 動態替換(使用SimpleDraweeView)
- 思路:
保持原有的程式碼不變,當需要呼叫圖片載入介面的時候,動態的把原有的ImageView及其子類替換成Fresco的SimpleDraweeView,然後使用SimpleDraweeView去載入圖片。
具體操作大概就是當呼叫瞭如下載入介面的時候,把ImageView動態替換成SimpleDraweeView,然後載入圖片
void showImage(ImageLoaderOptions options); // ImageLoaderOptions包含ImageView,Url 等等複製程式碼
- 優勢:
- 這個方案很好的解決了上面的擔憂,既不改動原有的程式碼,也不將Fresco的程式碼大量參雜到業務中去,將來的替換或者擴充將十分輕鬆。
- 缺陷:
- 這個方案在實現上卻有很多難點,比如如何動態替換,原有的ImageView中設定的click監聽事件怎麼轉移過去?最重要的一個問題是:listview,recycleview中的ImageView(快取)如何替換?
- 結論:
- 在實際測試的過程中,我發現RecycleView中載入的ImageView會有快取的問題,無法完成替換,因此這個方案也只能拋棄掉。
方案四 : 動態新增(使用SimpleDraweeView)
- 思路:
- 保持原有程式碼中的ImageView不變,在原有的佈局關係中插入一個“一模一樣”的SimpleDraweeView,保證SimpleDraweeView在父容器中的位置正好和ImageView完全重合,Click事件依然還保留原來的ImageView上,RecycleView的快取問題也可以避免,這樣,SimpleDraweeView負責載入顯示圖片,ImageView負責互動。
- 優勢:
- 這是方案三延伸出的一個折中方案,避免了很多難題,實現起來也會很簡單,而且程式碼的入侵度很低。
- 缺陷:
- 這個方案稍遜於方案三,但可行性極高。假如原專案中對ImageView的使用得當,那麼幾乎就無需再修改專案程式碼了。而且也無須擔心物件過多是否導致記憶體的問題,因為真正的記憶體消耗的大戶都是圖片,ImageView這個物件本身並不大。
比較方案
上面四個方案是我所想到的所有的實現方案,基本上方案三和方案四實現後的效果是最佳的。但是方案三的難度更大,所以綜合來看,方案四的價效比更好。(目前專案使用的就是方案四,效果良好,執行正常)
實踐
確定了方案,我們可以開始實踐了,結合方案四的思路,程式碼實現上基本上是沒有什麼難度了。
對於如何整合圖片載入模組,請務必參考之前的文章《封裝並實現統一的圖片載入架構》
Fresco載入模組的重點程式碼實現如下:
FrescoImageLoader.java
// 專案種幾乎所有的圖片載入都呼叫到了這裡
@Override
public void showImage(@NonNull ImageLoaderOptions options) {
showImgaeDrawee(options);
}
private void showImgaeDrawee(ImageLoaderOptions options) {
// 這個View就是載入圖片的ImageView
View view=options.getViewContainer();
SimpleDraweeView drawee=null;
Class clazz=null;
GenericDraweeHierarchy hierarchy=null;
GenericDraweeHierarchyBuilder hierarchyBuilder = GenericDraweeHierarchyBuilder.newInstance(getResources());
// 由於自己的專案中有好幾種ImageView,因此需要一一判斷
if (view instanceof SquareRImageView) {
clazz= SquareRImageView.class;
drawee=getDraweeView(view,clazz);
if (drawee != null) {
drawee.setAspectRatio(1);
}
}else if (view instanceof CircleImageView){
clazz= CircleImageView.class;
// 傳入
drawee=getDraweeView(view,clazz);
hierarchyBuilder.setFadeDuration(400).setRoundingParams(RoundingParams.asCircle());
}else if (view instanceof SimpleDraweeView){
drawee= (SimpleDraweeView) view;
hierarchy=drawee.getHierarchy();
}else if(view instanceof ImageView){
clazz= ImageView.class;
drawee=getDraweeView(view,clazz);
}
else {
Logger.i("no type !!");
return;
}
if (drawee != null) {
// 圖片地址
Uri uri=Uri.parse(options.getUrl());
if (options.getHolderDrawable()!=-1) {
hierarchyBuilder.setPlaceholderImage(options.getHolderDrawable());
}
if (options.getErrorDrawable()!=-1) {
hierarchyBuilder.setFailureImage(options.getErrorDrawable());
}
if (hierarchy == null) {
hierarchy= hierarchyBuilder.build();
}
drawee.setHierarchy(hierarchy);
PipelineDraweeControllerBuilder controllerBuilder=Fresco.newDraweeControllerBuilder().setUri(uri).setAutoPlayAnimations(true);
ImageRequestBuilder imageRequestBuilder= ImageRequestBuilder.newBuilderWithSource(uri);
if (options.getImageSize() != null) {
imageRequestBuilder.setResizeOptions(new ResizeOptions(getSize(options.getImageSize().getWidth(),view), getSize(options.getImageSize().getWidth(),view)));
}
if (options.isBlurImage()) {
// 是否做高斯模糊
imageRequestBuilder.setPostprocessor(new BlurPostprocessor(view.getContext().getApplicationContext(), 15));
}
ImageRequest request =imageRequestBuilder.build();
controllerBuilder.setImageRequest(request);
DraweeController controller=controllerBuilder.build();
drawee.setController(controller);
}
}複製程式碼
在圖片載入時,首先需要判斷載入圖片的容器是ImageView還是ImageView的子類,因為這意味著對圖片不同的處理,比如CircleImageView意味著是載入一個圓圖,所以我們需要設定SimpleDraweeView為圓圖等等。
// 傳入載入圖片的ImageView,返回一個相同位置,相同大小的SimpleDraweeView
private SimpleDraweeView getDraweeView(View viewContainer,Class<?> classType) {
if (viewContainer instanceof SimpleDraweeView){
return (SimpleDraweeView) viewContainer;
}
SimpleDraweeView mDraweeView=null;
if (classType.isInstance(viewContainer)){
FrameLayout layout=new FrameLayout(viewContainer.getContext());
if(viewContainer.getParent() instanceof FrameLayout){
FrameLayout parent= (FrameLayout) viewContainer.getParent();
FrameLayout.LayoutParams params= (FrameLayout.LayoutParams) viewContainer.getLayoutParams();
// 這個方法來完成最終的新增
mDraweeView=exchangeChilde(parent,viewContainer,params);
}else if(viewContainer.getParent() instanceof RelativeLayout){
RelativeLayout parent= (RelativeLayout) viewContainer.getParent();
RelativeLayout.LayoutParams params= (RelativeLayout.LayoutParams) viewContainer.getLayoutParams();
mDraweeView=exchangeChilde(parent,viewContainer,params);
}else if(viewContainer.getParent() instanceof LinearLayout){
// 當ImageView 的Parent時LinearLayout的時候,處理會有一些不同
LinearLayout parent= (LinearLayout) viewContainer.getParent();
LinearLayout.LayoutParams params= (LinearLayout.LayoutParams) viewContainer.getLayoutParams();
layout.setLayoutParams(params);
addToViewGroup(parent,viewContainer,layout);
layout.addView(viewContainer);
mDraweeView=exchangeChilde(layout,viewContainer,params);
}else{
//基本上可以涵蓋上面一個專案中用到的佈局型別了,
//其他的型別如Tablayout等等,視實際情況而定
ViewParent parent=viewContainer.getParent();
Logger.i("");
}
}else{
Logger.i("");
}
return mDraweeView;
}
// 該方將ImageView從原來的Parent種移除,並新增到一個FrameLayout中去
private void addToViewGroup(ViewGroup parent,View viewOld,View viewNew){
for (int i = 0; i < parent.getChildCount(); i++) {
if (parent.getChildAt(i).equals(viewOld)) {
parent.removeView(viewOld);
parent.addView(viewNew,i);
return;
}
}
}複製程式碼
這裡需要判斷ImageView的父容器ViewGroup是那些,需要著重區分LinearLayout這個父佈局,因為如果ImageView的父容器是LinearLayout,那麼我們就無法在LinearLayout中新增一個大小相同,位置和ImageView重合的SimpleDraweeView來載入圖片了,因此,此時我們需要把這個ImageView拿出來,把它和SimpleDraweeView一起裝在FrameLayout中,然後在把FrameLayout新增到ImageView原來在LinearLayout中所處的位置。
// 緊挨著ImageView新增SimpleDraweeView到原來的ImageView的位置
private SimpleDraweeView exchangeChilde(ViewGroup parent, View testImageView, ViewGroup.LayoutParams layoutParams) {
SimpleDraweeView draweeview =null;
for (int i = 0; i < parent.getChildCount(); i++) {
if (testImageView.equals(parent.getChildAt(i))) {
if (testImageView instanceof ImageView) {
ImageView img= (ImageView) testImageView;
img.setBackgroundDrawable(null);
img.setImageDrawable(null);
}
if (i+1 < parent.getChildCount()) {
View child=parent.getChildAt(i+1);
// 此處理應做更加仔細的判斷
if (child instanceof SimpleDraweeView) {
return (SimpleDraweeView) child;
}
}
draweeview=new SimpleDraweeView(testImageView.getContext());
draweeview.setLayoutParams(layoutParams);
parent.addView(draweeview,i+1);
return draweeview;
}
}
return draweeview;
}複製程式碼
以上基本上就是以Fresco來實現圖片載入模組的核心程式碼了,基本可以覆蓋原有的Glide的功能,並且入侵度低,無需修改原有程式碼,隨時可替換。
勘誤
暫無
專案已經上傳了github,點此獲取,求star! 求follow !