Android-RecyclerView学习总结

​​面试官​:

"你在项目中有遇到过 RecyclerView 滑动卡顿的情况吗?当时是怎么解决的?"

​(回忆项目场景,自然带入):

"有的!之前我们团队做了一款新闻阅读 App,首页的资讯列表用 RecyclerView 展示图文内容。上线后发现快速滑动时会出现掉帧,尤其在低端手机上,用户体验挺差的。

我们先用 Android Studio 的 Profiler 工具抓了一下性能数据,发现 onBindViewHolder 方法耗时特别长。仔细一看,原来每个 Item 里都直接加载高清大图,而且图片还是从网络请求来的,没做任何压缩。

后来我们做了几个优化:

  1. 图片压缩:在后台线程把图片缩放到 Item 的显示尺寸(比如 300x300),再加载到 ImageView,主线程压力立马小了很多。
  2. 内存缓存:用了 Glide 的 LruCache,避免重复解码同一张图。
  3. 布局扁平化:把原来嵌套三层的 LinearLayout 换成了 ConstraintLayout,测量时间减少了 40%。

改完之后再测,帧率从原来的 30 帧提到了 60 帧,用户反馈也好了很多。不过中间有个坑,Glide 的缓存策略一开始没配置好,导致频繁 GC,后来调整了缓存大小才稳定下来。"


面试官追问​:

"如果列表里有多种类型的 Item(比如文字、图片、视频),怎么保证 RecyclerView 的流畅度?"

​(结合技术细节,口语化解释):

"这个问题我们还真踩过坑!之前做社交 App 的时候,动态列表有十几种样式------文字、九宫格图片、视频、分享链接等等。一开始滑动起来特别卡,尤其快速翻页的时候。

后来发现是因为每种类型的 ViewHolder 都独立缓存,但 RecyclerView 的默认缓存池太小,导致频繁创建新 ViewHolder。我们的解决办法挺直接的:

  • 合并相似类型:比如把纯文字和带表情的文字合并成一种 ViewHolder,通过数据字段区分样式。
  • 动态计算 ViewType:比如视频封面加载中和已加载的用同一个 ViewType,但根据数据状态显示不同布局。
  • 扩大缓存池recyclerView.recycledViewPool.setMaxRecycledViews(viewType, 10),这样即使突然滑动到历史消息,也能快速复用已有的 ViewHolder。

不过最关键的还是 ​避免在 onBindViewHolder 里做复杂计算。比如视频封面需要根据分辨率计算缩略图尺寸,我们改到后台线程预处理,主线程只负责显示。"


面试官挑战​:

"假设现在有个需求:一个页面里外层是 ScrollView,内嵌一个 RecyclerView(比如商品详情页的'猜你喜欢'模块),怎么解决滑动冲突?"

​(用故事化解技术难点):

"这个需求我们做过!刚开始开发的时候,用户反馈说'猜你喜欢'的区域根本滑不动,手指一划就触发外层 ScrollView 滚动。

我们试了几种方案:

  1. 粗暴解法:把 RecyclerView 的固定高度设为全部内容高度,这样它自己就不滚动了,完全依赖外层 ScrollView 滚动。但这样如果'猜你喜欢'有 100 个商品,页面会变得巨长,直接 OOM。
  2. 改用 NestedScrollView :这是系统提供的支持嵌套滚动的容器,设置 android:fillViewport="true" 后,内层 RecyclerView 可以正常滑动,外层也能联动。但实测在旧机型上还是有卡顿。
  3. 自定义滚动逻辑 :通过 NestedScrollingParentNestedScrollingChild 接口协调滚动优先级。比如当用户手指在 RecyclerView 区域垂直滑动时,优先让 RecyclerView 滚动;滚动到底部后,再触发外层滚动。

最后选了第二种方案,因为开发成本低。不过上线前用 ​云真机测试​ 跑了一遍主流机型,发现华为部分机型有兼容性问题,加了版本判断代码才解决。"


面试官陷阱题​:

"DiffUtil 用起来会不会有性能问题?比如数据量特别大的时候。"

​(暴露思考过程,展示深度):

"这要看怎么用了!我们之前有个日程管理 App,每次同步数据时要用 DiffUtil 对比新旧 5000 条日程。一开始在主线程跑 DiffUtil.calculateDiff(),直接 ANR 了。

后来改到后台线程计算差异,再切回主线程 dispatchUpdatesTo,问题就解决了。不过这里有个细节:​DiffUtil 的时间复杂度是 O(N)​,如果数据量真的超大(比如 10 万条),对比耗时可能超过 100ms,连后台线程都会卡。

