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

作者

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

背景

游戏发行领域,我们做的渠道切包是基于下面的原理:

在进行切包的流程中有一个步骤是需要进行smali代码合并,目前我们所做的只是针对一个 smali 文件夹的情况,这样会导致渠道 sdk 在覆盖母包的时候,有些会覆盖不到。比如:

image.png

这样会使得同一个smali文件存在于多个smali文件夹中,导致在加载dex的时候,不同dex文件都有同一个类,那么假如这个类在不同版本里面类实现存在差异,就有可能会导致执行调用的时候出错。

于是便修改了整个打包的流程:

image.png
(自动打包博客指引:juejin.cn/post/690752…

其中优化了自动打包的流程:

1.自动分task 作为必执行阶段后,会先对当前 smali 方法数进行判断,假如不会超过,就不会执行分包操作,只是多了计算的时间

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

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

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

计数方案

关于官方给出的就是一个简单的说法,就是引用的函数个数(不重复的)。 针对于该说法,进行了一些实现。

  • 最终得出结论: 有定义并且未被调用的方法 + 被调用的方法,并且都只保留一次
    怎么判定写的方法计数和 最终生成dex后引用的个数是不是一样呢? AndroidStudio 的 APK Analyzer 的在分析一个dex的时候,有一个reference method count,该数字就是最后的计算结果:

image.png

做法:得出 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 语境下给出了代码实现。

企业微信截图_a1342dd2-f370-4faa-abc3-c843d7346c51.png

© 版权声明
THE END
喜欢就支持一下吧
点赞0

Warning: mysqli_query(): (HY000/3): Error writing file '/tmp/MY8ulOKl' (Errcode: 28 - No space left on device) in /www/wwwroot/583.cn/wp-includes/class-wpdb.php on line 2345
admin的头像-五八三
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

图形验证码
取消
昵称代码图片