iOS--NSURLSession Alamofire流程源码解析(万字详解版)

一、NSURLSession

NSURLSession的主要功能是发起网络请求获取网络数据,是Apple的网络请求原生库之一。Alamofire就是对NSURLSession的封装,如果对NSURLSession不熟悉的话,那么Alamofire源码看起来会比较费劲的。因此我们先简单学习下NSURLSession。

从NSURLSession这个名字中我们不难看出,主要是URL + Session。顾名思义,NSURLSession是用来URL会话的。iOS的NSURLSession的主要功能是通过URL与服务器交流的

一句话总结:我们的iOS客户端可以使用NSURLSession这个东西通过相应的URL与我们的服务器建立会话,然后通过此会话来完成一些交互任务(NSURLSessionTask)。Session有着不同的类型,每种类型的Session又可以执行不同类型的任务(Task)。接下来就来介绍一下Session的类型以及所执行的任务等。

1.NSURLSession的类型

在使用NSURLSession时你得知道你使用的是哪种类型的Session。从官方的NSURLSession API中不难看出,共有三种类型的Session:Default sessions,Ephemeral sessions,Background sessions这三种Session我们可以通过NSURLSessionConfiguration来指定。

  • 默认会话(Default Sessions)使用了持久的磁盘缓存,并且将证书存入用户的钥匙串中。它的特点是使用系统的缓存和凭证存储机制,适合处理需要缓存数据或保持会话状态的网络请求。
  • 临时会话(Ephemeral Session)没有像磁盘中存入任何数据,与该会话相关的证书、缓存等都会存在RAM中。因此当你的App临时会话无效时,证书以及缓存等数据就会被清除掉。因此,它非常适合那些对隐私要求较高的场景。
  • 后台会话(Background sessions)除了使用一个单独的线程来处理会话之外,与默认会话类似。适合处理需要长时间执行的任务,如文件下载和上传。此类会话通常会在后台传输文件,即使应用程序被挂起或终止,网络任务仍能继续执行,且完成后会通知应用。

Session在初始化时可以指定下方的任意一种SessionConfiguration:

let defaultSession = URLSession(configuration: .default)

let ephemeralSession = URLSession(configuration: .ephemeral)

let backgroundSession = URLSession(configuration: .background(withIdentifier: ".background"))

2. NSURLSession的各种任务

在一个Session会话中可以发起的任务可分为三种:数据任务(Data Task)、下载任务(Download Task)、上传任务(Upload Task)。在iOS8和OS X 10.10之前的版本中后台会话是不支持Data Task。下面来简述一下这三种任务。

  • Data Task(数据任务)负责使用NSData对象来发送和接收数据。Data Task是为了那些简短的并且经常从服务器请求的数据而准备的。该任务可以每请求一次就对返回的数据进行一次处理。
  • Download task(下载任务)以表单的形式接收一个文件的数据,该任务支持后台下载。
  • Upload task(上传任务)以表单的形式上传一个文件的数据,该任务同样支持后台下载。

3、URL

(1)URL编码概述

无论是GET、POST还是其他的请求,与服务器交互的URL是需要进行编码的。因为进行URL编码的参数服务器那边才能进行解析,为了能和服务器正常的交互,我们需要对我们的参数进行转义和编码。URL就是互联网上资源的地址,用户和服务器都可以通过URL来找到其想访问的资源。RFC3986文档规定,URL中只允许包含英文字母(a-zA-Z)、数字(0-9)、“-_.~”4个特殊字符以及所有保留字符,如果你的URL中含有汉字,那么就需要对其进行转码了。RFC3986中指定了以下字符为保留字符:! * ' ( ) ; : @ & = + $ , / ? # [ ]。

(2)URL编码的规则

在URL编码时有一定的规则,如下图是我们今天主要使用的URL格式的一个规则的一个图解:

需要注意的是图中Query的部分。其中,Path和Query之间使用的是?号进行分隔的,问号后边就是我们要传给服务器的参数了,该参数就是下方的Query的部分在GET请求中Query是存放在URL后边,而在POST中是放在Request的Body中

