U8SDK:支持自动拆分成多个dex文件(MultiDex支持)

引文:
最近在研究融合SDK,等业务逻辑写完再来做整理吧。
看到一篇挺好的文章,厚着脸皮抄下来做自己的笔记。
建议点链接去原文看,本文留给自己看的意味多一点。


转载来自:
本文出自 优优网事,转载时请注明出处及相应链接。
本文永久链接: http://www.uustory.com/?p=2061
本文永久链接: http://www.uustory.com/?p=2069

Android应用程序,最终发布成一个apk,安装到手机上。 apk文件随便用一个解压缩文件打开,可以看到里面有一个classes.dex文件,这就是之前工程中所有的代码,以及所有依赖的jar包全部合并在一起生成的一个dex文件。关于dex文件是什么,可以自己去科普一下。

google当初在设计dex文件的时候,限制了dex文件中最大的函数个数为65536(unsigned short),如果超出这个限制,那么如果不采用特殊处理,打包的时候,就会抛出:
org.jf.util.ExceptionWithContext: Unsigned short value out of range: *
类似这样的错误。

对于游戏开发来说,我们目前主要的问题是, 一个游戏本身采用的可能是unity或者cocos2dx等游戏引擎开发, 纯粹的Android代码并不多, 但是由于目前手机游戏需要接入很多渠道SDK, 部分渠道SDK的jar包是相当大的,里面对应的函数数量自然也少不了。这就会导致,这些渠道SDK在集成到游戏中,我们再次打包,会出现函数数量越界的问题。

另一个问题,就是我们除了接入渠道SDK之外,往往还需要同时接入统计,分享,推送等其他功能性SDK,所有这些jar包加起来,函数数量越界的可能就更大了。

最近不少用U8SDK的同学,遇到了这个问题。游戏母包,在通过U8SDK打包工具进行打包部分渠道(比如百度,360)的时候,在回编译的时候,抛出上面的错误。 这是因为百度和360渠道本身的函数数量已经接近这个上线的值了,再加上母包本身的函数,导致最终dex中的函数数量越界。

这里,就来统一处理下这个问题,这里我们先列出整体的思路

  • 打包工具,在回编译的之前, 先找出总共的函数数量,如果超出65536的限制,我们自动拆分出多个dex
  • apktool反编译之后,我们统一的代码格式为smali,我们需要将多余的smali文件,移到smali_classes2,smali_classes3…等等,目前最多支持5个,应该够用了。
  • U8Application中,在attachBaseContext 中,我们调用一下MultiDex.install(this); 以便,对多dex文件的支持。
  • 多个dex文件的支持,我们采用google提供的android-support-multidex.jar。我们将这个jar包,放在打包工具/config/local目录下,如果母包或者渠道SDK中不存在这个jar包,我们从这里将这个jar包添加进来
  • 因为程序入口是U8Application,所以我们必须保证U8Application等类和multidex这个jar包所有的文件在第一个classes.dex文件中,否则程序初始化就会找不到这个类。

根据这个思路,我们在打包工具/scripts/apk_utils.py中,增加一个函数:

def splitDex(workDir, decompileDir):
    """
        如果函数上限超过限制,自动拆分smali,以便生成多个dex文件
    """

    smaliPath = decompileDir + "/smali"

    multidexFilePath = file_utils.getFullPath(smaliPath + "/android/support/multidex/MultiDex.smali")
    if not os.path.exists(multidexFilePath):
        #android-support-multidex.jar不存在,从local下面拷贝,并编译
        dexJar = file_utils.getFullPath('config/local/android-support-multidex.jar')
        if not os.path.exists(dexJar):
            log_utils.error("the method num expired of dex, but no android-support-multidex.jar in u8.apk or in local folder")
            return

        targetPath = file_utils.getFullPath(workDir + "/local")
        if not os.path.exists(targetPath):
            os.makedirs(targetPath)

        file_utils.copy_file(dexJar, targetPath+"/android-support-multidex.jar") 

        jar2dex(targetPath, targetPath)
        smaliPath = file_utils.getFullPath(decompileDir + "/smali")
        ret = dex2smali(targetPath + '/classes.dex', smaliPath)       



    allFiles = []
    allFiles = file_utils.list_files(decompileDir, allFiles, [])   

    maxFuncNum = 65535
    currFucNum = 0

    currDexIndex = 1

    #保证U8Application等类在第一个classex.dex文件中
    for f in allFiles:
        f = f.replace("\\", "/")
        if "/com/u8/sdk" in f or "/android/support/multidex" in f:
            currFucNum = currFucNum + file_utils.get_smali_func_num(f)


    for f in allFiles:

        f = f.replace("\\", "/")
        if not f.endswith(".smali"):
            continue

        if "/com/u8/sdk" in f or "/android/support/multidex" in f:
            continue

        currFucNum = currFucNum + file_utils.get_smali_func_num(f)

        if currFucNum >= maxFuncNum:
            currFucNum = 0
            currDexIndex = currDexIndex + 1
            newDexPath = os.path.join(decompileDir, "smali_classes"+str(currDexIndex))
            os.makedirs(newDexPath)

        if currDexIndex > 1:
            targetPath = f[0:len(decompileDir)] + "/smali_classes"+str(currDexIndex) + f[len(smaliPath):]
            file_utils.copy_file(f, targetPath)
            file_utils.del_file_folder(f)


    print("split dex success. the classes.dex num:"+str(currDexIndex))

