iOS问题记录 - iOS 17通过NSUserDefaults设置UserAgent无效(续)

news2024/11/26 23:30:14

文章目录

  • 前言
  • 开发环境
  • 问题描述
  • 问题分析
    • 1. 准备源码
    • 2. 定位源码
    • 3. 对比源码
    • 4. 分析总结
  • 解决方案
  • 补充内容
    • 1. UserAgent的组成
    • 2. UserAgent的设置优先级
  • 最后


前言

在上篇文章中对该问题做了一些判断和猜测,并给出了解决方案。不过,美中不足的是没有进一步验证猜测,所以在这里进一步分析该问题作为上篇文章的补充。

开发环境

  • Xcode: 15.1
  • iOS: 17.2

问题描述

项目运行在iOS 17.2设备时,应用内网页无法成功获取设置后的UserAgent

项目中设置UserAgent的关键源码:

[self.webView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error) {
    NSString *userAgent = [NSString stringWithFormat:@"%@", result];
    NSString *newUserAgent = [userAgent stringByAppendingString:@" App/1.0.0"];
    [[NSUserDefaults standardUserDefaults] registerDefaults:@{@"UserAgent": newUserAgent}];
}];

问题分析

进一步分析的前提是有源码可以分析,好在WKWebView类属于WebKit.framework,这算是系统框架里面少有的开源框架。

1. 准备源码

找到GitHub上面的开源项目WebKit,通过以下命令克隆项目到本地:

git clone https://github.com/WebKit/WebKit.git WebKit

这项目有点大,最终占了约14GB的硬盘空间。因为后面可能需要搜索历史提交记录,所以我完整克隆了项目,如果你没有这个需求,可以按需浅克隆。

源码准备好了,现在可以直接用Xcode打开WebKit项目。那接下来该怎么定位导致该问题的关键源码呢?

2. 定位源码

一般常用的切入点有两个,各有各的优缺点:

  1. WKWebView类开始分析源码。耗时比较长且容易找错方向,但只要不断分析,定位到关键源码不是问题,而且能让你对该项目有更多的认识
  2. UserAgent作为关键词搜索相关的历史提交记录。运气好时能快速定位源码,运气不好时容易迷失方向

如果从WKWebView类开始分析源码,目前只知道早先可以通过NSUserDefaults设置UserAgent,所以要先确定导致该问题的改动大概是在什么时候提交的,然后再将源码切换到早于这个时候的提交,这样才有可能找到通过NSUserDefaults设置UserAgent的相关源码,再和最新的源码对比就能定位到改动的源码。

根据上篇文章的测试,极可能是在iOS 16.4发布(2023年3月27日发布)后,iOS 17发布(2023年9月18日发布)前改动的,所以先将源码切换到早于这个时间范围的提交应该是可行的。

不过呢,我喜欢先用UserAgent作为关键词搜索相关的历史提交记录,运气好的话能省很多事。在项目路径下执行命令:

git log --since=2023-03-27 --until=2023-09-18 --grep=UserAgent --pretty=format:'%C(auto)%h - %s (%ad)' --date=iso
  • --since--until:指定提交时间范围
  • --grep:指定提交说明需要包含的字符串
  • --pretty: 自定义输出格式。%C(auto)会自动为输出添加颜色,%h是简写哈希值,%s是提交说明,%ad是作者提交日期
  • --date: 指定日期显示格式

除了用命令搜索,还可以用其他一些可视化软件(例如Sourcetree等),体验会更好点。运气不错,很快就定位到了:

screenshot1

这里补充一点,由于--date参数我没找到更好的显示选项,所以图中显示的时间是UTC-0700时区的,换算成北京时间(UTC+0800)需要加上15小时。

根据提交说明里面的链接,找到这个:

We should remove the code in UserAgentIOS.mm that reads an override UA from the NSUserDefault [com.apple.WebFoundation UserAgent].
It is incompatible with the modern need to compose the UA from various bits of information these days (e.g. desktop vs. mobile). Clients should use the API to set the application name or UA instead.
I have stumbled upon one client (com.fark.hey), and there are likely others, so it should be a linked-on-or-after change.

