• android 之图片异步加载


    一.概述

    本文来自"慕课网" 的学习,只是对代码做一下分析

    图片异步加载有2种方式:  (多线程/线程池) 或者 用其实AsyncTask , 其实AsyncTask底层也是用的多线程.

    使用缓存的好处是 , 提高流畅度, 节约流量.

    二.代码

    1.先看图片加载工具类

    public class ImageLoader {
        private ImageView mImageview;
        private String mUrl;
        //创建缓存
        private LruCache<String, Bitmap> mCaches;
        private ListView mListView;
        private Set<NewsAsyncTask> mTask;
    
        public ImageLoader(ListView listView) {
            mListView = listView;
            mTask = new HashSet<>();
            //获得最大的缓存空间
            int maxMemory = (int) Runtime.getRuntime().maxMemory();
            //赋予缓存区最大缓存的四分之一进行缓存
            int cacheSize = maxMemory / 4;
            mCaches = new LruCache<String, Bitmap>(cacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    //在每次存入缓存的时候调用
                    return value.getByteCount();
                }
            };
        }
    
        //将图片通过url与bitmap的键值对形式添加到缓存中
        public void addBitmapToCache(String url, Bitmap bitmap) {
            if (getBitmapFromCache(url) == null) {
                mCaches.put(url, bitmap);
            }
        }
    
        //通过缓存得到图片
        public Bitmap getBitmapFromCache(String url) {
            return mCaches.get(url);
        }
    
        private Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                if (mImageview.getTag().equals(mUrl))
                    mImageview.setImageBitmap((Bitmap) msg.obj);
            }
        };
    
        //通过线程的方式去展示图片
        public void showImageByThread(ImageView imageView, String url) {
            mImageview = imageView;
            mUrl = url;
            new Thread() {
                @Override
                public void run() {
                    super.run();
                    Bitmap bitmap = getBitmapFromUrl(mUrl);
                    Message message = Message.obtain();
                    message.obj = bitmap;
                    mHandler.sendMessage(message);
                }
            }.start();
        }
    
        //通过异步任务的方式去加载图片
    
        public void showImageByAsyncTask(ImageView imageView, String url) {
            //先从缓存中获取图片
            Bitmap bitmap = getBitmapFromCache(url);
            if (bitmap == null) {
               imageView.setImageResource(R.mipmap.ic_launcher);
            } else {
                imageView.setImageBitmap(bitmap);
            }
        }
    
        private class NewsAsyncTask extends AsyncTask<String, Void, Bitmap> {
    
       //     private ImageView mImageView;
            private String mUrl;
    
            public NewsAsyncTask( String url) {
       //         mImageview = imageView;
                mUrl = url;
            }
    
            @Override
            protected Bitmap doInBackground(String... params) {
                String url = params[0];
                //从网络获取图片
                Bitmap bitmap = getBitmapFromUrl(url);
                //将图片加入缓存中
                if (bitmap != null) {
                    addBitmapToCache(url, bitmap);
                }
                return bitmap;
            }
    
            @Override
            protected void onPostExecute(Bitmap bitmap) {
                super.onPostExecute(bitmap);
                ImageView imageView = (ImageView) mListView.findViewWithTag(mUrl);
                if (imageView!=null&&bitmap!=null){
                    imageView.setImageBitmap(bitmap);
                }
                mTask.remove(this);
            }
        }
    
        //滑动时加载图片
        public void loadImages(int start, int end) {
            for (int i = start; i < end; i++) {
                String url = NewsAdapter.URLS[i];
                //先从缓存中获取图片
                Bitmap bitmap = getBitmapFromCache(url);
                if (bitmap == null) {
                    NewsAsyncTask task = new NewsAsyncTask(url);
                    task.execute(url);
                    mTask.add(task);
                } else {
                    ImageView imageView = (ImageView) mListView.findViewWithTag(url);
                    imageView.setImageBitmap(bitmap);
                }
            }
        }
    
        //停止时取消所有任务加载
        public void cancelAllTasks(){
            if (mTask!=null){
                for (NewsAsyncTask task :mTask){
                    task.cancel(false);
                }
            }
        }
        //网络获取图片
        private Bitmap getBitmapFromUrl(String urlString) {
            Bitmap bitmap;
            InputStream is = null;
            try {
                URL url = new URL(urlString);
                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
                is = new BufferedInputStream(connection.getInputStream());
                bitmap = BitmapFactory.decodeStream(is);
                connection.disconnect();
                return bitmap;
            } catch (IOException e) {
                e.printStackTrace();
            } finally {
                try {
                    assert is != null;
                    is.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            return null;
        }
    }

    需要注意的几个部分:

      <1

    LruCache<String, Bitmap> mCaches 这是创建一个集合去存储缓存的图片,底层是HashMap实现的,其实和我们之前java中用到HashMap  弱引用/软引用比较类似, 但是
    自2.3以后Android将更频繁的调用GC,导致软引用缓存的数据极易被释放。所以不能用之前的方式来缓存图片了,
    LruCache使用一个LinkedHashMap简单的实现内存的缓存,没有软引用,都是强引用。如果添加的数据大于设置的最大值,就删除最先缓存的数据来调整内存。
    我们可以在构造方法中,先得到当前应用所占总缓存大小,然后分出1/4用于存储图片,对应代码如下:

     //获得当前应用最大的缓存空间
            int maxMemory = (int) Runtime.getRuntime().maxMemory();
            //赋予缓存区最大缓存的四分之一进行缓存
            int cacheSize = maxMemory / 4;
            mCaches = new LruCache<String, Bitmap>(cacheSize) {
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    //在每次存入缓存的时候调用
                    return value.getByteCount();
                }
            };
    <2
    Set<NewsAsyncTask> mTask
    定义一个Task任务集合,每个任务对应一个图片,当该图片被加载后要是否这个对应的task,对应代码如下:
     ImageView imageView = (ImageView) mListView.findViewWithTag(mUrl);
                if (imageView!=null&&bitmap!=null){
                    imageView.setImageBitmap(bitmap);
                }
                mTask.remove(this);

    <3代码中根据 adapter 给每个图片设置 Tag 标识来获取图片,作用是: 避免 listview滚动时,由于convertView缓存造成图片错位显示, 对应代码如下: ----------> adapter代码后面给出

     private Handler mHandler = new Handler() {
            @Override
            public void handleMessage(Message msg) {
                super.handleMessage(msg);
                if (mImageview.getTag().equals(mUrl))//--------------根据adapter设置的tag获取
                    mImageview.setImageBitmap((Bitmap) msg.obj);
            }
        };

    <4

    当listview一边滚动,一边加载图片会造成一个问题,可能会出现暂时的卡顿现象,尽管这个现象是偶尔发生,如果网络不好情况下,会加重这种情况,这是为什么呢?

    因为listview滚动时,对画面流畅度要求比较高
    虽然异步加载是在新线程中执行的,并未阻塞UI线程,当加载好图片后,去更新UI线程
    就会导致UI线程发生一次重绘,如果这次重绘正好发生在listview滚动的时候
    就会导致这个listview滚动过程中卡顿一下, 这样用户体验大大滴不好

    为解决该问题:

    我们可以在 listview滚动停止后 才去加载可见项, listview滚动过程中,取消加载项(滚动过程中不加载图片数据)
    就能解决这个问题, 因为我们在滚动过程中,其实我并不关心 滚动的内容,我只会关心 滚动停止后要显示的内容,所以这么做是 完全OK的.  对应代码如下:

     //滑动时加载图片 , 这里的 start 和end是 listview第一个和最后一个可见项
    // adapter代码中会有详述
    public void loadImages(int start, int end) { for (int i = start; i < end; i++) { String url = NewsAdapter.URLS[i]; //先从缓存中获取图片 Bitmap bitmap = getBitmapFromCache(url); if (bitmap == null) { NewsAsyncTask task = new NewsAsyncTask(url); task.execute(url); mTask.add(task); } else { ImageView imageView = (ImageView) mListView.findViewWithTag(url); imageView.setImageBitmap(bitmap); } } }

    以上就是图片工具类比较重点的部分 ,下面介绍adapter

    常常的分割线-------------------------------------------------------------------------------------------------------------

    public class NewsAdapter extends BaseAdapter implements AbsListView.OnScrollListener{
    
        private List<NewsBeans> mList;
        private LayoutInflater mInflater;
        private ImageLoader mImageLoader;
        private int mStart;
        private int mEnd;
        //创建静态数组保存图片的url地址
        public static String[] URLS;
        private boolean mFirstIn;
    
        public NewsAdapter(Context context, List<NewsBeans> data,ListView listView) {
            mList = data;
            mInflater = LayoutInflater.from(context);
            mImageLoader = new ImageLoader(listView);
            URLS = new String[data.size()];
            for(int i=0;i<data.size();i++){
                URLS[i] = data.get(i).iv_title;
            }
            listView.setOnScrollListener(this);
            mFirstIn = true;
        }
    
        @Override
        public int getCount() {
            return mList.size();
        }
    
        @Override
        public Object getItem(int position) {
            return mList.get(position);
        }
    
        @Override
        public long getItemId(int position) {
            return position;
        }
    
        @Override
        public View getView(int position, View convertView, ViewGroup parent) {
            ViewHolder viewHolder;
            if (convertView == null) {
                viewHolder = new ViewHolder();
                convertView = mInflater.inflate(R.layout.list_item, null);
                viewHolder.iv_title = (ImageView) convertView.findViewById(R.id.iv_icon);
                viewHolder.tv_title = (TextView) convertView.findViewById(R.id.tv_title);
                viewHolder.tv_content = (TextView) convertView.findViewById(R.id.tv_content);
                convertView.setTag(viewHolder);
            } else {
                viewHolder = (ViewHolder) convertView.getTag();
            }
            //设置默认显示的图片
            viewHolder.iv_title.setImageResource(R.mipmap.ic_launcher);
            //避免缓存影响使同一位置图片加载多次混乱
            String url = mList.get(position).iv_title;
            viewHolder.iv_title.setTag(url);
         //   new ImageLoader().showImageByThread(viewHolder.iv_title, url);
            mImageLoader.showImageByAsyncTask(viewHolder.iv_title, url);
            viewHolder.tv_content.setText(mList.get(position).tv_content);
            viewHolder.tv_title.setText(mList.get(position).tv_title);
            return convertView;
        }
    
        @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            if(scrollState==SCROLL_STATE_IDLE){
                //加载可见项
                mImageLoader.loadImages(mStart,mEnd);
            }else{
                //停止加载
                mImageLoader.cancelAllTasks();
            }
        }
    
        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
            mStart = firstVisibleItem;
            mEnd = firstVisibleItem + visibleItemCount;
            if (mFirstIn && visibleItemCount>0){
                mImageLoader.loadImages(mStart,mEnd);
            }
        }
    
        class ViewHolder {
            private ImageView iv_title;
            private TextView tv_title;
            private TextView tv_content;
        }
    }

    这里我们只需要注意3点

    1.设置图片唯一标识Tag,避免图片错位显示

    viewHolder.iv_title.setTag(url);

    2.滚动过程中不加载图片,只有滚动停止后加载,下面重点分析2个方法

    @Override
        public void onScrollStateChanged(AbsListView view, int scrollState) {
            if(scrollState==SCROLL_STATE_IDLE){
                //加载可见项
                mImageLoader.loadImages(mStart,mEnd);
            }else{
                //停止加载
                mImageLoader.cancelAllTasks();
            }
        }
    
        @Override
        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) {
            mStart = firstVisibleItem;
            mEnd = firstVisibleItem + visibleItemCount;
            if (mFirstIn && visibleItemCount>0){
                mImageLoader.loadImages(mStart,mEnd);
            }
        }
    onScrollStateChanged()该方法,在listview第一次出现的时候,并不会执行,注意"并不会执行".


    oncroll()该方法在listview创建的时候就会执行,所以我们定义一个标志mFirstIn,在构造方法中初始为true,表示我们是第一次启动listview
    对 mFirstIn && visibleItemCount>0 判断的解释:
    "当前列表时第一次显示,并且listview的item已经展示出来",然后mFirstIn =false ,保证此段代码只有listview第一次显示的时候才会执行,之后滚动过程中不再执行

    这里为什么要判断visibleItemCount>0 呢?
    其实 oncroll会被多次回调的, 但是初次调用 
    visibleItemCount 是 等于0的,也就是说此时item还未被加载
    所以我们要判断 >0 跳过==0的情况,因为==0 item未被加载,当然 也就不会显示网络图片了
    分割线--------------------------------------------------------------------------------------------------------------------------------------

    最后是 MainActivity代码,比较简单不再分析
    public class MainActivity extends AppCompatActivity {
    
        private static String URL = "http://www.imooc.com/api/teacher?type=4&num=30";
        private ListView mListView;
    
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            mListView = (ListView) findViewById(R.id.list_main);
            new LoadImageAsync().execute(URL);
        }
    
        //异步加载所有的网络数据
        class LoadImageAsync extends AsyncTask<String, Void, List<NewsBeans>> {
    
            @Override
            protected List<NewsBeans> doInBackground(String... params) {
                return getJsonData(params[0]);
            }
    
            @Override
            protected void onPostExecute(List<NewsBeans> newsBeans) {
                super.onPostExecute(newsBeans);
                NewsAdapter adapter = new NewsAdapter(MainActivity.this, newsBeans,mListView);
                mListView.setAdapter(adapter);
            }
        }
    
        //得到JSON数据
        private List<NewsBeans> getJsonData(String url) {
            List<NewsBeans> data = new ArrayList<>();
            try {
                //读取流得到json数据
                String jsonList = readStream(new URL(url).openStream());
                JSONObject jsonObject;
                NewsBeans newsBeans;
                try {
                    //解析JSON数据
                    jsonObject = new JSONObject(jsonList);
                    JSONArray jsonArray = jsonObject.getJSONArray("data");
                    for (int i = 0; i <= jsonArray.length(); i++) {
                        jsonObject = jsonArray.getJSONObject(i);
                        newsBeans = new NewsBeans();
                        newsBeans.iv_title = jsonObject.getString("picSmall");
                        newsBeans.tv_title = jsonObject.getString("name");
                        newsBeans.tv_content = jsonObject.getString("description");
                        data.add(newsBeans);
                    }
                } catch (JSONException e) {
                    e.printStackTrace();
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return data;
        }
    
        //读取输入流
        private String readStream(InputStream is) {
            InputStreamReader isr;
            String result = "";
            try {
                String line;
                //读取输入流
                isr = new InputStreamReader(is, "utf-8");
                //输入流转换成字节流
                BufferedReader br = new BufferedReader(isr);
                //逐行读取
                while ((line = br.readLine()) != null) {
                    result += line;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return result;
        }
    }

    以上代码只是做了内存缓存,如果想做二级缓存,可以用
    DiskLruCache 硬盘缓存.最后来张图吧

    
    


  • 相关阅读:
    创建vue3 + vite + ts 项目
    js 使用redux 计算数组中每个元素出现的次数
    Vue3 defineComponent的作用
    uniapp 通用函数说明
    vue + elementUI 表单重置两种方法
    解决Vue.js devtools插件成功装上,却在控制台中找不到的问题 Bing
    关于flex元素超出父元素的解决方法 Bing
    Source Tree 1、解决打开闪退问题2、解决找不到项目的问题 Bing
    Chrome(谷歌浏览器)安装Vue插件vuedevtools Bing
    javaScript 获取对象数组的对象里面想要的属性,返回一个新的数组 Bing
  • 原文地址:https://www.cnblogs.com/android-zcq/p/5566126.html
Copyright © 2020-2023  润新知