本篇包含Service、Broadcast以及ContentProvider的常見用法
簡介
四大組件,包含了Activity、Service、Broadcast以及ContentProvider,而Activity使用最多,所以在感悟的第一篇就介紹了;而當(dāng)我們在項(xiàng)目開發(fā)過程中,就難免會用到其他的組件,這篇文章中就會講道如何使用其他組件,并且有一些個人的理解。
Service
Service生命周期
Service,我們又叫它服務(wù),這個東西在進(jìn)程間通信的文章中,就幫了我們大忙,通過綁定服務(wù),得到Binder對象,就能夠進(jìn)程間的通信了,而這只是啟動服務(wù)的一種方式,還有一種就是通過context來startService。
服務(wù)和Activity一樣,都是有生命周期的,下面就上一張官方Service的生命周期圖:

可以看到Service的生命周期都是以onCreate開始,onDestory結(jié)束,但是使用不同的啟動方式去啟動服務(wù),生命周期的中間是會有不同的地方。
總的來看Service中的回調(diào)方法有如下幾個:
1、onCreate:只有在第一次創(chuàng)建(不管如何創(chuàng)建)的時候,才會被回調(diào)
2、onStartCommand:只有通過startService啟動的服務(wù)才回回調(diào);
3、onBind:只有第一次通過bindService啟動的服務(wù)才回回調(diào),之后再綁定也不回調(diào)了,因?yàn)樵摲椒ㄆ鋵?shí)就是去返回一個已經(jīng)創(chuàng)建好的Binder;
4、onUnbind:只有最后一個unbindService的時候調(diào)用
5、onDestory:只有在所有啟動都結(jié)束后才會回調(diào),即startService多少次就要stopService多少次,才能被結(jié)束;或者bindService多少次就要unBindService多少次才能被結(jié)束;或者兩者都有,即啟動和結(jié)束要匹配完才能結(jié)束。
注:具體的測試demo的地址會在最后放上
前臺Service
大家都知道Service是沒有界面的,給人的感覺就是后臺運(yùn)行的,正常情況的確如此,但是它卻依舊運(yùn)行在主線程,所以耗時操作都要在子線程中處理,其實(shí)Service還有一種叫做前臺服務(wù),就好比網(wǎng)易云音樂的播放操作欄。
其必要條件是:必須在狀態(tài)欄提供通知,除非服務(wù)停止或從前臺移除,否則不能清除該通知。
而具體的方法就是調(diào)用startForeground()方法,其兩個參數(shù)為唯一標(biāo)識通知的整型數(shù)(不能為0)和狀態(tài)欄的Notification,例如:
NotificationCompat.Builder builder = new NotificationCompat.Builder(this);
builder.setSmallIcon(R.mipmap.ic_launcher);
builder.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher));
builder.setAutoCancel(false);
builder.setOngoing(true);
builder.setShowWhen(true);
builder.setContentTitle("這是一個前臺服務(wù)");
builder.setContentText("你好啊~");
Notification notification = builder.build();
startForeground(NOTIFICATION_DOWNLOAD_PROGRESS_ID, notification);
而如果該通知要做的更好,就需要用到RemoteView的知識,可能后續(xù)的文章中會講道。
在前臺demo中既然能start就肯定會有stopForeground(true),參數(shù)表示,是否移除該通知,而該通知的開和關(guān),這里讓啟動服務(wù)的組件去控制,便用到了Messenger,這是一種比較簡單的進(jìn)程間通信的方法,具體的代碼就不上了,想了解的就看看demo去吧,代碼比較簡單。
Broadcast
廣播,在目前的開發(fā)中用到的不多,用到的地方就如:軟件的安裝情況、極光推送的回調(diào)、短信監(jiān)聽等等;
廣播的回調(diào)方法只有一個onReceive(Context context, Intent intent),且在該方法中,操作不能超過10秒,否則會ANR。參數(shù)的具體含義就不多解釋了。
廣播有兩種注冊方式,一種是靜態(tài)注冊,一種是動態(tài)注冊,下面分辨來看看如何實(shí)現(xiàn)的:
1、靜態(tài)注冊:
需要我們在AndroidManifest.xml文件中,注冊receiver,所以我們就得先實(shí)現(xiàn)一個:
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.widget.Toast;
/**
* created by arvin on 17/2/24 00:02
* email:1035407623@qq.com
*/
public class StaticReceiver extends BroadcastReceiver {
@Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, "靜態(tài)注冊的廣播", Toast.LENGTH_SHORT).show();
}
}
然后在AndroidManifest.xml中聲明:
<receiver android:name=".broadcast.StaticReceiver">
<intent-filter>
<action android:name="net.arvin.androidart.broadcast.static" />
</intent-filter>
</receiver>
這樣就注冊成功了,這里有個細(xì)節(jié),exported沒有設(shè)置時,如果有intent-filter則表示,該廣播是可以被其他進(jìn)程訪問的,反之則不能被其他進(jìn)程方法;如果有exported則以其值為準(zhǔn);
2、動態(tài)注冊:
這種方式其實(shí)也是一樣只是聲明方式不同,首先依然時實(shí)現(xiàn)一個廣播接收者:
private BroadcastReceiver mReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
showToast("動態(tài)注冊的廣播");
}
};
然后通過context的registerReceiver方法注冊:
registerReceiver(mReceiver, getFilter());
private IntentFilter getFilter() {
IntentFilter filter = new IntentFilter();
filter.addAction(DYNAMICS_ACTION);
return filter;
}
動態(tài)注冊的廣播,需要在不用的時候調(diào)用unregisterReceiver,例如在onDestroy方法中調(diào)用,避免內(nèi)存泄露。
這樣兩種注冊方式簡單的介紹了一下。
然后發(fā)送廣播,其實(shí)這個也很簡單,發(fā)送的廣播有有序和無序兩種,首先來看無需的。
1、發(fā)送無序廣播:
只需調(diào)用context的sendBroadcast即可,傳入的事intent,這里就涉及到intent-filter的匹配規(guī)則,匹配到則發(fā)送到。具體如何匹配可以查看Android感悟之Intent文章中的講到的匹配規(guī)則。
2、發(fā)送有序廣播:
只需要調(diào)用context的sendOrderedBroadcast即可,而所謂有序廣播則是依次調(diào)用注冊的廣播,排序規(guī)則就是在廣播注冊的時候,設(shè)置的filter.setPriority(int priority);參數(shù)的范圍事[-1000,1000]其中值越大,就越先收到廣播,然后先收到的還能改變intent的值,或者是攔截掉該廣播,例如攔截在黑名單中的電話的短信或來電就是這個原理。攔截的方法也很簡單就是在準(zhǔn)備攔截的廣播中調(diào)用abortBroadcast()方法即可。
ContentProvider用法
ContentProvider它是一種系統(tǒng)提供的重要的進(jìn)程間數(shù)據(jù)交互的方式,底層的實(shí)現(xiàn)也是Binder,具體的實(shí)現(xiàn)這里也就不介紹了,先看看怎么用。交互肯定分為至少兩方,一方是數(shù)據(jù)提供者(實(shí)現(xiàn)ContentProvider),一方則是數(shù)據(jù)訪問者(ContentResolver),而這兩者的交互過程中又有一個最為重要的東西Uri以及UriMatcher,統(tǒng)一資源標(biāo)識符,下面也會挨著介紹。。
Uri
這是一種用于標(biāo)識某一互聯(lián)網(wǎng)資源名稱的字符串,它有三部分,在ContentProvider中大概可以這么分:content://<authority>/<path>
- "content://":這是第一部分,是協(xié)議;
- <authority>:這是第二部分,是主機(jī)IP;
- <path>:這是第三部分,是路徑,方便我們用來判斷怎么操作的部分。
數(shù)據(jù)訪問者
系統(tǒng)提供了ContentResolver類方便用戶通過Uri訪問ContentProvider中的提供的內(nèi)容,而數(shù)據(jù)的操作無外乎增刪查改,剛好這個類也提供了這幾種方法,下面就先簡單的介紹一下,具體如何用請參考demo:
1、query:查,其中參數(shù)有5個比較重要下面挨著說:
- Uri uri:這個是訪問數(shù)據(jù)的路徑;
- String[] projection:這個是查詢需要返回的對應(yīng)列的數(shù)據(jù),null則表示所有列;
- selection:這個是表示sql語句where后邊的條件;
- String[] selectionArgs:這個是條件中占位符對應(yīng)的參數(shù)值;
- sortOrder:這個是sql語句中order by后邊的排序方式。
2、insert:增,這個是插入單條數(shù)據(jù),參數(shù):
- Uri uri:這個是訪問數(shù)據(jù)的路徑;
- ContentValues values:以map的方式用來存儲插入的數(shù)據(jù),key是列名,value是值。
當(dāng)然也可以插入多條調(diào)用bulkInsert,只需要把第二個參數(shù)變成數(shù)組即可;
3、update:改,修改數(shù)據(jù),參數(shù):
- Uri uri:這個是訪問數(shù)據(jù)的路徑;
- ContentValues values:以map的方式用來存儲插入的數(shù)據(jù),key是列名,value是值。
- String where:這個是表示sql語句where后邊的條件,用于篩選;
- String[] selectionArgs:這個是條件中占位符對應(yīng)的參數(shù)值;
4、delete:刪,刪除數(shù)據(jù),參數(shù):
- Uri uri:這個是訪問數(shù)據(jù)的路徑;
- String where:這個是表示sql語句where后邊的條件,用于篩選;
- String[] selectionArgs:這個是條件中占位符對應(yīng)的參數(shù)值;
到這里增刪查改四個方法的參數(shù)也介紹的差不多了,具體如何使用請參考demo中的ProviderActivity和AddUserActivity。
ContentProvider
使用ContentProvider,則有兩部,第一繼承ContentProvider,第二在清單文件中配置provider屬性,記得設(shè)置authorities屬性,第二部沒什么好說的。來看第一部:
繼承ContentProvider,需要重寫6個方法:
- onCreate:在第一次被調(diào)用時創(chuàng)建,一般用于初始化工作
- getType:這個是用于匹配Intent中的mimeType屬性的,當(dāng)然必須要讓ContentProvider在清單文件的中的過濾器中設(shè)置該屬性才有用;
- query:查詢;
- insert:添加;
- update:修改;
- delete:刪除。
而增刪查改所操作的數(shù)據(jù)可以是,可以是數(shù)據(jù)庫、文件系統(tǒng)或網(wǎng)絡(luò);而更多使用到的是數(shù)據(jù)庫,所以這篇就以數(shù)據(jù)庫為例;下面就以User表為例,包含三個字?jǐn)啵篿d,name,age;id自增;創(chuàng)建表,我們使用現(xiàn)在比較流行的Greendao,3.0以后使用起來更加方便,使用注解即可,它和ButterKnife的原理一樣,有預(yù)編譯,也不會影響效率,方法如下:
1、在項(xiàng)目級的build.gradle文件中加入:
dependencies {
classpath 'org.greenrobot:greendao-gradle-plugin:3.2.1'
}
2、在module級的build.gradle文件中加入:
apply plugin: 'org.greenrobot.greendao'
greendao {//這是表示生成的包的目錄以及數(shù)據(jù)庫的版本
schemaVersion 1
daoPackage 'net.arvin.androidart.gen'
targetGenDir 'src/main/java'
}
dependencies {
//數(shù)據(jù)庫
compile 'org.greenrobot:greendao:3.2.0'
compile 'org.greenrobot:greendao-generator:3.2.0'
}
等下載完就配置完成,然后就能直接寫實(shí)體:
@Entity
public class User implements Parcelable {
@Id(autoincrement = true)
private Long id;
private String name;
private int age;
@Generated(hash = 1309193360)
public User(Long id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
}
@Generated(hash = 586692638)
public User() {
}
public Long getId() {
return id;
}
public void setId(Long 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 int describeContents() {
return 0;
}
@Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeValue(this.id);
dest.writeString(this.name);
dest.writeInt(this.age);
}
protected User(Parcel in) {
this.id = (Long) in.readValue(Long.class.getClassLoader());
this.name = in.readString();
this.age = in.readInt();
}
public static final Parcelable.Creator<User> CREATOR = new Parcelable.Creator<User>() {
@Override
public User createFromParcel(Parcel source) {
return new User(source);
}
@Override
public User[] newArray(int size) {
return new User[size];
}
};
}
寫好實(shí)體后,rebuild一下就生成好了,在net.arvin.androidart.gen包下會多出
DaoMaster、DaoSession、UserDao三個類;
這個實(shí)體其中構(gòu)造方法是greendao生成的,然后序列化是因?yàn)?,在demo中需要傳遞數(shù)據(jù);
他的那些注解這里就不介紹了,有興趣的可以自行Google。
然后我們來看ContentProvider的實(shí)現(xiàn):
這時候我們需要解決一個問題,數(shù)據(jù)訪問者發(fā)來的Uri,正確性我們應(yīng)該如何判斷呢?這里便引入了UriMatcher這個工具類,先來看使用方式:
public static final String MIME_ITEM = "user";
public static final String AUTHORITY = "net.arvin.androidart";
public static final String PATH_SINGLE = MIME_ITEM + "/#";
public static final String PATH_MULTIPLE = MIME_ITEM;
private static final int MULTIPLE_PEOPLE = 1;
private static final int SINGLE_PEOPLE = 2;
private static final UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, PATH_MULTIPLE, MULTIPLE_PEOPLE);
uriMatcher.addURI(AUTHORITY, PATH_SINGLE, SINGLE_PEOPLE);
}
在訪問者傳入Uri是,即可通過uriMatcher.match(uri),返回code,表示對應(yīng)的哪一個操作,這里有兩種操作操作單條,操作全部;其中uriMatcher.addUri的參數(shù),分別是:
- String authority:這個是Uri中的協(xié)議;
- String path:這個是Uri中的路徑,其中#可表示任意數(shù)字,而這里這個數(shù)字就是下面會用到的id。
- int code:這個是Uri中路徑對應(yīng)的code,匹配上就返回這個值。
下面就來看看全部的實(shí)現(xiàn)源碼:
import android.content.ContentProvider;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.UriMatcher;
import android.database.Cursor;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteQueryBuilder;
import android.net.Uri;
import android.support.annotation.NonNull;
import net.arvin.androidart.gen.DaoMaster;
import net.arvin.androidart.gen.UserDao;
/**
* created by arvin on 17/2/25 15:10
* email:1035407623@qq.com
*/
public class UserProvider extends ContentProvider {
//mimeType
public static final String MIME_DIR_PREFIX = "vnd.android.cursor.dir";
public static final String MIME_ITEM_PREFIX = "vnd.android.cursor.item";
public static final String MIME_ITEM = "user";
public static final String MIME_TYPE_SINGLE = MIME_ITEM_PREFIX + "/" + MIME_ITEM;
public static final String MIME_TYPE_MULTIPLE = MIME_DIR_PREFIX + "/" + MIME_ITEM;
//有效Uri
public static final String AUTHORITY = "net.arvin.androidart";
public static final String PATH_SINGLE = MIME_ITEM + "/#";
public static final String PATH_MULTIPLE = MIME_ITEM;
private static final int MULTIPLE_PEOPLE = 1;
private static final int SINGLE_PEOPLE = 2;
private static final UriMatcher uriMatcher;
static {
uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
uriMatcher.addURI(AUTHORITY, PATH_MULTIPLE, MULTIPLE_PEOPLE);
uriMatcher.addURI(AUTHORITY, PATH_SINGLE, SINGLE_PEOPLE);
}
public static final String CONTENT_URI_STRING = "content://" + AUTHORITY + "/" + PATH_MULTIPLE;
public static final Uri CONTENT_URI = Uri.parse(CONTENT_URI_STRING);
private SQLiteDatabase db;
@Override
public boolean onCreate() {
DaoMaster.DevOpenHelper mHelper = new DaoMaster.DevOpenHelper(getContext(), "art-db", null);
db = mHelper.getWritableDatabase();
return db != null;
}
@Override
public String getType(@NonNull Uri uri) {
switch (uriMatcher.match(uri)) {
case MULTIPLE_PEOPLE:
return MIME_TYPE_MULTIPLE;
case SINGLE_PEOPLE:
return MIME_TYPE_SINGLE;
default:
throw new IllegalArgumentException("UnKnow uri:" + uri);
}
}
@Override
public Uri insert(@NonNull Uri uri, ContentValues values) {
long id = db.insert(UserDao.TABLENAME, null, values);
if (id > 0) {
Uri newUri = ContentUris.withAppendedId(CONTENT_URI, id);
notifyChange(newUri);
return newUri;
}
throw new SQLException("failed to insert row into " + uri);
}
@Override
public Cursor query(@NonNull Uri uri, String[] projection, String selection, String[] selectionArgs, String sortOrder) {
SQLiteQueryBuilder qb = new SQLiteQueryBuilder();
qb.setTables(UserDao.TABLENAME);
qb.appendWhere(getWhere(uri, selection));
Cursor cursor = qb.query(db, projection, selection, selectionArgs, null, null, sortOrder);
notifyChange(uri);
return cursor;
}
@Override
public int delete(@NonNull Uri uri, String selection, String[] selectionArgs) {
int count = db.delete(UserDao.TABLENAME, getWhere(uri, selection), selectionArgs);
notifyChange(uri);
return count;
}
@Override
public int update(@NonNull Uri uri, ContentValues values, String selection, String[] selectionArgs) {
int count = db.update(UserDao.TABLENAME, values, getWhere(uri, selection), selectionArgs);
notifyChange(uri);
return count;
}
private String getWhere(@NonNull Uri uri, String selection) {
String where;
switch (uriMatcher.match(uri)) {
case SINGLE_PEOPLE:
where = UserDao.Properties.Id.columnName + "=" + uri.getPathSegments().get(1);
break;
case MULTIPLE_PEOPLE:
where = selection;
break;
default:
throw new IllegalArgumentException("UnKnow URI: " + uri);
}
return where;
}
private void notifyChange(@NonNull Uri uri) {
if (getContext() != null) {
getContext().getContentResolver().notifyChange(uri, null);
}
}
}
這里區(qū)分了操作一條還是多條數(shù)據(jù),不同的Uri其實(shí)就是判斷條件不一樣。下面依次描述如何操作數(shù)據(jù)庫:
- UriMatcher初始化:靜態(tài)初始化,把能匹配的Uri都加入到改類中,方便判斷;
- onCreate: 通過Grrendao拿到SQLiteDatabase,由于這里邊更多接近原生代碼,所以就使用SQLiteDatabase來操作,就不用Greendao的方法來操作了。
- getType:根據(jù)Uri來判斷是哪種類型;
- 增刪改查:完全是調(diào)用db的原生方法,簡單的操作表方法,這里也不再介紹了。
- notifyChange:這個的目的是在于回調(diào)告訴那些監(jiān)聽了這個ContentProvider的組件,哪個Uri發(fā)生了改變;這里也不再介紹。
到這里自定義ContentProvider就已經(jīng)實(shí)現(xiàn)了,而數(shù)據(jù)訪問者如何使用請查看demo。