Android通过okhttp访问网络
简介
okhttp是一个第三方类库,用于Android中访问网络。这是一个开源项目,是安卓端最火热的轻量级框架,由移动支付Square公司贡献。用于替代HttpUrlConnection和Apache HttpClient(android API23 里已移除HttpClient)。
准备工作
在Android Studio中创建一个新Android项目。
【方式一】本地导入jar包
OkHttp jar包下载:https://search.maven.org/artifact/com.squareup.okhttp3/okhttp/4.10.0/jar
Downloads里可以下载jar和sources.jar
我在这里使用大赛提供好的jar包。
- okhttp-4.9.0.jar
- okio-jvm-2.8.0.jar
- kotlin-stdlib-1.4.10.jar
- kotlin-stdlib-common-1.4.10.jar
- annotations-13.0.jar
由于okhttp依赖于okio(支持相关流操作),还需要把okio一并导入,但是需要知道okhttp对应的okio版本,比如4.9.0对应的是2.8.0。
另外还需要添加kotlin-stdlib库。
切换到“Project”视图,找到app/libs目录,将这些.jar逐一复制到这个目录下。
然后,逐一在这些.jar文件上右键,选择【Add As Library】
【方式二】Gradle添加依赖
在build.gradle中添加依赖
implementation 'com.squareup.okhttp3:okhttp:4.10.0'
【方式三】Maven构建
在pom.xml中添加
<dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>4.10.0</version> </dependency>
禁用掉明文流量请求的检查
1.在res目录下找到或者新建xml文件夹,在xml文件夹下新建network_security_config.xml
<?xml version="1.0" encoding="utf-8"?> <network-security-config xmlns:android="http://schemas.android.com/apk/res/android"> <!--禁用掉明文流量请求的检查--> <base-config cleartextTrafficPermitted="true" /> </network-security-config>
2.在manifests-AndroidManifest.xml中添加刚才创建的network_security_config.xml
给<application>标记添加属性:android:networkSecurityConfig="@xml/network_security_config"
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.sdbi.smartcitytest01"> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SmartCityTest01" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
否则,会报如下错误
报错信息:java.net.UnknownServiceException: CLEARTEXT communication to 124.93.196.45 not permitted by network security policy
W/System.err: java.net.UnknownServiceException: CLEARTEXT communication to 124.93.196.45 not permitted by network security policy
W/System.err: at okhttp3.internal.connection.RealConnection.connect(RealConnection.kt:188)
W/System.err: at okhttp3.internal.connection.ExchangeFinder.findConnection(ExchangeFinder.kt:226)
W/System.err: at okhttp3.internal.connection.ExchangeFinder.findHealthyConnection(ExchangeFinder.kt:106)
W/System.err: at okhttp3.internal.connection.ExchangeFinder.find(ExchangeFinder.kt:74)
W/System.err: at okhttp3.internal.connection.RealCall.initExchange$okhttp(RealCall.kt:255)
W/System.err: at okhttp3.internal.connection.ConnectInterceptor.intercept(ConnectInterceptor.kt:32)
W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
W/System.err: at okhttp3.internal.cache.CacheInterceptor.intercept(CacheInterceptor.kt:95)
W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
W/System.err: at okhttp3.internal.http.BridgeInterceptor.intercept(BridgeInterceptor.kt:83)
W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
W/System.err: at okhttp3.internal.http.RetryAndFollowUpInterceptor.intercept(RetryAndFollowUpInterceptor.kt:76)
W/System.err: at okhttp3.internal.http.RealInterceptorChain.proceed(RealInterceptorChain.kt:109)
W/System.err: at okhttp3.internal.connection.RealCall.getResponseWithInterceptorChain$okhttp(RealCall.kt:201)
W/System.err: at okhttp3.internal.connection.RealCall.execute(RealCall.kt:154)
W/System.err: at com.sdbi.smartcitytest01.MainActivity$1$1.run(MainActivity.java:51)
W/System.err: at java.lang.Thread.run(Thread.java:764)
添加网络请求权限
在manifests-AndroidManifest.xml中添加:<uses-permission android:name="android.permission.INTERNET" />
<?xml version="1.0" encoding="utf-8"?> <manifest xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" package="com.sdbi.smartcitytest01"> <!-- 允许用户访问网络,这一行要加在<manifest>的下一行 --> <uses-permission android:name="android.permission.INTERNET" /> <application android:allowBackup="true" android:dataExtractionRules="@xml/data_extraction_rules" android:fullBackupContent="@xml/backup_rules" android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.SmartCityTest01" tools:targetApi="31"> <activity android:name=".MainActivity" android:exported="true"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> </intent-filter> </activity> </application> </manifest>
还需要在Activity启动时,动态获取权限:
import androidx.appcompat.app.AppCompatActivity; import androidx.core.app.ActivityCompat; import androidx.core.content.ContextCompat; import android.Manifest; import android.content.pm.PackageManager; import android.os.Bundle; import com.sdbi.smartcityli01.R; public class MainActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_test); if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED) { // 动态申请权限 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.INTERNET}, 1); } } }
Get方式
我们知道,请求消息本质上就是客户端告诉服务器的一些信息。请求消息由请求行、请求头、请求体组成。
Get方式没有请求体,请求参数在请求行中,在url后。
1、基本步骤
1.构建网络访问OkHttpClient对象
OkHttpClient okHttpClient = new OkHttpClient(); // 创建OkHttpClient对象
2.创建请求对象request
Request request = new Request.Builder().url("https://www.baidu.com").build(); // 创建一个Request对象,设置请求参数
3.创建Call对象
Call call = okHttpClient.newCall(request); // 通过okHttpClient构造一个Call对象,将你的请求request封装成进去
4.创建接收返回数据的对象response,并执行请求
Response response = call.execute(); // 得到响应对象
5.获取响应体
String strBody = response.body().string();
注意!response.body().string() 只能调用一次!
因为响应主体 ResponseBody持有的资源可能会很大,所以 OkHttp 并不会将其直接保存到内存中,只是持有数据流连接。只有当我们需要时,才会从服务器获取数据并返回。同时,考虑到应用重复读取数据的可能性很小,所以将其设计为一次性流(one-shot)
,读取后即 “关闭并释放资源”。
2、编写Get同步请求方法
注意这个方法不能在主线程中直接调用,因为主线程不允许访问网络。
public String getJsonByGetSync(String url) throws Exception { OkHttpClient okHttpClient = new OkHttpClient(); // 创建OkHttpClient对象 Request request = new Request.Builder().url(url).build(); // 创建一个Request对象,设置请求参数 Call call = okHttpClient.newCall(request); Response response = call.execute(); // 得到响应对象 if (response.isSuccessful()) { return response.body().string(); } return null; }
3、编写Get异步请求
异步Get请求一般用来请求下载文件或者图片,不会用来获取Json返回值,所以一般不会返回字符串。
public void getJsonByGetAsync(String url) { OkHttpClient okHttpClient = new OkHttpClient(); // 创建OkHttpClient对象 Request request = new Request.Builder().url(url).build(); // 创建一个Request对象,设置请求参数 Call call = okHttpClient.newCall(request); call.enqueue(new Callback() { // 异步请求得到响应对象 @Override public void onFailure(@NonNull Call call, @NonNull IOException e) { Log.d(TAG, "onFailure: e = " + e.getMessage()); } @Override public void onResponse(@NonNull Call call, @NonNull Response response) throws IOException { if (response.isSuccessful()) { Log.d(TAG, "onResponse:response = " + response.body().string()); } } }); }
Post方式
Post请求方式与Get方式类似,但是,Post方式请求服务器,请求的参数是需要放到请求体中的,这样我们就需要使用RequestBody来封装请求体。
在创建RequestBody对象之前,我们需要先指定请求体的数据类型MediaType。
1、请求体的数据类型MediaType
(1)multipart/form-data
以表单形式提交,主要是上传文件用它。
它会将表单的数据处理为一条消息,以标签为单元,用分隔符分开。既可以上传键值对,也可以上传文件。
当上传的字段是文件时,会有Content-Type来说明文件类型;content-disposition,用来说明字段的一些信息.
使用Postman测试
(2)application/x-www-from-urlencoded
会将表单内的数据转换为键值对,比如:name=java&age=23
![](https://img2022.cnblogs.com/blog/520426/202207/520426-20220716111049320-364481022.png)
(3)raw
选择Text,则请求头是: text/plain
选择JavaScript,则请求头是: application/javascript
选择JSON,则请求头是: application/json(如果想以json格式传参,就用raw+JSON就行了)
选择HTML,则请求头是: text/html
选择XML,则请求头是: application/xml
(4)binary
相当于Content-Type:application/octet-stream,从字面意思得知,只可以上传二进制数据,通常用来上传文件,由于没有键值,所以,一次只能上传一个文件。(一般用的不多)
![](https://img2022.cnblogs.com/blog/520426/202207/520426-20220716111813445-1055905286.png)
注意:Postman中 Params和Body的区别
Params:它会将参数放入url的?后面提交到后台(带到请求的接口链接里)
Body:是放在请求体里面。2、编写Post请求方法
public String getJsonByPost(String url) throws Exception { OkHttpClient okHttpClient = new OkHttpClient(); //创建OkHttpClient对象 // 创建json请求体 JSONObject json = new JSONObject(); try { json.put("username", "panda"); json.put("password", "123456"); } catch (JSONException e) { e.printStackTrace(); } // 创建请求体 MediaType mediaType = MediaType.parse("application/json;charset=utf-8"); RequestBody requestBody = RequestBody.create(String.valueOf(json), mediaType); // 创建Post请求 Request request = new Request.Builder() .url(url) .post(requestBody) // 添加post请求 .build(); Response response = okHttpClient.newCall(request).execute(); if (response.isSuccessful()) { return response.body().string(); // 注意response.body().string() 只能调用一次! } return null; }
3、POST方式上传文件
(1)调用系统打开文件窗口,获取文件路径和文件名
xml文件:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context=".fragment.PersonalFragment"> <com.google.android.material.appbar.CollapsingToolbarLayout android:layout_width="match_parent" android:layout_height="wrap_content" app:titleEnabled="false"> <include layout="@layout/layout_toolbar" /> </com.google.android.material.appbar.CollapsingToolbarLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center" android:text="选择文件:" android:textColor="@color/blue" android:textSize="20sp" /> <Button android:id="@+id/btnOpen" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="打开" android:textColor="@color/blue" android:textSize="20sp" /> </LinearLayout> </LinearLayout>
java文件(Android9.0 API 28可以)
import android.content.Intent; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.sdbi.smartcityli01.R; import java.io.File; public class PersonalFragment extends MySuperFragment { private static final String TAG = "PersonalFragment"; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.d(TAG, "onCreateView: 个人"); View view = inflater.inflate(R.layout.fragment_personal, container, false); Button btnOpen = view.findViewById(R.id.btnOpen); btnOpen.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); // /*/ 此处是任意类型任意后缀 // intent.setType(“audio/*”) // 选择音频 // intent.setType(“video/*”) // 选择视频 (mp4 3gp 是android支持的视频格式) // intent.setType(“video/*;image/*”)//同时选择视频和图片 intent.addCategory(Intent.CATEGORY_OPENABLE); startActivityForResult(intent, 100); } }); return view; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); setMyTitle(view, "个 人"); } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case 100: String tempPath = data.getData().getPath(); String path = tempPath.substring(tempPath.indexOf(":") + 1); File sdcard = Environment.getExternalStorageDirectory(); String realPath = sdcard.getAbsolutePath() + "/" + path; Log.d(TAG, "onActivityResult: realPath = " + realPath); File file = new File(realPath); if (file.exists()) { Log.d(TAG, "onActivityResult: file.getName() = " + file.getName()); } else { Toast.makeText(getActivity(), realPath + "文件不存在", Toast.LENGTH_SHORT).show(); } break; } } }
增加读取文件的权限:
<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" />
if (ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.INTERNET) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED || ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { // 申请权限 ActivityCompat.requestPermissions(this, new String[]{Manifest.permission.INTERNET, Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE}, 1); }
选中文件我们可以通过Log看到文件的路径和名称。
(2)上传文件
使用大赛平台接口 http://10.54.38.55:10001/prod-api/common/upload
接口地址 POST /prod-api/common/upload
接口描述 请求头需要 token 参数,具体格式参见安全认证说明
请求数据类型 multipart/form-data
请求参数
参数名称 | 参数说明 | 请求类型 | 必须 | 数据类型 |
file | 上传的文件对象 | formData | false | file |
响应参数
参数名称 | 参数说明 | 类型 |
code | 状态码, 200 正确, 其他错误 | string |
msg | 返回消息内容 | string |
fileName | 文件名 | string |
url | 文件访问地址 | string |
响应示例
{ "code": 200, "fileName": "test.txt", "url": "/profile/upload/file/test.txt", "msg": "操作成功" }
修改Java代码
import android.content.Intent; import android.os.Bundle; import android.os.Environment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.Button; import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.sdbi.smartcityli01.R; import java.io.File; import java.io.IOException; import okhttp3.Call; import okhttp3.Callback; import okhttp3.MediaType; import okhttp3.MultipartBody; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; import okhttp3.Response; public class PersonalFragment extends MySuperFragment { private static final String TAG = "PersonalFragment"; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { Log.d(TAG, "onCreateView: 个人"); View view = inflater.inflate(R.layout.fragment_personal, container, false); Button btnOpen = view.findViewById(R.id.btnOpen); btnOpen.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(Intent.ACTION_GET_CONTENT); intent.setType("*/*"); // /*/ 此处是任意类型任意后缀 //intent.setType(“audio/*”) // 选择音频 //intent.setType(“video/*”) // 选择视频 (mp4 3gp 是android支持的视频格式) //intent.setType(“video/*;image/*”)//同时选择视频和图片 intent.addCategory(Intent.CATEGORY_OPENABLE); startActivityForResult(intent, 100); } }); return view; } @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); setMyTitle(view, "个 人"); } @Override public void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case 100: String tempPath = data.getData().getPath(); String path = tempPath.substring(tempPath.indexOf(":") + 1); File sdcard = Environment.getExternalStorageDirectory(); String realPath = sdcard.getAbsolutePath() + "/" + path; Log.d(TAG, "onActivityResult: realPath = " + realPath); File file = new File(realPath); if (file.exists()) { Log.d(TAG, "onActivityResult: file.getName() = " + file.getName()); // 上传 String imageType = "multipart/form-data"; RequestBody fileBody = RequestBody.create(file, MediaType.parse("image/jpg"));// RequestBody.create(MediaType.parse("image/jpg"), file); RequestBody requestBody = new MultipartBody.Builder() .setType(MultipartBody.FORM) .addFormDataPart("file", file.getName(), fileBody) .addFormDataPart("imagetype", imageType) .build(); String url = "http://10.54.38.55:10001/prod-api/common/upload"; // 校内服务器 // 通过登录接口获取到Token String token = "eyJhbGciOiJIUzUxMiJ9.eyJsb2dpbl91c2VyX2tleSI6ImI5MWI0Mjc4LTg5ZmUtNDgxNy05YjY5LTUyNmVhYThkNjI1ZiJ9.ZQcEZK2E2Q-_Ak3KzyBemw67FtbeqwhCg48yMBQPzpZVPRh1URcBjvg9NwayzP5BQvvzGmMmB9uBAM1X2FjMpA"; Request request = new Request.Builder() .url(url) .addHeader("Authorization", token) // 通过登录接口获取到Token .post(requestBody) .build(); OkHttpClient okHttpClient = new OkHttpClient(); okHttpClient.newCall(request).enqueue(new Callback() { @Override public void onResponse(Call call, Response response) throws IOException { String htmlStr = response.body().string(); Log.i(TAG, htmlStr); } @Override public void onFailure(Call call, IOException e) { Log.d(TAG, "onFailure: e = " + e.getMessage()); } }); } else { Toast.makeText(getActivity(), realPath + "文件不存在", Toast.LENGTH_SHORT).show(); } break; } } }
如果Token过期会提示认证失败,我们需要重写登录获取Token
上传成功返回如下信息:
{ "msg": "操作成功", "fileName": "/profile/upload/2022/11/23/5cedd4cf-59fa-4e1f-b546-52a293b399f4.jpg", "code": 200, "url": "http://10.54.38.55/profile/upload/2022/11/23/5cedd4cf-59fa-4e1f-b546-52a293b399f4.jpg" }
到服务器上查看
PUT方式
put方式和post方式类似
public String getJsonByPut(String url) throws Exception { OkHttpClient okHttpClient = new OkHttpClient(); //创建OkHttpClient对象 // 创建json请求体 JSONObject json = new JSONObject(); try { json.put("id", "1471"); json.put("name", "李华伟"); json.put("phone", "13963856177"); json.put("addressDetail", "烟台市芝罘区"); json.put("label", "家庭"); } catch (JSONException e) { e.printStackTrace(); } // 创建请求体 MediaType mediaType = MediaType.parse("application/json;charset=utf-8"); RequestBody requestBody = RequestBody.create(String.valueOf(json), mediaType); // 创建Put请求 Request request = new Request.Builder() .url(url) .addHeader("Authorization", "XXXXXXXXXXXXXXXXXXXXXXX234567890") .put(requestBody) // 添加put请求 .build(); Response response = okHttpClient.newCall(request).execute(); if (response.isSuccessful()) { Log.d(TAG, "getJsonByPut:response.code() = " + response.code()); return response.body().string(); // 注意response.body().string() 只能调用一次! } return null; }
Android不能在主线程中访问网络,不能在子线程中操作UI
Android不能在主线程中访问网络,不能在子线程中操作UI
不要阻塞主线程(UI线程)
不要从主线程以外的线程更新UI(UI线程)1.使用runOnUiThread方法
public class MainActivity extends Activity { private Button btn; private TextView tv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); tv = (TextView) findViewById(R.id.tv1); btn = (Button) findViewById(R.id.btn1); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(5000); // 点击按钮,等待5秒 } catch (InterruptedException e) { e.printStackTrace(); } runOnUiThread(new Runnable() { @Override public void run() { Toast.makeText(getApplicationContext(), "MainActivity click button", Toast.LENGTH_SHORT).show(); tv.setText("MainActivity click button"); } }); } }).start(); } }); } }2.Thread+Handler实现异步消息处理(最经典,扩展性最好,使用最广泛)
public class MainActivity extends Activity { private Button btn; private TextView tv; //这个地方的handler最好封装 防止内存泄漏 private Handler handler = new Handler() { @Override public void handleMessage(Message msg) { if (msg.what == 100) { String res = (String) msg.obj; tv.setText(res); Toast.makeText(getApplicationContext(), res, Toast.LENGTH_SHORT).show(); } } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_four); tv = (TextView) findViewById(R.id.tv1); btn = (Button) findViewById(R.id.btn1); btn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { new Thread(new Runnable() { @Override public void run() { try { Thread.sleep(5000); //等待5秒,模拟耗时操作,比如下载 } catch (InterruptedException e) { e.printStackTrace(); } Message msg = new Message(); msg.what = 100; //消息发送的标志 msg.obj = "MainActivity click button"; //消息发送的内容,如:Object、String、类、int handler.sendMessage(msg); } }).start(); } }); } }