Android MultiDex

使用过MultiDex都知道,AndroidStudio会在编译过程中划分多个dex,如class.dex,class2.dex。。。

这里有一个问题,主dex是如何划分的?

在构建完成后会在会生成miandexlist.txt,如图

1. TaskManager

/**

* Creates the post-compilation tasks for the given Variant.

*

* These tasks create the dex file from the .class files, plus optional intermediary steps like

* proguard and jacoco

*/

public void createPostCompilationTasks(

@NonNull final VariantScope variantScope) {

checkNotNull(variantScope.getJavacTask());

final BaseVariantData variantData = variantScope.getVariantData();

final GradleVariantConfiguration config = variantData.getVariantConfiguration();

TransformManager transformManager = variantScope.getTransformManager();

// ---- Code Coverage first -----

boolean isTestCoverageEnabled =

config.getBuildType().isTestCoverageEnabled()

&& !config.getType().isForTesting()

&& !variantScope.getInstantRunBuildContext().isInInstantRunMode();

if (isTestCoverageEnabled) {

createJacocoTransform(variantScope);

}

maybeCreateDesugarTask(variantScope, config.getMinSdkVersion(), transformManager);

AndroidConfig extension = variantScope.getGlobalScope().getExtension();

// Merge Java Resources.

createMergeJavaResTransform(variantScope);

// ----- External Transforms -----

// apply all the external transforms.

List customTransforms = extension.getTransforms();

List> customTransformsDependencies = extension.getTransformsDependencies();

for (int i = 0, count = customTransforms.size(); i < count; i++) {

Transform transform = customTransforms.get(i);

List deps = customTransformsDependencies.get(i);

transformManager

.addTransform(taskFactory, variantScope, transform)

.ifPresent(

t -> {

if (!deps.isEmpty()) {

t.dependsOn(deps);

}

// if the task is a no-op then we make assemble task depend on it.

if (transform.getScopes().isEmpty()) {

variantScope.getAssembleTask().dependsOn(t);

}

});

}

// ----- Android studio profiling transforms

for (String jar : getAdvancedProfilingTransforms(projectOptions)) {

if (variantScope.getVariantConfiguration().getBuildType().isDebuggable()

&& variantData.getType().equals(VariantType.DEFAULT)

&& jar != null) {

transformManager.addTransform(

taskFactory, variantScope, new CustomClassTransform(jar));

}

}

// ----- Minify next -----

maybeCreateJavaCodeShrinkerTransform(variantScope);

maybeCreateResourcesShrinkerTransform(variantScope);

// ----- 10x support

PreColdSwapTask preColdSwapTask = null;

if (variantScope.getInstantRunBuildContext().isInInstantRunMode()) {

DefaultTask allActionsAnchorTask = createInstantRunAllActionsTasks(variantScope);

assert variantScope.getInstantRunTaskManager() != null;

preColdSwapTask =

variantScope.getInstantRunTaskManager().createPreColdswapTask(projectOptions);

preColdSwapTask.dependsOn(allActionsAnchorTask);

// force pre-dexing to be true as we rely on individual slices to be packaged

// separately.

extension.getDexOptions().setPreDexLibraries(true);

variantScope.getInstantRunTaskManager().createSlicerTask();

extension.getDexOptions().setJumboMode(true);

}

// ----- Multi-Dex support

DexingType dexingType = variantScope.getDexingType();

// Upgrade from legacy multi-dex to native multi-dex if possible when using with a device

if (dexingType == DexingType.LEGACY_MULTIDEX) {

if (variantScope.getVariantConfiguration().isMultiDexEnabled()

&& variantScope

.getVariantConfiguration()

.getMinSdkVersionWithTargetDeviceApi()

.getFeatureLevel()

>= 21) {

dexingType = DexingType.NATIVE_MULTIDEX;

}

}

Optional multiDexClassListTask;

if (dexingType == DexingType.LEGACY_MULTIDEX) {

boolean proguardInPipeline = variantScope.getCodeShrinker() == CodeShrinker.PROGUARD;

// If ProGuard will be used, we'll end up with a "fat" jar anyway. If we're using the

// new dexing pipeline, we'll use the new MainDexListTransform below, so there's no need

// for merging all classes into a single jar.

if (!proguardInPipeline && !usingIncrementalDexing(variantScope)) {

// Create a transform to jar the inputs into a single jar. Merge the classes only,

// no need to package the resources since they are not used during the computation.

JarMergingTransform jarMergingTransform =

new JarMergingTransform(TransformManager.SCOPE_FULL_PROJECT);

transformManager

.addTransform(taskFactory, variantScope, jarMergingTransform)

.ifPresent(variantScope::addColdSwapBuildTask);

}

// ---------

// create the transform that's going to take the code and the proguard keep list

// from above and compute the main class list.

Transform multiDexTransform;

if (usingIncrementalDexing(variantScope)) {

if (projectOptions.get(BooleanOption.ENABLE_D8_MAIN_DEX_LIST)) {

multiDexTransform = new D8MainDexListTransform(variantScope);

} else {

multiDexTransform =

new MainDexListTransform(variantScope, extension.getDexOptions());

}

} else {

multiDexTransform = new MultiDexTransform(variantScope, extension.getDexOptions());

}

multiDexClassListTask =

transformManager.addTransform(taskFactory, variantScope, multiDexTransform);

multiDexClassListTask.ifPresent(variantScope::addColdSwapBuildTask);

} else {

multiDexClassListTask = Optional.empty();

}

if (usingIncrementalDexing(variantScope)) {

createNewDexTasks(variantScope, multiDexClassListTask.orElse(null), dexingType);

} else {

createDexTasks(variantScope, multiDexClassListTask.orElse(null), dexingType);

}

if (preColdSwapTask != null) {

for (DefaultTask task : variantScope.getColdSwapBuildTasks()) {

task.dependsOn(preColdSwapTask);

}

}

// ---- Create tasks to publish the pipeline output as needed.

final File intermediatesDir = variantScope.getGlobalScope().getIntermediatesDir();

createPipelineToPublishTask(

variantScope,

transformManager.getPipelineOutputAsFileCollection(StreamFilter.DEX),

FileUtils.join(intermediatesDir, "bundling", "dex"),

PUBLISHED_DEX);

createPipelineToPublishTask(

variantScope,

transformManager.getPipelineOutputAsFileCollection(StreamFilter.RESOURCES),

FileUtils.join(intermediatesDir, "bundling", "java-res"),

PUBLISHED_JAVA_RES);

createPipelineToPublishTask(

variantScope,

transformManager.getPipelineOutputAsFileCollection(StreamFilter.NATIVE_LIBS),

FileUtils.join(intermediatesDir, "bundling", "native-libs"),

PUBLISHED_NATIVE_LIBS);

}

