阅读 601

Android必读的四大组件汇总

Service

有两种启动方式:startServicebindService

service_lifecycle.png

startService

通过 startService 启动后,service 会一直无限期运行下去,只有外部调用了 stopService()或 stopSelf()方法时,该 Service 才会停止运行并销毁。开启者不能调用服务里面的方法。 生命周期为:onCreate - > onStartCommand - > onDestroy

多次调用 startService,会多次执行 onStartCommand,并不会多次执行 onCreate

bindService

使用 context 的bindService(Intent service, ServiceConnection conn,int flags)方法启动 service,不再使用时,调用 unbindService(ServiceConnection)方法手动停止该服务,或者等调用者销毁后,Service 会自动销毁。这种启动方式,调用者可以调用 Service 里的方法。 生命周期为:onCreate() - > onBind() -> onUnbind() - > onDestory() 调用者如果需要调用 Service 中的方法,可以通过如下几步:

  • Service 中定义一个 Binder 的子类。这个子类中定义一个返回 Service 的方法或属性。
  • 在 Service 中实例化一个 Binder 子类的对象。
  • onBind 方法中返回这个这个 Binder 子类的对象。
  • 调用者在 ServiceConnection 的 onServiceConnected 中将 iBinder 转型为对应的 Binder 子类,并调用返回 Service 的方法或属性获取到 Service 实例,此时就可以调用返回 Service 里的方法。也可以通过给 Service 设置接口或者设置 kotlin 高阶函数,来实现 Service 调用它的调用者(比如 Activity)中的方法。

示例代码如下:

class TestService : Service() {
    //2.在Service中实例化一个Binder子类的对象
    private val testBinder: TestBinder = TestBinder()
    var update: ((Int) -> Unit)? = null
    private var job: Job? = null

    override fun onBind(intent: Intent): IBinder {
        Log.i("zx", "TestService-onBind")
        job = CoroutineScope(Dispatchers.IO).launch {
            //模拟进度
            var index = 0
            while (true) {
                delay(1000)
                withContext(Dispatchers.Main) {
                    update?.let { it(++index) }
                }
            }
        }
        //3.onBind方法中返回这个这个Binder子类的对象
        return testBinder
    }

    override fun onDestroy() {
        super.onDestroy()
        job?.cancel()
    }

    //1.定义Binder的子类,并且是内部类,类中定义一个方法返回Service
    inner class TestBinder : Binder() {
        val service: TestService
            get() = this@TestService
    }
}


class ServiceActivity : AppCompatActivity() {
    lateinit var testService: TestService
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_service)
        val intent = Intent(this, TestService::class.java)
        bindService(intent, serviceConnection, BIND_AUTO_CREATE)
    }

    private var serviceConnection: ServiceConnection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName, iBinder: IBinder) {
            //4.获取到Service实例
            testService = (iBinder as TestService.TestBinder).service
            //通过设置高阶函数,来实现Service调用它的调用者Activity中的方法
            testService.update = {
                Log.i("zx", "当前模拟的进度为$it")
            }
        }

        override fun onServiceDisconnected(name: ComponentName) {}
    }

    override fun onDestroy() {
        super.onDestroy()
        unbindService(serviceConnection)//也可以不调用,Activity销毁时,Service会自动销毁
    }
}
复制代码

IntentService

IntentService 为 Service 的子类,IntentService会创建一个线程,onHandleIntent中的代码就是运行在这个线程之中,可以在 onHandleIntent 中执行耗时操作,执行完成后会自动销毁。使用示例如下:

class MyIntentService : IntentService("MyIntentService") {

    override fun onHandleIntent(intent: Intent?) {
        when (intent?.action) {
            ACTION_FOO -> {
                val param1 = intent.getStringExtra(EXTRA_PARAM1)
                val param2 = intent.getStringExtra(EXTRA_PARAM2)
                handleActionFoo(param1, param2)
            }
            ACTION_BAZ -> {
                val param1 = intent.getStringExtra(EXTRA_PARAM1)
                val param2 = intent.getStringExtra(EXTRA_PARAM2)
                handleActionBaz(param1, param2)
            }
        }
    }

