MapView.pause() 이후에도 vsync 콜백이 계속 등록됨 (플러터 사용)

문의 시 사용하시는 SDK 버전 정보와 플랫폼(Android / iOS) 및 디벨로퍼스 앱ID를 알려주세요.


플러터에서 지도 앱 SDK를 사용합니다.

안녕하세요. Android KakaoMap SDK 사용 중 pause() 이후의 렌더 루프 동작에 대해
문의드립니다.

환경

  • SDK: com.kakao.maps.open:android:2.15.0
  • 기기: Galaxy A15 (SM-A155N), Android 16 (API 36), MediaTek MT6789
  • 구성: Flutter PlatformView 안에서 MapView를 사용합니다.
    빠른 화면 복귀를 위해 setFinishManually(true)로 두고, 인스턴스를 유지한 채
    탭이 가려질 때 MapView.pause(), 다시 보일 때 MapView.resume()을 호출합니다.

증상

지도를 다른 탭으로 가려 MapView.pause()가 호출된 뒤에도, SDK가
RenderCallback$OnVsync를 Choreographer.postVsyncCallback으로 계속 등록해
매 프레임 메인 스레드가 깨어납니다. GLSurfaceView의 GL 스레드도 계속
돕니다. 그 결과 지도가 화면에 전혀 보이지 않는 상태에서도 앱이 CPU
코어 1개의 약 24%를 계속 사용합니다(그중 메인 스레드가 약 16%).

pause()가 정상 호출된 것은 확인했습니다. 저희 로그에
map_view_pause reason=map_tab_active=false 가 찍히고, 이후 resume까지
추가 pause/resume 호출은 없습니다.

실측 근거

지도가 가려진 상태에서 앱 프로세스를 12~14초간 프로파일링했습니다.

adb shell “run-as sh -c ‘cd /data/data/ &&
simpleperf record -e cpu-clock:u -p --duration 14 -f 300 -g -o g.data’”
adb shell “run-as sh -c ‘cd /data/data/ &&
simpleperf report -i g.data --sort symbol -n --children’”

618 샘플의 포함 비용(children) 상위입니다.

16.18% android.view.Choreographer.doFrame
11.97% kakao::base::invoke::AsyncInvokableWorker::execute()
8.74% com.kakao.vectormap.graphics.gl.GLSurfaceView$GLThread.guardedRun
7.77% com.kakao.vectormap.internal.RenderCallback$OnVsync.onVsync
4.85% android.view.Choreographer.postVsyncCallback

라이브러리별로는 libK3fAndroid.so가 7.6%이고, 앱이 쓰는 UI 프레임워크
쪽 렌더 라이브러리는 2.6%에 불과합니다. 즉 이 소비는 저희 화면 렌더가
아니라 지도 SDK 경로에서 발생합니다.

시도해 본 것

pause()와 함께 MapView의 뷰 visibility를 GONE으로 내려봤습니다.
dumpsys activity top에서 com.kakao.vectormap.MapView{… G.E…} 로
GONE이 적용된 것까지 확인했지만, 위 프로파일은 그대로였습니다
(Choreographer.doFrame 17.18%, RenderCallback$OnVsync.onVsync 8.59%).
자식 KGLSurfaceView는 부모가 GONE인 동안에도 V로 남고 GL 스레드가
계속 돌았습니다.

문의

  1. MapView.pause()가 멈추는 범위가 정확히 어디까지인가요? vsync 콜백
    등록과 GL 스레드 루프는 pause() 대상이 아닌 것으로 보입니다.
  2. 인스턴스를 파괴하지 않고(finish 없이) 렌더 루프와 vsync 콜백을
    완전히 멈추는 정식 방법이 있나요? 화면 복귀 속도 때문에 finish 후
    재생성은 피하고 싶습니다.
  3. 만약 현재 SDK에 그 방법이 없다면, 지도를 오래 가려두는 화면 구성에서
    카카오가 권장하는 패턴은 무엇인가요?
  4. 2.15.0 이후 버전에서 이 동작이 변경된 이력이 있나요?

지도 앱 SDK를 사용 중입니다.