这个方法的作用是在编译过程中创建即将完成构建的task,从编译的.class文件转换成.dex文件,再加上额外的任务例如:proguard

这里面可以添加的自定义transform,如可以添加一个修改字节码的transform在编译过程中进行插桩的操作(具体不在这里详述了)

接着添加android自己的transform,例如:JavaCodeShrinkerTransform,ResourcesShrinkerTransform,

MainDexListTransform

最后再执行dex的transform。

2.MultiDexTransform

MultiDexTransform.transform()

if (dexingType == DexingType.LEGACY_MULTIDEX) {

boolean proguardInPipeline = variantScope.getCodeShrinker() == CodeShrinker.PROGUARD;

// If ProGuard will be used, we'll end up with a "fat" jar anyway. If we're using the

// new dexing pipeline, we'll use the new MainDexListTransform below, so there's no need

// for merging all classes into a single jar.

if (!proguardInPipeline && !usingIncrementalDexing(variantScope)) {

// Create a transform to jar the inputs into a single jar. Merge the classes only,

// no need to package the resources since they are not used during the computation.

JarMergingTransform jarMergingTransform =

new JarMergingTransform(TransformManager.SCOPE_FULL_PROJECT);

transformManager

.addTransform(taskFactory, variantScope, jarMergingTransform)

.ifPresent(variantScope::addColdSwapBuildTask);

}

// ---------

// create the transform that's going to take the code and the proguard keep list

// from above and compute the main class list.

Transform multiDexTransform;

if (usingIncrementalDexing(variantScope)) {

if (projectOptions.get(BooleanOption.ENABLE_D8_MAIN_DEX_LIST)) {

multiDexTransform = new D8MainDexListTransform(variantScope);

} else {

multiDexTransform =

new MainDexListTransform(variantScope, extension.getDexOptions());

}

} else {

multiDexTransform = new MultiDexTransform(variantScope, extension.getDexOptions());

}

multiDexClassListTask =

transformManager.addTransform(taskFactory, variantScope, multiDexTransform);

multiDexClassListTask.ifPresent(variantScope::addColdSwapBuildTask);

} else {

multiDexClassListTask = Optional.empty();

}