如果你的参数只是一个key-Value, 那么Query的形式就是key = value。如果你的参数是一个数组比如key = [itme1, item2, item3,……],那么你的Query的格式就是key[]=item1&key[itme2]&key[item3]……。如果你的参数是一个字典比如key = ["subKey1":"item1", "subKey2":"item2"], 那么Query对应的形式就是key[subKey1]=item1&key[subKey2]=item2.

接下来我们要做的就是将字典进行URL编码。

(3)将Dictionary进行URL编码

在iOS开发中,有时候我们从VC层或者VM层获取到的数据是一个字典,字典中存储的就是要发给服务器的数据参数。直接将字典转成二进制数据发送给服务器,服务器那边是没法解析iOS这边的字典的,得有一个统一的交互标准,这个标准就是URL编码。我们要做的就是讲字典进行URL编码,然后将编码后的东西在传给服务器,这样一来服务器那边就能解析到我们请求的参数了。下方折叠的这段代码就是从AlamoFire框架中摘抄出来的几个方法,位于ParameterEncoding.swift文件中。该段代码就是负责将字典类型的参数进行URL编码的,在编码过程中进行转义是少不了的。

import Foundation

//MARK: Alamofire中的三个方法该方法将字典转换成URL编码的字符
/*
 用于将键值对转换为 URL 查询参数的形式。它递归地处理嵌套的字典和数组,
 并根据键和值生成合适的查询组件(键值对)。
 这些查询组件通常用于将复杂的字典或数组编码成 HTTP 请求 URL 中的查询字符串参数。
 */

private func query(_ parameters: [String: Any]) -> String {
    var components: [(String, String)] = []

    for key in parameters.keys.sorted(by: <) {
        let value = parameters[key]!
        components += queryComponents(fromKey: key, value: value)
    }
    return components.map { "\($0)=\($1)" }.joined(separator: "&")
}


public func queryComponents(fromKey key: String, value: Any) -> [(String, String)] {
    var components: [(String, String)] = []
//如果 value 是一个字典类型 [String: Any],这里会将字典的键用方括号括起来进行拼接,比如如果传入的 key 是 "user",而 nestedKey 是 "name",那么组合后的键会变为 "user[name]",从而表示字典的嵌套结构。
    if let dictionary = value as? [String: Any] {
        for (nestedKey, value) in dictionary {
            components += queryComponents(fromKey: "\(key)[\(nestedKey)]", value: value)
        }
    } //如果 value 是数组类型 [Any],代码会遍历数组中的每个值。
    else if let array = value as? [Any] {
        for value in array {
            components += queryComponents(fromKey: arrayEncoding.encode(key: key), value: value)
        }
    } //如果 value 是 NSNumber,首先判断它是否是布尔类型。
    else if let value = value as? NSNumber {
        if value.isBool {//如果是布尔值,使用 boolEncoding.encode 方法来处理布尔值的编码(通常布尔值会被转换为 "true" 或 "false" 字符串)。
            components.append((escape(key), escape(boolEncoding.encode(value: value.boolValue))))
        } else {//如果是数字类型,则直接将其转为字符串并添加到 components 数组中。
            components.append((escape(key), escape("\(value)")))
        }
    } //如果 value 是布尔类型 Bool,直接使用 boolEncoding.encode 方法来进行编码,然后将结果添加到 components 数组中。
    else if let bool = value as? Bool {
        components.append((escape(key), escape(boolEncoding.encode(value: bool))))
    } else {//如果 value 是其他任何类型(如字符串等),直接将 value 转换为字符串并添加到 components 中。
        components.append((escape(key), escape("\(value)")))
    }

    return components
}

这个方法通过递归地处理字典、数组、布尔值和数字,将它们转换成一对对的查询参数。它能够处理复杂的嵌套数据结构,并将它们编码成 URL 查询字符串所需的形式。之所以进行递归,因为字典中有可能含有字典或者数组,数组中又可能嵌套着数组或者字典。所以要进行递归,直到找到key=value这种形式为止。

调用上述代码段的query()方法就可以对字典进行转义。query()方法的参数是一个[String, AnyObject]类型的字典,返回参数是一个字符串。这个返回的字符串就是将该字典进行编码后的结果。

