Android学习总结之设计场景题

设计图片请求框架的缓存模块

核心目标是通过分层缓存策略(内存缓存 + 磁盘缓存)提升图片加载效率,同时兼顾内存占用和存储性能。以下是针对 Android 面试官的回答思路,结合代码注释说明关键设计点:

一、缓存架构设计:分层缓存策略

采用内存缓存(LRU)+ 磁盘缓存(持久化)+ 网络兜底的三级架构,优先从内存快速获取,其次从磁盘读取,最后网络加载,减少重复请求和资源消耗。

二、内存缓存设计(LruCache)

核心作用:利用内存快速访问特性,缓存近期使用的图片,避免重复解码 Bitmap。
实现要点

  1. 使用 Android 内置的LruCache(或 Kotlin 的LinkedHashMap手动实现 LRU),根据内存大小动态设置缓存上限(通常为应用可用内存的 1/8)。
  2. 以图片 URL 的 MD5 值作为 Key,确保唯一性;Value 存储解码后的Bitmap
  3. 结合onTrimMemory()回调,在系统内存紧张时主动释放内存缓存。
代码示例(带注释)
public class MemoryCache {private LruCache<String, Bitmap> lruCache;public MemoryCache(Context context) {// 计算内存缓存上限:取应用可用内存的1/8(避免OOM)int maxMemory = (int) (Runtime.getRuntime().maxMemory() / 1024);int cacheSize = maxMemory / 8;lruCache = new LruCache<String, Bitmap>(cacheSize) {// 重写尺寸计算(Bitmap的内存占用以像素数衡量:width * height * bytePerPixel)@Overrideprotected int sizeOf(String key, Bitmap value) {return value.getByteCount() / 1024; // 单位KB}};}// 存入内存缓存(主线程调用需注意同步,但LruCache本身线程安全)public void put(String url, Bitmap bitmap) {if (get(url) == null) { // 避免重复存储lruCache.put(hashKeyForUrl(url), bitmap);}}// 获取内存缓存public Bitmap get(String url) {return lruCache.get(hashKeyForUrl(url));}// 清理缓存(在Activity/Fragment销毁时调用,避免内存泄漏)public void clear() {if (!lruCache.isEmpty()) {lruCache.evictAll(); // 清空所有缓存}}// URL转MD5,确保Key唯一且合法(避免特殊字符导致的问题)private String hashKeyForUrl(String url) {try {MessageDigest md = MessageDigest.getInstance("MD5");byte[] hashBytes = md.digest(url.getBytes());// 转换为16进制字符串StringBuilder hexString = new StringBuilder();for (byte b : hashBytes) {String hex = String.format("%02X", b);hexString.append(hex.toLowerCase());}return hexString.toString();} catch (Exception e) {return String.valueOf(url.hashCode()); // 异常时用hashCode兜底}}
}

三、磁盘缓存设计(DiskLruCache)

核心作用:持久化存储图片文件,避免重复下载,同时减轻内存压力。
实现要点