我们的优化方案是:

  • 分片对比:比如每次只对比当前屏幕可见的 20 条数据。
  • 增量更新:后端返回数据时带上版本号,只拉取增量的数据,减少对比量。
  • 替代方案 :对于实时性要求不高的列表,直接用 notifyItemRangeChanged 手动控制刷新范围。

不过现在 Jetpack 的 ​Paging 3 库​ 已经内置了分页和差异对比,能自动处理这些优化,我们现在新项目都用这个方案了。"


面试官终极问题​:

"如果让你设计一个像抖音那样的全屏视频滑动列表,你会怎么保证流畅度?"

​(展现架构思维):

"抖音的流畅体验背后有很多细节!我们之前做过类似的短视频模块,核心优化点有三个:

  1. 预加载机制​:

    • 当前播放第 N 个视频时,预加载 N+1 和 N-1 的视频资源。
    • RecyclerView.addOnScrollListener 监听滑动方向,提前 500ms 加载下一批数据。
  2. 视图复用​:

    • 每个全屏 Item 的 ViewHolder 都包含视频播放器组件。
    • 滑动时,复用的 ViewHolder 不需要重新初始化播放器,只需替换数据源(比如 ExoPlayerprepare 新 URL)。
  3. 内存控制​:

    • 限制同时缓存的视频数(比如最多缓存 3 个),其他 ViewHolder 的播放器释放资源。
    • WeakReference 缓存解码后的第一帧图片,避免 OOM。

不过最难的还是 ​手势冲突处理 。比如用户上下滑动切换视频时,如果横向滑动触发点赞控件,体验会很割裂。我们最后通过自定义 GestureDetector 判断滑动方向,水平滑动超过 45 度才触发点赞,否则执行翻页。"


面试官​:

"RecyclerView 的缓存机制你了解吗?能简单说说它的工作原理吗?"

​(自然带入项目经验):

"RecyclerView 的缓存机制我们项目里优化过好几次,确实是个挺关键的点。比如之前做一款社交 App 的聊天页面,消息列表特别长,快速滑动的时候总感觉有点卡。后来我们仔细研究了一下缓存机制,发现它其实分了几个'暂存区',用来回收和复用 ViewHolder。"


面试官追问​:

"哦?具体有哪些'暂存区'?能举个例子吗?"

​(用生活场景比喻):

"可以想象成快递站的包裹柜------

  1. 临时货架(mAttachedScrap)​:比如你正在取快递,手头拿着的几个包裹暂时放在身边,等会儿可能还要用。RecyclerView 在布局的时候,会把当前屏幕上的 ViewHolder 先放在这里,方便快速调整位置。
  2. 最近包裹区(mCachedViews)​:快递员会把最近送到但暂时没人取的快递放在这里,比如你刚扫了一眼的某个消息项滑出屏幕,但可能马上又会滑回来,这时候直接从这拿,不用重新绑数据。
  3. 大仓库(RecycledViewPool)​:如果快递太多放不下,就会按类型分类存到仓库里。比如所有图片消息的 ViewHolder 放一个区,文本消息放另一个区,下次需要的时候虽然要重新绑数据,但至少不用重新造个新柜子。"

面试官深入​:

"那你们项目里是怎么利用这些机制优化的?"

​(结合实战案例):

"之前聊天页面的图片消息特别多,用户快速滑动时经常出现白屏。我们用 Android Studio 的 Profiler 一查,发现 onCreateViewHolder 耗时特别高,说明 ViewHolder 创建太频繁。

后来我们做了两件事:

  • 扩大'最近包裹区':recyclerView.setItemViewCacheSize(10),让更多滑出屏幕的 ViewHolder 留在 mCachedViews 里,反向滑动时直接复用,省去了重新绑定图片的时间。
  • 共享仓库 :因为 App 里还有个'动态'页面也用图片消息,我们让两个页面的 RecyclerView 共用同一个 RecycledViewPool,这样滑到'动态'页时,可以直接复用聊天页缓存过的图片 ViewHolder。"

面试官挑战​:

"如果遇到特别复杂的 Item 布局(比如直播间的弹幕),缓存机制还能有效吗?"

​(暴露问题并给出方案):

"确实会遇到挑战!我们做直播功能的时候,弹幕 Item 包含头像、昵称、消息内容,还有各种动画。一开始快速滚动时,FPS 直接掉到 40 以下。