简单概括就是:从NSUserDefault读取UA并覆盖的做法和现今的需求不符,需要删除这部分代码。

现在基本可以确定就是这个提交改动导致了当前的问题。用Xcode打开项目对比UserAgentIOS.mm文件提交,继续往后分析看看改了啥:

screenshot2

源码改动位置位于standardUserAgentWithApplicationName函数。

3. 对比源码

注意,以下关于源码的解释说明源于个人理解,仅供参考。

6a68b0c65d6c提交之前:

if (auto override = dynamic_cf_cast<CFStringRef>(adoptCF(CFPreferencesCopyAppValue(CFSTR("UserAgent"), CFSTR("com.apple.WebFoundation")))))
    return override.get();

这段源码的作用就是从偏好配置中获取UserAgent的值,并尝试将其动态转换为CFStringRef类型,如果转换成功(值不为空)便直接返回。

6a68b0c65d6c提交改动:

if (!linkedOnOrAfterSDKWithBehavior(SDKAlignedBehavior::DoesNotOverrideUAFromNSUserDefault)) {
    if (auto override = dynamic_cf_cast<CFStringRef>(adoptCF(CFPreferencesCopyAppValue(CFSTR("UserAgent"), CFSTR("com.apple.WebFoundation"))))) {
        static BOOL hasLoggedDeprecationWarning = NO;
        if (!hasLoggedDeprecationWarning) {
            NSLog(@"Reading an override UA from the NSUserDefault [com.apple.WebFoundation UserAgent]. This is incompatible with the modern need to compose the UA and clients should use the API to set the application name or UA instead.");
            hasLoggedDeprecationWarning = YES;
        }
        return override.get();
    }
}

相比改动前的源码,主要增加了linkedOnOrAfterSDKWithBehavior函数调用和过时警告日志输出。

先说说过时警告日志输出,这个逻辑很简单,因为hasLoggedDeprecationWarning是静态变量,所以只会在首次运行时输出一次。

再说说linkedOnOrAfterSDKWithBehavior函数,这个函数的作用是检查某个特定行为在当前应用程序链接的SDK版本上是否支持。函数入参DoesNotOverrideUAFromNSUserDefault这个特定行为,顾名思义,这行为就是不要从NSUserDefault覆盖UA

那应用程序链接的SDK版本是什么呢?

开发iOS App时都会基于某个版本的iOS SDK开发,通常是基于Xcode内置的iOS SDK(位于/Applications/Xcode.app/Contents/Developer/Platforms/iPhoneOS.platform/Developer/SDKs),这个版本就是链接的SDK版本。也就是说,如果你是用Xcode 15.1构建的应用程序,那么链接的SDK版本就是17.2;如果你是用Xcode 14.3.1构建的应用程序,那么链接的SDK版本就是16.4。更多关于Xcode内置的SDK版本信息,请看官方文档。

继续查看linkedOnOrAfterSDKWithBehavior的定义(位于WebKit项目/Source/WTF/wtf/cocoa/RuntimeApplicationChecksCocoa.cpp):

bool linkedOnOrAfterSDKWithBehavior(SDKAlignedBehavior behavior)
{
    return sdkAlignedBehaviors().get(static_cast<size_t>(behavior));
}

这里还看不出什么,继续往后面找,可以在computeSDKAlignedBehaviors函数中找到根据链接SDK版本禁用特定行为的源码(以下省略部分源码):

static SDKAlignedBehaviors computeSDKAlignedBehaviors()
{
    SDKAlignedBehaviors behaviors;
    behaviors.setAll();

    auto disableBehavior = [&] (SDKAlignedBehavior behavior) {
        behaviors.clear(static_cast<size_t>(behavior));
    };

    ...
    
    if (linkedBefore(dyld_fall_2023_os_versions, DYLD_IOS_VERSION_17_0, DYLD_MACOSX_VERSION_14_0)) {
        disableBehavior(SDKAlignedBehavior::FullySuspendsBackgroundContent);
        disableBehavior(SDKAlignedBehavior::RunningBoardThrottling);
        disableBehavior(SDKAlignedBehavior::PopoverAttributeEnabled);
        disableBehavior(SDKAlignedBehavior::LiveRangeSelectionEnabledForAllApps);
        disableBehavior(SDKAlignedBehavior::DoesNotOverrideUAFromNSUserDefault);
    }

    ...

    return behaviors;
}

