我该如何修复'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个回答

24

如果在主线程上执行的任何重任务需要花费过多时间,则会发生此异常。

为了避免这种情况,我们可以使用线程执行器来处理。

Executors.newSingleThreadExecutor().submit(new Runnable() {
    @Override
    public void run() {
        // You can perform your task here.
    }
});

21
有许多很好的答案已经回答了这个问题,但是自那些答案发布以来,许多优秀的库已经出现。本文旨在作为新手指南。
我将涵盖执行网络操作的几种用例和每种用例的一两个解决方案。 REST over HTTP
通常使用JSON,但也可以是XML或其他格式。
完整API访问
假设您正在编写一个应用程序,让用户跟踪股票价格、利率和货币汇率。您找到了一个类似于以下内容的JSON API:
http://api.example.com/stocks                       // ResponseWrapper<String> object containing a
                                                    // list of strings with ticker symbols
http://api.example.com/stocks/$symbol               // Stock object
http://api.example.com/stocks/$symbol/prices        // PriceHistory<Stock> object
http://api.example.com/currencies                   // ResponseWrapper<String> object containing a
                                                    // list of currency abbreviation
http://api.example.com/currencies/$currency         // Currency object
http://api.example.com/currencies/$id1/values/$id2  // PriceHistory<Currency> object comparing the prices
                                                    // of the first currency (id1) to the second (id2)

Retrofit from Square

