为面试准备的一些内容

开发中使用了什么技术?

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使用接口方式手动更新;

  1. 为什么选择MVVM而不是MVC?

1)数据绑定:mvvm中引入了数据绑定,而mvc没有,需要手动更新,可能出错;

2)更加清晰的职责分离和耕地的耦合性

3)mvvm使用单向数据流,数据从model流向view,view通过viewmodel反应model的变化,使得数据流向可预测,便于调试和管理;

mvc中,数据和交互可能在view和controller之间双向流动,特别是在处理用户输入时,增加了调试和维护的难度。

数据绑定和观察者模式

  1. 如何实现数据绑定(databinding)

在我的开发过程中,我使用了livedata进行数据绑定。通过这个机制,view可以观察viewmodel中的数据是否发生改变,并因此而改变view。

  1. 如何通过databinding简化mvvm中的代码

通过databinding,可以直接在xml中绑定ui组件和viewmodel中的数据,省去了findviewbyid,使得ui更新更加简洁直观。

ViewModel

  1. 如何管理和保存viewmodel中的数据,防止在配置更改(如屏幕旋转)时丢失?

viewmodel是感知生命周期的方式设计的,他的生命周期比activity和fragment更长。

当配置发生改变,activity会被重建,但viewmodel得以保留,因此可以用来持有和管理数据,避免数据丢失。

  1. ViewModelScope是什么,有什么作用?

它是一个CoroutineScope,用于在viewmodel中启动协程。当viewmodel被销毁时,ViewModelScope内部启动的所有协程也会被销毁,以确保资源得以释放,避免内存泄漏。

  1. 如何在viewmodel中处理异步操作?

使用viewmodelscope启动协程执行异步操作,如网络请求或数据库操作。

Kotlin 复制代码
class MyViewModel : ViewModel() {
    fun fetchData() {
        viewModelScope.launch{
            val result = repository.loadData() // 耗时操作的举例
            // 更新livedata
        }
    }
}
  1. 错误处理和状态管理

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

基础概念

  1. 什么是compose?与传统的xml相比有什么优势?

compose是现代化的ui框架,使用kotlin语言声明式的方式编写ui。

与传统xml相比,compose提供了更简洁的方式来构建界面,减少了大量样板代码。

  1. 解释一下compose的可组合函数的概念

@Composable是compose的核心概念,它标记了一个函数是可组合的,即可以用来描述ui。

状态管理(重点)

  1. 解释一下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的定义,我们分步解析:

  1. mutableStateOf(0):创建一个可变的状态对象,在它的值发生变化时会触发重组
  2. remember:在组合过程中会记住相应的值,只要作用组合域没有被销毁,remember返回的值保持不变
  3. by:关键字用于声明属性委托。

当使用by关键字进行属性委托时,kotlin会自动处理这个属性的访问和修改,并将其委托给后面的对象。具体到这个例子,就是count的getter和setter方法被委托给了MutableState对象。因此,count对象的状态可以被实时修改的mutableState对象更新。

  1. 如何在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中的数据会被保存。

  1. 什么是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排列方式 |

协程

  1. 请解释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")
    }
}
  1. 可组合函数内部能否直接调用挂起函数?

可组合函数不能直接调用,因为两者上下文(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

基本用法

  1. 如何在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设计模式。

  1. 在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需要处理更多的线程和生命周期的逻辑

单例模式

  1. 请解释什么是单例模式,并展示如何在kotlin中实现一个线程安全的单例模式。

单例模式是一种创建型模式,它保证在一个应用的生命周期之内,一个类只有一个实例,并提供一个全局访问点。他通常用于共享配置对象、缓存等。

在kotlin中,可以使用object关键字轻松实现线程安全的单例模式,而且是默认线程安全的。

Kotlin 复制代码
object Singleton {
    fun doSomething() {
        println("do something in singleton")
    }
}
  1. 在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
}
  1. 在我的项目中,把一个获取应用使用时长的工具类设计为了单例模式使用。

这是因为有用到一个获取当前前台app的方法,并且需要存储这个app。有对于状态的保存,因此将该工具类设计为单例模式。

工厂模式

  1. 请解释工厂模式,并举例说明在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实例。

  1. 请举例说明工厂模式在实际项目中的一个应用场景。

工厂模式常用于创建不同类型的对象实例,如根据传参创建不同类型的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")
  1. 在我的项目中,我用于实现不同dialog的密码输入完毕和取消输入的回调处理。

对于不同管控的dialog,会有不同的上述事件实现方式。我在自定义的dialog中在合适的时机调用这两个接口,而具体的实现交给引入了dialog具体界面。

弱引用

  1. 什么是弱引用,什么情况下使用?

弱引用是一种不会阻止垃圾回收的引用类型。在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");
        }
    }
}
  1. 在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被垃圾回收了
        }
    }
}
  1. 在我自己的开发场景中,我在生成和显示二维码时使用了弱引用。

