카테고리 없음

iOS용 Kotlin 컴파일러용 Parcelize 플러그인 작성

카카오블로거 2021. 10. 26. 19:33

이 기사에서는 Kotlin 컴파일러용 플러그인을 작성한 경험을 설명합니다. 내 주요 목표는 Android용 kotlin-parcelize 와 유사한 iOS용 플러그인(Kotlin/Native)을 만드는 것이었습니다 . 사실은 Android에서와 마찬가지로 iOS에서도 시스템에 의해 애플리케이션이 종료될 수 있다는 것입니다. 즉, 탐색 스택 및 기타 데이터를 저장해야 할 수도 있습니다. 이 작업을 수행한 결과 kotlin-parcelize-darwin 을 얻었 습니다 . 생성 및 사용에 대한 세부 정보 - 컷 아래.

Android에서 소포하기

이 기사에서는 iOS용 개발에 대해 설명하지만 Android용 Parcelable 인터페이스와 kotlin-parcelize 플러그인이 무엇인지 기억해 보겠습니다. Parcelable 인터페이스를 사용하면 Parcel 에서 구현 클래스 를 직렬화하여 바이트 배열로 표시할 수 있습니다. 또한 Parcel에서 클래스를 역직렬화하여 모든 데이터를 복구할 수 있습니다. 이 기능은 화면 상태를 기록하고 복원하는 데 널리 사용됩니다. 예를 들어 일시 중단된 응용 프로그램이 메모리 부족으로 인해 시스템에서 먼저 종료되었다가 다시 활성화되는 경우입니다. 

Parcelable 인터페이스를 구현하는 것은 어렵지 않습니다. 두 가지 주요 방법을 작성해야 합니다. 

  • writeToParcel (Parcel, ...) - 데이터를 Parcel에 씁니다. 
  • createFromParcel(소포) - 소포에서 읽습니다. 

정보 필드를 필드별로 작성한 다음 동일한 순서로 읽어야 합니다. 간단해 보이지만 상용구 코드를 작성하는 것은 지루해집니다. 또한 실수를 할 수 있으므로 Parcelable 클래스에 대한 테스트도 작성해야 합니다.

다행히 Kotlin 컴파일러용 kotlin-parcelize 플러그인이 있습니다. 활성화하면 @Parcelize 주석을 사용하여 Parcelable 클래스를 표시하기만 하면 됩니다. 그러면 플러그인이 자동으로 구현을 생성합니다. 이렇게 하면 적절한 상용구 코드를 작성하지 않아도 되며 컴파일 시 구현이 올바른지 확인할 수 있습니다.

iOS에서 Parcelize 사용하기

iOS 앱에도 앞서 언급한 기능이 있으므로 앱 상태를 저장하는 유사한 방법이 있습니다. 하나는 Android의 Parcelable 인터페이스와 매우 유사한 NSCoding 프로토콜을 사용하는 것 입니다. 클래스는 또한 두 가지 메서드를 구현해야 합니다. 

  • encode (with: NSCoder) - 객체를 NSCoder 로 인코딩합니다 
  • init? (coder: NSCoder) - NSCoder에서 객체를 디코딩합니다.

iOS용 코틀린 네이티브

Kotlin은 Android에 국한되지 않고 iOS용 Kotlin 네이티브 프레임워크와 다중 플랫폼 코드 를 작성하는 데 사용할 수 있습니다 . 그리고 iOS 애플리케이션도 시스템에 의해 종료될 수 있기 때문에 동일한 문제가 발생합니다. iOS용 Kotlin Native는 Objective-C와 양방향 호환성을 제공하므로 NSCoding 및 NSCoder를 사용할 수 있습니다. 

매우 간단한 데이터 클래스는 다음과 같습니다.

NSCoding 프로토콜의 구현을 추가해 보겠습니다.

간단해 보입니다. 컴파일을 시도해보자:

User 데이터 클래스를 사용하여 NSObject 클래스를 확장해 보겠습니다.

그리고 다시 컴파일되지 않습니다!

흥미로운. 컴파일러가 toString 메서드를 재정의하고 생성하려는 것처럼 보이지만 NSObject에서 상속하는 클래스의 경우 description 메서드를 재정의해야 합니다. 또한 상속을 사용해서는 안 됩니다. 상속을 사용하면 사용자가 자신의 Kotlin 클래스를 확장하지 못할 수 있기 때문입니다(다중 상속이 불가능하기 때문에).

iOS용 소포 가능

상속을 사용하지 않고 다른 솔루션이 필요합니다. Parcelable 인터페이스를 정의해 보겠습니다.

간단 해. Parcelable 클래스에는 NSCodingProtocol 인스턴스를 반환하는 코딩 메서드만 있습니다. 나머지는 프로토콜 구현에 의해 처리됩니다. 

이제 Parcelable 인터페이스를 구현하도록 User 클래스를 변경해 보겠습니다.