linkedBefore函数的作用就是判断当前应用程序链接的SDK是否小于入参中的指定版本,如果小于则返回true

所以,现在可以明确知道当链接的SDK版本大等于iOS 17时,DoesNotOverrideUAFromNSUserDefault这个特定行为不会被禁用(是支持的),在这时判断条件!linkedOnOrAfterSDKWithBehavior(SDKAlignedBehavior::DoesNotOverrideUAFromNSUserDefault)将不成立,也就不会从NSUserDefault覆盖UA

分析到这,再总结一下基本结束了,但是总感觉少了些什么?在上篇文章中我的猜测是:

可能是WKWebView初始化时不再从NSUserDefaults获取默认值导致的问题

咦?前面都没分析WKWebView,没有完整串起来,是不是初始化时获取的都还不确定。所以,别急着总结,先翻翻WKWebView类源码(位于WebKit项目/Source/WebKit/UIProcess/API/Cocoa/WKWebView.mm)。

WKWebView类的初始化方法中发现了关于UserAgent的设置:

- (void)_initializeWithConfiguration:(WKWebViewConfiguration *)configuration
{
    ...

    _contentView = adoptNS([[WKContentView alloc] initWithFrame:self.bounds processPool:processPool configuration:pageConfiguration.copyRef() webView:self]);
    _page = [_contentView page];
    
    ...

    if (NSString *applicationNameForUserAgent = configuration.applicationNameForUserAgent)
        _page->setApplicationNameForUserAgent(applicationNameForUserAgent);
    
    ...
}

不过,这是从WKWebViewConfiguration配置中获取applicationNameForUserAgent并设置。等等!这属性名称有点熟悉,这一看就是和前面源码改动位置所在的standardUserAgentWithApplicationName函数有关。找到WKContentView类中的page方法:

- (WebKit::WebPageProxy*)page
{
    return _page.get();
}

继续找到WebPageProxy中的setApplicationNameForUserAgent函数:

void WebPageProxy::setApplicationNameForUserAgent(const String& applicationName)
{
    if (m_applicationNameForUserAgent == applicationName)
        return;

    m_applicationNameForUserAgent = applicationName;
    if (!m_customUserAgent.isEmpty())
        return;

    setUserAgent(standardUserAgent(m_applicationNameForUserAgent));
}

setUserAgent函数的作用是完成设置,而standardUserAgent函数的具体实现是区分平台的。继续找到WebPageProxyIOS.mm文件(位于WebKit项目/Source/WebKit/UIProcess/ios),standardUserAgent函数的具体实现如下:

String WebPageProxy::standardUserAgent(const String& applicationNameForUserAgent)
{
    return standardUserAgentWithApplicationName(applicationNameForUserAgent);
}

这里调用的standardUserAgentWithApplicationName函数在iOS平台上的具体实现就是前面UserAgentIOS.mm文件中的同名函数,现在一切就都串起来了,猜测是对的!

4. 分析总结

WebKit.framework是一个系统动态框架,已经内置在系统中,应用运行时加载。

  • 对于iOS 17以下的系统,WebKit.framework中还没有增加DoesNotOverrideUAFromNSUserDefault特定行为判断,不管是Xcode 15及以上还是15以下版本构建的应用,都还可以通过NSUserDefaults设置UserAgent
  • 对于iOS 17及以上的系统,如果是Xcode 15及以上版本构建的应用,由于链接的SDK版本已经支持DoesNotOverrideUAFromNSUserDefault特定行为,所以通过NSUserDefaults设置UserAgent会失效;如果是Xcode 15以下版本构建的应用,由于链接的SDK版本低于iOS 17,DoesNotOverrideUAFromNSUserDefault特定行为会被禁用,所以还可以通过NSUserDefaults设置UserAgent,同时会输出过时警告日志。
Xcode 15以下版本构建Xcode 15及以上版本构建
iOS 17以下版本
iOS 17及以上版本✅ + ⚠️

