Merge remote-tracking branch 'aosp/upstream-master' am: 8764034eaf am: d9a6252d37
am: f9202b3a88

Change-Id: I6a1495c11353a2addcb0b89f6bf53eae20b6fb03
diff --git a/README.md b/README.md
index ccae135..a5169b4 100644
--- a/README.md
+++ b/README.md
@@ -2,7 +2,7 @@
 
 smali/baksmali is an assembler/disassembler for the dex format used by dalvik, Android's Java VM implementation. The syntax is loosely based on Jasmin's/dedexer's syntax, and supports the full functionality of the dex format (annotations, debug info, line info, etc.)
 
-Downloads are at  https://bitbucket.org/JesusFreke/smali/downloads. If you are interested in submitting a patch, feel free to send me a pull request here.
+Downloads are at  https://bitbucket.org/JesusFreke/smali/downloads/. If you are interested in submitting a patch, feel free to send me a pull request here.
 
 See [the wiki](https://github.com/JesusFreke/smali/wiki) for more info/news/release notes/etc.
 
diff --git a/baksmali/build.gradle b/baksmali/build.gradle
index f3a14b1..aae88a3 100644
--- a/baksmali/build.gradle
+++ b/baksmali/build.gradle
@@ -41,8 +41,8 @@
 dependencies {
     compile project(':util')
     compile project(':dexlib2')
-    compile depends.commons_cli
     compile depends.guava
+    compile depends.jcommander
 
     testCompile depends.junit
     testCompile project(':smali')
@@ -59,7 +59,7 @@
     classifier = 'fat'
 
     manifest {
-        attributes('Main-Class': 'org.jf.baksmali.main')
+        attributes('Main-Class': 'org.jf.baksmali.Main')
     }
 
     doLast {
@@ -92,7 +92,9 @@
     dontobfuscate
     dontoptimize
 
-    keep 'public class org.jf.baksmali.main { public static void main(java.lang.String[]); }'
+    keep 'public class org.jf.baksmali.Main { public static void main(java.lang.String[]); }'
+    keep 'public class org.jf.util.jcommander.ColonParameterSplitter'
+    keep 'class com.beust.jcommander.** { *; }'
     keepclassmembers 'enum * { public static **[] values(); public static ** valueOf(java.lang.String); }'
 
     dontwarn 'com.google.common.**'
@@ -100,3 +102,17 @@
 }
 
 tasks.getByPath(':release').dependsOn(proguard)
+
+task fastbuild(dependsOn: build) {
+}
+
+task fb(dependsOn: fastbuild) {
+}
+
+tasks.getByPath('javadoc').onlyIf({
+    !gradle.taskGraph.hasTask(fastbuild)
+})
+
+tasks.getByPath('test').onlyIf({
+    !gradle.taskGraph.hasTask(fastbuild)
+})
\ No newline at end of file
diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/CatchMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/CatchMethodItem.java
index 6c67d4a..4b545ee 100644
--- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/CatchMethodItem.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/CatchMethodItem.java
@@ -28,7 +28,7 @@
 
 package org.jf.baksmali.Adaptors;
 
-import org.jf.baksmali.baksmaliOptions;
+import org.jf.baksmali.BaksmaliOptions;
 import org.jf.util.IndentingWriter;
 
 import javax.annotation.Nonnull;
@@ -42,7 +42,7 @@
     private final LabelMethodItem tryEndLabel;
     private final LabelMethodItem handlerLabel;
 
-    public CatchMethodItem(@Nonnull baksmaliOptions options, @Nonnull MethodDefinition.LabelCache labelCache,
+    public CatchMethodItem(@Nonnull BaksmaliOptions options, @Nonnull MethodDefinition.LabelCache labelCache,
                            int codeAddress, @Nullable String exceptionType, int startAddress, int endAddress,
                            int handlerAddress) {
         super(codeAddress);
diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/ClassDefinition.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/ClassDefinition.java
index 2529af8..361826d 100644
--- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/ClassDefinition.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/ClassDefinition.java
@@ -28,8 +28,7 @@
 
 package org.jf.baksmali.Adaptors;
 
-import com.google.common.collect.Lists;
-import org.jf.baksmali.baksmaliOptions;
+import org.jf.baksmali.BaksmaliOptions;
 import org.jf.dexlib2.AccessFlags;
 import org.jf.dexlib2.dexbacked.DexBackedClassDef;
 import org.jf.dexlib2.dexbacked.DexBackedDexFile.InvalidItemIndex;
@@ -46,16 +45,16 @@
 import java.util.*;
 
 public class ClassDefinition {
-    @Nonnull public final baksmaliOptions options;
+    @Nonnull public final BaksmaliOptions options;
     @Nonnull public final ClassDef classDef;
     @Nonnull private final HashSet<String> fieldsSetInStaticConstructor;
 
     protected boolean validationErrors;
 
-    public ClassDefinition(@Nonnull baksmaliOptions options, @Nonnull ClassDef classDef) {
+    public ClassDefinition(@Nonnull BaksmaliOptions options, @Nonnull ClassDef classDef) {
         this.options = options;
         this.classDef = classDef;
-        fieldsSetInStaticConstructor = findFieldsSetInStaticConstructor();
+        fieldsSetInStaticConstructor = findFieldsSetInStaticConstructor(classDef);
     }
 
     public boolean hadValidationErrors() {
@@ -63,7 +62,7 @@
     }
 
     @Nonnull
-    private HashSet<String> findFieldsSetInStaticConstructor() {
+    private static HashSet<String> findFieldsSetInStaticConstructor(@Nonnull ClassDef classDef) {
         HashSet<String> fieldsSetInStaticConstructor = new HashSet<String>();
 
         for (Method method: classDef.getDirectMethods()) {
@@ -166,7 +165,7 @@
             writer.write("# annotations\n");
 
             String containingClass = null;
-            if (options.useImplicitReferences) {
+            if (options.implicitReferences) {
                 containingClass = classDef.getType();
             }
 
diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/EndTryLabelMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/EndTryLabelMethodItem.java
index aed315d..2680704 100644
--- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/EndTryLabelMethodItem.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/EndTryLabelMethodItem.java
@@ -28,14 +28,14 @@
 
 package org.jf.baksmali.Adaptors;
 
-import org.jf.baksmali.baksmaliOptions;
+import org.jf.baksmali.BaksmaliOptions;
 
 import javax.annotation.Nonnull;
 
 public class EndTryLabelMethodItem extends LabelMethodItem {
     private int endTryAddress;
 
-    public EndTryLabelMethodItem(@Nonnull baksmaliOptions options, int codeAddress, int endTryAddress) {
+    public EndTryLabelMethodItem(@Nonnull BaksmaliOptions options, int codeAddress, int endTryAddress) {
         super(options, codeAddress, "try_end_");
         this.endTryAddress = endTryAddress;
     }
diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/FieldDefinition.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/FieldDefinition.java
index ae01791..90291b7 100644
--- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/FieldDefinition.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/FieldDefinition.java
@@ -29,7 +29,7 @@
 package org.jf.baksmali.Adaptors;
 
 import org.jf.baksmali.Adaptors.EncodedValue.EncodedValueAdaptor;
-import org.jf.baksmali.baksmaliOptions;
+import org.jf.baksmali.BaksmaliOptions;
 import org.jf.dexlib2.AccessFlags;
 import org.jf.dexlib2.iface.Annotation;
 import org.jf.dexlib2.iface.Field;
@@ -41,7 +41,7 @@
 import java.util.Collection;
 
 public class FieldDefinition {
-    public static void writeTo(baksmaliOptions options, IndentingWriter writer, Field field,
+    public static void writeTo(BaksmaliOptions options, IndentingWriter writer, Field field,
                                boolean setInStaticConstructor) throws IOException {
         EncodedValue initialValue = field.getInitialValue();
         int accessFlags = field.getAccessFlags();
@@ -68,7 +68,7 @@
             writer.write(" = ");
 
             String containingClass = null;
-            if (options.useImplicitReferences) {
+            if (options.implicitReferences) {
                 containingClass = field.getDefiningClass();
             }
 
@@ -82,7 +82,7 @@
             writer.indent(4);
 
             String containingClass = null;
-            if (options.useImplicitReferences) {
+            if (options.implicitReferences) {
                 containingClass = field.getDefiningClass();
             }
 
diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/InstructionMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/InstructionMethodItem.java
index fe85fe0..d58b2b6 100644
--- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/InstructionMethodItem.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/InstructionMethodItem.java
@@ -32,7 +32,7 @@
 import org.jf.baksmali.Adaptors.MethodDefinition.InvalidSwitchPayload;
 import org.jf.baksmali.Adaptors.MethodItem;
 import org.jf.baksmali.Renderers.LongRenderer;
-import org.jf.baksmali.baksmaliOptions;
+import org.jf.baksmali.BaksmaliOptions;
 import org.jf.dexlib2.Opcode;
 import org.jf.dexlib2.ReferenceType;
 import org.jf.dexlib2.VerificationError;
@@ -67,7 +67,7 @@
     }
 
     private boolean isAllowedOdex(@Nonnull Opcode opcode) {
-        baksmaliOptions options = methodDef.classDef.options;
+        BaksmaliOptions options = methodDef.classDef.options;
         if (options.allowOdex) {
             return true;
         }
@@ -110,7 +110,7 @@
         if (instruction instanceof ReferenceInstruction) {
             ReferenceInstruction referenceInstruction = (ReferenceInstruction)instruction;
             String classContext = null;
-            if (methodDef.classDef.options.useImplicitReferences) {
+            if (methodDef.classDef.options.implicitReferences) {
                 classContext = methodDef.method.getDefiningClass();
             }
 
diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/OffsetInstructionFormatMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/OffsetInstructionFormatMethodItem.java
index 3ffb4bd..be76edf 100644
--- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/OffsetInstructionFormatMethodItem.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/Format/OffsetInstructionFormatMethodItem.java
@@ -30,7 +30,7 @@
 
 import org.jf.baksmali.Adaptors.LabelMethodItem;
 import org.jf.baksmali.Adaptors.MethodDefinition;
-import org.jf.baksmali.baksmaliOptions;
+import org.jf.baksmali.BaksmaliOptions;
 import org.jf.dexlib2.Opcode;
 import org.jf.dexlib2.iface.instruction.OffsetInstruction;
 import org.jf.util.IndentingWriter;
@@ -41,7 +41,7 @@
 public class OffsetInstructionFormatMethodItem extends InstructionMethodItem<OffsetInstruction> {
     protected LabelMethodItem label;
 
-    public OffsetInstructionFormatMethodItem(@Nonnull baksmaliOptions options, @Nonnull MethodDefinition methodDef,
+    public OffsetInstructionFormatMethodItem(@Nonnull BaksmaliOptions options, @Nonnull MethodDefinition methodDef,
                                              int codeAddress, OffsetInstruction instruction) {
         super(methodDef, codeAddress, instruction);
 
diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/LabelMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/LabelMethodItem.java
index b152bb6..268d643 100644
--- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/LabelMethodItem.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/LabelMethodItem.java
@@ -28,18 +28,18 @@
 
 package org.jf.baksmali.Adaptors;
 
-import org.jf.baksmali.baksmaliOptions;
+import org.jf.baksmali.BaksmaliOptions;
 import org.jf.util.IndentingWriter;
 
 import javax.annotation.Nonnull;
 import java.io.IOException;
 
 public class LabelMethodItem extends MethodItem {
-    private final baksmaliOptions options;
+    private final BaksmaliOptions options;
     private final String labelPrefix;
     private int labelSequence;
 
-    public LabelMethodItem(@Nonnull baksmaliOptions options, int codeAddress, @Nonnull String labelPrefix) {
+    public LabelMethodItem(@Nonnull BaksmaliOptions options, int codeAddress, @Nonnull String labelPrefix) {
         super(codeAddress);
         this.options = options;
         this.labelPrefix = labelPrefix;
@@ -76,7 +76,7 @@
     public boolean writeTo(IndentingWriter writer) throws IOException {
         writer.write(':');
         writer.write(labelPrefix);
-        if (options.useSequentialLabels) {
+        if (options.sequentialLabels) {
             writer.printUnsignedLongAsHex(labelSequence);
         } else {
             writer.printUnsignedLongAsHex(this.getLabelAddress());
diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/MethodDefinition.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/MethodDefinition.java
index ef2110a..8161fe4 100644
--- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/MethodDefinition.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/MethodDefinition.java
@@ -32,7 +32,7 @@
 import com.google.common.collect.Lists;
 import org.jf.baksmali.Adaptors.Debug.DebugMethodItem;
 import org.jf.baksmali.Adaptors.Format.InstructionMethodItemFactory;
-import org.jf.baksmali.baksmaliOptions;
+import org.jf.baksmali.BaksmaliOptions;
 import org.jf.dexlib2.AccessFlags;
 import org.jf.dexlib2.Format;
 import org.jf.dexlib2.Opcode;
@@ -163,7 +163,7 @@
     }
 
     public static void writeEmptyMethodTo(IndentingWriter writer, Method method,
-                                          baksmaliOptions options) throws IOException {
+                                          BaksmaliOptions options) throws IOException {
         writer.write(".method ");
         writeAccessFlags(writer, method.getAccessFlags());
         writer.write(method.getName());
@@ -180,7 +180,7 @@
         writeParameters(writer, method, methodParameters, options);
 
         String containingClass = null;
-        if (options.useImplicitReferences) {
+        if (options.implicitReferences) {
             containingClass = method.getDefiningClass();
         }
         AnnotationFormatter.writeTo(writer, method.getAnnotations(), containingClass);
@@ -212,7 +212,7 @@
         writer.write('\n');
 
         writer.indent(4);
-        if (classDef.options.useLocalsDirective) {
+        if (classDef.options.localsDirective) {
             writer.write(".locals ");
             writer.printSignedIntAsDec(methodImpl.getRegisterCount() - parameterRegisterCount);
         } else {
@@ -228,7 +228,7 @@
         }
 
         String containingClass = null;
-        if (classDef.options.useImplicitReferences) {
+        if (classDef.options.implicitReferences) {
             containingClass = method.getDefiningClass();
         }
         AnnotationFormatter.writeTo(writer, method.getAnnotations(), containingClass);
@@ -313,18 +313,18 @@
 
     private static void writeParameters(IndentingWriter writer, Method method,
                                         List<? extends MethodParameter> parameters,
-                                        baksmaliOptions options) throws IOException {
+                                        BaksmaliOptions options) throws IOException {
         boolean isStatic = AccessFlags.STATIC.isSet(method.getAccessFlags());
         int registerNumber = isStatic?0:1;
         for (MethodParameter parameter: parameters) {
             String parameterType = parameter.getType();
             String parameterName = parameter.getName();
             Collection<? extends Annotation> annotations = parameter.getAnnotations();
-            if ((options.outputDebugInfo && parameterName != null) || annotations.size() != 0) {
+            if ((options.debugInfo && parameterName != null) || annotations.size() != 0) {
                 writer.write(".param p");
                 writer.printSignedIntAsDec(registerNumber);
 
-                if (parameterName != null && options.outputDebugInfo) {
+                if (parameterName != null && options.debugInfo) {
                     writer.write(", ");
                     ReferenceFormatter.writeStringReference(writer, parameterName);
                 }
@@ -335,7 +335,7 @@
                     writer.indent(4);
 
                     String containingClass = null;
-                    if (options.useImplicitReferences) {
+                    if (options.implicitReferences) {
                         containingClass = method.getDefiningClass();
                     }
                     AnnotationFormatter.writeTo(writer, annotations, containingClass);
@@ -374,11 +374,11 @@
         }
 
         addTries(methodItems);
-        if (classDef.options.outputDebugInfo) {
+        if (classDef.options.debugInfo) {
             addDebugInfo(methodItems);
         }
 
-        if (classDef.options.useSequentialLabels) {
+        if (classDef.options.sequentialLabels) {
             setLabelSequentialNumbers();
         }
 
@@ -415,7 +415,7 @@
                 methodItems.add(new BlankMethodItem(currentCodeAddress));
             }
 
-            if (classDef.options.addCodeOffsets) {
+            if (classDef.options.codeOffsets) {
                 methodItems.add(new MethodItem(currentCodeAddress) {
 
                     @Override
@@ -432,7 +432,8 @@
                 });
             }
 
-            if (!classDef.options.noAccessorComments && (instruction instanceof ReferenceInstruction)) {
+            if (classDef.options.accessorComments && classDef.options.syntheticAccessorResolver != null &&
+                    (instruction instanceof ReferenceInstruction)) {
                 Opcode opcode = instruction.getOpcode();
 
                 if (opcode.referenceType == ReferenceType.METHOD) {
@@ -493,7 +494,7 @@
                 methodItems.add(new BlankMethodItem(currentCodeAddress));
             }
 
-            if (classDef.options.addCodeOffsets) {
+            if (classDef.options.codeOffsets) {
                 methodItems.add(new MethodItem(currentCodeAddress) {
 
                     @Override
@@ -597,7 +598,7 @@
 
     @Nullable
     private String getContainingClassForImplicitReference() {
-        if (classDef.options.useImplicitReferences) {
+        if (classDef.options.implicitReferences) {
             return classDef.classDef.getType();
         }
         return null;
diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/PostInstructionRegisterInfoMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/PostInstructionRegisterInfoMethodItem.java
index 812a282..62826b1 100644
--- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/PostInstructionRegisterInfoMethodItem.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/PostInstructionRegisterInfoMethodItem.java
@@ -28,7 +28,7 @@
 
 package org.jf.baksmali.Adaptors;
 
-import org.jf.baksmali.baksmaliOptions;
+import org.jf.baksmali.BaksmaliOptions;
 import org.jf.dexlib2.analysis.AnalyzedInstruction;
 import org.jf.dexlib2.analysis.RegisterType;
 import org.jf.util.IndentingWriter;
@@ -60,12 +60,12 @@
         int registerCount = analyzedInstruction.getRegisterCount();
         BitSet registers = new BitSet(registerCount);
 
-        if ((registerInfo & baksmaliOptions.ALL) != 0) {
+        if ((registerInfo & BaksmaliOptions.ALL) != 0) {
             registers.set(0, registerCount);
         } else {
-            if ((registerInfo & baksmaliOptions.ALLPOST) != 0) {
+            if ((registerInfo & BaksmaliOptions.ALLPOST) != 0) {
                 registers.set(0, registerCount);
-            } else if ((registerInfo & baksmaliOptions.DEST) != 0) {
+            } else if ((registerInfo & BaksmaliOptions.DEST) != 0) {
                 addDestRegs(registers, registerCount);
             }
         }
diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/PreInstructionRegisterInfoMethodItem.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/PreInstructionRegisterInfoMethodItem.java
index f532938..f934edd 100644
--- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/PreInstructionRegisterInfoMethodItem.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/PreInstructionRegisterInfoMethodItem.java
@@ -28,7 +28,7 @@
 
 package org.jf.baksmali.Adaptors;
 
-import org.jf.baksmali.baksmaliOptions;
+import org.jf.baksmali.BaksmaliOptions;
 import org.jf.dexlib2.analysis.AnalyzedInstruction;
 import org.jf.dexlib2.analysis.MethodAnalyzer;
 import org.jf.dexlib2.analysis.RegisterType;
@@ -68,29 +68,29 @@
         BitSet registers = new BitSet(registerCount);
         BitSet mergeRegisters = null;
 
-        if ((registerInfo & baksmaliOptions.ALL) != 0) {
+        if ((registerInfo & BaksmaliOptions.ALL) != 0) {
             registers.set(0, registerCount);
         } else {
-            if ((registerInfo & baksmaliOptions.ALLPRE) != 0) {
+            if ((registerInfo & BaksmaliOptions.ALLPRE) != 0) {
                 registers.set(0, registerCount);
             } else {
-                if ((registerInfo & baksmaliOptions.ARGS) != 0) {
+                if ((registerInfo & BaksmaliOptions.ARGS) != 0) {
                     addArgsRegs(registers);
                 }
-                if ((registerInfo & baksmaliOptions.MERGE) != 0) {
+                if ((registerInfo & BaksmaliOptions.MERGE) != 0) {
                     if (analyzedInstruction.isBeginningInstruction()) {
                         addParamRegs(registers, registerCount);
                     }
                     mergeRegisters = new BitSet(registerCount);
                     addMergeRegs(mergeRegisters, registerCount);
-                } else if ((registerInfo & baksmaliOptions.FULLMERGE) != 0 &&
+                } else if ((registerInfo & BaksmaliOptions.FULLMERGE) != 0 &&
                         (analyzedInstruction.isBeginningInstruction())) {
                     addParamRegs(registers, registerCount);
                 }
             }
         }
 
-        if ((registerInfo & baksmaliOptions.FULLMERGE) != 0) {
+        if ((registerInfo & BaksmaliOptions.FULLMERGE) != 0) {
             if (mergeRegisters == null) {
                 mergeRegisters = new BitSet(registerCount);
                 addMergeRegs(mergeRegisters, registerCount);
diff --git a/baksmali/src/main/java/org/jf/baksmali/Adaptors/RegisterFormatter.java b/baksmali/src/main/java/org/jf/baksmali/Adaptors/RegisterFormatter.java
index bffcb38..3d72f46 100644
--- a/baksmali/src/main/java/org/jf/baksmali/Adaptors/RegisterFormatter.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Adaptors/RegisterFormatter.java
@@ -28,7 +28,7 @@
 
 package org.jf.baksmali.Adaptors;
 
-import org.jf.baksmali.baksmaliOptions;
+import org.jf.baksmali.BaksmaliOptions;
 import org.jf.util.IndentingWriter;
 
 import javax.annotation.Nonnull;
@@ -38,11 +38,11 @@
  * This class contains the logic used for formatting registers
  */
 public class RegisterFormatter {
-    @Nonnull public final baksmaliOptions options;
+    @Nonnull public final BaksmaliOptions options;
     public final int registerCount;
     public final int parameterRegisterCount;
 
-    public RegisterFormatter(@Nonnull baksmaliOptions options, int registerCount, int parameterRegisterCount) {
+    public RegisterFormatter(@Nonnull BaksmaliOptions options, int registerCount, int parameterRegisterCount) {
         this.options = options;
         this.registerCount = registerCount;
         this.parameterRegisterCount = parameterRegisterCount;
@@ -58,7 +58,7 @@
      * @param lastRegister the last register in the range
      */
     public void writeRegisterRange(IndentingWriter writer, int startRegister, int lastRegister) throws IOException {
-        if (!options.noParameterRegisters) {
+        if (options.parameterRegisters) {
             assert startRegister <= lastRegister;
 
             if (startRegister >= registerCount - parameterRegisterCount) {
@@ -86,7 +86,7 @@
      * @param register the register number
      */
     public void writeTo(IndentingWriter writer, int register) throws IOException {
-        if (!options.noParameterRegisters) {
+        if (options.parameterRegisters) {
             if (register >= registerCount - parameterRegisterCount) {
                 writer.write('p');
                 writer.printSignedIntAsDec((register - (registerCount - parameterRegisterCount)));
diff --git a/baksmali/src/main/java/org/jf/baksmali/AnalysisArguments.java b/baksmali/src/main/java/org/jf/baksmali/AnalysisArguments.java
new file mode 100644
index 0000000..20bc45b
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/AnalysisArguments.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.Parameter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import org.jf.dexlib2.analysis.ClassPath;
+import org.jf.dexlib2.analysis.ClassPathResolver;
+import org.jf.dexlib2.dexbacked.OatFile.OatDexFile;
+import org.jf.dexlib2.iface.DexFile;
+import org.jf.util.jcommander.ColonParameterSplitter;
+import org.jf.util.jcommander.ExtendedParameter;
+
+import javax.annotation.Nonnull;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+import static org.jf.dexlib2.analysis.ClassPath.NOT_ART;
+
+public class AnalysisArguments {
+    @Parameter(names = {"-b", "--bootclasspath", "--bcp"},
+            description = "A colon separated list of the files to include in the bootclasspath when analyzing the " +
+                    "dex file. If not specified, baksmali will attempt to choose an " +
+                    "appropriate default. When analyzing oat files, this can simply be the path to the device's " +
+                    "boot.oat file. A single empty string can be used to specify that an empty bootclasspath should " +
+                    "be used. (e.g. --bootclasspath \"\") See baksmali help classpath for more information.",
+            splitter = ColonParameterSplitter.class)
+    @ExtendedParameter(argumentNames = "classpath")
+    public List<String> bootClassPath = null;
+
+    @Parameter(names = {"-c", "--classpath", "--cp"},
+            description = "A colon separated list of additional files to include in the classpath when analyzing the " +
+                    "dex file. These will be added to the classpath after any bootclasspath entries.",
+            splitter = ColonParameterSplitter.class)
+    @ExtendedParameter(argumentNames = "classpath")
+    public List<String> classPath = Lists.newArrayList();
+
+    @Parameter(names = {"-d", "--classpath-dir", "--cpd", "--dir"},
+            description = "A directory to search for classpath files. This option can be used multiple times to " +
+                    "specify multiple directories to search. They will be searched in the order they are provided.")
+    @ExtendedParameter(argumentNames = "dir")
+    public List<String> classPathDirectories = null;
+
+    public static class CheckPackagePrivateArgument {
+        @Parameter(names = {"--check-package-private-access", "--package-private", "--checkpp", "--pp"},
+                description = "Use the package-private access check when calculating vtable indexes. This is enabled " +
+                        "by default for oat files. For odex files, this is only needed for odexes from 4.2.0. It " +
+                        "was reverted in 4.2.1.")
+        public boolean checkPackagePrivateAccess = false;
+    }
+
+    @Nonnull
+    public ClassPath loadClassPathForDexFile(@Nonnull File dexFileDir, @Nonnull DexFile dexFile,
+                                             boolean checkPackagePrivateAccess) throws IOException {
+        return loadClassPathForDexFile(dexFileDir, dexFile, checkPackagePrivateAccess, NOT_ART);
+    }
+
+    @Nonnull
+    public ClassPath loadClassPathForDexFile(@Nonnull File dexFileDir, @Nonnull DexFile dexFile,
+                                             boolean checkPackagePrivateAccess, int oatVersion)
+            throws IOException {
+        ClassPathResolver resolver;
+
+        // By default, oatVersion should be NOT_ART, and we'll automatically set it if dexFile is an oat file. In some
+        // cases the caller may choose to override the oat version, in which case we should use the given oat version
+        // regardless of the actual version of the oat file
+        if (oatVersion == NOT_ART) {
+            if (dexFile instanceof OatDexFile) {
+                checkPackagePrivateAccess = true;
+                oatVersion = ((OatDexFile)dexFile).getContainer().getOatVersion();
+            }
+        } else {
+            // this should always be true for ART
+            checkPackagePrivateAccess = true;
+        }
+
+        if (classPathDirectories == null || classPathDirectories.size() == 0) {
+            classPathDirectories = Lists.newArrayList(dexFileDir.getPath());
+        }
+
+        List<String> filteredClassPathDirectories = Lists.newArrayList();
+        if (classPathDirectories != null) {
+            for (String dir: classPathDirectories) {
+                File file = new File(dir);
+                if (!file.exists()) {
+                    System.err.println(String.format("Warning: directory %s does not exist. Ignoring.", dir));
+                } else if (!file.isDirectory()) {
+                    System.err.println(String.format("Warning: %s is not a directory. Ignoring.", dir));
+                } else {
+                    filteredClassPathDirectories.add(dir);
+                }
+            }
+        }
+
+        if (bootClassPath == null) {
+            // TODO: we should be able to get the api from the Opcodes object associated with the dexFile..
+            // except that the oat version -> api mapping doesn't fully work yet
+            resolver = new ClassPathResolver(filteredClassPathDirectories, classPath, dexFile);
+        }  else if (bootClassPath.size() == 1 && bootClassPath.get(0).length() == 0) {
+            // --bootclasspath "" is a special case, denoting that no bootclasspath should be used
+            resolver = new ClassPathResolver(
+                    ImmutableList.<String>of(), ImmutableList.<String>of(), classPath, dexFile);
+        } else {
+            resolver = new ClassPathResolver(filteredClassPathDirectories, bootClassPath, classPath, dexFile);
+        }
+
+        if (oatVersion == 0 && dexFile instanceof OatDexFile) {
+            oatVersion = ((OatDexFile)dexFile).getContainer().getOatVersion();
+        }
+        return new ClassPath(resolver.getResolvedClassProviders(), checkPackagePrivateAccess, oatVersion);
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/baksmali.java b/baksmali/src/main/java/org/jf/baksmali/Baksmali.java
similarity index 60%
rename from baksmali/src/main/java/org/jf/baksmali/baksmali.java
rename to baksmali/src/main/java/org/jf/baksmali/Baksmali.java
index 5060734..1c0231b 100644
--- a/baksmali/src/main/java/org/jf/baksmali/baksmali.java
+++ b/baksmali/src/main/java/org/jf/baksmali/Baksmali.java
@@ -28,105 +28,28 @@
 
 package org.jf.baksmali;
 
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Ordering;
 import org.jf.baksmali.Adaptors.ClassDefinition;
-import org.jf.dexlib2.analysis.ClassPath;
-import org.jf.dexlib2.analysis.CustomInlineMethodResolver;
 import org.jf.dexlib2.iface.ClassDef;
 import org.jf.dexlib2.iface.DexFile;
-import org.jf.dexlib2.util.SyntheticAccessorResolver;
 import org.jf.util.ClassFileNameHandler;
 import org.jf.util.IndentingWriter;
-import org.xml.sax.Attributes;
-import org.xml.sax.SAXException;
-import org.xml.sax.helpers.DefaultHandler;
 
-import javax.xml.parsers.ParserConfigurationException;
-import javax.xml.parsers.SAXParser;
-import javax.xml.parsers.SAXParserFactory;
+import javax.annotation.Nullable;
 import java.io.*;
+import java.util.HashSet;
 import java.util.List;
-import java.util.Map.Entry;
+import java.util.Set;
 import java.util.concurrent.*;
 
-public class baksmali {
+public class Baksmali {
+    public static boolean disassembleDexFile(DexFile dexFile, File outputDir, int jobs, final BaksmaliOptions options) {
+        return disassembleDexFile(dexFile, outputDir, jobs, options, null);
+    }
 
-    public static boolean disassembleDexFile(DexFile dexFile, final baksmaliOptions options) {
-        if (options.registerInfo != 0 || options.deodex || options.normalizeVirtualMethods) {
-            try {
-                Iterable<String> extraClassPathEntries;
-                if (options.extraClassPathEntries != null) {
-                    extraClassPathEntries = options.extraClassPathEntries;
-                } else {
-                    extraClassPathEntries = ImmutableList.of();
-                }
-
-                options.classPath = ClassPath.fromClassPath(options.bootClassPathDirs,
-                        Iterables.concat(options.bootClassPathEntries, extraClassPathEntries), dexFile,
-                        options.apiLevel, options.checkPackagePrivateAccess, options.experimental);
-
-                if (options.customInlineDefinitions != null) {
-                    options.inlineResolver = new CustomInlineMethodResolver(options.classPath,
-                            options.customInlineDefinitions);
-                }
-            } catch (Exception ex) {
-                System.err.println("\n\nError occurred while loading boot class path files. Aborting.");
-                ex.printStackTrace(System.err);
-                return false;
-            }
-        }
-
-        if (options.resourceIdFileEntries != null) {
-            class PublicHandler extends DefaultHandler {
-                String prefix = null;
-                public PublicHandler(String prefix) {
-                    super();
-                    this.prefix = prefix;
-                }
-
-                public void startElement(String uri, String localName,
-                        String qName, Attributes attr) throws SAXException {
-                    if (qName.equals("public")) {
-                        String type = attr.getValue("type");
-                        String name = attr.getValue("name").replace('.', '_');
-                        Integer public_key = Integer.decode(attr.getValue("id"));
-                        String public_val = new StringBuffer()
-                            .append(prefix)
-                            .append(".")
-                            .append(type)
-                            .append(".")
-                            .append(name)
-                            .toString();
-                        options.resourceIds.put(public_key, public_val);
-                    }
-                }
-            };
-
-            for (Entry<String,String> entry: options.resourceIdFileEntries.entrySet()) {
-                try {
-                    SAXParser saxp = SAXParserFactory.newInstance().newSAXParser();
-                    String prefix = entry.getValue();
-                    saxp.parse(entry.getKey(), new PublicHandler(prefix));
-                } catch (ParserConfigurationException e) {
-                    continue;
-                } catch (SAXException e) {
-                    continue;
-                } catch (IOException e) {
-                    continue;
-                }
-            }
-        }
-
-        File outputDirectoryFile = new File(options.outputDirectory);
-        if (!outputDirectoryFile.exists()) {
-            if (!outputDirectoryFile.mkdirs()) {
-                System.err.println("Can't create the output directory " + options.outputDirectory);
-                return false;
-            }
-        }
+    public static boolean disassembleDexFile(DexFile dexFile, File outputDir, int jobs, final BaksmaliOptions options,
+                                             @Nullable List<String> classes) {
 
         //sort the classes, so that if we're on a case-insensitive file system and need to handle classes with file
         //name collisions, then we'll use the same name for each class, if the dex file goes through multiple
@@ -134,16 +57,20 @@
         //may still change of course
         List<? extends ClassDef> classDefs = Ordering.natural().sortedCopy(dexFile.getClasses());
 
-        if (!options.noAccessorComments) {
-            options.syntheticAccessorResolver = new SyntheticAccessorResolver(dexFile.getOpcodes(), classDefs);
-        }
+        final ClassFileNameHandler fileNameHandler = new ClassFileNameHandler(outputDir, ".smali");
 
-        final ClassFileNameHandler fileNameHandler = new ClassFileNameHandler(outputDirectoryFile, ".smali");
-
-        ExecutorService executor = Executors.newFixedThreadPool(options.jobs);
+        ExecutorService executor = Executors.newFixedThreadPool(jobs);
         List<Future<Boolean>> tasks = Lists.newArrayList();
 
+        Set<String> classSet = null;
+        if (classes != null) {
+            classSet = new HashSet<String>(classes);
+        }
+
         for (final ClassDef classDef: classDefs) {
+            if (classSet != null && !classSet.contains(classDef.getType())) {
+                continue;
+            }
             tasks.add(executor.submit(new Callable<Boolean>() {
                 @Override public Boolean call() throws Exception {
                     return disassembleClass(classDef, fileNameHandler, options);
@@ -174,7 +101,7 @@
     }
 
     private static boolean disassembleClass(ClassDef classDef, ClassFileNameHandler fileNameHandler,
-                                            baksmaliOptions options) {
+                                            BaksmaliOptions options) {
         /**
          * The path for the disassembly file is based on the package name
          * The class descriptor will look something like:
diff --git a/baksmali/src/main/java/org/jf/baksmali/BaksmaliOptions.java b/baksmali/src/main/java/org/jf/baksmali/BaksmaliOptions.java
new file mode 100644
index 0000000..7ad5124
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/BaksmaliOptions.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright 2013, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import org.jf.dexlib2.analysis.ClassPath;
+import org.jf.dexlib2.analysis.InlineMethodResolver;
+import org.jf.dexlib2.util.SyntheticAccessorResolver;
+import org.xml.sax.Attributes;
+import org.xml.sax.SAXException;
+import org.xml.sax.helpers.DefaultHandler;
+
+import javax.xml.parsers.ParserConfigurationException;
+import javax.xml.parsers.SAXParser;
+import javax.xml.parsers.SAXParserFactory;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+public class BaksmaliOptions {
+    public int apiLevel = 15;
+
+    public boolean parameterRegisters = true;
+    public boolean localsDirective = false;
+    public boolean sequentialLabels = false;
+    public boolean debugInfo = true;
+    public boolean codeOffsets = false;
+    public boolean accessorComments = true;
+    public boolean allowOdex = false;
+    public boolean deodex = false;
+    public boolean implicitReferences = false;
+    public boolean normalizeVirtualMethods = false;
+
+    // register info values
+    public static final int ALL = 1;
+    public static final int ALLPRE = 2;
+    public static final int ALLPOST = 4;
+    public static final int ARGS = 8;
+    public static final int DEST = 16;
+    public static final int MERGE = 32;
+    public static final int FULLMERGE = 64;
+
+    public int registerInfo = 0;
+
+    public Map<Integer,String> resourceIds = new HashMap<Integer,String>();
+    public InlineMethodResolver inlineResolver = null;
+    public ClassPath classPath = null;
+    public SyntheticAccessorResolver syntheticAccessorResolver = null;
+
+    /**
+     * Load the resource ids from a set of public.xml files.
+     *
+     * @param resourceFiles A map of resource prefixes -> public.xml files
+     */
+    public void loadResourceIds(Map<String, File> resourceFiles) throws SAXException, IOException {
+        for (Map.Entry<String, File> entry: resourceFiles.entrySet()) {
+            try {
+                SAXParser saxp = SAXParserFactory.newInstance().newSAXParser();
+                final String prefix = entry.getKey();
+                saxp.parse(entry.getValue(), new DefaultHandler() {
+                    @Override
+                    public void startElement(String uri, String localName, String qName,
+                                             Attributes attr) throws SAXException {
+                        if (qName.equals("public")) {
+                            String resourceType = attr.getValue("type");
+                            String resourceName = attr.getValue("name").replace('.', '_');
+                            Integer resourceId = Integer.decode(attr.getValue("id"));
+                            String qualifiedResourceName =
+                                    String.format("%s.%s.%s", prefix, resourceType, resourceName);
+                            resourceIds.put(resourceId, qualifiedResourceName);
+                        }
+                    }
+                });
+            } catch (ParserConfigurationException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/DeodexCommand.java b/baksmali/src/main/java/org/jf/baksmali/DeodexCommand.java
new file mode 100644
index 0000000..3ded479
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/DeodexCommand.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import com.beust.jcommander.ParametersDelegate;
+import org.jf.baksmali.AnalysisArguments.CheckPackagePrivateArgument;
+import org.jf.dexlib2.analysis.CustomInlineMethodResolver;
+import org.jf.dexlib2.analysis.InlineMethodResolver;
+import org.jf.dexlib2.dexbacked.DexBackedOdexFile;
+import org.jf.util.jcommander.ExtendedParameter;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+@Parameters(commandDescription = "Deodexes an odex/oat file")
+@ExtendedParameters(
+        commandName = "deodex",
+        commandAliases = { "de", "x" })
+public class DeodexCommand extends DisassembleCommand {
+
+    @ParametersDelegate
+    protected CheckPackagePrivateArgument checkPackagePrivateArgument = new CheckPackagePrivateArgument();
+
+    @Parameter(names = {"--inline-table", "--inline", "--it"},
+            description = "Specify a file containing a custom inline method table to use. See the " +
+                    "\"deodexerant\" tool in the smali github repository to dump the inline method table from a " +
+                    "device that uses dalvik.")
+    @ExtendedParameter(argumentNames = "file")
+    private String inlineTable;
+
+    public DeodexCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    @Override protected BaksmaliOptions getOptions() {
+        BaksmaliOptions options = super.getOptions();
+
+        options.deodex = true;
+
+        if (dexFile instanceof DexBackedOdexFile) {
+            if (inlineTable == null) {
+                options.inlineResolver = InlineMethodResolver.createInlineMethodResolver(
+                        ((DexBackedOdexFile)dexFile).getOdexVersion());
+            } else {
+                File inlineTableFile = new File(inlineTable);
+                if (!inlineTableFile.exists()) {
+                    System.err.println(String.format("Could not find file: %s", inlineTable));
+                    System.exit(-1);
+                }
+                try {
+                    options.inlineResolver = new CustomInlineMethodResolver(options.classPath, inlineTableFile);
+                } catch (IOException ex) {
+                    System.err.println(String.format("Error while reading file: %s", inlineTableFile));
+                    ex.printStackTrace(System.err);
+                    System.exit(-1);
+                }
+            }
+        }
+
+        return options;
+    }
+
+    @Override protected boolean shouldCheckPackagePrivateAccess() {
+        return checkPackagePrivateArgument.checkPackagePrivateAccess;
+    }
+
+    @Override protected boolean needsClassPath() {
+        return true;
+    }
+
+    @Override protected boolean showDeodexWarning() {
+        return false;
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java b/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java
new file mode 100644
index 0000000..c7660cb
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/DexInputCommand.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import org.jf.dexlib2.DexFileFactory;
+import org.jf.dexlib2.Opcodes;
+import org.jf.dexlib2.dexbacked.DexBackedDexFile;
+import org.jf.util.jcommander.Command;
+import org.jf.util.jcommander.ExtendedParameter;
+
+import javax.annotation.Nonnull;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * This class implements common functionality for commands that need to load a dex file based on
+ * command line input
+ */
+public abstract class DexInputCommand extends Command {
+
+    @Parameter(names = {"-a", "--api"},
+            description = "The numeric api level of the file being disassembled.")
+    @ExtendedParameter(argumentNames = "api")
+    public int apiLevel = 15;
+
+    @Parameter(description = "A dex/apk/oat/odex file. For apk or oat files that contain multiple dex " +
+            "files, you can specify the specific entry to use as if the apk/oat file was a directory. " +
+            "e.g. \"app.apk/classes2.dex\". For more information, see \"baksmali help input\".")
+    @ExtendedParameter(argumentNames = "file")
+    protected List<String> inputList = Lists.newArrayList();
+
+    protected File inputFile;
+    protected String inputEntry;
+    protected DexBackedDexFile dexFile;
+
+    public DexInputCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    /**
+     * Parses a dex file input from the user and loads the given dex file.
+     *
+     * In some cases, the input file can contain multiple dex files. If this is the case, you can refer to a specific
+     * dex file with a slash, followed by the entry name, optionally in quotes.
+     *
+     * If the entry name is enclosed in quotes, then it will strip the first and last quote and look for an entry with
+     * exactly that name. Otherwise, it will perform a partial filename match against the entry to find any candidates.
+     * If there is a single matching candidate, it will be used. Otherwise, an error will be generated.
+     *
+     * For example, to refer to the "/system/framework/framework.jar:classes2.dex" entry within the
+     * "framework/arm/framework.oat" oat file, you could use any of:
+     *
+     * framework/arm/framework.oat/"/system/framework/framework.jar:classes2.dex"
+     * framework/arm/framework.oat/system/framework/framework.jar:classes2.dex
+     * framework/arm/framework.oat/framework/framework.jar:classes2.dex
+     * framework/arm/framework.oat/framework.jar:classes2.dex
+     * framework/arm/framework.oat/classes2.dex
+     *
+     * The last option is the easiest, but only works if the oat file doesn't contain another entry with the
+     * "classes2.dex" name. e.g. "/system/framework/blah.jar:classes2.dex"
+     *
+     * It's technically possible (although unlikely) for an oat file to contain 2 entries like:
+     * /system/framework/framework.jar:classes2.dex
+     * system/framework/framework.jar:classes2.dex
+     *
+     * In this case, the "framework/arm/framework.oat/system/framework/framework.jar:classes2.dex" syntax will generate
+     * an error because both entries match the partial entry name. Instead, you could use the following for the
+     * first and second entry respectively:
+     *
+     * framework/arm/framework.oat/"/system/framework/framework.jar:classes2.dex"
+     * framework/arm/framework.oat/"system/framework/framework.jar:classes2.dex"
+     *
+     * @param input The name of a dex, apk, odex or oat file/entry.
+     */
+    protected void loadDexFile(@Nonnull String input) {
+        File file = new File(input);
+
+        while (file != null && !file.exists()) {
+            file = file.getParentFile();
+        }
+
+        if (file == null || !file.exists() || file.isDirectory()) {
+            System.err.println("Can't find file: " + input);
+            System.exit(1);
+        }
+
+        inputFile = file;
+
+        String dexEntry = null;
+        if (file.getPath().length() < input.length()) {
+            dexEntry = input.substring(file.getPath().length() + 1);
+        }
+
+        if (!Strings.isNullOrEmpty(dexEntry)) {
+            boolean exactMatch = false;
+            if (dexEntry.length() > 2 && dexEntry.charAt(0) == '"' && dexEntry.charAt(dexEntry.length() - 1) == '"') {
+                dexEntry = dexEntry.substring(1, dexEntry.length() - 1);
+                exactMatch = true;
+            }
+
+            inputEntry = dexEntry;
+
+            try {
+                dexFile = DexFileFactory.loadDexEntry(file, dexEntry, exactMatch, Opcodes.forApi(apiLevel));
+            } catch (IOException ex) {
+                throw new RuntimeException(ex);
+            }
+        } else {
+            try {
+                dexFile = DexFileFactory.loadDexFile(file, Opcodes.forApi(apiLevel));
+            } catch (IOException ex) {
+                throw new RuntimeException(ex);
+            }
+        }
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java b/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java
new file mode 100644
index 0000000..2e3eb79
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/DisassembleCommand.java
@@ -0,0 +1,287 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import com.beust.jcommander.ParametersDelegate;
+import com.beust.jcommander.validators.PositiveInteger;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import org.jf.dexlib2.util.SyntheticAccessorResolver;
+import org.jf.util.StringWrapper;
+import org.jf.util.jcommander.ExtendedParameter;
+import org.jf.util.jcommander.ExtendedParameters;
+import org.xml.sax.SAXException;
+
+import javax.annotation.Nonnull;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+
+@Parameters(commandDescription = "Disassembles a dex file.")
+@ExtendedParameters(
+        commandName = "disassemble",
+        commandAliases = { "dis", "d" })
+public class DisassembleCommand extends DexInputCommand {
+
+    @Parameter(names = {"-h", "-?", "--help"}, help = true,
+            description = "Show usage information for this command.")
+    private boolean help;
+
+    @ParametersDelegate
+    protected AnalysisArguments analysisArguments = new AnalysisArguments();
+
+    @Parameter(names = {"--debug-info", "--di"}, arity = 1,
+            description = "Whether to include debug information in the output (.local, .param, .line, etc.). True " +
+                    "by default, use --debug-info=false to disable.")
+    @ExtendedParameter(argumentNames = "boolean")
+    private boolean debugInfo = true;
+
+    @Parameter(names = {"--code-offsets", "--offsets", "--off"},
+            description = "Add a comment before each instruction with it's code offset within the method.")
+    private boolean codeOffsets = false;
+
+    @Parameter(names = {"--resolve-resources", "--rr"}, arity = 2,
+            description = "This will attempt to find any resource id references within the bytecode and add a " +
+                    "comment with the name of the resource being referenced. The parameter accepts 2 values:" +
+                    "an arbitrary resource prefix and the path to a public.xml file. For example: " +
+                    "--resolve-resources android.R framework/res/values/public.xml. This option can be specified " +
+                    "multiple times to provide resources from multiple packages.")
+    @ExtendedParameter(argumentNames = {"resource prefix", "public.xml file"})
+    private List<String> resourceIdFiles = Lists.newArrayList();
+
+    @Parameter(names = {"-j", "--jobs"},
+            description = "The number of threads to use. Defaults to the number of cores available.",
+            validateWith = PositiveInteger.class)
+    @ExtendedParameter(argumentNames = "n")
+    private int jobs = Runtime.getRuntime().availableProcessors();
+
+    @Parameter(names = {"-l", "--use-locals"},
+            description = "When disassembling, output the .locals directive with the number of non-parameter " +
+                    "registers instead of the .registers directive with the total number of registers.")
+    private boolean localsDirective = false;
+
+    @Parameter(names = {"--accessor-comments", "--ac"}, arity = 1,
+            description = "Generate helper comments for synthetic accessors. True by default, use " +
+                    "--accessor-comments=false to disable.")
+    @ExtendedParameter(argumentNames = "boolean")
+    private boolean accessorComments = true;
+
+    @Parameter(names = {"--normalize-virtual-methods", "--norm", "--nvm"},
+            description = "Normalize virtual method references to use the base class where the method is " +
+                    "originally declared.")
+    private boolean normalizeVirtualMethods = false;
+
+    @Parameter(names = {"-o", "--output"},
+            description = "The directory to write the disassembled files to.")
+    @ExtendedParameter(argumentNames = "dir")
+    private String outputDir = "out";
+
+    @Parameter(names = {"--parameter-registers", "--preg", "--pr"}, arity = 1,
+            description = "Use the pNN syntax for registers that refer to a method parameter on method entry. True " +
+                    "by default, use --parameter-registers=false to disable.")
+    @ExtendedParameter(argumentNames = "boolean")
+    private boolean parameterRegisters = true;
+
+    @Parameter(names = {"-r", "--register-info"},
+            description = "Add comments before/after each instruction with information about register types. " +
+                    "The value is a comma-separated list of any of ALL, ALLPRE, ALLPOST, ARGS, DEST, MERGE and " +
+                    "FULLMERGE. See \"baksmali help register-info\" for more information.")
+    @ExtendedParameter(argumentNames = "register info specifier")
+    private List<String> registerInfoTypes = Lists.newArrayList();
+
+    @Parameter(names = {"--sequential-labels", "--seq", "--sl"},
+            description = "Create label names using a sequential numbering scheme per label type, rather than " +
+                    "using the bytecode address.")
+    private boolean sequentialLabels = false;
+
+    @Parameter(names = {"--implicit-references", "--implicit", "--ir"},
+            description = "Use implicit method and field references (without the class name) for methods and " +
+                    "fields from the current class.")
+    private boolean implicitReferences = false;
+
+    @Parameter(names = "--classes",
+            description = "A comma separated list of classes. Only disassemble these classes")
+    @ExtendedParameter(argumentNames = "classes")
+    private List<String> classes = null;
+
+    public DisassembleCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    public void run() {
+        if (help || inputList == null || inputList.isEmpty()) {
+            usage();
+            return;
+        }
+
+        if (inputList.size() > 1) {
+            System.err.println("Too many files specified");
+            usage();
+            return;
+        }
+
+        String input = inputList.get(0);
+        loadDexFile(input);
+
+        if (showDeodexWarning() && dexFile.hasOdexOpcodes()) {
+            StringWrapper.printWrappedString(System.err,
+                    "Warning: You are disassembling an odex/oat file without deodexing it. You won't be able to " +
+                            "re-assemble the results unless you deodex it. See \"baksmali help deodex\"");
+        }
+
+        File outputDirectoryFile = new File(outputDir);
+        if (!outputDirectoryFile.exists()) {
+            if (!outputDirectoryFile.mkdirs()) {
+                System.err.println("Can't create the output directory " + outputDir);
+                System.exit(-1);
+            }
+        }
+
+        if (analysisArguments.classPathDirectories == null || analysisArguments.classPathDirectories.isEmpty()) {
+            analysisArguments.classPathDirectories = Lists.newArrayList(inputFile.getAbsoluteFile().getParent());
+        }
+
+        if (!Baksmali.disassembleDexFile(dexFile, outputDirectoryFile, jobs, getOptions(), classes)) {
+            System.exit(-1);
+        }
+    }
+
+    protected boolean needsClassPath() {
+        return !registerInfoTypes.isEmpty() || normalizeVirtualMethods;
+    }
+
+    protected boolean shouldCheckPackagePrivateAccess() {
+        return false;
+    }
+
+    protected boolean showDeodexWarning() {
+        return true;
+    }
+
+    protected BaksmaliOptions getOptions() {
+        if (dexFile == null) {
+            throw new IllegalStateException("You must call loadDexFile first");
+        }
+
+        final BaksmaliOptions options = new BaksmaliOptions();
+
+        if (needsClassPath()) {
+            try {
+                options.classPath = analysisArguments.loadClassPathForDexFile(
+                        inputFile.getAbsoluteFile().getParentFile(), dexFile, shouldCheckPackagePrivateAccess());
+            } catch (Exception ex) {
+                System.err.println("\n\nError occurred while loading class path files. Aborting.");
+                ex.printStackTrace(System.err);
+                System.exit(-1);
+            }
+        }
+
+        if (!resourceIdFiles.isEmpty()) {
+            Map<String, File> resourceFiles = Maps.newHashMap();
+
+            assert (resourceIdFiles.size() % 2) == 0;
+            for (int i=0; i<resourceIdFiles.size(); i+=2) {
+                String resourcePrefix = resourceIdFiles.get(i);
+                String publicXml = resourceIdFiles.get(i+1);
+
+                File publicXmlFile = new File(publicXml);
+
+                if (!publicXmlFile.exists()) {
+                    System.err.println(String.format("Can't find file: %s", publicXmlFile));
+                    System.exit(-1);
+                }
+
+                resourceFiles.put(resourcePrefix, publicXmlFile);
+            }
+
+            try {
+                options.loadResourceIds(resourceFiles);
+            } catch (IOException ex) {
+                System.err.println("Error while loading resource files:");
+                ex.printStackTrace(System.err);
+                System.exit(-1);
+            } catch (SAXException ex) {
+                System.err.println("Error while loading resource files:");
+                ex.printStackTrace(System.err);
+                System.exit(-1);
+            }
+        }
+
+        options.parameterRegisters = parameterRegisters;
+        options.localsDirective = localsDirective;
+        options.sequentialLabels = sequentialLabels;
+        options.debugInfo = debugInfo;
+        options.codeOffsets = codeOffsets;
+        options.accessorComments = accessorComments;
+        options.implicitReferences = implicitReferences;
+        options.normalizeVirtualMethods = normalizeVirtualMethods;
+
+        options.registerInfo = 0;
+
+        for (String registerInfoType: registerInfoTypes) {
+            if (registerInfoType.equalsIgnoreCase("ALL")) {
+                options.registerInfo  |= BaksmaliOptions.ALL;
+            } else if (registerInfoType.equalsIgnoreCase("ALLPRE")) {
+                options.registerInfo  |= BaksmaliOptions.ALLPRE;
+            } else if (registerInfoType.equalsIgnoreCase("ALLPOST")) {
+                options.registerInfo  |= BaksmaliOptions.ALLPOST;
+            } else if (registerInfoType.equalsIgnoreCase("ARGS")) {
+                options.registerInfo  |= BaksmaliOptions.ARGS;
+            } else if (registerInfoType.equalsIgnoreCase("DEST")) {
+                options.registerInfo  |= BaksmaliOptions.DEST;
+            } else if (registerInfoType.equalsIgnoreCase("MERGE")) {
+                options.registerInfo  |= BaksmaliOptions.MERGE;
+            } else if (registerInfoType.equalsIgnoreCase("FULLMERGE")) {
+                options.registerInfo  |= BaksmaliOptions.FULLMERGE;
+            } else {
+                System.err.println(String.format("Invalid register info type: %s", registerInfoType));
+                usage();
+                System.exit(-1);
+            }
+
+            if ((options.registerInfo & BaksmaliOptions.FULLMERGE) != 0) {
+                options.registerInfo &= ~BaksmaliOptions.MERGE;
+            }
+        }
+
+        if (accessorComments) {
+            options.syntheticAccessorResolver = new SyntheticAccessorResolver(dexFile.getOpcodes(),
+                    dexFile.getClasses());
+        }
+
+        return options;
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/DumpCommand.java b/baksmali/src/main/java/org/jf/baksmali/DumpCommand.java
new file mode 100644
index 0000000..433be12
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/DumpCommand.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import org.jf.dexlib2.dexbacked.DexBackedDexFile;
+import org.jf.dexlib2.dexbacked.raw.RawDexFile;
+import org.jf.dexlib2.dexbacked.raw.util.DexAnnotator;
+import org.jf.util.ConsoleUtil;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.io.*;
+import java.util.List;
+
+@Parameters(commandDescription = "Prints an annotated hex dump for the given dex file")
+@ExtendedParameters(
+        commandName = "dump",
+        commandAliases = "du")
+public class DumpCommand extends DexInputCommand {
+
+    @Parameter(names = {"-h", "-?", "--help"}, help = true,
+            description = "Show usage information for this command.")
+    private boolean help;
+
+    public DumpCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    public void run() {
+        if (help || inputList == null || inputList.isEmpty()) {
+            usage();
+            return;
+        }
+
+        if (inputList.size() > 1) {
+            System.err.println("Too many files specified");
+            usage();
+            return;
+        }
+
+        String input = inputList.get(0);
+        loadDexFile(input);
+
+        try {
+            dump(dexFile, System.out);
+        } catch (IOException ex) {
+            System.err.println("There was an error while dumping the dex file");
+            ex.printStackTrace(System.err);
+        }
+    }
+
+    /**
+     * Writes an annotated hex dump of the given dex file to output.
+     *
+     * @param dexFile The dex file to dump
+     * @param output An OutputStream to write the annotated hex dump to. The caller is responsible for closing this
+     *               when needed.
+     *
+     * @throws IOException
+     */
+    public static void dump(@Nonnull DexBackedDexFile dexFile, @Nonnull OutputStream output)
+            throws IOException {
+        Writer writer = new BufferedWriter(new OutputStreamWriter(output));
+
+        int consoleWidth = ConsoleUtil.getConsoleWidth();
+        if (consoleWidth <= 0) {
+            consoleWidth = 120;
+        }
+
+        RawDexFile rawDexFile = new RawDexFile(dexFile.getOpcodes(), dexFile);
+        DexAnnotator annotator = new DexAnnotator(rawDexFile, consoleWidth);
+        annotator.writeAnnotations(writer);
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java b/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java
new file mode 100644
index 0000000..149ac63
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/HelpCommand.java
@@ -0,0 +1,204 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import com.google.common.collect.Lists;
+import org.jf.util.ConsoleUtil;
+import org.jf.util.StringWrapper;
+import org.jf.util.jcommander.*;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+@Parameters(commandDescription = "Shows usage information")
+@ExtendedParameters(
+        commandName = "help",
+        commandAliases = "h")
+public class HelpCommand extends Command {
+
+    public HelpCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    @Parameter(description = "If specified, show the detailed usage information for the given commands")
+    @ExtendedParameter(argumentNames = "commands")
+    private List<String> commands = Lists.newArrayList();
+
+    public void run() {
+        JCommander parentJc = commandAncestors.get(commandAncestors.size() - 1);
+
+        if (commands == null || commands.isEmpty()) {
+            System.out.println(new HelpFormatter()
+                    .width(ConsoleUtil.getConsoleWidth())
+                    .format(commandAncestors));
+        } else {
+            boolean printedHelp = false;
+            for (String cmd : commands) {
+                if (cmd.equals("register-info")) {
+                    printedHelp = true;
+                    String registerInfoHelp = "The --register-info parameter will cause baksmali to generate " +
+                            "comments before and after every instruction containing register type " +
+                            "information about some subset of registers. This parameter accepts a comma-separated list" +
+                            "of values specifying which registers and how much information to include.\n" +
+                            "    ALL: all pre- and post-instruction registers\n" +
+                            "    ALLPRE: all pre-instruction registers\n" +
+                            "    ALLPOST: all post-instruction registers\n" +
+                            "    ARGS: any pre-instruction registers used as arguments to the instruction\n" +
+                            "    DEST: the post-instruction register used as the output of the instruction\n" +
+                            "    MERGE: any pre-instruction register that has been merged from multiple " +
+                            "incoming code paths\n" +
+                            "    FULLMERGE: an extended version of MERGE that also includes a list of all " +
+                            "the register types from incoming code paths that were merged";
+
+                    Iterable<String> lines = StringWrapper.wrapStringOnBreaks(registerInfoHelp,
+                            ConsoleUtil.getConsoleWidth());
+                    for (String line : lines) {
+                        System.out.println(line);
+                    }
+                } else if (cmd.equals("input")) {
+                    printedHelp = true;
+                    String registerInfoHelp = "Apks and oat files can contain multiple dex files. In order to " +
+                            "specify a particular dex file, the basic syntax is to treat the apk/oat file as a " +
+                            "directory. For example, to load the \"classes2.dex\" entry from \"app.apk\", you can " +
+                            "use \"app.apk/classes2.dex\".\n" +
+                            "\n" +
+                            "For ease of use, you can also specify a partial path to the dex file to load. For " +
+                            "example, to load a entry named \"/system/framework/framework.jar:classes2.dex\" from " +
+                            "\"framework.oat\", you can use any of the following:\n" +
+                            "\"framework.oat/classes2.dex\"\n" +
+                            "\"framework.oat/framework.jar:classes2.dex\"\n" +
+                            "\"framework.oat/framework/framework.jar:classes2.dex\"\n" +
+                            "\"framework.oat/system/framework/framework.jar:classes2.dex\"\n" +
+                            "\n" +
+                            "In some rare cases, an oat file could have entries that can't be differentiated with " +
+                            "the above syntax. For example \"/blah/blah.dex\" and \"blah/blah.dex\". In this case, " +
+                            "the \"blah.oat/blah/blah.dex\" would match both entries and generate an error. To get " +
+                            "around this, you can add double quotes around the entry name to specify an exact entry " +
+                            "name. E.g. blah.oat/\"/blah/blah.dex\" or blah.oat/\"blah/blah.dex\" respectively.";
+
+                    Iterable<String> lines = StringWrapper.wrapStringOnBreaks(registerInfoHelp,
+                            ConsoleUtil.getConsoleWidth());
+                    for (String line : lines) {
+                        System.out.println(line);
+                    }
+                } else if (cmd.equals("classpath")) {
+                    printedHelp = true;
+                    String registerInfoHelp = "When deodexing odex/oat files or when using the --register-info " +
+                            "option, baksmali needs to load all classes from the framework files on the device " +
+                            "in order to fully understand the class hierarchy. There are several options that " +
+                            "control how baksmali finds and loads the classpath entries.\n" +
+                            "\n"+
+                            "L+ devices (ART):\n" +
+                            "When deodexing or disassembling a file from an L+ device using ART, you generally " +
+                            "just need to specify the path to the boot.oat file via the --bootclasspath/-b " +
+                            "parameter. On pre-N devices, the boot.oat file is self-contained and no other files are " +
+                            "needed. In N, boot.oat was split into multiple files. In this case, the other " +
+                            "files should be in the same directory as the boot.oat file, but you still only need to " +
+                            "specify the boot.oat file in the --bootclasspath/-b option. The other files will be " +
+                            "automatically loaded from the same directory.\n" +
+                            "\n" +
+                            "Pre-L devices (dalvik):\n" +
+                            "When deodexing odex files from a pre-L device using dalvik, you " +
+                            "generally just need to specify the path to a directory containing the framework files " +
+                            "from the device via the --classpath-dir/-d option. odex files contain a list of " +
+                            "framework files they depend on and baksmali will search for these dependencies in the " +
+                            "directory that you specify.\n" +
+                            "\n" +
+                            "Dex files don't contain a list of dependencies like odex files, so when disassembling a " +
+                            "dex file using the --register-info option, and using the framework files from a " +
+                            "pre-L device, baksmali will attempt to use a reasonable default list of classpath files " +
+                            "based on the api level set via the -a option. If this default list is incorrect, you " +
+                            "can override the classpath using the --bootclasspath/-b option. This option accepts a " +
+                            "colon separated list of classpath entries. Each entry can be specified in a few " +
+                            "different ways.\n" +
+                            " - A simple filename like \"framework.jar\"\n" +
+                            " - A device path like \"/system/framework/framework.jar\"\n" +
+                            " - A local relative or absolute path like \"/tmp/framework/framework.jar\"\n" +
+                            "When using the first or second formats, you should also specify the directory " +
+                            "containing the framework files via the --classpath-dir/-d option. When using the third " +
+                            "format, this option is not needed.\n" +
+                            "It's worth noting that the second format matches the format used by Android for the " +
+                            "BOOTCLASSPATH environment variable, so you can simply grab the value of that variable " +
+                            "from the device and use it as-is.\n" +
+                            "\n" +
+                            "Examples:\n" +
+                            "  For an M device:\n" +
+                            "    adb pull /system/framework/arm/boot.oat /tmp/boot.oat\n" +
+                            "    baksmali deodex blah.oat -b /tmp/boot.oat\n" +
+                            "  For an N+ device:\n" +
+                            "    adb pull /system/framework/arm /tmp/framework\n" +
+                            "    baksmali deodex blah.oat -b /tmp/framework/boot.oat\n" +
+                            "  For a pre-L device:\n" +
+                            "    adb pull /system/framework /tmp/framework\n" +
+                            "    baksmali deodex blah.odex -d /tmp/framework\n" +
+                            "  Using the BOOTCLASSPATH on a pre-L device:\n" +
+                            "    adb pull /system/framework /tmp/framework\n" +
+                            "    export BOOTCLASSPATH=`adb shell \"echo \\\\$BOOTCLASPATH\"`\n" +
+                            "    baksmali disassemble --register-info ARGS,DEST blah.apk -b $BOOTCLASSPATH -d " +
+                            "/tmp/framework";
+
+                    Iterable<String> lines = StringWrapper.wrapStringOnBreaks(registerInfoHelp,
+                            ConsoleUtil.getConsoleWidth());
+                    for (String line : lines) {
+                        System.out.println(line);
+                    }
+                } else {
+                    JCommander command = ExtendedCommands.getSubcommand(parentJc, cmd);
+                    if (command == null) {
+                        System.err.println("No such command: " + cmd);
+                    } else {
+                        printedHelp = true;
+                        System.out.println(new HelpFormatter()
+                                .width(ConsoleUtil.getConsoleWidth())
+                                .format(((Command)command.getObjects().get(0)).getCommandHierarchy()));
+                    }
+                }
+            }
+            if (!printedHelp) {
+                System.out.println(new HelpFormatter()
+                        .width(ConsoleUtil.getConsoleWidth())
+                        .format(commandAncestors));
+            }
+        }
+    }
+
+    @Parameters(hidden =  true)
+    @ExtendedParameters(commandName = "hlep")
+    public static class HlepCommand extends HelpCommand {
+        public HlepCommand(@Nonnull List<JCommander> commandAncestors) {
+            super(commandAncestors);
+        }
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/ListClassesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListClassesCommand.java
new file mode 100644
index 0000000..fb172bd
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/ListClassesCommand.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import org.jf.dexlib2.iface.ClassDef;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+@Parameters(commandDescription = "Lists the classes in a dex file.")
+@ExtendedParameters(
+        commandName = "classes",
+        commandAliases = { "class", "c" })
+public class ListClassesCommand extends DexInputCommand {
+
+    @Parameter(names = {"-h", "-?", "--help"}, help = true,
+            description = "Show usage information")
+    private boolean help;
+
+    public ListClassesCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    @Override public void run() {
+        if (help || inputList == null || inputList.isEmpty()) {
+            usage();
+            return;
+        }
+
+        if (inputList.size() > 1) {
+            System.err.println("Too many files specified");
+            usage();
+            return;
+        }
+
+        String input = inputList.get(0);
+        loadDexFile(input);
+
+        for (ClassDef classDef: dexFile.getClasses()) {
+            System.out.println(classDef.getType());
+        }
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/ListCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListCommand.java
new file mode 100644
index 0000000..9547620
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/ListCommand.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import org.jf.baksmali.ListHelpCommand.ListHlepCommand;
+import org.jf.util.jcommander.Command;
+import org.jf.util.jcommander.ExtendedCommands;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+@Parameters(commandDescription = "Lists various objects in a dex file.")
+@ExtendedParameters(
+        commandName = "list",
+        commandAliases = "l")
+public class ListCommand extends Command {
+
+    @Parameter(names = {"-h", "-?", "--help"}, help = true,
+            description = "Show usage information")
+    private boolean help;
+
+    public ListCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    @Override protected void setupCommand(JCommander jc) {
+        List<JCommander> hierarchy = getCommandHierarchy();
+
+        ExtendedCommands.addExtendedCommand(jc, new ListStringsCommand(hierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new ListMethodsCommand(hierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new ListFieldsCommand(hierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new ListTypesCommand(hierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new ListClassesCommand(hierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new ListDexCommand(hierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new ListVtablesCommand(hierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new ListFieldOffsetsCommand(hierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new ListDependenciesCommand(hierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new ListHelpCommand(hierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new ListHlepCommand(hierarchy));
+    }
+
+    @Override public void run() {
+        JCommander jc = getJCommander();
+        if (help || jc.getParsedCommand() == null) {
+            usage();
+            return;
+        }
+
+        Command command = (Command)jc.getCommands().get(jc.getParsedCommand()).getObjects().get(0);
+        command.run();
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/ListDependenciesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListDependenciesCommand.java
new file mode 100644
index 0000000..636a87c
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/ListDependenciesCommand.java
@@ -0,0 +1,118 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import com.google.common.collect.Lists;
+import org.jf.dexlib2.Opcodes;
+import org.jf.dexlib2.dexbacked.DexBackedDexFile;
+import org.jf.dexlib2.dexbacked.DexBackedOdexFile;
+import org.jf.dexlib2.dexbacked.OatFile;
+import org.jf.util.jcommander.Command;
+import org.jf.util.jcommander.ExtendedParameter;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.io.*;
+import java.util.List;
+
+@Parameters(commandDescription = "Lists the stored dependencies in an odex/oat file.")
+@ExtendedParameters(
+        commandName = "dependencies",
+        commandAliases = { "deps", "dep" })
+public class ListDependenciesCommand extends Command {
+
+    @Parameter(names = {"-h", "-?", "--help"}, help = true,
+            description = "Show usage information")
+    private boolean help;
+
+    @Parameter(description = "An oat/odex file")
+    @ExtendedParameter(argumentNames = "file")
+    private List<String> inputList = Lists.newArrayList();
+
+    public ListDependenciesCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    @Override public void run() {
+        if (help || inputList == null || inputList.isEmpty()) {
+            usage();
+            return;
+        }
+
+        if (inputList.size() > 1) {
+            System.err.println("Too many files specified");
+            usage();
+            return;
+        }
+
+        String input = inputList.get(0);
+        InputStream inputStream = null;
+        try {
+            inputStream = new BufferedInputStream(new FileInputStream(input));
+        } catch (FileNotFoundException ex) {
+            System.err.println("Could not find file: " + input);
+            System.exit(-1);
+        }
+
+        try {
+            OatFile oatFile = OatFile.fromInputStream(inputStream);
+            for (String entry: oatFile.getBootClassPath()) {
+                System.out.println(entry);
+            }
+            return;
+        } catch (OatFile.NotAnOatFileException ex) {
+            // ignore
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        }
+
+        try {
+            DexBackedOdexFile odexFile = DexBackedOdexFile.fromInputStream(Opcodes.getDefault(), inputStream);
+            for (String entry: odexFile.getDependencies()) {
+                System.out.println(entry);
+            }
+            return;
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        } catch (DexBackedOdexFile.NotAnOdexFile ex) {
+            // handled below
+        } catch (DexBackedDexFile.NotADexFile ex) {
+            // handled below
+        }
+
+        System.err.println(input + " is not an odex or oat file.");
+        System.exit(-1);
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java
new file mode 100644
index 0000000..d5862eb
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/ListDexCommand.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import com.google.common.collect.Lists;
+import org.jf.dexlib2.DexFileFactory;
+import org.jf.dexlib2.Opcodes;
+import org.jf.dexlib2.dexbacked.DexBackedDexFile;
+import org.jf.dexlib2.iface.MultiDexContainer;
+import org.jf.util.jcommander.Command;
+import org.jf.util.jcommander.ExtendedParameter;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+
+@Parameters(commandDescription = "Lists the dex files in an apk/oat file.")
+@ExtendedParameters(
+        commandName = "dex",
+        commandAliases = "d")
+public class ListDexCommand extends Command {
+
+    @Parameter(names = {"-h", "-?", "--help"}, help = true,
+            description = "Show usage information")
+    private boolean help;
+
+    @Parameter(description = "An apk or oat file.")
+    @ExtendedParameter(argumentNames = "file")
+    private List<String> inputList = Lists.newArrayList();
+
+    public ListDexCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    @Override public void run() {
+        if (help || inputList == null || inputList.isEmpty()) {
+            usage();
+            return;
+        }
+
+        if (inputList.size() > 1) {
+            System.err.println("Too many files specified");
+            usage();
+            return;
+        }
+
+        String input = inputList.get(0);
+        File file = new File(input);
+
+        if (!file.exists()) {
+            System.err.println(String.format("Could not find the file: %s", input));
+            System.exit(-1);
+        }
+
+        List<String> entries;
+        try {
+            MultiDexContainer<? extends DexBackedDexFile> container =
+                    DexFileFactory.loadDexContainer(file, Opcodes.getDefault());
+            entries = container.getDexEntryNames();
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        }
+
+        for (String entry: entries) {
+            System.out.println(entry);
+        }
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/ListFieldOffsetsCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListFieldOffsetsCommand.java
new file mode 100644
index 0000000..d6dd6e2
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/ListFieldOffsetsCommand.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import com.beust.jcommander.ParametersDelegate;
+import org.jf.dexlib2.analysis.ClassProto;
+import org.jf.dexlib2.iface.ClassDef;
+import org.jf.dexlib2.iface.reference.FieldReference;
+import org.jf.util.SparseArray;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.util.List;
+
+@Parameters(commandDescription = "Lists the instance field offsets for classes in a dex file.")
+@ExtendedParameters(
+        commandName = "fieldoffsets",
+        commandAliases = { "fieldoffset", "fo" })
+public class ListFieldOffsetsCommand extends DexInputCommand {
+
+    @Parameter(names = {"-h", "-?", "--help"}, help = true,
+            description = "Show usage information")
+    private boolean help;
+
+    @ParametersDelegate
+    private AnalysisArguments analysisArguments = new AnalysisArguments();
+
+    public ListFieldOffsetsCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    @Override public void run() {
+        if (help || inputList == null || inputList.isEmpty()) {
+            usage();
+            return;
+        }
+
+        if (inputList.size() > 1) {
+            System.err.println("Too many files specified");
+            usage();
+            return;
+        }
+
+        String input = inputList.get(0);
+        loadDexFile(input);
+        BaksmaliOptions options = getOptions();
+
+        try {
+            for (ClassDef classDef: dexFile.getClasses()) {
+                ClassProto classProto = (ClassProto) options.classPath.getClass(classDef);
+                SparseArray<FieldReference> fields = classProto.getInstanceFields();
+                String className = "Class "  + classDef.getType() + " : " + fields.size() + " instance fields\n";
+                System.out.write(className.getBytes());
+                for (int i=0;i<fields.size();i++) {
+                    String field = fields.keyAt(i) + ":" + fields.valueAt(i).getType() + " " + fields.valueAt(i).getName() + "\n";
+                    System.out.write(field.getBytes());
+                }
+                System.out.write("\n".getBytes());
+            }
+            System.out.close();
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    @Nonnull
+    private BaksmaliOptions getOptions() {
+        if (dexFile == null) {
+            throw new IllegalStateException("You must call loadDexFile first");
+        }
+
+        final BaksmaliOptions options = new BaksmaliOptions();
+
+        options.apiLevel = apiLevel;
+
+        try {
+            options.classPath = analysisArguments.loadClassPathForDexFile(
+                    inputFile.getAbsoluteFile().getParentFile(), dexFile, false);
+        } catch (Exception ex) {
+            System.err.println("Error occurred while loading class path files.");
+            ex.printStackTrace(System.err);
+            System.exit(-1);
+        }
+
+        return options;
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/ListFieldsCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListFieldsCommand.java
new file mode 100644
index 0000000..c4d090d
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/ListFieldsCommand.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameters;
+import org.jf.dexlib2.ReferenceType;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+@Parameters(commandDescription = "Lists the fields in a dex file's field table.")
+@ExtendedParameters(
+        commandName = "fields",
+        commandAliases = { "field", "f" })
+public class ListFieldsCommand extends ListReferencesCommand {
+    public ListFieldsCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors, ReferenceType.FIELD);
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/ListHelpCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListHelpCommand.java
new file mode 100644
index 0000000..2e64286
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/ListHelpCommand.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import com.google.common.collect.Iterables;
+import org.jf.util.ConsoleUtil;
+import org.jf.util.jcommander.*;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+@Parameters(commandDescription = "Shows usage information")
+@ExtendedParameters(
+        commandName = "help",
+        commandAliases = "h")
+public class ListHelpCommand extends Command {
+
+    @Parameter(description = "If specified, show the detailed usage information for the given commands")
+    @ExtendedParameter(argumentNames = "commands")
+    private List<String> commands;
+
+    public ListHelpCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    public void run() {
+        if (commands == null || commands.isEmpty()) {
+            System.out.println(new HelpFormatter()
+                    .width(ConsoleUtil.getConsoleWidth())
+                    .format(commandAncestors));
+        } else {
+            boolean printedHelp = false;
+            JCommander parentJc = Iterables.getLast(commandAncestors);
+            for (String cmd : commands) {
+                JCommander command = ExtendedCommands.getSubcommand(parentJc, cmd);
+                if (command == null) {
+                    System.err.println("No such command: " + cmd);
+                } else {
+                    printedHelp = true;
+                    System.out.println(new HelpFormatter()
+                            .width(ConsoleUtil.getConsoleWidth())
+                            .format(((Command)command.getObjects().get(0)).getCommandHierarchy()));
+                }
+            }
+            if (!printedHelp) {
+                System.out.println(new HelpFormatter()
+                        .width(ConsoleUtil.getConsoleWidth())
+                        .format(commandAncestors));
+            }
+        }
+    }
+
+    @Parameters(hidden =  true)
+    @ExtendedParameters(commandName = "hlep")
+    public static class ListHlepCommand extends ListHelpCommand {
+        public ListHlepCommand(@Nonnull List<JCommander> commandAncestors) {
+            super(commandAncestors);
+        }
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/ListMethodsCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListMethodsCommand.java
new file mode 100644
index 0000000..603e764
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/ListMethodsCommand.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameters;
+import org.jf.dexlib2.ReferenceType;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+@Parameters(commandDescription = "Lists the methods in a dex file's method table.")
+@ExtendedParameters(
+        commandName = "methods",
+        commandAliases = { "method", "m" })
+public class ListMethodsCommand extends ListReferencesCommand {
+    public ListMethodsCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors, ReferenceType.METHOD);
+    }
+}
\ No newline at end of file
diff --git a/baksmali/src/main/java/org/jf/baksmali/ListReferencesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListReferencesCommand.java
new file mode 100644
index 0000000..da9c3e3
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/ListReferencesCommand.java
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import org.jf.dexlib2.iface.reference.Reference;
+import org.jf.dexlib2.util.ReferenceUtil;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+public abstract class ListReferencesCommand extends DexInputCommand {
+
+    private final int referenceType;
+
+    @Parameter(names = {"-h", "-?", "--help"}, help = true,
+            description = "Show usage information")
+    private boolean help;
+
+    public ListReferencesCommand(@Nonnull List<JCommander> commandAncestors, int referenceType) {
+        super(commandAncestors);
+        this.referenceType = referenceType;
+    }
+
+    @Override public void run() {
+        if (help || inputList == null || inputList.isEmpty()) {
+            usage();
+            return;
+        }
+
+        if (inputList.size() > 1) {
+            System.err.println("Too many files specified");
+            usage();
+            return;
+        }
+
+        String input = inputList.get(0);
+        loadDexFile(input);
+
+        for (Reference reference: dexFile.getReferences(referenceType)) {
+            System.out.println(ReferenceUtil.getReferenceString(reference));
+        }
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/ListStringsCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListStringsCommand.java
new file mode 100644
index 0000000..8694f91
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/ListStringsCommand.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameters;
+import org.jf.dexlib2.ReferenceType;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+@Parameters(commandDescription = "Lists the strings in a dex file's string table.")
+@ExtendedParameters(
+        commandName = "strings",
+        commandAliases = { "string", "str", "s" })
+public class ListStringsCommand extends ListReferencesCommand {
+    public ListStringsCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors, ReferenceType.STRING);
+    }
+}
\ No newline at end of file
diff --git a/baksmali/src/main/java/org/jf/baksmali/ListTypesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListTypesCommand.java
new file mode 100644
index 0000000..fbff2f2
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/ListTypesCommand.java
@@ -0,0 +1,50 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameters;
+import org.jf.dexlib2.ReferenceType;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+@Parameters(commandDescription = "Lists the type ids in a dex file's type table.")
+@ExtendedParameters(
+        commandName = "types",
+        commandAliases = { "type", "t" })
+public class ListTypesCommand extends ListReferencesCommand {
+    public ListTypesCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors, ReferenceType.TYPE);
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/ListVtablesCommand.java b/baksmali/src/main/java/org/jf/baksmali/ListVtablesCommand.java
new file mode 100644
index 0000000..74435b3
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/ListVtablesCommand.java
@@ -0,0 +1,157 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import com.beust.jcommander.ParametersDelegate;
+import org.jf.baksmali.AnalysisArguments.CheckPackagePrivateArgument;
+import org.jf.dexlib2.AccessFlags;
+import org.jf.dexlib2.analysis.ClassProto;
+import org.jf.dexlib2.iface.ClassDef;
+import org.jf.dexlib2.iface.Method;
+import org.jf.util.jcommander.ExtendedParameter;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.util.List;
+
+@Parameters(commandDescription = "Lists the virtual method tables for classes in a dex file.")
+@ExtendedParameters(
+        commandName = "vtables",
+        commandAliases = { "vtable", "v" })
+public class ListVtablesCommand extends DexInputCommand {
+
+    @Parameter(names = {"-h", "-?", "--help"}, help = true,
+            description = "Show usage information")
+    private boolean help;
+
+    @ParametersDelegate
+    private AnalysisArguments analysisArguments = new AnalysisArguments();
+
+    @ParametersDelegate
+    private CheckPackagePrivateArgument checkPackagePrivateArgument = new CheckPackagePrivateArgument();
+
+    @Parameter(names = "--classes",
+            description = "A comma separated list of classes. Only print the vtable for these classes")
+    @ExtendedParameter(argumentNames = "classes")
+    private List<String> classes = null;
+
+    @Parameter(names = "--override-oat-version",
+            description = "Uses a classpath for the given oat version, regardless of the actual oat version. This " +
+                    "can be used, e.g. to list vtables from a dex file, as if they were in an oat file of the given " +
+                    "version.")
+    private int oatVersion = 0;
+
+    public ListVtablesCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    @Override public void run() {
+        if (help || inputList == null || inputList.isEmpty()) {
+            usage();
+            return;
+        }
+
+        if (inputList.size() > 1) {
+            System.err.println("Too many files specified");
+            usage();
+            return;
+        }
+
+        String input = inputList.get(0);
+        loadDexFile(input);
+
+        BaksmaliOptions options = getOptions();
+        if (options == null) {
+            return;
+        }
+
+        try {
+            if (classes != null && !classes.isEmpty()) {
+                for (String cls: classes) {
+                    listClassVtable((ClassProto)options.classPath.getClass(cls));
+                }
+                return;
+            }
+
+            for (ClassDef classDef : dexFile.getClasses()) {
+                if (!AccessFlags.INTERFACE.isSet(classDef.getAccessFlags())) {
+                    listClassVtable((ClassProto)options.classPath.getClass(classDef));
+                }
+            }
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    private void listClassVtable(ClassProto classProto) throws IOException {
+        List<Method> methods = classProto.getVtable();
+        String className = "Class " + classProto.getType() + " extends " + classProto.getSuperclass() +
+                " : " + methods.size() + " methods\n";
+        System.out.write(className.getBytes());
+        for (int i = 0; i < methods.size(); i++) {
+            Method method = methods.get(i);
+
+            String methodString = i + ":" + method.getDefiningClass() + "->" + method.getName() + "(";
+            for (CharSequence parameter : method.getParameterTypes()) {
+                methodString += parameter;
+            }
+            methodString += ")" + method.getReturnType() + "\n";
+            System.out.write(methodString.getBytes());
+        }
+        System.out.write("\n".getBytes());
+    }
+
+    protected BaksmaliOptions getOptions() {
+        if (dexFile == null) {
+            throw new IllegalStateException("You must call loadDexFile first");
+        }
+
+        final BaksmaliOptions options = new BaksmaliOptions();
+
+        options.apiLevel = apiLevel;
+
+        try {
+            options.classPath = analysisArguments.loadClassPathForDexFile(inputFile.getAbsoluteFile().getParentFile(),
+                    dexFile, checkPackagePrivateArgument.checkPackagePrivateAccess, oatVersion);
+        } catch (Exception ex) {
+            System.err.println("Error occurred while loading class path files.");
+            ex.printStackTrace(System.err);
+            return null;
+        }
+
+        return options;
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/Main.java b/baksmali/src/main/java/org/jf/baksmali/Main.java
new file mode 100644
index 0000000..66d9b4f
--- /dev/null
+++ b/baksmali/src/main/java/org/jf/baksmali/Main.java
@@ -0,0 +1,126 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.baksmali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.google.common.collect.Lists;
+import org.jf.baksmali.HelpCommand.HlepCommand;
+import org.jf.util.jcommander.Command;
+import org.jf.util.jcommander.ExtendedCommands;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Properties;
+
+@ExtendedParameters(
+        includeParametersInUsage = true,
+        commandName = "baksmali",
+        postfixDescription = "See baksmali help <command> for more information about a specific command")
+public class Main extends Command {
+    public static final String VERSION = loadVersion();
+
+    @Parameter(names = {"--help", "-h", "-?"}, help = true,
+            description = "Show usage information")
+    private boolean help;
+
+    @Parameter(names = {"--version", "-v"}, help = true,
+            description = "Print the version of baksmali and then exit")
+    public boolean version;
+
+    private JCommander jc;
+
+    public Main() {
+        super(Lists.<JCommander>newArrayList());
+    }
+
+    @Override public void run() {
+    }
+
+    @Override protected JCommander getJCommander() {
+        return jc;
+    }
+
+    public static void main(String[] args) {
+        Main main = new Main();
+
+        JCommander jc = new JCommander(main);
+        main.jc = jc;
+        jc.setProgramName("baksmali");
+        List<JCommander> commandHierarchy = main.getCommandHierarchy();
+
+        ExtendedCommands.addExtendedCommand(jc, new DisassembleCommand(commandHierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new DeodexCommand(commandHierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new DumpCommand(commandHierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new HelpCommand(commandHierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new HlepCommand(commandHierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new ListCommand(commandHierarchy));
+
+        jc.parse(args);
+
+        if (main.version) {
+            version();
+        }
+
+        if (jc.getParsedCommand() == null || main.help) {
+            main.usage();
+            return;
+        }
+
+        Command command = (Command)jc.getCommands().get(jc.getParsedCommand()).getObjects().get(0);
+        command.run();
+    }
+
+    protected static void version() {
+        System.out.println("baksmali " + VERSION + " (http://smali.org)");
+        System.out.println("Copyright (C) 2010 Ben Gruver (JesusFreke@JesusFreke.com)");
+        System.out.println("BSD license (http://www.opensource.org/licenses/bsd-license.php)");
+        System.exit(0);
+    }
+
+    private static String loadVersion() {
+        InputStream propertiesStream = Baksmali.class.getClassLoader().getResourceAsStream("baksmali.properties");
+        String version = "[unknown version]";
+        if (propertiesStream != null) {
+            Properties properties = new Properties();
+            try {
+                properties.load(propertiesStream);
+                version = properties.getProperty("application.version");
+            } catch (IOException ex) {
+                // ignore
+            }
+        }
+        return version;
+    }
+}
diff --git a/baksmali/src/main/java/org/jf/baksmali/baksmaliOptions.java b/baksmali/src/main/java/org/jf/baksmali/baksmaliOptions.java
deleted file mode 100644
index 32685dd..0000000
--- a/baksmali/src/main/java/org/jf/baksmali/baksmaliOptions.java
+++ /dev/null
@@ -1,108 +0,0 @@
-/*
- * Copyright 2013, Google Inc.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- *     * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *     * Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following disclaimer
- * in the documentation and/or other materials provided with the
- * distribution.
- *     * Neither the name of Google Inc. nor the names of its
- * contributors may be used to endorse or promote products derived from
- * this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.jf.baksmali;
-
-import com.google.common.collect.Lists;
-import org.jf.dexlib2.analysis.ClassPath;
-import org.jf.dexlib2.analysis.InlineMethodResolver;
-import org.jf.dexlib2.util.SyntheticAccessorResolver;
-
-import javax.annotation.Nullable;
-import java.io.File;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class baksmaliOptions {
-    // register info values
-    public static final int ALL = 1;
-    public static final int ALLPRE = 2;
-    public static final int ALLPOST = 4;
-    public static final int ARGS = 8;
-    public static final int DEST = 16;
-    public static final int MERGE = 32;
-    public static final int FULLMERGE = 64;
-
-    public int apiLevel = 15;
-    public String outputDirectory = "out";
-    @Nullable public String dexEntry = null;
-    public List<String> bootClassPathDirs = Lists.newArrayList();
-
-    public List<String> bootClassPathEntries = Lists.newArrayList();
-    public List<String> extraClassPathEntries = Lists.newArrayList();
-
-    public Map<String,String> resourceIdFileEntries = new HashMap<String,String>();
-    public Map<Integer,String> resourceIds = new HashMap<Integer,String>();
-
-    public boolean noParameterRegisters = false;
-    public boolean useLocalsDirective = false;
-    public boolean useSequentialLabels = false;
-    public boolean outputDebugInfo = true;
-    public boolean addCodeOffsets = false;
-    public boolean noAccessorComments = false;
-    public boolean allowOdex = false;
-    public boolean deodex = false;
-    public boolean experimental = false;
-    public boolean ignoreErrors = false;
-    public boolean checkPackagePrivateAccess = false;
-    public boolean useImplicitReferences = false;
-    public boolean normalizeVirtualMethods = false;
-    public File customInlineDefinitions = null;
-    public InlineMethodResolver inlineResolver = null;
-    public int registerInfo = 0;
-    public ClassPath classPath = null;
-    public int jobs = Runtime.getRuntime().availableProcessors();
-    public boolean disassemble = true;
-    public boolean dump = false;
-    public String dumpFileName = null;
-
-    public SyntheticAccessorResolver syntheticAccessorResolver = null;
-
-    public void setBootClassPath(String bootClassPath) {
-        bootClassPathEntries = Lists.newArrayList(bootClassPath.split(":"));
-    }
-
-    public void addExtraClassPath(String extraClassPath) {
-        if (extraClassPath.startsWith(":")) {
-            extraClassPath = extraClassPath.substring(1);
-        }
-        extraClassPathEntries.addAll(Arrays.asList(extraClassPath.split(":")));
-    }
-
-    public void setResourceIdFiles(String resourceIdFiles) {
-        for (String resourceIdFile: resourceIdFiles.split(":")) {
-            String[] entry = resourceIdFile.split("=");
-            resourceIdFileEntries.put(entry[1], entry[0]);
-        }
-    }
-}
diff --git a/baksmali/src/main/java/org/jf/baksmali/dump.java b/baksmali/src/main/java/org/jf/baksmali/dump.java
deleted file mode 100644
index 79405e5..0000000
--- a/baksmali/src/main/java/org/jf/baksmali/dump.java
+++ /dev/null
@@ -1,73 +0,0 @@
-/*
- * [The "BSD licence"]
- * Copyright (c) 2010 Ben Gruver (JesusFreke)
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. The name of the author may not be used to endorse or promote products
- *    derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.jf.baksmali;
-
-import org.jf.dexlib2.Opcodes;
-import org.jf.dexlib2.dexbacked.DexBackedDexFile;
-import org.jf.dexlib2.dexbacked.raw.RawDexFile;
-import org.jf.dexlib2.dexbacked.raw.util.DexAnnotator;
-import org.jf.util.ConsoleUtil;
-
-import java.io.BufferedWriter;
-import java.io.FileWriter;
-import java.io.IOException;
-import java.io.Writer;
-
-public class dump {
-    public static void dump(DexBackedDexFile dexFile, String dumpFileName, int apiLevel) throws IOException {
-        if (dumpFileName != null) {
-            Writer writer = null;
-
-            try {
-                writer = new BufferedWriter(new FileWriter(dumpFileName));
-
-                int consoleWidth = ConsoleUtil.getConsoleWidth();
-                if (consoleWidth <= 0) {
-                    consoleWidth = 120;
-                }
-
-                RawDexFile rawDexFile = new RawDexFile(Opcodes.forApi(apiLevel), dexFile);
-                DexAnnotator annotator = new DexAnnotator(rawDexFile, consoleWidth);
-                annotator.writeAnnotations(writer);
-            } catch (IOException ex) {
-                System.err.println("There was an error while dumping the dex file to " + dumpFileName);
-                ex.printStackTrace(System.err);
-            } finally {
-                if (writer != null) {
-                    try {
-                        writer.close();
-                    } catch (IOException ex) {
-                        System.err.println("There was an error while closing the dump file " + dumpFileName);
-                        ex.printStackTrace(System.err);
-                    }
-                }
-            }
-        }
-    }
-}
diff --git a/baksmali/src/main/java/org/jf/baksmali/main.java b/baksmali/src/main/java/org/jf/baksmali/main.java
deleted file mode 100644
index 86f43d8..0000000
--- a/baksmali/src/main/java/org/jf/baksmali/main.java
+++ /dev/null
@@ -1,626 +0,0 @@
-/*
- * [The "BSD licence"]
- * Copyright (c) 2010 Ben Gruver (JesusFreke)
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. The name of the author may not be used to endorse or promote products
- *    derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.jf.baksmali;
-
-import com.google.common.collect.Lists;
-import org.apache.commons.cli.*;
-import org.jf.dexlib2.DexFileFactory;
-import org.jf.dexlib2.DexFileFactory.MultipleDexFilesException;
-import org.jf.dexlib2.analysis.InlineMethodResolver;
-import org.jf.dexlib2.dexbacked.DexBackedDexFile;
-import org.jf.dexlib2.dexbacked.DexBackedOdexFile;
-import org.jf.dexlib2.dexbacked.OatFile.OatDexFile;
-import org.jf.dexlib2.iface.DexFile;
-import org.jf.util.ConsoleUtil;
-import org.jf.util.SmaliHelpFormatter;
-
-import javax.annotation.Nonnull;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.util.List;
-import java.util.Locale;
-import java.util.Properties;
-
-public class main {
-
-    public static final String VERSION;
-
-    private static final Options basicOptions;
-    private static final Options debugOptions;
-    private static final Options options;
-
-    static {
-        options = new Options();
-        basicOptions = new Options();
-        debugOptions = new Options();
-        buildOptions();
-
-        InputStream templateStream = baksmali.class.getClassLoader().getResourceAsStream("baksmali.properties");
-        if (templateStream != null) {
-            Properties properties = new Properties();
-            String version = "(unknown)";
-            try {
-                properties.load(templateStream);
-                version = properties.getProperty("application.version");
-            } catch (IOException ex) {
-                // ignore
-            }
-            VERSION = version;
-        } else {
-            VERSION = "[unknown version]";
-        }
-    }
-
-    /**
-     * This class is uninstantiable.
-     */
-    private main() {
-    }
-
-    /**
-     * A more programmatic-friendly entry point for baksmali
-     *
-     * @param options a baksmaliOptions object with the options to run baksmali with
-     * @param inputDexFile The DexFile to disassemble
-     * @return true if disassembly completed with no errors, or false if errors were encountered
-     */
-    public static boolean run(@Nonnull baksmaliOptions options, @Nonnull DexFile inputDexFile) throws IOException {
-        if (options.bootClassPathEntries.isEmpty() &&
-                (options.deodex || options.registerInfo != 0 || options.normalizeVirtualMethods)) {
-            if (inputDexFile instanceof DexBackedOdexFile) {
-                options.bootClassPathEntries = ((DexBackedOdexFile)inputDexFile).getDependencies();
-            } else {
-                options.bootClassPathEntries = getDefaultBootClassPathForApi(options.apiLevel,
-                        options.experimental);
-            }
-        }
-
-        if (options.customInlineDefinitions == null && inputDexFile instanceof DexBackedOdexFile) {
-            options.inlineResolver =
-                    InlineMethodResolver.createInlineMethodResolver(
-                            ((DexBackedOdexFile)inputDexFile).getOdexVersion());
-        }
-
-        boolean errorOccurred = false;
-        if (options.disassemble) {
-            errorOccurred = !baksmali.disassembleDexFile(inputDexFile, options);
-        }
-
-        if (options.dump) {
-            if (!(inputDexFile instanceof DexBackedDexFile)) {
-                throw new IllegalArgumentException("Annotated hex-dumps require a DexBackedDexFile");
-            }
-            dump.dump((DexBackedDexFile)inputDexFile, options.dumpFileName, options.apiLevel);
-        }
-
-        return !errorOccurred;
-    }
-
-    /**
-     * Run!
-     */
-    public static void main(String[] args) throws IOException {
-        Locale locale = new Locale("en", "US");
-        Locale.setDefault(locale);
-
-        CommandLineParser parser = new PosixParser();
-        CommandLine commandLine;
-
-        try {
-            commandLine = parser.parse(options, args);
-        } catch (ParseException ex) {
-            usage();
-            return;
-        }
-
-        baksmaliOptions options = new baksmaliOptions();
-
-        String[] remainingArgs = commandLine.getArgs();
-        Option[] clOptions = commandLine.getOptions();
-
-        for (int i=0; i<clOptions.length; i++) {
-            Option option = clOptions[i];
-            String opt = option.getOpt();
-
-            switch (opt.charAt(0)) {
-                case 'v':
-                    version();
-                    return;
-                case '?':
-                    while (++i < clOptions.length) {
-                        if (clOptions[i].getOpt().charAt(0) == '?') {
-                            usage(true);
-                            return;
-                        }
-                    }
-                    usage(false);
-                    return;
-                case 'o':
-                    options.outputDirectory = commandLine.getOptionValue("o");
-                    break;
-                case 'p':
-                    options.noParameterRegisters = true;
-                    break;
-                case 'l':
-                    options.useLocalsDirective = true;
-                    break;
-                case 's':
-                    options.useSequentialLabels = true;
-                    break;
-                case 'b':
-                    options.outputDebugInfo = false;
-                    break;
-                case 'd':
-                    options.bootClassPathDirs.add(option.getValue());
-                    break;
-                case 'f':
-                    options.addCodeOffsets = true;
-                    break;
-                case 'r':
-                    String[] values = commandLine.getOptionValues('r');
-                    int registerInfo = 0;
-
-                    if (values == null || values.length == 0) {
-                        registerInfo = baksmaliOptions.ARGS | baksmaliOptions.DEST;
-                    } else {
-                        for (String value: values) {
-                            if (value.equalsIgnoreCase("ALL")) {
-                                registerInfo |= baksmaliOptions.ALL;
-                            } else if (value.equalsIgnoreCase("ALLPRE")) {
-                                registerInfo |= baksmaliOptions.ALLPRE;
-                            } else if (value.equalsIgnoreCase("ALLPOST")) {
-                                registerInfo |= baksmaliOptions.ALLPOST;
-                            } else if (value.equalsIgnoreCase("ARGS")) {
-                                registerInfo |= baksmaliOptions.ARGS;
-                            } else if (value.equalsIgnoreCase("DEST")) {
-                                registerInfo |= baksmaliOptions.DEST;
-                            } else if (value.equalsIgnoreCase("MERGE")) {
-                                registerInfo |= baksmaliOptions.MERGE;
-                            } else if (value.equalsIgnoreCase("FULLMERGE")) {
-                                registerInfo |= baksmaliOptions.FULLMERGE;
-                            } else {
-                                usage();
-                                return;
-                            }
-                        }
-
-                        if ((registerInfo & baksmaliOptions.FULLMERGE) != 0) {
-                            registerInfo &= ~baksmaliOptions.MERGE;
-                        }
-                    }
-                    options.registerInfo = registerInfo;
-                    break;
-                case 'c':
-                    String bcp = commandLine.getOptionValue("c");
-                    if (bcp != null && bcp.charAt(0) == ':') {
-                        options.addExtraClassPath(bcp);
-                    } else {
-                        options.setBootClassPath(bcp);
-                    }
-                    break;
-                case 'x':
-                    options.deodex = true;
-                    break;
-                case 'X':
-                    options.experimental = true;
-                    break;
-                case 'm':
-                    options.noAccessorComments = true;
-                    break;
-                case 'a':
-                    options.apiLevel = Integer.parseInt(commandLine.getOptionValue("a"));
-                    break;
-                case 'j':
-                    options.jobs = Integer.parseInt(commandLine.getOptionValue("j"));
-                    break;
-                case 'i':
-                    String rif = commandLine.getOptionValue("i");
-                    options.setResourceIdFiles(rif);
-                    break;
-                case 't':
-                    options.useImplicitReferences = true;
-                    break;
-                case 'e':
-                    options.dexEntry = commandLine.getOptionValue("e");
-                    break;
-                case 'k':
-                    options.checkPackagePrivateAccess = true;
-                    break;
-                case 'n':
-                    options.normalizeVirtualMethods = true;
-                    break;
-                case 'N':
-                    options.disassemble = false;
-                    break;
-                case 'D':
-                    options.dump = true;
-                    options.dumpFileName = commandLine.getOptionValue("D");
-                    break;
-                case 'I':
-                    options.ignoreErrors = true;
-                    break;
-                case 'T':
-                    options.customInlineDefinitions = new File(commandLine.getOptionValue("T"));
-                    break;
-                default:
-                    assert false;
-            }
-        }
-
-        if (remainingArgs.length != 1) {
-            usage();
-            return;
-        }
-
-        String inputDexPath = remainingArgs[0];
-        File dexFileFile = new File(inputDexPath);
-        if (!dexFileFile.exists()) {
-            System.err.println("Can't find the file " + inputDexPath);
-            System.exit(1);
-        }
-
-        //Read in and parse the dex file
-        DexBackedDexFile dexFile = null;
-        try {
-            dexFile = DexFileFactory.loadDexFile(dexFileFile, options.dexEntry, options.apiLevel, options.experimental);
-        } catch (MultipleDexFilesException ex) {
-            System.err.println(String.format("%s contains multiple dex files. You must specify which one to " +
-                    "disassemble with the -e option", dexFileFile.getName()));
-            System.err.println("Valid entries include:");
-
-            for (OatDexFile oatDexFile: ex.oatFile.getDexFiles()) {
-                System.err.println(oatDexFile.filename);
-            }
-            System.exit(1);
-        }
-
-        if (dexFile.hasOdexOpcodes()) {
-            if (!options.deodex) {
-                System.err.println("Warning: You are disassembling an odex file without deodexing it. You");
-                System.err.println("won't be able to re-assemble the results unless you deodex it with the -x");
-                System.err.println("option");
-                options.allowOdex = true;
-            }
-        } else {
-            options.deodex = false;
-        }
-
-        if (options.dump) {
-            if (options.dumpFileName == null) {
-                options.dumpFileName =  inputDexPath + ".dump";
-            }
-        }
-
-        try {
-            if (!run(options, dexFile)) {
-                System.exit(1);
-            }
-        } catch (IllegalArgumentException ex) {
-            System.err.println(ex.getMessage());
-            System.exit(1);
-        }
-    }
-
-    /**
-     * Prints the usage message.
-     */
-    private static void usage(boolean printDebugOptions) {
-        SmaliHelpFormatter formatter = new SmaliHelpFormatter();
-        int consoleWidth = ConsoleUtil.getConsoleWidth();
-        if (consoleWidth <= 0) {
-            consoleWidth = 80;
-        }
-
-        formatter.setWidth(consoleWidth);
-
-        formatter.printHelp("java -jar baksmali.jar [options] <dex-file>",
-                "disassembles and/or dumps a dex file", basicOptions, printDebugOptions?debugOptions:null);
-    }
-
-    private static void usage() {
-        usage(false);
-    }
-
-    /**
-     * Prints the version message.
-     */
-    protected static void version() {
-        System.out.println("baksmali " + VERSION + " (http://smali.googlecode.com)");
-        System.out.println("Copyright (C) 2010 Ben Gruver (JesusFreke@JesusFreke.com)");
-        System.out.println("BSD license (http://www.opensource.org/licenses/bsd-license.php)");
-        System.exit(0);
-    }
-
-    @SuppressWarnings("AccessStaticViaInstance")
-    private static void buildOptions() {
-        Option versionOption = OptionBuilder.withLongOpt("version")
-                .withDescription("prints the version then exits")
-                .create("v");
-
-        Option helpOption = OptionBuilder.withLongOpt("help")
-                .withDescription("prints the help message then exits. Specify twice for debug options")
-                .create("?");
-
-        Option outputDirOption = OptionBuilder.withLongOpt("output")
-                .withDescription("the directory where the disassembled files will be placed. The default is out")
-                .hasArg()
-                .withArgName("DIR")
-                .create("o");
-
-        Option noParameterRegistersOption = OptionBuilder.withLongOpt("no-parameter-registers")
-                .withDescription("use the v<n> syntax instead of the p<n> syntax for registers mapped to method " +
-                        "parameters")
-                .create("p");
-
-        Option deodexerantOption = OptionBuilder.withLongOpt("deodex")
-                .withDescription("deodex the given odex file. This option is ignored if the input file is not an " +
-                        "odex file")
-                .create("x");
-
-        Option experimentalOption = OptionBuilder.withLongOpt("experimental")
-                .withDescription("enable experimental opcodes to be disassembled, even if they aren't necessarily supported in the Android runtime yet")
-                .create("X");
-
-        Option useLocalsOption = OptionBuilder.withLongOpt("use-locals")
-                .withDescription("output the .locals directive with the number of non-parameter registers, rather" +
-                        " than the .register directive with the total number of register")
-                .create("l");
-
-        Option sequentialLabelsOption = OptionBuilder.withLongOpt("sequential-labels")
-                .withDescription("create label names using a sequential numbering scheme per label type, rather than " +
-                        "using the bytecode address")
-                .create("s");
-
-        Option noDebugInfoOption = OptionBuilder.withLongOpt("no-debug-info")
-                .withDescription("don't write out debug info (.local, .param, .line, etc.)")
-                .create("b");
-
-        Option registerInfoOption = OptionBuilder.withLongOpt("register-info")
-                .hasOptionalArgs()
-                .withArgName("REGISTER_INFO_TYPES")
-                .withValueSeparator(',')
-                .withDescription("print the specificed type(s) of register information for each instruction. " +
-                        "\"ARGS,DEST\" is the default if no types are specified.\nValid values are:\nALL: all " +
-                        "pre- and post-instruction registers.\nALLPRE: all pre-instruction registers\nALLPOST: all " +
-                        "post-instruction registers\nARGS: any pre-instruction registers used as arguments to the " +
-                        "instruction\nDEST: the post-instruction destination register, if any\nMERGE: Any " +
-                        "pre-instruction register has been merged from more than 1 different post-instruction " +
-                        "register from its predecessors\nFULLMERGE: For each register that would be printed by " +
-                        "MERGE, also show the incoming register types that were merged")
-                .create("r");
-
-        Option classPathOption = OptionBuilder.withLongOpt("bootclasspath")
-                .withDescription("A colon-separated list of bootclasspath jar/oat files to use for analysis. Add an " +
-                        "initial colon to specify that the jars/oats should be appended to the default bootclasspath " +
-                        "instead of replacing it")
-                .hasOptionalArg()
-                .withArgName("BOOTCLASSPATH")
-                .create("c");
-
-        Option classPathDirOption = OptionBuilder.withLongOpt("bootclasspath-dir")
-                .withDescription("the base folder to look for the bootclasspath files in. Defaults to the current " +
-                        "directory")
-                .hasArg()
-                .withArgName("DIR")
-                .create("d");
-
-        Option codeOffsetOption = OptionBuilder.withLongOpt("code-offsets")
-                .withDescription("add comments to the disassembly containing the code offset for each address")
-                .create("f");
-
-        Option noAccessorCommentsOption = OptionBuilder.withLongOpt("no-accessor-comments")
-                .withDescription("don't output helper comments for synthetic accessors")
-                .create("m");
-
-        Option apiLevelOption = OptionBuilder.withLongOpt("api-level")
-                .withDescription("The numeric api-level of the file being disassembled. If not " +
-                        "specified, it defaults to 15 (ICS).")
-                .hasArg()
-                .withArgName("API_LEVEL")
-                .create("a");
-
-        Option jobsOption = OptionBuilder.withLongOpt("jobs")
-                .withDescription("The number of threads to use. Defaults to the number of cores available, up to a " +
-                        "maximum of 6")
-                .hasArg()
-                .withArgName("NUM_THREADS")
-                .create("j");
-
-        Option resourceIdFilesOption = OptionBuilder.withLongOpt("resource-id-files")
-                .withDescription("the resource ID files to use, for analysis. A colon-separated list of prefix=file " +
-                        "pairs.  For example R=res/values/public.xml:" +
-                        "android.R=$ANDROID_HOME/platforms/android-19/data/res/values/public.xml")
-                .hasArg()
-                .withArgName("FILES")
-                .create("i");
-
-        Option noImplicitReferencesOption = OptionBuilder.withLongOpt("implicit-references")
-                .withDescription("Use implicit (type-less) method and field references")
-                .create("t");
-
-        Option checkPackagePrivateAccessOption = OptionBuilder.withLongOpt("check-package-private-access")
-                .withDescription("When deodexing, use the package-private access check when calculating vtable " +
-                        "indexes. It should only be needed for 4.2.0 odexes. The functionality was reverted for " +
-                        "4.2.1.")
-                .create("k");
-
-        Option normalizeVirtualMethods = OptionBuilder.withLongOpt("normalize-virtual-methods")
-                .withDescription("Normalize virtual method references to the reference the base method.")
-                .create("n");
-
-        Option dumpOption = OptionBuilder.withLongOpt("dump-to")
-                .withDescription("dumps the given dex file into a single annotated dump file named FILE" +
-                        " (<dexfile>.dump by default), along with the normal disassembly")
-                .hasOptionalArg()
-                .withArgName("FILE")
-                .create("D");
-
-        Option ignoreErrorsOption = OptionBuilder.withLongOpt("ignore-errors")
-                .withDescription("ignores any non-fatal errors that occur while disassembling/deodexing," +
-                        " ignoring the class if needed, and continuing with the next class. The default" +
-                        " behavior is to stop disassembling and exit once an error is encountered")
-                .create("I");
-
-        Option noDisassemblyOption = OptionBuilder.withLongOpt("no-disassembly")
-                .withDescription("suppresses the output of the disassembly")
-                .create("N");
-
-        Option inlineTableOption = OptionBuilder.withLongOpt("inline-table")
-                .withDescription("specify a file containing a custom inline method table to use for deodexing")
-                .hasArg()
-                .withArgName("FILE")
-                .create("T");
-
-        Option dexEntryOption = OptionBuilder.withLongOpt("dex-file")
-                .withDescription("looks for dex file named DEX_FILE, defaults to classes.dex")
-                .withArgName("DEX_FILE")
-                .hasArg()
-                .create("e");
-
-        basicOptions.addOption(versionOption);
-        basicOptions.addOption(helpOption);
-        basicOptions.addOption(outputDirOption);
-        basicOptions.addOption(noParameterRegistersOption);
-        basicOptions.addOption(deodexerantOption);
-        basicOptions.addOption(experimentalOption);
-        basicOptions.addOption(useLocalsOption);
-        basicOptions.addOption(sequentialLabelsOption);
-        basicOptions.addOption(noDebugInfoOption);
-        basicOptions.addOption(registerInfoOption);
-        basicOptions.addOption(classPathOption);
-        basicOptions.addOption(classPathDirOption);
-        basicOptions.addOption(codeOffsetOption);
-        basicOptions.addOption(noAccessorCommentsOption);
-        basicOptions.addOption(apiLevelOption);
-        basicOptions.addOption(jobsOption);
-        basicOptions.addOption(resourceIdFilesOption);
-        basicOptions.addOption(noImplicitReferencesOption);
-        basicOptions.addOption(dexEntryOption);
-        basicOptions.addOption(checkPackagePrivateAccessOption);
-        basicOptions.addOption(normalizeVirtualMethods);
-
-        debugOptions.addOption(dumpOption);
-        debugOptions.addOption(ignoreErrorsOption);
-        debugOptions.addOption(noDisassemblyOption);
-        debugOptions.addOption(inlineTableOption);
-
-        for (Object option: basicOptions.getOptions()) {
-            options.addOption((Option)option);
-        }
-        for (Object option: debugOptions.getOptions()) {
-            options.addOption((Option)option);
-        }
-    }
-
-    @Nonnull
-    private static List<String> getDefaultBootClassPathForApi(int apiLevel, boolean experimental) {
-        if (apiLevel < 9) {
-            return Lists.newArrayList(
-                    "/system/framework/core.jar",
-                    "/system/framework/ext.jar",
-                    "/system/framework/framework.jar",
-                    "/system/framework/android.policy.jar",
-                    "/system/framework/services.jar");
-        } else if (apiLevel < 12) {
-            return Lists.newArrayList(
-                    "/system/framework/core.jar",
-                    "/system/framework/bouncycastle.jar",
-                    "/system/framework/ext.jar",
-                    "/system/framework/framework.jar",
-                    "/system/framework/android.policy.jar",
-                    "/system/framework/services.jar",
-                    "/system/framework/core-junit.jar");
-        } else if (apiLevel < 14) {
-            return Lists.newArrayList(
-                    "/system/framework/core.jar",
-                    "/system/framework/apache-xml.jar",
-                    "/system/framework/bouncycastle.jar",
-                    "/system/framework/ext.jar",
-                    "/system/framework/framework.jar",
-                    "/system/framework/android.policy.jar",
-                    "/system/framework/services.jar",
-                    "/system/framework/core-junit.jar");
-        } else if (apiLevel < 16) {
-            return Lists.newArrayList(
-                    "/system/framework/core.jar",
-                    "/system/framework/core-junit.jar",
-                    "/system/framework/bouncycastle.jar",
-                    "/system/framework/ext.jar",
-                    "/system/framework/framework.jar",
-                    "/system/framework/android.policy.jar",
-                    "/system/framework/services.jar",
-                    "/system/framework/apache-xml.jar",
-                    "/system/framework/filterfw.jar");
-        } else if (apiLevel < 21) {
-            // this is correct as of api 17/4.2.2
-            return Lists.newArrayList(
-                    "/system/framework/core.jar",
-                    "/system/framework/core-junit.jar",
-                    "/system/framework/bouncycastle.jar",
-                    "/system/framework/ext.jar",
-                    "/system/framework/framework.jar",
-                    "/system/framework/telephony-common.jar",
-                    "/system/framework/mms-common.jar",
-                    "/system/framework/android.policy.jar",
-                    "/system/framework/services.jar",
-                    "/system/framework/apache-xml.jar");
-        } else if (apiLevel < 26) {
-            return Lists.newArrayList(
-                    "/system/framework/core-libart.jar",
-                    "/system/framework/conscrypt.jar",
-                    "/system/framework/okhttp.jar",
-                    "/system/framework/core-junit.jar",
-                    "/system/framework/bouncycastle.jar",
-                    "/system/framework/ext.jar",
-                    "/system/framework/framework.jar",
-                    "/system/framework/telephony-common.jar",
-                    "/system/framework/voip-common.jar",
-                    "/system/framework/ims-common.jar",
-                    "/system/framework/mms-common.jar",
-                    "/system/framework/android.policy.jar",
-                    "/system/framework/apache-xml.jar");
-        } else { // api >= 26
-            // TODO: verify, add new ones?
-            return Lists.newArrayList(
-                    "/system/framework/core-libart.jar",
-                    "/system/framework/conscrypt.jar",
-                    "/system/framework/okhttp.jar",
-                    "/system/framework/bouncycastle.jar",
-                    "/system/framework/ext.jar",
-                    "/system/framework/framework.jar",
-                    "/system/framework/telephony-common.jar",
-                    "/system/framework/voip-common.jar",
-                    "/system/framework/ims-common.jar",
-                    "/system/framework/mms-common.jar",
-                    "/system/framework/android.policy.jar",
-                    "/system/framework/apache-xml.jar");
-        }
-    }
-}
diff --git a/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java b/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java
index 2bb04dd..a68038d 100644
--- a/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java
+++ b/baksmali/src/test/java/org/jf/baksmali/AnalysisTest.java
@@ -36,7 +36,9 @@
 import junit.framework.Assert;
 import org.jf.baksmali.Adaptors.ClassDefinition;
 import org.jf.dexlib2.DexFileFactory;
+import org.jf.dexlib2.Opcodes;
 import org.jf.dexlib2.analysis.ClassPath;
+import org.jf.dexlib2.analysis.ClassProvider;
 import org.jf.dexlib2.iface.ClassDef;
 import org.jf.dexlib2.iface.DexFile;
 import org.jf.util.IndentingWriter;
@@ -49,6 +51,7 @@
 import java.io.StringWriter;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.util.ArrayList;
 
 public class AnalysisTest {
 
@@ -68,6 +71,11 @@
     }
 
     @Test
+    public void InstanceOfTest() throws IOException, URISyntaxException {
+        runTest("InstanceOfTest", true, true);
+    }
+
+    @Test
     public void MultipleStartInstructionsTest() throws IOException, URISyntaxException {
         runTest("MultipleStartInstructionsTest", true);
     }
@@ -83,16 +91,24 @@
     }
 
     public void runTest(String test, boolean registerInfo) throws IOException, URISyntaxException {
+        runTest(test, registerInfo, false);
+    }
+
+    public void runTest(String test, boolean registerInfo, boolean isArt) throws IOException, URISyntaxException {
         String dexFilePath = String.format("%s%sclasses.dex", test, File.separatorChar);
 
-        DexFile dexFile = DexFileFactory.loadDexFile(findResource(dexFilePath), 15, false);
+        DexFile dexFile = DexFileFactory.loadDexFile(findResource(dexFilePath), Opcodes.getDefault());
 
-        baksmaliOptions options = new baksmaliOptions();
+        BaksmaliOptions options = new BaksmaliOptions();
         if (registerInfo) {
-            options.registerInfo = baksmaliOptions.ALL | baksmaliOptions.FULLMERGE;
-            options.classPath = new ClassPath();
+            options.registerInfo = BaksmaliOptions.ALL | BaksmaliOptions.FULLMERGE;
+            if (isArt) {
+                options.classPath = new ClassPath(new ArrayList<ClassProvider>(), true, 56);
+            } else {
+                options.classPath = new ClassPath();
+            }
         }
-        options.useImplicitReferences = false;
+        options.implicitReferences = false;
 
         for (ClassDef classDef: dexFile.getClasses()) {
             StringWriter stringWriter = new StringWriter();
diff --git a/baksmali/src/test/java/org/jf/baksmali/BaksmaliTestUtils.java b/baksmali/src/test/java/org/jf/baksmali/BaksmaliTestUtils.java
index 1c570b6..4dd2ad9 100644
--- a/baksmali/src/test/java/org/jf/baksmali/BaksmaliTestUtils.java
+++ b/baksmali/src/test/java/org/jf/baksmali/BaksmaliTestUtils.java
@@ -48,10 +48,9 @@
 
 public class BaksmaliTestUtils {
     public static void assertSmaliCompiledEquals(String source, String expected,
-            baksmaliOptions options, boolean stripComments) throws IOException,
+                                                 BaksmaliOptions options, boolean stripComments) throws IOException,
             RecognitionException {
-        ClassDef classDef = SmaliTestUtils.compileSmali(source, options.apiLevel,
-                options.experimental);
+        ClassDef classDef = SmaliTestUtils.compileSmali(source, options.apiLevel);
 
         // Remove unnecessary whitespace and optionally strip all comments from smali file
         String normalizedActual = getNormalizedSmali(classDef, options, stripComments);
@@ -62,13 +61,13 @@
     }
 
     public static void assertSmaliCompiledEquals(String source, String expected,
-            baksmaliOptions options) throws IOException, RecognitionException {
+            BaksmaliOptions options) throws IOException, RecognitionException {
         assertSmaliCompiledEquals(source, expected, options, false);
     }
 
     public static void assertSmaliCompiledEquals(String source, String expected)
             throws IOException, RecognitionException {
-        baksmaliOptions options = new baksmaliOptions();
+        BaksmaliOptions options = new BaksmaliOptions();
         assertSmaliCompiledEquals(source, expected, options);
     }
 
@@ -81,7 +80,7 @@
     }
 
     @Nonnull
-    public static String getNormalizedSmali(@Nonnull ClassDef classDef, @Nonnull baksmaliOptions options,
+    public static String getNormalizedSmali(@Nonnull ClassDef classDef, @Nonnull BaksmaliOptions options,
                                             boolean stripComments)
             throws IOException {
         StringWriter stringWriter = new StringWriter();
diff --git a/baksmali/src/test/java/org/jf/baksmali/DexTest.java b/baksmali/src/test/java/org/jf/baksmali/DexTest.java
index 5a4db65..f9f5562 100644
--- a/baksmali/src/test/java/org/jf/baksmali/DexTest.java
+++ b/baksmali/src/test/java/org/jf/baksmali/DexTest.java
@@ -65,7 +65,7 @@
     }
 
     @Nonnull
-    protected DexBackedDexFile getInputDexFile(@Nonnull String testName, @Nonnull baksmaliOptions options) {
+    protected DexBackedDexFile getInputDexFile(@Nonnull String testName, @Nonnull BaksmaliOptions options) {
         try {
             // Load file from resources as a stream
             byte[] inputBytes = BaksmaliTestUtils.readResourceBytesFully(getInputFilename(testName));
diff --git a/baksmali/src/test/java/org/jf/baksmali/DisassemblyTest.java b/baksmali/src/test/java/org/jf/baksmali/DisassemblyTest.java
index 1a34e8c..769372e 100644
--- a/baksmali/src/test/java/org/jf/baksmali/DisassemblyTest.java
+++ b/baksmali/src/test/java/org/jf/baksmali/DisassemblyTest.java
@@ -57,10 +57,10 @@
     }
 
     protected void runTest(@Nonnull String testName) {
-        runTest(testName, new baksmaliOptions());
+        runTest(testName, new BaksmaliOptions());
     }
 
-    protected void runTest(@Nonnull String testName, @Nonnull baksmaliOptions options) {
+    protected void runTest(@Nonnull String testName, @Nonnull BaksmaliOptions options) {
         try {
             DexBackedDexFile inputDex = getInputDexFile(testName, options);
             Assert.assertEquals(1, inputDex.getClassCount());
diff --git a/baksmali/src/test/java/org/jf/baksmali/FieldGapOrderTest.java b/baksmali/src/test/java/org/jf/baksmali/FieldGapOrderTest.java
index 78fabc0..ad2aad5 100644
--- a/baksmali/src/test/java/org/jf/baksmali/FieldGapOrderTest.java
+++ b/baksmali/src/test/java/org/jf/baksmali/FieldGapOrderTest.java
@@ -42,7 +42,7 @@
 public class FieldGapOrderTest extends DexTest {
     @Test
     public void testOldOrder() {
-        DexFile dexFile = getInputDexFile("FieldGapOrder", new baksmaliOptions());
+        DexFile dexFile = getInputDexFile("FieldGapOrder", new BaksmaliOptions());
         Assert.assertEquals(3, dexFile.getClasses().size());
 
         ClassPath classPath = new ClassPath(Lists.newArrayList(new DexClassProvider(dexFile)), false, 66);
@@ -56,7 +56,7 @@
 
     @Test
     public void testNewOrder() {
-        DexFile dexFile = getInputDexFile("FieldGapOrder", new baksmaliOptions());
+        DexFile dexFile = getInputDexFile("FieldGapOrder", new BaksmaliOptions());
         Assert.assertEquals(3, dexFile.getClasses().size());
 
         ClassPath classPath = new ClassPath(Lists.newArrayList(new DexClassProvider(dexFile)), false, 67);
diff --git a/baksmali/src/test/java/org/jf/baksmali/ImplicitReferenceTest.java b/baksmali/src/test/java/org/jf/baksmali/ImplicitReferenceTest.java
index 1f2ae5b..962a6be 100644
--- a/baksmali/src/test/java/org/jf/baksmali/ImplicitReferenceTest.java
+++ b/baksmali/src/test/java/org/jf/baksmali/ImplicitReferenceTest.java
@@ -62,8 +62,8 @@
                 "return-void\n" +
                 ".end method\n";
 
-        baksmaliOptions options = new baksmaliOptions();
-        options.useImplicitReferences = true;
+        BaksmaliOptions options = new BaksmaliOptions();
+        options.implicitReferences = true;
 
         BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options);
     }
@@ -93,8 +93,8 @@
                 "    return-void\n" +
                 ".end method\n";
 
-        baksmaliOptions options = new baksmaliOptions();
-        options.useImplicitReferences = false;
+        BaksmaliOptions options = new BaksmaliOptions();
+        options.implicitReferences = false;
 
         BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options);
     }
@@ -118,8 +118,8 @@
                 ".field public static field3:Ljava/lang/reflect/Method; = I()V\n" +
                 ".field public static field4:Ljava/lang/Class; = I\n";
 
-        baksmaliOptions options = new baksmaliOptions();
-        options.useImplicitReferences = true;
+        BaksmaliOptions options = new BaksmaliOptions();
+        options.implicitReferences = true;
 
         BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options);
     }
@@ -143,8 +143,8 @@
                 ".field public static field3:Ljava/lang/reflect/Method; = LHelloWorld;->I()V\n" +
                 ".field public static field4:Ljava/lang/Class; = I\n";
 
-        baksmaliOptions options = new baksmaliOptions();
-        options.useImplicitReferences = false;
+        BaksmaliOptions options = new BaksmaliOptions();
+        options.implicitReferences = false;
 
         BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options);
     }
@@ -174,8 +174,8 @@
                 "    return-void\n" +
                 ".end method\n";
 
-        baksmaliOptions options = new baksmaliOptions();
-        options.useImplicitReferences = true;
+        BaksmaliOptions options = new BaksmaliOptions();
+        options.implicitReferences = true;
 
         BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options);
     }
@@ -205,8 +205,8 @@
                 "    return-void\n" +
                 ".end method\n";
 
-        baksmaliOptions options = new baksmaliOptions();
-        options.useImplicitReferences = false;
+        BaksmaliOptions options = new BaksmaliOptions();
+        options.implicitReferences = false;
 
         BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options);
     }
@@ -228,8 +228,8 @@
                 ".field public static field2:Ljava/lang/reflect/Field; = V:I\n" +
                 ".field public static field3:Ljava/lang/reflect/Field; = I:I\n";
 
-        baksmaliOptions options = new baksmaliOptions();
-        options.useImplicitReferences = true;
+        BaksmaliOptions options = new BaksmaliOptions();
+        options.implicitReferences = true;
 
         BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options);
     }
@@ -251,8 +251,8 @@
                 ".field public static field2:Ljava/lang/reflect/Field; = LHelloWorld;->V:I\n" +
                 ".field public static field3:Ljava/lang/reflect/Field; = LHelloWorld;->I:I\n";
 
-        baksmaliOptions options = new baksmaliOptions();
-        options.useImplicitReferences = false;
+        BaksmaliOptions options = new BaksmaliOptions();
+        options.implicitReferences = false;
 
         BaksmaliTestUtils.assertSmaliCompiledEquals(source, expected, options);
     }
diff --git a/baksmali/src/test/java/org/jf/baksmali/InterfaceOrderTest.java b/baksmali/src/test/java/org/jf/baksmali/InterfaceOrderTest.java
index d85d791..f1ade1e 100644
--- a/baksmali/src/test/java/org/jf/baksmali/InterfaceOrderTest.java
+++ b/baksmali/src/test/java/org/jf/baksmali/InterfaceOrderTest.java
@@ -36,6 +36,6 @@
 public class InterfaceOrderTest extends IdenticalRoundtripTest {
     @Test
     public void testInterfaceOrder() {
-        runTest("InterfaceOrder", new baksmaliOptions());
+        runTest("InterfaceOrder", new BaksmaliOptions());
     }
 }
diff --git a/baksmali/src/test/java/org/jf/baksmali/RoundtripTest.java b/baksmali/src/test/java/org/jf/baksmali/RoundtripTest.java
index c9ff2d4..81e98a3 100644
--- a/baksmali/src/test/java/org/jf/baksmali/RoundtripTest.java
+++ b/baksmali/src/test/java/org/jf/baksmali/RoundtripTest.java
@@ -69,10 +69,10 @@
     }
 
     protected void runTest(@Nonnull String testName) {
-        runTest(testName, new baksmaliOptions());
+        runTest(testName, new BaksmaliOptions());
     }
 
-    protected void runTest(@Nonnull String testName, @Nonnull baksmaliOptions options) {
+    protected void runTest(@Nonnull String testName, @Nonnull BaksmaliOptions options) {
         try {
             // Load file from resources as a stream
             String inputFilename = getInputFilename(testName);
diff --git a/baksmali/src/test/resources/InstanceOfTest/InstanceOfTest.smali b/baksmali/src/test/resources/InstanceOfTest/InstanceOfTest.smali
new file mode 100644
index 0000000..8e3337a
--- /dev/null
+++ b/baksmali/src/test/resources/InstanceOfTest/InstanceOfTest.smali
@@ -0,0 +1,118 @@
+.class public LInstanceOfTest;
+.super Ljava/lang/Object;
+
+
+# virtual methods
+.method public testInstanceOfEqz(Ljava/lang/Object;)I
+    .registers 3
+
+    #v0=(Uninit);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+    instance-of v0, p1, Ljava/lang/String;
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+    if-eqz v0, :cond_9
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Unknown);
+
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;);
+    invoke-virtual {p1}, Ljava/lang/String;->length()I
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;);
+
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;);
+    move-result v0
+    #v0=(Integer);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;);
+
+    #v0=(Integer);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;);
+    return v0
+    #v0=(Integer);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;);
+
+    :cond_9
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+    const v0, -0x1
+    #v0=(Byte);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+
+    #v0=(Byte);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+    return v0
+    #v0=(Byte);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+.end method
+
+.method public testInstanceOfNez(Ljava/lang/Object;)I
+    .registers 3
+
+    #v0=(Uninit);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+    instance-of v0, p1, Ljava/lang/String;
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+    if-nez v0, :cond_8
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Unknown);
+
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+    const v0, -0x1
+    #v0=(Byte);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+
+    #v0=(Byte);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+    return v0
+    #v0=(Byte);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+
+    :cond_8
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;);
+    invoke-virtual {p1}, Ljava/lang/String;->length()I
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;);
+
+    #v0=(Boolean);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;);
+    move-result v0
+    #v0=(Integer);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;);
+
+    #v0=(Integer);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;);
+    return v0
+    #v0=(Integer);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/String;);
+.end method
+
+.method public testRegisterAlias(Ljava/lang/Object;)I
+    .registers 4
+
+    #v0=(Uninit);v1=(Uninit);p0=(Reference,LInstanceOfTest;);p1=(Reference,Ljava/lang/Object;);
+    move-object p0, p1
+    #v0=(Uninit);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;);
+
+    #v0=(Uninit);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;);
+    instance-of v0, p0, Ljava/lang/String;
+    #v0=(Boolean);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;);
+
+    #v0=(Boolean);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;);
+    if-eqz v0, :cond_f
+    #v0=(Boolean);v1=(Uninit);p0=(Unknown);p1=(Unknown);
+
+    :cond_5
+    #v0=(Integer):merge{0x3:(Boolean),0xc:(Integer)}
+    #v1=(Conflicted):merge{0x3:(Uninit),0xc:(Null)}
+    #p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;);
+    invoke-virtual {p1}, Ljava/lang/String;->length()I
+    #v0=(Integer);v1=(Conflicted);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;);
+
+    #v0=(Integer);v1=(Conflicted);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;);
+    move-result v0
+    #v0=(Integer);v1=(Conflicted);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;);
+
+    #v0=(Integer);v1=(Conflicted);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;);
+    const v1, 0x0
+    #v0=(Integer);v1=(Null);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;);
+
+    #v0=(Integer);v1=(Null);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;);
+    if-le v0, v1, :cond_5
+    #v0=(Integer);v1=(Null);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;);
+
+    #v0=(Integer);v1=(Null);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;);
+    return v0
+    #v0=(Integer);v1=(Null);p0=(Reference,Ljava/lang/String;);p1=(Reference,Ljava/lang/String;);
+
+    :cond_f
+    #v0=(Boolean);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;);
+    const v0, -0x1
+    #v0=(Byte);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;);
+
+    #v0=(Byte);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;);
+    return v0
+    #v0=(Byte);v1=(Uninit);p0=(Reference,Ljava/lang/Object;);p1=(Reference,Ljava/lang/Object;);
+.end method
diff --git a/baksmali/src/test/resources/InstanceOfTest/classes.dex b/baksmali/src/test/resources/InstanceOfTest/classes.dex
new file mode 100644
index 0000000..571bdb8
--- /dev/null
+++ b/baksmali/src/test/resources/InstanceOfTest/classes.dex
Binary files differ
diff --git a/baksmali/src/test/resources/UninitRefIdentityTest/UninitRefIdentityTest.smali b/baksmali/src/test/resources/UninitRefIdentityTest/UninitRefIdentityTest.smali
index f9c4363..1970d3b 100644
--- a/baksmali/src/test/resources/UninitRefIdentityTest/UninitRefIdentityTest.smali
+++ b/baksmali/src/test/resources/UninitRefIdentityTest/UninitRefIdentityTest.smali
@@ -26,11 +26,37 @@
     #v0=(Conflicted):merge{0x5:(UninitRef,Ljava/lang/String;),0x7:(UninitRef,Ljava/lang/String;)}
     #v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;);
     invoke-direct {v0}, Ljava/lang/String;-><init>()V
-    #v0=(Unknown);v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;);
+    #v0=(Conflicted);v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;);
 
-    #v0=(Unknown);v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;);
+    #v0=(Conflicted);v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;);
     return-void
-    #v0=(Unknown);v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;);
+    #v0=(Conflicted);v1=(Uninit);v2=(Uninit);p0=(Reference,LUninitRefIdentityTest;);
+.end method
+
+.method public constructor <init>(I)V
+    .registers 2
+
+    #p0=(UninitThis,LUninitRefIdentityTest;);p1=(Integer);
+    move-object p1, p0
+    #p0=(UninitThis,LUninitRefIdentityTest;);p1=(UninitThis,LUninitRefIdentityTest;);
+
+    #p0=(UninitThis,LUninitRefIdentityTest;);p1=(UninitThis,LUninitRefIdentityTest;);
+    invoke-direct {p1}, Ljava/lang/Object;-><init>()V
+    #p0=(Reference,LUninitRefIdentityTest;);p1=(Reference,LUninitRefIdentityTest;);
+
+    :cond_4
+    #p0=(Reference,LUninitRefIdentityTest;);
+    #p1=(Reference,LUninitRefIdentityTest;):merge{0x1:(Reference,LUninitRefIdentityTest;),0x7:(Null)}
+    const p1, 0x0
+    #p0=(Reference,LUninitRefIdentityTest;);p1=(Null);
+
+    #p0=(Reference,LUninitRefIdentityTest;);p1=(Null);
+    if-nez p1, :cond_4
+    #p0=(Reference,LUninitRefIdentityTest;);p1=(Null);
+
+    #p0=(Reference,LUninitRefIdentityTest;);p1=(Null);
+    return-void
+    #p0=(Reference,LUninitRefIdentityTest;);p1=(Null);
 .end method
 
 .method public constructor <init>(Ljava/lang/String;)V
@@ -48,3 +74,37 @@
     return-void
     #p0=(Reference,LUninitRefIdentityTest;);p1=(Reference,LUninitRefIdentityTest;);
 .end method
+
+
+# virtual methods
+.method public overlappingInits()V
+    .registers 3
+
+    #v0=(Uninit);v1=(Uninit);p0=(Reference,LUninitRefIdentityTest;);
+    new-instance v0, Ljava/lang/String;
+    #v0=(UninitRef,Ljava/lang/String;);v1=(Uninit);p0=(Reference,LUninitRefIdentityTest;);
+
+    #v0=(UninitRef,Ljava/lang/String;);v1=(Uninit);p0=(Reference,LUninitRefIdentityTest;);
+    new-instance v1, Ljava/lang/String;
+    #v0=(UninitRef,Ljava/lang/String;);v1=(UninitRef,Ljava/lang/String;);p0=(Reference,LUninitRefIdentityTest;);
+
+    #v0=(UninitRef,Ljava/lang/String;);v1=(UninitRef,Ljava/lang/String;);p0=(Reference,LUninitRefIdentityTest;);
+    new-instance p0, Ljava/lang/String;
+    #v0=(UninitRef,Ljava/lang/String;);v1=(UninitRef,Ljava/lang/String;);p0=(UninitRef,Ljava/lang/String;);
+
+    #v0=(UninitRef,Ljava/lang/String;);v1=(UninitRef,Ljava/lang/String;);p0=(UninitRef,Ljava/lang/String;);
+    invoke-direct {p0}, Ljava/lang/String;-><init>()V
+    #v0=(UninitRef,Ljava/lang/String;);v1=(UninitRef,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;);
+
+    #v0=(UninitRef,Ljava/lang/String;);v1=(UninitRef,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;);
+    invoke-direct {v1}, Ljava/lang/String;-><init>()V
+    #v0=(UninitRef,Ljava/lang/String;);v1=(Reference,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;);
+
+    #v0=(UninitRef,Ljava/lang/String;);v1=(Reference,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;);
+    invoke-direct {v0}, Ljava/lang/String;-><init>()V
+    #v0=(Reference,Ljava/lang/String;);v1=(Reference,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;);
+
+    #v0=(Reference,Ljava/lang/String;);v1=(Reference,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;);
+    return-void
+    #v0=(Reference,Ljava/lang/String;);v1=(Reference,Ljava/lang/String;);p0=(Reference,Ljava/lang/String;);
+.end method
diff --git a/baksmali/src/test/resources/UninitRefIdentityTest/classes.dex b/baksmali/src/test/resources/UninitRefIdentityTest/classes.dex
index ea146cf..0f0caab 100644
--- a/baksmali/src/test/resources/UninitRefIdentityTest/classes.dex
+++ b/baksmali/src/test/resources/UninitRefIdentityTest/classes.dex
Binary files differ
diff --git a/build.gradle b/build.gradle
index 56aaa3f..9e8205d 100644
--- a/build.gradle
+++ b/build.gradle
@@ -31,9 +31,14 @@
 
 apply plugin: 'idea'
 
-version = '2.1.3'
+version = '2.2.0'
+def jcommanderVersion = ''
 
 if (!('release' in gradle.startParameter.taskNames)) {
+    // we compile against 1.48 normally, to match what's in AOSP, but switch to a newer version
+    // for release, because it has some fixes required when running on Android
+    jcommanderVersion = 'com.beust:jcommander:1.48'
+
     def versionSuffix
     try {
         def git = org.eclipse.jgit.api.Git.open(file('.'))
@@ -51,6 +56,8 @@
 
     version += "-${versionSuffix}"
 } else {
+    jcommanderVersion = 'com.beust:jcommander:1.64'
+
     if (System.env.JDK6_HOME == null && !JavaVersion.current().isJava6()) {
         throw new InvalidUserDataException("bzzzzzzzt. Release builds must be performed with java 6. " +
                 "Either run gradle with java 6, or define the JDK6_HOME environment variable.")
@@ -101,15 +108,16 @@
                 guava: 'com.google.guava:guava:18.0',
                 findbugs: 'com.google.code.findbugs:jsr305:1.3.9',
                 junit: 'junit:junit:4.6',
+                mockito: 'org.mockito:mockito-core:1.10.19',
                 antlr_runtime: 'org.antlr:antlr-runtime:3.5.2',
                 antlr: 'org.antlr:antlr:3.5.2',
                 stringtemplate: 'org.antlr:stringtemplate:3.2.1',
-                commons_cli: 'commons-cli:commons-cli:1.2',
                 jflex_plugin: 'org.xbib.gradle.plugin:gradle-plugin-jflex:1.1.0',
                 proguard_gradle: 'net.sf.proguard:proguard-gradle:5.2.1',
                 dx: 'com.google.android.tools:dx:1.7',
-                gson: 'com.google.code.gson:gson:2.3.1'
-         ]
+                gson: 'com.google.code.gson:gson:2.3.1',
+                jcommander: jcommanderVersion
+        ]
     }
 
     repositories {
@@ -196,5 +204,6 @@
 }
 
 task wrapper(type: Wrapper) {
-    gradleVersion = '2.14'
+    gradleVersion = '3.1'
+    distributionType = Wrapper.DistributionType.ALL
 }
\ No newline at end of file
diff --git a/dexlib2/OatVersions.txt b/dexlib2/OatVersions.txt
index 8aa9ea9..329c4f0 100644
--- a/dexlib2/OatVersions.txt
+++ b/dexlib2/OatVersions.txt
@@ -8,6 +8,7 @@
  - return-void-barrier -> return-void-no-barrier
 1412dfa4adcd511902e510fa0c948b168ab5840c - 61 (re-commit of f3251d12)
 9d6bf69ad3012a9d843268fdd5325b6719b6d5f2 - 62
+- classpath list was added
 0de1133ba600f299b3d67938f650720d9f859eb2 - 63
 07785bb98dc8bbe192970e0f4c2cafd338a8dc68 - 64
 fa2c054b28d4b540c1b3651401a7a091282a015f - 65
@@ -21,4 +22,69 @@
 6e2d5747d00697a25251d25dd33b953e54709507 - 68 (revert of 54b62480)
 0747466fca310eedea5fc49e37d54f240a0b3c0f - 69 (re-commit of 54b62480)
 501fd635a557645ab05f893c56e1f358e21bab82 - 70
-99170c636dfae4908b102347cfe9f92bad1881cc - 71
\ No newline at end of file
+99170c636dfae4908b102347cfe9f92bad1881cc - 71
+3cfa4d05afa76e19ca99ec964b535a15c73683f0 - 72
+- default methods
+d9786b0e5be23ea0258405165098b4216579209c - 73
+- fast class lookup table
+a4f1220c1518074db18ca1044e9201492975750b - 74
+625a64aad13905d8a2454bf3cc0e874487b110d5 - 75
+- bootclasspath list was added
+- class offsets moved out to a separate table
+919f5536182890d2e03f59b961acf8f7c836ff61 - 74 (revert of 625a64aa)
+9bdf108885a27ba05fae8501725649574d7c491b - 75 (re-commit of 625a64aa)
+a62d2f04a6ecf804f8a78e722a6ca8ccb2dfa931 - 76
+845e5064580bd37ad5014f7aa0d078be7265464d - 75 (revert of a62d2f04)
+29d38e77c553c6cf71fc4dafe2d22b4e3f814872 - 76 (re-commit of 845e5064)
+d1537b569b6cd18297c5e02d13cdd588c4366c51 - 77
+61b28a17d9b6e8e998103646e98e4a9772e11927 - 78
+9d07e3d128ccfa0ef7670feadd424a825e447d1d - 79
+952e1e3710158982941fc70326e9fddc3021235d - 80
+013e3f33495dcc31dba19c9de128d23ed441d7d3 - 81
+87f3fcbd0db352157fc59148e94647ef21b73bce - 82
+02b75806a80f8b75c3d6ba2ff97c995117630f36 - 83
+4359e61927866c254bc2d701e3ea4c48de10b79c - 84
+d549c28cfbddba945cb88857bcca3dce1414fb29 - 85
+952dbb19cd094b8bfb01dbb33e0878db429e499a - 86
+239d6eaff0cbb5c4c0139f7053a012758799f186 - 87 - introduction of vdex files
+77d9dd75d5d4a22ad1235f9a08d2cfbf2f0ae6fa - 89
+af1e2990cd1406a0fb7cba1d2e208208e950e413 - 90
+9fd8c60cdff7b28a89bb97fd90ae9d0f37cf8f8b - 91
+6beced4c017826f7c449f12fac7fa42403657f2b - 92
+58c3f6a0d15a4340c0a11ab7fbc8c4b990c64b77 - 93
+5923b5238091d9cd65f988fc059deb4fbb2e7f08 - 94
+2b615ba29c4dfcf54aaf44955f2eac60f5080b2e - 95
+f7aaacd97881c6924b8212c7f8fe4a4c8721ef53 - 94 (revert of 2b615ba)
+0d3998b5ff619364acf47bec0b541e7a49bd6fe7 - 95 (re-commit of 2b615ba)
+ac141397dc29189ad2b2df41f8d4312246beec60 - 96
+1998cd02603197f2acdc0734397a6d48b2f59b80 - 97
+e71b35446985835363a4508646cf7b1121bd95a3 - 98
+39cee66a8ddf0254626c9591662cf87e4a1cedc4 - 99
+cc99df230feb46ba717252f002d0cc2da6828421 - 100
+fee255039e30c1c3dfc70c426c3d176221c3cdf9 - 99 (revert of cc99df23)
+e761bccf9f0d884cc4d4ec104568cef968296492 - 100 (re-commit of cc99df23)
+8d91ac31ccb92557e434d89ffade3372466e1af5 - 101
+fd3161acfbe82c54ef49958f0ccc62511f224f91 - 102
+a2f526f889be06f96ea59624c9dfb1223b3839f3 - 103
+b048cb74b742b03eb6dd5f1d6dd49e559f730b36 - 104
+12f1b99775bbf7dd82d0a897587ab6ed0e75ee22 - 105
+ec7862283dd49f5a58d0ac45960ce27c2f7671b8 - 106
+45aa598cd1773f5eb1705dec13bea059238e054d - 107
+d16363a93053de0f32252c7897d839a46aff14ae - 108
+1a20b6801f2432a42b906f0de01e7e9586526aec - 109
+575d3e60c68b5cf481b615dde4a16283507b19ed - 110
+85c0f2ac03417f5125bc2ff1dab8109859c67d5c - 111
+5812e20ff7cbc8efa0b8d7486ada2f58840a6ad5 - 111
+b7ea3799c15b0090bb690e18ac1b5b0fddbdeee8 - 112
+    - version bump for missing bump in commits
+    - 3228908337fdfe851223f8ae374538de25cb5ad1
+    - 5812e20ff7cbc8efa0b8d7486ada2f58840a6ad5
+d776ff08e07494327716f0d2ea1a774b2ebfbca9 - 113
+bfb80d25eaeb7a604d5dd25a370e3869e96a33ab - 114
+1aea3510b8dd0c512cec61c91c5ef1f1e5d53d64 - 115
+6374c58f2ea403b3a05fb27376110fe4d0fc8e3f - 114 (revert of 1aea3510)
+0b66d6174bf1f6023f9d36dda8538490b79c2e9f - 113 (revert of bfb80d25)
+8d6768d47b66a688d35399d524ad5a5450e9d9d4 - 114 (i don't even)
+f44d36c8423f81cbb5e9f55d8813e26ffa1a7f3b - 115 (115 again. heck if I know what's going on)
+cbcedbf9382bc773713cd3552ed96f417bf1daeb - 116
+051071718085ce807a2e7c55278a8d723e238e86 - 116
diff --git a/dexlib2/VdexVersions.txt b/dexlib2/VdexVersions.txt
new file mode 100644
index 0000000..9cb1a73
--- /dev/null
+++ b/dexlib2/VdexVersions.txt
@@ -0,0 +1,8 @@
+7b49e6cade09bc65b3b5f22d45fc9d0a7184e4f2 - 0 - introduction of vdex files
+5d5a36bddbc008cd52a3207aa2b31177c47f9a49 - 0 - verifier deps
+4acefd33064d37b41ca55c3c9355345a20e5f9c2 - 0 - quickening info
+f54e5df37cb42d9a83fc54b375da5ef335d604a9 - 1 - dex file count + dex location checksum
+7498105ec7497bae2ba9f1a697da9efa0c979654 - 2 - verify profile
+3eba863e41d531340392d9ec64e17963ac898d81 - 3
+97fa9928c07d3e0ee631235e9619fb0f8949ed7a - 4
+6e54f78c7c1e01c1a91a458c6e51cca1c7d13ad4 - 5
diff --git a/dexlib2/build.gradle b/dexlib2/build.gradle
index 8fbe5ff..422d2c3 100644
--- a/dexlib2/build.gradle
+++ b/dexlib2/build.gradle
@@ -51,6 +51,7 @@
     compile depends.guava
 
     testCompile depends.junit
+    testCompile depends.mockito
 
     accessorTestGenerator project('accessorTestGenerator')
 
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java b/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java
index 60488ba..1caaf9f 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/DexFileFactory.java
@@ -31,14 +31,22 @@
 
 package org.jf.dexlib2;
 
-import com.google.common.base.MoreObjects;
+import com.google.common.base.Joiner;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
 import com.google.common.io.ByteStreams;
+import com.google.common.io.Files;
 import org.jf.dexlib2.dexbacked.DexBackedDexFile;
+import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile;
 import org.jf.dexlib2.dexbacked.DexBackedOdexFile;
 import org.jf.dexlib2.dexbacked.OatFile;
 import org.jf.dexlib2.dexbacked.OatFile.NotAnOatFileException;
 import org.jf.dexlib2.dexbacked.OatFile.OatDexFile;
+import org.jf.dexlib2.dexbacked.OatFile.VdexProvider;
+import org.jf.dexlib2.dexbacked.ZipDexContainer;
+import org.jf.dexlib2.dexbacked.ZipDexContainer.NotAZipFileException;
 import org.jf.dexlib2.iface.DexFile;
+import org.jf.dexlib2.iface.MultiDexContainer;
 import org.jf.dexlib2.writer.pool.DexPool;
 import org.jf.util.ExceptionWithContext;
 
@@ -46,80 +54,44 @@
 import javax.annotation.Nullable;
 import java.io.*;
 import java.util.List;
-import java.util.zip.ZipEntry;
-import java.util.zip.ZipFile;
 
 public final class DexFileFactory {
+
     @Nonnull
-    public static DexBackedDexFile loadDexFile(@Nonnull String path, int api) throws IOException {
-        return loadDexFile(path, api, false);
+    public static DexBackedDexFile loadDexFile(@Nonnull String path, @Nonnull Opcodes opcodes) throws IOException {
+        return loadDexFile(new File(path), opcodes);
     }
 
+    /**
+     * Loads a dex/apk/odex/oat file.
+     *
+     * For oat files with multiple dex files, the first will be opened. For zip/apk files, the "classes.dex" entry
+     * will be opened.
+     *
+     * @param file The file to open
+     * @param opcodes The set of opcodes to use
+     * @return A DexBackedDexFile for the given file
+     *
+     * @throws UnsupportedOatVersionException If file refers to an unsupported oat file
+     * @throws DexFileNotFoundException If file does not exist, if file is a zip file but does not have a "classes.dex"
+     * entry, or if file is an oat file that has no dex entries.
+     * @throws UnsupportedFileTypeException If file is not a valid dex/zip/odex/oat file, or if the "classes.dex" entry
+     * in a zip file is not a valid dex file
+     */
     @Nonnull
-    public static DexBackedDexFile loadDexFile(@Nonnull String path, int api, boolean experimental)
-            throws IOException {
-        return loadDexFile(new File(path), "classes.dex", Opcodes.forApi(api, experimental));
-    }
-
-    @Nonnull
-    public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, int api) throws IOException {
-        return loadDexFile(dexFile, api, false);
-    }
-
-    @Nonnull
-    public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, int api, boolean experimental)
-            throws IOException {
-        return loadDexFile(dexFile, null, Opcodes.forApi(api, experimental));
-    }
-
-    @Nonnull
-    public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, @Nullable String dexEntry, int api,
-            boolean experimental) throws IOException {
-        return loadDexFile(dexFile, dexEntry, Opcodes.forApi(api, experimental));
-    }
-
-    @Nonnull
-    public static DexBackedDexFile loadDexFile(@Nonnull File dexFile, @Nullable String dexEntry,
-                                               @Nonnull Opcodes opcodes) throws IOException {
-        ZipFile zipFile = null;
-        boolean isZipFile = false;
-        try {
-            zipFile = new ZipFile(dexFile);
-            // if we get here, it's safe to assume we have a zip file
-            isZipFile = true;
-
-            String zipEntryName = MoreObjects.firstNonNull(dexEntry, "classes.dex");
-            ZipEntry zipEntry = zipFile.getEntry(zipEntryName);
-            if (zipEntry == null) {
-                throw new DexFileNotFound("zip file %s does not contain a %s file", dexFile.getName(), zipEntryName);
-            }
-            long fileLength = zipEntry.getSize();
-            if (fileLength < 40) {
-                throw new ExceptionWithContext("The %s file in %s is too small to be a valid dex file",
-                        zipEntryName, dexFile.getName());
-            } else if (fileLength > Integer.MAX_VALUE) {
-                throw new ExceptionWithContext("The %s file in %s is too large to read in",
-                        zipEntryName, dexFile.getName());
-            }
-            byte[] dexBytes = new byte[(int)fileLength];
-            ByteStreams.readFully(zipFile.getInputStream(zipEntry), dexBytes);
-            return new DexBackedDexFile(opcodes, dexBytes);
-        } catch (IOException ex) {
-            // don't continue on if we know it's a zip file
-            if (isZipFile) {
-                throw ex;
-            }
-        } finally {
-            if (zipFile != null) {
-                try {
-                    zipFile.close();
-                } catch (IOException ex) {
-                    // just eat it
-                }
-            }
+    public static DexBackedDexFile loadDexFile(@Nonnull File file, @Nonnull Opcodes opcodes) throws IOException {
+        if (!file.exists()) {
+            throw new DexFileNotFoundException("%s does not exist", file.getName());
         }
 
-        InputStream inputStream = new BufferedInputStream(new FileInputStream(dexFile));
+        try {
+            ZipDexContainer container = new ZipDexContainer(file, opcodes);
+            return new DexEntryFinder(file.getPath(), container).findEntry("classes.dex", true);
+        } catch (NotAZipFileException ex) {
+            // eat it and continue
+        }
+
+        InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
         try {
             try {
                 return DexBackedDexFile.fromInputStream(opcodes, inputStream);
@@ -127,17 +99,18 @@
                 // just eat it
             }
 
-            // Note: DexBackedDexFile.fromInputStream will reset inputStream back to the same position, if it fails
-
             try {
                 return DexBackedOdexFile.fromInputStream(opcodes, inputStream);
             } catch (DexBackedOdexFile.NotAnOdexFile ex) {
                 // just eat it
             }
 
+            // Note: DexBackedDexFile.fromInputStream and DexBackedOdexFile.fromInputStream will reset inputStream
+            // back to the same position, if they fails
+
             OatFile oatFile = null;
             try {
-                oatFile = OatFile.fromInputStream(inputStream);
+                oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file));
             } catch (NotAnOatFileException ex) {
                 // just eat it
             }
@@ -150,71 +123,181 @@
                 List<OatDexFile> oatDexFiles = oatFile.getDexFiles();
 
                 if (oatDexFiles.size() == 0) {
-                    throw new DexFileNotFound("Oat file %s contains no dex files", dexFile.getName());
+                    throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName());
                 }
 
-                if (dexEntry == null) {
-                    if (oatDexFiles.size() > 1) {
-                        throw new MultipleDexFilesException(oatFile);
-                    }
-                    return oatDexFiles.get(0);
-                } else {
-                    // first check for an exact match
-                    for (OatDexFile oatDexFile : oatFile.getDexFiles()) {
-                        if (oatDexFile.filename.equals(dexEntry)) {
-                            return oatDexFile;
-                        }
-                    }
-
-                    if (!dexEntry.contains("/")) {
-                        for (OatDexFile oatDexFile : oatFile.getDexFiles()) {
-                            File oatEntryFile = new File(oatDexFile.filename);
-                            if (oatEntryFile.getName().equals(dexEntry)) {
-                                return oatDexFile;
-                            }
-                        }
-                    }
-
-                    throw new DexFileNotFound("oat file %s does not contain a dex file named %s",
-                            dexFile.getName(), dexEntry);
-                }
+                return oatDexFiles.get(0);
             }
         } finally {
             inputStream.close();
         }
 
-        throw new ExceptionWithContext("%s is not an apk, dex, odex or oat file.", dexFile.getPath());
+        throw new UnsupportedFileTypeException("%s is not an apk, dex, odex or oat file.", file.getPath());
     }
 
+    /**
+     * Loads a dex entry from a container format (zip/oat)
+     *
+     * This has two modes of operation, depending on the exactMatch parameter. When exactMatch is true, it will only
+     * load an entry whose name exactly matches that provided by the dexEntry parameter.
+     *
+     * When exactMatch is false, then it will search for any entry that dexEntry is a path suffix of. "path suffix"
+     * meaning all the path components in dexEntry must fully match the corresponding path components in the entry name,
+     * but some path components at the beginning of entry name can be missing.
+     *
+     * For example, if an oat file contains a "/system/framework/framework.jar:classes2.dex" entry, then the following
+     * will match (not an exhaustive list):
+     *
+     * "/system/framework/framework.jar:classes2.dex"
+     * "system/framework/framework.jar:classes2.dex"
+     * "framework/framework.jar:classes2.dex"
+     * "framework.jar:classes2.dex"
+     * "classes2.dex"
+     *
+     * Note that partial path components specifically don't match. So something like "work/framework.jar:classes2.dex"
+     * would not match.
+     *
+     * If dexEntry contains an initial slash, it will be ignored for purposes of this suffix match -- but not when
+     * performing an exact match.
+     *
+     * If multiple entries match the given dexEntry, a MultipleMatchingDexEntriesException will be thrown
+     *
+     * @param file The container file. This must be either a zip (apk) file or an oat file.
+     * @param dexEntry The name of the entry to load. This can either be the exact entry name, if exactMatch is true,
+     *                 or it can be a path suffix.
+     * @param exactMatch If true, dexE
+     * @param opcodes The set of opcodes to use
+     * @return A DexBackedDexFile for the given entry
+     *
+     * @throws UnsupportedOatVersionException If file refers to an unsupported oat file
+     * @throws DexFileNotFoundException If the file does not exist, or if no matching entry could be found
+     * @throws UnsupportedFileTypeException If file is not a valid zip/oat file, or if the matching entry is not a
+     * valid dex file
+     * @throws MultipleMatchingDexEntriesException If multiple entries match the given dexEntry
+     */
+    public static DexBackedDexFile loadDexEntry(@Nonnull File file, @Nonnull String dexEntry,
+                                                boolean exactMatch, @Nonnull Opcodes opcodes) throws IOException {
+        if (!file.exists()) {
+            throw new DexFileNotFoundException("Container file %s does not exist", file.getName());
+        }
+
+        try {
+            ZipDexContainer container = new ZipDexContainer(file, opcodes);
+            return new DexEntryFinder(file.getPath(), container).findEntry(dexEntry, exactMatch);
+        } catch (NotAZipFileException ex) {
+            // eat it and continue
+        }
+
+        InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
+        try {
+            OatFile oatFile = null;
+            try {
+                oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file));
+            } catch (NotAnOatFileException ex) {
+                // just eat it
+            }
+
+            if (oatFile != null) {
+                if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) {
+                    throw new UnsupportedOatVersionException(oatFile);
+                }
+
+                List<OatDexFile> oatDexFiles = oatFile.getDexFiles();
+
+                if (oatDexFiles.size() == 0) {
+                    throw new DexFileNotFoundException("Oat file %s contains no dex files", file.getName());
+                }
+
+                return new DexEntryFinder(file.getPath(), oatFile).findEntry(dexEntry, exactMatch);
+            }
+        } finally {
+            inputStream.close();
+        }
+
+        throw new UnsupportedFileTypeException("%s is not an apk or oat file.", file.getPath());
+    }
+
+    /**
+     * Loads a file containing 1 or more dex files
+     *
+     * If the given file is a dex or odex file, it will return a MultiDexContainer containing that single entry.
+     * Otherwise, for an oat or zip file, it will return an OatFile or ZipDexContainer respectively.
+     *
+     * @param file The file to open
+     * @param opcodes The set of opcodes to use
+     * @return A MultiDexContainer
+     * @throws DexFileNotFoundException If the given file does not exist
+     * @throws UnsupportedFileTypeException If the given file is not a valid dex/zip/odex/oat file
+     */
+    public static MultiDexContainer<? extends DexBackedDexFile> loadDexContainer(
+            @Nonnull File file, @Nonnull final Opcodes opcodes) throws IOException {
+        if (!file.exists()) {
+            throw new DexFileNotFoundException("%s does not exist", file.getName());
+        }
+
+        ZipDexContainer zipDexContainer = new ZipDexContainer(file, opcodes);
+        if (zipDexContainer.isZipFile()) {
+            return zipDexContainer;
+        }
+
+        InputStream inputStream = new BufferedInputStream(new FileInputStream(file));
+        try {
+            try {
+                DexBackedDexFile dexFile = DexBackedDexFile.fromInputStream(opcodes, inputStream);
+                return new SingletonMultiDexContainer(file.getPath(), dexFile);
+            } catch (DexBackedDexFile.NotADexFile ex) {
+                // just eat it
+            }
+
+            try {
+                DexBackedOdexFile odexFile = DexBackedOdexFile.fromInputStream(opcodes, inputStream);
+                return new SingletonMultiDexContainer(file.getPath(), odexFile);
+            } catch (DexBackedOdexFile.NotAnOdexFile ex) {
+                // just eat it
+            }
+
+            // Note: DexBackedDexFile.fromInputStream and DexBackedOdexFile.fromInputStream will reset inputStream
+            // back to the same position, if they fails
+
+            OatFile oatFile = null;
+            try {
+                oatFile = OatFile.fromInputStream(inputStream, new FilenameVdexProvider(file));
+            } catch (NotAnOatFileException ex) {
+                // just eat it
+            }
+
+            if (oatFile != null) {
+                // TODO: we should support loading earlier oat files, just not deodexing them
+                if (oatFile.isSupportedVersion() == OatFile.UNSUPPORTED) {
+                    throw new UnsupportedOatVersionException(oatFile);
+                }
+                return oatFile;
+            }
+        } finally {
+            inputStream.close();
+        }
+
+        throw new UnsupportedFileTypeException("%s is not an apk, dex, odex or oat file.", file.getPath());
+    }
+
+    /**
+     * Writes a DexFile out to disk
+     *
+     * @param path The path to write the dex file to
+     * @param dexFile a DexFile to write
+     */
     public static void writeDexFile(@Nonnull String path, @Nonnull DexFile dexFile) throws IOException {
         DexPool.writeTo(path, dexFile);
     }
 
     private DexFileFactory() {}
 
-    public static class DexFileNotFound extends ExceptionWithContext {
-        public DexFileNotFound(@Nullable Throwable cause) {
-            super(cause);
-        }
-
-        public DexFileNotFound(@Nullable Throwable cause, @Nullable String message, Object... formatArgs) {
-            super(cause, message, formatArgs);
-        }
-
-        public DexFileNotFound(@Nullable String message, Object... formatArgs) {
+    public static class DexFileNotFoundException extends ExceptionWithContext {
+        public DexFileNotFoundException(@Nullable String message, Object... formatArgs) {
             super(message, formatArgs);
         }
     }
 
-    public static class MultipleDexFilesException extends ExceptionWithContext {
-        @Nonnull public final OatFile oatFile;
-
-        public MultipleDexFilesException(@Nonnull OatFile oatFile) {
-            super("Oat file has multiple dex files.");
-            this.oatFile = oatFile;
-        }
-    }
-
     public static class UnsupportedOatVersionException extends ExceptionWithContext {
         @Nonnull public final OatFile oatFile;
 
@@ -223,4 +306,190 @@
             this.oatFile = oatFile;
         }
     }
+
+    public static class MultipleMatchingDexEntriesException extends ExceptionWithContext {
+        public MultipleMatchingDexEntriesException(@Nonnull String message, Object... formatArgs) {
+            super(String.format(message, formatArgs));
+        }
+    }
+
+    public static class UnsupportedFileTypeException extends ExceptionWithContext {
+        public UnsupportedFileTypeException(@Nonnull String message, Object... formatArgs) {
+            super(String.format(message, formatArgs));
+        }
+    }
+
+    /**
+     * Matches two entries fully, ignoring any initial slash, if any
+     */
+    private static boolean fullEntryMatch(@Nonnull String entry, @Nonnull String targetEntry) {
+        if (entry.equals(targetEntry)) {
+            return true;
+        }
+
+        if (entry.charAt(0) == '/') {
+            entry = entry.substring(1);
+        }
+
+        if (targetEntry.charAt(0) == '/') {
+            targetEntry = targetEntry.substring(1);
+        }
+
+        return entry.equals(targetEntry);
+    }
+
+    /**
+     * Performs a partial match against entry and targetEntry.
+     *
+     * This is considered a partial match if targetEntry is a suffix of entry, and if the suffix starts
+     * on a path "part" (ignoring the initial separator, if any). Both '/' and ':' are considered separators for this.
+     *
+     * So entry="/blah/blah/something.dex" and targetEntry="lah/something.dex" shouldn't match, but
+     * both targetEntry="blah/something.dex" and "/blah/something.dex" should match.
+     */
+    private static boolean partialEntryMatch(String entry, String targetEntry) {
+        if (entry.equals(targetEntry)) {
+            return true;
+        }
+
+        if (!entry.endsWith(targetEntry)) {
+            return false;
+        }
+
+        // Make sure the first matching part is a full entry. We don't want to match "/blah/blah/something.dex" with
+        // "lah/something.dex", but both "/blah/something.dex" and "blah/something.dex" should match
+        char precedingChar = entry.charAt(entry.length() - targetEntry.length() - 1);
+        char firstTargetChar = targetEntry.charAt(0);
+        // This is a device path, so we should always use the linux separator '/', rather than the current platform's
+        // separator
+        return firstTargetChar == ':' || firstTargetChar == '/' || precedingChar == ':' || precedingChar == '/';
+    }
+
+    protected static class DexEntryFinder {
+        private final String filename;
+        private final MultiDexContainer<? extends DexBackedDexFile> dexContainer;
+
+        public DexEntryFinder(@Nonnull String filename,
+                              @Nonnull MultiDexContainer<? extends DexBackedDexFile> dexContainer) {
+            this.filename = filename;
+            this.dexContainer = dexContainer;
+        }
+
+        @Nonnull
+        public DexBackedDexFile findEntry(@Nonnull String targetEntry, boolean exactMatch) throws IOException {
+            if (exactMatch) {
+                try {
+                    DexBackedDexFile dexFile = dexContainer.getEntry(targetEntry);
+                    if (dexFile == null) {
+                        throw new DexFileNotFoundException("Could not find entry %s in %s.", targetEntry, filename);
+                    }
+                    return dexFile;
+                } catch (NotADexFile ex) {
+                    throw new UnsupportedFileTypeException("Entry %s in %s is not a dex file", targetEntry, filename);
+                }
+            }
+
+            // find all full and partial matches
+            List<String> fullMatches = Lists.newArrayList();
+            List<DexBackedDexFile> fullEntries = Lists.newArrayList();
+            List<String> partialMatches = Lists.newArrayList();
+            List<DexBackedDexFile> partialEntries = Lists.newArrayList();
+            for (String entry: dexContainer.getDexEntryNames()) {
+                if (fullEntryMatch(entry, targetEntry)) {
+                    // We want to grab all full matches, regardless of whether they're actually a dex file.
+                    fullMatches.add(entry);
+                    fullEntries.add(dexContainer.getEntry(entry));
+                } else if (partialEntryMatch(entry, targetEntry)) {
+                    partialMatches.add(entry);
+                    partialEntries.add(dexContainer.getEntry(entry));
+                }
+            }
+
+            // full matches always take priority
+            if (fullEntries.size() == 1) {
+                try {
+                    DexBackedDexFile dexFile = fullEntries.get(0);
+                    assert dexFile != null;
+                    return dexFile;
+                } catch (NotADexFile ex) {
+                    throw new UnsupportedFileTypeException("Entry %s in %s is not a dex file",
+                            fullMatches.get(0), filename);
+                }
+            }
+            if (fullEntries.size() > 1) {
+                // This should be quite rare. This would only happen if an oat file has two entries that differ
+                // only by an initial path separator. e.g. "/blah/blah.dex" and "blah/blah.dex"
+                throw new MultipleMatchingDexEntriesException(String.format(
+                        "Multiple entries in %s match %s: %s", filename, targetEntry,
+                        Joiner.on(", ").join(fullMatches)));
+            }
+
+            if (partialEntries.size() == 0) {
+                throw new DexFileNotFoundException("Could not find a dex entry in %s matching %s",
+                        filename, targetEntry);
+            }
+            if (partialEntries.size() > 1) {
+                throw new MultipleMatchingDexEntriesException(String.format(
+                        "Multiple dex entries in %s match %s: %s", filename, targetEntry,
+                        Joiner.on(", ").join(partialMatches)));
+            }
+            return partialEntries.get(0);
+        }
+    }
+
+    private static class SingletonMultiDexContainer implements MultiDexContainer<DexBackedDexFile> {
+        private final String entryName;
+        private final DexBackedDexFile dexFile;
+
+        public SingletonMultiDexContainer(@Nonnull String entryName, @Nonnull DexBackedDexFile dexFile) {
+            this.entryName = entryName;
+            this.dexFile = dexFile;
+        }
+
+        @Nonnull @Override public List<String> getDexEntryNames() throws IOException {
+            return ImmutableList.of(entryName);
+        }
+
+        @Nullable @Override public DexBackedDexFile getEntry(@Nonnull String entryName) throws IOException {
+            if (entryName.equals(this.entryName)) {
+                return dexFile;
+            }
+            return null;
+        }
+
+        @Nonnull @Override public Opcodes getOpcodes() {
+            return dexFile.getOpcodes();
+        }
+    }
+
+    public static class FilenameVdexProvider implements VdexProvider {
+        private final File vdexFile;
+
+        @Nullable
+        private byte[] buf = null;
+        private boolean loadedVdex = false;
+
+        public FilenameVdexProvider(File oatFile) {
+            File oatParent = oatFile.getAbsoluteFile().getParentFile();
+            String baseName = Files.getNameWithoutExtension(oatFile.getAbsolutePath());
+            vdexFile = new File(oatParent, baseName + ".vdex");
+        }
+
+        @Nullable @Override public byte[] getVdex() {
+            if (!loadedVdex) {
+                if (vdexFile.exists()) {
+                    try {
+                        buf = ByteStreams.toByteArray(new FileInputStream(vdexFile));
+                    } catch (FileNotFoundException e) {
+                        buf = null;
+                    } catch (IOException ex) {
+                        throw new RuntimeException(ex);
+                    }
+                }
+                loadedVdex = true;
+            }
+
+            return buf;
+        }
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/Opcode.java b/dexlib2/src/main/java/org/jf/dexlib2/Opcode.java
index 843550f..60dffa2 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/Opcode.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/Opcode.java
@@ -330,8 +330,6 @@
     public static final int JUMBO_OPCODE = 0x200;
     //if the instruction can initialize an uninitialized object reference
     public static final int CAN_INITIALIZE_REFERENCE = 0x400;
-    //if the instruction is experimental (not potentially supported by Android runtime yet)
-    public static final int EXPERIMENTAL = 0x800;
 
     private static final int ALL_APIS = 0xFFFF0000;
 
@@ -471,10 +469,6 @@
         return (flags & CAN_INITIALIZE_REFERENCE) != 0;
     }
 
-    public final boolean isExperimental() {
-        return (flags & EXPERIMENTAL) != 0;
-    }
-
     private static class VersionConstraint {
         @Nonnull public final Range<Integer> apiRange;
         @Nonnull public final Range<Integer> artVersionRange;
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/Opcodes.java b/dexlib2/src/main/java/org/jf/dexlib2/Opcodes.java
index a137dee..5f8106d 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/Opcodes.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/Opcodes.java
@@ -56,41 +56,32 @@
 
     @Nonnull
     public static Opcodes forApi(int api) {
-        return new Opcodes(api, NO_VERSION, false);
-    }
-
-    @Nonnull
-    public static Opcodes forApi(int api, boolean experimental) {
-        return new Opcodes(api, NO_VERSION, experimental);
+        return new Opcodes(api, NO_VERSION);
     }
 
     @Nonnull
     public static Opcodes forArtVersion(int artVersion) {
-        return forArtVersion(artVersion, false);
+        return new Opcodes(NO_VERSION, artVersion);
     }
 
+    /**
+     * @return a default Opcodes instance for when the exact Opcodes to use doesn't matter or isn't known
+     */
     @Nonnull
-    public static Opcodes forArtVersion(int artVersion, boolean experimental) {
-        return new Opcodes(NO_VERSION, artVersion, experimental);
+    public static Opcodes getDefault() {
+        // The last pre-art api
+        return forApi(20);
     }
 
-    @Deprecated
-    public Opcodes(int api) {
-        this(api, false);
-    }
+    private Opcodes(int api, int artVersion) {
 
-    @Deprecated
-    public Opcodes(int api, boolean experimental) {
-        this(api, VersionMap.mapApiToArtVersion(api), experimental);
-    }
 
-    private Opcodes(int api, int artVersion, boolean experimental) {
         if (api >= 21) {
-            this.api = api;
+        this.api = api;
             this.artVersion = mapApiToArtVersion(api);
         } else if (artVersion >= 0 && artVersion < 39) {
             this.api = mapArtVersionToApi(artVersion);
-            this.artVersion = artVersion;
+        this.artVersion = artVersion;
         } else {
             this.api = api;
             this.artVersion = artVersion;
@@ -116,7 +107,7 @@
             }
 
             Short opcodeValue = versionToValueMap.get(version);
-            if (opcodeValue != null && (!opcode.isExperimental() || experimental)) {
+            if (opcodeValue != null) {
                 if (!opcode.format.isPayloadFormat) {
                     opcodesByValue[opcodeValue] = opcode;
                 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedInstruction.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedInstruction.java
index 55f1ddc..1a9b9ad 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedInstruction.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/AnalyzedInstruction.java
@@ -32,12 +32,14 @@
 package org.jf.dexlib2.analysis;
 
 import com.google.common.base.Objects;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import org.jf.dexlib2.Opcode;
 import org.jf.dexlib2.iface.instruction.*;
 import org.jf.dexlib2.iface.instruction.formats.Instruction22c;
 import org.jf.dexlib2.iface.reference.MethodReference;
 import org.jf.dexlib2.iface.reference.Reference;
+import org.jf.dexlib2.iface.reference.TypeReference;
 import org.jf.util.ExceptionWithContext;
 
 import javax.annotation.Nonnull;
@@ -54,7 +56,7 @@
     /**
      * The actual instruction
      */
-    @Nullable
+    @Nonnull
     protected Instruction instruction;
 
     /**
@@ -65,21 +67,25 @@
     /**
      * Instructions that can pass on execution to this one during normal execution
      */
+    @Nonnull
     protected final TreeSet<AnalyzedInstruction> predecessors = new TreeSet<AnalyzedInstruction>();
 
     /**
      * Instructions that can execution could pass on to next during normal execution
      */
+    @Nonnull
     protected final LinkedList<AnalyzedInstruction> successors = new LinkedList<AnalyzedInstruction>();
 
     /**
      * This contains the register types *before* the instruction has executed
      */
+    @Nonnull
     protected final RegisterType[] preRegisterMap;
 
     /**
      * This contains the register types *after* the instruction has executed
      */
+    @Nonnull
     protected final RegisterType[] postRegisterMap;
 
     /**
@@ -94,8 +100,8 @@
      */
     protected final Instruction originalInstruction;
 
-    public AnalyzedInstruction(MethodAnalyzer methodAnalyzer, Instruction instruction, int instructionIndex,
-                               int registerCount) {
+    public AnalyzedInstruction(@Nonnull MethodAnalyzer methodAnalyzer, @Nonnull Instruction instruction,
+                               int instructionIndex, int registerCount) {
         this.methodAnalyzer = methodAnalyzer;
         this.instruction = instruction;
         this.originalInstruction = instruction;
@@ -150,18 +156,17 @@
         instruction = originalInstruction;
     }
 
-    public int getSuccessorCount() {
-        return successors.size();
-    }
-
-    public List<AnalyzedInstruction> getSuccesors() {
+    @Nonnull
+    public List<AnalyzedInstruction> getSuccessors() {
         return Collections.unmodifiableList(successors);
     }
 
+    @Nonnull
     public Instruction getInstruction() {
         return instruction;
     }
 
+    @Nonnull
     public Instruction getOriginalInstruction() {
         return originalInstruction;
     }
@@ -184,11 +189,7 @@
         if (predecessors.size() == 0) {
             return false;
         }
-
-        if (predecessors.first().instructionIndex == -1) {
-            return true;
-        }
-        return false;
+        return predecessors.first().instructionIndex == -1;
     }
 
     /*
@@ -237,6 +238,7 @@
      * @param registerNumber the register number
      * @return The register type resulting from merging the post-instruction register types from all predecessors
      */
+    @Nonnull
     protected RegisterType getMergedPreRegisterTypeFromPredecessors(int registerNumber) {
         RegisterType mergedRegisterType = null;
         for (AnalyzedInstruction predecessor: predecessors) {
@@ -249,6 +251,10 @@
                 }
             }
         }
+        if (mergedRegisterType == null) {
+            // This is a start-of-method or unreachable instruction.
+            throw new IllegalStateException();
+        }
         return mergedRegisterType;
     }
     /**
@@ -275,10 +281,10 @@
      *
      * This is used to set the register type for only one branch from a conditional jump.
      *
-     * @param predecessor Which predecessor is being overriden
-     * @param registerNumber The register number of the register being overriden
+     * @param predecessor Which predecessor is being overridden
+     * @param registerNumber The register number of the register being overridden
      * @param registerType The overridden register type
-     * @param verifiedInstructions
+     * @param verifiedInstructions A bit vector of instructions that have been verified
      *
      * @return true if the post-instruction register type for this instruction changed as a result of this override
      */
@@ -308,8 +314,8 @@
         return false;
     }
 
-    protected boolean isInvokeInit() {
-        if (instruction == null || !instruction.getOpcode().canInitializeReference()) {
+    public boolean isInvokeInit() {
+        if (!instruction.getOpcode().canInitializeReference()) {
             return false;
         }
 
@@ -323,23 +329,26 @@
         return false;
     }
 
-    public boolean setsRegister() {
-        return instruction.getOpcode().setsRegister();
-    }
-
-    public boolean setsWideRegister() {
-        return instruction.getOpcode().setsWideRegister();
-    }
-
+    /**
+     * Determines if this instruction sets the given register, or alters its type
+     *
+     * @param registerNumber The register to check
+     * @return true if this instruction sets the given register or alters its type
+     */
     public boolean setsRegister(int registerNumber) {
-        //When constructing a new object, the register type will be an uninitialized reference after the new-instance
-        //instruction, but becomes an initialized reference once the <init> method is called. So even though invoke
-        //instructions don't normally change any registers, calling an <init> method will change the type of its
-        //object register. If the uninitialized reference has been copied to other registers, they will be initialized
-        //as well, so we need to check for that too
+        // This method could be implemented by calling getSetRegisters and checking if registerNumber is in the result
+        // However, this is a frequently called method, and this is a more efficient implementation, because it doesn't
+        // allocate a new list, and it can potentially exit earlier
+
         if (isInvokeInit()) {
+            // When constructing a new object, the register type will be an uninitialized reference after the
+            // new-instance instruction, but becomes an initialized reference once the <init> method is called. So even
+            // though invoke instructions don't normally change any registers, calling an <init> method will change the
+            // type of its object register. If the uninitialized reference has been copied to other registers, they will
+            // be initialized as well, so we need to check for that too
             int destinationRegister;
             if (instruction instanceof FiveRegisterInstruction) {
+                assert ((FiveRegisterInstruction)instruction).getRegisterCount() > 0;
                 destinationRegister = ((FiveRegisterInstruction)instruction).getRegisterC();
             } else {
                 assert instruction instanceof RegisterRangeInstruction;
@@ -348,34 +357,107 @@
                 destinationRegister = rangeInstruction.getStartRegister();
             }
 
+            RegisterType preInstructionDestRegisterType = getPreInstructionRegisterType(destinationRegister);
+            if (preInstructionDestRegisterType.category == RegisterType.UNKNOWN) {
+                // We never let an uninitialized reference propagate past an invoke-init if the object register type is
+                // unknown This is because the uninitialized reference may be an alias to the reference being
+                // initialized, but we can't know that until the object register's type is known
+                RegisterType preInstructionRegisterType = getPreInstructionRegisterType(registerNumber);
+                if (preInstructionRegisterType.category == RegisterType.UNINIT_REF ||
+                        preInstructionRegisterType.category == RegisterType.UNINIT_THIS) {
+                    return true;
+                }
+            }
+
+            if (preInstructionDestRegisterType.category != RegisterType.UNINIT_REF &&
+                    preInstructionDestRegisterType.category != RegisterType.UNINIT_THIS) {
+                return false;
+            }
+
             if (registerNumber == destinationRegister) {
                 return true;
             }
-            RegisterType preInstructionDestRegisterType = getPreInstructionRegisterType(registerNumber);
-            if (preInstructionDestRegisterType.category != RegisterType.UNINIT_REF &&
-                preInstructionDestRegisterType.category != RegisterType.UNINIT_THIS) {
 
-                return false;
-            }
             //check if the uninit ref has been copied to another register
-            if (getPreInstructionRegisterType(registerNumber).equals(preInstructionDestRegisterType)) {
-                return true;
-            }
-            return false;
+            return preInstructionDestRegisterType.equals(getPreInstructionRegisterType(registerNumber));
         }
 
-        if (instruction.getOpcode() == Opcode.IF_EQZ || instruction.getOpcode() == Opcode.IF_NEZ) {
-            AnalyzedInstruction previousInstruction = getPreviousInstruction();
-            if (previousInstruction != null &&
-                    previousInstruction.instruction != null &&
-                    previousInstruction.instruction.getOpcode() == Opcode.INSTANCE_OF &&
-                    registerNumber == ((Instruction22c)previousInstruction.instruction).getRegisterB() &&
-                    MethodAnalyzer.canNarrowAfterInstanceOf(previousInstruction, this, methodAnalyzer.getClassPath())) {
-                return true;
+        // On art, the optimizer will often nop out a check-cast instruction after an instance-of instruction.
+        // Normally, check-cast is where the register type actually changes.
+        // In order to correctly handle this case, we have to propagate the narrowed register type to the appropriate
+        // branch of the following if-eqz/if-nez
+        if (instructionIndex > 0 &&
+                methodAnalyzer.getClassPath().isArt() &&
+                getPredecessorCount() == 1 &&
+                (instruction.getOpcode() == Opcode.IF_EQZ || instruction.getOpcode() == Opcode.IF_NEZ)) {
+
+            AnalyzedInstruction prevInstruction = predecessors.first();
+            if (prevInstruction.instruction.getOpcode() == Opcode.INSTANCE_OF &&
+                    MethodAnalyzer.canPropagateTypeAfterInstanceOf(
+                            prevInstruction, this, methodAnalyzer.getClassPath())) {
+                Instruction22c instanceOfInstruction = (Instruction22c)prevInstruction.instruction;
+
+                if (registerNumber == instanceOfInstruction.getRegisterB()) {
+                    return true;
+                }
+
+                // Additionally, there may be a move instruction just before the instance-of, in order to put the value
+                // into a register that is addressable by the instance-of. In this case, we also need to propagate the
+                // new register type for the original register that the value was moved from.
+                // In some cases, the instance-of may have multiple predecessors. In this case, we should only do the
+                // propagation if all predecessors are move-object instructions for the same source register
+                // TODO: do we need to do some sort of additional check that these multiple move-object predecessors actually refer to the same value?
+                if (instructionIndex > 1) {
+                    int originalSourceRegister = -1;
+
+                    RegisterType newType = null;
+
+                    for (AnalyzedInstruction prevPrevAnalyzedInstruction : prevInstruction.predecessors) {
+                        Opcode opcode = prevPrevAnalyzedInstruction.instruction.getOpcode();
+                        if (opcode == Opcode.MOVE_OBJECT || opcode == Opcode.MOVE_OBJECT_16 ||
+                                opcode == Opcode.MOVE_OBJECT_FROM16) {
+                            TwoRegisterInstruction moveInstruction =
+                                    ((TwoRegisterInstruction)prevPrevAnalyzedInstruction.instruction);
+                            RegisterType originalType =
+                                    prevPrevAnalyzedInstruction.getPostInstructionRegisterType(
+                                            moveInstruction.getRegisterB());
+                            if (moveInstruction.getRegisterA() != instanceOfInstruction.getRegisterB()) {
+                                originalSourceRegister = -1;
+                                break;
+                            }
+                            if (originalType.type == null) {
+                                originalSourceRegister = -1;
+                                break;
+                            }
+
+                            if (newType == null) {
+                                newType = RegisterType.getRegisterType(methodAnalyzer.getClassPath(),
+                                        (TypeReference)instanceOfInstruction.getReference());
+                            }
+
+                            if (MethodAnalyzer.isNotWideningConversion(originalType, newType)) {
+                                if (originalSourceRegister != -1) {
+                                    if (originalSourceRegister != moveInstruction.getRegisterB()) {
+                                        originalSourceRegister = -1;
+                                        break;
+                                    }
+                                } else {
+                                    originalSourceRegister = moveInstruction.getRegisterB();
+                                }
+                            }
+                        } else {
+                            originalSourceRegister = -1;
+                            break;
+                        }
+                    }
+                    if (originalSourceRegister != -1 && registerNumber == originalSourceRegister) {
+                        return true;
+                    }
+                }
             }
         }
 
-        if (!setsRegister()) {
+        if (!instruction.getOpcode().setsRegister()) {
             return false;
         }
         int destinationRegister = getDestinationRegister();
@@ -383,20 +465,151 @@
         if (registerNumber == destinationRegister) {
             return true;
         }
-        if (setsWideRegister() && registerNumber == (destinationRegister + 1)) {
+        if (instruction.getOpcode().setsWideRegister() && registerNumber == (destinationRegister + 1)) {
             return true;
         }
         return false;
     }
 
-    @Nullable
-    private AnalyzedInstruction getPreviousInstruction() {
-        for (AnalyzedInstruction predecessor: predecessors) {
-            if (predecessor.getInstructionIndex() == getInstructionIndex() - 1) {
-                return predecessor;
+    public List<Integer> getSetRegisters() {
+        List<Integer> setRegisters = Lists.newArrayList();
+
+        if (instruction.getOpcode().setsRegister()) {
+            setRegisters.add(getDestinationRegister());
+        }
+        if (instruction.getOpcode().setsWideRegister()) {
+            setRegisters.add(getDestinationRegister() + 1);
+        }
+
+        if (isInvokeInit()) {
+            //When constructing a new object, the register type will be an uninitialized reference after the new-instance
+            //instruction, but becomes an initialized reference once the <init> method is called. So even though invoke
+            //instructions don't normally change any registers, calling an <init> method will change the type of its
+            //object register. If the uninitialized reference has been copied to other registers, they will be initialized
+            //as well, so we need to check for that too
+
+            int destinationRegister;
+            if (instruction instanceof FiveRegisterInstruction) {
+                destinationRegister = ((FiveRegisterInstruction)instruction).getRegisterC();
+                assert ((FiveRegisterInstruction)instruction).getRegisterCount() > 0;
+            } else {
+                assert instruction instanceof RegisterRangeInstruction;
+                RegisterRangeInstruction rangeInstruction = (RegisterRangeInstruction)instruction;
+                assert rangeInstruction.getRegisterCount() > 0;
+                destinationRegister = rangeInstruction.getStartRegister();
+            }
+
+            RegisterType preInstructionDestRegisterType = getPreInstructionRegisterType(destinationRegister);
+            if (preInstructionDestRegisterType.category == RegisterType.UNINIT_REF ||
+                    preInstructionDestRegisterType.category == RegisterType.UNINIT_THIS) {
+                setRegisters.add(destinationRegister);
+
+                RegisterType objectRegisterType = preRegisterMap[destinationRegister];
+                for (int i = 0; i < preRegisterMap.length; i++) {
+                    if (i == destinationRegister) {
+                        continue;
+                    }
+
+                    RegisterType preInstructionRegisterType = preRegisterMap[i];
+
+                    if (preInstructionRegisterType.equals(objectRegisterType)) {
+                        setRegisters.add(i);
+                    } else if (preInstructionRegisterType.category == RegisterType.UNINIT_REF ||
+                            preInstructionRegisterType.category == RegisterType.UNINIT_THIS) {
+                        RegisterType postInstructionRegisterType = postRegisterMap[i];
+                        if (postInstructionRegisterType.category == RegisterType.UNKNOWN) {
+                            setRegisters.add(i);
+                        }
+                    }
+                }
+            } else if (preInstructionDestRegisterType.category == RegisterType.UNKNOWN) {
+                // We never let an uninitialized reference propagate past an invoke-init if the object register type is
+                // unknown This is because the uninitialized reference may be an alias to the reference being
+                // initialized, but we can't know that until the object register's type is known
+
+                for (int i = 0; i < preRegisterMap.length; i++) {
+                    RegisterType registerType = preRegisterMap[i];
+                    if (registerType.category == RegisterType.UNINIT_REF ||
+                            registerType.category == RegisterType.UNINIT_THIS) {
+                        setRegisters.add(i);
+                    }
+                }
             }
         }
-        return null;
+
+        // On art, the optimizer will often nop out a check-cast instruction after an instance-of instruction.
+        // Normally, check-cast is where the register type actually changes.
+        // In order to correctly handle this case, we have to propagate the narrowed register type to the appropriate
+        // branch of the following if-eqz/if-nez
+        if (instructionIndex > 0 &&
+                methodAnalyzer.getClassPath().isArt() &&
+                getPredecessorCount() == 1 &&
+                (instruction.getOpcode() == Opcode.IF_EQZ || instruction.getOpcode() == Opcode.IF_NEZ)) {
+
+            AnalyzedInstruction prevInstruction = predecessors.first();
+            if (prevInstruction.instruction.getOpcode() == Opcode.INSTANCE_OF &&
+                    MethodAnalyzer.canPropagateTypeAfterInstanceOf(
+                            prevInstruction, this, methodAnalyzer.getClassPath())) {
+                Instruction22c instanceOfInstruction = (Instruction22c)prevInstruction.instruction;
+                setRegisters.add(instanceOfInstruction.getRegisterB());
+
+                // Additionally, there may be a move instruction just before the instance-of, in order to put the value
+                // into a register that is addressable by the instance-of. In this case, we also need to propagate the
+                // new register type for the original register that the value was moved from.
+                // In some cases, the instance-of may have multiple predecessors. In this case, we should only do the
+                // propagation if all predecessors are move-object instructions for the same source register
+                // TODO: do we need to do some sort of additional check that these multiple move-object predecessors actually refer to the same value?
+                if (instructionIndex > 1) {
+                    int originalSourceRegister = -1;
+
+                    RegisterType newType = null;
+
+                    for (AnalyzedInstruction prevPrevAnalyzedInstruction : prevInstruction.predecessors) {
+                        Opcode opcode = prevPrevAnalyzedInstruction.instruction.getOpcode();
+                        if (opcode == Opcode.MOVE_OBJECT || opcode == Opcode.MOVE_OBJECT_16 ||
+                                opcode == Opcode.MOVE_OBJECT_FROM16) {
+                            TwoRegisterInstruction moveInstruction =
+                                    ((TwoRegisterInstruction)prevPrevAnalyzedInstruction.instruction);
+                            RegisterType originalType =
+                                    prevPrevAnalyzedInstruction.getPostInstructionRegisterType(
+                                            moveInstruction.getRegisterB());
+                            if (moveInstruction.getRegisterA() != instanceOfInstruction.getRegisterB()) {
+                                originalSourceRegister = -1;
+                                break;
+                            }
+                            if (originalType.type == null) {
+                                originalSourceRegister = -1;
+                                break;
+                            }
+
+                            if (newType == null) {
+                                newType = RegisterType.getRegisterType(methodAnalyzer.getClassPath(),
+                                        (TypeReference)instanceOfInstruction.getReference());
+                            }
+
+                            if (MethodAnalyzer.isNotWideningConversion(originalType, newType)) {
+                                if (originalSourceRegister != -1) {
+                                    if (originalSourceRegister != moveInstruction.getRegisterB()) {
+                                        originalSourceRegister = -1;
+                                        break;
+                                    }
+                                } else {
+                                    originalSourceRegister = moveInstruction.getRegisterB();
+                                }
+                            }
+                        } else {
+                            originalSourceRegister = -1;
+                            break;
+                        }
+                    }
+                    if (originalSourceRegister != -1) {
+                        setRegisters.add(originalSourceRegister);
+                    }
+                }
+            }
+        }
+
+        return setRegisters;
     }
 
     public int getDestinationRegister() {
@@ -421,7 +634,7 @@
         return preRegisterMap[registerNumber];
     }
 
-    public int compareTo(AnalyzedInstruction analyzedInstruction) {
+    public int compareTo(@Nonnull AnalyzedInstruction analyzedInstruction) {
         if (instructionIndex < analyzedInstruction.instructionIndex) {
             return -1;
         } else if (instructionIndex == analyzedInstruction.instructionIndex) {
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java
index 9f9e396..48bf618 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPath.java
@@ -36,28 +36,18 @@
 import com.google.common.cache.CacheBuilder;
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
-import com.google.common.collect.ImmutableList;
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
-import org.jf.dexlib2.DexFileFactory;
-import org.jf.dexlib2.DexFileFactory.DexFileNotFound;
-import org.jf.dexlib2.DexFileFactory.MultipleDexFilesException;
 import org.jf.dexlib2.Opcodes;
 import org.jf.dexlib2.analysis.reflection.ReflectionClassDef;
-import org.jf.dexlib2.dexbacked.OatFile.OatDexFile;
 import org.jf.dexlib2.iface.ClassDef;
-import org.jf.dexlib2.iface.DexFile;
 import org.jf.dexlib2.immutable.ImmutableDexFile;
-import org.jf.util.ExceptionWithContext;
 
 import javax.annotation.Nonnull;
-import java.io.File;
 import java.io.IOException;
 import java.io.Serializable;
 import java.util.Arrays;
 import java.util.List;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
 
 public class ClassPath {
     @Nonnull private final TypeProto unknownClass;
@@ -70,8 +60,8 @@
     /**
      * Creates a new ClassPath instance that can load classes from the given providers
      *
-     * @param classProviders An iterable of ClassProviders. When loading a class, these providers will be searched in
-     *                       order
+     * @param classProviders A varargs array of ClassProviders. When loading a class, these providers will be searched
+     *                       in order
      */
     public ClassPath(ClassProvider... classProviders) throws IOException {
         this(Arrays.asList(classProviders), false, NOT_ART);
@@ -82,6 +72,16 @@
      *
      * @param classProviders An iterable of ClassProviders. When loading a class, these providers will be searched in
      *                       order
+     */
+    public ClassPath(Iterable<ClassProvider> classProviders) throws IOException {
+        this(classProviders, false, NOT_ART);
+    }
+
+    /**
+     * Creates a new ClassPath instance that can load classes from the given providers
+     *
+     * @param classProviders An iterable of ClassProviders. When loading a class, these providers will be searched in
+     *                       order
      * @param checkPackagePrivateAccess Whether checkPackagePrivateAccess is needed, enabled for ONLY early API 17 by
      *                                  default
      * @param oatVersion The applicable oat version, or NOT_ART
@@ -114,7 +114,7 @@
 
     private static ClassProvider getBasicClasses() {
         // fallbacks for some special classes that we assume are present
-        return new DexClassProvider(new ImmutableDexFile(Opcodes.forApi(19), ImmutableSet.of(
+        return new DexClassProvider(new ImmutableDexFile(Opcodes.getDefault(), ImmutableSet.of(
                 new ReflectionClassDef(Class.class),
                 new ReflectionClassDef(Cloneable.class),
                 new ReflectionClassDef(Object.class),
@@ -164,119 +164,6 @@
         return checkPackagePrivateAccess;
     }
 
-    @Nonnull
-    public static ClassPath fromClassPath(Iterable<String> classPathDirs, Iterable<String> classPath, DexFile dexFile,
-                                          int api, boolean experimental) {
-        return fromClassPath(classPathDirs, classPath, dexFile, api, api == 17, experimental);
-    }
-
-    @Nonnull
-    public static ClassPath fromClassPath(Iterable<String> classPathDirs, Iterable<String> classPath, DexFile dexFile,
-                                          int api, boolean checkPackagePrivateAccess, boolean experimental) {
-        List<ClassProvider> providers = Lists.newArrayList();
-
-        int oatVersion = NOT_ART;
-
-        for (String classPathEntry: classPath) {
-            List<? extends DexFile> classPathDexFiles =
-                    loadClassPathEntry(classPathDirs, classPathEntry, api, experimental);
-            if (oatVersion == NOT_ART) {
-                for (DexFile classPathDexFile: classPathDexFiles) {
-                    if (classPathDexFile instanceof OatDexFile) {
-                        oatVersion = ((OatDexFile)classPathDexFile).getOatVersion();
-                        break;
-                    }
-                }
-            }
-            for (DexFile classPathDexFile: classPathDexFiles) {
-                providers.add(new DexClassProvider(classPathDexFile));
-            }
-        }
-        providers.add(new DexClassProvider(dexFile));
-        return new ClassPath(providers, checkPackagePrivateAccess, oatVersion);
-    }
-
-    @Nonnull
-    public static ClassPath fromClassPath(Iterable<String> classPathDirs, Iterable<String> classPath, DexFile dexFile,
-                                          int api, boolean checkPackagePrivateAccess, boolean experimental,
-                                          int oatVersion) {
-        List<ClassProvider> providers = Lists.newArrayList();
-
-        for (String classPathEntry: classPath) {
-            List<? extends DexFile> classPathDexFiles =
-                    loadClassPathEntry(classPathDirs, classPathEntry, api, experimental);
-            for (DexFile classPathDexFile: classPathDexFiles) {
-                providers.add(new DexClassProvider(classPathDexFile));
-            }
-        }
-        providers.add(new DexClassProvider(dexFile));
-        return new ClassPath(providers, checkPackagePrivateAccess, oatVersion);
-    }
-
-    private static final Pattern dalvikCacheOdexPattern = Pattern.compile("@([^@]+)@classes.dex$");
-
-    @Nonnull
-    private static List<? extends DexFile> loadClassPathEntry(@Nonnull Iterable<String> classPathDirs,
-                                                              @Nonnull String bootClassPathEntry, int api,
-                                                              boolean experimental) {
-        File rawEntry = new File(bootClassPathEntry);
-        // strip off the path - we only care about the filename
-        String entryName = rawEntry.getName();
-
-        // if it's a dalvik-cache entry, grab the name of the jar/apk
-        if (entryName.endsWith("@classes.dex")) {
-            Matcher m = dalvikCacheOdexPattern.matcher(entryName);
-
-            if (!m.find()) {
-                throw new ExceptionWithContext(String.format("Cannot parse dependency value %s", bootClassPathEntry));
-            }
-
-            entryName = m.group(1);
-        }
-
-        int extIndex = entryName.lastIndexOf(".");
-
-        String baseEntryName;
-        if (extIndex == -1) {
-            baseEntryName = entryName;
-        } else {
-            baseEntryName = entryName.substring(0, extIndex);
-        }
-
-        for (String classPathDir: classPathDirs) {
-            String[] extensions;
-
-            if (entryName.endsWith(".oat")) {
-                extensions = new String[] { ".oat" };
-            } else {
-                extensions = new String[] { "", ".odex", ".jar", ".apk", ".zip" };
-            }
-
-            for (String ext: extensions) {
-                File file = new File(classPathDir, baseEntryName + ext);
-
-                if (file.exists() && file.isFile()) {
-                    if (!file.canRead()) {
-                        System.err.println(String.format(
-                                "warning: cannot open %s for reading. Will continue looking.", file.getPath()));
-                    } else {
-                        try {
-                            return ImmutableList.of(DexFileFactory.loadDexFile(file, api, experimental));
-                        } catch (DexFileNotFound ex) {
-                            // ignore and continue
-                        } catch (MultipleDexFilesException ex) {
-                            return ex.oatFile.getDexFiles();
-                        } catch (Exception ex) {
-                            throw ExceptionWithContext.withContext(ex,
-                                    "Error while reading boot class path entry \"%s\"", bootClassPathEntry);
-                        }
-                    }
-                }
-            }
-        }
-        throw new ExceptionWithContext("Cannot locate boot class path file %s", bootClassPathEntry);
-    }
-
     private final Supplier<OdexedFieldInstructionMapper> fieldInstructionMapperSupplier = Suppliers.memoize(
             new Supplier<OdexedFieldInstructionMapper>() {
                 @Override public OdexedFieldInstructionMapper get() {
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPathResolver.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPathResolver.java
new file mode 100644
index 0000000..10daa56
--- /dev/null
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassPathResolver.java
@@ -0,0 +1,465 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.dexlib2.analysis;
+
+import com.beust.jcommander.internal.Sets;
+import com.google.common.base.Joiner;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Lists;
+import org.jf.dexlib2.DexFileFactory;
+import org.jf.dexlib2.DexFileFactory.UnsupportedFileTypeException;
+import org.jf.dexlib2.Opcodes;
+import org.jf.dexlib2.dexbacked.DexBackedDexFile;
+import org.jf.dexlib2.dexbacked.DexBackedOdexFile;
+import org.jf.dexlib2.dexbacked.OatFile;
+import org.jf.dexlib2.dexbacked.OatFile.OatDexFile;
+import org.jf.dexlib2.iface.DexFile;
+import org.jf.dexlib2.iface.MultiDexContainer;
+import org.jf.dexlib2.iface.MultiDexContainer.MultiDexFile;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Set;
+
+public class ClassPathResolver {
+    private final Iterable<String> classPathDirs;
+    private final Opcodes opcodes;
+
+    private final Set<File> loadedFiles = Sets.newHashSet();
+    private final List<ClassProvider> classProviders = Lists.newArrayList();
+
+    /**
+     * Constructs a new ClassPathResolver using a specified list of bootclasspath entries
+     *
+     * @param bootClassPathDirs A list of directories to search for boot classpath entries. Can be empty if all boot
+     *                          classpath entries are specified as local paths
+     * @param bootClassPathEntries A list of boot classpath entries to load. These can either be local paths, or
+     *                             device paths (e.g. "/system/framework/framework.jar"). The entry will be interpreted
+     *                             first as a local path. If not found as a local path, it will be interpreted as a
+     *                             partial or absolute device path, and will be searched for in bootClassPathDirs
+     * @param extraClassPathEntries A list of additional classpath entries to load. Can be empty. All entries must be
+     *                              local paths. Device paths are not supported.
+     * @param dexFile The dex file that the classpath will be used to analyze
+     * @throws IOException If any IOException occurs
+     * @throws ResolveException If any classpath entries cannot be loaded for some reason
+     *
+     *  If null, a default bootclasspath is used,
+     *                             depending on the the file type of dexFile and the api level. If empty, no boot
+     *                             classpath entries will be loaded
+     */
+    public ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nonnull List<String> bootClassPathEntries,
+                             @Nonnull List<String> extraClassPathEntries, @Nonnull DexFile dexFile)
+            throws IOException {
+        this(bootClassPathDirs, bootClassPathEntries, extraClassPathEntries, dexFile, true);
+    }
+
+    /**
+     * Constructs a new ClassPathResolver using a default list of bootclasspath entries
+     *
+     * @param bootClassPathDirs A list of directories to search for boot classpath entries
+     * @param extraClassPathEntries A list of additional classpath entries to load. Can be empty. All entries must be
+     *                              local paths. Device paths are not supported.
+     * @param dexFile The dex file that the classpath will be used to analyze
+     * @throws IOException If any IOException occurs
+     * @throws ResolveException If any classpath entries cannot be loaded for some reason
+     *
+     *  If null, a default bootclasspath is used,
+     *                             depending on the the file type of dexFile and the api level. If empty, no boot
+     *                             classpath entries will be loaded
+     */
+    public ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nonnull List<String> extraClassPathEntries,
+                             @Nonnull DexFile dexFile)
+            throws IOException {
+        this(bootClassPathDirs, null, extraClassPathEntries, dexFile, true);
+    }
+
+    private ClassPathResolver(@Nonnull List<String> bootClassPathDirs, @Nullable List<String> bootClassPathEntries,
+                              @Nonnull List<String> extraClassPathEntries, @Nonnull DexFile dexFile, boolean unused)
+            throws IOException {
+        this.classPathDirs = bootClassPathDirs;
+        opcodes = dexFile.getOpcodes();
+
+        if (bootClassPathEntries == null) {
+            bootClassPathEntries = getDefaultBootClassPath(dexFile, opcodes.api);
+        }
+
+        for (String entry : bootClassPathEntries) {
+            try {
+                loadLocalOrDeviceBootClassPathEntry(entry);
+            } catch (NoDexException ex) {
+                if (entry.endsWith(".jar")) {
+                    String odexEntry = entry.substring(0, entry.length() - 4) + ".odex";
+                    try {
+                        loadLocalOrDeviceBootClassPathEntry(odexEntry);
+                    } catch (NoDexException ex2) {
+                        throw new ResolveException("Neither %s nor %s contain a dex file", entry, odexEntry);
+                    } catch (NotFoundException ex2) {
+                        throw new ResolveException(ex);
+                    }
+                } else {
+                    throw new ResolveException(ex);
+                }
+            } catch (NotFoundException ex) {
+                if (entry.endsWith(".odex")) {
+                    String jarEntry = entry.substring(0, entry.length() - 5) + ".jar";
+                    try {
+                        loadLocalOrDeviceBootClassPathEntry(jarEntry);
+                        } catch (NoDexException ex2) {
+                        throw new ResolveException("Neither %s nor %s contain a dex file", entry, jarEntry);
+                    } catch (NotFoundException ex2) {
+                        throw new ResolveException(ex);
+                    }
+                } else {
+                    throw new ResolveException(ex);
+                }
+            }
+        }
+
+        for (String entry: extraClassPathEntries) {
+            // extra classpath entries must be specified using a local path, so we don't need to do the search through
+            // bootClassPathDirs
+            try {
+                loadLocalClassPathEntry(entry);
+            } catch (NoDexException ex) {
+                throw new ResolveException(ex);
+            }
+        }
+
+        if (dexFile instanceof MultiDexContainer.MultiDexFile) {
+            MultiDexContainer<? extends MultiDexFile> container = ((MultiDexFile)dexFile).getContainer();
+            for (String entry: container.getDexEntryNames()) {
+                classProviders.add(new DexClassProvider(container.getEntry(entry)));
+            }
+        } else {
+            classProviders.add(new DexClassProvider(dexFile));
+        }
+    }
+
+    @Nonnull
+    public List<ClassProvider> getResolvedClassProviders() {
+        return classProviders;
+    }
+
+    private boolean loadLocalClassPathEntry(@Nonnull String entry) throws NoDexException, IOException {
+        File entryFile = new File(entry);
+        if (entryFile.exists() && entryFile.isFile()) {
+            try {
+                loadEntry(entryFile, true);
+                return true;
+            } catch (UnsupportedFileTypeException ex) {
+                throw new ResolveException(ex, "Couldn't load classpath entry %s", entry);
+            }
+        }
+        return false;
+    }
+
+    private void loadLocalOrDeviceBootClassPathEntry(@Nonnull String entry)
+            throws IOException, NoDexException, NotFoundException {
+        // first, see if the entry is a valid local path
+        if (loadLocalClassPathEntry(entry)) {
+            return;
+        }
+
+        // It's not a local path, so let's try to resolve it as a device path, relative to one of the provided
+        // directories
+        List<String> pathComponents = splitDevicePath(entry);
+        Joiner pathJoiner = Joiner.on(File.pathSeparatorChar);
+
+        for (String directory: classPathDirs) {
+            File directoryFile = new File(directory);
+            if (!directoryFile.exists()) {
+                continue;
+            }
+
+            for (int i=0; i<pathComponents.size(); i++) {
+                String partialPath = pathJoiner.join(pathComponents.subList(i, pathComponents.size()));
+                File entryFile = new File(directoryFile, partialPath);
+                if (entryFile.exists() && entryFile.isFile()) {
+                    loadEntry(entryFile, true);
+                    return;
+                }
+            }
+        }
+
+        throw new NotFoundException("Could not find classpath entry %s", entry);
+    }
+
+    private void loadEntry(@Nonnull File entryFile, boolean loadOatDependencies)
+            throws IOException, NoDexException {
+        if (loadedFiles.contains(entryFile)) {
+            return;
+        }
+
+        MultiDexContainer<? extends DexBackedDexFile> container;
+        try {
+            container = DexFileFactory.loadDexContainer(entryFile, opcodes);
+        } catch (UnsupportedFileTypeException ex) {
+            throw new ResolveException(ex);
+        }
+
+        List<String> entryNames = container.getDexEntryNames();
+
+        if (entryNames.size() == 0) {
+            throw new NoDexException("%s contains no dex file", entryFile);
+        }
+
+        loadedFiles.add(entryFile);
+
+        for (String entryName: entryNames) {
+            classProviders.add(new DexClassProvider(container.getEntry(entryName)));
+        }
+
+        if (loadOatDependencies && container instanceof OatFile) {
+            List<String> oatDependencies = ((OatFile)container).getBootClassPath();
+            if (!oatDependencies.isEmpty()) {
+                try {
+                    loadOatDependencies(entryFile.getParentFile(), oatDependencies);
+                } catch (NotFoundException ex) {
+                    throw new ResolveException(ex, "Error while loading oat file %s", entryFile);
+                } catch (NoDexException ex) {
+                    throw new ResolveException(ex, "Error while loading dependencies for oat file %s", entryFile);
+                }
+            }
+        }
+    }
+
+    @Nonnull
+    private static List<String> splitDevicePath(@Nonnull String path) {
+        return Lists.newArrayList(Splitter.on('/').split(path));
+    }
+
+    private void loadOatDependencies(@Nonnull File directory, @Nonnull List<String> oatDependencies)
+            throws IOException, NoDexException, NotFoundException {
+        // We assume that all oat dependencies are located in the same directory as the oat file
+        for (String oatDependency: oatDependencies) {
+            String oatDependencyName = getFilenameForOatDependency(oatDependency);
+            File file = new File(directory, oatDependencyName);
+            if (!file.exists()) {
+                throw new NotFoundException("Cannot find dependency %s in %s", oatDependencyName, directory);
+            }
+
+            loadEntry(file, false);
+        }
+    }
+
+    @Nonnull
+    private String getFilenameForOatDependency(String oatDependency) {
+        int index = oatDependency.lastIndexOf('/');
+
+        String dependencyLeaf = oatDependency.substring(index+1);
+        if (dependencyLeaf.endsWith(".art")) {
+            return dependencyLeaf.substring(0, dependencyLeaf.length() - 4) + ".oat";
+        }
+        return dependencyLeaf;
+    }
+
+    private static class NotFoundException extends Exception {
+        public NotFoundException(String message, Object... formatArgs) {
+            super(String.format(message, formatArgs));
+        }
+    }
+    
+    private static class NoDexException extends Exception {
+        public NoDexException(String message, Object... formatArgs) {
+            super(String.format(message, formatArgs));
+        }
+    }
+
+    /**
+     * An error that occurred while resolving the classpath
+     */
+    public static class ResolveException extends RuntimeException {
+        public ResolveException (String message, Object... formatArgs) {
+            super(String.format(message, formatArgs));
+        }
+
+        public ResolveException (Throwable cause) {
+            super(cause);
+        }
+
+        public ResolveException (Throwable cause, String message, Object... formatArgs) {
+            super(String.format(message, formatArgs), cause);
+        }
+    }
+
+    /**
+     * Returns the default boot class path for the given dex file and api level.
+     */
+    @Nonnull
+    private static List<String> getDefaultBootClassPath(@Nonnull DexFile dexFile, int apiLevel) {
+        if (dexFile instanceof OatFile.OatDexFile) {
+            List<String> bcp = ((OatDexFile)dexFile).getContainer().getBootClassPath();
+            if (!bcp.isEmpty()) {
+                for (int i=0; i<bcp.size(); i++) {
+                    String entry = bcp.get(i);
+                    if (entry.endsWith(".art")) {
+                        bcp.set(i, entry.substring(0, entry.length() - 4) + ".oat");
+                    }
+                }
+                return bcp;
+            }
+            return Lists.newArrayList("boot.oat");
+        }
+
+        if (dexFile instanceof DexBackedOdexFile) {
+            return ((DexBackedOdexFile)dexFile).getDependencies();
+        }
+
+        if (apiLevel <= 8) {
+            return Lists.newArrayList(
+                    "/system/framework/core.jar",
+                    "/system/framework/ext.jar",
+                    "/system/framework/framework.jar",
+                    "/system/framework/android.policy.jar",
+                    "/system/framework/services.jar");
+        } else if (apiLevel <= 11) {
+            return Lists.newArrayList(
+                    "/system/framework/core.jar",
+                    "/system/framework/bouncycastle.jar",
+                    "/system/framework/ext.jar",
+                    "/system/framework/framework.jar",
+                    "/system/framework/android.policy.jar",
+                    "/system/framework/services.jar",
+                    "/system/framework/core-junit.jar");
+        } else if (apiLevel <= 13) {
+            return Lists.newArrayList(
+                    "/system/framework/core.jar",
+                    "/system/framework/apache-xml.jar",
+                    "/system/framework/bouncycastle.jar",
+                    "/system/framework/ext.jar",
+                    "/system/framework/framework.jar",
+                    "/system/framework/android.policy.jar",
+                    "/system/framework/services.jar",
+                    "/system/framework/core-junit.jar");
+        } else if (apiLevel <= 15) {
+            return Lists.newArrayList(
+                    "/system/framework/core.jar",
+                    "/system/framework/core-junit.jar",
+                    "/system/framework/bouncycastle.jar",
+                    "/system/framework/ext.jar",
+                    "/system/framework/framework.jar",
+                    "/system/framework/android.policy.jar",
+                    "/system/framework/services.jar",
+                    "/system/framework/apache-xml.jar",
+                    "/system/framework/filterfw.jar");
+        } else if (apiLevel <= 17) {
+            // this is correct as of api 17/4.2.2
+            return Lists.newArrayList(
+                    "/system/framework/core.jar",
+                    "/system/framework/core-junit.jar",
+                    "/system/framework/bouncycastle.jar",
+                    "/system/framework/ext.jar",
+                    "/system/framework/framework.jar",
+                    "/system/framework/telephony-common.jar",
+                    "/system/framework/mms-common.jar",
+                    "/system/framework/android.policy.jar",
+                    "/system/framework/services.jar",
+                    "/system/framework/apache-xml.jar");
+        } else if (apiLevel <= 18) {
+            return Lists.newArrayList(
+                    "/system/framework/core.jar",
+                    "/system/framework/core-junit.jar",
+                    "/system/framework/bouncycastle.jar",
+                    "/system/framework/ext.jar",
+                    "/system/framework/framework.jar",
+                    "/system/framework/telephony-common.jar",
+                    "/system/framework/voip-common.jar",
+                    "/system/framework/mms-common.jar",
+                    "/system/framework/android.policy.jar",
+                    "/system/framework/services.jar",
+                    "/system/framework/apache-xml.jar");
+        } else if (apiLevel <= 19) {
+            return Lists.newArrayList(
+                    "/system/framework/core.jar",
+                    "/system/framework/conscrypt.jar",
+                    "/system/framework/core-junit.jar",
+                    "/system/framework/bouncycastle.jar",
+                    "/system/framework/ext.jar",
+                    "/system/framework/framework.jar",
+                    "/system/framework/framework2.jar",
+                    "/system/framework/telephony-common.jar",
+                    "/system/framework/voip-common.jar",
+                    "/system/framework/mms-common.jar",
+                    "/system/framework/android.policy.jar",
+                    "/system/framework/services.jar",
+                    "/system/framework/apache-xml.jar",
+                    "/system/framework/webviewchromium.jar");
+        } else if (apiLevel <= 22) {
+            return Lists.newArrayList(
+                    "/system/framework/core-libart.jar",
+                    "/system/framework/conscrypt.jar",
+                    "/system/framework/okhttp.jar",
+                    "/system/framework/core-junit.jar",
+                    "/system/framework/bouncycastle.jar",
+                    "/system/framework/ext.jar",
+                    "/system/framework/framework.jar",
+                    "/system/framework/telephony-common.jar",
+                    "/system/framework/voip-common.jar",
+                    "/system/framework/ims-common.jar",
+                    "/system/framework/mms-common.jar",
+                    "/system/framework/android.policy.jar",
+                    "/system/framework/apache-xml.jar");
+        } else if (apiLevel <= 23) {
+            return Lists.newArrayList(
+                    "/system/framework/core-libart.jar",
+                    "/system/framework/conscrypt.jar",
+                    "/system/framework/okhttp.jar",
+                    "/system/framework/core-junit.jar",
+                    "/system/framework/bouncycastle.jar",
+                    "/system/framework/ext.jar",
+                    "/system/framework/framework.jar",
+                    "/system/framework/telephony-common.jar",
+                    "/system/framework/voip-common.jar",
+                    "/system/framework/ims-common.jar",
+                    "/system/framework/apache-xml.jar",
+                    "/system/framework/org.apache.http.legacy.boot.jar");
+        } else /*if (apiLevel <= 24)*/ {
+            return Lists.newArrayList(
+                    "/system/framework/core-oj.jar",
+                    "/system/framework/core-libart.jar",
+                    "/system/framework/conscrypt.jar",
+                    "/system/framework/okhttp.jar",
+                    "/system/framework/core-junit.jar",
+                    "/system/framework/bouncycastle.jar",
+                    "/system/framework/ext.jar",
+                    "/system/framework/framework.jar",
+                    "/system/framework/telephony-common.jar",
+                    "/system/framework/voip-common.jar",
+                    "/system/framework/ims-common.jar",
+                    "/system/framework/apache-xml.jar",
+                    "/system/framework/org.apache.http.legacy.boot.jar");
+        }
+    }
+}
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassProto.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassProto.java
old mode 100644
new mode 100755
index cef683c..44cc5e2
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassProto.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/ClassProto.java
@@ -31,6 +31,7 @@
 
 package org.jf.dexlib2.analysis;
 
+import com.google.common.base.Joiner;
 import com.google.common.base.Predicates;
 import com.google.common.base.Supplier;
 import com.google.common.base.Suppliers;
@@ -38,12 +39,10 @@
 import com.google.common.primitives.Ints;
 import org.jf.dexlib2.AccessFlags;
 import org.jf.dexlib2.analysis.util.TypeProtoUtils;
-import org.jf.dexlib2.iface.ClassDef;
-import org.jf.dexlib2.iface.Field;
-import org.jf.dexlib2.iface.Method;
+import org.jf.dexlib2.base.reference.BaseMethodReference;
+import org.jf.dexlib2.iface.*;
 import org.jf.dexlib2.iface.reference.FieldReference;
 import org.jf.dexlib2.iface.reference.MethodReference;
-import org.jf.dexlib2.immutable.ImmutableMethod;
 import org.jf.dexlib2.util.MethodUtil;
 import org.jf.util.AlignmentUtils;
 import org.jf.util.ExceptionWithContext;
@@ -52,6 +51,7 @@
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 import java.util.*;
+import java.util.Map.Entry;
 
 /**
  * A class "prototype". This contains things like the interfaces, the superclass, the vtable and the instance fields
@@ -122,11 +122,18 @@
      */
     @Nonnull
     protected LinkedHashMap<String, ClassDef> getInterfaces() {
-        return interfacesSupplier.get();
+        if (!classPath.isArt() || classPath.oatVersion < 72) {
+            return preDefaultMethodInterfaceSupplier.get();
+        } else {
+            return postDefaultMethodInterfaceSupplier.get();
+        }
     }
 
+    /**
+     * This calculates the interfaces in the order required for vtable generation for dalvik and pre-default method ART
+     */
     @Nonnull
-    private final Supplier<LinkedHashMap<String, ClassDef>> interfacesSupplier =
+    private final Supplier<LinkedHashMap<String, ClassDef>> preDefaultMethodInterfaceSupplier =
             Suppliers.memoize(new Supplier<LinkedHashMap<String, ClassDef>>() {
                 @Override public LinkedHashMap<String, ClassDef> get() {
                     Set<String> unresolvedInterfaces = new HashSet<String>(0);
@@ -148,7 +155,8 @@
                                 ClassProto interfaceProto = (ClassProto) classPath.getClass(interfaceType);
                                 for (String superInterface: interfaceProto.getInterfaces().keySet()) {
                                     if (!interfaces.containsKey(superInterface)) {
-                                        interfaces.put(superInterface, interfaceProto.getInterfaces().get(superInterface));
+                                        interfaces.put(superInterface,
+                                                interfaceProto.getInterfaces().get(superInterface));
                                     }
                                 }
                                 if (!interfaceProto.interfacesFullyResolved) {
@@ -158,6 +166,7 @@
                             }
                         }
                     } catch (UnresolvedClassException ex) {
+                        interfaces.put(type, null);
                         unresolvedInterfaces.add(type);
                         interfacesFullyResolved = false;
                     }
@@ -196,6 +205,71 @@
                 }
             });
 
+    /**
+     * This calculates the interfaces in the order required for vtable generation for post-default method ART
+     */
+    @Nonnull
+    private final Supplier<LinkedHashMap<String, ClassDef>> postDefaultMethodInterfaceSupplier =
+            Suppliers.memoize(new Supplier<LinkedHashMap<String, ClassDef>>() {
+                @Override public LinkedHashMap<String, ClassDef> get() {
+                    Set<String> unresolvedInterfaces = new HashSet<String>(0);
+                    LinkedHashMap<String, ClassDef> interfaces = Maps.newLinkedHashMap();
+
+                    String superclass = getSuperclass();
+                    if (superclass != null) {
+                        ClassProto superclassProto = (ClassProto) classPath.getClass(superclass);
+                        for (String superclassInterface: superclassProto.getInterfaces().keySet()) {
+                            interfaces.put(superclassInterface, null);
+                        }
+                        if (!superclassProto.interfacesFullyResolved) {
+                            unresolvedInterfaces.addAll(superclassProto.getUnresolvedInterfaces());
+                            interfacesFullyResolved = false;
+                        }
+                    }
+
+                    try {
+                        for (String interfaceType: getClassDef().getInterfaces()) {
+                            if (!interfaces.containsKey(interfaceType)) {
+                                ClassProto interfaceProto = (ClassProto)classPath.getClass(interfaceType);
+                                try {
+                                    for (Entry<String, ClassDef> entry: interfaceProto.getInterfaces().entrySet()) {
+                                        if (!interfaces.containsKey(entry.getKey())) {
+                                            interfaces.put(entry.getKey(), entry.getValue());
+                                        }
+                                    }
+                                } catch (UnresolvedClassException ex) {
+                                    interfaces.put(interfaceType, null);
+                                    unresolvedInterfaces.add(interfaceType);
+                                    interfacesFullyResolved = false;
+                                }
+                                if (!interfaceProto.interfacesFullyResolved) {
+                                    unresolvedInterfaces.addAll(interfaceProto.getUnresolvedInterfaces());
+                                    interfacesFullyResolved = false;
+                                }
+                                try {
+                                    ClassDef interfaceDef = classPath.getClassDef(interfaceType);
+                                    interfaces.put(interfaceType, interfaceDef);
+                                } catch (UnresolvedClassException ex) {
+                                    interfaces.put(interfaceType, null);
+                                    unresolvedInterfaces.add(interfaceType);
+                                    interfacesFullyResolved = false;
+                                }
+                            }
+                        }
+                    } catch (UnresolvedClassException ex) {
+                        interfaces.put(type, null);
+                        unresolvedInterfaces.add(type);
+                        interfacesFullyResolved = false;
+                    }
+
+                    if (unresolvedInterfaces.size() > 0) {
+                        ClassProto.this.unresolvedInterfaces = unresolvedInterfaces;
+                    }
+
+                    return interfaces;
+                }
+            });
+
     @Nonnull
     protected Set<String> getUnresolvedInterfaces() {
         if (unresolvedInterfaces == null) {
@@ -219,7 +293,7 @@
 
         if (!interfacesFullyResolved) {
             throw new UnresolvedClassException("Interfaces for class %s not fully resolved: %s", getType(),
-                    getUnresolvedInterfaces());
+                    Joiner.on(',').join(getUnresolvedInterfaces()));
         }
 
         return directInterfaces;
@@ -378,7 +452,10 @@
     }
 
     public int findMethodIndexInVtable(@Nonnull MethodReference method) {
-        List<Method> vtable = getVtable();
+        return findMethodIndexInVtable(getVtable(), method);
+    }
+
+    private int findMethodIndexInVtable(@Nonnull List<Method> vtable, MethodReference method) {
         for (int i=0; i<vtable.size(); i++) {
             Method candidate = vtable.get(i);
             if (MethodUtil.methodSignaturesMatch(candidate, method)) {
@@ -391,7 +468,20 @@
         return -1;
     }
 
-    @Nonnull SparseArray<FieldReference> getInstanceFields() {
+    private int findMethodIndexInVtableReverse(@Nonnull List<Method> vtable, MethodReference method) {
+        for (int i=vtable.size() - 1; i>=0; i--) {
+            Method candidate = vtable.get(i);
+            if (MethodUtil.methodSignaturesMatch(candidate, method)) {
+                if (!classPath.shouldCheckPackagePrivateAccess() ||
+                        AnalyzedMethodUtil.canAccess(this, candidate, true, false, false)) {
+                    return i;
+                }
+            }
+        }
+        return -1;
+    }
+
+    @Nonnull public SparseArray<FieldReference> getInstanceFields() {
         if (classPath.isArt()) {
             return artInstanceFieldsSupplier.get();
         } else {
@@ -439,10 +529,8 @@
                     ClassProto superclass = null;
                     if (superclassType != null) {
                         superclass = (ClassProto) classPath.getClass(superclassType);
-                        if (superclass != null) {
                             startFieldOffset = superclass.getNextFieldOffset();
                         }
-                    }
 
                     int fieldIndexMod;
                     if ((startFieldOffset % 8) == 0) {
@@ -529,14 +617,12 @@
 
                         //add padding to align the wide fields, if needed
                         if (fieldTypes[i] == WIDE && !gotDouble) {
-                            if (!gotDouble) {
                                 if (fieldOffset % 8 != 0) {
                                     assert fieldOffset % 8 == 4;
                                     fieldOffset += 4;
                                 }
                                 gotDouble = true;
                             }
-                        }
 
                         instanceFields.append(fieldOffset, field);
                         if (fieldTypes[i] == WIDE) {
@@ -573,7 +659,7 @@
         public static FieldGap newFieldGap(int offset, int size, int oatVersion) {
             if (oatVersion >= 67) {
                 return new FieldGap(offset, size) {
-                    @Override public int compareTo(FieldGap o) {
+                    @Override public int compareTo(@Nonnull FieldGap o) {
                         int result = Ints.compare(o.size, size);
                         if (result != 0) {
                             return result;
@@ -583,7 +669,7 @@
                 };
             } else {
                 return new FieldGap(offset, size) {
-                    @Override public int compareTo(FieldGap o) {
+                    @Override public int compareTo(@Nonnull FieldGap o) {
                         int result = Ints.compare(size, o.size);
                         if (result != 0) {
                             return result;
@@ -777,12 +863,18 @@
         throw new ExceptionWithContext("Invalid type: %s", type);
     }
 
-    @Nonnull List<Method> getVtable() {
-        return vtableSupplier.get();
+    @Nonnull public List<Method> getVtable() {
+        if (!classPath.isArt() || classPath.oatVersion < 72) {
+            return preDefaultMethodVtableSupplier.get();
+        } else if (classPath.oatVersion < 87) {
+            return buggyPostDefaultMethodVtableSupplier.get();
+        } else {
+            return postDefaultMethodVtableSupplier.get();
+        }
     }
 
     //TODO: check the case when we have a package private method that overrides an interface method
-    @Nonnull private final Supplier<List<Method>> vtableSupplier = Suppliers.memoize(new Supplier<List<Method>>() {
+    @Nonnull private final Supplier<List<Method>> preDefaultMethodVtableSupplier = Suppliers.memoize(new Supplier<List<Method>>() {
         @Override public List<Method> get() {
             List<Method> vtable = Lists.newArrayList();
 
@@ -811,52 +903,315 @@
             //iterate over the virtual methods in the current class, and only add them when we don't already have the
             //method (i.e. if it was implemented by the superclass)
             if (!isInterface()) {
-                addToVtable(getClassDef().getVirtualMethods(), vtable, true);
+                addToVtable(getClassDef().getVirtualMethods(), vtable, true, true);
 
-                // assume that interface method is implemented in the current class, when adding it to vtable
-                // otherwise it looks like that method is invoked on an interface, which fails Dalvik's optimization checks
-                for (ClassDef interfaceDef: getDirectInterfaces()) {
+                // We use the current class for any vtable method references that we add, rather than the interface, so
+                // we don't end up trying to call invoke-virtual using an interface, which will fail verification
+                Iterable<ClassDef> interfaces = getDirectInterfaces();
+                for (ClassDef interfaceDef: interfaces) {
                     List<Method> interfaceMethods = Lists.newArrayList();
                     for (Method interfaceMethod: interfaceDef.getVirtualMethods()) {
-                        ImmutableMethod method = new ImmutableMethod(
-                                type,
-                                interfaceMethod.getName(),
-                                interfaceMethod.getParameters(),
-                                interfaceMethod.getReturnType(),
-                                interfaceMethod.getAccessFlags(),
-                                interfaceMethod.getAnnotations(),
-                                interfaceMethod.getImplementation());
-                        interfaceMethods.add(method);
+                        interfaceMethods.add(new ReparentedMethod(interfaceMethod, type));
                     }
-                    addToVtable(interfaceMethods, vtable, false);
+                    addToVtable(interfaceMethods, vtable, false, true);
                 }
             }
             return vtable;
         }
+    });
 
-        private void addToVtable(@Nonnull Iterable<? extends Method> localMethods,
-                                 @Nonnull List<Method> vtable, boolean replaceExisting) {
-            List<? extends Method> methods = Lists.newArrayList(localMethods);
-            Collections.sort(methods);
+    /**
+     * This is the vtable supplier for a version of art that had buggy vtable calculation logic. In some cases it can
+     * produce multiple vtable entries for a given virtual method. This supplier duplicates this buggy logic in order to
+     * generate an identical vtable
+     */
+    @Nonnull private final Supplier<List<Method>> buggyPostDefaultMethodVtableSupplier = Suppliers.memoize(new Supplier<List<Method>>() {
+        @Override public List<Method> get() {
+            List<Method> vtable = Lists.newArrayList();
 
-            outer: for (Method virtualMethod: methods) {
-                for (int i=0; i<vtable.size(); i++) {
-                    Method superMethod = vtable.get(i);
-                    if (MethodUtil.methodSignaturesMatch(superMethod, virtualMethod)) {
+            //copy the virtual methods from the superclass
+            String superclassType;
+            try {
+                superclassType = getSuperclass();
+            } catch (UnresolvedClassException ex) {
+                vtable.addAll(((ClassProto)classPath.getClass("Ljava/lang/Object;")).getVtable());
+                vtableFullyResolved = false;
+                return vtable;
+            }
+
+            if (superclassType != null) {
+                ClassProto superclass = (ClassProto) classPath.getClass(superclassType);
+                vtable.addAll(superclass.getVtable());
+
+                // if the superclass's vtable wasn't fully resolved, then we can't know where the new methods added by
+                // this class should start, so we just propagate what we can from the parent and hope for the best.
+                if (!superclass.vtableFullyResolved) {
+                    vtableFullyResolved = false;
+                    return vtable;
+                }
+            }
+
+            //iterate over the virtual methods in the current class, and only add them when we don't already have the
+            //method (i.e. if it was implemented by the superclass)
+            if (!isInterface()) {
+                addToVtable(getClassDef().getVirtualMethods(), vtable, true, true);
+
+                List<String> interfaces = Lists.newArrayList(getInterfaces().keySet());
+
+                List<Method> defaultMethods = Lists.newArrayList();
+                List<Method> defaultConflictMethods = Lists.newArrayList();
+                List<Method> mirandaMethods = Lists.newArrayList();
+
+                final HashMap<MethodReference, Integer> methodOrder = Maps.newHashMap();
+
+                for (int i=interfaces.size()-1; i>=0; i--) {
+                    String interfaceType = interfaces.get(i);
+                    ClassDef interfaceDef = classPath.getClassDef(interfaceType);
+
+                    for (Method interfaceMethod : interfaceDef.getVirtualMethods()) {
+
+                        int vtableIndex = findMethodIndexInVtableReverse(vtable, interfaceMethod);
+                        Method oldVtableMethod = null;
+                        if (vtableIndex >= 0) {
+                            oldVtableMethod = vtable.get(vtableIndex);
+                        }
+
+                        for (int j=0; j<vtable.size(); j++) {
+                            Method candidate = vtable.get(j);
+                            if (MethodUtil.methodSignaturesMatch(candidate, interfaceMethod)) {
                         if (!classPath.shouldCheckPackagePrivateAccess() ||
-                                AnalyzedMethodUtil.canAccess(ClassProto.this, superMethod, true, false, false)) {
-                            if (replaceExisting) {
-                                vtable.set(i, virtualMethod);
+                                        AnalyzedMethodUtil.canAccess(ClassProto.this, candidate, true, false, false)) {
+                                    if (interfaceMethodOverrides(interfaceMethod, candidate)) {
+                                        vtable.set(j, interfaceMethod);
                             }
-                            continue outer;
                         }
                     }
                 }
+
+                        if (vtableIndex >= 0) {
+                            if (!isOverridableByDefaultMethod(vtable.get(vtableIndex))) {
+                                continue;
+                            }
+                        }
+
+                        int defaultMethodIndex = findMethodIndexInVtable(defaultMethods, interfaceMethod);
+
+                        if (defaultMethodIndex >= 0) {
+                            if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) {
+                                ClassProto existingInterface = (ClassProto)classPath.getClass(
+                                        defaultMethods.get(defaultMethodIndex).getDefiningClass());
+                                if (!existingInterface.implementsInterface(interfaceMethod.getDefiningClass())) {
+                                    Method removedMethod = defaultMethods.remove(defaultMethodIndex);
+                                    defaultConflictMethods.add(removedMethod);
+                                }
+                            }
+                            continue;
+                        }
+
+                        int defaultConflictMethodIndex = findMethodIndexInVtable(
+                                defaultConflictMethods, interfaceMethod);
+                        if (defaultConflictMethodIndex >= 0) {
+                            // There's already a matching method in the conflict list, we don't need to do
+                            // anything else
+                            continue;
+                        }
+
+                        int mirandaMethodIndex = findMethodIndexInVtable(mirandaMethods, interfaceMethod);
+
+                        if (mirandaMethodIndex >= 0) {
+                            if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) {
+
+                                ClassProto existingInterface = (ClassProto)classPath.getClass(
+                                        mirandaMethods.get(mirandaMethodIndex).getDefiningClass());
+                                if (!existingInterface.implementsInterface(interfaceMethod.getDefiningClass())) {
+                                    Method oldMethod = mirandaMethods.remove(mirandaMethodIndex);
+                                    int methodOrderValue = methodOrder.get(oldMethod);
+                                    methodOrder.put(interfaceMethod, methodOrderValue);
+                                    defaultMethods.add(interfaceMethod);
+                                }
+                            }
+                            continue;
+                        }
+
+                        if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) {
+                            if (oldVtableMethod != null) {
+                                if (!interfaceMethodOverrides(interfaceMethod, oldVtableMethod)) {
+                                    continue;
+                                }
+                            }
+                            defaultMethods.add(interfaceMethod);
+                            methodOrder.put(interfaceMethod, methodOrder.size());
+                        } else {
+                            // TODO: do we need to check interfaceMethodOverrides here?
+                            if (oldVtableMethod == null) {
+                                mirandaMethods.add(interfaceMethod);
+                                methodOrder.put(interfaceMethod, methodOrder.size());
+                            }
+                        }
+                    }
+                }
+
+                Comparator<MethodReference> comparator = new Comparator<MethodReference>() {
+                    @Override public int compare(MethodReference o1, MethodReference o2) {
+                        return Ints.compare(methodOrder.get(o1), methodOrder.get(o2));
+                    }
+                };
+
+                // The methods should be in the same order within each list as they were iterated over.
+                // They can be misordered if, e.g. a method was originally added to the default list, but then moved
+                // to the conflict list.
+                Collections.sort(mirandaMethods, comparator);
+                Collections.sort(defaultMethods, comparator);
+                Collections.sort(defaultConflictMethods, comparator);
+
+                vtable.addAll(mirandaMethods);
+                vtable.addAll(defaultMethods);
+                vtable.addAll(defaultConflictMethods);
+            }
+            return vtable;
+        }
+    });
+
+    @Nonnull private final Supplier<List<Method>> postDefaultMethodVtableSupplier = Suppliers.memoize(new Supplier<List<Method>>() {
+        @Override public List<Method> get() {
+            List<Method> vtable = Lists.newArrayList();
+
+            //copy the virtual methods from the superclass
+            String superclassType;
+            try {
+                superclassType = getSuperclass();
+            } catch (UnresolvedClassException ex) {
+                vtable.addAll(((ClassProto)classPath.getClass("Ljava/lang/Object;")).getVtable());
+                vtableFullyResolved = false;
+                return vtable;
+            }
+
+            if (superclassType != null) {
+                ClassProto superclass = (ClassProto) classPath.getClass(superclassType);
+                vtable.addAll(superclass.getVtable());
+
+                // if the superclass's vtable wasn't fully resolved, then we can't know where the new methods added by
+                // this class should start, so we just propagate what we can from the parent and hope for the best.
+                if (!superclass.vtableFullyResolved) {
+                    vtableFullyResolved = false;
+                    return vtable;
+                }
+            }
+
+            //iterate over the virtual methods in the current class, and only add them when we don't already have the
+            //method (i.e. if it was implemented by the superclass)
+            if (!isInterface()) {
+                addToVtable(getClassDef().getVirtualMethods(), vtable, true, true);
+
+                Iterable<ClassDef> interfaces = Lists.reverse(Lists.newArrayList(getDirectInterfaces()));
+
+                List<Method> defaultMethods = Lists.newArrayList();
+                List<Method> defaultConflictMethods = Lists.newArrayList();
+                List<Method> mirandaMethods = Lists.newArrayList();
+
+                final HashMap<MethodReference, Integer> methodOrder = Maps.newHashMap();
+
+                for (ClassDef interfaceDef: interfaces) {
+                    for (Method interfaceMethod : interfaceDef.getVirtualMethods()) {
+
+                        int vtableIndex = findMethodIndexInVtable(vtable, interfaceMethod);
+
+                        if (vtableIndex >= 0) {
+                            if (interfaceMethodOverrides(interfaceMethod, vtable.get(vtableIndex))) {
+                                vtable.set(vtableIndex, interfaceMethod);
+                            }
+                        } else {
+                            int defaultMethodIndex = findMethodIndexInVtable(defaultMethods, interfaceMethod);
+
+                            if (defaultMethodIndex >= 0) {
+                                if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) {
+                                    ClassProto existingInterface = (ClassProto)classPath.getClass(
+                                            defaultMethods.get(defaultMethodIndex).getDefiningClass());
+                                    if (!existingInterface.implementsInterface(interfaceMethod.getDefiningClass())) {
+                                        Method removedMethod = defaultMethods.remove(defaultMethodIndex);
+                                        defaultConflictMethods.add(removedMethod);
+                                    }
+                                }
+                                continue;
+                            }
+
+                            int defaultConflictMethodIndex = findMethodIndexInVtable(
+                                    defaultConflictMethods, interfaceMethod);
+                            if (defaultConflictMethodIndex >= 0) {
+                                // There's already a matching method in the conflict list, we don't need to do
+                                // anything else
+                                continue;
+                            }
+
+                            int mirandaMethodIndex = findMethodIndexInVtable(mirandaMethods, interfaceMethod);
+
+                            if (mirandaMethodIndex >= 0) {
+                                if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) {
+
+                                    ClassProto existingInterface = (ClassProto)classPath.getClass(
+                                            mirandaMethods.get(mirandaMethodIndex).getDefiningClass());
+                                    if (!existingInterface.implementsInterface(interfaceMethod.getDefiningClass())) {
+                                        Method oldMethod = mirandaMethods.remove(mirandaMethodIndex);
+                                        int methodOrderValue = methodOrder.get(oldMethod);
+                                        methodOrder.put(interfaceMethod, methodOrderValue);
+                                        defaultMethods.add(interfaceMethod);
+                                    }
+                                }
+                                continue;
+                            }
+
+                            if (!AccessFlags.ABSTRACT.isSet(interfaceMethod.getAccessFlags())) {
+                                defaultMethods.add(interfaceMethod);
+                                methodOrder.put(interfaceMethod, methodOrder.size());
+                            } else {
+                                mirandaMethods.add(interfaceMethod);
+                                methodOrder.put(interfaceMethod, methodOrder.size());
+                            }
+                        }
+                    }
+                }
+
+                Comparator<MethodReference> comparator = new Comparator<MethodReference>() {
+                    @Override public int compare(MethodReference o1, MethodReference o2) {
+                        return Ints.compare(methodOrder.get(o1), methodOrder.get(o2));
+                    }
+                };
+
+                // The methods should be in the same order within each list as they were iterated over.
+                // They can be misordered if, e.g. a method was originally added to the default list, but then moved
+                // to the conflict list.
+                Collections.sort(defaultMethods, comparator);
+                Collections.sort(defaultConflictMethods, comparator);
+                Collections.sort(mirandaMethods, comparator);
+                addToVtable(defaultMethods, vtable, false, false);
+                addToVtable(defaultConflictMethods, vtable, false, false);
+                addToVtable(mirandaMethods, vtable, false, false);
+            }
+            return vtable;
+        }
+    });
+
+    private void addToVtable(@Nonnull Iterable<? extends Method> localMethods, @Nonnull List<Method> vtable,
+                             boolean replaceExisting, boolean sort) {
+        if (sort) {
+            ArrayList<Method> methods = Lists.newArrayList(localMethods);
+            Collections.sort(methods);
+            localMethods = methods;
+        }
+
+        for (Method virtualMethod: localMethods) {
+            int vtableIndex = findMethodIndexInVtable(vtable, virtualMethod);
+
+            if (vtableIndex >= 0) {
+                if (replaceExisting) {
+                    vtable.set(vtableIndex, virtualMethod);
+                }
+            } else {
                 // we didn't find an equivalent method, so add it as a new entry
                 vtable.add(virtualMethod);
             }
         }
-    });
+    }
 
     private static byte getFieldType(@Nonnull FieldReference field) {
         switch (field.getType().charAt(0)) {
@@ -870,4 +1225,68 @@
                 return 2; //OTHER
         }
     }
+
+    private boolean isOverridableByDefaultMethod(@Nonnull Method method) {
+        ClassProto classProto = (ClassProto)classPath.getClass(method.getDefiningClass());
+        return classProto.isInterface();
+    }
+
+    /**
+     * Checks if the interface method overrides the virtual or interface method2
+     * @param method A Method from an interface
+     * @param method2 A Method from an interface or a class
+     * @return true if the interface method overrides the virtual or interface method2
+     */
+    private boolean interfaceMethodOverrides(@Nonnull Method method, @Nonnull Method method2) {
+        ClassProto classProto = (ClassProto)classPath.getClass(method2.getDefiningClass());
+
+        if (classProto.isInterface()) {
+            ClassProto targetClassProto = (ClassProto)classPath.getClass(method.getDefiningClass());
+            return targetClassProto.implementsInterface(method2.getDefiningClass());
+        } else {
+            return false;
+        }
+    }
+
+    static class ReparentedMethod extends BaseMethodReference implements Method {
+        private final Method method;
+        private final String definingClass;
+
+        public ReparentedMethod(Method method, String definingClass) {
+            this.method = method;
+            this.definingClass = definingClass;
+        }
+
+        @Nonnull @Override public String getDefiningClass() {
+            return definingClass;
+        }
+
+        @Nonnull @Override public String getName() {
+            return method.getName();
+        }
+
+        @Nonnull @Override public List<? extends CharSequence> getParameterTypes() {
+            return method.getParameterTypes();
+        }
+
+        @Nonnull @Override public String getReturnType() {
+            return method.getReturnType();
+        }
+
+        @Nonnull @Override public List<? extends MethodParameter> getParameters() {
+            return method.getParameters();
+        }
+
+        @Override public int getAccessFlags() {
+            return method.getAccessFlags();
+        }
+
+        @Nonnull @Override public Set<? extends Annotation> getAnnotations() {
+            return method.getAnnotations();
+        }
+
+        @Nullable @Override public MethodImplementation getImplementation() {
+            return method.getImplementation();
+        }
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/DumpFields.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/DumpFields.java
deleted file mode 100644
index 2bb3e49..0000000
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/DumpFields.java
+++ /dev/null
@@ -1,180 +0,0 @@
-/*
- * Copyright 2013, Google Inc.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- *     * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *     * Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following disclaimer
- * in the documentation and/or other materials provided with the
- * distribution.
- *     * Neither the name of Google Inc. nor the names of its
- * contributors may be used to endorse or promote products derived from
- * this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.jf.dexlib2.analysis;
-
-import com.google.common.base.Splitter;
-import com.google.common.collect.Lists;
-import org.apache.commons.cli.*;
-import org.jf.dexlib2.DexFileFactory;
-import org.jf.dexlib2.dexbacked.DexBackedDexFile;
-import org.jf.dexlib2.iface.ClassDef;
-import org.jf.dexlib2.iface.Field;
-import org.jf.dexlib2.iface.Method;
-import org.jf.dexlib2.iface.reference.FieldReference;
-import org.jf.util.ConsoleUtil;
-import org.jf.util.SparseArray;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-
-public class DumpFields {
-    private static final Options options;
-
-    static {
-        options = new Options();
-        buildOptions();
-    }
-
-    public static void main(String[] args) {
-        CommandLineParser parser = new PosixParser();
-        CommandLine commandLine;
-
-        try {
-            commandLine = parser.parse(options, args);
-        } catch (ParseException ex) {
-            usage();
-            return;
-        }
-
-        String[] remainingArgs = commandLine.getArgs();
-
-        Option[] parsedOptions = commandLine.getOptions();
-        ArrayList<String> bootClassPathDirs = Lists.newArrayList();
-        String outFile = "fields.txt";
-        int apiLevel = 15;
-        boolean experimental = false;
-
-        for (int i=0; i<parsedOptions.length; i++) {
-            Option option = parsedOptions[i];
-            String opt = option.getOpt();
-
-            switch (opt.charAt(0)) {
-                case 'd':
-                    bootClassPathDirs.add(option.getValue());
-                    break;
-                case 'o':
-                    outFile = option.getValue();
-                    break;
-                case 'a':
-                    apiLevel = Integer.parseInt(commandLine.getOptionValue("a"));
-                    break;
-                case 'X':
-                    experimental = true;
-                    break;
-                default:
-                    assert false;
-            }
-        }
-
-        if (remainingArgs.length != 1) {
-            usage();
-            return;
-        }
-
-        String inputDexFileName = remainingArgs[0];
-
-        File dexFileFile = new File(inputDexFileName);
-        if (!dexFileFile.exists()) {
-            System.err.println("Can't find the file " + inputDexFileName);
-            System.exit(1);
-        }
-
-        try {
-            DexBackedDexFile dexFile = DexFileFactory.loadDexFile(dexFileFile, apiLevel, experimental);
-            Iterable<String> bootClassPaths = Splitter.on(":").split("core.jar:ext.jar:framework.jar:android.policy.jar:services.jar");
-            ClassPath classPath = ClassPath.fromClassPath(bootClassPathDirs, bootClassPaths, dexFile, apiLevel, experimental);
-            FileOutputStream outStream = new FileOutputStream(outFile);
-
-            for (ClassDef classDef: dexFile.getClasses()) {
-                ClassProto classProto = (ClassProto) classPath.getClass(classDef);
-                SparseArray<FieldReference> fields = classProto.getInstanceFields();
-                String className = "Class "  + classDef.getType() + " : " + fields.size() + " instance fields\n";
-                outStream.write(className.getBytes());
-                for (int i=0;i<fields.size();i++) {
-                    String field = fields.keyAt(i) + ":" + fields.valueAt(i).getType() + " " + fields.valueAt(i).getName() + "\n";
-                    outStream.write(field.getBytes());
-                }
-                outStream.write("\n".getBytes());
-            }
-            outStream.close();
-        } catch (IOException ex) {
-            System.out.println("IOException thrown when trying to open a dex file or write out vtables: " + ex);
-        }
-
-    }
-
-    /**
-     * Prints the usage message.
-     */
-    private static void usage() {
-        int consoleWidth = ConsoleUtil.getConsoleWidth();
-        if (consoleWidth <= 0) {
-            consoleWidth = 80;
-        }
-
-        System.out.println("java -cp baksmali.jar org.jf.dexlib2.analysis.DumpFields -d path/to/framework/jar/files <dex-file>");
-    }
-
-    private static void buildOptions() {
-        Option classPathDirOption = OptionBuilder.withLongOpt("bootclasspath-dir")
-                .withDescription("the base folder to look for the bootclasspath files in. Defaults to the current " +
-                        "directory")
-                .hasArg()
-                .withArgName("DIR")
-                .create("d");
-
-        Option outputFileOption = OptionBuilder.withLongOpt("out-file")
-                .withDescription("output file")
-                .hasArg()
-                .withArgName("FILE")
-                .create("o");
-
-        Option apiLevelOption = OptionBuilder.withLongOpt("api-level")
-                .withDescription("The numeric api-level of the file being disassembled. If not " +
-                                "specified, it defaults to 15 (ICS).")
-                .hasArg()
-                .withArgName("API_LEVEL")
-                .create("a");
-
-        Option experimentalOption = OptionBuilder.withLongOpt("experimental")
-                .withDescription("Enable dumping experimental opcodes, that aren't necessarily " +
-                                "supported by the android runtime yet.")
-                .create("X");
-
-        options.addOption(classPathDirOption);
-        options.addOption(outputFileOption);
-        options.addOption(apiLevelOption);
-        options.addOption(experimentalOption);
-    }
-}
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/DumpVtables.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/DumpVtables.java
deleted file mode 100644
index 193c0d3..0000000
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/DumpVtables.java
+++ /dev/null
@@ -1,184 +0,0 @@
-/*
- * Copyright 2013, Google Inc.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- *     * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *     * Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following disclaimer
- * in the documentation and/or other materials provided with the
- * distribution.
- *     * Neither the name of Google Inc. nor the names of its
- * contributors may be used to endorse or promote products derived from
- * this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.jf.dexlib2.analysis;
-
-import com.google.common.base.Splitter;
-import com.google.common.collect.Lists;
-import org.apache.commons.cli.*;
-import org.jf.dexlib2.DexFileFactory;
-import org.jf.dexlib2.dexbacked.DexBackedDexFile;
-import org.jf.dexlib2.iface.ClassDef;
-import org.jf.dexlib2.iface.Method;
-import org.jf.util.ConsoleUtil;
-
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.List;
-
-public class DumpVtables {
-    private static final Options options;
-
-    static {
-        options = new Options();
-        buildOptions();
-    }
-
-    public static void main(String[] args) {
-        CommandLineParser parser = new PosixParser();
-        CommandLine commandLine;
-
-        try {
-            commandLine = parser.parse(options, args);
-        } catch (ParseException ex) {
-            usage();
-            return;
-        }
-
-        String[] remainingArgs = commandLine.getArgs();
-
-        Option[] parsedOptions = commandLine.getOptions();
-        ArrayList<String> bootClassPathDirs = Lists.newArrayList();
-        String outFile = "vtables.txt";
-        int apiLevel = 15;
-        boolean experimental = false;
-
-        for (int i=0; i<parsedOptions.length; i++) {
-            Option option = parsedOptions[i];
-            String opt = option.getOpt();
-
-            switch (opt.charAt(0)) {
-                case 'd':
-                    bootClassPathDirs.add(option.getValue());
-                    break;
-                case 'o':
-                    outFile = option.getValue();
-                    break;
-                case 'a':
-                    apiLevel = Integer.parseInt(commandLine.getOptionValue("a"));
-                    break;
-                case 'X':
-                    experimental = true;
-                    break;
-                default:
-                    assert false;
-            }
-        }
-
-        if (remainingArgs.length != 1) {
-            usage();
-            return;
-        }
-
-        String inputDexFileName = remainingArgs[0];
-
-        File dexFileFile = new File(inputDexFileName);
-        if (!dexFileFile.exists()) {
-            System.err.println("Can't find the file " + inputDexFileName);
-            System.exit(1);
-        }
-
-        try {
-            DexBackedDexFile dexFile = DexFileFactory.loadDexFile(dexFileFile, apiLevel, experimental);
-            Iterable<String> bootClassPaths = Splitter.on(":").split("core.jar:ext.jar:framework.jar:android.policy.jar:services.jar");
-            ClassPath classPath = ClassPath.fromClassPath(bootClassPathDirs, bootClassPaths, dexFile, apiLevel, experimental);
-            FileOutputStream outStream = new FileOutputStream(outFile);
-
-            for (ClassDef classDef: dexFile.getClasses()) {
-                ClassProto classProto = (ClassProto) classPath.getClass(classDef);
-                List<Method> methods = classProto.getVtable();
-                String className = "Class "  + classDef.getType() + " extends " + classDef.getSuperclass() + " : " + methods.size() + " methods\n";
-                outStream.write(className.getBytes());
-                for (int i=0;i<methods.size();i++) {
-                    Method method = methods.get(i);
-
-                    String methodString = i + ":" + method.getDefiningClass() + "->" + method.getName() + "(";
-                    for (CharSequence parameter: method.getParameterTypes()) {
-                        methodString += parameter;
-                    }
-                    methodString += ")" + method.getReturnType() + "\n";
-                    outStream.write(methodString.getBytes());
-                }
-                outStream.write("\n".getBytes());
-            }
-            outStream.close();
-        } catch (IOException ex) {
-            System.out.println("IOException thrown when trying to open a dex file or write out vtables: " + ex);
-        }
-
-    }
-
-    /**
-     * Prints the usage message.
-     */
-    private static void usage() {
-        int consoleWidth = ConsoleUtil.getConsoleWidth();
-        if (consoleWidth <= 0) {
-            consoleWidth = 80;
-        }
-
-        System.out.println("java -cp baksmali.jar org.jf.dexlib2.analysis.DumpVtables -d path/to/framework/jar/files <dex-file>");
-    }
-
-    private static void buildOptions() {
-        Option classPathDirOption = OptionBuilder.withLongOpt("bootclasspath-dir")
-                .withDescription("the base folder to look for the bootclasspath files in. Defaults to the current " +
-                        "directory")
-                .hasArg()
-                .withArgName("DIR")
-                .create("d");
-
-        Option outputFileOption = OptionBuilder.withLongOpt("out-file")
-                .withDescription("output file")
-                .hasArg()
-                .withArgName("FILE")
-                .create("o");
-
-        Option apiLevelOption = OptionBuilder.withLongOpt("api-level")
-                .withDescription("The numeric api-level of the file being disassembled. If not " +
-                        "specified, it defaults to 15 (ICS).")
-                .hasArg()
-                .withArgName("API_LEVEL")
-                .create("a");
-
-        Option experimentalOption = OptionBuilder.withLongOpt("experimental")
-                .withDescription("Enable dumping experimental opcodes, that aren't necessarily " +
-                                "supported by the android runtime yet.")
-                .create("X");
-
-        options.addOption(classPathDirOption);
-        options.addOption(outputFileOption);
-        options.addOption(apiLevelOption);
-        options.addOption(experimentalOption);
-    }
-}
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/MethodAnalyzer.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/MethodAnalyzer.java
index b7a15a0..7a51c96 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/MethodAnalyzer.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/MethodAnalyzer.java
@@ -36,6 +36,7 @@
 import com.google.common.collect.Lists;
 import org.jf.dexlib2.AccessFlags;
 import org.jf.dexlib2.Opcode;
+import org.jf.dexlib2.base.reference.BaseMethodReference;
 import org.jf.dexlib2.iface.*;
 import org.jf.dexlib2.iface.instruction.*;
 import org.jf.dexlib2.iface.instruction.formats.*;
@@ -89,10 +90,10 @@
 
     @Nullable private AnalysisException analysisException = null;
 
-    //This is a dummy instruction that occurs immediately before the first real instruction. We can initialize the
-    //register types for this instruction to the parameter types, in order to have them propagate to all of its
-    //successors, e.g. the first real instruction, the first instructions in any exception handlers covering the first
-    //instruction, etc.
+    // This is a dummy instruction that occurs immediately before the first real instruction. We can initialize the
+    // register types for this instruction to the parameter types, in order to have them propagate to all of its
+    // successors, e.g. the first real instruction, the first instructions in any exception handlers covering the first
+    // instruction, etc.
     private final AnalyzedInstruction startOfMethod;
 
     public MethodAnalyzer(@Nonnull ClassPath classPath, @Nonnull Method method,
@@ -110,27 +111,16 @@
 
         this.methodImpl = methodImpl;
 
-        //override AnalyzedInstruction and provide custom implementations of some of the methods, so that we don't
-        //have to handle the case this special case of instruction being null, in the main class
-        startOfMethod = new AnalyzedInstruction(this, null, -1, methodImpl.getRegisterCount()) {
-            public boolean setsRegister() {
-                return false;
+        // Override AnalyzedInstruction and provide custom implementations of some of the methods, so that we don't
+        // have to handle the case this special case of instruction being null, in the main class
+        startOfMethod = new AnalyzedInstruction(this, new ImmutableInstruction10x(Opcode.NOP), -1, methodImpl.getRegisterCount()) {
+            @Override protected boolean addPredecessor(AnalyzedInstruction predecessor) {
+                throw new UnsupportedOperationException();
             }
 
-            @Override
-            public boolean setsWideRegister() {
-                return false;
-            }
-
-            @Override
-            public boolean setsRegister(int registerNumber) {
-                return false;
-            }
-
-            @Override
-            public int getDestinationRegister() {
-                assert false;
-                return -1;
+            @Override @Nonnull
+            public RegisterType getPredecessorRegisterType(@Nonnull AnalyzedInstruction predecessor, int registerNumber) {
+                throw new UnsupportedOperationException();
             }
         };
 
@@ -141,6 +131,7 @@
         analyze();
     }
 
+    @Nonnull
     public ClassPath getClassPath() {
         return classPath;
     }
@@ -362,6 +353,7 @@
     private void overridePredecessorRegisterTypeAndPropagateChanges(
             @Nonnull AnalyzedInstruction analyzedInstruction, @Nonnull AnalyzedInstruction predecessor,
             int registerNumber, @Nonnull RegisterType registerType) {
+
         BitSet changedInstructions = new BitSet(analyzedInstructions.size());
 
         if (!analyzedInstruction.overridePredecessorRegisterType(
@@ -383,6 +375,28 @@
         }
     }
 
+    private void initializeRefAndPropagateChanges(@Nonnull AnalyzedInstruction analyzedInstruction,
+                                                  int registerNumber, @Nonnull RegisterType registerType) {
+
+        BitSet changedInstructions = new BitSet(analyzedInstructions.size());
+
+        if (!analyzedInstruction.setPostRegisterType(registerNumber, registerType)) {
+            return;
+        }
+
+        propagateRegisterToSuccessors(analyzedInstruction, registerNumber, changedInstructions, false);
+
+        propagateChanges(changedInstructions, registerNumber, false);
+
+        if (registerType.category == RegisterType.LONG_LO) {
+            checkWidePair(registerNumber, analyzedInstruction);
+            setPostRegisterTypeAndPropagateChanges(analyzedInstruction, registerNumber+1, RegisterType.LONG_HI_TYPE);
+        } else if (registerType.category == RegisterType.DOUBLE_LO) {
+            checkWidePair(registerNumber, analyzedInstruction);
+            setPostRegisterTypeAndPropagateChanges(analyzedInstruction, registerNumber+1, RegisterType.DOUBLE_HI_TYPE);
+        }
+    }
+
     private void setPostRegisterTypeAndPropagateChanges(@Nonnull AnalyzedInstruction analyzedInstruction,
                                                         int registerNumber, @Nonnull RegisterType registerType) {
 
@@ -1176,32 +1190,46 @@
         setDestinationRegisterTypeAndPropagateChanges(analyzedInstruction, castRegisterType);
     }
 
-    static boolean canNarrowAfterInstanceOf(AnalyzedInstruction analyzedInstanceOfInstruction,
-                                            AnalyzedInstruction analyzedIfInstruction, ClassPath classPath) {
+    public static boolean isNotWideningConversion(RegisterType originalType, RegisterType newType) {
+        if (originalType.type == null || newType.type == null) {
+            return true;
+        }
+        if (originalType.type.isInterface()) {
+            return newType.type.implementsInterface(originalType.type.getType());
+        } else {
+            TypeProto commonSuperclass = newType.type.getCommonSuperclass(originalType.type);
+            if (commonSuperclass.getType().equals(originalType.type.getType())) {
+                return true;
+        }
+            if (commonSuperclass.getType().equals(newType.type.getType())) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    static boolean canPropagateTypeAfterInstanceOf(AnalyzedInstruction analyzedInstanceOfInstruction,
+                                                   AnalyzedInstruction analyzedIfInstruction, ClassPath classPath) {
+        if (!classPath.isArt()) {
+            return false;
+        }
+
         Instruction ifInstruction = analyzedIfInstruction.instruction;
-        assert analyzedIfInstruction.instruction != null;
         if (((Instruction21t)ifInstruction).getRegisterA() == analyzedInstanceOfInstruction.getDestinationRegister()) {
             Reference reference = ((Instruction22c)analyzedInstanceOfInstruction.getInstruction()).getReference();
             RegisterType registerType = RegisterType.getRegisterType(classPath, (TypeReference)reference);
 
-            if (registerType.type != null && !registerType.type.isInterface()) {
-                int objectRegister = ((TwoRegisterInstruction)analyzedInstanceOfInstruction.getInstruction())
-                        .getRegisterB();
+            try {
+                if (registerType.type != null && !registerType.type.isInterface()) {
+                    int objectRegister = ((TwoRegisterInstruction)analyzedInstanceOfInstruction.getInstruction())
+                            .getRegisterB();
 
-                RegisterType originalType = analyzedIfInstruction.getPreInstructionRegisterType(objectRegister);
+                    RegisterType originalType = analyzedIfInstruction.getPreInstructionRegisterType(objectRegister);
 
-                if (originalType.type != null) {
-                    // Only override if we're going from an interface to a class, or are going to a narrower class
-                    if (originalType.type.isInterface()) {
-                        return true;
-                    } else {
-                        TypeProto commonSuperclass = registerType.type.getCommonSuperclass(originalType.type);
-                        // only if it's a narrowing conversion
-                        if (commonSuperclass.getType().equals(originalType.type.getType())) {
-                            return true;
-                        }
-                    }
+                    return isNotWideningConversion(originalType, registerType);
                 }
+            } catch (UnresolvedClassException ex) {
+                return false;
             }
         }
         return false;
@@ -1210,21 +1238,16 @@
     /**
      * Art uses a peephole optimization for an if-eqz or if-nez that occur immediately after an instance-of. It will
      * narrow the type if possible, and then NOP out any corresponding check-cast instruction later on
-     *
-     * TODO: Is this still safe to do even for dalvik odexes? I think it should be..
      */
     private void analyzeIfEqzNez(@Nonnull AnalyzedInstruction analyzedInstruction) {
-        int instructionIndex = analyzedInstruction.getInstructionIndex();
-        if (instructionIndex > 0) {
-            AnalyzedInstruction prevAnalyzedInstruction = analyzedInstructions.valueAt(instructionIndex - 1);
-            if (prevAnalyzedInstruction.instruction != null &&
-                    prevAnalyzedInstruction.instruction.getOpcode() == Opcode.INSTANCE_OF) {
-                if (canNarrowAfterInstanceOf(prevAnalyzedInstruction, analyzedInstruction, classPath)) {
-                    // Propagate the original type to the failing branch, and the new type to the successful branch
-                    int narrowingRegister = ((Instruction22c)prevAnalyzedInstruction.instruction).getRegisterB();
-                    RegisterType originalType = analyzedInstruction.getPreInstructionRegisterType(narrowingRegister);
-                    RegisterType newType = RegisterType.getRegisterType(classPath,
-                            (TypeReference)((Instruction22c)prevAnalyzedInstruction.instruction).getReference());
+        if (classPath.isArt()) {
+            int instructionIndex = analyzedInstruction.getInstructionIndex();
+            if (instructionIndex > 0) {
+                if (analyzedInstruction.getPredecessorCount() != 1) {
+                    return;
+                }
+                AnalyzedInstruction prevAnalyzedInstruction = analyzedInstruction.getPredecessors().first();
+                if (prevAnalyzedInstruction.instruction.getOpcode() == Opcode.INSTANCE_OF) {
 
                     AnalyzedInstruction fallthroughInstruction = analyzedInstructions.valueAt(
                             analyzedInstruction.getInstructionIndex() + 1);
@@ -1233,16 +1256,25 @@
                             ((Instruction21t)analyzedInstruction.instruction).getCodeOffset();
                     AnalyzedInstruction branchInstruction = analyzedInstructions.get(nextAddress);
 
-                    if (analyzedInstruction.instruction.getOpcode() == Opcode.IF_EQZ) {
-                        overridePredecessorRegisterTypeAndPropagateChanges(fallthroughInstruction, analyzedInstruction,
-                                narrowingRegister, newType);
-                        overridePredecessorRegisterTypeAndPropagateChanges(branchInstruction, analyzedInstruction,
-                                narrowingRegister, originalType);
-                    } else {
-                        overridePredecessorRegisterTypeAndPropagateChanges(fallthroughInstruction, analyzedInstruction,
-                                narrowingRegister, originalType);
-                        overridePredecessorRegisterTypeAndPropagateChanges(branchInstruction, analyzedInstruction,
-                                narrowingRegister, newType);
+                    int narrowingRegister = ((Instruction22c)prevAnalyzedInstruction.instruction).getRegisterB();
+                    RegisterType originalType = analyzedInstruction.getPreInstructionRegisterType(narrowingRegister);
+
+                    Instruction22c instanceOfInstruction = (Instruction22c)prevAnalyzedInstruction.instruction;
+                    RegisterType newType = RegisterType.getRegisterType(classPath,
+                            (TypeReference)instanceOfInstruction.getReference());
+
+                    for (int register : analyzedInstruction.getSetRegisters()) {
+                        if (analyzedInstruction.instruction.getOpcode() == Opcode.IF_EQZ) {
+                            overridePredecessorRegisterTypeAndPropagateChanges(fallthroughInstruction,
+                                    analyzedInstruction, register, newType);
+                            overridePredecessorRegisterTypeAndPropagateChanges(branchInstruction, analyzedInstruction,
+                                    register, originalType);
+                        } else {
+                            overridePredecessorRegisterTypeAndPropagateChanges(fallthroughInstruction,
+                                    analyzedInstruction, register, originalType);
+                            overridePredecessorRegisterTypeAndPropagateChanges(branchInstruction, analyzedInstruction,
+                                    register, newType);
+                        }
                     }
                 }
             }
@@ -1380,44 +1412,32 @@
     }
 
     private void analyzeInvokeDirectCommon(@Nonnull AnalyzedInstruction analyzedInstruction, int objectRegister) {
-        //the only time that an invoke instruction changes a register type is when using invoke-direct on a
-        //constructor (<init>) method, which changes the uninitialized reference (and any register that the same
-        //uninit reference has been copied to) to an initialized reference
+        // This handles the case of invoking a constructor on an uninitialized reference. This propagates the
+        // initialized type for the object register, and also any known aliased registers.
+        //
+        // In some cases, unrelated uninitialized references may not have been propagated past this instruction. This
+        // happens when propagating those types and the type of object register of this instruction isn't known yet.
+        // In this case, we can't determine if the uninitialized reference being propagated in an alias of the object
+        // register, so we don't stop propagation.
+        //
+        // We check for any of these unpropagated uninitialized references here and propagate them.
+        if (analyzedInstruction.isInvokeInit()) {
+            RegisterType uninitRef = analyzedInstruction.getPreInstructionRegisterType(objectRegister);
+            if (uninitRef.category != RegisterType.UNINIT_REF && uninitRef.category != RegisterType.UNINIT_THIS) {
+                assert analyzedInstruction.getSetRegisters().isEmpty();
+                return;
+            }
 
-        ReferenceInstruction instruction = (ReferenceInstruction)analyzedInstruction.instruction;
+            RegisterType initRef = RegisterType.getRegisterType(RegisterType.REFERENCE, uninitRef.type);
 
-        MethodReference methodReference = (MethodReference)instruction.getReference();
+            for (int register: analyzedInstruction.getSetRegisters()) {
+                RegisterType registerType = analyzedInstruction.getPreInstructionRegisterType(register);
 
-        if (!methodReference.getName().equals("<init>")) {
-            return;
-        }
-
-        RegisterType objectRegisterType = analyzedInstruction.getPreInstructionRegisterType(objectRegister);
-
-        if (objectRegisterType.category != RegisterType.UNINIT_REF &&
-                objectRegisterType.category != RegisterType.UNINIT_THIS) {
-            return;
-        }
-
-        setPostRegisterTypeAndPropagateChanges(analyzedInstruction, objectRegister,
-                RegisterType.getRegisterType(RegisterType.REFERENCE, objectRegisterType.type));
-
-        for (int i=0; i<analyzedInstruction.postRegisterMap.length; i++) {
-            RegisterType postInstructionRegisterType = analyzedInstruction.postRegisterMap[i];
-            if (postInstructionRegisterType.category == RegisterType.UNKNOWN) {
-                RegisterType preInstructionRegisterType =
-                        analyzedInstruction.getPreInstructionRegisterType(i);
-
-                if (preInstructionRegisterType.category == RegisterType.UNINIT_REF ||
-                        preInstructionRegisterType.category == RegisterType.UNINIT_THIS) {
-                    RegisterType registerType;
-                    if (preInstructionRegisterType.equals(objectRegisterType)) {
-                        registerType = analyzedInstruction.postRegisterMap[objectRegister];
-                    } else {
-                        registerType = preInstructionRegisterType;
-                    }
-
-                    setPostRegisterTypeAndPropagateChanges(analyzedInstruction, i, registerType);
+                if (registerType == uninitRef) {
+                    setPostRegisterTypeAndPropagateChanges(analyzedInstruction, register, initRef);
+                } else {
+                    // This is unrelated uninitialized reference. propagate it as-is
+                    setPostRegisterTypeAndPropagateChanges(analyzedInstruction, register, registerType);
                 }
             }
         }
@@ -1695,13 +1715,13 @@
 
             // fieldClass is now the first accessible class found. Now. we need to make sure that the field is
             // actually valid for this class
-            resolvedField = classPath.getClass(fieldClass.getType()).getFieldByOffset(fieldOffset);
-            if (resolvedField == null) {
+            FieldReference newResolvedField = classPath.getClass(fieldClass.getType()).getFieldByOffset(fieldOffset);
+            if (newResolvedField == null) {
                 throw new ExceptionWithContext("Couldn't find accessible class while resolving field %s",
                         ReferenceUtil.getShortFieldDescriptor(resolvedField));
             }
-            resolvedField = new ImmutableFieldReference(fieldClass.getType(), resolvedField.getName(),
-                    resolvedField.getType());
+            resolvedField = new ImmutableFieldReference(fieldClass.getType(), newResolvedField.getName(),
+                    newResolvedField.getType());
         }
 
         String fieldType = resolvedField.getType();
@@ -1733,41 +1753,9 @@
             targetMethod = (MethodReference)instruction.getReference();
         }
 
-        TypeProto typeProto = classPath.getClass(targetMethod.getDefiningClass());
-        int methodIndex;
-        try {
-            methodIndex = typeProto.findMethodIndexInVtable(targetMethod);
-        } catch (UnresolvedClassException ex) {
-            return true;
-        }
+        MethodReference replacementMethod = normalizeMethodReference(targetMethod);
 
-        if (methodIndex < 0) {
-            return true;
-        }
-
-        Method replacementMethod = typeProto.getMethodByVtableIndex(methodIndex);
-        assert replacementMethod != null;
-        while (true) {
-            String superType = typeProto.getSuperclass();
-            if (superType == null) {
-                break;
-            }
-            typeProto = classPath.getClass(superType);
-            Method resolvedMethod = typeProto.getMethodByVtableIndex(methodIndex);
-            if (resolvedMethod == null) {
-                break;
-            }
-
-            if (!resolvedMethod.equals(replacementMethod)) {
-                if (!AnalyzedMethodUtil.canAccess(typeProto, replacementMethod, true, true, true)) {
-                    continue;
-                }
-
-                replacementMethod = resolvedMethod;
-            }
-        }
-
-        if (replacementMethod.equals(method)) {
+        if (replacementMethod == null || replacementMethod.equals(targetMethod)) {
             return true;
         }
 
@@ -1839,7 +1827,9 @@
         // no need to check class access for invoke-super. A class can obviously access its superclass.
         ClassDef thisClass = classPath.getClassDef(method.getDefiningClass());
 
-        if (!isSuper && !TypeUtils.canAccessClass(
+        if (classPath.getClass(resolvedMethod.getDefiningClass()).isInterface()) {
+            resolvedMethod = new ReparentedMethodReference(resolvedMethod, objectRegisterTypeProto.getType());
+        } else if (!isSuper && !TypeUtils.canAccessClass(
                 thisClass.getType(), classPath.getClassDef(resolvedMethod.getDefiningClass()))) {
 
             // the class is not accessible. So we start looking at objectRegisterTypeProto (which may be different
@@ -1860,13 +1850,20 @@
             MethodReference newResolvedMethod =
                     classPath.getClass(methodClass.getType()).getMethodByVtableIndex(methodIndex);
             if (newResolvedMethod == null) {
-                // TODO: fix NPE here
                 throw new ExceptionWithContext("Couldn't find accessible class while resolving method %s",
                         ReferenceUtil.getMethodDescriptor(resolvedMethod, true));
             }
             resolvedMethod = newResolvedMethod;
             resolvedMethod = new ImmutableMethodReference(methodClass.getType(), resolvedMethod.getName(),
                     resolvedMethod.getParameterTypes(), resolvedMethod.getReturnType());
+
+        }
+
+        if (normalizeVirtualMethods) {
+            MethodReference replacementMethod = normalizeMethodReference(resolvedMethod);
+            if (replacementMethod != null) {
+                resolvedMethod = replacementMethod;
+            }
         }
 
         Instruction deodexedInstruction;
@@ -1967,4 +1964,70 @@
                     "pair because it is the last register.", registerNumber));
         }
     }
+
+    @Nullable
+    private MethodReference normalizeMethodReference(@Nonnull MethodReference methodRef) {
+        TypeProto typeProto = classPath.getClass(methodRef.getDefiningClass());
+        int methodIndex;
+        try {
+            methodIndex = typeProto.findMethodIndexInVtable(methodRef);
+        } catch (UnresolvedClassException ex) {
+            return null;
+        }
+
+        if (methodIndex < 0) {
+            return null;
+        }
+
+        ClassProto thisClass = (ClassProto)classPath.getClass(method.getDefiningClass());
+
+        Method replacementMethod = typeProto.getMethodByVtableIndex(methodIndex);
+        assert replacementMethod != null;
+        while (true) {
+            String superType = typeProto.getSuperclass();
+            if (superType == null) {
+                break;
+            }
+            typeProto = classPath.getClass(superType);
+            Method resolvedMethod = typeProto.getMethodByVtableIndex(methodIndex);
+            if (resolvedMethod == null) {
+                break;
+            }
+
+            if (!resolvedMethod.equals(replacementMethod)) {
+                if (!AnalyzedMethodUtil.canAccess(thisClass, resolvedMethod, false, false, true)) {
+                    continue;
+                }
+
+                replacementMethod = resolvedMethod;
+            }
+        }
+        return replacementMethod;
+    }
+
+    private static class ReparentedMethodReference extends BaseMethodReference {
+        private final MethodReference baseReference;
+        private final String definingClass;
+
+        public ReparentedMethodReference(MethodReference baseReference, String definingClass) {
+            this.baseReference = baseReference;
+            this.definingClass = definingClass;
+        }
+
+        @Override @Nonnull public String getName() {
+            return baseReference.getName();
+        }
+
+        @Override @Nonnull public List<? extends CharSequence> getParameterTypes() {
+            return baseReference.getParameterTypes();
+        }
+
+        @Override @Nonnull public String getReturnType() {
+            return baseReference.getReturnType();
+        }
+
+        @Nonnull @Override public String getDefiningClass() {
+            return definingClass;
+        }
+    }
 }
\ No newline at end of file
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/RegisterType.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/RegisterType.java
index ba782fe..75478ca 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/RegisterType.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/RegisterType.java
@@ -235,7 +235,7 @@
             case '[':
                 return getRegisterType(REFERENCE, classPath.getClass(type));
             default:
-                throw new ExceptionWithContext("Invalid type: " + type);
+                throw new AnalysisException("Invalid type: " + type);
         }
     }
 
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/analysis/reflection/util/ReflectionUtils.java b/dexlib2/src/main/java/org/jf/dexlib2/analysis/reflection/util/ReflectionUtils.java
index 4a4615a..029ddb9 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/analysis/reflection/util/ReflectionUtils.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/analysis/reflection/util/ReflectionUtils.java
@@ -31,12 +31,43 @@
 
 package org.jf.dexlib2.analysis.reflection.util;
 
+import com.google.common.collect.ImmutableBiMap;
+
 public class ReflectionUtils {
+
+    private static ImmutableBiMap<String, String> primitiveMap = ImmutableBiMap.<String, String>builder()
+            .put("boolean", "Z")
+            .put("int", "I")
+            .put("long", "J")
+            .put("double", "D")
+            .put("void", "V")
+            .put("float", "F")
+            .put("char", "C")
+            .put("short", "S")
+            .put("byte", "B")
+            .build();
+
     public static String javaToDexName(String javaName) {
-        javaName = javaName.replace('.', '/');
-        if (javaName.length() > 1 && javaName.charAt(javaName.length()-1) != ';') {
-            javaName = 'L' + javaName + ';';
+        if (javaName.charAt(0) == '[') {
+            return javaName.replace('.', '/');
         }
-        return javaName;
+
+        if (primitiveMap.containsKey(javaName)) {
+            return primitiveMap.get(javaName);
+        }
+
+        return 'L' + javaName.replace('.', '/') + ';';
+    }
+
+    public static String dexToJavaName(String dexName) {
+        if (dexName.charAt(0) == '[') {
+            return dexName.replace('/', '.');
+        }
+
+        if (primitiveMap.inverse().containsKey(dexName)) {
+            return primitiveMap.inverse().get(dexName);
+        }
+
+        return dexName.replace('/', '.').substring(1, dexName.length()-2);
     }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseFieldReference.java b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseFieldReference.java
index f056f24..862e342 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseFieldReference.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseFieldReference.java
@@ -32,6 +32,7 @@
 package org.jf.dexlib2.base.reference;
 
 import org.jf.dexlib2.iface.reference.FieldReference;
+import org.jf.dexlib2.util.ReferenceUtil;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
@@ -64,4 +65,8 @@
         if (res != 0) return res;
         return getType().compareTo(o.getType());
     }
+
+    @Override public String toString() {
+        return ReferenceUtil.getFieldDescriptor(this);
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodProtoReference.java b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodProtoReference.java
index c0d38b0..2fc5ed1 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodProtoReference.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodProtoReference.java
@@ -33,6 +33,7 @@
 
 import com.google.common.collect.Ordering;
 import org.jf.dexlib2.iface.reference.MethodProtoReference;
+import org.jf.dexlib2.util.ReferenceUtil;
 import org.jf.util.CharSequenceUtils;
 import org.jf.util.CollectionUtils;
 
@@ -63,4 +64,8 @@
         if (res != 0) return res;
         return CollectionUtils.compareAsIterable(Ordering.usingToString(), getParameterTypes(), o.getParameterTypes());
     }
+
+    @Override public String toString() {
+        return ReferenceUtil.getMethodProtoDescriptor(this);
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodReference.java b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodReference.java
index 3ff6f7d..f297760 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodReference.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseMethodReference.java
@@ -33,6 +33,7 @@
 
 import com.google.common.collect.Ordering;
 import org.jf.dexlib2.iface.reference.MethodReference;
+import org.jf.dexlib2.util.ReferenceUtil;
 import org.jf.util.CharSequenceUtils;
 import org.jf.util.CollectionUtils;
 
@@ -70,4 +71,8 @@
         if (res != 0) return res;
         return CollectionUtils.compareAsIterable(Ordering.usingToString(), getParameterTypes(), o.getParameterTypes());
     }
+
+    @Override public String toString() {
+        return ReferenceUtil.getMethodDescriptor(this);
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseStringReference.java b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseStringReference.java
index c6daa91..2f13c1a 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseStringReference.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/base/reference/BaseStringReference.java
@@ -58,5 +58,5 @@
     @Override public int length() { return getString().length(); }
     @Override public char charAt(int index) { return getString().charAt(index); }
     @Override public CharSequence subSequence(int start, int end) { return getString().subSequence(start, end); }
-    @Override public String toString() { return getString(); }
+    @Override @Nonnull public String toString() { return getString(); }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/builder/BuilderOffsetInstruction.java b/dexlib2/src/main/java/org/jf/dexlib2/builder/BuilderOffsetInstruction.java
index d75d7b6..27e43d5 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/builder/BuilderOffsetInstruction.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/builder/BuilderOffsetInstruction.java
@@ -33,6 +33,7 @@
 
 import org.jf.dexlib2.Opcode;
 import org.jf.dexlib2.iface.instruction.OffsetInstruction;
+import org.jf.util.ExceptionWithContext;
 
 import javax.annotation.Nonnull;
 
@@ -48,9 +49,16 @@
 
     @Override public int getCodeOffset() {
         int codeOffset = internalGetCodeOffset();
-        if ((this.getCodeUnits() == 1 && (codeOffset < Byte.MIN_VALUE || codeOffset > Byte.MAX_VALUE)) ||
-            (this.getCodeUnits() == 2 && (codeOffset < Short.MIN_VALUE || codeOffset > Short.MAX_VALUE))) {
-            throw new IllegalStateException("Target is out of range");
+        if (this.getCodeUnits() == 1) {
+            if (codeOffset < Byte.MIN_VALUE || codeOffset > Byte.MAX_VALUE) {
+                throw new ExceptionWithContext("Invalid instruction offset: %d. " +
+                        "Offset must be in [-128, 127]", codeOffset);
+            }
+        } else if (this.getCodeUnits() == 2) {
+            if (codeOffset < Short.MIN_VALUE || codeOffset > Short.MAX_VALUE) {
+                throw new ExceptionWithContext("Invalid instruction offset: %d. " +
+                        "Offset must be in [-32768, 32767]", codeOffset);
+            }
         }
         return codeOffset;
     }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java
index 32505ee..fe260c5 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedDexFile.java
@@ -33,16 +33,24 @@
 
 import com.google.common.io.ByteStreams;
 import org.jf.dexlib2.Opcodes;
+import org.jf.dexlib2.ReferenceType;
 import org.jf.dexlib2.dexbacked.raw.*;
+import org.jf.dexlib2.dexbacked.reference.DexBackedFieldReference;
+import org.jf.dexlib2.dexbacked.reference.DexBackedMethodReference;
+import org.jf.dexlib2.dexbacked.reference.DexBackedStringReference;
+import org.jf.dexlib2.dexbacked.reference.DexBackedTypeReference;
 import org.jf.dexlib2.dexbacked.util.FixedSizeSet;
 import org.jf.dexlib2.iface.DexFile;
+import org.jf.dexlib2.iface.reference.Reference;
+import org.jf.dexlib2.util.DexUtil;
 import org.jf.util.ExceptionWithContext;
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
-import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
+import java.util.AbstractList;
+import java.util.List;
 import java.util.Set;
 
 public class DexBackedDexFile extends BaseDexBuffer implements DexFile {
@@ -61,13 +69,13 @@
     private final int classCount;
     private final int classStartOffset;
 
-    private DexBackedDexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] buf, int offset, boolean verifyMagic) {
+    protected DexBackedDexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] buf, int offset, boolean verifyMagic) {
         super(buf, offset);
 
         this.opcodes = opcodes;
 
         if (verifyMagic) {
-            verifyMagicAndByteOrder(buf, offset);
+            DexUtil.verifyDexHeader(buf, offset);
         }
 
         stringCount = readSmallUint(HeaderItem.STRING_COUNT_OFFSET);
@@ -85,7 +93,7 @@
     }
 
     public DexBackedDexFile(@Nonnull Opcodes opcodes, @Nonnull BaseDexBuffer buf) {
-        this(opcodes, buf.buf);
+        this(opcodes, buf.buf, buf.baseOffset);
     }
 
     public DexBackedDexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] buf, int offset) {
@@ -96,22 +104,10 @@
         this(opcodes, buf, 0, true);
     }
 
+    @Nonnull
     public static DexBackedDexFile fromInputStream(@Nonnull Opcodes opcodes, @Nonnull InputStream is)
             throws IOException {
-        if (!is.markSupported()) {
-            throw new IllegalArgumentException("InputStream must support mark");
-        }
-        is.mark(44);
-        byte[] partialHeader = new byte[44];
-        try {
-            ByteStreams.readFully(is, partialHeader);
-        } catch (EOFException ex) {
-            throw new NotADexFile("File is too short");
-        } finally {
-            is.reset();
-        }
-
-        verifyMagicAndByteOrder(partialHeader, 0);
+        DexUtil.verifyDexHeader(is);
 
         byte[] buf = ByteStreams.toByteArray(is);
         return new DexBackedDexFile(opcodes, buf, 0, false);
@@ -148,25 +144,6 @@
         };
     }
 
-    private static void verifyMagicAndByteOrder(@Nonnull byte[] buf, int offset) {
-        if (!HeaderItem.verifyMagic(buf, offset)) {
-            StringBuilder sb = new StringBuilder("Invalid magic value:");
-            for (int i=0; i<8; i++) {
-                sb.append(String.format(" %02x", buf[i]));
-            }
-            throw new NotADexFile(sb.toString());
-        }
-
-        int endian = HeaderItem.getEndian(buf, offset);
-        if (endian == HeaderItem.BIG_ENDIAN_TAG) {
-            throw new ExceptionWithContext("Big endian dex files are not currently supported");
-        }
-
-        if (endian != HeaderItem.LITTLE_ENDIAN_TAG) {
-            throw new ExceptionWithContext("Invalid endian tag: 0x%x", endian);
-        }
-    }
-
     public int getStringIdItemOffset(int stringIndex) {
         if (stringIndex < 0 || stringIndex >= stringCount) {
             throw new InvalidItemIndex(stringIndex, "String index out of bounds: %d", stringIndex);
@@ -265,6 +242,81 @@
         return getType(typeIndex);
     }
 
+    public List<DexBackedStringReference> getStrings() {
+        return new AbstractList<DexBackedStringReference>() {
+            @Override public DexBackedStringReference get(int index) {
+                if (index < 0 || index >= getStringCount()) {
+                    throw new IndexOutOfBoundsException();
+                }
+                return new DexBackedStringReference(DexBackedDexFile.this, index);
+            }
+
+            @Override public int size() {
+                return getStringCount();
+            }
+        };
+    }
+
+    public List<DexBackedTypeReference> getTypes() {
+        return new AbstractList<DexBackedTypeReference>() {
+            @Override public DexBackedTypeReference get(int index) {
+                if (index < 0 || index >= getTypeCount()) {
+                    throw new IndexOutOfBoundsException();
+                }
+                return new DexBackedTypeReference(DexBackedDexFile.this, index);
+            }
+
+            @Override public int size() {
+                return getTypeCount();
+            }
+        };
+    }
+
+    public List<DexBackedMethodReference> getMethods() {
+        return new AbstractList<DexBackedMethodReference>() {
+            @Override public DexBackedMethodReference get(int index) {
+                if (index < 0 || index >= getMethodCount()) {
+                    throw new IndexOutOfBoundsException();
+                }
+                return new DexBackedMethodReference(DexBackedDexFile.this, index);
+            }
+
+            @Override public int size() {
+                return getMethodCount();
+            }
+        };
+    }
+
+    public List<DexBackedFieldReference> getFields() {
+        return new AbstractList<DexBackedFieldReference>() {
+            @Override public DexBackedFieldReference get(int index) {
+                if (index < 0 || index >= getFieldCount()) {
+                    throw new IndexOutOfBoundsException();
+                }
+                return new DexBackedFieldReference(DexBackedDexFile.this, index);
+            }
+
+            @Override public int size() {
+                return getFieldCount();
+            }
+        };
+    }
+
+    public List<? extends Reference> getReferences(int referenceType) {
+        switch (referenceType) {
+            case ReferenceType.STRING:
+                return getStrings();
+            case ReferenceType.TYPE:
+                return getTypes();
+            case ReferenceType.METHOD:
+                return getMethods();
+            case ReferenceType.FIELD:
+                return getFields();
+            default:
+                throw new IllegalArgumentException(String.format("Invalid reference type: %d", referenceType));
+        }
+    }
+
     @Override
     @Nonnull
     public DexReader readerAt(int offset) {
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedMethodImplementation.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedMethodImplementation.java
index 676d86c..a82032a 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedMethodImplementation.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedMethodImplementation.java
@@ -84,7 +84,7 @@
                         // Does the instruction extend past the end of the method?
                         int offset = reader.getOffset();
                         if (offset > endOffset || offset < 0) {
-                            throw new ExceptionWithContext("The last instruction in the method is truncated");
+                            throw new ExceptionWithContext("The last instruction in method %s is truncated", method);
                         }
                         return instruction;
                     }
@@ -129,7 +129,11 @@
             return DebugInfo.newOrEmpty(dexFile, 0, this);
         }
         if (debugOffset < 0) {
-            System.err.println("%s: Invalid debug offset");
+            System.err.println(String.format("%s: Invalid debug offset", method));
+            return DebugInfo.newOrEmpty(dexFile, 0, this);
+        }
+        if (debugOffset >= dexFile.buf.length) {
+            System.err.println(String.format("%s: Invalid debug offset", method));
             return DebugInfo.newOrEmpty(dexFile, 0, this);
         }
         return DebugInfo.newOrEmpty(dexFile, debugOffset, this);
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedOdexFile.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedOdexFile.java
index 12f19db..379ecaa 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedOdexFile.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/DexBackedOdexFile.java
@@ -35,9 +35,9 @@
 import org.jf.dexlib2.Opcodes;
 import org.jf.dexlib2.dexbacked.raw.OdexHeaderItem;
 import org.jf.dexlib2.dexbacked.util.VariableSizeList;
+import org.jf.dexlib2.util.DexUtil;
 
 import javax.annotation.Nonnull;
-import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.UnsupportedEncodingException;
@@ -49,7 +49,6 @@
 
     private final byte[] odexBuf;
 
-
     public DexBackedOdexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] odexBuf, byte[] dexBuf) {
         super(opcodes, dexBuf);
 
@@ -64,7 +63,7 @@
         return true;
     }
 
-    public List<String> getDependencies() {
+    @Nonnull public List<String> getDependencies() {
         final int dexOffset = OdexHeaderItem.getDexOffset(odexBuf);
         final int dependencyOffset = OdexHeaderItem.getDependenciesOffset(odexBuf) - dexOffset;
 
@@ -85,22 +84,9 @@
         };
     }
 
-    public static DexBackedOdexFile fromInputStream(@Nonnull Opcodes opcodes, @Nonnull InputStream is)
+    @Nonnull public static DexBackedOdexFile fromInputStream(@Nonnull Opcodes opcodes, @Nonnull InputStream is)
             throws IOException {
-        if (!is.markSupported()) {
-            throw new IllegalArgumentException("InputStream must support mark");
-        }
-        is.mark(8);
-        byte[] partialHeader = new byte[8];
-        try {
-            ByteStreams.readFully(is, partialHeader);
-        } catch (EOFException ex) {
-            throw new NotADexFile("File is too short");
-        } finally {
-            is.reset();
-        }
-
-        verifyMagic(partialHeader);
+        DexUtil.verifyOdexHeader(is);
 
         is.reset();
         byte[] odexBuf = new byte[OdexHeaderItem.ITEM_SIZE];
@@ -115,18 +101,8 @@
         return new DexBackedOdexFile(opcodes, odexBuf, dexBuf);
     }
 
-    private static void verifyMagic(byte[] buf) {
-        if (!OdexHeaderItem.verifyMagic(buf)) {
-            StringBuilder sb = new StringBuilder("Invalid magic value:");
-            for (int i=0; i<8; i++) {
-                sb.append(String.format(" %02x", buf[i]));
-            }
-            throw new NotAnOdexFile(sb.toString());
-        }
-    }
-
     public int getOdexVersion() {
-        return OdexHeaderItem.getVersion(odexBuf);
+        return OdexHeaderItem.getVersion(odexBuf, 0);
     }
 
     public static class NotAnOdexFile extends RuntimeException {
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java
index dbeb67c..aaf942e 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/OatFile.java
@@ -31,22 +31,29 @@
 
 package org.jf.dexlib2.dexbacked;
 
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Iterators;
 import com.google.common.io.ByteStreams;
 import org.jf.dexlib2.Opcodes;
+import org.jf.dexlib2.dexbacked.OatFile.OatDexFile;
 import org.jf.dexlib2.dexbacked.OatFile.SymbolTable.Symbol;
 import org.jf.dexlib2.dexbacked.raw.HeaderItem;
+import org.jf.dexlib2.iface.MultiDexContainer;
 import org.jf.util.AbstractForwardSequentialList;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 import java.io.EOFException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.Charset;
 import java.util.AbstractList;
+import java.util.Arrays;
 import java.util.Iterator;
 import java.util.List;
 
-public class OatFile extends BaseDexBuffer {
+public class OatFile extends BaseDexBuffer implements MultiDexContainer<OatDexFile> {
     private static final byte[] ELF_MAGIC = new byte[] { 0x7f, 'E', 'L', 'F' };
     private static final byte[] OAT_MAGIC = new byte[] { 'o', 'a', 't', '\n' };
     private static final int MIN_ELF_HEADER_SIZE = 52;
@@ -54,7 +61,7 @@
     // These are the "known working" versions that I have manually inspected the source for.
     // Later version may or may not work, depending on what changed.
     private static final int MIN_OAT_VERSION = 56;
-    private static final int MAX_OAT_VERSION = 71;
+    private static final int MAX_OAT_VERSION = 86;
 
     public static final int UNSUPPORTED = 0;
     public static final int SUPPORTED = 1;
@@ -63,8 +70,13 @@
     private final boolean is64bit;
     @Nonnull private final OatHeader oatHeader;
     @Nonnull private final Opcodes opcodes;
+    @Nullable private final VdexProvider vdexProvider;
 
     public OatFile(@Nonnull byte[] buf) {
+        this(buf, null);
+    }
+
+    public OatFile(@Nonnull byte[] buf, @Nullable VdexProvider vdexProvider) {
         super(buf);
 
         if (buf.length < MIN_ELF_HEADER_SIZE) {
@@ -100,6 +112,7 @@
         }
 
         this.opcodes = Opcodes.forArtVersion(oatHeader.getVersion());
+        this.vdexProvider = vdexProvider;
     }
 
     private static void verifyMagic(byte[] buf) {
@@ -110,7 +123,11 @@
         }
     }
 
-    public static OatFile fromInputStream(@Nonnull InputStream is)
+    public static OatFile fromInputStream(@Nonnull InputStream is) throws IOException {
+        return fromInputStream(is, null);
+    }
+
+    public static OatFile fromInputStream(@Nonnull InputStream is, @Nullable VdexProvider vdexProvider)
             throws IOException {
         if (!is.markSupported()) {
             throw new IllegalArgumentException("InputStream must support mark");
@@ -130,7 +147,7 @@
         is.reset();
 
         byte[] buf = ByteStreams.toByteArray(is);
-        return new OatFile(buf);
+        return new OatFile(buf, vdexProvider);
     }
 
     public int getOatVersion() {
@@ -149,6 +166,22 @@
     }
 
     @Nonnull
+    public List<String> getBootClassPath() {
+        if (getOatVersion() < 75) {
+            return ImmutableList.of();
+        }
+        String bcp = oatHeader.getKeyValue("bootclasspath");
+        if (bcp == null) {
+            return ImmutableList.of();
+        }
+        return Arrays.asList(bcp.split(":"));
+    }
+
+    @Nonnull @Override public Opcodes getOpcodes() {
+        return opcodes;
+    }
+
+    @Nonnull
     public List<OatDexFile> getDexFiles() {
         return new AbstractForwardSequentialList<OatDexFile>() {
             @Override public int size() {
@@ -156,53 +189,57 @@
             }
 
             @Nonnull @Override public Iterator<OatDexFile> iterator() {
-                return new Iterator<OatDexFile>() {
-                    int index = 0;
-                    int offset = oatHeader.getDexListStart();
-
-                    @Override public boolean hasNext() {
-                        return index < size();
+                return Iterators.transform(new DexEntryIterator(), new Function<DexEntry, OatDexFile>() {
+                    @Nullable @Override public OatDexFile apply(DexEntry dexEntry) {
+                        return dexEntry.getDexFile();
                     }
-
-                    @Override public OatDexFile next() {
-                        int filenameLength = readSmallUint(offset);
-                        offset += 4;
-
-                        // TODO: what is the correct character encoding?
-                        String filename = new String(buf, offset, filenameLength, Charset.forName("US-ASCII"));
-                        offset += filenameLength;
-
-                        offset += 4; // checksum
-
-                        int dexOffset = readSmallUint(offset) + oatHeader.offset;
-                        offset += 4;
-
-                        int classCount = readSmallUint(dexOffset + HeaderItem.CLASS_COUNT_OFFSET);
-                        offset += 4 * classCount;
-
-                        index++;
-
-                        return new OatDexFile(dexOffset, filename);
-                    }
-
-                    @Override public void remove() {
-                        throw new UnsupportedOperationException();
-                    }
-                };
+                });
             }
         };
     }
 
-    public class OatDexFile extends DexBackedDexFile {
+    @Nonnull @Override public List<String> getDexEntryNames() throws IOException {
+        return new AbstractForwardSequentialList<String>() {
+            @Override public int size() {
+                return oatHeader.getDexFileCount();
+            }
+
+            @Nonnull @Override public Iterator<String> iterator() {
+                return Iterators.transform(new DexEntryIterator(), new Function<DexEntry, String>() {
+                    @Nullable @Override public String apply(DexEntry dexEntry) {
+                        return dexEntry.entryName;
+                    }
+                });
+            }
+        };
+    }
+
+    @Nullable @Override public OatDexFile getEntry(@Nonnull String entryName) throws IOException {
+        DexEntryIterator iterator = new DexEntryIterator();
+        while (iterator.hasNext()) {
+            DexEntry entry = iterator.next();
+
+            if (entry.entryName.equals(entryName)) {
+                return entry.getDexFile();
+            }
+        }
+        return null;
+    }
+
+    public class OatDexFile extends DexBackedDexFile implements MultiDexContainer.MultiDexFile {
         @Nonnull public final String filename;
 
-        public OatDexFile(int offset, @Nonnull String filename) {
-            super(opcodes, OatFile.this.buf, offset);
+        public OatDexFile(byte[] buf, int offset, @Nonnull String filename) {
+            super(opcodes, buf, offset);
             this.filename = filename;
         }
 
-        public int getOatVersion() {
-            return OatFile.this.getOatVersion();
+        @Nonnull @Override public String getEntryName() {
+            return filename;
+        }
+
+        @Nonnull @Override public OatFile getContainer() {
+            return OatFile.this;
         }
 
         @Override public boolean hasOdexOpcodes() {
@@ -211,57 +248,87 @@
     }
 
     private class OatHeader {
-        private final int offset;
+        private final int headerOffset;
 
         public OatHeader(int offset) {
-            this.offset = offset;
+            this.headerOffset = offset;
         }
 
         public boolean isValid() {
             for (int i=0; i<OAT_MAGIC.length; i++) {
-                if (buf[offset + i] != OAT_MAGIC[i]) {
+                if (buf[headerOffset + i] != OAT_MAGIC[i]) {
                     return false;
                 }
             }
 
             for (int i=4; i<7; i++) {
-                if (buf[offset + i] < '0' || buf[offset + i] > '9') {
+                if (buf[headerOffset + i] < '0' || buf[headerOffset + i] > '9') {
                     return false;
                 }
             }
 
-            return buf[offset + 7] == 0;
+            return buf[headerOffset + 7] == 0;
         }
 
         public int getVersion() {
-            return Integer.valueOf(new String(buf, offset + 4, 3));
+            return Integer.valueOf(new String(buf, headerOffset + 4, 3));
         }
 
         public int getDexFileCount() {
-            return readSmallUint(offset + 20);
+            return readSmallUint(headerOffset + 20);
         }
 
         public int getKeyValueStoreSize() {
-            int version = getVersion();
-            if (version < 56) {
+            if (getVersion() < MIN_OAT_VERSION) {
                 throw new IllegalStateException("Unsupported oat version");
             }
             int fieldOffset = 17 * 4;
-            return readSmallUint(offset + fieldOffset);
+            return readSmallUint(headerOffset + fieldOffset);
         }
 
         public int getHeaderSize() {
-            int version = getVersion();
-             if (version >= 56) {
-                return 18*4 + getKeyValueStoreSize();
-            } else {
+            if (getVersion() < MIN_OAT_VERSION) {
                 throw new IllegalStateException("Unsupported oat version");
             }
+            return 18*4 + getKeyValueStoreSize();
+        }
 
+        @Nullable
+        public String getKeyValue(@Nonnull String key) {
+            int size = getKeyValueStoreSize();
+
+            int offset = headerOffset + 18 * 4;
+            int endOffset = offset + size;
+
+            while (offset < endOffset) {
+                int keyStartOffset = offset;
+                while (offset < endOffset && buf[offset] != '\0') {
+                    offset++;
+                }
+                if (offset >= endOffset) {
+                    throw new InvalidOatFileException("Oat file contains truncated key value store");
+                }
+                int keyEndOffset = offset;
+
+                String k = new String(buf, keyStartOffset, keyEndOffset - keyStartOffset);
+                if (k.equals(key)) {
+                    int valueStartOffset = ++offset;
+                    while (offset < endOffset && buf[offset] != '\0') {
+                        offset++;
+                    }
+                    if (offset >= endOffset) {
+                        throw new InvalidOatFileException("Oat file contains truncated key value store");
+                    }
+                    int valueEndOffset = offset;
+                    return new String(buf, valueStartOffset, valueEndOffset - valueStartOffset);
+                }
+                offset++;
+            }
+            return null;
         }
 
         public int getDexListStart() {
-            return offset + getHeaderSize();
+            return headerOffset + getHeaderSize();
         }
     }
 
@@ -481,7 +548,74 @@
 
             return new String(buf, start, end-start, Charset.forName("US-ASCII"));
         }
+    }
 
+    private class DexEntry {
+        public final String entryName;
+        public final byte[] buf;
+        public final int dexOffset;
+
+
+        public DexEntry(String entryName, byte[] buf, int dexOffset) {
+            this.entryName = entryName;
+            this.buf = buf;
+            this.dexOffset = dexOffset;
+        }
+
+        public OatDexFile getDexFile() {
+            return new OatDexFile(buf, dexOffset, entryName);
+        }
+    }
+
+    private class DexEntryIterator implements Iterator<DexEntry> {
+        int index = 0;
+        int offset = oatHeader.getDexListStart();
+
+        @Override public boolean hasNext() {
+            return index < oatHeader.getDexFileCount();
+        }
+
+        @Override public DexEntry next() {
+            int filenameLength = readSmallUint(offset);
+            offset += 4;
+
+            // TODO: what is the correct character encoding?
+            String filename = new String(buf, offset, filenameLength, Charset.forName("US-ASCII"));
+            offset += filenameLength;
+
+            offset += 4; // checksum
+
+            int dexOffset = readSmallUint(offset);
+            offset += 4;
+
+            byte[] buf;
+            if (getOatVersion() >= 87 && vdexProvider != null && vdexProvider.getVdex() != null) {
+                buf = vdexProvider.getVdex();
+            } else {
+                buf = OatFile.this.buf;
+                dexOffset += oatHeader.headerOffset;
+            }
+
+            if (getOatVersion() >= 75) {
+                offset += 4; // offset to class offsets table
+            }
+            if (getOatVersion() >= 73) {
+                offset += 4; // lookup table offset
+            }
+            if (getOatVersion() < 75) {
+                // prior to 75, the class offsets are included here directly
+                int classCount = readSmallUint(dexOffset + HeaderItem.CLASS_COUNT_OFFSET);
+                offset += 4 * classCount;
+            }
+
+            index++;
+
+            return new DexEntry(filename, buf, dexOffset);
+        }
+
+        @Override public void remove() {
+            throw new UnsupportedOperationException();
+        }
     }
 
     public static class InvalidOatFileException extends RuntimeException {
@@ -493,4 +627,9 @@
     public static class NotAnOatFileException extends RuntimeException {
         public NotAnOatFileException() {}
     }
+
+    public interface VdexProvider {
+        @Nullable
+        byte[] getVdex();
+    }
 }
\ No newline at end of file
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/ZipDexContainer.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/ZipDexContainer.java
new file mode 100644
index 0000000..50052c2
--- /dev/null
+++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/ZipDexContainer.java
@@ -0,0 +1,201 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.dexlib2.dexbacked;
+
+import com.google.common.collect.Lists;
+import com.google.common.io.ByteStreams;
+import org.jf.dexlib2.Opcodes;
+import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile;
+import org.jf.dexlib2.dexbacked.ZipDexContainer.ZipDexFile;
+import org.jf.dexlib2.iface.MultiDexContainer;
+import org.jf.dexlib2.util.DexUtil;
+import org.jf.dexlib2.util.DexUtil.InvalidFile;
+import org.jf.dexlib2.util.DexUtil.UnsupportedFile;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+/**
+ * Represents a zip file that contains dex files (i.e. an apk or jar file)
+ */
+public class ZipDexContainer implements MultiDexContainer<ZipDexFile> {
+
+    private final File zipFilePath;
+    private final Opcodes opcodes;
+
+    /**
+     * Constructs a new ZipDexContainer for the given zip file
+     *
+     * @param zipFilePath The path to the zip file
+     * @param opcodes The Opcodes instance to use when loading dex files from this container
+     */
+    public ZipDexContainer(@Nonnull File zipFilePath, @Nonnull Opcodes opcodes) {
+        this.zipFilePath = zipFilePath;
+        this.opcodes = opcodes;
+    }
+
+    @Nonnull @Override public Opcodes getOpcodes() {
+        return opcodes;
+    }
+
+    /**
+     * Gets a list of the names of dex files in this zip file.
+     *
+     * @return A list of the names of dex files in this zip file
+     */
+    @Nonnull @Override public List<String> getDexEntryNames() throws IOException {
+        List<String> entryNames = Lists.newArrayList();
+        ZipFile zipFile = getZipFile();
+        try {
+            Enumeration<? extends ZipEntry> entriesEnumeration = zipFile.entries();
+
+            while (entriesEnumeration.hasMoreElements()) {
+                ZipEntry entry = entriesEnumeration.nextElement();
+
+                if (!isDex(zipFile, entry)) {
+                    continue;
+                }
+
+                entryNames.add(entry.getName());
+            }
+
+            return entryNames;
+        } finally {
+            zipFile.close();
+        }
+    }
+
+    /**
+     * Loads a dex file from a specific named entry.
+     *
+     * @param entryName The name of the entry
+     * @return A ZipDexFile, or null if there is no entry with the given name
+     * @throws NotADexFile If the entry isn't a dex file
+     */
+    @Nullable @Override public ZipDexFile getEntry(@Nonnull String entryName) throws IOException {
+        ZipFile zipFile = getZipFile();
+        try {
+            ZipEntry entry = zipFile.getEntry(entryName);
+            if (entry == null) {
+                return null;
+            }
+
+            return loadEntry(zipFile, entry);
+        } finally {
+            zipFile.close();
+        }
+    }
+
+    public boolean isZipFile() {
+        ZipFile zipFile = null;
+        try {
+            zipFile = getZipFile();
+            return true;
+        } catch (IOException ex) {
+            return false;
+        } catch (NotAZipFileException ex) {
+            return false;
+        } finally {
+            if(zipFile != null) {
+                try {
+                    zipFile.close();
+                } catch (IOException ex) {
+                    // just eat it
+                }
+            }
+        }
+    }
+
+    public class ZipDexFile extends DexBackedDexFile implements MultiDexContainer.MultiDexFile {
+
+        private final String entryName;
+
+        protected ZipDexFile(@Nonnull Opcodes opcodes, @Nonnull byte[] buf, @Nonnull String entryName) {
+            super(opcodes, buf, 0);
+            this.entryName = entryName;
+        }
+
+        @Nonnull @Override public String getEntryName() {
+            return entryName;
+        }
+
+        @Nonnull @Override public MultiDexContainer getContainer() {
+            return ZipDexContainer.this;
+        }
+    }
+
+    protected boolean isDex(@Nonnull ZipFile zipFile, @Nonnull ZipEntry zipEntry) throws IOException {
+        InputStream inputStream = new BufferedInputStream(zipFile.getInputStream(zipEntry));
+        try {
+            DexUtil.verifyDexHeader(inputStream);
+        } catch (NotADexFile ex) {
+            return false;
+        } catch (InvalidFile ex) {
+            return false;
+        } catch (UnsupportedFile ex) {
+            return false;
+        } finally {
+            inputStream.close();
+        }
+        return true;
+    }
+
+    protected ZipFile getZipFile() throws IOException {
+        try {
+            return new ZipFile(zipFilePath);
+        } catch (IOException ex) {
+            throw new NotAZipFileException();
+        }
+    }
+
+    @Nonnull
+    protected ZipDexFile loadEntry(@Nonnull ZipFile zipFile, @Nonnull ZipEntry zipEntry) throws IOException {
+        InputStream inputStream = zipFile.getInputStream(zipEntry);
+        try {
+            byte[] buf = ByteStreams.toByteArray(inputStream);
+            return new ZipDexFile(opcodes, buf, zipEntry.getName());
+        } finally {
+            inputStream.close();
+        }
+    }
+
+    public static class NotAZipFileException extends RuntimeException {
+    }
+}
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/HeaderItem.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/HeaderItem.java
index 2f3af4c..e8db969 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/HeaderItem.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/HeaderItem.java
@@ -42,15 +42,8 @@
 public class HeaderItem {
     public static final int ITEM_SIZE = 0x70;
 
-    /**
-     * The magic numbers for dex files.
-     *
-     * They are: "dex\n035\0", "dex\n037\0", and "dex\n038\0".
-     */
-    public static final byte[][] MAGIC_VALUES= new byte[][] {
-            new byte[]{0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0x00},
-            new byte[]{0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x37, 0x00},
-            new byte[]{0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x38, 0x00}};
+    private static final byte[] MAGIC_VALUE = new byte[] { 0x64, 0x65, 0x78, 0x0a, 0x00, 0x00, 0x00, 0x00 };
+    private static final int[] SUPPORTED_DEX_VERSIONS = new int[] { 35, 37, 38 };
 
     public static final int LITTLE_ENDIAN_TAG = 0x12345678;
     public static final int BIG_ENDIAN_TAG = 0x78563412;
@@ -231,51 +224,100 @@
         return "Invalid";
     }
 
-
     /**
-     * Get the higest magic number supported by Android for this api level.
+     * Get the highest magic number supported by Android for this api level.
      * @return The dex file magic number
      */
     public static byte[] getMagicForApi(int api) {
         if (api < 24) {
             // Prior to Android N we only support dex version 035.
-            return HeaderItem.MAGIC_VALUES[0];
+            return getMagicForDexVersion(35);
         } else if (api < 26) {
             // On android N and later we support dex version 037.
-            return HeaderItem.MAGIC_VALUES[1];
+            return getMagicForDexVersion(37);
         } else {
             // On android O and later we support dex version 038.
-            return HeaderItem.MAGIC_VALUES[2];
+            return getMagicForDexVersion(38);
         }
     }
 
-    private static int getVersion(byte[] buf, int offset) {
-        if (buf.length - offset < 8) {
-            return 0;
+    public static byte[] getMagicForDexVersion(int dexVersion) {
+        byte[] magic = MAGIC_VALUE.clone();
+
+        if (dexVersion < 0 || dexVersion > 999) {
+            throw new IllegalArgumentException("dexVersion must be within [0, 999]");
         }
 
-        boolean matches = true;
-        for (int i=0; i<MAGIC_VALUES.length; i++) {
-            byte[] expected = MAGIC_VALUES[i];
-            matches = true;
-            for (int j=0; j<8; j++) {
-                if (buf[offset + j] != expected[j]) {
-                    matches = false;
-                    break;
+        for (int i=6; i>=4; i--) {
+            int digit = dexVersion % 10;
+            magic[i] = (byte)('0' + digit);
+            dexVersion /= 10;
+        }
+
+        return magic;
+    }
+
+    /**
+     * Verifies the magic value at the beginning of a dex file
+     *
+     * @param buf A byte array containing at least the first 8 bytes of a dex file
+     * @param offset The offset within the buffer to the beginning of the dex header
+     * @return True if the magic value is valid
+     */
+    public static boolean verifyMagic(byte[] buf, int offset) {
+        if (buf.length - offset < 8) {
+            return false;
+        }
+
+        for (int i=0; i<4; i++) {
+            if (buf[offset + i] != MAGIC_VALUE[i]) {
+                return false;
                 }
             }
-            if (matches) {
-                return i==0?35:(i==1?37:38);
+        for (int i=4; i<7; i++) {
+            if (buf[offset + i] < '0' ||
+                    buf[offset + i] > '9') {
+                return false;
             }
         }
-        return 0;
+        if (buf[offset + 7] != MAGIC_VALUE[7]) {
+            return false;
     }
 
-    public static boolean verifyMagic(byte[] buf, int offset) {
-        // verifies the magic value
-        return getVersion(buf, offset) != 0;
+        return true;
     }
 
+    /**
+     * Gets the dex version from a dex header
+     *
+     * @param buf A byte array containing at least the first 7 bytes of a dex file
+     * @param offset The offset within the buffer to the beginning of the dex header
+     * @return The dex version if the header is valid or -1 if the header is invalid
+     */
+    public static int getVersion(byte[] buf, int offset) {
+        if (!verifyMagic(buf, offset)) {
+            return -1;
+        }
+
+        return getVersionUnchecked(buf, offset);
+    }
+
+    private static int getVersionUnchecked(byte[] buf, int offset) {
+        int version = (buf[offset + 4] - '0') * 100;
+        version += (buf[offset + 5] - '0') * 10;
+        version += buf[offset + 6] - '0';
+
+        return version;
+    }
+
+    public static boolean isSupportedDexVersion(int version) {
+        for (int i=0; i<SUPPORTED_DEX_VERSIONS.length; i++) {
+            if (SUPPORTED_DEX_VERSIONS[i] == version) {
+                return true;
+            }
+        }
+        return false;
+    }
 
     public static int getEndian(byte[] buf, int offset) {
         BaseDexBuffer bdb = new BaseDexBuffer(buf);
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/OdexHeaderItem.java b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/OdexHeaderItem.java
index c6599bc..7566834 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/OdexHeaderItem.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/dexbacked/raw/OdexHeaderItem.java
@@ -36,10 +36,8 @@
 public class OdexHeaderItem {
     public static final int ITEM_SIZE = 40;
 
-    public static final byte[][] MAGIC_VALUES= new byte[][] {
-            new byte[] {0x64, 0x65, 0x79, 0x0A, 0x30, 0x33, 0x35, 0x00}, // "dey\n035\0"
-            new byte[] {0x64, 0x65, 0x79, 0x0A, 0x30, 0x33, 0x36, 0x00}  // "dey\n036\0"
-    };
+    private static final byte[] MAGIC_VALUE = new byte[] { 0x64, 0x65, 0x79, 0x0A, 0x00, 0x00, 0x00, 0x00 };
+    private static final int[] SUPPORTED_ODEX_VERSIONS = new int[] { 35, 36 };
 
     public static final int MAGIC_OFFSET = 0;
     public static final int MAGIC_LENGTH = 8;
@@ -51,31 +49,66 @@
     public static final int AUX_LENGTH_OFFSET = 28;
     public static final int FLAGS_OFFSET = 32;
 
-    public static int getVersion(byte[] magic) {
-        if (magic.length < 8) {
-            return 0;
+    /**
+     * Verifies the magic value at the beginning of an odex file
+     *
+     * @param buf A byte array containing at least the first 8 bytes of an odex file
+     * @param offset The offset within the buffer to the beginning of the odex header
+     * @return True if the magic value is valid
+     */
+    public static boolean verifyMagic(byte[] buf, int offset) {
+        if (buf.length - offset < 8) {
+            return false;
         }
 
-        boolean matches = true;
-        for (int i=0; i<MAGIC_VALUES.length; i++) {
-            byte[] expected = MAGIC_VALUES[i];
-            matches = true;
-            for (int j=0; j<8; j++) {
-                if (magic[j] != expected[j]) {
-                    matches = false;
-                    break;
-                }
-            }
-            if (matches) {
-                return i==0?35:36;
+        for (int i=0; i<4; i++) {
+            if (buf[offset + i] != MAGIC_VALUE[i]) {
+                return false;
             }
         }
-        return 0;
+        for (int i=4; i<7; i++) {
+            if (buf[offset + i] < '0' ||
+                    buf[offset + i] > '9') {
+                return false;
+            }
+        }
+        if (buf[offset + 7] != MAGIC_VALUE[7]) {
+            return false;
+        }
+
+        return true;
     }
 
-    public static boolean verifyMagic(byte[] buf) {
-        // verifies the magic value
-        return getVersion(buf) != 0;
+    /**
+     * Gets the dex version from an odex header
+     *
+     * @param buf A byte array containing at least the first 7 bytes of an odex file
+     * @param offset The offset within the buffer to the beginning of the odex header
+     * @return The odex version if the header is valid or -1 if the header is invalid
+     */
+    public static int getVersion(byte[] buf, int offset) {
+        if (!verifyMagic(buf, offset)) {
+            return -1;
+        }
+
+        return getVersionUnchecked(buf, offset);
+    }
+
+    private static int getVersionUnchecked(byte[] buf, int offset) {
+        int version = (buf[offset + 4] - '0') * 100;
+        version += (buf[offset + 5] - '0') * 10;
+        version += buf[offset + 6] - '0';
+
+        return version;
+    }
+
+    public static boolean isSupportedOdexVersion(int version) {
+        for (int i=0; i<SUPPORTED_ODEX_VERSIONS.length; i++) {
+            if (SUPPORTED_ODEX_VERSIONS[i] == version) {
+                return true;
+            }
+        }
+        return false;
     }
 
     public static int getDexOffset(byte[] buf) {
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/iface/MultiDexContainer.java b/dexlib2/src/main/java/org/jf/dexlib2/iface/MultiDexContainer.java
new file mode 100644
index 0000000..6c4e769
--- /dev/null
+++ b/dexlib2/src/main/java/org/jf/dexlib2/iface/MultiDexContainer.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.dexlib2.iface;
+
+import org.jf.dexlib2.Opcodes;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+
+/**
+ * This class represents a dex container that can contain multiple, named dex files
+ */
+public interface MultiDexContainer<T extends DexFile> {
+    /**
+     * @return A list of the names of dex entries in this container
+     */
+    @Nonnull List<String> getDexEntryNames() throws IOException;
+
+    /**
+     * Gets the dex entry with the given name
+     *
+     * @param entryName The name of the entry
+     * @return A DexFile, or null if no entry with that name is found
+     */
+    @Nullable T getEntry(@Nonnull String entryName) throws IOException;
+
+    /**
+     * @return the Opcodes instance associated with this MultiDexContainer
+     */
+    @Nonnull Opcodes getOpcodes();
+
+    /**
+     * This class represents a dex file that is contained in a MultiDexContainer
+     */
+    interface MultiDexFile extends DexFile {
+        /**
+         * @return The name of this entry within its container
+         */
+        @Nonnull String getEntryName();
+
+        /**
+         * @return The MultiDexContainer that contains this dex file
+         */
+        @Nonnull MultiDexContainer<? extends MultiDexFile> getContainer();
+    }
+}
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/immutable/ImmutableDexFile.java b/dexlib2/src/main/java/org/jf/dexlib2/immutable/ImmutableDexFile.java
index 2112bd0..76f39a1 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/immutable/ImmutableDexFile.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/immutable/ImmutableDexFile.java
@@ -45,18 +45,6 @@
     @Nonnull protected final ImmutableSet<? extends ImmutableClassDef> classes;
     @Nonnull private final Opcodes opcodes;
 
-    @Deprecated
-    public ImmutableDexFile(@Nullable Collection<? extends ClassDef> classes) {
-        this.classes = ImmutableClassDef.immutableSetOf(classes);
-        this.opcodes = Opcodes.forApi(19);
-    }
-
-    @Deprecated
-    public ImmutableDexFile(@Nullable ImmutableSet<? extends ImmutableClassDef> classes) {
-        this.classes = ImmutableUtils.nullToEmptySet(classes);
-        this.opcodes = Opcodes.forApi(19);
-    }
-
     public ImmutableDexFile(@Nonnull Opcodes opcodes, @Nullable Collection<? extends ClassDef> classes) {
         this.classes = ImmutableClassDef.immutableSetOf(classes);
         this.opcodes = opcodes;
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/util/DexUtil.java b/dexlib2/src/main/java/org/jf/dexlib2/util/DexUtil.java
new file mode 100644
index 0000000..389edfd
--- /dev/null
+++ b/dexlib2/src/main/java/org/jf/dexlib2/util/DexUtil.java
@@ -0,0 +1,189 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.dexlib2.util;
+
+import com.google.common.io.ByteStreams;
+import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile;
+import org.jf.dexlib2.dexbacked.DexBackedOdexFile.NotAnOdexFile;
+import org.jf.dexlib2.dexbacked.raw.HeaderItem;
+import org.jf.dexlib2.dexbacked.raw.OdexHeaderItem;
+
+import javax.annotation.Nonnull;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+
+public class DexUtil {
+
+    /**
+     * Reads in the dex header from the given input stream and verifies that it is valid and a supported version
+     *
+     * The inputStream must support mark(), and will be reset to initial position upon exiting the method
+     *
+     * @param inputStream An input stream that is positioned at a dex header
+     * @throws NotADexFile If the file is not a dex file
+     * @throws InvalidFile If the header appears to be a dex file, but is not valid for some reason
+     * @throws UnsupportedFile If the dex header is valid, but uses unsupported functionality
+     */
+    public static void verifyDexHeader(@Nonnull InputStream inputStream) throws IOException {
+        if (!inputStream.markSupported()) {
+            throw new IllegalArgumentException("InputStream must support mark");
+        }
+        inputStream.mark(44);
+        byte[] partialHeader = new byte[44];
+        try {
+            ByteStreams.readFully(inputStream, partialHeader);
+        } catch (EOFException ex) {
+            throw new NotADexFile("File is too short");
+        } finally {
+            inputStream.reset();
+        }
+
+        verifyDexHeader(partialHeader, 0);
+    }
+
+    /**
+     * Verifies that the dex header is valid and a supported version
+     *
+     * @param buf A byte array containing at least the first 44 bytes of a dex file
+     * @param offset The offset within the array to the dex header
+     * @throws NotADexFile If the file is not a dex file
+     * @throws InvalidFile If the header appears to be a dex file, but is not valid for some reason
+     * @throws UnsupportedFile If the dex header is valid, but uses unsupported functionality
+     */
+    public static void verifyDexHeader(@Nonnull byte[] buf, int offset) {
+        int dexVersion = HeaderItem.getVersion(buf, offset);
+        if (dexVersion == -1) {
+            StringBuilder sb = new StringBuilder("Not a valid dex magic value:");
+            for (int i=0; i<8; i++) {
+                sb.append(String.format(" %02x", buf[i]));
+            }
+            throw new NotADexFile(sb.toString());
+        }
+
+        if (!HeaderItem.isSupportedDexVersion(dexVersion)) {
+            throw new UnsupportedFile(String.format("Dex version %03d is not supported", dexVersion));
+        }
+
+        int endian = HeaderItem.getEndian(buf, offset);
+        if (endian == HeaderItem.BIG_ENDIAN_TAG) {
+            throw new UnsupportedFile("Big endian dex files are not supported");
+        }
+
+        if (endian != HeaderItem.LITTLE_ENDIAN_TAG) {
+            throw new InvalidFile(String.format("Invalid endian tag: 0x%x", endian));
+        }
+    }
+
+    /**
+     * Reads in the odex header from the given input stream and verifies that it is valid and a supported version
+     *
+     * The inputStream must support mark(), and will be reset to initial position upon exiting the method
+     *
+     * @param inputStream An input stream that is positioned at an odex header
+     * @throws NotAnOdexFile If the file is not an odex file
+     * @throws UnsupportedFile If the odex header is valid, but is an unsupported version
+     */
+    public static void verifyOdexHeader(@Nonnull InputStream inputStream) throws IOException {
+        if (!inputStream.markSupported()) {
+            throw new IllegalArgumentException("InputStream must support mark");
+        }
+        inputStream.mark(8);
+        byte[] partialHeader = new byte[8];
+        try {
+            ByteStreams.readFully(inputStream, partialHeader);
+        } catch (EOFException ex) {
+            throw new NotAnOdexFile("File is too short");
+        } finally {
+            inputStream.reset();
+        }
+
+        verifyOdexHeader(partialHeader, 0);
+    }
+
+    /**
+     * Verifies that the odex header is valid and a supported version
+     *
+     * @param buf A byte array containing at least the first 8 bytes of an odex file
+     * @param offset The offset within the array to the odex header
+     * @throws NotAnOdexFile If the file is not an odex file
+     * @throws UnsupportedFile If the odex header is valid, but uses unsupported functionality
+     */
+    public static void verifyOdexHeader(@Nonnull byte[] buf, int offset) {
+        int odexVersion = OdexHeaderItem.getVersion(buf, offset);
+        if (odexVersion == -1) {
+            StringBuilder sb = new StringBuilder("Not a valid odex magic value:");
+            for (int i=0; i<8; i++) {
+                sb.append(String.format(" %02x", buf[i]));
+            }
+            throw new NotAnOdexFile(sb.toString());
+        }
+
+        if (!OdexHeaderItem.isSupportedOdexVersion(odexVersion)) {
+            throw new UnsupportedFile(String.format("Odex version %03d is not supported", odexVersion));
+        }
+    }
+
+    public static class InvalidFile extends RuntimeException {
+        public InvalidFile() {
+        }
+
+        public InvalidFile(String message) {
+            super(message);
+        }
+
+        public InvalidFile(String message, Throwable cause) {
+            super(message, cause);
+        }
+
+        public InvalidFile(Throwable cause) {
+            super(cause);
+        }
+    }
+
+    public static class UnsupportedFile extends RuntimeException {
+        public UnsupportedFile() {
+        }
+
+        public UnsupportedFile(String message) {
+            super(message);
+        }
+
+        public UnsupportedFile(String message, Throwable cause) {
+            super(message, cause);
+        }
+
+        public UnsupportedFile(Throwable cause) {
+            super(cause);
+        }
+    }
+}
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/DexWriter.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/DexWriter.java
index 6ca1ce9..00cce65 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/DexWriter.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/DexWriter.java
@@ -53,11 +53,7 @@
 import org.jf.dexlib2.iface.instruction.OneRegisterInstruction;
 import org.jf.dexlib2.iface.instruction.ReferenceInstruction;
 import org.jf.dexlib2.iface.instruction.formats.*;
-import org.jf.dexlib2.iface.reference.FieldReference;
-import org.jf.dexlib2.iface.reference.MethodProtoReference;
-import org.jf.dexlib2.iface.reference.MethodReference;
-import org.jf.dexlib2.iface.reference.StringReference;
-import org.jf.dexlib2.iface.reference.TypeReference;
+import org.jf.dexlib2.iface.reference.*;
 import org.jf.dexlib2.util.InstructionUtil;
 import org.jf.dexlib2.util.MethodUtil;
 import org.jf.dexlib2.util.ReferenceUtil;
@@ -92,7 +88,18 @@
         TypeListKey,
         FieldKey, MethodKey,
         EncodedValue,
-        AnnotationElement extends org.jf.dexlib2.iface.AnnotationElement> {
+        AnnotationElement extends org.jf.dexlib2.iface.AnnotationElement,
+        StringSectionType extends StringSection<StringKey, StringRef>,
+        TypeSectionType extends TypeSection<StringKey, TypeKey, TypeRef>,
+        ProtoSectionType extends ProtoSection<StringKey, TypeKey, ProtoRefKey, TypeListKey>,
+        FieldSectionType extends FieldSection<StringKey, TypeKey, FieldRefKey, FieldKey>,
+        MethodSectionType extends MethodSection<StringKey, TypeKey, ProtoRefKey, MethodRefKey, MethodKey>,
+        ClassSectionType extends ClassSection<StringKey, TypeKey, TypeListKey, ClassKey, FieldKey, MethodKey,
+                AnnotationSetKey, EncodedValue>,
+        TypeListSectionType extends TypeListSection<TypeKey, TypeListKey>,
+        AnnotationSectionType extends AnnotationSection<StringKey, TypeKey, AnnotationKey, AnnotationElement,
+                EncodedValue>,
+        AnnotationSetSectionType extends AnnotationSetSection<AnnotationKey, AnnotationSetKey>> {
     public static final int NO_INDEX = -1;
     public static final int NO_OFFSET = 0;
 
@@ -124,43 +131,34 @@
     protected int numCodeItemItems = 0;
     protected int numClassDataItems = 0;
 
-    protected final StringSection<StringKey, StringRef> stringSection;
-    protected final TypeSection<StringKey, TypeKey, TypeRef> typeSection;
-    protected final ProtoSection<StringKey, TypeKey, ProtoRefKey, TypeListKey> protoSection;
-    protected final FieldSection<StringKey, TypeKey, FieldRefKey, FieldKey> fieldSection;
-    protected final MethodSection<StringKey, TypeKey, ProtoRefKey, MethodRefKey, MethodKey> methodSection;
-    protected final ClassSection<StringKey, TypeKey, TypeListKey, ClassKey, FieldKey, MethodKey, AnnotationSetKey,
-            EncodedValue> classSection;
+    public final StringSectionType stringSection;
+    public final TypeSectionType typeSection;
+    public final ProtoSectionType protoSection;
+    public final FieldSectionType fieldSection;
+    public final MethodSectionType methodSection;
+    public final ClassSectionType classSection;
     
-    protected final TypeListSection<TypeKey, TypeListKey> typeListSection;
-    protected final AnnotationSection<StringKey, TypeKey, AnnotationKey, AnnotationElement, EncodedValue> annotationSection;
-    protected final AnnotationSetSection<AnnotationKey, AnnotationSetKey> annotationSetSection;
+    public final TypeListSectionType typeListSection;
+    public final AnnotationSectionType annotationSection;
+    public final AnnotationSetSectionType annotationSetSection;
 
-    protected DexWriter(Opcodes opcodes,
-                        StringSection<StringKey, StringRef> stringSection,
-                        TypeSection<StringKey, TypeKey, TypeRef> typeSection,
-                        ProtoSection<StringKey, TypeKey, ProtoRefKey, TypeListKey> protoSection,
-                        FieldSection<StringKey, TypeKey, FieldRefKey, FieldKey> fieldSection,
-                        MethodSection<StringKey, TypeKey, ProtoRefKey, MethodRefKey, MethodKey> methodSection,
-                        ClassSection<StringKey, TypeKey, TypeListKey, ClassKey, FieldKey, MethodKey, AnnotationSetKey,
-                                EncodedValue> classSection,
-                        TypeListSection<TypeKey, TypeListKey> typeListSection,
-                        AnnotationSection<StringKey, TypeKey, AnnotationKey, AnnotationElement,
-                                EncodedValue> annotationSection,
-                        AnnotationSetSection<AnnotationKey, AnnotationSetKey> annotationSetSection) {
+    protected DexWriter(Opcodes opcodes) {
         this.opcodes = opcodes;
 
-        this.stringSection = stringSection;
-        this.typeSection = typeSection;
-        this.protoSection = protoSection;
-        this.fieldSection = fieldSection;
-        this.methodSection = methodSection;
-        this.classSection = classSection;
-        this.typeListSection = typeListSection;
-        this.annotationSection = annotationSection;
-        this.annotationSetSection = annotationSetSection;
+        SectionProvider sectionProvider = getSectionProvider();
+        this.stringSection = sectionProvider.getStringSection();
+        this.typeSection = sectionProvider.getTypeSection();
+        this.protoSection = sectionProvider.getProtoSection();
+        this.fieldSection = sectionProvider.getFieldSection();
+        this.methodSection = sectionProvider.getMethodSection();
+        this.classSection = sectionProvider.getClassSection();
+        this.typeListSection = sectionProvider.getTypeListSection();
+        this.annotationSection = sectionProvider.getAnnotationSection();
+        this.annotationSetSection = sectionProvider.getAnnotationSetSection();
     }
 
+    @Nonnull protected abstract SectionProvider getSectionProvider();
+
     protected abstract void writeEncodedValue(@Nonnull InternalEncodedValueWriter writer,
                                               @Nonnull EncodedValue encodedValue) throws IOException;
 
@@ -192,12 +190,12 @@
 
     private int getDataSectionOffset() {
         return HeaderItem.ITEM_SIZE +
-                stringSection.getItems().size() * StringIdItem.ITEM_SIZE +
-                typeSection.getItems().size() * TypeIdItem.ITEM_SIZE +
-                protoSection.getItems().size() * ProtoIdItem.ITEM_SIZE +
-                fieldSection.getItems().size() * FieldIdItem.ITEM_SIZE +
-                methodSection.getItems().size() * MethodIdItem.ITEM_SIZE +
-                classSection.getItems().size() * ClassDefItem.ITEM_SIZE;
+                stringSection.getItemCount() * StringIdItem.ITEM_SIZE +
+                typeSection.getItemCount() * TypeIdItem.ITEM_SIZE +
+                protoSection.getItemCount() * ProtoIdItem.ITEM_SIZE +
+                fieldSection.getItemCount() * FieldIdItem.ITEM_SIZE +
+                methodSection.getItemCount() * MethodIdItem.ITEM_SIZE +
+                classSection.getItemCount() * ClassDefItem.ITEM_SIZE;
     }
 
     @Nonnull
@@ -227,6 +225,22 @@
         return classReferences;
     }
 
+    /**
+     * Checks whether any of the size-sensitive constant pools have overflowed.
+     *
+     * This checks whether the type, method, field pools are larger than 64k entries.
+     *
+     * Note that even if this returns true, it may still be possible to successfully write the dex file, if the
+     * overflowed items are not referenced anywhere that uses a 16-bit index
+     *
+     * @return true if any of the size-sensitive constant pools have overflowed
+     */
+    public boolean hasOverflowed() {
+        return methodSection.getItemCount() > (1 << 16) ||
+                typeSection.getItemCount() > (1 << 16) ||
+                fieldSection.getItemCount() > (1 << 16);
+    }
+
     public void writeTo(@Nonnull DexDataStore dest) throws IOException {
         this.writeTo(dest, MemoryDeferredOutputStream.getFactory());
     }
@@ -801,7 +815,14 @@
 
                 int debugItemOffset = writeDebugItem(offsetWriter, debugWriter,
                         classSection.getParameterNames(methodKey), debugItems);
-                int codeItemOffset = writeCodeItem(codeWriter, ehBuf, methodKey, tryBlocks, instructions, debugItemOffset);
+                int codeItemOffset;
+                try {
+                    codeItemOffset = writeCodeItem(
+                            codeWriter, ehBuf, methodKey, tryBlocks, instructions, debugItemOffset);
+                } catch (RuntimeException ex) {
+                    throw new ExceptionWithContext(ex, "Exception occurred while writing code_item for method %s",
+                            methodSection.getMethodReference(methodKey));
+                }
 
                 if (codeItemOffset != -1) {
                     codeOffsets.add(new CodeItemOffset<MethodKey>(methodKey, codeItemOffset));
@@ -950,105 +971,111 @@
                             methodSection, protoSection);
 
             writer.writeInt(codeUnitCount);
+            int codeOffset = 0;
             for (Instruction instruction: instructions) {
-                switch (instruction.getOpcode().format) {
-                    case Format10t:
-                        instructionWriter.write((Instruction10t)instruction);
-                        break;
-                    case Format10x:
-                        instructionWriter.write((Instruction10x)instruction);
-                        break;
-                    case Format11n:
-                        instructionWriter.write((Instruction11n)instruction);
-                        break;
-                    case Format11x:
-                        instructionWriter.write((Instruction11x)instruction);
-                        break;
-                    case Format12x:
-                        instructionWriter.write((Instruction12x)instruction);
-                        break;
-                    case Format20bc:
-                        instructionWriter.write((Instruction20bc)instruction);
-                        break;
-                    case Format20t:
-                        instructionWriter.write((Instruction20t)instruction);
-                        break;
-                    case Format21c:
-                        instructionWriter.write((Instruction21c)instruction);
-                        break;
-                    case Format21ih:
-                        instructionWriter.write((Instruction21ih)instruction);
-                        break;
-                    case Format21lh:
-                        instructionWriter.write((Instruction21lh)instruction);
-                        break;
-                    case Format21s:
-                        instructionWriter.write((Instruction21s)instruction);
-                        break;
-                    case Format21t:
-                        instructionWriter.write((Instruction21t)instruction);
-                        break;
-                    case Format22b:
-                        instructionWriter.write((Instruction22b)instruction);
-                        break;
-                    case Format22c:
-                        instructionWriter.write((Instruction22c)instruction);
-                        break;
-                    case Format22s:
-                        instructionWriter.write((Instruction22s)instruction);
-                        break;
-                    case Format22t:
-                        instructionWriter.write((Instruction22t)instruction);
-                        break;
-                    case Format22x:
-                        instructionWriter.write((Instruction22x)instruction);
-                        break;
-                    case Format23x:
-                        instructionWriter.write((Instruction23x)instruction);
-                        break;
-                    case Format30t:
-                        instructionWriter.write((Instruction30t)instruction);
-                        break;
-                    case Format31c:
-                        instructionWriter.write((Instruction31c)instruction);
-                        break;
-                    case Format31i:
-                        instructionWriter.write((Instruction31i)instruction);
-                        break;
-                    case Format31t:
-                        instructionWriter.write((Instruction31t)instruction);
-                        break;
-                    case Format32x:
-                        instructionWriter.write((Instruction32x)instruction);
-                        break;
-                    case Format35c:
-                        instructionWriter.write((Instruction35c)instruction);
-                        break;
-                    case Format3rc:
-                        instructionWriter.write((Instruction3rc)instruction);
-                        break;
-                    case Format45cc:
-                        instructionWriter.write((Instruction45cc) instruction);
-                        break;
-                    case Format4rcc:
-                        instructionWriter.write((Instruction4rcc) instruction);
-                        break;
-                    case Format51l:
-                        instructionWriter.write((Instruction51l)instruction);
-                        break;
-                    case ArrayPayload:
-                        instructionWriter.write((ArrayPayload)instruction);
-                        break;
-                    case PackedSwitchPayload:
-                        instructionWriter.write((PackedSwitchPayload)instruction);
-                        break;
-                    case SparseSwitchPayload:
-                        instructionWriter.write((SparseSwitchPayload)instruction);
-                        break;
-                    default:
-                        throw new ExceptionWithContext("Unsupported instruction format: %s",
-                                instruction.getOpcode().format);
+                try {
+                    switch (instruction.getOpcode().format) {
+                        case Format10t:
+                            instructionWriter.write((Instruction10t)instruction);
+                            break;
+                        case Format10x:
+                            instructionWriter.write((Instruction10x)instruction);
+                            break;
+                        case Format11n:
+                            instructionWriter.write((Instruction11n)instruction);
+                            break;
+                        case Format11x:
+                            instructionWriter.write((Instruction11x)instruction);
+                            break;
+                        case Format12x:
+                            instructionWriter.write((Instruction12x)instruction);
+                            break;
+                        case Format20bc:
+                            instructionWriter.write((Instruction20bc)instruction);
+                            break;
+                        case Format20t:
+                            instructionWriter.write((Instruction20t)instruction);
+                            break;
+                        case Format21c:
+                            instructionWriter.write((Instruction21c)instruction);
+                            break;
+                        case Format21ih:
+                            instructionWriter.write((Instruction21ih)instruction);
+                            break;
+                        case Format21lh:
+                            instructionWriter.write((Instruction21lh)instruction);
+                            break;
+                        case Format21s:
+                            instructionWriter.write((Instruction21s)instruction);
+                            break;
+                        case Format21t:
+                            instructionWriter.write((Instruction21t)instruction);
+                            break;
+                        case Format22b:
+                            instructionWriter.write((Instruction22b)instruction);
+                            break;
+                        case Format22c:
+                            instructionWriter.write((Instruction22c)instruction);
+                            break;
+                        case Format22s:
+                            instructionWriter.write((Instruction22s)instruction);
+                            break;
+                        case Format22t:
+                            instructionWriter.write((Instruction22t)instruction);
+                            break;
+                        case Format22x:
+                            instructionWriter.write((Instruction22x)instruction);
+                            break;
+                        case Format23x:
+                            instructionWriter.write((Instruction23x)instruction);
+                            break;
+                        case Format30t:
+                            instructionWriter.write((Instruction30t)instruction);
+                            break;
+                        case Format31c:
+                            instructionWriter.write((Instruction31c)instruction);
+                            break;
+                        case Format31i:
+                            instructionWriter.write((Instruction31i)instruction);
+                            break;
+                        case Format31t:
+                            instructionWriter.write((Instruction31t)instruction);
+                            break;
+                        case Format32x:
+                            instructionWriter.write((Instruction32x)instruction);
+                            break;
+                        case Format35c:
+                            instructionWriter.write((Instruction35c)instruction);
+                            break;
+                        case Format3rc:
+                            instructionWriter.write((Instruction3rc)instruction);
+                            break;
+                        case Format45cc:
+                            instructionWriter.write((Instruction45cc)instruction);
+                            break;
+                        case Format4rcc:
+                            instructionWriter.write((Instruction4rcc)instruction);
+                            break;
+                        case Format51l:
+                            instructionWriter.write((Instruction51l)instruction);
+                            break;
+                        case ArrayPayload:
+                            instructionWriter.write((ArrayPayload)instruction);
+                            break;
+                        case PackedSwitchPayload:
+                            instructionWriter.write((PackedSwitchPayload)instruction);
+                            break;
+                        case SparseSwitchPayload:
+                            instructionWriter.write((SparseSwitchPayload)instruction);
+                            break;
+                        default:
+                            throw new ExceptionWithContext("Unsupported instruction format: %s",
+                                    instruction.getOpcode().format);
+                    }
+                } catch (RuntimeException ex) {
+                    throw new ExceptionWithContext(ex, "Error while writing instruction at code offset 0x%x", codeOffset);
                 }
+                codeOffset += instruction.getCodeUnits();
             }
 
             if (tryBlocks.size() > 0) {
@@ -1274,4 +1301,16 @@
         // (https://code.google.com/p/android/issues/detail?id=35304)
         return (opcodes.api < 17);
     }
+
+    public abstract class SectionProvider {
+        @Nonnull public abstract StringSectionType getStringSection();
+        @Nonnull public abstract TypeSectionType getTypeSection();
+        @Nonnull public abstract ProtoSectionType getProtoSection();
+        @Nonnull public abstract FieldSectionType getFieldSection();
+        @Nonnull public abstract MethodSectionType getMethodSection();
+        @Nonnull public abstract ClassSectionType getClassSection();
+        @Nonnull public abstract TypeListSectionType getTypeListSection();
+        @Nonnull public abstract AnnotationSectionType getAnnotationSection();
+        @Nonnull public abstract AnnotationSetSectionType getAnnotationSetSection();
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/IndexSection.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/IndexSection.java
index 53d1447..8abc776 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/IndexSection.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/IndexSection.java
@@ -38,4 +38,5 @@
 public interface IndexSection<Key> {
     int getItemIndex(@Nonnull Key key);
     @Nonnull Collection<? extends Map.Entry<? extends Key, Integer>> getItems();
+    int getItemCount();
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/MethodSection.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/MethodSection.java
index 32e6d6b..f31e84c 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/MethodSection.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/MethodSection.java
@@ -39,6 +39,7 @@
 public interface MethodSection<StringKey, TypeKey, ProtoRefKey extends MethodProtoReference,
         MethodRefKey extends MethodReference, MethodKey>
         extends IndexSection<MethodRefKey> {
+    @Nonnull MethodRefKey getMethodReference(@Nonnull MethodKey key);
     @Nonnull TypeKey getDefiningClass(@Nonnull MethodRefKey key);
     @Nonnull ProtoRefKey getPrototype(@Nonnull MethodRefKey key);
     @Nonnull ProtoRefKey getPrototype(@Nonnull MethodKey key);
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BaseBuilderPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BaseBuilderPool.java
new file mode 100644
index 0000000..64ba1d7
--- /dev/null
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BaseBuilderPool.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.dexlib2.writer.builder;
+
+import javax.annotation.Nonnull;
+
+public class BaseBuilderPool {
+    @Nonnull protected final DexBuilder dexBuilder;
+
+    public BaseBuilderPool(@Nonnull DexBuilder dexBuilder) {
+        this.dexBuilder = dexBuilder;
+    }
+}
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationPool.java
index e16bff0..37e536c 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationPool.java
@@ -41,14 +41,13 @@
 import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentMap;
 
-class BuilderAnnotationPool implements AnnotationSection<BuilderStringReference, BuilderTypeReference,
-        BuilderAnnotation, BuilderAnnotationElement, BuilderEncodedValue> {
-    @Nonnull private final BuilderContext context;
+class BuilderAnnotationPool extends BaseBuilderPool implements AnnotationSection<BuilderStringReference,
+        BuilderTypeReference, BuilderAnnotation, BuilderAnnotationElement, BuilderEncodedValue> {
     @Nonnull private final ConcurrentMap<Annotation, BuilderAnnotation> internedItems =
             Maps.newConcurrentMap();
 
-    BuilderAnnotationPool(@Nonnull BuilderContext context) {
-        this.context = context;
+    public BuilderAnnotationPool(@Nonnull DexBuilder dexBuilder) {
+        super(dexBuilder);
     }
 
     @Nonnull public BuilderAnnotation internAnnotation(@Nonnull Annotation annotation) {
@@ -59,8 +58,8 @@
 
         BuilderAnnotation dexBuilderAnnotation = new BuilderAnnotation(
                 annotation.getVisibility(),
-                context.typePool.internType(annotation.getType()),
-                context.internAnnotationElements(annotation.getElements()));
+                dexBuilder.typeSection.internType(annotation.getType()),
+                dexBuilder.internAnnotationElements(annotation.getElements()));
         ret = internedItems.putIfAbsent(dexBuilderAnnotation, dexBuilderAnnotation);
         return ret==null?dexBuilderAnnotation:ret;
     }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSet.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSet.java
index 43ca745..ef9a9d6 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSet.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSet.java
@@ -39,7 +39,7 @@
 import java.util.Iterator;
 import java.util.Set;
 
-class BuilderAnnotationSet extends AbstractSet<BuilderAnnotation> {
+public class BuilderAnnotationSet extends AbstractSet<BuilderAnnotation> {
     public static final BuilderAnnotationSet EMPTY =
             new BuilderAnnotationSet(ImmutableSet.<BuilderAnnotation>of());
 
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSetPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSetPool.java
index 353190f..45af5cf 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSetPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderAnnotationSetPool.java
@@ -46,13 +46,13 @@
 import java.util.Set;
 import java.util.concurrent.ConcurrentMap;
 
-class BuilderAnnotationSetPool implements AnnotationSetSection<BuilderAnnotation, BuilderAnnotationSet> {
-    @Nonnull private final BuilderContext context;
+class BuilderAnnotationSetPool extends BaseBuilderPool
+        implements AnnotationSetSection<BuilderAnnotation, BuilderAnnotationSet> {
     @Nonnull private final ConcurrentMap<Set<? extends Annotation>, BuilderAnnotationSet> internedItems =
             Maps.newConcurrentMap();
 
-    BuilderAnnotationSetPool(@Nonnull BuilderContext context) {
-        this.context = context;
+    public BuilderAnnotationSetPool(@Nonnull DexBuilder dexBuilder) {
+        super(dexBuilder);
     }
 
     @Nonnull public BuilderAnnotationSet internAnnotationSet(@Nullable Set<? extends Annotation> annotations) {
@@ -69,7 +69,7 @@
                 ImmutableSet.copyOf(Iterators.transform(annotations.iterator(),
                         new Function<Annotation, BuilderAnnotation>() {
                             @Nullable @Override public BuilderAnnotation apply(Annotation input) {
-                                return context.annotationPool.internAnnotation(input);
+                                return dexBuilder.annotationSection.internAnnotation(input);
                             }
                         })));
 
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassDef.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassDef.java
index 1021592..9938a6e 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassDef.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassDef.java
@@ -31,7 +31,6 @@
 
 package org.jf.dexlib2.writer.builder;
 
-import com.google.common.base.Function;
 import com.google.common.base.Functions;
 import com.google.common.collect.*;
 import org.jf.dexlib2.base.reference.BaseTypeReference;
@@ -81,14 +80,10 @@
         this.interfaces = interfaces;
         this.sourceFile = sourceFile;
         this.annotations = annotations;
-        this.staticFields = ImmutableSortedSet.copyOf(
-                (Iterable<? extends BuilderField>)Iterables.filter(fields, FieldUtil.FIELD_IS_STATIC));
-        this.instanceFields = ImmutableSortedSet.copyOf(
-                (Iterable<? extends BuilderField>)Iterables.filter(fields, FieldUtil.FIELD_IS_INSTANCE));
-        this.directMethods = ImmutableSortedSet.copyOf(
-                (Iterable<? extends BuilderMethod>)Iterables.filter(methods, MethodUtil.METHOD_IS_DIRECT));
-        this.virtualMethods = ImmutableSortedSet.copyOf(
-                (Iterable<? extends BuilderMethod>)Iterables.filter(methods, MethodUtil.METHOD_IS_VIRTUAL));
+        this.staticFields = ImmutableSortedSet.copyOf(Iterables.filter(fields, FieldUtil.FIELD_IS_STATIC));
+        this.instanceFields = ImmutableSortedSet.copyOf(Iterables.filter(fields, FieldUtil.FIELD_IS_INSTANCE));
+        this.directMethods = ImmutableSortedSet.copyOf(Iterables.filter(methods, MethodUtil.METHOD_IS_DIRECT));
+        this.virtualMethods = ImmutableSortedSet.copyOf(Iterables.filter(methods, MethodUtil.METHOD_IS_VIRTUAL));
     }
 
     @Nonnull @Override public String getType() { return type.getType(); }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassPool.java
index 29980f3..232b482 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderClassPool.java
@@ -60,12 +60,14 @@
 import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentMap;
 
-public class BuilderClassPool implements ClassSection<BuilderStringReference, BuilderTypeReference, BuilderTypeList,
-        BuilderClassDef, BuilderField, BuilderMethod, BuilderAnnotationSet, BuilderEncodedValue> {
+public class BuilderClassPool extends BaseBuilderPool implements ClassSection<BuilderStringReference,
+        BuilderTypeReference, BuilderTypeList, BuilderClassDef, BuilderField, BuilderMethod, BuilderAnnotationSet,
+        BuilderEncodedValue> {
     @Nonnull private final ConcurrentMap<String, BuilderClassDef> internedItems =
             Maps.newConcurrentMap();
 
-    BuilderClassPool() {
+    public BuilderClassPool(@Nonnull DexBuilder dexBuilder) {
+        super(dexBuilder);
     }
 
     @Nonnull BuilderClassDef internClass(@Nonnull BuilderClassDef classDef) {
@@ -441,4 +443,8 @@
             }
         };
     }
+
+    @Override public int getItemCount() {
+        return internedItems.size();
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderContext.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderContext.java
deleted file mode 100644
index e6f8e22..0000000
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderContext.java
+++ /dev/null
@@ -1,175 +0,0 @@
-/*
- * Copyright 2013, Google Inc.
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions are
- * met:
- *
- *     * Redistributions of source code must retain the above copyright
- * notice, this list of conditions and the following disclaimer.
- *     * Redistributions in binary form must reproduce the above
- * copyright notice, this list of conditions and the following disclaimer
- * in the documentation and/or other materials provided with the
- * distribution.
- *     * Neither the name of Google Inc. nor the names of its
- * contributors may be used to endorse or promote products derived from
- * this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
- * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
- * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
- * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
- * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
- * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
- * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
- * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.jf.dexlib2.writer.builder;
-
-import com.google.common.base.Function;
-import com.google.common.collect.ImmutableList;
-import com.google.common.collect.ImmutableSet;
-import com.google.common.collect.Iterators;
-import org.jf.dexlib2.ValueType;
-import org.jf.dexlib2.iface.AnnotationElement;
-import org.jf.dexlib2.iface.value.*;
-import org.jf.dexlib2.writer.builder.BuilderEncodedValues.*;
-import org.jf.util.ExceptionWithContext;
-
-import javax.annotation.Nonnull;
-import javax.annotation.Nullable;
-import java.util.Set;
-
-class BuilderContext {
-    // keep our own local references to the various pools, using the Builder specific pool type;
-    @Nonnull final BuilderStringPool stringPool;
-    @Nonnull final BuilderTypePool typePool;
-    @Nonnull final BuilderFieldPool fieldPool;
-    @Nonnull final BuilderMethodPool methodPool;
-    @Nonnull final BuilderProtoPool protoPool;
-    @Nonnull final BuilderClassPool classPool;
-
-    @Nonnull final BuilderTypeListPool typeListPool;
-    @Nonnull final BuilderAnnotationPool annotationPool;
-    @Nonnull final BuilderAnnotationSetPool annotationSetPool;
-
-
-    BuilderContext() {
-        this.stringPool = new BuilderStringPool();
-        this.typePool = new BuilderTypePool(this);
-        this.fieldPool = new BuilderFieldPool(this);
-        this.methodPool = new BuilderMethodPool(this);
-        this.protoPool = new BuilderProtoPool(this);
-        this.classPool = new BuilderClassPool();
-
-        this.typeListPool = new BuilderTypeListPool(this);
-        this.annotationPool = new BuilderAnnotationPool(this);
-        this.annotationSetPool = new BuilderAnnotationSetPool(this);
-    }
-
-    @Nonnull Set<? extends BuilderAnnotationElement> internAnnotationElements(
-            @Nonnull Set<? extends AnnotationElement> elements) {
-        return ImmutableSet.copyOf(
-                Iterators.transform(elements.iterator(),
-                        new Function<AnnotationElement, BuilderAnnotationElement>() {
-                            @Nullable @Override
-                            public BuilderAnnotationElement apply(AnnotationElement input) {
-                                return internAnnotationElement(input);
-                            }
-                        }));
-    }
-
-    @Nonnull private BuilderAnnotationElement internAnnotationElement(@Nonnull AnnotationElement annotationElement) {
-        return new BuilderAnnotationElement(stringPool.internString(annotationElement.getName()),
-                internEncodedValue(annotationElement.getValue()));
-    }
-
-    @Nullable BuilderEncodedValue internNullableEncodedValue(@Nullable EncodedValue encodedValue) {
-        if (encodedValue == null) {
-            return null;
-        }
-        return internEncodedValue(encodedValue);
-    }
-
-    @Nonnull private BuilderEncodedValue internEncodedValue(@Nonnull EncodedValue encodedValue) {
-        switch (encodedValue.getValueType()) {
-            case ValueType.ANNOTATION:
-                return internAnnotationEncodedValue((AnnotationEncodedValue)encodedValue);
-            case ValueType.ARRAY:
-                return internArrayEncodedValue((ArrayEncodedValue)encodedValue);
-            case ValueType.BOOLEAN:
-                boolean value = ((BooleanEncodedValue)encodedValue).getValue();
-                return value?BuilderBooleanEncodedValue.TRUE_VALUE:BuilderBooleanEncodedValue.FALSE_VALUE;
-            case ValueType.BYTE:
-                return new BuilderByteEncodedValue(((ByteEncodedValue)encodedValue).getValue());
-            case ValueType.CHAR:
-                return new BuilderCharEncodedValue(((CharEncodedValue)encodedValue).getValue());
-            case ValueType.DOUBLE:
-                return new BuilderDoubleEncodedValue(((DoubleEncodedValue)encodedValue).getValue());
-            case ValueType.ENUM:
-                return internEnumEncodedValue((EnumEncodedValue)encodedValue);
-            case ValueType.FIELD:
-                return internFieldEncodedValue((FieldEncodedValue)encodedValue);
-            case ValueType.FLOAT:
-                return new BuilderFloatEncodedValue(((FloatEncodedValue)encodedValue).getValue());
-            case ValueType.INT:
-                return new BuilderIntEncodedValue(((IntEncodedValue)encodedValue).getValue());
-            case ValueType.LONG:
-                return new BuilderLongEncodedValue(((LongEncodedValue)encodedValue).getValue());
-            case ValueType.METHOD:
-                return internMethodEncodedValue((MethodEncodedValue)encodedValue);
-            case ValueType.NULL:
-                return BuilderNullEncodedValue.INSTANCE;
-            case ValueType.SHORT:
-                return new BuilderShortEncodedValue(((ShortEncodedValue)encodedValue).getValue());
-            case ValueType.STRING:
-                return internStringEncodedValue((StringEncodedValue)encodedValue);
-            case ValueType.TYPE:
-                return internTypeEncodedValue((TypeEncodedValue)encodedValue);
-            default:
-                throw new ExceptionWithContext("Unexpected encoded value type: %d", encodedValue.getValueType());
-        }
-    }
-
-    @Nonnull private BuilderAnnotationEncodedValue internAnnotationEncodedValue(@Nonnull AnnotationEncodedValue value) {
-        return new BuilderAnnotationEncodedValue(
-                typePool.internType(value.getType()),
-                internAnnotationElements(value.getElements()));
-    }
-
-    @Nonnull private BuilderArrayEncodedValue internArrayEncodedValue(@Nonnull ArrayEncodedValue value) {
-        return new BuilderArrayEncodedValue(
-                ImmutableList.copyOf(
-                        Iterators.transform(value.getValue().iterator(),
-                                new Function<EncodedValue, BuilderEncodedValue>() {
-                                    @Nullable @Override public BuilderEncodedValue apply(EncodedValue input) {
-                                        return internEncodedValue(input);
-                                    }
-                                })));
-    }
-
-    @Nonnull private BuilderEnumEncodedValue internEnumEncodedValue(@Nonnull EnumEncodedValue value) {
-        return new BuilderEnumEncodedValue(fieldPool.internField(value.getValue()));
-    }
-
-    @Nonnull private BuilderFieldEncodedValue internFieldEncodedValue(@Nonnull FieldEncodedValue value) {
-        return new BuilderFieldEncodedValue(fieldPool.internField(value.getValue()));
-    }
-
-    @Nonnull private BuilderMethodEncodedValue internMethodEncodedValue(@Nonnull MethodEncodedValue value) {
-        return new BuilderMethodEncodedValue(methodPool.internMethod(value.getValue()));
-    }
-
-    @Nonnull private BuilderStringEncodedValue internStringEncodedValue(@Nonnull StringEncodedValue string) {
-        return new BuilderStringEncodedValue(stringPool.internString(string.getValue()));
-    }
-
-    @Nonnull private BuilderTypeEncodedValue internTypeEncodedValue(@Nonnull TypeEncodedValue type) {
-        return new BuilderTypeEncodedValue(typePool.internType(type.getValue()));
-    }
-}
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderFieldPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderFieldPool.java
index 7a90649..3bc65cd 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderFieldPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderFieldPool.java
@@ -41,14 +41,13 @@
 import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentMap;
 
-public class BuilderFieldPool
+public class BuilderFieldPool extends BaseBuilderPool
         implements FieldSection<BuilderStringReference, BuilderTypeReference, BuilderFieldReference, BuilderField> {
-    @Nonnull private final BuilderContext context;
     @Nonnull private final ConcurrentMap<FieldReference, BuilderFieldReference> internedItems =
             Maps.newConcurrentMap();
 
-    BuilderFieldPool(@Nonnull BuilderContext context) {
-        this.context = context;
+    public BuilderFieldPool(@Nonnull DexBuilder dexBuilder) {
+        super(dexBuilder);
     }
 
     @Nonnull BuilderFieldReference internField(@Nonnull String definingClass, String name, String type) {
@@ -63,9 +62,9 @@
         }
 
         BuilderFieldReference dexPoolFieldReference = new BuilderFieldReference(
-                context.typePool.internType(fieldReference.getDefiningClass()),
-                context.stringPool.internString(fieldReference.getName()),
-                context.typePool.internType(fieldReference.getType()));
+                dexBuilder.typeSection.internType(fieldReference.getDefiningClass()),
+                dexBuilder.stringSection.internString(fieldReference.getName()),
+                dexBuilder.typeSection.internType(fieldReference.getType()));
         ret = internedItems.putIfAbsent(dexPoolFieldReference, dexPoolFieldReference);
         return ret==null?dexPoolFieldReference:ret;
     }
@@ -104,4 +103,8 @@
             }
         };
     }
+
+    @Override public int getItemCount() {
+        return internedItems.size();
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderMethodPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderMethodPool.java
index 2c5dd81..7f937fd 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderMethodPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderMethodPool.java
@@ -42,14 +42,13 @@
 import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentMap;
 
-class BuilderMethodPool implements MethodSection<BuilderStringReference, BuilderTypeReference,
+class BuilderMethodPool extends BaseBuilderPool implements MethodSection<BuilderStringReference, BuilderTypeReference,
         BuilderMethodProtoReference, BuilderMethodReference, BuilderMethod>{
-    @Nonnull private final BuilderContext context;
     @Nonnull private final ConcurrentMap<MethodReference, BuilderMethodReference> internedItems =
             Maps.newConcurrentMap();
 
-    BuilderMethodPool(@Nonnull BuilderContext context) {
-        this.context = context;
+    public BuilderMethodPool(@Nonnull DexBuilder dexBuilder) {
+        super(dexBuilder);
     }
 
     @Nonnull public BuilderMethodReference internMethod(@Nonnull MethodReference methodReference) {
@@ -59,9 +58,9 @@
         }
 
         BuilderMethodReference dexPoolMethodReference = new BuilderMethodReference(
-                context.typePool.internType(methodReference.getDefiningClass()),
-                context.stringPool.internString(methodReference.getName()),
-                context.protoPool.internMethodProto(methodReference));
+                dexBuilder.typeSection.internType(methodReference.getDefiningClass()),
+                dexBuilder.stringSection.internString(methodReference.getName()),
+                dexBuilder.protoSection.internMethodProto(methodReference));
         ret = internedItems.putIfAbsent(dexPoolMethodReference, dexPoolMethodReference);
         return ret==null?dexPoolMethodReference:ret;
     }
@@ -72,6 +71,10 @@
         return internMethod(new MethodKey(definingClass, name, parameters, returnType));
     }
 
+    @Nonnull @Override public BuilderMethodReference getMethodReference(@Nonnull BuilderMethod builderMethod) {
+        return builderMethod.methodReference;
+    }
+
     @Nonnull @Override
     public BuilderTypeReference getDefiningClass(@Nonnull BuilderMethodReference key) {
         return key.definingClass; 
@@ -112,6 +115,10 @@
         };
     }
 
+    @Override public int getItemCount() {
+        return internedItems.size();
+    }
+
     private static class MethodKey extends BaseMethodReference implements MethodReference {
         @Nonnull private final String definingClass;
         @Nonnull private final String name;
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderProtoPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderProtoPool.java
index de19fa3..969f243 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderProtoPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderProtoPool.java
@@ -44,14 +44,13 @@
 import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentMap;
 
-class BuilderProtoPool
+class BuilderProtoPool extends BaseBuilderPool
         implements ProtoSection<BuilderStringReference, BuilderTypeReference, BuilderMethodProtoReference, BuilderTypeList> {
-    @Nonnull private final BuilderContext context;
     @Nonnull private final ConcurrentMap<MethodProtoReference, BuilderMethodProtoReference> internedItems =
             Maps.newConcurrentMap();
 
-    BuilderProtoPool(@Nonnull BuilderContext context) {
-        this.context = context;
+    public BuilderProtoPool(@Nonnull DexBuilder dexBuilder) {
+        super(dexBuilder);
     }
 
     @Nonnull public BuilderMethodProtoReference internMethodProto(@Nonnull MethodProtoReference methodProto) {
@@ -61,10 +60,10 @@
         }
 
         BuilderMethodProtoReference protoReference = new BuilderMethodProtoReference(
-                context.stringPool.internString(MethodUtil.getShorty(
+                dexBuilder.stringSection.internString(MethodUtil.getShorty(
                         methodProto.getParameterTypes(), methodProto.getReturnType())),
-                context.typeListPool.internTypeList(methodProto.getParameterTypes()),
-                context.typePool.internType(methodProto.getReturnType()));
+                dexBuilder.typeListSection.internTypeList(methodProto.getParameterTypes()),
+                dexBuilder.typeSection.internType(methodProto.getReturnType()));
         ret = internedItems.putIfAbsent(protoReference, protoReference);
         return ret==null?protoReference:ret;
     }
@@ -103,4 +102,8 @@
             }
         };
     }
+
+    @Override public int getItemCount() {
+        return internedItems.size();
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderStringPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderStringPool.java
index 6b60e9f..95fe86f 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderStringPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderStringPool.java
@@ -86,4 +86,8 @@
             }
         };
     }
+
+    @Override public int getItemCount() {
+        return internedItems.size();
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypeListPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypeListPool.java
index 7b189fd..604e39c 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypeListPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypeListPool.java
@@ -45,13 +45,12 @@
 import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentMap;
 
-class BuilderTypeListPool implements TypeListSection<BuilderTypeReference, BuilderTypeList> {
-    @Nonnull private final BuilderContext context;
+class BuilderTypeListPool extends BaseBuilderPool implements TypeListSection<BuilderTypeReference, BuilderTypeList> {
     @Nonnull private final ConcurrentMap<List<? extends CharSequence>, BuilderTypeList> internedItems =
             Maps.newConcurrentMap();
 
-    BuilderTypeListPool(@Nonnull BuilderContext context) {
-        this.context = context;
+    public BuilderTypeListPool(@Nonnull DexBuilder dexBuilder) {
+        super(dexBuilder);
     }
 
     @Nonnull public BuilderTypeList internTypeList(@Nullable List<? extends CharSequence> types) {
@@ -67,7 +66,7 @@
         BuilderTypeList typeList = new BuilderTypeList(
                 ImmutableList.copyOf(Iterables.transform(types, new Function<CharSequence, BuilderTypeReference>() {
                     @Nonnull @Override public BuilderTypeReference apply(CharSequence input) {
-                        return context.typePool.internType(input.toString());
+                        return dexBuilder.typeSection.internType(input.toString());
                     }
                 })));
 
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypePool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypePool.java
index 29871fc..b9476de 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypePool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/BuilderTypePool.java
@@ -41,12 +41,12 @@
 import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentMap;
 
-class BuilderTypePool implements TypeSection<BuilderStringReference, BuilderTypeReference, BuilderTypeReference> {
-    @Nonnull private final BuilderContext context;
+class BuilderTypePool extends BaseBuilderPool
+        implements TypeSection<BuilderStringReference, BuilderTypeReference, BuilderTypeReference> {
     @Nonnull private final ConcurrentMap<String, BuilderTypeReference> internedItems = Maps.newConcurrentMap();
 
-    BuilderTypePool(@Nonnull BuilderContext context) {
-        this.context = context;
+    public BuilderTypePool(@Nonnull DexBuilder dexBuilder) {
+        super(dexBuilder);
     }
 
     @Nonnull public BuilderTypeReference internType(@Nonnull String type) {
@@ -54,7 +54,7 @@
         if (ret != null) {
             return ret;
         }
-        BuilderStringReference stringRef = context.stringPool.internString(type);
+        BuilderStringReference stringRef = dexBuilder.stringSection.internString(type);
         BuilderTypeReference typeReference = new BuilderTypeReference(stringRef);
         ret = internedItems.putIfAbsent(type, typeReference);
         return ret==null?typeReference:ret;
@@ -92,4 +92,8 @@
             }
         };
     }
+
+    @Override public int getItemCount() {
+        return internedItems.size();
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/DexBuilder.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/DexBuilder.java
index b7507fa..25938fe 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/DexBuilder.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/builder/DexBuilder.java
@@ -33,11 +33,13 @@
 
 import com.google.common.base.Function;
 import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Iterators;
 import com.google.common.collect.Sets;
 import org.jf.dexlib2.Opcodes;
 import org.jf.dexlib2.ValueType;
 import org.jf.dexlib2.iface.Annotation;
+import org.jf.dexlib2.iface.AnnotationElement;
 import org.jf.dexlib2.iface.MethodImplementation;
 import org.jf.dexlib2.iface.MethodParameter;
 import org.jf.dexlib2.iface.reference.*;
@@ -56,32 +58,16 @@
 public class DexBuilder extends DexWriter<BuilderStringReference, BuilderStringReference, BuilderTypeReference,
         BuilderTypeReference, BuilderMethodProtoReference, BuilderFieldReference, BuilderMethodReference,
         BuilderClassDef, BuilderAnnotation, BuilderAnnotationSet, BuilderTypeList, BuilderField, BuilderMethod,
-        BuilderEncodedValue, BuilderAnnotationElement> {
+        BuilderEncodedValue, BuilderAnnotationElement, BuilderStringPool, BuilderTypePool, BuilderProtoPool,
+        BuilderFieldPool, BuilderMethodPool, BuilderClassPool, BuilderTypeListPool, BuilderAnnotationPool,
+        BuilderAnnotationSetPool> {
 
-    @Nonnull private final BuilderContext context;
-
-    @Nonnull public static DexBuilder makeDexBuilder() {
-        BuilderContext context = new BuilderContext();
-        return new DexBuilder(Opcodes.forApi(20), context);
+    public DexBuilder(@Nonnull Opcodes opcodes) {
+        super(opcodes);
     }
 
-    @Deprecated
-    @Nonnull
-    public static DexBuilder makeDexBuilder(int api) {
-        BuilderContext context = new BuilderContext();
-        return new DexBuilder(Opcodes.forApi(api), context);
-    }
-
-    @Nonnull public static DexBuilder makeDexBuilder(@Nonnull Opcodes opcodes) {
-        BuilderContext context = new BuilderContext();
-        return new DexBuilder(opcodes, context);
-    }
-
-    private DexBuilder(@Nonnull Opcodes opcodes, @Nonnull BuilderContext context) {
-        super(opcodes, context.stringPool, context.typePool, context.protoPool,
-                context.fieldPool, context.methodPool, context.classPool, context.typeListPool, context.annotationPool,
-                context.annotationSetPool);
-        this.context = context;
+    @Nonnull @Override protected SectionProvider getSectionProvider() {
+        return new DexBuilderSectionProvider();
     }
 
     @Nonnull public BuilderField internField(@Nonnull String definingClass,
@@ -90,10 +76,10 @@
                                              int accessFlags,
                                              @Nullable EncodedValue initialValue,
                                              @Nonnull Set<? extends Annotation> annotations) {
-        return new BuilderField(context.fieldPool.internField(definingClass, name, type),
+        return new BuilderField(fieldSection.internField(definingClass, name, type),
                 accessFlags,
-                context.internNullableEncodedValue(initialValue),
-                context.annotationSetPool.internAnnotationSet(annotations));
+                internNullableEncodedValue(initialValue),
+                annotationSetSection.internAnnotationSet(annotations));
     }
 
     @Nonnull public BuilderMethod internMethod(@Nonnull String definingClass,
@@ -106,10 +92,10 @@
         if (parameters == null) {
             parameters = ImmutableList.of();
         }
-        return new BuilderMethod(context.methodPool.internMethod(definingClass, name, parameters, returnType),
+        return new BuilderMethod(methodSection.internMethod(definingClass, name, parameters, returnType),
                 internMethodParameters(parameters),
                 accessFlags,
-                context.annotationSetPool.internAnnotationSet(annotations),
+                annotationSetSection.internAnnotationSet(annotations),
                 methodImplementation);
     }
 
@@ -136,18 +122,18 @@
             }
         }
 
-        return context.classPool.internClass(new BuilderClassDef(context.typePool.internType(type),
+        return classSection.internClass(new BuilderClassDef(typeSection.internType(type),
                 accessFlags,
-                context.typePool.internNullableType(superclass),
-                context.typeListPool.internTypeList(interfaces),
-                context.stringPool.internNullableString(sourceFile),
-                context.annotationSetPool.internAnnotationSet(annotations),
+                typeSection.internNullableType(superclass),
+                typeListSection.internTypeList(interfaces),
+                stringSection.internNullableString(sourceFile),
+                annotationSetSection.internAnnotationSet(annotations),
                 fields,
                 methods));
     }
 
     @Nonnull public BuilderStringReference internStringReference(@Nonnull String string) {
-        return context.stringPool.internString(string);
+        return stringSection.internString(string);
     }
 
     @Nullable public BuilderStringReference internNullableStringReference(@Nullable String string) {
@@ -158,7 +144,7 @@
     }
 
     @Nonnull public BuilderTypeReference internTypeReference(@Nonnull String type) {
-        return context.typePool.internType(type);
+        return typeSection.internType(type);
     }
 
     @Nullable public BuilderTypeReference internNullableTypeReference(@Nullable String type) {
@@ -169,15 +155,15 @@
     }
 
     @Nonnull public BuilderFieldReference internFieldReference(@Nonnull FieldReference field) {
-        return context.fieldPool.internField(field);
+        return fieldSection.internField(field);
     }
 
     @Nonnull public BuilderMethodReference internMethodReference(@Nonnull MethodReference method) {
-        return context.methodPool.internMethod(method);
+        return methodSection.internMethod(method);
     }
 
     @Nonnull public BuilderMethodProtoReference internMethodProtoReference(@Nonnull MethodProtoReference methodProto) {
-        return context.protoPool.internMethodProto(methodProto);
+        return protoSection.internMethodProto(methodProto);
     }
 
     @Nonnull public BuilderReference internReference(@Nonnull Reference reference) {
@@ -214,9 +200,9 @@
 
     @Nonnull private BuilderMethodParameter internMethodParameter(@Nonnull MethodParameter methodParameter) {
         return new BuilderMethodParameter(
-                context.typePool.internType(methodParameter.getType()),
-                context.stringPool.internNullableString(methodParameter.getName()),
-                context.annotationSetPool.internAnnotationSet(methodParameter.getAnnotations()));
+                typeSection.internType(methodParameter.getType()),
+                stringSection.internNullableString(methodParameter.getName()),
+                annotationSetSection.internAnnotationSet(methodParameter.getAnnotations()));
     }
 
     @Override protected void writeEncodedValue(@Nonnull InternalEncodedValueWriter writer,
@@ -276,4 +262,143 @@
                 throw new ExceptionWithContext("Unrecognized value type: %d", encodedValue.getValueType());
         }
     }
+
+    @Nonnull Set<? extends BuilderAnnotationElement> internAnnotationElements(
+            @Nonnull Set<? extends AnnotationElement> elements) {
+        return ImmutableSet.copyOf(
+                Iterators.transform(elements.iterator(),
+                        new Function<AnnotationElement, BuilderAnnotationElement>() {
+                            @Nullable @Override
+                            public BuilderAnnotationElement apply(AnnotationElement input) {
+                                return internAnnotationElement(input);
+                            }
+                        }));
+    }
+
+    @Nonnull private BuilderAnnotationElement internAnnotationElement(@Nonnull AnnotationElement annotationElement) {
+        return new BuilderAnnotationElement(stringSection.internString(annotationElement.getName()),
+                internEncodedValue(annotationElement.getValue()));
+    }
+
+    @Nullable BuilderEncodedValue internNullableEncodedValue(@Nullable EncodedValue encodedValue) {
+        if (encodedValue == null) {
+            return null;
+        }
+        return internEncodedValue(encodedValue);
+    }
+
+    @Nonnull private BuilderEncodedValue internEncodedValue(@Nonnull EncodedValue encodedValue) {
+        switch (encodedValue.getValueType()) {
+            case ValueType.ANNOTATION:
+                return internAnnotationEncodedValue((AnnotationEncodedValue)encodedValue);
+            case ValueType.ARRAY:
+                return internArrayEncodedValue((ArrayEncodedValue)encodedValue);
+            case ValueType.BOOLEAN:
+                boolean value = ((BooleanEncodedValue)encodedValue).getValue();
+                return value?BuilderBooleanEncodedValue.TRUE_VALUE:BuilderBooleanEncodedValue.FALSE_VALUE;
+            case ValueType.BYTE:
+                return new BuilderByteEncodedValue(((ByteEncodedValue)encodedValue).getValue());
+            case ValueType.CHAR:
+                return new BuilderCharEncodedValue(((CharEncodedValue)encodedValue).getValue());
+            case ValueType.DOUBLE:
+                return new BuilderDoubleEncodedValue(((DoubleEncodedValue)encodedValue).getValue());
+            case ValueType.ENUM:
+                return internEnumEncodedValue((EnumEncodedValue)encodedValue);
+            case ValueType.FIELD:
+                return internFieldEncodedValue((FieldEncodedValue)encodedValue);
+            case ValueType.FLOAT:
+                return new BuilderFloatEncodedValue(((FloatEncodedValue)encodedValue).getValue());
+            case ValueType.INT:
+                return new BuilderIntEncodedValue(((IntEncodedValue)encodedValue).getValue());
+            case ValueType.LONG:
+                return new BuilderLongEncodedValue(((LongEncodedValue)encodedValue).getValue());
+            case ValueType.METHOD:
+                return internMethodEncodedValue((MethodEncodedValue)encodedValue);
+            case ValueType.NULL:
+                return BuilderNullEncodedValue.INSTANCE;
+            case ValueType.SHORT:
+                return new BuilderShortEncodedValue(((ShortEncodedValue)encodedValue).getValue());
+            case ValueType.STRING:
+                return internStringEncodedValue((StringEncodedValue)encodedValue);
+            case ValueType.TYPE:
+                return internTypeEncodedValue((TypeEncodedValue)encodedValue);
+            default:
+                throw new ExceptionWithContext("Unexpected encoded value type: %d", encodedValue.getValueType());
+        }
+    }
+
+    @Nonnull private BuilderAnnotationEncodedValue internAnnotationEncodedValue(@Nonnull AnnotationEncodedValue value) {
+        return new BuilderAnnotationEncodedValue(
+                typeSection.internType(value.getType()),
+                internAnnotationElements(value.getElements()));
+    }
+
+    @Nonnull private BuilderArrayEncodedValue internArrayEncodedValue(@Nonnull ArrayEncodedValue value) {
+        return new BuilderArrayEncodedValue(
+                ImmutableList.copyOf(
+                        Iterators.transform(value.getValue().iterator(),
+                                new Function<EncodedValue, BuilderEncodedValue>() {
+                                    @Nullable @Override public BuilderEncodedValue apply(EncodedValue input) {
+                                        return internEncodedValue(input);
+                                    }
+                                })));
+    }
+
+    @Nonnull private BuilderEnumEncodedValue internEnumEncodedValue(@Nonnull EnumEncodedValue value) {
+        return new BuilderEnumEncodedValue(fieldSection.internField(value.getValue()));
+    }
+
+    @Nonnull private BuilderFieldEncodedValue internFieldEncodedValue(@Nonnull FieldEncodedValue value) {
+        return new BuilderFieldEncodedValue(fieldSection.internField(value.getValue()));
+    }
+
+    @Nonnull private BuilderMethodEncodedValue internMethodEncodedValue(@Nonnull MethodEncodedValue value) {
+        return new BuilderMethodEncodedValue(methodSection.internMethod(value.getValue()));
+    }
+
+    @Nonnull private BuilderStringEncodedValue internStringEncodedValue(@Nonnull StringEncodedValue string) {
+        return new BuilderStringEncodedValue(stringSection.internString(string.getValue()));
+    }
+
+    @Nonnull private BuilderTypeEncodedValue internTypeEncodedValue(@Nonnull TypeEncodedValue type) {
+        return new BuilderTypeEncodedValue(typeSection.internType(type.getValue()));
+    }
+
+    protected class DexBuilderSectionProvider extends SectionProvider {
+        @Nonnull @Override public BuilderStringPool getStringSection() {
+            return new BuilderStringPool();
+        }
+
+        @Nonnull @Override public BuilderTypePool getTypeSection() {
+            return new BuilderTypePool(DexBuilder.this);
+        }
+
+        @Nonnull @Override public BuilderProtoPool getProtoSection() {
+            return new BuilderProtoPool(DexBuilder.this);
+        }
+
+        @Nonnull @Override public BuilderFieldPool getFieldSection() {
+            return new BuilderFieldPool(DexBuilder.this);
+        }
+
+        @Nonnull @Override public BuilderMethodPool getMethodSection() {
+            return new BuilderMethodPool(DexBuilder.this);
+        }
+
+        @Nonnull @Override public BuilderClassPool getClassSection() {
+            return new BuilderClassPool(DexBuilder.this);
+        }
+
+        @Nonnull @Override public BuilderTypeListPool getTypeListSection() {
+            return new BuilderTypeListPool(DexBuilder.this);
+        }
+
+        @Nonnull @Override public BuilderAnnotationPool getAnnotationSection() {
+            return new BuilderAnnotationPool(DexBuilder.this);
+        }
+
+        @Nonnull @Override public BuilderAnnotationSetPool getAnnotationSetSection() {
+            return new BuilderAnnotationSetPool(DexBuilder.this);
+        }
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationPool.java
index c4cfa39..b2db889 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationPool.java
@@ -41,26 +41,18 @@
 
 public class AnnotationPool extends BaseOffsetPool<Annotation>
         implements AnnotationSection<CharSequence, CharSequence, Annotation, AnnotationElement, EncodedValue> {
-    @Nonnull StringPool stringPool;
-    @Nonnull TypePool typePool;
-    @Nonnull FieldPool fieldPool;
-    @Nonnull MethodPool methodPool;
 
-    public AnnotationPool(@Nonnull StringPool stringPool, @Nonnull TypePool typePool,
-                          @Nonnull FieldPool fieldPool, @Nonnull MethodPool methodPool) {
-        this.stringPool = stringPool;
-        this.typePool = typePool;
-        this.fieldPool = fieldPool;
-        this.methodPool = methodPool;
+    public AnnotationPool(@Nonnull DexPool dexPool) {
+        super(dexPool);
     }
 
     public void intern(@Nonnull Annotation annotation) {
         Integer prev = internedItems.put(annotation, 0);
         if (prev == null) {
-            typePool.intern(annotation.getType());
+            dexPool.typeSection.intern(annotation.getType());
             for (AnnotationElement element: annotation.getElements()) {
-                stringPool.intern(element.getName());
-                DexPool.internEncodedValue(element.getValue(), stringPool, typePool, fieldPool, methodPool);
+                dexPool.stringSection.intern(element.getName());
+                dexPool.internEncodedValue(element.getValue());
             }
         }
     }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationSetPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationSetPool.java
index 6512dcb..2170b95 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationSetPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/AnnotationSetPool.java
@@ -40,10 +40,9 @@
 
 public class AnnotationSetPool extends BaseNullableOffsetPool<Set<? extends Annotation>>
         implements AnnotationSetSection<Annotation, Set<? extends Annotation>> {
-    @Nonnull private final AnnotationPool annotationPool;
 
-    public AnnotationSetPool(@Nonnull AnnotationPool annotationPool) {
-        this.annotationPool = annotationPool;
+    public AnnotationSetPool(@Nonnull DexPool dexPool) {
+        super(dexPool);
     }
 
     public void intern(@Nonnull Set<? extends Annotation> annotationSet) {
@@ -51,7 +50,7 @@
             Integer prev = internedItems.put(annotationSet, 0);
             if (prev == null) {
                 for (Annotation annotation: annotationSet) {
-                    annotationPool.intern(annotation);
+                    dexPool.annotationSection.intern(annotation);
                 }
             }
         }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseIndexPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseIndexPool.java
index 01109ad..c07dcf1 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseIndexPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseIndexPool.java
@@ -31,7 +31,6 @@
 
 package org.jf.dexlib2.writer.pool;
 
-import com.google.common.collect.Maps;
 import org.jf.dexlib2.writer.IndexSection;
 import org.jf.util.ExceptionWithContext;
 
@@ -39,8 +38,11 @@
 import java.util.Collection;
 import java.util.Map;
 
-public abstract class BaseIndexPool<Key> implements IndexSection<Key> {
-    @Nonnull protected final Map<Key, Integer> internedItems = Maps.newHashMap();
+public abstract class BaseIndexPool<Key> extends BasePool<Key, Integer> implements IndexSection<Key> {
+
+    public BaseIndexPool(@Nonnull DexPool dexPool) {
+        super(dexPool);
+    }
 
     @Nonnull @Override public Collection<? extends Map.Entry<? extends Key, Integer>> getItems() {
         return internedItems.entrySet();
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseNullableOffsetPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseNullableOffsetPool.java
index ed9dbb6..b04060a 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseNullableOffsetPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseNullableOffsetPool.java
@@ -34,10 +34,16 @@
 import org.jf.dexlib2.writer.DexWriter;
 import org.jf.dexlib2.writer.NullableOffsetSection;
 
+import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
 public abstract class BaseNullableOffsetPool<Key> extends BaseOffsetPool<Key>
         implements NullableOffsetSection<Key> {
+
+    public BaseNullableOffsetPool(@Nonnull DexPool dexPool) {
+        super(dexPool);
+    }
+
     @Override public int getNullableItemOffset(@Nullable Key key) {
         if (key == null) {
             return DexWriter.NO_OFFSET;
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseOffsetPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseOffsetPool.java
index e66a50a..789c954 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseOffsetPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BaseOffsetPool.java
@@ -31,7 +31,6 @@
 
 package org.jf.dexlib2.writer.pool;
 
-import com.google.common.collect.Maps;
 import org.jf.dexlib2.writer.OffsetSection;
 import org.jf.util.ExceptionWithContext;
 
@@ -39,8 +38,11 @@
 import java.util.Collection;
 import java.util.Map;
 
-public abstract class BaseOffsetPool<Key> implements OffsetSection<Key> {
-    @Nonnull protected final Map<Key, Integer> internedItems = Maps.newHashMap();
+public abstract class BaseOffsetPool<Key> extends BasePool<Key, Integer> implements OffsetSection<Key> {
+
+    public BaseOffsetPool(@Nonnull DexPool dexPool) {
+        super(dexPool);
+    }
 
     @Nonnull @Override public Collection<? extends Map.Entry<? extends Key, Integer>> getItems() {
         return internedItems.entrySet();
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BasePool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BasePool.java
new file mode 100644
index 0000000..4fa1810
--- /dev/null
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/BasePool.java
@@ -0,0 +1,75 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.dexlib2.writer.pool;
+
+import com.google.common.collect.Maps;
+
+import javax.annotation.Nonnull;
+import java.util.Iterator;
+import java.util.Map;
+
+public class BasePool<Key, Value> implements Markable {
+    @Nonnull protected final DexPool dexPool;
+    @Nonnull protected final Map<Key, Value> internedItems = Maps.newLinkedHashMap();
+    private int markedItemCount = -1;
+
+    public BasePool(@Nonnull DexPool dexPool) {
+        this.dexPool = dexPool;
+    }
+
+    public void mark() {
+        markedItemCount = internedItems.size();
+    }
+
+    public void reset() {
+        if (markedItemCount < 0) {
+            throw new IllegalStateException("mark() must be called before calling reset()");
+        }
+
+        if (markedItemCount == internedItems.size()) {
+            return;
+        }
+
+        Iterator<Key> keys = internedItems.keySet().iterator();
+        for (int i=0; i<markedItemCount; i++) {
+            keys.next();
+        }
+        while (keys.hasNext()) {
+            keys.next();
+            keys.remove();
+        }
+    }
+
+    public int getItemCount() {
+        return internedItems.size();
+    }
+}
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ClassPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ClassPool.java
index 0389973..7c1d681 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ClassPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ClassPool.java
@@ -58,30 +58,12 @@
 import java.util.*;
 import java.util.Map.Entry;
 
-public class ClassPool implements ClassSection<CharSequence, CharSequence,
+public class ClassPool extends BasePool<String, PoolClassDef> implements ClassSection<CharSequence, CharSequence,
         TypeListPool.Key<? extends Collection<? extends CharSequence>>, PoolClassDef, Field, PoolMethod,
         Set<? extends Annotation>, EncodedValue> {
-    @Nonnull private HashMap<String, PoolClassDef> internedItems = Maps.newHashMap();
 
-    @Nonnull private final StringPool stringPool;
-    @Nonnull private final TypePool typePool;
-    @Nonnull private final FieldPool fieldPool;
-    @Nonnull private final MethodPool methodPool;
-    @Nonnull private final AnnotationSetPool annotationSetPool;
-    @Nonnull private final TypeListPool typeListPool;
-
-    public ClassPool(@Nonnull StringPool stringPool,
-                     @Nonnull TypePool typePool,
-                     @Nonnull FieldPool fieldPool,
-                     @Nonnull MethodPool methodPool,
-                     @Nonnull AnnotationSetPool annotationSetPool,
-                     @Nonnull TypeListPool typeListPool) {
-        this.stringPool = stringPool;
-        this.typePool = typePool;
-        this.fieldPool = fieldPool;
-        this.methodPool = methodPool;
-        this.annotationSetPool = annotationSetPool;
-        this.typeListPool = typeListPool;
+    public ClassPool(@Nonnull DexPool dexPool) {
+        super(dexPool);
     }
 
     public void intern(@Nonnull ClassDef classDef) {
@@ -92,10 +74,10 @@
             throw new ExceptionWithContext("Class %s has already been interned", poolClassDef.getType());
         }
 
-        typePool.intern(poolClassDef.getType());
-        typePool.internNullable(poolClassDef.getSuperclass());
-        typeListPool.intern(poolClassDef.getInterfaces());
-        stringPool.internNullable(poolClassDef.getSourceFile());
+        dexPool.typeSection.intern(poolClassDef.getType());
+        dexPool.typeSection.internNullable(poolClassDef.getSuperclass());
+        dexPool.typeListSection.intern(poolClassDef.getInterfaces());
+        dexPool.stringSection.internNullable(poolClassDef.getSourceFile());
 
         HashSet<String> fields = new HashSet<String>();
         for (Field field: poolClassDef.getFields()) {
@@ -104,14 +86,14 @@
                 throw new ExceptionWithContext("Multiple definitions for field %s->%s",
                         poolClassDef.getType(), fieldDescriptor);
             }
-            fieldPool.intern(field);
+            dexPool.fieldSection.intern(field);
 
             EncodedValue initialValue = field.getInitialValue();
             if (initialValue != null) {
-                DexPool.internEncodedValue(initialValue, stringPool, typePool, fieldPool, methodPool);
+                dexPool.internEncodedValue(initialValue);
             }
 
-            annotationSetPool.intern(field.getAnnotations());
+            dexPool.annotationSetSection.intern(field.getAnnotations());
         }
 
         HashSet<String> methods = new HashSet<String>();
@@ -121,17 +103,17 @@
                 throw new ExceptionWithContext("Multiple definitions for method %s->%s",
                         poolClassDef.getType(), methodDescriptor);
             }
-            methodPool.intern(method);
+            dexPool.methodSection.intern(method);
             internCode(method);
             internDebug(method);
-            annotationSetPool.intern(method.getAnnotations());
+            dexPool.annotationSetSection.intern(method.getAnnotations());
 
             for (MethodParameter parameter: method.getParameters()) {
-                annotationSetPool.intern(parameter.getAnnotations());
+                dexPool.annotationSetSection.intern(parameter.getAnnotations());
             }
         }
 
-        annotationSetPool.intern(poolClassDef.getAnnotations());
+        dexPool.annotationSetSection.intern(poolClassDef.getAnnotations());
     }
 
     private void internCode(@Nonnull Method method) {
@@ -146,16 +128,16 @@
                     Reference reference = ((ReferenceInstruction)instruction).getReference();
                     switch (instruction.getOpcode().referenceType) {
                         case ReferenceType.STRING:
-                            stringPool.intern((StringReference)reference);
+                            dexPool.stringSection.intern((StringReference)reference);
                             break;
                         case ReferenceType.TYPE:
-                            typePool.intern((TypeReference)reference);
+                            dexPool.typeSection.intern((TypeReference)reference);
                             break;
                         case ReferenceType.FIELD:
-                            fieldPool.intern((FieldReference) reference);
+                            dexPool.fieldSection.intern((FieldReference) reference);
                             break;
                         case ReferenceType.METHOD:
-                            methodPool.intern((MethodReference)reference);
+                            dexPool.methodSection.intern((MethodReference)reference);
                             break;
                         default:
                             throw new ExceptionWithContext("Unrecognized reference type: %d",
@@ -172,7 +154,7 @@
 
             for (TryBlock<? extends ExceptionHandler> tryBlock: methodImpl.getTryBlocks()) {
                 for (ExceptionHandler handler: tryBlock.getExceptionHandlers()) {
-                    typePool.internNullable(handler.getExceptionType());
+                    dexPool.typeSection.internNullable(handler.getExceptionType());
                 }
             }
         }
@@ -182,7 +164,7 @@
         for (MethodParameter param: method.getParameters()) {
             String paramName = param.getName();
             if (paramName != null) {
-                stringPool.intern(paramName);
+                dexPool.stringSection.intern(paramName);
             }
         }
 
@@ -192,12 +174,12 @@
                 switch (debugItem.getDebugItemType()) {
                     case DebugItemType.START_LOCAL:
                         StartLocal startLocal = (StartLocal)debugItem;
-                        stringPool.internNullable(startLocal.getName());
-                        typePool.internNullable(startLocal.getType());
-                        stringPool.internNullable(startLocal.getSignature());
+                        dexPool.stringSection.internNullable(startLocal.getName());
+                        dexPool.typeSection.internNullable(startLocal.getType());
+                        dexPool.stringSection.internNullable(startLocal.getSignature());
                         break;
                     case DebugItemType.SET_SOURCE_FILE:
-                        stringPool.internNullable(((SetSourceFile) debugItem).getSourceFile());
+                        dexPool.stringSection.internNullable(((SetSourceFile) debugItem).getSourceFile());
                         break;
                 }
             }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/DexPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/DexPool.java
index d12457a..6d662ec 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/DexPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/DexPool.java
@@ -39,7 +39,7 @@
 import org.jf.dexlib2.iface.Field;
 import org.jf.dexlib2.iface.reference.*;
 import org.jf.dexlib2.iface.value.*;
-import org.jf.dexlib2.writer.DexWriter;
+import org.jf.dexlib2.writer.*;
 import org.jf.dexlib2.writer.io.DexDataStore;
 import org.jf.dexlib2.writer.io.FileDataStore;
 import org.jf.util.ExceptionWithContext;
@@ -54,59 +54,69 @@
         MethodProtoReference, FieldReference, MethodReference, PoolClassDef,
         Annotation, Set<? extends Annotation>,
         TypeListPool.Key<? extends Collection<? extends CharSequence>>, Field, PoolMethod,
-        EncodedValue, AnnotationElement> {
+        EncodedValue, AnnotationElement, StringPool, TypePool, ProtoPool, FieldPool, MethodPool, ClassPool,
+        TypeListPool, AnnotationPool, AnnotationSetPool> {
 
-    @Nonnull
-    public static DexPool makeDexPool() {
-        return makeDexPool(Opcodes.forApi(20));
+    private final Markable[] sections = new Markable[] {
+            stringSection, typeSection, protoSection, fieldSection, methodSection, classSection, typeListSection,
+            annotationSection, annotationSetSection
+    };
+
+    public DexPool(Opcodes opcodes) {
+        super(opcodes);
     }
 
-    @Deprecated
-    @Nonnull
-    public static DexPool makeDexPool(int api) {
-        return makeDexPool(Opcodes.forApi(api));
+    @Nonnull @Override protected SectionProvider getSectionProvider() {
+        return new DexPoolSectionProvider();
     }
 
-    @Nonnull
-    public static DexPool makeDexPool(@Nonnull Opcodes opcodes) {
-        StringPool stringPool = new StringPool();
-        TypePool typePool = new TypePool(stringPool);
-        FieldPool fieldPool = new FieldPool(stringPool, typePool);
-        TypeListPool typeListPool = new TypeListPool(typePool);
-        ProtoPool protoPool = new ProtoPool(stringPool, typePool, typeListPool);
-        MethodPool methodPool = new MethodPool(stringPool, typePool, protoPool);
-        AnnotationPool annotationPool = new AnnotationPool(stringPool, typePool, fieldPool, methodPool);
-        AnnotationSetPool annotationSetPool = new AnnotationSetPool(annotationPool);
-        ClassPool classPool = new ClassPool(stringPool, typePool, fieldPool, methodPool, annotationSetPool,
-                typeListPool);
-
-        return new DexPool(opcodes, stringPool, typePool, protoPool, fieldPool, methodPool, classPool, typeListPool,
-                annotationPool, annotationSetPool);
-    }
-
-    private DexPool(Opcodes opcodes, StringPool stringPool, TypePool typePool, ProtoPool protoPool, FieldPool fieldPool,
-                    MethodPool methodPool, ClassPool classPool, TypeListPool typeListPool,
-                    AnnotationPool annotationPool, AnnotationSetPool annotationSetPool) {
-        super(opcodes, stringPool, typePool, protoPool, fieldPool, methodPool,
-                classPool, typeListPool, annotationPool, annotationSetPool);
-    }
-
-    public static void writeTo(@Nonnull DexDataStore dataStore, @Nonnull org.jf.dexlib2.iface.DexFile input) throws IOException {
-        DexPool dexPool = makeDexPool();
+    public static void writeTo(@Nonnull DexDataStore dataStore, @Nonnull org.jf.dexlib2.iface.DexFile input)
+            throws IOException {
+        DexPool dexPool = new DexPool(input.getOpcodes());
         for (ClassDef classDef: input.getClasses()) {
-            ((ClassPool)dexPool.classSection).intern(classDef);
+            dexPool.internClass(classDef);
         }
         dexPool.writeTo(dataStore);
     }
 
     public static void writeTo(@Nonnull String path, @Nonnull org.jf.dexlib2.iface.DexFile input) throws IOException {
-        DexPool dexPool = makeDexPool();
+        DexPool dexPool = new DexPool(input.getOpcodes());
         for (ClassDef classDef: input.getClasses()) {
-            ((ClassPool)dexPool.classSection).intern(classDef);
+            dexPool.internClass(classDef);
         }
         dexPool.writeTo(new FileDataStore(new File(path)));
     }
 
+    /**
+     * Interns a class into this DexPool
+     * @param classDef The class to intern
+     */
+    public void internClass(ClassDef classDef) {
+        classSection.intern(classDef);
+    }
+
+    /**
+     * Creates a marked state that can be returned to by calling reset()
+     *
+     * This is useful to rollback the last added class if it causes a method/field/type overflow
+     */
+    public void mark() {
+        for (Markable section: sections) {
+            section.mark();
+        }
+    }
+
+    /**
+     * Resets to the last marked state
+     *
+     * This is useful to rollback the last added class if it causes a method/field/type overflow
+     */
+    public void reset() {
+        for (Markable section: sections) {
+            section.reset();
+        }
+    }
+
     @Override protected void writeEncodedValue(@Nonnull InternalEncodedValueWriter writer,
                                                @Nonnull EncodedValue encodedValue) throws IOException {
         switch (encodedValue.getValueType()) {
@@ -165,40 +175,74 @@
         }
     }
 
-    public static void internEncodedValue(@Nonnull EncodedValue encodedValue,
-                                          @Nonnull StringPool stringPool,
-                                          @Nonnull TypePool typePool,
-                                          @Nonnull FieldPool fieldPool,
-                                          @Nonnull MethodPool methodPool) {
+    void internEncodedValue(@Nonnull EncodedValue encodedValue) {
         switch (encodedValue.getValueType()) {
             case ValueType.ANNOTATION:
                 AnnotationEncodedValue annotationEncodedValue = (AnnotationEncodedValue)encodedValue;
-                typePool.intern(annotationEncodedValue.getType());
+                typeSection.intern(annotationEncodedValue.getType());
                 for (AnnotationElement element: annotationEncodedValue.getElements()) {
-                    stringPool.intern(element.getName());
-                    internEncodedValue(element.getValue(), stringPool, typePool, fieldPool, methodPool);
+                    stringSection.intern(element.getName());
+                    internEncodedValue(element.getValue());
                 }
                 break;
             case ValueType.ARRAY:
                 for (EncodedValue element: ((ArrayEncodedValue)encodedValue).getValue()) {
-                    internEncodedValue(element, stringPool, typePool, fieldPool, methodPool);
+                    internEncodedValue(element);
                 }
                 break;
             case ValueType.STRING:
-                stringPool.intern(((StringEncodedValue)encodedValue).getValue());
+                stringSection.intern(((StringEncodedValue)encodedValue).getValue());
                 break;
             case ValueType.TYPE:
-                typePool.intern(((TypeEncodedValue)encodedValue).getValue());
+                typeSection.intern(((TypeEncodedValue)encodedValue).getValue());
                 break;
             case ValueType.ENUM:
-                fieldPool.intern(((EnumEncodedValue)encodedValue).getValue());
+                fieldSection.intern(((EnumEncodedValue)encodedValue).getValue());
                 break;
             case ValueType.FIELD:
-                fieldPool.intern(((FieldEncodedValue)encodedValue).getValue());
+                fieldSection.intern(((FieldEncodedValue)encodedValue).getValue());
                 break;
             case ValueType.METHOD:
-                methodPool.intern(((MethodEncodedValue)encodedValue).getValue());
+                methodSection.intern(((MethodEncodedValue)encodedValue).getValue());
                 break;
         }
     }
+
+    protected class DexPoolSectionProvider extends SectionProvider {
+        @Nonnull @Override public StringPool getStringSection() {
+            return new StringPool(DexPool.this);
+        }
+
+        @Nonnull @Override public TypePool getTypeSection() {
+            return new TypePool(DexPool.this);
+        }
+
+        @Nonnull @Override public ProtoPool getProtoSection() {
+            return new ProtoPool(DexPool.this);
+        }
+
+        @Nonnull @Override public FieldPool getFieldSection() {
+            return new FieldPool(DexPool.this);
+        }
+
+        @Nonnull @Override public MethodPool getMethodSection() {
+            return new MethodPool(DexPool.this);
+        }
+
+        @Nonnull @Override public ClassPool getClassSection() {
+            return new ClassPool(DexPool.this);
+        }
+
+        @Nonnull @Override public TypeListPool getTypeListSection() {
+            return new TypeListPool(DexPool.this);
+        }
+
+        @Nonnull @Override public AnnotationPool getAnnotationSection() {
+            return new AnnotationPool(DexPool.this);
+        }
+
+        @Nonnull @Override public AnnotationSetPool getAnnotationSetSection() {
+            return new AnnotationSetPool(DexPool.this);
+        }
+    }
 }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/FieldPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/FieldPool.java
index 3403d12..135d79b 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/FieldPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/FieldPool.java
@@ -39,20 +39,17 @@
 
 public class FieldPool extends BaseIndexPool<FieldReference>
         implements FieldSection<CharSequence, CharSequence, FieldReference, Field> {
-    @Nonnull private final StringPool stringPool;
-    @Nonnull private final TypePool typePool;
 
-    public FieldPool(@Nonnull StringPool stringPool, @Nonnull TypePool typePool) {
-        this.stringPool = stringPool;
-        this.typePool = typePool;
+    public FieldPool(@Nonnull DexPool dexPool) {
+        super(dexPool);
     }
 
     public void intern(@Nonnull FieldReference field) {
         Integer prev = internedItems.put(field, 0);
         if (prev == null) {
-            typePool.intern(field.getDefiningClass());
-            stringPool.intern(field.getName());
-            typePool.intern(field.getType());
+            dexPool.typeSection.intern(field.getDefiningClass());
+            dexPool.stringSection.intern(field.getName());
+            dexPool.typeSection.intern(field.getType());
         }
     }
 
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/Markable.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/Markable.java
new file mode 100644
index 0000000..8b14574
--- /dev/null
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/Markable.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.dexlib2.writer.pool;
+
+public interface Markable {
+    void mark();
+    void reset();
+}
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/MethodPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/MethodPool.java
index 8103d31..2801abd 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/MethodPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/MethodPool.java
@@ -39,26 +39,24 @@
 
 public class MethodPool extends BaseIndexPool<MethodReference>
         implements MethodSection<CharSequence, CharSequence, MethodProtoReference, MethodReference, PoolMethod> {
-    @Nonnull private final StringPool stringPool;
-    @Nonnull private final TypePool typePool;
-    @Nonnull private final ProtoPool protoPool;
 
-    public MethodPool(@Nonnull StringPool stringPool, @Nonnull TypePool typePool,
-                      @Nonnull ProtoPool protoPool) {
-        this.stringPool = stringPool;
-        this.typePool = typePool;
-        this.protoPool = protoPool;
+    public MethodPool(@Nonnull DexPool dexPool) {
+        super(dexPool);
     }
 
     public void intern(@Nonnull MethodReference method) {
         Integer prev = internedItems.put(method, 0);
         if (prev == null) {
-            typePool.intern(method.getDefiningClass());
-            protoPool.intern(new PoolMethodProto(method));
-            stringPool.intern(method.getName());
+            dexPool.typeSection.intern(method.getDefiningClass());
+            dexPool.protoSection.intern(new PoolMethodProto(method));
+            dexPool.stringSection.intern(method.getName());
         }
     }
 
+    @Nonnull @Override public MethodReference getMethodReference(@Nonnull PoolMethod poolMethod) {
+        return poolMethod;
+    }
+
     @Nonnull @Override public CharSequence getDefiningClass(@Nonnull MethodReference methodReference) {
         return methodReference.getDefiningClass();
     }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ProtoPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ProtoPool.java
index 523e5f4..1209bd9 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ProtoPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/ProtoPool.java
@@ -43,23 +43,17 @@
 public class ProtoPool extends BaseIndexPool<MethodProtoReference>
         implements ProtoSection<CharSequence, CharSequence, MethodProtoReference,
         TypeListPool.Key<? extends Collection<? extends CharSequence>>> {
-    @Nonnull private final StringPool stringPool;
-    @Nonnull private final TypePool typePool;
-    @Nonnull private final TypeListPool typeListPool;
 
-    public ProtoPool(@Nonnull StringPool stringPool, @Nonnull TypePool typePool,
-                     @Nonnull TypeListPool typeListPool) {
-        this.stringPool = stringPool;
-        this.typePool = typePool;
-        this.typeListPool = typeListPool;
+    public ProtoPool(@Nonnull DexPool dexPool) {
+        super(dexPool);
     }
 
     public void intern(@Nonnull MethodProtoReference reference) {
         Integer prev = internedItems.put(reference, 0);
         if (prev == null) {
-            stringPool.intern(getShorty(reference));
-            typePool.intern(reference.getReturnType());
-            typeListPool.intern(reference.getParameterTypes());
+            dexPool.stringSection.intern(getShorty(reference));
+            dexPool.typeSection.intern(reference.getReturnType());
+            dexPool.typeListSection.intern(reference.getParameterTypes());
         }
     }
 
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringPool.java
index 5886b4f..61f1502 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringPool.java
@@ -39,6 +39,11 @@
 import javax.annotation.Nullable;
 
 public class StringPool extends StringTypeBasePool implements StringSection<CharSequence, StringReference> {
+
+    public StringPool(@Nonnull DexPool dexPool) {
+        super(dexPool);
+    }
+
     public void intern(@Nonnull CharSequence string) {
         internedItems.put(string.toString(), 0);
     }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringTypeBasePool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringTypeBasePool.java
index 768e562..54c6cea 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringTypeBasePool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/StringTypeBasePool.java
@@ -31,7 +31,6 @@
 
 package org.jf.dexlib2.writer.pool;
 
-import com.google.common.collect.Maps;
 import org.jf.dexlib2.writer.DexWriter;
 import org.jf.dexlib2.writer.NullableIndexSection;
 import org.jf.util.ExceptionWithContext;
@@ -41,8 +40,12 @@
 import java.util.Collection;
 import java.util.Map;
 
-public abstract class StringTypeBasePool implements NullableIndexSection<CharSequence> {
-    @Nonnull protected final Map<String, Integer> internedItems = Maps.newHashMap();
+public abstract class StringTypeBasePool extends BasePool<String, Integer>
+        implements NullableIndexSection<CharSequence>, Markable {
+
+    public StringTypeBasePool(@Nonnull DexPool dexPool) {
+        super(dexPool);
+    }
 
     @Nonnull @Override public Collection<Map.Entry<String, Integer>> getItems() {
         return internedItems.entrySet();
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypeListPool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypeListPool.java
index 7e0fbe0..038f4d1 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypeListPool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypeListPool.java
@@ -43,10 +43,10 @@
 
 public class TypeListPool extends BaseNullableOffsetPool<Key<? extends Collection<? extends CharSequence>>>
         implements TypeListSection<CharSequence, Key<? extends Collection<? extends CharSequence>>> {
-    @Nonnull private final TypePool typePool;
 
-    public TypeListPool(@Nonnull TypePool typePool) {
-        this.typePool = typePool;
+
+    public TypeListPool(@Nonnull DexPool dexPool) {
+        super(dexPool);
     }
 
     public void intern(@Nonnull Collection<? extends CharSequence> types) {
@@ -55,7 +55,7 @@
             Integer prev = internedItems.put(key, 0);
             if (prev == null) {
                 for (CharSequence type: types) {
-                    typePool.intern(type);
+                    dexPool.typeSection.intern(type);
                 }
             }
         }
diff --git a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypePool.java b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypePool.java
index 13bcd8a..7e43208 100644
--- a/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypePool.java
+++ b/dexlib2/src/main/java/org/jf/dexlib2/writer/pool/TypePool.java
@@ -39,17 +39,17 @@
 
 public class TypePool extends StringTypeBasePool
         implements TypeSection<CharSequence, CharSequence, TypeReference> {
-    @Nonnull private final StringPool stringPool;
 
-    public TypePool(@Nonnull StringPool stringPool) {
-        this.stringPool = stringPool;
+
+    public TypePool(@Nonnull DexPool dexPool) {
+        super(dexPool);
     }
 
     public void intern(@Nonnull CharSequence type) {
         String typeString = type.toString();
         Integer prev = internedItems.put(typeString, 0);
         if (prev == null) {
-            stringPool.intern(typeString);
+            dexPool.stringSection.intern(typeString);
         }
     }
 
diff --git a/dexlib2/src/test/java/org/jf/dexlib2/AccessorTest.java b/dexlib2/src/test/java/org/jf/dexlib2/AccessorTest.java
index 4c8f85b..ff832c2 100644
--- a/dexlib2/src/test/java/org/jf/dexlib2/AccessorTest.java
+++ b/dexlib2/src/test/java/org/jf/dexlib2/AccessorTest.java
@@ -79,7 +79,7 @@
     public void testAccessors() throws IOException {
         URL url = AccessorTest.class.getClassLoader().getResource("accessorTest.dex");
         Assert.assertNotNull(url);
-        DexFile f = DexFileFactory.loadDexFile(url.getFile(), 15, false);
+        DexFile f = DexFileFactory.loadDexFile(url.getFile(), Opcodes.getDefault());
 
         SyntheticAccessorResolver sar = new SyntheticAccessorResolver(f.getOpcodes(), f.getClasses());
 
diff --git a/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java b/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java
new file mode 100644
index 0000000..610d3c7
--- /dev/null
+++ b/dexlib2/src/test/java/org/jf/dexlib2/DexEntryFinderTest.java
@@ -0,0 +1,231 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.dexlib2;
+
+import com.beust.jcommander.internal.Maps;
+import com.google.common.collect.Lists;
+import org.jf.dexlib2.DexFileFactory.DexEntryFinder;
+import org.jf.dexlib2.DexFileFactory.DexFileNotFoundException;
+import org.jf.dexlib2.DexFileFactory.MultipleMatchingDexEntriesException;
+import org.jf.dexlib2.DexFileFactory.UnsupportedFileTypeException;
+import org.jf.dexlib2.dexbacked.DexBackedDexFile;
+import org.jf.dexlib2.dexbacked.DexBackedDexFile.NotADexFile;
+import org.jf.dexlib2.iface.MultiDexContainer;
+import org.junit.Assert;
+import org.junit.Test;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.io.IOException;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+
+import static org.mockito.Mockito.mock;
+
+public class DexEntryFinderTest {
+
+    @Test
+    public void testNormalStuff() throws Exception {
+        Map<String, DexBackedDexFile> entries = Maps.newHashMap();
+        DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class);
+        entries.put("/system/framework/framework.jar", dexFile1);
+        DexBackedDexFile dexFile2 = mock(DexBackedDexFile.class);
+        entries.put("/system/framework/framework.jar:classes2.dex", dexFile2);
+        DexEntryFinder testFinder = new DexEntryFinder("blah.oat", new TestMultiDexContainer(entries));
+
+        Assert.assertEquals(dexFile1, testFinder.findEntry("/system/framework/framework.jar", true));
+
+        assertEntryNotFound(testFinder, "system/framework/framework.jar", true);
+        assertEntryNotFound(testFinder, "/framework/framework.jar", true);
+        assertEntryNotFound(testFinder, "framework/framework.jar", true);
+        assertEntryNotFound(testFinder, "/framework.jar", true);
+        assertEntryNotFound(testFinder, "framework.jar", true);
+
+        Assert.assertEquals(dexFile1, testFinder.findEntry("system/framework/framework.jar", false));
+        Assert.assertEquals(dexFile1, testFinder.findEntry("/framework/framework.jar", false));
+        Assert.assertEquals(dexFile1, testFinder.findEntry("framework/framework.jar", false));
+        Assert.assertEquals(dexFile1, testFinder.findEntry("/framework.jar", false));
+        Assert.assertEquals(dexFile1, testFinder.findEntry("framework.jar", false));
+
+        assertEntryNotFound(testFinder, "ystem/framework/framework.jar", false);
+        assertEntryNotFound(testFinder, "ssystem/framework/framework.jar", false);
+        assertEntryNotFound(testFinder, "ramework/framework.jar", false);
+        assertEntryNotFound(testFinder, "ramework.jar", false);
+        assertEntryNotFound(testFinder, "framework", false);
+
+        Assert.assertEquals(dexFile2, testFinder.findEntry("/system/framework/framework.jar:classes2.dex", true));
+
+        assertEntryNotFound(testFinder, "system/framework/framework.jar:classes2.dex", true);
+        assertEntryNotFound(testFinder, "framework.jar:classes2.dex", true);
+        assertEntryNotFound(testFinder, "classes2.dex", true);
+
+        Assert.assertEquals(dexFile2, testFinder.findEntry("system/framework/framework.jar:classes2.dex", false));
+        Assert.assertEquals(dexFile2, testFinder.findEntry("/framework/framework.jar:classes2.dex", false));
+        Assert.assertEquals(dexFile2, testFinder.findEntry("framework/framework.jar:classes2.dex", false));
+        Assert.assertEquals(dexFile2, testFinder.findEntry("/framework.jar:classes2.dex", false));
+        Assert.assertEquals(dexFile2, testFinder.findEntry("framework.jar:classes2.dex", false));
+        Assert.assertEquals(dexFile2, testFinder.findEntry(":classes2.dex", false));
+        Assert.assertEquals(dexFile2, testFinder.findEntry("classes2.dex", false));
+
+        assertEntryNotFound(testFinder, "ystem/framework/framework.jar:classes2.dex", false);
+        assertEntryNotFound(testFinder, "ramework.jar:classes2.dex", false);
+        assertEntryNotFound(testFinder, "lasses2.dex", false);
+        assertEntryNotFound(testFinder, "classes2", false);
+    }
+
+    @Test
+    public void testSimilarEntries() throws Exception {
+        Map<String, DexBackedDexFile> entries = Maps.newHashMap();
+        DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class);
+        entries.put("/system/framework/framework.jar", dexFile1);
+        DexBackedDexFile dexFile2 = mock(DexBackedDexFile.class);
+        entries.put("system/framework/framework.jar", dexFile2);
+        DexEntryFinder testFinder = new DexEntryFinder("blah.oat", new TestMultiDexContainer(entries));
+
+        Assert.assertEquals(dexFile1, testFinder.findEntry("/system/framework/framework.jar", true));
+        Assert.assertEquals(dexFile2, testFinder.findEntry("system/framework/framework.jar", true));
+
+        assertMultipleMatchingEntries(testFinder, "/system/framework/framework.jar");
+        assertMultipleMatchingEntries(testFinder, "system/framework/framework.jar");
+
+        assertMultipleMatchingEntries(testFinder, "/framework/framework.jar");
+        assertMultipleMatchingEntries(testFinder, "framework/framework.jar");
+        assertMultipleMatchingEntries(testFinder, "/framework.jar");
+        assertMultipleMatchingEntries(testFinder, "framework.jar");
+    }
+
+    @Test
+    public void testMatchingSuffix() throws Exception {
+        Map<String, DexBackedDexFile> entries = Maps.newHashMap();
+        DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class);
+        entries.put("/system/framework/framework.jar", dexFile1);
+        DexBackedDexFile dexFile2 = mock(DexBackedDexFile.class);
+        entries.put("/framework/framework.jar", dexFile2);
+        DexEntryFinder testFinder = new DexEntryFinder("blah.oat", new TestMultiDexContainer(entries));
+
+        Assert.assertEquals(dexFile1, testFinder.findEntry("/system/framework/framework.jar", true));
+        Assert.assertEquals(dexFile2, testFinder.findEntry("/framework/framework.jar", true));
+
+        Assert.assertEquals(dexFile2, testFinder.findEntry("/framework/framework.jar", false));
+        Assert.assertEquals(dexFile2, testFinder.findEntry("framework/framework.jar", false));
+
+        assertMultipleMatchingEntries(testFinder, "/framework.jar");
+        assertMultipleMatchingEntries(testFinder, "framework.jar");
+    }
+
+    @Test
+    public void testNonDexEntries() throws Exception {
+        Map<String, DexBackedDexFile> entries = Maps.newHashMap();
+        DexBackedDexFile dexFile1 = mock(DexBackedDexFile.class);
+        entries.put("classes.dex", dexFile1);
+        entries.put("/blah/classes.dex", null);
+        DexEntryFinder testFinder = new DexEntryFinder("blah.oat", new TestMultiDexContainer(entries));
+
+        Assert.assertEquals(dexFile1, testFinder.findEntry("classes.dex", true));
+        Assert.assertEquals(dexFile1, testFinder.findEntry("classes.dex", false));
+
+        assertUnsupportedFileType(testFinder, "/blah/classes.dex", true);
+        assertDexFileNotFound(testFinder, "/blah/classes.dex", false);
+    }
+
+    private void assertEntryNotFound(DexEntryFinder finder, String entry, boolean exactMatch) throws IOException {
+        try {
+            finder.findEntry(entry, exactMatch);
+            Assert.fail();
+        } catch (DexFileNotFoundException ex) {
+            // expected exception
+        }
+    }
+
+    private void assertMultipleMatchingEntries(DexEntryFinder finder, String entry) throws IOException {
+        try {
+            finder.findEntry(entry, false);
+            Assert.fail();
+        } catch (MultipleMatchingDexEntriesException ex) {
+            // expected exception
+        }
+    }
+
+    private void assertUnsupportedFileType(DexEntryFinder finder, String entry, boolean exactMatch) throws IOException {
+        try {
+            finder.findEntry(entry, exactMatch);
+            Assert.fail();
+        } catch (UnsupportedFileTypeException ex) {
+            // expected exception
+        }
+    }
+
+    private void assertDexFileNotFound(DexEntryFinder finder, String entry, boolean exactMatch) throws IOException {
+        try {
+            finder.findEntry(entry, exactMatch);
+            Assert.fail();
+        } catch (DexFileNotFoundException ex) {
+            // expected exception
+        }
+    }
+
+    public static class TestMultiDexContainer implements MultiDexContainer<DexBackedDexFile> {
+        @Nonnull private final Map<String, DexBackedDexFile> entries;
+
+        public TestMultiDexContainer(@Nonnull Map<String, DexBackedDexFile> entries) {
+            this.entries = entries;
+        }
+
+        @Nonnull @Override public List<String> getDexEntryNames() throws IOException {
+            List<String> entryNames = Lists.newArrayList();
+
+            for (Entry<String, DexBackedDexFile> entry: entries.entrySet()) {
+                if (entry.getValue() != null) {
+                    entryNames.add(entry.getKey());
+                }
+            }
+
+            return entryNames;
+        }
+
+        @Nullable @Override public DexBackedDexFile getEntry(@Nonnull String entryName) throws IOException {
+            if (entries.containsKey(entryName)) {
+                DexBackedDexFile entry = entries.get(entryName);
+                if (entry == null) {
+                    throw new NotADexFile();
+                }
+                return entry;
+            }
+            return null;
+        }
+
+        @Nonnull @Override public Opcodes getOpcodes() {
+            return Opcodes.getDefault();
+        }
+    }
+}
diff --git a/dexlib2/src/test/java/org/jf/dexlib2/analysis/CommonSuperclassTest.java b/dexlib2/src/test/java/org/jf/dexlib2/analysis/CommonSuperclassTest.java
index 3f1ee56..d69dd81 100644
--- a/dexlib2/src/test/java/org/jf/dexlib2/analysis/CommonSuperclassTest.java
+++ b/dexlib2/src/test/java/org/jf/dexlib2/analysis/CommonSuperclassTest.java
@@ -32,8 +32,10 @@
 package org.jf.dexlib2.analysis;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Lists;
 import junit.framework.Assert;
 import org.jf.dexlib2.Opcodes;
+import org.jf.dexlib2.iface.ClassDef;
 import org.jf.dexlib2.immutable.ImmutableDexFile;
 import org.junit.Test;
 
@@ -51,49 +53,53 @@
     //     fivetwothree
     //   fivethree
 
-    private final ClassPath classPath;
+    private final ClassPath oldClassPath;
+    private final ClassPath newClassPath;
+
 
     public CommonSuperclassTest() throws IOException {
-        classPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.forApi(19),
-                ImmutableSet.of(
-                        TestUtils.makeClassDef("Ljava/lang/Object;", null),
-                        TestUtils.makeClassDef("Ltest/one;", "Ljava/lang/Object;"),
-                        TestUtils.makeClassDef("Ltest/two;", "Ljava/lang/Object;"),
-                        TestUtils.makeClassDef("Ltest/onetwo;", "Ltest/one;"),
-                        TestUtils.makeClassDef("Ltest/onetwothree;", "Ltest/onetwo;"),
-                        TestUtils.makeClassDef("Ltest/onethree;", "Ltest/one;"),
-                        TestUtils.makeClassDef("Ltest/fivetwo;", "Ltest/five;"),
-                        TestUtils.makeClassDef("Ltest/fivetwothree;", "Ltest/fivetwo;"),
-                        TestUtils.makeClassDef("Ltest/fivethree;", "Ltest/five;"),
-                        TestUtils.makeInterfaceDef("Ljava/lang/Cloneable;"),
-                        TestUtils.makeInterfaceDef("Ljava/io/Serializable;"),
+        ImmutableSet<ClassDef> classes = ImmutableSet.of(
+                TestUtils.makeClassDef("Ljava/lang/Object;", null),
+                TestUtils.makeClassDef("Ltest/one;", "Ljava/lang/Object;"),
+                TestUtils.makeClassDef("Ltest/two;", "Ljava/lang/Object;"),
+                TestUtils.makeClassDef("Ltest/onetwo;", "Ltest/one;"),
+                TestUtils.makeClassDef("Ltest/onetwothree;", "Ltest/onetwo;"),
+                TestUtils.makeClassDef("Ltest/onethree;", "Ltest/one;"),
+                TestUtils.makeClassDef("Ltest/fivetwo;", "Ltest/five;"),
+                TestUtils.makeClassDef("Ltest/fivetwothree;", "Ltest/fivetwo;"),
+                TestUtils.makeClassDef("Ltest/fivethree;", "Ltest/five;"),
+                TestUtils.makeInterfaceDef("Ljava/lang/Cloneable;"),
+                TestUtils.makeInterfaceDef("Ljava/io/Serializable;"),
 
-                        // basic class and interface
-                        TestUtils.makeClassDef("Liface/classiface1;", "Ljava/lang/Object;", "Liface/iface1;"),
-                        TestUtils.makeInterfaceDef("Liface/iface1;"),
+                // basic class and interface
+                TestUtils.makeClassDef("Liface/classiface1;", "Ljava/lang/Object;", "Liface/iface1;"),
+                TestUtils.makeInterfaceDef("Liface/iface1;"),
 
-                        // a more complex interface tree
-                        TestUtils.makeInterfaceDef("Liface/base1;"),
-                        // implements undefined interface
-                        TestUtils.makeInterfaceDef("Liface/sub1;", "Liface/base1;", "Liface/base2;"),
-                        // this implements sub1, so that its interfaces can't be fully resolved either
-                        TestUtils.makeInterfaceDef("Liface/sub2;", "Liface/base1;", "Liface/sub1;"),
-                        TestUtils.makeInterfaceDef("Liface/sub3;", "Liface/base1;"),
-                        TestUtils.makeInterfaceDef("Liface/sub4;", "Liface/base1;", "Liface/sub3;"),
-                        TestUtils.makeClassDef("Liface/classsub1;", "Ljava/lang/Object;", "Liface/sub1;"),
-                        TestUtils.makeClassDef("Liface/classsub2;", "Ljava/lang/Object;", "Liface/sub2;"),
-                        TestUtils.makeClassDef("Liface/classsub3;", "Ljava/lang/Object;", "Liface/sub3;",
-                                "Liface/base;"),
-                        TestUtils.makeClassDef("Liface/classsub4;", "Ljava/lang/Object;", "Liface/sub3;",
-                                "Liface/sub4;"),
-                        TestUtils.makeClassDef("Liface/classsubsub4;", "Liface/classsub4;"),
-                        TestUtils.makeClassDef("Liface/classsub1234;", "Ljava/lang/Object;", "Liface/sub1;",
-                                "Liface/sub2;", "Liface/sub3;", "Liface/sub4;")
-        ))));
+                // a more complex interface tree
+                TestUtils.makeInterfaceDef("Liface/base1;"),
+                // implements undefined interface
+                TestUtils.makeInterfaceDef("Liface/sub1;", "Liface/base1;", "Liface/base2;"),
+                // this implements sub1, so that its interfaces can't be fully resolved either
+                TestUtils.makeInterfaceDef("Liface/sub2;", "Liface/base1;", "Liface/sub1;"),
+                TestUtils.makeInterfaceDef("Liface/sub3;", "Liface/base1;"),
+                TestUtils.makeInterfaceDef("Liface/sub4;", "Liface/base1;", "Liface/sub3;"),
+                TestUtils.makeClassDef("Liface/classsub1;", "Ljava/lang/Object;", "Liface/sub1;"),
+                TestUtils.makeClassDef("Liface/classsub2;", "Ljava/lang/Object;", "Liface/sub2;"),
+                TestUtils.makeClassDef("Liface/classsub3;", "Ljava/lang/Object;", "Liface/sub3;",
+                        "Liface/base;"),
+                TestUtils.makeClassDef("Liface/classsub4;", "Ljava/lang/Object;", "Liface/sub3;",
+                        "Liface/sub4;"),
+                TestUtils.makeClassDef("Liface/classsubsub4;", "Liface/classsub4;"),
+                TestUtils.makeClassDef("Liface/classsub1234;", "Ljava/lang/Object;", "Liface/sub1;",
+                        "Liface/sub2;", "Liface/sub3;", "Liface/sub4;"));
+
+        oldClassPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.getDefault(), classes)));
+        newClassPath = new ClassPath(Lists.newArrayList(new DexClassProvider(
+                new ImmutableDexFile(Opcodes.forArtVersion(72), classes))), true, 72);
     }
 
-    public void superclassTest(String commonSuperclass,
-                                      String type1, String type2) {
+    public void superclassTest(ClassPath classPath, String commonSuperclass,
+                               String type1, String type2) {
         TypeProto commonSuperclassProto = classPath.getClass(commonSuperclass);
         TypeProto type1Proto = classPath.getClass(type1);
         TypeProto type2Proto = classPath.getClass(type2);
@@ -102,6 +108,11 @@
         Assert.assertSame(commonSuperclassProto, type2Proto.getCommonSuperclass(type1Proto));
     }
 
+    public void superclassTest(String commonSuperclass, String type1, String type2) {
+        superclassTest(oldClassPath, commonSuperclass, type1, type2);
+        superclassTest(newClassPath, commonSuperclass, type1, type2);
+    }
+
     @Test
     public void testGetCommonSuperclass() throws IOException {
         String object = "Ljava/lang/Object;";
@@ -131,7 +142,11 @@
         // same value, but different object
         Assert.assertEquals(
                 onetwo,
-                classPath.getClass(onetwo).getCommonSuperclass(new ClassProto(classPath, onetwo)).getType());
+                oldClassPath.getClass(onetwo).getCommonSuperclass(new ClassProto(oldClassPath, onetwo)).getType());
+
+        Assert.assertEquals(
+                onetwo,
+                newClassPath.getClass(onetwo).getCommonSuperclass(new ClassProto(newClassPath, onetwo)).getType());
 
         // other object is superclass
         superclassTest(object, object, one);
diff --git a/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java b/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java
index 90a6359..70e6a04 100644
--- a/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java
+++ b/dexlib2/src/test/java/org/jf/dexlib2/analysis/CustomMethodInlineTableTest.java
@@ -51,11 +51,12 @@
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.io.IOException;
 import java.util.List;
 
 public class CustomMethodInlineTableTest {
     @Test
-    public void testCustomMethodInlineTable_Virtual() {
+    public void testCustomMethodInlineTable_Virtual() throws IOException {
         List<ImmutableInstruction> instructions = Lists.newArrayList(
                 new ImmutableInstruction35mi(Opcode.EXECUTE_INLINE, 1, 0, 0, 0, 0, 0, 0),
                 new ImmutableInstruction10x(Opcode.RETURN_VOID));
@@ -67,10 +68,12 @@
         ClassDef classDef = new ImmutableClassDef("Lblah;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null,
                 null, null, null, null, null, ImmutableList.of(method));
 
-        DexFile dexFile = new ImmutableDexFile(Opcodes.forApi(19), ImmutableList.of(classDef));
+        DexFile dexFile = new ImmutableDexFile(Opcodes.getDefault(), ImmutableList.of(classDef));
 
-        ClassPath classPath = ClassPath.fromClassPath(ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile,
-                15, false);
+        ClassPathResolver resolver = new ClassPathResolver(ImmutableList.<String>of(),
+                ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile);
+        ClassPath classPath = new ClassPath(resolver.getResolvedClassProviders(), false, ClassPath.NOT_ART);
+
         InlineMethodResolver inlineMethodResolver = new CustomInlineMethodResolver(classPath, "Lblah;->blah()V");
         MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver, false);
 
@@ -82,7 +85,7 @@
     }
 
     @Test
-    public void testCustomMethodInlineTable_Static() {
+    public void testCustomMethodInlineTable_Static() throws IOException {
         List<ImmutableInstruction> instructions = Lists.newArrayList(
                 new ImmutableInstruction35mi(Opcode.EXECUTE_INLINE, 1, 0, 0, 0, 0, 0, 0),
                 new ImmutableInstruction10x(Opcode.RETURN_VOID));
@@ -94,10 +97,12 @@
         ClassDef classDef = new ImmutableClassDef("Lblah;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null,
                 null, null, null, null, ImmutableList.of(method), null);
 
-        DexFile dexFile = new ImmutableDexFile(Opcodes.forApi(19), ImmutableList.of(classDef));
+        DexFile dexFile = new ImmutableDexFile(Opcodes.getDefault(), ImmutableList.of(classDef));
 
-        ClassPath classPath = ClassPath.fromClassPath(ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile,
-                15, false);
+        ClassPathResolver resolver = new ClassPathResolver(ImmutableList.<String>of(),
+                ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile);
+        ClassPath classPath = new ClassPath(resolver.getResolvedClassProviders(), false, ClassPath.NOT_ART);
+
         InlineMethodResolver inlineMethodResolver = new CustomInlineMethodResolver(classPath, "Lblah;->blah()V");
         MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver, false);
 
@@ -109,7 +114,7 @@
     }
 
     @Test
-    public void testCustomMethodInlineTable_Direct() {
+    public void testCustomMethodInlineTable_Direct() throws IOException {
         List<ImmutableInstruction> instructions = Lists.newArrayList(
                 new ImmutableInstruction35mi(Opcode.EXECUTE_INLINE, 1, 0, 0, 0, 0, 0, 0),
                 new ImmutableInstruction10x(Opcode.RETURN_VOID));
@@ -121,10 +126,12 @@
         ClassDef classDef = new ImmutableClassDef("Lblah;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null,
                 null, null, null, null, ImmutableList.of(method), null);
 
-        DexFile dexFile = new ImmutableDexFile(Opcodes.forApi(19), ImmutableList.of(classDef));
+        DexFile dexFile = new ImmutableDexFile(Opcodes.getDefault(), ImmutableList.of(classDef));
 
-        ClassPath classPath = ClassPath.fromClassPath(ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile,
-                15, false);
+        ClassPathResolver resolver = new ClassPathResolver(ImmutableList.<String>of(),
+                ImmutableList.<String>of(), ImmutableList.<String>of(), dexFile);
+        ClassPath classPath = new ClassPath(resolver.getResolvedClassProviders(), false, ClassPath.NOT_ART);
+
         InlineMethodResolver inlineMethodResolver = new CustomInlineMethodResolver(classPath, "Lblah;->blah()V");
         MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, inlineMethodResolver, false);
 
diff --git a/dexlib2/src/test/java/org/jf/dexlib2/analysis/MethodAnalyzerTest.java b/dexlib2/src/test/java/org/jf/dexlib2/analysis/MethodAnalyzerTest.java
new file mode 100644
index 0000000..215ba17
--- /dev/null
+++ b/dexlib2/src/test/java/org/jf/dexlib2/analysis/MethodAnalyzerTest.java
@@ -0,0 +1,260 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.dexlib2.analysis;
+
+import com.google.common.collect.Lists;
+import org.jf.dexlib2.AccessFlags;
+import org.jf.dexlib2.Opcode;
+import org.jf.dexlib2.Opcodes;
+import org.jf.dexlib2.builder.MethodImplementationBuilder;
+import org.jf.dexlib2.builder.instruction.BuilderInstruction10x;
+import org.jf.dexlib2.builder.instruction.BuilderInstruction12x;
+import org.jf.dexlib2.builder.instruction.BuilderInstruction21t;
+import org.jf.dexlib2.builder.instruction.BuilderInstruction22c;
+import org.jf.dexlib2.iface.ClassDef;
+import org.jf.dexlib2.iface.DexFile;
+import org.jf.dexlib2.iface.Method;
+import org.jf.dexlib2.iface.MethodImplementation;
+import org.jf.dexlib2.immutable.ImmutableClassDef;
+import org.jf.dexlib2.immutable.ImmutableDexFile;
+import org.jf.dexlib2.immutable.ImmutableMethod;
+import org.jf.dexlib2.immutable.ImmutableMethodParameter;
+import org.jf.dexlib2.immutable.reference.ImmutableTypeReference;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+
+import static org.jf.dexlib2.Opcodes.forArtVersion;
+
+public class MethodAnalyzerTest {
+
+    @Test
+    public void testInstanceOfNarrowingEqz_art() throws IOException {
+        MethodImplementationBuilder builder = new MethodImplementationBuilder(2);
+
+        builder.addInstruction(new BuilderInstruction22c(Opcode.INSTANCE_OF, 0, 1,
+                new ImmutableTypeReference("Lmain;")));
+        builder.addInstruction(new BuilderInstruction21t(Opcode.IF_EQZ, 0, builder.getLabel("not_instance_of")));
+        builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
+
+        builder.addLabel("not_instance_of");
+        builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
+
+        MethodImplementation methodImplementation = builder.getMethodImplementation();
+
+        Method method = new ImmutableMethod("Lmain;", "narrowing",
+                Collections.singletonList(new ImmutableMethodParameter("Ljava/lang/Object;", null, null)), "V",
+                AccessFlags.PUBLIC.getValue(), null, methodImplementation);
+        ClassDef classDef = new ImmutableClassDef("Lmain;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null,
+                null, null, null, Collections.singletonList(method));
+        DexFile dexFile = new ImmutableDexFile(forArtVersion(56), Collections.singletonList(classDef));
+
+        ClassPath classPath = new ClassPath(Lists.newArrayList(new DexClassProvider(dexFile)), true, 56);
+        MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false);
+
+        List<AnalyzedInstruction> analyzedInstructions = methodAnalyzer.getAnalyzedInstructions();
+        Assert.assertEquals("Lmain;", analyzedInstructions.get(2).getPreInstructionRegisterType(1).type.getType());
+
+        Assert.assertEquals("Ljava/lang/Object;",
+                analyzedInstructions.get(3).getPreInstructionRegisterType(1).type.getType());
+    }
+
+    @Test
+    public void testInstanceOfNarrowingEqz_dalvik() throws IOException {
+        MethodImplementationBuilder builder = new MethodImplementationBuilder(2);
+
+        builder.addInstruction(new BuilderInstruction22c(Opcode.INSTANCE_OF, 0, 1,
+                new ImmutableTypeReference("Lmain;")));
+        builder.addInstruction(new BuilderInstruction21t(Opcode.IF_EQZ, 0, builder.getLabel("not_instance_of")));
+        builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
+
+        builder.addLabel("not_instance_of");
+        builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
+
+        MethodImplementation methodImplementation = builder.getMethodImplementation();
+
+        Method method = new ImmutableMethod("Lmain;", "narrowing",
+                Collections.singletonList(new ImmutableMethodParameter("Ljava/lang/Object;", null, null)), "V",
+                AccessFlags.PUBLIC.getValue(), null, methodImplementation);
+        ClassDef classDef = new ImmutableClassDef("Lmain;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null,
+                null, null, null, Collections.singletonList(method));
+        DexFile dexFile = new ImmutableDexFile(Opcodes.forApi(19), Collections.singletonList(classDef));
+
+        ClassPath classPath = new ClassPath(new DexClassProvider(dexFile));
+        MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false);
+
+        List<AnalyzedInstruction> analyzedInstructions = methodAnalyzer.getAnalyzedInstructions();
+        Assert.assertEquals("Ljava/lang/Object;",
+                analyzedInstructions.get(2).getPreInstructionRegisterType(1).type.getType());
+
+        Assert.assertEquals("Ljava/lang/Object;",
+                analyzedInstructions.get(3).getPreInstructionRegisterType(1).type.getType());
+    }
+
+    @Test
+    public void testInstanceOfNarrowingNez_art() throws IOException {
+        MethodImplementationBuilder builder = new MethodImplementationBuilder(2);
+
+        builder.addInstruction(new BuilderInstruction22c(Opcode.INSTANCE_OF, 0, 1,
+                new ImmutableTypeReference("Lmain;")));
+        builder.addInstruction(new BuilderInstruction21t(Opcode.IF_NEZ, 0, builder.getLabel("instance_of")));
+        builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
+
+        builder.addLabel("instance_of");
+        builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
+
+        MethodImplementation methodImplementation = builder.getMethodImplementation();
+
+        Method method = new ImmutableMethod("Lmain;", "narrowing",
+                Collections.singletonList(new ImmutableMethodParameter("Ljava/lang/Object;", null, null)), "V",
+                AccessFlags.PUBLIC.getValue(), null, methodImplementation);
+        ClassDef classDef = new ImmutableClassDef("Lmain;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null,
+                null, null, null, Collections.singletonList(method));
+        DexFile dexFile = new ImmutableDexFile(forArtVersion(56), Collections.singletonList(classDef));
+
+        ClassPath classPath = new ClassPath(Lists.newArrayList(new DexClassProvider(dexFile)), true, 56);
+        MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false);
+
+        List<AnalyzedInstruction> analyzedInstructions = methodAnalyzer.getAnalyzedInstructions();
+        Assert.assertEquals("Ljava/lang/Object;",
+                analyzedInstructions.get(2).getPreInstructionRegisterType(1).type.getType());
+
+        Assert.assertEquals("Lmain;", analyzedInstructions.get(3).getPreInstructionRegisterType(1).type.getType());
+    }
+
+    @Test
+    public void testInstanceOfNarrowingNez_dalvik() throws IOException {
+        MethodImplementationBuilder builder = new MethodImplementationBuilder(2);
+
+        builder.addInstruction(new BuilderInstruction22c(Opcode.INSTANCE_OF, 0, 1,
+                new ImmutableTypeReference("Lmain;")));
+        builder.addInstruction(new BuilderInstruction21t(Opcode.IF_NEZ, 0, builder.getLabel("instance_of")));
+        builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
+
+        builder.addLabel("instance_of");
+        builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
+
+        MethodImplementation methodImplementation = builder.getMethodImplementation();
+
+        Method method = new ImmutableMethod("Lmain;", "narrowing",
+                Collections.singletonList(new ImmutableMethodParameter("Ljava/lang/Object;", null, null)), "V",
+                AccessFlags.PUBLIC.getValue(), null, methodImplementation);
+        ClassDef classDef = new ImmutableClassDef("Lmain;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null,
+                null, null, null, Collections.singletonList(method));
+        DexFile dexFile = new ImmutableDexFile(Opcodes.getDefault(), Collections.singletonList(classDef));
+
+        ClassPath classPath = new ClassPath(new DexClassProvider(dexFile));
+        MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false);
+
+        List<AnalyzedInstruction> analyzedInstructions = methodAnalyzer.getAnalyzedInstructions();
+        Assert.assertEquals("Ljava/lang/Object;",
+                analyzedInstructions.get(2).getPreInstructionRegisterType(1).type.getType());
+
+        Assert.assertEquals("Ljava/lang/Object;",
+                analyzedInstructions.get(3).getPreInstructionRegisterType(1).type.getType());
+    }
+
+    @Test
+    public void testInstanceOfNarrowingAfterMove_art() throws IOException {
+        MethodImplementationBuilder builder = new MethodImplementationBuilder(3);
+
+        builder.addInstruction(new BuilderInstruction12x(Opcode.MOVE_OBJECT, 1, 2));
+        builder.addInstruction(new BuilderInstruction22c(Opcode.INSTANCE_OF, 0, 1,
+                new ImmutableTypeReference("Lmain;")));
+        builder.addInstruction(new BuilderInstruction21t(Opcode.IF_EQZ, 0, builder.getLabel("not_instance_of")));
+        builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
+
+        builder.addLabel("not_instance_of");
+        builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
+
+        MethodImplementation methodImplementation = builder.getMethodImplementation();
+
+        Method method = new ImmutableMethod("Lmain;", "narrowing",
+                Collections.singletonList(new ImmutableMethodParameter("Ljava/lang/Object;", null, null)), "V",
+                AccessFlags.PUBLIC.getValue(), null, methodImplementation);
+        ClassDef classDef = new ImmutableClassDef("Lmain;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null,
+                null, null, null, Collections.singletonList(method));
+        DexFile dexFile = new ImmutableDexFile(forArtVersion(56), Collections.singletonList(classDef));
+
+        ClassPath classPath = new ClassPath(Lists.newArrayList(new DexClassProvider(dexFile)), true, 56);
+        MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false);
+
+        List<AnalyzedInstruction> analyzedInstructions = methodAnalyzer.getAnalyzedInstructions();
+        Assert.assertEquals("Lmain;", analyzedInstructions.get(3).getPreInstructionRegisterType(1).type.getType());
+        Assert.assertEquals("Lmain;", analyzedInstructions.get(3).getPreInstructionRegisterType(2).type.getType());
+
+        Assert.assertEquals("Ljava/lang/Object;",
+                analyzedInstructions.get(4).getPreInstructionRegisterType(1).type.getType());
+        Assert.assertEquals("Ljava/lang/Object;",
+                analyzedInstructions.get(4).getPreInstructionRegisterType(2).type.getType());
+    }
+
+    @Test
+    public void testInstanceOfNarrowingAfterMove_dalvik() throws IOException {
+        MethodImplementationBuilder builder = new MethodImplementationBuilder(3);
+
+        builder.addInstruction(new BuilderInstruction12x(Opcode.MOVE_OBJECT, 1, 2));
+        builder.addInstruction(new BuilderInstruction22c(Opcode.INSTANCE_OF, 0, 1,
+                new ImmutableTypeReference("Lmain;")));
+        builder.addInstruction(new BuilderInstruction21t(Opcode.IF_EQZ, 0, builder.getLabel("not_instance_of")));
+        builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
+
+        builder.addLabel("not_instance_of");
+        builder.addInstruction(new BuilderInstruction10x(Opcode.RETURN_VOID));
+
+        MethodImplementation methodImplementation = builder.getMethodImplementation();
+
+        Method method = new ImmutableMethod("Lmain;", "narrowing",
+                Collections.singletonList(new ImmutableMethodParameter("Ljava/lang/Object;", null, null)), "V",
+                AccessFlags.PUBLIC.getValue(), null, methodImplementation);
+        ClassDef classDef = new ImmutableClassDef("Lmain;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null,
+                null, null, null, Collections.singletonList(method));
+        DexFile dexFile = new ImmutableDexFile(Opcodes.getDefault(), Collections.singletonList(classDef));
+
+        ClassPath classPath = new ClassPath(new DexClassProvider(dexFile));
+        MethodAnalyzer methodAnalyzer = new MethodAnalyzer(classPath, method, null, false);
+
+        List<AnalyzedInstruction> analyzedInstructions = methodAnalyzer.getAnalyzedInstructions();
+        Assert.assertEquals("Ljava/lang/Object;",
+                analyzedInstructions.get(3).getPreInstructionRegisterType(1).type.getType());
+        Assert.assertEquals("Ljava/lang/Object;",
+                analyzedInstructions.get(3).getPreInstructionRegisterType(2).type.getType());
+
+        Assert.assertEquals("Ljava/lang/Object;",
+                analyzedInstructions.get(4).getPreInstructionRegisterType(1).type.getType());
+        Assert.assertEquals("Ljava/lang/Object;",
+                analyzedInstructions.get(4).getPreInstructionRegisterType(2).type.getType());
+    }
+}
diff --git a/dexlib2/src/test/java/org/jf/dexlib2/analysis/util/SuperclassChainTest.java b/dexlib2/src/test/java/org/jf/dexlib2/analysis/util/SuperclassChainTest.java
index 84cd284..78bc8a5 100644
--- a/dexlib2/src/test/java/org/jf/dexlib2/analysis/util/SuperclassChainTest.java
+++ b/dexlib2/src/test/java/org/jf/dexlib2/analysis/util/SuperclassChainTest.java
@@ -57,7 +57,7 @@
         ImmutableSet<ClassDef> classes = ImmutableSet.<ClassDef>of(
                 objectClassDef, oneClassDef, twoClassDef, threeClassDef);
 
-        ClassPath classPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.forApi(19), classes)));
+        ClassPath classPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.getDefault(), classes)));
 
         TypeProto objectClassProto = classPath.getClass("Ljava/lang/Object;");
         TypeProto oneClassProto = classPath.getClass("Ltest/one;");
@@ -88,7 +88,7 @@
         ClassDef twoClassDef = TestUtils.makeClassDef("Ltest/two;", "Ltest/one;");
         ClassDef threeClassDef = TestUtils.makeClassDef("Ltest/three;", "Ltest/two;");
         ImmutableSet<ClassDef> classes = ImmutableSet.<ClassDef>of(twoClassDef, threeClassDef);
-        ClassPath classPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.forApi(19), classes)));
+        ClassPath classPath = new ClassPath(new DexClassProvider(new ImmutableDexFile(Opcodes.getDefault(), classes)));
 
         TypeProto unknownClassProto = classPath.getUnknownClass();
         TypeProto oneClassProto = classPath.getClass("Ltest/one;");
diff --git a/dexlib2/src/test/java/org/jf/dexlib2/pool/RollbackTest.java b/dexlib2/src/test/java/org/jf/dexlib2/pool/RollbackTest.java
new file mode 100644
index 0000000..6074de1
--- /dev/null
+++ b/dexlib2/src/test/java/org/jf/dexlib2/pool/RollbackTest.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.dexlib2.pool;
+
+import com.google.common.collect.Lists;
+import org.jf.dexlib2.AccessFlags;
+import org.jf.dexlib2.AnnotationVisibility;
+import org.jf.dexlib2.Opcodes;
+import org.jf.dexlib2.dexbacked.raw.MapItem;
+import org.jf.dexlib2.dexbacked.raw.RawDexFile;
+import org.jf.dexlib2.iface.ClassDef;
+import org.jf.dexlib2.iface.Field;
+import org.jf.dexlib2.iface.Method;
+import org.jf.dexlib2.iface.MethodParameter;
+import org.jf.dexlib2.immutable.*;
+import org.jf.dexlib2.writer.io.MemoryDataStore;
+import org.jf.dexlib2.writer.pool.DexPool;
+import org.junit.Assert;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.List;
+
+public class RollbackTest {
+    @Test
+    public void testRollback() throws IOException {
+        ClassDef class1 = new ImmutableClassDef("Lcls1;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, null,
+                Lists.newArrayList(new ImmutableAnnotation(AnnotationVisibility.RUNTIME, "Lannotation;", null)),
+                Lists.<Field>newArrayList(
+                        new ImmutableField("Lcls1;", "field1", "I", AccessFlags.PUBLIC.getValue(), null, null)
+                ),
+                Lists.<Method>newArrayList(
+                        new ImmutableMethod("Lcls1", "method1",
+                                Lists.<MethodParameter>newArrayList(new ImmutableMethodParameter("L", null, null)), "V",
+                                AccessFlags.PUBLIC.getValue(), null, null))
+                );
+
+        ClassDef class2 = new ImmutableClassDef("Lcls2;", AccessFlags.PUBLIC.getValue(), "Ljava/lang/Object;", null, null,
+                Lists.newArrayList(new ImmutableAnnotation(AnnotationVisibility.RUNTIME, "Lannotation2;", null)),
+                Lists.<Field>newArrayList(
+                        new ImmutableField("Lcls2;", "field2", "D", AccessFlags.PUBLIC.getValue(), null, null)
+                ),
+                Lists.<Method>newArrayList(
+                        new ImmutableMethod("Lcls2;", "method2",
+                                Lists.<MethodParameter>newArrayList(new ImmutableMethodParameter("D", null, null)), "V",
+                                AccessFlags.PUBLIC.getValue(), null, null))
+        );
+
+        RawDexFile dexFile1;
+        {
+            MemoryDataStore dataStore = new MemoryDataStore();
+            DexPool dexPool = new DexPool(Opcodes.getDefault());
+            dexPool.internClass(class1);
+            dexPool.mark();
+            dexPool.internClass(class2);
+            dexPool.reset();
+            dexPool.writeTo(dataStore);
+            dexFile1 = new RawDexFile(Opcodes.getDefault(), dataStore.getData());
+        }
+
+        RawDexFile dexFile2;
+        {
+            MemoryDataStore dataStore = new MemoryDataStore();
+            DexPool dexPool = new DexPool(Opcodes.getDefault());
+            dexPool.internClass(class1);
+            dexPool.writeTo(dataStore);
+            dexFile2 = new RawDexFile(Opcodes.getDefault(), dataStore.getData());
+        }
+
+        List<MapItem> mapItems1 = dexFile1.getMapItems();
+        List<MapItem> mapItems2 = dexFile2.getMapItems();
+        for (int i=0; i<mapItems1.size(); i++) {
+            Assert.assertEquals(mapItems1.get(i).getType(), mapItems2.get(i).getType());
+            Assert.assertEquals(mapItems1.get(i).getItemCount(), mapItems2.get(i).getItemCount());
+        }
+    }
+}
diff --git a/dexlib2/src/test/java/org/jf/dexlib2/writer/DexWriterTest.java b/dexlib2/src/test/java/org/jf/dexlib2/writer/DexWriterTest.java
index 1a0a289..bf55e37 100644
--- a/dexlib2/src/test/java/org/jf/dexlib2/writer/DexWriterTest.java
+++ b/dexlib2/src/test/java/org/jf/dexlib2/writer/DexWriterTest.java
@@ -72,12 +72,12 @@
         MemoryDataStore dataStore = new MemoryDataStore();
 
         try {
-            DexPool.writeTo(dataStore, new ImmutableDexFile(Opcodes.forApi(19), ImmutableSet.of(classDef)));
+            DexPool.writeTo(dataStore, new ImmutableDexFile(Opcodes.getDefault(), ImmutableSet.of(classDef)));
         } catch (IOException ex) {
             throw new RuntimeException(ex);
         }
 
-        DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.forApi(15), dataStore.getData());
+        DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), dataStore.getData());
         ClassDef dbClassDef = Iterables.getFirst(dexFile.getClasses(), null);
         Assert.assertNotNull(dbClassDef);
         Annotation dbAnnotation = Iterables.getFirst(dbClassDef.getAnnotations(), null);
@@ -112,12 +112,12 @@
         MemoryDataStore dataStore = new MemoryDataStore();
 
         try {
-            DexPool.writeTo(dataStore, new ImmutableDexFile(Opcodes.forApi(19), ImmutableSet.of(classDef)));
+            DexPool.writeTo(dataStore, new ImmutableDexFile(Opcodes.getDefault(), ImmutableSet.of(classDef)));
         } catch (IOException ex) {
             throw new RuntimeException(ex);
         }
 
-        DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.forApi(15), dataStore.getData());
+        DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), dataStore.getData());
         ClassDef dbClassDef = Iterables.getFirst(dexFile.getClasses(), null);
         Assert.assertNotNull(dbClassDef);
         Annotation dbAnnotation = Iterables.getFirst(dbClassDef.getAnnotations(), null);
diff --git a/dexlib2/src/test/java/org/jf/dexlib2/writer/JumboStringConversionTest.java b/dexlib2/src/test/java/org/jf/dexlib2/writer/JumboStringConversionTest.java
index c246e0e..340b1fa 100644
--- a/dexlib2/src/test/java/org/jf/dexlib2/writer/JumboStringConversionTest.java
+++ b/dexlib2/src/test/java/org/jf/dexlib2/writer/JumboStringConversionTest.java
@@ -62,7 +62,7 @@
 public class JumboStringConversionTest {
     @Test
     public void testJumboStringConversion() throws IOException {
-        DexBuilder dexBuilder = DexBuilder.makeDexBuilder(Opcodes.forApi(15));
+        DexBuilder dexBuilder = new DexBuilder(Opcodes.getDefault());
 
         MethodImplementationBuilder methodBuilder = new MethodImplementationBuilder(1);
         for (int i=0; i<66000; i++) {
@@ -92,7 +92,7 @@
         MemoryDataStore dexStore = new MemoryDataStore();
         dexBuilder.writeTo(dexStore);
 
-        DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.forApi(15), dexStore.getData());
+        DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), dexStore.getData());
 
         ClassDef classDef = Iterables.getFirst(dexFile.getClasses(), null);
         Assert.assertNotNull(classDef);
@@ -122,7 +122,7 @@
 
     @Test
     public void testJumboStringConversion_NonMethodBuilder() throws IOException {
-        DexBuilder dexBuilder = DexBuilder.makeDexBuilder(Opcodes.forApi(15));
+        DexBuilder dexBuilder = new DexBuilder(Opcodes.getDefault());
 
         final List<Instruction> instructions = Lists.newArrayList();
         for (int i=0; i<66000; i++) {
@@ -189,7 +189,7 @@
         MemoryDataStore dexStore = new MemoryDataStore();
         dexBuilder.writeTo(dexStore);
 
-        DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.forApi(15), dexStore.getData());
+        DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.getDefault(), dexStore.getData());
 
         ClassDef classDef = Iterables.getFirst(dexFile.getClasses(), null);
         Assert.assertNotNull(classDef);
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
index 5ccda13..6ffa237 100644
--- a/gradle/wrapper/gradle-wrapper.jar
+++ b/gradle/wrapper/gradle-wrapper.jar
Binary files differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 23bc0f5..8ee9c63 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,6 @@
-#Fri Jul 08 16:46:58 PDT 2016
+#Wed Sep 28 23:22:26 AEST 2016
 distributionBase=GRADLE_USER_HOME
 distributionPath=wrapper/dists
 zipStoreBase=GRADLE_USER_HOME
 zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-2.14-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-3.1-all.zip
diff --git a/gradlew b/gradlew
index 9d82f78..9aa616c 100755
--- a/gradlew
+++ b/gradlew
@@ -6,12 +6,30 @@
 ##
 ##############################################################################
 
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+        PRG="$link"
+    else
+        PRG=`dirname "$PRG"`"/$link"
+    fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
 
 APP_NAME="Gradle"
 APP_BASE_NAME=`basename "$0"`
 
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
 # Use the maximum available, or set MAX_FD != -1 to use that value.
 MAX_FD="maximum"
 
@@ -30,6 +48,7 @@
 cygwin=false
 msys=false
 darwin=false
+nonstop=false
 case "`uname`" in
   CYGWIN* )
     cygwin=true
@@ -40,26 +59,11 @@
   MINGW* )
     msys=true
     ;;
+  NONSTOP* )
+    nonstop=true
+    ;;
 esac
 
-# Attempt to set APP_HOME
-# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
-    ls=`ls -ld "$PRG"`
-    link=`expr "$ls" : '.*-> \(.*\)$'`
-    if expr "$link" : '/.*' > /dev/null; then
-        PRG="$link"
-    else
-        PRG=`dirname "$PRG"`"/$link"
-    fi
-done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
 CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
 
 # Determine the Java command to use to start the JVM.
@@ -85,7 +89,7 @@
 fi
 
 # Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
     MAX_FD_LIMIT=`ulimit -H -n`
     if [ $? -eq 0 ] ; then
         if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
@@ -157,4 +161,9 @@
 eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
 JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
 
+# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
+if [[ "$(uname)" == "Darwin" ]] && [[ "$HOME" == "$PWD" ]]; then
+  cd "$(dirname "$0")"
+fi
+
 exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
index 72d362d..e95643d 100644
--- a/gradlew.bat
+++ b/gradlew.bat
@@ -8,14 +8,14 @@
 @rem Set local scope for the variables with windows NT shell

 if "%OS%"=="Windows_NT" setlocal

 

-@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

-set DEFAULT_JVM_OPTS=

-

 set DIRNAME=%~dp0

 if "%DIRNAME%" == "" set DIRNAME=.

 set APP_BASE_NAME=%~n0

 set APP_HOME=%DIRNAME%

 

+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.

+set DEFAULT_JVM_OPTS=

+

 @rem Find java.exe

 if defined JAVA_HOME goto findJavaFromJavaHome

 

@@ -49,7 +49,6 @@
 @rem Get command-line arguments, handling Windows variants

 

 if not "%OS%" == "Windows_NT" goto win9xME_args

-if "%@eval[2+2]" == "4" goto 4NT_args

 

 :win9xME_args

 @rem Slurp the command line arguments.

@@ -60,11 +59,6 @@
 if "x%~1" == "x" goto execute

 

 set CMD_LINE_ARGS=%*

-goto execute

-

-:4NT_args

-@rem Get arguments from the 4NT Shell from JP Software

-set CMD_LINE_ARGS=%$

 

 :execute

 @rem Setup the command line

diff --git a/smali/build.gradle b/smali/build.gradle
index 318b5a9..6472f21 100644
--- a/smali/build.gradle
+++ b/smali/build.gradle
@@ -76,8 +76,8 @@
     compile project(':util')
     compile project(':dexlib2')
     compile depends.antlr_runtime
+    compile depends.jcommander
     compile depends.stringtemplate
-    compile depends.commons_cli
 
     testCompile depends.junit
 
@@ -95,7 +95,7 @@
     classifier = 'fat'
 
     manifest {
-        attributes('Main-Class': 'org.jf.smali.main')
+        attributes('Main-Class': 'org.jf.smali.Main')
     }
 
     doLast {
@@ -141,7 +141,8 @@
     dontobfuscate
     dontoptimize
 
-    keep 'public class org.jf.smali.main { public static void main(java.lang.String[]); }'
+    keep 'public class org.jf.smali.Main { public static void main(java.lang.String[]); }'
+    keep 'class com.beust.jcommander.** { *; }'
     keepclassmembers 'enum * { public static **[] values(); public static ** valueOf(java.lang.String); }'
 
     dontwarn 'com.google.common.**'
diff --git a/smali/src/main/antlr/smaliParser.g b/smali/src/main/antlr/smaliParser.g
index 29cd141..2d5ecca 100644
--- a/smali/src/main/antlr/smaliParser.g
+++ b/smali/src/main/antlr/smaliParser.g
@@ -263,8 +263,8 @@
       this.allowOdex = allowOdex;
   }
 
-  public void setApiLevel(int apiLevel, boolean experimental) {
-      this.opcodes = new Opcodes(apiLevel, experimental);
+  public void setApiLevel(int apiLevel) {
+      this.opcodes = Opcodes.forApi(apiLevel);
       this.apiLevel = apiLevel;
   }
 
diff --git a/smali/src/main/antlr/smaliTreeWalker.g b/smali/src/main/antlr/smaliTreeWalker.g
index d074579..171756e 100644
--- a/smali/src/main/antlr/smaliTreeWalker.g
+++ b/smali/src/main/antlr/smaliTreeWalker.g
@@ -85,8 +85,8 @@
       this.dexBuilder = dexBuilder;
   }
 
-  public void setApiLevel(int apiLevel, boolean experimental) {
-      this.opcodes = new Opcodes(apiLevel, experimental);
+  public void setApiLevel(int apiLevel) {
+      this.opcodes = Opcodes.forApi(apiLevel);
       this.apiLevel = apiLevel;
   }
 
diff --git a/smali/src/main/java/org/jf/smali/AssembleCommand.java b/smali/src/main/java/org/jf/smali/AssembleCommand.java
new file mode 100644
index 0000000..efde182
--- /dev/null
+++ b/smali/src/main/java/org/jf/smali/AssembleCommand.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.smali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import com.beust.jcommander.validators.PositiveInteger;
+import org.jf.util.jcommander.Command;
+import org.jf.util.jcommander.ExtendedParameter;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.util.List;
+
+@Parameters(commandDescription = "Assembles smali files into a dex file.")
+@ExtendedParameters(
+        commandName = "assemble",
+        commandAliases = { "ass", "as", "a" })
+public class AssembleCommand extends Command {
+
+    @Parameter(names = {"-h", "-?", "--help"}, help = true,
+            description = "Show usage information for this command.")
+    private boolean help;
+
+    @Parameter(names = {"-j", "--jobs"},
+            description = "The number of threads to use. Defaults to the number of cores available.",
+            validateWith = PositiveInteger.class)
+    @ExtendedParameter(argumentNames = "n")
+    private int jobs = Runtime.getRuntime().availableProcessors();
+
+    @Parameter(names = {"-a", "--api"},
+            description = "The numeric api level to use while assembling.")
+    @ExtendedParameter(argumentNames = "api")
+    private int apiLevel = 15;
+
+    @Parameter(names = {"-o", "--output"},
+            description = "The name/path of the dex file to write.")
+    @ExtendedParameter(argumentNames = "file")
+    private String output = "out.dex";
+
+    @Parameter(names = "--verbose",
+            description = "Generate verbose error messages.")
+    private boolean verbose = false;
+
+    @Parameter(names = {"--allow-odex-opcodes", "--allow-odex", "--ao"},
+            description = "Allows the odex opcodes that dalvik doesn't reject to be assembled.")
+    private boolean allowOdexOpcodes;
+
+    @Parameter(description = "Assembles the given files. If a directory is specified, it will be " +
+            "recursively searched for any files with a .smali prefix")
+    @ExtendedParameter(argumentNames = "[<file>|<dir>]+")
+    private List<String> input;
+
+    public AssembleCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    @Override public void run() {
+        if (help || input == null || input.isEmpty()) {
+            usage();
+            return;
+        }
+
+        try {
+            Smali.assemble(getOptions(), input);
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+
+    protected SmaliOptions getOptions() {
+        SmaliOptions options = new SmaliOptions();
+
+        options.jobs = jobs;
+        options.apiLevel = apiLevel;
+        options.outputDexFile = output;
+        options.allowOdexOpcodes = allowOdexOpcodes;
+        options.verboseErrors = verbose;
+
+        return options;
+    }
+}
diff --git a/smali/src/main/java/org/jf/smali/HelpCommand.java b/smali/src/main/java/org/jf/smali/HelpCommand.java
new file mode 100644
index 0000000..429a7df
--- /dev/null
+++ b/smali/src/main/java/org/jf/smali/HelpCommand.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.smali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.beust.jcommander.Parameters;
+import org.jf.util.ConsoleUtil;
+import org.jf.util.jcommander.*;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+@Parameters(commandDescription = "Shows usage information")
+@ExtendedParameters(
+        commandName = "help",
+        commandAliases = "h")
+public class HelpCommand extends Command {
+
+    @Parameter(description = "If specified, show the detailed usage information for the given commands")
+    @ExtendedParameter(argumentNames = "commands")
+    private List<String> commands;
+
+    public HelpCommand(@Nonnull List<JCommander> commandAncestors) {
+        super(commandAncestors);
+    }
+
+    public void run() {
+        JCommander parentJc = commandAncestors.get(commandAncestors.size() - 1);
+
+        if (commands == null || commands.isEmpty()) {
+            System.out.println(new HelpFormatter()
+                    .width(ConsoleUtil.getConsoleWidth())
+                    .format(commandAncestors));
+        } else {
+            boolean printedHelp = false;
+            for (String cmd : commands) {
+                JCommander command = ExtendedCommands.getSubcommand(parentJc, cmd);
+                if (command == null) {
+                    System.err.println("No such command: " + cmd);
+                } else {
+                    printedHelp = true;
+                    System.out.println(new HelpFormatter()
+                            .width(ConsoleUtil.getConsoleWidth())
+                            .format(((Command)command.getObjects().get(0)).getCommandHierarchy()));
+                }
+            }
+            if (!printedHelp) {
+                System.out.println(new HelpFormatter()
+                        .width(ConsoleUtil.getConsoleWidth())
+                        .format(commandAncestors));
+            }
+        }
+    }
+
+    @Parameters(hidden =  true)
+    @ExtendedParameters(commandName = "hlep")
+    public static class HlepCommand extends HelpCommand {
+        public HlepCommand(@Nonnull List<JCommander> commandAncestors) {
+            super(commandAncestors);
+        }
+    }
+}
diff --git a/smali/src/main/java/org/jf/smali/Main.java b/smali/src/main/java/org/jf/smali/Main.java
new file mode 100644
index 0000000..6b56fdd
--- /dev/null
+++ b/smali/src/main/java/org/jf/smali/Main.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.smali;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.Parameter;
+import com.google.common.collect.Lists;
+import org.jf.smali.HelpCommand.HlepCommand;
+import org.jf.util.jcommander.Command;
+import org.jf.util.jcommander.ExtendedCommands;
+import org.jf.util.jcommander.ExtendedParameters;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.List;
+import java.util.Properties;
+
+@ExtendedParameters(
+        includeParametersInUsage = true,
+        commandName = "smali",
+        postfixDescription = "See smali help <command> for more information about a specific command")
+public class Main extends Command {
+    public static final String VERSION = loadVersion();
+
+    @Parameter(names = {"-h", "-?", "--help"}, help = true,
+            description = "Show usage information")
+    private boolean help;
+
+    @Parameter(names = {"-v", "--version"}, help = true,
+            description = "Print the version of baksmali and then exit")
+    public boolean version;
+
+    private JCommander jc;
+
+    @Override public void run() {
+    }
+
+    @Override protected JCommander getJCommander() {
+        return jc;
+    }
+
+    public Main() {
+        super(Lists.<JCommander>newArrayList());
+    }
+
+    public static void main(String[] args) {
+        Main main = new Main();
+
+        JCommander jc = new JCommander(main);
+        main.jc = jc;
+        jc.setProgramName("smali");
+        List<JCommander> commandHierarchy = main.getCommandHierarchy();
+
+        ExtendedCommands.addExtendedCommand(jc, new AssembleCommand(commandHierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new HelpCommand(commandHierarchy));
+        ExtendedCommands.addExtendedCommand(jc, new HlepCommand(commandHierarchy));
+
+        jc.parse(args);
+
+        if (main.version) {
+            version();
+        }
+
+        if (jc.getParsedCommand() == null || main.help) {
+            main.usage();
+            return;
+        }
+
+        Command command = (Command)jc.getCommands().get(jc.getParsedCommand()).getObjects().get(0);
+        command.run();
+    }
+
+    protected static void version() {
+        System.out.println("smali " + VERSION + " (http://smali.org)");
+        System.out.println("Copyright (C) 2010 Ben Gruver (JesusFreke@JesusFreke.com)");
+        System.out.println("BSD license (http://www.opensource.org/licenses/bsd-license.php)");
+        System.exit(0);
+    }
+
+    private static String loadVersion() {
+        InputStream propertiesStream = Main.class.getClassLoader().getResourceAsStream("smali.properties");
+        String version = "[unknown version]";
+        if (propertiesStream != null) {
+            Properties properties = new Properties();
+            try {
+                properties.load(propertiesStream);
+                version = properties.getProperty("application.version");
+            } catch (IOException ex) {
+                // ignore
+            }
+        }
+        return version;
+    }
+}
diff --git a/smali/src/main/java/org/jf/smali/Smali.java b/smali/src/main/java/org/jf/smali/Smali.java
new file mode 100644
index 0000000..7f3762a
--- /dev/null
+++ b/smali/src/main/java/org/jf/smali/Smali.java
@@ -0,0 +1,208 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.smali;
+
+import com.google.common.collect.Lists;
+import org.antlr.runtime.CommonTokenStream;
+import org.antlr.runtime.Token;
+import org.antlr.runtime.TokenSource;
+import org.antlr.runtime.tree.CommonTree;
+import org.antlr.runtime.tree.CommonTreeNodeStream;
+import org.jf.dexlib2.Opcodes;
+import org.jf.dexlib2.writer.builder.DexBuilder;
+import org.jf.dexlib2.writer.io.FileDataStore;
+
+import javax.annotation.Nonnull;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.Arrays;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.*;
+
+public class Smali {
+
+    /**
+     * Assemble the specified files, using the given options
+     *
+     * @param options a SmaliOptions object with the options to run smali with
+     * @param input The files/directories to process
+     * @return true if assembly completed with no errors, or false if errors were encountered
+     */
+    public static boolean assemble(final SmaliOptions options, String... input) throws IOException {
+        return assemble(options, Arrays.asList(input));
+    }
+
+    /**
+     * Assemble the specified files, using the given options
+     *
+     * @param options a SmaliOptions object with the options to run smali with
+     * @param input The files/directories to process
+     * @return true if assembly completed with no errors, or false if errors were encountered
+     */
+    public static boolean assemble(final SmaliOptions options, List<String> input) throws IOException {
+        LinkedHashSet<File> filesToProcessSet = new LinkedHashSet<File>();
+
+        for (String fileToProcess: input) {
+            File argFile = new File(fileToProcess);
+
+            if (!argFile.exists()) {
+                throw new IllegalArgumentException("Cannot find file or directory \"" + fileToProcess + "\"");
+            }
+
+            if (argFile.isDirectory()) {
+                getSmaliFilesInDir(argFile, filesToProcessSet);
+            } else if (argFile.isFile()) {
+                filesToProcessSet.add(argFile);
+            }
+        }
+
+        boolean errors = false;
+
+        final DexBuilder dexBuilder = new DexBuilder(Opcodes.forApi(options.apiLevel));
+
+        ExecutorService executor = Executors.newFixedThreadPool(options.jobs);
+        List<Future<Boolean>> tasks = Lists.newArrayList();
+
+        for (final File file: filesToProcessSet) {
+            tasks.add(executor.submit(new Callable<Boolean>() {
+                @Override public Boolean call() throws Exception {
+                    return assembleSmaliFile(file, dexBuilder, options);
+                }
+            }));
+        }
+
+        for (Future<Boolean> task: tasks) {
+            while(true) {
+                try {
+                    try {
+                        if (!task.get()) {
+                            errors = true;
+                        }
+                    } catch (ExecutionException ex) {
+                        throw new RuntimeException(ex);
+                    }
+                } catch (InterruptedException ex) {
+                    continue;
+                }
+                break;
+            }
+        }
+
+        executor.shutdown();
+
+        if (errors) {
+            return false;
+        }
+
+        dexBuilder.writeTo(new FileDataStore(new File(options.outputDexFile)));
+
+        return true;
+    }
+
+    private static void getSmaliFilesInDir(@Nonnull File dir, @Nonnull Set<File> smaliFiles) {
+        File[] files = dir.listFiles();
+        if (files != null) {
+            for(File file: files) {
+                if (file.isDirectory()) {
+                    getSmaliFilesInDir(file, smaliFiles);
+                } else if (file.getName().endsWith(".smali")) {
+                    smaliFiles.add(file);
+                }
+            }
+        }
+    }
+
+    private static boolean assembleSmaliFile(File smaliFile, DexBuilder dexBuilder, SmaliOptions options)
+            throws Exception {
+        FileInputStream fis = null;
+        try {
+            fis = new FileInputStream(smaliFile);
+            InputStreamReader reader = new InputStreamReader(fis, "UTF-8");
+
+            LexerErrorInterface lexer = new smaliFlexLexer(reader);
+            ((smaliFlexLexer)lexer).setSourceFile(smaliFile);
+            CommonTokenStream tokens = new CommonTokenStream((TokenSource)lexer);
+
+            if (options.printTokens) {
+                tokens.getTokens();
+
+                for (int i=0; i<tokens.size(); i++) {
+                    Token token = tokens.get(i);
+                    if (token.getChannel() == smaliParser.HIDDEN) {
+                        continue;
+                    }
+
+                    System.out.println(smaliParser.tokenNames[token.getType()] + ": " + token.getText());
+                }
+
+                System.out.flush();
+            }
+
+            smaliParser parser = new smaliParser(tokens);
+            parser.setVerboseErrors(options.verboseErrors);
+            parser.setAllowOdex(options.allowOdexOpcodes);
+            parser.setApiLevel(options.apiLevel);
+
+            smaliParser.smali_file_return result = parser.smali_file();
+
+            if (parser.getNumberOfSyntaxErrors() > 0 || lexer.getNumberOfSyntaxErrors() > 0) {
+                return false;
+            }
+
+            CommonTree t = result.getTree();
+
+            CommonTreeNodeStream treeStream = new CommonTreeNodeStream(t);
+            treeStream.setTokenStream(tokens);
+
+            if (options.printTokens) {
+                System.out.println(t.toStringTree());
+            }
+
+            smaliTreeWalker dexGen = new smaliTreeWalker(treeStream);
+            dexGen.setApiLevel(options.apiLevel);
+
+            dexGen.setVerboseErrors(options.verboseErrors);
+            dexGen.setDexBuilder(dexBuilder);
+            dexGen.smali_file();
+
+            return dexGen.getNumberOfSyntaxErrors() == 0;
+        } finally {
+            if (fis != null) {
+                fis.close();
+            }
+        }
+    }
+}
diff --git a/smali/src/main/java/org/jf/smali/SmaliOptions.java b/smali/src/main/java/org/jf/smali/SmaliOptions.java
index 165c3a8..ac385fe 100644
--- a/smali/src/main/java/org/jf/smali/SmaliOptions.java
+++ b/smali/src/main/java/org/jf/smali/SmaliOptions.java
@@ -36,17 +36,7 @@
     public String outputDexFile = "out.dex";
 
     public int jobs = Runtime.getRuntime().availableProcessors();
-    public boolean allowOdex = false;
+    public boolean allowOdexOpcodes = false;
     public boolean verboseErrors = false;
     public boolean printTokens = false;
-    public boolean experimental = false;
-
-    public boolean listMethods = false;
-    public String methodListFilename = null;
-
-    public boolean listFields = false;
-    public String fieldListFilename = null;
-
-    public boolean listTypes = false;
-    public String typeListFilename = null;
 }
diff --git a/smali/src/main/java/org/jf/smali/SmaliTestUtils.java b/smali/src/main/java/org/jf/smali/SmaliTestUtils.java
index bef0741..a0fe55c 100644
--- a/smali/src/main/java/org/jf/smali/SmaliTestUtils.java
+++ b/smali/src/main/java/org/jf/smali/SmaliTestUtils.java
@@ -50,14 +50,14 @@
 public class SmaliTestUtils {
 
     public static ClassDef compileSmali(String smaliText) throws RecognitionException, IOException {
-        return compileSmali(smaliText, 15, false);
+        return compileSmali(smaliText, 15);
     }
 
-    public static ClassDef compileSmali(String smaliText, int apiLevel, boolean experimental)
+    public static ClassDef compileSmali(String smaliText, int apiLevel)
             throws RecognitionException, IOException {
         CommonTokenStream tokens;
         LexerErrorInterface lexer;
-        DexBuilder dexBuilder = DexBuilder.makeDexBuilder(Opcodes.forApi(apiLevel, experimental));
+        DexBuilder dexBuilder = new DexBuilder(Opcodes.forApi(apiLevel));
 
         Reader reader = new StringReader(smaliText);
 
@@ -67,7 +67,7 @@
         smaliParser parser = new smaliParser(tokens);
         parser.setVerboseErrors(true);
         parser.setAllowOdex(false);
-        parser.setApiLevel(apiLevel, experimental);
+        parser.setApiLevel(apiLevel);
 
         smaliParser.smali_file_return result = parser.smali_file();
 
@@ -81,7 +81,7 @@
         treeStream.setTokenStream(tokens);
 
         smaliTreeWalker dexGen = new smaliTreeWalker(treeStream);
-        dexGen.setApiLevel(apiLevel, experimental);
+        dexGen.setApiLevel(apiLevel);
         dexGen.setVerboseErrors(true);
         dexGen.setDexBuilder(dexBuilder);
         dexGen.smali_file();
@@ -94,7 +94,7 @@
 
         dexBuilder.writeTo(dataStore);
 
-        DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.forApi(apiLevel, experimental), dataStore.getData());
+        DexBackedDexFile dexFile = new DexBackedDexFile(Opcodes.forApi(apiLevel), dataStore.getData());
 
         return Iterables.getFirst(dexFile.getClasses(), null);
     }
diff --git a/smali/src/main/java/org/jf/smali/main.java b/smali/src/main/java/org/jf/smali/main.java
deleted file mode 100644
index e556280..0000000
--- a/smali/src/main/java/org/jf/smali/main.java
+++ /dev/null
@@ -1,491 +0,0 @@
-/*
- * [The "BSD licence"]
- * Copyright (c) 2010 Ben Gruver (JesusFreke)
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. The name of the author may not be used to endorse or promote products
- *    derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.jf.smali;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Ordering;
-import org.antlr.runtime.CommonTokenStream;
-import org.antlr.runtime.Token;
-import org.antlr.runtime.TokenSource;
-import org.antlr.runtime.tree.CommonTree;
-import org.antlr.runtime.tree.CommonTreeNodeStream;
-import org.apache.commons.cli.*;
-import org.jf.dexlib2.Opcodes;
-import org.jf.dexlib2.writer.builder.DexBuilder;
-import org.jf.dexlib2.writer.io.FileDataStore;
-import org.jf.util.ConsoleUtil;
-import org.jf.util.SmaliHelpFormatter;
-
-import javax.annotation.Nonnull;
-import java.io.*;
-import java.util.*;
-import java.util.concurrent.*;
-
-/**
- * Main class for smali. It recognizes enough options to be able to dispatch
- * to the right "actual" main.
- */
-public class main {
-
-    public static final String VERSION;
-
-    private final static Options basicOptions;
-    private final static Options debugOptions;
-    private final static Options options;
-
-    static {
-        basicOptions = new Options();
-        debugOptions = new Options();
-        options = new Options();
-        buildOptions();
-
-        InputStream templateStream = main.class.getClassLoader().getResourceAsStream("smali.properties");
-        if (templateStream != null) {
-            Properties properties = new Properties();
-            String version = "(unknown)";
-            try {
-                properties.load(templateStream);
-                version = properties.getProperty("application.version");
-            } catch (IOException ex) {
-                // just eat it
-            }
-            VERSION = version;
-        } else {
-            VERSION = "[unknown version]";
-        }
-    }
-
-
-    /**
-     * This class is uninstantiable.
-     */
-    private main() {
-    }
-
-    /**
-     * A more programmatic-friendly entry point for smali
-     *
-     * @param options a SmaliOptions object with the options to run smali with
-     * @param input The files/directories to process
-     * @return true if assembly completed with no errors, or false if errors were encountered
-     */
-    public static boolean run(final SmaliOptions options, String... input) throws IOException {
-        LinkedHashSet<File> filesToProcessSet = new LinkedHashSet<File>();
-
-        for (String fileToProcess: input) {
-            File argFile = new File(fileToProcess);
-
-            if (!argFile.exists()) {
-                throw new IllegalArgumentException("Cannot find file or directory \"" + fileToProcess + "\"");
-            }
-
-            if (argFile.isDirectory()) {
-                getSmaliFilesInDir(argFile, filesToProcessSet);
-            } else if (argFile.isFile()) {
-                filesToProcessSet.add(argFile);
-            }
-        }
-
-        boolean errors = false;
-
-        final DexBuilder dexBuilder = DexBuilder.makeDexBuilder(
-                Opcodes.forApi(options.apiLevel, options.experimental));
-
-        ExecutorService executor = Executors.newFixedThreadPool(options.jobs);
-        List<Future<Boolean>> tasks = Lists.newArrayList();
-
-        for (final File file: filesToProcessSet) {
-            tasks.add(executor.submit(new Callable<Boolean>() {
-                @Override public Boolean call() throws Exception {
-                    return assembleSmaliFile(file, dexBuilder, options);
-                }
-            }));
-        }
-
-        for (Future<Boolean> task: tasks) {
-            while(true) {
-                try {
-                    try {
-                        if (!task.get()) {
-                            errors = true;
-                        }
-                    } catch (ExecutionException ex) {
-                        throw new RuntimeException(ex);
-                    }
-                } catch (InterruptedException ex) {
-                    continue;
-                }
-                break;
-            }
-        }
-
-        executor.shutdown();
-
-        if (errors) {
-            return false;
-        }
-
-        if (options.listMethods) {
-            if (Strings.isNullOrEmpty(options.methodListFilename)) {
-                options.methodListFilename = options.outputDexFile + ".methods";
-            }
-            writeReferences(dexBuilder.getMethodReferences(), options.methodListFilename);
-        }
-
-        if (options.listFields) {
-            if (Strings.isNullOrEmpty(options.fieldListFilename)) {
-                options.fieldListFilename = options.outputDexFile + ".fields";
-            }
-            writeReferences(dexBuilder.getFieldReferences(), options.fieldListFilename);
-        }
-
-        if (options.listTypes) {
-            if (Strings.isNullOrEmpty(options.typeListFilename)) {
-                options.typeListFilename = options.outputDexFile + ".types";
-            }
-            writeReferences(dexBuilder.getTypeReferences(), options.typeListFilename);
-        }
-
-        dexBuilder.writeTo(new FileDataStore(new File(options.outputDexFile)));
-
-        return true;
-    }
-
-    /**
-     * Run!
-     */
-    public static void main(String[] args) {
-        Locale locale = new Locale("en", "US");
-        Locale.setDefault(locale);
-
-        CommandLineParser parser = new PosixParser();
-        CommandLine commandLine;
-
-        try {
-            commandLine = parser.parse(options, args);
-        } catch (ParseException ex) {
-            usage();
-            return;
-        }
-
-        SmaliOptions smaliOptions = new SmaliOptions();
-
-        String[] remainingArgs = commandLine.getArgs();
-
-        Option[] options = commandLine.getOptions();
-
-        for (int i=0; i<options.length; i++) {
-            Option option = options[i];
-            String opt = option.getOpt();
-
-            switch (opt.charAt(0)) {
-                case 'v':
-                    version();
-                    return;
-                case '?':
-                    while (++i < options.length) {
-                        if (options[i].getOpt().charAt(0) == '?') {
-                            usage(true);
-                            return;
-                        }
-                    }
-                    usage(false);
-                    return;
-                case 'o':
-                    smaliOptions.outputDexFile = commandLine.getOptionValue("o");
-                    break;
-                case 'x':
-                    smaliOptions.allowOdex = true;
-                    break;
-                case 'X':
-                    smaliOptions.experimental = true;
-                    break;
-                case 'a':
-                    smaliOptions.apiLevel = Integer.parseInt(commandLine.getOptionValue("a"));
-                    break;
-                case 'j':
-                    smaliOptions.jobs = Integer.parseInt(commandLine.getOptionValue("j"));
-                    break;
-                case 'm':
-                    smaliOptions.listMethods = true;
-                    smaliOptions.methodListFilename = commandLine.getOptionValue("m");
-                    break;
-                case 'f':
-                    smaliOptions.listFields = true;
-                    smaliOptions.fieldListFilename = commandLine.getOptionValue("f");
-                    break;
-                case 't':
-                    smaliOptions.listTypes = true;
-                    smaliOptions.typeListFilename = commandLine.getOptionValue("t");
-                    break;
-                case 'V':
-                    smaliOptions.verboseErrors = true;
-                    break;
-                case 'T':
-                    smaliOptions.printTokens = true;
-                    break;
-                default:
-                    assert false;
-            }
-        }
-
-        if (remainingArgs.length == 0) {
-            usage();
-            return;
-        }
-
-        try {
-            if (!run(smaliOptions, remainingArgs)) {
-                System.exit(1);
-            }
-        } catch (RuntimeException ex) {
-            System.err.println("\nUNEXPECTED TOP-LEVEL EXCEPTION:");
-            ex.printStackTrace();
-            System.exit(2);
-        } catch (Throwable ex) {
-            System.err.println("\nUNEXPECTED TOP-LEVEL ERROR:");
-            ex.printStackTrace();
-            System.exit(3);
-        }
-    }
-
-    private static void writeReferences(List<String> references, String filename) {
-        PrintWriter writer = null;
-        try {
-            writer = new PrintWriter(new BufferedWriter(new FileWriter(filename)));
-
-            for (String reference: Ordering.natural().sortedCopy(references)) {
-                writer.println(reference);
-            }
-        } catch (IOException ex) {
-            throw new RuntimeException(ex);
-        } finally {
-            if (writer != null) {
-                writer.close();
-            }
-        }
-    }
-
-    private static void getSmaliFilesInDir(@Nonnull File dir, @Nonnull Set<File> smaliFiles) {
-        File[] files = dir.listFiles();
-        if (files != null) {
-            for(File file: files) {
-                if (file.isDirectory()) {
-                    getSmaliFilesInDir(file, smaliFiles);
-                } else if (file.getName().endsWith(".smali")) {
-                    smaliFiles.add(file);
-                }
-            }
-        }
-    }
-
-    private static boolean assembleSmaliFile(File smaliFile, DexBuilder dexBuilder, SmaliOptions options)
-            throws Exception {
-        CommonTokenStream tokens;
-
-        LexerErrorInterface lexer;
-
-        FileInputStream fis = new FileInputStream(smaliFile);
-        InputStreamReader reader = new InputStreamReader(fis, "UTF-8");
-
-        lexer = new smaliFlexLexer(reader);
-        ((smaliFlexLexer)lexer).setSourceFile(smaliFile);
-        tokens = new CommonTokenStream((TokenSource)lexer);
-
-        if (options.printTokens) {
-            tokens.getTokens();
-
-            for (int i=0; i<tokens.size(); i++) {
-                Token token = tokens.get(i);
-                if (token.getChannel() == smaliParser.HIDDEN) {
-                    continue;
-                }
-
-                System.out.println(smaliParser.tokenNames[token.getType()] + ": " + token.getText());
-            }
-
-            System.out.flush();
-        }
-
-        smaliParser parser = new smaliParser(tokens);
-        parser.setVerboseErrors(options.verboseErrors);
-        parser.setAllowOdex(options.allowOdex);
-        parser.setApiLevel(options.apiLevel, options.experimental);
-
-        smaliParser.smali_file_return result = parser.smali_file();
-
-        if (parser.getNumberOfSyntaxErrors() > 0 || lexer.getNumberOfSyntaxErrors() > 0) {
-            return false;
-        }
-
-        CommonTree t = result.getTree();
-
-        CommonTreeNodeStream treeStream = new CommonTreeNodeStream(t);
-        treeStream.setTokenStream(tokens);
-
-        if (options.printTokens) {
-            System.out.println(t.toStringTree());
-        }
-
-        smaliTreeWalker dexGen = new smaliTreeWalker(treeStream);
-        dexGen.setApiLevel(options.apiLevel, options.experimental);
-
-        dexGen.setVerboseErrors(options.verboseErrors);
-        dexGen.setDexBuilder(dexBuilder);
-        dexGen.smali_file();
-
-        return dexGen.getNumberOfSyntaxErrors() == 0;
-    }
-
-
-    /**
-     * Prints the usage message.
-     */
-    private static void usage(boolean printDebugOptions) {
-        SmaliHelpFormatter formatter = new SmaliHelpFormatter();
-
-        int consoleWidth = ConsoleUtil.getConsoleWidth();
-        if (consoleWidth <= 0) {
-            consoleWidth = 80;
-        }
-
-        formatter.setWidth(consoleWidth);
-
-        formatter.printHelp("java -jar smali.jar [options] [--] [<smali-file>|folder]*",
-                "assembles a set of smali files into a dex file", basicOptions, printDebugOptions?debugOptions:null);
-    }
-
-    private static void usage() {
-        usage(false);
-    }
-
-    /**
-     * Prints the version message.
-     */
-    private static void version() {
-        System.out.println("smali " + VERSION + " (http://smali.googlecode.com)");
-        System.out.println("Copyright (C) 2010 Ben Gruver (JesusFreke@JesusFreke.com)");
-        System.out.println("BSD license (http://www.opensource.org/licenses/bsd-license.php)");
-        System.exit(0);
-    }
-
-    @SuppressWarnings("AccessStaticViaInstance")
-    private static void buildOptions() {
-        Option versionOption = OptionBuilder.withLongOpt("version")
-                .withDescription("prints the version then exits")
-                .create("v");
-
-        Option helpOption = OptionBuilder.withLongOpt("help")
-                .withDescription("prints the help message then exits. Specify twice for debug options")
-                .create("?");
-
-        Option outputOption = OptionBuilder.withLongOpt("output")
-                .withDescription("the name of the dex file that will be written. The default is out.dex")
-                .hasArg()
-                .withArgName("FILE")
-                .create("o");
-
-        Option allowOdexOption = OptionBuilder.withLongOpt("allow-odex-instructions")
-                .withDescription("allow odex instructions to be compiled into the dex file. Only a few" +
-                        " instructions are supported - the ones that can exist in a dead code path and not" +
-                        " cause dalvik to reject the class")
-                .create("x");
-
-        Option apiLevelOption = OptionBuilder.withLongOpt("api-level")
-                .withDescription("The numeric api-level of the file to generate, e.g. 14 for ICS. If not " +
-                        "specified, it defaults to 15 (ICS).")
-                .hasArg()
-                .withArgName("API_LEVEL")
-                .create("a");
-
-        Option listMethodsOption = OptionBuilder.withLongOpt("list-methods")
-                .withDescription("Lists all the method references to FILE" +
-                        " (<output_dex_filename>.methods by default)")
-                .hasOptionalArg()
-                .withArgName("FILE")
-                .create("m");
-
-        Option listFieldsOption = OptionBuilder.withLongOpt("list-fields")
-                .withDescription("Lists all the field references to FILE" +
-                        " (<output_dex_filename>.fields by default)")
-                .hasOptionalArg()
-                .withArgName("FILE")
-                .create("f");
-
-        Option listClassesOption = OptionBuilder.withLongOpt("list-types")
-                .withDescription("Lists all the type references to FILE" +
-                        " (<output_dex_filename>.types by default)")
-                .hasOptionalArg()
-                .withArgName("FILE")
-                .create("t");
-
-        Option experimentalOption = OptionBuilder.withLongOpt("experimental")
-                .withDescription("enable experimental opcodes to be assembled, even if they " +
-                        " aren't necessarily supported by the Android runtime yet")
-                .create("X");
-
-        Option jobsOption = OptionBuilder.withLongOpt("jobs")
-                .withDescription("The number of threads to use. Defaults to the number of cores available, up to a " +
-                        "maximum of 6")
-                .hasArg()
-                .withArgName("NUM_THREADS")
-                .create("j");
-
-        Option verboseErrorsOption = OptionBuilder.withLongOpt("verbose-errors")
-                .withDescription("Generate verbose error messages")
-                .create("V");
-
-        Option printTokensOption = OptionBuilder.withLongOpt("print-tokens")
-                .withDescription("Print the name and text of each token")
-                .create("T");
-
-        basicOptions.addOption(versionOption);
-        basicOptions.addOption(helpOption);
-        basicOptions.addOption(outputOption);
-        basicOptions.addOption(allowOdexOption);
-        basicOptions.addOption(apiLevelOption);
-        basicOptions.addOption(experimentalOption);
-        basicOptions.addOption(jobsOption);
-        basicOptions.addOption(listMethodsOption);
-        basicOptions.addOption(listFieldsOption);
-        basicOptions.addOption(listClassesOption);
-
-        debugOptions.addOption(verboseErrorsOption);
-        debugOptions.addOption(printTokensOption);
-
-        for (Object option: basicOptions.getOptions()) {
-            options.addOption((Option)option);
-        }
-
-        for (Object option: debugOptions.getOptions()) {
-            options.addOption((Option)option);
-        }
-    }
-}
\ No newline at end of file
diff --git a/smalidea/build.gradle b/smalidea/build.gradle
index 8cba19d..5720976 100644
--- a/smalidea/build.gradle
+++ b/smalidea/build.gradle
@@ -34,9 +34,13 @@
         maven {
             url "https://plugins.gradle.org/m2/"
         }
+        maven {
+            url 'http://dl.bintray.com/jetbrains/intellij-plugin-service'
+        }
+
     }
     dependencies {
-        classpath 'gradle.plugin.org.jetbrains:gradle-intellij-plugin:0.0.40'
+        classpath "gradle.plugin.org.jetbrains:gradle-intellij-plugin:0.1.10"
     }
 }
 
@@ -44,7 +48,8 @@
 apply plugin: 'idea'
 apply plugin: 'antlr'
 
-version = '0.03'
+
+version = '0.05'
 
 if (!('release' in gradle.startParameter.taskNames)) {
     def versionSuffix
@@ -86,14 +91,12 @@
 
 def sandboxDir = "${buildDir}/sandbox"
 
-// We don't want to use the org.jetbrains.intellij plugin when generating the idea project files,
-// so that idea classes aren't included as project dependencies, since they will already exist
-// in the plugin sdk defined for the project
 if (!('idea' in gradle.startParameter.taskNames)) {
+
     apply plugin: 'org.jetbrains.intellij'
 
     intellij {
-        version 'IC-15.0.6'
+        version 'IC-2016.3.5'
         pluginName 'smalidea'
 
         updateSinceUntilBuild false
@@ -106,8 +109,8 @@
     task ideaDirs() {
         project.afterEvaluate {
             if (intellij != null) {
-                println "IDEA Plugin jdk: ${intellij.ideaDirectory}"
-                println "sources: ${project.configurations['intellij-sources'].files[0]}"
+                println "IDEA Plugin jdk: ${intellij.ideaDependency.classes}"
+                println "sources: ${intellij.ideaDependency.getSources()}"
             }
         }
     }
@@ -115,13 +118,8 @@
     dependencies {
         compile files("${System.properties['java.home']}/../lib/tools.jar")
     }
-} else {
-    // If we're running the idea task, let's make sure nothing else is being run, since
-    // we have to use a special configuration for the idea task
-    if (gradle.startParameter.taskNames.size() > 1) {
-        throw new InvalidUserDataException("The idea task must be run by itself.")
-    }
 
+} else {
     project(':') {
         idea {
             project {
@@ -209,13 +207,15 @@
 }
 
 task extractTokens(type: org.gradle.api.tasks.Copy, dependsOn: ':smali:build') {
-    def allArtifacts = configurations.default.resolvedConfiguration.resolvedArtifacts
-    def smaliArtifact = allArtifacts.find { it.moduleVersion.id.name.equals('smali') }
+    project.afterEvaluate {
+        def allArtifacts = configurations.default.resolvedConfiguration.resolvedArtifacts
+        def smaliArtifact = allArtifacts.find { it.moduleVersion.id.name.equals('smali') }
 
-    from(zipTree(smaliArtifact.file)) {
-        include '**/*.tokens'
+        from(zipTree(smaliArtifact.file)) {
+            include '**/*.tokens'
+        }
+        into "${buildDir}/tokens"
     }
-    into "${buildDir}/tokens"
 }
 
 generateGrammarSource {
@@ -225,7 +225,6 @@
     outputDirectory(file("${buildDir}/generated-src/antlr/main/org/jf/smalidea"))
 }
 generateGrammarSource.dependsOn(extractTokens)
-
 ideaModule.dependsOn(generateGrammarSource)
 
 task release(dependsOn: 'buildPlugin') {
diff --git a/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliCodeFragmentFactory.java b/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliCodeFragmentFactory.java
index 5e2dd0c..82a190b 100644
--- a/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliCodeFragmentFactory.java
+++ b/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliCodeFragmentFactory.java
@@ -61,6 +61,7 @@
 import org.jf.smalidea.util.NameUtils;
 import org.jf.smalidea.util.PsiUtil;
 
+import javax.annotation.Nullable;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.InvocationTargetException;
 import java.util.List;
@@ -274,9 +275,14 @@
         return originalContext;
     }
 
+    @Nullable
     public static Value evaluateRegister(EvaluationContext context, final SmaliMethod smaliMethod,
                                          final int registerNum, final String type) throws EvaluateException {
 
+        if (registerNum >= smaliMethod.getRegisterCount()) {
+            return null;
+        }
+
         final StackFrameProxy frameProxy = context.getSuspendContext().getFrameProxy();
         if (frameProxy == null) {
             return null;
@@ -304,12 +310,21 @@
             for (SmaliInstruction instruction: smaliMethod.getInstructions()) {
                 methodSize += instruction.getInstructionSize();
             }
-            Location endLocation = method.locationOfCodeIndex((methodSize/2) - 1);
+            Location endLocation = null;
+            for (int endCodeIndex = (methodSize/2) - 1; endCodeIndex >= 0; endCodeIndex--) {
+                endLocation = method.locationOfCodeIndex(endCodeIndex);
+                if (endLocation != null) {
+                    break;
+                }
+            }
+            if (endLocation == null) {
+                return null;
+            }
 
             LocalVariable localVariable = localVariableConstructor.newInstance(vm,
                     method,
                     mapRegister(frameProxy.getStackFrame().virtualMachine(), smaliMethod, registerNum),
-                    method.locationOfCodeIndex(0),
+                    method.location(),
                     endLocation,
                     String.format("v%d", registerNum), type, null);
 
diff --git a/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliPositionManager.java b/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliPositionManager.java
index 781a856..9c0abbe 100644
--- a/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliPositionManager.java
+++ b/smalidea/src/main/java/org/jf/smalidea/debugging/SmaliPositionManager.java
@@ -38,6 +38,7 @@
 import com.intellij.debugger.requests.ClassPrepareRequestor;
 import com.intellij.openapi.application.ApplicationManager;
 import com.intellij.openapi.util.Computable;
+import com.intellij.psi.PsiFile;
 import com.intellij.psi.search.GlobalSearchScope;
 import com.sun.jdi.Location;
 import com.sun.jdi.ReferenceType;
@@ -60,10 +61,16 @@
         this.debugProcess = debugProcess;
     }
 
-    public SourcePosition getSourcePosition(String declaringType, String methodName, String methodSignature,
+    public SourcePosition getSourcePosition(final String declaringType, String methodName, String methodSignature,
                                             int codeIndex) throws NoDataException {
-        Collection<SmaliClass> classes = SmaliClassNameIndex.INSTANCE.get(declaringType,
-                debugProcess.getProject(), GlobalSearchScope.projectScope(debugProcess.getProject()));
+
+        Collection<SmaliClass> classes = ApplicationManager.getApplication().runReadAction(
+                new Computable<Collection<SmaliClass>>() {
+                    @Override public Collection<SmaliClass> compute() {
+                        return SmaliClassNameIndex.INSTANCE.get(declaringType, debugProcess.getProject(),
+                                GlobalSearchScope.projectScope(debugProcess.getProject()));
+                    }
+                });
 
         if (classes.size() > 0) {
             SmaliClass smaliClass = classes.iterator().next();
@@ -116,7 +123,13 @@
     @Override @NotNull
     public List<Location> locationsOfLine(@NotNull final ReferenceType type,
                                           @NotNull final SourcePosition position) throws NoDataException {
-        if (!(position.getElementAt().getContainingFile() instanceof SmaliFile)) {
+        PsiFile containingFile = ApplicationManager.getApplication().runReadAction(new Computable<PsiFile>() {
+            @Override public PsiFile compute() {
+                return position.getElementAt().getContainingFile();
+            }
+        });
+
+        if (!(containingFile instanceof SmaliFile)) {
             throw NoDataException.INSTANCE;
         }
 
@@ -125,6 +138,8 @@
         ApplicationManager.getApplication().runReadAction(new Runnable() {
             @Override
             public void run() {
+
+
                 String typeName = type.name();
                 Collection<SmaliClass> classes = SmaliClassNameIndex.INSTANCE.get(typeName, debugProcess.getProject(),
                         GlobalSearchScope.projectScope(debugProcess.getProject()));
diff --git a/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyStringReference.java b/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyStringReference.java
index 88fd007..2b52e2b 100644
--- a/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyStringReference.java
+++ b/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyStringReference.java
@@ -32,6 +32,8 @@
 package org.jf.smalidea.debugging.value;
 
 import com.intellij.openapi.project.Project;
+import com.intellij.psi.PsiSubstitutor;
+import com.sun.jdi.ObjectReference;
 import com.sun.jdi.StringReference;
 import org.jf.smalidea.psi.impl.SmaliMethod;
 
@@ -41,6 +43,11 @@
     }
 
     public String value() {
+        ObjectReference objectReference = getValue();
+        if (!(objectReference instanceof StringReference)) {
+            throw new IllegalStateException(String.format("Expecting type String, but got %s. method=%s, register=%d",
+                    objectReference.type().name(), this.method.getSignature(PsiSubstitutor.EMPTY), registerNumber));
+        }
         return getValue().value();
     }
 }
diff --git a/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyValue.java b/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyValue.java
index 0eeb010..f17df6d 100644
--- a/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyValue.java
+++ b/smalidea/src/main/java/org/jf/smalidea/debugging/value/LazyValue.java
@@ -48,10 +48,10 @@
 import javax.annotation.Nullable;
 
 public class LazyValue<T extends Value> implements Value {
-    private final int registerNumber;
-    private final Project project;
-    private final SmaliMethod method;
-    private final String type;
+    protected final int registerNumber;
+    protected final Project project;
+    protected final SmaliMethod method;
+    protected final String type;
 
     private EvaluationContext evaluationContext;
     private Value value;
diff --git a/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaPackedSwitchPayload.java b/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaPackedSwitchPayload.java
index 9d2a0fc..6056da3 100644
--- a/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaPackedSwitchPayload.java
+++ b/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaPackedSwitchPayload.java
@@ -86,7 +86,7 @@
                         return 0;
                     }
 
-                    return label.getOffset() - baseOffset;
+                    return (label.getOffset() - baseOffset) / 2;
                 }
             });
         }
diff --git a/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaSparseSwitchPayload.java b/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaSparseSwitchPayload.java
index 15eaea2..832c880 100644
--- a/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaSparseSwitchPayload.java
+++ b/smalidea/src/main/java/org/jf/smalidea/dexlib/instruction/SmalideaSparseSwitchPayload.java
@@ -79,7 +79,7 @@
                             return 0;
                         }
 
-                        return label.getOffset() - baseOffset;
+                        return (label.getOffset() - baseOffset) / 2;
                     }
                 };
             }
diff --git a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliAnnotation.java b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliAnnotation.java
index e36313b..ac3dd81 100644
--- a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliAnnotation.java
+++ b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliAnnotation.java
@@ -106,7 +106,7 @@
     }
 
     @Nullable @Override public PsiAnnotationOwner getOwner() {
-        return (PsiAnnotationOwner)getStubOrPsiParent();
+        return (PsiAnnotationOwner)getParentByStub();
     }
 
     @Nullable @Override public PsiMetaData getMetaData() {
diff --git a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliField.java b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliField.java
index 7bef4e9..579401c 100644
--- a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliField.java
+++ b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliField.java
@@ -88,7 +88,7 @@
     }
 
     @Nullable @Override public PsiClass getContainingClass() {
-        return (PsiClass)getStubOrPsiParent();
+        return (PsiClass)getParentByStub();
     }
 
     @NotNull @Override public PsiType getType() {
diff --git a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliImplementsList.java b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliImplementsList.java
index 8992ab0..fb4a788 100644
--- a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliImplementsList.java
+++ b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliImplementsList.java
@@ -56,7 +56,7 @@
     }
 
     @NotNull private SmaliClassTypeElement[] getImplementsElements() {
-        SmaliClass smaliClass = (SmaliClass)getStubOrPsiParent();
+        SmaliClass smaliClass = (SmaliClass)getParentByStub();
         assert smaliClass != null;
 
         SmaliImplementsStatement[] implementsStatements = smaliClass.getImplementsStatements();
diff --git a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliInstruction.java b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliInstruction.java
index 8cb2d77..ecbdbb3 100644
--- a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliInstruction.java
+++ b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliInstruction.java
@@ -76,7 +76,7 @@
             assert instructionNode != null;
 
             // TODO: put a project level Opcodes instance with the appropriate api level somewhere
-            opcode = new Opcodes(15, false).getOpcodeByName(instructionNode.getText());
+            opcode = Opcodes.getDefault().getOpcodeByName(instructionNode.getText());
             if (opcode == null) {
                 if (instructionNode.getText().equals(".packed-switch")) {
                     return Opcode.PACKED_SWITCH_PAYLOAD;
diff --git a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliMethod.java b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliMethod.java
index 085585b..8ba618b 100644
--- a/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliMethod.java
+++ b/smalidea/src/main/java/org/jf/smalidea/psi/impl/SmaliMethod.java
@@ -259,7 +259,7 @@
     }
 
     @Nullable @Override public SmaliClass getContainingClass() {
-        PsiElement parent = getStubOrPsiParent();
+        PsiElement parent = getParentByStub();
         if (parent instanceof SmaliClass) {
             return (SmaliClass) parent;
         }
diff --git a/smalidea/src/main/java/org/jf/smalidea/psi/index/SmaliClassFinder.java b/smalidea/src/main/java/org/jf/smalidea/psi/index/SmaliClassFinder.java
index 0ebb1ee..87b2aff 100644
--- a/smalidea/src/main/java/org/jf/smalidea/psi/index/SmaliClassFinder.java
+++ b/smalidea/src/main/java/org/jf/smalidea/psi/index/SmaliClassFinder.java
@@ -31,6 +31,7 @@
 
 package org.jf.smalidea.psi.index;
 
+import com.intellij.openapi.project.Project;
 import com.intellij.psi.PsiClass;
 import com.intellij.psi.PsiElementFinder;
 import com.intellij.psi.search.GlobalSearchScope;
@@ -40,9 +41,16 @@
 import java.util.Collection;
 
 public class SmaliClassFinder extends PsiElementFinder {
+
+    private final Project project;
+
+    public SmaliClassFinder(Project project) {
+        this.project = project;
+    }
+
     @Override
     public PsiClass findClass(@NotNull String qualifiedName, @NotNull GlobalSearchScope scope) {
-        Collection<SmaliClass> classes = SmaliClassNameIndex.INSTANCE.get(qualifiedName, scope.getProject(), scope);
+        Collection<SmaliClass> classes = SmaliClassNameIndex.INSTANCE.get(qualifiedName, project, scope);
         if (classes != null && classes.size() == 1) {
             return classes.iterator().next();
         }
diff --git a/smalidea/src/main/resources/META-INF/plugin.xml b/smalidea/src/main/resources/META-INF/plugin.xml
index 8ae92da..91326cf 100644
--- a/smalidea/src/main/resources/META-INF/plugin.xml
+++ b/smalidea/src/main/resources/META-INF/plugin.xml
@@ -1,7 +1,7 @@
 <idea-plugin version="2">
   <id>org.jf.smalidea</id>
   <name>Smalidea</name>
-  <version>0.02</version>
+  <version>0.04</version>
   <vendor email="jesusfreke@jesusfreke.com" url="http://smali.org">JesusFreke</vendor>
 
   <description><![CDATA[
diff --git a/util/build.gradle b/util/build.gradle
index 407ef71..23d6a3a 100644
--- a/util/build.gradle
+++ b/util/build.gradle
@@ -30,9 +30,9 @@
  */
 
 dependencies {
-    compile depends.commons_cli
     compile depends.findbugs
     compile depends.guava
+    compile depends.jcommander
     testCompile depends.junit
 }
 
diff --git a/util/src/main/java/org/jf/util/OldWrappedIndentingWriter.java b/util/src/main/java/org/jf/util/OldWrappedIndentingWriter.java
new file mode 100644
index 0000000..f457717
--- /dev/null
+++ b/util/src/main/java/org/jf/util/OldWrappedIndentingWriter.java
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2013, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.util;
+
+import java.io.FilterWriter;
+import java.io.IOException;
+import java.io.Writer;
+
+/**
+ * Writer that wraps another writer and passes width-limited and
+ * optionally-prefixed output to its subordinate. When lines are
+ * wrapped they are automatically indented based on the start of the
+ * line.
+ */
+public final class OldWrappedIndentingWriter extends FilterWriter {
+    /** null-ok; optional prefix for every line */
+    private final String prefix;
+
+    /** &gt; 0; the maximum output width */
+    private final int width;
+
+    /** &gt; 0; the maximum indent */
+    private final int maxIndent;
+
+    /** &gt;= 0; current output column (zero-based) */
+    private int column;
+
+    /** whether indent spaces are currently being collected */
+    private boolean collectingIndent;
+
+    /** &gt;= 0; current indent amount */
+    private int indent;
+
+    /**
+     * Constructs an instance.
+     *
+     * @param out non-null; writer to send final output to
+     * @param width &gt;= 0; the maximum output width (not including
+     * <code>prefix</code>), or <code>0</code> for no maximum
+     * @param prefix non-null; the prefix for each line
+     */
+    public OldWrappedIndentingWriter(Writer out, int width, String prefix) {
+        super(out);
+
+        if (out == null) {
+            throw new NullPointerException("out == null");
+        }
+
+        if (width < 0) {
+            throw new IllegalArgumentException("width < 0");
+        }
+
+        if (prefix == null) {
+            throw new NullPointerException("prefix == null");
+        }
+
+        this.width = (width != 0) ? width : Integer.MAX_VALUE;
+        this.maxIndent = width >> 1;
+        this.prefix = (prefix.length() == 0) ? null : prefix;
+
+        bol();
+    }
+
+    /**
+     * Constructs a no-prefix instance.
+     *
+     * @param out non-null; writer to send final output to
+     * @param width &gt;= 0; the maximum output width (not including
+     * <code>prefix</code>), or <code>0</code> for no maximum
+     */
+    public OldWrappedIndentingWriter(Writer out, int width) {
+        this(out, width, "");
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void write(int c) throws IOException {
+        synchronized (lock) {
+            if (collectingIndent) {
+                if (c == ' ') {
+                    indent++;
+                    if (indent >= maxIndent) {
+                        indent = maxIndent;
+                        collectingIndent = false;
+                    }
+                } else {
+                    collectingIndent = false;
+                }
+            }
+
+            if ((column == width) && (c != '\n')) {
+                out.write('\n');
+                column = 0;
+                /*
+                 * Note: No else, so this should fall through to the next
+                 * if statement.
+                 */
+            }
+
+            if (column == 0) {
+                if (prefix != null) {
+                    out.write(prefix);
+                }
+
+                if (!collectingIndent) {
+                    for (int i = 0; i < indent; i++) {
+                        out.write(' ');
+                    }
+                    column = indent;
+                }
+            }
+
+            out.write(c);
+
+            if (c == '\n') {
+                bol();
+            } else {
+                column++;
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void write(char[] cbuf, int off, int len) throws IOException {
+        synchronized (lock) {
+            while (len > 0) {
+                write(cbuf[off]);
+                off++;
+                len--;
+            }
+        }
+    }
+
+    /** {@inheritDoc} */
+    @Override
+    public void write(String str, int off, int len) throws IOException {
+        synchronized (lock) {
+            while (len > 0) {
+                write(str.charAt(off));
+                off++;
+                len--;
+            }
+        }
+    }
+
+    /**
+     * Indicates that output is at the beginning of a line.
+     */
+    private void bol() {
+        column = 0;
+        collectingIndent = (maxIndent != 0);
+        indent = 0;
+    }
+}
diff --git a/util/src/main/java/org/jf/util/PathUtil.java b/util/src/main/java/org/jf/util/PathUtil.java
index 91eb758..9ba9f30 100644
--- a/util/src/main/java/org/jf/util/PathUtil.java
+++ b/util/src/main/java/org/jf/util/PathUtil.java
@@ -28,9 +28,12 @@
 
 package org.jf.util;
 
+import com.google.common.collect.Lists;
+
 import java.io.File;
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.List;
 
 public class PathUtil {
     private PathUtil() {
@@ -44,19 +47,9 @@
         return new File(getRelativeFileInternal(baseFile.getCanonicalFile(), fileToRelativize.getCanonicalFile()));
     }
 
-    public static String getRelativePath(String basePath, String pathToRelativize) throws IOException {
-        File baseFile = new File(basePath);
-        if (baseFile.isFile()) {
-            baseFile = baseFile.getParentFile();
-        }
-
-        return getRelativeFileInternal(baseFile.getCanonicalFile(),
-                new File(pathToRelativize).getCanonicalFile());
-    }
-
     static String getRelativeFileInternal(File canonicalBaseFile, File canonicalFileToRelativize) {
-        ArrayList<String> basePath = getPathComponents(canonicalBaseFile);
-        ArrayList<String> pathToRelativize = getPathComponents(canonicalFileToRelativize);
+        List<String> basePath = getPathComponents(canonicalBaseFile);
+        List<String> pathToRelativize = getPathComponents(canonicalFileToRelativize);
 
         //if the roots aren't the same (i.e. different drives on a windows machine), we can't construct a relative
         //path from one to the other, so just return the canonical file
@@ -105,21 +98,21 @@
         return sb.toString();
     }
 
-    private static ArrayList<String> getPathComponents(File file) {
+    private static List<String> getPathComponents(File file) {
         ArrayList<String> path = new ArrayList<String>();
 
         while (file != null) {
             File parentFile = file.getParentFile();
 
             if (parentFile == null) {
-                path.add(0, file.getPath());
+                path.add(file.getPath());
             } else {
-                path.add(0, file.getName());
+                path.add(file.getName());
             }
 
             file = parentFile;
         }
 
-        return path;
+        return Lists.reverse(path);
     }
 }
diff --git a/util/src/main/java/org/jf/util/SmaliHelpFormatter.java b/util/src/main/java/org/jf/util/SmaliHelpFormatter.java
deleted file mode 100644
index 3d0137e..0000000
--- a/util/src/main/java/org/jf/util/SmaliHelpFormatter.java
+++ /dev/null
@@ -1,47 +0,0 @@
-/*
- * [The "BSD licence"]
- * Copyright (c) 2010 Ben Gruver (JesusFreke)
- * All rights reserved.
- *
- * Redistribution and use in source and binary forms, with or without
- * modification, are permitted provided that the following conditions
- * are met:
- * 1. Redistributions of source code must retain the above copyright
- *    notice, this list of conditions and the following disclaimer.
- * 2. Redistributions in binary form must reproduce the above copyright
- *    notice, this list of conditions and the following disclaimer in the
- *    documentation and/or other materials provided with the distribution.
- * 3. The name of the author may not be used to endorse or promote products
- *    derived from this software without specific prior written permission.
- *
- * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
- * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
- * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
- * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT,
- * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
- * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
- * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
- * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
- * INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
- * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
- */
-
-package org.jf.util;
-
-import org.apache.commons.cli.HelpFormatter;
-import org.apache.commons.cli.Options;
-
-import java.io.PrintWriter;
-
-public class SmaliHelpFormatter extends HelpFormatter {
-    public void printHelp(String cmdLineSyntax, String header, Options options, Options debugOptions) {
-        super.printHelp(cmdLineSyntax, header, options, "");
-        if (debugOptions != null) {
-            System.out.println();
-            System.out.println("Debug Options:");
-            PrintWriter pw = new PrintWriter(System.out);
-            super.printOptions(pw, getWidth(), debugOptions, getLeftPadding(), getDescPadding());
-            pw.flush();
-        }
-    }
-}
diff --git a/util/src/main/java/org/jf/util/StringWrapper.java b/util/src/main/java/org/jf/util/StringWrapper.java
index 9180830..304c297 100644
--- a/util/src/main/java/org/jf/util/StringWrapper.java
+++ b/util/src/main/java/org/jf/util/StringWrapper.java
@@ -33,9 +33,92 @@
 
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
+import java.io.PrintStream;
+import java.text.BreakIterator;
+import java.util.Iterator;
 
 public class StringWrapper {
     /**
+     * Splits the given string into lines of maximum width maxWidth. The splitting is done using the current locale's
+     * rules for splitting lines.
+     *
+     * @param string The string to split
+     * @param maxWidth The maximum length of any line
+     * @return An iterable of Strings containing the wrapped lines
+     */
+    public static Iterable<String> wrapStringOnBreaks(@Nonnull final String string, final int maxWidth) {
+        // TODO: should we strip any trailing newlines?
+        final BreakIterator breakIterator = BreakIterator.getLineInstance();
+        breakIterator.setText(string);
+
+        return new Iterable<String>() {
+            @Override
+            public Iterator<String> iterator() {
+                return new Iterator<String>() {
+                    private int currentLineStart = 0;
+                    private boolean nextLineSet = false;
+                    private String nextLine;
+
+                    @Override
+                    public boolean hasNext() {
+                        if (!nextLineSet) {
+                            calculateNext();
+                        }
+                        return nextLine != null;
+                    }
+
+                    private void calculateNext() {
+                        int lineEnd = currentLineStart;
+                        while (true) {
+                            lineEnd = breakIterator.following(lineEnd);
+                            if (lineEnd == BreakIterator.DONE) {
+                                lineEnd = breakIterator.last();
+                                if (lineEnd <= currentLineStart) {
+                                    nextLine = null;
+                                    nextLineSet = true;
+                                    return;
+                                }
+                                break;
+                            }
+
+                            if (lineEnd - currentLineStart > maxWidth) {
+                                lineEnd = breakIterator.preceding(lineEnd);
+                                if (lineEnd <= currentLineStart) {
+                                    lineEnd = currentLineStart + maxWidth;
+                                }
+                                break;
+                            }
+
+                            if (string.charAt(lineEnd-1) == '\n') {
+                                nextLine = string.substring(currentLineStart, lineEnd-1);
+                                nextLineSet = true;
+                                currentLineStart = lineEnd;
+                                return;
+                            }
+                        }
+                        nextLine = string.substring(currentLineStart, lineEnd);
+                        nextLineSet = true;
+                        currentLineStart = lineEnd;
+                    }
+
+                    @Override
+                    public String next() {
+                        String ret = nextLine;
+                        nextLine = null;
+                        nextLineSet = false;
+                        return ret;
+                    }
+
+                    @Override
+                    public void remove() {
+                        throw new UnsupportedOperationException();
+                    }
+                };
+            }
+        };
+    }
+
+    /**
      * Splits the given string into lines using on any embedded newlines, and wrapping the text as needed to conform to
      * the given maximum line width.
      *
@@ -103,4 +186,14 @@
         System.arraycopy(arr, 0, newArr, 0, arr.length);
         return newArr;
     }
+
+    public static void printWrappedString(@Nonnull PrintStream stream, @Nonnull String string) {
+        printWrappedString(stream, string, ConsoleUtil.getConsoleWidth());
+    }
+
+    public static void printWrappedString(@Nonnull PrintStream stream, @Nonnull String string, int maxWidth) {
+        for (String str: wrapStringOnBreaks(string, maxWidth)) {
+            stream.println(str);
+        }
+    }
 }
diff --git a/util/src/main/java/org/jf/util/WrappedIndentingWriter.java b/util/src/main/java/org/jf/util/WrappedIndentingWriter.java
index eb1acda..df4575b 100644
--- a/util/src/main/java/org/jf/util/WrappedIndentingWriter.java
+++ b/util/src/main/java/org/jf/util/WrappedIndentingWriter.java
@@ -1,18 +1,18 @@
 /*
- * Copyright 2013, Google Inc.
+ * Copyright 2016, Google Inc.
  * All rights reserved.
  *
  * Redistribution and use in source and binary forms, with or without
  * modification, are permitted provided that the following conditions are
  * met:
  *
- *     * Redistributions of source code must retain the above copyright
+ * Redistributions of source code must retain the above copyright
  * notice, this list of conditions and the following disclaimer.
- *     * Redistributions in binary form must reproduce the above
+ * Redistributions in binary form must reproduce the above
  * copyright notice, this list of conditions and the following disclaimer
  * in the documentation and/or other materials provided with the
  * distribution.
- *     * Neither the name of Google Inc. nor the names of its
+ * Neither the name of Google Inc. nor the names of its
  * contributors may be used to endorse or promote products derived from
  * this software without specific prior written permission.
  *
@@ -31,154 +31,94 @@
 
 package org.jf.util;
 
+import com.google.common.collect.Lists;
+
 import java.io.FilterWriter;
 import java.io.IOException;
 import java.io.Writer;
+import java.util.List;
 
-/**
- * Writer that wraps another writer and passes width-limited and
- * optionally-prefixed output to its subordinate. When lines are
- * wrapped they are automatically indented based on the start of the
- * line.
- */
-public final class WrappedIndentingWriter extends FilterWriter {
-    /** null-ok; optional prefix for every line */
-    private final String prefix;
+public class WrappedIndentingWriter extends FilterWriter {
 
-    /** &gt; 0; the maximum output width */
-    private final int width;
-
-    /** &gt; 0; the maximum indent */
     private final int maxIndent;
+    private final int maxWidth;
 
-    /** &gt;= 0; current output column (zero-based) */
-    private int column;
+    private int currentIndent = 0;
+    private final StringBuilder line = new StringBuilder();
 
-    /** whether indent spaces are currently being collected */
-    private boolean collectingIndent;
-
-    /** &gt;= 0; current indent amount */
-    private int indent;
-
-    /**
-     * Constructs an instance.
-     *
-     * @param out non-null; writer to send final output to
-     * @param width &gt;= 0; the maximum output width (not including
-     * <code>prefix</code>), or <code>0</code> for no maximum
-     * @param prefix non-null; the prefix for each line
-     */
-    public WrappedIndentingWriter(Writer out, int width, String prefix) {
+    public WrappedIndentingWriter(Writer out, int maxIndent, int maxWidth) {
         super(out);
-
-        if (out == null) {
-            throw new NullPointerException("out == null");
-        }
-
-        if (width < 0) {
-            throw new IllegalArgumentException("width < 0");
-        }
-
-        if (prefix == null) {
-            throw new NullPointerException("prefix == null");
-        }
-
-        this.width = (width != 0) ? width : Integer.MAX_VALUE;
-        this.maxIndent = width >> 1;
-        this.prefix = (prefix.length() == 0) ? null : prefix;
-
-        bol();
+        this.maxIndent = maxIndent;
+        this.maxWidth = maxWidth;
     }
 
-    /**
-     * Constructs a no-prefix instance.
-     *
-     * @param out non-null; writer to send final output to
-     * @param width &gt;= 0; the maximum output width (not including
-     * <code>prefix</code>), or <code>0</code> for no maximum
-     */
-    public WrappedIndentingWriter(Writer out, int width) {
-        this(out, width, "");
+    private void writeIndent() throws IOException {
+        for (int i=0; i<getIndent(); i++) {
+            write(' ');
+        }
     }
 
-    /** {@inheritDoc} */
-    @Override
-    public void write(int c) throws IOException {
-        synchronized (lock) {
-            if (collectingIndent) {
-                if (c == ' ') {
-                    indent++;
-                    if (indent >= maxIndent) {
-                        indent = maxIndent;
-                        collectingIndent = false;
-                    }
-                } else {
-                    collectingIndent = false;
-                }
+    private int getIndent() {
+        if (currentIndent < 0) {
+            return 0;
+        }
+        if (currentIndent > maxIndent) {
+            return maxIndent;
+        }
+        return currentIndent;
+    }
+
+    public void indent(int indent) {
+        currentIndent += indent;
+    }
+
+    public void deindent(int indent) {
+        currentIndent -= indent;
+    }
+
+    private void wrapLine() throws IOException {
+        List<String> wrapped = Lists.newArrayList(StringWrapper.wrapStringOnBreaks(line.toString(), maxWidth));
+        out.write(wrapped.get(0), 0, wrapped.get(0).length());
+        out.write('\n');
+
+        line.replace(0, line.length(), "");
+        writeIndent();
+        for (int i=1; i<wrapped.size(); i++) {
+            if (i > 1) {
+                write('\n');
             }
+            write(wrapped.get(i));
+        }
+    }
 
-            if ((column == width) && (c != '\n')) {
-                out.write('\n');
-                column = 0;
-                /*
-                 * Note: No else, so this should fall through to the next
-                 * if statement.
-                 */
-            }
-
-            if (column == 0) {
-                if (prefix != null) {
-                    out.write(prefix);
-                }
-
-                if (!collectingIndent) {
-                    for (int i = 0; i < indent; i++) {
-                        out.write(' ');
-                    }
-                    column = indent;
-                }
-            }
-
+    @Override public void write(int c) throws IOException {
+        if (c == '\n') {
+            out.write(line.toString());
             out.write(c);
-
-            if (c == '\n') {
-                bol();
-            } else {
-                column++;
+            line.replace(0, line.length(), "");
+            writeIndent();
+        } else {
+            line.append((char)c);
+            if (line.length() > maxWidth) {
+                wrapLine();
             }
         }
     }
 
-    /** {@inheritDoc} */
-    @Override
-    public void write(char[] cbuf, int off, int len) throws IOException {
-        synchronized (lock) {
-            while (len > 0) {
-                write(cbuf[off]);
-                off++;
-                len--;
-            }
+    @Override public void write(char[] cbuf, int off, int len) throws IOException {
+        for (int i=0; i<len; i++) {
+            write(cbuf[i+off]);
         }
     }
 
-    /** {@inheritDoc} */
-    @Override
-    public void write(String str, int off, int len) throws IOException {
-        synchronized (lock) {
-            while (len > 0) {
-                write(str.charAt(off));
-                off++;
-                len--;
-            }
+    @Override public void write(String str, int off, int len) throws IOException {
+        for (int i=0; i<len; i++) {
+            write(str.charAt(i+off));
         }
     }
 
-    /**
-     * Indicates that output is at the beginning of a line.
-     */
-    private void bol() {
-        column = 0;
-        collectingIndent = (maxIndent != 0);
-        indent = 0;
+    @Override public void flush() throws IOException {
+        out.write(line.toString());
+        line.replace(0, line.length(), "");
     }
 }
diff --git a/util/src/main/java/org/jf/util/jcommander/ColonParameterSplitter.java b/util/src/main/java/org/jf/util/jcommander/ColonParameterSplitter.java
new file mode 100644
index 0000000..eb628af
--- /dev/null
+++ b/util/src/main/java/org/jf/util/jcommander/ColonParameterSplitter.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ *     * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ *     * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ *     * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.util.jcommander;
+
+import com.beust.jcommander.converters.IParameterSplitter;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * A JCommander parameter splitter that splits a parameter value by colon
+ */
+public class ColonParameterSplitter implements IParameterSplitter {
+    @Override
+    public List<String> split(String value) {
+        return Arrays.asList(value.split(":"));
+    }
+}
diff --git a/util/src/main/java/org/jf/util/jcommander/Command.java b/util/src/main/java/org/jf/util/jcommander/Command.java
new file mode 100644
index 0000000..8fac0fa
--- /dev/null
+++ b/util/src/main/java/org/jf/util/jcommander/Command.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.util.jcommander;
+
+import com.beust.jcommander.JCommander;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import org.jf.util.ConsoleUtil;
+
+import javax.annotation.Nonnull;
+import java.util.List;
+
+public abstract class Command {
+
+    @Nonnull
+    protected final List<JCommander> commandAncestors;
+
+    public Command(@Nonnull List<JCommander> commandAncestors) {
+        this.commandAncestors = commandAncestors;
+    }
+
+    public void usage() {
+        System.out.println(new HelpFormatter()
+                .width(ConsoleUtil.getConsoleWidth())
+                .format(getCommandHierarchy()));
+    }
+
+    protected void setupCommand(JCommander jc) {
+    }
+
+    protected JCommander getJCommander() {
+        JCommander parentJc = Iterables.getLast(commandAncestors);
+        return parentJc.getCommands().get(this.getClass().getAnnotation(ExtendedParameters.class).commandName());
+    }
+
+    public List<JCommander> getCommandHierarchy() {
+        List<JCommander> commandHierarchy = Lists.newArrayList(commandAncestors);
+        commandHierarchy.add(getJCommander());
+        return commandHierarchy;
+    }
+
+    public abstract void run();
+}
diff --git a/util/src/main/java/org/jf/util/jcommander/ExtendedCommands.java b/util/src/main/java/org/jf/util/jcommander/ExtendedCommands.java
new file mode 100644
index 0000000..209d94e
--- /dev/null
+++ b/util/src/main/java/org/jf/util/jcommander/ExtendedCommands.java
@@ -0,0 +1,150 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.util.jcommander;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.ParameterDescription;
+import com.beust.jcommander.Parameterized;
+import com.beust.jcommander.Parameters;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+import java.lang.reflect.Field;
+
+/**
+ * Utilities related to "extended" commands - JCommander commands with additional information
+ */
+public class ExtendedCommands {
+
+    @Nonnull
+    private static ExtendedParameters getExtendedParameters(Object command) {
+        ExtendedParameters anno = command.getClass().getAnnotation(ExtendedParameters.class);
+        if (anno == null) {
+            throw new IllegalStateException("All extended commands should have an ExtendedParameters annotation: " +
+                    command.getClass().getCanonicalName());
+        }
+        return anno;
+    }
+
+    @Nonnull
+    public static String commandName(JCommander jc) {
+        return getExtendedParameters(jc.getObjects().get(0)).commandName();
+    }
+
+    @Nonnull
+    public static String commandName(Object command) {
+        return getExtendedParameters(command).commandName();
+    }
+
+    @Nonnull
+    public static String[] commandAliases(JCommander jc) {
+        return commandAliases(jc.getObjects().get(0));
+    }
+
+    @Nonnull
+    public static String[] commandAliases(Object command) {
+        return getExtendedParameters(command).commandAliases();
+    }
+
+    public static boolean includeParametersInUsage(JCommander jc) {
+        return includeParametersInUsage(jc.getObjects().get(0));
+    }
+
+    public static boolean includeParametersInUsage(Object command) {
+        return getExtendedParameters(command).includeParametersInUsage();
+    }
+
+    @Nonnull
+    public static String postfixDescription(JCommander jc) {
+        return postfixDescription(jc.getObjects().get(0));
+    }
+
+    @Nonnull
+    public static String postfixDescription(Object command) {
+        return getExtendedParameters(command).postfixDescription();
+    }
+
+    public static void addExtendedCommand(JCommander jc, Command command) {
+        jc.addCommand(commandName(command), command, commandAliases(command));
+        command.setupCommand(command.getJCommander());
+    }
+
+    @Nonnull
+    public static String[] parameterArgumentNames(ParameterDescription parameterDescription) {
+        Parameterized parameterized = parameterDescription.getParameterized();
+
+        Class cls = parameterDescription.getObject().getClass();
+        Field field = null;
+        while (cls != Object.class) {
+            try {
+                field = cls.getDeclaredField(parameterized.getName());
+            } catch (NoSuchFieldException ex) {
+                cls = cls.getSuperclass();
+                continue;
+            }
+            break;
+        }
+
+        assert field != null;
+        ExtendedParameter extendedParameter = field.getAnnotation(ExtendedParameter.class);
+        if (extendedParameter != null) {
+            return extendedParameter.argumentNames();
+        }
+
+        return new String[0];
+    }
+
+    @Nullable
+    public static JCommander getSubcommand(JCommander jc, String commandName) {
+        if (jc.getCommands().containsKey(commandName)) {
+            return jc.getCommands().get(commandName);
+        } else {
+            for (JCommander command : jc.getCommands().values()) {
+                for (String alias: commandAliases(command)) {
+                    if (commandName.equals(alias)) {
+                        return command;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    @Nullable
+    public static String getCommandDescription(@Nonnull JCommander jc) {
+        Parameters parameters = jc.getObjects().get(0).getClass().getAnnotation(Parameters.class);
+        if (parameters == null) {
+            return null;
+        }
+        return parameters.commandDescription();
+    }
+}
diff --git a/util/src/main/java/org/jf/util/jcommander/ExtendedParameter.java b/util/src/main/java/org/jf/util/jcommander/ExtendedParameter.java
new file mode 100644
index 0000000..81f78c2
--- /dev/null
+++ b/util/src/main/java/org/jf/util/jcommander/ExtendedParameter.java
@@ -0,0 +1,40 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.util.jcommander;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ExtendedParameter {
+    String[] argumentNames();
+}
diff --git a/util/src/main/java/org/jf/util/jcommander/ExtendedParameters.java b/util/src/main/java/org/jf/util/jcommander/ExtendedParameters.java
new file mode 100644
index 0000000..965d2b2
--- /dev/null
+++ b/util/src/main/java/org/jf/util/jcommander/ExtendedParameters.java
@@ -0,0 +1,43 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.util.jcommander;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+@Retention(RetentionPolicy.RUNTIME)
+public @interface ExtendedParameters {
+    boolean includeParametersInUsage() default false;
+    String commandName();
+    String[] commandAliases() default { };
+    String postfixDescription() default "";
+}
diff --git a/util/src/main/java/org/jf/util/jcommander/HelpFormatter.java b/util/src/main/java/org/jf/util/jcommander/HelpFormatter.java
new file mode 100644
index 0000000..e807d5f
--- /dev/null
+++ b/util/src/main/java/org/jf/util/jcommander/HelpFormatter.java
@@ -0,0 +1,316 @@
+/*
+ * Copyright 2016, Google Inc.
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+package org.jf.util.jcommander;
+
+import com.beust.jcommander.JCommander;
+import com.beust.jcommander.ParameterDescription;
+import com.beust.jcommander.Parameters;
+import com.beust.jcommander.internal.Lists;
+import com.google.common.base.Joiner;
+import com.google.common.collect.Iterables;
+import org.jf.util.WrappedIndentingWriter;
+
+import javax.annotation.Nonnull;
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map.Entry;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class HelpFormatter {
+
+    private int width = 80;
+
+    @Nonnull
+    public HelpFormatter width(int width) {
+        this.width = width;
+        return this;
+    }
+
+    @Nonnull
+    private static ExtendedParameters getExtendedParameters(JCommander jc) {
+        ExtendedParameters anno = jc.getObjects().get(0).getClass().getAnnotation(ExtendedParameters.class);
+        if (anno == null) {
+            throw new IllegalStateException("All commands should have an ExtendedParameters annotation");
+        }
+        return anno;
+    }
+
+    @Nonnull
+    private static List<String> getCommandAliases(JCommander jc) {
+        return Lists.newArrayList(getExtendedParameters(jc).commandAliases());
+    }
+
+    private static boolean includeParametersInUsage(@Nonnull JCommander jc) {
+        return getExtendedParameters(jc).includeParametersInUsage();
+    }
+
+    @Nonnull
+    private static String getPostfixDescription(@Nonnull JCommander jc) {
+        return getExtendedParameters(jc).postfixDescription();
+    }
+
+    private int getParameterArity(ParameterDescription param) {
+        if (param.getParameter().arity() > 0) {
+            return param.getParameter().arity();
+        }
+        Class<?> type = param.getParameterized().getType();
+        if ((type == boolean.class || type == Boolean.class)) {
+            return 0;
+        }
+        return 1;
+    }
+
+    private List<ParameterDescription> getSortedParameters(JCommander jc) {
+        List<ParameterDescription> parameters = Lists.newArrayList(jc.getParameters());
+
+        final Pattern pattern = Pattern.compile("^-*(.*)$");
+
+        Collections.sort(parameters, new Comparator<ParameterDescription>() {
+            @Override public int compare(ParameterDescription o1, ParameterDescription o2) {
+                String s1;
+                Matcher matcher = pattern.matcher(o1.getParameter().names()[0]);
+                if (matcher.matches()) {
+                    s1 = matcher.group(1);
+                } else {
+                    throw new IllegalStateException();
+                }
+
+                String s2;
+                matcher = pattern.matcher(o2.getParameter().names()[0]);
+                if (matcher.matches()) {
+                    s2 = matcher.group(1);
+                } else {
+                    throw new IllegalStateException();
+                }
+
+                return s1.compareTo(s2);
+            }
+        });
+        return parameters;
+    }
+
+    @Nonnull
+    public String format(@Nonnull JCommander... jc) {
+        return format(Arrays.asList(jc));
+    }
+
+    @Nonnull
+    public String format(@Nonnull List<JCommander> commandHierarchy) {
+        try {
+            StringWriter stringWriter = new StringWriter();
+            WrappedIndentingWriter writer = new WrappedIndentingWriter(stringWriter, width - 5, width);
+
+            JCommander leafJc = Iterables.getLast(commandHierarchy);
+
+            writer.write("usage:");
+            writer.indent(2);
+
+            for (JCommander jc: commandHierarchy) {
+                writer.write(" ");
+                writer.write(ExtendedCommands.commandName(jc));
+            }
+
+            if (includeParametersInUsage(leafJc)) {
+                for (ParameterDescription param : leafJc.getParameters()) {
+                    if (!param.getParameter().hidden()) {
+                        writer.write(" [");
+                        writer.write(param.getParameter().getParameter().names()[0]);
+                        writer.write("]");
+                    }
+                }
+            } else {
+                if (!leafJc.getParameters().isEmpty()) {
+                    writer.write(" [<options>]");
+                }
+            }
+
+            if (!leafJc.getCommands().isEmpty()) {
+                writer.write(" [<command [<args>]]");
+            }
+
+            if (leafJc.getMainParameter() != null) {
+                String[] argumentNames = ExtendedCommands.parameterArgumentNames(leafJc.getMainParameter());
+                if (argumentNames.length == 0) {
+                    writer.write(" <args>");
+                } else {
+                    String argumentName = argumentNames[0];
+                    boolean writeAngleBrackets = !argumentName.startsWith("<") && !argumentName.startsWith("[");
+                    writer.write(" ");
+                    if (writeAngleBrackets) {
+                        writer.write("<");
+                    }
+                    writer.write(argumentNames[0]);
+                    if (writeAngleBrackets) {
+                        writer.write(">");
+                    }
+                }
+            }
+
+            writer.deindent(2);
+
+            String commandDescription = ExtendedCommands.getCommandDescription(leafJc);
+            if (commandDescription != null) {
+                writer.write("\n");
+                writer.write(commandDescription);
+            }
+
+            if (!leafJc.getParameters().isEmpty() || leafJc.getMainParameter() != null) {
+                writer.write("\n\nOptions:");
+                writer.indent(2);
+                for (ParameterDescription param : getSortedParameters(leafJc)) {
+                    if (!param.getParameter().hidden()) {
+                        writer.write("\n");
+                        writer.indent(4);
+                        if (!param.getNames().isEmpty()) {
+                            writer.write(Joiner.on(',').join(param.getParameter().names()));
+                        }
+                        if (getParameterArity(param) > 0) {
+                            String[] argumentNames = ExtendedCommands.parameterArgumentNames(param);
+                            for (int i = 0; i < getParameterArity(param); i++) {
+                                writer.write(" ");
+                                if (i < argumentNames.length) {
+                                    writer.write("<");
+                                    writer.write(argumentNames[i]);
+                                    writer.write(">");
+                                } else {
+                                    writer.write("<arg>");
+                                }
+                            }
+                        }
+                        if (param.getDescription() != null && !param.getDescription().isEmpty()) {
+                            writer.write(" - ");
+                            writer.write(param.getDescription());
+                        }
+                        if (param.getDefault() != null) {
+                            String defaultValue = null;
+                            if (param.getParameterized().getType() == Boolean.class ||
+                                    param.getParameterized().getType() == Boolean.TYPE) {
+                                if ((Boolean)param.getDefault()) {
+                                    defaultValue = "True";
+                                }
+                            } else if (List.class.isAssignableFrom(param.getParameterized().getType())) {
+                                if (!((List)param.getDefault()).isEmpty()) {
+                                    defaultValue = param.getDefault().toString();
+                                }
+                            } else {
+                                defaultValue = param.getDefault().toString();
+                            }
+                            if (defaultValue != null) {
+                                writer.write(" (default: ");
+                                writer.write(defaultValue);
+                                writer.write(")");
+                            }
+                        }
+                        writer.deindent(4);
+                    }
+                }
+
+                if (leafJc.getMainParameter() != null) {
+                    String[] argumentNames = ExtendedCommands.parameterArgumentNames(leafJc.getMainParameter());
+                    writer.write("\n");
+                    writer.indent(4);
+                    if (argumentNames.length > 0) {
+                        writer.write("<");
+                        writer.write(argumentNames[0]);
+                        writer.write(">");
+                    } else {
+                        writer.write("<args>");
+                    }
+
+                    if (leafJc.getMainParameterDescription() != null) {
+                        writer.write(" - ");
+                        writer.write(leafJc.getMainParameterDescription());
+                    }
+                    writer.deindent(4);
+                }
+                writer.deindent(2);
+            }
+
+            if (!leafJc.getCommands().isEmpty()) {
+                writer.write("\n\nCommands:");
+                writer.indent(2);
+
+
+                List<Entry<String, JCommander>> entryList = Lists.newArrayList(leafJc.getCommands().entrySet());
+                Collections.sort(entryList, new Comparator<Entry<String, JCommander>>() {
+                    @Override public int compare(Entry<String, JCommander> o1, Entry<String, JCommander> o2) {
+                        return o1.getKey().compareTo(o2.getKey());
+                    }
+                });
+
+                for (Entry<String, JCommander> entry : entryList) {
+                    String commandName = entry.getKey();
+                    JCommander command = entry.getValue();
+
+                    Object arg = command.getObjects().get(0);
+                    Parameters parametersAnno = arg.getClass().getAnnotation(Parameters.class);
+                    if (!parametersAnno.hidden()) {
+                        writer.write("\n");
+                        writer.indent(4);
+                        writer.write(commandName);
+                        List<String> aliases = getCommandAliases(command);
+                        if (!aliases.isEmpty()) {
+                            writer.write("(");
+                            writer.write(Joiner.on(',').join(aliases));
+                            writer.write(")");
+                        }
+
+                        String commandDesc = leafJc.getCommandDescription(commandName);
+                        if (commandDesc != null) {
+                            writer.write(" - ");
+                            writer.write(commandDesc);
+                        }
+                        writer.deindent(4);
+                    }
+                }
+                writer.deindent(2);
+            }
+
+            String postfixDescription = getPostfixDescription(leafJc);
+            if (!postfixDescription.isEmpty()) {
+                writer.write("\n\n");
+                writer.write(postfixDescription);
+            }
+
+            writer.flush();
+
+            return stringWriter.getBuffer().toString();
+        } catch (IOException ex) {
+            throw new RuntimeException(ex);
+        }
+    }
+}
diff --git a/util/src/test/java/org/jf/util/StringWrapperTest.java b/util/src/test/java/org/jf/util/StringWrapperTest.java
index 64dca33..94c7914 100644
--- a/util/src/test/java/org/jf/util/StringWrapperTest.java
+++ b/util/src/test/java/org/jf/util/StringWrapperTest.java
@@ -31,11 +31,35 @@
 
 package org.jf.util;
 
+import com.google.common.collect.Lists;
 import org.junit.Assert;
 import org.junit.Test;
 
+import java.util.List;
+
 public class StringWrapperTest {
     @Test
+    public void testWrapStringByWords() {
+        validateResult2(new String[]{"abc", "abcdef", "abcdef"},
+                "abc\nabcdefabcdef", 6);
+
+        validateResult2(new String[]{"abc", "abcdef", " ", "abcdef"},
+                "abc\nabcdef abcdef", 6);
+
+        validateResult2(new String[]{"abc", "abcde ", "fabcde", "f"},
+                "abc\nabcde fabcdef", 6);
+
+        validateResult2(new String[]{"abc def ghi ", "kjl mon pqr ", "stu vwx yz"},
+                "abc def ghi kjl mon pqr stu vwx yz", 14);
+
+        validateResult2(new String[]{"abcdefg", "hikjlmo", "npqrstu", "vwxyz"},
+                "abcdefghikjlmonpqrstuvwxyz", 7);
+
+        validateResult2(new String[]{"abc", "defhig"},
+                "abc\ndefhig", 20);
+    }
+
+    @Test
     public void testWrapString() {
         validateResult(
                 new String[]{"abc", "abcdef", "abcdef"},
@@ -115,4 +139,15 @@
             Assert.assertEquals(expected[i], actual[i]);
         }
     }
+
+    public static void validateResult2(String[] expected, String textToWrap, int maxWidth) {
+        List<String> result = Lists.newArrayList(StringWrapper.wrapStringOnBreaks(textToWrap, maxWidth));
+
+        Assert.assertEquals(expected.length, result.size());
+        int i;
+        for (i=0; i<result.size(); i++) {
+            Assert.assertTrue(i < expected.length);
+            Assert.assertEquals(expected[i], result.get(i));
+        }
+    }
 }