Android Metaprogramming (Droidcon NYC 2015)

Android Metaprogramming (Droidcon NYC 2015)

Do you like clean, dry, reusable code? Sure, we all do. For some use cases the cleanest and most efficient way to build and maintain a feature is to get meta. Are you ready to level up your developer skills? Metaprogramming is working with code that works with your code. Aspects, annotations, bytecode manipulation, runtime wizardry. What is possible? What unique limitations and opportunities exist when working within Android code? This is a practical introduction to Android Metaprogramming from a cross-platform native SDK and app developer.

7d1513d777e2134ef8113e60b16ebf88?s=128

Audrey Troutt

August 28, 2015
Tweet

Transcript

  1. @auditty +AudreyTroutt Android Metaprogramming Audrey Troutt

  2. @auditty +AudreyTroutt What is metaprogramming? Writing code that reads, generates,

    analyzes, or transforms other code.
  3. @auditty +AudreyTroutt Why use metaprogramming? // BEFORE class AndroidWay extends

    Activity { TextView name; ImageView thumbnail; LocationManager loc; Drawable icon; String myName; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); name = (TextView) findViewById(R.id.name); thumbnail = (ImageView) findViewById(R.id.thumbnail); loc = (LocationManager) getSystemService(Activity. LOCATION_SERVICE); icon = getResources().getDrawable(R.drawable.icon); myName = getString(R.string.app_name); name.setText( "Hello, " + myName ); } } // AFTER @ContentView(R.layout.main) class RoboWay extends RoboActivity { @InjectView(R.id.name) TextView name; @InjectView(R.id.thumbnail) ImageView thumbnail; @InjectResource(R.drawable.icon) Drawable icon; @InjectResource(R.string.app_name) String myName; @Inject LocationManager loc; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); name.setText( "Hello, " + myName ); } } // example borrowed from RoboGuice :) https://github. com/roboguice/roboguice/wiki
  4. @auditty +AudreyTroutt Why use metaprogramming? // BEFORE class AndroidWay extends

    Activity { TextView name; ImageView thumbnail; LocationManager loc; Drawable icon; String myName; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); name = (TextView) findViewById(R.id.name); thumbnail = (ImageView) findViewById(R.id.thumbnail); loc = (LocationManager) getSystemService(Activity. LOCATION_SERVICE); icon = getResources().getDrawable(R.drawable.icon); myName = getString(R.string.app_name); name.setText( "Hello, " + myName ); } } // AFTER @ContentView(R.layout.main) class RoboWay extends RoboActivity { @InjectView(R.id.name) TextView name; @InjectView(R.id.thumbnail) ImageView thumbnail; @InjectResource(R.drawable.icon) Drawable icon; @InjectResource(R.string.app_name) String myName; @Inject LocationManager loc; public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); name.setText( "Hello, " + myName ); } } // example borrowed from RoboGuice :) https://github. com/roboguice/roboguice/wiki
  5. @auditty +AudreyTroutt Why use metaprogramming? • You are a library

    developer, probably
  6. @auditty +AudreyTroutt #import "UILabel+TextMagic.h" // This is a category @implementation

    UILabel (TextMagic) - (CGFloat)bottomYCoordinate { return self.frame.origin.y + self.frame.size.height; } - (CGFloat)rightXCoordinate { return self.frame.origin.x + self.frame.size.width; } @end // from https://github.com/ArtisanMobile/ArtisanCategoryMagic Metaprogramming is easy* on iOS
  7. @auditty +AudreyTroutt #import <objc/runtime.h> // This is method swizzling +

    (void)replaceOriginalInstanceMethod:(SEL)originalMethod from:(id)original withInstanceMethod:(SEL)replacementMethod from:(id)replacementObject { Method originalMethod = class_getInstanceMethod([original class], originalMethod); Method mockMethod = class_getInstanceMethod([replacementObject class], replacementMethod); method_exchangeImplementations(originalMethod, mockMethod); } // Don’t do this in production code! It’s more polite to always call the original code. Metaprogramming is easy* on iOS
  8. @auditty +AudreyTroutt // You can perform any selector, even hidden

    ones. id myClone = [anObject performSelector:@selector(copy)]; Metaprogramming is easy* on iOS
  9. @auditty +AudreyTroutt Reflection Useful parts: • Runtime tool • Inspect

    classes, their annotations, fields and methods • Invoke methods • Change the values of fields
  10. @auditty +AudreyTroutt import java.lang.reflect.Method; private void logPropertyValueForObject(Object t, String accessorMethodName)

    { try { Method m = t.getClass().getDeclaredMethod(accessorMethodName); // (1) m.setAccessible(true); // (2) just in case this is private/not accessible Object o = m.invoke(t); // (3) using reflection to invoke an arbitrary method Log.d("META", accessorMethodName + " value is " + o); } catch (Exception e) { Log.e("META", "uh oh! Something went wrong!", e); } } protected void onStop() { super.onStop(); logPropertyValueForObject(this, "timeOnScreen"); // (4) } private long timeOnScreen() { return new Date().getTime() - activityStartTime; } Reflection
  11. @auditty +AudreyTroutt import java.lang.reflect.Method; private void logPropertyValueForObject(Object t, String accessorMethodName)

    { try { Method m = t.getClass().getDeclaredMethod(accessorMethodName); // (1) m.setAccessible(true); // (2) just in case this is private/not accessible Object o = m.invoke(t); // (3) using reflection to invoke an arbitrary method Log.d("META", accessorMethodName + " value is " + o); } catch (Exception e) { Log.e("META", "uh oh! Something went wrong!", e); } } protected void onStop() { super.onStop(); logPropertyValueForObject(this, "timeOnScreen"); // (4) } private long timeOnScreen() { return new Date().getTime() - activityStartTime; } Reflection
  12. @auditty +AudreyTroutt import java.lang.reflect.Method; private void logPropertyValueForObject(Object t, String accessorMethodName)

    { try { Method m = t.getClass().getDeclaredMethod(accessorMethodName); // (1) m.setAccessible(true); // (2) just in case this is private/not accessible Object o = m.invoke(t); // (3) using reflection to invoke an arbitrary method Log.d("META", accessorMethodName + " value is " + o); } catch (Exception e) { Log.e("META", "uh oh! Something went wrong!", e); } } protected void onStop() { super.onStop(); logPropertyValueForObject(this, "timeOnScreen"); // (4) } private long timeOnScreen() { return new Date().getTime() - activityStartTime; } Reflection
  13. @auditty +AudreyTroutt import java.lang.reflect.Method; private void logPropertyValueForObject(Object t, String accessorMethodName)

    { try { Method m = t.getClass().getDeclaredMethod(accessorMethodName); // (1) m.setAccessible(true); // (2) just in case this is private/not accessible Object o = m.invoke(t); // (3) using reflection to invoke an arbitrary method Log.d("META", accessorMethodName + " value is " + o); } catch (Exception e) { Log.e("META", "uh oh! Something went wrong!", e); } } protected void onStop() { super.onStop(); logPropertyValueForObject(this, "timeOnScreen"); // (4) } private long timeOnScreen() { return new Date().getTime() - activityStartTime; } Reflection
  14. @auditty +AudreyTroutt import java.lang.reflect.Method; private void logPropertyValueForObject(Object t, String accessorMethodName)

    { try { Method m = t.getClass().getDeclaredMethod(accessorMethodName); // (1) m.setAccessible(true); // (2) just in case this is private/not accessible Object o = m.invoke(t); // (3) using reflection to invoke an arbitrary method Log.d("META", accessorMethodName + " value is " + o); } catch (Exception e) { Log.e("META", "uh oh! Something went wrong!", e); } } protected void onStop() { super.onStop(); logPropertyValueForObject(this, "timeOnScreen"); // (4) } private long timeOnScreen() { return new Date().getTime() - activityStartTime; } Reflection
  15. @auditty +AudreyTroutt Reflection Limitations: • is slow • does not

    change code behavior
  16. @auditty +AudreyTroutt Annotations Useful parts: • Define metadata about code

  17. @auditty +AudreyTroutt @Background // An annotation! void translateInBackground(String textToTranslate) {

    String translatedText = callGoogleTranslate(textToTranslate); showResult(translatedText); } // example from AndroidAnnotations https://github.com/excilys/androidannotations Annotations
  18. @auditty +AudreyTroutt import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface WatchedMethod {

    String eventName(); String logProperty() default ""; } // SAMPLE USAGE IN MY ACTIVITY @Override @WatchedMethod(eventName = "disappear", logProperty = "timeOnScreen") protected void onStop() { super.onStop(); } Annotations
  19. @auditty +AudreyTroutt import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface WatchedMethod {

    String eventName(); String logProperty() default ""; } // SAMPLE USAGE IN MY ACTIVITY @Override @WatchedMethod(eventName = "disappear", logProperty = "timeOnScreen") protected void onStop() { super.onStop(); } Annotations
  20. @auditty +AudreyTroutt import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface WatchedMethod {

    String eventName(); String logProperty() default ""; } // SAMPLE USAGE IN MY ACTIVITY @Override @WatchedMethod(eventName = "disappear", logProperty = "timeOnScreen") protected void onStop() { super.onStop(); } Annotations
  21. @auditty +AudreyTroutt import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface WatchedMethod {

    String eventName(); String logProperty() default ""; } // SAMPLE USAGE IN MY ACTIVITY @Override @WatchedMethod(eventName = "disappear", logProperty = "timeOnScreen") protected void onStop() { super.onStop(); } Annotations
  22. @auditty +AudreyTroutt import java.lang.annotation.*; @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface WatchedMethod {

    String eventName(); String logProperty() default ""; } // SAMPLE USAGE IN MY ACTIVITY @Override @WatchedMethod(eventName = "disappear", logProperty = "timeOnScreen") protected void onStop() { super.onStop(); } Annotations
  23. @auditty +AudreyTroutt • Before compilation • Inspect annotations, generate source

    Annotation Processors
  24. @auditty +AudreyTroutt Limitations: • Can’t modify existing implementations • Requires

    developer binding • Method counts Annotation Processors
  25. @auditty +AudreyTroutt Bytecode Manipulation Useful parts: • Can both modify

    and add methods and classes
  26. @auditty +AudreyTroutt Android build process

  27. @auditty +AudreyTroutt Libraries: • ASM ◦ Working directly with bytecode

    ◦ http://asm.ow2.org/ • DexMaker ◦ Enables runtime manipulation on android ◦ https://github.com/crittercism/dexmaker • Javaassist ◦ higher-level coding ◦ http://jboss-javassist.github.io/javassist • AspectJ ◦ the easiest to use solution* Bytecode Manipulation
  28. @auditty +AudreyTroutt // Examples of using javaassist // (1) add

    superclass ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("test.Child"); cc.setSuperclass(pool.get("test.Parent")); // (2) insert code into a method body before existing code CtMethod m = cc.getDeclaredMethod("say"); m.insertBefore("{ Log.d(\"META\",\"So META!\"); }"); // (3) wrap the existing method body with a try/catch CtClass etype = ClassPool.getDefault().get("java.io.IOException"); m.addCatch("{ Log.e(\"META\",\"There was an error\", $e); }", etype); // (4) write the modified class back out cc.writeFile(); Bytecode Manipulation
  29. @auditty +AudreyTroutt // Examples of using javaassist // (1) add

    superclass ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("test.Child"); cc.setSuperclass(pool.get("test.Parent")); // (2) insert code into a method body before existing code CtMethod m = cc.getDeclaredMethod("say"); m.insertBefore("{ Log.d(\"META\",\"So META!\"); }"); // (3) wrap the existing method body with a try/catch CtClass etype = ClassPool.getDefault().get("java.io.IOException"); m.addCatch("{ Log.e(\"META\",\"There was an error\", $e); }", etype); // (4) write the modified class back out cc.writeFile(); Bytecode Manipulation
  30. @auditty +AudreyTroutt // Examples of using javaassist // (1) add

    superclass ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("test.Child"); cc.setSuperclass(pool.get("test.Parent")); // (2) insert code into a method body before existing code CtMethod m = cc.getDeclaredMethod("say"); m.insertBefore("{ Log.d(\"META\",\"So META!\"); }"); // (3) wrap the existing method body with a try/catch CtClass etype = ClassPool.getDefault().get("java.io.IOException"); m.addCatch("{ Log.e(\"META\",\"There was an error\", $e); }", etype); // (4) write the modified class back out cc.writeFile(); Bytecode Manipulation
  31. @auditty +AudreyTroutt // Examples of using javaassist // (1) add

    superclass ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("test.Child"); cc.setSuperclass(pool.get("test.Parent")); // (2) insert code into a method body before existing code CtMethod m = cc.getDeclaredMethod("say"); m.insertBefore("{ Log.d(\"META\",\"So META!\"); }"); // (3) wrap the existing method body with a try/catch CtClass etype = ClassPool.getDefault().get("java.io.IOException"); m.addCatch("{ Log.e(\"META\",\"There was an error\", $e); }", etype); // (4) write the modified class back out cc.writeFile(); Bytecode Manipulation
  32. @auditty +AudreyTroutt // Examples of using javaassist // (1) add

    superclass ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("test.Child"); cc.setSuperclass(pool.get("test.Parent")); // (2) insert code into a method body before existing code CtMethod m = cc.getDeclaredMethod("say"); m.insertBefore("{ Log.d(\"META\",\"So META!\"); }"); // (3) wrap the existing method body with a try/catch CtClass etype = ClassPool.getDefault().get("java.io.IOException"); m.addCatch("{ Log.e(\"META\",\"There was an error\", $e); }", etype); // (4) write the modified class back out cc.writeFile(); Bytecode Manipulation
  33. @auditty +AudreyTroutt // Examples of using javaassist // (1) add

    superclass ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("test.Child"); cc.setSuperclass(pool.get("test.Parent")); // (2) insert code into a method body before existing code CtMethod m = cc.getDeclaredMethod("say"); m.insertBefore("{ Log.d(\"META\",\"So META!\"); }"); // (3) wrap the existing method body with a try/catch CtClass etype = ClassPool.getDefault().get("java.io.IOException"); m.addCatch("{ Log.e(\"META\",\"There was an error\", $e); }", etype); // (4) write the modified class back out cc.writeFile(); Bytecode Manipulation
  34. @auditty +AudreyTroutt // Examples of using javaassist // (1) add

    superclass ClassPool pool = ClassPool.getDefault(); CtClass cc = pool.get("test.Child"); cc.setSuperclass(pool.get("test.Parent")); // (2) insert code into a method body before existing code CtMethod m = cc.getDeclaredMethod("say"); m.insertBefore("{ Log.d(\"META\",\"So META!\"); }"); // (3) wrap the existing method body with a try/catch CtClass etype = ClassPool.getDefault().get("java.io.IOException"); m.addCatch("{ Log.e(\"META\",\"There was an error\", $e); }", etype); // (4) write the modified class back out cc.writeFile(); Bytecode Manipulation
  35. @auditty +AudreyTroutt Limitations: • Danger! • Jack & Jill? •

    Long developer ramp-up Bytecode Manipulation
  36. @auditty +AudreyTroutt AspectJ Useful parts: • Can both modify and

    add methods and classes • Easy to use*
  37. @auditty +AudreyTroutt AspectJ • aspect • pointcut • join point

    • advice
  38. @auditty +AudreyTroutt // (1) define pointcut pointcut onViewClicked(View view) :

    execution(* *.*(View)) && args(view) && (within(Activity+ || View.OnClickListener+)); // (2) Inject code before/after/around the existing method(s) before(View view) : onViewClicked(view) { TrackEvent.onViewClicked(view); } // inspired by Artisan's TrackEvent.aj AspectJ
  39. @auditty +AudreyTroutt // (1) define pointcut pointcut onViewClicked(View view) :

    execution(* *.*(View)) && args(view) && (within(Activity+ || View.OnClickListener+)); // (2) Inject code before/after/around the existing method(s) before(View view) : onViewClicked(view) { TrackEvent.onViewClicked(view); } // inspired by Artisan's TrackEvent.aj AspectJ
  40. @auditty +AudreyTroutt // (1) define pointcut pointcut onViewClicked(View view) :

    execution(* *.*(View)) && args(view) && (within(Activity+ || View.OnClickListener+)); // (2) Inject code before/after/around the existing method(s) before(View view) : onViewClicked(view) { TrackEvent.onViewClicked(view); } // inspired by Artisan's TrackEvent.aj AspectJ
  41. @auditty +AudreyTroutt // (1) define pointcut pointcut onViewClicked(View view) :

    execution(* *.*(View)) && args(view) && (within(Activity+ || View.OnClickListener+)); // (2) Inject code before/after/around the existing method(s) before(View view) : onViewClicked(view) { TrackEvent.onViewClicked(view); } // inspired by Artisan's TrackEvent.aj AspectJ
  42. @auditty +AudreyTroutt // (1) define pointcut pointcut onViewClicked(View view) :

    execution(* *.*(View)) && args(view) && (within(Activity+ || View.OnClickListener+)); // (2) Inject code before/after/around the existing method(s) before(View view) : onViewClicked(view) { TrackEvent.onViewClicked(view); } // inspired by Artisan's TrackEvent.aj AspectJ
  43. @auditty +AudreyTroutt // (3) set superclass with inter-type declarations declare

    parents: com.audreytroutt.demo.activity.MainActivity extends AudreyInjectedActivity; // (4) add a method public void com.audreytroutt.demo.activity.MainActivity. injectedMethodInYourActivity() { Log.d("META", "I'm in your code, changing your bytes!"); } AspectJ
  44. @auditty +AudreyTroutt // (3) set superclass with inter-type declarations declare

    parents: com.audreytroutt.demo.activity.MainActivity extends AudreyInjectedActivity; // (4) add a method public void com.audreytroutt.demo.activity.MainActivity. injectedMethodInYourActivity() { Log.d("META", "I'm in your code, changing your bytes!"); } AspectJ
  45. @auditty +AudreyTroutt // (3) set superclass with inter-type declarations declare

    parents: com.audreytroutt.demo.activity.MainActivity extends AudreyInjectedActivity; // (4) add a method public void com.audreytroutt.demo.activity.MainActivity. injectedMethodInYourActivity() { Log.d("META", "I'm in your code, changing your bytes!"); } AspectJ
  46. @auditty +AudreyTroutt AspectJ Limitations: • Can’t modify Android base classes

    • Android developers hated it!
  47. @auditty +AudreyTroutt First Metaprogramming Steps • Leverage existing libraries •

    Peek inside and see how they work • Thank a library developer
  48. @auditty +AudreyTroutt Android Metaprogramming Audrey Troutt