dexingType在minSdk低于21时为DexingType.LEGACY_MULTIDEX,大于21时为:DexingType.NATIVE_MULTIDEX

这是因为大于等于21时,即android 5.0运行的是ART,ART默认会在内部进行multidex的操作。

当我们在 gradle 中将 multiDexEnabled 设为 true 后,编译 app 的过程中 Terminal 会多出一行: :app:transformClassesWithMultidexlistForDebug

显然 MultiDex 相关操作也是通过 Transform Api 完成了,自然我们查看 MultiDexTransform 源码,直接看 #transform 方法:

@Override

public void transform(@NonNull TransformInvocation invocation)

throws IOException, TransformException, InterruptedException {

// Re-direct the output to appropriate log levels, just like the official ProGuard task.

LoggingManager loggingManager = invocation.getContext().getLogging();

loggingManager.captureStandardOutput(LogLevel.INFO);

loggingManager.captureStandardError(LogLevel.WARN);

try {

Map> inputs =

MainDexListTransform.getByInputType(invocation);

File input =

Iterables.getOnlyElement(

inputs.get(MainDexListTransform.ProguardInput.INPUT_JAR));

shrinkWithProguard(input, inputs.get(MainDexListTransform.ProguardInput.LIBRARY_JAR));

computeList(input);

} catch (ParseException | ProcessException e) {

throw new TransformException(e);

}

}

代码少,逻辑简单,可以猜出个大概来,通过proguard删除不必要的代码,然后执行computeList方法

MainDexListTransform.getByInputType(invocation)先将input中的DirectoryInputs和jarInput组合成单一集合,然后根据input的scope类型是否为PROVIDED_ONLY分离成ProguardInput.INPUT_JAR 和 ProguardInput.LIBRARY_JAR和对应的files存进map中。

input是个啥?Iterables.getOnlyElement拿到的是第一个INPUT_JAR,而这个对应的是DirectoryInputs,而DirectoryInputs又对应的app模块中的类。

所以我们大胆猜测下,computeList()生成了maindexlist.txt,并且是以app模块里的类加上它所依赖的类进行maindex的划分 。

shrinkWithProguard

接下来看看shrinkWithProguard

private void shrinkWithProguard(@NonNull File input, @NonNull Set libraryJars)

throws IOException, ParseException {

configuration.obfuscate = false;

configuration.optimize = false;

configuration.preverify = false;

dontwarn();

dontnote();

forceprocessing();

//把manifest_keep.txt的内容加进来

applyConfigurationFile(manifestKeepListProguardFile);

if (userMainDexKeepProguard != null) {

//如果用户设置了userMainDexKeep 也对其进行keep操作

applyConfigurationFile(userMainDexKeepProguard);

}

//multidex默认进行的keep

MainDexListTransform.getPlatformRules().forEach(this::keep);

//把tool目录下的shrinkedAndroid.jar,input 和 libraryJars加进classpath中

libraryJar(findShrinkedAndroidJar());

libraryJars.forEach(this::libraryJar);

inJar(input, null);

// 设置output,即中间产物中的componentClasses.jar

outJar(variantScope.getProguardComponentsJarFile());

printconfiguration(configFileOut);

// 执行proguard

runProguard();

}

