欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Neptune 跨Dex调用问题解决

程序员文章站 2022-11-23 10:56:22
前言最近在维护Neptune框架的时候,遇到了一个问题,这里简单的记录下问题原因和解决方案。Neptune这是爱奇艺开源的两个插件化框架之一,一个是Neptune,一个是Qigsaw。Neptune is a flexible, powerful and lightweight plugin framework for Android.Neptune早期借鉴了百度的插件化框架,后续借鉴了滴滴推出的插件化框架方案VirtualAPK,慢慢发展成为一套较为稳定的方案。目前服务于爱奇艺和随刻两个App...

前言

最近在维护Neptune框架的时候,遇到了一个问题,这里简单的记录下问题原因和解决方案。

Neptune

这是爱奇艺开源的两个插件化框架之一,一个是Neptune,一个是Qigsaw

Neptune is a flexible, powerful and lightweight plugin framework for Android.

Neptune早期借鉴了百度的插件化框架,后续借鉴了滴滴推出的插件化框架方案VirtualAPK,慢慢发展成为一套较为稳定的方案。

目前服务于爱奇艺和随刻两个App中,爱奇艺主App目前有25+插件,随刻有10+。

问题

[entrypoint_utils-inl.h:97] Inlined method resolution crossed dex file boundary: from java.lang.String com.xxx.xxx.xxx.a(android.content.Context, org.json.JSONObject) in /data/user/0/com.qiyi.video/app_pluginapp/...

分析

这是一个跨Dex调用的常见问题:

Google在Android P中添加了新的检测项,对国内大多数应用造成了严重影响:在调用resolve inline method时,如果检测到caller与callee处于不同的dex file,会主动发起abort(inline不允许跨dex文件),导致应用出现闪退等异常问题。

art/runtime/entrypoints/entrypoint_utils-inl.h

让我们看看相关的源码:

namespace art {

inline ArtMethod* GetResolvedMethod(ArtMethod* outer_method,
                                    const CodeInfo& code_info,
                                    const BitTableRange<InlineInfo>& inline_infos)
    REQUIRES_SHARED(Locks::mutator_lock_) {
  DCHECK(!outer_method->IsObsolete());

  // This method is being used by artQuickResolutionTrampoline, before it sets up
  // the passed parameters in a GC friendly way. Therefore we must never be
  // suspended while executing it.
  ScopedAssertNoThreadSuspension sants(__FUNCTION__);

  {
    InlineInfo inline_info = inline_infos.back();

    if (inline_info.EncodesArtMethod()) {
      return inline_info.GetArtMethod();
    }

    uint32_t method_index = code_info.GetMethodIndexOf(inline_info);
    if (inline_info.GetDexPc() == static_cast<uint32_t>(-1)) {
      // "charAt" special case. It is the only non-leaf method we inline across dex files.
      ArtMethod* inlined_method = jni::DecodeArtMethod(WellKnownClasses::java_lang_String_charAt);
      DCHECK_EQ(inlined_method->GetDexMethodIndex(), method_index);
      return inlined_method;
    }
  }

  // Find which method did the call in the inlining hierarchy.
  ClassLinker* class_linker = Runtime::Current()->GetClassLinker();
  ArtMethod* method = outer_method;
  for (InlineInfo inline_info : inline_infos) {
    DCHECK(!inline_info.EncodesArtMethod());
    DCHECK_NE(inline_info.GetDexPc(), static_cast<uint32_t>(-1));
    uint32_t method_index = code_info.GetMethodIndexOf(inline_info);
    ArtMethod* inlined_method = class_linker->LookupResolvedMethod(method_index,
                                                                   method->GetDexCache(),
                                                                   method->GetClassLoader());
    if (UNLIKELY(inlined_method == nullptr)) {
      LOG(FATAL) << "Could not find an inlined method from an .oat file: "
                 << method->GetDexFile()->PrettyMethod(method_index) << " . "
                 << "This must be due to duplicate classes or playing wrongly with class loaders";
      UNREACHABLE();
    }
    DCHECK(!inlined_method->IsRuntimeMethod());
    if (UNLIKELY(inlined_method->GetDexFile() != method->GetDexFile())) {
      // TODO: We could permit inlining within a multi-dex oat file and the boot image,
      // even going back from boot image methods to the same oat file. However, this is
      // not currently implemented in the compiler. Therefore crossing dex file boundary
      // indicates that the inlined definition is not the same as the one used at runtime.
      bool target_sdk_at_least_p =
          IsSdkVersionSetAndAtLeast(Runtime::Current()->GetTargetSdkVersion(), SdkVersion::kP);
      // 这里便是抛异常的地方    
      LOG(target_sdk_at_least_p ? FATAL : WARNING)
          << "Inlined method resolution crossed dex file boundary: from "
          << method->PrettyMethod()
          << " in " << method->GetDexFile()->GetLocation() << "/"
          << static_cast<const void*>(method->GetDexFile())
          << " to " << inlined_method->PrettyMethod()
          << " in " << inlined_method->GetDexFile()->GetLocation() << "/"
          << static_cast<const void*>(inlined_method->GetDexFile()) << ". "
          << "This must be due to duplicate classes or playing wrongly with class loaders. "
          << "The runtime is in an unsafe state.";
    }
    method = inlined_method;
  }

  return method;
}

...

}

