Android Threading - All You Need to Know

Android Threading - All You Need to Know
Android 提供了许多创建和管理线程的方法,而第三方库的存在使这变得更加容易。然而,有这么多的选择,选择正确的方法可能会相当混乱。在本文中,Toptal 自由职业者软件工程师 Eliran Goshen 讨论了 Android 开发中涉及线程的一些常见场景以及如何处理每个场景。
作者
Eliran 拥有扎实的计算机科学学士学位,已经用 Java 和 React Native 构建了许多专业的 Android 应用程序和 Android/iOS 应用程序。
每个 Android 开发人员,有时都需要处理其应用程序中的线程。
当应用程序在 Android 中启动时,它会创建第一个执行线程,称为“主”线程。主线程负责将事件分派给适当的用户界面小部件,并与来自 Android UI 工具包的组件进行通信。
为了让您的应用程序保持响应,必须避免使用主线程来执行任何可能最终导致其阻塞的操作。
网络操作和数据库调用,以及某些组件的加载,是应该在主线程中避免的常见操作示例。在主线程中调用它们时,它们是同步调用的,这意味着 UI 将保持完全无响应,直到操作完成。出于这个原因,它们通常在单独的线程中执行,从而避免在执行它们时阻塞 UI(即,它们与 UI 异步执行)。
Android 提供了许多创建和管理线程的方法,并且存在许多使线程管理更加愉快的第三方库。然而,手头有这么多不同的方法,选择正确的方法可能会很混乱。
在本文中,您将了解Android 开发中线程变得必不可少的一些常见场景,以及一些可以应用于这些场景的简单解决方案等等。
Android中的线程
在 Android 中,您可以将所有线程组件分为两个基本类别:
- **附加到活动/片段的线程:这些*线程***与活动/片段的生命周期相关联,并在活动/片段被销毁时终止。
- *未*附加到任何活动/片段的线程:这些线程可以在生成它们的活动/片段(如果有)的生命周期之后继续运行。
附加到 Activity/Fragment 的线程组件
异步任务
AsyncTask是最基本的Android线程组件。它使用简单,适用于基本场景。
示例用法:
public class ExampleActivity extends Activity {
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        
        new MyTask().execute(url);
    }
    private class MyTask extends AsyncTask<String, Void, String> {
        @Override
        protected String doInBackground(String... params) {
            String url = params[0];
            return doSomeWork(url);
        }
        @Override
        protected void onPostExecute(String result) {
            super.onPostExecute(result);
            // do something with result 
        }
    }
}
AsyncTask但是,如果您需要延迟任务在活动/片段的生命周期之外运行,那么它就不够用了。值得注意的是,即使是像屏幕旋转这样简单的事情也可能导致 Activity 被破坏。
LOADERS
装载机是上述问题的解决方案。Loaders 可以在 Activity 被销毁时自动停止,也可以在 Activity 重新创建后自行重启。
主要有两种类型的 LOADERS:AsyncTaskLoader和CursorLoader. CursorLoader您将在本文后面了解更多信息。
AsyncTaskLoader类似于AsyncTask,但稍微复杂一些。
示例用法:
public class ExampleActivity extends Activity{
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        getLoaderManager().initLoader(1, null, new MyLoaderCallbacks());
    }
    
    private class MyLoaderCallbacks implements LoaderManager.LoaderCallbacks {
        @Override
        public Loader onCreateLoader(int id, Bundle args) {
            return new MyLoader(ExampleActivity.this);
        }
        @Override
        public void onLoadFinished(Loader loader, Object data) {
        }
        @Override
        public void onLoaderReset(Loader loader) {
        }
    }
    private class MyLoader extends AsyncTaskLoader {
        public MyLoader(Context context) {
            super(context);
        }
        @Override
        public Object loadInBackground() {
            return someWorkToDo();
        }
        
    }
}
不附加到 Activity/Fragment 的线程组件
服务
Service是一个组件,可用于在没有任何 UI 的情况下执行长(或可能长)操作。
Service在其托管进程的主线程中运行;该服务不会创建自己的线程,也不会在单独的进程中运行,除非您另外指定。
示例用法:
public class ExampleService extends Service {
    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        doSomeLongProccesingWork();
        stopSelf();
        return START_NOT_STICKY;
    }
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }
}
使用Service,您stopSelf()有责任在其工作完成时通过调用或方法来停止它stopService()。
INTENTSERVICE
Like Service,IntentService在单独的线程上运行,并在完成工作后自动停止。
IntentService通常用于不需要附加到任何 UI 的短任务。
示例用法:
public class ExampleService extends IntentService {
    