当执行完shrinkWithProguard之后,接着执行compute

computeList

private void computeList(File _allClassesJarFile) throws ProcessException, IOException {

// manifest components plus immediate dependencies must be in the main dex.

Set mainDexClasses = callDx(

_allClassesJarFile,

variantScope.getProguardComponentsJarFile());

if (userMainDexKeepFile != null) {

mainDexClasses = ImmutableSet.builder()

.addAll(mainDexClasses)

.addAll(Files.readLines(userMainDexKeepFile, Charsets.UTF_8))

.build();

}

String fileContent = Joiner.on(System.getProperty("line.separator")).join(mainDexClasses);

//在这里我们终于看到mainDexListFile,其位置在"multi-dex/$variant/maindexlist.txt

Files.write(fileContent, mainDexListFile, Charsets.UTF_8);

}

接下来看看callDx,参数jarOfRoots就是上述提到的componentClasses.jar

private Set callDx(File allClassesJarFile, File jarOfRoots) throws ProcessException {

EnumSet mainDexListOptions =

EnumSet.noneOf(AndroidBuilder.MainDexListOption.class);

if (!keepRuntimeAnnotatedClasses) {

mainDexListOptions.add(

AndroidBuilder.MainDexListOption.DISABLE_ANNOTATION_RESOLUTION_WORKAROUND);

Logging.getLogger(MultiDexTransform.class).warn(

"Not including classes with runtime retention annotations in the main dex.\n"

+ "This can cause issues with reflection in older platforms.");

}

return variantScope.getGlobalScope().getAndroidBuilder().createMainDexList(

allClassesJarFile, jarOfRoots, mainDexListOptions);

}

真正工作的地方在createMainDexList()

public Set createMainDexList(

@NonNull File allClassesJarFile,

@NonNull File jarOfRoots,

@NonNull EnumSet options) throws ProcessException {

BuildToolInfo buildToolInfo = mTargetInfo.getBuildTools();

ProcessInfoBuilder builder = new ProcessInfoBuilder();

String dx = buildToolInfo.getPath(BuildToolInfo.PathId.DX_JAR);

if (dx == null || !new File(dx).isFile()) {

throw new IllegalStateException("dx.jar is missing");

}

builder.setClasspath(dx);

builder.setMain("com.android.multidex.ClassReferenceListBuilder");

if (options.contains(MainDexListOption.DISABLE_ANNOTATION_RESOLUTION_WORKAROUND)) {

builder.addArgs("--disable-annotation-resolution-workaround");

}

builder.addArgs(jarOfRoots.getAbsolutePath());

builder.addArgs(allClassesJarFile.getAbsolutePath());

CachedProcessOutputHandler processOutputHandler = new CachedProcessOutputHandler();

mJavaProcessExecutor.execute(builder.createJavaProcess(), processOutputHandler)

.rethrowFailure()

.assertNormalExitValue();

LineCollector lineCollector = new LineCollector();

processOutputHandler.getProcessOutput().processStandardOutputLines(lineCollector);

return ImmutableSet.copyOf(lineCollector.getResult());

}

mJavaProcessExecutor.execute最终会调用project.javaexec执行一个外部的java进程(MainDexListBuilder)

3. MainDexListBuilder

接下来分析MainDexListBuilder.main方法

public static void main(String[] args) {

int argIndex = 0;

boolean keepAnnotated = true;

while (argIndex < args.length -2) {

if (args[argIndex].equals(DISABLE_ANNOTATION_RESOLUTION_WORKAROUND)) {

keepAnnotated = false;

} else {

System.err.println("Invalid option " + args[argIndex]);

printUsage();

System.exit(STATUS_ERROR);

}

argIndex++;

}

...

...

try {

MainDexListBuilder builder = new MainDexListBuilder(keepAnnotated, args[argIndex],

args[argIndex + 1]);

Set toKeep = builder.getMainDexList();

printList(toKeep);

} catch (IOException e) {

System.err.println("A fatal error occured: " + e.getMessage());

System.exit(STATUS_ERROR);

return;

}

}

public MainDexListBuilder(boolean keepAnnotated, String rootJar, String pathString)

