Android利用okhttp实现图片上传之安卓客户端请求

版权声明:如果需要引用文中内容,希望可以带上相应的链接地址,万分感谢。地址: https://blog.csdn.net/shaowanyunBLOG/article/details/82179271

安卓端上传图片到后台,或者是从后台获取图片显示到手机上这种功能是再寻常不过了,可是对于刚刚接触这方面技术的新人来说,也是“旁观者迷”,甚至用什么方法去实现都无法确定,因为拿我自己来说,之前一直了解相关的技术,然后知道,有的人是说把图片上传到本地,然后用一个地址的映射,然后客户端去访问地址,我想了一下,那么这样的话,我的数据实体里面对于图片的处理就应该是一个String类型的Url,但是我又听人说,像图片或者是其他的文件应该使用byte数组存到数据库中,然后直接像是“所见即所得”的方式一样,直接把数组传过来,然后客户端再解析显示出来就好了,那么这样也很好理解,数据实体中应该使用byte[]作为图片的数据类型,比较两者,我觉得,后者更好理解一些,而且,后者的话是我的一个前辈推荐给我的一种方式,所以,思前想后,我就决定了,就是byte数组了。然后在具体的实现过程中也是遇到了不少的坑,然后下面我也会一步步的分享给大家。

okhttp的话,应该大家都很熟悉了,是目前很流行的一个框架,之前在做学校毕业设计的时候我找的是httpClient的例子,但是查了一下,现在基本上都不用httpClient了,而且也听说了okhttp的方便快捷,所以最后就选择了它作为我的网络请求框架。

那么接下来就一步步开始吧

首先是我的数据实体User的定义

/**
 * Created by 15927 on 2018/7/24.
 * 设计:
 * note是判断该组数据的使用用途,例如:0是表示注册;1表示登录;2表示更改i密码
 * icon是用户的头像,用户可以拍照或者是从相册中选取上传,并且做剪切和压缩处理
 * username是数据库的主键,用户注册时会先判断是否昵称占用
 * password是用户的登录密码,这里会把用户注册的密码进行md5加盐加密之后保存为真正的password
 */
public class User {
    private int note;
    private byte[] icon;
    private String username;
    private String password;

    public int getNote() {
        return note;
    }

    public void setNote(int note) {
        this.note = note;
    }

    public byte[] getIcon() {
        return icon;
    }

    public void setIcon(byte[] icon) {
        this.icon = icon;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public User() {
    }

    public User(int note, byte[] icon, String username, String password) {
        this.note = note;
        this.icon = icon;
        this.username = username;
        this.password = password;
    }

    @Override
    public String toString() {
        return "User{" +
                "note=" + note +
                ", icon=" + Arrays.toString(icon) +
                ", username='" + username + '\'' +
                ", password='" + password + '\'' +
                '}';
    }
}

一共有四个属性,然后具体的每个属性的使用,我最上面的注释里也有简单的说明。

然后为了讲解的简单性,我就以一个简单的注册功能为例,举例说明安卓端的okhttp的请求应该怎么去写

我先把我的关键的请求的代码一次性贴出来,然后我再分段进行进行讲解

class RegisterTask extends AsyncTask<Void, Integer, Boolean> {
    @Override
    protected void onPreExecute() {
        progressDialog = ProgressBarDialog.createLoadingDialog(RegisterActivity.this, "正在上传账户");
        progressDialog.setCancelable(false);
        progressDialog.show();
    }

    @Override
    protected Boolean doInBackground(Void... params) {
        try {
            register();
        } catch (Exception e) {
            return false;
        }
        return true;
    }

