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)
            @Override
            protected 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; // 50MB
    private 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 布局中使用 CustomScrollView
    public CustomScrollView(Context context, AttributeSet attrs) {
        super(context, attrs);
    }

    // 构造函数,带有默认样式属性
    public CustomScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
    }

    // 设置关联的两个 RecyclerView
    public void setRecyclerViews(RecyclerView recyclerView1, RecyclerView recyclerView2) {
        this.recyclerView1 = recyclerView1;
        this.recyclerView2 = recyclerView2;
    }

    // 重写 onInterceptTouchEvent 方法,用于拦截触摸事件
    @Override
    public 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 的 LayoutManager
        RecyclerView.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 的 LayoutManager
        RecyclerView.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;

    @Override
    protected 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
    @Override
    public 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
    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        // 将指定位置的数据设置到 TextView 中
        holder.textView.setText(data.get(position));
    }

    // 获取数据列表的大小
    @Override
    public 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 方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值