1 问题的提出,最常见在网络请求等耗时操作
- 当我们初学接触网络大千世界,可能会遇到这样的问题,感觉java代码的执行顺序似乎是乱的,某的代码并没有执行而直接跳到下一步。下面通过Android Demo说明问题:
- 布局很简单,就放了一个Button:activity_main.xml
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="点我执行"
android:id="@+id/bt_click_execute"
/>
</LinearLayout>
- 活动MainActivity.java:
public class MainActivity extends AppCompatActivity {
private String toastStr;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.bt_click_execute)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
execute();
}
});
}
private void execute() {
toastStr = "execute开始";
toastStr = getWebData();/*模拟获取网络数据等耗时操作*/
Toast.makeText(this, toastStr, Toast.LENGTH_LONG).show();
}
private String getWebData() {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000*2);
} catch (InterruptedException e) {
e.printStackTrace();
}
toastStr="模拟网络返回的数据";
}
}).start();
return toastStr;
}
}
- 简单说一下活动中的逻辑:
- 找到布局文件中的Button,并给它设置一个监听,点击Button,执行
execute()
- execute中有三个主要部分
2.1 将字符串toastStr
初始化为“execute开始”;
2.2 将方法getWebData
返回的字符串赋值给toastStr
;
2.3 将toastStr字符串显示出来。 -
getWebData
方法中:
3.1 开启一个新线程
3.2 让线程睡眠两秒(模拟耗时操作)
3.2 将"模拟网络返回的数据" 赋值给toastStr
3.3 返回toastStr
-
那么在上面逻辑下面,我们的显示的字符串会是什么呢?看结果:
1.png - 没有看错,这就是Toast显示的字符串,是不是有些惊喜:不对啊,我们明明在
getWebData
中将字符串的值改成了模拟网络返回的数据 - 为了跟踪toastStr的变化,我们再每一次toasStr的值可能变化的地方,添加Log,看看问题出在什么地方,修改代码如下:
public class MainActivity extends AppCompatActivity {
private String toastStr;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.bt_click_execute)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
execute();
}
});
}
private void execute() {
toastStr = "execute开始";
Log.d("20180621", "在execute方法中第一步时候,toastStr: " + toastStr);
toastStr = getWebData();/*模拟获取网络数据等耗时操作*/
Log.d("20180621", "在execute方法中第二步时候,toastStr: " + toastStr);
Toast.makeText(this, toastStr, Toast.LENGTH_LONG).show();
}
private String getWebData() {
Log.d("20180621", "在getWebData方法中开始时,toastStr: " + toastStr);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
toastStr = "模拟网络返回的数据";
Log.d("20180621", "在getWebData方法中修改数据后,toastStr: " + toastStr);
}
}).start();
Log.d("20180621", "在getWebData方法中返回前,toastStr: " + toastStr);
return toastStr;
}
}
- Log打印结果: 2.png
- 特别注意划线地方,我们注意到,从
getWebData
返回时,toastStr
的值还是原来的值,这也就是为什么最后显示的,就是该字符串。 - 同时,你也能发现,最后一条Log信息显示,我们确实在某一个时候,将字符串的值改成了"模拟网络返回的数据"
- 这里顺带提一下,Log打印的顺序不一定就是代码执行的顺序,所以要想依据Log打印的顺序来推断代码执行的顺序是不可取的,除非你没执行一条Log打印一下系统当前时间,或者你给每一条Log信息增加一个整形的标识,每打印一条,让它自增一下。最佳方案嘛,就是给条Log添加说明,像上面我做的那样,(^-^)V,毕竟这样清楚明了(然而这还是不能通过Log推断执行顺序,当然,其实绝大多数时间,两者的顺序是相同的_)。
- 想要了解执行顺序,Debug(小虫子图标)调试一下就OK了。
- 通过log信息我们得到以下信息:
- 在
getWebData
方法返回时,并没有修改字符串 - 也就是说在方法
getWebData
中,代码块
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
toastStr = "模拟网络返回的数据";
Log.d("20180621", "在getWebData方法中修改数据后,toastStr: " + toastStr);
}
}).start();
在方法返回后才执行的(通过最后字符串变为“模拟网络返回的数据”推断,其实只能推断赋值语句在方法返回后才执行。我这么写是因为认识了解了线程嘛,还是那句话,想看看执行顺序,请Debug),那么代码执行顺序“有问题”,是因为新线程的原因咯,正是如此!
2 多线程、异步任务给我们造成了代码执行顺序不对的假象
- 对主线程,其他线程,异步任务打个比方。
- 我们把每天上班比作主线程,那么某甲今天要出差去做某事情就叫子线程。本来他是应该和我们一起上班解决事情的,现在他出差去做其他事情了,他出差去做某事情我们就可以说成是,他开启一个新线程去干某事情了。因为他出差离开了,和我们上着班的不能及时沟通处理问题,他与我们的拍子和不在一起了,所以他做的事情也叫作异步任务。
- 如果结合上面实例,我们大家上班解决问题为主线程,称之为团队工作,甲出差去做某事情为子线程,称之为私人工作。主线程要求子线程把网络数据取回来(团队要求甲出差成果展现出来),甲说,没问题,我出差回来就提交成果。问题来了,出差是需要时间的,而这段时间之内,团队莫不是就停摆等着甲返回的成果,当然不是!
- 团队不可能停摆等着甲的成果,同样,主线程也不可能等着子线程的“成果”,这就是问题的所在。结合上面实例,主线程(Android中也称作UI线程)通过方法
getWebData
取得网络数据(模拟),但是子线程执行是需要时间的,主线程在这期间不能一直等着,所以它便继续执行,注意:代码块
private String getWebData() {
Log.d("20180621", "在getWebData方法中开始时,toastStr: " + toastStr);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
toastStr = "模拟网络返回的数据";
Log.d("20180621", "在getWebData方法中修改数据后,toastStr: " + toastStr);
}
}).start();
Log.d("20180621", "在getWebData方法中返回前,toastStr: " + toastStr);
return toastStr;
}
在new ...
到.start():
之外的代码都是在主线程中执行的(在两者之间的就是子线程),所以当我们在主线程调用getWebData
方法,想以此获取网络数据时,因为主线程不可能等待子线程,所以其实执行的是在子线程之外的、属于主线程的代码,所以其实执行的操作就是:return toastStr
,而toasStr
在之前赋值为"execute开始",那么
toastStr = getWebData();
其实就相当于把他它自身赋值给自己,所以值还是"execute开始",这样,Toast显示出这个字符串也就理所当然了。
- 疑惑倒是解开了,那么莫非我们就不能取到网络数据了,这自然不可能。
3 将一个子线程写在一个带返回值方法里,想获取子线程处理的数据在逻辑上就是错的
- 比如我们前面,将获取网络数据(模拟)的子线程写在方法
getWebData
中,以期获取网络数据,进而返回与网络数据相关的字符串,这是存在逻辑错误的:因为前面我们说过,主线程是不会等待子线程的,而之所以开辟子线程,就是需要其去做某事情,做事情是需要花费时间的,这个时间主线程不会等。所以如果非要把子线程写在带返回值的方法里,是达不到预期目的的(等子线程处理好数据,方法早就返回了,而返回值是没有经过子线程处理的(很可能是空之类的……))。
4 如何取回子线程中处理的数据
- 通过前面的分析我们知道,将子线程写在带返回值的方法里,想返回子线程处理的数据不可行,那么可行的方案是:其实也简单当子线程处理完数据,告诉主线程一声,我这边OK了,你那边拿去用吧
- 那么如何告诉主线程子线程OK了,这个可以用Handler,Broadcast,EventBus等等,下面我们用Handler做一下简单的示例。
public class MainActivity extends AppCompatActivity {
private String toastStr;
private MyHandler handler = new MyHandler(this);
/**
* Instances of static inner classes do not hold an implicit
* reference to their outer class.
*/
private static class MyHandler extends Handler {
private final WeakReference<MainActivity> mActivity;
private MyHandler(MainActivity activity) {
mActivity = new WeakReference<MainActivity>(activity);
}
@Override
public void handleMessage(Message msg) {
MainActivity activity = mActivity.get();
if (activity != null) {
if (msg.what == 1) {
Toast.makeText(activity, (String) msg.obj, Toast.LENGTH_LONG).show();
}
}
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
findViewById(R.id.bt_click_execute)
.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
execute();
}
});
}
private void execute() {
toastStr = "execute开始";
Log.d("20180621", "在execute方法中第一步时候,toastStr: " + toastStr);
requestWebData();/*模拟获取网络数据等耗时操作*/
Log.d("20180621", "在execute方法中第二步时候,toastStr: " + toastStr);
}
private void requestWebData() {
Log.d("20180621", "在getWebData方法中开始时,toastStr: " + toastStr);
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1000 * 2);
} catch (InterruptedException e) {
e.printStackTrace();
}
toastStr = "模拟网络返回的数据";
Message message = Message.obtain();
message.what = 1;
message.obj = toastStr;
handler.sendMessage(message);
Log.d("20180621", "在getWebData方法中修改数据后,toastStr: " + toastStr);
}
}).start();
Log.d("20180621", "在getWebData方法中返回前,toastStr: " + toastStr);
}
}
- 惭愧,原来的Handler写法好像有警告,又度娘了一下它的写法(@^_^@)。
- 简单说下逻辑:
- 与之前相比,定义了一个Handler来处理子线程传过来的数据
- 在子线程处理好数据后,将消息发给主线程,并将数据发送给主线程
- 既然
getWedData
没有实际用处,那么将返回值改成void类型,并改方法名为:requestWebData
,意在请求网络数据(至于怎么拿到,我不关心,我只是告诉子线程,去给我拿数据(子线程处理好数据,发送消息给主线程,那么也就拿到数据了))
5 异步任务请求数据的正确姿势
- 正确姿势是什么我不大懂,不过最起码,你应该分为三步:
- 告诉子线程,“小子去干活了”,相当于上述代码的
requestWebData
,其核心就是开启子线程,执行耗时操作 - 当子线程活干完了,要报告:“老大,活干好了,要看看成果不?”,上述代码中的,即向主线程发送消息,传递数据
Message message = Message.obtain();
message.what = 1;
message.obj = toastStr;
handler.sendMessage(message);
- 老大检查工作成果,并做进一步加工,也就是最后数据的使用
public void handleMessage(Message msg) {
MainActivity activity = mActivity.get();
if (activity != null) {
if (msg.what == 1) {
Toast.makeText(activity, (String) msg.obj, Toast.LENGTH_LONG).show();
}
}
}
- over
网友评论