    @Override
    protected void onPostExecute(Boolean result) {
        if (progressDialog != null) {
            progressDialog.dismiss();
        }
        Toast.makeText(RegisterActivity.this,responseInfo,Toast.LENGTH_SHORT).show();
        if(!TokenTimeout && responseCode == 1){
            finish();
        }
    }
}

private void register(){
    try {
        OkHttpClient client = new OkHttpClient(); //RequestBody中的MediaType指定为纯文本,编码方式是utf-8
        User user = new User();
        user.setNote(1);
        user.setIcon(Util.Bitmap2Bytes(((BitmapDrawable) ((ImageView) userlogo).getDrawable()).getBitmap()));
        user.setUsername(username);
        user.setPassword(md5(password,username));
        Gson gson = new Gson();
        RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain;charset=utf-8"), gson.toJson(user));
        final Request request = new Request.Builder().url("http://" + host + "/server/UserServlet").post(requestBody).build();
        Response response = client.newCall(request).execute();
        if (response.isSuccessful()) {
            Gson gson1 = new Gson();
            ServletResponse servletResponse = gson1.fromJson(response.body().string(),ServletResponse.class);
            Log.i("Marketlog","response.body().string() is : "+servletResponse.toString());
            responseCode = servletResponse.getResponseCode();
            responseInfo = servletResponse.getResponseInfo();
            Log.i("Marketlog","responseCode is : "+responseCode);
            Log.i("Marketlog","responseInfo is : "+responseInfo);
            if(responseCode != 0){
                if(responseCode == 99){
                    TokenTimeout = true;
                }else {
                    TokenTimeout = false;
                    RegisterSuccess = true;
                }
            }
        }
    }catch(Exception e){
        Log.i("Marketlog","Exception");
        e.printStackTrace();
    }
}

上面就是我的用户注册的异步任务部分,为了方便,我用了AsyncTask作为我的网络请求以及响应服务器的载体方法。对异步任务比较熟悉的朋友们可能知道,它有很多个回调,但是比较常用的就是onPreExecute()、doInBackground()、onPostExecute()。等等,还有其他的方法,但是我对它的使用不深,或者是我还没有做到其他的功能,所以就先按照我的理解给大家去讲解。仅是个人意见。

我想先说关键的方法doInBackground(),这个方法里面执行的是具体的耗时任务,也就是我这里对应的网络请求的方法register();在执行耗时任务的同时,为了界面上不至于傻傻干等着,所以用onPreExecute()缓解一下尴尬,同时提醒一下用户当前的状态,比如说我这里,自定义了一个进度条,然后提醒了用户“正在上传账户”,这样用户就知道了程序没有死,还在正常运行,正在向后台上传用户信息,当服务器收到了用户信息,然后做了相应的处理,并且把返回结果带回来了之后,怎么根据返回结果告知用户到底是成功了?还是失败了?还是说用户已经被注册过了?那么最后就来到了onPostExecute()这个方法,我在这个方法里面先把我的进度条取消了,然后Toast了对应的相应信息,这样对用户来说,整个过程无论结果是成功还是失败,就一目了然了,而且也很容易根据我们的需求去进行模块化设计。

那么上面只是大概讲解了使用AsyncTask执行耗时任务的大体流程,关键部分还是在register()这个方法里面到底是怎么写的,又是怎么获取后台的返回信息,然后又是怎样去告诉主线程,现在的状态是怎样的。所以,这部分才是重中之重。

一开始先new一个okhttpClient的对象没什么好说的,我这里为了传输过程更加方便快捷,所以统一使用了json字符串进行传输我得所有的数据,那么首先要考虑的就是如何把数据转化成json字符串,如果让我自己去写一个满足json数据格式的String字符串?那是不可能的,最起码太麻烦了,所以,想到了Gson的tojson方法可以将实体类转化成json串,所以就有了如下的步骤:

User user = new User();
user.setNote(1);
user.setIcon(Util.Bitmap2Bytes(((BitmapDrawable) ((ImageView) userlogo).getDrawable()).getBitmap()));
user.setUsername(username);
user.setPassword(md5(password,username));
Gson gson = new Gson();
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain;charset=utf-8"), gson.toJson(user));

这里面看着好像没什么重要的,但是我这篇文章的题目是安卓实现图片上传,而且我上面又说要把图片用byte[]进行保存,所以对我的头像的ImageView的处理,也就是user.setIcon的方法,对头像的上传就稍显重要了。

我的大体思路是这样的,先从ImageView拿到drawable,然后转成bitmap,最后再转成byte[],下面我先把那个Bitmap2Bytes的方法贴出来,这个方法是网上找的:

public static byte[] Bitmap2Bytes(Bitmap bm) {
    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    bm.compress(Bitmap.CompressFormat.PNG, 100, baos);
    return baos.toByteArray();
}

可以看到,非常简单。

如果再细心观察,其实我一开始贴我的数据实体类User的时候,我对password这个字段的描述中就说了“这里会把用户注册的密码进行md5加盐加密之后保存为真正的password”,一般来说,我们自己做一个小的demo自己玩的时候,可能不用考虑安全问题,但是稍微了解一下都会知道,一般在保存密码这种相对隐秘的信息的时候,数据库或者是网络传输过程中都是不能直接传明文的,所以我这里为了方便就使用了MD5的加密,然后为了进一步保证安全,就使用了MD5的加盐算法,其实加盐也不过是像是加了一个密钥一样的,就是增加了破解的难度,其实严格来说,我都不知道别人会不会用我的app,何谈别人还费心思破解= =。好吧,不管破解不破解,最起码这个安全意识还是最起码需要的。下面我把我用的MD5加盐算法的代码贴出来,也是从网上找的:

public static String md5(String string, String slat) {
    if (TextUtils.isEmpty(string)) {
        return "";
    }
    MessageDigest md5 = null;
    try {
        md5 = MessageDigest.getInstance("MD5");
        byte[] bytes = md5.digest((string + slat).getBytes());
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            String temp = Integer.toHexString(b & 0xff);
            if (temp.length() == 1) {
                temp = "0" + temp;
            }
            result.append(temp);
        }
        return result.toString();
    } catch (NoSuchAlgorithmException e) {
        e.printStackTrace();
    }
    return "";
}

