1 基礎認知
1.1 Service 是什麼?
Service是一個應用元件, 它用來在背景完成一個時間跨度比較大的工作且沒有關聯任何界面。
1.2 Service所做工作舉例
- 通路網絡
- 播放音樂
- 檔案IO操作
- 大量資料的資料庫操作
- 其他
1.3 Service的特點
- Service在背景運作,不與使用者直接互動
- 即使退出應用,服務也不會立即停止
- 在預設情況下,Service運作在應用程式程序的主線程中(也就是UI線程),如果需要在Service中處理一些如網絡連接配接這種耗時任務,那麼應該将這些任務放在工作線程中,避免阻塞使用者界面
1.4 Service的分類
- Local Service(本地服務):Service對象與Serive的啟動者在同個程序中運作, 兩者的通信是程序内通信
- Remote Service(遠端服務):Service對象與Service的啟動者不在同一個程序中運作, 這時存在一個程序間通信的問題, Android專門為此設計了AIDL來實作程序間通信
2 啟動與停止服務
2.1 啟動方式
2.1.1 代碼示例
布局檔案代碼:
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<Button
android:id="@+id/start_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="112dp"
android:layout_marginLeft="112dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="啟動服務"
app:layout_constraintEnd_toStartOf="@+id/stop_service"
app:layout_constraintHorizontal_bias="0.018"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/bind_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="綁定服務"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="0.345"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/start_service" />
<Button
android:id="@+id/stop_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="停止服務"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<Button
android:id="@+id/unbind_service"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginTop="40dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:text="解綁服務"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHorizontal_bias="1.0"
app:layout_constraintStart_toEndOf="@+id/bind_service"
app:layout_constraintTop_toBottomOf="@+id/stop_service" />
</androidx.constraintlayout.widget.ConstraintLayout>
清單檔案代碼:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.servicepractice">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".MyService"/>
</application>
</manifest>
activity代碼:
package com.example.servicepractice;
import androidx.appcompat.app.AppCompatActivity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.util.Log;
import android.view.View;
import android.widget.Button;
import android.widget.Toast;
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
private Button startService;
private Button stopService;
private Button bindService;
private Button unbindService;
private boolean isBind;
private Intent intent;
private ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.e(TAG, "onServiceConnected: ");
}
@Override
public void onServiceDisconnected(ComponentName name) {
Log.e(TAG, "onServiceDisconnected: ");
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
intent = new Intent(this, MyService.class);
initViews();
}
private void initViews() {
startService = findViewById(R.id.start_service);
stopService = findViewById(R.id.stop_service);
bindService = findViewById(R.id.bind_service);
unbindService = findViewById(R.id.unbind_service);
startService.setOnClickListener(onClickListener);
stopService.setOnClickListener(onClickListener);
bindService.setOnClickListener(onClickListener);
unbindService.setOnClickListener(onClickListener);
}
View.OnClickListener onClickListener = new View.OnClickListener() {
@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.start_service:
startService(intent);
break;
case R.id.stop_service:
stopService(intent);
break;
case R.id.bind_service:
if (!isBind) {
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
isBind = true;
Toast.makeText(MainActivity.this, "綁定成功",
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this,
"已經綁定成功,不需要再次綁定",
Toast.LENGTH_SHORT).show();
}
break;
case R.id.unbind_service:
if (isBind) {
isBind = false;
Toast.makeText(MainActivity.this, "解綁完成",
Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(MainActivity.this, "已經解綁,不需要再次解綁",
Toast.LENGTH_SHORT).show();
}
break;
}
}
};
@Override
protected void onDestroy() {
if (isBind) {
unbindService(serviceConnection);
isBind = false;
}
super.onDestroy();
}
}
service中的代碼:
package com.example.servicepractice;
import android.app.Service;
import android.content.Intent;
import android.os.Binder;
import android.os.IBinder;
import android.util.Log;
public class MyService extends Service {
private static final String TAG = MyService.class.getSimpleName();
public MyService() {
Log.e(TAG, "MyService: " );
}
@androidx.annotation.Nullable
@Override
public IBinder onBind(Intent intent) {
Log.e(TAG, "onBind: ");
return new MyBinder();
}
@Override
public boolean onUnbind(Intent intent) {
Log.e(TAG, "onUnbind: ");
return super.onUnbind(intent);
}
@Override
public void onCreate() {
Log.e(TAG, "onCreate: ");
super.onCreate();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.e(TAG, "onStartCommand: ");
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
Log.e(TAG, "onDestroy: ");
super.onDestroy();
}
class MyBinder extends Binder {
public MyBinder() {
Log.e(TAG, "MyBinder: ");
}
}
}
2.1.2 一般啟動
- context.startService(Intent intent)
- context.stopService(Intent intent)
點選開始服務
MyService: MyService:
MyService: onCreate:
MyService: onStartCommand:
當service已經啟動後再次調用startService(Intent intent),将不會再去建立service,而是調用onStartCommand()回調方法,也就是說Service的構造方法和onCreate()回調方法将不再調用。
onStartCommand:
點選停止服務
MyService: onDestroy:
2.1.3 綁定啟動與解綁
- context.bindService(Intent intent, ServiceConnection connection)
- context.unbindService(ServiceConnection connection)
點選綁定服務 , 綁定服務過後再次調用bindService()不會調用任何回調方法
MyService: MyService:
MyService: onCreate:
MyService: onBind:
MyService: MyBinder:
MainActivity: onServiceConnected:
點選解綁服務
MyService: onUnbind:
MyService: onDestroy:
如果綁定過後沒有解綁就銷毀activity,則會報如下錯誤
E/ActivityThread: Activity com.example.servicepractice.MainActivity has leaked ServiceConnection [email protected] that was originally bound here
android.app.ServiceConnectionLeaked: Activity com.example.servicepractice.MainActivity has leaked ServiceConnection [email protected] that was originally bound here
at android.app.LoadedApk$ServiceDispatcher.<init>(LoadedApk.java:1610)
at android.app.LoadedApk.getServiceDispatcher(LoadedApk.java:1502)
at android.app.ContextImpl.bindServiceCommon(ContextImpl.java:1659)
at android.app.ContextImpl.bindService(ContextImpl.java:1612)
at android.content.ContextWrapper.bindService(ContextWrapper.java:698)
at com.example.servicepractice.MainActivity$2.onClick(MainActivity.java:67)
at android.view.View.performClick(View.java:6597)
at android.view.View.performClickInternal(View.java:6574)
at android.view.View.access$3100(View.java:778)
at android.view.View$PerformClick.run(View.java:25885)
at android.os.Handler.handleCallback(Handler.java:873)
at android.os.Handler.dispatchMessage(Handler.java:99)
at android.os.Looper.loop(Looper.java:193)
at android.app.ActivityThread.main(ActivityThread.java:6669)
at java.lang.reflect.Method.invoke(Native Method)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:858)
2.1.3 Service的生命周期圖
從上面這張圖檔可以看出,一個Service可以和多個元件綁定,并且隻有當所有的元件都解綁之後才會調用onUnbind()。、
3 AIDL
3.1 基礎認知
- 每個應用程式都運作在自己的獨立程序中,并且可以啟動另一個應用程序的服務,而且經常需要在不同的程序間傳遞資料對象。
- 在Android平台,一個程序不能直接通路另一個程序的記憶體空間,是以要想對話,需要将對象分解成作業系統可以了解的基本單元,并且有序的通過程序邊界。
- AIDL (Android Interface Definition Language) 用于生成可以在Android裝置上兩個程序之間進行程序間通信(interprocess communication, IPC)的代碼。
- 如果在一個程序中(例如Activity)要調用另一個程序中(例如Service)對象的操作,就可以使用AIDL生成可序列化的參數。
3.2 編寫aidl注意事項
- 接口名和aidl檔案名必須相同
- 接口和方法前不能加通路權限修飾符(public, private, protected等),也不能用final,static
- Aidl預設支援類型包括java基本類型和String,List,Map,CharSequence,使用這些類型時不需要import聲明。對于List和Map中的元素類型必須是aidl支援的類型,如果使用自定義類型作為參數或者傳回值,自定義類型必須實作Parcelable接口。
- 自定義類型和aidl生成的其他接口類型在aidl描述檔案中,應該顯示import,即便該類和aidl檔案在同一個包中。
- 自定義類型的aidl檔案名要和自定義類的名字相同,如Book.java,Book.aidl
- aidl檔案和自定義實體類在服務端和用戶端都要有一套一樣的,包名也要一樣
- 在aidl檔案中所有非java基本類型參數必須加上in, out , inout标記,以指明參數是輸入參數、輸出參數還是輸入輸出參數。
- java 原始類型預設的标記為in,不能為其他标記。
3.3 遠端服務端代碼
清單檔案代碼:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.remoteservice">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".remote.MyRemoteService">
<intent-filter>
<action android:name="android.intent.action.test"/>
</intent-filter>
</service>
</application>
</manifest>
實體資料類:
package com.example.remoteservice.remote;
import android.os.Parcel;
import android.os.Parcelable;
import android.util.Log;
public class Student implements Parcelable {
private int id;
private String name;
private int age;
public Student(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
protected Student(Parcel in) {
id = in.readInt();
name = in.readString();
age = in.readInt();
}
public static final Creator<Student> CREATOR = new Creator<Student>() {
@Override
public Student createFromParcel(Parcel in) {
Log.e("TAG", "createFromParcel: ");
return new Student(in);
}
@Override
public Student[] newArray(int size) {
return new Student[size];
}
};
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Student{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
@Override
public int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
Log.e("TAG", "writeToParcel: ");
dest.writeInt(id);
dest.writeString(name);
dest.writeInt(age);
}
}
遠端Service代碼:
package com.example.remoteservice.remote;
import android.app.Service;
import android.content.Intent;
import android.os.IBinder;
import android.util.Log;
public class MyRemoteService extends Service {
@androidx.annotation.Nullable
@Override
public IBinder onBind(Intent intent) {
Log.e("TAG", "onBind()");
return new StudentService();
}
@Override
public boolean onUnbind(Intent intent) {
Log.e("TAG", "onUnbind()");
return super.onUnbind(intent);
}
//處理Student相關的業務邏輯類
class StudentService extends IStudentService.Stub {
@Override
public Student getStudentById(int id) {
Log.e("TAG", "Service getStudentById() "+id);
return new Student(id, "Tom", 80);
}
}
}
Service aidl接口檔案:
// IStudentService.aidl
package com.example.remoteservice.remote;
import com.example.remoteservice.remote.Student;
// Declare any non-default types here with import statements
interface IStudentService {
Student getStudentById(int id);
}
實體類的aidl檔案:
// Student.aidl
package com.example.remoteservice.remote;
parcelable Student;
檔案結構圖:
3.4 用戶端代碼:
清單檔案代碼:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.servicepractice">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/AppTheme">
<activity android:name=".MainActivity">
</activity>
<activity android:name=".client.ClientActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service android:name=".MyService"/>
</application>
</manifest>
xml檔案代碼:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="bindRemoteService"
android:text="bind remote Service" />
<EditText
android:id="@+id/et_aidl_id"
android:layout_width="match_parent"
android:layout_height="50dp"
android:hint="學員ID"
android:text="3" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="invokeRemote"
android:text="調用遠端服務端的方法" />
<Button
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:onClick="unbindRemoteService"
android:text="unbind remote Service" />
</LinearLayout>
activity代碼:
package com.example.servicepractice.client;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.os.RemoteException;
import android.util.Log;
import android.view.View;
import android.widget.EditText;
import android.widget.Toast;
import com.example.remoteservice.remote.IStudentService;
import com.example.remoteservice.remote.Student;
import com.example.servicepractice.R;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
public class ClientActivity extends AppCompatActivity {
private EditText et_aidl_id;
private ServiceConnection conn;
private IStudentService studentService;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_client);
et_aidl_id = findViewById(R.id.et_aidl_id);
}
public void bindRemoteService(View v) {
Intent intent = new Intent("android.intent.action.test");
intent.setPackage("com.example.remoteservice");
if (conn == null) {
conn = new ServiceConnection() {
@Override
public void onServiceDisconnected(ComponentName name) {
}
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
Log.e("TAG", "onServiceConnected()");
studentService = IStudentService.Stub.asInterface(service);
}
};
bindService(intent, conn, Context.BIND_AUTO_CREATE);
Toast.makeText(this, "綁定Service", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "已經綁定Service", Toast.LENGTH_SHORT).show();
}
}
public void invokeRemote(View v) throws RemoteException {
if (studentService != null) {
int id = Integer.parseInt(et_aidl_id.getText().toString());
Student student = studentService.getStudentById(id);
Toast.makeText(this, student.toString(), Toast.LENGTH_SHORT).show();
}
}
public void unbindRemoteService(View v) {
unbindRemoteService();
}
private void unbindRemoteService() {
if (conn != null) {
unbindService(conn);
conn = null;
studentService = null;
Toast.makeText(this, "解綁Service", Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(this, "還未綁定Service", Toast.LENGTH_SHORT).show();
}
}
@Override
protected void onDestroy() {
unbindRemoteService();
super.onDestroy();
}
}
用戶端檔案結構圖:
3.5 運作效果截圖
服務端日志:
TAG: onBind()
TAG: Service getStudentById() 3
TAG: writeToParcel:
用戶端日志:
TAG: onServiceConnected()
TAG: createFromParcel:
4 案例:播放音樂
xml代碼:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<Button
android:id="@+id/play"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="播放"/>
<Button
android:id="@+id/pause"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="暫停"/>
<Button
android:id="@+id/stop"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="停止"/>
<Button
android:id="@+id/exit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="退出"/>
</LinearLayout>
activity代碼:
package com.example.servicepractice;
import android.content.Intent;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AppCompatActivity;
public class MusicAcitivity extends AppCompatActivity implements View.OnClickListener {
private Button btnPlay;
private Button btnPause;
private Button btnStop;
private Button btnExit;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_music);
initViews();
}
private void initViews() {
btnPlay = findViewById(R.id.play);
btnPause = findViewById(R.id.pause);
btnStop = findViewById(R.id.stop);
btnExit = findViewById(R.id.exit);
btnPlay.setOnClickListener(this);
btnPause.setOnClickListener(this);
btnStop.setOnClickListener(this);
btnExit.setOnClickListener(this);
}
@Override
public void onClick(View v) {
Intent intent = new Intent(this, MusicService.class);
switch (v.getId()) {
case R.id.play:
intent.putExtra("action", "play");
break;
case R.id.pause:
intent.putExtra("action", "pause");
break;
case R.id.stop:
intent.putExtra("action", "stop");
break;
case R.id.exit:
intent.putExtra("action", "exit");
finish();
break;
}
startService(intent);
}
}
service代碼:
package com.example.servicepractice;
import android.app.Service;
import android.content.Intent;
import android.media.MediaPlayer;
import android.os.IBinder;
import android.util.Log;
import androidx.annotation.Nullable;
public class MusicService extends Service {
private MediaPlayer mediaPlayer;
@Nullable
@Override
public IBinder onBind(Intent intent) {
return null;
}
private void playMedia() {
if (mediaPlayer == null) {
mediaPlayer = MediaPlayer.create(this, R.raw.water_hander);
}
mediaPlayer.start();
}
private void pauseMedia() {
if (mediaPlayer != null && mediaPlayer.isPlaying()) {
mediaPlayer.pause();
}
}
private void stopMedia() {
if (mediaPlayer != null) {
mediaPlayer.stop();//停止
mediaPlayer.reset();//重置
mediaPlayer.release();//釋放資源
mediaPlayer = null;//使對象為空,否者重新播放會crash
}
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.i("TAG", "onStartCommand: ");
String action = intent.getStringExtra("action");
if (action.equals("play")) {
playMedia();
} else if (action.equals("pause")) {
pauseMedia();
} else if (action.equals("stop")) {
stopMedia();
} else if (action.equals("exit")) {
stopMedia();
stopSelf();
}
return super.onStartCommand(intent, flags, startId);
}
@Override
public void onDestroy() {
Log.i("TAG", "onDestroy: ");
super.onDestroy();
}
}