React Native disable Fresco image downsampling

박은정·2022년 8월 23일
0

리액트네이티브

목록 보기
9/27

원문출처

<Image/> 컴포넌트에 대해 Fresco (프레스코) 다운샘플링을 비활성화해야하며, Android/app/src/main/java/com/<project>/Main Application.java 안에서 프레스코를 초기화해야 한다고 읽었습니다.

  • Fresco > Resizing/Downsampling
  • Fresco > Configuring the Image Pipeline
  • RN issue report

Fresco > Resizing/Downsampling

이 섹션에서는 다음 용어를 사용합니다.

  • Scaling(스케일링) : 캔버스 작업이며 일반적으로 하드웨어를 가속시킵니다. 비트맵 자체는 항상 같은 크기지만 축소나 상향 조정됩니다. ScaleType을 참조하세요.

  • Resizing(크기조정/리사이징) : 소프트웨어에서 실행되는 파이프라인 작업입니다. 이렇게 하면 디코딩되기 전에 메모리에서 인코딩된 이미지가 변경됩니다. 디코딩된 비트맵은 원래 이미지보다 작습니다.

  • Downsampling(다운샘플링) : 소프트웨어에서 구현되는 파이프라인 작업이기도 합니다. 인코딩된 이미지를 새로 만드는 대신 픽셀의 하위 집합만 디코딩해서 출력 비트맵이 더 작아집니다.

Resizing

리사이징은 원본 파일을 수정하는 것이 아니라 디코딩되기 전에 메모리에서 인코딩된 이미지의 크기만을 조정합니다.

ImageRequest를 구성할 때 ResizeOptions 객체를 전달하기 위해 아래 코드를 작성합니다.

ImageRequest request = ImageRequestBuilder.newBuilderWithSource(uri)
    .setResizeOptions(new ResizeOptions(50, 50))
    .build();
mSimpleDraweeView.setController(
    Fresco.newDraweeControllerBuilder()
        .setOldController(mSimpleDraweeView.getController())
        .setImageRequest(request)
        .build()
);

리사이징에는 다음과 같은 몇 가지 제한이 있습니다

  • JPEG파일만 지원합니다.
  • 실제 리사이즈는 원래 크기의 1/8에 가장 가까운 크기로 수행됩니다.
  • 리사이징은 원래 이미지를 더 크게 만들 수 없고, 작게만 만들 수 있습니다. (실질적인 제한은 없습니다)

Downsampling

다운샘플링은 최근에 프레스코에 추가된 실험적인 특징입니다. 이미지 파이프라인을 구성하려면 이미지 파이프라인을 구성할 때 이 옵션을 명시적으로 활성화해야 합니다.

.setDownsampleEnabled(true)

이 옵션이 켜져 있으면 이미지 파이프라인은 크기를 조정하는 대신 이미지를 다운샘플링합니다. 위와 같이 각 이미지 요청에 대해 setResizeOptions 를 호출해야 합니다.

다운샘플링은 디코드 단계의 일부이기 때문에 크기가 조정되는 것보다는 일반적으로 더 빠릅니다. 또한 JPEG뿐만 아니라 PNG, 애니메이션을 제외한 WebP 이미지도 지원합니다.

지금은 안드로이드 4.4 (KitKat)의 경우 디코드가 진행되는 동안 리사이즈보다 메모리를 더 많이 사용합니다. 리사이징은 수 많은 이미지를 동시에 디코딩하는 앱에서만 문제가 되고, 이에 대한 해결책을 찾고향후 릴리스에서 기본값으로 설정될거라 희망합니다.

어떤걸 언제 사용해야 하나요?

이미지가 View보다 크지 않으면 리사이즈만 수행해야 합니다. 리사이즈가 더 빠르고 더 쉽게 코딩할 수 있으며 결과적으로 더 높은 품질로 출력됩니다.
물론 View보다 작은 이미지는 View보다 크지 않은 이미지의 하위집합이기 때문에 이미지를 크게 키워야하는 경우 스케일하지 말고 리사이즈작업을 해야 합니다.
이렇게 하면 더 나은 품질을 제공하지 않는 더 큰 비트맵에서 메모리가 낭비되지 않습니다.
하지만 로컬 카메라 이미지같이 View보다 훨씬 더 큰 이미지의 경우 라사이즈와 함께 스케일링도 하는 것이 좋습니다.

