Android中的多线程
1. 每一个应用的主线程都是死循环
2. 其他线程不能修改主线程中获取的控件中的内容
耗时操作/ANR异常
为什么主线程不能阻塞太久?
主线程需要响应界面发生的事件(响应用户操作), 接收消息, 处理消息, 更新UI, 所以不能阻塞.
耗时操作不要放在主线程中. 不是说不可以, 最好不要这样.
但是访问网络必定是个耗时操作, 从4.0开始, 主线程不可以访问网络.
为什么子线程不能更新UI, 只有主线程可以?
线程并发问题, 多线程操作UI, 可能导致UI混乱.
子线程获取数据后如何更新主界面?
通过 Handler 对象. Handler对象有两种使用方法:
- post
- sendMessage
-----------------
这里需要补充!!!!
-----------------
另: ProgressBar 是个特例, 因为只要使用 ProgressBar, 肯定是耗时操作.
所以 ProgressBar 一般都是在子线程中使用的, Google为了方便使用,
对其进行了处理, 内部已经使用了 Handler, 可以直接在子线程中使用.
另外 SeekBar, ProgressDialog 也是一样的道理.
通过网络获取数据
获取文本文件
下面演示一个从网络获取文本数据的例子, 这是android下访问网络的标准写法.
{{{class="brush:java"
public class MainActivity extends Activity {
private EditText et_address;
private TextView tv_content;
// 创建一个 Handler 对象, 用于其他线程和主线程之间进行数据通信.
private Handler handler = new Handler();
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
et_address = (EditText) findViewById(R.id.et_address);
tv_content = (TextView) findViewById(R.id.tv_content);
}
public void getText(View view) {
final String path = et_address.getText().toString().trim();
if (TextUtils.isEmpty(path)) {
Toast.makeText(getApplicationContext(), "地址不能为空", Toast.LENGTH_SHORT).show();
} else {
// 主线程不能调用操作时间过长的方法(如访问网络), 所以需要另起一个线程
new Thread() {
public void run() {
try {
final String text = UrlService.getText(path);
// 子线程是不能处理主线程中的控件中的数据的, 所以需要使用一个
// Handler对象告诉主线程怎样处理数据.
// Handler 的 post 方法可以传递一个 Runnable对象, 用于让主线程处理数据
handler.post(new Runnable() {
// 这里的 run 方法是在主线程中调用的.
@Override
public void run() {
tv_content.setText(text);
}
});
} catch (Exception e) {
e.printStackTrace();
handler.post(new Runnable() {
// 即使是弹出吐司, 也需要使用handler交给主线程处理.
@Override
public void run() {
Toast.makeText(getApplicationContext(), "获取失败",
Toast.LENGTH_SHORT).show();
}
});
}
};
}.start();
}
}
}
// 访问网络的标准方式
public class UrlService {
public static String getText(String path) throws Exception {
URL url = new URL(path);
// 通过URL的 openConnection 方法得到一个 HttpURLConnection
// 这个对象封装了程序和服务器之间的连接
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setRequestMethod("GET");
conn.setConnectTimeout(5000);
InputStream is = conn.getInputStream();
// 使用一个 字节数组输出流, 存放数据
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len = 0;
while((len = is.read(buf)) > 0) {
baos.write(buf, 0, len);
}
is.close();
byte[] bs = baos.toByteArray();
String string = new String(bs, "UTF-8");
return string;
}
}
--------------
无法创建Handler异常, 当设置了timeout后, 网速不给力或者路径错误, 就会出这个诡异异常
这是因为在 子线程try catch 中弹出吐司了, 相当于修改了UI, 只有主线程才能更新UI
子线程中能否创建handler?!!!!!
能, 但是要先调用一些方法!!!
-------------
获取图片
大体上和获取文本一样, 只是有一些细节需要处理, 这里只给出核心的一些代码.
{{{class="brush:java"
final Bitmap img = new UrlService(getApplicationContext())
.getImg(path);
handler.post(new Runnable() {
@Override
public void run() {
iv.setImageBitmap(img);
}
}
public Bitmap getImg(String path) throws Exception {
URL url = new URL(path);
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
//conn.setRequestMethod("GET");
// 下面这句话设置超时时间, 如果超过了指定时间, 程序会异常退出, 很奇怪
//conn.setConnectTimeout(5000);
// 缓存文件
File cacheDir = context.getCacheDir();
File cache = new File(cacheDir, getFileName(path));
if(cache.exists()) {
conn.setIfModifiedSince(System.currentTimeMillis());
}
// 获取并判断响应码
int code = conn.getResponseCode();
if(304 == code ) {
// 如果为 304, 表示要拿缓存
// conn.setIfModifiedSince(System.currentTimeMillis());
return BitmapFactory.decodeFile(cache.getAbsolutePath());
}else if(200 == code) {
// 如果为 200, 表示正常相应
InputStream is = conn.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buf = new byte[1024];
int len = 0;
while((len = is.read(buf)) > 0) {
baos.write(buf, 0, len);
}
is.close();
byte[] data = baos.toByteArray();
// 写到本地缓存中
File file = new File(cacheDir, getFileName(path));
FileOutputStream fos = new FileOutputStream(file);
fos.write(data);
fos.close();
// BitmapFactory 可以从各种源中解析图片.
Bitmap bitmap = BitmapFactory.decodeByteArray(data, 0, data.length);
return bitmap;
} else {
throw new RuntimeException("获取失败");
}
}
private String getFileName(String path) {
int start = path.lastIndexOf("/") + 1;
return path.substring(start);
}
}}}
实际开发中, 我们一般使用开源项目 SmartImageView 来获取网络上的图片.
这个项目提供了一个控件 SmartImageView , 操作起来十分方便, 内部自动开线程, 使用handler
<com.loopj.android.image.SmartImageView
android:id="@+id/siv"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/et_address"
android:layout_centerHorizontal="true"
android:layout_marginTop="15dp"
/>
public void getImgSmart(View view) {
SmartImageView siv = (SmartImageView) findViewById(R.id.siv);
// 通过URL地址加载图片, 设置加载失败时显示的图片, 设置加载过程中显示的图片
siv.setImageUrl(et_address.getText().toString().trim(),
android.R.drawable.ic_delete, android.R.drawable.ic_menu_gallery);
}
解析XML/JSON
解析XML就是pull解析, 解析JSON也有对应的工具类
public List<Person> getXml() throws Exception {
URL url = new URL("");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
int code = conn.getResponseCode();
if(200==code) {
InputStream is = conn.getInputStream();
List<Person> list = parseXml(is);
return list;
}
return null;
}
private List<Person> parseXml(InputStream is) throws Exception {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(is, "UTF-8");
List<Person> list = new ArrayList<Person>();
Person p = null;
for(int type=parser.getEventType(); type!=XmlPullParser.END_DOCUMENT; type=parser.next()) {
if("person".equals(parser.getName())) {
p = new Person();
String value = parser.getAttributeValue(0);
Long id = Long.parseLong(value);
p.setId(id);
list.add(p);
} else if("name".equals(parser.getName())) {
p.setName(parser.nextText());
} else if("age".equals(parser.getName())) {
p.setAge(Integer.parseInt(parser.nextText()));
}
}
return list;
}
解析JSON: 服务器传过来的是个 js 文件, 但是要求是 utf-8 编码.
Android 中使用 JSONArray, JSONObject 解析JSON
public List<Person> getJson() throws Exception {
URL url = new URL("");
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setConnectTimeout(5000);
int code = conn.getResponseCode();
if(200==code) {
InputStream is = conn.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while((len=is.read(buffer))>0) {
baos.write(buffer, 0, len);
}
byte[] bs = baos.toByteArray();
String json = new String(bs, "UTF-8");
List<Person> list = parseJson(json);
return list;
}
return null;
}
private List<Person> parseJson(String json) throws Exception {
List<Person> list = new ArrayList<Person>();
Person p = null;
JSONArray arr = new JSONArray(json);
for(int i=0; i<arr.length(); i++) {
JSONObject obj = arr.getJSONObject(i);
p = new Person();
p.setId(obj.getLong("id"));
p.setName(obj.getString("name"));
p.setAge(obj.getInt("age"));
list.add(p);
}
return list;
}
}}}
HttpClient
public static String loginByHttpClientGet(String username, String password)
throws Exception {
// 1.打开一个浏览器
HttpClient client = new DefaultHttpClient();
// 2.输入地址
String path = "http://192.168.1.100:8080/web/LoginServlet?username="
+ URLEncoder.encode(username, "UTF-8") + "&password="
+ URLEncoder.encode(password, "utf-8");
HttpGet httpGet = new HttpGet(path);
// 3.敲回车
HttpResponse response = client.execute(httpGet);
int code = response.getStatusLine().getStatusCode();
if (code == 200) {
InputStream is = response.getEntity().getContent();
// 把is里面的内容转化成 string文本.
String result = StreamUtils.readStream(is);
return result;
}else{
return null;
}
}
/**
* 采用httpclient的post方式提交数据到服务器
*/
public static String loginByHttpClientPost(String username, String password)
throws Exception {
// 1.打开一个浏览器
HttpClient client = new DefaultHttpClient();
// 2.输入地址
String path = "http://192.168.1.100:8080/web/LoginServlet";
HttpPost httpPost = new HttpPost(path);
//设置要提交的数据实体
List<NameValuePair> parameters = new ArrayList<NameValuePair>();
parameters.add(new BasicNameValuePair("username", username));
parameters.add(new BasicNameValuePair("password", password));
UrlEncodedFormEntity entity = new UrlEncodedFormEntity(parameters,"UTF-8");
// StringEntity e = new StringEntity("aaa","bbb");
// httpPost.setEntity(e);
httpPost.setEntity(entity);
// 3.敲回车
HttpResponse response = client.execute(httpPost);
int code = response.getStatusLine().getStatusCode();
if (code == 200) {
InputStream is = response.getEntity().getContent();
// 把is里面的内容转化成 string文本.
String result = StreamUtils.readStream(is);
return result;
}else{
return null;
}
}
关于二者的选择: 2.3之前用 HttpClient, 2.3及以后用 HttpUrlConnection
AsyncHttpClient
普通方式提交数据到服务器需要自己拼接参数, 非常麻烦. 使用开源项目 android-async-http 提供的 AsyncHttpClient 类就方便太多了.
以后一律使用 AsyncHttpClient. 而且前面所有的问题都可以用这个解决!
{{{class="brush:java"
public void asyncLogin(View v) {
String username = et_username.getText().toString().trim();
String password = et_password.getText().toString().trim();
// 把要传递的参数封装成一个对象
RequestParams params = new RequestParams();
params.add("username", username);
params.add("password", password);
// 创建异步HTTP客户端, 向指定路径发送GET请求, 传递参数,
// 请求成功或失败都会执行对应的回调函数
new AsyncHttpClient().post("http://192.168.1.228:8080/05.Web/LoginServlet", params,
new AsyncHttpResponseHandler() {
public void onSuccess(String response) {
Toast.makeText(getApplicationContext(), "Async 登录成功",
Toast.LENGTH_SHORT).show();
}
public void onFailure(Throwable error, String content) {
Toast.makeText(getApplicationContext(), "Async 登录失败",
Toast.LENGTH_SHORT).show();
}
});
}
public void upload(View view) throws Exception {
String username = et_username.getText().toString().trim();
String password = et_password.getText().toString().trim();
String filepath = et_file.getText().toString().trim();
RequestParams params = new RequestParams();
params.add("username", username);
params.add("password", password);
params.put("file", new File(filepath)); // 添加文件上传字段
new AsyncHttpClient().post("http://192.168.1.228:8080/05.Web/LoginServlet", params,
new AsyncHttpResponseHandler() {
public void onSuccess(String response) {
Toast.makeText(getApplicationContext(), "Async 上传成功",
Toast.LENGTH_SHORT).show();
}
public void onFailure(Throwable error, String content) {
Toast.makeText(getApplicationContext(), "Async 上传失败",
Toast.LENGTH_SHORT).show();
}
});
}
}}}
访问WebService
有些WebService可以通过HTTP的方式请求, 传回XML. 所以其实就是解析 XML.
{{{class="brush:java"
String phone = et_phone.getText().toString().trim();
RequestParams rp = new RequestParams();
rp.put("mobileCode", phone);
rp.put("userID", "");
client.get("http://webservice.webxml.com.cn/xxx/getMobileCodeInfo",
rp, new AsyncHttpResponseHandler() {
@Override
public void onSuccess(String content) {
try {
XmlPullParser parser = Xml.newPullParser();
parser.setInput(new StringReader(content));
for (int type = parser.getEventType(); type != XmlPullParser.END_DOCUMENT;
type = parser.next()) {
if(type == XmlPullParser.START_TAG && "string".equals(parser.getName())){
tv_result.setText(parser.nextText());
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
});
}}}
多线程断点续传下载
1.多线程
开启多个线程, 利用请求头"Range"控制下载数据的范围, 使用RandomAccessFile.seek()方法控制写入本地文件的范围
2.断点续传
在下载的过程中, 使用一种持久化存储的方式存储每个进程的下载进度, 如果下载中断, 读取上次存储的进度, 继续
3.手机端注意
网络操作必须在新线程中, 新线程中修改界面时需要回到主线程处理
ProgressBar内部已经实现了可以在新线程操作, 而如果使用TextView, 必须主线程处理
来自为知笔记(Wiz)