如何修复 ‘android.os.NetworkOnMainThreadException’
技术背景
在 Android 开发中,从 Android 3.0(API 级别 11)及以上版本开始,系统不允许在主线程(也称为 UI 线程)中执行网络操作。这是因为网络操作通常是耗时的,如果在主线程中进行,会阻塞主线程,导致应用无响应(ANR,Application Not Responding),严重影响用户体验。当应用尝试在主线程执行网络操作时,就会抛出 android.os.NetworkOnMainThreadException
异常。
实现步骤
1. 使用 AsyncTask(已在 API 级别 30 中弃用)
AsyncTask
是 Android 提供的一个方便的类,用于在后台线程执行任务,并在主线程更新 UI。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| 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; } }
protected void onPostExecute(RSSFeed feed) { } }
|
在 MainActivity
的 onCreate
方法中执行任务:
1
| new RetrieveFeedTask().execute(urlToRssFeed);
|
2. 使用新线程
创建一个新的线程来执行网络操作。
1 2 3 4 5 6 7 8 9 10 11
| Thread thread = new Thread(new Runnable() { @Override public void run() { try { } catch (Exception e) { e.printStackTrace(); } } }); thread.start();
|
3. 使用 IntentService
IntentService
是一个异步的、会自动停止的服务,适合执行长时间运行的后台任务。
步骤 1:创建 IntentService
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| 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); 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) { reply.send(ERROR_CODE); } } catch (PendingIntent.CanceledException exc) { Log.i(TAG, "reply cancelled", exc); } } }
|
步骤 2:在清单文件中注册服务
1 2 3
| <service android:name=".DownloadIntentService" android:exported="false"/>
|
步骤 3:从 Activity 调用服务
1 2 3 4 5 6
| 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);
|
步骤 4:在 onActivityResult
中处理结果
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @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; } } super.onActivityResult(requestCode, resultCode, data); }
|
4. 使用 Retrofit
Retrofit
是一个优秀的网络请求库,用于处理 RESTful API。
步骤 1:添加依赖
在 build.gradle
中添加依赖:
1 2
| implementation 'com.squareup.retrofit2:retrofit:2.3.0' implementation 'com.squareup.retrofit2:converter-gson:2.3.0'
|
步骤 2:定义 API 接口
1 2 3 4 5 6 7
| public interface FinancesApi { @GET("stocks") Call<ResponseWrapper<String>> listStocks(); @GET("stocks/{symbol}") Call<Stock> getStock(@Path("symbol")String tickerSymbol); }
|
步骤 3:构建 API 实例
1 2 3 4 5 6 7 8 9
| public class FinancesApiBuilder { public static FinancesApi build(String baseUrl){ return new Retrofit.Builder() .baseUrl(baseUrl) .addConverterFactory(GsonConverterFactory.create()) .build() .create(FinancesApi.class); } }
|
步骤 4:调用 API
1 2 3 4 5 6 7 8 9 10 11 12
| FinancesApi api = FinancesApiBuilder.build("http://api.example.com/"); api.getStock("INTC").enqueue(new Callback<Stock>(){ @Override public void onResponse(Call<Stock> stockCall, Response<Stock> stockResponse){ Stock stock = stockCall.body(); } @Override public void onFailure(Call<Stock> stockCall, Throwable t){ } });
|
最佳实践
- 选择合适的方法:根据任务的复杂度和需求选择合适的方法。对于简单的短期任务,可以使用
AsyncTask
(如果在 API 级别 30 以下);对于长时间运行的任务,建议使用 IntentService
或 Retrofit
等库。 - 权限声明:在
AndroidManifest.xml
中添加网络权限:
1
| <uses-permission android:name="android.permission.INTERNET"/>
|
- 异常处理:在网络操作中,要对可能出现的异常进行捕获和处理,如
IOException
、MalformedURLException
等。
常见问题
1. 使用新线程时无法更新 UI
由于 Android 不允许在非主线程更新 UI,因此在新线程中执行网络操作后,如果需要更新 UI,可以使用 runOnUiThread
方法:
1 2 3 4 5 6
| runOnUiThread(new Runnable() { @Override public void run() { } });
|
2. AsyncTask 导致内存泄漏
AsyncTask
如果作为非静态内部类使用,会持有外部 Activity 的引用,可能导致内存泄漏。可以将 AsyncTask
定义为静态内部类,并使用弱引用持有 Activity。
3. 网络请求超时或失败
网络请求可能会因为网络不稳定、服务器问题等原因超时或失败。可以设置合理的超时时间,并在请求失败时进行重试或提示用户。例如,使用 OkHttp
可以设置超时时间:
1 2 3 4 5
| OkHttpClient client = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .build();
|