然后我还了解到,MD5的加盐算法中的这个“盐”的选取还是挺讲究的,它好像必须是一个固定的串,然后大家一般用用户名作为“盐”,我心想,真的很有道理啊,自己的密码自己管,简直公平啊。

所以我上面的使用就是这样的:user.setPassword(md5(password,username));,也很简单。

然后把http的请求实体弄好之后,就开始向后台发请求了,然后又创建了一个Response静静等待后台给返回信息

Response response = client.newCall(request).execute();

上面我也说了,对于后台的返回值的解析处理是同样重要的,我这里的写法是这样的:

if (response.isSuccessful()) {
    Gson gson1 = new Gson();
    ServletResponse servletResponse = gson1.fromJson(response.body().string(),ServletResponse.class);
    Log.i("Marketlog","response.body().string() is : "+servletResponse.toString());
    responseCode = servletResponse.getResponseCode();
    responseInfo = servletResponse.getResponseInfo();
    Log.i("Marketlog","responseCode is : "+responseCode);
    Log.i("Marketlog","responseInfo is : "+responseInfo);
    if(responseCode != 0){
        if(responseCode == 99){
            TokenTimeout = true;
        }else {
            TokenTimeout = false;
            RegisterSuccess = true;
        }
    }
}

很直接,但是很简陋,只对请求成功做了判断,并还没有考虑到请求超时,或者是服务器请求异常或是响应异常等错误的判断,当然那些我后续会再加上的,然后这里就当是我的一个理想网络请状态吧。

我先简单的对里面涉及到的一些变量进行一下解释,首先映入眼帘的是Gson的对象,那个是因为我的后台也是返回的标准的json字符串,所以我需要用它进行相应的解析,那么解析的结果的实体类是什么呢,不卖关子了,就是ServletResponse 这样一个实体类,这个是我自己定义的。具体定义代码如下:

public class ServletResponse {
    private int responseCode;
    private String responseInfo;
    private byte[] icon;
    private String username;

