ZUI易入门Android之Volley

1. 开源意识和开放平台的培养

  • 了解Github网站java分类中star数最多的前2页开源项目
  • 了解常见开源项目网站和开放平台
  • 了解Android热门开源库
    • 图片加载: Volley,UniversalImageLoader,Picasso,Glide,Fresco,XUtil等;
    • 网络请求: Retrofit,OKHttp,Volley,AsyncHttpClient,ion,XUtil等;
    • 组件间通信:EventBus,otto
    • 控件绑定: ButterKnife,XUtil
    • orm数据库: greenDao, DBFlow, OrmLite,Realm
  • 注意:以上内容属于了解范畴和将来研究的方向,以及作为企业开发中可选择的解决方案; 并不是要求同学们全部搞明白,而且每个分类中选择一个研究即可,万变不离其中。

2. Volley的介绍

  • Volley是什么?
    • 2013年Google I/O大会上推出的网络请求和图片加载框架
    • 其优点是api简单,性能优秀
    • 非常适合数据量不大但是通信频繁的网络请求,而对于大数据量的操作,如文本下载,表现则没有那么好
    • Volley内部仍然是使用的HttpURLConnection和HttpClient进行网络请求的,只是对于不同的Android版本进行了响应的切换,2.3之前使用的HttpClient,2.3之后使用的是HttpURLConnection
  • 为什么用Volley,相比XUtil,Volley解决了以下问题:
    • 当用户finish当前的Activity的时候,而当前Activity正在进行网络请求,Volley支持取消网络请求
    • 当用户旋转屏幕,Activity重新创建,那么同样的网络请求会从新发送,而Volley支持对重复的请求进行缓存
    • 支持多样的网络请求返回封装

3. Volley的使用

  • 首先,引入Volley类库,添加相关权限

    <uses-permission android:name="android.permission.INTERNET"/>
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/> 
    
  • Volley中的核心类

    • Request,请求类,有几个实现类
      • StringRequest:请求的时候直接回来一个String
      • JsonObjectRequest:请求的时候直接回来一个JsonObject
      • JsonArrayRequest:请求的时候直接回来一个JsonArray
      • ImageRequest:请求的时候直接回来一个Bitmap
      • 自定义请求:一会我们会结合gson
    • RequestQueue:请求队列,用来执行Request的
    • ImageLoader:图片加载的类
    • NetWorkImageView:继承自ImageView,可以直接加载网络图片
  • 创建请求队列RequestQueue

    RequestQueue queue = Volley.newRequestQueue(this);
    
  • 使用StringRequest进行请求

    //2.创建网络请求
    StringRequest stringRequest = new StringRequest(url, new Listener<String>() {
        @Override
        public void onResponse(String response) {
            tv_result.setText(response);
        }
    },new MyErrorListener());
    //3.执行请求
    queue.add(stringRequest);
    
  • 使用JsonRequest进行请求

    //1.创建JsonRequest请求
    JsonObjectRequest joRequest = new JsonObjectRequest(url, null, new Listener<JSONObject>() {
        @Override
        public void onResponse(JSONObject response) {
            tv_result.setText(response.toString());
        }
    }, new MyErrorListener());
    //2.执行请求
    queue.add(joRequest);
    
  • 使用JsonArrayRequest进行请求

    JsonArrayRequest jsonArrayRequest = new JsonArrayRequest(listUrl, new Listener<JSONArray>() {
        @Override
        public void onResponse(JSONArray response) {
            tv_result.setText(response.toString());
        }
    }, new MyErrorListener());
    queue.add(jsonArrayRequest);
    
  • 使用ImageRequest进行请求

    ImageRequest imageRequest = new ImageRequest(imageUrl,new Listener<Bitmap>() {
        @Override
        public void onResponse(Bitmap response) {
            iv_result.setImageBitmap(response);
        }
    }, 200, 100, Config.RGB_565, new MyErrorListener());
    queue.add(imageRequest);
    

