Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions tinker-android/tinker-android-lib/proguard-rules.pro
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,16 @@
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}

# NOTE for tinker patch authors:
# When a patch introduces NEW static methods or fields on an existing class,
# proguard may strip or rename them in the patch build because they are not
# referenced from any code path exercised at minification time in the base
# build. To avoid this, either:
# (a) ensure the new static members are referenced from already-shipped
# code in the base apk, or
# (b) add explicit keep rules in the host app's proguard config, e.g.:
# -keep class your.pkg.YourClass { public static *; }
# Additionally, always feed the mapping file produced by the base build into
# the patch build via `-applymapping <base-mapping.txt>` so that obfuscated
# names stay consistent between the base apk and the patch dex.
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,9 @@ protected ClassData adjustItem(AbstractIndexMap indexMap, ClassData item) {
@Override
public int getPatchedSectionSize() {
// assume each uleb128 field's length may be inflate by 2 bytes.
return super.getPatchedSectionSize() + newDex.getTableOfContents().classDatas.size * SizeOf.USHORT;
// a class_data_item has 4 uleb128 size headers (static_fields_size,
// instance_fields_size, direct_methods_size, virtual_methods_size).
return super.getPatchedSectionSize() + newDex.getTableOfContents().classDatas.size * SizeOf.USHORT * 4;
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package com.tencent.tinker.build.dexpatcher;

import com.tencent.tinker.android.dex.Dex;
import com.tencent.tinker.android.dex.FieldId;
import com.tencent.tinker.android.dex.MethodId;
import com.tencent.tinker.commons.dexpatcher.DexPatchApplier;

import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;

import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;

public class StaticMemberPatchTest {

private static final String CLASS_C_DESCRIPTOR = "LC;";
private static final String FIXTURE_DIR = "/dexpatcher/staticmember/";

@Rule
public TemporaryFolder tempFolder = new TemporaryFolder();

@Test
public void addedStaticFieldAndMethodArePreservedAfterPatch() throws Exception {
Dex merged = generateAndApplyPatch("base.dex", "added.dex");
assertTrue(hasField(merged, CLASS_C_DESCRIPTOR, "A"));
assertTrue(hasField(merged, CLASS_C_DESCRIPTOR, "B"));
assertTrue(hasMethod(merged, CLASS_C_DESCRIPTOR, "m"));
assertTrue(hasMethod(merged, CLASS_C_DESCRIPTOR, "n"));
}

@Test
public void removedStaticFieldAndMethodAreAbsentAfterPatch() throws Exception {
Dex merged = generateAndApplyPatch("added.dex", "base.dex");
assertTrue(hasField(merged, CLASS_C_DESCRIPTOR, "A"));
assertFalse(hasField(merged, CLASS_C_DESCRIPTOR, "B"));
assertTrue(hasMethod(merged, CLASS_C_DESCRIPTOR, "m"));
assertFalse(hasMethod(merged, CLASS_C_DESCRIPTOR, "n"));
}

@Test
public void changedStaticFieldInitialValueIsAppliedAfterPatch() throws Exception {
Dex merged = generateAndApplyPatch("base.dex", "changed.dex");
assertTrue(hasField(merged, CLASS_C_DESCRIPTOR, "A"));
assertTrue(hasMethod(merged, CLASS_C_DESCRIPTOR, "m"));
}

private Dex generateAndApplyPatch(String oldFixture, String newFixture) throws Exception {
File oldDexFile = copyFixture(oldFixture);
File newDexFile = copyFixture(newFixture);
File patchFile = tempFolder.newFile(oldFixture + "-to-" + newFixture + ".patch");

Dex oldDex = new Dex(oldDexFile);
Dex newDex = new Dex(newDexFile);

DexPatchGenerator generator = new DexPatchGenerator(oldDex, newDex);
generator.executeAndSaveTo(patchFile);

ByteArrayOutputStream mergedOut = new ByteArrayOutputStream();
new DexPatchApplier(oldDex, new Dex(patchFile)).executeAndSaveTo(mergedOut);

return new Dex(mergedOut.toByteArray());
}

private File copyFixture(String name) throws Exception {
InputStream in = getClass().getResourceAsStream(FIXTURE_DIR + name);
if (in == null) {
fail("Missing test fixture: " + FIXTURE_DIR + name);
}
File file = tempFolder.newFile(name);
FileOutputStream out = new FileOutputStream(file);
try {
byte[] buf = new byte[4096];
int n;
while ((n = in.read(buf)) > 0) {
out.write(buf, 0, n);
}
} finally {
out.close();
in.close();
}
return file;
}

private boolean hasField(Dex dex, String classDescriptor, String fieldName) {
for (FieldId fieldId : dex.fieldIds()) {
String type = dex.typeNames().get(fieldId.declaringClassIndex);
String name = dex.strings().get(fieldId.nameIndex);
if (classDescriptor.equals(type) && fieldName.equals(name)) {
return true;
}
}
return false;
}

private boolean hasMethod(Dex dex, String classDescriptor, String methodName) {
for (MethodId methodId : dex.methodIds()) {
String type = dex.typeNames().get(methodId.declaringClassIndex);
String name = dex.strings().get(methodId.nameIndex);
if (classDescriptor.equals(type) && methodName.equals(name)) {
return true;
}
}
return false;
}
}