软件世界网 购物 网址 三丰软件 | 小说 美女秀 图库大全 游戏 笑话 | 下载 开发知识库 新闻 开发 图片素材
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
移动开发 架构设计 编程语言 Web前端 互联网
开发杂谈 系统运维 研发管理 数据库 云计算 Android开发资料
  软件世界网 -> 架构设计 -> Volley图片加载功能 -> 正文阅读

[架构设计]Volley图片加载功能


Gituhb项目


Volley源码中文注释项目我已经上传到github,欢迎大家fork和start.

为什么写这篇博客


本来文章是维护在github上的,但是我在分析ImageLoader源码过程中与到了一个问题,希望大家能帮助解答.

Volley获取网络图片


本来想分析Universal Image Loader的源码,但是发现Volley已经实现了网络图片的加载功能.其实,网络图片的加载也是分几个步骤:
1. 获取网络图片的url.
2. 判断该url对应的图片是否有本地缓存.
3. 有本地缓存,直接使用本地缓存图片,通过异步回调给ImageView进行设置.
4. 无本地缓存,就先从网络拉取,保存在本地后,再通过异步回调给ImageView进行设置.
我们通过Volley源码,看一下Volley是否是按照这个步骤实现网络图片加载的.

ImageRequest.java


按照Volley的架构,我们首先需要构造一个网络图片请求,Volley帮我们封装了ImageRequest类,我们来看一下它的具体实现:
/** 网络图片请求类. */
@SuppressWarnings("unused")
public class ImageRequest extends Request<Bitmap> {
    /** 默认图片获取的超时时间(单位:毫秒) */
    public static final int DEFAULT_IMAGE_REQUEST_MS = 1000;

    /** 默认图片获取的重试次数. */
    public static final int DEFAULT_IMAGE_MAX_RETRIES = 2;

    private final Response.Listener<Bitmap> mListener;
    private final Bitmap.Config mDecodeConfig;
    private final int mMaxWidth;
    private final int mMaxHeight;
    private ImageView.ScaleType mScaleType;

    /** Bitmap解析同步锁,保证同一时间只有一个Bitmap被load到内存进行解析,防止OOM. */
    private static final Object sDecodeLock = new Object();

    /**
     * 构造一个网络图片请求.
     * @param url 图片的url地址.
     * @param listener 请求成功用户设置的回调接口.
     * @param maxWidth 图片的最大宽度.
     * @param maxHeight 图片的最大高度.
     * @param scaleType 图片缩放类型.
     * @param decodeConfig 解析bitmap的配置.
     * @param errorListener 请求失败用户设置的回调接口.
     */
    public ImageRequest(String url, Response.Listener<Bitmap> listener, int maxWidth, int maxHeight,
                        ImageView.ScaleType scaleType, Bitmap.Config decodeConfig,
                        Response.ErrorListener errorListener) {
        super(Method.GET, url, errorListener);
        mListener = listener;
        mDecodeConfig = decodeConfig;
        mMaxWidth = maxWidth;
        mMaxHeight = maxHeight;
        mScaleType = scaleType;
    }

    /** 设置网络图片请求的优先级. */
    @Override
    public Priority getPriority() {
        return Priority.LOW;
    }

    @Override
    protected Response<Bitmap> parseNetworkResponse(NetworkResponse response) {
        synchronized (sDecodeLock) {
            try {
                return doParse(response);
            } catch (OutOfMemoryError e) {
                return Response.error(new VolleyError(e));
            }
        }
    }

