AAOS CarMediaService 问题分析
文章目录
- 问题描述
- 车载蓝牙音乐流程
- Music 监听焦点变化流程
- BT请求焦点的流程
- MediaSession 服务端的流程
- BT和music 之间的相互影响
问题描述
-
问题
AAOS界面连接蓝牙的情况下,Music应用播放音乐会暂停。
-
分析
暂停是应用的行为,Music应用会监听focus的变化,监听到焦点失去的情况会调用暂停。但是Music 应用刚启动播放的时候 也会请求焦点,焦点第一次是在bt这边的。bt失去焦点,但立马又重新请求了焦点。 BT请求焦点就导致Music应用失去焦点而暂停。
了解问题之前首先要理解
- 蓝牙音乐在哪里调用到音频框架?
- 音频焦点
- carMediaService
车载蓝牙音乐流程
-
A2DP端
代码位置:
system\bt\btif\src\btif_avrcp_audio_track.cc
system\bt\stack\a2dp\a2dp_aac.cc
在上述的BtifAvrcpAudioTrackCreate()函数中。在这里面会创建一个AAudioStreamBuilder, AAudio 通过legacy的模式 的audiotrack 来进行处理 写数据。 -
AAudio端
蓝牙音乐, 车载端是一个sink 端,作为播放来使用。 对应的流程在btif_avrcp_audio_track 是通过调用AAudio的接口来实现播放。其中AAudio 没有实现mmap的方式 走的是legacy模式
代码在frameworks\av\media\libaaudio\src\legacy\AudioStreamTrack.cpp中
也就是通过创建audiotrack,然后设置参数、往里面写数据实现的。audiotrack start 的时候 同样会getoutputAttr获取到设备, 这个时候的路由信息已经由AAOS 根据car_audio_policy.xml
进行注册。 -
整体流程:
从source端(也就是手机通过蓝牙)发送过来的是aac或者ldac编码的数据, 数据在a2dp中继续解码 并不是进入到audiotrack。 a2dp中有相当于播放器中解码器的功能,解码后的数据才调用audiotrak进行播放。
Music 监听焦点变化流程
- 实现AudioFocusListener 然后注册到AudioMananger
private OnAudioFocusChangeListener mAudioFocusListener = new OnAudioFocusChangeListener() {public void onAudioFocusChange(int focusChange) {mMediaplayerHandler.obtainMessage(FOCUSCHANGE, focusChange, 0).sendToTarget();}};mAudioManager.requestAudioFocus(mAudioFocusListener, AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
-
在框架层焦点发生变化的时候 回调到外部注册进去的listener
-
Music 中MediaPlayBackService的实现
通过looper发送消息进行处理,而如果是focus丢失的时候,所做的操作是pause。
case AudioManager.AUDIOFOCUS_LOSS:
Log.v(LOGTAG, "AudioFocus: received AUDIOFOCUS_LOSS");if (isPlaying()) {mPausedByTransientLossOfFocus = false;}pause();
break;
case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:Log.v(LOGTAG, "AudioFocus: received AUDIOFOCUS_LOSS_TRANSIENT");if (isPlaying()) {mPausedByTransientLossOfFocus = true;}pause();
break;
BT请求焦点的流程
- 监听mediassion 的onPrepare事件
有Prepare事件发生后会调用requestFocus。其usage是USAGE_MEDIA。这个会回调到AAOS的CarAudioFocus
进行处理, 其就是根据交互矩阵进行处理的。当前持有的是music应用,在BT 请求焦点后,会发送消息通知music 焦点失去了。
在上面的流程知道 失去焦点后 会调用player的 pause 进行暂停操作。
packages\apps\Bluetooth\src\com\android\bluetooth\avrcpcontroller\AvrcpControllerStateMachine.java
BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks);
sBluetoothMediaBrowserService.mSession.setCallback(callback);MediaSessionCompat.Callback mSessionCallbacks = new MediaSessionCompat.Callback() {@Overridepublic void onPrepare() {logD("onPrepare");A2dpSinkService a2dpSinkService = A2dpSinkService.getA2dpSinkService();if (a2dpSinkService != null) {a2dpSinkService.requestAudioFocus(mDevice, true);}}
}private synchronized int requestAudioFocus() {if (DBG) Log.d(TAG, "requestAudioFocus()");// Bluetooth A2DP may carry Music, Audio Books, Navigation, or other sounds so mark content// type unknown.AudioAttributes streamAttributes =new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA).setContentType(AudioAttributes.CONTENT_TYPE_UNKNOWN).build();// Bluetooth ducking is handled at the native layer at the request of AudioManager.AudioFocusRequest focusRequest =new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN).setAudioAttributes(streamAttributes).setOnAudioFocusChangeListener(mAudioFocusListener, this).build();int focusRequestStatus = mAudioManager.requestAudioFocus(focusRequest);// If the request is granted begin streaming immediately and schedule an upgrade.if (focusRequestStatus == AudioManager.AUDIOFOCUS_REQUEST_GRANTED) {startFluorideStreaming();mAudioFocus = AudioManager.AUDIOFOCUS_GAIN;}return focusRequestStatus;}
- 那现在的问题是 哪里触发了onPrepare ?
可以看到是实现了MediaSession的Callback。 理解MediaSession的概念
MediaSession 有客户端和服务端。 客户端对应的是UI 这一端,服务端对应的是player。
UI这一端的实现是在packages/app/Car/Media 中。
主要是几个类 的封装分别是
MediaControl:用来控制MediaSession, 在上面的AVrcpControl中 实现的MessaionCompat的callback回调的
play prepare都是由MediaControl这边调用的。
packages\apps\Car\Media\src\com\android\car\media\service\MediaConnectorService.java
packages\apps\Car\libs\car-media-common\src\com\android\car\media\common\playback\PlaybackViewModel.java
MediaControllerCompat.TransportControls controls = controller.getTransportControls();
controls.prepare();
这里的prepare 会调用到AvrcpControllerStateMachine的onPrepare中。
- 如何监听到playstate的变化的?
packages\apps\Music\src\com\android\music\MediaPlaybackService.java
可以确认是music 发送的play state 导致 carMusicApp这边MediaControl调用prepare的.
下面的RemoteControlClient实际是MediaSession的封装
private void notifyChange(String what) {Intent i = new Intent(what);i.putExtra("id", Long.valueOf(getAudioId()));i.putExtra("artist", getArtistName());i.putExtra("album", getAlbumName());i.putExtra("track", getTrackName());i.putExtra("playing", isPlaying());sendStickyBroadcast(i);if (what.equals(PLAYSTATE_CHANGED)) {mRemoteControlClient.setPlaybackState(isPlaying()? RemoteControlClient.PLAYSTATE_PLAYING: RemoteControlClient.PLAYSTATE_PAUSED);}
代码位置:
packages\services\Car\service\src\com\android\car\CarMediaService.java
是通过重新编写MediaController.Callback来实现的。MediaSession设置的状态变化会通过callback调用到MediaCotroller当中
private class MediaControllerCallback extends MediaController.Callback {public void onPlaybackStateChanged(@Nullable PlaybackState state) {setPrimaryMediaSource(mediaSource, MEDIA_SOURCE_MODE_PLAYBACK);}
}private void startMediaConnectorService(boolean startPlayback, UserHandle currentUser) {Intent serviceStart = new Intent(MEDIA_CONNECTION_ACTION);Log.d(CarLog.TAG_MEDIA, Log.getStackTraceString(new Throwable()));serviceStart.setPackage(mContext.getResources().getString(R.string.serviceMediaConnection));serviceStart.putExtra(EXTRA_AUTOPLAY, startPlayback);mContext.startForegroundServiceAsUser(serviceStart, currentUser);}
packages\apps\Car\Media\src\com\android\car\media\service\MediaConnectorService.java
在上述的startMediaConnectorService会启动一个service 这个service调用到MediaConnectorSerive中onStartCommand。
在startcommad 中会使用mediacontrol 进行prepare操作。
public int onStartCommand(Intent intent, int flags, int startId) {playbackViewModel.getPlaybackStateWrapper().observe(this,playbackStateWrapper -> {if (playbackStateWrapper != null) {// If the source to play was specified in the intent ignore others.ComponentName intentComp = mCurrentTask.mMediaComp;ComponentName stateComp = playbackStateWrapper.getMediaSource().getBrowseServiceComponentName();if (!Objects.equals(stateComp, intentComp)) {return;}if (playbackStateWrapper.isPlaying()) {stopTask();return;}if ((playbackStateWrapper.getSupportedActions()& PlaybackStateCompat.ACTION_PREPARE) != 0) {playbackViewModel.getPlaybackController().getValue().prepare();if (!autoplay) {stopTask();}}if (autoplay && (playbackStateWrapper.getSupportedActions()& PlaybackStateCompat.ACTION_PLAY) != 0) {playbackViewModel.getPlaybackController().getValue().play();stopTask();}}});
src\com\android\bluetooth\a2dpsink\A2dpSinkService.java
总结: 单单看MediaSession 和 MediaControl。 MediaControl是UI端控制Service端的类,在AAOS中所有的app播放控制客户端的实现都是carMediaApp中MediaControl的实现的(包括蓝牙audio localplayer界面中暂停播放,下一首 上一首等等)。 MediaSession是服务端, 这个服务端包括(蓝牙的src\com\android\bluetooth
,和/apps/Car/LocalMediaPlayer)。这这里面实现了Mediassion 的callback 用来响应client 端UI的控制。 而响应之后的状态改变可以通过继承MediaControl的callback 在客户端实现。
而Music应用中会向session发送状态改变的消息,客户端carMediaApp会响应这个消息,响应这个消息的结果就是prepare播放器。这个prepare调用到蓝牙的MediaSeesion。MediaSeeion正常应该 一个客户端和服务器一一对应的。
-
问题的解决
对于没有MediaSource的session变化 不启动MediaConnectService。
MediaSession 服务端的流程
首先实现MediaSessionCompat.Callback(),然后将这个callback 设置到MediaBrowserService的sesseion中。
BluetoothMediaBrowserService.addressedPlayerChanged(mSessionCallbacks);
sBluetoothMediaBrowserService.mSession.setCallback(callback);
session 中token的传递
- 为什么music 发送的消息 这边的session 可以接收到。
因为在CarMediaService 中已经注册了MediaSession变化的消息。 在音乐应用启动的时候 会新建MediaSession,
而在这里就会监听了active MediaSession的变化,同时传递当前所有的Mediacontrol, 然后对这些mediaControl注册callback。
在这个callback 中监听onPlaybackStateChanged事件。 而在这边对mediacontrol的管理是通过token实现的。
token 是在MediaSession 和 MdiaControl 直接建立连接的数据。可以通过getHashCode来打印其hash值确认。
private void initUser(@UserIdInt int userId) {updateMediaSessionCallbackForCurrentUser();if (mSessionsListener != null) {mMediaSessionManager.removeOnActiveSessionsChangedListener(mSessionsListener);}mSessionsListener = new SessionChangedListener(ActivityManager.getCurrentUser());UserHandle currentUserHandle = new UserHandle(ActivityManager.getCurrentUser());mMediaSessionManager.addOnActiveSessionsChangedListener(null, currentUserHandle,new HandlerExecutor(mHandler), mSessionsListener);}private class SessionChangedListener implements OnActiveSessionsChangedListener {private final int mCurrentUser;SessionChangedListener(int currentUser) {mCurrentUser = currentUser;}@Overridepublic void onActiveSessionsChanged(List<MediaController> controllers) {if (ActivityManager.getCurrentUser() != mCurrentUser) {Slog.e(CarLog.TAG_MEDIA, "Active session callback for old user: " + mCurrentUser);return;}Log.d(CarLog.TAG_MEDIA, Log.getStackTraceString(new Throwable()));Log.d(CarLog.TAG_MEDIA, "controllers szie " + controllers.size());mMediaSessionUpdater.registerCallbacks(controllers);}}private void registerCallbacks(List<MediaController> newControllers) {List<MediaController> additions = new ArrayList<>(newControllers.size());Map<MediaSession.Token, MediaControllerCallback> updatedCallbacks =new HashMap<>(newControllers.size());for (MediaController controller : newControllers) {MediaSession.Token token = controller.getSessionToken();String newPackageName = controller.getPackageName();Log.d(CarLog.TAG_MEDIA, Log.getStackTraceString(new Throwable()));MediaControllerCallback callback = mCallbacks.get(token);if (callback == null) {callback = new MediaControllerCallback(controller);callback.register();additions.add(controller);}updatedCallbacks.put(token, callback);}private MediaControllerCallback(MediaController mediaController) {public void onPlaybackStateChanged(@Nullable PlaybackState state) {}
BT和music 之间的相互影响
- 本地音乐在播放,手机播放蓝牙音乐 两个声音同时播放
这个是焦点管理的问题, 按理理解蓝牙音乐播放的时候 应该去请求焦点。需要设置属性才会请求,默认不会请求。
播放的时候 前面流程不用管,最后会调用到SRC_STR_START中,没有请求焦点,music应用就会一直播。修改方法:
解决方法:通过配置shouldRequestFocus 让BT应用在每次播放的时候都强制请求焦点解决。强制请求之后,music就会失去焦点暂停。
packages\apps\Bluetooth\src\com\android\bluetooth\a2dpsink\A2dpSinkStreamHandler.javaswitch (message.what) {case SRC_STR_START:mStreamAvailable = true;if (isTvDevice() || shouldRequestFocus()) {requestAudioFocusIfNone();}break;
- 蓝牙音乐在播放的时候,本地音乐播放会导致蓝牙直接stop掉。
这个stop 是在蓝牙的MediaSessions callback onstop中调用的。
而这个回调是在CarMediaService中被触发的。也是在上面的流程中music应用的playbackstate 回调中stop的。
解决方法:不调用stop, 调用pause进行暂停。
public void onPlaybackStateChanged(@Nullable PlaybackState state)private void setPlaybackMediaSource(ComponentName playbackMediaSource) {stopAndUnregisterCallback();相关文章:
AAOS CarMediaService 问题分析
文章目录 问题描述车载蓝牙音乐流程Music 监听焦点变化流程BT请求焦点的流程MediaSession 服务端的流程BT和music 之间的相互影响 问题描述 问题 AAOS界面连接蓝牙的情况下,Music应用播放音乐会暂停。 分析 暂停是应用的行为,Music应用会监听focus的变化…...
06-Flask-蓝图的使用
蓝图的使用 前言蓝图使用方式 前言 本篇来学习下Flask中蓝图的使用 蓝图 在Flask中使用蓝图(Blurprint)来分模块组织管理蓝图可以理解为存储一组视图方法的容器对象,特点如下: 一个应用可以具有多个Blueprint可以将一个Blueprint注册到任何一个未使用…...
【LeetCode力扣】189 53 轮转数组 | 最大子数组和
目录 1、189. 轮转数组 1.1、题目介绍 1.2、解题思路 2、53. 最大子数组和 2.1、题目介绍 2.2、解题思路 1、189. 轮转数组 1.1、题目介绍 原题链接:189. 轮转数组 - 力扣(LeetCode) 示例 1: 输入: nums [1,2,3,4,5,6,7], k 3输…...
Go学习第十七章——Gin中间件与路由
Go web框架——Gin中间件与路由 1 单独注册中间件1.1 入门案例1.2 多个中间件1.3 中间件拦截响应1.4 中间件放行 2 全局注册中间件3 自定义参数传递4 路由分组4.1 入门案例4.2 路由分组注册中间件4.3 综合使用 5 使用内置的中间件6 中间件案例权限验证耗时统计 1 单独注册中间件…...
真实感渲染的非正式调研与近期热门研究分享
真实感渲染的非正式调研与近期热门研究分享 1 期刊1 Top2 Venues 2 Rendering Reserach1 Material2 BRDF3 Appearance Modeling4 Capture5 Light Transport光线传播6 Differetiable Rendring-可微渲染7 Ray Tracing8 Denoising降噪9 NeRF 3 VR/AR4 Non-Photorealistic Renderin…...
matlab中字符串转换为数字(str2double函数)
str2double函数 将 str 中的文本转换为双精度值。str 包含表示实数或复数值的文本。str 可以是字符向量、字符向量元胞数组或字符串数组。如果 str 是字符向量或字符串标量,则 X 是数值标量。如果 str 是字符向量元胞数组或字符串数组,则 X 是与 str 具…...
基于java的ssm框架农夫果园管理系统设计与实现
项目描述 临近学期结束,还是毕业设计,你还在做java程序网络编程,期末作业,老师的作业要求觉得大了吗?不知道毕业设计该怎么办?网页功能的数量是否太多?没有合适的类型或系统?等等。这里根据疫情当下,你想解决的问…...
ctf md5爆破
1.知道组成的字符为数字,然后知道加密后的MD5,求组成的字符 import hashlibimport stringdef crackMd5(dst):dst dst.lower()for a in range(0,10):for b in range(0,10):for c in range(0,10):for d in range(0,10):word str(a) str(b) str(c) str(d) "_heetian&q…...
不同碳化硅晶体面带来的可能性
对于非立方晶体,它们天生具有各向异性,即不同方向具有不同的性质。以碳化硅晶体面为例: 4H-SIC和6H-SIC的空间群是P63mc,点群是6mm。两者都属于六方晶系,具有各向异性。3C-SIC的空间群是F-43m,点群是-43m。…...
Kafka集群
Kafka集群 1、Kafka 概述1.1消息队列背景1.2类型1.3Kafka 定义1.4Kafka 简介 2、消息队列好处3、消息队列的模式4、Kafka 的特性5、Kafka 系统架构4、部署 kafka 集群4.1下载安装包4.2 安装 Kafka4.2.1 修改配置文件4.2.2 修改环境变量4.2.3 配置 zookeeper启动脚本4.2.4 设置…...
国腾GM8775C完全替代CS5518 MIPIDSI转2 PORT LVDS
集睿致远CS5518描述: CS5518是一款MIPI DSI输入、LVDS输出转换芯片。MIPI DSI 支持多达4个局域网,每条通道以最 大 1Gbps 的速度运行。LVDS支持18位或24位像素,25Mhz至154Mhz,采用VESA或JEIDA格 式。它只能使用单个1.8v电源&am…...
搜索与图论:匈牙利算法
将所有点分成两个集合,使得所有边只出现在集合之间,就是二分图 二分图:一定不含有奇数个点数的环;可能包含长度为偶数的环, 不一定是连通图 二分图的最大匹配: #include<iostream> #include<cs…...
明星艺人类的百度百科怎么创建 ?
明星艺人们的知名度对于其事业的成功至关重要,而作为国内最大的中文百科全书网站,百度百科成为了人们获取信息的重要来源。一线明星当然百科不用自己操心,平台和网友就给维护了,但是刚刚走红的明星艺人应提早布局百科词条…...
类EMD的“信号分解方法”及MATLAB实现(第八篇)——离散小波变换DWT(小波分解)
在之前的系列文章里,我们介绍了EEMD、CEEMD、CEEMDAN、VMD、ICEEMDAN、LMD、EWT,我们继续补完该系列。 今天要讲到的是小波分解,通常也就是指离散小波变换(Discrete Wavelet Transform, DWT)。在网上有一些介绍该方法…...
python随手小练10(南农作业题)
题目1: 编写程序,输出1~1000之间所有能被4整除,但是不能被5整除的数 具体操作: for i in range(1,1000): #循环遍历1~999,因为range是左闭右开if (i % 4 0) and (i % 5 ! 0) :print(i) 结果展示: 题目2&…...
How to install mongodb-7.0 as systemd service with podman
How to install mongodb-7.0 as systemd service with podman 1、安装1.1、创建卷1.2、配置文件1.3、创建容器1.4、服务管理1.5、容器管理 2、客户端管理 1、安装 1.1、创建卷 配置卷 podman volume create --label typemongo-7.0 --label envdev mongo-7.0-conf数据卷 pod…...
一文彻底理解python浅拷贝和深拷贝
目录 一、必备知识二、基本概念三、列表,元组,集合,字符串,字典浅拷贝3.1 列表3.2 元组3.3 集合3.4 字符串3.5 字典3.6 特别注意浅拷贝总结 四、列表,元组,集合,字符串,字典深拷贝 一…...
什么是软件的生命周期?全方位解释软件的生命周期
软件的生命周期 软件生命周期是指从软件产品的设想开始到软件不再使用而结束的时间。 如果把软件看成是有生命的事 物,那么软件的生命周期可以分成6个阶段,即需求分析、计划、设计、编码、测试、运行维护 需求分析阶段: 分析需求的可行性&…...
网络安全—小白自学
1.网络安全是什么 网络安全可以基于攻击和防御视角来分类,我们经常听到的 “红队”、“渗透测试” 等就是研究攻击技术,而“蓝队”、“安全运营”、“安全运维”则研究防御技术。 2.网络安全市场 一、是市场需求量高; 二、则是发展相对成熟…...
List 3.5 详解原码、反码、补码
前言 欢迎来到我的博客,我是雨空集(全网同名),无论你是无意中发现我,还是有意搜索而来,我都感到荣幸。这里是一个分享知识、交流想法的平台,我希望我的博客能给你带来帮助和启发。如果你喜欢我…...
【网络】每天掌握一个Linux命令 - iftop
在Linux系统中,iftop是网络管理的得力助手,能实时监控网络流量、连接情况等,帮助排查网络异常。接下来从多方面详细介绍它。 目录 【网络】每天掌握一个Linux命令 - iftop工具概述安装方式核心功能基础用法进阶操作实战案例面试题场景生产场景…...
label-studio的使用教程(导入本地路径)
文章目录 1. 准备环境2. 脚本启动2.1 Windows2.2 Linux 3. 安装label-studio机器学习后端3.1 pip安装(推荐)3.2 GitHub仓库安装 4. 后端配置4.1 yolo环境4.2 引入后端模型4.3 修改脚本4.4 启动后端 5. 标注工程5.1 创建工程5.2 配置图片路径5.3 配置工程类型标签5.4 配置模型5.…...
Docker 运行 Kafka 带 SASL 认证教程
Docker 运行 Kafka 带 SASL 认证教程 Docker 运行 Kafka 带 SASL 认证教程一、说明二、环境准备三、编写 Docker Compose 和 jaas文件docker-compose.yml代码说明:server_jaas.conf 四、启动服务五、验证服务六、连接kafka服务七、总结 Docker 运行 Kafka 带 SASL 认…...
srs linux
下载编译运行 git clone https:///ossrs/srs.git ./configure --h265on make 编译完成后即可启动SRS # 启动 ./objs/srs -c conf/srs.conf # 查看日志 tail -n 30 -f ./objs/srs.log 开放端口 默认RTMP接收推流端口是1935,SRS管理页面端口是8080,可…...
JDK 17 新特性
#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持,不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的ÿ…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...
OPenCV CUDA模块图像处理-----对图像执行 均值漂移滤波(Mean Shift Filtering)函数meanShiftFiltering()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 算法描述 在 GPU 上对图像执行 均值漂移滤波(Mean Shift Filtering),用于图像分割或平滑处理。 该函数将输入图像中的…...
【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...
Python+ZeroMQ实战:智能车辆状态监控与模拟模式自动切换
目录 关键点 技术实现1 技术实现2 摘要: 本文将介绍如何利用Python和ZeroMQ消息队列构建一个智能车辆状态监控系统。系统能够根据时间策略自动切换驾驶模式(自动驾驶、人工驾驶、远程驾驶、主动安全),并通过实时消息推送更新车…...
前端工具库lodash与lodash-es区别详解
lodash 和 lodash-es 是同一工具库的两个不同版本,核心功能完全一致,主要区别在于模块化格式和优化方式,适合不同的开发环境。以下是详细对比: 1. 模块化格式 lodash 使用 CommonJS 模块格式(require/module.exports&a…...