后来分析发现,问题出在 ​缓存命中率低 ------因为弹幕类型多(普通弹幕、打赏消息、系统通知),每种类型的 ViewHolder 都被单独缓存,但缓存池默认每个类型只存 5 个。

我们的解决方案:

  1. 合并相似类型:把打赏消息和系统通知都合并成'特殊消息'类型,通过数据字段区分样式。
  2. 预加载关键 ViewHolder:在进入直播间时,提前创建 10 个弹幕 ViewHolder 并缓存,避免高峰时段密集创建。
  3. 优化 onBindViewHolder:把头像加载改成 Glide 的预加载机制,避免在滚动时主线程解码图片。"

面试官追问​:

"听起来你们对缓存机制理解很深,那如果让你设计一个新的列表控件,会参考 RecyclerView 的缓存设计吗?"

​(展示设计思维):

"肯定会参考它的分层思想!比如最近我们在做一个相机滤镜列表,需要横向滚动展示大量滤镜预览图。

借鉴 RecyclerView 的经验,我们设计了:

  • 预览图缓存池:保留最近使用过的 5 个滤镜预览 Renderer,避免每次滑动都重新初始化 OpenGL 资源。
  • 动态回收策略 :如果用户 30 秒没滑动,自动释放一半缓存,平衡内存和流畅度。
    不过我们也改了一点------因为滤镜列表是横向的,所以 mCachedViews 改成了优先缓存左右两侧的 ViewHolder,这样快速来回滑动更顺滑。"

面试官​:

"你在项目里用过 RecyclerView 的 DiffUtil 吗?能说说它的作用和你们是怎么用的吗?"

​(自然带入场景):

"当然用过!我们团队做新闻 App 的时候,首页的资讯列表经常需要更新,比如用户下拉刷新或者加载更多。一开始用 notifyDataSetChanged(),结果每次刷新整个列表都会闪一下,体验特别差。后来引入了 DiffUtil,只更新有变化的 Item,流畅多了。

比如有一次,用户点了一篇新闻的'点赞'按钮,点赞数要从 100 变成 101。用 DiffUtil 的话,它只会刷新这一行,其他没变的新闻标题、图片都不用动,看起来就像瞬间更新了一样,完全没有闪烁。"


面试官追问​:

"听起来不错,那 DiffUtil 具体是怎么判断哪些数据变化的?"

​(比喻化解释):

"可以把它想象成一个'数据侦探'!它会拿着新旧两份数据清单,挨个对比:

  1. 第一步:找熟人areItemsTheSame):比如通过新闻的 ID 判断是不是同一条数据。
  2. 第二步:查细节areContentsTheSame):如果 ID 对上了,再检查标题、图片这些内容有没有变化。
  3. 第三步:记小本本getChangePayload):如果只是某个小地方变了(比如点赞数),就记下来,告诉 Adapter 只更新这个部分,不用整个重画。"

面试官挑战​:

"那你们在实现的时候有没有踩过什么坑?比如数据量很大的时候会不会卡?"

​(暴露问题并给出方案):

"还真踩过!有一次测试同学扔了个 5000 条数据的列表过来,结果一刷新就 ANR 了。后来发现是因为在主线程跑 DiffUtil.calculateDiff(),计算量太大直接卡死主线程。

我们当时的解决方案:

  1. 扔到后台线程:用 Kotlin 协程或者 RxJava 在后台计算差异,算完了再切回主线程更新 UI。
  2. 数据分片:比如每次只对比当前屏幕能看到的 20 条数据,而不是全量 5000 条。
  3. 增量更新:让后端同学改接口,只返回变化的数据,比如'新增了 10 条,删了 2 条',这样 DiffUtil 只要处理 12 条,速度飞快。"

面试官深入​:

"如果遇到数据顺序变化,比如用户拖拽排序,DiffUtil 能自动处理吗?"

​(结合动画效果):

"可以的!比如我们做过一个任务管理 App,用户长按拖拽调整任务顺序。DiffUtil 会识别到位置变化,自动触发 notifyItemMoved,配合 RecyclerView 的默认动画,任务项会'滑'到新位置,特别丝滑。

不过有个细节:如果数据类的 equals 方法没重写,可能会导致 DiffUtil 误判内容变化,触发不必要的刷新。所以我们强制所有数据类必须实现 equalshashCode,只用 ID 和关键字段做对比。"


面试官陷阱题​:

