这个NetworkOnMainThreadException的原因是什么?

3

在您悬停关闭按钮之前,请完全阅读此问题。

前言

显然,此异常是由于在主线程上运行网络操作引起的。但是,从堆栈跟踪中可以看出,此错误源自AsyncTaskdoInBackground!此后我也没有转到主线程。

有趣的是,我无法在我的设备上复制此问题。对于绝大多数用户而言,这也不是一个经常出现的问题。因此,它不能是一般性的实现错误。

步骤

  1. 主线程
  2. 某些东西导致频道列表刷新(无论是手动还是初始加载)
  3. 调用我的API库(附录1)
  4. 后台线程
  5. AsyncTask调用API库以获取URL的正文(附录2)
  6. 我的缓存库检查是否已保存正文,它不会更改Thread
  7. API库调用必要的OkHttp方法来拉取URL的正文(附录3)

堆栈跟踪

Fatal Exception: java.lang.RuntimeException: An error occurred while executing doInBackground()
       at android.os.AsyncTask$3.done(AsyncTask.java:309)
       at java.util.concurrent.FutureTask.finishCompletion(FutureTask.java:354)
       at java.util.concurrent.FutureTask.setException(FutureTask.java:223)
       at java.util.concurrent.FutureTask.run(FutureTask.java:242)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
       at java.lang.Thread.run(Thread.java:818)
Caused by android.os.NetworkOnMainThreadException
       at android.os.StrictMode$AndroidBlockGuardPolicy.onNetwork(StrictMode.java:1273)
       at libcore.io.BlockGuardOs.recvfrom(BlockGuardOs.java:249)
       at libcore.io.IoBridge.recvfrom(IoBridge.java:549)
       at java.net.PlainSocketImpl.read(PlainSocketImpl.java:481)
       at java.net.PlainSocketImpl.access$000(PlainSocketImpl.java:37)
       at java.net.PlainSocketImpl$PlainSocketInputStream.read(PlainSocketImpl.java:237)
       at okio.Okio$2.read(SourceFile:139)
       at okio.AsyncTimeout$2.read(SourceFile:211)
       at okio.RealBufferedSource.indexOf(SourceFile:306)
       at okio.RealBufferedSource.indexOf(SourceFile:300)
       at okio.RealBufferedSource.readUtf8LineStrict(SourceFile:196)
       at okhttp3.internal.http.Http1xStream.readResponse(SourceFile:184)
       at okhttp3.internal.http.Http1xStream.readResponseHeaders(SourceFile:125)
       at okhttp3.internal.http.HttpEngine.readNetworkResponse(SourceFile:723)
       at okhttp3.internal.http.HttpEngine.access$200(SourceFile:81)
       at okhttp3.internal.http.HttpEngine$NetworkInterceptorChain.proceed(SourceFile:708)
       at okhttp3.internal.http.HttpEngine.readResponse(SourceFile:563)
       at okhttp3.RealCall.getResponse(SourceFile:241)
       at okhttp3.RealCall$ApplicationInterceptorChain.proceed(SourceFile:198)
       at okhttp3.RealCall.getResponseWithInterceptorChain(SourceFile:160)
       at okhttp3.RealCall.execute(SourceFile:57)
       at com.mypackagename.app.http.Api.getBody(SourceFile:1472)
       at com.mypackagename.app.tasks.GetChannelListTask$2.renewCache(SourceFile:72)
       at com.mypackagename.app.utils.Cache.get(SourceFile:27)
       at com.mypackagename.app.tasks.GetChannelListTask.doInBackground(SourceFile:69)
       at com.mypackagename.app.tasks.GetChannelListTask.doInBackground(SourceFile:24)
       at android.os.AsyncTask$2.call(AsyncTask.java:295)
       at java.util.concurrent.FutureTask.run(FutureTask.java:237)
       at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1113)
       at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:588)
       at java.lang.Thread.run(Thread.java:818)

附录1

public void getChannelList(final IGetChannelListCallback callback) {
    try {
        GetChannelListTask task = new GetChannelListTask(mContext.get(), callback);
        AsyncTaskCompat.executeParallel(task);
    } catch (RejectedExecutionException e) {
        Crashlytics.logException(e);
    }
}

