返回

JNI 创建 Android Activity:两种方案解析

Android

JNI 中创建 Android Activity 的探索

在 Android 应用开发中,经常会遇到这样的需求:在 C++ 层通过 JNI 直接与 JVM 交互,进而操作 Android 相关的 API。其中一个典型问题就是:是否能从 JNI 中创建并启动一个 Android Activity? 核心挑战在于如何桥接原始 JVM 环境与 Android 框架的上下文,本文将探讨可行方案,以及可能遇到的问题,并提供相应的解决方案。

问题核心:上下文的缺失

问题关键在于直接创建的 JVM 并没有 Android 应用上下文环境。 Android SDK 中的类,如 android.content.Intentandroid.content.Context,它们依赖于特定的 Android 系统服务和生命周期管理机制。简单的说,单独的JVM并不知道 Android 上下文。试图在这样一个隔离的环境中直接实例化 Context 对象并使用 startActivity() 会导致失败,因为 Context 是抽象类,必须通过系统赋予具体实现。

解决方案一:反射调用 Android 系统服务

这种方法通过反射来访问 Android 系统提供的服务,而不是直接实例化 Android 对象。我们可以利用 ActivityManager 来启动一个新的 Activity,这需要深入了解 ActivityManager 的工作机制以及相关 API 的签名。

步骤:

  1. 找到 ActivityManagerService : 首先,你需要获得 ActivityManagerService 的实例。Android 系统利用反射机制暴露此服务,具体方法为先获取 ActivityManager 类,再从 ActivityManager 中获取 IActivityManager 接口实例。
  2. 构建 Intent 对象: 像平时在 Android 开发中使用 Activity 一样,构建 intent。
  3. 利用 startActivity 启动 Activity: IActivityManager 提供了 startActivity() 方法。我们需要通过反射调用这个方法,传入必要的参数,包括 Intent 对象,flags 以及一个 call 对象 (一般可设为空)。

代码示例 (Java 部分,JNI 调用):

import android.content.ComponentName;
import android.content.Intent;
import android.os.Bundle;
import java.lang.reflect.Method;
import android.os.IInterface;
import java.lang.reflect.InvocationTargetException;
import android.app.ActivityManager;
public class JniHelper {
   public static void startMainActivity()  {

        try {
           
            Class<?> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
            Method getDefault = activityManagerNativeClass.getMethod("getDefault");
            IInterface activityManager = (IInterface)getDefault.invoke(null);

            Method startActivity = activityManager.getClass().getMethod(
                  "startActivity",
                  android.os.IBinder.class,  // IApplicationThread
                   java.lang.String.class, // calling package
                   android.content.Intent.class,
                   java.lang.String.class,  //resolvedType
                   android.os.IBinder.class, //ResultTo
                  java.lang.String.class,   // resultWho
                  int.class,        // requestCode
                  int.class,       // flag
                   android.app.ProfilerInfo.class,
                    android.os.Bundle.class     // options
                    );
           
              Intent intent = new Intent();
                intent.setComponent(new ComponentName("your.package.name", "your.package.name.MainActivity")); //设置ComponentName  MainActivity路径 
                 Bundle bundle = new Bundle();
                startActivity.invoke(activityManager, 
                 null, "your.package.name", intent,  null,null , null,0, 0,null, bundle);  // invoke startActivity()

       }
        catch(ClassNotFoundException  |  NoSuchMethodException | IllegalAccessException| InvocationTargetException e ){
              e.printStackTrace();
           }
        
       }
 }

命令行指令(编译 JniHelper.java)

假设已正确配置 android.jar 的路径:

javac JniHelper.java

操作步骤:

  1. 在 Java 层编写 JniHelper 类,并在其中实现 startMainActivity() 方法,代码如上所示。将包名 (your.package.name) 和 MainActivity 完全限定名(your.package.name.MainActivity) 替换为自己的值。
  2. 编译 Java 类,并将 class 文件路径添加到 JNI 代码的 JVM classpath 中。
  3. 通过 JNI 调用 JniHelper.startMainActivity() 方法。
  4. 确保 APP 拥有权限 android.permission.START_ACTIVITIES_FROM_BACKGROUND, 否则会失败。

优点: 这种方法不直接依赖实例化 Context。

缺点:

  1. 反射方法涉及隐藏API,可能有兼容性问题,随着Android系统升级,需要注意兼容性问题,建议充分测试。
  2. 代码逻辑较复杂,需要熟悉Android系统的实现细节。

解决方案二:通过 Android ContextWrapper 进行桥接

此方案并非完全从头构建Activity,而是借助 Android 环境现有的 Activity 或 Service Context。 我们可以通过在Android activity里进行JNI加载和调用。
这是一种将 JNI 层调用请求传递给已运行的 Android 实例的方法,这样就不会受到直接构造抽象类的限制。

