导入前一讲已经分析过,且本讲需要用到的代码
添加权限
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
application节点下添加
android:usesCleartextTraffic="true"
activity.java代码
public class MainActivity extends AppCompatActivity {
TextView textView1;
TextView textView2;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
textView1 = findViewById(R.id.textView);
textView2 = findViewById(R.id.textView2);
//怎么创建线程
Thread thread = new Thread() {//方法1
@Override
public void run() {
super.run();
try { // 子线程
Log.i("MainActivity", "start");
//服务器的地址
URL url = new URL("http://148.70.46.9/object3");
//和服务器建立连接
HttpURLConnection httpURLConnection = (HttpURLConnection)
url.openConnection();
//发送请求到服务器
httpURLConnection.setRequestMethod("GET");
// 服务器返回的数据 字节流 byte 字符流 char 文本
// 输入流 从磁盘(网络)到内存 输出内存到磁盘
InputStream inputStream = httpURLConnection.getInputStream(); //字节流
Reader reader = new InputStreamReader(inputStream);// 字符流
BufferedReader bufferedReader = new BufferedReader(reader); //缓存流
String result = "";
String temp;
while ((temp = bufferedReader.readLine()) != null) {
result += temp;
}
Log.i("MainActivity", "result:" + result);
// { "grade":"18级","classname":"中医学","students":["张三","李四","王五"] }
JSONObject jsonObject = new JSONObject(result);
final String grade = jsonObject.getString("grade");
final String classname = jsonObject.getString("classname");
Log.i("MainActivity", "grade:" + grade + " classname :" + classname);
JSONArray jsonArray = jsonObject.getJSONArray("students");
for (int i = 0; i < jsonArray.length(); i++) {
JSONObject jsonObject1 = jsonArray.getJSONObject(i);
String id = jsonObject1.getString("id");
int age = jsonObject1.getInt("age");
String name = jsonObject1.getString("name");
Log.i("MainActivity", "id:" + id + " age:" + age + " name :" + name);
}
// ViewRootImpl$CalledFromWrongThreadException 在一个错误的线程中
// 子线程 -网络访问只能在子线程
// ui操作 文本设置值 点击事件 有时候会问题 有时候不会有问题 -不能子线程
// 切到主线程
runOnUiThread(new Runnable() {
@Override
public void run() { // 主线程
textView1.setText("年级:" + grade);
textView2.setText("班级:" + classname);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
};
thread.start();
}
}
xml代码
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
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"
tools:context=".MainActivity">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="88dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_marginLeft="110dp" />
<TextView
android:id="@+id/textView2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="27dp"
app:layout_constraintStart_toStartOf="@+id/textView"
app:layout_constraintTop_toBottomOf="@+id/textView" />
</androidx.constraintlayout.widget.ConstraintLayout>
断点调试详细介绍推荐博客
第二步,点击按钮运行调试(Debug模式)
模拟器会出现这样一个弹框(我们不用管)
来,一起操作吧
视图介绍
功能:单步跳过,点击该按钮表示程序将执行下一行,如果该行是一句代码直接执行到下一行, 如果该行是一个方法,不会进入该方法,直接执行到下一行。
我们在这行代码key中加3个问号,使key与服务器的不对应,模拟错误
我们点击Step Over按钮,会发现程序执行到这一行,就跳到异常处理了
并且报错,我们找到刚才跳到异常的那一行代码,并且结合报错原因,便可得知错误所在
最后讲下为什么我们要学习断点调试,因为当我们找不到错误所在时,通过断点调试可以帮助我们查看程序运行期间每一步各变量的值,断点调试的方法可以帮助我们加快排查错误的速度,提高我们的工作效率,是一名合格程序员必备的技能
问题分析:在子线程中UI操作出现问题
再次运行,有时候会抛出这样一个异常(有时候会有问题 有时候不会有问题 带有一定的随机性)
// ViewRootImpl$CalledFromWrongThreadException 表示在一个错误的线程中
// 网络访问只能在子线程
// ui操作 (对于控件的操作-例如文本设置值 点击事件)-不能放在子线程
// 解决方法就是把ui操作放到主线程
耗时操作要放到子线程里面(例如网络访问),UI操作要放到主线程(例如文本设置)
那么问题来了,UI操作要放到主线程,不能放到子线程,按道理来说,UI操作放到子线程一定有问题,为什么结果却是有时候会有问题,有时候不会呢?
首先,我们来看一个方法,按住shift+ctrl+r 搜索这个文件
找到这个方法
这个方法实际实际上就是帮我们检查线程的,但是这个方法是在OnResume()方法中调用的,所以当这个checkThread()检查线程的方法没有执行的时候,ui操作放到子线程就不会有问题,相反,就会出问题
就比方:
有时候网络请求耗时很长,调用ui操作的代码还没有执行,而检查线程的方法执行了,然后再执行调用ui操作的代码,ui操作放到子线程就会检查出有问题,
有时候网络请求花的时间很短,调用ui操作的代码已经执行了,但是检查线程的方法还没有执行,所以也就ui操作放到子线程就没有检查出有问题
所以我们不要抱着侥幸的心态,养成一个好习惯,把UI操作放到主线程,把耗时操作放到子线程
方法一.我们之前讲到的直接写一个runOnUiThread方法来创建主线程,
runOnUiThread(new Runnable() {
@Override
public void run() {
//对ui的操作 只能放到主线程
try {
}catch (Exception e){
}
}
});`
实际上这个runOnUiThread方法也是Handler的机制
方法二-用Handler来切到主线程
Handler mHandler=new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {//重写handleMessage方法
super.handleMessage(msg);
}
};
把之前的创建的主线程代码注释,把对文本赋值的操作粘贴到handleMessage方法中,
并在子线程加一句代码
mHandler.sendEmptyMessage(1);
Handler mHandler=new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {//重写handleMessage方法
super.handleMessage(msg);
textView1.setText("年级:" );
textView2.setText("班级:" );
}
};
运行
如果去掉 mHandler.sendEmptyMessage(1); 不发消息通知的话
所以,也就是说
mHandler.sendEmptyMessage(1);在子线程中执行,主线程的handleMessage就会回调,我们也就可以肯定handleMessage方法中的代码一定会在主线程中执行,
所以这个mHandler.sendEmptyMessage(1);的目的是,在子线程中发送消息去通知主线程来进行UI操作
Message message=new Message();
message.what=1;
Bundle bundle=new Bundle();
bundle.putString("grade",grade);//把所需要传的值放入这个 Bundle 当中
bundle.putString("classname",classname);
message.setData(bundle);//再把 bundle 放到message当中
mHandler.sendMessage(message);
再回到handleMessage方法接收消息,结果运行成功
代码
Handler mHandler=new Handler(){
@Override
public void handleMessage(@NonNull Message msg) {//重写handleMessage方法
super.handleMessage(msg);
String grade= msg.getData().getString("grade");
String classname= msg.getData().getString("classname");
textView1.setText("年级:"+ grade);
textView2.setText("班级:" +classname);
}
};
2.小伙伴都知道,我们刚才分析的 Handler 是在主线程里面创建的,小伙伴注意,activity里面所有的成员变量,包括OnCreate方法里面的定义的变量都是在主线程,所以也就是说我们创建的 Handler 是在主线程的,在主线程里面可以用 Handler 进行各种操作,但是我们在子线程里面也用到了 Handler,再次证明了线程间 变量的引用是可以相互共享的结论
.因为我们创建了一个 Handler ,那么实际上 主线程和子线程是都可以拿到这个Handler 的引用,因为他就是在主线程里面创建的,那么子线程也可以对它进行操作
3.那我们在子线程对Handler 进行了一个什么操作呢?我们可以看到,我们实际上调用了一个方法-sendMessage方法,我们还创建了一个Message对象,这个Message对象里面有很多数据,比如说我们创建的这个Bundle类,可以帮助我们传递消息,还有一个what整型变量,也就是说这个 Message我们可以把他看成一个实体类,里面可以传递很多数据
5.当子线程调用sendMessage方法后,这个Message对象就被加到主线程里面去,加到一个叫MessageQueue里面去,MessageQueue是一个Message队列,MessageQueue里面会保存很多的Message ,所以每次发送一个Message到MessageQueue,就会把这个Message放到队列的队尾
6.我们知道队列知识一种数据结构,它里面的代码不会去执行,那么这个队列的代码是怎么样去执行的呢?实际上在主线程里面还有一个角色-Looper,
这里有一个loop()方法,然后在通过死循环先把队列中队首取出来,遵循先进先出的原则,
然后再一个一个取出Message,如果Message取完了,这个死循环就会在这里一直等着,来一个取一个
,为什么需要一个队列来存Message呢,因为如果某个时候有很多个UI操作同时进行,如果不是一个队列的话,那我们的应用程序就会卡死,因为执行不过来,如果是一个队列的话,就算有很多UI操作,我们的队列可以一个一个执行,就不会导致我们的应用程序界面被卡死
7.我们将这个Message取出来还进行了什么操作呢,再往下看Looper代码
也就是说,Message取出来以后,会去调用Handler的dispatchMessage方法,我们可以看到,Handler是有这个方法的
我们来看一下这个方法执行了什么操作
我们可以发现这里的dispatchMessage方法调用了handleMessage方法,所以也就相当于回调了handleMessage方法
博主原声讲解-Handler原理(多多关照哈哈)
Handler不仅可以实现主线程和子线程的切换,也可以实现子线程与子线程之间的切换
常见应用:例如 点击验证码发送以后需要间隔60秒才能再次发送
第一步,在xml文件当中添加一个按钮控件
第二步,把Button定义为全局变量 ,并且实例化Button,并且添加点击事件,然后再定义一个变量
第三步,子线程发送消息,然后主线通过判断time,再每延时1秒发送一次消息
1. Handler主要用来实现线程和线程之间的数据交互
2. CalledOnWrongThreadException异常都用Handler 来解决
小伙伴们,今天的内容就讲到这里啦,已经凌晨两点了,再动用了超级多的工具以后,终于把这篇博客写完了,还给你们录了个视频,讲的不好,多多指教,我会继续努力,谢谢您的阅读,晚安,玛卡巴卡。
Andrioid 入门第六讲04-网络请求第三方框架-xUtils(原生HTTP网络访问的缺点,xUtils简介,使用方法(网络请求访问,注解(布局文件+控件+点击事件)加载网络图片))