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

Bytecode Manipulation 으로 생산성 높이기

Sponsored · Ship Features Fearlessly Turn features on and off without deploys. Used by thousands of Ruby developers.
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) } }