    private fun handleActionFoo(param1: String?, param2: String?) {
        TODO("Handle action Foo")
    }

    private fun handleActionBaz(param1: String?, param2: String?) {
        Log.i("zx", "param1=$param1,param2=$param2," + Thread.currentThread().name)
    }

    companion object {
        @JvmStatic
        fun startActionFoo(context: Context, param1: String, param2: String) {
            val intent = Intent(context, MyIntentService::class.java).apply {
                action = ACTION_FOO
                putExtra(EXTRA_PARAM1, param1)
                putExtra(EXTRA_PARAM2, param2)
            }
            context.startService(intent)
        }


        @JvmStatic
        fun startActionBaz(context: Context, param1: String, param2: String) {
            val intent = Intent(context, MyIntentService::class.java).apply {
                action = ACTION_BAZ
                putExtra(EXTRA_PARAM1, param1)
                putExtra(EXTRA_PARAM2, param2)
            }
            context.startService(intent)
        }
    }
}

//使用时
MyIntentService.startActionBaz(this,"123","456")
复制代码

在 Android8.0 以上版本中,当应用本身未在前台运行时,系统会对运行后台服务施加限制,所以 IntentService 在 Android8.0 上被废弃了,可以用 WorkManager 或者 JobIntentService 代替。

BroadcastReceiver

Android 系统会在发生各种系统事件时发送广播,例如系统启动或设备开始充电时,应用也可以发送自定义广播来通知其他应用它们可能感兴趣的事件(例如,一些新数据已下载)。应用可以注册接收特定的广播。广播发出后,系统会自动将广播传送给同意接收这种广播的应用。

发送广播

有三种方式发送广播

  • sendOrderedBroadcast(Intent, String) 发送一个有序广播。接收器按照优先级逐个顺序执行,接收器可以向下传递结果,向下传递时可以往广播里存入数据(setResultExtras(Bundle)),下一个接受者就可以接收到存入的数据(val bundle = getResultExtras(true))。一个接受者也可以完全中止广播(BroadcastReceiver.abortBroadcast()),使其不再传递给后续其他接收器。接收器的运行顺序可以通过匹配的 intent-filter 的 android:priority 属性来控制,数值越大优先级越高;具有相同优先级的接收器将按注册先后顺序运行。
  • sendBroadcast(Intent) 按随机的顺序向所有接收器发送广播。这称为普通广播。这种方法效率更高,但也意味着接收器无法从其他接收器读取结果,无法传递从广播中收到的数据,也无法中止广播。
  • LocalBroadcastManager.sendBroadcast 将广播发送给同一应用中的接收器。这种实现方法的效率更高(无需进行进程间通信),而且无需担心其他应用在收发您的广播时带来的任何安全问题。

发送时可以指定接受者包名,也可以设置Action和Extra,示例代码如下:

val intent: Intent = Intent();
intent.action = "com.example.broadcast.MY_Action"
intent.putExtra("key", "value")
intent.setPackage("com.zx.test")
sendBroadcast(intent)
复制代码

接收广播

静态注册,接收广播步骤如下:

  1. 自定义一个类继承 BroadcastReceiver

  2. 重写 onReceive 方法

  3. 在 manifest.xml 中注册,并通过 intent-filter 指定需要接收的 action,例如:android.intent.action.BOOT_COMPLETED 表示系统启动完成的广播。

示例如下:

<receiver
    android:name=".broadcast.MyReceiver"
    android:enabled="true"
    android:exported="true">
    <intent-filter>
        <action android:name="android.intent.action.BOOT_COMPLETED"/>
        <action android:name="android.intent.action.INPUT_METHOD_CHANGED" />
        <action android:name="android.intent.action.LOCALE_CHANGED"/>
    </intent-filter>
</receiver>
复制代码

动态注册,接收广播步骤如下:

  1. 自定义一个类继承 BroadcastReceiver

  2. 重写 onReceive 方法

  3. 创建 IntentFilter 并调用 registerReceiver(BroadcastReceiver, IntentFilter) 来注册接收。