throws IOException {

ZipFile jarOfRoots = null;

Path path = null;

try {

try {

jarOfRoots = new ZipFile(rootJar);

} catch (IOException e) {

throw new IOException("\"" + rootJar + "\" can not be read as a zip archive. ("

+ e.getMessage() + ")", e);

}

path = new Path(pathString);

//拿到传入rootJar和pathString

ClassReferenceListBuilder mainListBuilder = new ClassReferenceListBuilder(path);

mainListBuilder.addRoots(jarOfRoots);

for (String className : mainListBuilder.getClassNames()) {

filesToKeep.add(className + CLASS_EXTENSION);

}

if (keepAnnotated) {

keepAnnotated(path);

}

} finally {

try {

jarOfRoots.close();

} catch (IOException e) {

// ignore

}

if (path != null) {

for (ClassPathElement element : path.elements) {

try {

element.close();

} catch (IOException e) {

// keep going, lets do our best.

}

}

}

}

}

MainDexListBuilder的构造函数中拿到传入rootJar和pathString,然后构造了mainListBuilder

主要是调用了 mainListBuilder.addRoots(jarOfRoots);

// keep roots

for (Enumeration extends ZipEntry> entries = jarOfRoots.entries();

entries.hasMoreElements();) {

ZipEntry entry = entries.nextElement();

String name = entry.getName();

if (name.endsWith(CLASS_EXTENSION)) {

classNames.add(name.substring(0, name.length() - CLASS_EXTENSION.length()));

}

}

// keep direct references of roots (+ direct references hierarchy)

for (Enumeration extends ZipEntry> entries = jarOfRoots.entries();

entries.hasMoreElements();) {

ZipEntry entry = entries.nextElement();

String name = entry.getName();

if (name.endsWith(CLASS_EXTENSION)) {

DirectClassFile classFile;

try {

classFile = path.getClass(name);

} catch (FileNotFoundException e) {

throw new IOException("Class " + name +

" is missing form original class path " + path, e);

}

addDependencies(classFile);

}

}

根据path找到jarOfRoot里面的类名,如果找到则加入到Dependencies,可以看到这个path其实就是我们一开始传进来的input目录,本质上是以app模块构建的jar文件的集合。

接下来我们回到开篇提到TaskManager.createPostCompilationTasks中,看看接下来将要执行的逻辑

public void createPostCompilationTasks(

@NonNull final VariantScope variantScope) {

....

....

//判断是否使用增量构建

if (usingIncrementalDexing(variantScope)) {

createNewDexTasks(variantScope, multiDexClassListTask.orElse(null), dexingType);

} else {

//在此以不使用增量进行分析

createDexTasks(variantScope, multiDexClassListTask.orElse(null), dexingType);

}

...

}

4.createDexTasks

private void createDexTasks(

@NonNull VariantScope variantScope,

@Nullable TransformTask multiDexClassListTask,

@NonNull DexingType dexingType) {

TransformManager transformManager = variantScope.getTransformManager();

AndroidBuilder androidBuilder = variantScope.getGlobalScope().getAndroidBuilder();

...

...

if (!preDexEnabled || dexingType != DexingType.NATIVE_MULTIDEX) {

// run if non native multidex or no pre-dexing

DexTransform dexTransform =

new DexTransform(

dexOptions,

dexingType,

preDexEnabled,

project.files(variantScope.getMainDexListFile()),

checkNotNull(androidBuilder.getTargetInfo(), "Target Info not set."),

androidBuilder.getDexByteCodeConverter(),

variantScope.getGlobalScope().getMessageReceiver(),

variantScope.getMinSdkVersion().getFeatureLevel());

Optional dexTask =

transformManager.addTransform(taskFactory, variantScope, dexTransform);

// need to manually make dex task depend on MultiDexTransform since there's no stream

// consumption making this automatic

dexTask.ifPresent(

t -> {

if (multiDexClassListTask != null) {

t.dependsOn(multiDexClassListTask);

}

variantScope.addColdSwapBuildTask(t);

});

}

}