"有人说用了 DiffUtil 就不需要 notifyItemChanged(position) 了,对吗?"

​(指出误区):

"不完全对!比如有个特殊场景:用户修改了某条数据的某个字段,但这个字段不在 areContentsTheSame 的对比范围内。这时候 DiffUtil 会认为内容没变,跳过刷新。

我们的解决方案:

  • 方案一 :在 areContentsTheSame 里加入这个字段的对比。
  • 方案二 (更灵活):手动调用 notifyItemChanged,但用 Payload 告诉 Adapter 只更新特定控件。比如点赞数变化时,只改数字,不碰标题和图片。"

基础知识扩展:


RecyclerView 缓存机制

一、缓存层级与核心设计思想

RecyclerView 的缓存机制通过 ​多级缓存池 ​ 实现高效复用,核心目标是 ​减少 ViewHolder 的重复创建和布局测量,从而提升滚动性能。其缓存层级可分为四个部分:

缓存层级 存储内容 复用条件 生命周期
mAttachedScrap 当前屏幕可见的 ViewHolder 同位置同类型 短暂(仅在布局阶段有效)
mCachedViews 近期滑出屏幕的 ViewHolder 同位置同类型 长期(容量满时淘汰到下一级)
RecycledViewPool 按类型分类的 ViewHolder 同类型即可复用 长期(应用生命周期内有效)
ViewCacheExtension 开发者自定义缓存(极少使用) 开发者控制 自定义

二、各级缓存详解与实战场景
1. mAttachedScrap:临时缓存,用于布局优化
  • 工作原理 :在 onLayoutChildren() 过程中,屏幕可见的 ViewHolder 会被临时存入 mAttachedScrap。当布局完成后,未被复用的 ViewHolder 会回到 mCachedViews 或 RecycledViewPool。

  • 场景案例:快速来回滑动时,刚滑出的 ViewHolder 可能还在 mAttachedScrap 中,直接复用无需重新绑定数据。

  • 关键代码

    java 复制代码
    // RecyclerView 源码中的处理逻辑
    void layoutChildren() {
        // 将当前可见的 ViewHolder 存入 mAttachedScrap
        scrapOrRecycleView(recycler, i, view);
        // 重新布局时优先从 mAttachedScrap 获取
        ViewHolder holder = getScrapOrCachedViewForPosition(position);
    }
2. mCachedViews:高频复用缓存(默认容量 2)​
  • 工作原理 :ViewHolder 滑出屏幕后,优先存入 mCachedViews。当用户反向滑动时,直接从 mCachedViews 取出复用(无需 onBindViewHolder)。

  • 优化技巧 :若列表项固定(如消息列表),增大 mCachedViews 容量可提升反向滑动性能。

    复制代码
    recyclerView.setItemViewCacheSize(10); // 增大缓存容量
  • 淘汰策略:当 mCachedViews 容量满时,最旧的 ViewHolder 会被转移到 RecycledViewPool。

3. RecycledViewPool:跨列表共享的全局缓存
  • 存储结构 :按 viewType 分类,每个类型默认缓存 5 个 ViewHolder。

  • 复用规则:不同位置、不同 RecyclerView 的同类型 ViewHolder 可复用(需重新绑定数据)。

  • 共享场景 :ViewPager 中多个 RecyclerView 共享同一个 Pool,避免重复创建。

    java 复制代码
    RecyclerView.RecycledViewPool pool = new RecyclerView.RecycledViewPool();
    recyclerView1.setRecycledViewPool(pool);
    recyclerView2.setRecycledViewPool(pool);
  • 容量调整

    复制代码
    pool.setMaxRecycledViews(TYPE_IMAGE, 10); // 增大图片类型缓存
4. ViewCacheExtension:自定义缓存(高级用法)​
  • 使用场景:需要特殊复用逻辑时(如根据业务状态缓存),但 99% 的项目无需使用。

  • 示例代码

    java 复制代码
    public class CustomCacheExtension extends RecyclerView.ViewCacheExtension {
        private SparseArray<ViewHolder> mCache = new SparseArray<>();
    
        @Override
        public View getViewForPositionAndType(int position, int type) {
            return mCache.get(position); // 根据位置返回缓存视图
        }
    
        public void addToCache(int position, ViewHolder holder) {
            mCache.put(position, holder);
        }
    }

