Forward navigation:
Alamofire source learning directory collection
The Request base class cannot be used directly, but instead uses its four subclasses:
- DataRequest
- Data is requested using URLSessionDataTask and stored in memory using Data objects
- DataStreamRequest
- Use URLSessionDataTask to request data and OutputStream to send data
- DownloadRequest
- Use the URLSessionDownloadTask to download data and files to disk
- UploadRequest
- Upload requests using URLSessionUploadTask. Upload content using forms, files, InputStream
These four subclasses simply define initialization and the necessary methods, which are extended in ResponseSerialization to parse and process data usage
DataRequest
DataRequest subclass is one of the most basic subclasses, request to the Data using the Data object is written in the memory, suitable for general request, request small pictures, small file request
public class DataRequest: Request {
/// The protocol object used to create URLRequest objects
public let convertible: URLRequestConvertible
/// open to external computations
public var data: Data? { mutableData }
/// a private thread-safe Data object that holds requested Data
@Protected
private var mutableData: Data? = nil
/// The initialization method has one more argument than the parent: URLRequestConvertible, used to initialize the URLRequest object
init(id: UUID = UUID(),
convertible: URLRequestConvertible.underlyingQueue: DispatchQueue.serializationQueue: DispatchQueue.eventMonitor: EventMonitor? .interceptor: RequestInterceptor? .delegate: RequestDelegate) {
self.convertible = convertible
super.init(id: id,
underlyingQueue: underlyingQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: delegate)
}
// reset the request to delete the downloaded data
override func reset(a) {
super.reset()
mutableData = nil
}
/// Called when `Data` is received by this instance.
///
/// - Note: Also calls `updateDownloadProgress`.
///
/// - Parameter data: The `Data` received.
/// When the task receives data, the SessionDelegate is called to save the data
func didReceive(data: Data) {
if self.data = = nil {
/ / the initial
mutableData = data
} else {
/ / append
$mutableData.write { $0?.append(data) }
}
// Update the download progress
updateDownloadProgress()
}
/// the parent method that must be implemented to return the Task corresponding to the DataRequest
override func task(for request: URLRequest.using session: URLSession) -> URLSessionTask {
// URLRequest is a struct in swift, so use assignment to copy it
let copiedRequest = request
return session.dataTask(with: copiedRequest)
}
/// Update download progress (downloaded bytes/to be downloaded bytes)
func updateDownloadProgress(a) {
let totalBytesReceived = Int64(data?.count ?? 0)
let totalBytesExpected = task?.response?.expectedContentLength ?? NSURLSessionTransferSizeUnknown
downloadProgress.totalUnitCount = totalBytesExpected
downloadProgress.completedUnitCount = totalBytesReceived
Call the progress callback in the progress callback queue
downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) }
}
/// Validates the request, using the specified closure.
///
/// - Note: If validation fails, subsequent calls to response handlers will have an associated error.
///
/// - Parameter validation: `Validation` closure used to validate the response.
///
/// - Returns: The instance.
/ / / add validity judgment callback, which is used to determine the current URLRequest, URLSessionTask, download the Data is valid
@discardableResult
public func validate(_ validation: @escaping Validation) -> Self {
// Create a no-argument processing closure
let validator: () -> Void ={[unowned self] in
// There must be no errors, there must be responses
guard self.error = = nil.let response = self.response else { return }
// Call the judgment callback
let result = validation(self.request, response, self.data)
// If there is an error, set it to error
if case let .failure(error) = result {
self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error)))
}
// Notify the listener that validation is complete
self.eventMonitor?.request(self,
didValidateRequest: self.request,
response: response,
data: self.data,
withResult: result)
}
// Add closure to array
$validators.write { $0.append(validator) }
return self}}Copy the code
DataStreamRequest
DataStreamRequest is similar to DataRequest, except that instead of using a single Data to store all Data, DataStreamRequest encapsulates the received Data into datastreamRequest. Stream types (including Data, done, and errors). It holds N closures for processing the data stream, encapsulates the data when requested and uses the closure to hand it to the caller for processing.
- When parsing DataStreamRequest in ResponseSerialization, a callback that encapsulates Data into DataStreamRequest.Stream type is created and saved. When Data is received, all callbacks are called one by one. A callback is removed each time a Stream is processed, and when all processing is complete, the call completes the callback.
- A pair of IOStreams can be created. If you choose to create an IOStream, the request will be sent immediately and the caller will get an InputStream. DataStreamRequest uses the corresponding OutputStream to write data out of the DataStreamRequest, and the upper layer uses the InputStream to receive data
- Because Stream processing is used, Data is not written in memory. The upper layer can convert Data to memory as required, or directly write Data to disk for storage.
/// `Request` subclass which streams HTTP response `Data` through a `Handler` closure.
public final class DataStreamRequest: Request {
/// Define the callback that the caller uses to handle the Stream, which can throw an error, and can control whether the request is cancelled when the error is thrown
public typealias Handler<Success.Failure: Error> = (Stream<Success.Failure>) throws -> Void
/// encapsulates the data stream object + cancel token, which is used for upper-layer processing data
public struct Stream<Success.Failure: Error> {
// The latest stream event (data or error)
public let event: Event<Success.Failure>
/// The token used to cancel the data stream encapsulates the DataStreamRequest object with a cancel method
public let token: CancellationToken
// cancel the data stream (cancel the request)
public func cancel(a) {
token.cancel()
}
}
/// encapsulates the data stream, including: data, error, complete three kinds
public enum Event<Success.Failure: Error> {
/// Data or error
case stream(Result<Success.Failure>)
Request completed, request cancelled, request error)
case complete(Completion)}/// The data carried in the Event when the data flow completes
public struct Completion {
/// The last request made
public let request: URLRequest?
/// The last response received
public let response: HTTPURLResponse?
/// Last received request indicator
public let metrics: URLSessionTaskMetrics?
/// Data flow error error
public let error: AFError?
}
// cancel the request token when used to cancel the data stream
public struct CancellationToken {
weak var request: DataStreamRequest?
init(_ request: DataStreamRequest) {
self.request = request
}
// cancel the request directly when canceling the data stream
public func cancel(a) {
request?.cancel()
}
}
/// used to create
public let convertible: URLRequestConvertible
// Whether to cancel the request directly if the data stream fails to parse. The default value is false
public let automaticallyCancelOnStreamError: Bool
// wrap some data that requires thread-safe operation
struct StreamMutableState {
// When DataStreamRequest is used as an InputStream, this object is created and the output stream is bound to the InputStream
var outputStream: OutputStream?
/// The Stream is a callback object, and the Stream is a callback object, and the Stream is a callback object
var streams: [(_ data: Data) - >Void] = []
/// The number of streams currently being executed
var numberOfExecutingStreams = 0
/// Hold the array that completed the callback while the data flow is still incomplete
var enqueuedCompletionEvents: [() -> Void] =[]}@Protected
var streamMutableState = StreamMutableState(a)1.URLRequestConvertible, 2. Whether to cancel the request if the Stream processing fails
init(id: UUID = UUID(),
convertible: URLRequestConvertible.automaticallyCancelOnStreamError: Bool.underlyingQueue: DispatchQueue.serializationQueue: DispatchQueue.eventMonitor: EventMonitor? .interceptor: RequestInterceptor? .delegate: RequestDelegate) {
self.convertible = convertible
self.automaticallyCancelOnStreamError = automaticallyCancelOnStreamError
super.init(id: id,
underlyingQueue: underlyingQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: delegate)
}
/// Task is also URLSessionDataTask
override func task(for request: URLRequest.using session: URLSession) -> URLSessionTask {
let copiedRequest = request
return session.dataTask(with: copiedRequest)
}
// Close OutputStream when done
override func finish(error: AFError? = nil) {
$streamMutableState.write { state in
state.outputStream?.close()
}
super.finish(error: error)
}
/// Receive Data processing
func didReceive(data: Data) {
$streamMutableState.write { state in
if let stream = state.outputStream {
// If there is an InputStream, the corresponding OutputStream will be created to output data
underlyingQueue.async {
var bytes = Array(data)
stream.write(&bytes, maxLength: bytes.count)
}
}
// Number of callbacks currently being processed + number of new callbacks to be processed
state.numberOfExecutingStreams + = state.streams.count
// Make a copy
let localState = state
// Process data
underlyingQueue.async { localState.streams.forEach { $0(data) } }
}
}
/// Add validation callback
@discardableResult
public func validate(_ validation: @escaping Validation) -> Self {
let validator: () -> Void ={[unowned self] in
guard self.error = = nil.let response = self.response else { return }
let result = validation(self.request, response)
if case let .failure(error) = result {
self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error)))
}
self.eventMonitor?.request(self,
didValidateRequest: self.request,
response: response,
withResult: result)
}
$validators.write { $0.append(validator) }
return self
}
/// Produces an `InputStream` that receives the `Data` received by the instance.
///
/// - Note: The `InputStream` produced by this method must have `open()` called before being able to read `Data`.
/// Additionally, this method will automatically call `resume()` on the instance, regardless of whether or
/// not the creating session has `startRequestsImmediately` set to `true`.
///
/// - Parameter bufferSize: Size, in bytes, of the buffer between the `OutputStream` and `InputStream`.
///
/// - Returns: The `InputStream` bound to the internal `OutboundStream`.
The request must be opened () and closed () before the data is read by InputStream.
public func asInputStream(bufferSize: Int = 1024) -> InputStream? {
// The request is sent immediately after the IOStream is created, regardless of whether the request is sent immediately
defer { resume() }
var inputStream: InputStream?
$streamMutableState.write { state in
// Create a pair of iostreams, OutputStream writes to InputStream, buffer defaults to 1K
Foundation.Stream.getBoundStreams(withBufferSize: bufferSize,
inputStream: &inputStream,
outputStream: &state.outputStream)
// Open OutputStream, ready to read data
state.outputStream?.open()
}
return inputStream
}
// Execute a callback that can throw an error, catch the exception, cancel the request, and throw the error (called in ResponseSerialization).
func capturingError(from closure: () throws -> Void) {
do {
try closure()
} catch {
self.error = error.asAFError(or: .responseSerializationFailed(reason: .customSerializationFailed(error: error)))
cancel()
}
}
// add data flow to complete callback and queue
func appendStreamCompletion<Success.Failure> (on queue: DispatchQueue.stream: @escaping Handler<Success.Failure>) {
// Add a parse callback first
appendResponseSerializer {
// Add a parsed callback to the internal queue execution
self.underlyingQueue.async {
self.responseSerializerDidComplete {
// The parse completed callback is the operation $streamMutableState
self.$streamMutableState.write { state in
guard state.numberOfExecutingStreams = = 0 else {
// If there are still streams processing data, append the completion callback to the array
state.enqueuedCompletionEvents.append {
self.enqueueCompletion(on: queue, stream: stream)
}
return
}
// Otherwise, execute the callback directly
self.enqueueCompletion(on: queue, stream: stream)
}
}
}
}
// This ensures that the completion callback is added after all other parser processing is complete
}
// Send the completion event
func enqueueCompletion<Success.Failure> (on queue: DispatchQueue.stream: @escaping Handler<Success.Failure>) {
queue.async {
do {
// Create the completion event
let completion = Completion(request: self.request,
response: self.response,
metrics: self.metrics,
error: self.error)
/ / you
try stream(.init(event: .complete(completion), token: .init(self)))}catch {
// Ignore the error, the completion error cannot be processed, the data is complete}}}}Copy the code
We then extend datastreamRequest. Stream to quickly retrieve internal data:
/// The principle is the same
extension DataStreamRequest.Stream {
public var result: Result<Success.Failure>? {
guard case let .stream(result) = event else { return nil }
return result
}
public var value: Success? {
guard case let .success(value) = result else { return nil }
return value
}
public var error: Failure? {
guard case let .failure(error) = result else { return nil }
return error
}
public var completion: DataStreamRequest.Completion? {
guard case let .complete(completion) = event else { return nil }
return completion
}
}
Copy the code
DownloadRequest
DownloadRequest is used to handle downloads and is used when saving files as files on disk.
Document processing:
Handles local file saving policies and local file saving paths
// define a structure that complies with the OptionSet protocol to determine the local file saving strategy:
public struct Options: OptionSet {
/// Whether to create an intermediate directory
public static let createIntermediateDirectories = Options(rawValue: 1 << 0)
/// Whether to remove old files before downloading them
public static let removePreviousFile = Options(rawValue: 1 << 1)
public let rawValue: Int
public init(rawValue: Int) {
self.rawValue = rawValue
}
}
// MARK: Download target path
/// The download task will first download the file to the temporary cache path, then copy the file to the download destination path, using closures to defer the download path decision until the download is complete, depending on the temporary file directory and the download response header to determine the download destination path and file saving policy
/// note: if you are downloading a local file (url starting with file://), the callback will not respond
public typealias Destination = (_ temporaryURL: URL._ response: HTTPURLResponse) -> (destinationURL: URL, options: Options)
// create the default recommended download path callback (document root)
public class func suggestedDownloadDestination(for directory: FileManager.SearchPathDirectory = .documentDirectory.in domain: FileManager.SearchPathDomainMask=.userDomainMask.options: Options= []) - >Destination {
{ temporaryURL, response in
let directoryURLs = FileManager.default.urls(for: directory, in: domain)
let url = directoryURLs.first?.appendingPathComponent(response.suggestedFilename!) ?? temporaryURL
return (url, options)
}
}
// default download path callback (see below)
static let defaultDestination: Destination = { url, _ in
(defaultDestinationURL(url), [])
}
/// return the default file storage path (just rename the default file name to Alamofire_ prefix, the file is still in the cache directory, will be deleted, if you want to save the file, you need to move to another directory)
static let defaultDestinationURL: (URL) - >URL = { url in
let filename = "Alamofire_\(url.lastPathComponent)"
let destination = url.deletingLastPathComponent().appendingPathComponent(filename)
return destination
}
Copy the code
Download the source Downloadable
/// defines the download source for the download request
public enum Downloadable {
/// Download from URLRequest
case request(URLRequestConvertible)
/// Resumable
case resumeData(Data)}Copy the code
Thread-safe mutable state
// wrap it privately
private struct DownloadRequestMutableState {
/// The downloaded data that needs to be processed when the task supporting breakpoint continuation is cancelled
var resumeData: Data?
/// Save the file after downloading
var fileURL: URL?
}
// private thread-safe property
@Protected
private var mutableDownloadState = DownloadRequestMutableState(a)/// open to external acquisition
public var resumeData: Data? { mutableDownloadState.resumeData }
public var fileURL: URL? { mutableDownloadState.fileURL }
Copy the code
Initialization and two parameters that need to be determined during initialization
/ / / download the source
public let downloadable: Downloadable
/// Download path callback
let destination: Destination
/ / / initialized
init(id: UUID = UUID(),
downloadable: Downloadable.underlyingQueue: DispatchQueue.serializationQueue: DispatchQueue.eventMonitor: EventMonitor? .interceptor: RequestInterceptor? .delegate: RequestDelegate.destination: @escaping Destination) {
self.downloadable = downloadable
self.destination = destination
super.init(id: id,
underlyingQueue: underlyingQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: delegate)
}
// Clear data when retry
override func reset(a) {
super.reset()
$mutableDownloadState.write {
$0.resumeData = nil
$0.fileURL = nil}}Copy the code
Handling of downloads and cancellations
/// URLSession download completed/failed, call back to update status
func didFinishDownloading(using task: URLSessionTask.with result: Result<URL.AFError>) {
eventMonitor?.request(self, didFinishDownloadingUsing: task, with: result)
switch result {
case let .success(url): mutableDownloadState.fileURL = url
case let .failure(error): self.error = error
}
}
/// update progress is called back when URLSession is downloaded
func updateDownloadProgress(bytesWritten: Int64.totalBytesExpectedToWrite: Int64) {
downloadProgress.totalUnitCount = totalBytesExpectedToWrite
downloadProgress.completedUnitCount + = bytesWritten
downloadProgressHandler?.queue.async { self.downloadProgressHandler?.handler(self.downloadProgress) }
}
Create URLSessionTask / / /
/// New file download
override func task(for request: URLRequest.using session: URLSession) -> URLSessionTask {
session.downloadTask(with: request)
}
/// Resumable
public func task(forResumeData data: Data.using session: URLSession) -> URLSessionTask {
session.downloadTask(withResumeData: data)
}
/// 1. Cancel the request without setting the resumeData attribute to the listener
@discardableResult
override public func cancel(a) -> Self {
cancel(producingResumeData: false)}/// 2. Cancel the download and check whether the listener needs to be populated with the resumeData property
@discardableResult
public func cancel(producingResumeData shouldProduceResumeData: Bool) -> Self {
cancel(optionallyProducingResumeData: shouldProduceResumeData ? { _ in } : nil)}/// 3. Cancel the download and use a callback to process the downloaded data, which will first fill the resumeData property, notify the listener and invoke the callback
@discardableResult
public func cancel(byProducingResumeData completionHandler: @escaping (_ data: Data?). ->Void) -> Self {
cancel(optionallyProducingResumeData: completionHandler)
}
/// the integration of 1, 2, 3 above
private func cancel(optionallyProducingResumeData completionHandler: ((_ resumeData: Data?). ->Void)?) -> Self {
// Thread safety
$mutableState.write { mutableState in
// Determine whether to cancel
guard mutableState.state.canTransitionTo(.cancelled) else { return }
// Update the status
mutableState.state = .cancelled
// The parent class tells the listener that the call was cancelled
underlyingQueue.async { self.didCancel() }
guard let task = mutableState.tasks.last as? URLSessionDownloadTask, task.state ! = .completed else {
// If the download is complete, go to finish logic
underlyingQueue.async { self.finish() }
return
}
if let completionHandler = completionHandler {
// Cancel callbacks that handle downloaded data
// Let's make sure the request metric is retrieved
task.resume()
task.cancel { resumeData in
// Fill in the resumeData attribute
self.mutableDownloadState.resumeData = resumeData
// The parent class tells the listener that the cancellation succeeded
self.underlyingQueue.async { self.didCancelTask(task) }
// Execute the downloaded data processing callback
completionHandler(resumeData)
}
} else {
// No callback to handle downloaded data
task.resume()
// Cancel directly
task.cancel(byProducingResumeData: { _ in })
/ / to inform
self.underlyingQueue.async { self.didCancelTask(task) }
}
}
return self
}
/// Determine the validity of the response
@discardableResult
public func validate(_ validation: @escaping Validation) -> Self {
let validator: () -> Void ={[unowned self] in
guard self.error = = nil.let response = self.response else { return }
let result = validation(self.request, response, self.fileURL)
if case let .failure(error) = result {
self.error = error.asAFError(or: .responseValidationFailed(reason: .customValidationFailed(error: error)))
}
self.eventMonitor?.request(self,
didValidateRequest: self.request,
response: response,
fileURL: self.fileURL,
withResult: result)
}
$validators.write { $0.append(validator) }
return self
}
Copy the code
UploadRequest
It is used to create upload requests, which can upload Data from memory (Data), disk (file), and InputStream (InputStream). Note that upload requests are subclasses of DataRequest
Upload the source
public enum Uploadable {
/ / / from memory
case data(Data)
/// Upload from a file, and you can set whether to delete the source file after the upload is complete
case file(URL, shouldRemove: Bool)
// upload from stream
case stream(InputStream)}Copy the code
Initialization and initial state properties
// the protocol used to create the upload source
public let upload: UploadableConvertible
/// File manager, used to clear tasks, including multi-form upload data written to the hard disk
public let fileManager: FileManager
// upload the source, which is created when the request starts. Optional because it is possible to fail when creating a protocol object from the upload source
public var uploadable: Uploadable?
/ / / initialized
init(id: UUID = UUID(),
convertible: UploadConvertible.underlyingQueue: DispatchQueue.serializationQueue: DispatchQueue.eventMonitor: EventMonitor? .interceptor: RequestInterceptor? .fileManager: FileManager.delegate: RequestDelegate) {
upload = convertible
self.fileManager = fileManager
super.init(id: id,
convertible: convertible,
underlyingQueue: underlyingQueue,
serializationQueue: serializationQueue,
eventMonitor: eventMonitor,
interceptor: interceptor,
delegate: delegate)
}
// tell the listener that the upload source was created successfully (called in Session)
func didCreateUploadable(_ uploadable: Uploadable) {
self.uploadable = uploadable
eventMonitor?.request(self, didCreateUploadable: uploadable)
}
/// tell the listener that it failed to create the upload source (called during Session) and retry or complete the logic
func didFailToCreateUploadable(with error: AFError) {
self.error = error
eventMonitor?.request(self, didFailToCreateUploadableWithError: error)
retryOrFinish(error: error)
}
// Create URLSessionUploadTask from URLRequest to URLSession
override func task(for request: URLRequest.using session: URLSession) -> URLSessionTask {
guard let uploadable = uploadable else {
fatalError("Attempting to create a URLSessionUploadTask when Uploadable value doesn't exist.")}switch uploadable {
case let .data(data): return session.uploadTask(with: request, from: data)
case let .file(url, _) :return session.uploadTask(with: request, fromFile: url)
case .stream: return session.uploadTask(withStreamedRequest: request)
}
}
/// Delete data during retry. In retry, the upload source must be created again
override func reset(a) {
// Uploadable must be recreated on every retry.
uploadable = nil
super.reset()
}
/// Convert to InputStream for external connection. Uploadable. Stream must be used otherwise an exception will be thrown
func inputStream(a) -> InputStream {
guard let uploadable = uploadable else {
fatalError("Attempting to access the input stream but the uploadable doesn't exist.")}guard case let .stream(stream) = uploadable else {
fatalError("Attempted to access the stream of an UploadRequest that wasn't created with one.")
}
eventMonitor?.request(self, didProvideInputStream: stream)
return stream
}
// When the request completes, clean up the task (delete the source file)
override public func cleanup(a) {
defer { super.cleanup() }
guard
let uploadable = self.uploadable,
case let .file(url, shouldRemove) = uploadable,
shouldRemove
else { return }
try? fileManager.removeItem(at: url)
}
Copy the code
Protocol for creating the upload source
UploadableConvertible Specifies the protocol used to create the upload source
public protocol UploadableConvertible {
func createUploadable(a) throws -> UploadRequest.Uploadable
}
* / *UploadRequest.Uploadable** Implements the UploadableConvertible protocol. UploadableConvertible ** Returns to UploadableConvertible
extension UploadRequest.Uploadable: UploadableConvertible {
public func createUploadable(a) throws -> UploadRequest.Uploadable {
self}}UploadableConvertible; URLRequestConvertible: UploadableConvertible;
public protocol UploadConvertible: UploadableConvertible & URLRequestConvertible {}
Copy the code
conclusion
- The Request class is responsible for connecting the URLRequest and URLSessionTask. The base class mainly defines a bunch of attributes used in the Request, and then defines the callback method for each stage of the Request (excluding the Response parsing part, which is related to the Response parsing, in Response.swift). Call external or self to notify the listener and proceed with the logic (retry, complete)
- The four subclasses add attributes, states, and callbacks with their own characteristics
- Each of the four subclasses uses a protocol to isolate the creation of the Request source, so that the Request class doesn’t care where the upload source comes from. The upload Request also uses a protocol to isolate the creation of the upload source, and uses a composite protocol to combine the Request’s protocol from the upload source
- Request. Most of the content in Swift is the creation of Request and processing of various states. The sending time of Task is as follows:
- If startImmediately is true (the default), response*** in your request will add responseSerializers to your request. When you add the first responseSerializers, The resume method is automatically called to begin the request
- Otherwise, you need to manually call the Resume method on the Request object to send a Request
note
Alamofire is very simple to use:
AF.request("Request address").responseJSON(completionHandler: {
resp in
// Process the response
})
Copy the code
However, the internal processing is very complex, from the initial processing of the request address, to the creation of the URLRequest, to the creation of the URLSessionTask, each step has a lot of fine processing. Session schedules requests. Request and Response are the core of requests. In combination with various validity judgments, authentication, caching, retry, error throwing, etc., the EventMonitor interface alone has a full 43 callback methods, which run through all the state processing from the beginning of the request to the completion of the Response parsing. After watching the Response parsing, I will try to draw a sequence diagram to see if I can clarify the logic. Learn about code design and packaging logic, to their own thinking when the code has a great improvement.
All of the above are personal understanding, if there is a mistake, welcome to comment pointed out, will be the first time to modify ~~ very thank ~~