步骤:

  1. 在现有 Activity 中建立 JNI 调用: 将 JNI 方法封装在 Android Activity 中调用, Activity 本身具备合适的 Context, 可以传递这个上下文到 JNI 层。

  2. 在 JNI 层存储 context : 通过JNI层把 context 转化为 java对象句柄传递到 C++ 层进行存储。

  3. 使用 context.startActivity(): 在 JNI 层可以使用 Context 对象执行 context.startActivity() ,注意通过 Jobect 调用方法

代码示例(Activity.java):

public class MainActivity extends Activity{
  @Override
  protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // 调用 C++ 方法
        System.loadLibrary("native-lib");  //加载 .so 库
        nativeStart(this); // 传递 Activity Context 到 JNI 层

  }

   //native 方法声明
   public native void nativeStart(Context ctx);


}

代码示例(native-lib.cpp):

#include <jni.h>
#include <string>

//保存Java context 句柄的全局变量
static jobject g_activity_context = nullptr;
static JavaVM* jvm = nullptr; // 全局的jvm

jint JNI_OnLoad(JavaVM *vm, void *reserved) {
  jvm = vm;
  JNIEnv* env;
  if (vm->GetEnv((void**)&env, JNI_VERSION_1_6) != JNI_OK)
      return JNI_ERR;
    return JNI_VERSION_1_6;

}


extern "C" JNIEXPORT void JNICALL
Java_your_package_name_MainActivity_nativeStart(
        JNIEnv* env,
        jobject  activity_object,  // 接收 MainActivity 的  Activity 上下文 context
      jobject j_activity_context )  //Java activity context obj

  {
      g_activity_context = env->NewGlobalRef(j_activity_context);
      //后续可以用该全局变量 context调用 start activity方法
       env->DeleteLocalRef(j_activity_context);

      // 创建intent 和 ComponentName
      jclass intent_class = env->FindClass("android/content/Intent");
       jmethodID intent_constructor = env->GetMethodID(intent_class, "<init>", "()V");
      jobject intent = env->NewObject(intent_class, intent_constructor);

      jclass component_name_class = env->FindClass("android/content/ComponentName");
       jmethodID component_constructor = env->GetMethodID(component_name_class, "<init>",
                        "(Ljava/lang/String;Ljava/lang/String;)V");
       jstring package_name = env->NewStringUTF("your.package.name");
       jstring mainactivity_name =  env->NewStringUTF("your.package.name.MainActivity");


     jobject component = env->NewObject(component_name_class, component_constructor, package_name,mainactivity_name);

    jmethodID intent_setComponent =  env->GetMethodID(intent_class, "setComponent","(Landroid/content/ComponentName;)Landroid/content/Intent;");

   env->CallObjectMethod(intent, intent_setComponent, component );



    jclass context_class =  env->GetObjectClass(g_activity_context);
       jmethodID start_activity_method =  env->GetMethodID(context_class,"startActivity",
        "(Landroid/content/Intent;)V");



         env->CallVoidMethod(g_activity_context, start_activity_method, intent);
     // 这里你可以根据自己的需求启动 Activity


   if(g_activity_context){
       env->DeleteGlobalRef(g_activity_context);
      }

     if(package_name){
          env->DeleteLocalRef(package_name);
        }

      if(mainactivity_name){
         env->DeleteLocalRef(mainactivity_name);
      }
    }

命令行指令(CMakeLists.txt示例):

cmake_minimum_required(VERSION 3.4.1)
project(native-lib)


set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11")


add_library(native-lib
             SHARED
             src/native-lib.cpp )

target_include_directories(native-lib
  PUBLIC
       # your Android NDK include path ,example below.
        ${ANDROID_NDK}/sysroot/usr/include  
    
 )
find_library(
        log-lib
        log
)


target_link_libraries( # Specifies the target library.
                      native-lib

                         ${log-lib})

操作步骤:

  1. MainActivity 中加载 native-lib 并调用 JNI 方法 nativeStart(Context)。传递 activity context 。
  2. 在 JNI 中将 context 设置为全局变量,方便在合适的地方进行使用
  3. C++ 代码中实现业务逻辑并利用保存的 context 启动新的 Activity。

优点:

  • 利用 Android 系统的环境,更容易使用相关 Android API。
  • 无需处理抽象 Context 实例化的问题。

缺点:

  • 需要至少有一个活动的Android Activity才能启动新的 Activity。

安全建议

无论是反射还是 ContextWrapper 方案,在JNI 调用过程中需要特别关注:

  • 参数校验: 确保从 JNI 层接收的数据合法,防止数据异常导致应用崩溃。
  • 线程安全: JNI 调用在多线程环境下,尤其需要考虑线程安全,比如防止 context 被并发访问。
  • 资源释放: 在 C++ 中操作完 context 和 Intent 对象记得及时释放内存,避免内存泄漏。

选择哪种方案,取决于具体场景和你的偏好。如果只是简单地想在特定 Activity 触发的情况下启动一个新 Activity ,那么 Context 传递会是更好的选择; 如果你需要在更底层,没有 Context 的情况下操作, 可以采用反射机制,当然其复杂度也更高。每种方案都需要谨慎的实现和测试。