Androidサービスに飛び込む

画像







Nazmul Idrisの記事「Deep Dive into Android Services」の翻訳。 著者の元の名前を残しましたが、これは「没入」ではなく「知人」である可能性が高いです。 このテキストは、初心者の開発者に役立つと思います。 記事は完全に補完します。 Androidサービスのドキュメント 。 この記事では、実行中のサービスおよびバインドされたサービスとの対話の機能について説明します。 記事のプラスは、Android Oでサービスを操作する際の変更を考慮に入れていることです。明確にするために、オリジナルと比較して小さな変更が追加されています。







はじめに



最新のAndroidアプリケーションのほとんどは、バックグラウンドでいくつかのタスクを実行します。 つまり、タスクはユーザーインターフェイススレッド(UIスレッド)ではなくバックグラウンドスレッドで実行されます。







アプリケーションの特定のActivity



Thread



(スレッド)またはExecutor



(スレッド制御ラッパー)を作成すると、予期しない結果が生じる可能性があります。 たとえば、画面の向きを簡単に変更すると、 Activity



が再作成され、古いActivity



アタッチされたスレッドには結果を返す場所がなくなります。







これを処理するには、 AsyncTask



使用できます。 しかし、アプリケーションがActivity



からだけでなく、通知または別のコンポーネントからもこのバックグラウンドスレッドを開始する必要がある場合はどうでしょうか。







この場合、サービスは適切なAndroidコンポーネントであり、スレッドのライフサイクルをそのライフサイクルに関連付けるため、失われることはありません。







サービスは、アプリケーションのメインスレッドで実行される目に見えるインターフェイスのないAndroidアプリケーションのコンポーネントです。 サービスはマニフェストで宣言する必要があります。 バックグラウンドスレッドでサービスを実行する必要がある場合は、自分で実装する必要があります。







背景前景という用語オーバーロードされており、以下に適用できます。







  1. Androidコンポーネントのライフサイクル
  2. 流れ


この記事では、デフォルトで、 バックグラウンドフォアグラウンドという用語はライフサイクルを指すと想定します。 ただし、スレッドに関しては、 バックグラウンドスレッドまたはフォアグラウンドスレッド について明示的に説明します







IntentService



と呼ばれるAndroidサービスのサブクラスがあり、 IntentService



にバックグラウンドスレッドでタスクを実行します。 ただし、この記事ではそのようなサービスについては説明しません。









Androidコンポーネントストリーム、サービス、ライフサイクル



一歩後退して、サービスが行うべきことのより一般的な図を見てみましょう。 Thread



Executor



などのバックグラウンドスレッドで実行されるコードは、実際にはAndroidコンポーネントのライフサイクルとは関係ありません。 Activity



について話している場合、ユーザーの操作に基づいた特定の開始点と停止点があります。 ただし、 Activity



これらの開始点と終了点は、必ずしもThread



またはExecutor



ライフサイクルに関連してActivity



わけでActivity



ません。







画像







以下は、このガントチャートの主要な時点の説明です。 これらのポイントの詳細(および説明)は、記事の残りの部分で提供されます。







onCreate()



サービスメソッドは、作成時に(開始またはバインドによってonCreate()



れます。







その後、しばらくすると、サービスはThread



またはExecutor



開始します。 Thread



完了すると、 stopSelf()



メソッドを呼び出すことができるように、サービスにstopSelf()



ます。 これはかなり一般的なサービス実装パターンです。







Thread



またはExecutor



記述するコードは、バックグラウンドスレッドの開始または停止についてサービスに通知する必要があります。









onDestroy()



サービスメソッドは、サービスにシャットダウンする時間を通知した場合にのみ、システムによって呼び出されます。 サービスは、 Thread



またはExecutor



コードで何が起こるかを知りません-これはあなたの責任の範囲です。 したがって、プログラマのタスクは、作業の開始と完了についてサービスに通知します。







サービスは、 runningtiedの 2つのタイプに分けられます。 さらに、サービスが実行中であり、バインドが許可されている場合があります。 各ケースを検討します。







  1. ランニングサービス
  2. 提携サービス
  3. バインドされたサービスと実行中のサービス




Android Oの変更



Android O(API 26)では、システムによるバックグラウンドサービスの規制に大きな変更がありました。 主な変更点の1つは、ホワイトリストにない実行中のサービス(ユーザーに作業が表示されるサービスはホワイトリストに配置されます。詳細については、 オフマニュアルを参照)またはユーザーに作業について明示的に通知せず、バックグラウンドで開始しないことですActivity



を閉じた後のスレッド。 つまり、実行中のサービスを添付する通知を作成する必要があります。 また、新しいstartForegroundService()



メソッドを使用してサービスを開始する必要がありstartService()



を使用しないでください)。 また、サービスを作成した後、実行中のサービスのstartForeground()