总的来讲,就是与服务器交互发送的URL是需要编码的,而编码是需要遵从某种规则,这种规则在Alamofire中是以query()和queryComponents()方法中进行的,并且规则的本质上就是将键值对转换成一个字符串。

tips:关于数据的传输:

  • GET 请求 将数据作为查询参数附加在 URL 中发送。
  • POST 请求 将数据放在 请求体 中,并通过 Content-Type 告知服务器如何解析数据(例如:json)。

4.URLSessionDelegate 代理机制

URLSessionDelegate 代理机制是 NSURLSession 的一部分,用于处理与网络请求相关的各种事件。

NSURLSession用法:

  1.GET--从服务器请求数据

import Foundation
//MARK: GET--从服务器请求数据
let myURLString = "https://httpbin.org/get"
let url = URL(string: myURLString)!
let urlRequest = URLRequest(url: url)
let session = URLSession.shared

// 创建 dataTask
let task = session.dataTask(with: urlRequest) { data, response, error in
    // 检查是否有错误
    if let error = error {
        print("Error: \(error.localizedDescription)")
        return
    }
    
    // 检查响应是否为 HTTP 响应
    if let httpResponse = response as? HTTPURLResponse {
        print("Status code: \(httpResponse.statusCode)")
    }
    
    // 确保有数据返回
    guard let data = data else {
        print("No data received")
        return
    }
    
    // 尝试将数据转换为 JSON 格式
    do {
        if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
            print("JSON response: \(json)")
        }
    } catch {
        print("Failed to serialize JSON: \(error.localizedDescription)")
    }
}

// 启动任务
task.resume()

在GET样例代码中,可以看到,首先我们创建了一个URL,然后构建了URLRequest对象,接着通过 URLSession.shared 创建一个共享的会话(session),这样可以复用会话中的缓存、Cookie 等信息。紧接着使用 URLSession 的 dataTask(with:) 方法创建一个任务,并为其指定回调闭包,用于处理请求完成后的响应和数据。最后,我们将数据转成JSON格式。

  2.POST--向服务器发送数据

import Foundation

let myURLString = "https://httpbin.org/post"
let url = URL(string: myURLString)!
var urlRequest = URLRequest(url: url)
urlRequest.httpMethod = "POST"
//设置请求头(Header)--“application/json” -> 告诉服务器我们将以 JSON 格式发送数据
urlRequest.setValue("application/json", forHTTPHeaderField: "Content-Type")

//构建请求体(Parameters)--要发送的数据
let parameters: [String: Any] = [
    "name": "John",
    "age": 30
]

// 将请求体 转换为 JSON 格式 -- 使用 JSONSerialization 将字典序列化为 Data 对象(即 JSON 格式的二进制数据)
do {
    let jsonData = try JSONSerialization.data(withJSONObject: parameters, options: [])
    urlRequest.httpBody = jsonData
} catch {
    print("Error in JSON serialization: \(error.localizedDescription)")
}

// 创建 URLSession 并发送请求
let session = URLSession.shared
let task = session.dataTask(with: urlRequest) { data, response, error in
    // 检查是否有错误
    if let error = error {
        print("Error: \(error.localizedDescription)")
        return
    }

    // 检查响应是否为 HTTP 响应
    if let httpResponse = response as? HTTPURLResponse {
        print("Status code: \(httpResponse.statusCode)")
    }

    // 确保有数据返回
    guard let data = data else {
        print("No data received")
        return
    }

    // 尝试将数据转换为 JSON 格式
    do {
        if let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] {
            print("Response JSON: \(json)")
        }
    } catch {
        print("Failed to serialize JSON: \(error.localizedDescription)")
    }
}

// 启动任务
task.resume()

在POST样例代码中,可以看到,我们需要发送请求给服务器,请求主要分为请求头、请求体:请求头是告诉服务器如何处理请求(告诉服务器应该如何解析数据),以及携带关于客户端的环境、数据格式、认证等内容、请求体是实际要通过 HTTP 请求传递给服务器的数据。

其余与GET大差不差。。。

基本流程:

