Slide 1

Slide 1 text

Bytecode manipulation in Realm How and why? Tokyo Android Meetup - August Edition [email protected]

Slide 2

Slide 2 text

Makoto Yamazaki Realm Inc. / uPhyca Inc. [email protected] @zaki50

Slide 3

Slide 3 text

Bytecode manipulation? [email protected]

Slide 4

Slide 4 text

[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.

Slide 5

Slide 5 text

Slide 6

Slide 6 text

Slide 7

Slide 7 text

[email protected] Realm is mobile database Runs directly inside the phones, tablets, wearables

Slide 8

Slide 8 text

Fast [email protected] Advanced Easy Why mobile database?

Slide 9

Slide 9 text

[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 comments;
 }

Slide 10

Slide 10 text

[email protected] Realm is Fast Lazy loading

Slide 11

Slide 11 text

[email protected] Realm is Fast RealmQuery query = realm.where(Article.class); query = query.contains("title", "realm"); RealmResults 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.


Slide 12

Slide 12 text

Annotation processing [email protected] Bytecode manipulation How to achieve lazy loading of field values

Slide 13

Slide 13 text

[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

Slide 14

Slide 14 text

[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

Slide 15

Slide 15 text

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]

Slide 16

Slide 16 text

Annotation processor [email protected] Bytecode manipulation How to achieve field lazy loading without restrictions!

Slide 17

Slide 17 text

[email protected] Current Realm-java public class Article extends RealmObject {
 @PrimaryKey
 public String id; // other fields } User defines this

Slide 18

Slide 18 text

[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

Slide 19

Slide 19 text

[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

Slide 20

Slide 20 text

[email protected] Current Realm-java RealmResults results = realm.where(Article.class).findAll();
 Article article = results.first();
 String title = article.title; String title = article.realmGet$title(); by bytecode manipulation

Slide 21

Slide 21 text

Transform API [email protected] Javassist Infrastructure

Slide 22

Slide 22 text

[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 ...

Slide 23

Slide 23 text

[email protected] Transform API @Beta public abstract class Transform { // many other methods here public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { } }

Slide 24

Slide 24 text

[email protected] Transform API class RealmTransformer extends Transform { @Override public void transform(TransformInvocation transformInvocation) throws TransformException, InterruptedException, IOException { Context context = transformInvocation.getContext(); Collection inputs = transformInvocation.getInputs(); Collection referencedInputs; referencedInputs = transformInvocation.getReferencedInputs(); TransformOutputProvider outputProvider = transformInvocation.getOutputProvider(); boolean isIncremental = transformInvocation.isIncremental(); ... } }

Slide 25

Slide 25 text

[email protected] Javassist Low level: ASM, Apache commons BCEL Middle level : javassist, CGLIB High level : Retro lambda, AspectJ many similar libraries

Slide 26

Slide 26 text

[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) }

Slide 27

Slide 27 text

[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))
 }
 }
 }
 }

Slide 28

Slide 28 text

[email protected] Javassist public static void useRealmAccessors(CtClass clazz, List managedFields, List 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))
 }
 }
 }

Slide 29

Slide 29 text

[email protected] Javassist private static class FieldAccessToAccessorConverter extends ExprEditor {
 final List 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);')
 }
 }
 }
 }

Slide 30

Slide 30 text

You can find everything in https://github.com/realm/realm-java [email protected]