我该如何修复'android.os.NetworkOnMainThreadException'错误?

2718

在运行RssReader Android项目时,我遇到了一个错误。

代码:

URL url = new URL(urlToRssFeed);
SAXParserFactory factory = SAXParserFactory.newInstance();
SAXParser parser = factory.newSAXParser();
XMLReader xmlreader = parser.getXMLReader();
RssHandler theRSSHandler = new RssHandler();
xmlreader.setContentHandler(theRSSHandler);
InputSource is = new InputSource(url.openStream());
xmlreader.parse(is);
return theRSSHandler.getFeed();

这会显示以下错误:

android.os.NetworkOnMainThreadException

我应该如何解决这个问题?


139
请阅读这篇博客文章以获取有关NetworkOnMainThreadException的更多信息。该文章解释了为什么在Android 3.0及以上版本中会发生这种情况。 - Adrian Monk
6
首先,要了解Android中的网络请求,请阅读相关内容,然后我建议学习“Volley”库。 - Anuj Sharma
3
有许多替代的库可以解决这个问题。许多库列在此页面底部。如果您还知道其他库,我们也欢迎使用它们 :) - Snicolas
由于之前版本的Android中存在一个bug,系统没有将在主线程上写入TCP套接字标记为严格模式违规。Android 7.0修复了这个bug。表现出这种行为的应用程序现在会抛出android.os.NetworkOnMainThreadException异常。- 所以我们中的一些人最近才遇到这个问题! https://developer.android.com/about/versions/nougat/android-7.0-changes.html - Jay
66个回答

2674

注意:AsyncTask已在API级别30中被弃用。
AsyncTask | Android Developers

当应用程序试图在其主线程上执行网络操作时,将抛出此异常。请在 AsyncTask 中运行您的代码:

class RetrieveFeedTask extends AsyncTask<String, Void, RSSFeed> {

    private Exception exception;

    protected RSSFeed doInBackground(String... urls) {
        try {
            URL url = new URL(urls[0]);
            SAXParserFactory factory = SAXParserFactory.newInstance();
            SAXParser parser = factory.newSAXParser();
            XMLReader xmlreader = parser.getXMLReader();
            RssHandler theRSSHandler = new RssHandler();
            xmlreader.setContentHandler(theRSSHandler);
            InputSource is = new InputSource(url.openStream());
            xmlreader.parse(is);

            return theRSSHandler.getFeed();
        } catch (Exception e) {
            this.exception = e;

            return null;
        } finally {
            is.close();
        }
    }

    protected void onPostExecute(RSSFeed feed) {
        // TODO: check this.exception
        // TODO: do something with the feed
    }
}

如何执行任务:

MainActivity.java文件中的oncreate()方法中,您可以添加此行代码。

new RetrieveFeedTask().execute(urlToRssFeed);
不要忘记将以下内容添加到 AndroidManifest.xml 文件中:
<uses-permission android:name="android.permission.INTERNET"/>

所以在主线程上运行网络操作只会在Android中出现问题,而不会在标准Java代码(用Java编写但不是为Android应用程序编写的代码)中出现问题吗? - y_159
23
由于AsyncTask已被弃用,什么是最新的解决方案? - Erel Segal-Halevi
@ErelSegal-Halevi 使用线程。 - Harshil
@Harshil ... 那么怎么做呢? - DimeCadmium

772

几乎总是应该在线程或异步任务中运行网络操作。

但如果你愿意接受后果,你可以取消这个限制并覆盖默认行为。

补充:

StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build();

StrictMode.setThreadPolicy(policy);

在你的类里,

并且

添加这个权限到 Android manifest.xml 文件中:

<uses-permission android:name="android.permission.INTERNET"/>

后果:

在网络连接不稳定的区域,你的应用程序会变得无响应并锁死,用户会感知到缓慢并被迫强制终止,同时你也面临着活动管理器杀死你的应用程序并告诉用户应用已停止的风险。

Android对于设计响应性良好的编程实践有一些好的建议: NetworkOnMainThreadException | Android Developers


1
哇,谢谢你的解释,我现在明白了。我看到一个应用程序,在它的Java类中实现了ThreadPolicy,我有点困惑它在做什么。当网络不好时,我看到了你所说的后果。 - MosesK