メソッドを呼び出して、ユーザーに表示される通知を表示するstartForeground()



5秒startForeground()



ます。 そうでない場合、システムはサービスを停止し、 ANRを表示します(「アプリケーションが応答しません」)。 これらのポイントについて、コード例を使用して以下に説明します。









実行中のサービス



実行中のサービスは、 Activity



またはサービスでstartService(Intent)



メソッドを呼び出した後に作業を開始します。 この場合、 Intent



は明示的でなければなりません。 つまり、開始するサービスのクラス名をIntent



明示的に指定する必要があります。 または、どのサービスが開始されているかについてあいまいさを許可することが重要な場合は、サービスにインテントフィルターを提供し、Intentからコンポーネント名を除外できますが、十分な曖昧性をなくすsetPackage()



を使用してインテントのパッケージをインストールする必要がありますターゲットサービス用。 以下は、明示的なIntent



を作成する例です。







 public class MyIntentBuilder{ public static MyIntentBuilder getInstance(Context context) { return new MyIntentBuilder(context); } public MyIntentBuilder(Context context) { this.mContext = context; } public MyIntentBuilder setMessage(String message) { this.mMessage = message; return this; } public MyIntentBuilder setCommand(@Command int command) { this.mCommandId = command; return this; } public Intent build() { Assert.assertNotNull("Context can not be null!", mContext); Intent intent = new Intent(mContext, MyTileService.class); if (mCommandId != Command.INVALID) { intent.putExtra(KEY_COMMAND, mCommandId); } if (mMessage != null) { intent.putExtra(KEY_MESSAGE, mMessage); } return intent; } }
      
      





サービスを開始するには、明示的な目的でstartService()



を呼び出す必要があります。 そうしないと、サービスは実行状態になりません。 したがって、最前線に行くことはできず、 stopSelf()



は実際には何もしません。







そのため、サービスを実行状態にしないと、通知に添付できません。 これらは、サービスを実行状態にする必要があるときに留意すべき非常に重要なことです。







サービスは数回開始できます。 起動するたびに、 onStartCommand()



呼び出されます。 いくつかのパラメーターは、明示的なIntent



とともにこのメソッドに渡されます。 サービスを複数回開始した場合でも、 onCreate()



は1回だけ呼び出されonCreate()



もちろん、サービスが以前にバインドされていなかった場合)。 シャットダウンするには、サービスはstopSelf()



呼び出す必要があります。 サービスが停止した後(停止したとき)、他にサービスが接続されていない場合、 onDestroy()



呼び出されます。 実行中のサービスにリソースを割り当てるときは、このことに留意してください。









意図



実行中のサービスを開始するには、 Intent



必要です。 サービスが開始されるAndroidコンポーネントは、実際にはサービスとの接続を保存しません。実行中のサービスに何かを伝える必要がある場合、別のIntent



を使用して再度開始できます。 これは、実行中のサービスとバインドされたサービスの主な違いです。 バインドされたサービスは、 クライアントサーバーテンプレートを実装します 。 クライアント(Android UIコンポーネントまたは別のサービス)が接続を保存し、それを介してサービスから直接メソッドを呼び出すことができる場所。







 public class MyActivity extends Activity{ @TargetApi(Build.VERSION_CODES.O) private void moveToStartedState() { Intent intent = new MyIntentBuilder(this) .setCommand(Command.START).build(); if (isPreAndroidO()) { Log.d(TAG, "Running on Android N or lower"); startService(intent); } else { Log.d(TAG, "Running on Android O"); startForegroundService(intent); } } }
      
      





Android Oでは、サービスの実行に関して多くの変更が行われたことを思い出してください。 一定の通知メカニズムがないと、バックグラウンドで十分に長く動作できなくなります。 また、Android Oのバックグラウンドで実行中のサービスを開始する方法はstartForegroundService(Intent)