✅:通过NSUserDefaults设置UserAgent有效
❌:通过NSUserDefaults设置UserAgent无效
⚠️:有过时警告日志输出

解决方案

请看上篇文章中的解决方案。

如果你只需要设置一次,还可以参考后面补充内容里面的方法。

补充内容

1. UserAgent的组成

iOS 17.2模拟器获取的UserAgent

screenshot3

Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148

以上UserAgent的组成大家应该基本看得懂,所以不再一一赘述。让我感到困惑的是Mobile/15E148中的15E148,一开始以为是当前的系统版本,尝试更换其他版本的系统获取,结果这个一直没变。

可以通过Mozilla/5.0或其他关键词在WebKit项目中全局搜索,找到以上UserAgent生成相关的源码(位于UserAgentIOS.mm文件的standardUserAgentWithApplicationName函数):

String standardUserAgentWithApplicationName(const String& applicationName, const String& userAgentOSVersion, UserAgentType type)
{
    auto separator = applicationName.isEmpty() ? "" : " ";

    if (type == UserAgentType::Desktop)
        return makeString("Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko)", separator, applicationName);

#if USE(STATIC_IPAD_USER_AGENT_VALUE)
    UNUSED_PARAM(userAgentOSVersion);
    UNUSED_PARAM(separator);
    return makeString("Mozilla/5.0 (iPad; CPU OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/16.3 Mobile/15E148 Safari/604.1");
#else
    if (!linkedOnOrAfterSDKWithBehavior(SDKAlignedBehavior::DoesNotOverrideUAFromNSUserDefault)) {
        if (auto override = dynamic_cf_cast<CFStringRef>(adoptCF(CFPreferencesCopyAppValue(CFSTR("UserAgent"), CFSTR("com.apple.WebFoundation"))))) {
            static BOOL hasLoggedDeprecationWarning = NO;
            if (!hasLoggedDeprecationWarning) {
                NSLog(@"Reading an override UA from the NSUserDefault [com.apple.WebFoundation UserAgent]. This is incompatible with the modern need to compose the UA and clients should use the API to set the application name or UA instead.");
                hasLoggedDeprecationWarning = YES;
            }
            return override.get();
        }
    }

    auto osVersion = userAgentOSVersion.isEmpty() ? systemMarketingVersionForUserAgentString() : userAgentOSVersion;
    return makeString("Mozilla/5.0 (", deviceNameForUserAgent(), "; CPU ", osNameForUserAgent(), " ", osVersion, " like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko)", separator, applicationName);
#endif
}

在前面的问题分析中已经分析了该函数关于当前问题的源码,再剔除其他平台相关的源码,可见最后的return语句是UserAgent生成的关键。和前面获取的UserAgent一一对应:

  • deviceNameForUserAgent()对应iPhone,该函数在当前文件有定义
  • osNameForUserAgent()对应iPhone OS,该函数在当前文件有定义
  • osVersion对应17_2
  • applicationName对应Mobile/15E148。当applicationName为空时,分隔符separator为空字符串

由于applicationName是函数入参,所以现在需要知道函数是在哪调用的。根据前面问题分析可知,standardUserAgentWithApplicationName函数是在WKWebView类初始化时调用的,applicationName入参的值来自WKWebViewConfiguration类中的applicationNameForUserAgent方法(位于WebKit项目/Source/WebKit/UIProcess/API/Cocoa/WKWebViewConfiguration.mm):

std::optional<RetainPtr<NSString>> _applicationNameForUserAgent;

...

static NSString *defaultApplicationNameForUserAgent()
{
#if PLATFORM(IOS_FAMILY)
    return @"Mobile/15E148";
#else
    return nil;
#endif
}

...

- (NSString *)applicationNameForUserAgent
{
    return _applicationNameForUserAgent.value_or(defaultApplicationNameForUserAgent()).get();
}

value_orC++std::optional类型的函数,该函数的作用就是如果_applicationNameForUserAgent为空则返回提供的默认值。