    private Response<Bitmap> doParse(NetworkResponse response) {
        byte[] data = response.data;
        BitmapFactory.Options decodeOptions = new BitmapFactory.Options();
        Bitmap bitmap;
        if (mMaxWidth == 0 && mMaxHeight == 0) {
            decodeOptions.inPreferredConfig = mDecodeConfig;
            bitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
        } else {
            // 获取网络图片的真实尺寸.
            decodeOptions.inJustDecodeBounds = true;
            BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);
            int actualWidth = decodeOptions.outWidth;
            int actualHeight = decodeOptions.outHeight;

            int desiredWidth = getResizedDimension(mMaxWidth, mMaxHeight,
                    actualWidth, actualHeight, mScaleType);
            int desireHeight = getResizedDimension(mMaxWidth, mMaxHeight,
                    actualWidth, actualHeight, mScaleType);

            decodeOptions.inJustDecodeBounds = false;
            decodeOptions.inSampleSize =
                    findBestSampleSize(actualWidth, actualHeight, desiredWidth, desireHeight);
            Bitmap tempBitmap = BitmapFactory.decodeByteArray(data, 0, data.length, decodeOptions);

            if (tempBitmap != null && (tempBitmap.getWidth() > desiredWidth ||
                    tempBitmap.getHeight() > desireHeight)) {
                bitmap = Bitmap.createScaledBitmap(tempBitmap, desiredWidth, desireHeight, true);
                tempBitmap.recycle();
            } else {
                bitmap = tempBitmap;
            }
        }

        if (bitmap == null) {
            return Response.error(new VolleyError(response));
        } else {
            return Response.success(bitmap, HttpHeaderParser.parseCacheHeaders(response));
        }
    }

    static int findBestSampleSize(
            int actualWidth, int actualHeight, int desiredWidth, int desireHeight) {
        double wr = (double) actualWidth / desiredWidth;
        double hr = (double) actualHeight / desireHeight;
        double ratio = Math.min(wr, hr);
        float n = 1.0f;
        while ((n * 2) <= ratio) {
            n *= 2;
        }
        return (int) n;
    }

    /** 根据ImageView的ScaleType设置图片的大小. */
    private static int getResizedDimension(int maxPrimary, int maxSecondary, int actualPrimary,
                                           int actualSecondary, ImageView.ScaleType scaleType) {
        // 如果没有设置ImageView的最大值,则直接返回网络图片的真实大小.
        if ((maxPrimary == 0) && (maxSecondary == 0)) {
            return actualPrimary;
        }

        // 如果ImageView的ScaleType为FIX_XY,则将其设置为图片最值.
        if (scaleType == ImageView.ScaleType.FIT_XY) {
            if (maxPrimary == 0) {
                return actualPrimary;
            }
            return maxPrimary;
        }

        if (maxPrimary == 0) {
            double ratio = (double)maxSecondary / (double)actualSecondary;
            return (int)(actualPrimary * ratio);
        }

        if (maxSecondary == 0) {
            return maxPrimary;
        }

        double ratio = (double) actualSecondary / (double) actualPrimary;
        int resized = maxPrimary;

        if (scaleType == ImageView.ScaleType.CENTER_CROP) {
            if ((resized * ratio) < maxSecondary) {
                resized = (int)(maxSecondary / ratio);
            }
            return resized;
        }

        if ((resized * ratio) > maxSecondary) {
            resized = (int)(maxSecondary / ratio);
        }

        return resized;
    }


    @Override
    protected void deliverResponse(Bitmap response) {
        mListener.onResponse(response);
    }
}

因为Volley本身框架已经实现了对网络请求的本地缓存,所以ImageRequest做的主要事情就是解析字节流为Bitmap,再解析过程中,通过静态变量保证每次只解析一个Bitmap防止OOM,使用ScaleType和用户设置的MaxWidth和MaxHeight来设置图片大小.
总体来说,ImageRequest的实现非常简单,这里不做过多的讲解.ImageRequest的缺陷在于:
  1. 需要用户进行过多的设置,包括图片的大小的最大值.
  2. 没有图片的内存缓存,因为Volley的缓存是基于Disk的缓存,有对象反序列化的过程.

ImageLoader.java


鉴于以上两个缺点,Volley又提供了一个更牛逼的ImageLoader类.其中,最关键的就是增加了内存缓存.
再讲解ImageLoader的源码之前,需要先介绍一下ImageLoader的使用方法.和之前的Request请求不同,ImageLoader并不是new出来直接扔给RequestQueue进行调度,它的使用方法大体分为4步:
  • 创建一个RequestQueue对象.
RequestQueue queue = Volley.newRequestQueue(context);
  • 创建一个ImageLoader对象.ImageLoader构造函数接收两个参数,第一个是RequestQueue对象,第二个是ImageCache对象(也就是内存缓存类,我们先不给出具体实现,讲解完ImageLoader源码之后,我会提供一个利用LRU算法的ImageCache实现类)
