Android 磁盘最近最少缓存实现 - DiskLruCache 源码分析

 DiskLruCache 是一个 Android 端使用广泛的磁盘 LRU (最近最少使用)缓存算法的实现库,甚至在 AOSP 中都有使用。 基础使用 为了对整体架构有个印象,方便找分析入口,先看看基本的使用。 实例化: val cache = DiskLruCache.open(cacheFile, version, valueCount, maxSize) 写入: val editor = cache.edit(key.md5()) val os = editor.newOutputStream(0) 读取: val snapshot = cache.get(key.md5()) val is = snapshot?.getInputStream(0) 关于它的具体使用可以看这篇文章。 整体来看并不复杂,实际上代码量也确实不多。通过读取时的 snapshot 封装猜测后续修改不会影响已经读取到的值。 日志文件 DiskLruCache 的一个显著特征就是在缓存目录有一个 journal 文件,用于记录所有操作日志,大概长这个样子: libcore.io.DiskLruCache 1 19 1 DIRTY c915a1313f3e413252fae2957469dcd2 CLEAN c915a1313f3e413252fae2957469dcd2 50196 READ c915a1313f3e413252fae2957469dcd2 DIRTY 8b33d00d5f9d443095e20829a1367e05 CLEAN 8b33d00d5f9d443095e20829a1367e05 159594 前五行是元数据: 一个魔法数,固定字符串,用于标识这是一个 DiskLruCache 的日志文件。 DiskLruCache 版本号,从发布以来就没变过,预留一下便于后续格式更新。 开发者声明的应用版本号。 每个 key 对应几个 value,一般都是1。一个 key 支持写入多个值,每个值都是一个文件,文件名是 key....

March 9, 2020 · Chenhe

Android 自定义 View 的默认属性值(四个构造函数的作用)

