項目實訓(一) 網絡圖片下載進度監聽

我們的項目是一個圖片分享社區,所以要對圖片處理的一些邏輯進行學習。


學習自郭霖大神glide系列博客,自己經過思考重新整理。


一。全局替換加載策略

首先要知道Glide在實例化時的工作(也就是框架的初始化):

設計模式是builder模式,可以分爲兩部分,首先builder調用Glide構造器傳入所需模塊(內存策略,圖片解碼模式等),然後Glide構造器中對各種類型的圖片需求加載進行註冊(如File,int,String,GlideUrl等,需要注意的是String類型方法實際調用了GlideUrl的方法)。

相關代碼:


遍歷Manifest文件尋找所有的GlideModule放入列表,然後遍歷執行這些GlideModule的applyOptions(此方法中可以對builder的幾個模塊使用set方法配置),然後調用構造器生成Glide實例,然後遍歷執行這些GlideModule的registerComponents(此方法中可以調用Glide單例註冊新的圖片類型加載策略,或者替換默認的策略。)


綜上,因爲我們的目標是對String(實際是GlideUrl)類型加載請求進行改寫,所以需要自定義一個GlideModule並且在其registerComponents中對GlideUrl的策略進行替換。另外,因爲網絡請求要用okhttp而不是默認的httpUrlClient所以要添加okhttp的依賴。


首先自定義MyGlideModule  implements GlideModule,然後在Manifest文件中註冊:


看看原本Glide構造器中是怎麼樣註冊的:


前兩個參數都不用變,我們只需要自定義第三個參數,參照原HttpUrlGlideUrlLoader自定義MyModelLoader imp ModelLoader<GlideUrl,InputStream>。

public class MyModolLoader implements ModelLoader<GlideUrl,InputStream> {

    private OkHttpClient client;

    public MyModolLoader(OkHttpClient client) {
        this.client = client;
    }

    @Override
    public DataFetcher getResourceFetcher(GlideUrl model, int width, int height) {
        return new MyDataFetcher(client,model);
    }

    public static class Factory implements ModelLoaderFactory<GlideUrl,InputStream>{

        private OkHttpClient client;

        public Factory(){

        }

        public Factory(OkHttpClient client) {
            this.client = client;
        }

        @Override
        public ModelLoader<GlideUrl, InputStream> build(Context context, GenericLoaderFactory factories) {
            return new MyModolLoader(getOkhttpClient());
        }

        private OkHttpClient getOkhttpClient() {
            if (client==null){
                synchronized (this){
                    if (client==null){
                        client=new OkHttpClient();
                    }
                }
            }
            return client;
        }

        @Override
        public void teardown() {

        }
    }
}


這裏有一個構造器傳入了一個okhttpclient,因爲後面的操作需要用到我們改造的okhttpclient,用了兩次判空做單例。

這個類可以看到並沒什麼操作,只是將OkHttpClient和GlideUrl交給了一個DataFetcher.

所以自定義MyDataFetcher imp DataFetcher<InputStream>(依然是參照源碼寫):

public class MyDataFetcher implements DataFetcher<InputStream> {

    private OkHttpClient client;
    private InputStream stream;
    private GlideUrl glideUrl;
    private volatile boolean isCanceled = false;
    private ResponseBody responseBody;

    public MyDataFetcher(OkHttpClient client, GlideUrl glideUrl) {
        this.client = client;
        this.glideUrl = glideUrl;
    }

    @Override
    public InputStream loadData(Priority priority) throws Exception {
        Log.v("tag","loaddata");
        Request.Builder requestBuilder = new Request.Builder()
                .url(glideUrl.toStringUrl());
        for (Map.Entry<String, String> headerEntry : glideUrl.getHeaders().entrySet()) {
            String key = headerEntry.getKey();
            requestBuilder.addHeader(key, headerEntry.getValue());
        }
        //requestBuilder.addHeader("httplib", "OkHttp");
        Request request = requestBuilder.build();
        if (isCanceled) {//在發出請求前進行最後一遍確認
            return null;
        }
        Response response = client.newCall(request).execute();
        responseBody = response.body();
        if (!response.isSuccessful() || responseBody == null) {
            throw new IOException("Request failed with code: " + response.code());
        }
        stream = ContentLengthInputStream.obtain(responseBody.byteStream(),
                responseBody.contentLength());
        return stream;
    }