在这部分代码中关键注释如下:

TODO: We could permit inlining within a multi-dex oat file and the boot image, even going back from boot image methods to the same oat file. However, this is not currently implemented in the compiler. Therefore crossing dex file boundary indicates that the inlined definition is not the same as the one used at runtime.

解决方案

这里根据Neptune的实现来分析解决方案。

Neptune源码分析

SdkLibrary/src/org/qiyi/pluginlibrary/loader/PluginClassLoader.java

/**
 * 插件的DexClassLoader,用来做一些"更高级"的特性,
 * 比如添加插件依赖,支持multidex
 */
public class PluginClassLoader extends DexClassLoader {
    // 插件的包名
    private String pkgName;
    // 依赖的插件的ClassLoader
    private List<DexClassLoader> dependencies;

    public PluginClassLoader(PluginPackageInfo packageInfo, String dexPath, String optimizedDirectory,
                             String librarySearchPath, ClassLoader parent) {
        super(dexPath, optimizedDirectory, librarySearchPath, parent);
        this.pkgName = packageInfo.getPackageName();
        this.dependencies = new ArrayList<>();
        MultiDex.install(packageInfo, dexPath, this);
    }

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        // 根据Java ClassLoader的双亲委托模型,执行到此在parent ClassLoader中没有找到
        // 类似的,我们优先在依赖的插件ClassLoader中查找
        for (ClassLoader classLoader : dependencies) {
            try {
                Class<?> c = classLoader.loadClass(name);
                if (c != null) {
                    // find class in the dependency
                    return c;
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found ini dependency class loader
            }
        }
        // If still not found, find in this class loader
        try {
            return super.findClass(name);
        } catch (ClassNotFoundException e) {
            StringBuilder sb = new StringBuilder("tried ClassLoaders ");
            if (dependencies.isEmpty()) {
                sb.append("none;");
            } else {
                for (DexClassLoader dependency : dependencies) {
                    sb.append(dependency.toString());
                    sb.append(";");
                }
            }
            throw new ClassNotFoundException(sb.toString(), e);
        }
    }

    /**
     * 获取插件ClassLoader对应的插件包名
     */
    public String getPackageName() {
        return pkgName;
    }

    /**
     * 添加依赖的插件ClassLoader
     */
    public void addDependency(DexClassLoader classLoader) {
        dependencies.add(classLoader);
    }

    @Override
    public String toString() {
        String self = super.toString();
        StringBuilder deps = new StringBuilder();
        for (ClassLoader classLoader : dependencies) {
            deps.append(classLoader.toString());
        }
        String parent = getParent().toString();
        return "self: " + self
                + "; deps: size=" + dependencies.size()
                + ", content=" + deps
                + "; parent: " + parent;
    }
}

可以看到,Neptune插件化框架的ClassLoader并没有打破双亲委派模型,优先从宿主中找相关的类,如果找不到再从插件中查找。

出现场景

  • 场景一

插件宿主的dex和插件dex中存在重复类,系统后台优化inline编译后,便会出现此问题。

  • 场景二

由 Classloader A 加载的 class1 调用一个由 Classloader B 加载的 class2里的某个 inline 方法,将导致应用闪退。

解决方案

  • 插件中不应该将重复的基础依赖打进去,应该以compileOnly的方式。
  • PluginClassLoader打破双亲委派模型,优先从插件中读取。
public class PluginClassLoader extends DexClassLoader {
    @Override
    public Class<?> loadClass(String name) throws ClassNotFoundException {
        // 优先加载插件Dex,解决Android9之后虚拟机oat后禁止跨Dex调用的问题
        try {
            Class clazz = super.findClass(name);
            if (clazz == null) {
                return super.loadClass(name);
            }
            return clazz;
        } catch (ClassNotFoundException e) {
            return super.loadClass(name);
        }
    }
}

其他修复方案

通告 | Android P新增检测项 应用热修复受重大影响

  1. 不要将ROM中预置的jar包打包至apk。
  2. 不要使用相同的class loader加载重复类。
  3. 如果必须要有重复类的话,避免内联现象(比如,在不期望被inline的函数里面加try catch,这样compiler就不会将此函数inline)。

附录

本文地址:https://blog.csdn.net/Notzuonotdied/article/details/107288136