只截取关键核心代码,可以看到DexTransform,二话不说,关键步骤肯定是在DexTransform的transform中

5.DexTransform

@Override

public void transform(@NonNull TransformInvocation transformInvocation)

throws TransformException, IOException, InterruptedException {

TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();

Preconditions.checkNotNull(outputProvider,

"Missing output object for transform " + getName());

if (!dexOptions.getKeepRuntimeAnnotatedClasses() && mainDexListFile == null) {

logger.info("DexOptions.keepRuntimeAnnotatedClasses has no affect in native multidex.");

}

ProcessOutputHandler outputHandler =

new ParsingProcessOutputHandler(

new ToolOutputParser(new DexParser(), Message.Kind.ERROR, logger),

new ToolOutputParser(new DexParser(), logger),

messageReceiver);

outputProvider.deleteAll();

try {

// these are either classes that should be converted directly to DEX, or DEX(s) to merge

Collection transformInputs =

TransformInputUtil.getAllFiles(transformInvocation.getInputs());

File outputDir =

outputProvider.getContentLocation(

"main",

getOutputTypes(),

TransformManager.SCOPE_FULL_PROJECT,

Format.DIRECTORY);

// this deletes and creates the dir for the output

FileUtils.cleanOutputDir(outputDir);

File mainDexList = null;

if (mainDexListFile != null && dexingType == DexingType.LEGACY_MULTIDEX) {

mainDexList = mainDexListFile.getSingleFile();

}

dexByteCodeConverter.convertByteCode(

transformInputs,

outputDir,

dexingType.isMultiDex(),

mainDexList,

dexOptions,

outputHandler,

minSdkVersion);

} catch (Exception e) {

throw new TransformException(e);

}

}

接着 dexByteCodeConverter.convertByteCode会执行runDexer方法

public void runDexer(

@NonNull final DexProcessBuilder builder,

@NonNull final DexOptions dexOptions,

@NonNull final ProcessOutputHandler processOutputHandler)

throws ProcessException, IOException, InterruptedException {

initDexExecutorService(dexOptions);

if (shouldDexInProcess(dexOptions)) {

dexInProcess(builder, dexOptions, processOutputHandler);

} else {

dexOutOfProcess(builder, dexOptions, processOutputHandler);

}

}

其中,dexInProcess会在当前进程开启一个固定数量为4的线程池

将class转成dex,具体操作在Main.runMultiDex()中。

此外,dexOutOfProcess则调用系统提供的dx工具进行转化。

至此,MultiDex过程全部捋清了