  1. 使用 Android 推荐的DiskLruCache(需处理 Android 10 + 的分区存储适配),按文件大小或时间实现 LRU 淘汰。
  2. 缓存路径建议放在应用私有目录(如Context.getCacheDir()),避免用户删除或权限问题。
  3. 异步处理磁盘 IO(如使用ExecutorService),避免阻塞主线程。
  4. 支持缓存有效期(如 7 天),定期清理过期文件。
代码示例(带注释)
public class DiskCache {private static final int MAX_DISK_CACHE_SIZE = 50 * 1024 * 1024; // 50MBprivate static final int APP_VERSION = 1; // 版本号变更时清空缓存private static final String DISK_CACHE_SUBDIR = "image_cache"; // 子目录名称private DiskLruCache diskLruCache;private ExecutorService diskExecutor;public DiskCache(Context context) {diskExecutor = Executors.newSingleThreadExecutor(); // 单线程保证磁盘操作有序File cacheDir = new File(context.getCacheDir(), DISK_CACHE_SUBDIR);try {diskLruCache = DiskLruCache.open(cacheDir, APP_VERSION, 1, MAX_DISK_CACHE_SIZE);} catch (IOException e) {e.printStackTrace();}}// 异步写入磁盘缓存(在子线程调用)public void asyncPut(String url, byte[] data) {diskExecutor.execute(() -> {String key = hashKeyForUrl(url);try (DiskLruCache.Editor editor = diskLruCache.edit(key)) {if (editor != null) {OutputStream outputStream = editor.newOutputStream(0);outputStream.write(data);editor.commit(); // 提交写入}} catch (IOException e) {e.printStackTrace();try {if (editor != null) {editor.abort(); // 失败时回滚}} catch (IOException ex) {ex.printStackTrace();}}});}// 同步读取磁盘缓存(建议在子线程调用,避免ANR)public byte[] get(String url) {String key = hashKeyForUrl(url);try (DiskLruCache.Snapshot snapshot = diskLruCache.get(key)) {if (snapshot != null) {InputStream inputStream = snapshot.getInputStream(0);return inputStreamToByteArray(inputStream); // 转换为字节数组}} catch (IOException e) {e.printStackTrace();}return null;}// 清理过期缓存(可结合定时任务或开机广播触发)public void cleanExpiredCache(long expirationMillis) {diskExecutor.execute(() -> {File cacheDir = diskLruCache.getDirectory();for (File file : cacheDir.listFiles()) {if (System.currentTimeMillis() - file.lastModified() > expirationMillis) {file.delete(); // 删除超过有效期的文件}}try {diskLruCache.trimToSize(MAX_DISK_CACHE_SIZE); // 按大小LRU淘汰} catch (IOException e) {e.printStackTrace();}});}// 输入流转字节数组(工具方法)private byte[] inputStreamToByteArray(InputStream is) throws IOException {ByteArrayOutputStream os = new ByteArrayOutputStream();byte[] buffer = new byte[1024];int len;while ((len = is.read(buffer)) != -1) {os.write(buffer, 0, len);}return os.toByteArray();}
}

四、缓存协同逻辑

  1. 获取图片流程

    • 先查内存缓存,存在则直接使用(无需解码,最快)。
    • 内存无缓存则查磁盘缓存,存在则解码为 Bitmap 并存入内存(下次直接读内存)。
    • 磁盘无缓存则发起网络请求,下载后同时写入磁盘和内存。
  2. 内存与磁盘的一致性

    • 磁盘缓存写入完成后,再更新内存缓存,避免内存与磁盘数据不一致。
    • 图片尺寸适配:根据 ImageView 的目标尺寸(width/height)缓存对应尺寸的图片,避免内存浪费(如存储 1080p 图片到仅需 200x200 的 View)。

五、面试官高频问题补充

  1. 为什么选择 LruCache 而不是 HashMap?
    LruCache 内置 LRU 淘汰算法,自动管理内存释放,避免 OOM;HashMap 需手动实现淘汰逻辑,容易导致内存泄漏。

  2. 磁盘缓存为什么用 DiskLruCache 而不是直接写文件?
    DiskLruCache 封装了文件 IO 的原子性操作(如写入失败时回滚)、LRU 淘汰策略、版本管理(版本号变更时清空缓存),比手动管理文件更可靠。

  3. 如何处理缓存穿透和缓存击穿?

