AndroidのUIのシングルスレッドモデルについて
みなさん、こんにちは。
今回も当校の授業の中で生徒さんが混乱しがちな点を解説します。
javaのスレッドって覚えてます?
今回のお話はスレッドに関するお話です。
スレッドって…なに?
という方でもなんとなくわかるように解説していきます。
スレッドについて
スレッドは単体の処理の流れを表す単位です。
マルチスレッドはこのスレッドを複数使用し並列処理を可能にするものです。
例えば以下の状態を想像してください。
スレッド名 | 用途 |
---|---|
スレッド1 | ダウンロード処理 |
スレッド2 | 音楽再生 |
スレッド3 | PowerPoint編集 |
皆さんは資料をダウンロードしながら、音楽を流しながら、PowerPointで資料を作成しています。
ここでスレッドが1つしかないと、同時並行でなにか行うことができません。
さて、Threadがなんとく想像できたところで次に行きましょう。
Androidのスレッドについて
Androidでも上記同様に、音楽を流しながらメールを作成できます。
それ故マルチスレッドのように思えますが、Androidはシングルスレッドモデルを採用しています。
このシングルスレッド(単体のスレッド)をUIスレッドと呼びます。
時間のかかる処理は別スレッドで行う
UIスレッドはユーザーの操作に使用するスレッドです。
それでは以下の図の場合、どのような状態になるでしょう?
想像できますか?
ここではServiceを起動しバックグラウンドでダウンロード処理を実行しています。
しかしながらダウンロード処理自体をUIスレッド内にて実行しています。
するとどうなるでしょう?
この場合、Android端末はユーザーの操作を、15秒間まったく受け付けない状態です。
(なぜならUIスレッドは、バックグラウンドで実行されているダウンロード処理で使用しているからです。)
これはAndroid端末がフリーズしているかのような状態となり、ユーザーに迷惑をかけることになりますよね。
したがって、Androidでは端末にも依りますが、速いものだと5秒間ユーザーの操作を受け付けられない状態になった場合に、Androidはそのアプリがフリーズしていると捉え、強制終了させてしまいます。
したがって、次のようにServiceが起動したら別スレッドを立ち上げて、そのスレッド内で処理を行う必要があります。
これで強制終了の危険性はなくなり、正常にバックグラウンド処理を実施することができます。
別スレッドからUIスレッドを操作してはいけない
上記の状態でダウンロード処理が完了し、「ダウンロードが完了しました。」というToastを表示させたい場合、どのように表示させるのでしょう?
上図は誤りです。
Threadを複数立ち上げることは可能ですが、立ち上げたスレッドからUIを直接操作できません。
Toastはユーザーが見ている画面上に出現しますよね?
したがってこの状態でToastを表示させるということは、別スレッドからUIスレッドを直接操作するということになります。
上図では別スレッド上でToastを表示させようとしています。
それ故に強制終了してしまいます。
Handlerを用いて別スレッドからUIスレッドにタスク依頼
別スレッドからUIスレッドを直接操作することはできません。
そこでHandlerを使用します。
HandlerはUIスレッドにジョブを登録する際に使用します。
今回の場合、Handlerを用いて別スレッドからUIスレッドに「Toastを表示する」というジョブを登録すればOKということになります。
サンプルアプリで確認してみよう
それでは実際のプログラムで確認してみましょう。
細かい説明はプログラム内にコメントとして記述しています。
また、このアプリにProgressDialogを併用したものは、こちらの記事に掲載しています。
- activity_main.xml
- MainActivity.java
- SubService.java
- AndroidManifest.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | android:layout_width = "match_parent" android:layout_height = "match_parent" android:padding = "15dp" android:orientation = "vertical" tools:context = ".MainActivity" > < TextView android:id = "@+id/textView1" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:text = "ボタンを押してネ。" /> < Button android:id = "@+id/button1" android:layout_width = "wrap_content" android:layout_height = "wrap_content" android:text = "Service起動" /> </ LinearLayout > |
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 | package com.example.serviceexample; import android.app.Activity; import android.content.Intent; import android.os.Bundle; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; public class MainActivity extends Activity { @Override protected void onCreate(Bundle savedInstanceState) { super .onCreate(savedInstanceState); setContentView(R.layout.activity_main); Button button = (Button) findViewById(R.id.button1); button.setOnClickListener( new OnClickListener() { @Override public void onClick(View v) { Intent intent = new Intent(MainActivity. this , SubService. class ); startService(intent); } }); } } |
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 52 53 | package com.example.serviceexample; import android.app.Service; import android.content.Intent; import android.os.Handler; import android.os.IBinder; import android.widget.Toast; public class SubService extends Service { // Handlerオブジェクトを生成 private Handler handler = new Handler(); // このメソッドは抽象メソッドの為実装必須(今回は使用しないのでnullを返す。) @Override public IBinder onBind(Intent intent) { return null ; } // Serviceの実行直前に呼ばれる @Override public int onStartCommand(Intent intent, int flags, int startId) { // 別スレッドを作成 new Thread( new Runnable() { // 別スレッドをスタート @Override public void run() { try { //スレッドを10秒間停止 Thread.sleep( 10000 ); } catch (InterruptedException e) { e.printStackTrace(); } // 登録するジョブをRunableオブジェクトとして生成 Runnable runnable = new Runnable() { @Override public void run() { Toast.makeText(SubService. this , "ダウンロードが完了しました。" , Toast.LENGTH_SHORT).show(); } }; // ジョブを登録 handler.post(runnable); } }).start(); return super .onStartCommand(intent, flags, startId); } } |
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 | <? xml version = "1.0" encoding = "utf-8" ?> package = "com.example.serviceexample" android:versionCode = "1" android:versionName = "1.0" > < uses-sdk android:minSdkVersion = "14" android:targetSdkVersion = "18" /> < application android:allowBackup = "true" android:icon = "@drawable/ic_launcher" android:label = "@string/app_name" android:theme = "@style/AppTheme" > < activity android:name = "com.example.serviceexample.MainActivity" android:label = "@string/app_name" > < intent-filter > < action android:name = "android.intent.action.MAIN" /> < category android:name = "android.intent.category.LAUNCHER" /> </ intent-filter > </ activity > < service android:name = ".SubService" ></ service > </ application > </ manifest > |