创建一个URLRequest 对象,URLRequest 对象描述了发送到服务器的 HTTP 请求,包括 URL、请求体、请求头等信息。然后通过 NSURLSession 使用 dataTask 将 URLRequest 发送到服务器。

二、Alamofire

博客中的Alamofire源码的版本是以5.6.4版本为例。其平台较少,不像最新版本新添了visionOS等一系列新平台,框架较大不容易阅读。

在上述NSURLSession的学习中我们大概了解了NSURLSession是如何工作的:先建立URLRequest, URLSession, 接着创建 dataTask ,并调用方法resume() 去唤醒. 结束以后通过回调dataTask 告诉我们结果。当然最后需要通过序列化JSONSerialization 把二进制Data转换为JSON,或者处理错误errors。

Alamofire实际上是封装了URLSession,所以Alamofire 也会创建dataTask,然后调用方法.resume() 去唤醒执行. 接下来就进一步揭露Alamofire 的神秘面纱。

我们从Alamofire的用法反推其具体流程。

以下是我在项目中Alamofire的一种用法:

AF.request("https://nlp.aliyuncs.com/v2/api/chat/send", 
method: .post, 
parameters: parameters, 
encoding: JSONEncoding.default, 
headers: headers).response 
{ response in

}

我们看底层封装代码:

    open func request(_ convertible: URLConvertible,
                      method: HTTPMethod = .get,
                      parameters: Parameters? = nil,
                      encoding: ParameterEncoding = URLEncoding.default,
                      headers: HTTPHeaders? = nil,
                      interceptor: RequestInterceptor? = nil,
                      requestModifier: RequestModifier? = nil) -> DataRequest {
        let convertible = RequestConvertible(url: convertible,
                                             method: method,
                                             parameters: parameters,
                                             encoding: encoding,
                                             headers: headers,
                                             requestModifier: requestModifier)

        return request(convertible, interceptor: interceptor)
    }

方法参数:

  • convertible: URLConvertible :请求的 URL,可以是 String 或 URL 类型,Alamofire 会将其转换为 URL。
  • method: HTTPMethod = .get :HTTP 请求方法,默认为 GET,可设为 POSTPUT 等。
  • parameters: Parameters? = nil :请求体、请求的参数(字典 [String: Any]),用于发送数据。
  • encoding: ParameterEncoding = URLEncoding.default :参数的编码方式,常见有:

    URLEncoding.default :GET 请求将参数编码为 URL 查询字符串。

    JSONEncoding.default :POST 请求会将参数作为请求体的 JSON 数据发送。

  • headers: HTTPHeaders? = nil :自定义请求头,可传入 HTTPHeaders 类型对象。
  • interceptor: RequestInterceptor? = nil :拦截器,用于修改请求或处理重试策略等。
  • requestModifier: RequestModifier? = nil :请求修改器,在请求创建后进一步调整请求。

此外,可以看到方法中构建了一个RequestConvertible对象,这是一个结构体对象,是 Alamofire 内部的一个工具类,用于将 URL、请求方法、参数、编码等组合成 URLRequest

让我们看看RequestConvertible的源码:

    struct RequestConvertible: URLRequestConvertible {
        let url: URLConvertible
        let method: HTTPMethod
        let parameters: Parameters?
        let encoding: ParameterEncoding
        let headers: HTTPHeaders?
        let requestModifier: RequestModifier?

        func asURLRequest() throws -> URLRequest {
            var request = try URLRequest(url: url, method: method, headers: headers)
            try requestModifier?(&request)

            return try encoding.encode(request, with: parameters)
        }
    }

我们重点关注下asURLRequest()方法:

 var request = try URLRequest(url: url, method: method, headers: headers)

根据传入的 URL、HTTP 方法、和请求头来创建一个基础的 URLRequest 对象(可以看成一个数据包,描述了发送到服务器的 HTTP 请求,包括 URL、请求体、请求头等信息)。

return try encoding.encode(request, with: parameters)

encoding.encode:根据指定的 ParameterEncoding(如 URLEncoding 或 JSONEncoding),将请求参数编码到 URLRequest 中。

GET 请求:参数通常会被编码为 URL 查询字符串(附加在 URL 后面)。

POST 请求:参数通常会被编码为请求体中的 JSON。

