2016년 10월 26일 수요일

Alamofire 소스 분석 2


  • Alamofire --- SessionManager --- SessionDelegate --- Request --- TaskDelegate -- Response


Alamofire

  • 사용자가 웹주소를 String, URL, URLComponents으로 사용하면 이를 URL로 변경해주는 URLConvertible을 제공한다.
  • 사용자가 URLRequest를 뽑아낼 수 있는 URLRequestConvertible을 사용할 수도 있다.

SessionManager

  • default SessionManager를 제공한다.
    • URLSessionConfiguration을 사용해서 항상 사용할 HTTP 헤더를 지정한다: Accept-Encoding, Accept-Language, User-Agent
  • request, download, upload, stream
  • SessionDelegate를 URLSession에 연결하여 연결 세션에 대한 콜백을 받도록 해준다.
    • 보통 콜백이 불리면 상황에 따라 Request의 delegate를 호출한다.
  • "delegate[task] = request"를 통해 delegate가 request를 저장하고 있도록 한다.
  • 기본은 request.resume()를 호출하여 request에 관련된 task를 실행한다.
  • 서버 요청은 별도의 쓰레드에서 이루어지도록 한다: DispatchQueue
  • upload
    • async로 시작한다: DispatchQueue.global(qos: .utility).async
    • multipartFormData를 실행하여 사용자로부터 data를 입력받는다. <- MultipartFormData
      • "Content-Type: multipart/form-data; boundary=alamofire.boundary.%08x%08x"
    • form data의 크기가 지정한 크기 보다 큰지 확인한다.(기본: 10MB)
      • 크지 않은 경우 메모리에서 바로 처리한다.
      • 큰 경우 데이타를 파일에 먼저 쓰고 이것을 서버에 보낸다.

SessionDelegate

  • URLSessionTask를 기준으로 하여 request를 저장하고 있는다.
  • NSObject를 상속한다.
  • extension으로 URLSessionDelegate, URLSessionTaskDelegate, URLSessionDataDelegate, URLSessionDownloadDelegate, URLSessionStreamDelegate를 구현한다.
  • Requests credentials
    • NSURLAuthenticationMethodServerTrust이면 serverTrustPolicyManager로부터 등록된 serverTrustPolicy를 사용한다.
    • serverTrustPolicy가 유효하면 credential을 생성하여 넘겨준다.

ParameterEncoding

  • encode: URLRequestConvertible에 parameters를 포함한 URLRequest를 생성한다.
    • parameter는 percent-escaped, URL encoded query string으로 변환한다.

Request

  • URLSession과 RequestTask로 초기화 한다.
  • RequestTask의 종류(data, download, upload, stream)에 따라 관련 TaskDelegate를 갖는다.
  • authenticate
  • authorizationHeader
    • header에 추가하기 위한 것
    • Authorization: Basic 'base64 encoded of user:password'

DataRequest

  • session의 dataTask를 사용하여 URLRequest를 요청한다.
  • DataTaskDelegate와 연결된다.
  • extension으로 response 함수를 추가한다.

TaskDelegate

  • NSObject를 상속
    • objc에 의해 실행되는 함수들을 가지고 있다.
  • 사용자로부터의 response handler를 실행하기 위한 queue를 가지고 있는다.
    • OperationQueue
    • DataTaskDelegate, DownloadTaskDelegate, UploadTaskDelegate

Response

  • DefaultDataResponse: data나 upload의 non-serialized response에 사용
  • DataResponse<Value>: data나 upload의 serialized response에 사용
  • DefaultDownloadResponse: download의 non-serialized response에 사용
  • DownloadResponse<Value>: download의 serialized response에 사용
  • 애플이 제공하는 'URL Session Task Metrics'의 사용
    • 위의 모든 response에 추가로 Response 프로토콜을 정의하여 metric을 내부에 저장한다.

ResponseSerialization

  • extension의 형태로 서버로부터 온 데이타를 serialize해주는 함수들을 정의한다.
    • Raw data -> no serialize
    • Data -> serializeResponseData
    • String -> serializeResponseString
    • JSON -> serializeResponseJSON
    • Property List -> serializeResponsePropertyList
  • 내부에 저장할 타입과 이를 만들 serialize 함수를 가지고 있는 DataResponseSerializerProtocol 프로토콜을 정의한다.
    • DataResponseSerializerProtocol를 구현한 구조체로 DataResponseSerializer<Value>를 정의한다.
    • 특정 serialize 함수를 사용할 response 함수를 정의한다: responseJSON, responseString 등...
  • JSON을 응답으로 주는 경우에 대한 코드를 살펴보자.
    • JSON의 serializer로 DataRequest.jsonResponseSerializer를 사용한다.
    • DataRequest.jsonResponseSerializer는 'struct DataResponseSerializer<Any>'를 리턴하는데 이 struct는 DataResponseSerializerProtocol을 구현하고 있다.
    • DataResponseSerializerProtocol은 serialize의 결과를 리턴하는 serializeResponse 함수(closure)를 가지고 있다.
    • DataResponseSerializer는 serializeResponse 함수로 Request.serializeResponseJSON를 사용한다.
    • serializeResponseJSON는 JSONSerialization.jsonObject를 사용해서 응답 data로부터 object를 생성한다.
  • 내가 새로운 serializer를 추가한다면 DataResponseSerializerProtocol를 상속한 struct를 만들고 이를 response 함수에 넘겨주면 된다.

Timeline

  • Timeline
    • Alamofire 자체에서 추적하는 값
    • startTime: request 시작시 CFAbsoluteTimeGetCurrent()로 저장
    • requestCompletedTime: request 시작시 delegate.queue에 endTime을 계산하는 함수를 추가해주어 응답 시간이 바로 계산되어 저장되도록 한다.
    • initialResponseTime: delegate의 urlSession이 호출되는 시점에 값을 계산해준다.
    • serializationCompletedTime: completionHandler에서 serialization을 마친 후의 값.
    • latency = initialResponseTime - requestStartTime
    • requestDuration = requestCompletedTime - requestStartTime
    • serializationDuration = serializationCompletedTime - requestCompletedTime
    • totalDuration = serializationCompletedTime - requestStartTime
    • CustomStringConvertible과 CustomDebugStringConvertible을 extension으로 구현.
  • URL Session Task Metrics
    • iOS와 tvOS 10과 macOS 10.12에서 사용가능한 URLSessionTaskMetrics API.
    • SessionDelegate에 didFinishCollectingMetrics의 경우 불리는 urlSession을 정의한다: metric을 TaskDelegate의 metrics에 넘긴다.
    • completionHandler가 불릴 때 delegate로부터의 metrics를 response에 넘긴다: response의 _metrics에 metric이 저장이 된다.
    • 사용자가 response.metrics를 부르면 _metrics를 리턴한다.