ImageLoader imageLoader = new ImageLoader(queue, new ImageCache() {
    @Override
    public void putBitmap(String url, Bitmap bitmap) {}
    @Override
    public Bitmap getBitmap(String url) { return null; }
});
  • 获取一个ImageListener对象.
ImageListener listener = ImageLoader.getImageListener(imageView, R.drawable.default_imgage, R.drawable.failed_image);
  • 调用ImageLoader的get方法加载网络图片.
imageLoader.get(mImageUrl, listener, maxWidth, maxHeight, scaleType);

有了ImageLoader的使用方法,我们结合使用方法来看一下ImageLoader的源码:
@SuppressWarnings({"unused", "StringBufferReplaceableByString"})
public class ImageLoader {
    /**
     * 关联用来调用ImageLoader的RequestQueue.
     */
    private final RequestQueue mRequestQueue;

    /** 图片内存缓存接口实现类. */
    private final ImageCache mCache;

    /** 存储同一时间执行的相同CacheKey的BatchedImageRequest集合. */
    private final HashMap<String, BatchedImageRequest> mInFlightRequests =
            new HashMap<String, BatchedImageRequest>();

    private final HashMap<String, BatchedImageRequest> mBatchedResponses =
            new HashMap<String, BatchedImageRequest>();

    /** 获取主线程的Handler. */
    private final Handler mHandler = new Handler(Looper.getMainLooper());


    private Runnable mRunnable;

    /** 定义图片K1缓存接口,即将图片的内存缓存工作交给用户来实现. */
    public interface ImageCache {
        Bitmap getBitmap(String url);
        void putBitmap(String url, Bitmap bitmap);
    }

    /** 构造一个ImageLoader. */
    public ImageLoader(RequestQueue queue, ImageCache imageCache) {
        mRequestQueue = queue;
        mCache = imageCache;
    }

    /** 构造网络图片请求成功和失败的回调接口. */
    public static ImageListener getImageListener(final ImageView view, final int defaultImageResId,
                                                 final int errorImageResId) {
        return new ImageListener() {
            @Override
            public void onResponse(ImageContainer response, boolean isImmediate) {
                if (response.getBitmap() != null) {
                    view.setImageBitmap(response.getBitmap());
                } else if (defaultImageResId != 0) {
                    view.setImageResource(defaultImageResId);
                }
            }

            @Override
            public void onErrorResponse(VolleyError error) {
                if (errorImageResId != 0) {
                    view.setImageResource(errorImageResId);
                }
            }
        };
    }

    public ImageContainer get(String requestUrl, ImageListener imageListener,
                               int maxWidth, int maxHeight, ScaleType scaleType) {
        // 判断当前方法是否在UI线程中执行.如果不是,则抛出异常.
        throwIfNotOnMainThread();

        final String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);

        // 从L1级缓存中根据key获取对应的Bitmap.
        Bitmap cacheBitmap = mCache.getBitmap(cacheKey);
        if (cacheBitmap != null) {
            // L1缓存命中,通过缓存命中的Bitmap构造ImageContainer,并调用imageListener的响应成功接口.
            ImageContainer container = new ImageContainer(cacheBitmap, requestUrl, null, null);
            // 注意:因为目前是在UI线程中,因此这里是调用onResponse方法,并非回调.
            imageListener.onResponse(container, true);
            return container;
        }

        ImageContainer imageContainer =
                new ImageContainer(null, requestUrl, cacheKey, imageListener);
        // L1缓存命中失败,则先需要对ImageView设置默认图片.然后通过子线程拉取网络图片,进行显示.
        imageListener.onResponse(imageContainer, true);

        // 检查cacheKey对应的ImageRequest请求是否正在运行.
        BatchedImageRequest request = mInFlightRequests.get(cacheKey);
        if (request != null) {
            // 相同的ImageRequest正在运行,不需要同时运行相同的ImageRequest.
            // 只需要将其对应的ImageContainer加入到BatchedImageRequest的mContainers集合中.
            // 当正在执行的ImageRequest结束后,会查看当前有多少正在阻塞的ImageRequest,
            // 然后对其mContainers集合进行回调.
            request.addContainer(imageContainer);
            return imageContainer;
        }