• 最终返回经过编码的 URLRequest,这是发送 HTTP 请求的最终形态。

由此可见,RequestConvertible的作用是将 Alamofire 的网络请求参数(包括 URL、方法、参数、编码、请求头等)打包并生成 URLRequest

接着我们返回到AF.request()方法中,我们再来深入了解下返回值:

return request(convertible, interceptor: interceptor)

底层实现:

    open func request(_ convertible: URLRequestConvertible, interceptor: RequestInterceptor? = nil) -> DataRequest {
        let request = DataRequest(convertible: convertible,
                                  underlyingQueue: rootQueue,
                                  serializationQueue: serializationQueue,
                                  eventMonitor: eventMonitor,
                                  interceptor: interceptor,
                                  delegate: self)

        perform(request)

        return request
    }

其中convertible就是我们上面了解的RequestConvertible,也就是说,这里参数输入的是一个URLRequest对象,另外一个参数interceptor: RequestInterceptor?是请求拦截器,允许在请求执行之前或之后进行额外的操作,如重试请求、修改请求等。它是一个可选参数,默认值为 nil。

此外,DataRequest是 Alamofire 用于管理网络请求的核心对象。它继承自 Request,封装了请求的生命周期管理、进度跟踪、响应解析等功能。其底层是一些初始化,用于赋值,这里就不多赘述。

我们重点了解下perform(request)的底层代码:

var activeRequests: Set<Request> = []   
public let rootQueue: DispatchQueue


func perform(_ request: Request) {
        rootQueue.async {
            guard !request.isCancelled else { return }

            self.activeRequests.insert(request)//将当前请求加入到 activeRequests 集合中,这个集合存储所有正在执行的请求,便于管理和追踪。

            self.requestQueue.async {
       
                switch request {
                case let r as UploadRequest: self.performUploadRequest(r) // UploadRequest must come before DataRequest due to subtype relationship.
                case let r as DataRequest: self.performDataRequest(r)
                case let r as DownloadRequest: self.performDownloadRequest(r)
                case let r as DataStreamRequest: self.performDataStreamRequest(r)
                default: fatalError("Attempted to perform unsupported Request subclass: \(type(of: request))")
                }
            }
        }
    }

perform(_ request: Request) 方法接收一个 Request 对象,并根据具体的请求类型(DataRequest、UploadRequest、DownloadRequest 等)选择合适的执行逻辑来处理该请求

我们看下switch这条语句,可以看到Alamofire根据 request 的类型进行分派,选择合适的处理方法。其中request 的类型有:

  • UploadRequest:执行上传请求逻辑,调用 performUploadRequest(_:)。
  • DataRequest:执行普通的数据请求,调用 performDataRequest(_:),这是最常用的请求类型。
  • DownloadRequest:执行下载请求逻辑,调用 performDownloadRequest(_:)。
  • DataStreamRequest:处理数据流请求,调用 performDataStreamRequest(_:)。
  • default:如果遇到不支持的请求类型,直接抛出致命错误。

其中performUploadRequestperformDataRequest这些方法都是由performSetupOperations()这个方法实现的。

我们来看下performSetupOperations()的底层实现:

    func performSetupOperations(for request: Request,
                                convertible: URLRequestConvertible,
                                shouldCreateTask: @escaping () -> Bool = { true }) {
        dispatchPrecondition(condition: .onQueue(requestQueue))//确保请求操作被安全地调度到指定队列上。

        let initialRequest: URLRequest

        do {
            initialRequest = try convertible.asURLRequest()//将 URLRequestConvertible 转换为实际的 URLRequest。
            try initialRequest.validate()//验证生成的 URLRequest,检查 URL 是否合法、方法是否合理等。
        } catch {
            rootQueue.async { request.didFailToCreateURLRequest(with: error.asAFError(or: .createURLRequestFailed(error: error))) }
            return
        }

        rootQueue.async { request.didCreateInitialURLRequest(initialRequest) }//该回调通知请求对象,已经成功创建了初始的 URLRequest,为后续执行做好准备。

        guard !request.isCancelled else { return }

        guard let adapter = adapter(for: request) else {
            guard shouldCreateTask() else { return }
            rootQueue.async { self.didCreateURLRequest(initialRequest, for: request) }
            return
        }

        let adapterState = RequestAdapterState(requestID: request.id, session: self)

        adapter.adapt(initialRequest, using: adapterState) { result in
            do {
                let adaptedRequest = try result.get()
                try adaptedRequest.validate()

                self.rootQueue.async { request.didAdaptInitialRequest(initialRequest, to: adaptedRequest) }

                guard shouldCreateTask() else { return }

                self.rootQueue.async { self.didCreateURLRequest(adaptedRequest, for: request) }
            } catch {
                self.rootQueue.async { request.didFailToAdaptURLRequest(initialRequest, withError: .requestAdaptationFailed(error: error)) }
            }
        }
    }

