作者

大家好,我叫小嘉; 自己20年本科毕业于广东工业大学,于2020年6月参加37手游安卓团队;现在作业是国内游戏发行安卓相关开发。

背景

游戏发行领域,咱们做的途径切包是基于下面的原理:

【游戏发行】自动分包优化之方法数计算解析

在进行切包的流程中有一个过程是需求进行smali代码合并,现在咱们所做的仅仅针对一个 smali 文件夹的情况,这样会导致途径 sdk 在掩盖母包的时分,有些会掩盖不到。比方:

【游戏发行】自动分包优化之方法数计算解析

这样会使得同一个smali文件存在于多个smali文件夹中,导致在加载dex的时分,不同dex文件都有同一个类,那么假设这个类在不同版别里面类完成存在差异,就有可能会导致履行调用的时分犯错。

所以便修改了整个打包的流程:

【游戏发行】自动分包优化之方法数计算解析
(主动打包博客指引:/post/690752…

其间优化了主动打包的流程:

1.主动分task 作为必履行阶段后,会先对当时 smali 办法数进行判别,假设不会超越,就不会履行分包操作,仅仅多了核算的时刻

2.主动分包task 履行后,会再次履行 smali 办法数目判别,如果会超出(因为现在的分包逻辑是根据获取 Application 以及清单文件中的四大组件的索引一切类来作为第一个smali 的内容,不是直接根据办法数来做判别,所以有可能会超出),就进行以办法数目为根据的分包。

这样履行下来打包的时分就不会再有 65535 问题。

整个修改流程中一个比较重要的是判别办法数: 原先的做法是:扫描 smali文件中的内容: 经过 .end method 字段来计数,本能的理解为这便是办法数的计数。 但是尝试过几次后发现禁绝,严格来说,是小了很多。

计数计划

关于官方给出的便是一个简略的说法,便是引证的函数个数(不重复的)。 针对于该说法,进行了一些完成。

  • 最终得出结论: 有界说而且未被调用的办法 + 被调用的办法,而且都只保留一次 怎么判定写的办法计数和 最终生成dex后引证的个数是不是相同呢? AndroidStudio 的 APK Analyzer 的在剖析一个dex的时分,有一个reference method count,该数字便是最后的核算成果:

【游戏发行】自动分包优化之方法数计算解析

做法:得出 apk 中的 dex 的引证办法后,转化为 smali 文件,用自己写的算法成果来比对得出算法是否正确。

算法思路:

方针:完成有界说而且未被调用的办法 + 被调用的办法,而且都只保留一次,核算一共的次数

因为都只保留一次,所以合适选用set数据接口,对于重复只算一次。 在smali语法里面: 办法调用是用 .invoke-xxxx 来表示: 比方:

invoke-static {v2, v3}, Lcom/netease/ntunisdk/base/UniSdkUtils;->e(Ljava/lang/String;Ljava/lang/String;)V

此句表达的是 调用 UniSdkUtils 类的 e(String,String)静态办法,回来值为空

办法界说是 .method xxxx 来表示: 比方:

.method public static obj2Json(Lcom/netease/ntunisdk/base/AccountInfo;)Lorg/json/JSONObject;

此句表达的是界说了一个 obj2Json(AccountInfo) 回来值为 JSONObject 的public办法

因为不同类的办法签名有可能相同,所以需求类+办法的形式来做区分。

  • 转化为算法便是:

找到smali文件后读取内容:

  1. 新建一个 set 数据结构,确保有界说的办法而且和被调用的办法是同一个的情况下,只核算一次。

  2. 找到以.class 最初的内容,截取当时类名: Lcom/test/test_class;

  3. 找到以.method 最初的内容,转译为类名+“->”+函数名+字段类型+回来格局 .method public constructor ()V 转译为: Lcom/test/test_class;->()V
    将该字符串参加 set 数据结构

  4. 找到以.invoke- 最初的内容,直接保留 Lcom/test/test_class;->()V 将该字符串参加 set 数据结构

代码完成

/*
* dfs 遍历,当时smali的引证办法数有多少,超越sum数目的话,回来需求移动的文件
public static boolean dfsByStack(String path,int sum,List<String> list){
        HashSet<String> hashSet = new HashSet<String>(70000);
        int current = 0;
        File file = new File(path);
        if (!file.exists()) return false;
        Stack<String> stack = new Stack<>();
        stack.push(path);
        while (!stack.isEmpty()){
            String path1 = stack.pop();
            File file1 = new File(path1);
            if (file1.exists()){
                if (file1.isFile()){
                    String className = "";
                    List<String> lines = FileUtil.readAllLines(file1.getAbsolutePath());
                    if (lines == null) continue;
                    for (String line : lines){
                        line = trimStart(line);
                        //1 找到类名
                        if (line.startsWith(".class")){
                            String[] content = line.split(" ");
                            className = content[content.length-1];
                        }
                        //2 找到办法名
                        if (line.startsWith(".method")){
                            String[] content = line.split(" ");
                            String method = content[content.length-1];
                            hashSet.add(className + "->" + method);
                        }
                        //3 找到调用办法
                        if (line.startsWith("invoke-")){
                            String[] content = line.split(" ");
                            String method = content[content.length-1];
                            hashSet.add(method);
                        }
                    }
                    if (hashSet.size() > sum){
                        System.out.println("hashSet: " + hashSet.size());
                        System.out.println("current: " + current);
                        return true;
                    }
                    current = hashSet.size();
                    if (list != null) {
                        if (current <= sum /2) {
                            list.add(file1.getAbsolutePath());
                        }
                    }
                }
                if (file1.isDirectory()){
                    for (File path2: file1.listFiles()){
                        stack.push(path2.getAbsolutePath());
                    }
                }
            }
        }
        System.out.println("current: " + current);
        return  false;
    }

小结

本次博客主要解说之前打包机完成上的一些问题,提出了改善办法后,对主动分包进行优化,解说了 dex 文件中办法计数的完成,并在 smali 语境下给出了代码完成。

【游戏发行】自动分包优化之方法数计算解析