ServerTrustPolicy

  • ServerTrustPolicyManager에 의해 호스트 별로 관리될 수 있다.
  • URLSession에 ServerTrustPolicyManager를 저장해 놓기 위해 AssociatedKeys를 사용한다.
  • 5가지의 policy가 있다: performDefaultEvaluation, pinCertificates, pinPublicKeys, disableEvaluation, customEvaluation
  • SessionDelegate에서 서버로부터 authentication관련 응답이 왔을 때 사용된다.

MultipartFormData

  • 서버에 보낼 내용을 BodyPart에 저장한다.
  • append: Data, InputStream, URL의 형태로 추가한다.
  • 파일의 URL을 주는 경우
    • mimetype을 사용자가 지정해 주지 않으면 mimetype을 확인한다.
      • content type을 가져올 수 있으면 가져오고 안되면 "application/octet-stream"으로 한다.
      • content type가져올 때 사용하는 함수: UTTypeCreatePreferredIdentifierForTag -> UTTypeCopyPreferredTagWithClass
    • 파일이 올바른지를 위해 아래의 5가지를 확인한다.
      • is file URL?
      • is file URL reachable?
      • is file URL a directory?
      • can the file size be extracted?
      • can a stream be created from file URL?
  • BodyPart
    • HTTPHeaders: [String: String]
    • InputStream
  • encode
    • initialData
      • "--(boundary)(EncodingCharacters.crlf)"
    • headerData
      • Content-Disposition: form-data; name="file1"; filename="a.txt"
      • Content-Type: text/plain
      • 마지막에 EncodingCharacters.crlf 를 추가한다.
    • bodyStreamData
    • finalBoundaryData
      • "(EncodingCharacters.crlf)--(boundary)--(EncodingCharacters.crlf)"
  • writeEncodedData
    • 서버에 보낼 내용이 큰 경우에는 파일로 저장한 후 보낼 수 있도록 한다.

RequestAdapter

  • public protocol RequestAdapter
    • adapt 함수를 구현하도록 한다.
  • URLRequest를 변경하여 변경된 Request를 서버에 요청하도록 한다.
  • SessionManager에서 adapter를 로컬 변수로 들고 있다. 따라서, 여기에 내가 원하는 adapter를 구현한 후 이것을 SessionManager의 adapter에 설정해 준다.
  • URLRequest에 extension으로 adapt 함수를 구현한다. 이 함수는 RequestAdapter를 input으로 받아서 RequestAdapter의 adapt 함수를 호출해준다.
  • 동작 방법
    • SessionManager의 request 함수를 호출한다.
    • DataRequest의 Requestable의 task 함수를 통해 URLSessionTask를 생성한다.
    • task 함수의 input으로 RequestAdapter를 넣는다.
    • task 함수는 URLSessionTask를 생성하기 전에 URLRequest의 adapt를 먼저 호출한다.
    • adapt 함수로부터 리턴받은 URLRequest를 URLSessionTask를 생성할 때 input으로 넣어준다.

RequestRetrier

  • 실행중 에러가 발생했을 때 다시 시도할 수 있도록 한다.
  • public protocol RequestRetrier
    • should 함수를 구현하도록 한다. -> 사용자가 필요한 작업을 한 후 completion을 호출하도록 한다.
  • Request에 retryCount가 로컬 변수로 있어서 retry시 값을 증가시켜준다. 따라서, should 함수 구현시 retryCount 값을 통해 몇번이나 다시 시도 중인지 파악할 수 있다.(예를 들어 무한 시도를 막는 용도로 사용할 수 있다.)
  • 동작 방법
    • SessionManager의 request 함수를 호출한다.
    • 에러 발생시 allowRetrier 함수를 호출한다.
    • DispatchQueue.global의 qos를 utility로 한 asynchronous로 RequestRetrier의 should 함수를 호출한다.
    • should 함수에서의 completion은 input으로 다시 시도를 할지 안할지를 받아서 다시 시도 할 거면 URLSessionTask를 만들어서 실행하는 작업을 다시 시도하고 아니면 끝낸다.

Response시 실행되는 handler의 동작

  • TaskDelegate에 OperationQueue를 설정한다.
    • maxConcurrentOperationCount = 1로 한번에 하나씩 순서대로 실행되도록 한다.
    • isSuspended = true로 중단된 상태로 둔다.
    • qualityOfService = utility로 한다.
  • request에 response handler를 등록하면 OperationQueue에 관련된 operation을 하는 함수를 등록하는데 보통 이 함수의 끝에서 response handler를 호출해 준다.
    • 사용자가 별도로 queue를 설정해 주지 않았으면 보통 DispatchQueue.main에서 handler를 실행한다.
  • 서버로부터 응답이 오거나 request 중 에러가 발생하면 isSuspended를 false로 설정하여 등록된 operation들이 순차적으로 실행되도록 한다.

PyBitmessage 소스 분석

https://github.com/Bitmessage/PyBitmessage


Protocol

  • 'version' : 주위 노드와 첫 연결시 보낸다.
  • 'verack' : 'version'에의 응답이다. payload없이 header만 보낸다.
  • 'addr' : 'verack'를 받으면 주위 노드들에게 노드 정보를 알린다.
  • 'inv' : 주위의 노드들에게 내가 원하는 inventory hash를 보낸다.
  • 'getdata' : 'inv'에의 응답이다. 'inv'의 object를 달라고 요청하는 것이다.
  • 'object' : 'getdata'에의 응답이다. 무조건 다른 node들에게 전달하고('inv'를 propagate한다.) 내가 처리해야 할 object인지를 본다. 아니면 무시한다.
    • 'getpubkey': public key를 요청한다.
    • 'pubkey': public key를 보낸다.
  • 'error' : 받은 데이터에 문제가 있는 경우 에러 메시지로 응답한다.

