简单的说,只要利用了HTTP协议(http://www.ietf.org/rfc/rfc2616.txt)中的如下字段来和服务器端交互,就可以实现文件下载的断点续传:
Range : 用于客户端到服务器端的请求,可通过该字段指定下载文件的某一段大小,及其单位。典型的格式如:
Range: bytes=0-499 下载第0-499字节范围的内容
Range: bytes=500-999 下载第500-999字节范围的内容
Range: bytes=-500 下载最后500字节的内容
Range: bytes=500- 下载从第500字节开始到文件结束部分的内容(这是最常用的一种格式)
Range: bytes=0-0,-1 下载第一以及最后一个字节的内容(这个看上去有点变态...)
Accept-Ranges : 用于服务器端到客户端的应答,客户端通过该字段可以判断服务器是否支持断点续传(注意RFC中注明了这一部分并不是必须的)。格式如下:
Accept-Ranges: bytes 表示支持以bytes为单位进行传输。
Accept-Ranges: none 表示不支持
Content-Ranges : 用于服务器端到客户端的应答,与Accept-Ranges在同一个报文内,通过该字段指定了返回的文件资源的字节范围。格式如下:
Content-Ranges: bytes 0-499/1234 大小为1234的文件的第0-499字节范围的内容
Content-Ranges: bytes 734-1233/1234 大小为1234字节的文件的第734-结尾范围的内容
据此我们可以知道,断点续传这个功能是需要客户端和服务器端同时支持才能完成。
Android平台面向开发者提供了DownloadManager这个服务(service),可以用来完成下载,同时异步地得到下载进度的实时更新提示。原生的浏览器,Android Market以及GMail等客户端都使用了该接口。该接口也部分的提供了断点续传功能:如果在下载过程中遇到网络错误,如信号中断等,DownloadManager会在网络恢复时尝试断点续传继续下载该文件。但不支持由用户发起的暂停然后断点续传。
要扩展该功能也不难,只要为下载任务新增一种状态(类似paused_by_user),以及相关逻辑即可,这里暂不赘述,把话题引到一些常见问题上。
1. 关于ETag
RFC中的定义有些抽象,简单的说,ETag可以用来标识/保证文件的唯一性或完整性,你可以把它看作是服务器为某个文件生产的唯一标识值,每次文件有更新该值就会变化。通过这种机制客户端可以检查某个文件在断点续传(当然它不仅仅用于断点续传)的前后是否有所改动:如果ETag改变了就应该重新下载整个文件以保证它的完整性。
但是在现实环境中,有一些服务器并不返回ETag字段,同时它又是支持断点续传的,这种情况下原生的Android就会认为服务器端不支持断点续传。这应该不是什么bug,仅仅是这么实现而已。还有更麻烦的情况是,有些服务器给了错误的ETag,但文件是从未更改的,这时候要想从客户端修改这个“bug”,估计只能忽略ETag值了。
2. 关于HTTP 206
RFC中定义了断点续传时服务器端的应答情况:如果支持且返回的内容如请求所要求的那样,是该文件的一部分,则使用HTTP 206状态码;如果不支持,或需要返回整个文件,则使用HTTP 200状态码。但是现实网络中有些服务器不管三七二十一,都返回200。没办法,如果还是想从客户端来修改这个“bug”,那就多做一些判断处理吧:如果服务器指定了“Content-Ranges”,就忽略HTTP 200的状态码。
附图一张,简述流程。
补记:有一次被问起如何在原生的Android手机上暂停一个下载任务,回头再断点续传。我想是不是可以在下载过程中将手机信号关闭,下次再打开手机信号时,那个下载任务就可以自动接着续传了(当然前提是服务器支持)...这个用例没多大实用价值,懒得实测了。
多线程下载的原理是这样的:通常服务器同时与多个用户连接,用户之间共享带宽。如果N个用户的优先级都相同,那么每个用户连接到该服务器上的实际带宽就是服务器带宽的N分之一。可以想象,如果用户数目较多,则每个用户只能占有可怜的一点带宽,下载将会是个漫长的过程。如果你通过多个线程同时与服务器连接,那么你就可以榨取到较高的带宽了。
FileDownload
任务类:
1:联网获取下载文件的信息,在SD卡上建立相应文件
2:给每个任务创建固定数目的线程,并给每个线程分配任务,然后开启
3:暂停、重启线程,根据每个线程的运行情况,通过Handler反馈信息到主线程(进度、是否结束)
源码:
- public class FileDownloader {
- private static final String TAG = "FileDownloader";
- private String urlStr;
- private int fileLength;
- private int downloadLength;
- private File saveFile;
- private int block;
- private int threadCount = 2;
- private DownloadThread[] loadThreads;
- private Handler handler;
- private Boolean isFinished = false;
- private Boolean isPause = false;
- private Context context;
- RandomAccessFile accessFile;
-
- protected synchronized void append(int size){
- this.downloadLength += size;
- }
-
-
-
-
- public FileDownloader(String urlStr, Handler handler,Context context){
- this.urlStr = urlStr;
- this.handler = handler;
- this.loadThreads = new DownloadThread[threadCount];
- this.context = context;
- try {
- URL url = new URL(urlStr);
- HttpURLConnection conn = (HttpURLConnection)url.openConnection();
- conn.setConnectTimeout(3000);
-
- if(conn.getResponseCode() == 200){
- fileLength = conn.getContentLength();
- String tmpFile = urlStr.substring(urlStr.lastIndexOf('/')+1);
- print(tmpFile);
- saveFile = new File(Environment.getExternalStorageDirectory(),tmpFile);
- accessFile= new RandomAccessFile(saveFile,"rws");
- accessFile.setLength(fileLength);
- accessFile.close();
- }
- } catch (Exception e) {
-
- e.printStackTrace();
- }
- }
- public void Download(CompleteListener listener) throws Exception{
-
- URL url = new URL(urlStr);
- HttpURLConnection conn = (HttpURLConnection)url.openConnection();
- conn.setConnectTimeout(3000);
-
- if(conn.getResponseCode() == 200){
-
-
- Message msg = new Message();
- msg.what = 0;
- msg.getData().putInt("filelength", fileLength);
- handler.sendMessage(msg);
-
- block = fileLength%threadCount == 0? fileLength/threadCount : fileLength/threadCount+1;
- print(Integer.toString(block));
- for(int i = 0 ; i < threadCount ; i++){
- print("哈哈,你妹啊");
- this.loadThreads = new DownloadThread(context,this,urlStr,saveFile,block,i);
- print("嘿嘿");
- this.loadThreads.start();
- print("线程:"+Integer.toString(i)+" 开始下载");
- }
-
- while(!isPause && !this.isFinished){
- this.isFinished = true;
- Thread.sleep(900);
- for(int i = 0 ; i < threadCount ; i++){
- if(loadThreads != null && !loadThreads.isFinished()){
- this.isFinished = false;
- }
- }
-
- Message msg2 = new Message();
- msg2.what = 1;
- msg2.getData().putInt("currentlength", downloadLength);
- handler.sendMessage(msg2);
-
- }
- if(this.isFinished && listener != null){
- listener.isComplete(downloadLength);
- }
- }
- }
-
- private void print(String msg){
- Log.d(FileDownloader.TAG,msg);
- }
-
- public void setPause(){
- isPause = true;
- for(int i = 0 ;i < threadCount ; i++){
- if(loadThreads!= null && !loadThreads.isFinished()){
- loadThreads.setPause();
- print(Integer.toString(i)+"草泥马");
- }
- }
- }
-
- public void setResume(final CompleteListener listener) throws Exception{
- isPause = false;
- this.downloadLength = 0;
- this.Download(new CompleteListener(){
- public void isComplete(int size) {
- listener.isComplete(size);
- print("listener");
- }
- });
- }
- public Boolean isFinished(){
- return this.isFinished;
- }
- }
DownloadThread
下载线程类:
1:需要参数:下载地址 、存储位置、起始结束下载位置、结束位置。 (时间、数据库等信息)
2:读取数据库判断是否有下载记录,然后根据情况计算下载的起始与结束位置,进行下载
3:每读取一定数据需要通过任务类更新任务显示,并写入数据库(多线程访问数据库需要加锁)
4:根据任务类的需要,暂停则让该线程运行结束,而非阻塞该线程(考虑到android内存的需要)
源码:
- public class DownloadThread extends Thread{
- private String urlStr;
- private int startPosition;
- private int downloadLength=0;
- private int endPosition;
- private File saveFile;
- private int threadId;
- private FileDownloader fileDownloader;
- private Boolean isFinished = false;
- private DatabaseUtil databaseUtil;
- private Boolean isPause = false;
- private Context context;
-
- public DownloadThread(String urlStr){
- this.urlStr = urlStr;
- }
- public DownloadThread(Context context,FileDownloader fileDownloader,String urlStr , File file , int block ,int threadId){
- this.context = context;
- this.fileDownloader = fileDownloader;
- this.urlStr = urlStr;
- this.saveFile = file;
- this.threadId = threadId;
- this.startPosition = threadId * block;
- this.endPosition = (threadId+1) * block -1;
- }
-
- public void run(){
- try{
- RandomAccessFile accessFile = new RandomAccessFile(saveFile,"rwd");
-
- databaseUtil = new DatabaseUtil(context);
- ItemRecord record = databaseUtil.query(urlStr, threadId);
- if(record != null){
- downloadLength = record.getDownloadLength();
- fileDownloader.append(downloadLength);
- print("线程"+Integer.toString(threadId)+"存在记录"+Integer.toString(downloadLength));
- }else{
- synchronized(DatabaseUtil.lock){
- databaseUtil.insert(urlStr, threadId, downloadLength);
- print("线程"+Integer.toString(threadId)+"不存在记录");
- }
- }
- accessFile.seek(startPosition+downloadLength);
- if((endPosition+1) == (startPosition + downloadLength) || endPosition == (startPosition + downloadLength)){
- print(Integer.toString(endPosition)+"endPosition");
- print(Integer.toString(startPosition)+"startPosition");
- print(Integer.toString(downloadLength)+"downloadLength");
- isFinished = true;
- print("线程:"+Integer.toString(threadId)+" 曾成功下载");
- }else{
- URL url = new URL(urlStr);
- HttpURLConnection conn = (HttpURLConnection)url.openConnection();
- conn.setConnectTimeout(3000);
- conn.setRequestMethod("GET");
- int tmpStartPosition = startPosition + downloadLength;
- conn.setRequestProperty("Range", "bytes=" + tmpStartPosition + "-" + endPosition);
-
- if(conn.getResponseCode()==206){
- InputStream inStream = conn.getInputStream();
- byte[] buffer = new byte[1024];
- int len = 0;
- while(!isPause && (len = inStream.read(buffer))!= -1){
- accessFile.write(buffer, 0, len);
- fileDownloader.append(len);
- downloadLength += len;
-
- synchronized(DatabaseUtil.lock){
- databaseUtil.update(urlStr, threadId, downloadLength);
- }
- }
- if((endPosition+1) == (startPosition + downloadLength) || endPosition == (startPosition + downloadLength)){
- print(Integer.toString(endPosition)+"endPosition");
- print(Integer.toString(startPosition)+"startPosition");
- print(Integer.toString(downloadLength)+"downloadLength");
- isFinished = true;
- print("线程:"+Integer.toString(threadId)+" 下载完毕");
- }else{
- print(Integer.toString(endPosition)+"endPosition");
- print(Integer.toString(startPosition)+"startPosition");
- print(Integer.toString(downloadLength)+"downloadLength");
- print("线程:"+Integer.toString(threadId)+" 未下载完!");
- }
- }else{
- print(conn.getResponseMessage());
- print(Integer.toString(conn.getResponseCode()));
- print("擦,线程:"+Integer.toString(threadId)+" 没开始下载");
- }
- }
- }catch(Exception e){
- e.printStackTrace();
- }
- }
-
-
- public Boolean isFinished(){
- return this.isFinished;
- }
-
- public void setPause(){
- this.isPause = true;
-
- }
- private void print(String msg){
- Log.d("DownloadThrea",msg);
- }
- }
DatabaseHelper
建立数据库文件,并建表
源码:
- public class DatabaseHelper extends SQLiteOpenHelper{
-
- public DatabaseHelper(Context context , String name ,CursorFactory factory , int version) {
- super(context, "downloadFile.db", null,1);
-
- }
-
- @Override
- public void onCreate(SQLiteDatabase db) {
- Cursor cursor = null;
- try {
- Log.d("哈哈哈","leixun");
- cursor = db.rawQuery("SELECT * FROM sqlite_master WHERE type = ? AND name = ?", new String[]{"table","info"});
- if(cursor.moveToNext()){
- Log.d("DatabaseHelper","该表已经存在");
- }else{
- Log.d("DatabaseHelper","该表不存在 ,马上建立");
- db.execSQL("CREATE TABLE info (path VARCHAR(1024), threadid INTEGER , " +
- "downloadlength INTEGER , PRIMARY KEY(path,threadid))");
- }
- cursor.close();
- } catch (Exception e) {
- e.printStackTrace();
- }
-
-
- }
-
- @Override
- public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {
-
-
- }
- }
DatabaseUtil
1:增删改查任务列表
2:通过静态常量lock实现数据库写入时的并发锁功能
源码:
- public class DatabaseUtil {
- private DatabaseHelper helper;
- public static final String lock = "访问";
- public DatabaseUtil(Context context){
- helper = new DatabaseHelper(context, "downloadFile.db", null,1);
- }
-
- public void insert(String path , int threadid , int downloadlength){
- SQLiteDatabase db = helper.getWritableDatabase();
- db.execSQL("INSERT INTO info(path,threadid,downloadlength) VALUES (?,?,?)", new Object[]{path,threadid,downloadlength});
- print("成功添加");
- db.close();
- }
-
- public void delete(String path , int threadid){
- SQLiteDatabase db = helper.getWritableDatabase();
- db.execSQL("DELETE FROM info WHERE path = ? AND threadid = ?",new Object[]{path,threadid});
- print("成功删除");
- db.close();
- }
-
- public void update(String path , int threadid , int downloadlength){
- SQLiteDatabase db = helper.getWritableDatabase();
-
- db.execSQL("UPDATE info SET downloadlength = ? WHERE path = ? AND threadid = ?",new Object[]{downloadlength,path,threadid});
-
- db.close();
- }
-
- public ItemRecord query(String path, int threadid){
- SQLiteDatabase db = helper.getWritableDatabase();
- Cursor c = db.rawQuery("SELECT path,threadid,downloadlength FROM info WHERE path = ? AND threadid = ?", new String[]{path,Integer.toString(threadid)});
- ItemRecord record = null;
- if(c.moveToNext()){
- record = new ItemRecord(c.getString(0),c.getInt(1),c.getInt(2));
- }
- c.close();
- db.close();
- return record;
- }
-
- public List<String> query(String path){
- print("List<String> query 开始");
- SQLiteDatabase db = helper.getWritableDatabase();
- Cursor c = db.rawQuery("SELECT DISTINCT path FROM info", new String[]{path});
- List<String> arrayList = new ArrayList<String>();
- while(c.moveToNext()){
- arrayList.add(c.getString(0));
- }
- c.close();
- db.close();
- print("List<String> query 结束");
- return arrayList;
- }
-
- private void print(String msg){
- Log.d("DatabaseUtil",msg);
- }
- }
ItemRecord
任务记录类,方便数据库操作而建立的任务记录类
- public class ItemRecord {
-
- private String path;
- private int threadId;
- private int downloadLength;
-
- public ItemRecord(String path,int threadId,int downloadLength){
- this.path = path;
- this.threadId = threadId;
- this.downloadLength = downloadLength;
- }
-
- public String getPath() {
- return path;
- }
-
- public void setPath(String path) {
- this.path = path;
- }
-
- public int getThreadId() {
- return threadId;
- }
-
- public void setThreadId(int threadId) {
- this.threadId = threadId;
- }
-
- public int getDownloadLength() {
- return downloadLength;
- }
-
- public void setDownloadLength(int downloadLength) {
- this.downloadLength = downloadLength;
- }
-
-
- }
ApplicationUrlSpinner操作用来完成应用名称与下载地址的对应关系源码:
- public class ApplicationUrl {
- private String applicationName = "";
- private String applicationUrl = "";
-
- public String getApplicationName() {
- return applicationName;
- }
- public void setApplicationName(String applicationName) {
- this.applicationName = applicationName;
- }
- public String getApplicationUrl() {
- return applicationUrl;
- }
- public void setApplicationUrl(String applicationUrl) {
- this.applicationUrl = applicationUrl;
- }
-
- public String toString() {
-
- return applicationName;
- }
- }
CompleteListener下载完成判断接口:
- public interface CompleteListener {
- public void isComplete(int size);
- }
MainActivity
1:动态添加下载任务
2:根据任务动态添加布局,并给相应按钮添加监听,开启下载任务
源码:
- public class MaintActivity extends Activity{
-
- private LayoutInflater inflater;
- private LinearLayout root;
- private Spinner spinnerApp;
- private List<ApplicationUrl> appUrl;
- private ArrayAdapter appAdapter;
- private Button startButton;
- FileDownloader fileDownloader;
- private String url5 = "http://update.android.doplive.com.cn/dopoolv2.4.player.apk";
- private String url5App = "Dopool手机电视";
- private String url = "http://www.ipmsg.org.cn/downloads/ipmsg.apk";
- private String urlApp = "飞鸽传书";
- private String url2 = "http://down1.cnmo.com/app/a135/aiku_2.0.apk";
- private String url2App = "爱酷天气";
- private ApplicationUrl userChoose;
-
- @Override
- public void onCreate(Bundle savedInstanceState){
- super.onCreate(savedInstanceState);
- setContentView(R.layout.main_layout);
-
- inflater = (LayoutInflater)this.getSystemService(LAYOUT_INFLATER_SERVICE);
- root = (LinearLayout)findViewById(R.id.root);
-
- spinnerApp = (Spinner)findViewById(R.id.urlSpinner);
- startButton = (Button)findViewById(R.id.startBtn);
-
-
-
-
- init();
- }
-
- public void init(){
-
- appUrl = new ArrayList<ApplicationUrl>();
- ApplicationUrl[] tmp = new ApplicationUrl[3];
- for(int i = 0 ; i<3;i++){
- tmp = new ApplicationUrl();
- }
- tmp[0].setApplicationName(urlApp);
- tmp[0].setApplicationUrl(url);
- tmp[1].setApplicationName(url2App);
- tmp[1].setApplicationUrl(url2);
- tmp[2].setApplicationName(url5App);
- tmp[2].setApplicationUrl(url5);
- appUrl.add(tmp[0]);
- appUrl.add(tmp[1]);
- appUrl.add(tmp[2]);
-
- userChoose = new ApplicationUrl();
- userChoose.setApplicationName(tmp[0].getApplicationName());
- userChoose.setApplicationUrl(tmp[0].getApplicationUrl());
- appAdapter = new ArrayAdapter(MaintActivity.this,android.R.layout.simple_spinner_item,appUrl);
- appAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
- spinnerApp.setAdapter(appAdapter);
- spinnerApp.setSelection(0, true);
- spinnerApp.setOnItemSelectedListener(new OnItemSelectedListener(){
-
- @Override
- public void onItemSelected(AdapterView<?> arg0, View arg1,
- int position, long arg3) {
- print("onItemSelected:"+appUrl.get(position).getApplicationName()+Integer.toString(position));
- userChoose.setApplicationName(appUrl.get(position).getApplicationName());
- userChoose.setApplicationUrl(appUrl.get(position).getApplicationUrl());
- }
- @Override
- public void onNothingSelected(AdapterView<?> arg0) {
-
-
- }
- });
- startButton.setOnClickListener(new View.OnClickListener() {
- @Override
- public void onClick(View v) {
- if(!appUrl.isEmpty()){
- createDownloadTask(userChoose);
- }else{
- Toast.makeText(MaintActivity.this, "没有应用程序可下载!", Toast.LENGTH_LONG).show();
- }
- int count = appUrl.size();
- print(Integer.toString(count));
- for(int i = 0 ;i< count;i++){
- if(appUrl.get(i).getApplicationName().equals(userChoose.getApplicationName())){
- print("+++++++++++++++++"+Integer.toString(i)+"++++++++++++++++");
- printappUrl();
- appUrl.remove(i);
- printappUrl();
- count = appUrl.size();
- print("for循环找到"+Integer.toString(count));
- appAdapter.notifyDataSetChanged();
- if(!appUrl.isEmpty()){
- spinnerApp.setSelection(0,true);
- userChoose.setApplicationName(appUrl.get(0).getApplicationName());
- userChoose.setApplicationUrl(appUrl.get(0).getApplicationUrl());
-
- }
- printappUrl();
- print(userChoose.getApplicationName()+"数据源已更新"+Integer.toString(count));
- }else{
- print("数据源未更新");
- }
- }
- }
- });
-
- }
-
- @Override
- public boolean onCreateOptionsMenu(Menu menu) {
- getMenuInflater().inflate(R.menu.main_layout, menu);
- return true;
- }
-
-
- private void createDownloadTask(ApplicationUrl path){
- LayoutInflater inflater = (LayoutInflater)this.getSystemService(LAYOUT_INFLATER_SERVICE);
-
- LinearLayout linearLayout = (LinearLayout)inflater.inflate(R.layout.download_layout, null);
-
- LinearLayout childLayout = (LinearLayout)linearLayout.getChildAt(0);
- ProgressBar progressBar = (ProgressBar)childLayout.getChildAt(0);
- TextView textView = (TextView)childLayout.getChildAt(1);
-
- Button button = (Button)linearLayout.getChildAt(1);
- try{
- button.setOnClickListener(new MyListener(progressBar,textView,path));
- root.addView(linearLayout);
- }catch(Exception e){
- e.printStackTrace();
- }
- }
-
- private final class MyListener implements OnClickListener{
- private ProgressBar pb ;
- private TextView tv;
- private String url;
- private FileDownloader fileDownloader;
- private String name ;
-
- public MyListener(ProgressBar pb ,TextView tv, ApplicationUrl url){
- this.pb = pb;
- this.tv = tv;
- this.url = url.getApplicationUrl();
- this.name = url.getApplicationName();
- }
-
- public void onClick(View v) {
- final Button pauseButton = (Button)v;
- final Handler mainHandler = new Handler(){
-
- @Override
- public void handleMessage(Message msg) {
- switch(msg.what){
- case 2:
- pauseButton.setText("安装");
- break;
- }
- }
-
- };
- if(pauseButton.getText().equals("开始")){
- pauseButton.setText("暂停");
- new Thread(){
- public void run(){
- try{
- fileDownloader = new FileDownloader(url,handler,MaintActivity.this);
- fileDownloader.Download(new CompleteListener(){
- public void isComplete(int size) {
- Message msg = new Message();
- msg.what = 2;
- mainHandler.sendMessage(msg);
- }
- });
- }catch(Exception e){
- e.printStackTrace();
- }
- }
- }.start();
- }else if(pauseButton.getText().equals("暂停")){
- print("暂停");
- fileDownloader.setPause();
- pauseButton.setText("继续");
- }else if(pauseButton.getText().equals("继续")){
- print("继续");
- pauseButton.setText("暂停");
- new Thread(){
- public void run(){
- try{
- fileDownloader.setResume(new CompleteListener(){
- public void isComplete(int size) {
- print("妹纸");
- Message msg = new Message();
- msg.what = 2;
- mainHandler.sendMessage(msg);
- }
- });
- }catch(Exception e){
- e.printStackTrace();
- }
- }
- }.start();
- }else if(pauseButton.getText().equals("安装")){
- Intent intent = new Intent(Intent.ACTION_VIEW);
- intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
- String installPath = Environment.getExternalStorageDirectory() + "/"+url.substring(url.lastIndexOf('/')+1);
- print(installPath);
- intent.setDataAndType(Uri.fromFile(new File(installPath)), "application/vnd.android.package-archive");
- MaintActivity.this.startActivity(intent);
- }
- }
-
- private Handler handler = new Handler(){
- int fileLength = 1;
- @Override
- public void handleMessage(Message msg) {
- switch(msg.what){
- case 0:
- fileLength = msg.getData().getInt("filelength");
- pb.setMax(fileLength);
- break;
- case 1:
- int currentLength = msg.getData().getInt("currentlength");
- pb.setProgress(currentLength);
- tv.setText(name+" 已下载:"+currentLength*100 / fileLength+"%");
- if(currentLength == fileLength){
- tv.setText(name+" 下载完成:"+currentLength*100 / fileLength+"%");
- }
- break;
- default: print("handleMessage msg.what 没有曲子好");
- }
- }
-
- };
-
- }
-
-
-
-
-
- private void print(String msg){
- Log.d("MainActivity",msg);
- }
-
- private void printappUrl(){
- Log.d("=========================","=========================");
- for(int i = 0;appUrl!=null && i<appUrl.size();i++){
- print(appUrl.get(i).getApplicationName());
- }
- Log.d("=========================","=========================");
- }
- }
源码下载