4. 加载图片的压缩处理的核心

  • 第一步: 从原图进行等宽高比的采样,采样的值最好是2的倍数,代码如下:

    Options opts = new Options();
    opts.inSampleSize = 4;
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.fast, opts);
    
  • 第二步: 根据图片的原始宽高比和控件的宽高比,科学的计算采样比例,代码如下:

    Options opts = new Options();
    opts.inJustDecodeBounds = true;//设置只解析图片的边界参数,即宽高
    Bitmap bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.fast, opts);
    //科学计算图片所需的采样比例
    opts.inSampleSize = caculateSampleSize(opts.outWidth,opts.outHeight);
    
    opts.inJustDecodeBounds = false;//关闭标记,解析真实的图片
    bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.fast, opts);
    
    /**
     * 根据图片的原始宽高比和ImageView的宽高计算图片的采样比例
     * @param outWidth
     * @param outHeight
     * @return
     */
    private int caculateSampleSize(int outWidth, int outHeight) {
        int inSampleSize = 1;
        if(outWidth>outHeight){
            //参考宽进行缩放
            inSampleSize = outWidth/iv_result.getWidth();//1920/300
        }else{
            //参考高进行缩放
            inSampleSize = outHeight/iv_result.getHeight();
        }
        if(inSampleSize<1){
            inSampleSize = 1;
        }
        return inSampleSize;
    }
    
  • 第三步: 设置图片加载的渲染模式为Config.RGB_565,能降低一半内存:

    opts.inPreferredConfig = Config.RGB_565
    

5. Volley中的ImageLoader使用以及内存缓存详解

  • 使用ImageLoader加载图片

    protected void loadImage() {
        ImageListener imageListener = ImageLoader.getImageListener(iv_result, R.drawable.ic_launcher, R.drawable.ic_launcher);
        MemoryCache imageCache  = new MemoryCache();
        ImageLoader imageLoader = new ImageLoader(queue, imageCache);
        imageLoader.get(imageUrl, imageListener);
    }
    /**
     * 图片内存缓存
     * @author lxj
     *
     */
    public class MemoryCache implements ImageCache{
        private LruCache<String, Bitmap> lruCache;
        //app可用内存的8分之一
        private int maxSize = (int) (Runtime.getRuntime().totalMemory()/8);
        public MemoryCache(){
            lruCache = new LruCache<String, Bitmap>(maxSize){
                @Override
                protected int sizeOf(String key, Bitmap value) {
                    return value.getHeight()*value.getRowBytes();
                }
            };
        }
        @Override
        public Bitmap getBitmap(String url) {
            return lruCache.get(url);
        }
    
        @Override
        public void putBitmap(String url, Bitmap bitmap) {
            lruCache.put(url, bitmap);
        }
    }
    
  • 内存缓存详解

    • 内存缓存的存储结构:一般是map,因为需要存取
    • 在Android2.3之前还没有好的内存缓存策略出现,一般采用SoftRefrence对Bitmap进行包装,能尽量保证不会出现oom,几种引用的解释如下:

      • 强引用 : 引用默认就是强引用, 即使内存oom, 也不会去回收对象
      • 软应用:使用SoftRefrence去包装一个对象,内存不足的时候去回收对象,尽量保证不oom,代码如下:

        HashMap<String, SoftReference<Bitmap>> map = new HashMap<String, SoftReference<Bitmap>>();
        SoftReference<Bitmap> reference = map.get(url);
        Bitmap bitmap = reference.get();
        
      • 弱引用:使用WeakReference保证一个对象,一般不用
    • 在Android2.3之后Google提供了Lru算法的实现类,即LruCache,并推荐我们使用LruCache来实现图片的内存缓存,该类有效解决了Android中图片内存缓存的难题,常见几种内存缓存策略有:
      • Least Frequently Used(LFU): 删除使用频率最少的
      • Least Recently Used(LRU): 删除最近最少使用的
      • First in First out(FIFO): 删除最先添加进来的
      • Most Recently Used(MRU): 删除最近最多使用的
    • LruCache内部实现原理(重点)
      • 内部使用按照访问顺序排序的LinkedHashMap来存储数据
      • 每次缓存命中,会将该条数据移到上方
      • 并会判断缓存size是否超出maxSize,如果超出则移除最下方的数据,即最少使用的数据
      • 我们必须实现sizeOf方法,用来指定每条数据的size,此处是返回bitmap的大小
    • 了解XUtil等开源类库对图片内存缓存的实现
    • 了解图片的磁盘缓存的实现DiskLruCache