这个方法的主要作用是设置网络请求的初始配置,包括将传入的 URLRequestConvertible 转换为 URLRequest,进行请求验证,适配器操作(如果有),并最终为网络请求创建 URLSessionTask。该方法对请求生命周期中的准备阶段做了全面的处理。

我们重点看下这一段代码:

guard let adapter = adapter(for: request) else {
    guard shouldCreateTask() else { return }
    rootQueue.async { self.didCreateURLRequest(initialRequest, for: request) }
    return
}

adapter(for: request):检查是否为请求配置了 RequestAdapter(适配器),适配器允许在发出请求前对 URLRequest 进行修改(如添加认证信息、修改 URL 等)。

如果没有的话,则执行request的请求,也就是self.didCreateURLRequest(initialRequest, for: request)

self.didCreateURLRequest(initialRequest, for: request)的代码实现:

    func didCreateURLRequest(_ urlRequest: URLRequest, for request: Request) {
        dispatchPrecondition(condition: .onQueue(rootQueue))

        request.didCreateURLRequest(urlRequest)

        guard !request.isCancelled else { return }

        let task = request.task(for: urlRequest, using: session)
        requestTaskMap[request] = task
        request.didCreateTask(task)

        updateStatesForTask(task, request: request)
    }



    func updateStatesForTask(_ task: URLSessionTask, request: Request) {
        dispatchPrecondition(condition: .onQueue(rootQueue))

        request.withState { state in
            switch state {
            case .initialized, .finished:
                // Do nothing.
                break
            case .resumed:
                task.resume()
                rootQueue.async { request.didResumeTask(task) }
            case .suspended:
                task.suspend()
                rootQueue.async { request.didSuspendTask(task) }
            case .cancelled:
                // Resume to ensure metrics are gathered.
                task.resume()
                task.cancel()
                rootQueue.async { request.didCancelTask(task) }
            }
        }
    }

看到这里是不是就很熟悉了,没错这里就是NSURLSession中的URLSessionTask(NSURLSession的各种任务)。上述方法创建RequestTask,最后调用task.resume()。其实本质上和NSURLSession一样,只不过是封装了而已。

小结:

1. Alamofire 概述:

• Alamofire 是对 NSURLSession 的封装,简化了网络请求的处理流程,提供更便捷的 API。

• 主要流程包括创建 URLRequest、发起请求、处理响应等,内部依然使用 URLSession 和 URLSessionTask。

2. 核心步骤:

AF.request() 方法是入口,参数包括 URL、请求方法(如 GET/POST)、请求参数、编码方式、请求头等。

• 该方法内部会创建一个 RequestConvertible 对象,将所有请求参数封装成 URLRequest。

3. 执行请求:

• DataRequest 是 Alamofire 处理网络请求的核心对象。它继承自 Request,管理请求的生命周期。

• perform(request) 根据不同请求类型(DataRequest、UploadRequest 等)调用相应的处理方法。

4. 请求最终执行:

• 通过 performSetupOperations 设置请求参数并验证生成的 URLRequest,接着通过 URLSessionTask 发起请求,最后调用 .resume() 来执行任务。

总结:Alamofire 的封装本质上是对 NSURLSession 工作流的简化和扩展,主要增强了参数编码、请求头管理、响应处理等功能,便于开发者更方便地进行网络操作。

参考:

GitHub - Alamofire/Alamofire: Elegant HTTP Networking in Swift