    @Override
    public void cleanup() {
        if (stream!=null){
            try {
                stream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        if (responseBody!=null){
            responseBody.close();
        }
    }

    @Override
    public String getId() {
        return glideUrl.getCacheKey();
    }

    @Override
    public void cancel() {
        isCanceled = true;
    }
}


只是在loadData中使用okhttp進行網絡請求操作而已。

到這裏,我們已經用自定義的模塊替換掉原模塊,實現了用okhttp加載圖片,接下來要獲取下載進度,要通過自定義okhttp的攔截器。在自定義的攔截器中,我們將響應體(ResponseBody)替換爲自定義的ResponseBody,獲取進度的操作就在這個ResponseBody中。

先來看這個自定義ResponseBody,構造器傳入原ResponseBody:

public class ProgressResponseBody extends ResponseBody {

    private ResponseBody responseBody;
    private BufferedSource bufferedSource;

    public ProgressResponseBody(ResponseBody responseBody) {
        this.responseBody = responseBody;
    }

    @Nullable
    @Override
    public MediaType contentType() {
        return responseBody.contentType();
    }

    @Override
    public long contentLength() {
        return responseBody.contentLength();
    }

    @Override
    public BufferedSource source() {
        if (bufferedSource==null){
            bufferedSource= Okio.buffer(new ProgressSource(responseBody.source()));
        }
        return bufferedSource;
    }

    private class ProgressSource extends ForwardingSource{

        private long currentBytes;//已讀取的數據長度
        private long totalLength;//數據總長度

        public ProgressSource(Source delegate) {
            super(delegate);
            totalLength=responseBody.contentLength();
        }

        @Override
        public long read(Buffer sink, long byteCount) throws IOException {
            Log.v("tag","read");
            long result=super.read(sink,byteCount);//這一次讀取的長度

            if (result==-1){
                currentBytes=totalLength;
            } else {
                currentBytes+=result;
            }

            int progress= (int) (currentBytes*100f/totalLength);
            Log.v("tag","進度"+progress);

            return result;
        }
    }
}

主要操作在自定義內部類的read中,read指的是每次讀取數據流的過程,返回值爲本次成功讀取的長度,-1表示讀完。所以思路也很清楚了,首先在ProgressSource的構造器中獲取數據源總長度,然後維護一個currentBytes變量每次讀完累加,相除就是當前的加載進度。

在自定義攔截器中用這個body替換原body:

public class ProcessInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        //將響應體換成我們自定義的響應體(讀取時觸發進度回調)
        Request request = chain.request();
        Response response = chain.proceed(request);
        //String url = request.url().toString();
        ResponseBody body = response.body();
        Response newResponse = response.newBuilder().body(new ProgressResponseBody(body)).build();
        return newResponse;
    }
}

最後在自定義GlideModule中創建okhttpclient並add這個攔截器,傳入自定義ModuleLoader即可:

public class MyGlideMoudule implements GlideModule {
    @Override
    public void applyOptions(Context context, GlideBuilder builder) {

    }

    @Override
    public void registerComponents(Context context, Glide glide) {
        OkHttpClient.Builder builder = new OkHttpClient.Builder();
        builder.addInterceptor(new ProcessInterceptor());
        OkHttpClient okHttpClient = builder.build();
        glide.register(GlideUrl.class, InputStream.class,new MyModolLoader.Factory(okHttpClient));
    }
}

至此,我們已經成功的獲取到了加載進度。但是這肯定不夠,實際開發中一般需要把進度通過回調接口與活動或者其他組件交互。這裏郭霖大神的方案是找個地方放一個全局static Map<String url,回調接口>,每次加載圖片時手動向map中加入回調接口,然後在上面的read中在map裏取出回調接口執行回調方法。

感覺這樣使用起來有點彆扭,想了很久有沒有更好的辦法,也沒想出來。。。。、


二。單次使用自定義加載

上面的方案直接替換了框架對於網絡請求的加載,是全局性的。如果我們想要只進行單個的替換也是可以的:

Glide.with.using(StreamModelLoader).load.into;

來看看StreamModelLoader裏有什麼:


是不是很熟悉,還是找DataFetcher,直接返回我們上面自定義的DataFetcher就可以了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章