첫 연결시 보내는 정보

  • payload = protocol version number(3) + bitfield of features + 현재 시간(unix time) + boolservices of remote connection + remote host + bitflags of the services + my host + nonce + user agent의 길이를 숫자 인코딩한 값 + user agent + stream numbers를 숫자 인코딩한 값 + 내 stream number를 숫자 인코딩한 값
  • protocol version이 나보다 작으면 연결하지 않는다.
  • bitfield = NODE_NETWORK(1) | NODE_SSL(2, ssl을 지원하는 경우)
  • 보낸 시간과 받은 시간이 1시간 이상 차이가 나면 에러로 응답한다.
  • boolservices of remote connection = 1(8bytes)
  • remote host = 0x00000000000000000000FFFF(12bytes) + host의 16진수 표현(4bytes) + remote port(2bytes)
    • 이 값을 받은 노드는 외부에서 알고있는 자신의 IP를 알 수 있다.
  • bitflags of the services = 1(8bytes)
  • my host = 0x00000000000000000000FFFF(12bytes) + 2130706433을 숫자 인코딩한 값(4bytes, 127.0.0.1을 의미, 이 값을 받은 노드는 실제 연결된 IP를 사용한다.) + 내 port(2bytes)
  • nonce는 랜덤 값이다.
  • user agent = "/PyBitMessage:softwareVersion/" - softwareVersion은 현재 사용중인 프로그램의 버전을 적는다(예:0.6.1)
    • 이 값을 통해 상대방이 누구인지 알 수 있다.(예: 같은 프로그램인데 버전이 높다면 업그레이드 메시지를 사용자에게 보여줄 수 있다.)
  • stream numbers는 관심있는 stream의 수를 의미한다. PyBitMessage는 1을 준다.
  • 같은 stream number와만 연결이 가능하다.

주위 노드들에게 연결된 노드의 정보를 알리기

  • 노드의 수 + (verack를 받은 시간 + stream number + service bit flags(1) + host + port)를 노드의 수만큼 반복
  • host는 (0x00000000000000000000FFFF + host의 16진수 표현)으로 바꾸고 나머지는 다 숫자 인코딩을 한다.
  • 이 값을 받은 노드는 (time, host, port)를 내부에 저장한다.

주소 만들기

  • Deterministic address와 Random address 두가지 형태로 주소를 만들 수 있다.
  • Random address
    • 완전히 랜덤으로 생성된다: 주소값을 잊으면 다시는 같은 주소를 생성할 수 없다.
    • address version number로 4를 stream number로는 1을 사용한다.
    • 생성 방법
    • a. signing key를 생성한다.
      • potential private signing key를 생성한다: OpenSSL.rand(32)
      • potential private signing key로부터 potential public signing key를 생성한다: Elliptic Curve DSA 알고리즘을 사용하면 private key로부터 public key를 항상 생성할 수 있다.
    • b. encryption key를 생성한다.
      • potential private encryption key를 생성한다: OpenSSL.rand(32)
      • potential private encryption key로부터 potential public encryption key를 생성한다: ECDSA 알고리즘 사용
    • c. (potential public signing key + potential public encryption key)에 sha512를 적용한 후 ripemd160을 적용한다. ripemd160을 적용한 값이 0x00으로 시작하지 않으면 0x00이 나올 때까지 다시 계산한다.
    • d. (address version number를 숫자 인코딩한 값 + stream number를 숫자 인코딩한 값 + 앞의 0x00을 제외한 ripe값)
    • e. 앞의 값에 sha512를 두번 적용해서 나온 값의 앞 4byte를 checksum으로 한다.
    • f. (d에서 나온 값을 hex로 표현한 형태로 바꾼 값 + e에서의 checksum을 hex로 표현한 형태로 바꾼 값)을 16진수값으로 바꾼 후 base58로 인코딩하고 앞에 'BM-'를 추가한다: 이 값이 주소!!!
  • Deterministic address
    • passphrase를 지정하여 주소를 필요할 때마다 다시 생성할 수 있도록 한다: 같은 passphrase로는 같은 주소가 만들어진다.
    • address version number로 3을 stream number로는 1을 사용한다.
    • 앞의 Random address에서와 같은 방법을 사용하나 key를 만드는 방법만이 다르다: passphrase로 키를 만들기 때문에 나중에 재생성이 가능해진다.
    • signingKeyNonce = 0, encryptionKeyNonce = 1로 설정하여 key 만들기를 시작한다.
    • (passphrase + signingKeyNonce)을 sha512를 하여 나오는 값 중 앞의 256bit를 potential private signing key로 한다.
    • (passphrase + encryptionKeyNonce)을 sha512를 하여 나오는 값 중 앞의 256bit를 potential private encryption key로 한다.
    • 이 key들을 가지고 Random address에서 주소를 만들 때 한 과정과 같은 방법으로 주소를 만든다.
      • 이때, 0x00을 확인하는 과정에서 0x00이 아니면 (signingKeyNonce += 2, encryptionKeyNonce += 2)를 하여 새로운 key를 만들어서 과정을 반복한다.

private key 저장하기

  • Wallet_import_format을 사용한다.
  • private key(private signing key와 private encryption key)의 앞에 0x80을 추가한다.
  • sha256을 두번 적용하여 나온 값의 앞 4byte를 checksum으로 한다.
  • (0x80 + private key + checksum)을 Base58로 인코딩한다.

주위 노드들에게 내가 알고 있는 정보를 알리기

  • 'inv' 보내기
  • 주소로부터 address version number와 stream number와 hash를 뽑아낸다.
  • key의 유지 시간(embeddedTime이라 하자)을 계산한다: 현재 시간 + TTL
    • TTL을 (28일 +- 5분)으로 한다.
  • payload = embeddedTime을 숫자 인코딩한 값 + 0x00000001 + address version number를 숫자 인코딩한 값 + stream number를 숫자 인코딩한 값 + doubleHashOfAddressData의 뒤쪽 32byte(tag)
    • doubleHashOfAddressData = (address version number를 숫자 인코딩한 값 + stream number를 숫자 인코딩한 값 + hash)에 sha512를 두번 적용한 값
  • dataToEncrypt = 주소의 bitfield 계산 + public signing key + public encryption key + nonceTrialsPerByte를 숫자 인코딩한 값 + payloadLengthExtraBytes를 숫자 인코딩한 값
    • WIF로 저장된 주소로부터 public key 값을 가져온다.
    • WIF로부터 가져온 key값은 0x80을 앞에 가지고 있으므로 이를 뺴주어야 한다.
  • (payload + dataToEncrypt)를 private signing key를 사용해서 ECDSA로 sign을 한다. sign해서 나온 값을 signature라 하자.
  • dataToEncrypt에 (signature의 길이를 숫자 인코딩한 값 + signature)를 추가한다.
  • dataToEncrypt를 encryption할 때 사용하기 위해 doubleHashOfAddressData의 앞쪽 32byte를 private key로 하는 public/private key 쌍을 만든다. message를 encryption할 때 사용하는 것과 같은 알고리즘인 ECDSA로 public key를 만든다.
  • dataToEncrypt를 앞의 public key를 사용해서 ECDSA로 encryption을 한다. encryption된 값을 payload에 추가한다.
    • 받는 노드에서 주소를 알고 있다면 이 주소의 double hash를 통해 private key를 알아낼 수 있고 decrypt할 수 있게 된다.
  • POW를 실행하여 trialValue와 nonce를 얻는다.
  • payload의 앞에 nonce를 숫자로 변환한 값을 추가한다.
  • 현재까지의 전체 payload로부터 inventory hash를 만든다.
    • inventory hash = payload에 sha512를 두번 적용하여 나온 값의 앞 32byte
  • inventory hash를 키로하여 (objectType(pubkey는 1이다), streamNumber, payload, embeddedTime, doubleHashOfAddressData의 뒤쪽 32byte)를 로컬에 저장한다.
  • (inventory hash의 개수 + inventory hash의 리스트)를 주위 노드들에게 보낸다.
    • 위에서 설명은 하나의 inventory hash를 만드는 것을 설명하였으나 이전에 저장되어 있는 다른 inventory hash가 있으면 한번에 보낸다.
    • (inventory hash의 개수 + inventory hash의 리스트)를 payload라 하자.
    • inventory hash는 길이가 32byte로 정해져 있으므로 리스트로부터 개수를 쉽게 알아낼 수 있다.
    • payload를 sha512한 값의 앞 4byte를 checksum이라 한다.
    • header = 0xE9BEB4D9 + command('inv') + payload_length + checksum
    • (header + payload)를 연결된 주위 노드들에게 보낸다.