Alamofire 5源码解析一: 执行HTTP Request_alamofire5.0 interceptor-CSDN博客

iOS开发之Alamofire源码解析前奏--NSURLSession全家桶-腾讯云开发者社区-腾讯云

NSURLSession | Apple Developer Documentation

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Springboot整合抖音小程序获取access-token图片检测V3

抽取配置文件 appId以及secret需要自行在抖音开放平台获取 dy:appId: ttb0xxxxxsecret: 12a19a426xxxxxxxxxxxxx获取access-token 参照文档我们调用此接口需要先获取access-token 获取access-token官方文档地址 注意事项 client_token 的有效时间为 2 个小时&#xff0c;重复获…

力扣- 背包问题

关于背包问题,推荐卡哥的视频,结合代码随想录食用,效果绝佳!!! 传送门: 带你学透0-1背包问题&#xff01;| 关于背包问题&#xff0c;你不清楚的地方&#xff0c;这里都讲了&#xff01;| 动态规划经典问题 | 数据结构与算法_哔哩哔哩_bilibili 带你学透01背包问题&#xff…

HyperWorks汽车B-柱网格变形

在这一节&#xff0c;将练习如何使用变形域&#xff0c;实现汽车 B-柱有限元模型的网格变形。 图 7-13 网格变形前后的 B 柱模型 Step01&#xff1a;读取并查看模型。 打开模型文件 Exercise_7c.hm。 Step02&#xff1a;创建变形域。 (1) 通过路径 HyperMorph > Morph…

C++笔记之原子操作

C++笔记之原子操作 code review! 文章目录 C++笔记之原子操作1.初始化2.赋值3.取值4.赋给另一个原子类型5.`exchange`6.`compare_exchange_weak` 和 `compare_exchange_strong`使用场景7.注意事项在 C++ 中,原子类型提供了对共享变量的无锁操作,确保多线程环境下的安全。以下…

【Linux】为什么创建目录文件,硬链接数是2;创建普通文件时,硬链接数是1?(超详细图文解答)

前言 大家好吖&#xff0c;欢迎来到 YY 滴Linux系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的《Lin…

图示详解OpenEuler下Samba多用户身份验证配置、测试

前言 前文《图例详解OpenEuler下Samba安装、配置和测试》已对Samba服务的工作原理、安装、配置和测试&#xff0c;做了系统的介绍&#xff0c;并对匿名用户的访问samba服务器做了配置&#xff0c;相必读者已对samba服务的流程有了初步、系统的了解&#xff0c;本文在以上基础上…

DevExpress WinForms中文教程:Data Grid - 如何完成数据输入验证?

本教程介绍DevExpress WinForm的Data Grid控件是如何利用网格组件完成数据输入验证的。 P.S&#xff1a;DevExpress WinForms拥有180组件和UI库&#xff0c;能为Windows Forms平台创建具有影响力的业务解决方案。DevExpress WinForms能完美构建流畅、美观且易于使用的应用程序…

vim 操作

vim编辑器的有三种工作模式&#xff1a;命令模式、插入模式和底行命令模式 打开进入命令模式&#xff1a; 由命令模式到输入模式&#xff1a;i:在光标前插&#xff1b;a:在光标后插&#xff1b;o:在下一行插 由输入模式进入命令模式&#xff1a;esc 由命令模式进入底行命令…

LabVIEW技术难度最大的程序

在LabVIEW开发中&#xff0c;技术难度最大的程序通常涉及复杂的系统架构、高精度的控制要求、大量数据处理&#xff0c;以及跨平台或多硬件设备的集成。以下是几类具有高技术难度的LabVIEW程序&#xff1a; 1. 高精度实时控制系统 LabVIEW中涉及高精度实时控制的系统程序&…

探索极致性能:R9-9950X与I9-14900K的深度较量

处理器是电脑及服务器的心脏&#xff0c;处理器的性能直接影响着电脑或服务器的运行效率、多任务处理能力以及整体用户体验。一款优秀的处理器&#xff0c;能够确保系统流畅运行&#xff0c;无论是处理复杂的数据分析、高强度的图形渲染&#xff0c;还是享受沉浸式的游戏体验&a…

