当前位置: 首页 > news >正文

如何把网站能搜到上海专业网站建设多少钱

如何把网站能搜到,上海专业网站建设多少钱,pc蛋蛋游戏体验网站建设,产品推广计划书怎么写本章介绍App开发常用的以下网络通信技术#xff0c;主要包括#xff1a;如何以官方推荐的方式使用多线程技术#xff0c;如何通过okhttp实现常见的HTTP接口访问操作#xff0c;如何使用Dlide框架加载网络图片#xff0c;如何分别运用SocketIO和WebSocket实现及时通信功能等…本章介绍App开发常用的以下网络通信技术主要包括如何以官方推荐的方式使用多线程技术如何通过okhttp实现常见的HTTP接口访问操作如何使用Dlide框架加载网络图片如何分别运用SocketIO和WebSocket实现及时通信功能等。 多线程 本节介绍App开发对多线程的几种进阶用法内容包括如何利用Message配合Handler完成主线程与分线程之间的简单通信如何通过runOnUiThread方法简化分线程与处理器的通信机制日和使用工作管理器代替IntentService实现后台任务管理。 分线程通过Handler操作界面 为了使App运行得更流畅多线程技术被广泛应用于App开发。由于Android规定只有主线程UI线程才能直接操作界面因此分线程若想修改界面就得另想办法这要求有一种线程之间相互通信得机制。如果时主线程向分线程传递消息可以在分线程的构造方法中传递参数然而分线程向主线程传递消息并无捷径为此Android设计了一个消息工具Message通过结合Handler与Message能够实现线程间通信。 由分线程向主线程传递消息的过程主要有4个步骤分别说明如下。 1.在主线程中构造一个处理器对象并启动分线程 在Android中启动分线程有两种方式一种是直接调用线程实例的start方法另一种是通过处理器Handler对象的post方法启动线程实例。 2.在分线程中构造一个Message类型的消息包 Message是线程间通信存放消息的包裹其作用类似于Intent机制的Bundle工具。消息实例可通过Message的obtain方法获得比如下面这行代码 Message message Message.obtain(); // 获得默认的消息对象也可以通过处理器对象的obtainMessage方法获得比如下面这行代码 Message message mHandler.obtainMessage(); // 获得处理器的消息对象获得消息实例之后再给它补充详细的包裹信息下面是Message工具的属性说明。 what整型数可存放本次消息的唯一标识。 arg1整形数可存放消息的处理结果。 arg2整型数可存放消息的处理代码。 objObject类型可存放返回消息的数据结构。 replyToMessager回应信使类型在跨进程通信中使用在线程间通信用不着。 3.在分线程中通过处理器对象将Message消息发出去 处理器的消息操作主要包括各种send***方法和remove***方法下面是这些消息操作方法的使用说明。 obtainMessage获取当前的消息对象。sendMessage立即发送指定消息。sendMessageDelayed延迟一段时间后发送指定消息。sendMessageAtTime在设置的时间点发送指定消息。sendEmptyMessage立即发送空消息。sendEmptyMessageDelayed延迟一段时间后发送空消息。sendEmptyMessageAtTime在设置的时间点发送空消息。removeMessages从消息队列移除指定标识的消息。hasMessages判断消息队列是否存在指定标识的消息。 4.主线程的handler对象处理接收到的消息 主线程收到分线程发出的消息之后需要实现处理器对象的handleMessage方法在该方法中根据消息内容分别进行相应的处理因为handleMessage方法在主线程UI线程中调用所以方法内部可以直接操作界面元素。 综合上面的4个线程通信步骤接下来通过一个实验观察线程间通信的效果。下面便是利用多线程技术实现新闻滚动的活动代码例子其中结合了Handler与Message。 public class HandlerMessageActivity extends AppCompatActivity implements View.OnClickListener {private TextView tv_message; // 声明一个文本视图对象private boolean isPlaying false; // 是否正在播放新闻private int BEGIN 0, SCROLL 1, END 2; // 0为开始1为滚动2为结束private String[] mNewsArray { 北斗导航系统正式开通定位精度媲美GPS,黑人之死引发美国各地反种族主义运动, 印度运营商禁止华为中兴反遭诺基亚催债,贝鲁特发生大爆炸全球紧急救援黎巴嫩, 日本货轮触礁毛里求斯造成严重漏油污染};Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_handler_message);tv_message findViewById(R.id.tv_message);findViewById(R.id.btn_start).setOnClickListener(this);findViewById(R.id.btn_stop).setOnClickListener(this);}Overridepublic void onClick(View v) {if (v.getId() R.id.btn_start) { // 点击了开始播放新闻的按钮if (!isPlaying) { // 如果不在播放就开始播放isPlaying true;new PlayThread().start(); // 创建并启动新闻播放线程}} else if (v.getId() R.id.btn_stop) { // 点击了结束播放新闻的按钮isPlaying false;}}// 定义一个新闻播放线程private class PlayThread extends Thread {Overridepublic void run() {mHandler.sendEmptyMessage(BEGIN); // 向处理器发送播放开始的空消息while (isPlaying) { // 正在播放新闻try {sleep(2000); // 睡眠两秒2000毫秒} catch (InterruptedException e) {e.printStackTrace();}Message message Message.obtain(); // 获得默认的消息对象//Message message mHandler.obtainMessage(); // 获得处理器的消息对象message.what SCROLL; // 消息类型message.obj mNewsArray[new Random().nextInt(5)]; // 消息描述mHandler.sendMessage(message); // 向处理器发送消息}mHandler.sendEmptyMessage(END); // 向处理器发送播放结束的空消息isPlaying false;}}// 创建一个处理器对象private Handler mHandler new Handler(Looper.myLooper()) {// 在收到消息时触发public void handleMessage(Message msg) {String desc tv_message.getText().toString();if (msg.what BEGIN) { // 开始播放desc String.format(%s\n%s %s, desc, DateUtil.getNowTime(), 开始播放新闻);} else if (msg.what SCROLL) { // 滚动播放desc String.format(%s\n%s %s, desc, DateUtil.getNowTime(), msg.obj);} else if (msg.what END) { // 结束播放desc String.format(%s\n%s %s, desc, DateUtil.getNowTime(), 新闻播放结束);}tv_message.setText(desc);}}; }运行App先点击“开始播放新闻”按钮此时分线程每隔两秒添加一条新闻正在播放新闻的界面如下图所示。 稍等片刻再点击“停止播放新闻”按钮此时主线程收到分线程的END消息在界面上提示用户“新闻播放结束”如下图所示。 根据以上的新闻播放效果可知分线程的播放开始和播放结束指令都成功送到了主线程。 通过runOnUiThread快速操作界面 因为Android规定分线程不能直接操纵界面所以它设计了处理程序Handler工具由处理程序负责在主线程和分线程之间传递数据。如果分线程想刷新界面就得向处理程序发送消息由处理程序在handleMessage方法中操作控件。举个例子上一小节“分线程通过Handler操作界面”讲到的通过分线程播报新闻便是经由处理程序操纵文本视图。分线程与处理程序交互的代码片段如下 // 是否正在播放新闻 private boolean isPlaying false; // 定义一个新闻播放线程 private class PlayThread extends Thread {Overridepublic void run() {mHandler.sendEmptyMessage(BEGIN); // 向处理器发送播放开始的空消息while (isPlaying) { // 正在播放新闻try {sleep(2000); // 睡眠两秒2000毫秒} catch (InterruptedException e) {e.printStackTrace();}Message message Message.obtain(); // 获得默认的消息对象//Message message mHandler.obtainMessage(); // 获得处理器的消息对象message.what SCROLL; // 消息类型message.obj mNewsArray[new Random().nextInt(5)]; // 消息描述mHandler.sendMessage(message); // 向处理器发送消息}mHandler.sendEmptyMessage(END); // 向处理器发送播放结束的空消息isPlaying false;} }// 创建一个处理器对象 private Handler mHandler new Handler(Looper.myLooper()) {// 在收到消息时触发public void handleMessage(Message msg) {String desc tv_message.getText().toString();if (msg.what BEGIN) { // 开始播放desc String.format(%s\n%s %s, desc, DateUtil.getNowTime(), 开始播放新闻);} else if (msg.what SCROLL) { // 滚动播放desc String.format(%s\n%s %s, desc, DateUtil.getNowTime(), msg.obj);} else if (msg.what END) { // 结束播放desc String.format(%s\n%s %s, desc, DateUtil.getNowTime(), 新闻播放结束);}tv_message.setText(desc);} };以上代码定义了一个新闻播放线程接着主线程启动该线程启动代码如下 new PlayThread().start(); // 创建并启动新闻播放线程上述代码处理分线程与处理程序的交互甚是繁琐既要区分消息类型又要来回类型。为此Android提供了一种简单的交互方式分线程若想操纵界面控件在线程内部调用runOnUiThread方法即可调用代码如下 // 回到主线程UI线程操作界面 runOnUiThread(new Runnable() {Overridepublic void run() {// 操作界面的代码放这里} });由于Runnable属于函数式接口因此调用代码可简化如下 // 回到主线程UI线程操作界面 runOnUiThread(()-{// 操作界面代码放这里 });倘若Runnable的运行代码只有一行那么Lambda表达式允许进一步简化也就是省略外面的花括号于是精简的代码编程以下这样 // 回到主线程UI线程操作界面 runOnUiThread(()- /* 如果只有一行代码那么连花括号也可省掉 */ );回看之前的新闻播报线程把原来的消息发送代码系统统统改成runOnUiThread方法修改后的播放代码如下 // 是否正在播放新闻 private boolean isPlaying false; // 播放新闻 private void broadcastNews() {String startDesc String.format(%s\n%s %s, tv_message.getText().toString(),DateUtil.getNowTime(), 开始播放新闻);// 回到主线程UI线程操纵界面runOnUiThread(() - tv_message.setText(startDesc));while (isPlaying) { // 正在播放新闻try {Thread.sleep(2000); // 睡眠两秒2000毫秒} catch (InterruptedException e) {e.printStackTrace();}String runDesc String.format(%s\n%s %s, tv_message.getText().toString(),DateUtil.getNowTime(), mNewsArray[new Random().nextInt(5)]);// 回到主线程UI线程操纵界面runOnUiThread(() - tv_message.setText(runDesc));}String endDesc String.format(%s\n%s %s, tv_message.getText().toString(),DateUtil.getNowTime(), 新闻播放结束谢谢观看);// 回到主线程UI线程操纵界面runOnUiThread(() - tv_message.setText(endDesc));isPlaying false; }从以上代码可见处理程序的相关代码不见了取而代之的是一行又一行runOnUiThread方法。 主线程启动播放器线程也只需要下面一行代码就够了 new Thread(() - broadcastNews()).start(); // 启动新闻播放线程改造完毕后运行测试App可观察到开始新闻播报效果如下图所示 停止播放新闻效果如下如图所示 工作管理器WorkManager Android 11不光废弃了AsyncTask还把IntentService一起废弃了对于后台的异步服务官方建议改为使用工作管理器WorkManager。 除了IntentService之外Android也提供了其他后台任务工具例如工作调用器JobScheduler、闹钟管理器AlarmManager等。当然这些后台工具的用法各不相同徒增开发者的学习时间而已所以谷歌索性把它们统一起来在Jetpack库中推出了工作管理器WorkManager。这个WorkManager的兼容性很强对于Android 6.0或更高版本的系统它通过JobScheduler完成后台任务对于Android 6.0以下版本的系统不含Android 6.0通过AlarmManager和广播接收器组合完成后台任务。无论采取哪种方案后台任务最终都是由线程池Executor执行的。 因为WorkManager来自Jetpack库所以使用之前要修改build.gradle.kts增加下面一行以来配置 implementation(androidx.work:work-runtime:2.9.0)接着定义一个处理后台业务逻辑的工作者该工作继承自Worker抽象类就像异步任务需要从IntentService派生而来那样。自定义的工作者必须实现构造方法并重写doWork方法其中构造方法可获得外部传来的请求数据而doWork方法处理具体的业务逻辑。特别注意由于doWork方法运行于分线程因此该方法内部不能操作界面控件。自定义工作者的示例代码如下 public class CollectWork extends Worker {private final static String TAG CollectWork;private Data mInputData; // 工作者的输入数据public CollectWork(Context context, WorkerParameters workerParams) {super(context, workerParams);mInputData workerParams.getInputData();}// doWork内部不能操纵界面控件Overridepublic Result doWork() {String desc String.format(请求参数包括姓名%s身高%d体重%f,mInputData.getString(name),mInputData.getInt(height, 0),mInputData.getDouble(weight, 0));Log.d(TAG, doWork desc);Data outputData new Data.Builder().putInt(resultCode, 0).putString(resultDesc, 处理成功).build();//Result.success();//Result.failure();return Result.success(outputData); // success表示成功failure表示失败} }然后在活动页面中构建并启动工作任务详细过程主要分为下列4个步骤 构建约束条件 该步骤说明在哪些情况下才能执行后台任务也就是运行后台任务的前提条件此时用到了约束工具Constraints。约束条件的构建代码如下 // 1、构建约束条件 Constraints constraints new Constraints.Builder()//.setRequiresBatteryNotLow(true) // 设备电量充足//.setRequiresCharging(true) // 设备正在充电.setRequiredNetworkType(NetworkType.CONNECTED) // 已经连上网络.build();构建输入数据 该步骤把后台任务需要的参数封装到一个数据对象此时用到了数据工具Data构建输入数据的示例代码如下 // 2、构建输入数据 Data inputData new Data.Builder().putString(name, 小明).putInt(height, 180).putDouble(weight, 80).build();构建工作请求 该步骤把约束条件、输入数据等请求内容组装起来。此时用到了工作请求工具OneTimeWorkRequest构建工作请求的示例代码如下 // 3、构建一次性任务的工作请求。OneTimeWorkRequest表示一次性任务PeriodicWorkRequest表示周期性任务 String workTag OnceTag; OneTimeWorkRequest onceRequest new OneTimeWorkRequest.Builder(CollectWork.class).addTag(workTag) // 添加工作标签.setConstraints(constraints) // 设置触发条件.setInputData(inputData) // 设置输入参数.build(); UUID workId onceRequest.getId(); // 获取工作请求的编号执行工作请求 该步骤生成工作管理器实例并将步骤3的工作请求对象加入管理器的执行队列中由管理器调度并执行请求任务执行工作请求的实例代码如下 // 4、执行工作请求 WorkManager workManager WorkManager.getInstance(this); workManager.enqueue(onceRequest); // 将工作请求加入执行队列工作管理器不知拥有enqueue方法还有其他的调度方法常用的几个方法分别说明如下 enqueue将工作请求加入执行队列中。cancelWorkById取消指定编号步骤3 getId方法返回workId的工作。cancelAllWorkByTag取消指定标签步骤3设置的workTag的所有工作。cancelAllWork取消所有工作。getWorkInfoByIdLiveData获取指定编号的工作信息。 鉴于后台任务是异步执行的因此若想知晓工作任务的处理结果就得调用getWorkInfoByIdLiveData方法获取工作信息并实时监听它的运行情况。查询工作结果的示例代码 // 获取指定编号的工作信息并实时监听工作的处理结果 workManager.getWorkInfoByIdLiveData(workId).observe(this, workInfo - {Log.d(TAG, workInfo: workInfo.toString());if (workInfo.getState() WorkInfo.State.SUCCEEDED) { // 工作处理成功Data outputData workInfo.getOutputData(); // 获得工作信息的输出数据int resultCode outputData.getInt(resultCode, 0);String resultDesc outputData.getString(resultDesc);String desc String.format(工作处理结果为resultCode%dresultDesc%s,resultCode, resultDesc);tv_result.setText(desc);} });至此工作管理器的任务操作步骤都过了一遍。有的读者可能会发现步骤3的工作请求类的名称为OneTimeWorkRequest读起来像是一次性工作。其实工作管理器不止支持设定依次性工作也支持设定周期性工作此时用到的工作请求名为PeriodicWorkRequest构建的示例代码如下 // 构建周期性任务的工作请求。周期性任务的间隔时间不能小于15分钟 String workTag PeriodTag; PeriodicWorkRequest periodRequest new PeriodicWorkRequest.Builder(CollectWork.class, 15, TimeUnit.MINUTES).addTag(workTag) // 添加工作标签.setConstraints(constraints) // 设置触发条件.setInputData(inputData) // 设置输入参数.build(); UUID workId periodRequest.getId(); // 获取工作请求的编号最后在活动页面中继承工作管理器运行App后点击启动按钮执行结果如下图所示。 HTTP访问 本节介绍okhttp在App接口访问中的详细用法内容包括如何利用移动数据格式JSON封装结构信息以及如何从JSON串解析得结构对象通过okhttp调用HTTP接口得三种方式GET方式、表单格式得POST请求、JSON格式得POST请求如何使用okhttp下载网络文件以及如何将本地文件上传到服务器。 移动数据格式JSON 网络通信的交互数据格式有两大类分别是JSON和XML前者短小精悍后者表现力丰富。对于App来说基本采用JSON格式于服务器通信。原因很多一个是手机流量很贵表达同样的信息JSON串比XML串短很多在节省流量方面占了上风另一个是JSON串解析得很快也更省电XML不但慢而且耗电。于是JSON格式成了移动端事实上的网络数据格式标准。 先来看个购物订单的JSON串例子 {user_info: {name: 思无邪,address: 桃花岛水帘洞123号,phone: 12345678901},goods_list: [{goods_name: Mate70,goods_number: 1,goods_price: 10086},{goods_name: 小米15,goods_number: 1,goods_price: 8888},{goods_name: oneplus13,goods_number: 3,goods_price: 6666}] }从以上JSON串的内容可以梳理出它的基本格式定义详细说明如下 整个JSON串由一对花括号包裹并且内部的每个结构都以花括号包起来。参数格式类似键值对其中键名与键值以冒号分隔形如“键名:键值”。两个键值对之间以逗号分隔。键名需要用双引号引起来键值为数字的话则无需双引号为字符串的话仍需双引号。JSON数组通过方括号表达方括号内部依次罗列各个元素具体格式形如“数组的键名:[元素1,元素2,元素3]”。 针对JSON字符串Android提供了JSON解析工具支持JSONObjectJSON对象和 JSON数组的解析处理。 1.JSONObject 下面是JSONObject的常用方法。 JSONObject构造函数从指定字符串构造一个JSONObject对象。getJSONObject获取指定名称的JSONObject对象。getString获取指定名称的字符串。getInt获取指定名称的整型数。getDouble获取指定名称的双精度数。getBoolean获取指定名称的布尔数。getJSONArray获取指定名称的JSONArray数组对象。put添加一个JSONObject对象。toString把当前的JSONObject对象输出为一个JSON字符串。 2.JSONArray 下面是JSONArray的常用方法。 length获取JSONArray数组长度。getJSONObject获取JSONArray数组在指定位置的JSONObject对象。put往JSONArray数组中添加一个JSONObject对象。 虽然Android自带的JSONObject和JSONArray能够解析JSON串但是这种手工解析实在太麻烦费时费力还容易犯错故而谷歌公司推出了专门的GSon支持库方便开发者快速处理JSON串。 由于Gson是第三方库因此首先要修改build.gradle.kts文件往dependencies节点添加下面一行配置表示导入指定版本的Gson库 implementation(com.google.code.gson:gson:2.10)接着在Java代码文件的头部添加如下一行导入语句表示后面会用到Gson工具 import com.google.gson.Gson;完成上述两个步骤就能在代码中调用Gson的各种处理方法了。Gson常见的应用场合主要有下列两个 将数据对象转换为JSON字符串。此时可调用Gson工具的toJson方法把指定的数据对象转换为JSON字符串。从JSON字符串解析出数据对象。此时可调用Gson工具的fromJson方法从JSON字符串解析得到指定类型的数据对象。 下面是通过Gson库封装与解析JSON串的活动代码例子 public class JsonConvertActivity extends AppCompatActivity {private TextView tv_json; // 声明一个文本视图对象private UserInfo mUser; // 声明一个用户信息对象private String mJsonStr; // JSON格式的字符串Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_json_convert);mUser new UserInfo(阿四, 25, 165L, 50.0f); // 创建用户实例mJsonStr new Gson().toJson(mUser); // 把用户实例转换为JSON串tv_json findViewById(R.id.tv_json);findViewById(R.id.btn_origin_json).setOnClickListener(v - {mJsonStr new Gson().toJson(mUser); // 把用户实例转换为JSON字符串tv_json.setText(JSON串内容如下\n mJsonStr);});findViewById(R.id.btn_convert_json).setOnClickListener(v - {// 把JSON串转换为UserInfo类型的对象UserInfo newUser new Gson().fromJson(mJsonStr, UserInfo.class);String desc String.format(\n\t姓名%s\n\t年龄%d\n\t身高%d\n\t体重%f,newUser.name, newUser.age, newUser.height, newUser.weight);tv_json.setText(从JSON串解析而来的用户信息如下 desc);});} }运行App先点击“原始JSON串”按钮把用户对象转换为JSON字符串此时JSON界面如下图所示可见包含用户信息的JSON字符串。 接着点击“转换JSON串”按钮将JSON字符串转换为用户对象此时JSON界面如下图所示可见用户对象的各字段值。 通过okhttp调用HTTP 尽管使用HttpURLConnection能够实现大多数的网络访问操作但是它的用法实在繁琐很多细节都要开发者关注一不留神就可能导致访问异常。于是各种网络开源框架纷纷涌现比如声明显赫的Apache的HttpClient、Square的okhttp。Android从9.0开始正式弃用HttpClient使得okhttp成为App开发流行的网络框架。 因为okhttp属于第三方框架所以使用之前要修改build.gradle.kts增加下面一行依赖配置 implementation(com.squareup.okhttp3:okhttp:4.9.3)当然访问网络之前得先申请上网权限也就是在AndroidManifest.xml里面补充以下权限 uses-permission android:nameandroid.permission.INTERNET /除此之外从Android 9开始默认只能访问https开头的安全地址不能直接访问以http开头的网络地址。如果应用仍想访问http开头的普通地址就是修改AndroidManifest.xml给application节点添加如下属性表示继续使用http明文地址 android:usesCleartextTraffictrueokhttp的网络访问功能十分强大单就HTTP接口调用而言它就支持三种访问方式GET方式的请求表单格式的POST请求、JSON格式的POST请求下面分别进行说明。 1.GET方式的请求 不管是GET方式还是POST方式okhttp在访问网络时都离不开下面4个步骤 使用OkHttpClient类创建一个okhttp客户端对象。创建客户端对象的示例代码如下 OkHttpClient client new OkHttpClient(); // 创建一个okhttp客户端对象使用Request类创建一个GET和POST方式的请求结构。采取GET方式时调用get方法采取POST方法时调用post方法。此外需要指定本次请求的网络地址还可以添加个性化HTTP头部信息。 创建请求结构的示例代码如下 // 创建一个GET方式的请求结构 Request request new Request.Builder()//.get() // 因为OkHttp默认采用get方式所以这里可以不调get方法.header(Accept-Language, zh-CN) // 给http请求添加头部信息.header(Referer, https://finance.sina.com.cn) // 给http请求添加头部信息.url(URL_STOCK) // 指定http请求的调用地址.build();调用步骤1中客户端对象的newCall方法方法参数为步骤2中的请求结构从而创建Call类型的调用对象。创建调用对象的实例代码如下 Call call client.newCall(request); // 根据请求结构创建调用对象调用步骤3中Call对象的enqueue方法将本次请求加入HTTP访问的执行队列中并编写请求失败与请求成功两种情况的处理代码。加入执行队列的示例代码如下 // 加入HTTP请求队列。异步调用并设置接口应答的回调方法 call.enqueue(new Callback() {Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(调用股指接口报错e.getMessage()));}Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp response.body().string();// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(调用股指接口返回\nresp));} });综合上述4个步骤接下来以查询上证指数为例来熟悉okhttp的完整使用过程。上证指数的查询接口来自新浪网的证券板块具体的接口调用代码如下 // 发起GET方式的HTTP请求 private void doGet() {OkHttpClient client new OkHttpClient(); // 创建一个okhttp客户端对象// 创建一个GET方式的请求结构Request request new Request.Builder()//.get() // 因为OkHttp默认采用get方式所以这里可以不调get方法.header(Accept-Language, zh-CN) // 给http请求添加头部信息.header(Referer, https://finance.sina.com.cn) // 给http请求添加头部信息.url(URL_STOCK) // 指定http请求的调用地址.build();Call call client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用并设置接口应答的回调方法call.enqueue(new Callback() {Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(调用股指接口报错e.getMessage()));}Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp response.body().string();// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(调用股指接口返回\nresp));}}); }运行测试App可观察到上证指数的查询结果如下图所示。 2.表单格式的POST请求 对于okhttp来说POST方式与GET方式的调用过程大同小异主要区别于如何让创建请求结构。除了通过post方法表示本次请求采取POST方式外还要给post方法填入请求参数比如表单格式的请求参数放在FormBody结构中示例代码如下 String username et_username.getText().toString(); String password et_password.getText().toString(); // 创建一个表单对象 FormBody body new FormBody.Builder().add(username, username).add(password, password).build(); // 创建一个POST方式的请求结构 Request request new Request.Builder().post(body).url(URL_LOGIN).build();以登录功能为例用户在界面上输入用户名和密码然后点击登录按钮时App会把用户名和密码封装进FormBody结构后提交给后端服务器。采取表单格式的登录代码如下 // 发起POST方式的HTTP请求报文为表单格式 private void postForm() {String username et_username.getText().toString();String password et_password.getText().toString();// 创建一个表单对象FormBody body new FormBody.Builder().add(username, username).add(password, password).build();OkHttpClient client new OkHttpClient(); // 创建一个okhttp客户端对象// 创建一个POST方式的请求结构Request request new Request.Builder().post(body).url(URL_LOGIN).build();Call call client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用并设置接口应答的回调方法call.enqueue(new Callback() {Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(调用登录接口报错e.getMessage()));}Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp response.body().string();// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(调用登录接口返回\nresp));}}); }确保服务端的登录接口正常开启点击查看服务端程序并且手机和计算机连接同一个WiFi再运行测试App。打开登录页面填入登录信息然后点击“发起接口调用”按钮接收到服务器端返回的数据如下图所示可见表单格式的POST请求被正常调用。 3.JSON格式的POST请求结果 由于表单格式不能传递复杂的数据因此App在与服务端交互时经常使用JSON格式。设定好JSON串的字符编码后再放入RequestBody结构中示例代码如下 // 创建一个POST方式的请求结构 RequestBody body RequestBody.create(jsonString, MediaType.parse(text/plain;charsetutf-8)); Request request new Request.Builder().post(body).url(URL_LOGIN).build();仍以登录功能为例App先将用户名和密码组装进JSON对象再把JSON对象转为字符串后续便是常规的okhttp调用过程了。采取JSON格式的登录代码示例如下 // 发起POST方式的HTTP请求报文为JSON格式 private void postJson() {String username et_username.getText().toString();String password et_password.getText().toString();String jsonString ;try {JSONObject jsonObject new JSONObject();jsonObject.put(username, username);jsonObject.put(password, password);jsonString jsonObject.toString();} catch (Exception e) {e.printStackTrace();}// 创建一个POST方式的请求结构RequestBody body RequestBody.create(jsonString, MediaType.parse(text/plain;charsetutf-8));OkHttpClient client new OkHttpClient(); // 创建一个okhttp客户端对象Request request new Request.Builder().post(body).url(URL_LOGIN).build();Call call client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用并设置接口应答的回调方法call.enqueue(new Callback() {Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(调用登录接口报错e.getMessage()));}Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp response.body().string();// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(调用登录接口返回\nresp));}}); }同样确保服务端的登录接口正常开启点击查看服务端程序并且手机和计算机连接同一个WiFi再运行测试该App。打开登陆界面填入登录信息后点击“发起接口调用”按钮接收到服务端返回的数据如下图所示可见JSON格式的POST请求被正常调用。 使用okhttp下载和上传文件 okhttp不但简化了HTTP接口的调用过程连下载文件都变简单了。对于一般的文件下载按照常规的GET方式调用流程只要重写回调方法onResponse在该方法中通过应答对象的body方法即可获得应答的数据包对象调用数据包对象的string方法即可获得到文本形式的字符串调用数据包对象的byteStream方法即可得到InputStream类型的输入流对象从输入流就能读出原始的二进制数据。 以下载网络图片为例位图工具BitmapFactory刚好提供了decodeStream方法允许直接从输入流中解码获取位图对象。此时通过okhttp下载图片的示例代码如下 private final static String URL_IMAGE https://img-blog.csdnimg.cn/2018112123554364.png;// 下载网络图片 private void downloadImage() {tv_progress.setVisibility(View.GONE);iv_result.setVisibility(View.VISIBLE);OkHttpClient client new OkHttpClient(); // 创建一个okhttp客户端对象// 创建一个GET方式的请求结构Request request new Request.Builder().url(URL_IMAGE).build();Call call client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用并设置接口应答的回调方法call.enqueue(new Callback() {Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(下载网络图片报错e.getMessage()));}Overridepublic void onResponse(Call call, final Response response) { // 请求成功InputStream is response.body().byteStream();// 从返回的输入流中解码获得位图数据Bitmap bitmap BitmapFactory.decodeStream(is);String mediaType response.body().contentType().toString();long length response.body().contentLength();String desc String.format(文件类型为%s文件大小为%d, mediaType, length);// 回到主线程操纵界面runOnUiThread(() - {tv_result.setText(下载网络图片返回desc);iv_result.setImageBitmap(bitmap);});}}); }回到活动代码中调用downloadImage方法再运行并测试App可观察到图片下载结果如下图所示可见网络图片成功下载并显示了出来。 当然网络文件不只是图片还有其他各式各样的文件这些文件没有专门的解码工具只能从输入流老老实实地读取字节数据。不过读取字节数据有个好处就是能够根据自己读写的数据长度计算下载进度特别是在下载大文件的时候实际展示当前的下载进度非常有用。下面是通过okhttp下载普通文件的示例代码 // 下载网络文件 private void downloadFile() {tv_progress.setVisibility(View.VISIBLE);iv_result.setVisibility(View.GONE);OkHttpClient client new OkHttpClient(); // 创建一个okhttp客户端对象// 创建一个GET方式的请求结构Request request new Request.Builder().url(URL_MP4).build();Call call client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用并设置接口应答的回调方法call.enqueue(new Callback() {Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(下载网络文件报错e.getMessage()));}Overridepublic void onResponse(Call call, final Response response) { // 请求成功String mediaType response.body().contentType().toString();long length response.body().contentLength();String desc String.format(文件类型为%s文件大小为%d, mediaType, length);// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(下载网络文件返回desc));String path String.format(%s/%s.mp4,getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(),DateUtil.getNowDateTime());// 下面从返回的输入流中读取字节数据并保存为本地文件try (InputStream is response.body().byteStream();FileOutputStream fos new FileOutputStream(path)) {byte[] buf new byte[100 * 1024];int sum0, len0;while ((len is.read(buf)) ! -1) {fos.write(buf, 0, len);sum len;int progress (int) (sum * 1.0f / length * 100);String detail String.format(文件保存在%s。已下载%d%%, path, progress);// 回到主线程操纵界面runOnUiThread(() - tv_progress.setText(detail));}} catch (Exception e) {e.printStackTrace();}}}); }回到活动代码调用downloadFile方法再运行测试该App可观察到文件下载结果如下图所示。 okhttp不仅让下载文件变简单了还让上传文件变得更加灵活易用。修改个人资料上传头像图片、在朋友圈发动态视频等都用到了文件上传功能并且上传文件常常带着文字说明比如上传头像时可能一并修改了昵称、发布视频时附加了视频描述甚至可能同时上传多个文件等。 像这种组合上传的业务场景倘若使用HttpUTLConnection编码就难了有了okhttp就好办多了。它引入分段结构MultipartyBody及其建造器并提供了名为addFormDataPart的两种重载方法分别适用于文本格式与文件格式的数据。带两个参数的addFormDataPart方法它的第一个参数是字符串的键名第二个参数是字符串的键值该方法用来传递文本消息。带三个参数的addFormDataPart方法它的第一个参数是文件类型第二个参数是文件名第三个参数是文件体。 举个带头像进行用户注册的例子既要把用户和密码发送给服务端也要把头像图片传给服务端此时需要多次调用addFormDataPart方法并通过POST方式提交数据。虽然存在文件上传的交互操作但整体操作流程与POST方式调用接口保持一致唯一啥区别在于请求结构由MultipartyBody生成下面是上传文件之时根据MultipartyBody构建请求结构的代码模板 // 创建分段内容的建造器对象 MultipartBody.Builder builder new MultipartBody.Builder(); // 往建造器对象添加文本格式的分段数据 builder.addFormDataPart(username, username); builder.addFormDataPart(password, password); File file new File(path); // 根据文件路径创建文件对象 // 往建造器对象添加图像格式的分段数据 builder.addFormDataPart(image, file.getName(),RequestBody.create(file, MediaType.parse(image/*))); RequestBody body builder.build(); // 根据建造器生成请求结构 // 创建一个POST方式的请求结构 Request request new Request.Builder().post(body).url(URL_REGISTER).build();合理的文件上传代码要求具备容错机制譬如判断文本内容是否为空、不能上传空文件、支持上传多个文件等。综合考虑之后重新编写文件上传部分的示例代码如下 private ListString mPathList new ArrayList(); // 头像文件的路径列表// 执行文件上传动作 private void uploadFile() {// 创建分段内容的建造器对象MultipartBody.Builder builder new MultipartBody.Builder();String username et_username.getText().toString();String password et_password.getText().toString();if (!TextUtils.isEmpty(username)) {// 往建造器对象添加文本格式的分段数据builder.addFormDataPart(username, username);builder.addFormDataPart(password, password);}for (String path : mPathList) { // 添加多个附件File file new File(path); // 根据文件路径创建文件对象// 往建造器对象添加图像格式的分段数据builder.addFormDataPart(image, file.getName(),RequestBody.create(file, MediaType.parse(image/*)));}RequestBody body builder.build(); // 根据建造器生成请求结构OkHttpClient client new OkHttpClient(); // 创建一个okhttp客户端对象// 创建一个POST方式的请求结构Request request new Request.Builder().post(body).url(URL_REGISTER).build();Call call client.newCall(request); // 根据请求结构创建调用对象// 加入HTTP请求队列。异步调用并设置接口应答的回调方法call.enqueue(new Callback() {Overridepublic void onFailure(Call call, IOException e) { // 请求失败// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(调用注册接口报错\ne.getMessage()));}Overridepublic void onResponse(Call call, final Response response) throws IOException { // 请求成功String resp response.body().string();// 回到主线程操纵界面runOnUiThread(() - tv_result.setText(调用注册接口返回\nresp));}}); }确保服务端的注册接口正常开启点击查看服务端程序并且手机和计算机连接同一个WiFi再运行测试该App。打开初始的注册界面如下图所示。 依次输入用户名称和密码跳转到相册选择头像图片然后点击“注册”按钮接收到服务器的数据如谢图所示可见服务端正常收到了注册信息与头像图片。 图片加载 本节介绍App加载网络图片的相关技术首先描述如何利用第三方的Glide库加载网络图片然后阐述图片加载框架的三级缓存机制以及如何有效地运用Glide地缓存功能最后讲述如何使用Glide加载特殊图像GIF动图、视频封面等。 使用Glide加载网络图片 上一小节通过异步任务获取网络图片尽管能够实现图片加载功能但是编码过程仍显繁琐。如果方便而又快速地显示网络图片一直是安卓网络编程的热门课题前些年图片加载框架Piecasso、Fresco等大行其道以至于谷歌也按耐不住开发了自己的Glide来源库。由于Android本身就是谷歌开发的Glide与Android系出同门因此Glide成为事实上的官方推荐图片加载框架。不过Glide并未集成到Android的SDK中开发者需要另外给App工程导入Glide库也就是修改模块的build.gradle.kts在dependencies节点内部添加如下一行依赖库配置 implementation(com.github.bumptech.glide:glide:4.13.0)导包完成之后即可在代码中正常使用Glide。当然Glide的用法确实简单默认情况下只要以下这行代码就够了 Glide.with(活动实例).load(网址字符串).into(图像视图);可见Glide的图片加载代码至少需要3个参数说明如下 当前页面的活动实例参数类型为Activity。如果是在页面代码内部调用则填写this表示当前活动即可。网络图片的谅解地址以http或者https大头参数类型为字符串。准备显示网络图片的图像视图实例参数类型为ImageView。 假设在Activity内部调用Glide且图片链接放在mImageUrl演示的图像视图名为iv_network那么实际的Glide加载代码是下面这样的 Glide.with(this).load(mImageUrl).into(iv_network);如果不指定图像视图的缩放类型Glide默认采用FIT_CENTER方式显示图片相当于在load方法和into方法中间增加调用fitCenter方法迹象如下代码这般 // 显示方式为容纳居中fitCenter Glide.with(this).load(mImageUrl).fitCenter().into(iv_network);除了fitCenter方法Glide还提供了centerCrop方法对应CENTER_CROP提供了centerInside方法对应CENTER_INSIDE其中增加centerCrop方法的加载代码如下 // 显示方式为居中剪裁centerCrop Glide.with(this).load(mImageUrl).centerCrop().into(iv_network);增加centerInside方法的加载代码如下 // 显示方式为居中入内centerInside Glide.with(this).load(mImageUrl).centerInside().into(iv_network);另外Glide还支持圆形裁剪也就是只显示图片中央的圆形区域此时方法调用改成零零circleCrop具体代码实例如下 // 显示方式为圆形剪裁circleCrop Glide.with(this).load(mImageUrl).circleCrop().into(iv_network);以上四种显示效果如下图所示。 虽然Glide支持上述4种显示类型但它无法设定FIT_XY对应的平铺方式若想让图片平铺至充满整个图像视图还得调用图像视图的setScaleType方法将缩放类型设置为ImageView.ScaleType.FIT_XY。 一旦把图像视图的缩放类型改为FIT_XY则之前的4种显示方式也将呈现不一样的景象缩放类型变更后的界面分别如下图所示。 利用Glide实现图片的三级缓存 图片加载框架之所以高效是因为它不但封装了访问网络的步骤而且引入了三级缓存机制。具体来说是先到内存运存中查找图片有找到就直接显示内存图片没找到的话再去磁盘闪存查找图片在磁盘能找到就直接显示磁盘图片没找到的话再去请求网络如此便形成“内存-磁盘-网络”的三级缓存完整的缓存流程如下图 对于Glide而言默认已经开启了三级缓存机制当然也可以根据实际情况另行调整。除此之外Glide还提供了一些个性化的功能方便开发者定制不同场景的需求。具体到编码上则需想办法将个性化选项告知Glide比如下面这段图片加载代码 Glide.with(this).load(mImageUrl).into(iv_network);可以拆分为以下两行代码 // 构建一个加载网络图片的建造器 RequestBuilderDrawable builder Glide.with(this).load(mImageUrl); builder.into(iv_network);原来load方法返回的是请求建造器调用建造器对象的into方法方能在图像视图上展示网络图片。除了into方法建造器RequestBuilder还提供了apply方法该方法表示启用指定的请求选项。于是添加了请求选项的完整代码示例如下 // 构建一个加载网络图片的建造器 RequestBuilderDrawable builder Glide.with(this).load(mImageUrl); RequestOptions options new RequestOptions(); // 创建Glide的请求选项 // 在图像视图上展示网络图片。apply方法表示启用指定的请求选项 builder.apply(options).into(iv_network);可见请求选项为RequestOptions类型详细的选项参数就交给它的下列方法了 placeholder设置加载开始的占位图。在得到网络图片之前会先在图像视图上展现占位图。 error设置发生错误的提示图。网络图片获取失败之时会在图像视图上展现提示图。 override设置图片的尺寸。注意该方法有多个重载方法倘若调用只有一个参数的方法并设置Target.SIZE_ORIGINAL表示展示原始图片倘若调用拥有两个参数的方法表示先将图片缩放到指定的宽度和高度再展示缩放后的图片。 diskCacheStrategy设置指定的缓存策略。各种缓存策略的取值见下表。 | DiskCacheStrategy类的缓存策略 | 说明 | |–|–| | AUTOMATIC | 自动选择缓存策略 | | NONE | 不缓存图片 | | DATA | 只缓存原始图片 | | RESOURCE | 只缓存压缩后的图片 | | ALL | 同时缓存原始图片和压缩图片 | skipMemoryCache设置是否跳过内存但不影响硬盘缓存。为true表示跳过为false则表示不跳过。 disallowHardwareConfig关闭硬件加速防止过大尺寸的图片加载报错。 fitCenter保持图片的宽高比例并居中显示图片需要顶到某个方向的边界但不能越过边界对应缩放类型FIT_CENTER。 centerCrop把排斥图片的宽高比例充满整个图像视图裁剪之后居中显示对应缩放类型CENTER_CROP。 centerInside保持图片的宽高比例在图像视图内部居中显示图片只能拉小不能拉大对应缩放类型CENTER_INSIDE。 circleCrop展示圆形裁剪之后的图片。 另外Glide允许播放器加载过程的渐变动画让图片从迷雾中逐渐变得清晰有助于提高用户体验。这个渐变动画通过建造器的transition方法设置调用代码示例如下 // 设置时长3秒的渐变动画 builder.transition(DrawableTransitionOptions.withCrossFade(3000)); 加载网络图片的渐变效果如下图所示。 使用Glide加载特殊图像 从Android 9.0开始增加了新的图像解码器ImageDecoder该解码器支持直接读取GIF文件的图形数据结合图形工具Animatable即可在图像视图上显示GIF动图。虽然通过ImageDecoder能够在界面上播放GIF动画但是一方面实现代码有些臃肿另一方面在Android 9.0之后才支持显然不太好用。现在有了Glide轻松加载GIF动图不在话下简简单单只需下面一行代码 Glide.with(this).load(R.drawable.happy).into(iv_cover);使用Glide播放GIF动画的效果如下图所示 除了支持GIF动画之外Glide甚至还能自动加载视频封面也就是把某个视频文件的首帧画面渲染到图像视图上。这个功能可谓是非常实在先展示视频封面等用户点击再开始播放可以有效防止资源浪费。以加载本地视频的封面为例首先到系统视频库中挑选某个视频得到该视频的Uri对象后采用Glide加载即可在图像上显示视频封面。视频挑选与封面加载代码示例如下 // 注册一个善后工作的活动结果启动器获取指定类型的内容 ActivityResultLauncher launcher registerForActivityResult(new ActivityResultContracts.GetContent(), uri - {if (uri ! null) { // 视频路径非空则加载视频封面Glide.with(this).load(uri).into(iv_cover);} }); findViewById(R.id.btn_local_cover).setOnClickListener(v - launcher.launch(video/*));使用Glide加载视频封面的效果如下图 Glide不仅能加载本地视频的封面还能加载网络视频的封面。当然由于下载网络视频很消耗带宽因此要事先指定视频帧所处的时间点这样Glide只会加载该位置的视频画面无需下载整个视频。指定视频的时间点用到了RequestOptions类的frameOf方法具体的请求参数构建代码如下 // 获取指定时间点的请求参数 private RequestOptions getOptions(int position) {// 指定某个时间位置的帧单位微秒RequestOptions options RequestOptions.frameOf(position*1000*1000);// 获取最近的视频帧options.set(VideoDecoder.FRAME_OPTION, MediaMetadataRetriever.OPTION_CLOSEST);// 执行从视频帧到位图对象的转换操作options.transform(new BitmapTransformation() {Overrideprotected Bitmap transform(BitmapPool pool, Bitmap toTransform, int outWidth, int outHeight) {return toTransform;}Overridepublic void updateDiskCacheKey(MessageDigest messageDigest) {try {messageDigest.update((getPackageName()).getBytes(StandardCharsets.UTF_8));} catch (Exception e) {e.printStackTrace();}}});return options; }接着调用Glide的apply方法设置请求参数并加载网络视频的封面图片详细的加载代码示例如下 // 加载第10秒处的视频画面 findViewById(R.id.btn_network_one).setOnClickListener(v - {// 获取指定时间点的请求参数RequestOptions options getOptions(10);// 加载网络视频的封面图片Glide.with(this).load(URL_MP4).apply(options).into(iv_cover); }); // 加载第45秒处的视频画面 findViewById(R.id.btn_network_nine).setOnClickListener(v - {// 获取指定时间点的请求参数RequestOptions options getOptions(45);// 加载网络视频的封面图片Glide.with(this).load(URL_MP4).apply(options).into(iv_cover); });Glide加载网络视频封面的效果如下图所示。 即时通信 本节介绍App开发即时通信方面的几种进阶用法内容包括如何通过SocketIO在两台设备之间传输文本消息如何通过Socket IO在两台设备之间传输图片消息SocketIO的局限性和WebSocket协议以及如何利用WebSocket更方便在设备之间传输各类消息。 通过SocketIO传输文本消息 虽然HTTP协议能够满足多数常见的接口交互但是它属于短链接每次调用完就自动断开连接并且HTTP协议区分了服务端和客户端双方的通信过程是单向的只有客户端可以请求服务端服务端无法向客户端推送消息。基于这些特点HTTP协议仅能用于一次性的接口访问而不适用于点对点的即时通信功能。 即时通信技术需要满足两方面的基本条件一方面是长连接以便在两台设备间持续通信避免频繁的”连接-断开“再”连接-断开“如此反复而造成资源浪费另一方面支持双向交流既允许A设备主动向B设备发消息又允许B设备主动向A设备发消息。这要求在套接字Socket层面进行通信Socket连接一旦成功连上便默认维持连接直到有一方主动断开。而且Socket服务端支持向客户端的套接字推送消息从而实现双向通信功能。 可是Java的Socket百年城比较繁琐不仅要自行编写线程通信与IO处理的代码还要自己定义数据包的内部格式以及编解码。为此出现了第三方Socket通信框架SocketIO该框架提供服务端和客户端的依赖包大大简化了SocketIO要先引入相关JAR包点击查看服务端程序接着编写如下的main方法监听文本发送事件 public static void main(String[] args) {Configuration config new Configuration();// 如果调用了setHostname方法就只能通过主机名访问不能通过IP访问//config.setHostname(localhost);config.setPort(9010); // 设置监听端口final SocketIOServer server new SocketIOServer(config);// 添加连接连通的监听事件server.addConnectListener(client - {System.out.println(client.getSessionId().toString()已连接);});// 添加连接断开的监听事件server.addDisconnectListener(client - {System.out.println(client.getSessionId().toString()已断开);});// 添加文本发送的事件监听器server.addEventListener(send_text, String.class, (client, message, ackSender) - {System.out.println(client.getSessionId().toString()发送文本消息message);client.sendEvent(receive_text, 不开不开我不开妈妈没回来谁来也不开。);});// 添加图像发送的事件监听器server.addEventListener(send_image, JSONObject.class, (client, json, ackSender) - {String desc String.format(%s序号为%d, json.getString(name), json.getIntValue(seq));System.out.println(client.getSessionId().toString()发送图片消息desc);client.sendEvent(receive_image, json);});server.start(); // 启动Socket服务 }然后服务端执行main方法即可启动Socket服务进行监听。 在客户端继承SocketIO的话要先修改build.gradle.kts增加下面一行依赖配置 implementation(io.socket:socket.io-client:1.0.1)接着适用SocketIO提供的Socket工具完成消息的收发操作Socket对象是由IO工具的socket方法获得的它的常用方法分别说明如下 connect建立Socket连接。connected判断是否连上Socket。emit向服务器提交指定事件的消息。on开始监听服务器端推送的事件消息。off取消监听服务端的推送的事件消息。disconnect断开Socket连接。close关闭Socket连接。关闭之后要重新获取新的Socket对象才能连接。 在两部手机之间Socket通信依旧区分发送方与接收方且二者的消息收发通过Socket服务器中转。对于发送方的App来说发消息的Socket操作流程获取Socket对象-调用connect方法-调用emit方法往Socekt服务器发送消息。遂于接收方的App来说收消息的Sokcet操作流程获取Socket对象-调用connect方法-调用on方法从服务器接收消息。若想把Socket消息的收发功能集中在一个App上让它既然充当发送方又充当接收方则整理后的App消息收发流程如下图所示。 上图的实线表示代码的调用顺序虚线表示异步的事件触发例如用户的点击事件以及服务器的消息推送等。根据这个收发流程编写代码逻辑具体实现代码如下 public class SocketioTextActivity extends AppCompatActivity {private static final String TAG SocketioTextActivity;private EditText et_input; // 声明一个编辑框对象private TextView tv_response; // 声明一个文本视图对象private Socket mSocket; // 声明一个套接字对象Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_socketio_text);et_input findViewById(R.id.et_input);tv_response findViewById(R.id.tv_response);findViewById(R.id.btn_send).setOnClickListener(v - {String content et_input.getText().toString();if (TextUtils.isEmpty(content)) {Toast.makeText(this, 请输入聊天消息, Toast.LENGTH_SHORT).show();return;}mSocket.emit(send_text, content); // 往Socket服务器发送文本消息});initSocket(); // 初始化套接字}// 初始化套接字private void initSocket() {// 检查能否连上Socket服务器SocketUtil.checkSocketAvailable(this, NetConst.BASE_IP, NetConst.BASE_PORT);try {String uri String.format(http://%s:%d/, NetConst.BASE_IP, NetConst.BASE_PORT);mSocket IO.socket(uri); // 创建指定地址和端口的套接字实例} catch (URISyntaxException e) {throw new RuntimeException(e);}mSocket.connect(); // 建立Socket连接// 等待接收传来的文本消息mSocket.on(receive_text, (args) - {String desc String.format(%s 收到服务端消息%s,DateUtil.getNowTime(), (String) args[0]);runOnUiThread(() - tv_response.setText(desc));});}Overrideprotected void onDestroy() {super.onDestroy();mSocket.off(receive_text); // 取消接收传来的文本消息if (mSocket.connected()) { // 已经连上Socket服务器mSocket.disconnect(); // 断开Socket连接}mSocket.close(); // 关闭Socket连接} }确保服务器的SocketServer正在运行点击查看服务端代码再运行测试该App在编辑框输入待发送的文本此时交互界面如下图所示。 接着点击“发送文本消息”按钮向Socket服务器发送文本消息随后接收到服务器推送的应答消息应答内容展示在按钮下方此时交互界面如下图所示可见文本消息的收发流程成功走通。 通过SocketIO传输图片消息 上一小节借助SocketIO成功实现了文本消息的即时通信然而文本内容只用到字符串本来就比较简单。倘若让SocketIO实时传输图片便步那么容易了。因为SocketIO不支持直接传输二进制数据使得位图对象的字节数据无法作为emit方法的参数。除了字符串类型SocketIO还支持JSONObject类型的数据所以可以考虑利用JSON对象封装图像信息把图像的字节数据通过BASE64编码成字符串保存起来。 鉴于JSON格式允许容纳多个字段同时图片很有可能很大因此建议将图片拆开分段传输每段标明本次的分段序号、分段长度以及分段数据由接收方在收到后重新拼成完整的图像。为此需要将原来的Socket收发过程改造一番使之支持图片数据的即时通信改造步骤说明如下。 给服务端的Socket监听程序添加以下代码表示新增图像发送事件 // 添加图像发送的事件监听器 server.addEventListener(send_image, JSONObject.class, (client, json, ackSender) - {client.sendEvent(receive_image, json); });在App模块中定义一个图像分段结构用于存放分段名称、分段数据、分段序号、分段长度等信息该结构的关键代码如下 public class ImagePart {private String name; // 分段名称private String data; // 分段数据private int seq; // 分段序号private int length; // 分段长度public ImagePart(String name, String data, int seq, int length) {this.name name;this.data data;this.seq seq;this.length length;} }回到App的活动代码补充实现图像的分段传输功能。先将位图数据转为字节数组再将字节数组分段编码为BASE64字符串再组装成JSON对象传给Socket服务器。发送图像的示例代码如下 private int mBlock 50*1024; // 每段的数据包大小 // 分段传输图片数据 private void sendImage() {ByteArrayOutputStream baos new ByteArrayOutputStream();// 把位图数据压缩到字节数组输出流mBitmap.compress(Bitmap.CompressFormat.JPEG, 80, baos);byte[] bytes baos.toByteArray();int count bytes.length/mBlock 1;// 下面把图片数据经过BASE64编码后发给Socket服务器for (int i0; icount; i) {String encodeData ;if (i count-1) { // 是最后一段图像数据int remain bytes.length % mBlock;byte[] temp new byte[remain];System.arraycopy(bytes, i*mBlock, temp, 0, remain);encodeData Base64.encodeToString(temp, Base64.DEFAULT);} else { // 不是最后一段图像数据byte[] temp new byte[mBlock];System.arraycopy(bytes, i*mBlock, temp, 0, mBlock);encodeData Base64.encodeToString(temp, Base64.DEFAULT);}// 往Socket服务器发送本段的图片数据ImagePart part new ImagePart(mFileName, encodeData, i, bytes.length);SocketUtil.emit(mSocket, send_image, part); // 向服务器提交图像数据} }除了要实现发送方的图像发送功能还需实现接收方的图像接收功能。先从服务器获取各段图像数据等所有分段都接收完毕再按照分段序号依次凭借图像的字节数组再从拼接好的字节数组解码得到位图对象接收图像的示例代码如下 private String mLastFile; // 上次的文件名 private int mReceiveCount; // 接收包的数量 private byte[] mReceiveData; // 收到的字节数组 // 接收对方传来的图片数据 private void receiveImage(Object... args) {JSONObject json (JSONObject) args[0];ImagePart part new Gson().fromJson(json.toString(), ImagePart.class);if (!part.getName().equals(mLastFile)) { // 与上次文件名不同表示开始接收新文件mLastFile part.getName();mReceiveCount 0;mReceiveData new byte[part.getLength()];}mReceiveCount;// 把接收到的图片数据通过BASE64解码为字节数组byte[] temp Base64.decode(part.getData(), Base64.DEFAULT);System.arraycopy(temp, 0, mReceiveData, part.getSeq()*mBlock, temp.length);// 所有数据包都接收完毕if (mReceiveCount part.getLength()/mBlock1) {// 从字节数组中解码得到位图对象Bitmap bitmap BitmapFactory.decodeByteArray(mReceiveData, 0, mReceiveData.length);String desc String.format(%s 收到服务端消息%s, DateUtil.getNowTime(), part.getName());runOnUiThread(() - { // 回到主线程展示图片与描述文字tv_response.setText(desc);iv_response.setImageBitmap(bitmap);});} }在App代码中记得调用Socket对象的on方法这样App才能正常接收服务器传来的图像数据。下面是on方法的调用代码 // 等待接收传来的图片数据 mSocket.on(receive_image, (args) - receiveImage(args));完成上述几个步骤之后确保服务器的SocketServer正在运行点击查看服务器端代码再运行测试该App从系统相册中选择待发送的图片此时交互界面如下图所示。 接着点击“发送图片”按钮向Socket服务器发送图片消息随后接收到服务器推送的应答消息应答消息内容显示再按钮下方包含文本和图片此时交互界面如下图所示。可见图片消息发送流程成功完成。 利用WebSocket传输消息 在前面两小节中文本与图片的即时通信都可以由SocketIO实现看似它要统一即时通信了可是深究起来会发现SocektIO存在很多局限包括但不限以下几点 SocketIO不能直接传输字节数据只能重新编码成字符串比如BASE64后再传输造成了额外的系统开销。SokcetIO不能保证前后发送的数据被接收到时仍然是同样顺序如果业务要求实现分段数据的有序性开发者就得自己采取某种机制确保这种有序性。SocketIO服务器只有一个main程序不可避免地会产生性能瓶颈。倘若有许多通信请求奔涌过来一个main程序很难应对。 为了解决上述几点问题业界提出了一种互联网时代的Socket协议名叫WebSocket。它支持在TCP连接上进行全双工通信这个协议在2011年被定义为互联网的标准之一并纳入HTML5的规范体系。相对于传统的HTTP与Socket来说WebSocket具备以下几点优势 实时性更强无须轮询即可实时获得对方设备的消息推送。利用率更高连接创建之后基于相同的控制协议每次交互的数据包头较小节省了数据处理的开销。功能更强大WebSocket定义了二进制帧使得传输二进制的字节数组十分容易。扩展更方便WebSocekt接口被托管在普通的Web服务至上跟着Web服务扩容方便有效规避了性能瓶颈。 WebSocket不仅拥有如此丰富的特性而且用起来也特别简单。先说服务器的WebSocekt编程除了引入它的依赖包javaee-api-8.0.1.jar就只需添加如下的服务器代码 ServerEndpoint(/testWebSocket) public class WebSocketServer {// 存放每个客户端对应的WebSocket对象private static CopyOnWriteArraySetWebSocketServer webSocketSet new CopyOnWriteArraySetWebSocketServer();private Session mSession; // 当前的连接会话// 连接成功后调用OnOpenpublic void onOpen(Session session) {System.out.println(WebSocket连接成功);this.mSession session;webSocketSet.add(this);}// 连接关闭后调用OnClosepublic void onClose() {System.out.println(WebSocket连接关闭);webSocketSet.remove(this);}// 连接异常时调用OnErrorpublic void onError(Throwable error) {System.out.println(WebSocket连接异常);error.printStackTrace();}// 收到客户端消息时调用OnMessagepublic void onMessage(String msg) throws Exception {System.out.println(接收到客户端消息 msg);for(WebSocketServer item : webSocketSet){item.mSession.getBasicRemote().sendText(我听到消息啦“msg”);}} }接着启动服务器Web工程便能通过形如http://192.168.10.121:8000/HttpServer/testWebSocket这样的地址访问WebSocket。 再说App端的WebSocket编程由于WebSocket协议尚未纳入JDK因此要引入它所依赖的JAR包tyrus-standalone-client-1.17.jar。代码方面则需要自定义客户端的连接任务注意给任务类添加注解ClientEndpoint表示该类属于WebSocket的客户端任务。任务内部需要重写onOpen连接成功后调用、processMessage收到服务端消息时调用、processError收到服务端错误时调用三个方法还得定义一个向服务端发消息方法消息内容支持文本与二进制两种格式。下面是处理客户端消息交互工作的示例代码 ClientEndpoint public class AppClientEndpoint {private final static String TAG AppClientEndpoint;private Activity mAct; // 声明一个活动实例private OnRespListener mListener; // 消息应答监听器private Session mSession; // 连接会话public AppClientEndpoint(Activity act, OnRespListener listener) {mAct act;mListener listener;}// 向服务器发送请求报文public void sendRequest(String req) {Log.d(TAG, 发送请求报文req);try {if (mSession ! null) {RemoteEndpoint.Basic remote mSession.getBasicRemote();remote.sendText(req); // 发送文本数据// remote.sendBinary(buffer); // 发送二进制数据}} catch (Exception e) {e.printStackTrace();}}// 连接成功后调用OnOpenpublic void onOpen(final Session session) {mSession session;Log.d(TAG, 成功创建连接);}// 收到服务端消息时调用OnMessagepublic void processMessage(Session session, String message) {Log.d(TAG, WebSocket服务端返回 message);if (mListener ! null) {mAct.runOnUiThread(() - mListener.receiveResponse(message));}}// 收到服务端错误时调用OnErrorpublic void processError(Throwable t) {t.printStackTrace();}// 定义一个WebSocket应答的监听器接口public interface OnRespListener {void receiveResponse(String resp);} }回到App的活动代码依次执行下述步骤就能向WebSocket服务器发送消息获取WebSocket容器-连接WebSocekt服务器-调用WebSocket任务的发送方法。其中前两步涉及的初始化代码如下 // 初始化WebSocket的客户端任务 private void initWebSocket() {// 创建文本传输任务并指定消息应答监听器mAppTask new AppClientEndpoint(this, resp - {String desc String.format(%s 收到服务端返回%s,DateUtil.getNowTime(), resp);tv_response.setText(desc);});// 获取WebSocket容器WebSocketContainer container ContainerProvider.getWebSocketContainer();try {URI uri new URI(SERVER_URL); // 创建一个URI对象// 连接WebSocket服务器并关联文本传输任务获得连接会话Session session container.connectToServer(mAppTask, uri);// 设置文本消息的最大缓存大小session.setMaxTextMessageBufferSize(1024 * 1024 * 10);// 设置二进制消息的最大缓存大小//session.setMaxBinaryMessageBufferSize(1024 * 1024 * 10);} catch (Exception e) {e.printStackTrace();} }因为WebSocket接口任为网络操作所以必须在分线程中初始化WebSocekt启动初始化线程的代码如下 new Thread(() - initWebSocket()).start(); // 启动线程初始化WebSocket客户端同理发送WebSocket消息也要在分线程中操作启动消息发送线程的代码如下 new Thread(() - mAppTask.sendRequest(content)).start(); // 启动线程发送文本消息最后确保后端的Web服务正在运行点击查看服务端代码再运行测试该App在编辑框输入待发送的文本此时交互界面如下图所示。 接着点击“发送WEBSOCKET消息”按钮向WebSocket服务器发送文本消息随后接收到服务器推送的应答消息应答内容显示在按钮下方此时监护界面如下图所示。 工程源码 文章涉及所有代码可点击工程源码下载。
http://www.w-s-a.com/news/620437/

