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

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.

Audrey Troutt

August 28, 2015
Tweet

More Decks by Audrey Troutt

Other Decks in Programming

Transcript

  1. @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
  2. @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
  3. @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
  4. @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
  5. @auditty +AudreyTroutt // You can perform any selector, even hidden

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

    classes, their annotations, fields and methods • Invoke methods • Change the values of fields
  7. @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
  8. @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
  9. @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
  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 @Background // An annotation! void translateInBackground(String textToTranslate) {

    String translatedText = callGoogleTranslate(textToTranslate); showResult(translatedText); } // example from AndroidAnnotations https://github.com/excilys/androidannotations Annotations
  13. @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
  14. @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
  15. @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
  16. @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
  17. @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
  18. @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
  19. @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
  20. @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
  21. @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
  22. @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
  23. @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
  24. @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
  25. @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
  26. @auditty +AudreyTroutt Limitations: • Danger! • Jack & Jill? •

    Long developer ramp-up Bytecode Manipulation
  27. @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
  28. @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
  29. @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
  30. @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
  31. @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
  32. @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
  33. @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
  34. @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
  35. @auditty +AudreyTroutt First Metaprogramming Steps • Leverage existing libraries •

    Peek inside and see how they work • Thank a library developer