6. Volley中的NetworlImageView的使用

MemoryCache imageCache  = new MemoryCache();
ImageLoader imageLoader = new ImageLoader(queue, imageCache);
net_imageview.setImageUrl(imageUrl, imageLoader);

7. 自定义Volley的Request

  • 我们在开发中更多的是请求一个url,然后将结果解析为java bean,所以封装一个具有该功能的Request类,GosnRequest
  • 代码如下:

    protected void execGsonRequest() {
        GsonRequest<Stu> gsonRequest = new GsonRequest<Stu>(url, Stu.class, new Listener<Stu>() {
            @Override
            public void onResponse(Stu stu) {
                tv_result.setText(stu.toString());
            }
        }, new MyErrorListener());
        queue.add(gsonRequest);
    }
    public class GsonRequest<T> extends Request<T>{
        private Class<T> clazz;
        private final Listener<T> mListener;
         public GsonRequest(String url,Class<T> clazz,Listener<T> listener, ErrorListener errorListener) {
            super(url, errorListener);
            mListener = listener;
            this.clazz = clazz;
        }
        @Override
        protected Response<T> parseNetworkResponse(NetworkResponse response) {
             String parsed;
                try {
                    parsed = new String(response.data, HttpHeaderParser.parseCharset(response.headers));
                } catch (UnsupportedEncodingException e) {
                    parsed = new String(response.data);
                }
                //解析json,返回response
                T t = new Gson().fromJson(parsed,clazz );
                return Response.success(t, HttpHeaderParser.parseCacheHeaders(response));
        }
        @Override
        protected void deliverResponse(T response) {
             mListener.onResponse(response);
        }
    }
    

8. 使用Volley取消网络请求

  • 给每个Request添加tag标识

    stringRequest.setTag(this);
    
  • 调用取消请求的方法

    queue.cancelAll(this);
    

9. 使用Volley发送post请求,需要自己重写Request的getParams方法

    public class PostReuqest extends StringRequest {
        private Map<String, String> params;
        public PostReuqest(String url, Response.Listener<String> listener, Response.ErrorListener errorListener) {
            super(url, listener, errorListener);
        }
        public PostReuqest(int method,String url, Response.Listener<String> listener, Response.ErrorListener errorListener) {
            super(method,url, listener, errorListener);
        }
        public void setParams(Map<String, String> params){
            this.params = params;
        }
        @Override
        protected Map<String, String> getParams() throws AuthFailureError {
            return params;
        }
    }

    PostReuqest stringRequest = new PostReuqest(Request.Method.POST,Api.LOGIN, new com.android.volley.Response.Listener<String>() {
        @Override
        public void onResponse(String response) {
            text.setText(response);
        }
    }, new com.android.volley.Response.ErrorListener() {
        @Override
        public void onErrorResponse(VolleyError error) {

        }
    });
    HashMap<String,String> map = new HashMap<>();
    map.put("username","hehehe");
    map.put("password","12321");
    stringRequest.setParams(map);

10. 对Volley进行二次封装

/**
 * Volley的二次封装类
 * @author lxj
 *
 */
public class VolleyHelper {
    private RequestQueue requestQueue;
    MemoryCache imageCache  = new MemoryCache();
    private static VolleyHelper mInstance = null;
    private VolleyHelper(Context context){
        requestQueue = Volley.newRequestQueue(context);

    }
    public static VolleyHelper get(Context context){
        if(mInstance==null){
            mInstance = new VolleyHelper(context);
        }
        return mInstance;
    }
    public <T> void executeRequest(Request<T> request){
        requestQueue.add(request);
    }
    /**
     * 执行GsonRequest
     * @param url
     * @param clazz
     * @param listener
     * @param errorListener
     */
    public <T> void executeGsonRequest(String url,Class<T> clazz,Listener<T> listener,ErrorListener errorListener){
        GsonRequest<T> gsonRequest = new GsonRequest<T>(url, clazz, listener, errorListener);
        gsonRequest.setTag(url);
        requestQueue.add(gsonRequest);
    }
    /**
     * 取消请求
     * @param tag
     */
    public void cancelRequest(String tag){
        requestQueue.cancelAll(tag);
    }
    /**
     * 加载图片
     * @param imageUrl
     * @param imageView
     */
    public void loadImage(String imageUrl,ImageView imageView){
        ImageListener imageListener = ImageLoader.getImageListener(imageView, R.drawable.ic_launcher, R.drawable.ic_launcher);
        ImageLoader imageLoader = new ImageLoader(requestQueue, imageCache);
        imageLoader.get(imageUrl, imageListener);
    }
}