附录2

protected ArrayList<Channel> doInBackground(String... urls) {
    final ArrayList<Channel> channelList = new ArrayList<Channel>();

    // ...

    String json = Cache.get(context, GET_CHANNELS_CACHE, GET_CHANNELS_CACHE_TIME, new IBadCache() {
        public String renewCache() {
            return Api.getInstance(context).getBody(GET_CHANNELS_URL);
        }
    });

    // ...

    return channelList;
}

附录3

@WorkerThread
@NonNull
public String getBody(@NonNull final String url) {
    try {
        Request request = new Request.Builder()
                .url(logUrl(url))
                .build();

        Response response = mClient.newCall(request).execute();

        return response.body().string();
    } catch (IOException e) {
        Utils.log("API", "E::"+e.toString());
    } catch (SecurityException e) {
        reportNoInternetPermission();
    }

    return "";
}

更新1

检查线程的结果与预期相符:

refreshChannelList: MainThread: true
getChannelList: MainThread: true
onPreExecute: MainThread: true
doInBackground: MainThread: false
renewCache: MainThread: false
getBody: MainThread: false
onPostExecute: MainThread: true

doInBackground中切换到后台线程并继续执行OkHttp调用,在到达onPostExecute时返回到主线程。


1
你能否添加一些日志语句来查找哪些代码在哪个线程上运行?例如,在doInBackground、缓存回调、getBody等方法的开头和结尾处。你可以使用Thread.currentThread()来实现。 - nhaarman
getChannelList是从Activity内部调用的吗? - Ozzz
不确定,但是这一行 getBody(..) -> mClient.newCall(request).execute() 是否会导致在工作线程上执行而不是主线程上执行的原因。 - Bharatesh
@nhaarman:更新问题。期望的结果。OmriErEz:它是从'Fragment'调用的。具体地说,在'onResume'中,并在主线程上创建了一个'Handler'。skadoosh:这是我代码的一部分,导致了OkHttp。此部分绝对在后台线程上运行,而不是在主线程上运行。(它在'doInBackground'中运行,但我也进行了测试以确保) - Knossos
1个回答

0
嗯,看起来您的问题出现在doInbackground方法中执行某些操作时(使用上下文),如果您的上下文实例是Activity / Fragment等(从UI组件获得),那么使用您的上下文进行操作将使用MainThread执行。这就是为什么您可能会遇到此类异常。请尝试执行以下操作:

String json = Cache.get(context, GET_CHANNELS_CACHE, GET_CHANNELS_CACHE_TIME, new IBadCache() {
    public String renewCache() {
        return Api.getInstance(context).getBody(GET_CHANNELS_URL);
    }
}); 

关于

@Override
protected void onProgressUpdate(Object... values) {
  super.onProgressUpdate(values);

}

在 doInBackground 方法中使用 publishProgress(Object obj)。它必须有所帮助。


Context并不暗示代码运行在哪个线程中。更改传递到缓存方法(或Api)的Context不会影响它。为什么建议在onProgressUpdate中运行代码?那将保证缓存方法在主线程上运行,这与我此时需要的相反。 - Knossos
好的,抱歉我误解了。如果您想在高负载执行中使用上下文依赖操作,我可以看到两种解决方案。第一种是IntentService(其他线程)。另一个是AIDL(其他进程)。您知道这些吗?还是需要说明? - once2go
我可以将API移动到服务中,但现在这确实有些过度设计。这也不能解决根本问题。我不应该需要探索其他类型的框架,因为AsyncTask应该处理后台线程。 - Knossos
在“额外类型的框架”下,你是什么意思? - once2go
您IP地址为143.198.54.68,由于运营成本限制,当前对于免费用户的使用频率限制为每个IP每72小时10次对话,如需解除限制,请点击左下角设置图标按钮(手机用户先点击左上角菜单按钮)。 - Knossos
也许AIDL很难理解,但大多数高负载的Android服务都使用它,特别是对于大量数据交易(IO、网络等)。您可以将代码格式化为函数块,并与实体一起使用,而不是编写繁琐的代码。 - once2go

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