        // L1缓存没命中,还是需要构造ImageRequest,通过RequestQueue的调度来获取网络图片
        // 获取方法可能是:L2缓存(ps:Disk缓存)或者HTTP网络请求.
        Request<Bitmap> newRequest =
                makeImageRequest(requestUrl, maxWidth, maxHeight, scaleType, cacheKey);
        mRequestQueue.add(newRequest);
        mInFlightRequests.put(cacheKey, new BatchedImageRequest(newRequest, imageContainer));

        return imageContainer;
    }

    /** 构造L1缓存的key值. */
    private String getCacheKey(String url, int maxWidth, int maxHeight, ScaleType scaleType) {
        return new StringBuilder(url.length() + 12).append("#W").append(maxWidth)
                .append("#H").append(maxHeight).append("#S").append(scaleType.ordinal()).append(url)
                .toString();
    }

    public boolean isCached(String requestUrl, int maxWidth, int maxHeight) {
        return isCached(requestUrl, maxWidth, maxHeight, ScaleType.CENTER_INSIDE);
    }

    private boolean isCached(String requestUrl, int maxWidth, int maxHeight, ScaleType scaleType) {
        throwIfNotOnMainThread();

        String cacheKey = getCacheKey(requestUrl, maxWidth, maxHeight, scaleType);
        return mCache.getBitmap(cacheKey) != null;
    }


    /** 当L1缓存没有命中时,构造ImageRequest,通过ImageRequest和RequestQueue获取图片. */
    protected Request<Bitmap> makeImageRequest(final String requestUrl, int maxWidth, int maxHeight,
                                               ScaleType scaleType, final String cacheKey) {
        return new ImageRequest(requestUrl, new Response.Listener<Bitmap>() {
            @Override
            public void onResponse(Bitmap response) {
                onGetImageSuccess(cacheKey, response);
            }
        }, maxWidth, maxHeight, scaleType, Bitmap.Config.RGB_565, new Response.ErrorListener() {
            @Override
            public void onErrorResponse(VolleyError error) {
                onGetImageError(cacheKey, error);
            }
        });
    }

    /** 图片请求失败回调.运行在UI线程中. */
    private void onGetImageError(String cacheKey, VolleyError error) {
        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
        if (request != null) {
            request.setError(error);
            batchResponse(cacheKey, request);
        }
    }

    /** 图片请求成功回调.运行在UI线程中. */
    protected void onGetImageSuccess(String cacheKey, Bitmap response) {
        // 增加L1缓存的键值对.
        mCache.putBitmap(cacheKey, response);

        // 同一时间内最初的ImageRequest执行成功后,回调这段时间阻塞的相同ImageRequest对应的成功回调接口.
        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
        if (request != null) {
            request.mResponseBitmap = response;
            // 将阻塞的ImageRequest进行结果分发.
            batchResponse(cacheKey, request);
        }
    }

    private void batchResponse(String cacheKey, BatchedImageRequest request) {
        mBatchedResponses.put(cacheKey, request);
        if (mRunnable == null) {
            mRunnable = new Runnable() {
                @Override
                public void run() {
                    for (BatchedImageRequest bir :  mBatchedResponses.values()) {
                        for (ImageContainer container : bir.mContainers) {
                            if (container.mListener == null) {
                                continue;
                            }

                            if (bir.getError() == null) {
                                container.mBitmap = bir.mResponseBitmap;
                                container.mListener.onResponse(container, false);
                            } else {
                                container.mListener.onErrorResponse(bir.getError());
                            }
                        }
                    }
                    mBatchedResponses.clear();
                    mRunnable = null;
                }
            };
            // Post the runnable
            mHandler.postDelayed(mRunnable, 100);
        }
    }

    private void throwIfNotOnMainThread() {
        if (Looper.myLooper() != Looper.getMainLooper()) {
            throw new IllegalStateException("ImageLoader must be invoked from the main thread.");
        }
    }

    /** 抽象出请求成功和失败的回调接口.默认可以使用Volley提供的ImageListener. */
    public interface ImageListener extends Response.ErrorListener {
        void onResponse(ImageContainer response, boolean isImmediate);
    }

    /** 网络图片请求的承载对象. */
    public class ImageContainer {
        /** ImageView需要加载的Bitmap. */
        private Bitmap mBitmap;

        /** L1缓存的key */
        private final String mCacheKey;

        /** ImageRequest请求的url. */
        private final String mRequestUrl;

        /** 图片请求成功或失败的回调接口类. */
        private final ImageListener mListener;

        public ImageContainer(Bitmap bitmap, String requestUrl, String cacheKey,
                              ImageListener listener) {
            mBitmap = bitmap;
            mRequestUrl = requestUrl;
            mCacheKey = cacheKey;
            mListener = listener;

        }

        public void cancelRequest() {
            if (mListener == null) {
                return;
            }

            BatchedImageRequest request = mInFlightRequests.get(mCacheKey);
            if (request != null) {
                boolean canceled = request.removeContainerAndCancelIfNecessary(this);
                if (canceled) {
                    mInFlightRequests.remove(mCacheKey);
                }
            } else {
                request = mBatchedResponses.get(mCacheKey);
                if (request != null) {
                    request.removeContainerAndCancelIfNecessary(this);
                    if (request.mContainers.size() == 0) {
                        mBatchedResponses.remove(mCacheKey);
                    }
                }
            }
        }

        public Bitmap getBitmap() {
            return mBitmap;
        }

        public String getRequestUrl() {
            return mRequestUrl;
        }
    }

    /**
     * CacheKey相同的ImageRequest请求抽象类.
     * 判定两个ImageRequest相同包括:
     * 1. url相同.
     * 2. maxWidth和maxHeight相同.
     * 3. 显示的scaleType相同.
     * 同一时间可能有多个相同CacheKey的ImageRequest请求,由于需要返回的Bitmap都一样,所以用BatchedImageRequest
     * 来实现该功能.同一时间相同CacheKey的ImageRequest只能有一个.
     * 为什么不使用RequestQueue的mWaitingRequestQueue来实现该功能?
     * 答:是因为仅靠URL是没法判断两个ImageRequest相等的.
     */
    private class BatchedImageRequest {
        /** 对应的ImageRequest请求. */
        private final Request<?> mRequest;

        /** 请求结果的Bitmap对象. */
        private Bitmap mResponseBitmap;

        /** ImageRequest的错误. */
        private VolleyError mError;

        /** 所有相同ImageRequest请求结果的封装集合. */
        private final LinkedList<ImageContainer> mContainers = new LinkedList<ImageContainer>();

        public BatchedImageRequest(Request<?> request, ImageContainer container) {
            mRequest = request;
            mContainers.add(container);
        }

        public VolleyError getError() {
            return mError;
        }

        public void setError(VolleyError error) {
            mError = error;
        }

        public void addContainer(ImageContainer container) {
            mContainers.add(container);
        }

        public boolean removeContainerAndCancelIfNecessary(ImageContainer container) {
            mContainers.remove(container);
            if (mContainers.size() == 0) {
                mRequest.cancel();
                return true;
            }
            return false;
        }
    }
}

