Merge "Apply the prior lense _after_ finding members to be bound."
diff --git a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
index 362fc2c..83cb824 100644
--- a/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
+++ b/src/main/java/com/android/tools/r8/graph/DexEncodedMethod.java
@@ -47,6 +47,7 @@
import com.android.tools.r8.utils.InternalOptions;
import com.google.common.collect.ImmutableList;
import java.util.Arrays;
+import java.util.BitSet;
import java.util.Collections;
import java.util.List;
import java.util.function.Consumer;
@@ -677,6 +678,7 @@
private ClassInlinerEligibility classInlinerEligibility = null;
private TrivialInitializer trivialInitializerInfo = null;
private ParameterUsagesInfo parametersUsages = null;
+ private BitSet kotlinNotNullParamHints = null;
private OptimizationInfo() {
// Intentionally left empty.
@@ -700,6 +702,14 @@
return parametersUsages == null ? null : parametersUsages.getParameterUsage(parameter);
}
+ public BitSet getKotlinNotNullParamHints() {
+ return kotlinNotNullParamHints;
+ }
+
+ public void setKotlinNotNullParamHints(BitSet hints) {
+ this.kotlinNotNullParamHints = hints;
+ }
+
public boolean returnsArgument() {
return returnedArgument != -1;
}
@@ -843,6 +853,10 @@
ensureMutableOI().setParameterUsages(parametersUsages);
}
+ synchronized public void setKotlinNotNullParamHints(BitSet hints) {
+ ensureMutableOI().setKotlinNotNullParamHints(hints);
+ }
+
synchronized public void setTrivialInitializer(TrivialInitializer info) {
ensureMutableOI().setTrivialInitializer(info);
}
diff --git a/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java b/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
index cd3c093..bfdc1b7 100644
--- a/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
+++ b/src/main/java/com/android/tools/r8/graph/DexMethodHandle.java
@@ -177,7 +177,8 @@
case INVOKE_SUPER:
return Type.SUPER;
default:
- throw new Unreachable("DexMethodHandle with unexpected type: " + this);
+ throw new Unreachable(
+ "Conversion to invoke type with unexpected method handle: " + this);
}
}
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
index 1a43128..51e1891 100644
--- a/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
+++ b/src/main/java/com/android/tools/r8/ir/code/CheckCast.java
@@ -35,6 +35,11 @@
this.type = type;
}
+ @Override
+ boolean computeNeverNull() {
+ return object().isNeverNull();
+ }
+
public DexType getType() {
return type;
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Instruction.java b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
index 5d37ea6..62e518d 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Instruction.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Instruction.java
@@ -68,6 +68,12 @@
this.position = position;
}
+ boolean computeNeverNull() {
+ // By default just return the flag from the out value since it never changes.
+ assert outValue != null;
+ return outValue.isNeverNull();
+ }
+
public final void forceSetPosition(Position position) {
this.position = position;
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Invoke.java b/src/main/java/com/android/tools/r8/ir/code/Invoke.java
index 819bb4b..fc5ee05 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Invoke.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Invoke.java
@@ -12,6 +12,7 @@
import com.android.tools.r8.graph.AppInfo;
import com.android.tools.r8.graph.DexItem;
import com.android.tools.r8.graph.DexMethod;
+import com.android.tools.r8.graph.DexMethodHandle.MethodHandleType;
import com.android.tools.r8.graph.DexProto;
import com.android.tools.r8.graph.DexType;
import com.android.tools.r8.ir.analysis.type.TypeLatticeElement;
@@ -30,7 +31,29 @@
NEW_ARRAY,
MULTI_NEW_ARRAY,
CUSTOM,
- POLYMORPHIC
+ POLYMORPHIC;
+
+ public MethodHandleType toMethodHandle(DexMethod targetMethod) {
+ switch (this) {
+ case STATIC:
+ return MethodHandleType.INVOKE_STATIC;
+ case VIRTUAL:
+ return MethodHandleType.INVOKE_INSTANCE;
+ case DIRECT:
+ if (targetMethod.name.toString().equals("<init>")) {
+ return MethodHandleType.INVOKE_CONSTRUCTOR;
+ } else {
+ return MethodHandleType.INVOKE_DIRECT;
+ }
+ case INTERFACE:
+ return MethodHandleType.INVOKE_INTERFACE;
+ case SUPER:
+ return MethodHandleType.INVOKE_SUPER;
+ default:
+ throw new Unreachable(
+ "Conversion to method handle with unexpected invoke type: " + this);
+ }
+ }
}
public Invoke(Value result, List<Value> arguments) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Move.java b/src/main/java/com/android/tools/r8/ir/code/Move.java
index f27153d..b095bb4 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Move.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Move.java
@@ -29,6 +29,11 @@
}
}
+ @Override
+ boolean computeNeverNull() {
+ return src().isNeverNull();
+ }
+
public Value dest() {
return outValue;
}
diff --git a/src/main/java/com/android/tools/r8/ir/code/Phi.java b/src/main/java/com/android/tools/r8/ir/code/Phi.java
index 51850a0..ed09ee7 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Phi.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Phi.java
@@ -57,7 +57,6 @@
// period of time to break cycles. When the cycle has been resolved they are completed
// exactly once by adding the operands.
assert operands.isEmpty();
- boolean canBeNull = false;
if (block.getPredecessors().size() == 0) {
throwUndefinedValueError();
}
@@ -66,13 +65,10 @@
// Since this read has been delayed we must provide the local info for the value.
Value operand = builder.readRegister(register, type, pred, edgeType, getLocalInfo());
operand.constrainType(type);
- canBeNull |= operand.canBeNull();
appendOperand(operand);
}
- if (!canBeNull) {
- markNeverNull();
- }
removeTrivialPhi();
+ recomputeNeverNull();
}
public void addOperands(List<Value> operands) {
@@ -84,20 +80,24 @@
// period of time to break cycles. When the cycle has been resolved they are completed
// exactly once by adding the operands.
assert this.operands.isEmpty();
- boolean canBeNull = false;
if (operands.size() == 0) {
throwUndefinedValueError();
}
for (Value operand : operands) {
- canBeNull |= operand.canBeNull();
appendOperand(operand);
}
- if (!canBeNull) {
- markNeverNull();
- }
if (removeTrivialPhi) {
removeTrivialPhi();
}
+ recomputeNeverNull();
+ }
+
+ // Implementation assumes that canBeNull may change to neverNull, but
+ // not other way around. This will need to be revised later.
+ void recomputeNeverNull() {
+ if (canBeNull() && operands.stream().allMatch(Value::isNeverNull)) {
+ markNeverNull();
+ }
}
public void addDebugValue(Value value) {
@@ -131,7 +131,10 @@
public void removeOperand(int index) {
operands.get(index).removePhiUser(this);
- operands.remove(index);
+ Value value = operands.remove(index);
+ if (value.canBeNull()) {
+ recomputeNeverNull();
+ }
}
public void removeOperandsByIndex(List<Integer> operandsToRemove) {
@@ -147,6 +150,7 @@
current = i + 1;
}
operands.addAll(copy.subList(current, copy.size()));
+ recomputeNeverNull();
}
public void replaceOperandAt(int predIndex, Value newValue) {
@@ -154,6 +158,9 @@
operands.set(predIndex, newValue);
newValue.addPhiUser(this);
current.removePhiUser(this);
+ if (current.canBeNull() && newValue.isNeverNull()) {
+ recomputeNeverNull();
+ }
}
void replaceOperand(Value current, Value newValue) {
@@ -163,6 +170,9 @@
newValue.addPhiUser(this);
}
}
+ if (current.canBeNull() && newValue.isNeverNull()) {
+ recomputeNeverNull();
+ }
}
void replaceDebugValue(Value current, Value newValue) {
diff --git a/src/main/java/com/android/tools/r8/ir/code/Value.java b/src/main/java/com/android/tools/r8/ir/code/Value.java
index 05dd801..815a38f 100644
--- a/src/main/java/com/android/tools/r8/ir/code/Value.java
+++ b/src/main/java/com/android/tools/r8/ir/code/Value.java
@@ -628,6 +628,17 @@
public void markNeverNull() {
assert !neverNull;
neverNull = true;
+
+ // Propagate never null flag change.
+ for (Instruction user : users) {
+ Value value = user.outValue;
+ if (value != null && value.canBeNull() && user.computeNeverNull()) {
+ value.markNeverNull();
+ }
+ }
+ for (Phi phi : phiUsers) {
+ phi.recomputeNeverNull();
+ }
}
/**
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
index 479af89..dce8aa6 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/IRConverter.java
@@ -65,6 +65,7 @@
import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;
import java.util.ArrayList;
+import java.util.BitSet;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
@@ -630,6 +631,11 @@
printC1VisualizerHeader(method);
printMethod(code, "Initial IR (SSA)");
+ if (method.getCode() != null && method.getCode().isJarCode() &&
+ appInfo.definitionFor(method.method.holder).hasKotlinInfo()) {
+ computeKotlinNotNullParamHints(feedback, method, code);
+ }
+
if (options.canHaveArtStringNewInitBug()) {
CodeRewriter.ensureDirectStringNewToInit(code);
}
@@ -655,7 +661,8 @@
}
if (memberValuePropagation != null) {
- memberValuePropagation.rewriteWithConstantValues(code, method.method.holder);
+ memberValuePropagation.rewriteWithConstantValues(
+ code, method.method.holder, isProcessedConcurrently);
}
if (options.enableSwitchMapRemoval && appInfo.hasLiveness()) {
codeRewriter.removeSwitchMaps(code);
@@ -779,6 +786,36 @@
finalizeIR(method, code, feedback);
}
+ private void computeKotlinNotNullParamHints(
+ OptimizationFeedback feedback, DexEncodedMethod method, IRCode code) {
+ // Try to infer Kotlin non-null parameter check to use it as a hint.
+ List<Value> arguments = code.collectArguments(true);
+ BitSet paramsCheckedForNull = new BitSet();
+ DexMethod checkParameterIsNotNull =
+ code.options.itemFactory.kotlin.intrinsics.checkParameterIsNotNull;
+ for (int index = 0; index < arguments.size(); index++) {
+ Value argument = arguments.get(index);
+ for (Instruction user : argument.uniqueUsers()) {
+ // To enforce parameter non-null requirement Kotlin uses intrinsic:
+ // kotlin.jvm.internal.Intrinsics.checkParameterIsNotNull(param, message)
+ //
+ // with the following we simply look if the parameter is ever passed
+ // to the mentioned intrinsic as the first argument. We do it only for
+ // code coming from Java classfile, since after the method is rewritten
+ // by R8 this call gets inlined.
+ if (!user.isInvokeStatic() ||
+ user.asInvokeMethod().getInvokedMethod() != checkParameterIsNotNull ||
+ user.inValues().indexOf(argument) != 0) {
+ continue;
+ }
+ paramsCheckedForNull.set(index);
+ }
+ }
+ if (paramsCheckedForNull.length() > 0) {
+ feedback.setKotlinNotNullParamHints(method, paramsCheckedForNull);
+ }
+ }
+
private void finalizeIR(DexEncodedMethod method, IRCode code, OptimizationFeedback feedback) {
code.traceBlocks();
if (options.isGeneratingClassFiles()) {
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
index 6b016b9..9a88674 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/LensCodeRewriter.java
@@ -78,13 +78,13 @@
DexProto newMethodProto =
appInfo.dexItemFactory.applyClassMappingToProto(
callSite.methodProto, graphLense::lookupType, protoFixupCache);
- DexMethodHandle newBootstrapMethod = rewriteDexMethodHandle(method,
- callSite.bootstrapMethod);
+ DexMethodHandle newBootstrapMethod =
+ rewriteDexMethodHandle(callSite.bootstrapMethod, method);
List<DexValue> newArgs = callSite.bootstrapArgs.stream().map(
(arg) -> {
if (arg instanceof DexValueMethodHandle) {
return new DexValueMethodHandle(
- rewriteDexMethodHandle(method, ((DexValueMethodHandle) arg).value));
+ rewriteDexMethodHandle(((DexValueMethodHandle) arg).value, method));
}
return arg;
})
@@ -226,16 +226,17 @@
}
private DexMethodHandle rewriteDexMethodHandle(
- DexEncodedMethod method, DexMethodHandle methodHandle) {
+ DexMethodHandle methodHandle, DexEncodedMethod context) {
if (methodHandle.isMethodHandle()) {
DexMethod invokedMethod = methodHandle.asMethod();
+ MethodHandleType oldType = methodHandle.type;
GraphLenseLookupResult lenseLookup =
- graphLense.lookupMethod(invokedMethod, method, methodHandle.type.toInvokeType());
+ graphLense.lookupMethod(invokedMethod, context, oldType.toInvokeType());
DexMethod actualTarget = lenseLookup.getMethod();
+ MethodHandleType newType = lenseLookup.getType().toMethodHandle(actualTarget);
if (actualTarget != invokedMethod) {
- MethodHandleType newType = methodHandle.type;
DexClass clazz = appInfo.definitionFor(actualTarget.holder);
- if (clazz != null && (newType.isInvokeInterface() || newType.isInvokeInstance())) {
+ if (clazz != null && (oldType.isInvokeInterface() || oldType.isInvokeInstance())) {
newType =
lenseLookup.getType() == Type.INTERFACE
? MethodHandleType.INVOKE_INTERFACE
@@ -243,6 +244,9 @@
}
return new DexMethodHandle(newType, actualTarget);
}
+ if (oldType != newType) {
+ return new DexMethodHandle(newType, actualTarget);
+ }
} else {
DexField field = methodHandle.asField();
DexField actualField = graphLense.lookupField(field);
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
index 7ed34ba..ad6d66d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedback.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo;
import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.BitSet;
public interface OptimizationFeedback {
void methodReturnsArgument(DexEncodedMethod method, int argument);
@@ -21,4 +22,5 @@
void setClassInlinerEligibility(DexEncodedMethod method, ClassInlinerEligibility eligibility);
void setTrivialInitializer(DexEncodedMethod method, TrivialInitializer info);
void setParameterUsages(DexEncodedMethod method, ParameterUsagesInfo parameterUsagesInfo);
+ void setKotlinNotNullParamHints(DexEncodedMethod method, BitSet hints);
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
index 44588ab..326fa03 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackDirect.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo;
import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.BitSet;
public class OptimizationFeedbackDirect implements OptimizationFeedback {
@@ -62,4 +63,9 @@
public void setParameterUsages(DexEncodedMethod method, ParameterUsagesInfo parameterUsagesInfo) {
method.setParameterUsages(parameterUsagesInfo);
}
+
+ @Override
+ public void setKotlinNotNullParamHints(DexEncodedMethod method, BitSet hints) {
+ method.setKotlinNotNullParamHints(hints);
+ }
}
diff --git a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
index c2d6ede..359c90d 100644
--- a/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
+++ b/src/main/java/com/android/tools/r8/ir/conversion/OptimizationFeedbackIgnore.java
@@ -9,6 +9,7 @@
import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo;
import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
import com.android.tools.r8.ir.optimize.Inliner.Constraint;
+import java.util.BitSet;
public class OptimizationFeedbackIgnore implements OptimizationFeedback {
@@ -45,4 +46,8 @@
@Override
public void setParameterUsages(DexEncodedMethod method, ParameterUsagesInfo parameterUsagesInfo) {
}
+
+ @Override
+ public void setKotlinNotNullParamHints(DexEncodedMethod method, BitSet hints) {
+ }
}
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
index 7ea7f57..22e3c32 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/CodeRewriter.java
@@ -755,7 +755,9 @@
//
// (1) as a receiver of reads/writes of instance fields of the holder class
// (2) as a return value
- // (3) as a receiver of a call to the superclass initializer
+ // (3) as a receiver of a call to the superclass initializer. Note that we don't
+ // check what is passed to superclass initializer as arguments, only check
+ // that it is not the instance being initialized.
//
boolean instanceInitializer = method.isInstanceInitializer();
if (method.accessFlags.isNative() ||
@@ -787,23 +789,25 @@
(insn.isInstancePut() && insn.asInstancePut().object() == receiver)) {
DexField field = insn.asFieldInstruction().getField();
if (field.clazz == clazz.type && clazz.lookupInstanceField(field) != null) {
- // Since class inliner currently only supports classes directly extending
- // java.lang.Object, we don't need to worry about fields defined in superclasses.
+ // Require only accessing instance fields of the *current* class.
continue;
}
return;
}
- // If this is an instance initializer allow one call
- // to java.lang.Object.<init>() on 'this'.
- if (instanceInitializer && insn.isInvokeDirect()) {
+ // If this is an instance initializer allow one call to superclass instance initializer.
+ if (insn.isInvokeDirect()) {
InvokeDirect invokedDirect = insn.asInvokeDirect();
- if (invokedDirect.getInvokedMethod() == dexItemFactory.objectMethods.constructor &&
- invokedDirect.getReceiver() == receiver &&
- !seenSuperInitCall) {
+ DexMethod invokedMethod = invokedDirect.getInvokedMethod();
+ if (dexItemFactory.isConstructor(invokedMethod) &&
+ invokedMethod.holder == clazz.superType &&
+ invokedDirect.inValues().lastIndexOf(receiver) == 0 &&
+ !seenSuperInitCall &&
+ instanceInitializer) {
seenSuperInitCall = true;
continue;
}
+ // We don't support other direct calls yet.
return;
}
@@ -870,7 +874,7 @@
// This method defines trivial instance initializer as follows:
//
- // ** The initializer may only call the initializer of the base class, which
+ // ** The initializer may call the initializer of the base class, which
// itself must be trivial.
//
// ** java.lang.Object.<init>() is considered trivial.
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
index b05fd4f..cc7f571 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/DefaultInliningOracle.java
@@ -14,11 +14,14 @@
import com.android.tools.r8.ir.code.InvokeMethodWithReceiver;
import com.android.tools.r8.ir.code.InvokePolymorphic;
import com.android.tools.r8.ir.code.InvokeStatic;
+import com.android.tools.r8.ir.code.Value;
import com.android.tools.r8.ir.conversion.CallSiteInformation;
import com.android.tools.r8.ir.optimize.Inliner.InlineAction;
import com.android.tools.r8.ir.optimize.Inliner.Reason;
import com.android.tools.r8.logging.Log;
import com.android.tools.r8.utils.InternalOptions;
+import java.util.BitSet;
+import java.util.List;
import java.util.ListIterator;
import java.util.function.Predicate;
@@ -218,13 +221,33 @@
if (reason == Reason.SIMPLE) {
// If we are looking for a simple method, only inline if actually simple.
Code code = candidate.getCode();
- if (!code.estimatedSizeForInliningAtMost(inliningInstructionLimit)) {
+ int instructionLimit = computeInstructionLimit(invoke, candidate);
+ if (!code.estimatedSizeForInliningAtMost(instructionLimit)) {
return false;
}
}
return true;
}
+ private int computeInstructionLimit(InvokeMethod invoke, DexEncodedMethod candidate) {
+ int instructionLimit = inliningInstructionLimit;
+ BitSet hints = candidate.getOptimizationInfo().getKotlinNotNullParamHints();
+ if (hints != null) {
+ List<Value> arguments = invoke.inValues();
+ if (invoke.isInvokeMethodWithReceiver()) {
+ arguments = arguments.subList(1, arguments.size());
+ }
+ for (int index = 0; index < arguments.size(); index++) {
+ Value argument = arguments.get(index);
+ if (argument.isNeverNull() && hints.get(index)) {
+ // 5-4 instructions per parameter check are expected to be removed.
+ instructionLimit += 4;
+ }
+ }
+ }
+ return instructionLimit;
+ }
+
@Override
public InlineAction computeForInvokeWithReceiver(
InvokeMethodWithReceiver invoke, DexType invocationContext) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
index cdfa2b1..f92eb80 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/MemberValuePropagation.java
@@ -7,6 +7,8 @@
import com.android.tools.r8.graph.DexClass;
import com.android.tools.r8.graph.DexEncodedField;
import com.android.tools.r8.graph.DexEncodedMethod;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.TrivialClassInitializer;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItem;
import com.android.tools.r8.graph.DexMethod;
@@ -23,6 +25,7 @@
import com.android.tools.r8.ir.code.ValueType;
import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
import com.android.tools.r8.shaking.ProguardMemberRule;
+import java.util.function.Predicate;
public class MemberValuePropagation {
@@ -113,7 +116,8 @@
* <p>
* Also assigns value ranges to values where possible.
*/
- public void rewriteWithConstantValues(IRCode code, DexType callingContext) {
+ public void rewriteWithConstantValues(
+ IRCode code, DexType callingContext, Predicate<DexEncodedMethod> isProcessedConcurrently) {
InstructionIterator iterator = code.instructionIterator();
while (iterator.hasNext()) {
Instruction current = iterator.next();
@@ -155,7 +159,7 @@
if (!invokeReplaced && invoke.outValue() != null) {
DexEncodedMethod target = invoke.computeSingleTarget(appInfo);
if (target != null) {
- if (target.getOptimizationInfo().neverReturnsNull()) {
+ if (target.getOptimizationInfo().neverReturnsNull() && invoke.outValue().canBeNull()) {
invoke.outValue().markNeverNull();
}
if (target.getOptimizationInfo().returnsConstant()) {
@@ -182,12 +186,11 @@
} else if (current.isStaticGet()) {
StaticGet staticGet = current.asStaticGet();
DexField field = staticGet.getField();
- Instruction replacement = null;
DexEncodedField target = appInfo.lookupStaticTarget(field.getHolder(), field);
ProguardMemberRuleLookup lookup = null;
if (target != null) {
// Check if a this value is known const.
- replacement = target.valueAsConstInstruction(appInfo, staticGet.dest());
+ Instruction replacement = target.valueAsConstInstruction(appInfo, staticGet.dest());
if (replacement == null) {
lookup = lookupMemberRule(target);
if (lookup != null) {
@@ -207,6 +210,37 @@
} else {
iterator.replaceCurrentInstruction(replacement);
}
+ } else if (staticGet.dest() != null) {
+ // In case the class holder of this static field satisfying following criteria:
+ // -- cannot trigger other static initializer except for its own
+ // -- is final
+ // -- has a class initializer which is classified as trivial
+ // (see CodeRewriter::computeClassInitializerInfo) and
+ // initializes the field being accessed
+ //
+ // ... and the field itself is not pinned by keep rules (in which case it might
+ // be updated outside the class constructor, e.g. via reflections), it is safe
+ // to assume that the static-get instruction reads the value it initialized value
+ // in class initializer and is never null.
+ //
+ DexClass holderDefinition = appInfo.definitionFor(field.getHolder());
+ if (holderDefinition != null &&
+ holderDefinition.accessFlags.isFinal() &&
+ !appInfo.canTriggerStaticInitializer(field.getHolder(), true)) {
+
+ Value outValue = staticGet.dest();
+ DexEncodedMethod classInitializer = holderDefinition.getClassInitializer();
+ if (classInitializer != null && !isProcessedConcurrently.test(classInitializer)) {
+ TrivialInitializer info =
+ classInitializer.getOptimizationInfo().getTrivialInitializerInfo();
+ if (info != null &&
+ ((TrivialClassInitializer) info).field == field &&
+ !appInfo.isPinned(field) &&
+ outValue.canBeNull()) {
+ outValue.markNeverNull();
+ }
+ }
+ }
}
}
} else if (current.isStaticPut()) {
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
index b28158a..219b5b1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/ClassInliner.java
@@ -16,6 +16,7 @@
import com.android.tools.r8.ir.optimize.InliningOracle;
import com.android.tools.r8.shaking.Enqueuer.AppInfoWithLiveness;
import com.google.common.collect.Streams;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
@@ -127,31 +128,51 @@
.filter(insn -> insn.isNewInstance() || insn.isStaticGet())
.collect(Collectors.toList());
- for (Instruction root : roots) {
- InlineCandidateProcessor processor =
- new InlineCandidateProcessor(factory, appInfo,
- type -> isClassEligible(appInfo, type),
- isProcessedConcurrently, method, root);
+ // We loop inlining iterations until there was no inlining, but still use same set
+ // of roots to avoid infinite inlining. Looping makes possible for some roots to
+ // become eligible after other roots are inlined.
- // Assess eligibility and compute inlining of direct calls and extra methods needed.
- if (!processor.isInstanceEligible() ||
- !processor.isClassAndUsageEligible() ||
- !processor.areInstanceUsersEligible(defaultOracle)) {
- continue;
+ boolean repeat;
+ do {
+ repeat = false;
+
+ Iterator<Instruction> rootsIterator = roots.iterator();
+ while (rootsIterator.hasNext()) {
+ Instruction root = rootsIterator.next();
+ InlineCandidateProcessor processor =
+ new InlineCandidateProcessor(factory, appInfo,
+ type -> isClassEligible(appInfo, type),
+ isProcessedConcurrently, method, root);
+
+ // Assess eligibility of instance and class.
+ if (!processor.isInstanceEligible() ||
+ !processor.isClassAndUsageEligible()) {
+ // This root will never be inlined.
+ rootsIterator.remove();
+ continue;
+ }
+
+ // Assess users eligibility and compute inlining of direct calls and extra methods needed.
+ if (!processor.areInstanceUsersEligible(defaultOracle)) {
+ // This root may succeed if users change in future.
+ continue;
+ }
+
+ // Is inlining allowed.
+ if (processor.getEstimatedCombinedSizeForInlining() >= totalMethodInstructionLimit) {
+ continue;
+ }
+
+ // Inline the class instance.
+ processor.processInlining(code, inliner);
+
+ // Restore normality.
+ code.removeAllTrivialPhis();
+ assert code.isConsistentSSA();
+ rootsIterator.remove();
+ repeat = true;
}
-
- // Is inlining allowed.
- if (processor.getEstimatedCombinedSizeForInlining() >= totalMethodInstructionLimit) {
- continue;
- }
-
- // Inline the class instance.
- processor.processInlining(code, inliner);
-
- // Restore normality.
- code.removeAllTrivialPhis();
- assert code.isConsistentSSA();
- }
+ } while (repeat);
}
private boolean isClassEligible(AppInfo appInfo, DexType clazz) {
@@ -167,7 +188,6 @@
// Class is eligible for this optimization. Eligibility implementation:
// - is not an abstract class or interface
- // - directly extends java.lang.Object
// - does not declare finalizer
// - does not trigger any static initializers except for its own
private boolean computeClassEligible(AppInfo appInfo, DexType clazz) {
@@ -177,11 +197,6 @@
return false;
}
- // Must directly extend Object.
- if (definition.superType != factory.objectType) {
- return false;
- }
-
// Class must not define finalizer.
for (DexEncodedMethod method : definition.virtualMethods()) {
if (method.method.name == factory.finalizeMethodName &&
diff --git a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
index 9c553bd..4d6e3b1 100644
--- a/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
+++ b/src/main/java/com/android/tools/r8/ir/optimize/classinliner/InlineCandidateProcessor.java
@@ -14,6 +14,7 @@
import com.android.tools.r8.graph.DexEncodedMethod.ParameterUsagesInfo.SingleCallOfArgumentMethod;
import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer;
import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.TrivialClassInitializer;
+import com.android.tools.r8.graph.DexEncodedMethod.TrivialInitializer.TrivialInstanceInitializer;
import com.android.tools.r8.graph.DexField;
import com.android.tools.r8.graph.DexItemFactory;
import com.android.tools.r8.graph.DexMethod;
@@ -21,6 +22,7 @@
import com.android.tools.r8.ir.code.BasicBlock;
import com.android.tools.r8.ir.code.ConstNumber;
import com.android.tools.r8.ir.code.IRCode;
+import com.android.tools.r8.ir.code.If;
import com.android.tools.r8.ir.code.InstanceGet;
import com.android.tools.r8.ir.code.Instruction;
import com.android.tools.r8.ir.code.Invoke.Type;
@@ -114,6 +116,10 @@
}
if (isNewInstance()) {
+ // NOTE: if the eligible class does not directly extend java.lang.Object,
+ // we also have to guarantee that it is initialized with initializer classified as
+ // TrivialInstanceInitializer. This will be checked in areInstanceUsersEligible(...).
+
// There must be no static initializer on the class itself.
return !eligibleClassDefinition.hasClassInitializer();
}
@@ -238,6 +244,16 @@
}
}
+ // Allow some IF instructions.
+ if (user.isIf()) {
+ If ifInsn = user.asIf();
+ If.Type type = ifInsn.getType();
+ if (ifInsn.isZeroTest() && (type == If.Type.EQ || type == If.Type.NE)) {
+ // Allow ==/!= null tests, we know that the instance is a non-null value.
+ continue;
+ }
+ }
+
return false; // Not eligible.
}
return true;
@@ -256,7 +272,8 @@
replaceUsagesAsUnusedArgument(code);
forceInlineExtraMethodInvocations(inliner);
forceInlineDirectMethodInvocations(inliner);
- removeSuperClassInitializerAndFieldReads(code);
+ removeMiscUsages(code);
+ removeFieldReads(code);
removeFieldWrites();
removeInstruction(root);
}
@@ -308,19 +325,58 @@
}
}
- // Remove call to superclass initializer, replace field reads with appropriate
- // values, insert phis when needed.
- private void removeSuperClassInitializerAndFieldReads(IRCode code) {
- Map<DexField, FieldValueHelper> fieldHelpers = new IdentityHashMap<>();
+ // Remove miscellaneous users before handling field reads.
+ private void removeMiscUsages(IRCode code) {
+ boolean needToRemoveUnreachableBlocks = false;
for (Instruction user : eligibleInstance.uniqueUsers()) {
// Remove the call to superclass constructor.
if (root.isNewInstance() &&
user.isInvokeDirect() &&
- user.asInvokeDirect().getInvokedMethod() == factory.objectMethods.constructor) {
+ factory.isConstructor(user.asInvokeDirect().getInvokedMethod()) &&
+ user.asInvokeDirect().getInvokedMethod().holder == eligibleClassDefinition.superType) {
removeInstruction(user);
continue;
}
+ if (user.isIf()) {
+ If ifInsn = user.asIf();
+ assert ifInsn.isZeroTest()
+ : "Unexpected usage in non-zero-test IF instruction: " + user;
+ BasicBlock block = user.getBlock();
+ If.Type type = ifInsn.getType();
+ assert type == If.Type.EQ || type == If.Type.NE
+ : "Unexpected type in zero-test IF instruction: " + user;
+ BasicBlock newBlock = type == If.Type.EQ
+ ? ifInsn.fallthroughBlock() : ifInsn.getTrueTarget();
+ BasicBlock blockToRemove = type == If.Type.EQ
+ ? ifInsn.getTrueTarget() : ifInsn.fallthroughBlock();
+ assert newBlock != blockToRemove;
+
+ block.replaceSuccessor(blockToRemove, newBlock);
+ blockToRemove.removePredecessor(block);
+ assert block.exit().isGoto();
+ assert block.exit().asGoto().getTarget() == newBlock;
+ needToRemoveUnreachableBlocks = true;
+ continue;
+ }
+
+ if (user.isInstanceGet() || user.isInstancePut()) {
+ // Leave field reads/writes until next steps.
+ continue;
+ }
+
+ throw new Unreachable("Unexpected usage left after method inlining: " + user);
+ }
+
+ if (needToRemoveUnreachableBlocks) {
+ code.removeUnreachableBlocks();
+ }
+ }
+
+ // Replace field reads with appropriate values, insert phis when needed.
+ private void removeFieldReads(IRCode code) {
+ Map<DexField, FieldValueHelper> fieldHelpers = new IdentityHashMap<>();
+ for (Instruction user : eligibleInstance.uniqueUsers()) {
if (user.isInstanceGet()) {
// Replace a field read with appropriate value.
replaceFieldRead(code, user.asInstanceGet(), fieldHelpers);
@@ -385,6 +441,19 @@
return null;
}
+ // If the superclass of the initializer is NOT java.lang.Object, the super class
+ // initializer being called must be classified as TrivialInstanceInitializer.
+ //
+ // NOTE: since we already classified the class as eligible, it does not have
+ // any class initializers in superclass chain or in superinterfaces, see
+ // details in ClassInliner::computeClassEligible(...).
+ if (eligibleClassDefinition.superType != factory.objectType) {
+ TrivialInitializer info = definition.getOptimizationInfo().getTrivialInitializerInfo();
+ if (!(info instanceof TrivialInstanceInitializer)) {
+ return null;
+ }
+ }
+
if (!definition.isInliningCandidate(method, Reason.SIMPLE, appInfo)) {
// We won't be able to inline it here.
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java
index d1fe04c..031d161 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/RegisterMoveScheduler.java
@@ -14,6 +14,8 @@
import com.android.tools.r8.ir.code.Move;
import com.android.tools.r8.ir.code.Position;
import com.android.tools.r8.ir.code.Value;
+import it.unimi.dsi.fastutil.ints.IntArraySet;
+import it.unimi.dsi.fastutil.ints.IntSet;
import java.util.ArrayList;
import java.util.Deque;
import java.util.HashMap;
@@ -59,6 +61,8 @@
}
public void schedule() {
+ assert everyDestinationOnlyWrittenOnce();
+
// Worklist of moves that are ready to be inserted.
Deque<RegisterMove> worklist = new LinkedList<>();
@@ -196,4 +200,13 @@
iterator.remove();
return move;
}
+
+ private boolean everyDestinationOnlyWrittenOnce() {
+ IntSet destinations = new IntArraySet(moveSet.size());
+ for (RegisterMove move : moveSet) {
+ boolean changed = destinations.add(move.dst);
+ assert changed;
+ }
+ return true;
+ }
}
diff --git a/src/main/java/com/android/tools/r8/ir/regalloc/SpillMoveSet.java b/src/main/java/com/android/tools/r8/ir/regalloc/SpillMoveSet.java
index 1ea788d..cebcc8c 100644
--- a/src/main/java/com/android/tools/r8/ir/regalloc/SpillMoveSet.java
+++ b/src/main/java/com/android/tools/r8/ir/regalloc/SpillMoveSet.java
@@ -61,7 +61,46 @@
assert to.getSplitParent() == from.getSplitParent();
BasicBlock atEntryToBlock = blockStartMap.get(i + 1);
if (atEntryToBlock == null) {
- addInMove(i, to, from);
+ Value value = from.getValue();
+ if (value.definition != null
+ && value.definition.isMoveException()
+ && to.getStart() == value.definition.asMoveException().getNumber() + 1) {
+ // Consider the following IR code.
+ // 40: ...
+ // 42: v0 <- move-exception
+ // 44: ...
+ // ...
+ // 50: ... v0 ... // 4-bit constrained use of v0
+ //
+ // Initially, the liveness interval of v0 is [42; 50[. If the method has overlapping move-
+ // exception intervals (and the register allocator needs more than 16 registers), then the
+ // intervals of v0 will be split immediately after its definition. Therefore, v0 will have
+ // two liveness intervals: I1=[42; 43[ and I2=[43;50[.
+ //
+ // When allocating a register for the interval I2, we may need to spill an existing value v1
+ // that is currently active. As a result, we will split the liveness interval of v1 before
+ // the start of I2 (i.e., at position 43). The live intervals of v1 after the split
+ // therefore becomes J1=[x, y[, J2=[41, 43[, J3=[43, z[.
+ //
+ // If the registers assigned to J1 and J2 are different, we will create an in-move at
+ // position 43 in resolveControlFlow() (not position 41, since the first instruction of the
+ // target block is a move-exception instruction). If the the registers assigned to I1 and I2
+ // are also different, then an in-move will also be created at position 43 by the call to
+ // addSpillOrRestoreMove() in insertMoves().
+ //
+ // If the registers of I2 and J2 are the same (which is a valid assignment), then we will
+ // do parallel move scheduling for the following two in-moves:
+ // move X, reg(I2)
+ // move X, reg(J2)
+ //
+ // Therefore, there is a risk that we end up with the value that has been spilled instead of
+ // the exception object in register X at position 44. To avoid this situation, we schedule
+ // the move of the exception object (in this case, "move X, reg(I2)") as an out-move, such
+ // that it always gets inserted *after* the resolution moves of the current block.
+ addOutMove(i, to, from);
+ } else {
+ addInMove(i, to, from);
+ }
}
}
diff --git a/src/main/java/com/android/tools/r8/kotlin/Kotlin.java b/src/main/java/com/android/tools/r8/kotlin/Kotlin.java
index 0af41b1..05c7b92 100644
--- a/src/main/java/com/android/tools/r8/kotlin/Kotlin.java
+++ b/src/main/java/com/android/tools/r8/kotlin/Kotlin.java
@@ -80,6 +80,9 @@
public final DexType type = factory.createType("Lkotlin/jvm/internal/Intrinsics;");
public final DexMethod throwParameterIsNullException = factory.createMethod(type,
factory.createProto(factory.voidType, factory.stringType), "throwParameterIsNullException");
+ public final DexMethod checkParameterIsNotNull = factory.createMethod(type,
+ factory.createProto(factory.voidType, factory.objectType, factory.stringType),
+ "checkParameterIsNotNull");
public final DexMethod throwNpe = factory.createMethod(
type, factory.createProto(factory.voidType), "throwNpe");
}
diff --git a/src/test/examples/classmerging/PinnedParameterTypesTest.java b/src/test/examples/classmerging/PinnedParameterTypesTest.java
new file mode 100644
index 0000000..bdfe3b7
--- /dev/null
+++ b/src/test/examples/classmerging/PinnedParameterTypesTest.java
@@ -0,0 +1,47 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package classmerging;
+
+import java.lang.reflect.Method;
+
+public class PinnedParameterTypesTest {
+
+ public static void main(String[] args) throws Exception {
+ for (Method method : TestClass.class.getMethods()) {
+ if (method.getName().equals("method")) {
+ Class<?> parameterType = method.getParameterTypes()[0];
+
+ // Should print classmerging.PinnedParameterTypesTest$Interface when -keepparameternames is
+ // used.
+ System.out.println(parameterType.getName());
+
+ method.invoke(null, new InterfaceImpl());
+ break;
+ }
+ }
+ }
+
+ public interface Interface {
+
+ void foo();
+ }
+
+ public static class InterfaceImpl implements Interface {
+
+ @Override
+ public void foo() {
+ System.out.println("In InterfaceImpl.foo()");
+ }
+ }
+
+ public static class TestClass {
+
+ // This method has been kept explicitly by a keep rule. Therefore, since -keepparameternames is
+ // used, Interface must not be merged into InterfaceImpl.
+ public static void method(Interface obj) {
+ obj.foo();
+ }
+ }
+}
diff --git a/src/test/examples/classmerging/keep-rules.txt b/src/test/examples/classmerging/keep-rules.txt
index fa48747..516e39d 100644
--- a/src/test/examples/classmerging/keep-rules.txt
+++ b/src/test/examples/classmerging/keep-rules.txt
@@ -19,6 +19,12 @@
-keep public class classmerging.RewritePinnedMethodTest {
public static void main(...);
}
+-keep public class classmerging.PinnedParameterTypesTest {
+ public static void main(...);
+}
+-keep public class classmerging.PinnedParameterTypesTest$TestClass {
+ public static void method(...);
+}
-keep public class classmerging.SimpleInterfaceAccessTest {
public static void main(...);
}
diff --git a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
index 4da4821..59accb3 100644
--- a/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
+++ b/src/test/java/com/android/tools/r8/classmerging/ClassMergingTest.java
@@ -45,8 +45,7 @@
import org.junit.runner.RunWith;
// TODO(christofferqa): Add tests to check that statically typed invocations on method handles
-// continue to work after class merging. Rewriting of method handles should be carried out by
-// LensCodeRewriter.rewriteDexMethodHandle.
+// continue to work after class merging.
@RunWith(VmTestRunner.class)
public class ClassMergingTest extends TestBase {
@@ -207,6 +206,29 @@
}
@Test
+ public void testPinnedParameterTypes() throws Exception {
+ String main = "classmerging.PinnedParameterTypesTest";
+ Path[] programFiles =
+ new Path[] {
+ CF_DIR.resolve("PinnedParameterTypesTest.class"),
+ CF_DIR.resolve("PinnedParameterTypesTest$Interface.class"),
+ CF_DIR.resolve("PinnedParameterTypesTest$InterfaceImpl.class"),
+ CF_DIR.resolve("PinnedParameterTypesTest$TestClass.class")
+ };
+ Set<String> preservedClassNames =
+ ImmutableSet.of(
+ "classmerging.PinnedParameterTypesTest",
+ "classmerging.PinnedParameterTypesTest$Interface",
+ "classmerging.PinnedParameterTypesTest$InterfaceImpl",
+ "classmerging.PinnedParameterTypesTest$TestClass");
+ runTest(
+ main,
+ programFiles,
+ preservedClassNames::contains,
+ getProguardConfig(EXAMPLE_KEEP, "-keepparameternames"));
+ }
+
+ @Test
public void testSuperCallWasDetected() throws Exception {
String main = "classmerging.SuperCallRewritingTest";
Path[] programFiles =
diff --git a/src/test/java/com/android/tools/r8/ir/optimize/CheckCastRemovalTest.java b/src/test/java/com/android/tools/r8/ir/optimize/CheckCastRemovalTest.java
index be7ee92..f894322 100644
--- a/src/test/java/com/android/tools/r8/ir/optimize/CheckCastRemovalTest.java
+++ b/src/test/java/com/android/tools/r8/ir/optimize/CheckCastRemovalTest.java
@@ -89,7 +89,7 @@
"-keep class B { *; }",
"-keep class C { *; }",
"-dontshrink");
- AndroidApp app = compileWithR8(builder, pgConfigs, null);
+ AndroidApp app = compileWithR8(builder, pgConfigs, opts -> opts.enableClassInlining = false);
DexEncodedMethod method = getMethod(app, CLASS_NAME, main);
assertNotNull(method);
diff --git a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
index f99934e..fd78cb7 100644
--- a/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
+++ b/src/test/java/com/android/tools/r8/kotlin/AbstractR8KotlinTestBase.java
@@ -231,7 +231,7 @@
}
// Build classpath for compilation (and java execution)
- assert classpath.isEmpty();
+ classpath.clear();
classpath.add(getKotlinJarFile(folder));
classpath.add(getJavaJarFile(folder));
classpath.addAll(extraClasspath);
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
index 46af30d..a690429 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinClassInlinerTest.java
@@ -7,7 +7,9 @@
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+import com.android.tools.r8.ToolHelper.KotlinTargetVersion;
import com.android.tools.r8.code.NewInstance;
import com.android.tools.r8.code.SgetObject;
import com.android.tools.r8.graph.DexClass;
@@ -16,14 +18,26 @@
import com.android.tools.r8.naming.MemberNaming.MethodSignature;
import com.android.tools.r8.utils.DexInspector;
import com.android.tools.r8.utils.DexInspector.ClassSubject;
+import com.google.common.collect.ImmutableList;
import com.google.common.collect.Sets;
+import java.util.Collection;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.junit.Test;
+import org.junit.runners.Parameterized.Parameters;
public class KotlinClassInlinerTest extends AbstractR8KotlinTestBase {
+ @Parameters(name = "allowAccessModification: {0} target: {1}")
+ public static Collection<Object[]> data() {
+ ImmutableList.Builder<Object[]> builder = new ImmutableList.Builder<>();
+ for (KotlinTargetVersion targetVersion : KotlinTargetVersion.values()) {
+ builder.add(new Object[]{Boolean.TRUE, targetVersion});
+ }
+ return builder.build();
+ }
+
private static boolean isLambda(DexClass clazz) {
return !clazz.type.getPackageDescriptor().startsWith("kotlin") &&
(isKStyleLambdaOrGroup(clazz) || isJStyleLambdaOrGroup(clazz));
@@ -49,7 +63,17 @@
@Test
public void testJStyleLambdas() throws Exception {
final String mainClassName = "class_inliner_lambda_j_style.MainKt";
- runTest("class_inliner_lambda_j_style", mainClassName, (app) -> {
+ runTest("class_inliner_lambda_j_style", mainClassName, false, (app) -> {
+ DexInspector inspector = new DexInspector(app);
+ assertTrue(
+ inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful$1").isPresent());
+ assertTrue(
+ inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful2$1").isPresent());
+ assertTrue(
+ inspector.clazz("class_inliner_lambda_j_style.MainKt$testStateful3$1").isPresent());
+ });
+
+ runTest("class_inliner_lambda_j_style", mainClassName, true, (app) -> {
DexInspector inspector = new DexInspector(app);
Predicate<DexType> lambdaCheck = createLambdaCheck(inspector);
ClassSubject clazz = inspector.clazz(mainClassName);
@@ -81,6 +105,40 @@
});
}
+ @Test
+ public void testKStyleLambdas() throws Exception {
+ final String mainClassName = "class_inliner_lambda_k_style.MainKt";
+ runTest("class_inliner_lambda_k_style", mainClassName, false, (app) -> {
+ DexInspector inspector = new DexInspector(app);
+ assertTrue(inspector.clazz(
+ "class_inliner_lambda_k_style.MainKt$testKotlinSequencesStateless$1").isPresent());
+ assertTrue(inspector.clazz(
+ "class_inliner_lambda_k_style.MainKt$testKotlinSequencesStateful$1").isPresent());
+ });
+
+ runTest("class_inliner_lambda_k_style", mainClassName, true, (app) -> {
+ DexInspector inspector = new DexInspector(app);
+ Predicate<DexType> lambdaCheck = createLambdaCheck(inspector);
+ ClassSubject clazz = inspector.clazz(mainClassName);
+
+ assertEquals(
+ Sets.newHashSet(),
+ collectAccessedLambdaTypes(lambdaCheck, clazz,
+ "testKotlinSequencesStateless", "kotlin.sequences.Sequence"));
+
+ assertFalse(inspector.clazz(
+ "class_inliner_lambda_k_style.MainKt$testKotlinSequencesStateless$1").isPresent());
+
+ assertEquals(
+ Sets.newHashSet(),
+ collectAccessedLambdaTypes(lambdaCheck, clazz,
+ "testKotlinSequencesStateful", "int", "int", "kotlin.sequences.Sequence"));
+
+ assertFalse(inspector.clazz(
+ "class_inliner_lambda_k_style.MainKt$testKotlinSequencesStateful$1").isPresent());
+ });
+ }
+
private Set<String> collectAccessedLambdaTypes(
Predicate<DexType> isLambdaType, ClassSubject clazz, String methodName, String... params) {
assertNotNull(clazz);
@@ -97,14 +155,13 @@
.collect(Collectors.toSet());
}
- @Override
protected void runTest(String folder, String mainClass,
- AndroidAppInspector inspector) throws Exception {
+ boolean enabled, AndroidAppInspector inspector) throws Exception {
runTest(
folder, mainClass, null,
options -> {
options.enableInlining = true;
- options.enableClassInlining = true;
+ options.enableClassInlining = enabled;
options.enableLambdaMerging = false;
}, inspector);
}
diff --git a/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java b/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
index ee9065f..08d43ae 100644
--- a/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
+++ b/src/test/java/com/android/tools/r8/kotlin/KotlinLambdaMergingTest.java
@@ -14,12 +14,14 @@
import com.android.tools.r8.ir.optimize.lambda.CaptureSignature;
import com.android.tools.r8.utils.AndroidApp;
import com.android.tools.r8.utils.DexInspector;
+import com.android.tools.r8.utils.InternalOptions;
import com.google.common.collect.Lists;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.ExecutionException;
+import java.util.function.Consumer;
import org.junit.Test;
public class KotlinLambdaMergingTest extends AbstractR8KotlinTestBase {
@@ -29,6 +31,7 @@
"-keepattributes InnerClasses,EnclosingMethod\n";
private static final String KEEP_SIGNATURE_INNER_ENCLOSING =
"-keepattributes Signature,InnerClasses,EnclosingMethod\n";
+ private Consumer<InternalOptions> optionsModifier = opts -> opts.enableClassInlining = false;
abstract static class LambdaOrGroup {
abstract boolean match(DexClass clazz);
@@ -252,7 +255,7 @@
@Test
public void testTrivialKs() throws Exception {
final String mainClassName = "lambdas_kstyle_trivial.MainKt";
- runTest("lambdas_kstyle_trivial", mainClassName, (app) -> {
+ runTest("lambdas_kstyle_trivial", mainClassName, optionsModifier, (app) -> {
Verifier verifier = new Verifier(app);
String pkg = "lambdas_kstyle_trivial";
@@ -293,7 +296,7 @@
@Test
public void testCapturesKs() throws Exception {
final String mainClassName = "lambdas_kstyle_captures.MainKt";
- runTest("lambdas_kstyle_captures", mainClassName, (app) -> {
+ runTest("lambdas_kstyle_captures", mainClassName, optionsModifier, (app) -> {
Verifier verifier = new Verifier(app);
String pkg = "lambdas_kstyle_captures";
String grpPkg = allowAccessModification ? "" : pkg;
@@ -318,7 +321,7 @@
@Test
public void testGenericsNoSignatureKs() throws Exception {
final String mainClassName = "lambdas_kstyle_generics.MainKt";
- runTest("lambdas_kstyle_generics", mainClassName, (app) -> {
+ runTest("lambdas_kstyle_generics", mainClassName, optionsModifier, (app) -> {
Verifier verifier = new Verifier(app);
String pkg = "lambdas_kstyle_generics";
String grpPkg = allowAccessModification ? "" : pkg;
@@ -339,55 +342,57 @@
@Test
public void testInnerClassesAndEnclosingMethodsKs() throws Exception {
final String mainClassName = "lambdas_kstyle_generics.MainKt";
- runTest("lambdas_kstyle_generics", mainClassName, KEEP_INNER_AND_ENCLOSING, (app) -> {
- Verifier verifier = new Verifier(app);
- String pkg = "lambdas_kstyle_generics";
- String grpPkg = allowAccessModification ? "" : pkg;
+ runTest("lambdas_kstyle_generics", mainClassName,
+ KEEP_INNER_AND_ENCLOSING, optionsModifier, (app) -> {
+ Verifier verifier = new Verifier(app);
+ String pkg = "lambdas_kstyle_generics";
+ String grpPkg = allowAccessModification ? "" : pkg;
- verifier.assertLambdaGroups(
- kstyle(grpPkg, 1, 3), // Group for Any
- kstyle(grpPkg, "L", 1), // Group for Beta // First
- kstyle(grpPkg, "L", 1), // Group for Beta // Second
- kstyle(grpPkg, "LS", 1), // Group for Gamma // First
- kstyle(grpPkg, "LS", 1), // Group for Gamma // Second
- kstyle(grpPkg, 1, 2) // Group for int
- );
+ verifier.assertLambdaGroups(
+ kstyle(grpPkg, 1, 3), // Group for Any
+ kstyle(grpPkg, "L", 1), // Group for Beta // First
+ kstyle(grpPkg, "L", 1), // Group for Beta // Second
+ kstyle(grpPkg, "LS", 1), // Group for Gamma // First
+ kstyle(grpPkg, "LS", 1), // Group for Gamma // Second
+ kstyle(grpPkg, 1, 2) // Group for int
+ );
- verifier.assertLambdas(
- new Lambda(pkg, "MainKt$main$4", 1)
- );
- });
+ verifier.assertLambdas(
+ new Lambda(pkg, "MainKt$main$4", 1)
+ );
+ });
}
@Test
public void testGenericsSignatureInnerEnclosingKs() throws Exception {
final String mainClassName = "lambdas_kstyle_generics.MainKt";
- runTest("lambdas_kstyle_generics", mainClassName, KEEP_SIGNATURE_INNER_ENCLOSING, (app) -> {
- Verifier verifier = new Verifier(app);
- String pkg = "lambdas_kstyle_generics";
- String grpPkg = allowAccessModification ? "" : pkg;
+ runTest("lambdas_kstyle_generics", mainClassName,
+ KEEP_SIGNATURE_INNER_ENCLOSING, optionsModifier, (app) -> {
+ Verifier verifier = new Verifier(app);
+ String pkg = "lambdas_kstyle_generics";
+ String grpPkg = allowAccessModification ? "" : pkg;
- verifier.assertLambdaGroups(
- kstyle(grpPkg, 1, 3), // Group for Any
- kstyle(grpPkg, "L", 1), // Group for Beta in First
- kstyle(grpPkg, "L", 1), // Group for Beta in Second
- kstyle(grpPkg, "LS", 1), // Group for Gamma<String> in First
- kstyle(grpPkg, "LS", 1), // Group for Gamma<Integer> in First
- kstyle(grpPkg, "LS", 1), // Group for Gamma<String> in Second
- kstyle(grpPkg, "LS", 1), // Group for Gamma<Integer> in Second
- kstyle(grpPkg, 1, 2) // Group for int
- );
+ verifier.assertLambdaGroups(
+ kstyle(grpPkg, 1, 3), // Group for Any
+ kstyle(grpPkg, "L", 1), // Group for Beta in First
+ kstyle(grpPkg, "L", 1), // Group for Beta in Second
+ kstyle(grpPkg, "LS", 1), // Group for Gamma<String> in First
+ kstyle(grpPkg, "LS", 1), // Group for Gamma<Integer> in First
+ kstyle(grpPkg, "LS", 1), // Group for Gamma<String> in Second
+ kstyle(grpPkg, "LS", 1), // Group for Gamma<Integer> in Second
+ kstyle(grpPkg, 1, 2) // Group for int
+ );
- verifier.assertLambdas(
- new Lambda(pkg, "MainKt$main$4", 1)
- );
- });
+ verifier.assertLambdas(
+ new Lambda(pkg, "MainKt$main$4", 1)
+ );
+ });
}
@Test
public void testTrivialJs() throws Exception {
final String mainClassName = "lambdas_jstyle_trivial.MainKt";
- runTest("lambdas_jstyle_trivial", mainClassName, (app) -> {
+ runTest("lambdas_jstyle_trivial", mainClassName, optionsModifier, (app) -> {
Verifier verifier = new Verifier(app);
String pkg = "lambdas_jstyle_trivial";
String grp = allowAccessModification ? "" : pkg;
@@ -435,7 +440,7 @@
@Test
public void testSingleton() throws Exception {
final String mainClassName = "lambdas_singleton.MainKt";
- runTest("lambdas_singleton", mainClassName, (app) -> {
+ runTest("lambdas_singleton", mainClassName, optionsModifier, (app) -> {
Verifier verifier = new Verifier(app);
String pkg = "lambdas_singleton";
String grp = allowAccessModification ? "" : pkg;
diff --git a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/defaultctor/ImplicitlyKeptDefaultConstructorTest.java b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/defaultctor/ImplicitlyKeptDefaultConstructorTest.java
index 61b1454..d8615e0 100644
--- a/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/defaultctor/ImplicitlyKeptDefaultConstructorTest.java
+++ b/src/test/java/com/android/tools/r8/shaking/forceproguardcompatibility/defaultctor/ImplicitlyKeptDefaultConstructorTest.java
@@ -155,8 +155,10 @@
runTest(
mainClass,
ImmutableList.of(mainClass, SuperClass.class, SubClass.class),
- // Prevent SuperClass from being merged into SubClass.
- keepMainProguardConfiguration(mainClass, ImmutableList.of("-keep class **.SuperClass")),
+ // Prevent SuperClass from being merged into SubClass and keep both
+ // SuperClass and SubClass after instantiation is inlined.
+ keepMainProguardConfiguration(mainClass, ImmutableList.of(
+ "-keep class **.SuperClass", "-keep class **.SubClass")),
this::checkAllClassesPresentWithDefaultConstructor);
}
diff --git a/src/test/kotlinR8TestResources/class_inliner_lambda_k_style/main.kt b/src/test/kotlinR8TestResources/class_inliner_lambda_k_style/main.kt
new file mode 100644
index 0000000..136f72d
--- /dev/null
+++ b/src/test/kotlinR8TestResources/class_inliner_lambda_k_style/main.kt
@@ -0,0 +1,49 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package class_inliner_lambda_k_style
+
+private var COUNT = 0
+
+fun next() = "${COUNT++}".padStart(3, '0')
+
+fun main(args: Array<String>) {
+ testKotlinSequencesStateless(produceSequence(10))
+ testKotlinSequencesStateful(5, 2, produceSequence(10))
+}
+
+data class Record(val foo: String, val good: Boolean)
+
+@Synchronized
+fun testKotlinSequencesStateless(strings: Sequence<String>) {
+ useRecord()
+ // Stateless k-style lambda
+ strings.map { Record(it, false) }.forEach { println(it) }
+}
+
+@Synchronized
+fun testKotlinSequencesStateful(a: Int, b: Int, strings: Sequence<String>) {
+ useRecord()
+ // Big stateful k-style lambda
+ val capture = next()
+ strings.map {
+ val x = it.toInt()
+ val y = a + x
+ val z = capture.toInt() + b
+ println("logging $x/$y/$z") // Intentional
+ Record(it, y % z == 0)
+ }.forEach {
+ println(it)
+ }
+}
+
+private fun produceSequence(size: Int): Sequence<String> {
+ var count = size
+ return generateSequence { if (count-- > 0) next() else null }
+}
+
+// Need this to make sure testKotlinSequenceXXX is not processed
+// concurrently with invoke() on lambdas.
+fun useRecord() = useRecord2()
+fun useRecord2() = Record("", true)
diff --git a/src/test/sampleApks/split/AndroidManifest.xml b/src/test/sampleApks/split/AndroidManifest.xml
new file mode 100644
index 0000000..e46cd1e
--- /dev/null
+++ b/src/test/sampleApks/split/AndroidManifest.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+for details. All rights reserved. Use of this source code is governed by a
+BSD-style license that can be found in the LICENSE file.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tools.r8.sample.split"
+ android:versionCode="1"
+ android:versionName="0.1" >
+
+ <uses-sdk android:minSdkVersion="21" />
+
+ <application
+ android:icon="@drawable/icon"
+ android:label="@string/app_name" >
+ <activity
+ android:name=".R8Activity"
+ android:label="@string/app_name" >
+ <intent-filter>
+ <action android:name="android.intent.action.MAIN" />
+ <category android:name="android.intent.category.LAUNCHER" />
+ </intent-filter>
+ </activity>
+ </application>
+</manifest>
diff --git a/src/test/sampleApks/split/assets/README.txt b/src/test/sampleApks/split/assets/README.txt
new file mode 100644
index 0000000..ecb060c
--- /dev/null
+++ b/src/test/sampleApks/split/assets/README.txt
@@ -0,0 +1 @@
+Sample split app from R8 project
diff --git a/src/test/sampleApks/split/res/drawable-mdpi/icon.png b/src/test/sampleApks/split/res/drawable-mdpi/icon.png
new file mode 100644
index 0000000..0799d58
--- /dev/null
+++ b/src/test/sampleApks/split/res/drawable-mdpi/icon.png
Binary files differ
diff --git a/src/test/sampleApks/split/res/layout/main.xml b/src/test/sampleApks/split/res/layout/main.xml
new file mode 100644
index 0000000..7859435
--- /dev/null
+++ b/src/test/sampleApks/split/res/layout/main.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+for details. All rights reserved. Use of this source code is governed by a
+BSD-style license that can be found in the LICENSE file.
+-->
+<LinearLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:orientation="vertical" android:layout_width="fill_parent"
+ android:layout_height="fill_parent" android:id="@+id/MainLayout"
+ android:background="@android:color/background_light">
+
+ <Button
+ android:layout_height="wrap_content"
+ android:layout_width="match_parent" android:id="@+id/PressButton"
+ android:layout_margin="2dip"
+ android:text="Do something"/>
+</LinearLayout>
diff --git a/src/test/sampleApks/split/res/values/strings.xml b/src/test/sampleApks/split/res/values/strings.xml
new file mode 100644
index 0000000..ecac20f
--- /dev/null
+++ b/src/test/sampleApks/split/res/values/strings.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+for details. All rights reserved. Use of this source code is governed by a
+BSD-style license that can be found in the LICENSE file.
+-->
+<resources>
+ <string name="app_name">R8 split app</string>
+</resources>
diff --git a/src/test/sampleApks/split/split.spec b/src/test/sampleApks/split/split.spec
new file mode 100644
index 0000000..a06e259
--- /dev/null
+++ b/src/test/sampleApks/split/split.spec
@@ -0,0 +1 @@
+com.android.tools.r8.sample.split.SplitClass:split
diff --git a/src/test/sampleApks/split/split_manifest/AndroidManifest.xml b/src/test/sampleApks/split/split_manifest/AndroidManifest.xml
new file mode 100644
index 0000000..21ea9f3
--- /dev/null
+++ b/src/test/sampleApks/split/split_manifest/AndroidManifest.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+for details. All rights reserved. Use of this source code is governed by a
+BSD-style license that can be found in the LICENSE file.
+-->
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.tools.r8.sample.split"
+ split="featuresplit"
+ android:versionCode="1"
+ android:versionName="0.1" >
+
+ <uses-sdk android:minSdkVersion="21" />
+
+</manifest>
diff --git a/src/test/sampleApks/split/src/com/android/tools/r8/sample/split/R8Activity.java b/src/test/sampleApks/split/src/com/android/tools/r8/sample/split/R8Activity.java
new file mode 100644
index 0000000..a80cf55
--- /dev/null
+++ b/src/test/sampleApks/split/src/com/android/tools/r8/sample/split/R8Activity.java
@@ -0,0 +1,40 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.sample.split;
+
+import android.app.Activity;
+import android.os.Bundle;
+import com.android.tools.r8.sample.split.R;
+import com.android.tools.r8.sample.split.SplitClass;
+
+public class R8Activity extends Activity {
+ private int res = 0;
+
+ public void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setTheme(android.R.style.Theme_Light);
+ setContentView(R.layout.main);
+ // Currently this is split up into 100 iterations to be able to better see
+ // the impact of the jit on later versions of art.
+ long total = 0;
+ for (int i = 0; i < 100; i++) {
+ total += benchmarkCall();
+ }
+ System.out.println("Total: " + total);
+ }
+
+ public long benchmarkCall() {
+ SplitClass split = new SplitClass(3);
+ long start = System.nanoTime();
+ for (int i = 0; i < 1000; i++) {
+ // Ensure no dead code elimination.
+ res = split.calculate(i);
+ }
+ long finish = System.nanoTime();
+ long timeElapsed = finish - start;
+ System.out.println("Took: " + timeElapsed);
+ return timeElapsed;
+ }
+}
diff --git a/src/test/sampleApks/split/src/com/android/tools/r8/sample/split/SplitClass.java b/src/test/sampleApks/split/src/com/android/tools/r8/sample/split/SplitClass.java
new file mode 100644
index 0000000..9b6990d
--- /dev/null
+++ b/src/test/sampleApks/split/src/com/android/tools/r8/sample/split/SplitClass.java
@@ -0,0 +1,21 @@
+// Copyright (c) 2018, the R8 project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+package com.android.tools.r8.sample.split;
+
+public class SplitClass {
+ final int initialValue;
+
+ public SplitClass(int initialValue) {
+ this.initialValue = initialValue;
+ }
+
+ public int calculate(int x) {
+ int result = 2;
+ for (int i = 0; i < 42; i++) {
+ result += initialValue + x;
+ }
+ return result;
+ }
+}
diff --git a/tools/build_sample_apk.py b/tools/build_sample_apk.py
index e1b2deb..b31ade4 100755
--- a/tools/build_sample_apk.py
+++ b/tools/build_sample_apk.py
@@ -18,12 +18,14 @@
ANDROID_JAR = 'third_party/android_jar/lib-v{api}/android.jar'
DEFAULT_AAPT = 'aapt' # Assume in path.
DEFAULT_D8 = os.path.join(utils.REPO_ROOT, 'tools', 'd8.py')
+DEFAULT_DEXSPLITTER = os.path.join(utils.REPO_ROOT, 'tools', 'dexsplitter.py')
DEFAULT_JAVAC = 'javac'
SRC_LOCATION = 'src/com/android/tools/r8/sample/{app}/*.java'
DEFAULT_KEYSTORE = os.path.join(os.getenv('HOME'), '.android', 'debug.keystore')
SAMPLE_APKS = [
- 'simple'
+ 'simple',
+ 'split'
]
def parse_options():
@@ -38,6 +40,12 @@
result.add_option('--keystore',
help='Keystore used for signing',
default=DEFAULT_KEYSTORE)
+ result.add_option('--split',
+ help='Split the app using the split.spec file',
+ default=False, action='store_true')
+ result.add_option('--install',
+ help='Install the app (including featuresplit)',
+ default=False, action='store_true')
result.add_option('--app',
help='Which app to build',
default='simple',
@@ -72,6 +80,12 @@
def get_src_path(app):
return os.path.join(get_sample_dir(app), 'src')
+def get_dex_path(app):
+ return os.path.join(get_bin_path(app), 'classes.dex')
+
+def get_split_path(app, split):
+ return os.path.join(get_bin_path(app), split, 'classes.dex')
+
def run_aapt_pack(aapt, api, app):
with utils.ChangedWorkingDirectory(get_sample_dir(app)):
args = ['package',
@@ -86,6 +100,16 @@
'-G', os.path.join(get_build_dir(app), 'proguard_options')]
run_aapt(aapt, args)
+def run_aapt_split_pack(aapt, api, app):
+ with utils.ChangedWorkingDirectory(get_sample_dir(app)):
+ args = ['package',
+ '-v', '-f',
+ '-I', get_android_jar(api),
+ '-M', 'split_manifest/AndroidManifest.xml',
+ '-S', 'res',
+ '-F', os.path.join(get_bin_path(app), 'split_resources.ap_')]
+ run_aapt(aapt, args)
+
def compile_with_javac(api, app):
with utils.ChangedWorkingDirectory(get_sample_dir(app)):
files = glob.glob(SRC_LOCATION.format(app=app))
@@ -110,28 +134,75 @@
utils.PrintCmd(command)
subprocess.check_call(command)
-def create_temp_apk(app):
+def split(app):
+ split_spec = os.path.join(get_sample_dir(app), 'split.spec')
+ command = [DEFAULT_DEXSPLITTER,
+ '--input', get_dex_path(app),
+ '--output', get_bin_path(app),
+ '--feature-splits', split_spec]
+ utils.PrintCmd(command)
+ subprocess.check_call(command)
+
+def run_adb(args):
+ command = ['adb']
+ command.extend(args)
+ utils.PrintCmd(command)
+ subprocess.check_call(command)
+
+def adb_install(apks):
+ args = [
+ 'install-multiple' if len(apks) > 1 else 'install',
+ '-r',
+ '-d']
+ args.extend(apks)
+ run_adb(args)
+
+def create_temp_apk(app, prefix):
temp_apk_path = os.path.join(get_bin_path(app), '%s.ap_' % app)
- shutil.move(os.path.join(get_bin_path(app), 'resources.ap_'),
- temp_apk_path)
+ shutil.copyfile(os.path.join(get_bin_path(app), '%sresources.ap_' % prefix),
+ temp_apk_path)
return temp_apk_path
-def aapt_add_dex(aapt, app, temp_apk_path):
+def aapt_add_dex(aapt, dex, temp_apk_path):
args = ['add',
'-k', temp_apk_path,
- os.path.join(get_bin_path(app), 'classes.dex')]
+ dex]
run_aapt(aapt, args)
def Main():
(options, args) = parse_options()
+ apks = []
+ is_split = options.split
run_aapt_pack(options.aapt, options.api, options.app)
+ if is_split:
+ run_aapt_split_pack(options.aapt, options.api, options.app)
compile_with_javac(options.api, options.app)
dex(options.app, options.api)
- temp_apk_path = create_temp_apk(options.app)
- aapt_add_dex(options.aapt, options.app, temp_apk_path)
+ dex_files = { options.app: get_dex_path(options.app)}
+ dex_path = get_dex_path(options.app)
+ if is_split:
+ split(options.app)
+ dex_path = get_split_path(options.app, 'base')
+
+ temp_apk_path = create_temp_apk(options.app, '')
+ aapt_add_dex(options.aapt, dex_path, temp_apk_path)
apk_path = os.path.join(get_bin_path(options.app), '%s.apk' % options.app)
apk_utils.sign(temp_apk_path, apk_path, options.keystore)
- print('Apk available at: %s' % apk_path)
+ apks.append(apk_path)
+
+ if is_split:
+ split_temp_apk_path = create_temp_apk(options.app, 'split_')
+ aapt_add_dex(options.aapt,
+ get_split_path(options.app, 'split'),
+ temp_apk_path)
+ split_apk_path = os.path.join(get_bin_path(options.app), 'featuresplit.apk')
+ apk_utils.sign(temp_apk_path, split_apk_path, options.keystore)
+ apks.append(split_apk_path)
+
+ print('Generated apks available at: %s' % ' '.join(apks))
+ if options.install:
+ adb_install(apks)
+
if __name__ == '__main__':
sys.exit(Main())