第3.2.3节 Android动态调用链路的获取

3.2.3 Android App动态调用链路

在Android应用中,动态调用链路指的是应用在运行时的调用路径。这通常涉及到方法调用的顺序和调用关系,特别是在应用的复杂逻辑中,理解这些调用链路对于调试和性能优化非常重要。

1,动态调用链路获取方法

要获取Android应用的动态调用链路,可以使用以下几种方法:

(1)使用调试工具:

(2)使用日志:

(3)使用性能分析工具:

(4)使用第三方工具:

(5)动态分析工具:

(6)字节码插桩:

每种方法都有其优缺点,选择合适的方法取决于你的具体需求和应用的复杂性。如果你只是想简单地查看某个功能的调用链路,使用调试工具或日志可能是最简单的选择;如果需要更深入的分析,Profiler或第三方工具可能更合适。

2,动态调用链路的使用

静态调用链路主要用来分析需求开发的影响范围,界定测试范围。动态调用链路主要和用例关联起来,需要知道哪些用例执行过程中,会调用哪些类或是函数,如下图所示:

通过获取用例的动态调用链路后,可以做如下事情:

  • 过滤用例执行中的前置和后置步骤,减少用例关联的代码;
  • 通过路径覆盖,推荐用例,用最小的用例集来覆盖最大的功能面;
  • 用例执行失败,问题定位与修复。

Android App的动态调用链路采取插桩方式进行记录,由于jacoco Android插件没有开源,可以借助于服务端的jacoco来开发新的插件来实现,主要流程如下:

一,Trace插件开发

1,保存类函数对应关系

借助于jacoco插件的核心功能,对项目代码进行插桩,对每一个类和函数进行插桩,记录类的classid, methodid,执行次序和执行时间。插桩的过程中,将classid, 类名,methodid,函数名与参数等信息记录到class-method-map.txt文件中,打包的时候会将文件生成保存到工作目录中。

java 复制代码
public class JacocoPlugin extends Transform implements Plugin<Project> {
    .....
    @Override
public void transform(TransformInvocation transformInvocation)
        throws TransformException, InterruptedException, IOException {
    super.transform(transformInvocation);
    System.out.println("AutoTest " + project.getName() + "," + project.getDisplayName() + "," + project.getPath());
    System.out.println("--------------------------");

    long start = System.currentTimeMillis();
    long copyCost = 0;
    long instrumentCost = 0;
    long copyDirCost = 0;
    Collection<TransformInput> inputs = transformInvocation.getInputs();
    TransformOutputProvider outputProvider = transformInvocation.getOutputProvider();
    for (TransformInput input : inputs) {

        //对项目中的jar包进行处理
        for (JarInput jarInput : input.getJarInputs()) {
            String jarName = jarInput.getName();
            File dest = outputProvider.getContentLocation(jarName,
                    jarInput.getContentTypes(), jarInput.getScopes(), Format.JAR);
            //暂时不对app应用引用的jar包进行注入,防止因引用包造成二次注入 2024013
            if (filterJar(jarInput, sKwaiTestConfig) && project.getPlugins().hasPlugin("com.android.library")) {
                System.out.println("process jar is:" + jarName);
                System.out.println("process jar abspath is :" + jarInput.getFile().getAbsolutePath());
                sKwaiTestConfig.classJars.add(jarInput.getFile().getAbsolutePath());
                long tStart = System.currentTimeMillis();
                transformJar(jarInput, dest, jarName);
                long tEnd = System.currentTimeMillis();
                instrumentCost += (tEnd - tStart);
            } else {
                long cStart = System.currentTimeMillis();
                FileUtils.copyFile(jarInput.getFile(), dest);
                long cEnd = System.currentTimeMillis();
                copyCost += (cEnd - cStart);
                System.out.println("App Project " + project.getName() + "," + project.getDisplayName() + "," + project.getPath() + " not transform jar files!");
            }
            //System.out.println("transform jar的位置:"+dest.getAbsolutePath());
        }
        long dStart = System.currentTimeMillis();
        for (DirectoryInput directoryInput : input.getDirectoryInputs()) {
            File dest = outputProvider.getContentLocation(directoryInput.getName(),
                    directoryInput.getContentTypes(), directoryInput.getScopes(), Format.DIRECTORY);
            if (filterDir(directoryInput, sKwaiTestConfig)) {
                String relatePath = directoryInput.getFile().getAbsolutePath();
                System.out.println("process dir file is:" + relatePath);
                sKwaiTestConfig.classDirs.add(relatePath);
                Instrumenter instrumenter = new Instrumenter(new OfflineInstrumentationAccessGenerator());
                transformDirectory(directoryInput.getFile(), dest, relatePath, instrumenter);
            } else {
                FileUtils.copyDirectory(directoryInput.getFile(), dest);
            }
            //System.out.println("transform 类的位置:" + dest.getAbsolutePath());
        }
        long dEnd = System.currentTimeMillis();
        copyDirCost = dEnd - dStart;
    }

    if (FLAG.decrementAndGet() == 0) {
        if (!TraceMethodVisitor.sMap.isEmpty()) {
            Utils.writeTraceMap(sTraceMapPath, TraceMethodVisitor.sMap);
            sKwaiTestConfig.customTraceMapPath = sTraceMapPath;
            //清除早期的trac数据
            TraceMethodVisitor.sMap.clear();
        } else {
            System.out.println("TraceMethodVisitor 为空!");
        }
        String configStr = (new Gson()).toJson(sKwaiTestConfig);
        Utils.writeKwaiTestFile(sKwaiTestConfigPath, configStr);
        System.out.println("Auto test jacoco writeKwaiTestFile");
    }
    long end = System.currentTimeMillis();
    System.out.println("---------------Auto test jacoco transform end-----------");
    System.out.println("Auto test jacoco copy jar cost:" + copyCost + "ms");
    System.out.println("Auto test jacoco copy dir cost:" + copyDirCost + "ms");
    System.out.println("Auto test jacoco instrument cost:" + instrumentCost + "ms");
    System.out.println("Auto test jacoco transform cost:" + (end - start) / 1000 + "s");
    System.out.println("---------执行 transform---------------");
}
.....
}

