Всеки разработчик на Android, в един или друг момент, трябва да се справя с нишки в своето приложение.
Когато приложението се стартира в Android, то създава първата нишка на изпълнение, известна като „основната“ нишка. Основната нишка е отговорна за изпращане на събития към подходящите приспособления на потребителския интерфейс, както и комуникация с компоненти от инструментариума на потребителския интерфейс на Android.
За да поддържате приложението си отзивчиво, от съществено значение е да избягвате използването на основната нишка за извършване на всяка операция, която може да доведе до блокиране.
Мрежовите операции и обажданията към база данни, както и зареждането на определени компоненти, са често срещани примери за операции, които човек трябва да избягва в основната нишка. Когато се извикат в основната нишка, те се извикват синхронно, което означава, че потребителският интерфейс ще остане напълно неотговарящ, докато операцията завърши. Поради тази причина те обикновено се изпълняват в отделни нишки, което по този начин избягва блокирането на потребителския интерфейс, докато се изпълнява (т.е. те се изпълняват асинхронно от потребителския интерфейс).
Android предоставя много начини за създаване и управление на нишки и съществуват много библиотеки на трети страни, които правят управлението на нишките много по-приятно. Въпреки това, с толкова много различни подходи под ръка, изборът на правилния може да бъде доста объркващ.
В тази статия ще научите за някои често срещани сценарии в Разработка на Android където резбата става от съществено значение и някои прости решения, които могат да бъдат приложени към тези сценарии и не само.
В Android можете да категоризирате всички компоненти за резби в две основни категории:
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 { @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
, обаче се проваля, ако имате нужда от вашата отложена задача да продължи след живота на активността / фрагмента. Струва си да се отбележи, че дори нещо толкова просто като въртенето на екрана може да доведе до унищожаване на активността.
Товарачите са решението на споменатия по-горе проблем. Зареждащите устройства могат автоматично да спрат, когато активността бъде унищожена, а също така могат да се рестартират след пресъздаването на дейността.
Има основно два вида товарачи: 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(); } } }
Service
е компонент, който е полезен за извършване на дълги (или потенциално дълги) операции без никакъв потребителски интерфейс.
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()
метод.
Като Service
, IntentService
работи на отделна нишка и се спира автоматично, след като завърши работата си.
IntentService
обикновено се използва за кратки задачи, които не е необходимо да се прикачват към който и да е потребителски интерфейс.
Примерно използване:
public class ExampleService extends IntentService { public ExampleService() { super('ExampleService'); } @Override protected void onHandleIntent(Intent intent) { doSomeShortWork(); } }
Понякога може да искате да изпратите заявка за API до сървър, без да се притеснявате за отговора му. Например може да изпращате маркер за push регистрация до задния край на приложението си.
Тъй като това включва отправяне на заявка по мрежата, трябва да го направите от нишка, различна от основната нишка.
Можете да използвате AsyncTask
или товарачи за осъществяване на обаждането и ще работи.
Въпреки това, AsyncTask
и товарачите са зависими от жизнения цикъл на дейността. Това означава, че или ще трябва да изчакате повикването да се изпълни и да се опитате да попречите на потребителя да напусне активността, или да се надявате, че ще се изпълни, преди активността да бъде унищожена.
Service
може да е по-подходящ за този случай на употреба, тъй като не е свързан с никаква дейност. Следователно той ще може да продължи с мрежовото повикване дори след като активността бъде унищожена. Освен това, тъй като отговорът от сървъра не е необходим, и тук услугата не би била ограничаваща.
Тъй като обаче услугата ще започне да се изпълнява на нишката на потребителския интерфейс, пак ще трябва да управлявате себе си. Също така ще трябва да се уверите, че услугата е спряна, след като мрежовото повикване завърши.
Това би изисквало повече усилия, отколкото би било необходимо за такова просто действие.
Това според мен би било най-добрият вариант.
Тъй като IntentService
не се привързва към никаква дейност и работи на нишка, която не е с потребителски интерфейс, тук перфектно обслужва нуждите ни. Освен това, IntentService
спира се автоматично, така че няма нужда и ръчно да го управлявате.
Този случай на употреба вероятно е малко по-често срещан. Например, може да искате да извикате API в задния край и да използвате отговора му за попълване на полета на екрана.
Въпреки че Service
или IntentService
се справи добре за предишния случай на употреба, използването им тук не би било добра идея. Опит за извличане на данни от Service
или IntentService
в основната нишка на потребителския интерфейс би направило нещата много сложни.
Първо руж, AsyncTask
или товарачите изглежда са очевидното решение тук. Те са лесни за използване - прости и ясни.
Въпреки това, когато използвате AsyncTask
или товарачи, ще забележите, че е необходимо да напишете някакъв код за шаблон. Освен това обработката на грешки се превръща в основна работа с тези компоненти. Дори и с обикновено обаждане в мрежа, трябва да сте наясно с потенциалните изключения, да ги хванете и да действате по съответния начин. Това ни принуждава да обгърнем отговора си в персонализиран клас, съдържащ данните, с възможна информация за грешка, а флаг показва дали действието е било успешно или не.
Това е доста работа за всяко едно обаждане. За щастие сега има много по-добро и опростено решение: RxJava.
Може да сте чували за RxJava , библиотеката, разработена от Netflix. Това е почти магия в Java.
RxAndroid ви позволява да използвате RxJava в Android и прави работата с асинхронни задачи бриз. Можете да научите повече за RxJava на Android тук .
RxJava предоставя два компонента: Observer
и Subscriber
.
An наблюдател е компонент, който съдържа някакво действие. Той изпълнява това действие и връща резултата, ако успее, или грешка, ако не успее.
ДА СЕ абонат , от друга страна, е компонент, който може да получи резултата (или грешката) от наблюдаем, като се абонира за него.
С RxJava първо създавате наблюдаемо:
Observable.create((ObservableOnSubscribe) e -> { Data data = mRestApi.getData(); e.onNext(data); })
След като наблюдаваното е създадено, можете да се абонирате за него.
С библиотеката RxAndroid можете да контролирате нишката, в която искате да изпълните действието в наблюдаемия, и нишката, в която искате да получите отговора (т.е. резултата или грешката).
Вие обвързвате наблюдаемото с тези две функции:
.subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()
Планировъците са компоненти, които изпълняват действието в определена нишка. AndroidSchedulers.mainThread()
е планировчикът, свързан с основната нишка.
Като се има предвид, че нашето API извикване е mRestApi.getData()
и връща a Data
обект, основното повикване може да изглежда така:
Observable.create((ObservableOnSubscribe) 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 ни позволява да напишем по-зрял код, като абстрахираме сложността на нишките.
За мрежови повиквания, които трябва да се извършват последователно (т.е. когато всяка операция зависи от отговора / резултата от предишната операция), трябва да бъдете особено внимателни при генерирането на код за спагети.
Например може да се наложи да осъществите API повикване с маркер, който трябва да изтеглите първо чрез друго API повикване.
Използване на AsyncTask
или товарачите почти определено ще доведат до код за спагети. Цялостната функционалност ще бъде трудно да се получи правилно и ще изисква огромно количество излишен код на шаблона по време на вашия проект.
В RxJava, flatMap
оператор взема излъчена стойност от източника на наблюдаемо и връща друго наблюдаемо. Можете да създадете наблюдаемо и след това да създадете друго наблюдаемо, като използвате излъчената стойност от първата, която основно ще ги веригира.
Етап 1. Създайте наблюдаемото, което извлича маркера:
public Observable getTokenObservable() { return Observable.create(subscriber -> { try { String token = mRestApi.getToken(); subscriber.onNext(token); } catch (IOException e) { subscriber.onError(e); } }); }
Стъпка 2. Създайте наблюдаемото, което получава данните с помощта на маркера:
public Observable getDataObservable(String token) { return Observable.create(subscriber -> { try { Data data = mRestApi.getData(token); subscriber.onNext(data); } catch (IOException e) { subscriber.onError(e); } }); }
Стъпка 3. Свържете двете наблюдаеми заедно и се абонирайте:
getTokenObservable() .flatMap(new Function() { @Override public Observable apply(String token) throws Exception { return getDataObservable(token); } }) .subscribe(data -> { doSomethingWithData(data) }, error -> handleError(e));
Имайте предвид, че използването на този подход не се ограничава до мрежови разговори; той може да работи с всякакъв набор от действия, които трябва да се изпълняват в последователност, но на отделни нишки.
Всички случаи на употреба по-горе са доста прости. Превключването между нишките се случва само след като всеки завърши своята задача. По-напреднали сценарии - например, когато две или повече нишки трябва активно да комуникират помежду си - могат да бъдат подкрепени и от този подход.
Помислете за сценарий, при който искате да качите файл и да актуализирате потребителския интерфейс, след като завърши.
Тъй като качването на файл може да отнеме много време, не е необходимо потребителят да чака. Можете да използвате услуга и вероятно IntentService
за внедряване на функционалността тук.
В този случай обаче по-голямото предизвикателство е възможността да се извика метод в потребителския интерфейс след като качването на файла (което е извършено в отделна нишка) приключи.
RxJava, самостоятелно или в IntentService
, може да не е идеален. Ще трябва да използвате механизъм, базиран на обратно повикване, когато се абонирате за Observable
и IntentService
е създаден да прави прости синхронни обаждания, а не обратно извикване.
От друга страна, с Service
ще трябва да спрете ръчно услугата, което изисква повече работа.
Android предоставя този компонент, който може да слуша глобални събития (например събития от батерията, мрежови събития и т.н.), както и персонализирани събития. Можете да използвате този компонент, за да създадете персонализирано събитие, което се задейства, когато качването приключи.
За да направите това, трябва да създадете персонализиран клас, който се разширява BroadcastReceiver
, да го регистрирате в манифеста и да използвате Intent
и IntentFilter
за да създадете персонализирано събитие. За да задействате събитието, ще ви трябва sendBroadcast
метод.
Манифест:
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);
Подател:
Handler
Този подход е жизнеспособен вариант. Но както забелязахте, това включва известна работа и твърде много предавания могат да забавят нещата.
A Runnable
е компонент, който може да се прикачи към нишка и след това да се извърши някакво действие върху тази нишка чрез прости съобщения или Looper
задачи. Той работи заедно с друг компонент, Handler
, който отговаря за обработката на съобщения в определена нишка.
Когато a Looper
е създаден, той може да получи Looper.getMainLooper()
обект в конструктора, който показва към коя нишка е прикрепен манипулаторът. Ако искате да използвате манипулатор, прикрепен към основната нишка, трябва да използвате цикъла, свързан с основната нишка, като извикате Runnable
.
В този случай, за да актуализирате потребителския интерфейс от фонова нишка, можете да създадете манипулатор, прикрепен към нишката на потребителския интерфейс, и след това да публикувате действие като Handler handler = new Handler(Looper.getMainLooper()); handler.post(new Runnable() { @Override public void run() { // update the ui from here } });
:
EventBus
Този подход е много по-добър от първия, но има още по-опростен начин да направите това ...
UIEvent
, популярна библиотека от GreenRobot, позволява на компонентите безопасно да комуникират помежду си. Тъй като случаят ни на употреба е такъв, при който искаме само да актуализираме потребителския интерфейс, това може да бъде най-простият и безопасен избор.
Етап 1. Създайте клас на събитие. напр. @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); }
.
Стъпка 2. Абонирайте се за събитието.
EventBus.getDefault().post(new UIEvent());
Стъпка 3. Публикувайте събитието: ThreadMode
С 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
Използвайки EventBus
, комуникацията между нишките става много по-опростена.
Да предположим, че изграждате мултимедиен плейър и искате той да може да продължи да възпроизвежда музика дори когато екранът на приложението е затворен. В този сценарий ще искате потребителският интерфейс да може да комуникира с медийната нишка (например възпроизвеждане, пауза и други действия) и също така медийната нишка да актуализира потребителския интерфейс въз основа на определени събития (например грешка, състояние на буфериране и т.н.).
Пълен пример за медиен плейър е извън обхвата на тази статия. Можете обаче да намерите добри уроци тук и тук .
Можете да използвате BoundService
тук. Обикновено обаче е опасно да се публикува събитие от потребителския интерфейс и да се получи в услуга. Това е така, защото нямате начин да разберете дали услугата работи, когато сте изпратили съобщението.
A 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
или merge()
).
Да предположим, че създавате туристическо приложение и искате да покажете атракции на карта, извлечена от множество източници (различни доставчици на данни). Тъй като не всички източници може да са надеждни, може да искате да игнорирате неуспешните и да продължите да изобразявате картата така или иначе.
За да паралелизирате процеса, всяко API извикване трябва да се извършва в различна нишка.
В RxJava можете да комбинирате множество наблюдаеми в едно, използвайки concat()
или ExecutorService
оператори. След това можете да се абонирате за „обединеното“ наблюдаемо и да изчакате всички резултати.
Този подход обаче няма да работи според очакванията. Ако едно API повикване се провали, обединеният наблюдаем ще отчете общ неуспех.
Future
в Java създава фиксиран (конфигурируем) брой нишки и изпълнява едновременно задачи върху тях. Услугата връща a invokeAll()
обект, който в крайна сметка връща всички резултати чрез ExecutorService
метод.
Всяка задача, която изпращате на Callable
трябва да се съдържа в invokeAll()
интерфейс, който е интерфейс за създаване на задача, която може да създаде изключение.
След като получите резултатите от ExecutorService pool = Executors.newFixedThreadPool(3); List
, можете да проверите всеки резултат и да продължите съответно.
Да кажем например, че имате три вида привличане, идващи от три различни крайни точки и искате да направите три паралелни обаждания:
Cursor
По този начин вие изпълнявате всички действия успоредно. Следователно можете да проверите за грешки във всяко действие поотделно и да игнорирате отделни грешки, както е подходящо.
Този подход е по-лесен от използването на RxJava. Той е по-прост, по-кратък и не проваля всички действия поради едно изключение.
Когато се занимавате с локална база данни на SQLite, се препоръчва базата данни да се използва от фонова нишка, тъй като повикванията на базата данни (особено при големи бази данни или сложни заявки) могат да отнемат много време, което води до замразяване на потребителския интерфейс.
При заявка за данни на SQLite получавате Cursor cursor = getData(); String name = cursor.getString();
обект, който след това може да се използва за извличане на действителните данни.
public Observable getLocalDataObservable() { return Observable.create(subscriber -> { Cursor cursor = mDbHandler.getData(); subscriber.onNext(cursor); }); }
Можете да използвате RxJava и да получите данните от базата данни, точно както получаваме данни от нашия back-end:
getLocalDataObservable()
Можете да използвате наблюдаваното, върнато от getLocalDataObservable() .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(cursor -> String name = cursor.getString(0), throwable -> Log.e(“db, 'error: %s' + throwable.getMessage()));
както следва:
CursorLoader
Въпреки че това със сигурност е добър подход, има още по-добър, тъй като има компонент, който е създаден точно за този сценарий.
Android предоставя Loader
, естествен компонент за зареждане на данни на SQLite и управление на съответната нишка. Това е Cursor
което връща getString()
, което можем да използваме, за да получим данните, като извикаме прости методи като getLong()
, public class SimpleCursorLoader extends FragmentActivity implements LoaderManager.LoaderCallbacks { 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 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 cursorLoader, Cursor cursor) { if (cursor != null && cursor.moveToFirst()) { String text = textView.getText().toString(); while (cursor.moveToNext()) { text += '
и т.н.
' + cursor.getString(1); cursor.moveToNext(); } textView.setText(Html.fromHtml(text) ); } } public void onLoaderReset(Loader cursorLoader) { } }
CursorLoader
ContentProvider
работи с
|_+_|съставна част. Този компонент предоставя множество функции в базата данни в реално време (напр. Известия за промени, задействания и т.н.), които позволяват на разработчиците да внедрят по-лесно потребителско изживяване много по-лесно.
Android предоставя много начини за обработка и управление на нишки, но нито един от тях не е сребърни куршуми.
Изборът на правилния подход за резби, в зависимост от вашия случай на употреба, може да направи всичко различно в улесняването на цялостното решение за прилагане и разбиране. Родните компоненти се вписват добре за някои случаи, но не за всички. Същото важи и за изисканите решения на трети страни.
Надявам се тази статия да ви бъде полезна, когато работите по следващия си проект за Android. Споделете с нас своя опит с нишки в Android или всеки случай на употреба, при който горните решения работят добре - или не, в този смисъл - в коментарите по-долу.