我的开发场景是需要将生产一个二维码并显示显示在dialog,此处我使用弱引用指向dialog和dialog上需要显示二维码的imageview实例。这里使用弱引用的原因是避免直接持有两者的引用,以防止内存泄漏。

此外我将生成二维码的操作放在了runnable中异步执行,以避免该耗时操作影响主线程的执行。

在获取到dialog的弱引用后,我将其用于判断dialog是否为空、获取dialog的imageview弱引用、并最终回到主线程,在dialog的handler上发送更新二维码的消息,以达到内存使用和性能优化。

线程池

  1. 什么是线程池,为什么使用线程池?

线程池是一种线程管理技术,预先创建一定量的线程,任务提交到线程池之后,由线程池管理这些任务和线程。而线程池会复用线程来处理多个任务,从而减少频繁创建和销毁线程。

使用线程池的原因:

  • 提高性能:通过复用线程,减少线程创建和销毁的开销
  • 资源控制:限制并发线程的数量,避免资源过度消耗
  • 更好管理:统一管理线程的生命周期,简化并发开发的复杂度
  1. 线程池的核心参数有哪些?
  • corePoolSize:核心线程数,线程池维护的最小线程数量,即使空闲也不会被回收。
  • maximumPoolSize:线程池允许的最大线程数。
  • keepAliveTime:空闲线程存活时间,当线程池中的线程数量超过 corePoolSize 时,多余的线程在等待新任务的最长时间,超过这个时间将被终止和回收。
  • unit:keepAliveTime 的时间单位,可以是秒、毫秒、微秒等。
  • workQueue:任务队列,用于存放待执行的任务。
  • threadFactory:线程工厂,用于创建新线程。
  • handler:拒绝策略,当线程池已达到最大线程数且任务队列已满时,新的任务会被拒绝,处理被拒绝任务的策略。
  1. 线程池的几种常见类型及其区别?

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. 如何配置线程池?

需要考虑如下因素:

1)任务特性

CPU密集型任务:核心线程数可以设置为cpu数量

io密集型任务:核心线程数设置为cpu数量的两倍或者更多

2)资源限制

根据系统内存和其他资源的限制,合理设置最大线程数和任务队列大小,以避免资源耗尽

3)业务需求

根据具体业务场景调整线程池参数,如需要快速响应的可以增加核心线程数

4)拒绝策略

考虑任务队列满时的处理方式,选择合适的拒绝策略,如抛出异常、丢弃任务、调用者执行等

  1. 拒绝策略是什么,有几种常见的拒绝策略?

当线程池无法接受新的任务、即最大线程数且任务队列已满时,如何处理新提交任务的策略。

常见的拒绝策略如下:

AbortPolicy:默认的拒绝策略,直接抛出异常,阻止系统正常工作

CallerRunsPolicy:由调用线程去执行任务,即线程不会丢弃任务,但可能影响线程

DiscardPolicy:直接丢弃任务,不抛出异常

DiscardOldestPolicy:丢弃队列中最老的任务,然后重新提交新任务

Handler

  1. 什么是handler,什么时候需要使用handler?

handler是用于处理线程间通信的一个类。允许我们在线程中(通常是主线程)排队执行message和runnable。handler使得我们可以在不同的线程中更新ui,并在指定的时间执行任务。

  1. 如何避免handler的内存泄漏?

内存泄漏在使用handler时很常见,特别是在处理长生命周期任务的时候。可以使用:

  1. 静态内部类:将handler生命为静态内部类,避免隐式持有外部类(如activity)引用
  2. 弱引用:使用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)
  1. 什么是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);
    }
};

比较可知:

  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方法。

java 复制代码
@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方法。

java 复制代码
@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实际上是一个整型值,由测量模式和测量大小两部份组成。

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失效。

RecyclerView

布局优化怎么做的?

LinearLayout和RelativeLayout在性能上的区别

RecyclerView的缓存机制

相关推荐
lzb_kkk6 分钟前
【JavaEE】JUC的常见类
java·开发语言·java-ee
CYRUS STUDIO18 分钟前
ARM64汇编寻址、汇编指令、指令编码方式
android·汇编·arm开发·arm·arm64
爬山算法30 分钟前
Maven(28)如何使用Maven进行依赖解析?
java·maven
2401_857439691 小时前
SpringBoot框架在资产管理中的应用
java·spring boot·后端
怀旧6661 小时前
spring boot 项目配置https服务
java·spring boot·后端·学习·个人开发·1024程序员节
李老头探索1 小时前
Java面试之Java中实现多线程有几种方法
java·开发语言·面试
weixin_449310841 小时前
高效集成:聚水潭采购数据同步到MySQL
android·数据库·mysql
芒果披萨1 小时前
Filter和Listener
java·filter
qq_4924484461 小时前
Java实现App自动化(Appium Demo)
java
阿华的代码王国1 小时前
【SpringMVC】——Cookie和Session机制
java·后端·spring·cookie·session·会话