android multidex 3个dex,Android MultiDex中一个疑问相关推荐

  1. 【Android 逆向】启动 DEX 字节码中的 Activity 组件 ( 在 PathClassLoader 和 BootClassLoader 之间插入 DexClassLoader )

    文章目录 前言 一.在 PathClassLoader 和 BootClassLoader 之间插入 DexClassLoader 1.创建 DexClassLoader 2.使用 DexClassL ...

  2. 【Android 逆向】启动 DEX 字节码中的 Activity 组件 ( 使用 DexClassLoader 获取组件类失败 | 失败原因分析 | 自定义类加载器没有加载组件类的权限 )

    文章目录 一.使用 DexClassLoader 获取组件类失败报错 二.失败原因分析 一.使用 DexClassLoader 获取组件类失败报错 在上一篇博客 [Android 逆向]启动 DEX ...

  3. 【Android 逆向】启动 DEX 字节码中的 Activity 组件 ( DEX 文件准备 | 拷贝资源目录下的文件到内置存储区 | 配置清单文件 | 启动 DEX 文件中的组件 | 执行结果 )

    文章目录 一.DEX 字节码文件准备 二.拷贝 Assets 目录下的 classes2.dex 字节码文件到内置存储区 三.在 AndroidManifest.xml 清单文件中配置组件 四.启动 ...

  4. com.android.support:multidex,Android 使用android-support-multidex解决Dex超出方法数的限制问题...

    随着应用不断迭代,业务线的扩展,应用越来越大(比如集成了各种第三方sdk或者公共支持的jar包,项目耦合性高,重复作用的类越来越多),相信很多人都遇到过如下的错误: UNEXPECTED TOP-LE ...

  5. android linux 优化,【「Android」UE手游研发中,如何做好Android内存优化?】|Linux|DEX|腾讯游戏|_傻大方...

    傻大方提要:[「Android」UE手游研发中,如何做好Android内存优化?]编者按在大年夜多半人的印象里,用UE引擎制造出来的游戏实际占用内存会比较高.腾讯游戏学院专家Leonn,将和大年夜家分 ...

  6. android 动态 dex,Android 动态加载dex

    首先如果仅仅是因为64K method的问题可以直接看这里DexGuard.Proguard.Multi-dex给出的解决方案. 本文主要讨论从编译层面,dex动态加载器选择层面以及安全层面讨论dex ...

  7. Android 使用android-support-multidex解决Dex超出方法数的限制问题,让你的应用不再爆棚

    时之沙: http://blog.csdn.net/t12x3456 随着应用不断迭代,业务线的扩展,应用越来越大(比如集成了各种第三方sdk或者公共支持的jar包,项目耦合性高,重复作用的类越来越多 ...

  8. Android Studio:64K问题com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536

    问题 AS安装报错: com.android.dex.DexIndexOverflowException: method ID not in [0, 0xffff]: 65536at com.andr ...

  9. 【Android 安全】DEX 加密 ( 不同 Android 版本的 DEX 加载 | Android 8.0 版本 DEX 加载分析 | Android 5.0 版本 DEX 加载分析 )

    文章目录 一.不同版本的 DEX 加载 1.Android 8.0 版本 DEX 加载分析 2.Android 6.0 版本 DEX 加载分析 3.Android 5.0 版本 DEX 加载分析 一. ...

最新文章

  1. 融合视频目标检测与单目标、多目标跟踪,港中文开源一体化视频感知平台 MMTracking...
  2. 【智力问题】25匹马赛跑,每次只能跑5匹马,最快能赛几次找出跑得最快的3匹马?赛跑不能计时,并假设每匹马的速度是恒定不变的。...
  3. 大数据互联网架构阶段 Redis(二)
  4. 【Scrum】2010.12.27
  5. Cloudera-Manager 与 原生集群 免密登录问题
  6. Androidstudio项目更换gradle版本
  7. 基于【CentOS-7+ Ambari 2.7.0 + HDP 3.0】搭建HAWQ数据仓库02 ——使用ambari-server安装HDP...
  8. 如何寻找互联网红利期产品?
  9. Sqlite Developer 3.8 破解
  10. 计算机5800计算道路标高程序,CASIOfx-5800p计算器土木工程测量计算程序开发与应用...
  11. 在Linux中使用7zip/7zz
  12. 内存超频对游戏提升大吗 玩游戏有必要超频吗
  13. 【渝粤题库】陕西师范大学200531 英语测试 作业(高起本、专升本)
  14. 云算法——骑士在棋盘上的概率之公主当不了堂吉诃德
  15. 如何做一个自己的网站?
  16. MySQL生成36位、32位UUID以及32位大写的UUID
  17. excel.h的简单使用
  18. 前淘宝技术专家谈12306:这个网站很神奇
  19. opm openresty的包管理器-中文文档
  20. 假设你是个妹子,你敢这样谈恋爱吗?

热门文章

  1. 清华/阿里巴巴开源的周期型——Donut
  2. Quic 0RTT详解
  3. 5月19日至20日,俄罗斯最大的开放式网络安全节Positive Hack Days举行!
  4. 无法添加此网站的应用,扩展程序和用户脚本
  5. android 获取图片bitmap对象,Android中Glide获取图片Path、Bitmap用法详解
  6. windows把nginx安装成服务
  7. vitest支持cjs的workaround(TypeScript产物commonjs场景)
  8. 3D人脸识别技术,正在全面入侵我们的日常生活
  9. 相位调制信号matlab,信号的相位调制及解调.doc
  10. Jina AI 受邀出席 WAIC 2023「科技无障碍」论坛,与行业专家共话 AI 普惠未来