Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Bytecode Manipulation 으로 생산성 높이기

Avatar for Daekyu Daekyu
June 16, 2025

Bytecode Manipulation 으로 생산성 높이기

Droid Knights 2025 - Bytecode Manipulation 으로 생산성 높이기
github : https://github.com/bigstark/droidknights-2025-bytecode-manipulation/

Avatar for Daekyu

Daekyu

June 16, 2025
Tweet

Other Decks in Programming

Transcript

  1. 개발 생산성을 높이는 방법? • 최신 언어 및 프레임워크를 활용

    → Boilerplate 감소 • AI 도구 → 시간 절약 • 빌드 CI 자동화 및 최적화 → 시간 절약 • Lint, 테스트 자동화 → 유지보수 • 기타등등... (by chatgpt)
  2. 개발 생산성을 높이는 방법? • 최신 언어 및 프레임워크를 활용

    → Boilerplate 감소 • AI 도구 → 시간 절약 • 빌드 CI 자동화 및 최적화 → 시간 절약 • Lint, 테스트 자동화 → 유지보수 • 기타등등... (by chatgpt)
  3. 다음과 같은 요구사항이 있다고 가정한다면 ?? : 이 화면에서 클릭

    가능한 요소가 각각 몇 번 클릭되었는지 로그를 심고 싶어요.
  4. 다음과 같은 요구사항이 있다고 가정한다면 ?? : 이 화면에서 클릭

    가능한 요소가 각각 몇 번 클릭되었는지 로그를 심고 싶어요. 일단 몇 개인지 세어보자
  5. 다음과 같은 요구사항이 있다고 가정한다면 ?? : 이 화면에서 클릭

    가능한 요소가 각각 몇 번 클릭되었는지 로그를 심고 싶어요. 일단 몇 개인지 세어보자
  6. 다음과 같은 요구사항이 있다고 가정한다면 ?? : 이 화면에서 클릭

    가능한 요소가 각각 몇 번 클릭되었는지 로그를 심고 싶어요. 많아도 너무 많다!!
  7. override fun onCreate(savedInstanceState: Bundle) { // 초기화 로직 ... binding.button1.setOnClickListener

    { val count = it.tag as? Int ?: 0 it.tag = count + 1 Toast.makeText(this, “button click count: ${it.tag}”, Toast.LENGTH_SHORT).show() // button 1 이 클릭되었을 때 로직 실행 … } binding.button2.setOnClickListener { val count = it.tag as? Int ?: 0 it.tag = count + 1 Toast.makeText(this, “button click count: ${it.tag}”, Toast.LENGTH_SHORT).show() // button 2 가 클릭되었을 때 로직 실행 … } }
  8. override fun onCreate(savedInstanceState: Bundle) { // 초기화 로직 ... binding.button1.setOnClickListener

    { val count = it.tag as? Int ?: 0 it.tag = count + 1 Toast.makeText(this, “button click count: ${it.tag}”, Toast.LENGTH_SHORT).show() // button 1 이 클릭되었을 때 로직 실행 … } binding.button2.setOnClickListener { val count = it.tag as? Int ?: 0 it.tag = count + 1 Toast.makeText(this, “button click count: ${it.tag}”, Toast.LENGTH_SHORT).show() // button 2 가 클릭되었을 때 로직 실행 … } } 중복 코드 발생
  9. override fun onCreate(savedInstanceState: Bundle) { // 초기화 로직 ... binding.button1.setOnClickListener

    { increaseButtonClickCount(it) // button 1 이 클릭되었을 때 로직 실행 … } binding.button2.setOnClickListener { increaseButtonClickCount(it) // button 2 가 클릭되었을 때 로직 실행 … } } private fun increaseButtonClickCount(view: View) { val count = view.tag as? Int ?: 0 view.tag = count + 1 Toast.makeText(this, “button click count: ${view.tag}”, Toast.LENGTH_SHORT).show() }
  10. override fun onCreate(savedInstanceState: Bundle) { // 초기화 로직 ... binding.button1.setOnClickListener

    { ButtonClickUtils.increaseButtonClickCount(it) // button 1 이 클릭되었을 때 로직 실행 … } binding.button2.setOnClickListener { ButtonClickUtils.increaseButtonClickCount(it) // button 2 가 클릭되었을 때 로직 실행 … } } object ButtonClickUtils { fun increaseButtonClickCount(view: View) { val count = view.tag as? Int ?: 0 view.tag = count + 1 } }
  11. object ButtonClickUtils { fun View.setOnCustomClickListener(block: (View) -> Unit) { increaseButtonClickCount(this)

    } private fun increaseButtonClickCount(view: View) { val count = view.tag as? Int ?: 0 view.tag = count + 1 } } override fun onCreate(savedInstanceState: Bundle) { // 초기화 로직 ... binding.button1.setOnCustomClickListener { // button 1 이 클릭되었을 때 로직 실행 … } binding.button2.setOnCustomClickListener { // button 2 가 클릭되었을 때 로직 실행 … } }
  12. Boilerplate 코드를 줄이기 위해 • 반복된 코드가 있다면 함수를 정의하여

    호출한다. • 공통 유틸 객체를 활용한다. • 확장함수를 정의하여 호출한다.
  13. 하지만 위의 방법들은 • (만약 모든 버튼에 적용되어야 한다면) •

    작업 시 확장함수를 알아야하는 불편함이 존재 • 작업자의 실수에 의해 코드에 영향을 끼칠 수 있음
  14. 하지만 위의 방법들은 • (만약 모든 버튼에 적용되어야 한다면) •

    작업 시 확장함수를 알아야하는 불편함이 존재 • 작업자의 실수에 의해 코드에 영향을 끼칠 수 있음 • 위의 문제를 해결하면서도, 한 줄의 Boilerplate 도 제거할 수 있는 방법이 없을까요?
  15. KSP로 해볼 수 있지 않을까? • 프로세서는 소스 프로그램과 리소스를

    읽고 분석합니다. • 프로세서는 코드나 그 밖의 출력물을 생성합니다. • Kotlin Compiler 는 생성된 코드와 함께 소스 프로그램을 컴파일합니다. • 본격적인 컴파일러 플러그인과 달리, 프로세서는 코드를 수정할 수 없습니다. 언어 의미를 변경하는 컴파일러 플러그인은 때때로 매우 혼란을 초래할 수 있는데, KSP는 소스 프로그램을 읽기 전용으로 다룸으로써 이러한 문제를 피합니다.
  16. Bytecode Manipulation • 이미 컴파일된 클래스파일(.class) 을 대상으로 코드를 삽입하거나

    변형하는 과정 • AGP 의 Instrumentation API 를 통해 transform 가능 (by ASM)
  17. class ClickCountPlugin : Plugin<Project> { override fun apply(project: Project) {

    project.plugins.withType(AppPlugin::class.java) { // get androidComponents via project extension androidComponents.onVariants { variant -> variant.instrumentation.transformClassesWith( ClickCountClassVisitorFactory::class.java, InstrumentationScope.PROJECT ) { params -> // no action } } } } }
  18. class ClickCountPlugin : Plugin<Project> { override fun apply(project: Project) {

    project.plugins.withType(AppPlugin::class.java) { // get androidComponents via project extension androidComponents.onVariants { variant -> variant.instrumentation.transformClassesWith( ClickCountClassVisitorFactory::class.java, InstrumentationScope.PROJECT ) { params -> // no action } } } } } Plugin 선언
  19. class ClickCountPlugin : Plugin<Project> { override fun apply(project: Project) {

    project.plugins.withType(AppPlugin::class.java) { // get androidComponents via project extension androidComponents.onVariants { variant -> variant.instrumentation.transformClassesWith( ClickCountClassVisitorFactory::class.java, InstrumentationScope.PROJECT ) { params -> // no action } } } } } Instrumentation API
  20. class ClickCountPlugin : Plugin<Project> { override fun apply(project: Project) {

    project.plugins.withType(AppPlugin::class.java) { // get androidComponents via project extension androidComponents.onVariants { variant -> variant.instrumentation.transformClassesWith( ClickCountClassVisitorFactory::class.java, InstrumentationScope.PROJECT ) { params -> // no action } } } } } ASM 코드 진입
  21. Bytecode Manipulation - ASM • ASM은 범용 Java 바이트코드 조작

    및 분석 프레임워크 • 기존 클래스의 바이트코드를 수정하거나, 클래스 파일을 직접 바이너리 형태로 동적으로 생성하는 데 사용
  22. class ClassA { fun methodA() { log("Hello World droid knights

    2025!") } private fun log(message: String) { Log.v("TAG", message) } }
  23. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I 6: pop 7: return }
  24. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I 6: pop 7: return } ASM ClassReader ClassVisitor MethodVisitor ClassWriter
  25. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I 6: pop 7: return } ASM visitCode ClassReader ClassVisitor MethodVisitor ClassWriter
  26. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I 6: pop 7: return } ASM visitCode visitCode ClassReader ClassVisitor MethodVisitor ClassWriter
  27. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I 6: pop 7: return } ASM visitCode visitCode visitCode ClassReader ClassVisitor MethodVisitor ClassWriter
  28. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I 6: pop 7: return } ASM visitCode visitCode visitCode visitMethod ClassReader ClassVisitor MethodVisitor ClassWriter
  29. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I 6: pop 7: return } ASM visitCode visitCode visitCode visitMethod visitMethod ClassReader ClassVisitor MethodVisitor ClassWriter NEW
  30. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I 6: pop 7: return } ASM visitCode visitCode visitCode visitMethod visitMethod ClassReader ClassVisitor MethodVisitor ClassWriter NEW visitCode
  31. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I 6: pop 7: return } ASM visitCode visitCode visitCode visitMethod visitMethod ClassReader ClassVisitor MethodVisitor ClassWriter NEW visitCode visitVarInsn
  32. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I 6: pop 7: return } ASM visitCode visitCode visitCode visitMethod visitMethod ClassReader ClassVisitor MethodVisitor ClassWriter NEW visitCode visitVarInsn visitLdcInsn
  33. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I 6: pop 7: return } ASM visitCode visitCode visitCode visitMethod visitMethod ClassReader ClassVisitor MethodVisitor ClassWriter NEW visitCode visitVarInsn visitLdcInsn visitMethodInsn
  34. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I 6: pop 7: return } ASM visitCode visitCode visitCode visitMethod visitMethod ClassReader ClassVisitor MethodVisitor ClassWriter NEW visitCode visitVarInsn visitLdcInsn visitMethodInsn visitLdcInsn visitLdcInsn visitMethodInsn
  35. public final class com.bigstark.example.ClassA { public com.bigstark.example.ClassA(); Code: 0: aload_0

    1: invokespecial #8 // Method java/lang/Object."<init>":()V 4: return public final void methodA(); Code: 0: aload_0 1: ldc #13 // String Hello World droid knights 2025! 3: invokespecial #17 // Method log:(Ljava/lang/String;)V 6: ldc 8: ldc 10: invokestatic 13: return private final void log(java.lang.String); Code: 0: ldc #19 // String TAG 2: aload_1 3: invokestatic #25 // Method android/util/Log.v:(Ljava/lang/String;Ljava/lang/String;)I ASM visitCode visitCode visitCode visitMethod visitMethod ClassReader ClassVisitor MethodVisitor ClassWriter NEW visitCode visitVarInsn visitLdcInsn visitMethodInsn visitLdcInsn visitLdcInsn visitMethodInsn 바이트코드 삽입
  36. class ClassA { fun methodA() { log("Hello World droid knights

    2025!") } private fun log(message: String) { Log.v("TAG", message) } }
  37. class ClassA { fun methodA() { log("Hello World droid knights

    2025!") Log.v("TAG", "hello world") } private fun log(message: String) { Log.v("TAG", message) } } class ClassA { fun methodA() { log("Hello World droid knights 2025!") } private fun log(message: String) { Log.v("TAG", message) } }
  38. Method Call Stack class ClassA { fun methodA() { log("Hello

    World droid knights 2025!") } private fun log(message: String) { Log.v("TAG", message) } }
  39. Method Call Stack class ClassA { fun methodA() { log("Hello

    World droid knights 2025!") } private fun log(message: String) { Log.v("TAG", message) } }
  40. Method Call Stack class ClassA { fun methodA() { log("Hello

    World droid knights 2025!") } private fun log(message: String) { Log.v("TAG", message) } }
  41. Method Call Stack class ClassA { fun methodA() { log("Hello

    World droid knights 2025!") } private fun log(message: String) { Log.v("TAG", message) } }
  42. Method Call Stack class ClassA { fun methodA() { log("Hello

    World droid knights 2025!") } private fun log(message: String) { Log.v("TAG", message) } }
  43. 예시로 알아보는 Bytecode public final void someMethod(); Code: 0: aload_0

    1: getfield #53 // Field binding:Lcom/bigstark/example/databinding/ActivitySampleBinding; 15: getfield #62 // Field com/bigstark/example/databinding/ActivitySampleBinding.button:Landroid/widget/Button; 18: invokedynamic #108, 0 // InvokeDynamic #1:onClick:()Landroid/view/View$OnClickListener; 23: invokevirtual #93 // Method android/widget/Button.setOnClickListener:(Landroid/view/View$OnClickListener;)V 26: return
  44. 예시로 알아보는 Bytecode public final void someMethod(); Code: 0: aload_0

    1: getfield #53 // Field binding:Lcom/bigstark/example/databinding/ActivitySampleBinding; 15: getfield #62 // Field com/bigstark/example/databinding/ActivitySampleBinding.button:Landroid/widget/Button; 18: invokedynamic #108, 0 // InvokeDynamic #1:onClick:()Landroid/view/View$OnClickListener; 23: invokevirtual #93 // Method android/widget/Button.setOnClickListener:(Landroid/view/View$OnClickListener;)V 26: return ALOAD 0
  45. 예시로 알아보는 Bytecode public final void someMethod(); Code: 0: aload_0

    1: getfield #53 // Field binding:Lcom/bigstark/example/databinding/ActivitySampleBinding; 15: getfield #62 // Field com/bigstark/example/databinding/ActivitySampleBinding.button:Landroid/widget/Button; 18: invokedynamic #108, 0 // InvokeDynamic #1:onClick:()Landroid/view/View$OnClickListener; 23: invokevirtual #93 // Method android/widget/Button.setOnClickListener:(Landroid/view/View$OnClickListener;)V 26: return GETFIELD this.binding
  46. 예시로 알아보는 Bytecode public final void someMethod(); Code: 0: aload_0

    1: getfield #53 // Field binding:Lcom/bigstark/example/databinding/ActivitySampleBinding; 15: getfield #62 // Field com/bigstark/example/databinding/ActivitySampleBinding.button:Landroid/widget/Button; 18: invokedynamic #108, 0 // InvokeDynamic #1:onClick:()Landroid/view/View$OnClickListener; 23: invokevirtual #93 // Method android/widget/Button.setOnClickListener:(Landroid/view/View$OnClickListener;)V 26: return GETFIELD this.binding
  47. 예시로 알아보는 Bytecode public final void someMethod(); Code: 0: aload_0

    1: getfield #53 // Field binding:Lcom/bigstark/example/databinding/ActivitySampleBinding; 15: getfield #62 // Field com/bigstark/example/databinding/ActivitySampleBinding.button:Landroid/widget/Button; 18: invokedynamic #108, 0 // InvokeDynamic #1:onClick:()Landroid/view/View$OnClickListener; 23: invokevirtual #93 // Method android/widget/Button.setOnClickListener:(Landroid/view/View$OnClickListener;)V 26: return GETFIELD binding.button
  48. 예시로 알아보는 Bytecode public final void someMethod(); Code: 0: aload_0

    1: getfield #53 // Field binding:Lcom/bigstark/example/databinding/ActivitySampleBinding; 15: getfield #62 // Field com/bigstark/example/databinding/ActivitySampleBinding.button:Landroid/widget/Button; 18: invokedynamic #108, 0 // InvokeDynamic #1:onClick:()Landroid/view/View$OnClickListener; 23: invokevirtual #93 // Method android/widget/Button.setOnClickListener:(Landroid/view/View$OnClickListener;)V 26: return INVOKEDYNAMIC onClick lambda
  49. 예시로 알아보는 Bytecode public final void someMethod(); Code: 0: aload_0

    1: getfield #53 // Field binding:Lcom/bigstark/example/databinding/ActivitySampleBinding; 15: getfield #62 // Field com/bigstark/example/databinding/ActivitySampleBinding.button:Landroid/widget/Button; 18: invokedynamic #108, 0 // InvokeDynamic #1:onClick:()Landroid/view/View$OnClickListener; 23: invokevirtual #93 // Method android/widget/Button.setOnClickListener:(Landroid/view/View$OnClickListener;)V 26: return
  50. 예시로 알아보는 Bytecode public final void someMethod(); Code: 0: aload_0

    1: getfield #53 // Field binding:Lcom/bigstark/example/databinding/ActivitySampleBinding; 15: getfield #62 // Field com/bigstark/example/databinding/ActivitySampleBinding.button:Landroid/widget/Button; 18: invokedynamic #108, 0 // InvokeDynamic #1:onClick:()Landroid/view/View$OnClickListener; 23: invokevirtual #93 // Method android/widget/Button.setOnClickListener:(Landroid/view/View$OnClickListener;)V 26: return INVOKEVIRTUAL setOnClickListener
  51. 예시로 알아보는 Bytecode public final void someMethod(); Code: 0: aload_0

    1: getfield #53 // Field binding:Lcom/bigstark/example/databinding/ActivitySampleBinding; 15: getfield #62 // Field com/bigstark/example/databinding/ActivitySampleBinding.button:Landroid/widget/Button; 18: invokedynamic #108, 0 // InvokeDynamic #1:onClick:()Landroid/view/View$OnClickListener; 23: invokevirtual #93 // Method android/widget/Button.setOnClickListener:(Landroid/view/View$OnClickListener;)V 26: return INVOKEVIRTUAL setOnClickListener
  52. 오늘의 목표 • 자동으로 클릭을 집계하는 코드를 작성 • 단,

    기존 코드는 건드리지 않고 오로지 Bytecode Manipulation 으로 코드 삽입
  53. 오늘의 목표 • 자동으로 클릭을 집계하는 코드를 작성 • 단,

    기존 코드는 건드리지 않고 오로지 Bytecode Manipulation 으로 코드 삽입 • 어디에 코드를 삽입할 것인가? • 어떤 코드를 삽입할 것인가?
  54. override fun onCreate(savedInstanceState: Bundle) { binding.button.setOnClickListener { // 자동으로 삽입되고

    싶은 부분 val count = it.tag as? Int ?: 0 it.tag = count + 1 Log.v(“TAG”, “count: ${it.tag}”) // 기존 로직 시작 Toast.makeText(this, “Hello World Droid knights!”, Toast.LENGTH_SHORT).show() } } 자동으로 코드 삽입 희망
  55. 오늘의 목표 • 자동으로 클릭을 집계하는 코드를 작성 • 단,

    기존 코드는 건드리지 않고 오로지 Bytecode Manipulation 으로 코드 삽입 • 어디에 코드를 삽입할 것인가? → onClick 람다의 최상단 • 어떤 코드를 삽입할 것인가? → 클릭을 집계하는 코드
  56. override fun onCreate(savedInstanceState: Bundle) { binding.button.setOnClickListener { // 코드를 삽입하고

    싶은 부분 // 아래는 버튼이 클릭됐을 때의 로직 수행 } } 이미지 영역 블랙 상자 지우고 사용하세요 오른쪽의 코드를 Bytecode 로 변환하면
  57. override fun onCreate(savedInstanceState: Bundle) { binding.button.setOnClickListener { // 코드를 삽입하고

    싶은 부분 // 아래는 버튼이 클릭됐을 때의 로직 수행 } } 이미지 영역 블랙 상자 지우고 사용하세요 오른쪽의 코드를 Bytecode 로 변환하면 this.binding ALOAD 0 GETFIELD
  58. override fun onCreate(savedInstanceState: Bundle) { binding.button.setOnClickListener { // 코드를 삽입하고

    싶은 부분 // 아래는 버튼이 클릭됐을 때의 로직 수행 } } 이미지 영역 블랙 상자 지우고 사용하세요 오른쪽의 코드를 Bytecode 로 변환하면 this.binding.button GETFIELD
  59. override fun onCreate(savedInstanceState: Bundle) { binding.button.setOnClickListener { // 코드를 삽입하고

    싶은 부분 // 아래는 버튼이 클릭됐을 때의 로직 수행 } } 이미지 영역 블랙 상자 지우고 사용하세요 오른쪽의 코드를 Bytecode 로 변환하면 onClick lambda INVOKEDYNAMIC
  60. override fun onCreate(savedInstanceState: Bundle) { binding.button.setOnClickListener { // 코드를 삽입하고

    싶은 부분 // 아래는 버튼이 클릭됐을 때의 로직 수행 } } 이미지 영역 블랙 상자 지우고 사용하세요 오른쪽의 코드를 Bytecode 로 변환하면 lambda reference
  61. class ClickCountClassVisitor(apiVersion: Int, cv: ClassVisitor) : ClassVisitor(apiVersion, cv) { private

    var countableMethods = mutableSetOf<CountableMethod>() override fun visitMethod( access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>? ): MethodVisitor { return ClickCountLambdaMethodVisitor( api = api, next = super.visitMethod(access, name, descriptor, signature, exceptions) ) { name, descriptor -> countableMethods.add(CountableMethod(name, descriptor)) } } }
  62. class ClickCountClassVisitor(apiVersion: Int, cv: ClassVisitor) : ClassVisitor(apiVersion, cv) { private

    var countableMethods = mutableSetOf<CountableMethod>() override fun visitMethod( access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>? ): MethodVisitor { return ClickCountLambdaMethodVisitor( api = api, next = super.visitMethod(access, name, descriptor, signature, exceptions) ) { name, descriptor -> countableMethods.add(CountableMethod(name, descriptor)) } } } onClick Lambda 찾는 MethodVisitor 생성
  63. class ClickCountLambdaMethodVisitor( api: Int, next: MethodVisitor, private val callback: (lambdaName:

    String, lambdaDescriptor: String) -> Unit ) : MethodVisitor(api, next) { override fun visitInvokeDynamicInsn( name: String?, descriptor: String?, bootstrapMethodHandle: Handle?, vararg bootstrapMethodArguments: Any? ) { super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, *bootstrapMethodArguments) if (name == "onClick" && descriptor?.contains("Landroid/view/View\$OnClickListener;") == true) { bootstrapMethodArguments.forEach { if (it is Handle && it.desc.contains("Landroid/view/View;")) { callback.invoke(it.name, it.desc) } } } } }
  64. class ClickCountLambdaMethodVisitor( api: Int, next: MethodVisitor, private val callback: (lambdaName:

    String, lambdaDescriptor: String) -> Unit ) : MethodVisitor(api, next) { override fun visitInvokeDynamicInsn( name: String?, descriptor: String?, bootstrapMethodHandle: Handle?, vararg bootstrapMethodArguments: Any? ) { super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, *bootstrapMethodArguments) if (name == "onClick" && descriptor?.contains("Landroid/view/View\$OnClickListener;") == true) { bootstrapMethodArguments.forEach { if (it is Handle && it.desc.contains("Landroid/view/View;")) { callback.invoke(it.name, it.desc) } } } } } 해당 람다의 이름이 onClick 이면
  65. class ClickCountLambdaMethodVisitor( api: Int, next: MethodVisitor, private val callback: (lambdaName:

    String, lambdaDescriptor: String) -> Unit ) : MethodVisitor(api, next) { override fun visitInvokeDynamicInsn( name: String?, descriptor: String?, bootstrapMethodHandle: Handle?, vararg bootstrapMethodArguments: Any? ) { super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, *bootstrapMethodArguments) if (name == "onClick" && descriptor?.contains("Landroid/view/View\$OnClickListener;") == true) { bootstrapMethodArguments.forEach { if (it is Handle && it.desc.contains("Landroid/view/View;")) { callback.invoke(it.name, it.desc) } } } } } 해당 method name 을 callback
  66. class ClickCountClassVisitor(apiVersion: Int, cv: ClassVisitor) : ClassVisitor(apiVersion, cv) { private

    var countableMethods = mutableSetOf<CountableMethod>() override fun visitMethod( access: Int, name: String?, descriptor: String?, signature: String?, exceptions: Array<out String>? ): MethodVisitor { return ClickCountLambdaMethodVisitor( api = api, next = super.visitMethod(access, name, descriptor, signature, exceptions) ) { name, descriptor -> countableMethods.add(CountableMethod(name, descriptor)) } } } 람다 메소드 이름 캐시
  67. // ClickCountClassVisitor 내부 override fun visitMethod( access: Int, name: String?,

    descriptor: String?, signature: String?, exceptions: Array<out String>? ): MethodVisitor { countableMethods.find { it.name == name && it.descriptor == descriptor }?.let { val methodType = Type.getMethodType(it.descriptor) val viewVarIndex = methodType.argumentTypes.indexOfFirst { type -> type.descriptor == "Landroid/view/View;" }.takeIf { index -> index >= 0 } ?: 0 return IncreaseCountMethodVisitor( api = api, next = super.visitMethod(access, name, descriptor, signature, exceptions), viewVarIndex = viewVarIndex ) } // ClickCountLambdaMethodVisitor 생성 } } 람다 method 에 visit 하면 MethodVisitor 생성
  68. class IncreaseCountMethodVisitor( api: Int, next: MethodVisitor, private val viewVarIndex: Int

    ) : MethodVisitor(api, next) { override fun visitCode() { super.visitCode() // 바이트코드 여기에 삽입하면 됨 } }
  69. 삽입할 코드를 Bytecode 로 변환하면 val count = it.tag as?

    Int ?: 0 it.tag = count + 1 Log.v(“TAG”, “count: ${it.tag}”)
  70. class IncreaseCountMethodVisitor(api: Int, next: MethodVisitor, private val viewVarIndex: Int) :

    MethodVisitor(api, next) { override fun visitCode() { super.visitCode() visitVarInsn(Opcodes.ALOAD, viewVarIndex) // View 객체 가져오기 visitMethodInsn( Opcodes.INVOKEVIRTUAL, // 메소드 실행 "android/view/View", // View 의 "getTag", // getTag 메소드를 "()Ljava/lang/Object;", // parameter 는 없고, 반환값은 Object (Stack 에 푸시) false ) … visitMethodInsn( Opcodes.INVOKESTATIC, // static 메소드 실행 "android/util/Log", // Log 의 "v", // v 메소드를 "(Ljava/lang/String;Ljava/lang/String;)I", // parameter 는 String 2개, 반환값은 Int (Stack 에 푸시) false ) visitInsn(Opcodes.POP) // Stack 에서 제거 } } INVOKEVIRTUAL … INVOKESTATIC POP ALOAD 1
  71. Bytecode Manipulation 의 단점 • Bytecode 에 대한 이해도가 필요하므로,

    러닝커브가 매우 높다. • 디버깅이 어렵다. • Rename, Package 이동 등으로 Descriptor 가 변경되었을 때 대응이 쉽지 않다. • 버전에 따라 동작이 달라질 수 있다. (AGP, Kotlin, Gradle, ASM 등) • 빌드 속도가 느려질 수 있다.
  72. Bytecode Manipulation 의 단점 • Bytecode 에 대한 이해도가 필요하므로,

    러닝커브가 매우 높다. • 디버깅이 어렵다. • Rename, Package 이동 등으로 Descriptor 가 변경되었을 때 대응이 쉽지 않다. • 버전에 따라 동작이 달라질 수 있다. (AGP, Kotlin, Gradle, ASM 등) • 빌드 속도가 느려질 수 있다.
  73. Bytecode Manipulation 의 장점 • 소스 코드 변경 없이 동작

    제어 • 반복 코드 삽입 / 추상화 가능 • 정밀한 코드 흐름 분석 가능
  74. 어디에 쓰이고 있을까? • Hugo (@DebugLog) @DebugLog public String getName(String

    first, String last) { SystemClock.sleep(15); // Don't ever really do this! return first + " " + last; }
  75. 어디에 쓰이고 있을까? • Hugo (@DebugLog) • Firebase Performance Monitoring

    (@AddTrace) @AddTrace(name = "onCreateTrace", enabled = true) override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) }
  76. 어디에 쓰이고 있을까? • Hugo (@DebugLog) • Firebase Performance Monitoring

    (@AddTrace) • Hilt (@AndroidEntryPoint) (AndroidEntryPointClassVisitor) @AndroidEntryPoint class ExampleActivity : AppCompatActivity() { @Inject lateinit var analytics: AnalyticsAdapter ... }
  77. annotation class Loggable(val name: String) override fun onCreate(savedInstanceState: Bundle) {

    // 초기화 로직 ... binding.button.setOnClickListener @Loggable(“clicked_btn_show_toast”) { Toast.makeText(this, “Hello World Droid knights!”, Toast.LENGTH_SHORT).show() } }
  78. annotation class Loggable(val name: String) override fun onCreate(savedInstanceState: Bundle) {

    // 초기화 로직 ... binding.button.setOnClickListener @Loggable(“clicked_btn_show_toast”) { Toast.makeText(this, “Hello World Droid knights!”, Toast.LENGTH_SHORT).show() } } override fun onCreate(savedInstanceState: Bundle) { // 초기화 로직 ... binding.button.setOnClickListener { Toast.makeText(this, “Hello World Droid knights!”, Toast.LENGTH_SHORT).show() Log.v(“TAG”, “clicked_btn_show_toast”) } } @Loggable 이 존재하면 최하단에 로그 삽입
  79. class ClickLogMethodVisitor(api: Int, next: MethodVisitor) : MethodVisitor(api, next) { private

    var loggableName = "" override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor { if (descriptor == "Lcom/bigstark/example/log/Loggable;") { return ClickLogAnnotationVisitor(api = api, next = super.visitAnnotation(descriptor, visible)) { loggableName = it } } return super.visitAnnotation(descriptor, visible) } } MethodVisitor 에서 Annotation 을 visit 하면
  80. class ClickLogMethodVisitor(api: Int, next: MethodVisitor) : MethodVisitor(api, next) { private

    var loggableName = "" override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor { if (descriptor == "Lcom/bigstark/example/log/Loggable;") { return ClickLogAnnotationVisitor(api = api, next = super.visitAnnotation(descriptor, visible)) { loggableName = it } } return super.visitAnnotation(descriptor, visible) } } class ClickLogAnnotationVisitor(api: Int, next: AnnotationVisitor, private val onLoggableName: (String) -> Unit) : AnnotationVisitor(api, next) { override fun visit(name: String?, value: Any?) { super.visit(name, value) if (name == "name") { onLoggableName.invoke(value as? String ?: return) } } } 해당 Annotation 이 @Loggable 인지 확인
  81. class ClickLogMethodVisitor(api: Int, next: MethodVisitor) : MethodVisitor(api, next) { private

    var loggableName = "" override fun visitAnnotation(descriptor: String?, visible: Boolean): AnnotationVisitor { if (descriptor == "Lcom/bigstark/example/log/Loggable;") { return ClickLogAnnotationVisitor(api = api, next = super.visitAnnotation(descriptor, visible)) { loggableName = it } } return super.visitAnnotation(descriptor, visible) } } class ClickLogAnnotationVisitor(api: Int, next: AnnotationVisitor, private val onLoggableName: (String) -> Unit) : AnnotationVisitor(api, next) { override fun visit(name: String?, value: Any?) { super.visit(name, value) if (name == "name") { onLoggableName.invoke(value as? String ?: return) } } } loggable 의 name 캐시
  82. class ClickLogMethodVisitor(api: Int, next: MethodVisitor) : MethodVisitor(api, next) { private

    var loggableName = "" override fun visitInsn(opcode: Int) { if (loggableName.isEmpty()) { super.visitInsn(opcode) return } when (opcode) { Opcodes.IRETURN, Opcodes.LRETURN, Opcodes.FRETURN, Opcodes.DRETURN, Opcodes.ARETURN, Opcodes.RETURN -> { insertLog() } else -> Unit } super.visitInsn(opcode) } Method 의 마지막은 RETURN 으로 끝남
  83. class ClickLogMethodVisitor(api: Int, next: MethodVisitor) : MethodVisitor(api, next) { private

    var loggableName = "" override fun visitInsn(opcode: Int) { if (loggableName.isEmpty()) { super.visitInsn(opcode) return } when (opcode) { Opcodes.IRETURN, Opcodes.LRETURN, Opcodes.FRETURN, Opcodes.DRETURN, Opcodes.ARETURN, Opcodes.RETURN -> { insertLog() } else -> Unit } super.visitInsn(opcode) } RETURN 전에 로그 코드 삽입
  84. class ComposableClickLogTransformer( private val pluginContext: IrPluginContext, private val messageCollector: MessageCollector

    ) : IrElementTransformerVoid() { override fun visitCall(expression: IrCall): IrExpression { // ComposableClickCountActivity 내부에서만 감지 if (currentClass == "ComposableClickLogActivity") { val functionName = expression.symbol.owner.name.asString() // clickable 함수 호출 감지 if (functionName == "clickable") { clickableMethodFound = true messageCollector.report( CompilerMessageSeverity.INFO, "[composable] clickable method found - ${expression.symbol.owner.name}" ) } } return super.visitCall(expression) } }
  85. class ComposableClickLogTransformer( private val pluginContext: IrPluginContext, private val messageCollector: MessageCollector

    ) : IrElementTransformerVoid() { … override fun visitFunction(declaration: IrFunction): IrStatement { // clickable visitCall 이후에 visitFunction 에 들어왔다면 declaration.transformChildrenVoid(object : IrElementTransformerVoid() { // clickable 익명 함수 진입 시점 override fun visitDeclaration(declaration: IrDeclarationBase): IrStatement { declaration.acceptVoid(object : IrElementVisitorVoid { // clickable 익명 함수 진입 시점 override fun visitSimpleFunction(declaration: IrSimpleFunction) { super.visitSimpleFunction(declaration) generateLogFunction(pluginContext, declaration) } }) return super.visitDeclaration(declaration) } }) return super.visitFunction(declaration) } }