示例如下:

val networkStateReceiver = NetworkStateReceiver() //自定义的广播接收器
val intentFilter = IntentFilter()
intentFilter.addAction(ConnectivityManager.CONNECTIVITY_ACTION) //网络连接发生了变化 的系统广播
registerReceiver(networkStateReceiver, intentFilter)
复制代码

静态注册和动态注册区别

  • 动态注册广播不是常驻型广播,也就是说广播跟随activity的生命周期。注意: 在activity结束前,移除广播接收器。 静态注册是常驻型,也就是说当应用程序关闭后,如果有信息广播来,程序也会被系统调用自动运行。
  • 当广播为有序广播时: 1 优先级高的先接收 2 同优先级的广播接收器,动态优先于静态 3 同优先级的同类广播接收器,静态:先扫描的优先于后扫描的,动态:先注册的优先于后注册的。
  • 当广播为普通广播时: 1 无视优先级,动态广播接收器优先于静态广播接收器 2 同优先级的同类广播接收器,静态:先扫描的优先于后扫描的,动态:先注册的优先于后注册的。

对进程的影响

当接收器的onReceive()中的代码正在运行时,它被认为是前台进程,系统不会杀死它。但是当 onReceive() 返回了(执行完成后),BroadcastReceiver 就不再活跃,如果它的进程中只有BroadcastReceiver(APP最近未启动,整个进程中没有活跃的Activity或者前台任务),系统会将其进程视为低优先级进程,并随时可能会将其终止,那么进程中的线程也就会被终止,因此不应从广播接收器启动长时间运行的后台线程,也不应该启动一个后台Service(8.0以上,启动后台Service会被限制)。当然了,如果你能确保当前BroadcastReceiver所在进程不会被杀死(比如动态注册的广播,此时APP有很多Activity正在运行,是活跃进程),你也可以在onReceive()中开一个线程,用于处理耗时任务。由于onReceive()运行在主线程,所以主线程不能做的事情,onReceive()中都不能做,比如网路请求、耗时任务等等。那如果想要在onReceive()中执行耗时任务怎么办呢?官方给出的方案是使用goAsync(),告诉系统即使onReceive()返回了,也不要回收广播,我会在广播处理完成之后调用pendingResult.finish()通知你,你再回收,这种方式代码依旧运行在主线程,所以需要开启一个线程,在线程中执行耗时操作,耗时操作执行完成后调用pendingResult.finish(),这样广播就可以被回收了。即使新开了线程,但是耗时操作依旧被限制在10s内(也就是说10s内必须调用pendingResult.finish()),超过10s,依然会触发ANR。示例代码如下:

override fun onReceive(context: Context, intent: Intent) {
    //告诉系统,即使onReceive返回了,在pendingResult.finish()之前,不要回收BroadcastReceiver
    val pendingResult: PendingResult = goAsync()

    //用协程开一个线程
    CoroutineScope(Dispatchers.IO).launch {
        delay(9000)//模拟耗时操作。即使不在主线程,但是依旧不能超过10秒,否则仍然会ANR
        withContext(Dispatchers.Main)
        {
            //切换到主线程,并告诉系统,广播执行完了。
            pendingResult.finish()
        }
    }
}
复制代码

如果是动态注册的接收器,onReceive执行时肯定是在APP运行期间,此时进程不会被回收,如果要执行耗时任务,创建的线程也不会被回收,此时不需要使用goAsync(),因为用了goAsync(),即使不在主线程中,也必须要在10秒内调用pendingResult.finish(),否则就会ANR,这个规则反而限制了线程的运行时间。这种情况只需要创建一个线程,并将耗时操作放在线程里就行了。

ContentProvider

内容提供者,提供了与其他应用共享数据的方法,通过ContentProvider,可以使其他应用安全地访问和修改我们的应用数据。与ContentProvider一起使用的是ContentResolver,ContentResolver用于从其他应用或者系统获取数据。ContentProvider分为系统ContentProvider和自定义ContentProvider,我们可以通过系统的ContentProvider获取系统中的数据,比如联系人、短信、图库等等,此时我们只需要关注获取数据的ContentResolver即可。