    public ExampleService() {
        super("ExampleService");
    }
    @Override
    protected void onHandleIntent(Intent intent) {
        doSomeShortWork();
    }
}
Android 中的七种线程模式
用例 1:通过网络发出请求而不需要服务器的响应
有时您可能希望将 API 请求发送到服务器,而无需担心其响应。例如,您可能正在向应用程序的后端发送推送注册令牌。
由于这涉及通过网络发出请求,因此您应该从主线程以外的线程执行此操作。
选项 1:ASYNCTASK 或 LOADERS
您可以使用AsyncTask或加载程序进行调用,它会起作用。
但是,AsyncTask加载程序都依赖于活动的生命周期。这意味着您要么需要等待调用执行并尝试阻止用户离开活动,要么希望它在活动被销毁之前执行。
选项 2:服务
Service可能更适合此用例,因为它不附加到任何活动。因此,即使在活动被破坏后,它也能够继续进行网络调用。另外,由于不需要来自服务器的响应,因此这里的服务也不会受到限制。
但是,由于服务将开始在 UI 线程上运行,您仍然需要自己管理线程。您还需要确保在网络调用完成后停止服务。
这将需要比这样一个简单的动作所需要的更多的努力。
选项 3:INTENTSERVICE
在我看来,这将是最好的选择。
由于IntentService不附加到任何活动并且它在非 UI 线程上运行,因此它完美地满足了我们的需求。此外,IntentService它会自动停止,因此也无需手动管理它。
用例 2:进行网络调用,并从服务器获取响应
这个用例可能更常见一些。例如,您可能希望在后端调用 API 并使用其响应来填充屏幕上的字段。
选项 1:SERVICE 或 INTENTSERVICE
尽管 Service或IntentService在前面的用例中表现良好,但在这里使用它们并不是一个好主意。试图从 aService或 an中获取数据IntentService到主 UI 线程会使事情变得非常复杂。
选项 2:ASYNCTASK 或 LOADERS
乍一看,AsyncTask装载机似乎是这里显而易见的解决方案。它们易于使用——简单明了。
但是,当使用AsyncTaskor  LOADERS时,您会注意到需要编写一些样板代码。此外,错误处理成为这些组件的主要工作。即使是简单的网络调用,您也需要了解潜在的异常,捕捉它们并采取相应的行动。这迫使我们将响应包装在包含数据的自定义类中,并带有可能的错误信息,并且标志指示操作是否成功。
每次通话都需要做很多工作。幸运的是,现在有一个更好、更简单的解决方案可用:RxJava。
选项 3:RXJAVA
您可能听说过RxJava,这是由 Netflix 开发的库。这在 Java 中几乎是魔法。
RxAndroid让你在 Android 中使用 RxJava,让处理异步任务变得轻而易举。您可以在此处了解有关 Android 上 RxJava 的更多信息。
RxJava 提供了两个组件:Observer和Subscriber.
观察者是一个包含一些动作的组件。它执行该操作,如果成功则返回结果,如果失败则返回错误。
另一方面,subscriber是一个组件,它可以通过订阅从 observable 接收结果(或错误)。
使用 RxJava,你首先创建一个 observable:
Observable.create((ObservableOnSubscribe<Data>) e -> {
    Data data = mRestApi.getData();
    e.onNext(data);
})
创建 observable 后,您可以订阅它。
借助 RxAndroid 库,您可以控制要在 observable 中执行操作的线程,以及要在其中获取响应(即结果或错误)的线程。
您可以使用以下两个函数链接可观察对象:
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()
调度程序是在某个线程中执行操作的组件。AndroidSchedulers.mainThread()是与主线程关联的调度程序。
鉴于我们的 API 调用是mRestApi.getData()并且它返回一个Data对象,基本调用可能如下所示:
Observable.create((ObservableOnSubscribe<Data>) e -> {
            try { 
                        Data data = mRestApi.getData();
                        e.onNext(data);
            } catch (Exception ex) {
                        e.onError(ex);
            }
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(match -> Log.i(“rest api, "success"),
            throwable -> Log.e(“rest api, "error: %s" + throwable.getMessage()));
甚至不用讨论使用 RxJava 的其他好处,您已经可以看到 RxJava 如何通过抽象出线程的复杂性来让我们编写更成熟的代码。
用例 3:链接网络调用
对于需要按顺序执行的网络调用(即,每个操作取决于前一个操作的响应/结果),您需要特别小心生成意大利面条代码。
例如,您可能必须使用令牌进行 API 调用,您需要先通过另一个 API 调用获取该令牌。
选项 1:ASYNCTASK 或 LOADERS
使用AsyncTask或 LOADERS几乎肯定会导致意大利面条代码。整体功能将很难正确完成,并且在整个项目中需要大量冗余的样板代码。
选项 2:使用 FLATMAP 的 RXJAVA
在 RxJava 中,flatMap操作符从源 observable 中获取一个发出的值并返回另一个 observable。您可以创建一个可观察对象,然后使用第一个发出的值创建另一个可观察对象,这基本上会将它们链接起来。
**步骤 1.**创建获取令牌的 observable:
public Observable<String> getTokenObservable() {
    return Observable.create(subscriber -> {
        try {
            String token = mRestApi.getToken();
            subscriber.onNext(token);
        } catch (IOException e) {
            subscriber.onError(e);
        }
    });
}
**步骤 2.**创建使用令牌获取数据的 observable:
public Observable<String> getDataObservable(String token) {
    return Observable.create(subscriber -> {
        try {
            Data data = mRestApi.getData(token);
            subscriber.onNext(data);
        } catch (IOException e) {
            subscriber.onError(e);
        }
    });
}
**步骤 3.**将两个 observable 链接在一起并订阅:
getTokenObservable()
.flatMap(new Function<String, Observable<Data>>() {
    @Override
    public Observable<Data> apply(String token) throws Exception {
        return getDataObservable(token);
    }
})
.subscribe(data -> {
    doSomethingWithData(data)
}, error -> handleError(e));
请注意,这种方法的使用不仅限于网络调用;它可以处理任何需要按顺序运行但在单独线程上运行的操作集。
上面的所有用例都非常简单。线程之间的切换只发生在每个完成其任务之后。这种方法也可以支持更高级的场景——例如,两个或更多线程需要主动相互通信的情况。
用例 4:从另一个线程与 UI 线程通信
考虑一个场景,您希望上传文件并在完成后更新用户界面。
由于上传文件可能需要很长时间,因此无需让用户等待。您可以使用服务,并且可能使用IntentService来实现此处的功能。
然而,在这种情况下,更大的挑战是能够在文件上传(在单独的线程中执行)完成后调用 UI 线程上的方法。
选项 1:SERVICE 内的 RXJAVA
RxJava,无论是单独使用还是在IntentService. 订阅时需要使用基于回调的机制Observable,并且IntentService构建用于执行简单的同步调用,而不是回调。
另一方面,使用Service,您将需要手动停止服务,这需要更多工作。
选项 2:BROADCASTRECEIVER
Android 提供了这个组件,它可以监听全局事件(例如,电池事件、网络事件等)以及自定义事件。您可以使用此组件创建上传完成时触发的自定义事件。
为此,您需要创建一个扩展的自定义类,将BroadcastReceiver其注册到清单中,并使用Intent和IntentFilter创建自定义事件。要触发事件,您将需要该sendBroadcast方法。
显现:
<receiver android:name="UploadReceiver">
   
    <intent-filter>
        <action android:name="com.example.upload">
        </action>
    </intent-filter>
   
</receiver>
接收者:
public class UploadReceiver extends BroadcastReceiver {
    @Override
    public void onReceive(Context context, Intent intent) {
        if (intent.getBoolean(“success”, false) {
            Activity activity = (Activity)context;
            activity.updateUI();
    }
}
发件人:
Intent intent = new Intent();
intent.setAction("com.example.upload"); 
sendBroadcast(intent);
这种方法是一个可行的选择。但正如您所注意到的,它涉及一些工作,并且广播过多会减慢速度。
选项 3:使用 HANDLER
AHandler是一个组件,可以附加到线程,然后通过简单的消息或Runnable任务在该线程上执行一些操作。它与另一个组件一起工作,该组件Looper负责特定线程中的消息处理。
在Handler创建a时,它可以Looper在构造函数中获取一个对象,该对象指示handler附加到哪个线程。如果要使用附加到主线程的处理程序,则需要通过调用Looper.getMainLooper().
在这种情况下,要从后台线程更新 UI,您可以创建一个附加到 UI 线程的处理程序,然后将操作发布为Runnable:
Handler handler = new Handler(Looper.getMainLooper());
    handler.post(new Runnable() {
        @Override
        public void run() {
            // update the ui from here                
        }
    });
这种方法比第一种方法好很多,但是还有一种更简单的方法可以做到这一点……
选项 3:使用 EVENTBUS
EventBus,GreenRobot 的一个流行库,使组件能够安全地相互通信。由于我们的用例是我们只想更新 UI 的用例,因此这可能是最简单和最安全的选择。
**步骤 1.**创建一个事件类。例如,UIEvent。
**步骤 2.**订阅活动。
@Subscribe(threadMode = ThreadMode.MAIN)  
public void onUIEvent(UIEvent event) {/* Do something */};
 register and unregister eventbus : 
@Override
public void onStart() {
    super.onStart();
    EventBus.getDefault().register(this);
}
@Override
public void onStop() {
    super.onStop();
    EventBus.getDefault().unregister(this);
}
**步骤 3.**发布活动:EventBus.getDefault().post(new UIEvent());
使用ThreadMode注释中的参数,您可以指定要订阅此事件的线程。在我们的示例中,我们选择了主线程,因为我们希望事件的接收者能够更新 UI。
您可以UIEvent根据需要构建您的类以包含其他信息。
在服务中:
class UploadFileService extends IntentService {
    // …
    Boolean success = uploadFile(File file);
    EventBus.getDefault().post(new UIEvent(success));
    // ...
}
在活动/片段中:
@Subscribe(threadMode = ThreadMode.MAIN)  
public void onUIEvent(UIEvent event) {//show message according to the action success};
使用EventBus library,线程之间的通信变得更加简单。
用例 5:基于用户操作的线程之间的双向通信
假设您正在构建一个媒体播放器,并且您希望它能够在应用程序屏幕关闭时继续播放音乐。在这种情况下,您将希望 UI 能够与媒体线程通信(例如,播放、暂停和其他操作),并且还希望媒体线程根据某些事件(例如错误、缓冲状态)更新 UI , 等等)。
完整的媒体播放器示例超出了本文的范围。但是,您可以在此处和此处找到好的教程。
选项 1:使用 EVENTBUS
你可以EventBus在这里使用。但是,从 UI 线程发布事件并在服务中接收它通常是不安全的。这是因为您在发送消息时无法知道服务是否正在运行。
选项 2:使用 BOUNDSERVICE
ABoundService是Service绑定到活动/片段的。这意味着活动/片段始终知道服务是否正在运行,此外,它还可以访问服务的公共方法。
要实现它,您需要Binder在服务内部创建一个自定义并创建一个返回服务的方法。
public class MediaService extends Service {
    private final IBinder mBinder = new MediaBinder();
    public class MediaBinder extends Binder {
        MediaService getService() {
            // Return this instance of LocalService so clients can call public methods
            return MediaService.this;
        }
    }
    @Override
    public IBinder onBind(Intent intent) {
        return mBinder;
    }
}
要将活动绑定到服务,您需要实现ServiceConnection,即监控服务状态的类,并使用方法bindService进行绑定:
// in the activity
MediaService mService;
// flag indicates the bound status
boolean mBound;
@Override
    protected void onStart() {
        super.onStart();
        // Bind to LocalService
        Intent intent = new Intent(this, MediaService.class);
        bindService(intent, mConnection, Context.BIND_AUTO_CREATE);
    }
    private ServiceConnection mConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName className,
                IBinder service) {
            MediaBinder binder = (MediaBinder) service;
            mService = binder.getService();
            mBound = true;
        }
        @Override
        public void onServiceDisconnected(ComponentName arg0) {
            mBound = false;
        }
    };
您可以在此处找到完整的实现示例。
要在用户点击播放或暂停按钮时与服务通信,您可以绑定到服务,然后在服务上调用相关的公共方法。
当存在媒体事件并且您想将其传达回活动/片段时,您可以使用较早的技术之一(例如BroadcastReceiver、Handler或EventBus)。
用例 6:并行执行操作并获得结果
假设您正在构建一个旅游应用程序,并且您想在从多个来源(不同数据提供者)获取的地图上显示景点。由于并非所有来源都是可靠的,因此您可能希望忽略失败的来源并继续渲染地图。
为了使流程并行化,每个 API 调用都必须在不同的线程中进行。
选项 1:使用 RXJAVA
merge()在 RxJava 中,您可以使用or运算符将多个 observable 组合为一个concat()。然后,您可以订阅“合并”的 observable 并等待所有结果。
但是,这种方法不会按预期工作。如果一个 API 调用失败,合并后的 observable 将报告整体失败。
选项 2:使用本机 JAVA 组件
在ExecutorServiceJava 中创建固定(可配置)数量的线程并同时在它们上执行任务。该服务返回一个Future对象,该对象最终通过该invokeAll()方法返回所有结果。
您发送到的每个任务都ExecutorService应该包含在Callable接口中,该接口是用于创建可以抛出异常的任务的接口。
从 获得结果后invokeAll(),您可以检查每个结果并相应地继续。
例如,假设您有来自三个不同端点的三个景点类型,并且您想要进行三个并行调用:
ExecutorService pool = Executors.newFixedThreadPool(3);
    List<Callable<Object>> tasks = new ArrayList<>();
    tasks.add(new Callable<Object>() {
        @Override
        public Integer call() throws Exception {
            return mRest.getAttractionType1();
        }
    });
    // ...
    try {
        List<Future<Object>> results = pool.invokeAll(tasks);
        for (Future result : results) {
        try {
            Object response = result.get();
            if (response instance of AttractionType1... {}
            if (response instance of AttractionType2... {}
                ...
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
这样,您就可以并行运行所有操作。因此,您可以分别检查每个操作中的错误,并酌情忽略个别故障。
这种方法比使用 RxJava 更容易。它更简单、更短,并且不会因为一个异常而使所有操作失败。
用例 7:查询本地 SQLite 数据库
在处理本地 SQLite 数据库时,建议从后台线程使用数据库,因为数据库调用(尤其是大型数据库或复杂查询)可能很耗时,导致 UI 冻结。
查询 SQLite 数据时,您会得到一个Cursor对象,然后可以使用该对象获取实际数据。
Cursor cursor = getData();
String name = cursor.getString(<colum_number>);
选项 1:使用 RXJAVA
您可以使用 RxJava 从数据库中获取数据,就像我们从后端获取数据一样:
public Observable<Cursor> getLocalDataObservable() {
    return Observable.create(subscriber -> {
        Cursor cursor = mDbHandler.getData();
        subscriber.onNext(cursor);
    });
}
您可以使用返回的可观察对象getLocalDataObservable(),如下所示:
getLocalDataObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
 .subscribe(cursor -> String name = cursor.getString(0),
                   throwable -> Log.e(“db, "error: %s" + throwable.getMessage()));
虽然这肯定是一种好方法,但还有一种更好的方法,因为有一个组件就是为这种情况而构建的。
选项 2:使用 CURSORLOADER + CONTENTPROVIDER
Android 提供CursorLoader了一个原生组件,用于加载 SQLite 数据并管理相应的线程。它是一个Loader返回 a的 a Cursor,我们可以通过调用简单的方法来获取数据,例如getString(),getLong()等。
public class SimpleCursorLoader extends FragmentActivity implements
LoaderManager.LoaderCallbacks<Cursor> {
  public static final String TAG = SimpleCursorLoader.class.getSimpleName();
  private static final int LOADER_ID = 0x01;
  private TextView textView;
  @Override
  public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.simple_cursor_loader);
    textView = (TextView) findViewById(R.id.text_view);
    getSupportLoaderManager().initLoader(LOADER_ID, null, this);
  }
  public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
    return new CursorLoader(this,
      Uri.parse("content://com.github.browep.cursorloader.data")
      , new String[]{"col1"}, null, null, null);
    }
    public void onLoadFinished(Loader<Cursor> cursorLoader, Cursor cursor) {
      if (cursor != null && cursor.moveToFirst()) {
        String text =  textView.getText().toString();
        while (cursor.moveToNext()) {
          text += "<br />" + cursor.getString(1);
          cursor.moveToNext();
        }
        textView.setText(Html.fromHtml(text) );
      }
    }
    public void onLoaderReset(Loader<Cursor> cursorLoader) {
      
    }
}
CursorLoader与ContentProvider组件一起使用。该组件提供了大量实时数据库功能(例如,更改通知、触发器等),使开发人员能够更轻松地实现更好的用户体验。
Android 中的线程没有灵丹妙药的解决方案
Android 提供了许多处理和管理线程的方法,但它们都不是灵丹妙药。
根据您的用例选择正确的线程方法,可以使整个解决方案易于实施和理解。本机组件非常适合某些情况,但并非适用于所有情况。这同样适用于花哨的第三方解决方案。
我希望您在处理下一个 Android 项目时会发现这篇文章很有用。在下面的评论中与我们分享您在 Android 中使用线程的经验或上述解决方案运行良好的任何用例(或不运行)。



 A lonely cat  回复 @冒着泡泡的汽水儿
 A lonely cat  回复 @冒着泡泡的汽水儿 



