"훨씬 더 크다" 는 것이 정확히 무엇을 의미하는지 알려면, 이미지가 view보다 2배 이상 너비x높이인 전체 픽셀 수가 더 크면 리사이즈를 해야 합니다. 이는 거의 항상 카메라로 촬영한 로컬 이미지에서 적용됩니다.
예를 들어, 화면 크기가 1080X1920픽셀로 대략 2MP이고 카메라가 16MP인 디바이스는 디스플레이보다 8배 큰 이미지를 생성합니다. 이러한 경우 리사이즈해주는 것이 가장 좋습니다.

네트워크 이미지의 경우 표시할 크기와 가장 가깝게 이미지를 다운로드해야 합니다. 부적절한 크기의 이미지를 다운로드하는 것은 사용자의 시간과 데이터를 낭비하는 것입니다.

이미지가 View보다 클 때 리사이즈를 하지 않으면 메모리가 낭비되지만 성능 trade-off도 고려해야 합니다. 리사이즈는 추가 CPU 비용을 부과하는 것이 분명하지만 View보다 큰 이미지 크기를 리사이즈하지 않음으로써 더 많은 바이트가 GPU에 전송되어야 하며, 비트맵 캐시에서 이미지가 더 자주 제거되어 더 많은 디코딩이 발생합니다.
즉, 리사이즈하지 않으면 CPU 비용이 추가로 발생하기 때문에 silver bullet은 없으며 디바이스 특성에 따라 임계점이 존재하고, 임계점은 디바이스없이 리사이즈하는 것보다 더 성능이 좋아집니다.

예시

Fresco 쇼케이스 앱에는 placeholder, failure, retry image를 시연하는 ImagePipelineResizingFragment 가 있습니다.

Fresco > Configuring the Image Pipeline

대부분의 앱은 간단한 명령으로 프레스코를 완전히 초기화할 수 있습니다.

Fresco.initialize(context);

고급 사용자 지정이 필요한 앱의 경우, ImagePipelineConfig 클래스를 사용해서 제공합니다.

ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)
    .setBitmapMemoryCacheParamsSupplier(bitmapCacheParamsSupplier)
    .setCacheKeyFactory(cacheKeyFactory)
    .setDownsampleEnabled(true)
    .setWebpSupportEnabled(true)
    .setEncodedMemoryCacheParamsSupplier(encodedCacheParamsSupplier)
    .setExecutorSupplier(executorSupplier)
    .setImageCacheStatsTracker(imageCacheStatsTracker)
    .setMainDiskCacheConfig(mainDiskCacheConfig)
    .setMemoryTrimmableRegistry(memoryTrimmableRegistry)
    .setNetworkFetchProducer(networkFetchProducer)
    .setPoolFactory(poolFactory)
    .setProgressiveJpegConfig(progressiveJpegConfig)
    .setRequestListeners(requestListeners)
    .setSmallImageDiskCacheConfig(smallImageDiskCacheConfig)
    .build();
Fresco.initialize(context, config);

ImagePipelineConfig 객체를 Fresco.initialize 에 전달해야 합니다. 그렇지 않으면 Fresco는 사용자가 만든 구성대신 기본구성을 사용합니다.

Supplier의 이해

구성 빌더의 방법 중 일부는 인스턴스 객체보다는 인스턴스의 supplier의 argument를 사용합니다. 인스턴스의 supplier의 argument는 만들기 좀 복잡하지만, 앱이 실행되는 동안 동작을 변경할 수 있습니다. 우선 메모리캐시는 5분마다 supplier를 확인해야 합니다.

만약 매개변수를 동적으로 변경할 필요가 없는경우, 매번 동일한 객체를 리턴하는 supplier를 사용하면 됩니다.

Supplier<X> xSupplier = new Supplier<X>() {
  private X mX = new X(xparam1, xparam2...);
  public X get() {
    return mX;
  }
);
// when creating image pipeline
.setXSupplier(xSupplier);

(이하생략)

리액트네이티브 0.57버전에서 큰 사이즈의 Image에서 저품질 이슈

큰 번들 이미지를 로드할 때, resizeMethod='resize' 를 사용하면 품질이 매우 안좋습니다.
이것은 iOS 애뮬이나 기기가 아닌 안드로이드에서만 발생합니다. 안드로이드 8.1 애뮬레이터와 안드로이드 8.0을 탑재한 LG G6에서 테스트했습니다.