这个函数的功能,就是找出所有的函数数量,大于最大阀值的部分,我们依次拷贝到额外的smali文件夹中。

这里用到file_utils.py中增加的一个获取smali文件中函数数量的函数:

def get_smali_func_num(smaliFile):

    if not os.path.exists(smaliFile):
        return 0

    f = open(smaliFile)
    lines = f.readlines()
    f.close()

    num = 0
    for line in lines:
        if line.startswith(".method"):
            num = num + 1

    return num

有了这个,我们在core.py中,在回编译(apk_utils.recompileApk)之前,调用apk_utils.splitDex(workDir, decompileDir)。

这样,打包的时候,就可以自动拆分出多个dex文件了。 记得在打包工具/config/local/中放一个android-support-multidex.jar。 这个jar包可以从Android SDK安装目录/extras/android/support/multidex/library/libs中拷贝

这样,你再打包,如果函数个数超出上线,那么会生成classes.dex,classes2.dex…..

最后,我们在抽象层U8SDK2中的U8Application类中的attachBaseContext 中调用一下(先将android-support-multidex.jar拷贝到U8SDK2抽象层的libs中,必要时,引用下这个jar包,否则可能找不到MultiDex)

MultiDex.install(this);

然后重新编译,在bin目录下,会生成最新的u8sdk2.jar包,拷贝到游戏工程中。然后,重新打母包,然后打出渠道包。

新的方式,测试下来是OK的,唯一需要注意的一点,是入口Application等类必须在第一个classex.dex文件中。

备注:

后来发现上面计算函数个数的方式有问题, 和实际dex中函数个数计算方式不同,没有考虑调用的情况,导致计算的函数个数比实际的要小,不过目前已经修正了这个问题.

之前写的那篇处理dex文件中函数个数超出65K之后的自动分割dex的方法有一个问题(之前的文章),就是从smali文件中计算函数个数,和最终dex文件中计算的函数个数不一致,严格来说,是小了很多。

后来,研究了下,发现dex文件中所谓的函数个数65K上限,指的是除了dex中本身定义的函数之外,还包含引用的外部方法(这些方法主要就是android本身的一些函数)

所以,我就在查, dex中函数个数计算公式到底是啥。不过,官方文档给出的就是一个简单的说法,就是引用的函数个数(不重复的)

后来,没办法,只能自己做实验,写了一个简单的Android应用,打包成apk,看dex中的函数个数,然后再将dex通过打包工具转换为smali,再研究smali中怎么样计算才能和dex文件中函数个数匹配上。

经过一系列的试验,主要有如下几点:

1、所有定义的函数算一次,如果这个函数同时也被调用了,不再计算次数

2、重载的函数,算多次

3、多个子类中调用同一个父类的方法,如果直接调用,不加super.,算多次

计算要点确认之后,我们就可以着手在smali文件中按照这个规则来计算函数个事了。

smali文件的格式,方法定义是”.method”开头;方法调用是“invoke-”开头;方法参数列表是多个参数的拼接,无分割符,不过对于我们来说, 方法的本身就是(方法名称+参数列表),所以,我们不用分割方法和参数,而是将其整体作为一个方法,这样重载的方法就可以区分了。(有兴趣的同学可以去了解下smali文件格式)

针对这几点要素,我们重新调整了之前函数个数计算的方式,我们增加了一个smali_utils.py来处理smali相关的逻辑:

import os
import os.path
import re
import platform
import subprocess
import inspect
import sys
import codecs
import threading
import time
import log_utils


