开发中使用了什么技术?
mvvm、compose、livedata、单例模式、工厂模式、弱引用、线程池、Handler。
对于项目一开始我们打算使用aosp原生的管控方式,如UsageStatManager获取每个app的使用时长,和使用PackageManager的setPackagesSuspended方法置灰图标,但是系统的方法在进入管控后,弹出的dialog无法自定义,因此我们最终使用了弹出属性为WindowManager.LayoutParams.TYPE_PHONE的全覆盖顶层窗口dialog的方式管控设备使用。
开发中遇到的问题:家长管控中有一个"应用使用时长管控"的管控选项,用于在出现某个app使用时长达标后弹出对话框管理。我们使用window的方式无法暂停短视频app的播放,此类应用在window出现后不会进入onStop方法,而是单纯的进入onPause方法。
MVVM
基本概念和组成部份
即model-view-viewmodel软件架构模式,与mvc和mvp相同的一点是,model和view的角色没有发生改变,model负责处理数据和业务逻辑,view负责与用户的交互。
viewmodel负责的是管理界面相关的数据,它与model和view之间的交互关系如下(以livedata为例):
- 与model:model返回的数据通过livedata传递给viewmodel
- 与view:viewmodel通过livedata通知view发生更改
与其他架构比较(重点)
- MVP与MVVM有什么不同?
与MVP区别在于数据绑定和通信方式。mvvm使用数据绑定(livedata或databinding)使得view和model自动同步,而mvp使用接口方式手动更新;
- 为什么选择MVVM而不是MVC?
1)数据绑定:mvvm中引入了数据绑定,而mvc没有,需要手动更新,可能出错;
2)更加清晰的职责分离和耕地的耦合性
3)mvvm使用单向数据流,数据从model流向view,view通过viewmodel反应model的变化,使得数据流向可预测,便于调试和管理;
mvc中,数据和交互可能在view和controller之间双向流动,特别是在处理用户输入时,增加了调试和维护的难度。
数据绑定和观察者模式
- 如何实现数据绑定(databinding)
在我的开发过程中,我使用了livedata进行数据绑定。通过这个机制,view可以观察viewmodel中的数据是否发生改变,并因此而改变view。
- 如何通过databinding简化mvvm中的代码
通过databinding,可以直接在xml中绑定ui组件和viewmodel中的数据,省去了findviewbyid,使得ui更新更加简洁直观。
ViewModel
- 如何管理和保存viewmodel中的数据,防止在配置更改(如屏幕旋转)时丢失?
viewmodel是感知生命周期的方式设计的,他的生命周期比activity和fragment更长。
当配置发生改变,activity会被重建,但viewmodel得以保留,因此可以用来持有和管理数据,避免数据丢失。
- ViewModelScope是什么,有什么作用?
它是一个CoroutineScope,用于在viewmodel中启动协程。当viewmodel被销毁时,ViewModelScope内部启动的所有协程也会被销毁,以确保资源得以释放,避免内存泄漏。
- 如何在viewmodel中处理异步操作?
使用viewmodelscope启动协程执行异步操作,如网络请求或数据库操作。
Kotlin
class MyViewModel : ViewModel() {
fun fetchData() {
viewModelScope.launch{
val result = repository.loadData() // 耗时操作的举例
// 更新livedata
}
}
}
- 错误处理和状态管理
1)如何在mvvm中处理错误和应用状态?
可以封装一个result类型的数据累来确保统一的错误处理的状态管理,然后在viewmodel中根据结果更新livedata。
Kotlin
sealed class Result<out T> {
data class Success<out T>(val data: T) : Result<T>()
data class Error(val exception: Exception) : Result<Nothing>()
object Loading : Result<Nothing>()
}
2)怎么在viewmodel中确保ui状态的一致性?
使用livedata并在view中观察数据变化,使用单一数据源修改。
Compose
基础概念
- 什么是compose?与传统的xml相比有什么优势?
compose是现代化的ui框架,使用kotlin语言声明式的方式编写ui。
与传统xml相比,compose提供了更简洁的方式来构建界面,减少了大量样板代码。
- 解释一下compose的可组合函数的概念
@Composable是compose的核心概念,它标记了一个函数是可组合的,即可以用来描述ui。
状态管理(重点)
- 解释一下compose中状态管理和remember、mutableStateOf的作用
compose的状态管理基于可观察的状态,当观察到内容发生变化时,compose会重新组合受影响的部份,实时更新ui。
remember用于记住状态,也可以避免在加载或重组时数据丢失。
mutableStateOf创建一个可变状态的对象,当对象的值发生变化时,会通知compose进行重组。
Kotlin
@Composable
fun Counter() {
var count by remember { mutableStateOf(0) }
Button(onClick = { count++ }) {
Text("Click $count times")
}
}
根据示例中对count的定义,我们分步解析:
- mutableStateOf(0):创建一个可变的状态对象,在它的值发生变化时会触发重组
- remember:在组合过程中会记住相应的值,只要作用组合域没有被销毁,remember返回的值保持不变
- by:关键字用于声明属性委托。
当使用by关键字进行属性委托时,kotlin会自动处理这个属性的访问和修改,并将其委托给后面的对象。具体到这个例子,就是count的getter和setter方法被委托给了MutableState对象。因此,count对象的状态可以被实时修改的mutableState对象更新。
- 如何在compose中实现状态的持久化,比如在屏幕旋转是状态不丢失?
在屏幕旋转的时候,当前的activity会被销毁并重建一个新的activity实例,生命周期如下:
而这也意味着app需要在销毁和重建时保存和恢复activity的状态。我们可以通过两个方式实现。
1)保存和恢复实例状态
java
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putString("key", "value"); // 保存状态
}
@Override
protected void onRestoreInstanceState(Bundle saveInstanceState) {
super. onRestoreInstanceState(saveInstanceState);
String value = saveInstanceState.getString("key"); // 恢复状态
}
在上述重写的方法中,第一个方法会在activity被销毁前调用,我们可以在这里将需要保存的内容存放在Bundle对象中;而第二个方法会在activity重建后调用,我们可以从Bundle对象中恢复之前的状态;
2)配置更改回调
除了保存和恢复实例状态,还可以重写onConfigurationChanged方法来配置更改的回调。
java
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
// 横屏处理逻辑
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
// 竖屏处理逻辑
}
}
当系统属性发生改变时,系统会调用onConfigurationChanged方法,具体的使用方法可以参考:onConfigurationChanged方法介绍及问题解决
回到compose,我们可以使用rememberSaveable在配置发生改变时保存并恢复状态。
Kotlin
@Composable
fun Counter() {
var count by rememberSaveable { mutableStateOf(0) }
Button(onClick{ count++ }) {
Text("Clicked $count times")
}
}
rememberSaveable会在配置发生改变时自动保存状态,并在重建时恢复,以此防止状态丢失。
同时我们前面还学到了可以使用viewmodel来保存,原因是viewmodel的生命周期略长于activity,配置发生改变时activity被销毁并重建,但viewmodel中的数据会被保存。
- 什么是state hoisting?为什么在compose中推荐使用这种模式?
state hoisting直译状态提升,是一种把状态从子组件提升到父组件的模式。在此模式下,组件的状态不是由自己管理,而是由其父组件管理,并通过参数传递。
Kotlin
@Composable
fun CounterParent() {
var count by remember { mutableStateOf(0) }
Counter(counter = count, onIncrease = { count++ })
}
@Composable
fun Counter(count: Int, onIncrease: () -> Unit) {
Button(onClick = onIncrease) {
Text("Clicked $count times")
}
}
可见子组件Counter并没有管理自己的状态,即没有对变量的存储和变更作处理,这些内容都在父组件被定制。
这样做的好处是:
- 可组合性:组件更加通用和易于组合,使用组件只需要传入状态,提高了复用性
- 单一数据源:确保一个状态有一个单一、可信的数据源,减少了状态不一致带来的风险
- 易于测试:外部管理状态的组件更容易进行单元测试,因为子组件的状态可以轻松地被控制
核心概念
|------------------|-------------------------------------------------------------------|
| 名称 | 含义 |
| Composable:可组合函数 | Compose中构建ui的基本单元,加入该注解的普通的Kotlin函数在运行时会被Compose框架识别 |
| State:状态 | 主要指的是mutableState,它创建的对象的值发生变化时会触发compose重组 |
| Side Effects:副作用 | 指的是在可组合函数执行过程中发生的不直接生成ui、但会影响ui的行为,如启动协程,注册观察者 |
| Modifier:修饰符 | 修改可组合函数的修饰符,可以用来设置点击事件、布局大小等 |
| Lifecycle:生命周期 | compose可以感知生命周期,通过感知Activity或Fragment的生命周期执行到哪一步了,我们就可以在合适的时机执行操作 |
| Layout:布局 | compose的布局由多个基础组件构成,如Row、Column、Box,用于定义UI排列方式 |
协程
- 请解释LaunchedEffect的用途,并提供一个使用的场景示例
LaunchedEffect用于在可组合函数中启动协程,通常用于执行需要挂起的操作,如网络请求或数据库操作。
Kotlin
@Composable
fun DataFetcher() {
var data by remember { mutableStateOf<String?>(null) }
LaunchedEffect(Unit) {
// 模拟网络请求
delay(2000)
data = "Fetched Data"
}
if (data == null) {
CircularProgressIndicator()
} else {
Text("Data: $data")
}
}
- 可组合函数内部能否直接调用挂起函数?
可组合函数不能直接调用,因为两者上下文(context)不同,不能直接混用。这是因为挂起函数需要一个协程环境,而composable函数没有提供这种环境。
因此可以使用LaunchedEffect或rememberCorouroutinecope等API来启动协程调用挂起函数。
Kotlin
@Composable
fun UserProfileView(userId: String) {
val scope = rememberCorountineScope()
var user by remember { mutableStateOf<User?>(null) }
var isLoading by remember { mutableStateOf(false) }
// LaunchedEffect 使得在启动的时候能够自动获取数据
LaunchedEffect(userId) {
val userData = fetchUserById(userId) // 假设此处是一个挂起函数
user = userData // 在获取到userdata之后修改user
isLoading = false
}
// 因为变量user被mutableStateOf修饰,在变化时会触发重组进入以下代码
if (isLoading) {
CircularProgressIndicator()
} else {
Column {
if (user == null) {
Text("User not found")
} else {
Text("User: ${user.name}")
}
// 使用记住的 CoroutineScope 来启动协程
Button(onClick = {
// 使用记住的 CoroutineScope 启动一个新的协程
scope.launch {
isLoading = true
user = fetchUserById(userId) // 假设这是一个挂起函数
isLoading = false
}
}) {
Text("Refresh User") // 刷新按钮
}
}
}
}
// 假设这是一个挂起函数,用于从网络或数据库获取用户数据
suspend fun fetchUserById(userId: String): User {
// 模拟网络延迟
delay(2000)
return User(userId, "John Doe")
}
- rememberCoroutineScope:创建一个与可组合函数生命周期关联的CoroutineScope。他的生命周期在可组合函数重组期间不会改变或丢失;
- LaunchedEffect:用于启动协程进行数据初始加载。当userId变化时,LaunchedEffect代码块会重新执行;
- scope.launch:在按钮的点击事件启动一个新的协程,用于执行异步地获取用户数据,以防止阻塞主线程。
LiveData
基本用法
- 如何在viewmodel中使用livedata?
Kotlin
class MyViewModel: ViewModel {
// 私有的MutableLiveData,只能在类内部修改
private val _data = MutableLiveData<String>()
// 供外部访问的LiveData,此处使用了一个内联函数get(),在每次访问data时会返回_data
val data: LiveData<String> get() = _data
// 供外部调用的更新数据的方法
fun updateData(newData: String) {
_data.value = newData
}
}
这样的设计保证了外部如activity和fragment只能呈现并观察数据,但不能修改,避免了组件之间耦合,符合单一责任原则。
val data: LiveData<String> get() = _data
的写法是一种封装和暴露数据的设计模式,确保 LiveData
的数据只读。
而同时updateData方法提供了一种集中控制数据更新的方法,这种写法符合mvvm设计模式。
- 在activity或fragment中使用livedata?
Kotlin
class MyActivity: AppCompatAcitvity() {
private lateinit var viewModel: MyViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModel = ViewModelProvider(this).get(MyViewModel::class.java)
// 观察livedata的数据变化
viewModel.data.observe(this, Observer { newData ->
// 数据变化时更新ui
textView.text = newData
})
// 更新数据的事件
button.setOnClickListener {
viewModel.updateData("New data")
}
}
}
MutableLiveData
什么是MutableLiveData?他和LiveData有什么区别?
MutableLiveData是LiveData的子类,他允许数据的读写。
通常在ViewModel中使用MutableLiveData更新数据,同时通过LiveData同步MutableLiveData的数据,并暴露给外部观察者。
Kotlin
class MyViewModel: ViewModel {
private val _data = MutableLiveData<String>()
val data: LiveData<String> get() = _data
fun updateData(newData: String) {
_data.value = newData
}
}
与Observable的区别
两者都属于实现响应式编程的数据持有类,但是设计和使用上有细微区别:
- 生命周期感知:Observable没有这个特性,需要手动处理
- 主线程操作:livedata的更新在主线程执行,而Observable需要显式指定订阅和发布线程
- 简单易用:Livedata更简单易用,因为他本质是面向生命周期,Observable需要处理更多的线程和生命周期的逻辑
单例模式
- 请解释什么是单例模式,并展示如何在kotlin中实现一个线程安全的单例模式。
单例模式是一种创建型模式,它保证在一个应用的生命周期之内,一个类只有一个实例,并提供一个全局访问点。他通常用于共享配置对象、缓存等。
在kotlin中,可以使用object关键字轻松实现线程安全的单例模式,而且是默认线程安全的。
Kotlin
object Singleton {
fun doSomething() {
println("do something in singleton")
}
}
- 在java中如何实现单例模式及其线程安全性
在java中可以使用"双重锁检查锁定"机制来实现一个线程安全的单例模式。
java
public class Singleton {
// volatile关键字保证变量可见性
private static volatile Singleton instance;
private Singleton() {
// 构造方法
}
// 双重锁
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
此处使用了 volatile
关键字来确保instance变量的可见性,是为了确保在多线程环境下的正确性可可见性。
在多线程环境中,多个线程可能会同时访问并修改变量,而单例模式要做的就是全局访问同一个变量不出错;
同时线程在内存中有一个工作内存,用于存储从主内存中读取和写入的数据,但每个线程的工作内存彼此隔离,导致每个线程对变量的修改对于其他线程是不可见的;
在这种情况下,使用volatile
关键字确保共享变量的可见性尤为必要。如果一个线程修改了volatile变量,那么修改后的值会立刻对所有线程可见,这确保了在双重锁模式中,所有线程都能看到最新的值。
3.单例模式的使用场景
1)全局配置管理
包含应用程序配置、数据库配置等全局配置的对象,需要通过单例保证配置一致性和全局访问性。
Kotlin
object ConfigurationManager {
var configuration: Configuration? = null
}
2)全局的日志记录对象,用于记录应用程序的运行日志
Kotlin
object Logger {
fun log(message: String) {
println("Log: $message")
}
}
3)全局线程池管理,用于应用程序中的并发调度任务
Kotlin
object ThreaadPoolManager {
private val executorService: ExecutorService = Executors.newFixedThreadPool(4)
fun execute(task: Runnable) {
executorService.execute(task)
}
}
4) 用于缓存应用程序中经常使用的对象,避免重复创建开销。
Kotlin
object CacheManager {
private val cache = mutableMapOf<String, Any>()
fun put(key: String, value: Any) {
cache[key] = value
}
fun get(key: String): Any? {
return cache[key]
}
}
5)管理数据库的连接,确保全局只有一个连接池实例。
Kotlin
object DatabaseConnectionPool {
private val connectionPool: MutableList<Connection> = MutableListOf()
fun getConnection(): Connection {
// 从数据库池中返回一个connectoin
}
}
6)应用程序上下文:用于管理应用程序的生命周期和全局状态
Kotlin
object ApplicationContext {
var context: Context? = null
}
- 在我的项目中,把一个获取应用使用时长的工具类设计为了单例模式使用。
这是因为有用到一个获取当前前台app的方法,并且需要存储这个app。有对于状态的保存,因此将该工具类设计为单例模式。
工厂模式
- 请解释工厂模式,并举例说明在Android开发中如何使用
工厂模式是一种创建型模式,通过定义一个接口或抽象类,让子类决定实例化的具体对象。同时它创建对象的逻辑被封装了起来,使代码更具扩展性和可维护性。
在Android开发中常用语viewmodel的创建。
Kotlin
class MyViewModelFactory(private val repository: MyRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel?> create(modelClass: Class<T>): T {
MyViewModel(repository) as T
} else {
throw IllegalArgumentException("Unknown ViewModel class")
}
}
此处使用create方法常见具体的viewmodel实例。
- 请举例说明工厂模式在实际项目中的一个应用场景。
工厂模式常用于创建不同类型的对象实例,如根据传参创建不同类型的fragment实例。
Kotlin
class FragmentFactory {
companion object {
fun createFragment(type: String):Fragemnt {
return when(type) {
"Home" -> HomeFragment()
"Settings" -> SettingsFragment()
else -> throw IllegalArgumentException("Unknown fragment type")
}
}
}
}
此处使用了伴生对象(companion object)。在kotlin中没有静态方法的定义,伴生对象提供了类似java中静态方法的功能。通过定义在伴生对象代码块中的方法,我们可以直接使用类名去调用这些方法,而无需创建类的实例。
Kotlin
// 调用示例
val homeFrag = FragmentFactory.createFragment("Home")
val settingsFrag = FragmentFactory.createFragment("Settings")
- 在我的项目中,我用于实现不同dialog的密码输入完毕和取消输入的回调处理。
对于不同管控的dialog,会有不同的上述事件实现方式。我在自定义的dialog中在合适的时机调用这两个接口,而具体的实现交给引入了dialog具体界面。
弱引用
- 什么是弱引用,什么情况下使用?
弱引用是一种不会阻止垃圾回收的引用类型。在java中使用WeakReference实现,用于缓存、监听器和其他不应影响对象生命周期的场景。
使用弱引用可以避免内存泄漏,如一个长时间存在的对象持有大量临时对象的引用,而这些临时对象不应该影响gc的回收。
java
import java.lang.ref.WeakReference;
public void Example {
public static void main(String[] args) {
Object obj = new Object();
// 创建一个指向obj的弱引用
WeakReference<Object> weakReference = new WeakReference<>(obj);
// 置空obj后进行垃圾回收
obj = null;
System.gc();
if (weakReference.get() != null) {
System.out.println("Object is still alive");
} else {
System.out.println("Object has been garbage collected");
}
}
}
- 在Android开发中,弱引用的实际应用场景是什么?
在Android中,弱引用常用于持有context对象,避免内存泄漏。如常见场景是持有activity和fragment的context,如果使用强引用可能导致内存泄漏。
java
public class MyWorker {
private WeakReference<Context> contextReference;
public MyWorker(Context context) {
this.contextReference = new WeakReference<>(context);
}
public void doWork() {
Context context = contextReference.get();
if (context != null) {
// 使用context做操作
} else {
// context被垃圾回收了
}
}
}
- 在我自己的开发场景中,我在生成和显示二维码时使用了弱引用。
我的开发场景是需要将生产一个二维码并显示显示在dialog,此处我使用弱引用指向dialog和dialog上需要显示二维码的imageview实例。这里使用弱引用的原因是避免直接持有两者的引用,以防止内存泄漏。
此外我将生成二维码的操作放在了runnable中异步执行,以避免该耗时操作影响主线程的执行。
在获取到dialog的弱引用后,我将其用于判断dialog是否为空、获取dialog的imageview弱引用、并最终回到主线程,在dialog的handler上发送更新二维码的消息,以达到内存使用和性能优化。
线程池
- 什么是线程池,为什么使用线程池?
线程池是一种线程管理技术,预先创建一定量的线程,任务提交到线程池之后,由线程池管理这些任务和线程。而线程池会复用线程来处理多个任务,从而减少频繁创建和销毁线程。
使用线程池的原因:
- 提高性能:通过复用线程,减少线程创建和销毁的开销
- 资源控制:限制并发线程的数量,避免资源过度消耗
- 更好管理:统一管理线程的生命周期,简化并发开发的复杂度
- 线程池的核心参数有哪些?
- corePoolSize:核心线程数,线程池维护的最小线程数量,即使空闲也不会被回收。
- maximumPoolSize:线程池允许的最大线程数。
- keepAliveTime:空闲线程存活时间,当线程池中的线程数量超过 corePoolSize 时,多余的线程在等待新任务的最长时间,超过这个时间将被终止和回收。
- unit:keepAliveTime 的时间单位,可以是秒、毫秒、微秒等。
- workQueue:任务队列,用于存放待执行的任务。
- threadFactory:线程工厂,用于创建新线程。
- handler:拒绝策略,当线程池已达到最大线程数且任务队列已满时,新的任务会被拒绝,处理被拒绝任务的策略。
- 线程池的几种常见类型及其区别?
1)FixedThreadPool
具有固定数量的线程池,线程数量不会发生改变
适用于负载稳定的场景,如处理固定数目的任务
java
ExecutorService fixedThreadpool = Executors.newFixedThreadPool(4);
2)CachedThreadpool
一个可缓存的线程池,如果在线程池中存在空闲线程,则复用;若无,则创建新线程
适用于大量小任务,且任务执行时间较短的场景
java
ExecutorService canchedThreadpool = Executors.newCachedThreadPool();
3)SingleThreadExectuor
单个线程的线程池,所有任务按照顺序执行,适用于需要确保执行顺序的场景
java
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
4)ScheduledThreadPool
支持定时和周期性执行任务的线程池,适用于定时任务或周期性任务的执行
java
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
- 如何配置线程池?
需要考虑如下因素:
1)任务特性
CPU密集型任务:核心线程数可以设置为cpu数量
io密集型任务:核心线程数设置为cpu数量的两倍或者更多
2)资源限制
根据系统内存和其他资源的限制,合理设置最大线程数和任务队列大小,以避免资源耗尽
3)业务需求
根据具体业务场景调整线程池参数,如需要快速响应的可以增加核心线程数
4)拒绝策略
考虑任务队列满时的处理方式,选择合适的拒绝策略,如抛出异常、丢弃任务、调用者执行等
- 拒绝策略是什么,有几种常见的拒绝策略?
当线程池无法接受新的任务、即最大线程数且任务队列已满时,如何处理新提交任务的策略。
常见的拒绝策略如下:
AbortPolicy:默认的拒绝策略,直接抛出异常,阻止系统正常工作
CallerRunsPolicy:由调用线程去执行任务,即线程不会丢弃任务,但可能影响线程
DiscardPolicy:直接丢弃任务,不抛出异常
DiscardOldestPolicy:丢弃队列中最老的任务,然后重新提交新任务
Handler
- 什么是handler,什么时候需要使用handler?
handler是用于处理线程间通信的一个类。允许我们在线程中(通常是主线程)排队执行message和runnable。handler使得我们可以在不同的线程中更新ui,并在指定的时间执行任务。
- 如何避免handler的内存泄漏?
内存泄漏在使用handler时很常见,特别是在处理长生命周期任务的时候。可以使用:
- 静态内部类:将handler生命为静态内部类,避免隐式持有外部类(如activity)引用
- 弱引用:使用WeakReference持有外部引用类,确保在外部类销毁时可以进行垃圾回收
Kotlin
// 静态内部类创建MyHandler
static class MyHandler(activity: MainActivity): Handler() {
// 使用弱引用避免外部类持有
private val weakActivity: WeakReference<MainActivity> = WeakReference(activity)
override fun handleMessage(msg: Message) {
val activity = weakActivity.get()
if (activity != null) { // 处理事件 }
}
}
// 在activity中创建handler实例
private val handler = MyHandler(this)
- 什么是HandlerThread,与普通的线程相比,它有什么优势?
HandlerThread是一个带有looper的线程,便于在后台线程中运行消息循环,简化了在后台线程中使用handler的实现,使得我们不用手动设置looper和messageQueue。
在activity和fragment中我们不用配置looper和messageQueue,是因为在主线程中已经内置有这两者。而此处我们讨论的是在子线程中开辟的handler,因此是需要手动实现的。
java
// 普通的线程实现handler
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
// 准备looper
Looper.prepare();
// 创建hanlder
Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
// 处理消息
Log.d("Handler", "收到消息:" + msg.what);
}
}
// 启动消息循环
Looper.loop();
}
}).start;
// 使用handlerThread
HandlerThread handlerThread = new HandlerThread("MyHandlerThread");
// 启动handlerThread
handlerThread.start();
// 使用handlerThread的looper创建handler
Handler handler = new Handler(handlerThread.getLooper()) {
@Override
public void handleMessage(Message msg) {
// 处理消息
Log.d("Handler", "收到消息:" + msg.what);
}
};
比较可知:
-
代码简化:普通的thread需要手动调用looper和messageQueue
-
线程生命周期管理:普通的thread需要自行管理线程的开始、结束和异常处理,handlerThread封装了生命周期管理
-
提升了代码可读性
-
在我的项目中,我将handler用于轮询当前前台app,并记录其运行时间。
在网上的面经中出现的问题
事件分发机制
- 什么是事件分发机制?
事件分发机制是Android中处理触摸事件的核心机制。
触摸事件的传递遵循一个明确的顺序,从activity开始,经由ViewGroup,最后到达具体的view。
1)为什么是从activity开始?
当用户与屏幕交互时,底层系统会首先捕获触摸事件,然后传递到当前活跃的activity。
而activity时主要的事件分发入口点,它负责接收系统传来的触摸事件并开始分发过程。
2)ViewGroup和View的关系是什么?
ViewGroup是所有视图容器(如LinearLayout
、RelativeLayout
、ConstraintLayout
等)的父类,ViewGroup可以包含多个子View或子ViewGroup。
举例来说,有一个页面使用RelativeLayout实现,里面包含了一个Button、嵌套了一个LinearLayout用于在button点击后显示信息。
根据前面的概念可知,RelativeLayout等视图容器继承了ViewGroup,是它的子类;
而Button是具体的View,LinearLayout是RelativeLayout这个大的ViewGroup的子ViewGroup。
3)为什么事件分发是这个顺序?
事件分发链:触摸事件的分发是沿着视图树自顶向下分发的,从activity到根视图,然后从根视图递归到各个子视图
Activity.dispatchTouchEvent( ):首先activity接收到触摸事件,进入其dispatchTouchEvent方法
ViewGroup.dispatchTouchEvent( ):然后事件被传递给根视图(通常是一个ViewGroup),也进入其dispatchTouchEvent方法
递归分发:如果当前ViewGroup不拦截事件,他会继续将事件传递给其子视图的dispatchTouchEvent方法。这个过程会一直执行,直到叶子节点View。而叶子节点的View也封装了dispatchTouchEvent方法。
基本流程如下:
- Activity.dispatchTouchEvent( ):Activity接收到事件,用这方法分发事件
- ViewGroup.dispatchTouchEvent( ):Activity传递给了根布局(通常是ViewGroup),再由这个方法处理和分发事件
- ViewGroup.onInterceptTouchEvent( ):ViewGroup内部可以根据此方法决定是否拦截事件。返回true拦截事件,则该ViewGroup自己处理该事件,否则事件继续向下传递
- View.dispatchTouchEvent( ):若事件未被拦截,会传递到具体的View的dispatchTouchEvent
- View.onTouchEvent( ):View通过自身的onTouchEvent方法处理事件。如果该方法返回true,该事件被消费,否则事件继续向上传递
向上传递指的是:
- 当view不愿意或无法处理当前事件时,父级ViewGroup可以尝试处理该事件。
- 此时View.onTouchEvent( )返回了false,表示当前事件没有被消费,会走父级的ViewGroup.onTouchEvent( )。
- 如果父级ViewGroup也不处理该事件,将会向上递归,直到被消费或丢弃。
View绘制
1. 什么是View的绘制流程
包括3个主要阶段:measure(测量)、layout(布局)和draw(绘制)。
这些都是在View分层树(View和ViewGroup形成的树形结构)中逐层调用的。
1)measure
测量是为了确定每个View的宽高。ViewGroup会调用子视图的measure方法。
java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
super.onMeasure();
// 测量逻辑
}
执行流程:
- 根视图调用measure方法
- ViewGroup调用每个子视图的measure方法
- 每个子视图根据传入的MeasureSpec计算自己的宽高,并调用
setMeasuredDimension(w, h)将获取到的每个子视图的宽高用于设置View的大小
2)layout
布局是为了确定每个view在父视图的位置。ViewGroup会递归调用子视图的layout方法。
java
@Override
protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
super.onLayout(changed, left, top, right, bottom);
// 布局逻辑
}
执行流程:
- 根视图调用layout方法
- ViewGroup根据自身尺寸和布局参数,确定每个子视图的位置,并调用子视图的layout方法
- 每个子视图根据传入的布局边界(left, top, right, bottom)确定自己的显示区域
3)draw
确定了大小和位置,最后就是将每个view画在屏幕上。
draw方法调用会向下传递,最终绘制出整个视图树。
2. onMeasure中的measureSpec是什么,wrap_content为什么会失效?
在Android中,wrap_content是常用的布局属性,他表示视图应优先考虑自身内容大小。
但某种情况下wrap_content可能会失效,这在自定义视图(尤其是自定义视图容器ViewGroup)中更为明显。
在了解wrap_content失效原因之前,我们先了解一下MeasureSpec。
MeasureSpec详解
measureSpec实际上是一个整型值,由测量模式和测量大小两部份组成。
java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
int widthSize = MeasureSpec.getSize(widthMeasureSpec);
int heightSize = MeasureSpec.getSize(heightMeasureSpec);
// 错误:忽略子视图测量结果
int width = widthMode == MeasureSpec.EXACTLY ? widthSize : 300; // 错误的固定值
int height = heightMode == MeasureSpec.EXACTLY ? heightSize : 300; // 错误的固定值
setMeasuredDimension(width, height);
}
可见,测量模式可以根据MeasureSpec.getMode方法获得,测量大小由MeasureSpec.getSize方法获得。测量模式氛围如下几种模式:
- EXACTLY:表示父视图已经确定了view的大小,即MeasureSpec中指定的值
- AT_MOST:表示view应当优先考虑尺寸建议值,但不得超过MeasureSpec指定的最大值
- UNSPECIFIED:表示view大小没有限制,通常父视图不使用该模式
wrap_content失败原因分析
1)父视图的测量模式是EXACTLY
此情况下,子视图无论设置什么布局参数(包括wrap_content),最终尺寸都会按照父视图的要求设置成指定的值。
java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
// View 被设置为父视图明确指定的大小
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
}
2)父视图未正确处理wrap_content
在自定义ViewGroup的onMeasure方法中,如果未正确处理子视图的wrap_content,可能导致wrap_content的设置无效,如直接给子视图一个固定的大小,即使使用了AT_MOST或者UNSPECIFIED,子视图的表现也可能不符合预期。
java
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
int widthMode = MeasureSpec.getMode(widthMeasureSpec);
int heightMode = MeasureSpec.getMode(heightMeasureSpec);
// 错误处理:父视图没有正确测量子视图
int width = widthMode == MeasureSpec.EXACTLY ? MeasureSpec.getSize(widthMeasureSpec) : 300; // 错误的固定大小,不考虑子视图实际大小
int height = heightMode == MeasureSpec.EXACTLY ? MeasureSpec.getSize(heightMeasureSpec) : 300; // 错误的固定大小
setMeasuredDimension(width, height);
}
3)子视图中的onMeasure没有效果
如果在自定义视图的onMeasure中未正确处理传入的MeasureSpec,也会导致wrap_content失效。