开发中使用了什么技术?
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发生更改
与其他架构比较(重点)
1. MVP与MVVM有什么不同?
与MVP区别在于数据绑定和通信方式。mvvm使用数据绑定(livedata或databinding)使得view和model自动同步,而mvp使用接口方式手动更新;
2. 为什么选择MVVM而不是MVC?
1)数据绑定:mvvm中引入了数据绑定,而mvc没有,需要手动更新,可能出错;
2)更加清晰的职责分离和耕地的耦合性
3)mvvm使用单向数据流,数据从model流向view,view通过viewmodel反应model的变化,使得数据流向可预测,便于调试和管理;
mvc中,数据和交互可能在view和controller之间双向流动,特别是在处理用户输入时,增加了调试和维护的难度。
数据绑定和观察者模式
1. 如何实现数据绑定(databinding)
在我的开发过程中,我使用了livedata进行数据绑定。通过这个机制,view可以观察viewmodel中的数据是否发生改变,并因此而改变view。
2. 如何通过databinding简化mvvm中的代码
通过databinding,可以直接在xml中绑定ui组件和viewmodel中的数据,省去了findviewbyid,使得ui更新更加简洁直观。
ViewModel
1. 如何管理和保存viewmodel中的数据,防止在配置更改(如屏幕旋转)时丢失?
viewmodel是感知生命周期的方式设计的,他的生命周期比activity和fragment更长。
当配置发生改变,activity会被重建,但viewmodel得以保留,因此可以用来持有和管理数据,避免数据丢失。
2. ViewModelScope是什么,有什么作用?
它是一个CoroutineScope,用于在viewmodel中启动协程。当viewmodel被销毁时,ViewModelScope内部启动的所有协程也会被销毁,以确保资源得以释放,避免内存泄漏。
3. 如何在viewmodel中处理异步操作?
使用viewmodelscope启动协程执行异步操作,如网络请求或数据库操作。
class MyViewModel : ViewModel() { fun fetchData() { viewModelScope.launch{ val result = repository.loadData() // 耗时操作的举例 // 更新livedata } } }
4. 错误处理和状态管理
1)如何在mvvm中处理错误和应用状态?
可以封装一个result类型的数据累来确保统一的错误处理的状态管理,然后在viewmodel中根据结果更新livedata。
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
基础概念
1. 什么是compose?与传统的xml相比有什么优势?
compose是现代化的ui框架,使用kotlin语言声明式的方式编写ui。
与传统xml相比,compose提供了更简洁的方式来构建界面,减少了大量样板代码。
2. 解释一下compose的可组合函数的概念
@Composable是compose的核心概念,它标记了一个函数是可组合的,即可以用来描述ui。
状态管理(重点)
1. 解释一下compose中状态管理和remember、mutableStateOf的作用
compose的状态管理基于可观察的状态,当观察到内容发生变化时,compose会重新组合受影响的部份,实时更新ui。
remember用于记住状态,也可以避免在加载或重组时数据丢失。
mutableStateOf创建一个可变状态的对象,当对象的值发生变化时,会通知compose进行重组。
@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对象更新。
2. 如何在compose中实现状态的持久化,比如在屏幕旋转是状态不丢失?
在屏幕旋转的时候,当前的activity会被销毁并重建一个新的activity实例,生命周期如下:
而这也意味着app需要在销毁和重建时保存和恢复activity的状态。我们可以通过两个方式实现。
1)保存和恢复实例状态
@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方法来配置更改的回调。
@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在配置发生改变时保存并恢复状态。
@Composable fun Counter() { var count by rememberSaveable { mutableStateOf(0) } Button(onClick{ count++ }) { Text("Clicked $count times") } }
rememberSaveable会在配置发生改变时自动保存状态,并在重建时恢复,以此防止状态丢失。
同时我们前面还学到了可以使用viewmodel来保存,原因是viewmodel的生命周期略长于activity,配置发生改变时activity被销毁并重建,但viewmodel中的数据会被保存。
3. 什么是state hoisting?为什么在compose中推荐使用这种模式?
state hoisting直译状态提升,是一种把状态从子组件提升到父组件的模式。在此模式下,组件的状态不是由自己管理,而是由其父组件管理,并通过参数传递。
@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排列方式 |
协程
1. 请解释LaunchedEffect的用途,并提供一个使用的场景示例
LaunchedEffect用于在可组合函数中启动协程,通常用于执行需要挂起的操作,如网络请求或数据库操作。
@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") } }
2. 可组合函数内部能否直接调用挂起函数?
可组合函数不能直接调用,因为两者上下文(context)不同,不能直接混用。这是因为挂起函数需要一个协程环境,而composable函数没有提供这种环境。
因此可以使用LaunchedEffect或rememberCorouroutinecope等API来启动协程调用挂起函数。
@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
基本用法
1. 如何在viewmodel中使用livedata?
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设计模式。
2. 在activity或fragment中使用livedata?
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的数据,并暴露给外部观察者。
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需要处理更多的线程和生命周期的逻辑
单例模式
1. 请解释什么是单例模式,并展示如何在kotlin中实现一个线程安全的单例模式。
单例模式是一种创建型模式,它保证在一个应用的生命周期之内,一个类只有一个实例,并提供一个全局访问点。他通常用于共享配置对象、缓存等。
在kotlin中,可以使用object关键字轻松实现线程安全的单例模式,而且是默认线程安全的。
object Singleton { fun doSomething() { println("do something in singleton") } }
2. 在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)全局配置管理
包含应用程序配置、数据库配置等全局配置的对象,需要通过单例保证配置一致性和全局访问性。
object ConfigurationManager { var configuration: Configuration? = null }
2)全局的日志记录对象,用于记录应用程序的运行日志
object Logger { fun log(message: String) { println("Log: $message") } }
3)全局线程池管理,用于应用程序中的并发调度任务
object ThreaadPoolManager { private val executorService: ExecutorService = Executors.newFixedThreadPool(4) fun execute(task: Runnable) { executorService.execute(task) } }
4) 用于缓存应用程序中经常使用的对象,避免重复创建开销。
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)管理数据库的连接,确保全局只有一个连接池实例。
object DatabaseConnectionPool { private val connectionPool: MutableList<Connection> = MutableListOf() fun getConnection(): Connection { // 从数据库池中返回一个connectoin } }
6)应用程序上下文:用于管理应用程序的生命周期和全局状态
object ApplicationContext { var context: Context? = null }
4. 在我的项目中,把一个获取应用使用时长的工具类设计为了单例模式使用。
这是因为有用到一个获取当前前台app的方法,并且需要存储这个app。有对于状态的保存,因此将该工具类设计为单例模式。
工厂模式
1. 请解释工厂模式,并举例说明在Android开发中如何使用
工厂模式是一种创建型模式,通过定义一个接口或抽象类,让子类决定实例化的具体对象。同时它创建对象的逻辑被封装了起来,使代码更具扩展性和可维护性。
在Android开发中常用语viewmodel的创建。
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实例。
2. 请举例说明工厂模式在实际项目中的一个应用场景。
工厂模式常用于创建不同类型的对象实例,如根据传参创建不同类型的fragment实例。
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中静态方法的功能。通过定义在伴生对象代码块中的方法,我们可以直接使用类名去调用这些方法,而无需创建类的实例。
// 调用示例 val homeFrag = FragmentFactory.createFragment("Home") val settingsFrag = FragmentFactory.createFragment("Settings")
3. 在我的项目中,我用于实现不同dialog的密码输入完毕和取消输入的回调处理。
对于不同管控的dialog,会有不同的上述事件实现方式。我在自定义的dialog中在合适的时机调用这两个接口,而具体的实现交给引入了dialog具体界面。
弱引用
1. 什么是弱引用,什么情况下使用?
弱引用是一种不会阻止垃圾回收的引用类型。在java中使用WeakReference实现,用于缓存、监听器和其他不应影响对象生命周期的场景。
使用弱引用可以避免内存泄漏,如一个长时间存在的对象持有大量临时对象的引用,而这些临时对象不应该影响gc的回收。
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"); } } }
2. 在Android开发中,弱引用的实际应用场景是什么?
在Android中,弱引用常用于持有context对象,避免内存泄漏。如常见场景是持有activity和fragment的context,如果使用强引用可能导致内存泄漏。
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被垃圾回收了 } } }
3. 在我自己的开发场景中,我在生成和显示二维码时使用了弱引用。
我的开发场景是需要将生产一个二维码并显示显示在dialog,此处我使用弱引用指向dialog和dialog上需要显示二维码的imageview实例。这里使用弱引用的原因是避免直接持有两者的引用,以防止内存泄漏。
此外我将生成二维码的操作放在了runnable中异步执行,以避免该耗时操作影响主线程的执行。
在获取到dialog的弱引用后,我将其用于判断dialog是否为空、获取dialog的imageview弱引用、并最终回到主线程,在dialog的handler上发送更新二维码的消息,以达到内存使用和性能优化。
线程池
1. 什么是线程池,为什么使用线程池?
线程池是一种线程管理技术,预先创建一定量的线程,任务提交到线程池之后,由线程池管理这些任务和线程。而线程池会复用线程来处理多个任务,从而减少频繁创建和销毁线程。
使用线程池的原因:
- 提高性能:通过复用线程,减少线程创建和销毁的开销
- 资源控制:限制并发线程的数量,避免资源过度消耗
- 更好管理:统一管理线程的生命周期,简化并发开发的复杂度
2. 线程池的核心参数有哪些?
- corePoolSize:核心线程数,线程池维护的最小线程数量,即使空闲也不会被回收。
- maximumPoolSize:线程池允许的最大线程数。
- keepAliveTime:空闲线程存活时间,当线程池中的线程数量超过 corePoolSize 时,多余的线程在等待新任务的最长时间,超过这个时间将被终止和回收。
- unit:keepAliveTime 的时间单位,可以是秒、毫秒、微秒等。
- workQueue:任务队列,用于存放待执行的任务。
- threadFactory:线程工厂,用于创建新线程。
- handler:拒绝策略,当线程池已达到最大线程数且任务队列已满时,新的任务会被拒绝,处理被拒绝任务的策略。
3. 线程池的几种常见类型及其区别?
1)FixedThreadPool
具有固定数量的线程池,线程数量不会发生改变
适用于负载稳定的场景,如处理固定数目的任务
ExecutorService fixedThreadpool = Executors.newFixedThreadPool(4);
2)CachedThreadpool
一个可缓存的线程池,如果在线程池中存在空闲线程,则复用;若无,则创建新线程
适用于大量小任务,且任务执行时间较短的场景
ExecutorService canchedThreadpool = Executors.newCachedThreadPool();
3)SingleThreadExectuor
单个线程的线程池,所有任务按照顺序执行,适用于需要确保执行顺序的场景
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
4)ScheduledThreadPool
支持定时和周期性执行任务的线程池,适用于定时任务或周期性任务的执行
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);
4. 如何配置线程池?
需要考虑如下因素:
1)任务特性
CPU密集型任务:核心线程数可以设置为cpu数量
io密集型任务:核心线程数设置为cpu数量的两倍或者更多
2)资源限制
根据系统内存和其他资源的限制,合理设置最大线程数和任务队列大小,以避免资源耗尽
3)业务需求
根据具体业务场景调整线程池参数,如需要快速响应的可以增加核心线程数
4)拒绝策略
考虑任务队列满时的处理方式,选择合适的拒绝策略,如抛出异常、丢弃任务、调用者执行等
5. 拒绝策略是什么,有几种常见的拒绝策略?
当线程池无法接受新的任务、即最大线程数且任务队列已满时,如何处理新提交任务的策略。
常见的拒绝策略如下:
AbortPolicy:默认的拒绝策略,直接抛出异常,阻止系统正常工作
CallerRunsPolicy:由调用线程去执行任务,即线程不会丢弃任务,但可能影响线程
DiscardPolicy:直接丢弃任务,不抛出异常
DiscardOldestPolicy:丢弃队列中最老的任务,然后重新提交新任务
Handler
1. 什么是handler,什么时候需要使用handler?
handler是用于处理线程间通信的一个类。允许我们在线程中(通常是主线程)排队执行message和runnable。handler使得我们可以在不同的线程中更新ui,并在指定的时间执行任务。
2. 如何避免handler的内存泄漏?
内存泄漏在使用handler时很常见,特别是在处理长生命周期任务的时候。可以使用:
- 静态内部类:将handler生命为静态内部类,避免隐式持有外部类(如activity)引用
- 弱引用:使用WeakReference持有外部引用类,确保在外部类销毁时可以进行垃圾回收
// 静态内部类创建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)
3. 什么是HandlerThread,与普通的线程相比,它有什么优势?
HandlerThread是一个带有looper的线程,便于在后台线程中运行消息循环,简化了在后台线程中使用handler的实现,使得我们不用手动设置looper和messageQueue。
在activity和fragment中我们不用配置looper和messageQueue,是因为在主线程中已经内置有这两者。而此处我们讨论的是在子线程中开辟的handler,因此是需要手动实现的。
// 普通的线程实现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封装了生命周期管理
- 提升了代码可读性
4. 在我的项目中,我将handler用于轮询当前前台app,并记录其运行时间。
在网上的面经中出现的问题
事件分发机制
1. 什么是事件分发机制?
事件分发机制是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方法。
@Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(); // 测量逻辑 }
执行流程:
- 根视图调用measure方法
- ViewGroup调用每个子视图的measure方法
- 每个子视图根据传入的MeasureSpec计算自己的宽高,并调用
setMeasuredDimension(w, h)将获取到的每个子视图的宽高用于设置View的大小
2)layout
布局是为了确定每个view在父视图的位置。ViewGroup会递归调用子视图的layout方法。
@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实际上是一个整型值,由测量模式和测量大小两部份组成。
@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),最终尺寸都会按照父视图的要求设置成指定的值。
@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,子视图的表现也可能不符合预期。
@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失效。