def get_smali_method_count(smaliFile, allMethods):

    if not os.path.exists(smaliFile):
        return 0

    f = open(smaliFile)
    lines = f.readlines()
    f.close()

    classLine = lines[0]
    classLine.strip()
    if not classLine.startswith(".class"):
        log_utils.error(f + " not startswith .class")
        return 0

    className = parse_class(classLine)
    #log_utils.debug("the class Name is "+className)

    count = 0
    for line in lines:
        line = line.strip()

        method = None
        if line.startswith(".method"):
            method = parse_method_default(className, line)
        elif line.startswith("invoke-"):
            method = parse_method_invoke(line)

        if method is None:
            continue

        #log_utils.debug("the method is "+method)

        if method not in allMethods:
            count = count + 1
            allMethods.append(method)
        else:
            pass
            #log_utils.debug(method + " is already exists in allMethods.")

    return count



def parse_class(line):

    if not line.startswith(".class"):
        log_utils.error("line parse error. not startswith .class : "+line)
        return None

    blocks = line.split()
    return blocks[len(blocks)-1]



def parse_method_default(className, line):
    if not line.startswith(".method"):
        log_utils.error("the line parse error in parse_method_default:"+line)
        return None

    blocks = line.split()
    return className + "->" + blocks[len(blocks)-1]


def parse_method_invoke(line):
    if not line.startswith("invoke-"):
        log_utils.error("the line parse error in parse_method_invoke:"+line)

    blocks = line.split()
    return blocks[len(blocks)-1]

然后,我们在apk_utils.py中,将我们之前的splitDex这个方法也调整下,改为调用这个文件中的函数:


def splitDex(workDir, decompileDir):
    """
        如果函数上限超过限制,自动拆分smali,以便生成多个dex文件
    """

    log_utils.info("now to check split dex ... ")

    smaliPath = decompileDir + "/smali"

    multidexFilePath = file_utils.getFullPath(smaliPath + "/android/support/multidex/MultiDex.smali")
    if not os.path.exists(multidexFilePath):
        #android-support-multidex.jar不存在,从local下面拷贝,并编译
        dexJar = file_utils.getFullPath('config/local/android-support-multidex.jar')
        if not os.path.exists(dexJar):
            log_utils.error("the method num expired of dex, but no android-support-multidex.jar in u8.apk or in local folder")
            return

        targetPath = file_utils.getFullPath(workDir + "/local")
        if not os.path.exists(targetPath):
            os.makedirs(targetPath) 

        file_utils.copy_file(dexJar, targetPath+"/android-support-multidex.jar")  

        jar2dex(targetPath, targetPath)
        smaliPath = file_utils.getFullPath(decompileDir + "/smali")
        ret = dex2smali(targetPath + '/classes.dex', smaliPath)        



    allFiles = []
    allFiles = file_utils.list_files(decompileDir, allFiles, [])    

    maxFuncNum = 65535
    currFucNum = 0
    totalFucNum = 0

    currDexIndex = 1

    allRefs = []

    #保证U8Application等类在第一个classex.dex文件中
    for f in allFiles:
        f = f.replace("\\", "/")
        if "/com/u8/sdk" in f or "/android/support/multidex" in f:
            currFucNum = currFucNum + smali_utils.get_smali_method_count(f, allRefs)

    totalFucNum = currFucNum
    for f in allFiles:

        f = f.replace("\\", "/")
        if not f.endswith(".smali"):
            continue

        if "/com/u8/sdk" in f or "/android/support/multidex" in f:
            continue

        thisFucNum = smali_utils.get_smali_method_count(f, allRefs)
        totalFucNum = totalFucNum + thisFucNum
        if currFucNum + thisFucNum >= maxFuncNum:
            currFucNum = thisFucNum
            currDexIndex = currDexIndex + 1
            newDexPath = os.path.join(decompileDir, "smali_classes"+str(currDexIndex))
            os.makedirs(newDexPath)
        else:
            currFucNum = currFucNum + thisFucNum


        if currDexIndex > 1:
            targetPath = f[0:len(decompileDir)] + "/smali_classes"+str(currDexIndex) + f[len(smaliPath):]
            file_utils.copy_file(f, targetPath)
            file_utils.del_file_folder(f)


    log_utils.info("the total func num:"+str(totalFucNum))
    log_utils.info("split dex success. the classes.dex num:"+str(currDexIndex))

这样重新调整之后,测试了几个apk,和几个渠道,最终计算出来的函数个数, 和通过最终的dex比对,发现函数个数都是一致的。dex自动拆分功能也正常。

猜你喜欢

转载自blog.csdn.net/yeshennet/article/details/81773181