です。









前景および連続通知メカニズム



実行中のサービスはフォアグラウンドで動作できます。 繰り返しますが、 フォアグラウンドという用語は、サービスがバックグラウンドスレッドで実行されているのか、メインスレッドで実行されているのかを意味しません。 しかし、これは、システムがサービスに最高の優先度を与えることを意味します。したがって、メモリが不十分な場合、サービスはシステムによる削除の候補ではありません。 サービスを最前線に置くのは、最新のレスポンシブアプリケーションを作成することが本当に必要な場合のみです。







フロントエンドサービスによる使用例:







  1. バックグラウンドでメディアファイルを再生するアプリケーション。
  2. バックグラウンドで位置データを更新するアプリケーション。


実行中のサービスが最前面に表示されると、通知が表示され、サービスが実行されていることがユーザーに明確に通知されます。 フォアグラウンドで実行中のサービスはUIコンポーネントのライフサイクルから分離されているため、これは重要です(もちろん、最も頻繁に行われる通知は除きます)。 また、UIに一定の通知を表示する以外に、自分の電話で何かが機能している(そして潜在的に多くのリソースを消費している)ことをユーザーに通知する他の方法はありません。







以下は、フォアグラウンドで実行中のサービスを開始する例です。







 public class MyActivity extends Activity{ private void commandStart() { if (!mServiceIsStarted) { moveToStartedState(); return; } if (mExecutor == null) { // Start Executor task in Background Thread. } } }
      
      





バージョンで永続的な通知を作成するためのコードは次のとおりです







Android Oの前
 @TargetApi(25) public static class PreO { public static void createNotification(Service context) { // Create Pending Intents. PendingIntent piLaunchMainActivity = getLaunchActivityPI(context); PendingIntent piStopService = getStopServicePI(context); // Action to stop the service. NotificationCompat.Action stopAction = new NotificationCompat.Action.Builder( STOP_ACTION_ICON, getNotificationStopActionText(context), piStopService) .build(); // Create a notification. Notification mNotification = new NotificationCompat.Builder(context) .setContentTitle(getNotificationTitle(context)) .setContentText(getNotificationContent(context)) .setSmallIcon(SMALL_ICON) .setContentIntent(piLaunchMainActivity) .addAction(stopAction) .setStyle(new NotificationCompat.BigTextStyle()) .build(); context.startForeground( ONGOING_NOTIFICATION_ID, mNotification); } }
      
      





Android OのNotificationChannel経由
 @TargetApi(26) public static class O { public static final String CHANNEL_ID = String.valueOf(getRandomNumber()); public static void createNotification(Service context) { String channelId = createChannel(context); Notification notification = buildNotification(context, channelId); context.startForeground( ONGOING_NOTIFICATION_ID, notification); } private static Notification buildNotification( Service context, String channelId) { // Create Pending Intents. PendingIntent piLaunchMainActivity = getLaunchActivityPI(context); PendingIntent piStopService = getStopServicePI(context); // Action to stop the service. Notification.Action stopAction = new Notification.Action.Builder( STOP_ACTION_ICON, getNotificationStopActionText(context), piStopService) .build(); // Create a notification. return new Notification.Builder(context, channelId) .setContentTitle(getNotificationTitle(context)) .setContentText(getNotificationContent(context)) .setSmallIcon(SMALL_ICON) .setContentIntent(piLaunchMainActivity) .setActions(stopAction) .setStyle(new Notification.BigTextStyle()) .build(); } @NonNull private static String createChannel(Service ctx) { // Create a channel. NotificationManager notificationManager = (NotificationManager) ctx.getSystemService(Context.NOTIFICATION_SERVICE); CharSequence channelName = "Playback channel"; int importance = NotificationManager.IMPORTANCE_DEFAULT; NotificationChannel notificationChannel = new NotificationChannel( CHANNEL_ID, channelName, importance); notificationManager.createNotificationChannel( notificationChannel); return CHANNEL_ID; } }
      
      





さらに、MediaStyleで通知を作成する方法の詳細について説明する別の記事があります(オーディオファイルのバックグラウンド再生には通知が必要であり、リンクされたサービスと実行中のサービスがあるため)









サービスの実行を停止する



(通知コンストラクターに渡される)タイプPendingIntent



piStopService



パラメーターは、実際にはInteger



タイプの定数Command.STOP



Intent



を渡すことに注意してください。 startService(Intent)



は複数回startService(Intent)



ことを覚えていstartService(Intent)



か? これはこの動作の例です。 サービスを停止するには、 startService(Intent)



を介してIntent



を開始し、実行中のサービスのonStartCommand()



メソッドでこのIntent



を処理します。







 public class HandleNotifications{ private static PendingIntent getStopServicePI(Service context) { PendingIntent piStopService; { Intent iStopService = new MyIntentBuilder(context) .setCommand(Command.STOP).build(); piStopService = PendingIntent.getService( context, getRandomNumber(), iStopService, 0); } return piStopService; } }
      
      





これは、 onStartCommand()



メソッドがIntent



を処理できる理由を説明しています。 このメカニズムを使用して、作業を停止するようサービスに「伝える」ことができます。 以下は、これらの機能を説明するコードです。







  public class MyService extends Service{ @Override public int onStartCommand(Intent intent, int flags, int startId) { boolean containsCommand = MyIntentBuilder .containsCommand(intent); d(TAG, String.format( "Service in [%s] state. cmdId: [%d]. startId: [%d]", mServiceIsStarted ? "STARTED" : "NOT STARTED", containsCommand ? MyIntentBuilder.getCommand(intent) : "N/A", startId)); mServiceIsStarted = true; routeIntentToCommand(intent); return START_NOT_STICKY; } private void routeIntentToCommand(Intent intent) { if (intent != null) { // process command if (containsCommand(intent)) { processCommand(MyIntentBuilder.getCommand(intent)); } // process message if (MyIntentBuilder.containsMessage(intent)) { processMessage(MyIntentBuilder.getMessage(intent)); } } } }
      
      





実行中のサービスをフォアグラウンドで終了する場合は、 stopForeground(true)



呼び出す必要があります。 このメソッドは、永続的な通知も終了します。 ただし、このサービスは停止しません。 これを行うには、 stopSelf()



呼び出します。







サービスを停止するには、次のいずれかを実行できます。







  1. 上記のように、追加のパラメーターを指定してIntent



    startService()



    onStartCommand()



    と、 onStartCommand()



    処理され、サービスは実際にstopSelf()



    呼び出します。 また、他のコンポーネントがサービスに接続されていない場合、 onDestroy()



    が呼び出され、サービスがシャットダウンします。
  2. また、明示的なIntent



    (サービスクラスを指す)を作成してstopService()



    メソッドに渡すこともできます。このメソッドは、段落1と同じ方法onDestroy()



    を呼び出してからstopSelf()



    を呼び出します。


Activity



からサービスを停止する例を次に示します。







 public class MyActivity extends Activity{ void stopService1(){ stopService(new MyIntentBuilder(this).build()); } void stopService2(){ startService(new MyIntentBuilder(this) .setCommand(Command.STOP).build()); } }
      
      





そして、これらの要求を処理するサービス内のコードは次のとおりです(実行中のサービスがフォアグラウンドにあると想定)。







 public class MyService extends Service{ private void stopCommand(){ stopForeground(true); stopSelf(); } }
      
      







バウンドサービス



実行中のサービスとは異なり、バインドされたサービスを使用すると、サービスにバインドされているAndroidコンポーネントとサービスの間の接続を確立できます。 この接続は、サービスと対話するためのメソッドを定義するIBinder



インターフェイスの実装によって提供されます。 これの簡単な例は、クライアントと同じプロセスで(つまり、独自のアプリケーション内で)バインドされたサービスを実装することです。 この場合、 Binder



サブクラスであるJavaオブジェクトがクライアントに渡され、クライアントはそれを使用してサービスメソッドを呼び出すことができます。







より複雑なシナリオでは、さまざまなプロセスでサービスインターフェイスにアクセスできる必要がある場合、クライアントにサービスインターフェイスを提供するには、 Messenger



オブジェクト(これはクライアントからの呼び出しごとにコールバックを受け取るHandler



オブジェクトへの参照です)を使用して、サービスと対話できますMessage



オブジェクトを使用します。 Messenger



オブジェクトは、実際にはAIDL (Android Interface Definition Language)に基づいています。 Messenger



は、単一のスレッド内のすべてのクライアント要求からキューを作成するため、サービスは一度に1つの要求のみを受け取ります。 サービスで複数のリクエストを一度に処理する場合は、AIDLを直接使用できます。







バインドされたサービスと実行中のサービスの違い:







  1. クライアントコンポーネントには、実行中のサービスへの接続がありません。 startService()



    またはstopService()



    を介してIntent



    オブジェクトを使用するだけで、 onStartCommand()



    メソッドでサービスによって処理されます。
  2. クライアントコンポーネント( Activity



    Fragment



    または別のサービス)がバインドされたサービスに接続すると、バインドされたサービスのメソッドを呼び出すことができるIBinder



    実装を取得します。


いずれの場合でも、サービス(バインドまたは実行中)がバインドされたクライアントにメッセージを送信する必要がある場合、 LocalBroadcastManager



ようなものを使用する必要があります(クライアントとサービスが同じプロセスで動作している場合)。 通常、バインドされたサービスは、バインドされたクライアントコンポーネントに直接接続しません。









bindService()およびonCreate()



クライアントコンポーネントがサービスにバインドされるようにするには、実行中のサービスの場合のように、明示的なIntent



指定してbindService()



を呼び出す必要があります。







例:







 public class MyActivity extends Activity{ void bind(){ bindService( new MyIntentBuilder(this).build(), mServiceConnection, BIND_AUTO_CREATE); } }
      
      





BIND_AUTO_CREATE



は、 bindService()



呼び出すときの最も一般的なフラグです。 他のフラグが存在します(たとえば、 BIND_DEBUG_UNBIND



またはBIND_NOT_FOREGROUND



)。 BIND_AUTO_CREATE



の場合、バインドされたサービスは、サービスがまだ作成されていない場合にonCreate()



呼び出しonCreate()



。 実際、これは、最初のバインド時にサービスが作成されることを意味します。







bindService()



が呼び出されるとすぐに、サービスはクライアントのリクエストに応答し、 IBinder



インスタンスを提供する必要があります。これにより、クライアントはバインドされたサービスのメソッドを呼び出すことができます。 上記の例では、これはmServiceConnection



参照を使用して実装されています。 これは、バインドされたサービスがクライアントにバインディングの完了を通知するために使用するServiceConnection



コールバックです。 また、サービスからの切断についてクライアントに通知します。







つまり、バインディングは非同期です。 bindService()



はすぐに戻り、 IBinder



オブジェクトをIBinder



返しませんIBinder



オブジェクトを取得するにIBinder



クライアントはServiceConnection



インスタンスを作成し、それをbindService()



メソッドに渡す必要があります。 ServiceConnection



インターフェイスには、システムがIBinder



オブジェクトをIBinder



ために使用するコールバックメソッドが含まれています。







以下は、 ServiceConnection



実装例です。







 public class MyActivity extends Activity{ private ServiceConnection mServiceConnection = new ServiceConnection(){ public void onServiceConnected( ComponentName cName, IBinder service){ MyBinder binder = (MyService.MyBinder) service; mService = binder.getService(); // Get a reference to the Bound Service object. mServiceBound = true; } public void onServiceDisconnected(ComponentName cName){ mServiceBound= false; } }; }
      
      







サービスのバインド



クライアントがbindService(Intent)



呼び出すと、バインドされたサービス側で何が起こるか見てみましょう。







バインドされたサービスでは、 onBind()



メソッドを実装して、クライアントにIBinder



インスタンスを取得するIBinder



ます。 'onBind()'メソッドは、クライアントが最初にバインドされたときに1回だけ呼び出されます。 後続のクライアントに対して、システムは同じIBinder



インスタンスを発行します。







 public class MyService extends Service{ public IBinder onBind(Intent intent){ if (mBinder == null){ mBinder = new MyBinder(); } return mBinder; } }
      
      





IBinder



オブジェクトは、クライアントがサービスと対話できるプログラミングインターフェイスを提供します。 上記のように、 IBinder



を実装する最も簡単な方法は、 Binder



クラスを拡張することです。そのインスタンスはonBind()



メソッドから返されます。







 public class MyService extends Service{ public class MyBinder extends android.os.Binder { MyService getService(){ // Simply return a reference to this instance //of the Service. return MyService.this; } } }
      
      





上記の例では、単にgetService()



メソッドを使用しgetService()



。このメソッドは、クライアントコンポーネントにバインドされたJavaサービスオブジェクトを返すだけです。 IBinder



このインスタンスを参照すると、クライアントはバインドされたサービスのパブリックメソッドを直接呼び出すことができます。 これらのメソッドはクライアントスレッドで実行されることに注意してください。 また、 Activity



またはFragment



場合Fragment



これらのメソッドはメインスレッドで実行されます。 したがって、フローをブロックしたりANRを引き起こしたりする可能性のあるバインドされたサービスのメソッドに注意する必要があります。









サービスのバインド解除とonDestroy()呼び出し



, unbindService(mServiceConnection)



. onUnbind()



. , , , , , onDestroy



.







unbindService()



:







 public class MyActivity extends Activity{ protected void onStop(){ if (mServiceBound){ unbindService(mServiceConnection); mServiceBound = false; } } }
      
      





, onStop()



Activity



unbindService()



. UX onStart()



onStop()



, .







onUnbind()



:







 public class MyService extends Service{ public boolean onUnbind(Intent i){ return false; } }
      
      





false



. , true



, onBind()



onRebind()



.











, , . , . , .







, , , onCreate()



. , . , , onDestroy()



.







startService(), . , , . , , , bindService()



. ''" , , , stopSelf()



, , stopService()



.











, , , , . , Android O:







多くのコード
 public class MyService extends Service{ private void commandStart() { if (!mServiceIsStarted) { moveToStartedState(); return; } if (mExecutor == null) { mTimeRunning_sec = 0; if (isPreAndroidO()) { HandleNotifications.PreO.createNotification(this); } else { HandleNotifications.O.createNotification(this); } mExecutor = Executors .newSingleThreadScheduledExecutor(); Runnable runnable = new Runnable() { @Override public void run() { recurringTask(); } }; mExecutor.scheduleWithFixedDelay( runnable, DELAY_INITIAL, DELAY_RECURRING, DELAY_UNIT); d(TAG, "commandStart: starting executor"); } else { d(TAG, "commandStart: do nothing"); } } @TargetApi(Build.VERSION_CODES.O) private void moveToStartedState() { Intent intent = new MyIntentBuilder(this) .setCommand(Command.START).build(); if (isPreAndroidO()) { Log.d(TAG, "moveToStartedState: on N/lower"); startService(intent); } else { Log.d(TAG, "moveToStartedState: on O"); startForegroundService(intent); } } @Override public int onStartCommand( Intent intent, int flags, int startId) { boolean containsCommand = MyIntentBuilder .containsCommand(intent); d(TAG, String.format( "Service in [%s] state. id: [%d]. startId: [%d]", mServiceIsStarted ? "STARTED" : "NOT STARTED", containsCommand ? MyIntentBuilder.getCommand(intent) : "N/A", startId)); mServiceIsStarted = true; routeIntentToCommand(intent); return START_NOT_STICKY; } private void routeIntentToCommand(Intent intent) { if (intent != null) { // process command if (containsCommand(intent)) { processCommand(MyIntentBuilder.getCommand(intent)); } // process message if (MyIntentBuilder.containsMessage(intent)) { processMessage(MyIntentBuilder.getMessage(intent)); } } } private void processMessage(String message) { try { d(TAG, String.format("doMessage: message from client: '%s'", message)); } catch (Exception e) { e(TAG, "processMessage: exception", e); } } private void processCommand(int command) { try { switch (command) { case Command.START: commandStart(); break; case Command.STOP: commandStop(); break; } } catch (Exception e) { e(TAG, "processCommand: exception", e); } } /*...*/ }
      
      





:







  1. commandStart()



    , .
  2. commandStart()



    startService()



    startForegroundService()



    ( Android O).


, , .







, , commandStart()



. . , , :







  1. , ( mServiceStarted



    false



    )
  2. moveToStarted()



    Intent



    Extras Command.START



    , startService()



    startForegroundService()



    .
  3. onStartCommand()



    , commandStart()



    .
  4. commandStart()



    mServiceIsStarted



    true



    , commandStart()



    , .. .






, onDestroy()









. "", ( stopService(Intent)



startService(Intent)



c Extras Intent



, , Command.STOP



).







, :







画像











, , GitHub .







Android O N, , .








All Articles