常见的套路 自定义 View 同学们一定或多或少都接触过,毕竟设计给出的样式往往从来不是系统默认的🙂。 自定义 View 时往往遵守下面这样的套路: 在 attrs.xml 中声明自定义属性(或使用系统预定义属性),经常用 declare-styleable 包裹起来。 继承一个 View 并写两个构造函数:constructor(context: Context), constructor(context: Context, attrs: AttributeSet?). 前者用于代码创建,后者用于 xml 创建。 context.theme.obtainStyledAttributes() 来读取属性值,并在 getXXX() 函数中给出一个默认值。 一切似乎理所应当行云流水并且工作良好,但这真的合理吗?(废话 要是合理这篇文章是在自嗨嘛😅 考虑以下问题: 用户(本文指使用 View 的开发者)如何统一设置默认样式? 默认值硬编码在代码文件中是否耦合过重? 当然用户可以定义一个 style 每次使用都设置一下,你不觉得太麻烦了吗? 当然也可以直接写在主题的样式中,但如果有冲突属性怎么办?例如自定义 View 使用了系统定义的 android:textColor 但又不希望其他控件例如 TextView 受影响。 为了后边便于说明,我们定义个 CircleCheckBox,并定义如下属性: <!--lib: attrs.xml--> <resources> <declare-styleable name="CircleCheckBox"> <attr name="normalColor" format="color" /> <attr name="selectColor" format="color" /> <attr name="outlineColor" format="color" /> <attr name="outlineStrokeWidth" format="dimension" /> <attr name="android:checked" /> </declare-styleable> </resources> 被忽略的两兄弟 除了常用的两个构造函数之外,还有一个三个参数的构造函数,从 Android 5....

February 16, 2020 · Chenhe

一起动才够嗨!Android CoordinatorLayout 自定义 Behavior

CoordinatorLayout 的今生前世 联动效果 现代化的 Android 开发一定对 CoordinatorLayout 不陌生,CoordinatorLayout + AppBarLayout + CollapsingToolbarLayout + Toolbar 的全家桶更是信手拈来,无需一行代码光靠 xml 就能实现下面这种折叠导航栏的炫酷效果: 这种搭配的教程已经非常多了,不是本文的重点。在使用 xml 时候肯定不少同学掉过一个坑:界面主要内容与头部元素重叠了!粗略了解一下因为 CoordinatorLayout 的布局方式类似 FrameLayout 默认情况下所有元素都会叠加在一起,解决方案也非常玄学,就是给内容元素添加一个 app:layout_behavior="@string/appbar_scrolling_view_behavior" 属性就好了,简直像黑魔法! Unfortunately,代码并没有魔法,我们能偷懒是因为有人封装好了。跟踪进这个字符串是 com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior 显然这是个类!事实上这就是今天的重头戏 —— Behavior. 这个效果太复杂了,所以 Google 才会帮我们包装好,下面换一个简单的例子便于学习: 这是仿三星 One UI 的界面。上面是一个头布局,下面是一个 RecyclerView,向上滑动时首先头布局收缩渐隐并有个视差效果,头部彻底隐藏后 RecyclerView 无缝衔接。向下滑动时同理。 事件拦截实现 在继续探索之前,先思考一下如果没有 CoordinatorLayout 这种现代化东西怎么办?因为这牵扯到滑动手势与 View 效果的糅合,毫无疑问应该从触摸事件上入手。简单起见暂时只考虑手指向上滑动(列表向下展示更多内容),大概需要进行以下操作: 在父布局 onInterceptTouchEvent 中拦截事件。 父布局 onTouchEvent 处理事件,对 HeaderView 进行操作(移动、改变透明度等)。 HeaderView 完全折叠后父布局不再拦截事件,RecyclerView 正常处理滑动。 现在已经遇到问题了。因为一开始父布局拦截了事件,因此根据 Android 事件分发机制,哪怕后续不再拦截其子控件也无法收到事件,除非重新触摸,这就造成了两者的滑动不能无缝衔接。 接着还有一个问题,反过来当 RecyclerView 向下滑动至顶部时,如何通知 HeaderView 展开? 哪怕解决了上述主要问题,肯定还有其他小毛病,例如子控件无法触发点击事件等等等非常恼人💢。假设你是大佬完美解决了所有问题,肯定耦合特别严重,又是自定义 View 又是互相引用的乱七八糟😵 所以现在就不往下深究了,有闲情雅致有能力的同学可以尝试实现。...

February 15, 2020 · Chenhe

Android 事件分发机制

Android 事件分发机制一直让人头痛,之前也是面向 GitHub 编程得过且过。今天下定决心了解一下,以便后面自己定制 View 效果。Android 触摸事件有三个基本类型:ACTION_DOWN, ACTION_MOVE, ACTION_UP,后两者的传递顺序取决于 DOWN 的传递结果,所以就从 ACTION_DOWN 开始分析。 ACTION_DOWN 全景 借用一张下面参考文章里的全景图片,注意这里指的仅仅是 ACTION_DOWN 事件的传递。先解释一下: 白色箭头表示事件传递(函数调用) 箭头上的标注表示调用前提。(supper 表示上一级直接调用,false 表示若上级返回 false 则系统继续向下调用) 白色方块内的消费箭头表示若此函数返回对应值,则事件终止传递(也称作被消费了) 以左上角事件入口为例,首先 Activity 收到事件触发 dispatchTouchEvent,不论返回 true 还是 false 事件均终止,任何组件的任何函数均不会再被调用(包括 activity 自己的 onTouchEvent),只有 return super.dispatchTouchEvent() 也就是调用了 super 才会继续传递到下一级。 对于下一级 ViewGroup 的 dispatchTouchEvent 来讲,返回 true 同样消费事件立即终止传递。返回 false 则会回溯到上一层的 onTouchEvent。调用 super 则继续向下传递。 全程传递 我们假设事件没有被拦截、消费,那么整个传输流程类似 U 型: 不难看出,整个流程分为左右两部分,我们暂且叫做分派与回溯。分派是自顶到底的,主要用于事件的传递。回溯是从底到顶的,主要用于事件的处理。所有方法的默认实现就是 return super.xxx() 因此事件默认情况下可以走完整个流程。 拦截器 ViewGroup 细心的同学应该注意到了,在分派过程中除了整齐的 dispatchTouchEvent 方法外,乱入了一个 onInterceptEvent 方法,可以称之为拦截器。顾名思义拦截器的作用就是拦截此事件供自己使用(就像大理 gov 一样对待口罩那样)。不难看出 dispatchTouchEvent 方法调用后根据内部处理的不同有三个后果,分别是 ①终止传递 ②向下传递 ③向上回溯,而前面提到过,一般来讲处理具体的处理会放在 onTouchEvent 中。那么问题来了,终止传递后我居然自己无法处理事件?(参考 U 型图中的 ViewGroup 层,无论 dispatchTouchEvent 作何响应都无法调用自己的 onTouchEvent)...

February 13, 2020 · Chenhe

Android7 https 抓包

系统限制 Android 7 开始系统有了一个非常安全(讨厌)的限制,所有应用默认情况下不再信任用户自行安装的证书,而仅信任系统预装的。因此,即便我们安装证书后也无法愉快地抓包了:confused: 但是这一限制仅对 targetSdkVersion>=24 的应用生效。 下面提供几种方案绕过这一限制。 添加证书信任 本方案需要修改 AndroidManifest,推荐用于自己的应用,其他应用需要重新打包了。 首先在 res/xml 中创建一个网络安全配置文件,信任用户证书: <?xml version="1.0" encoding="utf-8"?> <network-security-config> <base-config cleartextTrafficPermitted="true"> <trust-anchors> <certificates src="system" /> <certificates src="user" /> <!--信任用户添加的证书--> </trust-anchors> </base-config> </network-security-config> 然后在 Manifest 的 application 节点新增一个 android:networkSecurityConfig 属性指向刚才创建的配置文件即可: <?xml version="1.0" encoding="utf-8"?> <manifest> <application android:networkSecurityConfig="@xml/network_security_config"> </application> </manifest> 使用子系统 此方案本质上是降低系统的版本,例如 平行空间 等双开应用,会创建一个独立的运行环境,相当于子系统。这个环境内的系统版本不一定与手机系统相同。目前,平行空间 4.0.8625 版本以下系统 API < 24,应该可以正常使用。 同理,你也可以使用模拟器抓包,或者干脆找一个旧系统的手机。 修改系统证书 此方案需要 ROOT. 既然默认只信任系统证书,那我们何不直接把自己的证书导入为系统级别? 注意,Android 系统证书使用的是 .0 格式,不同于常见的 .cer/pem,需要使用 openssl 命令行工具转换下。 确定文件名 .cer 格式证书执行:...

November 26, 2019 · Chenhe

Koin in Android: 更简单的依赖注入

Dagger 之殇 如果还不清楚什么是依赖注入,那么请参考之前写的 Dagger2 in Android(一)通俗基础开头部分。如果你不了解 Dagger 倒也无妨,本文会进行一定的对比,但仅针对接触过 Dagger 的同学,否则大可以忽略。 Dagger2 作为著名优秀的依赖注入框架广为流传,何况还是 Android 的亲爸爸 - Google 在维护,因此相信很多人会将其作为 Android 开发的首选 DI 框架。Dagger 从入门到放弃一定是很多很多人必经甚至多次经历的历程:anger: 诚然,Dagger 很强大。但它的学习曲线太过于陡峭,即使好不容易搞清楚了各种注解与概念,也很难适当地运用到项目中。同时对于 Activity 之类重要但却不能我们自己初始化的类 Dagger 明显水土不服。为此,Google 搞了个 .android 扩展库来「简化」使用。我不否认最终确实简化了代码,但是这玩意本身就很难度,学习成本堪称指数级。 除此之外,.android 扩展库对于 ViewModel 依然是严重的水土不服,甚至 Google 官方 Demo 的实现也是一堆问题。 Koin 基础 Koin 是纯 Kotlin 编写的轻量级依赖注入框架,轻量是因为它只使用 Kotlin 的函数解析特性,没有代理,没有代码生成,没有反射!官方声称5分钟快速上手。随着 Kotlin 的推广,Koin 这个后起之秀也获得了越来越多的关注。当然它也提供了 implementation "org.koin: koin-java:1.0.0" 扩展库来支持 java,但本文不会涉及。 不建议新手阅读 Koin 源码。作为 DSL,它大量使用了 Kotlin 的高级特性,例如 inline 函数。相对来说难以理解。 使用 Koin 所需的依赖在官方文档已经说得很明确了。这里因为使用了 AndroidX 库,所以引入 org....

July 5, 2019 · Chenhe

[译] Coroutines on Android(三)实战

本系列文章主要翻译自 medium-AndroidDevelopers. 使用协程解决实际问题 前两章重点研究了协程如何简化代码,在 Android 中提供主线程安全,以及如何避免协程泄露。在此基础上,协程是一个在 Android 中进行后台处理以及以及简化回调的优秀方案。 到目前为止,我们主要关注的是什么是协程以及如何管理它们。在这篇文章中,我们将看看如何使用它们来完成实战任务。协程是一个通用的语言特性,与函数在同一个级别——你可以用它实现任何可以用函数或对象来实现的东西。当然,实际工作中有两种情况是非常适合使用协程解决的: 一次性的请求。一旦得到结果那个这次请求就结束了。 流式请求。它持续地观察数据改变并通知调用者——即使得到结果也会持续执行。 这两种任务都非常适合协程。这篇文章中,我们将深入研究一次性请求,并探索如何在 Android 中用协程实现它们。 一次性请求 一次性请求当被调用时执行,当得到结果时返回并结束。就像普通的函数那样——调用它,执行一些工作,然后返回。由于与函数调用的相似性,它比流式请求更容易理解。 一次性请求当被调用时执行,得到结果时结束。 A one shot request is performed each time it’s called. It stops executing as soon as a result is ready. 举个例子,想想浏览器是如何加载网页的。当你点击这篇文章的链接,浏览器会向服务器发送一个请求来加载页面。一旦页面成功传输到浏览器,便会停止与服务器的通讯——浏览器已经得到了所有它需要的数据。如果服务器修改了文章,除非你刷新网页,否则浏览器不会及时修改。 尽管这缺少流式请求的实时推送特性,但一次性请求还是非常强大。一个应用的许多功能都可以通过一次性请求来实现,比如获取、保存、更新数据。对于排序列表之类的事情,它也是一种很好的模式。 需求:显示一个排序的列表 让我们通过探索如何显示排序列表来研究一次性请求。为了更加具体,让我们构建一个库存管理应用,供商店的员工使用。它可以根据最后一次进货的时间查找商品——他们希望能够对列表进行升序和降序排序。但是商品太多了,排序需要几乎1秒才能完成——所以我们使用协程来避免阻塞主线程! 在这个应用中,所有的商品都存储在一个数据库中,使用了 Room 框架。这是一个很好的用例,因为它不需要涉及网络请求,我们可以专注于一次性请求模式本身,麻雀虽小,五脏俱全。 要实现此功能,你需要将协程引入 ViewModel, Repository 以及 Dao。让我们逐个看看怎么与协程进行结合。 class ProductsViewModel(val productsRepository: ProductsRepository): ViewModel() { private val _sortedProducts = MutableLiveData<List<ProductListing>>() val sortedProducts: LiveData<List<ProductListing>> = _sortedProducts /** * 当用户点击排序按钮时调用这些函数。 */ fun onSortAscending() = sortPricesBy(ascending = true) fun onSortDescending() = sortPricesBy(ascending = false) private fun sortPricesBy(ascending: Boolean) { viewModelScope....

July 2, 2019 · Chenhe

[译] Coroutines on Android(二)起步

本系列文章主要翻译自 medium-AndroidDevelopers. 本篇将开始整合协程与 Android,探索如何启动并跟踪协程,以便适配 UI 生命周期。 为何跟踪协程 在第一篇中,我们探索了协程能解决的问题。总结一下,协程是解决这两个问题的优秀方案: 在主线程运行长时间任务导致阻塞。 从主线程上安全地调用一切 suspend 函数。也即主线程安全(Main-safety) 为了解决这些问题,协程通过给常规函数添加 suspend 与 resume 操作,使得协程被挂起时在单独线程执行,主线程可以继续执行其他工作。 但是,协程本身不能跟踪正在执行的工作。协程本身很轻量,你完全可以同时启动大量的协程,甚至成百上千个。 如果要手动编写代码跟踪这几千个协程的进度将非常困难。也许能够追踪他们,确保他们完成或者取消,但这非常枯燥且容易出错。稍有不注意,就会失去对协程的跟踪,也就是协程泄露。 协程泄露就像内存泄露,但是更严重。如果协程泄露,除了占用内存,还会占用 CPU、硬盘甚至启动网络请求。 泄露的协程会浪费内存、CPU、硬盘、网络,而这些都是不需要的。 A leaked coroutine can waste memory, CPU, disk, or even launch a network request that’s not needed. 为了帮助减少协程泄露,Kotlin 介绍了 结构化并发。结构化并发是协程特性与最佳实践的结合,帮助你保持对所有协程的追踪。 在 Android,我们可以利用结构化并发完成三件事: 当不再需要时取消任务。 对运行中的任务保持追踪。 捕获协程中的异常。 让我们深入研究每一个,看看结构化并发如何帮助我们确保始终跟踪所有协程。 使用 scopes 取消任务 在 Kotlin,协程必须在一个叫做 CoroutineScope 内执行。CoroutineScope 保持了对所有协程的跟踪,尽管被挂起也是这样。不像之前我们讨论的 Dispatchers,Scope 实际上不执行协程,只是确保你不会跟丢或忘记它们。 为了保证所有协程都被跟踪,Kotlin 不允许在没有 CoroutineScope 的情况下启动协程。Scope 能够启动具备我们上一节讨论的挂起与恢复能力的协程。...

July 2, 2019 · Chenhe

[译] Coroutines on Android(一)背景知识

本系列文章主要翻译自 medium-AndroidDevelopers. 本系列文章主要关注 Kotlin Coroutine(协程) 是如何工作的,以及如何解决实际的 Android 问题。但是相对来说不会过于深入底层,更偏向于应用。 协程解决了什么? Kotlin 协程提供了一个全新的,更加简单地方式来实现异步。协程在 Kotlin 1.3 中正式发布,API 已经稳定,可以用于生产环境。事实上,协程的概念始终存在,最早是 Simula 语言在1967年探索了协程的使用。 在过去的几年,协程正逐步流行开来,众多的语言,例如 js, c#, python, ruby, go 都有所支持。Kotlin 协程基于这些基础,可以用于构建大型应用程序。 在 Android 上,协程可以优雅地解决下面两个问题: 在主线程运行长时间任务导致阻塞。 从主线程上安全地调用一切 suspend 函数。也即主线程安全(Main-safety) 主线程安全指的是:调用者不必关心这个函数应当在那个线程(调度器)调用,总能“同步”取得正确的返回值且不会阻塞。 下面从这两个角度入手,深入看看协程任何高效整洁地解决这些问题。 长时间运行的任务 调用 API 等行为会涉及到网络请求。读取数据库或加载配置文件涉及到文件读写。这类工作就是所谓的长时间任务,因为相对来说他们花费的时间很长,应用必须停下来等待他们。 在 Android 每个应用都有一个主线程来处理 UI 与交互。如果在这个线程执行了太繁重的工作,应用就会变卡,导致非常糟糕的用户体验。任何长时间运行的任务都不应该阻塞主线程,这样应用才能流畅运行,避免掉帧或未响应的出现。 为了在子线程执行网络请求,最常见的模式是使用回调(Callback)。回调提供了一个引用,它可以在在未来的某个时间通知代码继续运行,使用回调,代码可能是这样的: class ViewModel: ViewModel() { fun fetchDocs() { get("developer.android.com") { result -> show(result) } } } 尽管 get 函数在主线程被调用,但他会在另一个线程执行网络请求。然后一旦得到相应,回调函数就会在主线程执行。这是一个很棒的模式来出来长时间运行的任务。Retorfit 就是一个非常著名的网络请求库帮助你完成这些工作。 使用协程来解决 协程可以大幅简化上面的代码,现在我们用协程来重写上面的功能:...

July 1, 2019 · Chenhe

Dagger2 in Android(四).android 扩展库

问题 在之前我们讨论过,Dagger 在 Android 上普遍的结构是:定义一个全局的 AppComponent,其他组件依赖或继承它。假设现在有 AppComponent 与 ActivityComponent 两个,他们可能是这样编写的: @Module(subcomponents = [ActivityComponent::class]) class AppModule(val context: Context) { @Provides @Singleton fun provideContext() = context } @Component(modules = [AppModule::class]) @Singleton interface AppComponent { fun inject(app: MyApplication) fun activityComponent(): ActivityComponent.Builder } @Module class ActivityModule { @Provides fun provideSp(context: Context) = context.getSharedPreferences("Cooker", Context.MODE_PRIVATE) } @SubComponent(modules = [ActivityModule::class]) interface ActivityComponent { fun inject(activity: MainActivity) @Subcomponent.Builder interface Builder { fun build(): ActivityComponent } } 上面定义了这两个 Component 并且他们是包含关系。然后我们必须在 Appliction 中实例化 AppComponent 来保证单例:...

June 10, 2019 · Chenhe