ContentResolver

使用ContentResolver有如下几步:

  1. 申请相关权限。
  2. 确定URI
  3. 获取ContentResolver,并调用ContentResolver相关的方法,比如查询。

示例代码如下:

//查询系统图库中的图片
val uri: Uri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI
val cursor = contentResolver.query(uri, null, null, null, MediaStore.Images.Media.DATE_ADDED + " desc LIMIT 5")
if (cursor != null) {
    while (cursor.moveToNext()) {
        val title = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.TITLE));
        //图片的地址
        val path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
        val height = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media.HEIGHT));
        val width = cursor.getInt(cursor.getColumnIndex(MediaStore.Images.Media.WIDTH));
        val size = cursor.getLong(cursor.getColumnIndex(MediaStore.Images.Media.SIZE));
        Log.i("zx", "图片文件名:$title,路径:$path,尺寸:$height x $width,文件大小:$size");
    }
    cursor.close()
}
复制代码

通过ContentResolver的query(uri, projection, selection, selectionArgs, sortOrder)即可查询,示例中按照图片添加时间降序排列并且限制了只查询5条,输出结果如下:

文件名:IMG_20210523_160524,路径:/storage/emulated/0/DCIM/Camera/IMG_20210523_160524.jpg,尺寸:3016 x 4032,大小:4765375
文件名:wx_camera_1620566260144,路径:/storage/emulated/0/Pictures/WeiXin/wx_camera_1620566260144.jpg,尺寸:1920 x 1080,大小:995780
文件名:IMG_20210507_230154,路径:/storage/emulated/0/DCIM/Camera/IMG_20210507_230154.jpg,尺寸:3016 x 4032,大小:5354687
文件名:IMG_20210507_190849,路径:/storage/emulated/0/DCIM/Camera/IMG_20210507_190849.jpg,尺寸:3016 x 4032,大小:4360429
文件名:IMG_20210505_154931,路径:/storage/emulated/0/DCIM/Camera/IMG_20210505_154931.jpg,尺寸:3016 x 4032,大小:9205474
复制代码

query方法的参数类似于SQL查询参数,如下表:

query() 参数SELECT 关键字/参数备注
UriFROM *table_name*类似于SQL中的表名,上边的例子制定了从"系统图库这个表"
projection*col,col,col,...*指定需要查询哪些列
selectionWHERE *col* = *value*指定查询行的条件
selectionArgs没有SQL等效项selection中的 ? 占位符会被这里的参数所替换
sortOrderORDER BY *col,col,...*指定在返回的 Cursor 中各行的显示顺序

除了query,ContentResolver提供了其他几个方法insert、delete、update方法,同样是类似于SQL的增删改查操作。

ContentProvider

下面来讲讲如何使用ContentProvider将数据提供给其他应用。

  1. 定义如何存储数据。ContentProvider只是一个介于数据与外部应用之间的桥梁,ContentProvider本身并不能存储数据,一般使用SQLite进行存储数据。

  2. 继承ContentProvider,制定唯一标识authorities,实现增删改查等方法,并在manifest注册。

  3. 外部应用获取ContentResolver,调用ContentResolver相关的方法,比如查询。

示例代码如下:

数据库存储

class MyDBHelper(context: Context?) : SQLiteOpenHelper(context, "zx.db", null, 1) {
    override fun onCreate(db: SQLiteDatabase) {

        // 创建两个表:用户表 和职业表
        db.execSQL("CREATE TABLE IF NOT EXISTS $USER_TABLE_NAME(_id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT)")
        db.execSQL("CREATE TABLE IF NOT EXISTS $JOB_TABLE_NAME(_id INTEGER PRIMARY KEY AUTOINCREMENT, job TEXT)")
    }

    override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {}

    companion object {
        // 表名
        const val USER_TABLE_NAME = "user"
        const val JOB_TABLE_NAME = "job"
    }
}
复制代码