inventory hash에 대한 응답 보내기

  • 'getdata'를 'inv'에 대한 응답으로 보낸다.
  • 'inv'로부터 받은 내용을 그대로 되돌려 보낸다.

public key 저장하기

  • 'object'로부터 payload를 받는다.
  • nonce, time, object type은 건너뛴다.
  • address version number, stream number를 뽑아낸다.
  • tag를 확인한다.
    • 내가 'getdata'를 요청한 public key인지를 확인한다.
  • payload의 encryption과 signature를 통해 정상적인지를 확인한다.
  • (address, address version number, storedData)를 저장한다.
    • storedData는 payload로부터 (address version number + stream number + 주소의 bitfield 계산 + public signing key + public encryption key + nonceTrialsPerByte + payloadLengthExtraBytes)를 뽑아내어 따로 가지고 있는 값이다.

subscribe와 unsubscribe

  • subscribe이면 address와 label을 입력받아서 (address, label, 구독 여부)를 저장한다.
  • unsubscribe이면 address를 입력 받아서 지운다.
  • 주소를 만든이가 메시지를 broadcast로 보내면 subscribe한 이들은 받아볼 수 있다.(broadcast는 주소를 알면 decrypt할 수 있다.)

channel 만들기

  • 채널 만들기
    • 채널 이름을 정한다.
    • 그 이름을 passphrase로 해서 주소를 생성한다.
  • 채널 가입하기
    • 채널의 주소와 이름을 입력한다.
    • 이름으로 주소를 만들어서 입력된 주소와 같은지 확인한다.
  • 상대방의 주소로 메시지를 보내는 것과 거의 같은 형태로 메시지를 보낸다.
    • 메시지를 보낼 때 상대방 주소가 내 주소와 같을 것이다.
    • POW를 할지 말지 확인하지 않고 항상 POW를 한다.
    • ack를 empty string으로 보낸다.

메시지 보내기

  • (toAddress, fromAddress, subject, message)를 입력 받는다.
  • TTL은 기본 (4일-5분)과 (4일 + 5분)사이로 설정한다.
  • random으로 ackdata를 생성한다.
  • ('', toAddress, toRipe, fromAddress, subject, message, ackdata, sent time, last action time, 0, 'msgqueued', 0, 'sent', 2, TTL)
  • toAddress의 public key가 있는지 확인한다.
    • public key는 한번이라도 이 주소로 메시지를 보낸적이 있으면 그에 관련된 로컬에 저장되어 있거나 사용한적이 없으면 inv를 통한 내부적으로 inventory에 저장되어 있을 수 있다. 그러므로 두군데를 모두 확인해 보아야 한다.
    • 없으면 'msgqueued'를 'doingpubkeypow'로 바꾸고 inv(with getpubkey)를 요청한다.
    • 이전에 inv 요청을 했으면 'awaitingpubkey'로 바꾼다.
    • 있으면 'msgqueued'를 'doingmsgpow'로 변경한다.
  • public key로부터 POW를 진행한다.
    • 로컬 설정에 지정된 maxacceptablenoncetrialsperbyte나 maxacceptablepayloadlengthextrabytes를 넘어서는 값을 가지고 있으면 'doingmsgpow'를 'toodifficult'로 바꾸고 메시지를 보내지 않는다.
  • payload = encodeVarint(fromAddressVersionNumber) + encodeVarint(fromStreamNumber) + behaviour bitfield(1) + pubSigningKey + pubEncryptionKey + encodeVarint(noncetrialsperbyte) + encodeVarint(payloadlengthextrabytes) + toRipe + SIMPLE(0x02) + encodeVarint(len(messageToTransmit)) + messageToTransmit + encodeVarint(len(fullAckPayload)) + fullAckPayload + encodeVarint(len(signature)) + signature
    • fullAckPayload = (현재시간 + TTL) + 0x00000002 + encodeVarint(1) + encodeVarint(toStreamNumber) + ackdata
    • POW로 trialValue와 nonce를 얻은 후 앞에 nonce를 추가한다.
    • fullAckPayload를 가지고 object를 생성한다.
    • ack를 위한 data가 자체로 object가 되기 때문에 메시지를 받은 노드는 이 object를 그대로 리턴하면 된다.
    • signature는 아래의 방법으로 계산한다.
    • dataToSign = (현재시간 + TTL) + 0x00000002 + encodeVarint(1) + encodeVarint(toStreamNumber) + payload
    • private signing key로 dataToSign을 signing한다.
  • encrypted = payload를 public encryption key로 encrypt한다.
  • encryptedPayload = (현재시간 + TTL) + 0x00000002 + encodeVarint(1) + encodeVarint(toStreamNumber) + encrypted
  • POW로 trialValue와 nonce를 얻는다.
  • encryptedPayload = nonce + encryptedPayload
  • encryptedPayload로부터 inventory hash를 계산하고 inv(with msg)를 보낸다.
  • 메시지를 보내고 나면 'doingmsgpow'를 'msgsent'로 바꾼다.