三、缓存工作流程(以向下滑动为例)​
  1. ViewHolder 滑出屏幕

    • 存入 mCachedViews(若未满) → 复用时不触发 onBindViewHolder
    • 若 mCachedViews 已满,转移到 RecycledViewPool。
  2. 新 ViewHolder 需要显示

    • 优先从 mAttachedScrap 查找(布局阶段)。
    • 若未找到,从 mCachedViews 查找(同位置)。
    • 若未找到,从 RecycledViewPool 获取(同类型)。
    • 若未找到,调用 onCreateViewHolder 创建新实例。
  3. ViewHolder 回收到池中

    • 从 RecycledViewPool 获取的 ViewHolder 必须重新绑定数据(onBindViewHolder)。

四、性能优化实战技巧
1. 提升缓存命中率
  • 预加载布局 :在空闲期预创建 ViewHolder。

    java 复制代码
    recyclerView.post(() -> {
        RecyclerView.LayoutManager layoutManager = recyclerView.getLayoutManager();
        layoutManager.scrollToPosition(preloadPosition); // 触发预加载
    });
  • 避免频繁变更 ViewType:相同数据尽量使用相同 ViewType。

2. 监控缓存状态
  • 通过 RecyclerView.Recycler 调试

    java 复制代码
    RecyclerView.Recycler recycler = recyclerView.getRecycler();
    int cachedCount = recycler.getCachedViews().size(); // mCachedViews 当前数量
    int poolSize = recycler.getRecycledViewPool().getRecycledViewCount(TYPE_TEXT); // 某类型缓存数
3. 解决常见问题
  • 卡顿问题 :检查 onBindViewHolder 是否耗时,避免主线程操作。

  • 内存泄漏 :在 onViewRecycled() 中释放资源。

    java 复制代码
    @Override
    public void onViewRecycled(@NonNull ViewHolder holder) {
        Glide.with(holder.imageView).clear(holder.imageView); // 释放图片资源
    }

五、大厂面试高频问题
问题 1:mCachedViews 和 RecycledViewPool 的区别?​
  • mCachedViews:按位置缓存,复用无需重新绑定数据,容量小(默认 2)。
  • RecycledViewPool:按类型缓存,复用需重新绑定数据,容量可跨列表共享。
问题 2:如何实现类似微信聊天列表的流畅滑动?​
  • 优化点
    1. 使用 DiffUtil 局部更新,减少 onBindViewHolder 触发次数。
    2. 增大 mCachedViews 容量(setItemViewCacheSize(20))。
    3. 避免在 onBindViewHolder 中加载图片,用 Glidepreload() 预加载。
问题 3:为什么 RecyclerView 比 ListView 更高效?​
  • 缓存机制:RecyclerView 通过多级缓存和 ViewHolder 模式,减少布局测量和视图创建开销。
  • 布局解耦:支持横向、网格、瀑布流等布局,避免 ListView 的全局重绘。

DiffUtil

1. DiffUtil 是什么?​

DiffUtil 是 Android 中用于优化 RecyclerView 数据更新的工具。它通过智能对比新旧数据集,精确计算出哪些数据项发生了改变(增、删、改、移动),从而触发局部刷新 ,避免无脑调用 notifyDataSetChanged() 导致整个列表重绘。


2. 为什么需要 DiffUtil?​
  • 传统方法的弊端 ​:

    使用 notifyDataSetChanged() 会强制刷新整个列表,即使只有一项数据变化,所有 Item 都会重新执行 onBindViewHolder,导致性能浪费(如卡顿、闪烁)。

  • DiffUtil 的优势​:

    • 仅更新变化的 Item,减少 UI 操作次数。
    • 自动处理移动动画(如数据项位置交换时的平滑过渡)。
    • 支持局部更新(仅刷新变化的控件,如点赞数)。

3. 核心原理:DiffUtil.Callback

要使用 DiffUtil,需实现 DiffUtil.Callback 抽象类,定义四个关键方法:

方法 作用
getOldListSize() 返回旧数据集的长度。
getNewListSize() 返回新数据集的长度。
areItemsTheSame(oldPos, newPos) 判断新旧位置的数据项是否代表同一对象(通常通过唯一 ID 比较)。
areContentsTheSame(oldPos, newPos) 判断同一对象的数据内容是否变化(如标题、图片是否修改)。
getChangePayload()(可选) 返回变化的"载荷"(如仅标题变化),用于更细粒度的局部更新。