...
type Props = {};
export default class App extends Component<Props> {
  render() {
    return (
      <View style={styles.container}>
        {/*<Text style={styles.welcome}>Welcome to React Native!</Text>
        <Text style={styles.instructions}>To get started, edit App.js</Text>
        <Text style={styles.instructions}>{instructions}</Text>*/}
        <Image source={require('./assets/ELHall1.png')} resizeMethod="resize" />
      </View>
    );
  }
}
...

2020년 1월 업데이트

이 문제와 관련된 이슈는 RN이 아니라 Fresco이슈로 판명되었습니다. 이미지 다운샘플링을 비활성화 할 수 있는 유일한 방법은 DecodeProducer.java 에서 다운샘플링 코드를 제거하고 소스로부터 프레스코를 컴파일하는 것입니다.

프레스코를 컴파일하고 이미지 다운샘플링을 비활성화 하는 방법에 대한 자세한 지침을 살펴보세요.

프레스코에 버그가 있었고 MainApplication.java에서 ImagePipelineConfigMainPackageConfig 를 사용하는 프레스코 구성을 적용하지 않는 것을 확인되었습니다.
리액트 네이티브에는 기본적으로 다운샘플링이 비활성화되어있습니다. 프레스코가 이 문제를 해결할 때까지 이미지 다운샘플링을 비활성화하는 유일한 방법은 다운샘플 코드를 제거하고 원본에서 프레스코를 컴파일하는 것입니다.

추가적인 논의

// 변경전
if (mDownsampleEnabled || !statusHasFlag(status, Consumer.IS_RESIZING_DONE))

// 변경후
if (mDownsampleEnabled && !statusHasFlag(status, Consumer.IS_RESIZING_DONE))

위처럼 변경하고 MainApplication.java 안에서 구성만을 사용해서 이미지 다운샘플을 비활성화할 수 있습니다. 즉, 누구도 프레스코를 컴파일할 필요가 없고 프레스코 설정만 변경하면 됩니다.

// MainApplication.java

@Override
protected List<ReactPackage> getPackages() {
  Context context = getApplicationContext();

  ImagePipelineConfig frescoConfig = ImagePipelineConfig
    .newBuilder(context)
    .setDownsampleEnabled(false)
    .build();

  MainPackageConfig appConfig = new MainPackageConfig
    .Builder()
    .setFrescoConfig(frescoConfig)
    .build();

  @SuppressWarnings("UnnecessaryLocalVariable")
  List<ReactPackage> packages = new PackageList(this, appConfig).getPackages();
  // Packages that cannot be autolinked yet can be added manually here, for example:
  // packages.add(new MyReactNativePackage());
  return packages;
}

한 번만 변경하면 구성 코드가 작동해서 다운샘플링을 비활성화합니다. 해당 구성을 제거하거나 .setDoensampleEnabled(true) 를 설정하면 다운샘플을 올바르게 사용하도록 설정되지만, 이것또한 버그처럼 보입니다. 물론 크기를 조절할 수 있는게 이상적이어서 위처럼 다운샘플을 동적으로 하는 방법이 가능할지 모르겠습니다.

결론적으로, RN은 기본적으로 다운샘플링을 비활성화되어있습니다.

만약 다운샘플링을 활성화하고 싶다면 ReactAndroid/src/main/java/facebook/react/modules/frescoModule.java#L155 파일에서 .setDownSampleEnabled(false) 으로 설정하면 됩니다.

2020년 2월 업데이트

RN 0.61.5 프로젝트기반의react-native-community/cli 템플릿을 만들었고, 이것은 소스로부터 프레스코를 빌드하기 위해 수정이 필요합니다. react-native-community/cli 템플릿은 사용자 지정 프로젝트 이름과 원본에서 프레스코를 컴파일하는 데 필요한 변경사항으로 만들어진 새로운 리액트 네이티브 프로젝트를 쉽고 빠르게 만들 수 있습니다. Android NDK Revision 21도 사용하고 있으면, yarn 1.21을 사용해서 MacOS와 윈도우에서 테스트했습니다.

깃허브 레파지토리의 README에 자세한 설치 지침이 있습니다. yarn fresco-setup 명령어를 사용해서 프레스코를 복제 및 패치한 다음 Android NDK를 설치하고 Android NDK 경로로 Android/librarys/fresco/local.properties 를 생성해야 합니다.

