ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Flutter] Android 14 - WebView가 하얗게 나온다?
    Flutter 2024. 3. 31. 06:57
    반응형

     
    글을 시작하기 전에, 찾은 자료와 코드를 보고 예측한 수준이기 때문에 추측이 완전히 틀렸다거나 약간 안 맞는다거나 할 수 있습니다. 그런 부분이 있다면 댓글로 알려주세요. 빠르게 수정하겠습니다..!
     

    Flutter에서 WebView가 안보여요

    Flutter의 WebView를 사용하다가 Android 13, Android 14 버전 (대략 Android 13부터 였던 것 같습니다.) 이후부터 이상한 현상이 있었습니다. 웹뷰를 가지고 있는 앱을 Background 상태로 내려놓고 다른 앱을 사용하다가, 다시 앱으로 돌아오게 되면 화면이 사라지는 현상이었습니다. 
     
    심지어 웹뷰 자체가 사라졌다기보다, 클릭을 하면 반응은 합니다. 디버깅을 해보면 다른 주소로 이동한다거나, 웹뷰 내의 컨텐츠는 클릭이 되고, 스크롤도 되는 것처럼 보였습니다. 뭔가 렌더링 자체만 안되는 것처럼 보였습니다. 그리고 해당 현상이 재현될 때 아래와 같이 콘솔 로그에 나오는 녀석들이 있었습니다.

    D/OpenGLRenderer( 8924): RenderThread::destroyRenderingContext()
    D/OpenGLRenderer( 8924): CacheManager.trimMemory(40)

     
    음? 내용만 봐선 (1) 메모리가 부족해서 (2) 렌더링을 해주는 컨텍스트가 날라간다로 보입니다.
     
    정확한 내용은 진짜로 디버깅하면서 가야하지만, Android 버전이 올라가면서 디바이스의 메모리 정책이 뭔가 좀 더 쌔진 것 같습니다. 실제로 커뮤니티에도 관련 문의가 많이 올라오네요.

     

    Flutter webview package와 갤럭시S23 안드로이드14 버전에서 심각한 버그가 있습니다.

    어디에 남겨야 할지 몰라 이곳에도 남깁니다. Flutter 공식 Webview 패키지인 webview_flutter를 갤럭시S23 시리즈의 안드로이드14 업데이트 버전 사용시 심각한 버그가 있습니다. webview_flutter 패키지를 사

    r1.community.samsung.com

     

    Flutter에서 WebView 사용하기

    기본적으로 Flutter는 webview_flutter라는 놈이 존재합니다. Flutter가 Android와 Swift를 Channeling해서 웹뷰를 쓸 수 있게끔 만들어준 내부 라이브러리로, 기본적으로 네이티브 웹뷰에서 사용할 수 있는 기능들을 지원합니다.
     
    혹은 대중적으로 많이 알려진 InAppWebView라는 라이브러리가 있는데, 이것도 webview_flutter만큼 개발이 잘 되어있고, 업데이트도 꾸준히 해줘서 많이 사랑받는 라이브러리입니다. 둘 중 하나를 사용하면 되겠네요.
     
    문제는 두 라이브러리 모두 아무런 조치를 안한다면 (혹은 낮은 버전을 사용했다면) 위의 문제를 안고 있다는 것입니다.

     

    webview_flutter | Flutter package

    A Flutter plugin that provides a WebView widget on Android and iOS.

    pub.dev

     

    flutter_inappwebview | Flutter package

    A Flutter plugin that allows you to add an inline webview, to use an headless webview, and to open an in-app browser window.

    pub.dev

    반응형

     

    위의 라이브러리로 하얗게 나오는 문제를 해결할 수 있을까?

    결론부터 얘기하면 webview_flutter는 확인하지 못했는데, flutter_inappwebview는 해당 이슈를 인지하고 5.8.0 버전부터 지원하기 시작했습니다.
     
    해결 방식이 조금 독특한데, 아래의 flutter_inappwebview 중 in_app_webview.dart에서 확인할 수 있습니다. 이 중에서 _createAndroidViewController라는 메서드가 핵심 키워드입니다.

     

    flutter_inappwebview/flutter_inappwebview_android/lib/src/in_app_webview/in_app_webview.dart at master · pichillilorenzo/flutte

    A Flutter plugin that allows you to add an inline webview, to use a headless webview, and to open an in-app browser window. - pichillilorenzo/flutter_inappwebview

    github.com

    AndroidViewController _createAndroidViewController({
    	required bool hybridComposition,
    	required int id,
    	required String viewType,
    	required TextDirection layoutDirection,
    	required Map<String, dynamic> creationParams,
    }) {
        if (hybridComposition) {
          return PlatformViewsService.initExpensiveAndroidView(
            id: id,
            viewType: viewType,
            layoutDirection: layoutDirection,
            creationParams: creationParams,
            creationParamsCodec: const StandardMessageCodec(),
          );
        }
        return PlatformViewsService.initSurfaceAndroidView(
          id: id,
          viewType: viewType,
          layoutDirection: layoutDirection,
          creationParams: creationParams,
          creationParamsCodec: const StandardMessageCodec(),
        );
    }

     
    여기서 hybrid Composition이라는 옵션에 따라 initExpensiveAndroidViewinitSurfaceAndroidView를 사용한 것을 볼 수 있습니다. 일단 결론부터 말하자면, initExpensiveAndroidView를 사용하면 위의 현상을 없앨 수 있습니다.
     
    다만 initSurfaceAndroidView를 사용하게 되면, Android 9, Android 10 등의 낮은 버전에서는 문제가 없지만, Android 12 버전 이상부터는 해당 현상이 발현하게 됩니다.
     
     

    그럼 initExpensiveAndroidView만 사용하면 되지 않나요?

     
    문제는 Android 9와 Android 10 등의 낮은 버전, 또는 성능이 낮기 때문에 해당 버전을 사용하는 low spec의 디바이스들은 웹뷰를 사용할 때 상대적으로 버벅이고 느려지는 현상이 발생합니다.
     
    그래서 만약 사용한다면, 해당 현상이 발생하는 Android 버전을 찾고 (아마 Android 11까지는 해당 현상이 발생하지 않았던 것으로 기억합니다.) 버전에 따른 분기 처리를 해야할 것입니다.
     
    즉 Android 11 이하는 hybridComposition을 사용하지 않고, Android 12 이상은 hybridComposition을 사용하도록 말이죠.
     
     

    그럼 왜 이런 현상이 발생할까요?

    기본적으로 Android의 PlatformView를 이용해서 렌더링하는데, SurfaceView 기반의 Texture란 녀석으로 PlatformView에 연결해서 그리거나 (Skia Engine이 SurfaceView를 이용해 rasterize) 좀 더 복잡한 방식으로 렌더링을 하는 등 다양한 방식으로 View를 그리게 됩니다.
     
    웹뷰의 경우에도 안드로이드 네이티브 웹뷰를 PlatformView 안에 넣고, 이것을 PlatformViewFactory라는 곳에 등록하고, 이 PlatformViewFactory를 특정 viewType과 함께 FlutterEngine에 등록하게 됩니다. 그래서 렌더링을 하는 방식에 아래의 3가지가 있다고 하는데,

    • Virtual Display (VD)
    • Hybrid Composition (HC)
    • Texture Layer Hybrid Composition (TLHC)

    단순한 렌더링 작업, 그래픽을 가속화하지 않아도 쉽게 그릴 수 있는, 예를 들어 Flutter의 container 하나 달랑 그리는 수준이면 VD로 그리고, 좀 더 복잡한 뷰를 표현해야할 때는 GPU->CPU->GPU로 번갈아가면서 그리는 HC를, 그리고 이 둘을 적당하게 섞어서 사용하는 TLHC로 표현한다고 합니다. 좀 더 자세한 내용은 아래의 글에서 확인할 수 있습니다.

     

    Android Platform Views

    Flutter makes it easy and fast to build beautiful apps for mobile and beyond - flutter/flutter

    github.com

     
    결국에 initExpensiveAndroidView를 사용하게 되면 그래픽 가속을 하게 되고, 이것은 웹뷰가 계속 렌더링되어 유지될 수 있도록하여, 즉 작업 상태를 유지하게 하여 백그라운드로 내려간다고 해도 메모리에서 trim되지 않게 되며 하얀 화면을 막을 수 있는 것으로 예상됩니다.
     
    다만 그래픽 가속은 결국 하드웨어 성능을 많이 요하기 때문에 low spec 디바이스에서는 버티지 못해서 버벅이는 현상이 발생하는 것으로 보입니다. 뭐가 됐던 간에, 직접 웹뷰를 구현하는 입장이라면, 우리도 똑같이 따라할 수 있다는 것입니다.

    // PlatformViewLink는 AndroidView 위젯이 아닌 AndroidViewController를 이용해 PlatformView를
    // 그리기 위한 좀 더 복잡한 형태의 위젯입니다.
    PlatformViewLink(
    	key: params.key,
        // ViewType은 PlatformViewFactory에 해당 뷰를 등록할 때 필요한 고유의 값으로,
        // unique 값으로 필수로 넣어야 합니다.
    	viewType: 'com.pichillilorenzo/flutter_inappwebview',
    	surfaceFactory: (
    	  BuildContext context,
    	  PlatformViewController controller,
    	) {
          // 아래에서 등록한 controller를 이용해 실제로 그리기 위한 위젯입니다.
    	  return AndroidViewSurface(
    	    controller: controller as AndroidViewController,
    	    gestureRecognizers: params.gestureRecognizers ??
    	        const <Factory<OneSequenceGestureRecognizer>>{},
    	    hitTestBehavior: PlatformViewHitTestBehavior.opaque,
    	  );
    	},
    	onCreatePlatformView: (PlatformViewCreationParams params) {
          // PlatformView가 실제로 Android 네이티브에 등록될 때, 어떤 controller로 등록할 지
          // 정해줍니다.
    	  return _createAndroidViewController(
    	    hybridComposition: useHybridComposition,
    	    id: params.id,
            // 요건 pichillilorenzo님이 특정 작업을 하기 위해 넣으신 것으로, 위의 viewType과 다릅니다.
    	    viewType: 'com.pichillilorenzo/flutter_inappwebview',
    	    layoutDirection: this.params.layoutDirection ??
    	        Directionality.maybeOf(context) ??
    	        TextDirection.rtl,
    	    creationParams: <String, dynamic>{
    	      'initialUrlRequest': this.params.initialUrlRequest?.toMap(),
    	      'initialFile': this.params.initialFile,
    	      'initialData': this.params.initialData?.toMap(),
    	      'initialSettings': settingsMap,
    	      'contextMenu': this.params.contextMenu?.toMap() ?? {},
    	      'windowId': this.params.windowId,
    	      'headlessWebViewId':
    	          this.params.headlessWebView?.isRunning() ?? false
    	              ? this.params.headlessWebView?.id
    	              : null,
    	      'initialUserScripts': this
    	              .params
    	              .initialUserScripts
    	              ?.map((e) => e.toMap())
    	              .toList() ??
    	          [],
    	      'pullToRefreshSettings': pullToRefreshSettings,
    	      'keepAliveId': this.params.keepAlive?.id
    	    },
    	  )
    	    ..addOnPlatformViewCreatedListener(params.onPlatformViewCreated)
    	    ..addOnPlatformViewCreatedListener((id) => _onPlatformViewCreated(id))
    	    ..create();
    	},
    );

     
    hybridComposition을 사용할 것인지 말 것인지는 AndroidController가 결정하고 그것을 실제로 그려주는 것은 PlatformViewLink입니다. PlatformViewLink를 통해 Flutter에서 위젯을 만들어주고, 이를 통해 Android의 PlatformView를 등록해 준다면 흰색 화면으로 바뀌는 것을 해결할 수 있습니다.
    물론, 위는 dart에서의 채널링을 준비하는 코드이고 안드로이드 네이티브 웹뷰 개발 후 PlatformView 등록은 별개로 작업해야 됩니다.

     

     

     

     

     


     

    요약하자면,

    1.  initExpensiveAndroidView를 사용해라. InAppWebView에서는 5.8.0 버전 이상부터 해당 기능이 포함되어 있으며, 이 버전부터는 hybridComposition = true가 디폴트다.
    2. 다만 low spec 디바이스에서는 웹뷰 성능이 크게 저하될 수 있으므로 hybridComposition을 사용하지 않게 하는 것이 좋을 것 같다.

    사실 여기서 추가적인 문제가 하나 있는데, initExpensiveAndroidView를 사용할 경우, 프레임 당 렌더링을 계속해서 그런 것인지 아래와 같은 현상이 발생하는 것을 목격했습니다.

    -> auto rolling carousel을 사용한다거나 InkWell 등의 Click Animation이 발동
    -> 웹뷰로 이동하고
    -> 뒤로가기 버튼으로 돌아왔을 때 (이 때 스와이프되면서 화면이 닫히는 애니메이션이 존재할 경우)
    -> 이전 프레임의 화면이 0.5초 정도 잠깐 보였다가 사라짐. (깜빡거리는 현상이 발생)

     
    위의 경우 해결책은 나중에 기회가 된다면 또 간단하게 작성해보겠습니다.

     

     

    2024.04.12 추가

    깜빡거리는 현상에 대한 이유를 찾은 것 같습니다! (아닐 수도 있고..) 아래의 글을 읽어보면, hybrid Composition은 Flutter의 각 프레임을 캡쳐해서 그리는 방식인데, 이것 때문에 위처럼 이전 프레임이 잠깐 나오는 것 같습니다. 이전 프레임이 등장하는 이유는 원인이 여러가지 있을 수 있을 것 같은데, 성능 상 해당 부분에 대한 렌더링이 우선 순위에서 밀린다거나, jank가 걸렸다던가 등..

    아래 글에서 자세하게 볼 수 있습니다.

     

    Hosting native Android views in your Flutter app with Platform Views

    Learn how to host native Android views in your Flutter app with Platform Views.

    docs.flutter.dev

     

    반응형
Designed and Written by keykat.