自定义的ContentProvider

class MyContentProvider : ContentProvider() {
    //外部应用使用ContentProvider时拼接URI时需要,在manifest也指定了,推荐为ContentProvider包名+类名
    //外部应用使用时URI就必须为 content://zx.com.myContentProvider/表名
    //之所以在manifest中指定了,这里又写一遍是为了在UriMatcher中使用
    private val AUTOHORITY = "zx.com.myContentProvider"
    private val mMatcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH)
    private val User_Code = 1
    private val Job_Code = 2
    private lateinit var myDbHelper: MyDBHelper
    private lateinit var db: SQLiteDatabase
    private lateinit var scope: CoroutineScope

    init {
        // 若URI资源路径 = content://cn.scu.myprovider/user ,则返回注册码User_Code
        // 若URI资源路径 = content://cn.scu.myprovider/job ,则返回注册码Job_Code
        mMatcher.addURI(AUTOHORITY, "user", User_Code);
        mMatcher.addURI(AUTOHORITY, "job", Job_Code);
    }
    
    override fun onCreate(): Boolean {
        myDbHelper = MyDBHelper(context);
        db = myDbHelper.writableDatabase;
        return true
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri {
        // 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
        val table = getTableName(uri)

        // 向该表添加数据
        db.insert(table, null, values);

        // 当该URI的ContentProvider数据发生变化时,通知外界(即访问该ContentProvider数据的访问者)
        context!!.contentResolver?.notifyChange(uri, null)
        return uri;
    }
    
    override fun query(
        uri: Uri, projection: Array<String>?, selection: String?,
        selectionArgs: Array<String>?, sortOrder: String?
    ): Cursor? {
        // 根据URI匹配 URI_CODE,从而匹配ContentProvider中相应的表名
        // 该方法在最下面
        val table = getTableName(uri)

        // 查询数据
        return db.query(table, projection, selection, selectionArgs, null, null, sortOrder, null)
    }

    /**
     * 根据URI匹配 URI_CODE,从而匹配数据库中相应的表名
     */
    private fun getTableName(uri: Uri): String? {
        var tableName: String? = null
        when (mMatcher.match(uri)) {
            User_Code -> tableName = MyDBHelper.USER_TABLE_NAME
            Job_Code -> tableName = MyDBHelper.JOB_TABLE_NAME
        }
        return tableName
    }

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int {
        return 0;
    }

    override fun update(
        uri: Uri, values: ContentValues?, selection: String?,
        selectionArgs: Array<String>?
    ): Int {
        return 0
    }

    override fun getType(uri: Uri): String? {
        return null
    }

}
复制代码

外部应用使用时

findViewById<Button>(R.id.button).setOnClickListener {
    val uri = Uri.parse("content://zx.com.myContentProvider/user");
    val cursor = contentResolver.query(uri, arrayOf("_id", "name"), null, null, null)
    if (cursor != null) {
        Log.i("zx", "插入数据前")
        while (cursor.moveToNext()) {
            Log.i("zx", "_id=" + cursor.getInt(0) + ",name=" + cursor.getString(1))
        }
        cursor.close()
    }

    // 插入表中数据
    val values = ContentValues()
    values.put("name", "name" + SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Date()))
    contentResolver.insert(uri, values)

    //耗时查询应该使用AsyncQueryHandler,AsyncQueryHandler会开启子线程异步查询,不会阻塞主线程,实际项目中应该使用AsyncQueryHandler,或者自己新开一个线程。
    val afterInsertCursor =
        contentResolver.query(uri, arrayOf("_id", "name"), null, null, null)
    if (afterInsertCursor != null) {
        Log.i("zx", "插入数据后")
        while (afterInsertCursor.moveToNext()) {
            Log.i(
                "zx",
                "_id=" + afterInsertCursor.getInt(0) + ",name=" + afterInsertCursor.getString(
                    1
                )
            )
        }
        afterInsertCursor.close()
    }
}
复制代码

点击按钮后输出