broadcast 보내기

  • (fromAddress, subject, message)를 입력 받는다.
  • TTL은 기본 (4일-5분)과 (4일 + 5분)사이로 설정한다.
  • random으로 ackdata를 생성한다.
  • ('', '[Broadcast subscribers]', toRipe, fromAddress, subject, message, ackdata, sent time, last action time, 0, 'broadcastqueued', 0, 'sent', 2, TTL)를 로컬에 저장한다.
  • payload = (현재시간 + TTL) + 0x00000003 + broadcast version(5)을 숫자 인코딩한 값 + stream number를 숫자 인코딩한 값 + doubleHashOfAddressData의 뒤 32byte
  • dataToEncrypt = addressVersionNumber를 숫자 인코딩한 값 + streamNumber를 숫자 인코딩한 값 + behaviour bitfield(1) + public signing key + public encryption key + nonceTrialsPerByte를 숫자 인코딩한 값 + payloadLengthExtraBytes를 숫자 인코딩한 값 + 0x02 + message length를 숫자 인코딩한 값 + message
    • message = ('Subject:' + subject + '\n' + 'Body:' + message)
  • (payload + dataToEncrypt)를 private signing key로 signing을 해서 signature를 얻는다.
  • dataToEncrypt = dataToEncrypt + signature의 길이를 숫자 인코딩한 값 + signature
    • dataToEncrypt를 encryption할 때 사용하기 위해 doubleHashOfAddressData의 앞쪽 32byte를 private key로 하는 public/private key 쌍을 만든다. message를 encryption할 때 사용하는 것과 같은 알고리즘인 ECDSA로 public key를 만든다.
  • dataToEncrypt를 앞의 public key를 사용해서 ECDSA로 encryption을 한다. encryption된 값을 payload에 추가한다.
    • 받는 노드에서 주소를 알고 있다면 이 주소의 double hash를 통해 private key를 알아낼 수 있고 decrypt할 수 있게 된다.
  • POW를 실행하여 trialValue와 nonce를 얻는다.
  • payload의 앞에 nonce를 숫자로 인코딩한 값을 추가한다.
  • 현재까지의 전체 payload로부터 inventory hash를 만든다.
    • inventory hash = payload에 sha512를 두번 적용하여 나온 값의 앞 32byte
  • inventory hash를 키로하여 (objectType(3), streamNumber, payload, embeddedTime, doubleHashOfAddressData의 뒤쪽 32byte)를 로컬에 저장한다.
  • 앞에서 로컬에 저장한 값중 아래 내용을 변경한다.
    • last action time
    • broadcastqueued -> broadcastsent
    • 제일 앞에 비워두었던 값에는 inventory hash를 넣는다.

메시지에 파일 첨부하기

  • message와 파일의 구분을 "\n\n"으로 한다. 여러 파일을 첨부하는 경우에도 파일간의 구분을 "\n\n"으로 한다.
  • 파일의 data는 base64로 인코딩한다.
  • 파일이 이미지인 경우와 그외의 경우로 구분한다.
  • 이미지인 경우 아래와 같은 tag의 형태로 첨부한다: fileName, fileSize, fileName, filetype, data
    • <!-- Note: Image attachment below. Please use the right click "View HTML code ..." option to view it. -->
    • <!-- Sent using Bitmessage Daemon. https://github.com/Dokument/PyBitmessage-Daemon -->
    • Filename:%s
    • Filesize:%sKB
    • Encoding:base64
    • <center>
    • <div id="image">
    • <img alt = "%s" src='data:image/%s;base64, %s' />
    • </div>
    • </center>
  • 이미지가 아닌 경우: fileName, fileSize, fileName, filetype, data
    • <!-- Note: File attachment below. Please use a base64 decoder, or Daemon, to save it. -->
    • <!-- Sent using Bitmessage Daemon. https://github.com/Dokument/PyBitmessage-Daemon -->
    • Filename:%s
    • Filesize:%sKB
    • Encoding:base64
    • <attachment alt = "%s" src='data:file/%s;base64, %s' />

에러 메시지

  • fatal + ban time + inventory vector의 길이 + inventory vector + error text의 길이 + error text
  • inventory vector와 error text는 string 그대로 사용하고 나머지는 숫자 인코딩을 한다.

POW 계산하기

  • target = 2^64 / (requiredAverageProofOfWorkNonceTrialsPerByte(len(encryptedPayload) + 8 + requiredPayloadLengthExtraBytes + ((TTL(len(encryptedPayload)+8+requiredPayloadLengthExtraBytes))/(2 ** 16))))
  • initialHash = payload의 sha512
  • (nonce(8byte) + initialHash)를 sha512를 두번 적용하여 나온 값을 trialValue라 하자. nonce는 1부터 1씩 증가시켜서 계산한다.
  • trialValue가 target보다 작은 경우가 결과가 된다.
  • 이때의 (trialValue, nonce)를 결과로 내보낸다.

숫자 인코딩하기

  • 숫자 값이 어느 영역에 속하느냐에 따라 인코딩을 한다.
  • big-endian을 사용한다.
  • 0 <= ... < 253 : unsigned char(1)
  • < 65536(2^16) : 253(0xfd, unsigned char) + unsigned short(2)
  • < 4294967296(2^32) : 254(0xfe, unsigned char) + unsigned int(4)
  • < 18446744073709551616(2^64) : 255(0xff, unsigned char) + unsigned long long(8)

2016년 10월 14일 금요일

Electron Internals 요약

1. http://electron.atom.io/blog/2016/07/28/electron-internals-node-integration

Node와 GUI programming을 같이 사용하는 경우 Node 자체의 event message loop(libuv를 사용한다.)와 GUI 자체의 event loop 두개가 있게 되는 문제가 있다.  main thread는 한 loop에서만 동작해야 하기 때문이다.
이의 해결을 위한 첫 시도는 크로미움의 main process와 renderer process의 message loop를 libuv로 변경하는 것이었다. renderer process의 message loop를 변경하는 것은 간단했으나 main process의 message loop를 변경하는 것은 매우 어려웠다. 각각의 플랫폼마다 다른 message loop를 사용하기 때문에 이를 libuv에 전달해주는 것이 간단하지 않았다. 결국 매우 작은 주기의 timer를 GUI message loop에 추가하는 방법으로 문제를 해결하고자 하였다.
하지만, libuv가 발전하면서 backend fd라는 개념이 도입이 되었고 이것을 polling함으로서 libuv에서의 event에 노티를 받는 것이 가능해졌다. 그래서 backend fd를 polling하여 event가 발생하면 이를 크로미움의 message loop에 전달해주는 별도의 쓰레드를 만드는 것으로 변경하였다. 이 방법을 사용함으로서 크로미움과 노드의 코드를 변경할 필요가 없게 되었다.

2. http://electron.atom.io/blog/2016/08/08/electron-internals-using-node-as-a-library