안녕하세요. 자세하게 문의주셔서 감사합니다.

  1. 우선, 말씀하신대로 MapView.pause() 로는 GLThread 렌더 루프와 Vsync 콜백에 영향을 주지 않습니다. 지도 엔진 내부에서 렌더링 되는 것 자체를 멈춥니다. 그래서, 지도 렌더링이 멈춘 상태에서도 화면을 회전하거나 전체 MapView 크기가 다른 View 에 의해 바뀌더라도 타이밍에 맞춰 회전되고 리사이즈 될 수 있는 이점이 있습니다.
  2. 현재로서는 지도종료 말고는 Vsync 콜백자체를 멈추는 방법은 API 에는 없습니다. 등록과 해제를 View 와 렌더링 사이클에 맞춰 엄격하게 맞춰주지 않으면 leak 이 발생하기 때문에 일반적인 API 로 열려있진 않습니다. 다만, 앱 사이클에 맞춰 직접 Vsync 를 제어하고 싶으시다면 아래와 같이 SDK 내부 Vsync 를 해제하시고 직접 Vsync 를 등록해서 제어하셔야 합니다.
  private Choreographer.VsyncCallback externalVsyncCallback;   // API 33+
  private Choreographer.FrameCallback externalFrameCallback;   // ~API 32

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      // ... 기존 코드 ...

      mapView.start(getMapLifeCycleCallback(), new KakaoMapReadyCallback() {
          @Override
          public void onMapReady(KakaoMap map) {
              kakaoMap = map;

              // 1) SDK 내부 vsync 콜백 제거 (중복 방지)
              KGLSurfaceView surfaceView = (KGLSurfaceView) mapView.getSurfaceView();
              GLMapRenderer renderer = (GLMapRenderer) surfaceView.getMapRenderer(); 
              renderer.removeVsyncCallback();

              // 2) 외부에서 vsync 콜백 등록
              startExternalVsync(surfaceView);
          }
      });
      mapView.setFinishManually(true);  // 3) !!! 꼭 MapView.start() 이후 호출 !!! 
  }

  private void startExternalVsync(final KGLSurfaceView surfaceView) {
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
          externalVsyncCallback = new Choreographer.VsyncCallback() {
              @Override
              public void onVsync(@NonNull Choreographer.FrameData frameData) {
                  // 다음 프레임에도 계속 받으려면 자신을 재등록
                  Choreographer.getInstance().postVsyncCallback(this);

                  surfaceView.requestRender();
              }
          };
          Choreographer.getInstance().postVsyncCallback(externalVsyncCallback);
      } else {
          externalFrameCallback = new Choreographer.FrameCallback() {
              @Override
              public void doFrame(long frameTimeNanos) {
                  Choreographer.getInstance().postFrameCallback(this);
                  surfaceView.requestRender();
              }
          };
          Choreographer.getInstance().postFrameCallback(externalFrameCallback);
      }
  }

  private void stopExternalVsync() {
      Choreographer c = Choreographer.getInstance();
      if (externalVsyncCallback != null) {
          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
              c.removeVsyncCallback(externalVsyncCallback);
          }
          externalVsyncCallback = null;
      }
      if (externalFrameCallback != null) {
          c.removeFrameCallback(externalFrameCallback);
          externalFrameCallback = null;
      }
  }

  @Override
  protected void onDestroy() {
      // 반드시 해제 (leak 방지) 
      // onDestroy() 에 넣어놓은 것은 단순히 예제입니다. 꼭, 앱 사이클에 맞춰 해제해야 합니다.
      stopExternalVsync();   
      super.onDestroy();
      mapView.finish();
  }
  1. 지도 화면이 오랫동안 안보이는 경우라면, 일반적으로 액티비티 혹은 프래그먼트 pause 사이클에 맞춰서 SurfaceView 가 onPause 가 불리면서 GLThread 까지 wait 되고 있습니다. 다만, 이런 라이프사이클과 별개로 MapView 만 visible 처리로 안보이는 상태라면 강제로 onPause 를 불러서 GLThread 까지 wait 상태로 만드는 2가지 방법이 있습니다.
// 1. 직접 onPause/onResume 호출하기
((KGLSurfaceView) mapView.getSurfaceView()).onPause();
((KGLSurfaceView) mapView.getSurfaceView()).onResume();

// 2. MapView 를 detach/attach 하기
아예 MapView 자체를 부모뷰로부터 remove 함으로써 GLThread 자체를 wait 상태로 멈출 수 있습니다. mapView.setFinishManually(true);(주의: MapView.start() 호출 이후 설정해야함) 로 설정되어 있고,  MapView 를 parent view 로부터 필요할 때마다 attach/detach 함으로써 GLThread 자체 쓰레드 상태를 제어하실 수 있습니다
  1. 2.15.0 이후, 위와 관련 된 내용의 변경사항은 없습니다.

대단히 감사합니다.. 꾸뻑^2

3번 문의내용에 추가 코멘트가 있어서 수정해서 다시 올려습니다.

참고 부탁 드립니다.