【spring ai】java 实现RAG检索增强,超快速入门

rag 需求产生的背景介绍&#xff1a; 在使用大模型时&#xff0c;一个常见的问题是模型会产生幻觉&#xff08;即生成的内容与事实不符&#xff09;&#xff0c;同时由于缺乏企业内部数据的支持&#xff0c;导致其回答往往不够精准和具体&#xff0c;偏向于泛泛而谈。这些问题…

Selenium实现滑动滑块验证码验证!

背景&#xff1a;在部分的登录中有滑动验证码的验证&#xff0c;由于滑动验证码的缺块是随机的就导致实现起来比较困难&#xff01; 01、实现方案 模板匹配 通过openCV分析两个图片的相似度&#xff0c;获取两个相似度很高图片的坐标&#xff0c;从而计算两个图片的距离。 轮…

基础sql

在执行删除操作之前&#xff0c;建议先运行一个 SELECT 查询来确认你要删除的记录。这可以帮助你避免误删数据。 删除字段id默认值为空字符串的所有数据 delete from users where id ; 删除字段id默认值为null的所有数据 delete from users where id is null; 删除字段upd…

数据容器(序列)的切片

1.数据容器&#xff1a;列表&#xff0c;元组&#xff0c;字符串 2..切片可以提取序列中的片段或整个序列 ##切片的格式为&#xff1a;变量名[ 起始位置:停止位置&#xff1a;步数] #起始位置为序列首位时可省略不写&#xff0c;停止位置为序列尾部时也如此&#xff0c;##停止…

多jdk版本环境下,jenkins系统设置需指定JAVA_HOME环境变量

一、背景 由于不同项目对jdk版本的要求不同&#xff0c;有些是要求jdk11&#xff0c;有些只需要jdk8即可。 而linux机器上安装jdk的方式又多种多样&#xff0c;最后导致jenkins打包到底使用的是哪个jdk&#xff0c;比较混乱。 1、java在哪 > whereis java java: /usr/bin/…

sql实战解析-sum()over(partition by xx order by xx)

该窗口函数功能 sum( c )over( partition by a order by b) 按照一定规则汇总c的值&#xff0c;具体规则为以a分组&#xff0c;每组内按照b进行排序&#xff0c;汇总第一行至当前行的c的加和值。 从简单开始一步一步讲&#xff0c; 1、sum( )over( ) 对所有行进行求和 2、sum(…

第二十五:IP网络层的数据,IP数据报

在数据链路层传输的数据叫帧&#xff0c;帧是数据链路层的传输单元。 那么在IP网络层的数据也有一个叫法IP数据报。 IP数据报 IP数据报首部 数据。 数据是传输层传递过来的报文&#xff1b;IP数据报首部格式如下&#xff1a; IP 报头的最小长度为 20 字节&#xff0c;上图…

打造爆款店铺:eBay、Temu、亚马逊卖家如何借助测评提升流量转销量?

无论是eBay还是在亚马逊、沃尔玛、Temu、速卖通、敦煌网、shopee、lazada平台上&#xff0c;流量是店铺获得曝光和销售的关键因素之一。提高店铺流量意味着能够吸引更多的买家浏览和关注&#xff0c;从而增加销售机会。那么&#xff0c;在eBay、Temu、亚马逊店铺中如何有效地提…

大模型还能让我们望梅止渴多久?

大模型梦碎的时间点似乎越来越近。过去一周&#xff0c;有关人工智能的消息糟糕多于积极。 周初&#xff0c;诺贝尔物理学奖和化学奖接连砸向时下正热的人工智能领域。这些奖项出人意料且鼓舞人心&#xff0c;意味着人工智能的确已经根本性地改变了我们生活和科学体系的方方面…

这是我见过最全LLM大模型基础知识学习汇总,建议收藏!

关于如何入门LLM&#xff0c;大多数回答都提到了调用API、训练微调和应用。但是大模型更新迭代太快&#xff0c;这个月发布的大模型打榜成功&#xff0c;仅仅过了一个月就被其他模型超越。训练微调也已经不是难事&#xff0c;有大量开源的微调框架&#xff08;llamafactory、fi…