Android 下载二进制文件存在的问题

72

我在我的应用程序中从互联网下载二进制文件(视频)时遇到了问题。在 Quicktime 中,如果直接下载,则可以正常使用,但通过我的应用程序,某种方式会出现问题(尽管它们在文本编辑器中看起来完全相同)。以下是一个示例:

    URL u = new URL("http://www.path.to/a.mp4?video");
    HttpURLConnection c = (HttpURLConnection) u.openConnection();
    c.setRequestMethod("GET");
    c.setDoOutput(true);
    c.connect();
    FileOutputStream f = new FileOutputStream(new File(root,"Video.mp4"));


    InputStream in = c.getInputStream();

    byte[] buffer = new byte[1024];
    int len1 = 0;
    while ( (len1 = in.read(buffer)) > 0 ) {
         f.write(buffer);
    }
    f.close();
6个回答

93

我不知道是否只有这一个问题,但你在代码中遇到了一个经典的 Java 故障:你没有考虑到 read() 方法总是可以返回少于请求的字节数。因此,你的读取操作可能会少于 1024 字节,但是你的写入操作始终会写入正好 1024 字节,其中可能包括上一次循环迭代的字节。

修改后代码:

 while ( (len1 = in.read(buffer)) > 0 ) {
         f.write(buffer,0, len1);
 }

也许 Android 上的 3G 网络延迟更高或数据包更小会加剧这种影响?


4
多么愚蠢的错误啊,谢谢!当你没有认真阅读教程时就会发生这种事 :) - Isaac Waller
3
我想指出,> 0测试可能会过早结束阅读。文档说明在流的末尾返回-1。 - Clint
2
@Clint :没错,但是文档(自Java 5起)还说,除非'len'参数为0,否则不能返回0(如果没有字节可用,则返回-1;否则至少读取一个字节)。在Java 2中可以返回0。 - njzk2
缓冲包装器对于任何网络IO来说肯定是一个好主意,但它们并不能解决问题——它们仍然可以返回比请求的字节数少的情况。 - Ry4an Brase
1
你知道你是耶稣吗?你刚刚救了我脱离地狱 :) - Mehdi Haghgoo
显示剩余5条评论

29
new DefaultHttpClient().execute(new HttpGet("http://www.path.to/a.mp4?video"))
        .getEntity().writeTo(
                new FileOutputStream(new File(root,"Video.mp4")));

1
我也喜欢一行解决方案。但是在写入文件之前,您应该检查实体,否则即使下载出现问题,文件也会被创建。因此,下次您可能会尝试打开一个损坏的文件。 - thanhbinh84
如何动态地将下载的文件命名为与原始文件名相同? - Compaq LE2202x
使用该方法,您需要添加一些内容,例如获取包含文件名的Content-Disposition头。 - njzk2
太棒了!(+1) 这个真的很好用,而且下载的 PDF 文件从未出现过损坏的情况。 - Rat-a-tat-a-tat Ratatouille
@doreamon - 我该如何检查实体?使用它的contentLength方法吗?请协助。 - Rat-a-tat-a-tat Ratatouille
显示剩余2条评论

16

问题在于您对缓冲区的读取。如果输入流的每次读取不是1024的整数倍,您将复制错误的数据。使用以下代码:

byte[] buffer = new byte[1024];
int len1 = 0;
while ( (len1 = in.read(buffer)) != -1 ) {
  f.write(buffer,0, len1);
}

14
 public class download extends Activity {

     private static String fileName = "file.3gp";
     private static final String MY_URL = "Your download url goes here";

     @Override
     public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);

        try {
            URL url = new URL(MY_URL);
            HttpURLConnection c = (HttpURLConnection) url.openConnection();
            c.setRequestMethod("GET");
            c.setDoOutput(true);
            c.connect();

            String PATH = Environment.getExternalStorageDirectory()
                + "/download/";
            Log.d("Abhan", "PATH: " + PATH);
            File file = new File(PATH);
            if(!file.exists()) {
               file.mkdirs();
            }
            File outputFile = new File(file, fileName);
            FileOutputStream fos = new FileOutputStream(outputFile);
            InputStream is = c.getInputStream();
            byte[] buffer = new byte[1024];
            int len1 = 0;
            while ((len1 = is.read(buffer)) != -1) {
                fos.write(buffer, 0, len1);
            }
            fos.flush();
            fos.close();
            is.close();
        } catch (IOException e) {
            Log.e("Abhan", "Error: " + e);
        }
        Log.i("Abhan", "Check Your File.");
    } 
}

这个答案不可行。在主线程上的网络连接会抛出 android.os.NetworkOnMainThreadException 异常。 - JBirdVegas
@JBirdVegas 请勿在主线程上运行与网络相关的操作。请创建一个工作线程。 - user456118
使用AsyncTask doInBackground来执行try {}catch代码,并从中删除setDoOutput(true)。 - Zar E Ahmer

3

我根据之前在该线程上的反馈修复了代码。我使用eclipse和多个大文件进行了测试,一切正常。只需将其复制粘贴到您的环境中,并更改http路径和要下载文件的位置即可。

try {
    //this is the file you want to download from the remote server
    String path ="http://localhost:8080/somefile.zip";
    //this is the name of the local file you will create
    String targetFileName
        boolean eof = false;
    URL u = new URL(path);
    HttpURLConnection c = (HttpURLConnection) u.openConnection();
    c.setRequestMethod("GET");
    c.setDoOutput(true);
    c.connect();
    FileOutputStream f = new FileOutputStream(new File("c:\\junk\\"+targetFileName));
        InputStream in = c.getInputStream();
        byte[] buffer = new byte[1024];
        int len1 = 0;
        while ( (len1 = in.read(buffer)) > 0 ) {
        f.write(buffer,0, len1);
                 }
    f.close();
    } catch (MalformedURLException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    } catch (ProtocolException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    } catch (FileNotFoundException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
    } catch (IOException e) {
    // TODO Auto-generated catch block
    e.printStackTrace();
}

祝你好运,阿里雷扎·阿格哈莫哈迪


这样做,同一个文件会被下载吗?我的意思是,如果文件已经下载了,它会发出警报吗? - Loshi

2

只需使用Apache的复制方法(Apache Commons IO)- 使用Java的优点!

IOUtils.copy(is, os);

不要忘记在finally块中关闭流:

try{
      ...
} finally {
  IOUtils.closeQuietly(is);
  IOUtils.closeQuietly(os);
}

2
不要交叉使用流。 - Jarett Millard

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接