相关文章:

  • 如何做自己的在线作品网站深圳网站设计公司的
  • 网站开发外包公司wordpress最简单模板
  • 湖南省建设人力资源网站wordpress主机pfthost
  • 淮安软件园哪家做网站各网站特点
  • 网站长尾关键词排名软件重庆荣昌网站建设
  • 建个商城网站多少钱茂名专业网站建设
  • 开通公司网站免费的网站app下载
  • 跨境电商网站模板wordpress壁纸
  • 国内做网站网站代理电子商务网站建设与维护概述
  • 如何做地方网站推广沈阳网势科技有限公司
  • 哈尔滨网站优化技术涵江网站建设
  • 做网站搞笑口号wordpress全屏动画
  • 怎么可以建网站小程序代理项目
  • 怎样做软件网站哪个网站用帝国cms做的
  • 网站开发编程的工作方法wordpress dux-plus
  • 廊坊电子商务网站建设公司网站进不去qq空间
  • 南宁网站推广费用创意网页设计素材模板
  • 深圳技术支持 骏域网站建设wordpress 酒主题
  • 东莞网站建设+旅游网站改版数据来源表改怎么做
  • 手机端做的优秀的网站设计企业做网站大概多少钱
  • 优化网站使用体验手机网站解析域名
  • 网站制作 商务做网站的软件名字全拼
  • 阿里巴巴网官方网站温州网站建设设计
  • 传奇购买域名做网站国外网站设计 网址
  • 西安凤城二路网站建设seo网站是什么
  • 网站后台如何更换在线qq咨询代码在线种子资源网
  • 东莞网站优化制作免费中文wordpress主题下载
  • 东莞建筑设计院排名网络优化论文
  • 做牙工作网站郑州前端开发培训机构
  • 温州专业建站网站制作的管理