插入数据前
插入数据后
_id=1,name=name2021-05-30 21:21:16
复制代码

再次点击按钮输出

插入数据前
_id=1,name=name2021-05-30 21:21:16
插入数据后
_id=1,name=name2021-05-30 21:21:16
_id=2,name=name2021-05-30 21:21:22
复制代码

外部应用中通过ContentProvider实现了查询和插入,更新和删除操作类似,不再赘述。

注意以下几点

  • ContentProvider的底层是Binder,当跨进程访问ContentProvider的时候,CRUD运行在Binder线程池中,不是线程安全的。而如果在同一个进程访问ContentProvider,根据Binder的原理,同进程的Binder调用就是直接的对象调用,这个时候CRUD运行在调用者的线程中。
  • ContentProvider的内部存储不一定是sqlite,它可以是任意数据。
  • ContentProvider的onCreate早于Application的onCreate而执行。

3个工具类

另外,Android 提供了3个用于辅助ContentProvider的工具类。

ContentUris

主要通过withAppendedId()parseId()操作URI

// withAppendedId()作用:向URI追加一个id
val uri = Uri.parse("content://zx.com.myContentProvider/user")
val resultUri = ContentUris.withAppendedId(uri, 7);
// 最终生成后的Uri为:content://zx.com.myContentProvider/user/7

// parseId()作用:从URL中获取ID
val uri1 = Uri.parse("content://zx.com.myContentProvider/user/7")
val personid = ContentUris.parseId(uri1);
//获取的结果为:7
复制代码

UriMatcher

根据Uri匹配对应的数据表,步骤如下:

//1.初始化UriMatcher,未匹配上时会返回-1
private val mMatcher: UriMatcher = UriMatcher(UriMatcher.NO_MATCH)

//2.指定uri的返回规则
mMatcher.addURI(AUTOHORITY, "user", User_Code)
mMatcher.addURI(AUTOHORITY, "job", Job_Code)
// 若URI资源路径 = content://zx.com.myContentProvider/user ,则返回User_Code
// 若URI资源路径 = content://zx.com.myContentProvider/job ,则返回Job_Code
//若URI中的authorities与指定的不相同,则返回NO_MATCH,如果authorities后的path没有匹配上也返回NO_MATCH

//3.使用UriMatcher获取规则中定义的code,根据code获取表名
when (mMatcher.match(uri)) {
    User_Code -> tableName = MyDBHelper.USER_TABLE_NAME
    Job_Code -> tableName = MyDBHelper.JOB_TABLE_NAME
}
复制代码

ContentObserver

根据Uri观察ContentProvider 中的数据变化,数据变化时自动回调ContentObserver的onChange(),使用示例如下:

//继承ContentObserver并实现onChange
class MyContentObserver(handler: Handler?) : ContentObserver(handler) {
	override fun onChange(selfChange: Boolean, uri: Uri?) {
		super.onChange(selfChange, uri)
		Log.i("zx", "数据改变了")
	}
}

//注册观察者
myContentObserver = MyContentObserver(null)
contentResolver.registerContentObserver(uri, true, myContentObserver)

// 在ContentProvider的增删改方法中,当该Uri的ContentProvider数据发生变化时,通知ContentObserver
context!!.contentResolver?.notifyChange(uri, null)

//解除观察者
contentResolver.unregisterContentObserver(myContentObserver)
复制代码

Activity

没什么好讲的,参照mp.weixin.qq.com/s/2O2dGQQpC…

setResult()调用时机

在Activity-A中用startActivityForResult()方法启动了Activity-B,B退回到A的过程中,生命周期如下:

B---onPause

A---onActivityResult

A---onRestart

A---onStart

A---onResume

B---onStop

B---onDestroy

B调用setResult()时必须在A的onActivityResult之前,这样A才能接收到返回值,所以B必须在onPause之前调用setResult(onPause里调用也是不行的)。常见的2种调用setResult()正确时机如下:

  1. finish()之前
  2. onBackPressed()里 super.onBackPressed()之前
文章分类
Android
文章标签