Before 지역 코드 조회 region = kr 채팅 ID 조회 chatId = 123 채팅 초기화 채팅 ID 조회 chatId = 123 채팅 초기화 조합 kr.kmkim.com/chat/123#order=... region = kr 조합 kr.kmkim.com/chat/123#order=... 18
ms WebViewChromium.constructor 10 ms ① WebViewFactory.getProvider() ② WebViewChromium.init 98 ms 213 ms Load WebView APK Provider init Browser process startup Per-instance 59 ms 35 ms 144 ms 68 ms loadNativeLibrary 24 ms Finish browser process start Profile AwContents 96 ms 28 ms 67 ms · native object 29
ms WebViewChromium.constructor 10 ms ① WebViewFactory.getProvider() ② WebViewChromium.init 98 ms 213 ms 325 ms ≈ 20 frames 6.5 % 메인 스레드 점유 60fps 기준 ANR 입력 타임아웃 비중 325 / 16.7 ≈ 19.5 30
Start Main thread startUpWebView() Page Start 283 ms Start Chromium: total 41 ms Java object 9 ms WebView() 53 ms WebViewChromium.init 40 ms Per-instance 40 ms ` BG thread WebViewFactory.getProvider() 88ms WebView apk 38ms, Provider init 38ms 44
웹뷰 생성 val cached = WebViewHolder.webView ② 사전 생성 val webView = if (cached != null) { 캐싱된 웹뷰 사용 ?.baseContext = this (cached.context as? MutableContextWrapper) cached } else { WebView(this) // fallback } Trace.endSection() 50
사전 생성 val webView = loadUrl() 호출 및 브릿지 통신으로 미리 완성 페이지 진입 전 웹뷰 생성 URL 로딩 WebView(MutableContextWrapper(this)) webView.loadUrl("https://example.com") 페이지 진입 // Initialize Bridge … 바로 표시 // Bridge Communication … 54
열리는 과정 WebView index.html CDN 서버 index.html CDN 서버 CSS · JS 실행 · 렌더링 <html> <head> <!-- ... --> </head> 파싱 DOM 생성 파싱 <body> <div id="root"></div> </body> </html> div#root 70
열리는 과정 WebView DOM에 추가 div#root main.js CDN 서버 index.html CDN 서버 CSS · JS root.append( Header(), Messages(), Input() ) 컴파일 실행 header messages input 실행 · 렌더링 71
사전 다운로드 다운로드 여부 판단 파일 명이 해시값이므로, 메타데이터로 인덱싱 Release B 에셋 다운로드 무결성 검증 메타데이터 메타데이터 READY 8be2…07 main.js ↔ 8be2…07 a41d…9e styles.css↔a41d…9e 3f9a…e1 /chat/* ↔ 3f9a…e1 파일 시스템 저장 82
사전 다운로드 다운로드 여부 판단 에셋 다운로드 무결성 검증 customer-chat-assets / release A release B 메타데이터 메타데이터 3f9a…e1 3f9a…e1 8be2…07 8be2…07 a41d…9e a41d…9e READY 파일 시스템 저장 83
사용 준비 인덱스: 파일 접근을 위한, 런타임에 활용할 자료 구조 (원본 파일명 : File) 파일 시스템 메타데이터 인메모리 인덱스 main.js ↔ 8be2…07 main.js → File("/8be2…07") styles.css ↔ a41d…9e styles.css → File("/a41d…9e") … … /chat/* ↔ 3f9a…e1 /chat/* → File(/"3f9a…e1") 85
html 서빙 원본 html 상태 <head> <link rel="stylesheet" href="https://kmkim.com/styles.css"> </head> <body> <div id="root"></div> <script src="https://kmkim.com/main.js"> </script> </body> css, js 서빙 위해 변형 필요 91
savePort(socket.localPort) while (true) { serve(socket.accept()) } WebView 루프백 서버 } private fun serve(client: Socket) { if (active.size >= 8) return client.close() val req = parse(client) 동시 연결 8개 허용 if (req.hasBody) return reply(400) if (req.method !in GET_HEAD_OPTIONS) return reply(405) if (req.origin != expectedOrigin) return reply(404) val asset = index[req.path] ?: return reply(404) writeHeaders(200, asset) asset.file.inputStream().copyTo(client.out) } 105
savePort(socket.localPort) while (true) { serve(socket.accept()) } WebView 루프백 서버 부적절한 요청 거부 } private fun serve(client: Socket) { if (active.size >= 8) return client.close() val req = parse(client) if (req.hasBody) return reply(400) if (req.method !in GET_HEAD_OPTIONS) return reply(405) if (req.origin != expectedOrigin) return reply(404) val asset = index[req.path] ?: return reply(404) writeHeaders(200, asset) asset.file.inputStream().copyTo(client.out) } 요청에 Body 존재 GET, HEAD, OPTIONS 읽기 요청이 아닌 경우 Origin 비교 허용된 origin만 필터링 106
) savePort(socket.localPort) HTTP/1.1 200 OK while (true) { serve(socket.accept()) } Content-Type: text/javascript Cache-Control: public, max-age=31536000, immutable } Access-Control-Allow-Origin: * private fun serve(client: Socket) { if (active.size >= 8) return client.close() val req = parse(client) X-Content-Type-Options: nosniff if (req.hasBody) return reply(400) if (req.method !in GET_HEAD_OPTIONS) return reply(405) if (req.origin != expectedOrigin) return reply(404) val asset = index[req.path] ?: return reply(404) writeHeaders(200, asset) asset.file.inputStream().copyTo(client.out) } 루프백 서버 헤더 쓰기 파일 시스템 css/js 소켓 통해 전송 108
포트 스쿼팅 루프백 서버의 포트를 통해 다른 앱도 접근 가능 악성 웹 에셋 실행 우려 루프백 서버 타 앱의 포트 선점 회피 → 비어있는 랜덤 포트 사용 서버 먼저 열고, 포트 확정 후 URL 생성 타 Origin의 에셋 요청 방지 → Origin 확인, 허용 호스트 이외 모두 거부 파일 해시 이중 검증 → 다운로드 시점 + 실행 시점 112
integrity="sha256-8be2..07"> </script> 해시 이중 검사 sha256 = 8be2…07 CDN 서버 ? ① SHA-256 앱 디스크 루프백 서버 ② integrity ? WebView 릴리즈 저장 여부 실행 여부 파일에서 추출한 해시 == 예상 해시? 파일에서 추출한 해시 == integrity 해시? 113