2,记录用例执行路径

在类函数中添加记录用例执行路径的操作,如:新建CSTraceHelper类

java 复制代码
public class CSTraceHelper {
    ....
     private Thread sConsumer = new Thread(() -> {
    if (sFirstInit) {
      sFirstInit = false;
    } else {
      return;
    }
    if (sStoragePath == null) {
      return;
    }
    for (; ; ) {
      flushTraceData();
      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  });
  
  public static void flushTraceData() {
  if (sStoragePath != null && (!sKDSStore.isEmpty() || !sNativeStore.isEmpty())) {
    appendDataToFile();
  }
}
/**
将trace信息写入文件
**/
private static void appendDataToFile() {
  synchronized (sNativeStore) {
    FileWriter fileWriter = null;
    BufferedWriter bw = null;

    try {
      File dataDir = new File(sStoragePath);
      if (!dataDir.exists()) {
        dataDir.mkdirs();
      }
      File dataFile = new File(dataDir, TRACE_FILE_NAME);
      if (!dataFile.exists()) {
        dataFile.createNewFile();
      }
      Log.d(TAG, "Trace文件:" + dataFile.getAbsolutePath());
      fileWriter = new FileWriter(dataFile, true);
      bw = new BufferedWriter(fileWriter);

      Long value = sNativeStore.poll();
      
      HashMap<String, TraceDataSimple> map = new HashMap<>();
      while (value != null) {
        // 从 long 值中恢复三个 int 值
        int classId = (int) (value >> 42);
        int methodId = (int) ((value >> 21) & 0x1FFFFF);
        int line = (int) (value & 0x1FFFFF);

        //Log.d(TAG,"appendDataToFile cid:"+classId+",mId:"+methodId+",line:"+line+",value="+value);

        String key = classId + "" + methodId;
        
        if (map.containsKey(key)) {
          map.get(key).lines.add(String.valueOf(line));
        } else {
          TraceDataSimple method = new TraceDataSimple();
          method.tName = "t";
          method.timeMillis = System.currentTimeMillis();
          method.cid = classId;
          method.mid = methodId;
          method.lines.add(String.valueOf(line));
          map.put(key, method);
        }

        value = sNativeStore.poll();
      }
      
      Iterator<Map.Entry<String, TraceDataSimple>> it = map.entrySet().iterator();
      while (it.hasNext()) {
        bw.write(it.next().getValue().toSimpleString());
      }
      bw.flush();
    } catch (IOException e) {
      Log.d(TAG, "appendDataToFile catch error:" + e.getMessage());
      e.printStackTrace();
    } finally {
      try {
        if (bw != null) {
          bw.close();
        }
        if (fileWriter != null) {
          fileWriter.close();
        }
      } catch (IOException e) {
        Log.d(TAG, "appendDataToFile finally error:" + e.getMessage());
        e.printStackTrace();
      }
    }
    Log.d(TAG,"Trace 数据写入完成:"+sStoragePath);
  }
}
/**
 * 重新注入项目
 * @param cId 类ID
 * @param mId 方法ID
 * @param line 行号
 */
public static void injectTcEp(long cId, int mId, int line) {
  // Log.d(TAG,"sStoragePath="+sStoragePath+",sNeedInject="+sNeedInject);
  if (sStoragePath != null && sNeedInject) {
    // 将三个 int 值存储到一个 long 值中
    long value = ((long) cId << 42) | ((long) mId << 21) | (long) line;
    Log.d(TAG,"Inject cid:"+cId+",mId:"+mId+",line:"+line+",value="+value);
    sNativeStore.offer(value);
  }
}
......
}

同时在Android插件JacocoPlugin的TraceMethodVisitor类中添加给函数注入记录trace信息的操作

java 复制代码
public class TraceMethodVisitor extends MethodVisitor {
    ......
    private void injectMethod() {
    mv.visitLdcInsn(classID);
    mv.visitLdcInsn(id);
    mv.visitLdcInsn(line);
    mv.visitMethodInsn(INVOKESTATIC, "com/kwai/test/core/CSTraceHelper", "injectTcEp",
            "(JII)V", false);
}
.......
}

执行打包命令

java 复制代码
./gradlew upload

将会在指定的目录下,生成新插件的所有包。

二,新插件使用

在要关联用例的App中,添加新的jacoco插件,打包后就会自动对函数中的类,函数进行插桩。如果想同时采集覆盖率数据,控制好相关的开关即可。打包完成后做如下操作:

1,将打包后的class文件上传到指定位置,如精准测试平台,在生成覆盖率报告时候要用到。

2,将生成的class-method-map.txt文件上传到精准测试平台,解析用例与代码的关联关系时需要查询类和函数信息。

java 复制代码
C: com.gavin.asmdemo.MainActivity 1 com/gavin/asmdemo/MainActivity.java
M: onCreate(android.os.Bundle) 0 12
M: toSecond(android.view.View) 1 18
M: toThrid(android.view.View) 2 24
M: onStop() 3 8
M: onStart() 4 8
C: com.gavin.asmdemo.ThridActivity 2 com/gavin/asmdemo/ThridActivity.java
M: onCreate(android.os.Bundle) 0 14
M: toShow(android.view.View) 1 19
M: thToIndexPage(android.view.View) 2 26
M: onStop() 3 10
M: onStart() 4 10
C: com.gavin.asmdemo.BaseActivity 3 com/gavin/asmdemo/BaseActivity.java
M: onCreate(android.os.Bundle) 0 17
M: onStart() 1 24
M: onStop() 2 32
C: com.gavin.asmdemo.SecondActivity 4 com/gavin/asmdemo/SecondActivity.java
M: onCreate(android.os.Bundle) 0 11
M: seToThrid(android.view.View) 1 16
M: toIndexPage(android.view.View) 2 21
M: onStop() 3 7
M: onStart() 4 7

3,在测试需求时,根据需求,上传覆盖率,trace信息文件。

4,生成覆盖率报告时,解析trace信息,记录追溯关系。

5,根据业务特点,过滤一下trace信息,就可以拿到用例与代码的精准关联关系。

相关推荐
雨白5 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹7 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空8 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭9 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日10 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安10 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑10 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟14 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡15 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0015 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体