521
我使用了一个新的Thread来解决这个问题。
Thread thread = new Thread(new Runnable() {
    
    @Override
    public void run() {
        try {
            // Your code goes here
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
});

thread.start(); 

        

你会如何传递参数给它? - Isiah
如果您需要在请求后访问UI,则需要按照此处所述返回主线程。 - Donald Duck
简单易用,非常感谢 :) - Mang Jojot

195

接受的答案有一些显著的缺点。不建议使用AsyncTask进行网络编程,除非你真的知道你在做什么。其中一些缺点包括:

  • 作为非静态内部类创建的AsyncTask隐含引用了封闭Activity对象、它的上下文以及该Activity创建的整个View层次结构。此引用会阻止Activity被垃圾回收直到AsyncTask的后台工作完成。如果用户的连接速度很慢和/或下载很大,这些短期内存泄漏可能会成为问题 - 例如,如果方向改变了几次(而且你没有取消执行任务),或者用户从活动中导航离开。
  • AsyncTask在不同平台上具有不同的执行特性:在API级别4之前,AsyncTask在单个后台线程上串行执行;从API级别4到API级别10,AsyncTask在多达128个线程的池中执行;从API级别11开始,AsyncTask在单个后台线程上串行执行(除非您使用重载的executeOnExecutor方法并提供另一个执行程序)。当ICS上串行运行良好的代码在Gingerbread上同时执行时,可能会出现问题,比如无意中有顺序执行依赖。

如果你想避免短期内存泄漏,在所有平台上具有明确定义的执行特性,并且有一个构建真正健壮的网络处理的基础,你可能会考虑:

  1. 使用一个很好为你做这个工作的库 - 在这个问题中有关于网络库的很好比较,或者
  2. 改用ServiceIntentService,也许使用PendingIntent通过Activity的onActivityResult方法返回结果。

IntentService方法

缺点:

  • AsyncTask需要更多的代码和复杂性,但不像你想象的那么多
  • 将请求排队并在一个单独的后台线程上运行。您可以通过使用等效的Service实现来替换IntentService来轻松控制它,例如这个
  • 呃,我现在想不出其他的了

优点:

  • 避免短期内存泄漏问题
  • 如果您的活动在网络操作进行中重新启动,仍然可以通过其 onActivityResult 方法接收下载结果
  • 比 AsyncTask 更好的平台来构建和重用强大的网络代码。例如:如果您需要进行重要的上传,您可以在Activity中使用AsyncTask完成,但是如果用户上下文切换到接听电话,则系统可能在上传完成之前杀死应用程序。具有活动Service的应用程序被杀死的可能性较小。
  • 如果使用自己的并发版本的IntentService(如我上面链接的那个),则可以通过Executor控件并发级别。

实现摘要

您可以很容易地实现一个IntentService来在单个后台线程上执行下载。

步骤1:创建一个IntentService来执行下载。您可以通过Intent extras告诉它要下载什么,并传递一个PendingIntent以用于将结果返回给Activity:

import android.app.IntentService;
import android.app.PendingIntent;
import android.content.Intent;
import android.util.Log;

import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;

public class DownloadIntentService extends IntentService {

    private static final String TAG = DownloadIntentService.class.getSimpleName();

    public static final String PENDING_RESULT_EXTRA = "pending_result";
    public static final String URL_EXTRA = "url";
    public static final String RSS_RESULT_EXTRA = "url";

    public static final int RESULT_CODE = 0;
    public static final int INVALID_URL_CODE = 1;
    public static final int ERROR_CODE = 2;

    private IllustrativeRSSParser parser;

    public DownloadIntentService() {
        super(TAG);

        // make one and reuse, in the case where more than one intent is queued
        parser = new IllustrativeRSSParser();
    }

    @Override
    protected void onHandleIntent(Intent intent) {
        PendingIntent reply = intent.getParcelableExtra(PENDING_RESULT_EXTRA);
        InputStream in = null;
        try {
            try {
                URL url = new URL(intent.getStringExtra(URL_EXTRA));
                IllustrativeRSS rss = parser.parse(in = url.openStream());

                Intent result = new Intent();
                result.putExtra(RSS_RESULT_EXTRA, rss);

                reply.send(this, RESULT_CODE, result);
            } catch (MalformedURLException exc) {
                reply.send(INVALID_URL_CODE);
            } catch (Exception exc) {
                // could do better by treating the different sax/xml exceptions individually
                reply.send(ERROR_CODE);
            }
        } catch (PendingIntent.CanceledException exc) {
            Log.i(TAG, "reply cancelled", exc);
        }
    }
}

第二步:在清单文件中注册服务:

<service
        android:name=".DownloadIntentService"
        android:exported="false"/>

步骤3:从活动中调用服务,传递一个PendingResult对象,该对象将被服务用于返回结果:

PendingIntent pendingResult = createPendingResult(
    RSS_DOWNLOAD_REQUEST_CODE, new Intent(), 0);
Intent intent = new Intent(getApplicationContext(), DownloadIntentService.class);
intent.putExtra(DownloadIntentService.URL_EXTRA, URL);
intent.putExtra(DownloadIntentService.PENDING_RESULT_EXTRA, pendingResult);
startService(intent);

第四步:在onActivityResult中处理结果:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == RSS_DOWNLOAD_REQUEST_CODE) {
        switch (resultCode) {
            case DownloadIntentService.INVALID_URL_CODE:
                handleInvalidURL();
                break;
            case DownloadIntentService.ERROR_CODE:
                handleError(data);
                break;
            case DownloadIntentService.RESULT_CODE:
                handleRSS(data);
                break;
        }
        handleRSS(data);
    }
    super.onActivityResult(requestCode, resultCode, data);
}

