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

Bytecode manipulation in Realm How and why?

zaki50
August 18, 2016

Bytecode manipulation in Realm How and why?

zaki50

August 18, 2016
Tweet

More Decks by zaki50

Other Decks in Technology

Transcript

  1. Bytecode manipulation in Realm How and why? Tokyo Android Meetup

    - August Edition [email protected]
  2. Makoto Yamazaki Realm Inc. / uPhyca Inc. [email protected] @zaki50

  3. Bytecode manipulation? [email protected]

  4. [email protected] Bytecode manipulation is... modifying the bytecode at compile time

    or at the loading of the classes. Bytecode is the instruction set of JVM.
  5. Realm? [email protected]

  6. [email protected]

  7. [email protected] Realm is mobile database Runs directly inside the phones,

    tablets, wearables
  8. Fast [email protected] Advanced Easy Why mobile database?

  9. [email protected] Realm is Easy public class Article extends RealmObject {


    @PrimaryKey
 public String id;
 public String title;
 public String contents;
 public Date createdAt;
 public Date modifiedAt;
 public User user;
 public RealmList<Comment> comments;
 }
  10. [email protected] Realm is Fast Lazy loading

  11. [email protected] Realm is Fast RealmQuery<Article> query = realm.where(Article.class); query =

    query.contains("title", "realm"); RealmResults<Article> results = query.findAll();
 // results does not contain any actual Article instances here.
 
 Article article = results.first();
 // article does not contain any field values here.
 
 String title = article.title;
 // it looks just a field access. // but it fetches the actual value from the database.

  12. Annotation processing [email protected] Bytecode manipulation How to achieve lazy loading

    of field values
  13. [email protected] Old Realm-java public class Article extends RealmObject {
 @PrimaryKey


    private String id; // other fields public String getId() { return id; }
 public void setId(String id) { this.id = id; }
 } User defines this
  14. [email protected] Old Realm-java public class ArticleRealmProxy extends Article { private

    Row row; @Override
 public String getId() { return row.get(columnInfo.columnIdIndex); }
 public void setId(String id) { row.set(columnInfo.columnIdIndex, id); }
 } Generated by annotation processor
  15. Many restrictions 1. fields must be private 2. must have

    public getters and setters 3. getters and setters must not have a logic 4. no custom methods 5. must be a subclass of RealmObject class 6. and so on... [email protected]
  16. Annotation processor [email protected] Bytecode manipulation How to achieve field lazy

    loading without restrictions!
  17. [email protected] Current Realm-java public class Article extends RealmObject {
 @PrimaryKey


    public String id; // other fields } User defines this
  18. [email protected] Current Realm-java public class ArticleRealmProxy extends Article { private

    Row row; public String realmGet$id() { return row.get(columnInfo.columnIdIndex); }
 public void realmSet$id(String id) { row.set(columnInfo.columnIdIndex, id); }
 } Generated by annotation processor
  19. [email protected] Current Realm-java public class Article extends RealmObject { @PrimaryKey


    public String id; public String realmGet$id() { return return id; }
 public void realmSet$id(String id) { return row.set(columnInfo.columnIdIndex, id); }
 } by bytecode manipulation
  20. [email protected] Current Realm-java RealmResults<Article> results = realm.where(Article.class).findAll();
 Article article =

    results.first();
 String title = article.title; String title = article.realmGet$title(); by bytecode manipulation
  21. Transform API [email protected] Javassist Infrastructure

  22. [email protected] Transform API project.android.registerTransform(new RealmTransformer(project)) in build.gradle $ ./gradlew assemble

    ... :app:compileDebugSources :app:prePackageMarkerForDebug :app:unzipJacocoAgent :app:transformClassesWithJacocoForDebug :app:transformClassesWithRealmTransformerForDebug :app:transformClassesWithDexForDebug ...
  23. [email protected] Transform API @Beta public abstract class Transform { //

    many other methods here public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { } }
  24. [email protected] Transform API class RealmTransformer extends Transform { @Override public

    void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { Context context = transformInvocation.getContext(); Collection<TransformInput> inputs = transformInvocation.getInputs(); Collection<TransformInput> referencedInputs; referencedInputs = transformInvocation.getReferencedInputs(); TransformOutputProvider outputProvider = transformInvocation.getOutputProvider(); boolean isIncremental = transformInvocation.isIncremental(); ... } }
  25. [email protected] Javassist Low level: ASM, Apache commons BCEL Middle level

    : javassist, CGLIB High level : Retro lambda, AspectJ many similar libraries
  26. [email protected] Javassist inputModelClasses.each { BytecodeModifier.addRealmAccessors(it) } inputClassNames.each { def ctClass

    = classPool.getCtClass(it) BytecodeModifier.useRealmAccessors(ctClass, allManagedFields, allModelClasses) ctClass.writeFile(getOutputFile(outputProvider).canonicalPath) }
  27. [email protected] Javassist public static void addRealmAccessors(CtClass clazz) {
 def methods

    = clazz.getDeclaredMethods()*.name
 clazz.declaredFields.each { CtField field ->
 if (!Modifier.isStatic(field.getModifiers()) && !field.hasAnnotation(Ignore.class)) {
 if (!methods.contains("realmGet\$${field.name}")) {
 clazz.addMethod(CtNewMethod.getter("realmGet\$${field.name}", field))
 }
 if (!methods.contains("realmSet\$${field.name}")) {
 clazz.addMethod(CtNewMethod.setter("realmSet\$${field.name}", field))
 }
 }
 }
 }
  28. [email protected] Javassist public static void useRealmAccessors(CtClass clazz, List<CtField> managedFields, List<CtClass>

    modelClasses) {
 clazz.getDeclaredBehaviors().each { behavior ->
 if ((
 behavior instanceof CtMethod &&
 !behavior.name.startsWith('realmGet$') &&
 !behavior.name.startsWith('realmSet$')
 ) || (
 behavior instanceof CtConstructor &&
 !modelClasses.contains(clazz)
 )) {
 behavior.instrument(new FieldAccessToAccessorConverter(managedFields, clazz, behavior))
 }
 }
 }
  29. [email protected] Javassist private static class FieldAccessToAccessorConverter extends ExprEditor {
 final

    List<CtField> managedFields
 final CtClass ctClass
 final CtBehavior behavior
 
 @Override
 void edit(FieldAccess fieldAccess) throws CannotCompileException {
 def isRealmFieldAccess = managedFields.find {
 fieldAccess.className.equals(it.declaringClass.name) && fieldAccess.fieldName.equals(it.name)
 }
 if (isRealmFieldAccess != null) {
 def fieldName = fieldAccess.fieldName
 if (fieldAccess.isReader()) {
 fieldAccess.replace('$_ = $0.realmGet$' + fieldName + '();')
 } else if (fieldAccess.isWriter()) {
 fieldAccess.replace('$0.realmSet$' + fieldName + '($1);')
 }
 }
 }
 }
  30. You can find everything in https://github.com/realm/realm-java [email protected]