为面试准备的一些内容

开发中使用了什么技术?

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的定义,我们分步解析:

  1. mutableStateOf(0):创建一个可变的状态对象,在它的值发生变化时会触发重组
  2. remember:在组合过程中会记住相应的值,只要作用组合域没有被销毁,remember返回的值保持不变
  3. 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之后修改userisLoading = 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 = trueuser = 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时会返回_dataval 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: MyViewModeloverride 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 ->// 数据变化时更新uitextView.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() = _datafun 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时很常见,特别是在处理长生命周期任务的时候。可以使用:

  1. 静态内部类:将handler生命为静态内部类,避免隐式持有外部类(如activity)引用
  2. 弱引用:使用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() {@Overridepublic void run() {// 准备looperLooper.prepare();// 创建hanlderHandler handler = new Handler() {@Overridepublic 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()) {@Overridepublic void handleMessage(Message msg) {// 处理消息Log.d("Handler", "收到消息:" + msg.what);}
};

比较可知:

  1. 代码简化:普通的thread需要手动调用looper和messageQueue
  2. 线程生命周期管理:普通的thread需要自行管理线程的开始、结束和异常处理,handlerThread封装了生命周期管理
  3. 提升了代码可读性

4. 在我的项目中,我将handler用于轮询当前前台app,并记录其运行时间。

在网上的面经中出现的问题

事件分发机制

1. 什么是事件分发机制?

事件分发机制是Android中处理触摸事件的核心机制。

触摸事件的传递遵循一个明确的顺序,从activity开始,经由ViewGroup,最后到达具体的view。

1)为什么是从activity开始?

当用户与屏幕交互时,底层系统会首先捕获触摸事件,然后传递到当前活跃的activity。

而activity时主要的事件分发入口点,它负责接收系统传来的触摸事件并开始分发过程。

2)ViewGroup和View的关系是什么?

ViewGroup是所有视图容器(如LinearLayoutRelativeLayoutConstraintLayout 等)的父类,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方法。

基本流程如下:

  1. Activity.dispatchTouchEvent( ):Activity接收到事件,用这方法分发事件
  2. ViewGroup.dispatchTouchEvent( ):Activity传递给了根布局(通常是ViewGroup),再由这个方法处理和分发事件
  3. ViewGroup.onInterceptTouchEvent( ):ViewGroup内部可以根据此方法决定是否拦截事件。返回true拦截事件,则该ViewGroup自己处理该事件,否则事件继续向下传递
  4. View.dispatchTouchEvent( ):若事件未被拦截,会传递到具体的View的dispatchTouchEvent
  5. View.onTouchEvent( ):View通过自身的onTouchEvent方法处理事件。如果该方法返回true,该事件被消费,否则事件继续向上传递

向上传递指的是:

  1. 当view不愿意或无法处理当前事件时,父级ViewGroup可以尝试处理该事件。
  2. 此时View.onTouchEvent( )返回了false,表示当前事件没有被消费,会走父级的ViewGroup.onTouchEvent( )。
  3. 如果父级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();// 测量逻辑
}

执行流程:

  1. 根视图调用measure方法
  2. ViewGroup调用每个子视图的measure方法
  3. 每个子视图根据传入的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);// 布局逻辑
}