这里有一个包含完整可工作的Android Studio/Gradle项目的GitHub仓库,请点此查看


152

Honeycomb上,不能在UI线程上执行网络I/O。从技术上讲,在早期版本的Android上是可能的,但这是一个非常糟糕的想法,因为它会导致您的应用程序停止响应,并可能导致操作系统将您的应用程序视为行为不端而终止。您需要运行后台进程或使用AsyncTask在后台线程上执行网络事务。

Android开发者网站上有一篇关于Painless Threading的文章,这是一个很好的介绍,并且它将为您提供比在此处实际提供的更深入的答案。


130

这个问题有两个解决方案:

  1. 不要在主 UI 线程中使用网络调用,使用异步任务来处理。

  2. 将以下代码写入 MainActivity 文件的 setContentView(R.layout.activity_main); 后面:

    if (android.os.Build.VERSION.SDK_INT > 9) { StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder().permitAll().build(); StrictMode.setThreadPolicy(policy); }

另外,在您的 Java 文件中导入以下语句。

import android.os.StrictMode;

14
遵循你的第二种解决方案是一个不好的做法。异步(Async)才是正确的做法。如果改变政策,就像是在隐藏问题! - Richi González

96
在另一个线程上执行网络操作。
例如:
new Thread(new Runnable() {
    @Override
    public void run() {
        // Do network action in this function
    }
}).start();

并将此添加到文件 AndroidManifest.xml 中:

<uses-permission android:name="android.permission.INTERNET"/>

这有什么不利之处吗?它与ExecutorService有何不同? - Sujith S Manjavana

83

8
值得强调的是,如果您使用服务,仍然需要创建一个单独的线程-服务回调在主线程上运行。另一方面,IntentService会在后台线程上运行其onHandleIntent方法。 - Stevie
不应该使用AsyncTask来进行长时间运行的操作!指南规定最多2到3秒。 - Dage

75

您可以使用以下代码禁用严格模式:

if (android.os.Build.VERSION.SDK_INT > 9) {
    StrictMode.ThreadPolicy policy = 
        new StrictMode.ThreadPolicy.Builder().permitAll().build();
    StrictMode.setThreadPolicy(policy);
}

不推荐使用:请使用AsyncTask接口。

两种方法的完整代码


2
是的,ANR错误会出现。这意味着应用程序在5秒内没有响应。 - Muhammad Mubashir
13
这是一个很糟糕的答案。你不应该改变主题的政策,而是要编写更好的代码:不要在主线程上执行网络操作! - shkschneider

64
网络操作不能在主线程上运行。您需要在子线程上运行所有基于网络的任务或实现AsyncTask
以下是如何在子线程中运行任务的方法:
new Thread(new Runnable() {
    @Override
    public void run() {
        try {
            // Your implementation goes here
        } 
        catch (Exception ex) {
            ex.printStackTrace();
        }
    }
}).start();

1
匿名Runnable并不是最好的方式,因为它隐式地引用了封闭类,并防止它在线程完成之前被GC回收!此外,该线程将以与主/美国线程相同的优先级运行,与生命周期方法和UI帧速率竞争! - Yousha Aleayoub
@YoushaAleayoub那么,要使用什么替代? - Sujith S Manjavana

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