NSCoding 프로토콜을 구현하는 중첩 클래스 CodingImpl을 만들었습니다. encodeWithCoder 메서드는 변경되지 않은 상태로 유지되지만 initWithCoder의 상황은 조금 더 복잡합니다. NSCoding 프로토콜의 인스턴스를 반환해야 하지만 User 클래스는 더 이상 존재하지 않습니다. 중간 클래스인 일종의 해결 방법이 필요합니다.

DecodedValue 클래스는 NSCoding 프로토콜을 구현하고 일부 값 개체를 저장합니다. 이 클래스는 인코딩도 디코딩도 되지 않기 때문에 모든 메서드는 비어 있을 수 있습니다. 이제 User 클래스의 initWithCoder 메소드에서 사용할 수 있습니다.

테스트

모든 것이 올바르게 작동하는지 테스트를 작성해 보겠습니다. 

  1. 일부 데이터로 User 클래스의 인스턴스를 만듭니다.
  2. NSKeyedArchiver를 사용하여 인코딩하고 결과로 NSData를 얻습니다.
  3. NSKeyedUnarchiver를 사용하여 NSData를 디코딩합니다.
  4. 디코딩된 개체가 원본 개체와 동일한지 확인하십시오.

컴파일러 플러그인 작성

iOS용 Parcelable 인터페이스를 정의하고 User 클래스로 시도하고 코드를 테스트했습니다. 이제 Android에서 kotlin-parcelize를 사용할 때처럼 코드가 자동으로 생성되도록 Parcelable 구현을 자동화할 수 있습니다.

KSP( Kotlin Symbol Processing)를 사용할 수 없습니다. 기존 클래스를 변경할 수 없고 새 클래스만 생성할 수 있기 때문입니다. 따라서 유일한 해결책은 Kotlin 컴파일러용 플러그인을 작성하는 것입니다. 그러나 이것은 여전히 ​​문서가 없고 API가 불안정하기 때문에 그렇게 간단하지 않습니다. 여전히 컴파일러용 플러그인을 작성하려는 경우 다음 소스를 참조하는 것이 좋습니다.

플러그인은 kotlin-parcelize와 동일하게 작동합니다. 클래스는 Parcelable 인터페이스를 구현하고 @Parcelize 주석으로 표시해야 합니다. 컴파일되면 플러그인은 Parcelable 구현을 생성합니다. Parcelable 클래스는 다음과 같습니다.

플러그인 이름

플러그인 이름은 kotlin-parcelize-darwin입니다. "-darwin" 부분은 플러그인이 모든 Darwin(Apple) 플랫폼에서 작동해야 함을 의미하지만 지금은 iOS에만 관심이 있습니다.

Gradle 모듈

  1. Kotlin-parcelize-darwin은 우리가 필요로 하는 첫 번째 모듈입니다. 여기에는 컴파일러용 플러그인을 등록하고 두 개의 아티팩트를 참조하는 Gradle용 플러그인이 포함되어 있습니다. 하나는 Kotlin/Native 컴파일러 플러그인용이고 다른 하나는 다른 모든 플랫폼용 컴파일러 플러그인용입니다.
  2. kotlin-parcelize-darwin-compiler - Kotlin/Native 컴파일러용 플러그인 모듈.
  3. kotlin-parcelize-darwin-compiler-j는 네이티브가 아닌 컴파일러를 위한 플러그인 모듈입니다. Gradle 플러그인에서 필요하고 참조하기 때문에 필요합니다. 사실 이 모듈은 비네이티브 버전에서 아무것도 필요하지 않기 때문에 비어 있습니다.
  4. kotlin-parcelize-darwin-runtime - 컴파일러 플러그인에 대한 런타임 종속성을 포함합니다. 예를 들어 다음은 Parcelable 인터페이스와 @Parcelize 주석입니다.
  5. 테스트 - 컴파일러 플러그인에 대한 테스트를 포함하고 포함된 빌드 형태로 플러그인에 모듈을 추가 합니다 .

플러그인 설치 과정 

루트 build.gradle 파일에서:

프로젝트의 build.gradle 파일에서:

구현

Parcelable에서 코드를 생성하는 데는 두 가지 주요 단계가 있습니다. 다음이 필요합니다.

  1. 누락된 재미있는 코딩()에 대한 합성 스텁을 추가하여 코드를 컴파일 가능하게 만드십시오. Parcelable 인터페이스의 NSCodingProtocol 메소드.
  2. 이전 단계에서 추가한 스텁에 대한 구현을 생성합니다.

스텁 추가

이것은 SyntheticResolveExtension 인터페이스를 구현하는 ParcelizeResolveExtension 클래스를 사용하여 수행 됩니다. 매우 간단합니다. 클래스는 각 클래스에 대해 컴파일하는 동안 호출되는 getSyntheticFunctionNames 및 generateSyntheticMethods 메서드를 구현합니다.

보시다시피 먼저 Parcelize 로직을 현재 클래스에 적용할 수 있는지 확인해야 합니다. isValidForParcelize 함수는 다음을 위해 사용됩니다.

@Parcelize 주석이 있고 Parcelable 인터페이스를 구현하는 클래스만 처리합니다.

