• Meet mergeable libraries
    • static library
      • object file을 모아놓은 것
      • 빌드 때 정적 링커가 사용되는 API들을 찾아서 바이너리에 복사한다.
      • 복사가 되기 떄문에 빌드 이후에는 라이브러리가 필요없다.
      • 라이브러리 코드가 바뀌거나 라이브러리가 추가되면 빌드 타임 전체가 느려진다.
    • dynamic library
      • dylib라고도 한다.
      • framework 타겟을 위한 바이너리 파일 타입
      • 실행파일에 직접 들어가지 않고, 정적 링커는 경로만 실행파일에 넣어놓는다.
      • Apple SDK 이외의 모든 dylib은 앱 번들에 내장되어야 한다.
      • 라이브러리가 추가 및 업데이트되어도 정적 링커가 코드 복사를 안해도 되므로 빌드에 이점이 있다.
      • 런타임에 동적 링커가 의존성을 찾아서 로딩해야 하므로 많이 쓸쓰록 메모리 소비량과 앱 실행시간이 늘어난다.
        • 애플 SDK는 수백개가 로딩되기도 하는데 이를 고려한 수많은 최적화가 되어 있다.
        • 하지만 앱에 임베딩된 SDK들에 해당하지는 않는 이야기다.
    • 정적 라이브러리와 동적 라이브러는 이런 상반된 특성을 가지고 있기 때문에 그동안은 앱에 맞는 구조를 찾을 수 있게 측정하기를 권장해왔다.
      • 하지만 mergeable libraries를 쓰면 이럴 필요가 없다.
      • mergeable libraries를 쓰면 양쪽 링킹 전략의 장점만을 취할 수 있다.
    • 원리
      • 정적 링커가 mergeable library를 보고 merge를 할 수 있다.
      • mergeable library가 되는 과정
        • dynamic library를 빌드할 때, 정적 링커가 메타데이터를 만들어서 바이너리에 넣는다.
          • 그래서 바이너리 전체 크기는 늘어난다.
        • 정적 링커는 이렇게 빌드된 라이브러리는 static library로 취급할 수 있게 된다.
        • 사용자가 이를 그냥 dynamic library로 보고 링크할 건지, 바이너리에 합칠지를 결정하면 정적 링커가 이를 수행한다. 이는 정적 링킹 과정과 비슷하다.
      • merge의 대상은 앱 같은 실행 파일일 수도 있고, 다른 dynamic library일 수도 있다.
      • merge를 하더라도 바이너리 타입은 유지된다.
    • Xcode 15부터 지원
      • 새로운 정적 링커를 통해서 지원되기 때문
      • 라이브러리에 -make_mergeable 링커 옵션을 줘서 빌드해야 한다.
      • 라이브러리를 쓰는 쪽에서는 -merger_library와 -merge_framework 옵션을 통해서 머지를 조정한다.
      • Xcode가 이런 옵션을 주는 부분을 조정하지만, 이러한 옵션이 사용되는 것을 빌드 로그를 통해서 볼 수 있다.
    • merge의 장점
      • 라이브러리와 메타데이터는 빌드할 때만 필요하기 때문에 실제 봐야할 건 최종 바이너리 크기다. 머지할 때 중복되는 컨텐츠를 효율적으로 제거할 수 있어서 바이너리 크기를 줄일 수 있다.
        • 문자열, 심볼 참조, Objective-C 셀렉터, objc_msgsend 스텁 등
      • 최종 바이너리 타입은 동일하기 때문에 기존 링크 최적화 기능을 그대로 활용할 수 있다.
      • 앱 실행할 때 로딩해야할 프레임워크 수가 줄어서 dyld와 커널이 할 일이 줄어든다.
      • 기존 구성에서 최소한의 수정으로 적용할 수 있다.
        • ex. 전체 의존성을 묶고 있는 dylib하나만 남기고 그걸 의존하게 해서 의존성 사슬을 단순화한다던지
  • Using mergeable libraries
    • automatic merging
      • 직접 의존성들을 전부 merge한다.
      • 앱 타겟에 특히 유용하다
        • 앱 바이너리에 라이브러리의 세그먼트들이 바로 링크된다.
        • 다만 라이브러리가 export하고 있던 심볼들도 그대로 export된다.
          • 이를 막기 위해서는 export 자체를 막거나
            • Other Linker Flags → “-Wl, -no_exported_symbols”
          • app extension등에서 사용할 엔트리 포인트가 필요하다면 export 리스트를 관리해야 한다.
            • Linking-General → Exported Symbol File
      • Create Merged Binary(MERGED_BINARY_TYPE) 빌드 설정을 업데이트해야 한다(Automatic)
        • Linking 쪽에 별도 섹션을 가지고 있다.(Linking-Mergeable Libraries)
    • manual merging
      • 특정 라이브러리만 선택해서 머지하기
        • 특정 의존성이 디스크에 남아있어야 할 때 유용
      • Create Merged Binary(MERGED_BINARY_TYPE) 빌드 설정을 업데이트해야 한다(Manual)
      • 개별 의존성들에 대해서 Build Mergeable Library옵션을 설정한다.
        • MERGEABLE_LIBRARY 설정에 대응
        • YES로 설정된 라이브러리면 Mergeable Library로 빌드되서 Merge가 됨
    • debug workflow
      • merge에는 시간이 걸리기 때문에 개발 단계에서 매번 하는 건 부담된다.
      • 그래서 디버그 모드에서는 merge가 일어나지 않고, 라이브러리의 심볼을 reexport하도록 한다.
        • 특정 dylib안에 있는 코드 구현이 마치 다른 곳에 구현된 것 처럼 보이게 해주는 옵션
        • 외부에서는 실제 dylib이 아닌 reexport된 모듈에만 의존해서 해당 API를 사용할 수 있다.
      • 실행될 때 dyld가 이 심볼들에 대한 redirection을 처리한다.
        • 즉, mergeable library들이 디스크에 있게 된다.
    • symbolication support
      • symbolication: 기계 명령을 원래 소스코드에 매칭하는 과정
        • 크래시 로그를 이해하고 프로파일링이나 디버깅하는데 유용
      • merge를 하더라도 소스 코드 위치 정보는 그대로 유지된다.
        • 다만 stack trace등에서 라이브러리 정보는 merge된 곳으로 뜬다.
          • 크래시 로그, instrument, debugger등에서 뜨는 정보가 영향을 받는다.
  • Considerations for mergeable libraries
    • mergeable library를 의존하고 있던 경우는 이를 최종 merge 타겟을 의존하는 것으로 바꿔야 한다.

      • 이미 디스크에 없기 때문에, 의존성을 못찾는다.
      • Build Phase에서 Link Binary with Libraries 에서 설정하기
    • Autolinking

      • 소스코드에서 import문을 봤을 때, 이 프레임워크의 의존성을 찾아서 링커에 자동으로 넘겨주는 옵션
        • 기본적으로 활성화
      • mergeable library에 대해서 autolinking을 쓰면, 동적 링킹에 이슈가 생길 수 있다.
      • 해결책은 역시 최종 merge 타겟으로 의존성을 바꾸는 것
    • 만약에 dlopen을 쓰는 경우도 merge된 프레임워크 경로를 명시해야 한다.

      void* handler = dlopen("@rpath/Merged.framework/Merged", RTLD_NOW) 
      
    • Resource Lookup

      • Bundle API는 런타임이 프레임워크의 번들을 찾아서 로딩하게 만든다.

        • 번들 구조를 신경쓰지 않고 프레임워크의 리소스를 다루게 해준다.
        let myBundle = Bundle(for: NSClassFromString("MergeableClass")!) 
        
        NSBundle *myBundle = [NSBundle bundleForClass: [MergeableClass class]];
        
      • iOS 12 이전까지는 이 과정에서 프레임워크의 바이너리를 필요로 했다.

      • iOS 12에서는 이 상황에서도 lookup을 할 수 있게 해주는 훅이 추가되었다.

    • Xcode 15 Linker

    • mergeable library배포하기

  • recommendation