由此可见,Mobile/15E148竟然是硬编码的!如果在初始化WKWebView对象时,没有通过WKWebViewConfiguration对象设置applicationNameForUserAgent属性,那么将使用Mobile/15E148作为默认值返回。

那这个15E148有什么含义呢?在Xcode中选中return @"Mobile/15E148";这一行,然后右键选择[Show Last Change for Line],查看关于这行源码的上次改动信息:

screenshot4

We noticed that we get the following default UA from a WKWebView on iOS using a sample app in the iPhone 6 simulator:

Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/16A342

The default WKWebView UA should have a frozen OS build number, as we do in Safari.

Mobile/16A342 -> Mobile/15E148.

上面这段话来自192809 - WKWebView default UA doesn’t freeze the build number页面。简单概括就是WKWebViewUA应该和Safari一样,固定一个系统构建版本号。

再具体看看源码改动对比:

screenshot5

从改动对比可知,原先是通过[UIDevice currentDevice].buildVersion动态获取系统构建版本号,后续才固定为15E148这个版本号。这里有几个疑问:

  1. 找不到buildVersion方法?

通常应用开发者只能通过systemVersion获取系统版本(例如17.2),buildVersion是私有API,更多详情可以参考UIKitSPI.h文件(SPISystem Programming Interface,系统级编程接口),如果找不到,可能是当前提交已经移除了相关源码,请切换到更早的提交。

  1. 15E148具体是哪个系统版本的构建版本号?

官方文档中没找到系统构建版本号相关的列表,不过找到了这个网站Xcode Releases。由于这是以Xcode版本为主的网站,所以iOS相关的版本数据也不是很全,例如在Xcode可以看到iOS 17.0.1的构建版本号是21A342

screenshot6

这个版本在该网站找不到,不过好在这个构建版本号是递增的,最终可以明确15E148对应的系统版本介于iOS 11.2(15C107)和iOS 11.3(15E217)之间。

  1. 为什么要固定系统构建版本号?

我没有找到官方说明或解释,以下个人猜测仅供参考:

iOS 11和iPhone X同一年发布,iPhone X带来刘海屏的同时,iOS 11也带来了新的网页特性用于适配安全区域,再往后感觉也没有什么新的特性需要网页适配。那么,为了方便开发者根据版本对网页进行适配,将UA中的系统构建版本号固定为15E148倒也合理。

2. UserAgent的设置优先级

UserAgent设置的三种方式:

  • NSUserDefaults方式:在WKWebView对象初始化前,通过registerDefaults方法设置,一次设置全局生效,已过时
  • applicationNameForUserAgent方式:在WKWebView对象初始化时,通过WKWebViewConfiguration对象设置,后续再修改无效
  • customUserAgent方式:在WKWebView对象初始化后,直接赋值设置,支持动态修改

如果三种方式同时设置,结合之前的分析,可知优先级如下:

customUserAgent方式 > NSUserDefaults方式 > applicationNameForUserAgent方式

现阶段NSUserDefaults方式已过时,如果你只需要设置一次,那么建议通过applicationNameForUserAgent方式设置。不过,这里需要注意一点,请不要像下面例子般直接赋值:

let webViewConfiguration = WKWebViewConfiguration()
webViewConfiguration.applicationNameForUserAgent = "App/1.0.0"
webView = WKWebView(frame: view.bounds, configuration: webViewConfiguration)

这样会出现默认值Mobile/15E148被替换的情况(原因请看前面的分析):

Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) App/1.0.0

UA中缺失Mobile/15E148会导致一些网页无法正常适配,特别是Mobile这部分,很多网页会根据这个判断是不是属于移动端。所以,可以参考下面的例子先获取默认值拼接成新的再设置:

let webViewConfiguration = WKWebViewConfiguration()
webViewConfiguration.applicationNameForUserAgent = "\(webViewConfiguration.applicationNameForUserAgent ?? "") App/1.0.0"
webView = WKWebView(frame: view.bounds, configuration: webViewConfiguration)

设置后效果:

Mozilla/5.0 (iPhone; CPU iPhone OS 17_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 App/1.0.0

最后

如果这篇文章对你有所帮助,点赞👍收藏🌟支持一下吧,谢谢~


本篇文章由@crasowas发布于CSDN。

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

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

相关文章

十四:爬虫-Redis基础

1、背景 随着互联网大数据时代的来临&#xff0c;传统的关系型数据库已经不能满足中大型网站日益增长的访问量和数据量。这个时候就需要一种能够快速存取数据的组件来缓解数据库服务I/O的压力&#xff0c;来解决系统性能上的瓶颈。 2、redis是什么 Redis 全称 Remote Dictio…

C/C++面向对象(OOP)编程-回调函数详解(回调函数、C/C++异步回调、函数指针)

本文主要介绍回调函数的使用&#xff0c;包括函数指针、异步回调编程、主要通过详细的例子来指导在异步编程和事件编程中如何使用回调函数来实现。 &#x1f3ac;个人简介&#xff1a;一个全栈工程师的升级之路&#xff01; &#x1f4cb;个人专栏&#xff1a;C/C精进之路 &…

【Spring实战】16 Profile

文章目录 1. 定义2. 使用2.1 定义 Profile2.2 激活 Profile 3. 演示3.1 properties文件3.2 打印日志3.3 启动服务&验证3.4 修改 active3.5 重启服务&验证 4. 应用场景4.1 数据库配置4.2 日志配置 5. 代码详细总结 Spring 框架提供了一种强大的机制&#xff0c;允许在不…

图像分割实战-系列教程9:U2NET显著性检测实战1

&#x1f341;&#x1f341;&#x1f341;图像分割实战-系列教程 总目录 有任何问题欢迎在下面留言 本篇文章的代码运行界面均在Pycharm中进行 本篇文章配套的代码资源已经上传 U2NET显著性检测实战1 1、任务概述

第7课 利用FFmpeg将摄像头画面与麦克风数据合成后推送到rtmp服务器

上节课我们已经拿到了摄像头数据和麦克风数据&#xff0c;这节课我们来看一下如何将二者合并起来推送到rtmp服务器。推送音视频合成流到rtmp服务器地址的流程如下&#xff1a; 1.创建输出流 //初始化输出流上下文 avformat_alloc_output_context2(&outFormatCtx, NULL, &…

Java EE Servlet之Cookie 和 Session

文章目录 1. Cookie 和 Session1.1 Cookie1.2 理解会话机制 (Session)1.2.1 核心方法 2. 用户登录2.1 准备工作2.2 登录页面2.3 写一个 Servlet 处理上述登录请求2.4 实现登录后的主页 3. 总结 1. Cookie 和 Session 1.1 Cookie cookie 是 http 请求 header 中的一个属性 浏…

AI 工具探索(二)

我参加了 奇想星球 与 Datawhale 举办的 【AI办公 X 财务】第一期&#xff0c;现在这是第二次打卡&#xff0c;也即自由探索&#xff0c;我选择 Modelscope 的 Agent 探索&#xff0c;并用gpts创作助理对比&#xff01; 最近想学学小红书的运营方法&#xff0c;选择了 小红书I…

【微服务】1.虚拟机配置

创建虚拟机选经典&#xff0c;其他配置同其他讲解文档 特殊注意 如果要自己设置IP地址&#xff0c;修改/etc/sysconfig/network-scripts/ 编辑ifcfg-ens33需改ip地址 #开机加载网络配置启动网络服务 ONBOOT"yes" #分配ip的协议 none static :不自动分配&#xff0c…

axios的使用及说明

目录 1.说明 2.直接使用 3.封装使用 4.注意 1.说明 官网&#xff1a;Axios 实例 | Axios中文文档 | Axios中文网 Axios 是一个基于 promise 网络请求库&#xff0c;作用于node.js 和浏览器中。 它是 isomorphic 的(即同一套代码可以运行在浏览器和node.js中)。在服务端它使…

FL Studio 21最新版本for mac 21.2.2.3740中文解锁版2024最新图文安装教程

FL Studio 21最新版本for mac 21.2.0.3740中文解锁版是最新强大的音乐制作工具。它可以与所有类型的音乐一起创作出令人惊叹的音乐。它提供了一个非常简单且用户友好的集成开发环境&#xff08;IDE&#xff09;来工作。这个完整的音乐工作站是由比利时公司 Image-Line 开发的。…

redis容灾的方案设计

背景 今年各个大厂的机房事故频繁&#xff0c;其中关键组件Redis是重灾区&#xff0c;本文就来看下怎么做Redis的多机房容灾 Redis多机房容灾方案 1.首先最最直观的是直接利用Redis内部的主从数据同步来进行灾备&#xff0c;但是由于Redis内部的主从实现对机房间的网络延迟等…

2024 React 后台系统 搭建学习看这一篇就够了(1)

年初&#xff0c;自己想写一篇关于 React 实战后台项目的 课程文章&#xff0c;也算是对自己 2023的前端学习做一个系统性总结&#xff0c;方便后续查阅&#xff0c;也方便自己浏览&#xff0c;还能增加自己的文笔 网上很多平台都不太稳定&#xff0c;所以用了阿里的语雀&…

声明式导航传参详情

1 动态路由传参 路由规则path ->/article/:aid 导航链接 <router-link to"/article/1">查看第一篇文章</router-link> 组件获取参数: this.$route.params.aid 如果想要所有的值&#xff0c;就用this. $route. params 注意&#xff1a;这两个必须匹配…

实战入门 K8s剩下三个模块

1.Label Label是kubernetes系统中的一个重要概念。它的作用就是在资源上添加标识&#xff0c;用来对它们进行区分和选择。 Label的特点&#xff1a; 一个Label会以key/value键值对的形式附加到各种对象上&#xff0c;如Node、Pod、Service等等 一个资源对象可以定义任意数量…

信创之国产浪潮电脑+统信UOS Linux操作系统体验10:visual studio code中调试C++程序

☞ ░ 前往老猿Python博客 ░ https://blog.csdn.net/LaoYuanPython 一、引言 老猿在CSDN的《信创之国产浪潮电脑统信UOS操作系统体验2&#xff1a;安装visual studio code和cmake搭建C开发环镜》介绍了在国产浪潮电脑统信UOS操作系统中安装visual studio code和cmake搭建C开…

2.3物理层下面的传输媒体

目录 2.3物理层下面的传输媒体2.3.1导引型传输媒体1.双绞线2.同轴电缆3.光纤 2.3.2非导引型传输媒体无线电微波通信 2.3物理层下面的传输媒体 传输媒体是数据传输系统中在发送器和接收器之间的物理通路 两大类&#xff1a; 导引型传输媒体&#xff1a;电磁波被导引沿着固体媒体…

linux下docker搭建Prometheus +SNMP Exporter +Grafana进行核心路由器交换机监控

一、安装 Docker 和 Docker Compose https://docs.docker.com/get-docker/ # 安装 Docker sudo apt-get update sudo apt-get install -y docker.io# 安装 Docker Compose sudo apt-get install -y docker-compose二、创建配置文件及测试平台是否正常 1、选个文件夹作为自建…

jenkins+pytest+allure

jenkinspytestallure allure下载地址 Releases allure-framework/allure2 GitHub allure环境变量配置 allure --version 查看版本(确定是否配置完成) python安装allure插件 pip install allure-pytest pytest的运行指令 pytest -sv test_demo.py 开发完毕后将代码上传到…

【Unity自制手册】基于Unity中物体移动相关方法和API集锦(动图详解)

&#x1f468;‍&#x1f4bb;个人主页&#xff1a;元宇宙-秩沅 &#x1f468;‍&#x1f4bb; hallo 欢迎 点赞&#x1f44d; 收藏⭐ 留言&#x1f4dd; 加关注✅! &#x1f468;‍&#x1f4bb; 本文由 秩沅 原创 &#x1f468;‍&#x1f4bb; 收录于专栏&#xff1a;uni…

关于python解析mf4中二维信号数据的注意事项

python解析mf4中的信号数据一般用np.ndarray存储&#xff0c;但是mf4中的一个信号有时不一定是一维数据&#xff0c;有时会是一个二维的&#xff0c;没错&#xff0c;就是一个信号数据就是二维的&#xff0c;这时候&#xff0c;np数组的每个元素也是一个数组&#xff0c;这个时…