    • 缓存穿透(请求不存在的 Key):对无效 Key 进行短期内存缓存(如缓存空结果 1 分钟)。
    • 缓存击穿(热点 Key 失效):加分布式锁(或本地锁),确保同一 Key 的网络请求仅发起一次。
  4. Android 10 + 分区存储对磁盘缓存的影响?
    缓存路径必须使用应用私有目录(如getCacheDir()),避免使用外部存储公共目录(需申请权限且可能被用户清理)。

ScrollView里面嵌套两个高度都为两个屏幕RecycleView

整体思路阐述

当 ScrollView 嵌套两个高度为两个屏幕的 RecyclerView 时,要实现特定的 ACTION_MOVE 事件处理逻辑,也就是让 MOVE 事件先由 RecyclerView1 处理,等 RecyclerView1 滚动到底部后将事件交给 ScrollView 处理,待 RecyclerView2 完全展示在屏幕上时再把事件交给 RecyclerView2 处理,关键在于重写 ScrollView 的 onInterceptTouchEvent 方法来精确控制事件的拦截与分发。同时,需要编写方法来判断 RecyclerView 是否滚动到底部以及是否完全显示在屏幕上。

代码实现:

import android.content.Context;
import android.util.AttributeSet;
import android.view.MotionEvent;
import android.widget.ScrollView;
import androidx.recyclerview.widget.RecyclerView;// 自定义 ScrollView 类,用于处理嵌套 RecyclerView 的触摸事件
public class CustomScrollView extends ScrollView {// 声明两个 RecyclerView 成员变量private RecyclerView recyclerView1;private RecyclerView recyclerView2;// 构造函数,用于在代码中创建 CustomScrollView 实例public CustomScrollView(Context context) {super(context);}// 构造函数,用于在 XML 布局中使用 CustomScrollViewpublic CustomScrollView(Context context, AttributeSet attrs) {super(context, attrs);}// 构造函数,带有默认样式属性public CustomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {super(context, attrs, defStyleAttr);}// 设置关联的两个 RecyclerViewpublic void setRecyclerViews(RecyclerView recyclerView1, RecyclerView recyclerView2) {this.recyclerView1 = recyclerView1;this.recyclerView2 = recyclerView2;}// 重写 onInterceptTouchEvent 方法,用于拦截触摸事件@Overridepublic boolean onInterceptTouchEvent(MotionEvent ev) {// 确保两个 RecyclerView 已经被正确设置if (recyclerView1 != null && recyclerView2 != null) {// 根据触摸事件的动作类型进行处理switch (ev.getAction()) {case MotionEvent.ACTION_MOVE:// 判断 RecyclerView1 是否滚动到底部if (!isRecyclerViewAtBottom(recyclerView1)) {// 如果 RecyclerView1 未滚动到底部,不拦截事件,让 RecyclerView1 处理return false;}// 判断 RecyclerView2 是否完全显示在屏幕上if (!isRecyclerViewFullyVisible(recyclerView2)) {// 如果 RecyclerView2 未完全显示,拦截事件,由 ScrollView 处理return true;}break;}}// 其他情况,调用父类的 onInterceptTouchEvent 方法return super.onInterceptTouchEvent(ev);}// 判断 RecyclerView 是否滚动到底部的方法private boolean isRecyclerViewAtBottom(RecyclerView recyclerView) {// 检查 RecyclerView 的适配器是否为空if (recyclerView.getAdapter() == null) {return false;}// 获取 RecyclerView 的 LayoutManagerRecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();// 检查 LayoutManager 是否为空if (layoutManager == null) {return false;}// 获取最后一个可见项的位置int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();// 判断最后一个可见项是否是列表中的最后一项return lastVisibleItemPosition == recyclerView.getAdapter().getItemCount() - 1;}// 判断 RecyclerView 是否完全显示在屏幕上的方法private boolean isRecyclerViewFullyVisible(RecyclerView recyclerView) {// 检查 RecyclerView 的适配器是否为空if (recyclerView.getAdapter() == null) {return false;}// 获取 RecyclerView 的 LayoutManagerRecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();// 检查 LayoutManager 是否为空if (layoutManager == null) {return false;}// 获取第一个可见项的位置int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition();// 获取最后一个可见项的位置int lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition();// 判断第一个可见项是否是列表中的第一项,且最后一个可见项是否是列表中的最后一项return firstVisibleItemPosition == 0 && lastVisibleItemPosition == recyclerView.getAdapter().getItemCount() - 1;}
}    
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import java.util.ArrayList;
import java.util.List;// 主 Activity 类
public class MainActivity extends AppCompatActivity {// 声明 CustomScrollView 和两个 RecyclerView 成员变量private CustomScrollView customScrollView;private RecyclerView recyclerView1;private RecyclerView recyclerView2;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);// 设置布局文件setContentView(R.layout.activity_main);// 从布局文件中获取 CustomScrollView 和两个 RecyclerView 的实例customScrollView = findViewById(R.id.customScrollView);recyclerView1 = findViewById(R.id.recyclerView1);recyclerView2 = findViewById(R.id.recyclerView2);// 为 RecyclerView1 设置线性布局管理器recyclerView1.setLayoutManager(new LinearLayoutManager(this));// 为 RecyclerView2 设置线性布局管理器recyclerView2.setLayoutManager(new LinearLayoutManager(this));// 为 RecyclerView1 设置适配器,并传入模拟数据recyclerView1.setAdapter(new MyAdapter(createDummyData()));// 为 RecyclerView2 设置适配器,并传入模拟数据recyclerView2.setAdapter(new MyAdapter(createDummyData()));// 将两个 RecyclerView 关联到 CustomScrollView 中customScrollView.setRecyclerViews(recyclerView1, recyclerView2);}// 创建模拟数据的方法private List<String> createDummyData() {// 创建一个字符串列表来存储模拟数据List<String> data = new ArrayList<>();// 循环添加 50 条模拟数据for (int i = 0; i < 50; i++) {data.add("Item " + i);}return data;}
}    

 

import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import java.util.List;// RecyclerView 的适配器类
public class MyAdapter extends RecyclerView.Adapter<MyAdapter.ViewHolder> {// 存储要显示的数据列表private List<String> data;// 构造函数,传入数据列表public MyAdapter(List<String> data) {this.data = data;}// 创建 ViewHolder 实例@NonNull@Overridepublic ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {// 从布局文件中加载单个列表项的视图View view = LayoutInflater.from(parent.getContext()).inflate(android.R.layout.simple_list_item_1, parent, false);// 创建 ViewHolder 实例并传入视图return new ViewHolder(view);}// 绑定数据到 ViewHolder@Overridepublic void onBindViewHolder(@NonNull ViewHolder holder, int position) {// 将指定位置的数据设置到 TextView 中holder.textView.setText(data.get(position));}// 获取数据列表的大小@Overridepublic int getItemCount() {return data.size();}// ViewHolder 类,用于缓存视图组件public static class ViewHolder extends RecyclerView.ViewHolder {// 声明 TextView 成员变量TextView textView;// 构造函数,传入视图public ViewHolder(@NonNull View itemView) {super(itemView);// 从视图中获取 TextView 实例textView = itemView.findViewById(android.R.id.text1);}}
}    

代码调用逻辑说明