重大疑问


个人对Imageloader的源码有两个重大疑问?
  • batchResponse方法的实现.

我很奇怪,为什么ImageLoader类里面要有一个HashMap来保存BatchedImageRequest集合呢?
    private final HashMap<String, BatchedImageRequest> mBatchedResponses =
            new HashMap<String, BatchedImageRequest>();

毕竟batchResponse是在特定的ImageRequest执行成功的回调中被调用的,调用代码如下:
    protected void onGetImageSuccess(String cacheKey, Bitmap response) {
        // 增加L1缓存的键值对.
        mCache.putBitmap(cacheKey, response);

        // 同一时间内最初的ImageRequest执行成功后,回调这段时间阻塞的相同ImageRequest对应的成功回调接口.
        BatchedImageRequest request = mInFlightRequests.remove(cacheKey);
        if (request != null) {
            request.mResponseBitmap = response;
            // 将阻塞的ImageRequest进行结果分发.
            batchResponse(cacheKey, request);
        }
    }

从上述代码可以看出,ImageRequest请求成功后,已经从mInFlightRequests中获取了对应的BatchedImageRequest对象.而同一时间被阻塞的相同的ImageRequest对应的ImageContainer都在BatchedImageRequest的mContainers集合中.
那我认为,batchResponse方法只需要遍历对应BatchedImageRequest的mContainers集合即可.
但是,ImageLoader源码中,我认为多余的构造了一个HashMap对象mBatchedResponses来保存BatchedImageRequest集合,然后在batchResponse方法中又对集合进行两层for循环各种遍历,实在是非常诡异,求指导.
诡异代码如下:
    private void batchResponse(String cacheKey, BatchedImageRequest request) {
        mBatchedResponses.put(cacheKey, request);
        if (mRunnable == null) {
            mRunnable = new Runnable() {
                @Override
                public void run() {
                    for (BatchedImageRequest bir :  mBatchedResponses.values()) {
                        for (ImageContainer container : bir.mContainers) {
                            if (container.mListener == null) {
                                continue;
                            }

                            if (bir.getError() == null) {
                                container.mBitmap = bir.mResponseBitmap;
                                container.mListener.onResponse(container, false);
                            } else {
                                container.mListener.onErrorResponse(bir.getError());
                            }
                        }
                    }
                    mBatchedResponses.clear();
                    mRunnable = null;
                }
            };
            // Post the runnable
            mHandler.postDelayed(mRunnable, 100);
        }
    }