10. UniversalImageloader介绍与使用

  • UniversalImageloader的介绍
    • Github上star数最多的图片加载库
    • 拥有丰富的配置项,如线程池,下载器,缓存策略,缓存文件名称生成器等
    • 支持加载多种来源的图片,如sd卡,网络,uri,asset,res目录的
    • 支持给加载图片添加动画和圆角图片
  • UniversalImageloader的使用

    • 先初始化UniversalImageloader,一般在application的onCreate中初始化

      public static void initImageLoader(Context context) {
          // This configuration tuning is custom. You can tune every option, you may tune some of them,
          // or you can create default configuration by
      //        ImageLoaderConfiguration.createDefault(this);
              // method.
              ImageLoaderConfiguration.Builder config = new ImageLoaderConfiguration.Builder(context);
              config.threadPriority(Thread.NORM_PRIORITY - 2);
              config.denyCacheImageMultipleSizesInMemory();//不会在内存中缓存多个大小的图片
              config.diskCacheFileNameGenerator(new Md5FileNameGenerator());//为了保证图片名称唯一
              config.diskCacheSize(50 * 1024 * 1024); // 50 MiB
              //内存缓存大小默认是:app可用内存的1/8
              config.tasksProcessingOrder(QueueProcessingType.LIFO);
              config.writeDebugLogs(); // Remove for release app
      
              // Initialize ImageLoader with configuration.
              ImageLoader.getInstance().init(config.build());
      //      ImageLoader.getInstance().init( ImageLoaderConfiguration.createDefault(this));
      }
      
    • 调用displayImage方法显示图片:

      ImageLoader.getInstance().displayImage(IMAGE_URLS[position], holder.image, options);
      
    • options的创建如下:

      options = new DisplayImageOptions.Builder()
              .showImageOnLoading(R.drawable.ic_stub)
              .showImageForEmptyUri(R.drawable.ic_empty)
              .showImageOnFail(R.drawable.ic_error)
              .cacheInMemory(true)
              .cacheOnDisk(true)
              .considerExifParams(true)//会识别图片的方向信息
              .displayer(new FadeInBitmapDisplayer(500)).build();
            //.displayer(new RoundedBitmapDisplayer(36)).build();
      