这是一个优秀的API选择,适用于具有多个端点的API,并允许您声明REST端点,而不必像其他库(如Amazon Ion JavaVolley(网站:Retrofit)一样单独编写代码。

如何将其与财务API一起使用?

文件build.gradle

将以下行添加到您的模块级别的build.gradle文件中:

implementation 'com.squareup.retrofit2:retrofit:2.3.0' // Retrofit library, current as of September 21, 2017
implementation 'com.squareup.retrofit2:converter-gson:2.3.0' // Gson serialization and deserialization support for retrofit, version must match retrofit version

文件 FinancesApi.java

public interface FinancesApi {
    @GET("stocks")
    Call<ResponseWrapper<String>> listStocks();
    @GET("stocks/{symbol}")
    Call<Stock> getStock(@Path("symbol")String tickerSymbol);
    @GET("stocks/{symbol}/prices")
    Call<PriceHistory<Stock>> getPriceHistory(@Path("symbol")String tickerSymbol);

    @GET("currencies")
    Call<ResponseWrapper<String>> listCurrencies();
    @GET("currencies/{symbol}")
    Call<Currency> getCurrency(@Path("symbol")String currencySymbol);
    @GET("currencies/{symbol}/values/{compare_symbol}")
    Call<PriceHistory<Currency>> getComparativeHistory(@Path("symbol")String currency, @Path("compare_symbol")String currencyToPriceAgainst);
}

FinancesApiBuilder,编号为


public class FinancesApiBuilder {
    public static FinancesApi build(String baseUrl){
        return new Retrofit.Builder()
                    .baseUrl(baseUrl)
                    .addConverterFactory(GsonConverterFactory.create())
                    .build()
                    .create(FinancesApi.class);
    }
}

FinancesFragment 代码片段

FinancesApi api = FinancesApiBuilder.build("http://api.example.com/"); //trailing '/' required for predictable behavior
api.getStock("INTC").enqueue(new Callback<Stock>(){
    @Override
    public void onResponse(Call<Stock> stockCall, Response<Stock> stockResponse){
        Stock stock = stockCall.body();
        // Do something with the stock
    }
    @Override
    public void onResponse(Call<Stock> stockCall, Throwable t){
        // Something bad happened
    }
}

如果您的API需要API密钥或其他标头(例如用户令牌等)发送,Retrofit可以轻松实现此目的(有关详细信息,请参见这个很棒的答案在Retrofit中添加标头参数)。
一次性REST API访问
假设您正在构建一个“心情天气”应用程序,该应用程序查找用户的GPS位置并检查该地区的当前温度,并告诉他们心情。这种类型的应用程序不需要声明API端点;它只需要能够访问一个API端点。
Ion
这是一种非常适合这种访问的库。
请阅读msysmilu的好答案如何修复“android.os.NetworkOnMainThreadException”?
通过HTTP加载图像
Volley 虽然Volley也可用于REST API,但由于需要更复杂的设置,我更喜欢使用如上所述的Retrofit

假设您正在构建一款社交网络应用,并希望加载朋友的个人资料图片。

build.gradle文件

将以下行添加到您的模块级别的build.gradle文件中:

implementation 'com.android.volley:volley:1.0.0'

文件 ImageFetch.java

Volley需要比Retrofit更多的设置。您需要创建一个类来设置RequestQueue、ImageLoader和ImageCache,但这并不太困难:

public class ImageFetch {
    private static ImageLoader imageLoader = null;
    private static RequestQueue imageQueue = null;

    public static ImageLoader getImageLoader(Context ctx){
        if(imageLoader == null){
            if(imageQueue == null){
                imageQueue = Volley.newRequestQueue(ctx.getApplicationContext());
            }
            imageLoader = new ImageLoader(imageQueue, new ImageLoader.ImageCache() {
                Map<String, Bitmap> cache = new HashMap<String, Bitmap>();
                @Override
                public Bitmap getBitmap(String url) {
                    return cache.get(url);
                }
                @Override
                public void putBitmap(String url, Bitmap bitmap) {
                    cache.put(url, bitmap);
                }
            });
        }
        return imageLoader;
    }
}

文件 user_view_dialog.xml

将以下内容添加到您的布局XML文件中以添加图像:

<com.android.volley.toolbox.NetworkImageView
    android:id="@+id/profile_picture"
    android:layout_width="32dp"
    android:layout_height="32dp"
    android:layout_alignParentTop="true"
    android:layout_centerHorizontal="true"
    app:srcCompat="@android:drawable/spinner_background"/>

文件 UserViewDialog.java

将以下代码添加到onCreate方法(Fragment,Activity)或构造函数(Dialog)中:

NetworkImageView profilePicture = view.findViewById(R.id.profile_picture);
profilePicture.setImageUrl("http://example.com/users/images/profile.jpg", ImageFetch.getImageLoader(getContext());

Picasso

Picasso是来自Square的另一个优秀库。请查看网站以获取一些很棒的示例。


16

简单来说,

不要在UI线程中进行网络操作

例如,如果你发起一个HTTP请求,那就是一个网络操作。

解决方案:

  1. 创建一个新的线程
  2. 或者使用AsyncTask类

方式:

将所有工作放入:

  1. 新线程的run()方法中
  2. 或者AsyncTask类的doInBackground()方法中。

但是:

当你从网络响应中获取一些内容并想在视图中显示它(比如在TextView中展示响应消息),你需要回到UI线程

如果不这样做,你将会得到ViewRootImpl$CalledFromWrongThreadException异常。

如何实现:

  1. 使用AsyncTask时,从onPostExecute()方法中更新视图
  • 或者调用runOnUiThread()方法,并在run()方法内更新视图。

  • 14
    您可以将代码的一部分移入另一个线程以卸载主线程,避免出现ANRNetworkOnMainThreadExceptionIllegalStateException(例如,无法在主线程访问数据库,因为这可能会锁定UI很长一段时间)。

    根据情况,有一些方法可供选择

    Java Thread 或 Android HandlerThread

    Java 线程仅可使用一次,并在执行其运行方法后终止。

    HandlerThread 是一个方便的类,用于启动具有 looper 的新线程。

    AsyncTask(在 API 级别 30 中已弃用

    AsyncTask 被设计为 Thread 和 Handler 周围的辅助类,不构成通用的线程框架。AsyncTasks 应该理想地用于短操作(最多几秒钟)。如果您需要使线程长时间运行,强烈建议您使用 java.util.concurrent 包提供的各种 API,例如 ExecutorThreadPoolExecutorFutureTask

    由于 main 线程垄断了 UI 组件,因此无法访问某些 View,这就是 Handler 出现的原因

    [Executor framework]

    ThreadPoolExecutor类实现了ExecutorService,可以对线程池进行精细控制(例如,核心池大小,最大池大小,保持活动时间等)。

    ScheduledThreadPoolExecutor是扩展了ThreadPoolExecutor的一个类。它可以在给定延迟后或定期安排任务。

    FutureTask

    FutureTask执行异步处理,但如果结果尚未准备好或处理尚未完成,则调用get()将阻塞线程。

    AsyncTaskLoader

    AsyncTaskLoader可以解决AsyncTask固有的许多问题。

    IntentService

    这是在Android上长时间运行处理的默认选择,一个很好的例子是上传或下载大文件。即使用户退出应用程序,上传和下载也可能会继续进行,您肯定不希望在进行这些任务时阻止用户使用应用程序。

    JobScheduler

    实际上,您必须创建一个服务,并使用JobInfo.Builder创建一个作业,该作业指定了运行服务的条件。

    RxJava

    通过使用可观察序列来组合异步和基于事件的程序的库。

    协程 (Kotlin)

    其主要要点是,它使异步代码看起来非常像同步代码。

    阅读更多内容,请点击这里这里这里以及这里


    14

    Kotlin

    如果您正在使用Kotlin,则可以使用协程

    fun doSomeNetworkStuff() {
        GlobalScope.launch(Dispatchers.IO) {
            // ...
        }
    }
    

    11

    已经解释了新的ThreadAsyncTask解决方案。

    AsyncTask应该用于短操作。在Android中,正常的Thread不可取。

    看看使用HandlerThreadHandler的备选方案。

    HandlerThread

    方便启动一个具有looper的新线程的类。然后可以使用looper来创建handler类。注意仍然必须调用start()

    Handler:

    处理程序允许您发送和处理与线程的消息队列相关联的消息和可运行对象。每个处理程序实例与单个线程及其消息队列相关联。当您创建一个新的处理程序时,它将绑定到创建它的线程/消息队列 -- 从那时起,它将向该消息队列传递消息和可运行对象,并在它们从消息队列中出来时执行它们。
    解决方案:
    1. 创建HandlerThread。 2. 在HandlerThread上调用start()。 3. 通过从HanlerThread获取Looper来创建Handler。 4. 将与网络操作相关的代码嵌入Runnable对象中。 5. 将Runnable任务提交给Handler。
    以下是一个示例代码片段,用于解决NetworkOnMainThreadException问题。
    HandlerThread handlerThread = new HandlerThread("URLConnection");
    handlerThread.start();
    handler mainHandler = new Handler(handlerThread.getLooper());
    
    Runnable myRunnable = new Runnable() {
        @Override
        public void run() {
            try {
                Log.d("Ravi", "Before IO call");
                URL page = new URL("http://www.google.com");
                StringBuffer text = new StringBuffer();
                HttpURLConnection conn = (HttpURLConnection) page.openConnection();
                conn.connect();
                InputStreamReader in = new InputStreamReader((InputStream) conn.getContent());
                BufferedReader buff = new BufferedReader(in);
                String line;
                while ( (line =  buff.readLine()) != null) {
                    text.append(line + "\n");
                }
                Log.d("Ravi", "After IO call");
                Log.d("Ravi",text.toString());
    
            } catch (Exception err) {
                err.printStackTrace();
            }
        }
    };
    mainHandler.post(myRunnable);
    

    使用这种方法的优点:
    1. 为每个网络操作创建一个新的Thread/AsyncTask是昂贵的。Thread/AsyncTask将在下一次网络操作时被销毁和重新创建。但是通过使用Handler和HandlerThread的方法,您可以将许多网络操作(作为Runnable任务)提交给单个HandlerThread,从而节省资源。

    10

    这个有效。我只是把Dr.Luiji的答案简化了一点。

    new Thread() {
        @Override
        public void run() {
            try {
                //Your code goes here
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }.start();
    

    10
    尽管有很多解决方案,但没有人提到 com.koushikdutta.ionhttps://github.com/koush/ion 它也是异步非常简单易用的:
    Ion.with(context)
    .load("http://example.com/thing.json")
    .asJsonObject()
    .setCallback(new FutureCallback<JsonObject>() {
       @Override
        public void onCompleted(Exception e, JsonObject result) {
            // do stuff with the result or error
        }
    });
    

    9
    主线程是UI线程,您不能在主线程中执行可能会阻塞用户交互的操作。您可以通过以下两种方式解决此问题:
    强制在主线程中执行任务,如下所示:
    StrictMode.ThreadPolicy threadPolicy = new StrictMode.ThreadPolicy.Builder().permitAll().build();
    StrictMode.setThreadPolicy(threadPolicy);
    

    如果需要的话,您可以创建一个简单的处理程序并更新主线程。

    Runnable runnable;
    Handler newHandler;
    
    newHandler = new Handler();
    runnable = new Runnable() {
        @Override
        public void run() {
             try {
                //update UI
            } catch (Exception e) {
                e.printStackTrace();
            } 
        }
    };
    newHandler.post(runnable);
    

    要停止线程,请使用以下方法:

    newHandler.removeCallbacks(runnable);
    

    欲了解更多信息,请访问:无痛线程处理


    9

    Android不允许在主线程上执行长时间操作。

    因此,只需使用不同的线程,并在需要时将结果发送到主线程即可。

    new Thread(new Runnable() {
            @Override
            public void run() {
                /*
                // Run operation here
                */
                // After getting the result
                runOnUiThread(new Runnable() {
                    @Override
                    public void run() {
                        // Post the result to the main thread
                    }
                });
            }
        }).start();
    

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