我认为的代码实现应该是:
    private void batchResponse(String cacheKey, BatchedImageRequest request) {
        if (mRunnable == null) {
            mRunnable = new Runnable() {
                @Override
                public void run() {
                    for (ImageContainer container : request.mContainers) {
                        if (container.mListener == null) {
                            continue;
                        }

                        if (request.getError() == null) {
                            container.mBitmap = request.mResponseBitmap;
                            container.mListener.onResponse(container, false);
                        } else {
                            container.mListener.onErrorResponse(request.getError());
                        }
                    }
                    mRunnable = null;
                }
            };
            // Post the runnable
            mHandler.postDelayed(mRunnable, 100);
        }
    }
  • 使用ImageLoader默认提供的ImageListener,我认为存在一个缺陷,即图片闪现问题.当为ListView的item设置图片时,需要增加TAG判断.因为对应的ImageView可能已经被回收利用了.

自定义L1缓存类


首先说明一下,所谓的L1和L2缓存分别指的是内存缓存和硬盘缓存.
实现L1缓存,我们可以使用Android提供的Lru缓存类,示例代码如下:
import android.graphics.Bitmap;
import android.support.v4.util.LruCache;

/** Lru算法的L1缓存实现类. */
@SuppressWarnings("unused")
public class ImageLruCache implements ImageLoader.ImageCache {
    private LruCache<String, Bitmap> mLruCache;

    public ImageLruCache() {
        this((int) Runtime.getRuntime().maxMemory() / 8);
    }

    public ImageLruCache(final int cacheSize) {
        createLruCache(cacheSize);
    }

    private void createLruCache(final int cacheSize) {
        mLruCache = new LruCache<String, Bitmap>(cacheSize) {
            @Override
            protected int sizeOf(String key, Bitmap value) {
                return value.getRowBytes() * value.getHeight();
            }
        };
    }

    @Override
    public Bitmap getBitmap(String url) {
        return mLruCache.get(url);
    }

    @Override
    public void putBitmap(String url, Bitmap bitmap) {
        mLruCache.put(url, bitmap);
    }
}

......显示全文...
    点击查看全文


上一篇文章      下一篇文章      查看所有文章
2016-03-29 23:00:06  
架构设计 最新文章
Opengl教程之读取obj并绘制在picturecontro
读《企业应用架构模式》第五章并发
StepbyStepintoSpring(IOC)
设计模式(2)用例图之一
使用实体组件系统(ECS)开发”吃方块”游戏实
编程学习之简单工厂模式与策略模式
Invalidprojectdescription.
基于Redis实现分布式消息队列(2)
《开源框架那点事儿15》:借船下海还是造船
原型模式——浅复制和深复制
360图书馆 软件开发资料 文字转语音 购物精选 软件下载 美食菜谱 新闻资讯 电影视频 小游戏 Chinese Culture 股票 租车
生肖星座 三丰软件 视频 开发 短信 中国文化 网文精选 搜图网 美图 阅读网 多播 租车 短信 看图 日历 万年历 2018年1日历
2018-1-17 17:14:58
多播视频美女直播
↓电视,电影,美女直播,迅雷资源↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  软件世界网 --