$30 off During Our Annual Pro Sale. View Details »

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
    Android
    Metaprogramming
    Audrey Troutt

    View Slide

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

    View Slide

  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

    View Slide

  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

    View Slide

  5. @auditty
    +AudreyTroutt
    Why use metaprogramming?
    ● You are a library developer, probably

    View Slide

  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

    View Slide

  7. @auditty
    +AudreyTroutt
    #import
    // 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

    View Slide

  8. @auditty
    +AudreyTroutt
    // You can perform any selector, even hidden ones.
    id myClone = [anObject performSelector:@selector(copy)];
    Metaprogramming is easy* on iOS

    View Slide

  9. @auditty
    +AudreyTroutt
    Reflection
    Useful parts:
    ● Runtime tool
    ● Inspect classes, their annotations,
    fields and methods
    ● Invoke methods
    ● Change the values of fields

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  15. @auditty
    +AudreyTroutt
    Reflection
    Limitations:
    ● is slow
    ● does not change code behavior

    View Slide

  16. @auditty
    +AudreyTroutt
    Annotations
    Useful parts:
    ● Define metadata about code

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  23. @auditty
    +AudreyTroutt
    ● Before compilation
    ● Inspect annotations, generate source
    Annotation Processors

    View Slide

  24. @auditty
    +AudreyTroutt
    Limitations:
    ● Can’t modify existing
    implementations
    ● Requires developer binding
    ● Method counts
    Annotation Processors

    View Slide

  25. @auditty
    +AudreyTroutt
    Bytecode Manipulation
    Useful parts:
    ● Can both modify and add methods
    and classes

    View Slide

  26. @auditty
    +AudreyTroutt
    Android build process

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  35. @auditty
    +AudreyTroutt
    Limitations:
    ● Danger!
    ● Jack & Jill?
    ● Long developer ramp-up
    Bytecode Manipulation

    View Slide

  36. @auditty
    +AudreyTroutt
    AspectJ
    Useful parts:
    ● Can both modify and add methods
    and classes
    ● Easy to use*

    View Slide

  37. @auditty
    +AudreyTroutt
    AspectJ
    ● aspect
    ● pointcut
    ● join point
    ● advice

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  46. @auditty
    +AudreyTroutt
    AspectJ
    Limitations:
    ● Can’t modify Android base classes
    ● Android developers hated it!

    View Slide

  47. @auditty
    +AudreyTroutt
    First Metaprogramming Steps
    ● Leverage existing libraries
    ● Peek inside and see how they work
    ● Thank a library developer

    View Slide

  48. @auditty
    +AudreyTroutt
    Android
    Metaprogramming
    Audrey Troutt

    View Slide