4. 使用步骤
步骤 1:实现 DiffUtil.Callback
java 复制代码
class MyDiffCallback(
    private val oldList: List<Item>,
    private val newList: List<Item>
) : DiffUtil.Callback() {

    override fun getOldListSize() = oldList.size
    override fun getNewListSize() = newList.size

    override fun areItemsTheSame(oldPos: Int, newPos: Int): Boolean {
        // 通过唯一 ID 判断是否是同一项
        return oldList[oldPos].id == newList[newPos].id
    }

    override fun areContentsTheSame(oldPos: Int, newPos: Int): Boolean {
        // 判断内容是否一致(需重写数据类的 equals())
        return oldList[oldPos] == newList[newPos]
    }

    // 可选:返回变化的部分数据
    override fun getChangePayload(oldPos: Int, newPos: Int): Any? {
        val oldItem = oldList[oldPos]
        val newItem = newList[newPos]
        return if (oldItem.title != newItem.title) "UPDATE_TITLE" else null
    }
}
步骤 2:在后台线程计算差异
java 复制代码
// 在协程或异步任务中执行
GlobalScope.launch(Dispatchers.Default) {
    val diffResult = DiffUtil.calculateDiff(MyDiffCallback(oldList, newList))
    withContext(Dispatchers.Main) {
        // 先更新数据源,再应用变更
        adapter.updateData(newList)
        diffResult.dispatchUpdatesTo(adapter)
    }
}
步骤 3:Adapter 处理局部更新
java 复制代码
override fun onBindViewHolder(holder: ViewHolder, position: Int, payloads: List<Any>) {
    if (payloads.isEmpty()) {
        // 全量更新
        holder.bind(dataList[position])
    } else {
        // 局部更新(如仅更新标题)
        payloads.forEach { payload ->
            if (payload == "UPDATE_TITLE") {
                holder.titleView.text = dataList[position].title
            }
        }
    }
}
步骤 4:启用稳定 ID(关键!)​
java 复制代码
class MyAdapter : RecyclerView.Adapter<ViewHolder>() {
    init {
        setHasStableIds(true) // 必须启用
    }

    override fun getItemId(position: Int): Long {
        return dataList[position].id // 返回唯一 ID
    }
}

5. 常见错误与避坑指南
  1. 未在后台线程计算差异​:

    • 问题 :大数据集下 DiffUtil.calculateDiff() 会阻塞主线程,导致卡顿。
    • 解决 :始终在后台线程执行计算,通过协程或 AsyncTask 切回主线程更新。
  2. areItemsTheSame 实现错误​:

    • 错误示例 :直接比较对象引用(oldItem == newItem),而非唯一 ID。
    • 解决:确保比较的是业务唯一标识(如数据库主键)。
  3. 未设置 setHasStableIds(true)​​:

    • 问题:RecyclerView 无法正确匹配新旧项,导致动画异常或数据错乱。
    • 解决 :在 Adapter 初始化时调用 setHasStableIds(true),并正确实现 getItemId()
  4. 数据更新顺序错误​:

    • 错误流程 :先调用 diffResult.dispatchUpdatesTo(adapter),再更新数据源。
    • 正确顺序:先更新 Adapter 的数据源,再应用差异。

6. 性能优化技巧
  • 合理设计数据类 ​:

    重写 equals()hashCode(),确保 areContentsTheSame 能正确判断内容变化。

  • 使用 Payload 局部更新 ​:

    对于部分变化的项(如点赞数),通过 getChangePayload 返回变化字段,减少 onBindViewHolder 的计算量。

  • 分页加载大数据集 ​:

    避免一次性对比数万条数据,采用分页加载减少单次计算量。

相关推荐
东风破1374 分钟前
DM8达梦共享存储集群DSC搭建步骤
数据库·学习·dm达梦数据库
星幻元宇VR39 分钟前
VR科普大空间:沉浸式公共教育新模式
科技·学习·安全·vr·虚拟现实
笨鸟先飞的橘猫3 小时前
MMO游戏中的“跨服团队副本”匹配与状态同步系统
分布式·学习·游戏·lua·skynet
赏金术士4 小时前
Kotlin ViewModel
android·kotlin
雨落在了我的手上4 小时前
如何学习java?
java·开发语言·学习
吃好睡好便好5 小时前
汽车基本组成
学习·汽车
vistaup5 小时前
kotlin 二维码实现高斯模糊
android·kotlin
拾忆丶夜6 小时前
unity 热力图学习
学习·unity·游戏引擎
愈努力俞幸运6 小时前
function calling与mcp
android·数据库·redis
red_redemption6 小时前
自由学习记录(183)
学习·ue项目改名字的学问