  1. 初始化阶段:在 MainActivity 的 onCreate 方法中,首先通过 setContentView 设置布局文件,然后从布局文件中获取 CustomScrollView 和两个 RecyclerView 的实例。接着为两个 RecyclerView 设置 LinearLayoutManager 和 MyAdapter,并调用 customScrollView.setRecyclerViews 方法将两个 RecyclerView 关联到 CustomScrollView 中。
  2. 触摸事件处理阶段:当用户进行触摸操作时,触摸事件会先传递到 CustomScrollView 的 onInterceptTouchEvent 方法。在该方法中,会根据 RecyclerView1 是否滚动到底部以及 RecyclerView2 是否完全显示在屏幕上的情况来决定是否拦截事件。如果 RecyclerView1 未滚动到底部,不拦截事件,让 RecyclerView1 处理;如果 RecyclerView2 未完全显示,拦截事件,由 CustomScrollView 处理;其他情况则调用父类的 onInterceptTouchEvent 方法。

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

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

相关文章

Webug3.0通关笔记14 第十四关:存储型XSS

目录 第十四关:存储型XSS 1.打开靶场 2.源码分析 3.渗透实战 第十四关:存储型XSS 本文通过《webug3靶场第十四关 存储型XSS》来进行存储型XSS关卡的渗透实战。 存储型 XSS&#xff08;Stored Cross - Site Scripting&#xff09;&#xff0c;也被称为持久型 XSS&#xff…

Java父类、子类实例初始化顺序详解

1、完整的初始化顺序&#xff08;含继承&#xff09; 1、父类的静态初始化 父类静态变量默认值 → 父类静态变量显式赋值 父类静态代码块&#xff08;按代码顺序执行&#xff09;。 2、子类的静态初始化 子类静态变量默认值 → 子类静态变量显式赋值 子类静态代码块&…

13.组合模式:思考与解读

原文地址:组合模式&#xff1a;思考与解读 更多内容请关注&#xff1a;7.深入思考与解读设计模式 引言 在软件开发中&#xff0c;是否曾经遇到过这样一种情况&#xff1a;你有一个对象&#xff0c;它本身很简单&#xff0c;但是它包含了其他类似的对象。随着系统变得越来越复…

OpenCV实战教程 第一部分:基础入门

第一部分&#xff1a;基础入门 1. OpenCV简介 什么是OpenCV及其应用领域 OpenCV&#xff08;Open Source Computer Vision Library&#xff09;是一个开源的计算机视觉和机器学习软件库&#xff0c;于1999年由Intel公司发起&#xff0c;现在由非营利组织OpenCV.org维护。Ope…

虚幻商城 Quixel 免费资产自动化入库(2025年版)

文章目录 一、背景二、问题讲解1. Quixel 免费资产是否还能一键入库?2. 是不是使用了一键入库功能 Quixel 的所有资产就能入库了?3. 一键入库会入库哪些资产?三、实现效果展示四、实现自动化入库五、常见问题1. 出现401报错2. 出现429报错3. 入库过于缓慢4. 入库 0 个资产一…

uni-app - 小程序使用高德地图完整版

文章目录 🍉功能描述🍉效果🍉开发环境🍉代码部分🍉功能描述 页面自动通过定位获取用户位置并展示周边POI数据,同时支持关键词输入实时联想推荐关联地点信息, 实现精准智能的地点发现与检索功能。 🍉效果 🍉开发环境 unibest2.5.4nodev18.20.5pnpm9.14.2wot-des…

牛客:AB4 逆波兰表达式求值

链接&#xff1a;逆波兰表达式求值_牛客题霸_牛客网 题解&#xff1a; 利用栈&#xff0c;遍历字符串数组&#xff0c;遇到运算数则入栈&#xff0c;遇到运算符则取出栈顶两个运算数进行运算&#xff0c;并将运算结果入栈。 class Solution { public:/*** 代码中的类名、方法…

Ant(Ubuntu 18.04.6 LTS)安装笔记

一、前言 本文与【MySQL 8&#xff08;Ubuntu 18.04.6 LTS&#xff09;安装笔记】同批次&#xff1a;先搭建数据库&#xff0c;再安装JDK&#xff0c;后面肯定就是部署Web应用。其中Web应用的部署使用 Ant 方式&#xff0c;善始善终&#xff0c;特以笔记。 二、准备 &#xf…

ultralytics 目标检测 混淆矩阵 背景图像 没被记录

修改 utils/metrics.py ConfusionMatrix def process_batch(self, detections, gt_bboxes, gt_cls):"""Update confusion matrix for object detection task.Args:detections (Array[N, 6] | Array[N, 7]): Detected bounding boxes and their associated inf…

iview 如何设置sider宽度

iview layout组件中&#xff0c;sider设置了默认宽度和最大宽度&#xff0c;在css样式文件中修改无效&#xff0c;原因是iview默认样式设置在了element.style中&#xff0c;只能通过行内样式修改 样式如下&#xff1a; image.png image.png 修改方式&#xff1a; 1.官方文档中写…

go-zero(十七)结合DTM :实现分布式事务

1. 基础概念介绍 1.1 什么是分布式事务 在微服务架构中&#xff0c;一个业务操作常常需要调用多个服务来完成。例如&#xff0c;在电商系统中下单时&#xff0c;需要同时操作订单服务和库存服务。这种跨服务的操作就需要分布式事务来保证数据一致性。 分布式事务面临以下挑战…

2025 简易Scrum指南(简体中文版)

Scrum是一个轻量级的、以团队为中心的框架&#xff0c;用于解决复杂的问题并创造价值。Scrum有意保持非完整性&#xff0c;Scrum的设计初衷旨在依靠使用者的集体智慧来不断演进构建。 Scrum建立在实验主义和精益思想的基础上&#xff0c;它赋能团队灵活巧妙地工作&#xff0c;…

2025最新福昕PDF编辑器,PDF万能处理工具

软件介绍 Foxit PDF Editor Pro 2025 中文特别版&#xff08;以前称为 Foxit PhantomPDF Business&#xff09;是一款专为满足各种办公需求而设计的业务就绪的PDF工具包。 软件特点 1. 强大的PDF编辑能力 创建新文档&#xff1a;用户可以从无到有地构建PDF文档&#xff0c;添…

ollama的若干实践

1. 本地ollama 1.1 本地安装ollama 方法 1&#xff1a;手动检查最新版本并下载 访问 Ollama 的 GitHub Releases 页面&#xff1a; 打开 https://github.com/ollama/ollama/releases 查看最新的稳定版本&#xff08;如 v0.7.0 或更高&#xff09; 手动下载最新版本&#xff08…

Spring Security源码解析

秒懂SpringBoot之全网最易懂的Spring Security教程 SpringBoot整合Spring-Security 认证篇&#xff08;保姆级教程&#xff09; SpringBoot整合Spring Security【超详细教程】 spring security 超详细使用教程&#xff08;接入springboot、前后端分离&#xff09; Security 自…

LeetCode 3392.统计符合条件长度为 3 的子数组数目:一次遍历模拟

【LetMeFly】3392.统计符合条件长度为 3 的子数组数目&#xff1a;一次遍历模拟 力扣题目链接&#xff1a;https://leetcode.cn/problems/count-subarrays-of-length-three-with-a-condition/ 给你一个整数数组 nums &#xff0c;请你返回长度为 3 的 子数组&#xff0c;满足…

读论文笔记-CoOp:对CLIP的handcrafted改进

读论文笔记-Learning to Prompt for Vision-Language Models Problems 现有基于prompt engineering的多模态模型在设计合适的prompt时有很大困难&#xff0c;从而设计了一种更简单的方法来制作prompt。 Motivations prompt engineering虽然促进了视觉表示的学习&#xff0c…

从零构建 MCP Server 与 Client:打造你的第一个 AI 工具集成应用

目录 &#x1f680; 从零构建 MCP Server 与 Client&#xff1a;打造你的第一个 AI 工具集成应用 &#x1f9f1; 1. 准备工作 &#x1f6e0;️ 2. 构建 MCP Server&#xff08;服务端&#xff09; 2.1 初始化服务器 &#x1f9e9; 3. 添加自定义工具&#xff08;Tools&…

Django 自定义celery-beat调度器,查询自定义表的Cron表达式进行任务调度

学习目标&#xff1a; 通过自定义的CronScheduler调度器在兼容标准的调度器的情况下&#xff0c;查询自定义任务表去生成调度任务并分配给celery worker进行执行 不了解Celery框架的小伙伴可以先看一下我的上一篇文章&#xff1a;Celery框架组件分析及使用 学习内容&#xff…

蓝桥杯 1. 确定字符串是否包含唯一字符

确定字符串是否包含唯一字符 原题目链接 题目描述 实现一个算法来识别一个字符串的字符是否是唯一的&#xff08;忽略字母大小写&#xff09;。 若唯一&#xff0c;则输出 YES&#xff0c;否则输出 NO。 输入描述 输入一行字符串&#xff0c;长度不超过 100。 输出描述 输…