11. MVP架构模式

  • 概念解释
    • MVP是Model(数据) View(界面) Presenter(表现层)的缩写,它是MVC架构的变种,强调Model和View的最大化解耦和单一职责原则;
    • Model:负责数据相关的业务逻辑,包括数据库,网络,本地缓存,内存数据的业务逻辑处理,并提供接口暴露自己处理数据的状态和进度。
    • View:负责UI相关的业务逻辑,包括显示对话框,Toast,展示listview,gridview等所有的UI相关逻辑,并提供接口暴露自己处理UI逻辑的状态和进度。
    • Presenter:Model和View的互相调用的桥梁,本身不应该有任何具体的逻辑代码,只是在对应的状态下调用Model和View的方法实现数据相关逻辑和UI展示相关逻辑,并且与Fragment和Activity直接打交道;
    • 目前MVP的P有分歧,部分人当Presenter独立抽出,与Activity或Fragment交互,还有部分人直接将Activity和Fragment充当Presenter
  • 先看看一个登录界面的数据业务逻辑和UI逻辑全部写在Activity的代码:

    public class LoginActivity extends ActionBarActivity implements OnClickListener{
        EditText etUsername,etPassword;
        Button btn_login;
        @Override
        protected void onCreate(Bundle savedInstanceState) {
            super.onCreate(savedInstanceState);
            setContentView(R.layout.activity_main);
            etUsername = (EditText) findViewById(R.id.et_username);
            etPassword = (EditText) findViewById(R.id.et_password);
            btn_login = (Button) findViewById(R.id.btn_login);
    
            //设置点击事件
            btn_login.setOnClickListener(this);
        }
        @Override
        public void onClick(View v) {
            switch (v.getId()) {
            case R.id.btn_login:
                String username = etUsername.getText().toString();
                String password = etPassword.getText().toString();
    
                if(checkInput(username,password)){
                    //提交登录
                    //显示正在登录对话框
                    showLoginDialog(this);
                    //执行登录请求
                    execLogin(username, password);
                }
    
                break;
            }
        }
    
        private ProgressDialog progressDialog;
        //显示登录进度对话框
        public void showLoginDialog(Context context){
            if(progressDialog==null){
                progressDialog = new ProgressDialog(context);
                progressDialog.setTitle("正在登录中...");
            }
            progressDialog.show();
        }
        //关闭登录进度对话框
        public void hideLoginDialog(Context context){
            if(progressDialog!=null){
                progressDialog.dismiss();
            }
        }
    
        //执行登录请求
        public void execLogin(String username,String password){
            //执行登录请求的伪代码
            HttpHelper helper = new HttpHelper();
            helper.execRequest("http://www.baidu.com", new HttpCallback() {
                @Override
                public void onSuccess() {
                    //关闭登录对话框
                    hideLoginDialog(LoginActivity.this);
                    //提示登录成功
                    showLoginSuccess(LoginActivity.this);
    
                    //保存登录相关数据,如登录的标记,用户的唯一标识
                    saveLoginData();
                }
                @Override
                public void onFail() {
                    //关闭登录对话框
                    hideLoginDialog(LoginActivity.this);
                    //提示登录失败
                    showLoginFail(LoginActivity.this);
                }
            });
        }
        //保存登录数据
        public void saveLoginData(){
            //保存登录相关的数据,代码略过...
        }
        //提示登录成功
        public void showLoginFail(Context context){
            Toast.makeText(context, "登录失败", 0).show();
        }
        //提示登录失败
        public void showLoginSuccess(Context context){
            Toast.makeText(context, "登录成功", 0).show();
        }
        //检查输入的合法性
        private boolean checkInput(String username, String password) {
            boolean result = true;
            //1.检查为空
            if(TextUtils.isEmpty(username) || TextUtils.isEmpty(password)){
                Toast.makeText(this, "用户名或者密码不能为空!", 0).show();
                result = false;
            }
            //2.检查长度
            if(username.length()!=11){
                Toast.makeText(this, "用户名长度不正确!", 0).show();
                result = false;
            }
            if(password.length()<5){
                Toast.makeText(this, "密码长度不能小于5位!", 0).show();
                result = false;
            }
            return result;
        }
    }
    
  • 现在抽取出LoginModel,LoginView和LoginPresenter类,如下:

    • LoginView类:

      /**
       * 负责登录模块相关的UI等所有UI相关逻辑
       * @author lxj
       *
       */
      public class LoginView {
          public void showLoginSuccess(Context context){
              Toast.makeText(context, "登录成功", 0).show();
          }
          public void showLoginFail(Context context){
              Toast.makeText(context, "登录失败", 0).show();
          }
          public void showInputNoNull(Context context){
              Toast.makeText(context, "用户名和密码不能为空!", 0).show();
          }
          public void showUsernameLengthError(Context context){
              Toast.makeText(context, "用户名长度不正确!", 0).show();
          }
          private ProgressDialog progressDialog;
          //显示登录中对话框
          public void showLoginDialog(Context context){
              if(progressDialog==null){
                  progressDialog = new ProgressDialog(context);
                  progressDialog.setTitle("正在登录中...");
              }
              progressDialog.show();
          }
          //隐藏登录中对话框
          public void hideLoginDialog(Context context){
              if(progressDialog!=null){
                  progressDialog.dismiss();
              }
          }
      }
      
    • LoginModel类如下:

      /**
       * 负责处理登录相关数据的业务逻辑,并根据数据处理的状态和进度
       * 暴露接口,通知外界
       * @author lxj
       * 
       */
      public class LoginModel {
          private LoginModelObserver loginModelObserver;
          public void setLoginModelObserver(LoginModelObserver loginModelObserver) {
              this.loginModelObserver = loginModelObserver;
          }
          // 执行登录请求
          public void execLogin(String username, String password) {
              if(!checkInput(username, password)){
                  return;
              }
              // 执行登录请求的伪代码
              HttpHelper helper = new HttpHelper();
              helper.execRequest("http://www.baidu.com", new HttpCallback() {
                  @Override
                  public void onSuccess() {
                      // 需要UI展示,暴露接口
                      if(loginModelObserver!=null){
                          loginModelObserver.onLoginSuccess();
                      }
                      // 保存登录相关数据,如登录的标记,用户的唯一标识
                      saveLoginData();
                  }
                  @Override
                  public void onFail() {
                      // 需要UI展示,暴露接口
                      if(loginModelObserver!=null){
                          loginModelObserver.onLoginFail();
                      }
                  }
              });
          }
          // 保存登录数据
          public void saveLoginData() {
              // 保存登录相关的数据,代码略过...
          }
          // 检查输入的合法性
          private boolean checkInput(String username, String password) {
              boolean result = true;
              // 1.检查为空
              if (TextUtils.isEmpty(username) || TextUtils.isEmpty(password)) {
                  if (loginModelObserver != null) {
                      loginModelObserver.onInputNoNull();
                  }
                  result = false;
              }
              // 2.检查长度
              if (username.length() != 11) {
                  if (loginModelObserver != null) {
                      loginModelObserver.onUsernameLengthError();
                  }
                  result = false;
              }
              return result;
          }
      
          // 定义数据处理状态的回调
          public interface LoginModelObserver {
              // 输入数据不能为空的回调
              void onInputNoNull();
              // 用户名长度不正确的回调
              void onUsernameLengthError();
              //登录成功的回调
              void onLoginSuccess();
              //登录失败的回调
              void onLoginFail();
          }
      }
      
    • LoginPresenter类如下:

      /**
       * 负责连接LoginView和LoginModel的表现层
       * 具体就是调用LoginModel和LoginView的方法
       * @author lxj
       *
       */
      public class LoginPresenter implements LoginModelObserver{
          private LoginModel loginModel;
          private LoginView loginView;
          private Context context;
          public LoginPresenter(Context context){
              this.context = context;
      
              loginModel = new LoginModel();
              loginView = new LoginView();
      
              loginModel.setLoginModelObserver(this);
          }
          //登录的方法
          public void login(String username, String password) {
              //调用loginModel封装好的方法
              loginModel.execLogin(username, password);
          }
          @Override
          public void onInputNoNull() {
              //调用loginView展示UI的方法
              loginView.showInputNoNull(context);
          }
          @Override
          public void onUsernameLengthError() {
              loginView.showUsernameLengthError(context);
          }
          @Override
          public void onLoginSuccess() {
              loginView.showLoginSuccess(context);
          }
          @Override
          public void onLoginFail() {
              loginView.showLoginFail(context);
          }
      }
      
    • 最后LoginActivity实现如下:

      public class LoginActivity extends ActionBarActivity implements OnClickListener{
          EditText etUsername,etPassword;
          Button btn_login;
          private LoginPresenter loginPresenter;
          @Override
          protected void onCreate(Bundle savedInstanceState) {
              super.onCreate(savedInstanceState);
              setContentView(R.layout.activity_main);
              etUsername = (EditText) findViewById(R.id.et_username);
              etPassword = (EditText) findViewById(R.id.et_password);
              btn_login = (Button) findViewById(R.id.btn_login);
      
              //设置点击事件
              btn_login.setOnClickListener(this);
      
              //与loginPresenter交互
              loginPresenter = new LoginPresenter(this);
          }
          @Override
          public void onClick(View v) {
              switch (v.getId()) {
              case R.id.btn_login:
                  String username = etUsername.getText().toString();
                  String password = etPassword.getText().toString();
                  loginPresenter.login(username,password);
                  break;
              }
          }
      }
      
  • 总结:MVP只是给我们提出了分层解耦的思想,并没有一个固定的实现。Google虽然出了官方的MVP实现示例,但是并没有太多人去跟随,很多公司在对Presenter层都有自己的理解