    public int getResponseCode() {
        return responseCode;
    }

    public void setResponseCode(int responseCode) {
        this.responseCode = responseCode;
    }

    public String getResponseInfo() {
        return responseInfo;
    }

    public void setResponseInfo(String responseInfo) {
        this.responseInfo = responseInfo;
    }

    public byte[] getIcon() {
        return icon;
    }

    public void setIcon(byte[] icon) {
        this.icon = icon;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public ServletResponse() {
    }

    public ServletResponse(int responseCode, String responseInfo, byte[] icon, String username) {
        this.responseCode = responseCode;
        this.responseInfo = responseInfo;
        this.icon = icon;
        this.username = username;
    }

    @Override
    public String toString() {
        return "ServletResponse{" +
                "responseCode=" + responseCode +
                ", responseInfo='" + responseInfo + '\'' +
                ", icon=" + Arrays.toString(icon) +
                ", username='" + username + '\'' +
                '}';
    }
}

里面现在是共有四个参数,前两个分别是返回码和响应信息,按照我对注册的返回结果的设计是这样的:0代表注册失败,1代表注册成功,2代表是用户已经被注册过了。然后相应的响应信息我已经在服务端进行了封装,那么除了这两个之外,还可以看到又两个参数,一个是username,另一个就是icon,这两个我简单的说明一下,它们和注册功能其实无关,主要是在我的登录过程中,如果登陆验证通过的话,后台需要把相关的用户名和头像再返回一下,因为我的客户端的“我的”界面要用这两个信息。像我得注册确定不用这两个信息,所以后台也不返回这两个信息,然后我这里也不去接这两个参数,只拿了前两个参数,然后再根据我的设计去进行处理,然后,从上面代码可以看到我的后台定义了一个统一的返回码是99,这个返回码是用来判断token是否过期的,然后关于token的话,大家有兴趣的可以先了解一下,然后后面我会再给大家分享我的后台对token功能的设计,这里稍微有个了解就行了。

然后responseCode 和responseInfo 这两个是我new的两个全局String变量,用于接后台返回的错误码信息和响应信息数据进行全局的判断处理,TokenTimeout和RegisterSuccess这两个参数是我new的两个全局boolean型变量,通过这两个参数的true或false,然后对应提示给用户不一样的信息,如上面的AsyncTask的onPostExecute()这个方法里面写的:

Toast.makeText(RegisterActivity.this,responseInfo,Toast.LENGTH_SHORT).show();
if(!TokenTimeout && responseCode == 1){
    finish();
}

无论是true还是false,无论成功与否,都先把后台返回的状态信息responseInfo显示给用户,告知用户一个状态,然后如果token验证通过,并且登陆成功,那么就返回登录界面,我这里直接使用了finish()方法,这个也算是对返回栈的一个使用吧,然后关于安卓的Activity返回栈,我也是有自己的一些粗鄙的理解,可能不是很严谨,但是简单的理解也应该无可厚非。最起码使用起来还是很好用的,那么我这里就厚着脸皮的把地址给大家了:https://blog.csdn.net/shaowanyunBLOG/article/details/82180914

那么讲到这里,其实关于我的Android实现图像上传的安卓客户端的请求部分的讲解就基本上告一段落了,但是整个讲解才刚刚开始,关于本文对应的后台的讲解的话,欢迎大家看我的另一篇博客《Java后台实现用户登录注册Servlet以及对数据库怎么处理》,链接地址:https://blog.csdn.net/shaowanyunBLOG/article/details/82181175,然后我也会经常更新我的博客的,然后我呢也是才疏学浅,很多东西讲解的不是那么的完善和严谨,希望大家多多理解,然后欢迎大家在评论区讨论问题,我也是尽我最大的努力进行回复,反正讲的到与不到的,大家多多原谅,对于有些错误的,也请大家多多指正,提前谢过。

猜你喜欢

转载自blog.csdn.net/shaowanyunBLOG/article/details/82179271