- Node와 Electron은 빌드 시스템으로 GYP를 사용한다.
- Electron은 node의 빌드를 위해 node의 configure script를 사용하지 않고 자체 빌드 스크립트를 사용한다.
- Electron은 node를 shared library로 사용하고 node의 v8은 사용하지 않는다.
- Electron은 과거에는 node를 static library했다가 지금은 shared library로 빌드한다.
- Node의 native module들은 V8의 심볼이 필요한데 Electron은 node의 V8을 사용하지 않기 때문에 문제가 있다. 그래서 크로미움의 V8의 심볼들은 외부로 expose 하는게 필요하다.
- Electron은 node를 두가지 모드로 시작할 수 있다. standalone mode와 embedded mode

3. http://electron.atom.io/blog/2016/09/20/electron-internals-weak-references

Weak reference는 object가 가비지 콜렉트 되는 것과 상관없이 object를 사용할 수 있도록 해주는 object에의 reference이다.
자바스크립트에서 weak reference와 관련된 것은 WeakMap이다.
Remote module: RPC를 사용하여 render process에서 main process의 object를 사용할 수 있도록 해준다.
-- renderer process에서 object를 요청하면 main process에 메시지를 보낸다.
-- 메시지를 받은 main process는 map에 object를 생성하고 ID를 생성한다. 이 ID를 renderer process에 돌려준다.
-- remote module은 ID를 받으면 이것을 proxy object로 감싼다. renderer process는 이 proxy object를 사용한다. 사용이 끝나면 proxy object가 가비지 콜렉트 될 때 메시지를 main process에 보내 할당된 object도 가비지 콜렉트 될 수 있도록 한다.

같은 object를 여러번 요청하는 경우 메모리 낭비의 문제가 생길 수 있다. - 이의 해결책은?
- Cache: 같은 ID의 object가 이미 있는 경우 새로 생성하지 않고 기존의 object를 리턴한다.
- 이를 위해 WeakMap을 사용한다.

2016년 10월 11일 화요일

Alamofire 소스 분석 - 1

Alamofire 소스 분석해보기.

1. http://happyhourguide.blogspot.kr/2016/10/uiimaveview-extension.html
2. http://happyhourguide.blogspot.kr/2016/10/alamofire-2.html
3. http://happyhourguide.blogspot.kr/2016/12/alamofire-queue.html

https://github.com/Alamofire/Alamofire

* 사용자의 URL 입력을 내부에서 사용하기 위한 형태로 변환하기
url의 형태로 다음의 타입을 사용할 수 있다: String, NSURL, NSURLComponents, NSURLRequest. 이들은 모두 URLStringConvertible을 구현하고 있다. URLRequest 함수를 통해 앞의 타입을 NSMutableURLRequest(내부에 NSURL을 가지고 있는다)로 변환한다. ParameterEncoding.swift의 encode함수를 통해 url을 서버에 보내기 위한 형태로 변환한다. encode 함수는 URLRequestConvertible을 파라메터로 받는다.
URLStringConvertible -> NSMutableRequest -> URLRequestConvertible

- Alamofire.swift
request, upload, download의 함수를 통해 서버에 HTTP 요청을 할 수 있다. Manager를 통해 작업이 이루어진다. 리턴은 Request.
URLStringConvertible과 URLRequestConvertible을 정의한다.

- Manager.swift
Alamofire에서 사용되는 글로벌한 인스턴스를 가지고 있다.
NSURLSessionConfiguration: 기본 session configuration과 기본 헤더 값(Accept-Encoding, Accept-Language, User-Agent)으로 초기화.
SessionDelegate: session에 대한 delegate
생성시 NSURLSession 초기화하고 종료시 invalidateAndCancel()을 통해 실행중이던 테스크들을 정리한다.

사용자가 backgroundCompletionHandler를 통해 서버 요청이 끝났을 때 실행될 내용을 등록할 수 있다.(sessionDidFinishEventsForBackgroundURLSession에 handler를 등록, dispatch_async(dispatch_get_main_queue())를 통해 메인 쓰레드에서 동작하도록 한다.)

- ParameterEncoding.swift
parameter를 percent encoding하고 이를 URL request에 적용한다.
URL, URLEncodedInURL, JSON, PropertyList, Custom의 다섯가지 방법이 있다.
(URLEncodedInURL은 URL과 같이 인코딩을 하나 항상 url의 뒤에 붙인다.)
encode로 인코딩을 시작하고 queryComponents로 key/value를 나누고 escape로 percent-escaped string으로 바꾼다.

URL의 경우 method가 GET, HEAD, DELETE이면 parameter를 url의 뒤에 붙이고 아니면 body에 넣는다. body에 넣을 때는 헤더에 "Content-Type: application/x-www-form-urlencoded; charset=utf-8"도 같이 추가해 준다.
JSON의 경우 NSJSONSerialization을 사용하고 "Content-Type: application/json"을 헤더에 넣는다.
PropertyList의 경우 NSPropertyListSerialization을 사용하고 "Content-Type: application/x-plist"를 헤더에 넣는다.
Custom은 사용자가 직접 인코딩을 처리하도록 한다.

인코딩의 경우 다음과 같은 예가 있다:
parameter에서 key가 array일 경우는 다음처럼 바꾼다: ?key[]=value1&key[]=value2
nested key를 가지고 있는 경우는 다음처럼 바꾼다: ?key[k1]=value1&key[k2]=value2

percent-escaping에 string.stringByAddingPercentEncodingWithAllowedCharacters를 사용한다.



Solicit 소스 분석

https://github.com/mlalic/solicit

An HTTP/2 implementation in Rust.

- 개요
HttpConnect를 가지고 SimpleClient를 생성한다. 생성시 client preface를 서버에 보내고 또한 서버로부터 settings frame을 받는다. 이 작업이 끝나고 나면 request/get_response 함수의 호출을 통하여 서버로부터 응답을 받을 수 있다.
solicit는 request시 실제로 요청을 서버에 보낸다.(OkHttp나 hyper의 HTTP1.1구현의 경우는 request시 데이터만 만들어 놓고 요청을 서버에 보내지 않고 있다가 response를 받으려고 할 때 실제로 요청을 서버에 보낸다.)

- SimpleClient 만들기
SimpleClient::with_connector에 TlsConnector(HttpConnect를 구현하고 있음)를 넘겨줌. TlsConnector는 TCP를 연결(TcpStream::connect)하여 TcpStream을 생성하고 여기에 인증서를 합하여 SslStream을 생성함. 연결이 되고 나면 처음으로 SettingsFrame(client preface)를 서버에 보냄
첫 settings는 ENABLE_PUSH(0)만 서버에 보냄.

