以下各个api的开发与测试均在车载设备上,可能在手机上不一定生效。
并且主动触发系统环境切换的方法,只有系统权限的app才可以调用。
触发入口调用 一般厂商,都会自己定义一个切换入口,像系统设置,或者桌面的通知页面。这里面去调用系统的api,来达到切换的目的。
主题切换 主题切换可以直接使用系统的UiModeManager即可。
private val uimodeManager =
appContext . getSystemService ( UI_MODE_SERVICE ) as UiModeManager
例如深浅主题切换调用:
// UiModeManager.java
/**
* Sets the system-wide night mode.
*
* @param mode the night mode to set
* @see #getNightMode()
* @see #setApplicationNightMode(int)
*/
public void setNightMode ( @NightMode int mode ) {
if ( mService != null ) {
try {
mService . setNightMode ( mode );
} catch ( RemoteException e ) {
throw e . rethrowFromSystemServer ();
}
}
}
我们自定义的触发按钮,按下时可以这样设置:
mMainView . findViewById < Button >( R . id . btn_night ). setOnClickListener {
uimodeManager . nightMode = UiModeManager . MODE_NIGHT_YES
}
mMainView . findViewById < Button >( R . id . btn_light ). setOnClickListener {
uimodeManager . nightMode = UiModeManager . MODE_NIGHT_NO
}
语言切换 通过反射调用ActivityManager的updatePersistentConfiguration方法,即可实现系统语言切换。
/**
* 切换语言
* @param language 语言
*/
fun changeLanguageSettings ( language : Locale ) {
try {
val activityManagerNative = Class . forName ( "android.app.ActivityManager" )
val am = activityManagerNative . getMethod ( "getService" ). invoke ( activityManagerNative )
val config = am ?. javaClass ?. getMethod ( "getConfiguration" )
?. invoke ( am ) as Configuration
config . setLocale ( language )
config . javaClass . getDeclaredField ( "userSetLocale" ). setBoolean ( config , true )
am . javaClass . getMethod ( "updatePersistentConfiguration" , config . javaClass )
. invoke ( am , config )
BackupManager . dataChanged ( "com.android.providers.settings" )
} catch ( e : Exception ) {
e . message ?. let { error ( it ) }
}
}
触发按钮调用:
mMainView . findViewById < Button >( R . id . btn_chinese ). setOnClickListener {
changeLanguageSettings ( Locale . SIMPLIFIED_CHINESE )
}
mMainView . findViewById < Button >( R . id . btn_english ). setOnClickListener {
changeLanguageSettings ( Locale . ENGLISH )
}
适配方app 在上面的app完成触发之后,系统会将环境切换到对应的深浅模式,或者对应的语言状态下,这时候其他的app就需要响应刷新自己的页面。
在开发过程中,可以按下面的几种方法来适配。
资源目录设置 首先不管是Activity应用还是浮窗应用,我们都需要在res资源目录下添加对应的语言和主题的资源目录。
语言目录
以英文为例,新建value-en目录,将翻译之后的strings.xml复制进去即可,字段的名称和中文目录是一致的。
主题目录
深色的主题资源放在带-night后缀的目录下。 例如图片等资源,放置于drawable-mdpi-night目录下,color字段放置于values-night目录下。和语言切换一样,图片,颜色等文件名称和浅色主题一致,切换的时候使用 R 类会自己索引。
逻辑代码 Activity型应用 切换主题和语言时,Activity都会销毁重建,按照触发顺序,大致为:
onConfigurationChanged:当系统配置发生变化时,例如屏幕方向改变或主题切换,Activity会首先调用onConfigurationChanged方法。你可以重写这个方法来处理配置变化,例如重新加载资源或更新UI。 onSaveInstanceState:在Activity可能被销毁之前,系统会调用onSaveInstanceState方法,允许你保存一些关键的状态信息,以便在Activity重新创建时恢复。 全部的流程如下 {thread:main(2) MainActivity:431 onPause}
{thread:main(2) MainActivity:495 onStop}
{thread:main(2) MainActivity:170 onSaveInstanceState}
{thread:main(2) MainActivity:500 onDestroy}
{thread:main(2) MainActivity:131 onCreate}
{thread:main(2) MainActivity:180 initData}
{thread:main(2) MainActivity:400 initView}
{thread:main(2) MainActivity:155 onStart}
{thread:main(2) MainActivity:175 onResume}
{thread:main(2) MainActivity:481 onWindowFocusChanged} onWindowFocusChanged hasFocus: true
重建之后,Activity会按照提前设置好的资源目录进行资源的获取,自动地刷新界面。切到浅色主题,就会拿取drawable-mdpi目录下的资源,深色主题则会拿取drawable-mdpi-night目录下的资源。
Service加浮窗型应用 在车机开发中,经常会设计一些临时性的悬浮窗app,例如天气,时间,快捷车控等功能。
这类app一般的架构为,开机之后,会启动一个Service,然后在Service中获取WindowManager,来进行悬浮窗的添加移除等管理操作。
// LanguageService.kt
private val mWmParams = WindowManager . LayoutParams (). apply {
//设置可以显示在状态栏上
flags = ( WindowManager . LayoutParams . FLAG_LAYOUT_IN_SCREEN
or WindowManager . LayoutParams . FLAG_LAYOUT_NO_LIMITS
or WindowManager . LayoutParams . FLAG_NOT_FOCUSABLE
or WindowManager . LayoutParams . FLAG_KEEP_SCREEN_ON
or WindowManager . LayoutParams . FLAG_WATCH_OUTSIDE_TOUCH
or WindowManager . LayoutParams . FLAG_NOT_TOUCH_MODAL )
type = WindowManager . LayoutParams . TYPE_APPLICATION_OVERLAY
//设置窗口长宽数据
width = WindowManager . LayoutParams . WRAP_CONTENT
height = WindowManager . LayoutParams . WRAP_CONTENT
gravity = Gravity . CENTER_HORIZONTAL or Gravity . TOP
x = 600
y = 0
format = PixelFormat . TRANSLUCENT
}
private val mWindowManager = appContext . getSystemService ( WINDOW_SERVICE ) as WindowManager
private lateinit var mMainView : View
fun showWindow (){
mMainView = LayoutInflater . from ( appContext ). inflate ( R . layout . layout_language_change , null , false )
mWindowManager . addView ( mMainView , mWmParams )
}
不像Activity都是自动化,高层级的悬浮窗app的生命周期比较复杂,需要我们自己去管理。
这时候系统不会自动去重走生命周期,刷新资源了,我们需要手动去切换主题和语言。
首先,需要在service中复写onConfigurationChanged方法。系统在语言和主题切换时,会调用这个方法。主题其他类型的配置切换,例如旋转屏幕等,也会走这个方法。
所以需要设置一个主题(语言)管理类,对比前后的状态,是否这个触发的类型是主题(语言)切换。
监听到了变化之后,有两种方案:
手动刷新置换资源 第一种,对于界面简单的浮窗界面,可以直接在onConfigurationChanged中,重新加载资源,然后重新设置布局。这种方法不用考虑窗口的状态,直接对每个View进行定点刷新,不容易出问题。
语言切换:
override fun onConfigurationChanged ( newConfig : Configuration ) {
super . onConfigurationChanged ( newConfig )
LogUtils . i ( TAG , "onConfigurationChanged" )
mMainView . findViewById < Button >( R . id . btn_chinese ). text = getString ( R . string . chinese )
mMainView . findViewById < Button >( R . id . btn_english ). text = getString ( R . string . english )
}
主题切换:
override fun onConfigurationChanged ( newConfig : Configuration ) {
super . onConfigurationChanged ( newConfig )
LogUtils . i ( TAG , "onConfigurationChanged" )
mMainView . setBackgroundColor (
ResourcesCompat . getColor ( this . resources , R . color . theme_test , null )
)
mMainView . findViewById < ImageView >( R . id . iv_close )
. setImageResource ( R . drawable . ic_close_dialog )
}
置空重建 第二种方案比较适合复杂的View,内部组件众多,挨个手动替换比较麻烦。
这时就可以模仿Activity的切换方式,直接移除掉之前的view,将其置空后,重新创建一个view,设置好子View的监听方法,然后重新添加到windowManager中。
override fun onConfigurationChanged ( newConfig : Configuration ) {
super . onConfigurationChanged ( newConfig )
LogUtils . i ( TAG , "onConfigurationChanged" )
mWindowManager . removeView ( mMainView )
mMainView = null
mMainView =
LayoutInflater . from ( appContext )
. inflate ( R . layout . layout_language_change , null , false )
initViews ()
mWindowManager . addView ( mMainView , mWmParams )
}
这种方法简单粗暴,但是必须要管控好窗口的状态和置空的时机,否则可能会导致内存泄漏。
而且这种方法也会导致窗口的闪烁,最好在系统切换时有一个过度的效果动画。
Settings.System数据库本来是用来存储用户偏好设置的机制,在车载系统开发时,由于车辆的设置信号都发给了底层的控制器ECU等去记忆了,这一块更多的是被用来做键值对存储和 IPC跨进程通信使用。
存储位置和权限 在Android中,用户的默认设置和偏好设置是存在数据库中,在Android 6.0 以前,settings的数据是存在settings.db中,系统可以通过sqlite来进行读写。 这样的话,所有的第三方应用都可以对settings.db进行操作,修改用户设置的数据。存储位置为,{system, secure, global} 对应的是目录 /data/data/com.android.providers.settings/databases/settings.db 的三个表。
所以在 在Android 6.0版本以后,SettingsProvider被重构,从安全和性能等方面考虑,把SettingsProvider中原本保存在settings.db中的数据,目前全部保存在XML文件中。一般位于 /data/system/users/0 目录下,该目录的settings_global.xml,settings_secure.xml和settings_system.xml三个xml文件就是SettingsProvider中的数据文件。
Settingsprovider中对数据也进行了分类,分别是Global、System、Secure、Ssaid四种类型,说明如下:
Global:所有的偏好设置对系统的所有用户公开,第三方APP有读没有写的权限; System:包含各种各样的用户偏好系统设置,第三方APP有读没有写的权限; Secure:安全性的用户偏好系统设置,第三方APP有读没有写的权限。 另外,还有一个不被熟知的Ssaid 表:此表包括所有应用的id;这样的话,只是可以从文件权限类型来做权限的管控,可以让第三方APP有读没有写的权限,或者直接不给读写权限等 车载使用很多,可以当作键值对存储使用,也可以多进程共享发通知使用。
第三方APP使用 读取数据 使用adb写入一个测试字段
montecarlo:/ # settings put global audio_test_result 234
使用Settings.Global.getInt来读取这个字段,可以看到显示。
CoroutineScope ( Dispatchers . IO ). launch {
val tesetData = Settings . Global . getInt ( this @MainActivity . contentResolver , "audio_test_result" )
Log . i ( TAG , "getGlobalData : $tesetData" )
}
监听 除了单次读取数据库的值,还可以通过 contentResolver.registerContentObserver 来添加持续的监听。
private val observer = object : ContentObserver ( null ) {
override fun onChange ( selfChange : Boolean ) {
val stringData =
Settings . System . getString (
appContext . contentResolver ,
ACTION_MUTUAL_NOTIFY
)
debugLog ( "onChange: data:$stringData" )
}
}
fun registerSystemSettingOberver () {
appContext . contentResolver . registerContentObserver (
Settings . System . getUriFor ( ACTION_MUTUAL_NOTIFY ),
true ,
observer
)
}
fun unRegisterSystemSettingOberver () {
appContext . contentResolver . unregisterContentObserver (
observer
)
}
尝试写入 CoroutineScope ( Dispatchers . IO ). launch {
Settings . Global . putInt ( this @MainActivity . contentResolver , "audio_test_result" , 2415 )
}
报错信息可以看到系统拒绝了第三方APP的写入操作:
Process: com.example.composedemo, PID: 28630
java.lang.SecurityException: Permission denial: writing to settings requires:android.permission.WRITE_SECURE_SETTINGS
at android.os.Parcel.createExceptionOrNull(Parcel.java:3011)
at android.os.Parcel.createException(Parcel.java:2995)
at android.os.Parcel.readException(Parcel.java:2978)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:190)
at android.database.DatabaseUtils.readExceptionFromParcel(DatabaseUtils.java:142)
at android.content.ContentProviderProxy.call(ContentProviderNative.java:732)
at android.provider.Settings$NameValueCache.putStringForUser(Settings.java:3017)
at android.provider.Settings$Global.putStringForUser(Settings.java:16970)
at android.provider.Settings$Global.putString(Settings.java:16811)
at android.provider.Settings$Global.putInt(Settings.java:17041)
at com.example.composedemo.MainActivity$onCreate$1.invokeSuspend(MainActivity.kt:42)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:108)
at kotlinx.coroutines.internal.LimitedDispatcher$Worker.run(LimitedDispatcher.kt:115)
at kotlinx.coroutines.scheduling.TaskImpl.run(Tasks.kt:103)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:584)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:793)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:697)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:684)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@d6b0268, Dispatchers.IO]
Caused by: android.os.RemoteException: Remote stack trace:
at com.android.providers.settings.SettingsProvider.enforceWritePermission(SettingsProvider.java:2299)
at com.android.providers.settings.SettingsProvider.mutateGlobalSetting(SettingsProvider.java:1452)
at com.android.providers.settings.SettingsProvider.insertGlobalSetting(SettingsProvider.java:1406)
at com.android.providers.settings.SettingsProvider.call(SettingsProvider.java:450)
at android.content.ContentProvider.call(ContentProvider.java:2511)
系统APP的通信 系统app可以写入系统数据库的内容,很多场景下也被用来作为 IPC 多进程通信的方式。
有两种使用场景
数据传输 一方来修改需要传输的值,另一方监听变化读取获取。
修改数据库的值,以字符串数据为例,调用 Settings.System.putString()
修改方:
fun changeSystemSettingData () {
val tsStringData = "test string"
Settings . System . putString (
appContext . contentResolver ,
ACTION_MUTUAL_NOTIFY ,
tsStringData
)
}
接收方,这里和第三方APP没有区别:
private val observer = object : ContentObserver ( null ) {
override fun onChange ( selfChange : Boolean ) {
val stringData =
Settings . System . getString (
appContext . contentResolver ,
ACTION_MUTUAL_NOTIFY
)
debugLog ( "onChange: data:$stringData" )
}
}
fun registerSystemSettingOberver () {
appContext . contentResolver . registerContentObserver (
Settings . System . getUriFor ( ACTION_MUTUAL_NOTIFY ),
true ,
observer
)
}
fun unRegisterSystemSettingOberver () {
appContext . contentResolver . unregisterContentObserver (
observer
)
}
单次通知式 这种类似于广播,做触发式的逻辑,但是希望点对点建立通信协议,互相发通知。
一般是在一个单独的Service里面加入监听和发送通知的逻辑,同时还要屏蔽自己发出去的通知。
修改数据库的值,以字符串数据为例,调用 Settings.System.putString()
需要注意的是,这里的onChange回调是只有在变化时才会调用的。如果两次写入的是一样的值,接收方是收不到通知的。
所以这种连续发通知式的调用,再调用写入之后,还要调用notifyChange()方法。
fun changeSystemSettingData () {
val tsStringData = "same string"
Settings . System . putString (
appContext . contentResolver ,
ACTION_MUTUAL_NOTIFY ,
tsStringData
)
appContext . contentResolver . notifyChange (
Settings . System . getUriFor ( ACTION_MUTUAL_NOTIFY ), null
)
}
如果是两方需要互相发通知怎么办呢,自己发出去的修改,自己的observer也收到了。
这时候我们打开notifyChange的源码看一看:
/**
* Notify registered observers that a row was updated and attempt to sync
* changes to the network.
* <p>
* To observe events sent through this call, use
* {@link #registerContentObserver(Uri, boolean, ContentObserver)}.
* <p>
* Starting in {@link android.os.Build.VERSION_CODES#O}, all content
* notifications must be backed by a valid {@link ContentProvider}.
*
* @param uri The uri of the content that was changed.
* @param observer The observer that originated the change, may be
* <code>null</null>. The observer that originated the change
* will only receive the notification if it has requested to
* receive self-change notifications by implementing
* {@link ContentObserver#deliverSelfNotifications()} to return
* true.
*/
public void notifyChange ( @NonNull Uri uri , @Nullable ContentObserver observer ) {
notifyChange ( uri , observer , true /* sync to network */ );
}
在调用notifyChange的时候,将第二个参数observer传入自己这方的监听器,同时observer在继承的时候,需要复写deliverSelfNotifications()方法返回true,这样在自己发通知的时候,onChangee方法的回调selfChange标志位会被正确的置为true,可以用以筛选。
private val observer = object : ContentObserver ( null ) {
override fun onChange ( selfChange : Boolean ) {
val stringData =
Settings . System . getString (
appContext . contentResolver ,
ACTION_MUTUAL_NOTIFY
)
debugLog ( "onChange: data:$stringData selfchange:$selfChange" )
}
override fun deliverSelfNotifications () = true
}
ContentProvider 见另一篇文章: 【2022-9-12-四大组件之ContentProvider】
内部存储(Internal Storage) 内部存储用于存储应用私有的数据,其他应用无法访问。数据存储在应用的内部目录中,注意,此处的文件会随应用的卸载而删除。适合存储应用的缓存文件,配置文件等。
路径: /storage/emulated/0/Android/data/${packageName}/files
存储代码示例,以下是一个app管理功能中存储应用图标Drawable文件到内部缓存目录:
/**
* 存储图标png到app内部目录
*/
private fun saveDrawableToFile ( context : Context , drawable : Drawable , fileName : String ) {
val bitmap = drawable . toBitmap ()
val file = File ( context . getExternalFilesDir ( null ), "$fileName.png" )
val fos = FileOutputStream ( file )
infoLog ( "path:${file.absolutePath}" )
bitmap . compress ( Bitmap . CompressFormat . PNG , 100 , fos )
fos . flush ()
fos . close ()
}
外部存储(External Storage) 用于存储公共的、可共享的数据,其他应用可以访问。数据存储在设备的外部存储设备(如SD卡)上,即使应用被卸载,数据仍然保留。适合存储用户生成的文件,如照片、视频等。
路径一般为sdcard内。此处的存储操作想要顺利完成,一般需要手动申请运行时权限。
存储代码示例,往sdcard的Pictures目录下写一张图片:
/**
* 保存一个bitmap到本地sdcard的Pictures目录
*/
fun saveImageToGallery (
context : Context ,
bitmap : Bitmap ,
filename : String = "FilmSimulation.jpg" ,
) {
val values = ContentValues (). apply {
put ( MediaStore . MediaColumns . DISPLAY_NAME , filename )
put ( MediaStore . Images . Media . MIME_TYPE , "image/jpeg" ) // 文件类型
put ( MediaStore . MediaColumns . RELATIVE_PATH , Environment . DIRECTORY_PICTURES )
}
runCatching {
context . contentResolver . insert ( MediaStore . Images . Media . EXTERNAL_CONTENT_URI , values )
?. let {
context . contentResolver . openOutputStream ( it ) ?. apply {
bitmap . compress ( Bitmap . CompressFormat . JPEG , 100 , this )
flush ()
close ()
}
context . contentResolver . notifyChange ( it , null )
}
}. onFailure {
Log . e ( TAG , "save image error:${it.message}" )
}
}
数据库存储 数据库存储用于存储结构化的数据,如用户信息、聊天记录等。数据库存储可以支持复杂的查询和关联操作。
数据库存储的路径一般在
/storage/emulated/0/Android/data/${packageName}/databases
像下面的用以举例的 Demo,数据库路径如下:
emu64xa:/data/data/com.example.roomdemo/databases # ls
camera_database camera_database-shm camera_database-wal
Room数据库介绍 SQLite数据库
Android平台选取了SqLlite数据库作为结构化数据库的存储方案。
用于存储结构化的数据,如用户信息、聊天记录等。轻量级的关系型数据库,支持SQL查询,适合存储大量的结构化数据。应用可极大地受益于在本地保留这些数据。最常见的使用场景是缓存相关的数据,这样一来,当设备无法访问网络时,用户仍然可以在离线状态下浏览该内容。
长期以来,SQLite 数据库繁杂的使用体验,也让开发者们感到困惑。
Room 持久性库在 SQLite 上提供了一个抽象层,以便在充分利用 SQLite 的强大功能的同时,能够流畅地访问数据库。具体来说,Room 具有以下优势:
提供针对 SQL 查询的编译时验证。 提供方便注解,可最大限度减少重复和容易出错的样板代码。 简化了数据库迁移路径。 使用Room数据库 首先添加依赖库,app级的 build.gradle.kts 依赖添加:
val room_version = "2.6.1"
implementation ( "androidx.room:room-runtime:$room_version" )
/**
* KSP (Kotlin Symbol Processing)是以 Kotlin 优先的 kapt 替代方案。
* KSP 可直接分析 Kotlin 代码,使得速度提高多达 2 倍。
* 此外,它还可以更好地了解 Kotlin 的语言结构。
*/
ksp ( "androidx.room:room-compiler:$room_version" )
Kapt背景知识: Kapt可以将 Java 注解处理器与 Kotlin 代码搭配使用,即使这些处理器没有特定的 Kotlin 支持也是如此。方法是从 Kotlin 文件生成 Java 桩,然后处理器就可以读取这些桩。生成桩是一项成本高昂的操作,并且对构建速度有很大影响。
而KSP 可以说是Kapt的升级替代方案,它是一个 Kotlin 编译器插件,它可以在编译时读取和分析 Kotlin 代码,然后生成 Java 代码。KSP 可以直接分析 Kotlin 代码,而不需要通过 Java 桩。这使得 KSP 可以更好地了解 Kotlin 的语言结构。
如果没有提前添加ksp插件,上面的依赖引入应该是报红的。
添加KSP插件
项目顶级build.gradle.kts文件: plugins {
id ( "com.google.devtools.ksp" ) version "2.0.21-1.0.27" apply false
}
app级的build.gradle.kts文件: plugins {
id ( "com.google.devtools.ksp" )
}
Room数据库组件 Room 包含三个主要组件:
数据库类,用于保存数据库并作为应用持久性数据底层连接的主要访问点。 数据实体,用于表示应用的数据库中的表。 数据访问对象 (DAO),为您的应用提供在数据库中查询、更新、插入和删除数据的方法。 也就是说,至少需要定义三个类,才可以使用Room数据库来存储数据。
数据实体 数据实体是数据库中表的映射。数据实体是一个类,需要添加 @Entity 注解。
@Entity
data class Camera (
@PrimaryKey
val cameraId : Int ,
@ColumnInfo ( name = "brand_name" ) val brandName : String ,
@ColumnInfo ( name = "camera_model" ) val cameraModel : String ,
)
数据访问对象 (DAO) 数据访问对象 (DAO) 是用于在数据库中执行查询和更新的接口。数据访问对象 (DAO) 是一个接口,需要添加 @Dao 注解。
@Dao
interface CameraDao {
@Query ( "SELECT * FROM camera" )
fun getAll (): List < Camera >
@Query ( "SELECT * FROM camera WHERE cameraId IN (:cameraIds)" )
fun loadAllByIds ( cameraIds : IntArray ): List < Camera >
@Query ( "SELECT * FROM camera WHERE brand_name LIKE :first AND " + "camera_model LIKE :last LIMIT 1" )
fun findByName ( first : String , last : String ): Camera
@Insert
fun insertAll ( vararg cameras : Camera )
@Delete
fun delete ( camera : Camera )
}
数据库类 数据库类是应用的入口点,用于访问应用的数据库。
数据库类为应用提供与该数据库关联的 DAO 的实例。反过来,应用可以使用 DAO 从数据库中检索数据,作为关联的数据实体对象的实例。此外,应用还可以使用定义的数据实体更新相应表中的行,或者创建新行供插入。
数据库类是一个抽象类,需要继承它,并且需要添加 @Database 注解。
@Database 注解有两个参数:entities 和 version。entities 是一个数组,用于指定数据库中包含的实体类。version 是一个整数,用于指定数据库的版本号。
@Database ( entities = [ Camera :: class ], version = 1 )
abstract class CameraDatabase : RoomDatabase () {
abstract fun cameraDao (): CameraDao
}
数据库使用 数据库类是应用的入口点,用于访问应用的数据库。
使用 Room.databaseBuilder 方法来创建数据库实例。然后,可以使用 CameraDatabase 中的抽象方法获取 DAO 的实例,转而可以使用 DAO 实例中的方法与数据库进行交互。
下面是Demo测试代码,注意要在 IO 线程中进行数据库的创建,读写等操作:
CoroutineScope ( Dispatchers . IO ). launch {
val db = Room . databaseBuilder ( appContext , CameraDatabase :: class . java , "camera_database" ). build ()
val camereDao = db . cameraDao ()
camereDao . insertAll (
Camera ( 1000 , "Canon" , "EOS R6 II" ),
Camera ( 1001 , "Sony" , "A9 II" ),
Camera ( 1002 , "LUMIX" , "S5M2K" )
)
delay ( 3000L )
Log . i ( TAG , "room database test: ${camereDao.getAll()}" )
}
四种流行的键值对存储 前三种方案对比结论,来自扔物线朱凯大佬的测试数据。
SharedPreferences 如果您有想要保存的相对较小键值对集合,则可以使用 SharedPreferences API。SharedPreferences 对象指向包含键值对的文件,并提供读写这些键值对的简单方法。每个 SharedPreferences 文件均由框架进行管理,可以是私有文件,也可以是共享文件。
键值对的存储在移动开发里非常常见。比如深色模式的开关、软件语言、字体大小,这些用户偏好设置,很适合用键值对来存。
SharedPreferences 使用起来很简单,但是有性能问题,容易卡顿,甚至有时候会出现 ANR。
MMKV 腾讯开源了一个叫做 MMKV 的项目。它和 SP 一样,都是做键值对存储的,可是它的性能比 SP 强很多。
MMKV的开发背景:
微信在遇到一些无法处理的字符的时候,会出现崩溃的问题,而微信为了及时地找出导致崩溃的字符或者字符串,所有的对话内容在显示之前,先保存到磁盘再显示。而且防止崩溃之后数据还没存好,必须要在主线程去完成这个写操作,耗时就绝对无法避免。一帧的时间也就 16 毫秒,在16 毫秒里来个写磁盘的操作,用户很可能就会感受到一次卡顿。如果用户点开了一个活跃的群,这个群里有几百条没看过的消息。而如果把这几条消息都记录下来,是不是每条消息的记录都会涉及一次写磁盘的操作?这几次写磁盘行为,是发生在同一帧里的,所以在这一帧里因为记录文字而导致的主线程耗时,也会相比起刚才的例子翻上好几倍,卡顿时间就同样也会翻上好几倍。
最终微信找到了解决方案。使用了一种叫做内存映射(mmap())的底层方法。
它可以让系统为你指定的文件开辟一块专用的内存,这块内存和文件之间是自动映射、自动同步的关系,你对文件的改动会自动写到这块内存里,对这块内存的改动也会自动写到文件里。
有了这一层内存作为中间人,我们就可以用「写入内存」的方式来实现「写入磁盘」的目标了。它在程序崩溃的时候,并不会随着进程一起被销毁掉,而是会继续有条不紊地把它里面还没同步完的内容同步到它所映射的文件里面去。
MMKV缺点:
MMKV 优势:
SP和DataStore对比 DataStore 被创造出来的目标就是替代 SharedPreferences,而它解决的 SharedPreferences 最大的问题有两点:一是性能问题,二是回调问题。
先说性能问题:SharedPreferences 虽然可以用异步的方式来保存更改,以此来避免 I/O 操作所导致的主线程的耗时。
但在 Activity 启动和关闭的时候,Activity 会等待这些异步提交完成保存之后再继续,这就相当于把异步操作转换成同步操作了,从而会导致卡顿甚至 ANR(程序未响应)。
这是为了保证数据的一致性而不得不做的决定,但它也确实成为了 SharedPreferences 的一个弱点。
但是,SharedPreferences 所导致的卡顿和 ANR,是非常低概率的事件。
读取文件卡顿 其实除了写数据时的卡顿,SharedPreferences 在读取数据的时候也会卡顿。
虽然它的文件加载过程是在后台进行的,但如果代码在它加载完成之前就去尝试读取键值对,线程就会被卡住,直到文件加载完成,而如果这个读取的过程发生在主线程,就会造成界面卡顿,并且数据文件越大就会越卡。
这种卡顿,不是 SharedPreferences 独有的,MMKV 也是存在的,因为它初始化的过程同样也是从磁盘里读取文件,而且是一股脑把整个文件读完,所以耗时并不会比 SharedPreferences 少。
而 DataStore,就没有这种问题。DataStore 不管是读文件还是写文件,都是用的协程在后台进行读写,所有的 I/O 操作都是在后台线程发生的,所以不论读还是写,都不会卡主线程。
DataStore回调更方便 DataStore 解决的 SharedPreferences 的另一个问题就是回调。
SharedPreferences 如果使用同步方式来保存更改commit(),会导致主线程的耗时;
但如果使用异步的方式,给它加回调又很不方便,也就是如果你想做一些「等这个异步提交完成之后再怎么怎么样」的工作,会很麻烦。
而 DataStore 由于是用协程来做的,线程的切换是非常简单的,你就把「保存完成之后做什么」直接写在保存代码的下方就可以了,很直观、很简单。
对比来说,MMKV 虽然没有使用协程,但是它太快了,所以大多数时候并不需要切线程也不会卡顿。
三种方式的总结 区别大概就是这么些区别了,大致总结一下就是:
如果你有多进程支持的需求,可以选择MMKV,也可以选择DataStore(1.1.0版本新增);如果你有高频写入的需求,你也应该优先考虑 MMKV。但如果你使用 MMKV,一定要知道它是可能丢失数据的,不过概率很低就是了,所以你要在权衡之后做好决定:是自行实现数据的备份和恢复方案,还是直接接受丢数据的事实,在每次丢失数据之后帮用户把相应的数据进行初始化。
DataStore 在任何时候都不会卡顿,而 MMKV 在写大字符串和初次加载文件的时候是可能会卡顿的,而且初次加载文件的卡顿不是概率性的,只要文件大到了引起卡顿的程度,就是 100% 的卡顿。
三种方案提取的工具类 在我之前研究AOSP redfin平台的项目的时候,在CommonHelper库里面对这三种存储方式都做了一个很简单的工具类。
SharedPreferences /**
* SharedPreference存储工具类
* 不会丢数据
* 但是加回调不方便
*/
object SPHelper {
private lateinit var share : SharedPreferences
private lateinit var editor : SharedPreferences . Editor
private const val SHARED_NAME = "SPHelper"
fun init ( context : Context ) {
share = context . getSharedPreferences ( SHARED_NAME , Context . MODE_PRIVATE );
editor = share . edit ();
}
// 采用同步保存,获取保存成功与失败的result
fun putString ( key : String , value : String ?): Boolean {
infoLog ( "putString key: $key, value: $value" )
editor . putString ( key , value )
return editor . commit ()
}
fun getString ( key : String ): String ? {
val value = share . getString ( key , null )
infoLog ( "getString key: $key, value: $value" )
return value
}
fun getString ( key : String , defaultValue : String ): String {
val value = share . getString ( key , null )
infoLog ( "getString key: $key, value: $value, defaultValue: $defaultValue" )
return value ?: defaultValue
}
fun putLong ( key : String ?, value : Long ): Boolean {
infoLog ( "putLong key: $key, value: $value" )
editor . putLong ( key , value )
return editor . commit ()
}
fun putFloat ( key : String ?, value : Float ): Boolean {
infoLog ( "putFloat key: $key, value: $value" )
editor . putFloat ( key , value )
return editor . commit ()
}
fun putInt ( key : String ?, value : Int ): Boolean {
infoLog ( "putInt key: $key, value: $value" )
editor . putInt ( key , value )
return editor . commit ()
}
fun putBoolean ( key : String ?, value : Boolean ): Boolean {
infoLog ( "putBoolean key: $key, value: $value" )
editor . putBoolean ( key , value )
return editor . commit ()
}
fun getLong ( key : String ?): Long {
val value = share . getLong ( key , - 1 )
infoLog ( "getLong key: $key, value: $value" )
return value
}
fun getInt ( key : String ?, defaultValue : Int ): Int {
val value = share . getInt ( key , defaultValue )
infoLog ( "getInt key: $key, value: $value" )
return value
}
fun getFloat ( key : String ?, defaultValue : Float ): Float {
val value = share . getFloat ( key , defaultValue )
infoLog ( "getFloat key: $key, value: $value" )
return value
}
fun getBoolean ( key : String ?, defaultValue : Boolean ): Boolean {
val value = share . getBoolean ( key , defaultValue )
infoLog ( "getBoolean key: $key, value: $value" )
return value
}
fun removeSharedPreferenceByKey ( key : String ?): Boolean {
infoLog ( "removeSharedPreferenceByKey key: $key" )
editor . remove ( key )
return editor . commit ()
}
}
MMKV /**
* 最适合同步写入小数据
* 支持多进程,高频写入性能好
* 但有可能丢数据
*/
object MMKVHelper {
private lateinit var mmkv : MMKV
fun init ( context : Context , databaseId : String , isMultiProcess : Boolean ) {
val rootDir = MMKV . initialize ( context )
infoLog ( "MMKV rootDir: $rootDir" )
mmkv =
if ( isMultiProcess ) MMKV . mmkvWithID ( databaseId , MMKV . MULTI_PROCESS_MODE )
else MMKV . mmkvWithID ( databaseId )
}
fun putString ( key : String , value : String ?) {
infoLog ( "putString key: $key, value: $value" )
mmkv . encode ( key , value )
}
fun getString ( key : String ): String ? {
val value = mmkv . decodeString ( key )
infoLog ( "getString key: $key, value: $value" )
return value
}
fun getString ( key : String , defaultValue : String ): String {
val value = mmkv . decodeString ( key )
infoLog ( "getString key: $key, value: $value, defaultValue: $defaultValue" )
return value ?: defaultValue
}
fun putLong ( key : String ?, value : Long ) {
infoLog ( "putLong key: $key, value: $value" )
mmkv . encode ( key , value )
}
fun putFloat ( key : String ?, value : Float ) {
infoLog ( "putFloat key: $key, value: $value" )
mmkv . encode ( key , value )
}
fun putInt ( key : String ?, value : Int ) {
infoLog ( "putInt key: $key, value: $value" )
mmkv . encode ( key , value )
}
fun putBoolean ( key : String ?, value : Boolean ) {
infoLog ( "putBoolean key: $key, value: $value" )
mmkv . encode ( key , value )
}
fun getLong ( key : String ?): Long {
val value = mmkv . decodeLong ( key , - 1 )
infoLog ( "getLong key: $key, value: $value" )
return value
}
fun getInt ( key : String ?, defaultValue : Int ): Int {
val value = mmkv . decodeInt ( key , defaultValue )
infoLog ( "getInt key: $key, value: $value" )
return value
}
fun getFloat ( key : String ?, defaultValue : Float ): Float {
val value = mmkv . decodeFloat ( key , defaultValue )
infoLog ( "getFloat key: $key, value: $value" )
return value
}
fun getBoolean ( key : String ?, defaultValue : Boolean ): Boolean {
val value = mmkv . decodeBool ( key , defaultValue )
infoLog ( "getBoolean key: $key, value: $value" )
return value
}
}
DataStore /**
* 谷歌推荐的最新存储方式
* 协程实现,可以方便地获取存储的结果回调
*/
object DataStoreHelper {
// 定义一个 DataStore 实例
val Context . dataStore : DataStore < Preferences > by preferencesDataStore ( name = "data_store_settings" )
private lateinit var outDataStore : DataStore < Preferences >
/**
* 初始化,使get和set不受Context域限制
*/
fun init ( context : Context ) {
outDataStore = context . dataStore
}
// 定义一个 suspend 函数,用于从 DataStore 中读取数据
suspend fun < T > getData ( key : Preferences . Key < T >, defaultValue : T ): T {
return ( outDataStore . data . first ()[ key ] ?: defaultValue )
}
// 定义一个 suspend 函数,用于将数据保存到 DataStore 中
suspend fun < T > saveData ( key : Preferences . Key < T >, value : T ) {
outDataStore . edit { preferences ->
preferences [ key ] = value
}
}
}
使用:
CoroutineScope ( Dispatchers . IO ). launch {
val INT_PREF_KEY = intPreferencesKey ( "IntKey" )
val FLOAT_PERF_KEY = floatPreferencesKey ( "FloatKey" )
val STRING_PREF_KEY = stringPreferencesKey ( "SteringKey" )
DataStoreHelper . saveData ( INT_PREF_KEY , 45 )
DataStoreHelper . saveData ( FLOAT_PERF_KEY , 45.0f )
DataStoreHelper . saveData ( STRING_PREF_KEY , "45" )
delay ( 1000L )
// 拿取刚刚存的值
Log . i ( TAG , "intData: ${DataStoreHelper.getData(INT_PREF_KEY, -1)}" )
Log . i ( TAG , "floatData: ${DataStoreHelper.getData(FLOAT_PERF_KEY, -1f)}" )
Log . i ( TAG , "stringData: ${DataStoreHelper.getData(STRING_PREF_KEY, " default ")}" )
}
Setting.System系统数据库 这里在车载上使用的更多,单独写了一篇总结:
【Android系统数据库通信使用方式】
Fragment 是 Android UI 开发中一个非常重要的组件,用于构建模块化、可复用且灵活的用户界面。
Fragment 可以被视为一个Activity 的一部分或行为 。它拥有自己的生命周期、布局和输入事件,但它必须托管在一个 Activity 中。一个 Activity 可以包含一个或多个 Fragment,也可以在不同的 Activity 中复用同一个 Fragment。
Fragment 的主要作用 模块化 UI: 可以将一个复杂的用户界面分解成独立的、可管理的模块。UI 可复用性: 可以在不同的 Activity 或同一 Activity 的不同配置(如横竖屏)中复用 Fragment。适应不同屏幕尺寸: 尤其在平板电脑等大屏幕设备上,可以同时显示多个 Fragment,例如列表-详情布局(List-Detail Flow)。简化 Activity 代码: 将 UI 逻辑和行为从 Activity 中分离出来,使 Activity 变得更轻量和专注于协调。支持回退栈: 可以像 Activity 一样管理 Fragment 的回退栈,实现前进和后退导航。生命周期 Fragment 的生命周期与它所依附的 Activity 的生命周期紧密相关 。理解这些回调方法对于正确管理 Fragment 的状态至关重要。
以下是 Fragment 生命周期中几个关键的方法及其大致顺序:
onAttach() : 当 Fragment 与 Activity 关联时调用。此时可以获取到 Context 对象。onCreate() : Fragment 被创建时调用。在这里进行非 UI 的初始化,如变量设置、数据加载等。onCreateView() : 创建 Fragment 的用户界面(View)。在这里膨胀(inflate)布局并返回根视图。onViewCreated() : onCreateView() 返回后调用。在这里可以初始化 View 组件,设置监听器等。onActivityCreated() : 当宿主 Activity 的 onCreate() 方法完成时调用。可以在这里执行依赖于 Activity 已创建的代码。onStart() : Fragment 可见时调用。onResume() : Fragment 获得焦点并可与用户交互时调用。onPause() : Fragment 失去焦点,但仍然部分可见时调用(例如,另一个 Fragment 覆盖了它)。onStop() : Fragment 不再可见时调用。onDestroyView() : Fragment 的视图被移除时调用。在这里释放与 View 相关的资源。onDestroy() : Fragment 实例被销毁时调用。在这里释放所有非 View 相关的资源。onDetach() : Fragment 与 Activity 解除关联时调用。使用流程 1. 创建 Fragment 创建一个继承自 androidx.fragment.app.Fragment 的 Java/Kotlin 类,并通常重写 onCreateView() 方法来提供其布局:
class MyFragment : Fragment () {
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
// Fragment 初始化逻辑
}
override fun onCreateView (
inflater : LayoutInflater , container : ViewGroup ?,
savedInstanceState : Bundle ?
): View ? {
// 膨胀 Fragment 的布局
return inflater . inflate ( R . layout . fragment_my , container , false )
}
override fun onViewCreated ( view : View , savedInstanceState : Bundle ?) {
super . onViewCreated ( view , savedInstanceState )
// 初始化 View 组件
// val myTextView = view.findViewById<TextView>(R.id.myTextView)
// myTextView.text = "Hello from Fragment!"
}
}
对应的 fragment_my.xml 布局文件:
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android"
android:layout_width= "match_parent"
android:layout_height= "match_parent" >
<TextView
android:id= "@+id/myTextView"
android:layout_width= "wrap_content"
android:layout_height= "wrap_content"
android:text= "My Fragment Content"
android:textSize= "24sp"
android:layout_gravity= "center" />
</FrameLayout>
2. 将 Fragment 添加到 Activity 有两种主要方式将 Fragment 添加到 Activity 中:
a. 在布局 XML 中声明 你可以在 Activity 的布局 XML 文件中直接声明一个 Fragment。这是 静态添加 Fragment 的方式。
<LinearLayout xmlns:android= "http://schemas.android.com/apk/res/android"
android:layout_width= "match_parent"
android:layout_height= "match_parent"
android:orientation= "vertical" >
<fragment
android:id= "@+id/my_static_fragment"
android:name= "com.example.yourapp.MyFragment" // 完整的 Fragment 类名
android:layout_width= "match_parent"
android:layout_height= "match_parent" />
</LinearLayout>
这种方式下,Fragment 的生命周期会与 Activity 的生命周期紧密耦合,并且在 Activity 创建时就被实例化。
b. 运行时动态添加(推荐) 通过 FragmentManager 和 FragmentTransaction 在 Activity 运行时动态添加、移除、替换或显示/隐藏 Fragment。这是最常用的方式,因为它提供了更大的灵活性。
class MainActivity : AppCompatActivity () {
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
// 检查 Fragment 是否已经添加,避免重复添加(例如在配置变化后)
if ( savedInstanceState == null ) {
val fragmentManager : FragmentManager = supportFragmentManager
val fragmentTransaction : FragmentTransaction = fragmentManager . beginTransaction ()
val myFragment = MyFragment ()
// 将 Fragment 添加到一个容器视图中 (例如一个 FrameLayout)
fragmentTransaction . add ( R . id . fragment_container , myFragment )
// fragmentTransaction.addToBackStack(null) // 可选:添加到回退栈
fragmentTransaction . commit ()
}
}
}
初始化添加时最好是先行检查一下 savedInstanceState 是否为 null,避免重复添加 Fragment。如果是系统的配置变更,如语言和主题,我们知道Activity会自动重建,而FagmentManager 会在 Activity 重建时,自动恢复那些在 Activity 被销毁前已经存在的 Fragment 实例。如果此时又调用了 fragmentTransaction.add() 方法添加 Fragment,就会导致重复添加,引发异常,界面内容可能会重叠显示多个fragment。
对应的 activity_main.xml 布局需要一个用于容纳 Fragment 的容器:
<FrameLayout xmlns:android= "http://schemas.android.com/apk/res/android"
android:id= "@+id/fragment_container"
android:layout_width= "match_parent"
android:layout_height= "match_parent" />
Fragment 的通信 由于 Fragment 之间是独立的,它们之间以及与宿主 Activity 之间需要明确的通信机制。
Fragment 到 Activity:
推荐方式: 定义一个接口,让 Activity 实现该接口。Fragment 通过 onAttach() 获取 Activity 实例并将其转换为接口类型,然后调用接口方法。// Fragment
class MyFragment : Fragment () {
interface OnMessageListener {
fun onMessageFromFragment ( message : String )
}
private var listener : OnMessageListener ? = null
override fun onAttach ( context : Context ) {
super . onAttach ( context )
if ( context is OnMessageListener ) {
listener = context
} else {
throw RuntimeException ( "$context must implement OnMessageListener" )
}
}
// ... 某个事件触发时
fun sendMessage () {
listener ?. onMessageFromFragment ( "Hello from Fragment!" )
}
override fun onDetach () {
super . onDetach ()
listener = null
}
}
// Activity
class MainActivity : AppCompatActivity (), MyFragment . OnMessageListener {
override fun onMessageFromFragment ( message : String ) {
Log . d ( "MainActivity" , "Received message: $message" )
}
// ...
}
ViewModel (推荐,尤其是 Fragment 间通信): 使用共享的 ViewModel 可以非常方便地在 Fragment 和 Activity 之间共享数据和通信,尤其是在导航组件的场景下。Activity 到 Fragment:
调用 Fragment 公开方法: Activity 可以获取 Fragment 实例并直接调用其公共方法。// Activity
fun sendMessageToFragment ( message : String ) {
val myFragment = supportFragmentManager . findFragmentById ( R . id . fragment_container ) as ? MyFragment
myFragment ?. updateText ( message )
}
// Fragment
class MyFragment : Fragment () {
fun updateText ( message : String ) {
// 更新 TextView
}
}
通过 Bundle 传递参数: 在创建 Fragment 实例时,通过 setArguments(Bundle) 方法传递参数。// Activity
val args = Bundle (). apply {
putString ( "key_message" , "Data from Activity" )
}
val myFragment = MyFragment (). apply {
arguments = args
}
// Fragment
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
val message = arguments ?. getString ( "key_message" )
// 使用 message
}
Fragment 到 Fragment:
通过宿主 Activity 中转(旧版,不推荐): 一个 Fragment 通知 Activity,Activity 再通知另一个 Fragment。共享 ViewModel (推荐): 多个 Fragment 可以观察同一个 ViewModel 中的 LiveData,实现数据共享和通信。Parent-to-Child FragmentManager (如果存在嵌套 Fragment): 可以通过 getParentFragmentManager() 或 getChildFragmentManager() 获取对应的 FragmentManager。Navigation Component (推荐): 使用 Android Navigation 组件是处理 Fragment 之间导航和参数传递的现代化且强大的方式。FragmentTransaction 和回退栈 当使用 FragmentManager 动态管理 Fragment 时,FragmentTransaction 是执行操作(如添加、移除、替换)的批处理API。
add(containerId, fragment): 将一个 Fragment 添加到容器中。remove(fragment): 移除一个 Fragment。replace(containerId, fragment): 移除容器中现有 Fragment,然后添加新的 Fragment。hide(fragment) / show(fragment): 隐藏或显示一个 Fragment,但不会销毁其 View。addToBackStack(name): 将当前 FragmentTransaction 添加到 Activity 的回退栈中。当用户按返回键时,会依次弹出栈中的 Fragment 事务,回退到之前的 Fragment 状态。commit(): 提交事务。这是异步操作。commitNow(): 提交事务。这是同步操作,但可能会阻塞 UI 线程,除非确定操作很快。通常不推荐。commitAllowingStateLoss(): 提交事务,即使 Activity 状态已保存,允许状态丢失。一般不推荐,除非你清楚这样做的后果。最佳实践与注意事项 避免 Fragment 嵌套过多: 复杂的 Fragment 嵌套会导致生命周期管理变得困难,并可能引入性能问题。使用 setArguments() 传递参数: 避免在 Fragment 构造函数中传递参数,因为系统可能会在屏幕旋转等情况下重新创建 Fragment 而不调用自定义构造函数。Fragment 应该尽可能独立和可复用: 它们不应该直接依赖于特定的 Activity 类型,而是通过接口或 ViewModel 进行通信。处理配置变更: Fragment 在 Activity 重建时也会重建。确保在 onSaveInstanceState() 和 onCreate()/onCreateView() 中正确保存和恢复 Fragment 的状态。内存泄漏: 注意在 onDestroyView() 或 onDestroy() 中释放不再需要的引用(尤其是对 View 的引用),以避免内存泄漏。例如,清理在 onCreateView() 中创建的监听器。getChildFragmentManager() vs getFragmentManager() / getParentFragmentManager():getParentFragmentManager() (原 getFragmentManager()):用于获取管理当前 Fragment 的 FragmentManager。getChildFragmentManager():用于获取管理当前 Fragment 内部嵌套 Fragment 的 FragmentManager。在使用 FragmentContainerView 或 supportFragmentManager.beginTransaction() 动态添加 Fragment 时,请确保使用正确的 FragmentManager。 Navigation Component: 对于复杂的导航和 Fragment 间的通信,强烈推荐使用 Android Jetpack 的 Navigation Component。它简化了 Fragment 的管理、深层链接和安全参数传递。View Binding 或 Data Binding: 在 Fragment 中使用 View Binding 或 Data Binding 可以更安全、高效地访问 View 组件,避免 findViewById 带来的空指针异常。常见用例 标签页(Tabbed Layouts): 每个标签页内容可以是一个 Fragment。滑动视图(Swipe Views / ViewPager2): ViewPager2 经常与 FragmentStateAdapter 结合使用,每个页面都是一个 Fragment。大屏幕设备布局: 例如,在平板上,一个 Fragment 显示列表,另一个 Fragment 显示详情。底部导航栏(Bottom Navigation): 每个导航项对应一个 Fragment。向导流(Wizard Flows): 多个 Fragment 按顺序引导用户完成任务。作为一名 Android 开发者,Activity 绝对是你最常用、也是最重要的组件。它是用户界面的单一入口点,承载着应用与用户交互的各种操作。你可以把它想象成应用中的一个“屏幕”或“页面”。
Activity 提供一个绑定好的窗口,你可以在其中使用各种View和ViewGroup来绘制 UI 界面(如按钮、文本框、图片等),供用户进行交互。 Activity 拥有一套定义好的生命周期回调方法,应用开发者根据这些回调来配置特定的任务,比如在创建的时候配置View的交互行为,数据初始化,销毁时释放资源。 每个 Activity 实例都与一个任务(task)相关联。当用户启动应用时,系统会为它创建一个任务,并在这个任务中管理 Activity 的堆栈。 Activity 也可以启动其他 Activity(包括自己应用内或第三方应用的 Activity),并通过 Intent 和 Bundle 传递数据。 生命周期 理解 Activity 的生命周期是 Android 开发的基石。当你用户在应用中导航、接电话、切换应用等操作时,Activity 的状态会发生变化,系统会调用相应的回调方法。
以下是 Activity 生命周期中的几个核心方法:
onCreate() :何时调用: Activity 首次创建时调用。作用: 进行 Activity 的初始化工作,如设置布局(setContentView())、初始化视图组件、绑定数据、恢复 savedInstanceState 中的数据等。这是你放置大部分一次性设置代码的地方。onStart() :何时调用: Activity 可见时调用,无论是因为首次创建还是从后台回到前台。作用: Activity 即将对用户可见,但尚未获得用户焦点。onResume() :何时调用: Activity 获得用户焦点并可与用户交互时调用。作用: 在这里启动动画、访问设备相机或传感器等独占性资源。任何需要 Activity 处于前台才能进行的轻量级操作都应放在这里。onPause() :何时调用: Activity 失去焦点时调用,例如用户点击返回键,或启动了另一个 Activity 但当前 Activity 仍然部分可见(如弹出一个对话框),或屏幕关闭。作用: 在这里暂停动画、释放独占性资源(如相机预览),并保存任何需要持久化的小量数据(但不要在这里执行耗时操作,因为下一个 Activity 必须等待当前 Activity 的 onPause() 执行完毕才能 onResume())。onStop() :何时调用: Activity 不再可见时调用,例如用户切换到另一个应用、按下 Home 键、或启动了一个完全覆盖当前 Activity 的新 Activity。作用: 释放那些在 Activity 不可见时不再需要的资源。重量级 CPU 操作,例如向数据库写入数据,应该在这里完成。onDestroy() :何时调用: Activity 被系统销毁时调用。这可能是以下原因之一:用户通过按下返回键完全退出 Activity。 系统为了回收资源而销毁 Activity(例如,当内存不足时)。 配置变更(如屏幕旋转、主题切换)导致 Activity 重新创建。 作用: 释放所有在 onCreate() 中创建的资源,如解绑广播接收器、关闭数据库游标、停止后台线程等。可以用下面这个图来概括:
Activity 状态和数据保存 当 Activity 被销毁后又重建时(例如屏幕旋转,主题切换),你可能需要恢复之前的用户界面状态或数据。Activity提供了以下两个方法,分别用于在Activity被销毁前和重建后保存和恢复数据:
onSaveInstanceState(Bundle outState) :Activity 即将被销毁,但未来可能会被重新创建时调用(例如屏幕旋转、内存不足导致系统回收)。 你可以将少量瞬态数据(如 UI 状态、滚动位置等)保存到 Bundle 中。这个 Bundle 会在 Activity 重新创建时通过 onCreate() 方法传递回来。 onRestoreInstanceState(Bundle savedInstanceState) :在 onStart() 之后,并且仅当 Activity 之前因系统原因被销毁并重新创建时调用。 在这里恢复 onSaveInstanceState() 中保存的数据。通常,也可以在 onCreate() 中通过 savedInstanceState 参数来恢复这些数据。 注意: 对于大量数据或需要长期保存的数据,不应依赖 onSaveInstanceState()。而应该使用 Room 数据库、SharedPreferences(建议迁移到DataStore)或 ViewModel 来持久化保存数据。
启动 Activity 和数据传递 你可以使用 Intent 来启动其他 Activity。
// 启动一个显式 Activity (明确指定要启动的 Activity 类)
val intent = Intent ( this , SecondActivity :: class . java )
startActivity ( intent )
// 启动 Activity 并传递数据
val dataIntent = Intent ( this , DetailActivity :: class . java ). apply {
putExtra ( "item_id" , 123 )
putExtra ( "item_name" , "Awesome Product" )
}
startActivity ( dataIntent )
// 在 DetailActivity 中获取 Intent 的数据
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// val itemId = intent.getIntExtra("item_id", -1)
// val itemName = intent.getStringExtra("item_name")
// }
// 4. 启动 Activity 并获取返回结果 (旧方法,现在推荐 Activity Result API)
// override fun onCreate(...) {
// val startForResult = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
// if (result.resultCode == Activity.RESULT_OK) {
// val data: Intent? = result.data
// val message = data?.getStringExtra("return_message")
// // 处理返回的数据
// }
// }
//
// // 在某个点击事件中启动
// myButton.setOnClickListener {
// val intent = Intent(this, ResultActivity::class.java)
// startForResult.launch(intent)
// }
// }
如果一个Activity已经启动到前台了,但是其他组件仍然调用了startActivity,这个时候一般会回调 onNewIntent() 方法,可以在这个回调里获取数据。
Activity 任务栈 (Task Stack) Android 系统通过 任务(Task) 来管理 Activity 的组织结构。一个任务是用户执行某项工作时与之交互的 Activity 的集合。这些 Activity 被组织在一个“后退栈”(Back Stack)中,以 堆栈(LIFO,后进先出) 的形式排列。
当用户启动一个新 Activity 时,它会被推送到当前任务栈的顶部,并成为焦点。 当用户按下返回键时,栈顶的 Activity 会被弹出并销毁,前一个 Activity 恢复到顶部。 当栈中最后一个 Activity 被弹出时,这个task任务就不再存在。 你可以在 AndroidManifest.xml 中使用 android:launchMode 来调整 Activity 的启动模式 ,从而影响其在任务栈中的行为,有以下四种启动模式:
standard (默认): 每次启动都会创建新的实例。singleTop: 也叫栈顶复用,如果目标 Activity 已经在栈顶,则不会创建新实例,而是调用其 onNewIntent() 方法。singleTask: 也叫栈内复用,确保一个任务栈中只有一个该 Activity 的实例。如果实例已存在于任何位置,则将其移动到栈顶并清理其上方的所有 Activity。singleInstance: 类似于 singleTask,单例模式,但它会创建一个全新的任务来包含这个 Activity,并且这个任务中只能有这一个 Activity。Activity 和 Fragment 的关系 一般稍微大型一点的项目,都会使用 Activity 和 Fragment 一起工作。理解它们的区别和联系至关重要:
Activity 是骨架: Activity 提供应用窗口和基本框架,管理整个屏幕的生命周期。Fragment 是模块: Fragment 是 Activity 的一部分,有自己的生命周期和布局,但必须依附于 Activity。它用于构建模块化、可复用的 UI 片段。分工合作: Activity 负责协调不同 Fragment 之间的交互、处理系统事件,而 Fragment 负责管理其内部的 UI 逻辑和数据。跳转和创建流程 可以参考应用冷启动流程的文章,详细介绍了进程初始化和Activity内部窗口,DecorView和contentView的绑定流程。
APP冷启动流程解析
车载Android开发和手机端的一个重大区别之一,就是车载Android设备通常拥有多个屏幕,比如一个主屏幕和一个副屏幕。在手机端通常只需要一个屏幕,但是在车载Android开发中,我们很多时候需要同时处理多个屏幕。
例如现在相当一部分的新能源车拥有主副驾两块大屏幕,主驾显示的界面为导航和车辆状态等,副驾屏幕用来显示一些娱乐app的流媒体等。甚至很多车企,还会有吸顶屏幕给后排乘客使用。
本文将介绍Android多屏开发主流的两种实现方式:Presentation和Activity。以下两种方式默认层级场景下,无系统权限的app也可以使用。
多屏设备的获取 首先,我们需要获取到多屏设备的信息,包括屏幕的数量、屏幕的尺寸、屏幕的方向等。
在Android中,我们可以通过DisplayManager来获取到多屏设备的信息。
fun getConnectedScreenIds ( context : Context ): List < Int > {
val displayManager = context . getSystemService ( Context . DISPLAY_SERVICE ) as DisplayManager
val displays = displayManager . displays
val screenIds = mutableListOf < Int >()
for ( display in displays ) {
screenIds . add ( display . displayId )
display . name
infoLog ( "displayId = ${display.displayId}, name = ${display.name}" )
}
return screenIds
}
其中,displayId是屏幕的唯一标识符,displayName是屏幕的名称。
其中display.getSize方法已经废弃,改为采用windowmanager中获取密度的方法来获取尺寸。
每次开关机之后,,displayId有可能不是固定的,主要看系统厂商是否对多屏的id进行了重置。
Presentation Presentation是Android提供的一种用于显示UI的类,它可以在一个单独的窗口中显示UI。
Presentation继承自Dialog类,因此它也可以设置窗口的属性,比如窗口的大小、窗口的位置等。
设置Presentation时,需要传入一个Context,一个Display对象,以确定Presentation的显示位置。然后在onCreate方法中设置UI即可。
class MyPresentation ( context : Context , display : Display ) : Presentation ( context , display ) {
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . presentation )
}
}
创建实例,调用show方法。同时,我们也可以精细地定义其显示层级,显示大小,以及是否可以点击外部取消。
val displayManager =
this @MainActivity . getSystemService ( DISPLAY_SERVICE ) as DisplayManager
val display = displayManager . getDisplay ( 5 )
val presentation = MyPresentation ( this @MainActivity , display )
presentation . setCancelable ( false )
presentation . show ()
Activity 第二种方案可以直接使用Activity来实现多屏开发。
使用到了ActivityOptions类,官方定义为,其是一个用于构建一个选项Bundle的辅助类,该Bundle可与Context.startActivity(Intent, Bundle)及相关方法一起使用。
我们可以利用它来定义转场动画等关于显示的很多参数。这里用到了其指定displayid来显示的功能。
fun startPassengerActivity() {
val intent = Intent().apply {
setComponent(ComponentName("com.stephen.redfindemo", "com.stephen.redfindemo.PassengerActivity"))
setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK)
}
val displayId = 5;
val options = ActivityOptions.makeBasic();
options.setLaunchDisplayId(displayId);
try {
this.startActivity(intent, options.toBundle());
} catch (e: Exception) {
e.printStackTrace()
}
}
通常包括以下步骤
定义AIDL接口文件 首先,通信的服务端和客户端,需要创建一个AIDL文件来定义接口。AIDL文件类似于Java接口文件,但它使用AIDL语法。例如,你可以创建一个名为IAirConditionerService.aidl的文件,并在其中定义你的接口方法。客户端和服务端,需要在同一个包名的文件夹里面创建。
interface IAIRConditionerService {
void setTemperature ( int temperature );
int getTemperature ();
// 其他方法...
}
编译AIDL文件 在Android Studio中,点击build之后,AIDL文件会自动编译。编译后,会生成一个Java接口文件,位于app/build/generated/source/aidl/debug/目录下(假设你的项目是在debug模式下编译的)。这个生成的Java接口文件包含了与AIDL文件中定义的接口相对应的方法。
实现服务端的Service类 接下来,你需要创建一个Service类来实现AIDL接口。这个Service类将处理来自客户端的请求。在Service类中,你需要实现AIDL接口中定义的所有方法。
import android.app.Service ;
import android.content.Intent ;
import android.os.IBinder ;
import android.os.RemoteException ;
public class AirConditionerService extends Service {
private final IAIRConditionerService . Stub binder = new IAIRConditionerService . Stub () {
@Override
public void setTemperature ( int temperature ) throws RemoteException {
// 实现设置温度的逻辑
}
@Override
public int getTemperature () throws RemoteException {
// 实现获取温度的逻辑
return 0 ;
}
// 其他方法的实现...
};
@Override
public IBinder onBind ( Intent intent ) {
return binder ;
}
}
然后在AndroidManifest.xml中注册Service:你需要在AndroidManifest.xml文件中注册你的Service,以便系统能够找到并启动它。
客户端调用 在客户端应用中,你可以通过绑定到Service来调用AIDL接口中的方法。首先,你需要创建一个ServiceConnection对象,并在其中实现onServiceConnected和onServiceDisconnected方法。然后,你可以使用bindService方法来绑定到Service。
// 客户端Activity或Service
public class AirConditionerClientActivity extends AppCompatActivity {
private IAIRConditionerService airConditionerService ;
private ServiceConnection serviceConnection = new ServiceConnection () {
@Override
public void onServiceConnected ( ComponentName name , IBinder service ) {
airConditionerService = IAIRConditionerService . Stub . asInterface ( service );
try {
int temperature = airConditionerService . getTemperature ();
// 处理返回的温度值
} catch ( RemoteException e ) {
e . printStackTrace ();
}
}
@Override
public void onServiceDisconnected ( ComponentName name ) {
airConditionerService = null ;
}
};
@Override
protected void onCreate ( Bundle savedInstanceState ) {
super . onCreate ( savedInstanceState );
setContentView ( R . layout . activity_air_conditioner_client );
Intent intent = new Intent ( this , AirConditionerService . class );
bindService ( intent , serviceConnection , Context . BIND_AUTO_CREATE );
}
@Override
protected void onDestroy () {
super . onDestroy ();
unbindService ( serviceConnection );
}
}
一般手机平台上,两个三方app之间要建立这个连接,客户端需要在AndroidManifest里面加一条权限声明:
<uses-permission
android:name= "android.permission.QUERY_ALL_PACKAGES"
tools:ignore= "QueryAllPackagesPermission" />
实际的例子 这是我的第一个Android项目,模仿网易云音乐开发了一个约等于静态界面的app,体验了AIDL接口的跨进程通信。
客户端:“网抑云”app 服务端:一个扫描sdcard内音乐文件的服务进程 直接先看项目文件:
Server
首先就是aidl文件夹,里面定义了通信的接口声明aidl和需要传输的实体类Music的aidl.
AIDLtest.aidl
package com.example.server ;
import com.example.server.Music ;
interface AIDLtest {
int add ( int num1 , int num2 );
List < Music > getmusiclist ();
List < Music > addmusic ();
}
我们扫描到音频文件之后,需要将信息通过Music类包装传过去。
Music.aidl
// Music.aidl
package com.example.server ;
parcelable Music ;
Mudic.java
package com.example.server ;
import android.os.Parcel ;
import android.os.Parcelable ;
public class Music implements Parcelable {
public String name ;
public String singer ;
public Music () {
}
protected Music ( Parcel in ) {
name = in . readString ();
singer = in . readString ();
}
public static final Creator < Music > CREATOR = new Creator < Music >() {
@Override
public Music createFromParcel ( Parcel in ) {
return new Music ( in );
}
@Override
public Music [] newArray ( int size ) {
return new Music [ size ];
}
};
public String getName () {
return name ;
}
public void setName ( String name ) {
this . name = name ;
}
public String getSinger () {
return singer ;
}
public void setSinger ( String singer ) {
this . singer = singer ;
}
@Override
public String toString () {
return "Music{" +
"name='" + name + '\'' +
", singer='" + singer + '\'' +
'}' ;
}
@Override
public int describeContents () {
return 0 ;
}
@Override
public void writeToParcel ( Parcel dest , int flags ) {
dest . writeString ( name );
dest . writeString ( singer );
}
}
java类需要实现Parcelable接口才可以顺利传输,写法是比较固定的。
Client
客户端建立aidl文件夹时,各个类所属的包名一定要和服务端一致,可以看到图片中aidl文件夹是直接从服务端移植过来的。
需要注意的是,实体类Music,java文件的定义,也需要和服务端的包名一致,这里是单独建了一个包来放置它。
至于后面的调用流程就和上面一节提到的是一致的了。
车载Android上普遍的方式 上面的是手机端比较基础的通信集成方式,在车载Android领域,需要跨进程通信时,往往会采取对客户端最友好的方式来实现。
我们会在服务端的代码仓库里面直接新建一个libary的模组。
或者单独做一个仓库,来放置不同app的对外接口文件。
在这些地方,由服务端,也就是接口提供方的开发工程师,来完成客户端的Service连接逻辑,并把方法包装成aar提供给需求方。
然后客户端需求方的项目去集成这个aar,自己决定什么时机调用。
本文中源码来自郭神的《第一行代码》(第三版)。
概念 ContentProvider,即内容提供者,主要用于在不同应用之间共享数据。它提供了一种标准的方式来访问和操作应用程序中的数据。ContentProvider可以被多个应用程序共享,并且可以在不同的应用程序之间进行数据的共享。
ContentProvider的创建与使用 ContentP rovider 的用法一般有两种:
使用现有的ContentP rovider 对于每一个应用程序来说,如果想要访问ContentProvider 中共享的数据,就一定要借助 ContentResolver 类,可以通过 Context 中的 getContentResolver() 方法获取该类的实例。 ContentResolver 中提供了一系列的方法用于对数据进行增删改查操作,其中insert()方法用于添加数据,update()方法用于更新数据,delete()方法用于删除数据,query()方法用于查询数据。
ContentResolver 中的增删改查方法都是不接收表名参数的,而是使用一个Uri参数代替,这个参数被称为内容URI。内容URI给ContentProvider 中的数据建立了唯一标识符,它主要由两部分组成:authority 和path 。authority 是用于对不同的应用程序做区分的,一般为了避免冲突,会采用应用包名的方式进行命名。path 则是用于对同一应用程序中不同的表做区分的,通常会添 加到authority 的后面。
内容URI最标准的格式如下:
content://com.example.app.provider/table1
content://com.example.app.provider/table2
在得到了内容URI字符串之后,我们还需要将它解析成Uri对象才可以作为参数传入。解析的方法也相当简单,代码如下所示:
val uri = Uri . parse ( "content://com.example.app.provider/table1" )
只需要调用Uri.parse()方法,就可以将内容URI字符串解析成Uri对象了。
拿到uri对象之后,就可以调用ContentResolver中的增删改查方法了。
使用cursor对象读取数据时,需要使用moveToFirst()方法将游标移动到第一行数据的位置,然后使用getColumnIndex()方法获取每一列数据的列索引,最后使用getString()方法获取指定列索引对应的数据。
代码如下:
/** uri from table_name 指定查询某个应用程序下的某一张表
projection select column1, column2 指定查询的列名
selection where column = value 指定where的约束条件
selectionArgs - 为where中的占位符提供具体的值
sortOrder order by column1, column2 指定查询结果的排序方式
*/
val cursor = contentResolver . query (
uri ,
projection ,
selection ,
selectionArgs ,
sortOrder )
while ( cursor . moveToNext ()) {
val column1 = cursor . getString ( cursor . getColumnIndex ( "column1" ))
val column2 = cursor . getInt ( cursor . getColumnIndex ( "column2" ))
}
cursor . close ()
// 更新数据
val values = ContentValues ()
values . put ( "column1" , "value1" )
values . put ( "column2" , 1 )
val rows = contentResolver . update (
uri ,
values ,
selection ,
)
// 删除数据
val rows = contentResolver . delete (
uri ,
selection ,
selectionArgs
)
// 插入数据
val values = ContentValues ()
values . put ( "column1" , "value1" )
values . put ( "column2" , 1 )
val uri = contentResolver . insert (
uri ,
values
)
以读取系统联系人为例,代码如下:
private fun readContacts () {
// 查询联系人数据
contentResolver . query ( ContactsContract . CommonDataKinds . Phone . CONTENT_URI ,
null , null , null , null ) ?. apply {
while ( moveToNext ()) {
// 获取联系人姓名
val displayName = getString ( getColumnIndex (
ContactsContract . CommonDataKinds . Phone . DISPLAY_NAME ))
// 获取联系人手机号
val number = getString ( getColumnIndex (
ContactsContract . CommonDataKinds . Phone . NUMBER ))
contactsList . add ( "$displayName\n$number" )
}
adapter . notifyDataSetChanged ()
close ()
}
}
注意需要提前检查和申请权限:
<uses-permission android:name= "android.permission.READ_CONTACTS" />
运行时权限申请:
public class MainActivity : AppCompatActivity () {
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
if ( ContextCompat . checkSelfPermission ( this , Manifest . permission . READ_CONTACTS )
!= PackageManager . PERMISSION_GRANTED ) {
ActivityCompat . requestPermissions ( this ,
arrayOf ( Manifest . permission . READ_CONTACTS ), 1 )
} else {
readContacts ()
}
}
}
创建自己的ContentProvider 首先在包名文件夹上右键New->Other->Content Provider。设置好ContentProvider的名称和authority,然后点击Finish。就可以在Manifest里看到对应的类和authority了。
然后在ContentProvider类中重写onCreate()、query()、insert()、update()、delete()方法。
<provider
android:name= ".MyContentProvider"
android:authorities= "com.example.app.provider"
android:exported= "true" />
Kotlin代码实现类如下:
class DatabaseProvider : ContentProvider () {
private val bookDir = 0
private val bookItem = 1
private val categoryDir = 2
private val categoryItem = 3
private val authority = "com.example.databasetest.provider"
private var dbHelper : MyDatabaseHelper ? = null
private val uriMatcher by lazy {
val matcher = UriMatcher ( UriMatcher . NO_MATCH )
matcher . addURI ( authority , "book" , bookDir )
matcher . addURI ( authority , "book/#" , bookItem )
matcher . addURI ( authority , "category" , categoryDir )
matcher . addURI ( authority , "category/#" , categoryItem )
matcher
}
override fun onCreate () = context ?. let {
dbHelper = MyDatabaseHelper ( it , "BookStore.db" , 2 )
true
} ?: false
override fun query (
uri : Uri , projection : Array < String >?, selection : String ?,
selectionArgs : Array < String >?, sortOrder : String ?
) = dbHelper ?. let {
// 查询数据
val db = it . readableDatabase
val cursor = when ( uriMatcher . match ( uri )) {
bookDir -> db . query (
"Book" , projection , selection , selectionArgs ,
null , null , sortOrder
)
bookItem -> {
val bookId = uri . pathSegments [ 1 ]
db . query (
"Book" , projection , "id = ?" , arrayOf ( bookId ), null , null ,
sortOrder
)
}
categoryDir -> db . query (
"Category" , projection , selection , selectionArgs ,
null , null , sortOrder
)
categoryItem -> {
val categoryId = uri . pathSegments [ 1 ]
db . query (
"Category" , projection , "id = ?" , arrayOf ( categoryId ),
null , null , sortOrder
)
}
else -> null
}
cursor
}
override fun insert ( uri : Uri , values : ContentValues ?) = dbHelper ?. let {
// 添加数据
val db = it . writableDatabase
val uriReturn = when ( uriMatcher . match ( uri )) {
bookDir , bookItem -> {
val newBookId = db . insert ( "Book" , null , values )
Uri . parse ( "content://$authority/book/$newBookId" )
}
categoryDir , categoryItem -> {
val newCategoryId = db . insert ( "Category" , null , values )
Uri . parse ( "content://$authority/category/$newCategoryId" )
}
else -> null
}
uriReturn
}
override fun update (
uri : Uri , values : ContentValues ?, selection : String ?,
selectionArgs : Array < String >?
) = dbHelper ?. let {
// 更新数据
val db = it . writableDatabase
val updatedRows = when ( uriMatcher . match ( uri )) {
bookDir -> db . update ( "Book" , values , selection , selectionArgs )
bookItem -> {
val bookId = uri . pathSegments [ 1 ]
db . update ( "Book" , values , "id = ?" , arrayOf ( bookId ))
}
categoryDir -> db . update ( "Category" , values , selection , selectionArgs )
categoryItem -> {
val categoryId = uri . pathSegments [ 1 ]
db . update ( "Category" , values , "id = ?" , arrayOf ( categoryId ))
}
else -> 0
}
updatedRows
} ?: 0
override fun delete ( uri : Uri , selection : String ?, selectionArgs : Array < String >?) =
dbHelper ?. let {
// 删除数据
val db = it . writableDatabase
val deletedRows = when ( uriMatcher . match ( uri )) {
bookDir -> db . delete ( "Book" , selection , selectionArgs )
bookItem -> {
val bookId = uri . pathSegments [ 1 ]
db . delete ( "Book" , "id = ?" , arrayOf ( bookId ))
}
categoryDir -> db . delete ( "Category" , selection , selectionArgs )
categoryItem -> {
val categoryId = uri . pathSegments [ 1 ]
db . delete ( "Category" , "id = ?" , arrayOf ( categoryId ))
}
else -> 0
}
deletedRows
} ?: 0
override fun getType ( uri : Uri ) = when ( uriMatcher . match ( uri )) {
bookDir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.book"
bookItem -> "vnd.android.cursor.item/vnd.com.example.databasetest.provider.book"
categoryDir -> "vnd.android.cursor.dir/vnd.com.example.databasetest.provider.category"
categoryItem -> "vnd.android.cursor.item/vnd.com.example.databasetest.provider.category"
else -> null
}
}
首先,在类的一开始,同样是定义了4个变量,分别用于表示访问Book 表中的所有数据、访问Book 表中的单条数据、访问Category 表中的所有数据和访问Category 表中的单条数据。然后在一个by lazy代码块里对UriMatcher进行了初始化操作,将期望匹配的几种URI格式添加了进去。by lazy代码块是Kotlin 提供的一种懒加载技术,代码块中的代码一开始并不会执行,只有当uriMatcher变量首次被调用的时候才会执行,并且会将代码块中最后一行代码的返回值赋给uriMatcher。
使用方的调用:
class MainActivity : AppCompatActivity () {
var bookId : String ? = null
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
addData . setOnClickListener {
// 添加数据
val uri = Uri . parse ( "content://com.example.databasetest.provider/book" )
val values = contentValuesOf ( "name" to "A Clash of Kings" ,
"author" to "George Martin" , "pages" to 1040 , "price" to 22.85 )
val newUri = contentResolver . insert ( uri , values )
bookId = newUri ?. pathSegments ?. get ( 1 )
}
queryData . setOnClickListener {
// 查询数据
val uri = Uri . parse ( "content://com.example.databasetest.provider/book" )
contentResolver . query ( uri , null , null , null , null ) ?. apply {
while ( moveToNext ()) {
val name = getString ( getColumnIndex ( "name" ))
val author = getString ( getColumnIndex ( "author" ))
val pages = getInt ( getColumnIndex ( "pages" ))
val price = getDouble ( getColumnIndex ( "price" ))
Log . d ( "MainActivity" , "book name is $name" )
Log . d ( "MainActivity" , "book author is $author" )
Log . d ( "MainActivity" , "book pages is $pages" )
Log . d ( "MainActivity" , "book price is $price" )
}
close ()
}
}
updateData . setOnClickListener {
// 更新数据
bookId ?. let {
val uri = Uri . parse ( "content://com.example.databasetest.provider/
book / $ it ")
val values = contentValuesOf ( "name" to "A Storm of Swords" ,
"pages" to 1216 , "price" to 24.05 )
contentResolver . update ( uri , values , null , null )
}
}
deleteData . setOnClickListener {
// 删除数据
bookId ?. let {
val uri = Uri . parse ( "content://com.example.databasetest.provider/
book / $ it ")
contentResolver . delete ( uri , null , null )
}
}
}
}
添加数据的时候,首先调用了Uri.parse()方法将一个内容URI解析成Uri对象,然后把要添加的数据都存放到ContentValues对象中,接着调用ContentResolver的insert()方法执行添加操作就可以了。注意,insert()方法会返回一个Uri对象,这个对象中包含了新增数据的id,我们通过getPathSegments()方法将这个id取出,稍后会用到它。
查询数据的时候,同样是调用了Uri.parse()方法将一个内容URI解析成Uri对象,然后调用ContentResolver的query()方法查询数据,查询的结果当然还是存放在Cursor对象中。之后对Cursor进行遍历,从中取出查询结果,并一一打印出来。
更新数据的时候,也是先将内容URI解析成Uri对象,然后把想要更新的数据存放到ContentValues对象中,再调用ContentResolver的update()方法执行更新操作就可以了。注意,这里我们为了不想让Book 表中的其他行受到影响,在调用Uri.parse()方法时,给内容URI的尾部增加了一个id,而这个id正是添加数据时所返回的。这就表示我们只希望更新刚刚添加的那条数据,Book 表中的其他行都不会受影响。
删除数据的时候,也是使用同样的方法解析了一个以id结尾的内容URI,然后调用 ContentResolver的delete()方法执行删除操作就可以了。由于我们在内容URI里指定了一个id,因此只会删掉拥有相应id的那行数据,Book 表中的其他数据都不会受影响。
任务栈 应用任务栈定义 Activity的任务栈Task Stacks,是用户在执行某项工作时与之互动的一系列 Activity 的集合。
这些 Activity 按照每个 Activity 打开的顺序排列在一个 TaskRecord 中。TaskRecord即任务栈,或者叫返回堆栈 ( back Stack ) ,是一种栈的数据结构,按照“后进先出”的规则管理着其中的元素。
Task的前后台切换 一般来说,每一个app都有自己的任务栈,应用冷启动时,系统首先会创建一个Task name 为应用包名的一个Task,应用的主Activity放入栈底作为根Activity,后面打开新的Activity就会依次压入栈中。
用户按Home键退回桌面,这个Task就退回后台,在后台时,Task 中的所有 Activity 都会停止,但Task 的 TaskRecord 会保持不变,这样一来,用户再次点击图标时,Task 就可以返回到“前台”,以便用户可以从他们离开的地方继续操作。 如果用户是按返回键,则Task内的Activity就会被一个个推出去,最后根Activity也退出,这个Task就不存在了。 Task里的顺序不会变,只会进出。所以用户多次打开同一个界面时,默认会重复创建多次这个Activity。在按返回键时又会依次显示。想改变这种默认的方式,可以通过定义启动模式来确定 Activity 的新实例如何与当前的 Task 关联。
Activity属性 Activity在Manifest里可配置的相关的属性:
<activity
android:taskAffinity= "string"
android:allowTaskReparenting= ["true" | "false"]
android:alwaysRetainTaskState= ["true" | "false"]
android:clearTaskOnLaunch= ["true" | "false"]
android:finishOnTaskLaunch= ["true" | "false"]
android:launchMode= ["standard" | "singleTop" | "singleTask" | "singleInstance"] />
taskAffinity亲和性表示这个Activity更倾向于加载到哪一个栈里。一般都默认设置成包名所对应这个Task里。
allowTaskReparenting当下一次将启动 Activity 的 Task 转至前台时,Activity 是否能从该 Task 转移至与其有相似性的 Task 。Activity启动时会默认记录在拉起它的那个Task内,这个属性如果是true,那么下次这个Activity亲和性对应的Task被创建出来的时候,它就会切到其倾向的Task的栈顶,如果这个属性为false,那其就会留在这个Task内。
LaunchMode 主要有四种:
standard(标准模式) 是Activity默认的启动模式,即当前Activity未显式设置启动模式情况下,其启动模式为standard。在standard模式下,每次启动该Activity系统都会创建一个新的实例(instance)并压入当前任务栈顶,不论是否已有相同Activity实例存在。Activity实例数量没有限制,具体取决于任务栈深度。该模式适用于大多数情景,例如在一个应用中打开多个页面。
SingleTop 当Activity启动模式设置为singleTop时,系统启动该Activity时若发现当前任务栈栈顶 (并非系统全局) 为该Activity实例,则会直接复用该实例(调用onNewIntent()方法)而不会重新创建;若当前任务栈栈顶不为该Activity实例时,则会创建新的实例无论当前或其他任务栈中是否已存在对应实例。该模式适用于在同一任务中频繁跳转回当前页面的场景,例如从通知点击进入消息页面。
singletop的应用:避免多次创建,比如点击一个按钮启动一个activity,如果快速点击多次会导致反复启动,一种办法是在点击事件里过滤,另一个办法是设置目标activity是singletop
SingleTask 当Activity启动模式设置为singleTask时,系统启动该Activity时若发现存在一个任务栈 (系统全局) 中存在该Activity实例时,则会直接复用该实例(调用onNewIntent()方法)而不会重新创建,同时将该任务栈中位于该实例之上的其它Activity实例都将会被弹出栈,该实例作为当前任务栈栈顶;
若所有任务栈中都不存在该Activity实例时,则会在当前任务栈中创建该Activity实例作为栈顶。该实例在所有任务栈中唯一存在。该模式适用于“主页”类Activity,或者需要保证该Activity在整个应用中只有一个实例的场景,如应用的主界面、设置界面。
一般从主页跳转到各个Activity,都希望可以只保留一个主页的实例,而不是多个。AS创建的项目,MainActivity的默认配置就是singleTask。
SingleInstance 当Activity启动模式设置为singleInstance时, 系统全局 只允许存在该Activity的一个实例,并且该Activity将独占一个任务栈。
系统启动该Activity时,若所有任务栈中都不存在该Activity实例时,系统会使用一个独立的任务栈,并在该栈中创建该Activity实例,该任务栈只用于管理该Activity实例,唯一存在;当该Activity实例已经存在于独立的任务栈中时,系统会直接复用该实例,该实例在所有任务栈中唯一存在。
该模式适用于锁屏页面、视频播放等需要独立管理的Activity,防止干扰。
,强制的单实例模式。该模式下,Task 栈里面只能有这一个 Activity 。当 Activity 被启动后,系统会为它创建一个新的 Task 栈,然后把该 Activity 的实例压入该 Task 栈中,并且该 Task 栈不允许其他 Activity 压入其中。该 Activity 实例是 Task 中唯一的 Activity。比如首次进入应用的介绍页,就是一个单实例,跳转到其他Activity后不允许再返回到这个界面。
两种配置方式 在AndroidManifest.xml中配置 <activity
android:name= ".MainActivity"
android:launchMode= "singleTop" />
在代码中配置 进行Activity的跳转时,一般像下面这样写:
Intent intent = new Intent ( this , MainActivity . class );
intent . setFlags ( Intent . FLAG_ACTIVITY_NEW_TASK | Intent . FLAG_ACTIVITY_CLEAR_TASK );
startActivity ( intent );
我们可以通过为Intent添加特定的Flag(标志)来指定Activity的启动行为。这些Flag会影响Activity的启动模式和任务栈的管理。示例如下:
常用Flag介绍 FLAG_ACTIVITY_NEW_TASK:这个标志会告诉系统为新启动的 Activity 创建一个新的任务栈(Task),如果该 Activity 已经存在于某个栈中,它将复用现有的任务栈并将 Activity 添加到栈顶。该标志在启动一个新的 Activity 时是强制性的,特别是在从非 Activity 上下文(如 Service 或 BroadcastReceiver)启动 Activity 时。 FLAG_ACTIVITY_SINGLE_TOP:如果启动的 Activity 已经在栈顶,则复用它的实例,而不是创建新的实例。如果该 Activity 不在栈顶,系统将创建一个新的实例并将其推到栈顶。这个标志类似于在 launchMode 中设置 singleTop。 FLAG_ACTIVITY_CLEAR_TOP:当启动的 Activity 已经存在于任务栈中时,它会清除这个 Activity 之上的所有其他 Activity。然后,系统会将这个 Activity 置于栈顶并复用它的实例。这通常与 * FLAG_ACTIVITY_NEW_TASK 结合使用。 FLAG_ACTIVITY_CLEAR_TASK:这个标志会清除目标 Activity 所在的整个任务栈(Task)。所有位于该任务栈中的 Activity 都会被销毁,然后启动目标 Activity 作为新的栈根。 FLAG_ACTIVITY_REORDER_TO_FRONT:如果目标 Activity 已经在任务栈中,但不是栈顶,系统会将它移动到栈顶,而不销毁栈中的其他 Activity,也不会创建新的实例。 FLAG_ACTIVITY_NO_HISTORY:该标志表示启动的 Activity 不会被添加到任务栈中,也就是说当用户离开这个 Activity 后,系统将不会保存它的状态。这常用于启动短暂的页面,如登录页面、广告页面等。 FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS:这个标志会防止启动的 Activity 出现在“最近任务”(Recents)中。即使用户通过多任务按钮查看任务列表,该 Activity 也不会出现在列表中。 Activity跳转 生命周期流程,可以参考 【APP冷启动流程解析】
AMS会判断拉起的Activity是否为当前Activity,如果不是,当前Activity会先进入到pause状态,执行onPause(),然后再去创建新的Activity,走目标Activity的onCreate(),onResume()生命周期。然后是当前Activity的onStop()生命周期。
即如果是Activity A 跳转到 Activity B,那么完整的流程是:
A的onPause() -> B的onCreate() -> B的onStart() -> B的onResume() -> A的onStop()。
需要注意的是,A的onPause方法如果有耗时操作,并不会拖慢B的生命周期的执行,尽管在系统层面,是先通知A去执行onPause方法,然后再去执行B的生命周期方法。
但是B的周期不依赖A的onPause方法执行完毕,所以B的生命周期方法是可以并行执行的。
什么是BroadcastReceiver Android系统里面的广播可以看作村里的大喇叭。而BroadcastReceiver是Android四大组件之一,主要用于接收系统或其他应用发送的广播消息。
广播类型 标准广播(nor mal br oadcasts )是一种完全异步执行的广播,在广播发出之后,所有的BroadcastR eceiver 几乎会在同一时刻收到这条广播消息,因此它们之间没有任何先后顺序可言。这种广播的效率会比较高,但同时也意味着它是无法被截断的。
有序广播(order ed br oadcasts )则是一种同步执行的广播,在广播发出之后,同一时刻只会有一个BroadcastR eceiver 能够收到这条广播消息,当这个BroadcastR eceiver 中的逻辑执行完毕后,广播才会继续传递。所以此时的BroadcastR eceiver 是有先后顺序的,优先级高的BroadcastR eceiver 就可以先收到广播消息,并且前面的BroadcastR eceiver还可以截断正在传递的广播,这样后面的BroadcastR eceiver 就无法收到广播消息了。
BroadcastReceiver的作用 BroadcastReceiver的主要工作机制为:
接收系统或其他应用发送的广播消息。 处理接收到的广播消息。 提供数据的安全性,使得数据只能被授权的应用访问。 提供数据的可扩展性,使得数据可以被多个应用共享。 使用 静态注册 静态注册的方式是在AndroidManifest.xml文件中进行配置。在配置的过滤器中的广播消息发送之后,广播接收器就会接收到该广播消息。
<receiver android:name= ".MyBroadcastReceiver" >
<intent-filter>
<action android:name= "android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
动态注册 动态注册的方式是在代码中进行动态,用于在某个任务启动之后,动态监听某一个广播消息来做出反应。
IntentFilter filter = new IntentFilter ();
filter . addAction ( "android.intent.action.BOOT_COMPLETED" );
registerReceiver ( new MyBroadcastReceiver (), filter );
注意事项 广播接收器的动态注册方式,需要在任务销毁的时候,进行注销。一定需要是成对出现调用,否则会导致内存泄漏。
// 注册监听
IntentFilter filter = new IntentFilter ();
filter . addAction ( "android.intent.action.BOOT_COMPLETED" );
registerReceiver ( new MyBroadcastReceiver (), filter );
// 注销监听
unregisterReceiver ( new MyBroadcastReceiver ());
广播的发送 标准广播的发送方式 先定义一个接收器,然后再发送广播测试。
class MyBroadcastReceiver : BroadcastReceiver () {
override fun onReceive ( context : Context , intent : Intent ) {
Toast . makeText ( context , "received in MyBroadcastReceiver" , Toast . LENGTH_SHORT ). show ()
}
}
Manifest文件注册:
<receiver
android:name= ".MyBroadcastReceiver"
android:enabled= "true"
android:exported= "true" >
<intent-filter>
<action android:name= "com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
然后在Activity中发送广播:
class MainActivity : AppCompatActivity () {
.. .
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
button . setOnClickListener {
val intent = Intent ( "com.example.broadcasttest.MY_BROADCAST" )
intent . setPackage ( packageName )
sendBroadcast ( intent )
}
.. .
}
.. .
}
点击按钮之后,就可以看到Toast的提示了。
有序广播的发送方式 有序广播的发送方式和标准广播的发送方式类似,只不过需要在发送广播的时候,指定广播的优先级。
class MyBroadcastReceiver : BroadcastReceiver () {
override fun onReceive ( context : Context , intent : Intent ) {
Toast . makeText ( context , "received in MyBroadcastReceiver" , Toast . LENGTH_SHORT ). show ()
}
}
Manifest文件注册:
<receiver
android:name= ".MyBroadcastReceiver"
android:enabled= "true"
android:exported= "true" >
<intent-filter android:priority= "1000" >
<action android:name= "com.example.broadcasttest.MY_BROADCAST" />
</intent-filter>
</receiver>
然后在Activity中发送广播:
class MainActivity : AppCompatActivity () {
.. .
override fun onCreate ( savedInstanceState : Bundle ?) {
super . onCreate ( savedInstanceState )
setContentView ( R . layout . activity_main )
button . setOnClickListener {
val intent = Intent ( "com.example.broadcasttest.MY_BROADCAST" )
intent . setPackage ( packageName )
sendOrderedBroadcast ( intent , null )
}
}
}
可以看到,区别就是sendOrderedBroadcast方法。
如果有多个广播接收器接收这一条广播,那么优先级高的广播接收器会先接收到广播。注意,priority数值越大,优先级越高。
Pagination © 2024. All rights reserved. LICENSE | NOTICE | CHANGELOG
Powered by Hydejack v9.2.1