1.自定义View 【主要见教学138】
2.触摸事件 【主要见教学139】
3.activity生命周期
4,多线程通信
5.okhttp相关(优点,拦截器)
6.协程调度器
1.自定义View
“在项目开发过程中,我们经常会碰到业务场景需要使用到标准的View无法满足UI或交互需求的时候,这时自定义View就显得尤为重要。我常常通过继承View或者ViewGroup来实现自定义控件,从而达到精准控制布局、绘制和交互的目的。
首先,自定义View通常有两种方式:一种是对现有View进行扩展,另一种则是完全从零开始实现。当我需要扩展一个控件,只需要继承它,然后重写一些必要的方法,比如修改onDraw或者增加一些额外的逻辑;如果需求与现有控件差异较大,可能就需要直接继承View,完全掌控整个绘制和交互流程。
在设计自定义View时,需要关注三个核心部分:测量、布局和绘制。测量阶段主要在onMeasure中完成,根据父布局传过来的MeasureSpec来计算自身应该占用的空间,这一点在处理wrap_content与match_parent时尤为关键。然后,如果是容器类控件,还需要在onLayout中定位子View的位置,而这通常涉及到对子View的循环遍历和位置计算。最后一环就是onDraw,这里我们会直接在Canvas上绘制各种内容,比如形状、颜色、文字,甚至结合硬件加速的方式来实现更流畅的动画效果。
此外,自定义View在事件处理上也是一个重要部分。通常我们会重写onTouchEvent或者甚至dispatchTouchEvent去处理用户的触摸操作。比如当设计一个滑动控件或者手势敏感的控件时,就需要仔细分析手指按下、移动和抬起这几个状态,并进行相应的处理。重点在于合理处理控件内部的拖拽、点击判定以及手势冲突,以确保交互的准确和流畅。
另一个需要注意的点是自定义属性和样式。为了让自定义View更易复用并且能够通过XML配置,我们通常会定义一系列自定义属性,这样开发者在布局文件中就能直接设置各种参数,比如颜色、尺寸、动画时长等。通过在控件构造函数中读取这些属性,我们可以使View的表现更加灵活且符合设计要求,而且也便于后期的维护和扩展。
最后,性能优化也是自定义View的重要部分。在onDraw中,尽量避免创建临时对象和过于复杂的状态计算,减少不必要的重绘;必要时可以结合invalidate()和postInvalidate()来局部更新,确保界面渲染高效。另外,对于比较复杂的View,提前进行缓存绘制结果也是一种常见的优化手段,防止多次绘制同样的内容带来性能开销。
2.触摸事件
“触摸事件是Android交互的核心机制之一,实际上整个过程可以分为事件的捕获、分发和处理三个主要阶段。在触摸事件的生命周期中,系统首先在Activity中的dispatchTouchEvent方法捕获事件,然后递归地传递到根View,再由各个ViewGroup和View做进一步分发和消费。
首先,谈谈事件捕获。在用户触摸屏幕时,系统会构造一个MotionEvent对象,其中包含手指按下、移动、抬起等一系列动作。整个事件首先被传到Activity中,Activity在dispatchTouchEvent中可以进行最初的拦截或一些全局性的处理,但实际工作主要交给View层级的分发。
进入ViewGroup的分发机制后,每个ViewGroup的dispatchTouchEvent内部,会先调用onInterceptTouchEvent。这个方法的作用在于决定是由自身处理这次事件,还是将该事件分发给其子View。比如在一个复杂的布局中,如果父View发现当前手势是滚动或拖拽的趋势,就可能拦截子View的点击事件,由自己处理滚动操作。相反,如果手势比较细微且目标明确,就允许事件传递给子View来处理点击或其他具体交互。
当事件到达具体的View时,View会在其onTouchEvent方法中进行处理。通常我们会根据不同的事件类型,如ACTION_DOWN、ACTION_MOVE和ACTION_UP做出相应的响应。ACTION_DOWN一般是确定手指触摸的起始点,很多控件会在此时开始记录按下时的状态。而在ACTION_MOVE阶段,控件会根据手指移动的距离进行拖动或滑动更新,最后在ACTION_UP阶段完成整个交互,可能触发点击、松开等操作。
此外,Android还内置了手势识别机制,通过GestureDetector等工具可以识别双击、长按、滑动等复杂手势,极大地简化了开发者自己分析一系列触摸事件的工作流程。这个过程平时我们也需要注意事件的优先级和冲突问题,特别是在嵌套滚动、滑动冲突场景中,父子View之间必须协调好事件的分发和拦截,确保最合理的响应方式。
3.activity生命周期
“在Android开发中,Activity的生命周期是一个非常核心的概念,它直接影响到我们的应用如何响应用户交互和系统资源管理。Activity的生命周期包含了一系列状态和回调方法,每个方法在特定时机触发,帮助我们更好地管理资源、保存状态以及响应外部事件。
首先,当一个Activity首次启动的时候,系统会调用onCreate方法。在这个阶段,我们主要完成初始化工作,比如设置布局、初始化变量、绑定视图和数据。这是一个非常关键的环节,因为所有的初始化工作都需要在这里完成,且只会调用一次。
接下来,onStart方法在Activity即将对用户可见时调用,这时候Activity虽然不可交互,但已经可以加载一些视觉元素。紧接着,会进入onResume方法,此时Activity已经位于前台,用户可以与其进行交互。onResume往往伴随着我们启动定时器、启动动画或者注册传感器等操作,因为在这个状态下系统认为用户正在和Activity交互。
当Activity处于前台时,如果出现短暂的中断例如弹出一个对话框或来电,Activity会进入onPause状态。在onPause方法中,我们需要停止那些不必要的资源占用,比如暂停动画、停止音乐或者保存部分数据,这样可以保证当用户返回时不会遇到资源占用或数据丢失的问题。需要注意的是,onPause应该尽快执行完毕,因为它会影响系统对Activity优雅切换的处理。
如果Activity继续被其他Activity覆盖,那么它会调用onStop方法,这时Activity完全不可见了。在onStop中,我们会释放一些占用资源较多的对象或者注销不再必须的监听器,甚至保存一些持久化数据,因为系统在内存压力较大的时候可能会直接终止它。
最后,当Activity被销毁之前,系统会回调onDestroy方法,这是整个生命周期中的最后一个调用点。在onDestroy中,我们通常做最后的清理工作,比如释放所有占用的内存和资源,确保不会出现内存泄露。需要注意的是,并非所有情况下onDestroy都会被调用,比如系统在紧急回收内存时可能会直接终止Activity,所以在数据持久性上不能单单依赖onDestroy。
此外,还有一点非常重要的是在生命周期方法中如何处理状态保存与恢复。比如在onSaveInstanceState中,我们可以将一些必要的数据保存起来,以便Activity被回收后在onRestoreInstanceState或者onCreate中重现之前的状态,这在旋转屏幕或者内存紧张时非常关键。
4,多线程通信
“在Android开发中,多线程通信是一个非常关键的话题,因为我们通常需要将耗时操作放到子线程中处理,然后再把结果传递回主线程更新UI。多线程通信核心在于确保线程之间安全高效地传递数据和同步状态,同时避免线程安全问题和内存泄漏风险。在我的经验中,有几种常见的多线程通信方式和思路:
首先,最基本的方法通常是利用Handler和Looper机制。主线程默认拥有一个Looper,也就是消息循环,而我们可以创建一个Handler与之关联,从子线程通过发送消息或者Runnable对象的方式,将任务或数据传递到主线程。Handler在接收到消息之后会在关联的线程上执行回调,这保证了UI线程安全地处理更新,这种方式简单且易于理解,是Android多线程通信的传统做法。
其次,使用异步任务(如AsyncTask)也是比较常见的方式,虽然它在新项目中已经不推荐使用,但依然能说明问题。AsyncTask内部本质上依然借助Handler来实现线程间的切换。它为我们封装了一套方便的接口:后台执行任务、任务结束时更新UI、以及任务执行中更新进度。这种方式适合简单的、一次性的异步操作,但对于复杂的并发和线程池管理就显得不够灵活了。
对于更为复杂的场景,像RxJava这样的响应式编程框架提供了一种更简洁的方式来进行线程调度。通过在Observable链上配置Scheduler,我们可以在上游的计算线程处理数据,然后指定下游切换到主线程更新UI。这种方式不仅使多线程的代码逻辑更清晰,还能方便地处理流式数据和链式异步操作,同时具备更好的错误处理和组合操作能力。
另外,还有一种近年来被广泛使用的方式是使用Kotlin协程。在Kotlin中,使用协程能够大大简化异步编程模型,我们可以通过挂起函数写出类似同步的代码,同时利用Dispatchers指定在哪个线程或线程池上运行。比如我们可以在IO线程中执行网络请求,然后使用withContext切换到Main线程更新UI。协程在开发中越来越流行,因为它减少了线程切换和回调监听的复杂性,同时也有更好的可控性和错误管理特性。
除此之外,我们还要考虑线程间通信中的一些额外问题,比如防止并发修改数据、避免内存泄漏(尤其是Handler使用内部类时容易持有Activity引用的问题),以及响应取消和异常处理等。所以在实际开发中,会特别关注Thread-Safe的设计模式,比如使用线程安全的数据结构或者同步工具,甚至采用消息总线(EventBus)这种方式来解耦模块之间的事件传递。当然,每种方法都有其适用场景,如果只需要简单的UI更新,我可能会选择最简单的Handler或者runOnUiThread方法;如果任务较为复杂且需求灵活,那么RxJava和Kotlin协程都是非常强大的工具。
5.okhttp相关(优点,拦截器)
“OkHttp是目前在Android和Java开发中非常流行的网络请求库之一,它的优点主要集中在性能、易用性和扩展性上。首先,它支持连接池和keep-alive,能够复用TCP连接,在高并发场景下减少了建立和断开连接的额外开销,从而提升了整个网络请求的效率。另外,它内置了对GZIP压缩响应的透明支持,可以减少数据传输量,同时自动管理缓存策略,这对于提升响应速度和降低流量消耗都非常有好处。
说到扩展性,OkHttp的拦截器机制是一个非常出色的设计。拦截器可以在请求发出前和响应返回后进行拦截和处理,既可以对请求或响应做统一的修改,也方便我们在应用中增加一些特定的功能,比如添加公共的请求头、日志打印、错误重试策略、请求重定向、甚至模拟离线数据等。
具体来说,拦截器可以分为两类:应用拦截器和网络拦截器。应用拦截器在整个请求过程中作用较大,它能够允许我们在请求生成之前和响应返回后做一些统一的逻辑处理,这样既可以简化调用方逻辑,又能提供全局统一的修改入口。网络拦截器则与实际的网络传输紧密相关,它可以观察原始的请求和响应数据,允许我们做一些更底层的操作,比如对响应数据进行缓存控制,或者动态调整网络传输策略。这个设计使得开发者能够在不同的层级上,针对不同的场景选择合适的介入点,以满足业务需求。
此外,OkHttp还提供了同步和异步的请求方式,同时还非常方便与RxJava等响应式编程框架集成。它对HTTP/2的支持也保证了在复杂网络环境下能够最大化地利用带宽资源,这在多请求并行场景中表现得尤为突出。
总的来说,我认为使用OkHttp主要有以下几点优势:
- 性能优化:连接复用、自动压缩、智能缓存都极大提升了网络通信的效率。
- 扩展性强:通过拦截器机制,无论是全局的请求日志、统一的Header处理还是异常处理,都能灵活实现。
- 简单易用:API设计直观,开发者只需专注于业务层逻辑,而网络层的大小细节都被库很好地封装起来。
- 异步与同步兼备:可根据场景选择合适的方式处理网络请求,满足不同业务场景下的需求。
6.协程调度器
“协程调度器是Kotlin协程中非常核心的一个概念,它决定了协程运行在哪个线程或者线程池上,从而使得协程的使用更具灵活性和高效性。在实际开发中,我们常常需要处理不同类型的任务,比如UI更新、IO操作和计算密集型任务,而协程调度器的存在正好解决了这些场景下的线程切换问题。
首先,调度器的主要作用就是指定协程应该在哪个线程上运行。Kotlin协程提供了一些内置的调度器:
-
Dispatchers.Main:这个调度器主要用在Android应用中,用于在主线程上执行任务。UI更新必须在主线程上进行,所以当我们需要处理诸如点击事件反馈、UI刷新等操作时,就会用到Dispatchers.Main。使用这个调度器能够确保我们在不阻塞主线程的前提下更新界面。
-
Dispatchers.IO:专门设计来处理耗时的IO操作,比如网络请求、文件读写和数据库操作。IO操作通常涉及等待数据或资源,所以使用这个调度器能在后台线程中处理相关任务,避免因为IO阻塞而影响主线程性能。
-
Dispatchers.Default:适用于CPU密集型的任务,比如复杂的计算、数据处理或算法运算。这个调度器会利用系统的CPU核心数来分配线程池,这样能够极大地提高并发处理能力,同时避免因占用主线程资源而导致UI卡顿。
此外,我们也可以通过自定义调度器来满足特定场景的需求。比如说,在项目中有一些定制的任务调度要求,或者需要对线程池进行更精细的配置,这时我们可以利用Executors创建自定义的ThreadPoolExecutor,再通过asCoroutineDispatcher转换为协程调度器,实现更灵活的任务调度。
在实际项目中,我经常需要在不同调度器之间进行切换。例如,我可能在Dispatchers.IO中启动网络请求,然后通过withContext(Dispatchers.Main)切换回主线程更新UI;或者在大量数据处理时使用Dispatchers.Default来充分利用多核CPU。这种灵活的调度方式不仅简化了线程管理,还大大降低了传统多线程开发中容易出现的竞态条件和线程同步问题。
“asCoroutineDispatcher
是Kotlin协程中的一个扩展函数,它可以将一个Java的Executor
或ExecutorService
转换为一个CoroutineDispatcher
,从而使得协程能够在指定的线程池或执行器上运行。
讲讲asCoroutineDispatcher:
package com.example.dispatcher
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.asCoroutineDispatcher
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.ThreadFactory
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicInteger
/**
* 自定义线程池转为 CoroutineDispatcher,支持精细化配置和按需扩缩。
*/
/*
object 关键字用于定义单例对象(Singleton)。它的主要功能是创建一个全局唯一的实例,且线程安全。
将 CustomCoroutineDispatcher 定义为单例有以下好处:
线程池唯一性:避免重复创建线程池,节省资源。
统一调度策略:全局统一的调度逻辑,行为一致。
线程安全:object 确保线程安全的初始化。
易于管理生命周期:集中管理线程池的创建和销毁,防止资源泄露。
全局方便访问:任何地方都可以轻松使用调度器,无需重复初始化。
*/
object CustomCoroutineDispatcher {
// 线程序号计数器,用于给线程命名
/*
AtomicInteger 是 Java 中提供的一种线程安全的整数类型,位于 java.util.concurrent.atomic 包中
它主要用于在多线程环境下实现高效的整数操作,解决了传统 int 类型在并发场景下的线程安全问题。
*/
private val threadCount = AtomicInteger(0)
// 自定义 ThreadFactory,用于给线程池里的线程加上可识别的名字
/*
ThreadFactory 是一个接口,用于创建线程的工厂类。它的主要作用是提供一种创建线程的方式,可以自定义线程的属性和行为。
在默认情况下,线程池会直接使用 Executors.defaultThreadFactory() 来创建线程
这些线程的名字和属性是系统默认的,可能无法满足实际需求。而通过实现 ThreadFactory 接口
我们可以自定义线程的名字、优先级、是否为守护线程等属性,从而更好地管理线程池中的线程。
isDaemon = false
isDaemon = false 是在设置线程的守护线程(Daemon Thread)属性,这里将线程设置为非守护线程。
守护线程是一种特殊的线程,主要为其他线程提供服务或运行后台任务。
它的特点是:
当所有非守护线程(User Thread)结束运行时,守护线程会自动终止,JVM 也随之退出。
守护线程通常用于运行一些辅助性任务,比如垃圾回收器(GC)、日志记录器、后台数据同步等。
而非守护线程(也称为用户线程,User Thread)是普通线程,它们的运行会阻止 JVM 退出:
只要有一个非守护线程还在运行,整个 JVM 就会继续运行。
runnable:
ThreadFactory 是一个接口,用来创建线程。
它的 newThread(Runnable r) 方法会被线程池调用。
runnable 参数 是线程需要执行的任务(即 Runnable 对象),它封装了任务需要运行的代码逻辑。
*/
private val threadFactory = ThreadFactory { runnable ->
Thread(runnable, "custom-pool-thread-${threadCount.incrementAndGet()}").apply {
isDaemon = false
}
}
// ThreadPoolExecutor 配置示例:
// corePoolSize = 4, maximumPoolSize = 8, keepAliveTime = 60 秒
// 阻塞队列容量 = 100,拒绝策略 = CallerRunsPolicy(队列满时由提交线程自己执行任务)
private val threadPoolExecutor = ThreadPoolExecutor(
4,
8,
60L,
TimeUnit.SECONDS,
LinkedBlockingQueue<Runnable>(100),
threadFactory,
ThreadPoolExecutor.CallerRunsPolicy()
).apply {
// 允许核心线程超时回收,空闲时也能释放资源
allowCoreThreadTimeOut(true)
}
// 将上面配置好的线程池包装成 CoroutineDispatcher
/*
CoroutineDispatcher 是 Kotlin Coroutines 中的一个抽象类,用于定义协程的调度策略
它决定协程在哪个线程或线程池上执行,以及如何调度协程中的任务。
*/
val dispatcher: CoroutineDispatcher = threadPoolExecutor.asCoroutineDispatcher()
/** 手动关闭线程池(可选)。确保应用退出前或调度器不再使用时调用。 */
fun shutdown() {
threadPoolExecutor.shutdown()
}
}
package com.example.dispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
/*
runBlocking:
它是一个 Coroutine Builder,用来在常规阻塞式调用栈(也就是普通的 main、测试函数或其他非挂起函数)中启动一个协程作用域。
当你调用 runBlocking { /* 协程代码 */ } 时,当前线程会被“阻塞”,直到它里面的所有挂起函数或者子协程都执行完才继续往下走。
对比于普通的 launch 或 async,runBlocking 保证了:
主线程不会在协程内部的工作未完成时就退出或继续往下执行后续代码(比如你的 Thread.sleep(2000))。
它在本质上把 异步的协程世界 “桥接”到 同步的阻塞调用,常用于示例、测试或顶层的 main 函数中。
CoroutineScope:
CoroutineScope 是 Kotlin 协程的一个核心概念,它定义了协程的生命周期和上下文
简单来说,CoroutineScope 是一个协程的容器,负责管理协程的启动和结束。
*/
fun main() = runBlocking {
// 使用自定义调度器启动协程作用域
val scope = CoroutineScope(CustomCoroutineDispatcher.dispatcher)
// 提交 10 个任务到自定义线程池
repeat(10) { index ->
scope.launch {
println("Task #$index running on ${Thread.currentThread().name}")
}
}
// 等待所有任务执行完毕(简化示例)
Thread.sleep(2000)
// 应用退出前清理资源
CustomCoroutineDispatcher.shutdown()
}