데이터 보내는데 사용하는 방법은 필요한 버퍼를 io::Cursor::new를 사용하여 할당하고, settings의 헤더와 데이터를 이 버퍼에 저장한 후 SslStream을 통해 이 버퍼의 내용을 보냄.
io::Cursor<Vec<u8>>는 FrameBuilder를 구현하고 있음.
frame header를 먼저 구성하여 버퍼에 쓰고 SettingsFrame에 설정한 HttpSetting을 하나씩 버퍼에 씀.
frame header는 (length, frame_type, flags, stream_id)의 FrameHeader로 되어 있음.


SslStream은 TransportStream을 구현하고 있음. 따라서, try_split를 통해 sender와 receiver를 구별.

ClientConnection을 HttpConnection과 DefaultSessionState을 가지고 생성.
결과적으로 SimpleClient는 ClientConnection과 host와 SslStream을 sender와 receiver로 나누어서 가지고 있게 됨.

처음으로 서버로부터 받아야 하는 패킷은 SettingsFrame임.
TransportStream은 SendFrame을 구현하고 있고 TransportReceiveFrame은 RecvFrame을 구현하고 있음.

- request 보내기

-- send_frame
io::Cursor를 사용해서 1024 크기의 버퍼를 할당한다.
보내려는 frame을 serialize하여 할당한 버퍼에 넣는다.
버퍼의 내용을 stream에 보낸다.

-- recv_frame
frame의 header만큼인 9byte를 read_exact를 사용해서 읽는다.
전체 frame을 위한 버퍼를 Vec::with_capacity를 사용해서 할당한다.
read_exact를 사용해서 frame의 나머지 부분을 읽는다.
읽은 raw frame으로 부터 frame type에 맞는 frame을 생성한다.

- stream 만들기
DefaultStream을 만든다. 처음 state는 StreamState::Open이다. body가 없는 경우 StreamState::HalfClosedLocal로 바뀐다.

- start_request
HalfClosedLocal이면 EndStream flag를 설정한다.
stream id와 Stream을 연결(HashMap)한다.
stream id는 client의 경우 1로 시작하고 2씩 증가한다.(홀수)
HttpConnectionSender를 통해 헤더의 key/value를 HPACK의 encoder를 통해 encoding하여 HeaderFrame을 만들어서 보낸다.
TODO: 멀티 헤더 지원 암함

- send_next_data

- simple.rs
http2를 사용할 때 사용자가 간단하게 사용할 수 있도록 해주는 SimpleClient가 있다. SimpleClient는 TransportStream을 sender와 receiver로 구별하고 내부에 가지고 있고 데이터를 보내고 받는데에 ClientConnection을 사용한다.
SimpleClient의 with_connector에 tcp연결을 하는 connector를 넘겨준다. 이의 구현은 ClearTextConnector와 TlsConnector가 있다.
서버에 요청은 request함수로 하고 get_response를 통해 응답을 받을 수 있다. 이에 대한 도움함수로 get과 post가 있다.

- client/mod.rs
암호화 없이 데이터를 주고 받는 CleartextConnector가 정의되어 있다. TCP 연결을 위해 HttpConnect를 구현한다.
CleartextConnector의 connect 구현을 보면 TcpStream::connect를 통해 TcpStream을 얻어오고 이를 ClientStream으로 감싸서 리턴한다. 이때 에러가 발생하면 io::Error를 그대로 리턴하지 않고 이를 CleartextConnectError로 감싸서 리턴한다.
서버와의 첫 연결시 client preface를 서버에 보내는 함수가 write_preface이다. 이 함수를 보면 Settings frame을 보낼 때 push를 disable해서 보낸다.(TODO: push 기능 지원 안함)
SimpleClient가 사용하는 ClientConnection이 정의되어 있다.
ClientConnection은 HttpConnection와 SessionState를 가지고 있다. start_request로 헤더를 보내고 send_next_data로 바디를 보낸다.
start_request는 stream id를 생성하고 HttpConnectionSender를 통해 헤더를 서버에 보낸다.
서버로부터 온 frame의 응답에 대한 처리를 위한 ClientSession이 있다.



- transport.rs
네트워크로 데이터를 보내기 위해 사용하는 TcpStream과 SslStream<TcpStream>가 TransportStream을 구현한다.
read_exact는 버퍼가 찰때까지 stream으로부터 데이터를 읽는다.
데이터를 보내기 위해 SendFrame을 구현하고 데이터를 받기 위해 ReceiveFrame을 구현한다.
여기서 데이터를 받을 때 직접 ReceiveFrame을 사용하지 않고 TransportReceiveFrame으로 한번 감싸서 사용한다. TransportReceiveFrame은 받은 frame을 가지고 있다.






Xi-Editor 소스 분석

https://github.com/google/xi-editor

front-end는 swift를 사용한 cocoa로 back-end는 rust로 되어있다.
(그냥 간단하게 맥쪽 코드는 swift라고 하고 rust쪽 코드는 rust라고 부르겠다.)

* Swift

- AppDelegate
CoreConnection을 초기화한다.
AppWindowController의 window를 화면에 띄운다.
AppWindowController가 실행되면 appWindowControllers 리스트에 등록하고 종료하면 리스트에서 제거한다.
rust로 부터 응답이 오면 실행되는 콜백을 CoreConnection에 등록한다: handleCoreCmd
메뉴 선택에 따른 연결 함수들이 여기에 있다.

- AppWindowController
첫 로딩시 "new_tab" 메시지를 rust에게 보낸다.
filename이 변경되면 window의 타이틀을 변경한다.
EditView와 ClipView의 constraint를 설정한다.
ScrollView의 frame이 바뀌거나 ScrollView.contentView의 bound가 바뀌면 EditView와 ShadowView의 scroll위치를 다시 지정한다.

- CoreConnection
NSTask를 사용하여 rust를 실행한다.
swift와 rust의 연결은 NSPipe를 사용해서
(swift의 writing -> rust의 standard input,
rust의 standard ooutput -> swift의 reading)로 연결한다.
rust로 보내는 메시지는 sync 또는 async로 보낼 수 있다.
sync의 경우는 semaphore를 사용해서 block되어 응답을 기다린다.
async의 경우는 응답이 오면 실행하기 위한 콜백을 등록한다. 콜백은 응답 json이 id를 가지고 있는 경우  실행된다.

- ShadowView
텍스트가 위(topShadow), 왼쪽(leadingShadow), 오른쪽(trailingShadow)을 벗어나는 경우 관련 shadow를 화면에 표시해준다.

- EditView
실제 텍스트를 보여주는 뷰

* Rust