스텁 구현 생성

짐작하셨겠지만 플러그인 생성의 이 단계는 훨씬 더 복잡합니다. ParcelizeGenerationExtension 클래스는 IrGenerationExtension 인터페이스를 구현하는 이를 담당합니다 . 여기에는 단 하나의 메서드가 포함되어 있습니다.

제공된 IrModuleFragment에 포함된 모든 클래스를 살펴봐야 합니다. 이를 위해 ClassLoweringPass 에서 상속된 ParcelizeClassLoweringPass 클래스가 사용됩니다 . ParcelizeClassLoweringPass 클래스는 하나의 메서드만 재정의합니다.

수업을 진행하는 것은 어렵지 않습니다.

이러한 기본 단계 외에도 코드 생성 프로세스에는 몇 가지 더 많은 단계가 있습니다. 구현의 모든 세부 사항을 설명하지는 않을 것입니다. 왜냐하면 너무 많은 코드를 가져와야 하기 때문입니다. 대신 기본 호출을 간략하게 설명하고 생성된 코드를 직접 작성하면 어떻게 보일지 보여드리겠습니다. 나는 이것이 기사의 맥락에서 더 유용한 정보라고 생각합니다. 그러나 더 자세히 알고 싶다면 여기 에서 구현 세부 정보를 찾을 수 있습니다 .

따라서 먼저 Parcelize 논리를 현재 클래스(irClass)에 적용할 수 있는지 다시 확인해야 합니다.

그런 다음 중첩된 CodingImpl 클래스를 irClass에 추가하고 상위 유형(NSObject 및 NSCoding)을 정의하고 @ExportObjCClass 주석으로 표시합니다(런타임에 검색할 때 클래스를 사용할 수 있도록).

이제 CodingImpl 클래스에 기본 생성자를 추가해 보겠습니다. 하나의 인수인 data:Class만 필요하므로 데이터 필드, 속성 및 getter도 생성해야 합니다.

우리가 얻은 것은 다음과 같습니다.

NSCoding 프로토콜의 구현을 추가해 보겠습니다.

생성된 클래스는 이제 다음과 같습니다.

마지막으로 단순히 CodingImpl 클래스를 인스턴스화하여 coding() 메서드의 본문을 생성해야 합니다.

생성된 코드:

플러그인 사용

플러그인은 Kotlin에서 Parcelable 클래스를 작성할 때 사용됩니다. 일반적으로 화면 상태를 저장하는 데 사용됩니다. 플러그인을 사용하면 iOS에 의해 종료된 애플리케이션의 원래 상태를 복원할 수 있습니다. 또 다른 사용 사례는 Kotlin에서 구현될 때 탐색 스택을 유지하는 것입니다.

다음은 데이터를 저장하고 복원하는 방법을 보여주는 Kotlin에서 Parcelable을 사용하는 일반적인 예입니다.

다음은 iOS 애플리케이션에서 Parcelable 클래스를 인코딩 및 디코딩하는 방법의 예입니다.

Kotlin 멀티플랫폼에서 Parcelize

이제 Android용 kotlin-parcelize와 iOS용 kotlin-parcelize-darwin의 두 가지 플러그인이 있습니다. 그리고 우리는 둘 다 적용하고 공유 코드에서 @Parcelize를 사용할 수 있습니다!

공유 모듈의 build.gradle 파일은 다음과 같습니다.

이제 androidMain 및 iosMain 키트에서 Parcelable 인터페이스와 @Parcelize 주석에 액세스할 수 있습니다. commonMain에서 사용하려면 예상/실제를 사용하여 수동으로 정의해야 합니다.

commonMain으로 작성해 보겠습니다.

iosMain에서:

androidMain에서:

다른 모든 세트:

이제 commonMain에서 평소와 같이 Parcelize를 사용할 수 있습니다. Android용으로 빌드할 때 코드는 kotlin-parcelize 플러그인으로 처리되고 iOS용으로 빌드할 때는 kotlin-parcelize-darwin 플러그인에서 처리됩니다. 다른 플랫폼의 경우 Parcelable 인터페이스가 비어 있고 주석이 누락되기 때문에 아무 작업도 수행되지 않습니다.

결론

우리는 kotlin-parcelize-darwin 컴파일러 플러그인을 다뤘습니다. 구조와 작동 원리를 조사하고 Kotlin Native에 적용하는 방법, Kotlin Multiplatform에서 Android 플러그인 kotlin-parcelize와 친구를 만드는 방법, iOS 측에서 Parcelable을 사용하는 방법을 배웠습니다.

소스 코드는 GitHub  있습니다 . 플러그인은 아직 게시되지 않았지만 로컬 Maven 저장소에 게시하거나 Gradle Composite 빌드를 사용하여 이미 실험할 수 있습니다 .

저장소에는 Android 및 iOS 애플리케이션뿐만 아니라 공통 모듈이 있는 매우 간단한 예제 프로젝트  포함되어 있습니다 . 읽어주셔서 감사합니다. Twitter에서 저를 팔로우하는 것도 잊지 마세요 !