추가적인 논의

아래와 같이 FaseImage를 테스트했을 때 더 나은 품질을 가집니다.

<FastImage source={require('./assets/ELHall1.png')} style={{height: '100%', aspectRatio: 2.5}} />

FastImage는 assets 번들의 이미지는 사용하지 않고 원격 이미지만 사용됩니다.
왜 0.56버전의 RN에선 Image가 잘 작동되고 0.57버전의 RN에서는 작동되지 않는지 궁급합니다.

또 어떤 사람은 FaseImage를 사용해도 여전히 저품질로 보인다고 합니다.

(참고자료 끝)


두 가지방법을 찾았지만 효과는 없었고 프레스코 이미지 다운샘플링을 비활성화할 수도 없었습니다.

  1. 다음과 같이 오버라이드에서 프레스코를 만들고 초기화합니다.
@Override
public void onCreate() {
  super.onCreate();

  Context context = getApplicationContext();
  ImagePipelineConfig config = ImagePipelineConfig.newBuilder(context)
    .setDownsampleEnabled(false)
    .build();
  Fresco.initialize(context, config);

  SoLoader.init(this, /* native exopackage */ false);
}
  1. getPackages에서 MainReactPackage를 사용해서 프레스코를 초기화합니다.
protected List<ReactPackage> getPackages() {
  Context context = getApplicationContext();
  ImagePipelineConfig frescoConfig = ImagePipelineConfig.newBuilder(context)
    .setDownsampleEnabled(false)
    .build();

  MainPackageConfig appConfig = new MainPackageConfig
    .Builder()
    .setFrescoConfig(frescoConfig)
    .build();

  return new ArrayList<>(Arrays.<ReactPackage>asList(
    new MainReactPackage(appConfig),
    new ReactNativeFirebaseAppPackage(),
    new FastImageViewPackage()
  ));
}

해결책: 프레스코 레포를 복제하고 다운샘플링 코드를 코멘트한 다음 프레스코를 컴파일하고 APK에 포함될 사용자 지정 바이너리를 만드는 것입니다.

물론 설명없이 프레스코가 행동을 바꾼것이지만, RN은 이 프레스코 기능을 활성화/비활성화하는 선택은 개발자에게 맡겨야합니다.

또한 이러한 동작의 변화는 iOS에서 당연히 일어나지 않는다는 것을 잊지마세요. 따라서 우리는 조치를 해줘야 되는데, 프레스코 구성은 리액트 네이티브에서 발생하기 때문에 저는 이 구성이 최종 RN 개발자에게 노출되어야 한다고 생각합니다.

참고로 나는 최신 RN 0.61.2로 프레스코 2.0.0을 컴파일할 수 있었고, DecodeProducer.java에서 다운샘플링 조건 검사를 코멘트 아웃해서 다운샘플링을 비활성화했습니다.

프레스코 레포 디렉터리에 local.properties 파일을 만들고 윈도우 64비트에 Android NDK Revision 19c x86_64을 사용했습니다.

ndk.dir=G:\\Dev\\Android\\android-ndk-r19c
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.configureondemand=true

프레스코 바이너리가 성공적으로 컴파일되었으며 이미지 다운샘플링은 다음을 실행해서 빌드된 RN앱에 대해서 비활성화되었습니다.

cd android
.\gradlew assembleDebug --include-build ..\fresco\

결론

오늘 알아본 내용으로는 리액트 네이티브에는 문제가 없지만 페이스북에서 만든 fresco 이미지 라이브러리에서 다운샘플링 이슈가 발생하는 것 같다.

다만 여기서 고민되는 부분은 UI/UX 디자인 이론과 실무에서 읽은 내용에서는 일부 아이폰에서 비율이 변경되었기 때문에 발생하는 다운샘플링인줄 알았는데 이번에 찾은 내용은 fresco 라이브러리의 이슈로 인해 안드로이드 기기에서 다운샘플링 이슈가 발생한다는 것이다.

@lytrax/react-native-fresco 템플릿을 사용해서 리액트네이티브 프로젝트를 만들면 된다고 하는데 레파지토리에서 적용하는데 필요한 파일들을 찾아서 기존 프로젝트에서 테스트해보려고 한다.

profile
새로운 것을 도전하고 노력한다

0개의 댓글