AlamofireImage 소스 분석

https://github.com/Alamofire/AlamofireImage

UIImaveView의 extension.

  • 지정한 이미지를 보여주기 위한 extension
  • enum ImageTransition
    • UIView의 transition animation에 사용되는 값을 저장하고 있는다.
    • duration, animationOptions, animations, completion(기본은 nil)
    • completion을 사용하고 싶으면 custom을 사용해야 한다.
  • af_imageDownloader와 af_sharedImageDownloader
    • 이미지 다운로드시 사용하는 instance.
    • 보통은 ImageDownloader.default를 사용하는 af_sharedImageDownloader를 사용하면 되나, 별도의 ImageDownloader가 필요(different auth credential)한 경우에는 af_imageDownloader를 설정하여 이것이 사용되도록 한다.
    • 내부에서 값의 저장은 objc_getAssociatedObject와 objc_setAssociatedObject를 사용해서 key/value의 형태로 저장하고 있는다.
  • af_activeRequestReceipt
    • 현재 진행중인 request에 대한 정보를 가지고 있다. download 요청이 끝나면 nil로 설정된다.
    • request의 취소에 사용한다.
  • af_setImage
    • 이미지 로딩을 요청하는 함수. 1. 이전에 진행중인 이미지 요청이 있으면 취소한다. 1. 캐시가 있으면 캐시에 있는 것을 가져온다. 1. placeholder를 지정해 주었으면 이미지 다운로드가 완료될 때까지 이것을 이미지로 설정한다. 1. ImageDownloader를 통해 이미지를 다운로드 한다.
      • 각각의 request에 unique id를 할당하여 최신인지 아닌지를 파악한다.

ImageDownloader

  • 기본 캐시로 메모리는 20MB, 디스크는 150MB, 디스크 경로로는 "org.alamofire.imagedownloader"를 사용한다. - URLCache에 지정한다.
  • enum DownloadPrioritization을 통해 다운로드 우선순위를 지정한다: FIFO, LIFO
  • 동시에 다운로드 할 수 있는 수를 제한한다. 기본은 4 - maximumActiveDownloads
    • 제한 값이 넘어가면 queuedRequests에 request를 넣어두었다가 기존 request에의 응답이 오면 이 queue에서 request를 꺼내서 실행한다.
  • synchronizationQueue와 responseQueue로 작업을 진행한다.
    • request와 관련된 작업의 경우 쓰레드 문제가 발생하지 않도록 하기 위해 synchronizationQueue를 사용한다.
    • request에 대한 응답은 responseQueue에서 동작하도록 한다.
  • download
    1. 이미 진행중인 request이면 filter와 completion handler를 response handler에 추가한다.
    2. 캐시된 이미지가 있으면 이를 로드한다.
    3. credential이 지정되어 있으면 request에 넣어주고 이미지를 요청한다.
    4. image 응답시 imageResponseSerializer를 사용한다.
    5. 서버로부터 응답시 캐시에 이미지를 넣는다.

DataRequest에 extension을 구현한다.

  • 지원하는 이미지 타입은 acceptableImageContentTypes에 지정해 놓는다.
  • imageResponseSerializer
    • 다운로드한 data를 UIImage에 넣어서 image를 생성한다.
    • 지원하지 않는 이미지 타입인 경우 에러를 리턴한다.

AutoPurgingImageCache

  • in-memory image cache
  • 내부에 저장하는 이미지의 key로 request와 unique filter identifier를 사용한다.
  • 기본 memoryCapacity는 100MB로 설정하고 preferredMemoryUsageAfterPurge는 60MB로 설정한다.
  • 이미지는 CachedImage의 형태로 저장된다. - 내부에 마지막으로 접근한 시간을 기록하고 있는다.
  • 이미지를 추가할 때마다 currentMemoryUsage에 이미지 크기를 추가한다.
  • currentMemoryUsage가 memoryCapacity보다 커지면 내부의 이미지를 마지막으로 접근한 시간 기준으로 정렬한 다음 오래된 이미지를 제거한다. 이미지 제거시 currentMemoryUsage가 preferredMemoryUsageAfterPurge보다 작아지면 이미지 제거를 중단한다.

ImageFilter

  • 이미지 변경을 위한 필터들을 제공한다.
  • UIImage extension에서 제공하는 이미지 변경 함수들에 대한 필터 기능이라고 보면 된다.
  • protocol ImageFilter

UIImage extension

  • thread safe하게 UIImage를 초기화하기 위한 함수를 제공한다.
    • af_threadSafeImage
    • UIImage()의 앞뒤로 NSLock()의 lock/unlock를 사용한다.
  • UIImage에 stored property를 저장하기 위해 AssociatedObject를 사용한다.
    • struct AssociatedKey, objc_getAssociatedObject, objc_setAssociatedObject
  • inflation하기
    • cgImage?.dataProvider?.data
    • inflation을 여러번 반복하지 않기 위해 한번 inflation을 하고 나면 af_inflated를 AssociatedObject를 사용해서 true로 설정한다.
  • alpha정보 확인하기
    • cgImage?.alphaInfo
  • 아래의 함수들은 모두 새로운 UIImage를 리턴한다.
  • af_imageScaled
    • 지정한 크기로 이미지 스케일링한다.
    • UIGraphicsBeginImageContextWithOptions -> draw -> UIGraphicsGetImageFromCurrentImageContext -> UIGraphicsEndImageContext
  • af_imageAspectScaled
    • 비율을 유지하면서 스케일링한다.
    • fit(큰쪽 기준)과 fill(작은쪽 기준) 두가지로 구분되어 있다.
  • af_imageRounded
    • 이미지를 모서리가 둥글게 처리한다.
    • UIBezierPath를 사용해서 이미지 clipping을 하여 그 영역 안에 draw를 하도록 한다.
  • af_imageRoundedIntoCircle
    • 이미지를 원형으로 변경한다.
    • 이미지가 1:1이 아니면 먼저 af_imageAspectScaled(toFill)를 사용해서 1:1로 변경한다.
  • af_imageFiltered
    • CIFilter를 사용하여 이미지를 변경한다.

XCTest

  • Unit test
  • BaseTestCase
    • setUp and tearDown
  • expectation으로 새 기대값을 만들고, 테스트 하고자 하는 결과가 완료되면 fulfill을 불러준다.
  • waitForExpectations에서 fulfill이 되거나 timeout이 될 때까지 기다린다.
  • 원하는 결과값들은 테스트한다.

Building asynchronous views in SwiftUI 정리

Handling loading states within SwiftUI views self loading views View model 사용하기 Combine을 사용한 AnyPublisher Making SwiftUI views refreshable r...