首页 > 代码库 > 小猪的Android入门之路 Day 8 part 3
小猪的Android入门之路 Day 8 part 3
小猪的Android入门之路 Day 8 part 3
Android网络编程浅析——Android网络数据的下载
——转载请注明出处:coder-pig
本节引言:
我们的应用很多时候都会涉及到网络数据的下载,比如缓存图片,当我们的
Apk需要进行版本更新时都需要下载文件;通常下载文件的操作都是放在后台
进行的,就是使用Service来完成,鉴于我们还没学到,所以这里都用Activity进行
演示!本章讲解的知识点有三个,单线程下载;普通多线程下载;多线程断点续传下载!
多线程断点续传比较难以理解,如果实在理解不了,就把demo下下来,改改就能用了!
正文:
J2SE单线程下载文件
就是简单的使用URLConnection.openStream()打开网络输入流,然后使用JavaSE中的
方法将流写入到文件中!
核心代码如下:
下载的方法:
public static void downLoad(String path,Context context)throws Exception { URL url = new URL(path); InputStream is = url.openStream(); //截取最后的文件名 String end = path.substring(path.lastIndexOf(".")); //打开手机对应的输出流,输出到文件中 OutputStream os = context.openFileOutput("Cache_"+System.currentTimeMillis()+end, Context.MODE_PRIVATE); byte[] buffer = new byte[1024]; int len = 0; //从输入六中读取数据,读到缓冲区中 while((len = is.read(buffer)) > 0) { os.write(buffer,0,len); } //关闭输入输出流 is.close(); os.close(); }效果如下:
代码太简单,这里就不给出其他的代码了,有需要的可以下载demo
demo下载:DownLoadDemo1.zip
J2SE普通多线程下载:
我们都知道使用多线程下载文件可以更快地完成文件的下载,但是为什么呢?
答:因为抢占的服务器资源多,假设服务器最多服务100个用户,服务器中的一个线程对应一个用户
100条线程在计算机中并发执行,由CPU划分时间片轮流执行,加入a有99条线程下载文件,那么
相当于占用了99个用户资源,自然就有用较快的下载速度
ps:当然不是线程越多就越好,开启过多线程的话,app需要维护和同步每条线程的开销,这些开销
反而会导致下载速度的降低,另外还和你的网速有关!
多线程下载的流程
获取网络连接——本地磁盘创建相同大小的空文件——计算每条线程需从文件哪个部分开始下载,结束
——依次创建,启动多条线程来下载网络资源的指定部分
ps:这里直接用J2SE来完成多线程操作,直接建立一个工程,使用Junit运行指定方法即可
如果再Android下用单元测试有点麻烦,照顾一部分朋友,你直接new一个Java Project即可!
代码如下:
DownLoader.java
package com.jay.test; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; import org.junit.Test; public class Downloader { //添加@Test标记是表示该方法是Junit测试的方法,就可以直接运行该方法了 @Test public void download() throws Exception { //设置URL的地址和下载后的文件名 String filename = "meitu.exe"; String path = "http://10.13.20.32:8080/Test/XiuXiu_Green.exe"; URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5000); conn.setRequestMethod("GET"); //获得需要下载的文件的长度(大小) int filelength = conn.getContentLength(); System.out.println("要下载的文件长度"+filelength); //生成一个大小相同的本地文件 RandomAccessFile file = new RandomAccessFile(filename, "rwd"); file.setLength(filelength); file.close(); conn.disconnect(); //设置有多少条线程下载 int threadsize = 3; //计算每个线程下载的量 int threadlength = filelength % 3 == 0 ? filelength/3:filelength+1; for(int i = 0;i < threadsize;i++) { //设置每条线程从哪个位置开始下载 int startposition = i * threadlength; //从文件的什么位置开始写入数据 RandomAccessFile threadfile = new RandomAccessFile(filename, "rwd"); threadfile.seek(startposition); //启动三条线程分别从startposition位置开始下载文件 new DownLoadThread(i,startposition,threadfile,threadlength,path).start(); } int quit = System.in.read(); while('q' != quit) { Thread.sleep(2000); } } private class DownLoadThread extends Thread { private int threadid; private int startposition; private RandomAccessFile threadfile; private int threadlength; private String path; public DownLoadThread(int threadid, int startposition, RandomAccessFile threadfile, int threadlength, String path) { this.threadid = threadid; this.startposition = startposition; this.threadfile = threadfile; this.threadlength = threadlength; this.path = path; } public DownLoadThread() {} @Override public void run() { try { URL url = new URL(path); HttpURLConnection conn = (HttpURLConnection) url.openConnection(); conn.setConnectTimeout(5000); conn.setRequestMethod("GET"); //指定从什么位置开始下载 conn.setRequestProperty("Range", "bytes="+startposition+"-"); //System.out.println(conn.getResponseCode()); if(conn.getResponseCode() == 206) { InputStream is = conn.getInputStream(); byte[] buffer = new byte[1024]; int len = -1; int length = 0; while(length < threadlength && (len = is.read(buffer)) != -1) { threadfile.write(buffer,0,len); //计算累计下载的长度 length += len; } threadfile.close(); is.close(); System.out.println("线程"+(threadid+1) + "已下载完成"); } }catch(Exception ex){System.out.println("线程"+(threadid+1) + "下载出错"+ ex);} } } }
运行截图:
如图,使用多线程完成了对文件的下载!双击exe文件可运行,说明文件并没有损坏!
注意事项:
①int filelength = conn.getContentLength(); //获得下载文件的长度(大小)
②RandomAccessFile file = new RandomAccessFile(filename, "rwd");
//该类运行对文件进行读写,是多线程下载的核心
③int threadlength = filelength % 3 == 0 ? filelength/3:filelength+1;
//计算每个线程要下载的量
④conn.setRequestProperty("Range", "bytes="+startposition+"-");
//指定从哪个位置开始读写,这个是URLConnection提供的方法
⑤//System.out.println(conn.getResponseCode());
//这个注释了的代码是用来查看conn的返回码的,我们前面用的都是200,
而针对多线程的话,通常是206,必要时我们可以通过调用该方法查看返回码!
⑥int quit = System.in.read();
while(‘q‘ != quit){Thread.sleep(2000);}
//这段代码是做延时操作的,因为我们用的是本地下载,可能该方法运行完了,而我们的
线程还没有开启,这样会引发异常,这里的话,让用户输入一个字符,如果是‘q‘的话就退出
代码下载:J2SEMulDownLoader.zip
Android多线程断点下载
一看标题,多线程断点下载,多线程我们就知道了,那么什么是断点啊?
答:其实就是使用数据库记录每天线程所下载的进度!每次启动时根据线程id查询某线程的下载进度,
在继续下载!这一块的话如果能看懂的话很好,看不懂也没什么,更别说写出来了
笔者自己也写不出来,建议是把demo下下来,等遇到实际情况时再修改下就可以直接用了;
比如别人写的一个音乐下载和在线播放器,就是改了下MainActivity添加了一个音乐播放类而已!
直接上源码吧:参考的是网上的代码,笔者在代码中已添加了详细的注释,应该能看懂的!
示例代码:多线程断点下载器demo
扩展代码:多线程断点下载+在线音乐播放器
示例代码效果图:
=================================要深入了解或者你公司不允许下东西,你在看下面的代码把
我们要做的第一步就是创建一个用来记录线程下载信息的表:
step 1:
创建数据库表,于是乎我们创建一个数据库的管理器类,继承SQLiteOpenHelper类
重写onCreate()与onUpgrade()方法,我们创建的表字段如下:
DBOpenHelper.java:
package com.jay.example.db; import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteDatabase.CursorFactory; import android.database.sqlite.SQLiteOpenHelper; public class DBOpenHelper extends SQLiteOpenHelper { public DBOpenHelper(Context context) { super(context, "downs.db", null, 1); } @Override public void onCreate(SQLiteDatabase db) { //数据库的结构为:表名:filedownlog 字段:id,downpath:当前下载的资源, //threadid:下载的线程id,downlength:线程下载的最后位置 db.execSQL("CREATE TABLE IF NOT EXISTS filedownlog " + "(id integer primary key autoincrement," + " downpath varchar(100)," + " threadid INTEGER, downlength INTEGER)"); } @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { //当版本号发生改变时调用该方法,这里删除数据表,在实际业务中一般是要进行数据备份的 db.execSQL("DROP TABLE IF EXISTS filedownlog"); onCreate(db); } }
step 2:有了数据库管理器,下一步我们就要创建一个数据库的操作类了
那么我们需要一些创建一些什么样的方法呢?
①我们需要一个根据URL获得每条线程当前下载长度的方法
②接着,当我们的线程新开辟后,我们需要往数据库中插入与该线程相关参数的方法
③还要定义一个可以实时更新下载文件长度的方法
④我们线程下载完,还需要根据线程id,删除对应记录的方法
FileService.java
package com.jay.example.db; import java.util.HashMap; import java.util.Map; import android.content.Context; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; /* * 该类是一个业务bean类,完成数据库的相关操作 * */ public class FileService { //声明数据库管理器 private DBOpenHelper openHelper; //在构造方法中根据上下文对象实例化数据库管理器 public FileService(Context context) { openHelper = new DBOpenHelper(context); } /** * 获得指定URI的每条线程已经下载的文件长度 * @param path * @return * */ public Map<Integer, Integer> getData(String path) { //获得可读数据库句柄,通常内部实现返回的其实都是可写的数据库句柄 SQLiteDatabase db = openHelper.getReadableDatabase(); //根据下载的路径查询所有现场的下载数据,返回的Cursor指向第一条记录之前 Cursor cursor = db.rawQuery("select threadid, downlength from filedownlog where downpath=?", new String[]{path}); //建立一个哈希表用于存放每条线程已下载的文件长度 Map<Integer,Integer> data = http://www.mamicode.com/new HashMap();> step 3:好了,数据库管理器与操作类都完成了,接下来弄什么呢:
接着就该弄一个文件下载器类了,在该类中又要完成什么操作呢?
①定义一堆变量,核心是线程池threads和同步集合ConcurrentHashMap,用于缓存线程下载长度的
②定义一个获取线程池中线程数的方法;
③定义一个退出下载的方法,
④获取当前文件大小的方法
⑤累计当前已下载长度的方法,这里需要添加一个synchronized关键字,用来解决并发访问的问题
⑥更新指定线程最后的下载位置,同样也需要用同步
⑦在构造方法中完成文件下载,线程开辟等操作
⑦获取文件名的方法:先截取提供的url最后的‘/‘后面的字符串,如果获取不到,再从头字段查找,还是
找不到的话,就使用网卡标识数字+cpu的唯一数字生成一个16个字节的二进制作为文件名
⑧开始下载文件的方法
⑨获取http响应头字段的方法
⑩打印http头字段的方法
?打印日志信息的方法
FileDownloadered.java:
package com.jay.example.service; import java.io.File; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; import java.util.LinkedHashMap; import java.util.Map; import java.util.UUID; import java.util.concurrent.ConcurrentHashMap; import java.util.regex.Matcher; import java.util.regex.Pattern; import android.content.Context; import android.util.Log; import com.jay.example.db.FileService; public class FileDownloadered { private static final String TAG = "文件下载类"; //设置一个查log时的一个标志 private static final int RESPONSEOK = 200; //设置响应码为200,代表访问成功 private FileService fileService; //获取本地数据库的业务Bean private boolean exited; //停止下载的标志 private Context context; //程序的上下文对象 private int downloadedSize = 0; //已下载的文件长度 private int fileSize = 0; //开始的文件长度 private DownloadThread[] threads; //根据线程数设置下载的线程池 private File saveFile; //数据保存到本地的文件中 private Map<Integer, Integer> data = http://www.mamicode.com/new ConcurrentHashMap(); //缓存个条线程的下载的长度> step 4:接着就要弄一个自定义的下载线程类了:
①首先肯定是要继承Thread类啦,然后重写Run()方法
②Run()方法:先判断是否下载完成,没有得话:打开URLConnection链接,接着RandomAccessFile
进行数据读写,完成时设置完成标记为true,发生异常的话设置长度为-1,打印异常信息
③打印log信息的方法
④判断下载是否完成的方法(根据完成标记)
⑤获得已下载的内容大小
DownLoadThread.java:
package com.jay.example.service; import java.io.File; import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.URL; import android.util.Log; public class DownloadThread extends Thread { private static final String TAG = "下载线程类"; //定义TAG,在打印log时进行标记 private File saveFile; //下载的数据保存到的文件 private URL downUrl; //下载的URL private int block; //每条线程下载的大小 private int threadId = -1; //初始化线程id设置 private int downLength; //该线程已下载的数据长度 private boolean finish = false; //该线程是否完成下载的标志 private FileDownloadered downloader; //文件下载器 public DownloadThread(FileDownloadered downloader, URL downUrl, File saveFile, int block, int downLength, int threadId) { this.downUrl = downUrl; this.saveFile = saveFile; this.block = block; this.downloader = downloader; this.threadId = threadId; this.downLength = downLength; } @Override public void run() { if(downLength < block){//未下载完成 try { HttpURLConnection http = (HttpURLConnection) downUrl.openConnection(); http.setConnectTimeout(5 * 1000); http.setRequestMethod("GET"); http.setRequestProperty("Accept", "image/gif, image/jpeg, image/pjpeg, image/pjpeg, application/x-shockwave-flash, application/xaml+xml, application/vnd.ms-xpsdocument, application/x-ms-xbap, application/x-ms-application, application/vnd.ms-excel, application/vnd.ms-powerpoint, application/msword, */*"); http.setRequestProperty("Accept-Language", "zh-CN"); http.setRequestProperty("Referer", downUrl.toString()); http.setRequestProperty("Charset", "UTF-8"); int startPos = block * (threadId - 1) + downLength;//开始位置 int endPos = block * threadId -1;//结束位置 http.setRequestProperty("Range", "bytes=" + startPos + "-"+ endPos);//设置获取实体数据的范围 http.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.2; Trident/4.0; .NET CLR 1.1.4322; .NET CLR 2.0.50727; .NET CLR 3.0.04506.30; .NET CLR 3.0.4506.2152; .NET CLR 3.5.30729)"); http.setRequestProperty("Connection", "Keep-Alive"); InputStream inStream = http.getInputStream(); //获得远程连接的输入流 byte[] buffer = new byte[1024]; //设置本地数据的缓存大小为1MB int offset = 0; //每次读取的数据量 print("Thread " + this.threadId + " start download from position "+ startPos); //打印该线程开始下载的位置 RandomAccessFile threadfile = new RandomAccessFile(this.saveFile, "rwd"); threadfile.seek(startPos); //用户没有要求停止下载,同时没有达到请求数据的末尾时会一直循环读取数据 while (!downloader.getExited() && (offset = inStream.read(buffer, 0, 1024)) != -1) { threadfile.write(buffer, 0, offset); //直接把数据写入到文件中 downLength += offset; //把新线程已经写到文件中的数据加入到下载长度中 downloader.update(this.threadId, downLength); //把该线程已经下载的数据长度更新到数据库和内存哈希表中 downloader.append(offset); //把新下载的数据长度加入到已经下载的数据总长度中 } threadfile.close(); inStream.close(); print("Thread " + this.threadId + " download finish"); this.finish = true; //设置完成标记为true,无论下载完成还是用户主动中断下载 } catch (Exception e) { this.downLength = -1; //设置该线程已经下载的长度为-1 print("Thread "+ this.threadId+ ":"+ e); } } } private static void print(String msg){ Log.i(TAG, msg); } /** * 下载是否完成 * @return */ public boolean isFinish() { return finish; } /** * 已经下载的内容大小 * @return 如果返回值为-1,代表下载失败 */ public long getDownLength() { return downLength; } }step 6:另外在,FileDownloader中使用了DownloadProgressListener进行进度监听,
所以这里需要创建一个接口,同时定义一个方法的空实现:
DownloadProgressListener.java:
package com.jay.example.service; public interface DownloadProgressListener { public void onDownloadSize(int downloadedSize); }step 6:接着就要弄下我们的布局了,布局很简单,
另外调用android:enabled="false"设置组件是否可点击, 代码如下
activity_main.xml:<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/LinearLayout1" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" tools:context="com.jay.example.multhreadcontinuabledemo.MainActivity" > <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="请输入要下载的文件地址" /> <EditText android:id="@+id/editpath" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="http://10.13.20.32:8080/Test/twelve.mp3" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/btndown" android:text="下载" /> <Button android:layout_width="wrap_content" android:layout_height="wrap_content" android:id="@+id/btnstop" android:text="停止" android:enabled="false" /> <ProgressBar android:layout_width="fill_parent" android:layout_height="18dp" style="?android:attr/progressBarStyleHorizontal" android:id="@+id/progressBar" /> <TextView android:layout_width="fill_parent" android:layout_height="wrap_content" android:gravity="center" android:id="@+id/textresult" android:text="显示实时下载的百分比" /> </LinearLayout>step 7:最后就是我们的MainActivity了,完成组件以及相关变量的初始化;
使用handler来完成界面的更新操作,另外耗时操作不能够在主线程中进行,
所以这里需要开辟新的线程,这里用Runnable实现,详情见代码把:
MainActivity.java
package com.jay.example.multhreadcontinuabledemo; import java.io.File; import com.jay.example.service.FileDownloadered; import android.app.Activity; import android.os.Bundle; import android.os.Environment; import android.os.Handler; import android.os.Message; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; public class MainActivity extends Activity { private EditText editpath; private Button btndown; private Button btnstop; private TextView textresult; private ProgressBar progressbar; private static final int PROCESSING = 1; //正在下载实时数据传输Message标志 private static final int FAILURE = -1; //下载失败时的Message标志 private Handler handler = new UIHander(); private final class UIHander extends Handler{ public void handleMessage(Message msg) { switch (msg.what) { //下载时 case PROCESSING: int size = msg.getData().getInt("size"); //从消息中获取已经下载的数据长度 progressbar.setProgress(size); //设置进度条的进度 //计算已经下载的百分比,此处需要转换为浮点数计算 float num = (float)progressbar.getProgress() / (float)progressbar.getMax(); int result = (int)(num * 100); //把获取的浮点数计算结果转换为整数 textresult.setText(result+ "%"); //把下载的百分比显示到界面控件上 if(progressbar.getProgress() == progressbar.getMax()){ //下载完成时提示 Toast.makeText(getApplicationContext(), "文件下载成功", 1).show(); } break; case FAILURE: //下载失败时提示 Toast.makeText(getApplicationContext(), "文件下载失败", 1).show(); break; } } } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); editpath = (EditText) findViewById(R.id.editpath); btndown = (Button) findViewById(R.id.btndown); btnstop = (Button) findViewById(R.id.btnstop); textresult = (TextView) findViewById(R.id.textresult); progressbar = (ProgressBar) findViewById(R.id.progressBar); ButtonClickListener listener = new ButtonClickListener(); btndown.setOnClickListener(listener); btnstop.setOnClickListener(listener); } private final class ButtonClickListener implements View.OnClickListener{ public void onClick(View v) { switch (v.getId()) { case R.id.btndown: String path = editpath.getText().toString(); if(Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)){ File saveDir = Environment.getExternalStorageDirectory(); download(path, saveDir); }else{ Toast.makeText(getApplicationContext(), "sd卡读取失败", 1).show(); } btndown.setEnabled(false); btnstop.setEnabled(true); break; case R.id.btnstop: exit(); btndown.setEnabled(true); btnstop.setEnabled(false); break; } } /* 由于用户的输入事件(点击button, 触摸屏幕....)是由主线程负责处理的,如果主线程处于工作状态, 此时用户产生的输入事件如果没能在5秒内得到处理,系统就会报“应用无响应”错误。 所以在主线程里不能执行一件比较耗时的工作,否则会因主线程阻塞而无法处理用户的输入事件, 导致“应用无响应”错误的出现。耗时的工作应该在子线程里执行。 */ private DownloadTask task; /** * 退出下载 */ public void exit(){ if(task!=null) task.exit(); } private void download(String path, File saveDir) {//运行在主线程 task = new DownloadTask(path, saveDir); new Thread(task).start(); } /* * UI控件画面的重绘(更新)是由主线程负责处理的,如果在子线程中更新UI控件的值,更新后的值不会重绘到屏幕上 * 一定要在主线程里更新UI控件的值,这样才能在屏幕上显示出来,不能在子线程中更新UI控件的值 */ private final class DownloadTask implements Runnable{ private String path; private File saveDir; private FileDownloadered loader; public DownloadTask(String path, File saveDir) { this.path = path; this.saveDir = saveDir; } /** * 退出下载 */ public void exit(){ if(loader!=null) loader.exit(); } public void run() { try { loader = new FileDownloadered(getApplicationContext(), path, saveDir, 3); progressbar.setMax(loader.getFileSize());//设置进度条的最大刻度 loader.download(new com.jay.example.service.DownloadProgressListener() { public void onDownloadSize(int size) { Message msg = new Message(); msg.what = 1; msg.getData().putInt("size", size); handler.sendMessage(msg); } }); } catch (Exception e) { e.printStackTrace(); handler.sendMessage(handler.obtainMessage(-1)); } } } } }step 8:在AndroidManifest.xml文件中添加相关权限:
<!-- 访问internet权限 --> <uses-permission android:name="android.permission.INTERNET"/> <!-- 在SDCard中创建与删除文件权限 --> <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/> <!-- 往SDCard写入数据权限 --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
小猪的Android入门之路 Day 8 part 3