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 的计算量。

  • 分页加载大数据集 ​:

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

相关推荐
Lester_11011 小时前
嵌入式学习笔记 - 关于ARM编辑器compiler version 5 and compiler version 6
arm开发·笔记·学习
5171 小时前
Django学习
学习·django·sqlite
等什么君!2 小时前
学习vue3:跨组件通信(provide+inject)
前端·vue.js·学习
努力学习的小廉2 小时前
深入了解linux系统—— 文件系统
android·linux·运维
拾忆-eleven2 小时前
NLP学习路线图(四):Python编程语言
python·学习·自然语言处理
moxiaoran57533 小时前
uni-app学习笔记八-vue3条件渲染
笔记·学习·uni-app
Digitally3 小时前
如何在 Android 手机和平板电脑上下载应用程序
android·智能手机·电脑
androidwork3 小时前
Android车载应用开发:Kotlin与Automotive OS深度实践
android·开发语言·kotlin
栒U3 小时前
配置gem5环境:Dockerfile使用
学习·系统架构
快乐肚皮4 小时前
Netty学习专栏(一):Java NIO编程与核心组件详解
java·学习·nio