执行流程:

  1. 根视图调用layout方法
  2. ViewGroup根据自身尺寸和布局参数,确定每个子视图的位置,并调用子视图的layout方法
  3. 每个子视图根据传入的布局边界(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失效。

RecyclerView

布局优化怎么做的?

LinearLayout和RelativeLayout在性能上的区别

RecyclerView的缓存机制

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/389729.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

一文弄清Java的四大引用及其两大传递

开场白 Hello大家好呀&#xff0c;我是CodeCodeBond✊最近在复习很多很多的基础知识&#xff0c;有了很多新的感悟~ 话不多说&#xff0c;直接发车✈ 四大引用 问题切入点 在学习 Thread线程利用ThreadLocalMap实现线程的本地内存&#xff08;变量副本&#xff09;的时候&…

mac配置git的sshkey

在MAC中配置Git的SSH Key&#xff1a; 1.打开终端 2.生成SSH密钥&#xff0c;输入以下命令&#xff1a; ssh-keygen -t rsa -b 4096 -C “你自己的账号电子邮件地址” 按回车键后&#xff0c;系统会提示你输入文件保存路径&#xff0c;默认为~/.ssh/id_rsa直接按回车键使用默…

Mybatis实战:#{} 和 ${}的使用区别和数据库连接池

一.#{} 和 ${} #{} 和 ${} 在MyBatis框架中都是用于SQL语句中参数替换的标记&#xff0c;但它们在使用方式和处理参数值上存在一些显著的区别。 #{}的作用&#xff1a; #{} 是MyBatis中用于预编译SQL语句的参数占位符。它会将参数值放入一个预编译的PreparedStatement中&am…

Java语言程序设计——篇十一(2)

&#x1f33f;&#x1f33f;&#x1f33f;跟随博主脚步&#xff0c;从这里开始→博主主页&#x1f33f;&#x1f33f;&#x1f33f; 欢迎大家&#xff1a;这里是我的学习笔记、总结知识的地方&#xff0c;喜欢的话请三连&#xff0c;有问题可以私信&#x1f333;&#x1f333;&…

MySQL(8.0)数据库安装和初始化以及管理

1.MySQL下载安装和初始化 1.下载安装包 下载地址&#xff1a;https://downloads.mysql.com/archives/get/p/23/file/mysql-8.0.33-1.el7.x86_64.rpm-bundle.tar wget https://downloads.mysql.com/archives/get/p/23/file/mysql-8.0.33-1.el7.x86_64.rpm-bundle.tar 2.解压…

数据同步策略概览

数据同步在业务开发中比较普遍&#xff0c;例如 订阅MySQL的binlog将数据同步至异构数据库。数据同步方案需要考虑一下几点&#xff1a; 数据实时性要求数据量级是否有数据转换逻辑 可分为两种模式 发布订阅模式&#xff1a;分为订阅数据库log还是订阅应用层发的消息点对点模…

问界M7是不是换壳东风ix7? 这下有答案了

文 | AUTO芯 作者 | 谦行 终于真相大白了 黑子们出来挨打啊 问界M7是换壳的东风ix7&#xff1f; 你们没想到&#xff0c;余大嘴会亲自出来正面回应吧 瞧瞧黑子当时乐的 问界你可以啊&#xff01;靠改名字造车呢&#xff1f; 还有更过分的&#xff0c;说M7是东风小康ix7…

【网络】网络入门(第一篇)

网络入门可以从多个方面开始&#xff0c;以下是一个基本的网络入门指南&#xff0c;涵盖了网络的基本概念、网络类型、网络协议、网络拓扑、网络设备以及网络地址等方面。 一、网络基本概念 计算机网络&#xff1a;将多个计算机系统和设备连接在一起&#xff0c;以实现资源共…

CANoe系统变量模块里定义的结构体类型和变量从CAPL代码角度理解

CAPL里声明一个结构体类型&#xff1a; variables {struct DoIPMessage{byte version;byte inVersion;word type;dword length;byte payload[1500];};struct DoIPMessage doipMessage; }声明一个结构体类型DoIPMessage&#xff0c;定义了一个此结构体…

【数据结构】哈希表(散列表)

目录 1、unordered系列关联式容器 2、哈希概念 3、哈希函数 3.1 直接定址法 3.2 除留余数法 4、哈希冲突 4.1 闭散列(开放定址法) 4.1.1 线性探测 4.1.2 二次探测 4.1.3 线性探测代码实现 插入 搜索 删除 对于不可以取模的类型 4.2 开散列(哈希桶/拉链法) 插入…

【pyhton】Python中zip用法详细解析与应用实战

✨✨ 欢迎大家来到景天科技苑✨✨ &#x1f388;&#x1f388; 养成好习惯&#xff0c;先赞后看哦~&#x1f388;&#x1f388; &#x1f3c6; 作者简介&#xff1a;景天科技苑 &#x1f3c6;《头衔》&#xff1a;大厂架构师&#xff0c;华为云开发者社区专家博主&#xff0c;…

在WordPress上启用reCAPTCHA的指南

随着网络安全问题的日益严重&#xff0c;网站管理员必须采取措施保护自己的网站免受恶意攻击。对于WordPress用户来说&#xff0c;可以通过启用谷歌的reCAPTCHA功能来增强网站的安全性。本文将介绍两种在WordPress上启用reCAPTCHA的方法&#xff1a;使用插件和手动添加代码。 一…

白盒测试基础与实践:Python示例及流程图设计

文章目录 前言一、白盒测试是什么&#xff1f;主要特点常用方法优点缺点 二、白盒测试常用技术语句覆盖判定覆盖条件覆盖判定/条件覆盖条件组合覆盖路径覆盖 三、程序流程图设计四、测试用例设计1. 基本路径法2. 语句覆盖3. 判断覆盖4. 条件覆盖5. 判断/条件覆盖6. 条件组合覆盖…

两个好消息,你先听哪个?

1.第五大数据、人工智能与软件工程国际研讨会&#xff08;ICBASE 2024)成功申请IEEE出版&#xff0c;上线IEEE官网&#xff0c;欢迎投稿参会&#xff01;&#xff01;&#xff01; &#x1f4e3;IEEE独立出版&#xff0c;设置优秀评选 &#x1f525;院士加盟&#xff0c;中外高…

一个私有化的中文笔记工具个人知识库,极空间Docker部署中文版『Trilium Notes』

一个私有化的中文笔记工具&个人知识库&#xff0c;极空间Docker部署中文版『Trilium Notes』 哈喽小伙伴们好&#xff0c;我是Stark-C~ 最近被很多小伙伴问到NAS上的笔记工具&#xff0c;虽说之前也出过Memos&#xff0c;刚开始用起来还不错&#xff0c;但是用了一段时间…

(vue)el-cascader级联选择器按勾选的顺序传值,摆脱层级约束

(vue)el-cascader级联选择器按勾选的顺序传值,摆脱层级约束 需求&#xff1a;按勾选的顺序给后端传值 难点&#xff1a;在 Element UI 的 el-cascader 组件中&#xff0c;默认的行为是根据数据的层级结构来显示选项&#xff0c;用户的选择也会基于这种层级结构&#xff0c;el-…

文件解析漏洞—IIS解析漏洞—IIS7.X

在IIS7.0和IIS7.5版本下也存在解析漏洞&#xff0c;在默认Fast-CGI开启状况下&#xff0c;在一个文件路径/xx.jpg后面加上/xx.php会将 “/xx.jpg/xx.php” 解析为 php 文件 利用条件 php.ini里的cgi.fix_pathinfo1 开启IIS7在Fast-CGI运行模式下 在 phpstudy2018 根目录创建…

红酒与夜晚:享受静谧的品酒时光

当夜幕低垂&#xff0c;星光点点&#xff0c;世界仿佛进入了一个宁静而神秘的领域。在这样的夜晚&#xff0c;与一瓶定制红酒洒派红酒&#xff08;Bold & Generous&#xff09;相伴&#xff0c;便是一场令人陶醉的品酒之旅&#xff0c;让人在静谧中感受生活的美好。 一、夜…

《BiFormer: Vision Transformer with Bi-Level Routing Attention》CVPR2023

摘要 这篇论文提出了一种新型的视觉Transformer&#xff0c;名为BiFormer&#xff0c;它采用了双层路由注意力&#xff08;Bi-Level Routing Attention, BRA&#xff09;机制。注意力机制是视觉变换器的核心构建模块&#xff0c;能够捕获数据中的长期依赖性。然而&#xff0c;…

java远程调试

java远程调试 idea2024创一个Spring Web项目springdemo1 使用maven-assembly-plugin插件打包成JAR文件 pom.xml参考如下 <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0" xmlns:xsi&quo…