Android学习
2023-03-09 16:07:26 0 举报
AI智能生成
Android学习
作者其他创作
大纲/内容
数据存储<br>
SharedPreferences
存储key-value键值对,多用来存放用户的简单偏好配置<br>
使用
通过getSharedPreferences(String spName, int mode)获取当前应用的sp
通过sp.edit()可以获取sp对应的SharedPreferences.Editor
通过sp.getX(key, X default)可以根据key查询值
通过editor.putX(key, X value)可以插入或更新键值对<br>
通过editor.remove(key)可以删除对应的key<br>
SQLite
Android内置的嵌入式关系型数据库
原生
创建一个约定类,该类存储表名、创建命令等,包含一个静态内部类,存储表中字段的信息
SQLiteOpenHelper,重写onCreate定义创建表的逻辑以及onUpgrade定义升级表的逻辑
在调用类中通过SQLiteOpenHelper对象的getReadableDatabase/getWritableDatabase获取到SQLiteDatabase数据库操作对象
通过SQLiteDatabase对象来调用query、insert、update、delete来实现增删该查
room
创建一个与表同名的实体类,加上@Entity注解,同时定义域,与数据库字段通过@ColumnInfo等注解进行关联<br>
创建表名为前缀的Dao接口,加上@Dao注解。将需要做的读写方法抽象为接口,并加上注解,交给Room实现
拓展RoomDatabase,通过@Database指定对应的实体类和版本号,创建一个获取Dao的抽象方法交给room实现。
通过Room.databaseBuilder().build()获取RoomDatabase对象,再通过RoomDatabase.getDao获取到Dao<br>
通过Dao定义的读写接口构建任务,交给线程池执行<br>
网络<br>
HttpURLConnection
Java用来建立http连接的HTTP客户端库
使用<br>
通过URL.openConnection开启一个与url的连接,将其强转为HttpURLConnection
通过setRequestMethod指定请求方法
通过setDoInput设置允许输入流,通过setDoOutput设置允许输出流(POST下)
通过getInputStream拿到输入流,getOutputStream拿到输出流(POST)
如果是POST,根据Content-Type格式化请求体的数据并写入输出流
通过getResponseCode获取响应码并进行判断和处理<br>
通过InputStream读如服务端响应报文
关闭输入流、输出流、http连接
其他<br>
网络请求属于IO操作,不应该在主线程中操作,可以通过Service来后台通信,也可以直接子线程操作
要使数据得到后通知主线程而不是主线程等待同步工作线程的Future,可以使用Handler向主线程发送消息,主线程自定义消息处理机制,进而来异步更新<br>
Android操作系统
进程和线程
android启动应用的过程
系统加载并启动应用
系统启动后立即显示应用空白启动窗口
系统创建应用进程
应用进程创建应用对象
应用进程启动主线程<br>
主进程创建主Activity
主线程扩充视图
主线程布局屏幕
主线程执行初始绘制
系统替换启动窗口为主Activity
启动状态
冷启动
系统为应用创建进程
温启动
进程没有被系统回收,但需要重新创建Activity,执行onCreate<br>
热启动
activity仍驻留在内存中,需要从onStart开始
进程与组件
默认情况下,android中所有的组件元素均属于同一进程
各类组件元素在menifast注册时,都可以通过android:process属性指定组件所属进程<br>
application元素中也可以设置android:process属性,设置所有组件的默认值
Android进程分类<br>
前台进程
条件<br>
拥有一个与用户交互的Activity,正在onResume
有一个正在响应广播的BroadcastReceiver,正在onReceive
有一个正在执行回调的Service,onCreate | onStart | onDestroy<br>
特点
除非系统内存过低,导致前台进程都无法继续运行,才会终止这些进程。
可见进程
条件
有一个可见的Activity,但没有获得焦点(没有进行onResume)
有一个正在启动的前台Service,正在执行Service.startForeground
有一个托管的服务某功能正在被系统使用,比如壁纸、输入法<br>
特点
除非为了保证前台进程运行而终止可见进程,否则一般不会被终止
服务进程
条件
包含一个已经启动的服务,已经startService
特点
系统会尽量保证其运行。但如果该进程已经持续执行了很长时间,该进程可能会降位
缓存进程
条件
目前不需要的进程,不可见、没有提供服务
特点
是内存管理涉及的唯一进程
在紧急情况下,系统会终止所有缓存进程
缓存进程会采取LRU进行淘汰
线程
主线程<br>
应用启动时,系统会创建一个主线程main,负责将事件分派给相应的界面微件。
几乎所有的界面工具包组件都与是主线程交互<br>
系统不会为每一个组件创建单独的线程,一些耗时长的逻辑会阻塞主线程
如果界面线程被阻塞超过一定时间,5s左右,就会弹出ANR应用无响应
不要阻塞UI线程
UI线程之外的线程不能访问Android UI包<br>
工作线程
为了防止阻塞主线程导致ANR,一般将耗时的IO操作通过新线程来处理<br>
工作线程访问界面线程的方法
Activity.runOnUiThread(Runnable)
View.post(Runnable)
View.postDelayed(Runnable, long)
主线程中实例化一个并Hanlder重写handleMessage方法,其他线程通过Handler发送Message
拓展AsyncTask<Params, Progress, Result>调用execute(Params...)执行
void onPreExecute()
在task执行之前在UI线程中调用,用于启动task,做进度条展示之类的
Result doInBackground(Params...)
在一个后台线程中执行任务,返回任务结果
void onProgressUpdate(Progress...)
在后台任务执行publishProgress(Progress...)后,在UI线程中调用,被用来更新进度条
void onPostExecute(Result)
在后台任务执行完毕后,拿到返回结果并回到主线程来执行,用来根据结果更新UI
内存
RAM
4k分页机制
缓存页
有存储器中的文件(例如代码或内存映射文件)支持的内存
私有页
有一个进程拥有且未共享
“干净”页<br>
读如内存后还没有修改过或修改已经提交,可用kswapd删除<br>
脏页
有修改的内容还没有写回磁盘,可用kswapd移动到zRAM进行压缩<br>
共享页
由多个进程使用
“干净”页
同上
脏页
可用kswapd或者msync、munmap将脏数据写回磁盘
匿名页
没有存储器文件支持的内存(不是磁盘上的文件数据,而是堆、栈这类)
脏页
可用kswapd移动到zRAM进行压缩<br>
zRAM<br>
用于交换空间的RAM区
所有数据放入zRAM时都会被压缩,而从zRAM向外复制时会进行解压
存储器
不像其他linux的实现用于交换空间,因为频繁写入会导致这种内存出现损坏
kswapd-内核交换守护进程
Linux内核的一部分,用于将已用内存转化为可用内存。<br>
当内存不足时,可用内存下降至Linux内核内存下限阈值以下时,该守护进程进入活动状态,开始回收内存
回收页帧方式
非脏页直接回收
后续缺页时,直接查页表从磁盘读入内存
私有页和匿名页的脏页放入zRAM压缩
缺页时从zRAM解压到RAM
关联进程终止时页面从zRAM移除
共享页脏页写回磁盘再回收
缺页时再调入
相关接口
实现ComponentCallback2并重写onTrimMemory(int)<br>
系统会在内存不足时发起onTrimMemory回调,可以再其中自定义面对不同内存不足事件时的处理方案
通过getMemoryInfo获取ActivityManger.MemoryInfo
可通过该对象获取设备当前内存状态信息
内存效率<br>
尽量少用Service,能不用则不用。如果必须使用,除非Service必须还要运行作业,否则完成任务后要直接关闭服务
系统会更倾向于让服务进程一直运行,导致其占用LRU,从而淘汰其他可能会用到的缓存进程
使用JobSchedual代替
使用Android提供的集合类来替代Java库中对移动设备内存不友好的集合类<br>
HashMap<K, Object> =>SparseArray<K>
HashMap会带来频繁的自动装箱拆厢,会多创建出很多对象,造成内存效率低
HashMap<Integer, Boolean> => SparseBooleanArray<br>
HashMap<Integer, Long> => SparseLongArray<br>
Android开发中如果抽象无法带来很好的收益,则慎用抽象
抽象会增添很多类,会增加逻辑实现的代码量,需要更多时间和内存来load class<br>
压缩外部库
GC
ART和Dalvik虚拟机的垃圾回收机制类似于CMS或是G1
分代、分区
GC过程中会出现STW<br>
四大组件<br>
Activity
出现原因
使用
必须在manifest中注册才能使用
父子Activity必须拥有相同的权限<br>
Intent<br>
生命周期<br>
onCreate()
在系统创建Activity时触发<br>
必须实现该callback
配置Activity组件,setContentView()设置布局文件……
onStart()
在onCreate()执行完成后触发
此时Activity已启动,对用户可见
完成Activity 进入前台与用户进行互动之前的最后准备工作
onStart()初始化的资源需要在onStop()中回收<br>
onResume()
在 Activity 开始与用户互动之前调用此回调
此时该Activity位于Activity stack顶部,并会捕捉所有用户输入
大部分核心功能都是在 onResume() 方法中实现的
总会跟着一个onPause()<br>
onResume()中初始化的资源需要在onPause()中回收
onPause()
失去焦点或暂停时调用<br>
不能使用onPause()来保存应用或用户数据、进行网络呼叫或执行数据库事务。
执行完毕后会跟上onStop()或onResume()<br>
onStop()
在Activity对用户不可见时(已结束并即将终止)会执行该callback
现有Activity被销毁或被新Activity覆盖,再也不可见
可以用来完成CPU相对密集的关闭操作(例如将数据存储到SQLite)<br>
执行完毕后跟上onRestart()或onDestory()<br>
onRestart()
处于停止状态的Activity重启时会调用
调用完成后总会再调用onStart()
onDestory()
Activity被销毁前(设备旋转)调用
此时可以清理该Activity中的任何数据
确保在销毁 Activity 或包含该 Activity 的进程时释放该 Activity 的所有资源
可复用的生命周期callback定义
Lifecycle
用于存储activity或fragment的生命周期状态信息<br>
DefaultLifecycleObserver
可以通过实现该类来监控相应的生命周期
LifecycleOwner
表示类具有Lifecycle,可以通过addObserver()添加生命周期监听器,与DefaultLifecycleObserver完美协作
Activity与系统RAM
系统会在需要释放RAM时终止进程,终止进程的可能性取决于进程中Activity的状态
已创建、已开始、已恢复被终止可能性很小
已暂停被终止可能性较大
已停止、已销毁可能性最大<br>
系统不会直接终止Activity来释放RAM,而会终止Activity所在的进程,销毁进程中的所有内容。
Activity的恢复
Activity可以通过Bundle恢复<br>
当Activity被销毁后,可以通过Bundle中存储的数据来设置新建的Activity成员变量,进而恢复Activity。
Activity中的每个视图必须拥有唯一的android:id才能恢复Activity的视图状态
Bundle不适合保留大量数据,因为它需要在主线程中序列化。大量数据应组合使用持久化、onSaveInstanceState()和ViewModel
在Activity开始停止时,系统会调用onSaveInstanceState()
此方法可以方便地将状态信息保存到Bundle中
如果希望默认实现保存视图层次结构状态,需要调用super.onSaveInstanceState(Bundle)
用户显式关闭Activity或调用finish()时,系统不会调用onSaveInstanceState()<br>
重建被销毁的Activity<br>
onCreate(Bundle)<br>
会接收到系统传递给Activity的Bundle保存的状态。需要判断是否为null<br>
onRestoreInstanceState(Bundle)
在onStart()之后调用<br>
只有在需要恢复状态时才会调用,因此无需判断Bundle是否为null
Activity之间导航
Activity启动Activity
startActivity(Intent)
新启动的Activity不需要返回结果<br>
startActivityForResult(Intent, int)
结果通过onActivityResult(int, int, Intent)返回<br>
子Activity退出时可以调用setResult(int)将数据返回给父Activity,int为flag
生命周期转换
A: onPause() -> B: onCreate() onStart() onResume()获得用户焦点 -> A: onStop()不再可见<br>
Activity状态更改
配置更改
配置更改时Activity会销毁(onPause->onStop->onDestory)然后重建(onCreate->onStart->onResume)。
结合使用ViewModels、onSaveInstanceState()可以让Activity在配置改变后界面状态保持不变
进入多窗口模式或多窗口模式中应用调整了大小,系统会向当前Activity发送配置更改通知,并进行上述生命周期状态更改<br>
多窗口模式下,当焦点由A移到B,A会调用onPause,B会调用onResume
可以自行处理配置更改
对话框
新的Activity或对话框出现在前台<br>
部分覆盖了当前Activity
失去焦点并调用onPause<br>
重新获得焦点时调用onResume<br>
完全覆盖了当前Activity<br>
进入已停止状态调用onStop
重新获得焦点时调用onRestart->onStart->onResume
返回按钮
当用户点击返回,退出应用
Activity会依次调用onPause->onStop->onDestory
Activity不仅会被销毁,而且会从返回堆栈中移除
该情况下默认不触发onSaveInstanceState
可以通过替换onBackPressed实现自定义行为,建议在其中调用super.onBackPressed()
通用应用需要注意的表现
被其他应用中断
系统销毁并重建Activity
Activity被放在新窗口环境中,例如画中画、多窗口
可以使用AndroidX Test库中的ActivityScenario实例来测试<br>
任务与Activity堆栈<br>
任务<br>
用户在执行某项工作时与之互动的一系列Activity的集合
多窗口下每一个窗口都是一个任务
当新启动一个应用时,会将该应用的主Activity作为该任务的Activity堆栈的根Activity
当Activity启动新activity时,会将新Activity压入Activity堆栈的栈顶并获得焦点;当用户按下返回按钮时,当前Activity会从堆栈顶弹出并销毁,新栈顶获取焦点。
当Activity栈所有元素都被弹出,堆栈为空,任务不复存在<br>
自定义activity堆栈管理
通过资源文件
使用<activity>的launchMode 属性指定<br>
launchMode="standard"
默认模式。<br>
launchMode="singleTop"
如果栈顶已存在Activity实例,系统会调用onNewIntent来将Intent转送给实例,而不创建实例
launchMode="singleTask"
如果另外的任务中已经存在Activity实例,会直接使用onNewIntent将Intent传给现有实例,而不重新创建
launchMode="singleInstance"<br>
任务中只有一个根activity,当需要新建activity时,如果系统中已存在,则直接使用onNewIntent发送给已有实例;如果不存在,则新建一个任务,并将新activity作为新任务的根activity<br>
通过Intent
使用Intent的flag来确定
FLAG_ACTIVITY_NEW_TASK
相当于上述的singleTask<br>
FLAG_ACTIVITY_SINGLE_TOP
相当于上述的singleTop
FLAG_ACTIVITY_CLEAR_TOP
如果需要启动的activity已在当前任务中,会将栈顶activity以此弹出销毁,直至栈顶为需要启动的activity,然后通过onNewIntent()将intent传送给目标activity<br>
清除返回堆栈
用户离开任务较长时间后,系统会清除根Activity外的所有Activity
可以通过修改Activity的属性来修改清除行为。
alwaysRetainTaskState
根Activity中该属性设为true,则会保留任务中的所有activity
clearTaskOnLaunch
根Activity中该属性设为true,只要用户离开再返回,堆栈就会只剩下根activity
finishOnTaskLaunch
某Activity中该属性设为true,只要用户离开再返回,该activity就不存在于任务<br>
启动任务
为入口Activity添加过滤器,并添加"android.intent.action.MAIN"作为指定操作,将"android.intent.category.LAUNCHER"作为指定类别<br>
该操作可以在启动器中现实Activity的图标和标签<br>
只有进行了该操作,才能使用"singleTask"和"singleInstance",才会被标记为始终启动任务。否则当跳转到其他任务后再离开该任务,就无法回到根任务。
Service
可在后台执行长时间运行操作而不提供界面的应用组件。
前台
执行用户能注意到的操作,必须显示通知。如音乐播放应用,前台服务用来播放音频。
后台
执行用户不会直接注意到的操作。例如压缩存储空间服务。<br>
使用服务的时机
当必须在主线程之外进行操作,但操作只在用户与应用交互时进行,就新建线程来进行操作
其他后台操作可以考虑使用服务<br>
服务运行在主线程中,服务中的密集型和阻塞类任务要创建线程来处理
创建服务
必须在manifest中注册服务<br>
两种服务<br>
Service
适用于所有服务的基类,如果继承该类,需要为执行的工作创建新线程,因为其默认使用应用的主线程。
IntentService
是Service的子类,其使用工作线程逐一处理所有启动请求。如果不需要同时处理多个请求,最好用该类。
继承Service并重写回调方法<br>
onCreate()
首次创建服务时会调用(在onStartCommand和onBind前)。如果服务正在执行,不会调用。
onStartCommand()
其他组件请求启动服务时会调用,执行该方法时服务就会启动并且无期限运行。服务执行完需要使用Service.stopSelf()或Context.stopService()停止服务
返回值<br>
START_NOT_STICKY
如果系统在 onStartCommand() 返回后终止服务,则除非有待传递的挂起 Intent,否则系统不会重建服务。
START_STICKY
在返回后,如果系统终止服务,就会重建服务并且调用onStartCommand(),除非有挂起的Intent要启动服务,否则会调用空的Intent<br>
START_REDELIVER_INTENT
在返回后,如果系统终止服务,就会重建服务并通过最后一个Intent调用onStartCommand()
onBind()
其他组件请求绑定服务时会调用。如果启用绑定,则必须在此返回一个IBinder,以供客户端与服务通信。如果不允许绑定则直接返回null<br>
onDestory()
准备销毁服务时调用。在此方法中可以清理线程、监听器等资源
继承IntentService并给出构造函数和onHandleIntent()<br>
IntentService提供了默认的onBind和onStartCommand,且在处理完所有请求后自动停止服务,所以只需要重写onHandleIntent即可
如果需要重写生命周期callback,需要确保调用父类实现。
onHandleIntent()
IntentService会将收到的Intent送往工作队列,依次交给onHandleIntent处理
绑定服务的创建方法<br>
Binder<br>
用途
服务仅供本地使用,无需跨进程
操作<br>
拓展Binder,提供获取Service的方法
在onBind()中将Binder返回给客户端
在需要绑定的组件中创建匿名ServiceConnection重写onServiceConnection,拿到onBind返回的IBinder进而获取到Service<br>
通过ServiceConnection调用bindService绑定服务,通过获取的Service调用服务工作。<br>
Messenger
用途
无需AIDL就可以实现IPC
操作
拓展Handler为服务端实现消息处理句柄,在handleMessage中定义消息处理逻辑
在onBind中基于Handler构建Messenger,获取Messenger内部IBinder返回给客户端
在需要绑定的组件中创建匿名ServiceConnection重写onServiceConnection,基于服务端返回的IBinder构建一个向服务端发送消息的Messenger
通过Message.obtain生成要发送的消息,并调用Messenger.send将消息发送给服务端,交给Handler.handleMessage处理
如果需要CS双向通信,客户端也需要拓展Handler来处理消息,并基于Handler创建一个Messenger,发送消息时将Message的replyTo指定为客户端的Messenger,即可在服务端接收消息后回复客户端。<br>
AIDL
...
启用服务<br>
bindService()
一个或多个应用绑定同一个服务,当没有应用绑定该服务时,服务立即销毁<br>
会引起onBind()
只有第一个客户端绑定时,系统会效用onBind生成并返回IBinder,后续会直接将该IBinder绑定到
绑定服务通常不允许使用startService启动
可用来IPC
此操作是异步操作
客户端销毁时会自动取消绑定,但最好是交互完成立即手动取消绑定<br>
startService()
创建服务并一直在后台运行,直到调用stopSelf()或stopService()停止服务<br>
会引起onStartCommand()
最好使用stopSelf(int id)来停止onStartCommand(Intent intent, int flags, int startId)所启动的特定的已完成的工作<br>
同时绑定和启动<br>
如果确实要同时具有已启动和已绑定状态,则服务不会在所有客户端均取消绑定后销毁,而必须调用stopService或stopSelf
音乐播放器<br>
Activity可以启动服务来播放音乐,在用户离开或Activity销毁后音乐服务不中断。<br>
在Activity重新启动时,可以通过绑定,让Activity重新获得播放器控制权
系统回收服务资源<br>
内存过低以致焦点activity无法正常工作时系统才会停止服务
如果服务绑定到焦点activity上,则该服务不太可能被回收
如果是前台服务,则几乎永远不会终止
服务长时间运行,系统会逐渐降低其在后台任务列表的位置,被终止的概率也会大幅提升
Broadcast Receiver
应用注册广播接收器
资源清单注册<br>
AndroidManifest.xml添加<receiver>并指定<intent-filter><br>
拓展BroadcastReceiver并重写onReceive(Context, Intent)定义接收广播后的回调逻辑<br>
清单注册广播接收器,系统会在发出广播后启动该应用
上下文注册
拓展BroadcastReceiver并重写onReceive指定逻辑
创建BroadcastReceiver实例
创建IntentFilter并调用registerReceiver(BroadcastReceiver, IntentFilter)注册接收器
如果需要停止接收,调用unregisterReceiver(BroadcastReceiver)<br>
上下文注册的接收器仅在创建处的上下文会接收广播<br>
CONNECTIVITY_ACTION只会传送给上下文注册的接收器
如果注册接收器指定了权限参数,广播方应用必须具有该权限才能向接收器发送Intent
发送广播<br>
sendOrderedBroadcast(Intent, String)
每次向一个接收器发送广播,按照android:priority控制顺序,相同优先级随机顺序。<br>
接收器可以向下传递结果,也可以终止广播
sendBroadcast(Intent)
随机顺序向所有接收器发送广播<br>
无法从其他接收器读取结果
效率更高
LocalBroadcastManager.sendBroadcast(Intent)
将广播发送给同一应用内的接收器<br>
效率更高,本地广播
可以通过在发送方法中指定权限参数,那么接收方应用必须要具有指定的权限才能接受该广播
Content Provider
主要作用<br>
提供对应用数据存储的统一封装<br>
对外暴露便于操作内部数据的接口
Android中一般数据存储技术
关系型数据库SQLite
非关系型键值存储LevelDB
非结构化数据--文件存储<br>
ContentProvider与SQL的关系
Cursor query(Uri, projection, selection, selectionArgs, sortOrder)
Uri -> FROM table_name<br>
Uri content://user_dictionary/words
projection -> col1, col2, ...<br>
String[]查询的列名
selection -> WHERE col = value<br>
String查询的条件:"_ID = ?"
selectionArgs没有完全等价项<br>
String[]查询条件中替换占位符的值: new String[] {"1"} => "_ID = 1"<br>
sortOrder -> ORDER BY col1, col2, ...<br>
String排序
Cursor:返回结果集合的包装类<br>
Uri insert(uri, newValues)
uri -> FROM table_name<br>
Uri content://user_dictionary/words
newValues
ContentValues插入值集合包装对象,通过put(col_name, value)来设置一行中各字段的值<br>
Uri:返回的Uri e.g. content://user_dictionary/words/<id_value>,id_value是插入行的_ID<br>
int update(uri, updateValues, selectionClause, selectionArgs)
uri -> FROM table_name<br>
同上<br>
updateValues: ContentValues需要更新的一行的值<br>
selectionClause: String查询条件"_id LIKE ?"<br>
selectionArgs: String[]替换占位符的值new String[] {"1%"} => "_id LIKE 1%"<br>
返回更新的行数
delete(uri, selectionClause, selectionArgs)
全部同上
返回删除的行数<br>
创建<br>
设计数据存储方式以及Uri<br>
创建对数据存储的内部封装类,比如SQLite需要拓展SQLiteOpenHelper,重写创建和更新的逻辑
拓展ContentProvider,借助上一步的封装类提供的读写数据接口重写增删该查以及getType
在资源清单中注册<provider>配置authorites,enabled,exported等属性并加上申请权限
使用
找到需要调用的ContentProvider的授权路径
通过getContentResolver()获取应用内容解析器
使用Uri.Builder根据授权路径、表、id构建Uri
调用contentResolver.xxx,将跨进程的数据读写操作交给对应的ContentProvider来完成
Notification
外观设计
模板设计<br>
由操作系统决定<br>
可自定义布局<br>
内容
用户只需定义模板中个部分的内容
小图标
setSmallIcon()
应用名称
系统提供
时间戳
setWhen()设置时间
setShowWhen()是否显示
可选
大图标
setLargeIcon()
标题<br>
setContenttTiltle()
文本
setContentText()
按钮<br>
addAction()<br>
其他配置<br>
点击事件
点击打开新组件<br>
setContentIntent(PendingIntent)
设置全屏紧急提示<br>
setFullScreenIntent(PendingIntent, true)
点击后移除通知
setAutoCancel()
设置进度条<br>
setProcess(MAX, current, indeterminate)
每次进度变更时,需要重新调用setProgress和notify
创建及发送<br>
如果版本大于Build.VERSION.O,需要根据自己定义的CHANNEL_ID来创建一个新的channel,并且通过NotificationManager创建该Channel
创建NotificationCompat.Builder,并设置外观、按钮等配置
通过NotificationManagerCompat.from(Context)获取到NotificationManagerCompat对象,可以通过该对象发出、销毁通知
然后通过该对象调用notify()传入自定义的notification id以及builder.build构建的Notification对象发出通知<br>
0 条评论
下一页