Android+Flutter混合开发实战

在实战开发中,混合开发很常见,本篇文章讲解在混合开发中的各种问题。比如:Flutter Activity内存泄漏问题,多引擎通信问题。

截止目前本篇文章使用flutter3.35.4+Android Studio 2025.2.2.8

一. 创建flutter模块

在原有项目基础上,创建flutter module,步骤如下:

flutter_lib创建完成后,目录结构如下:

开始集成flutter_lib如下步骤:

如果遇到下边错误,在settings.gradle中进行这样修改:

kotlin 复制代码
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
改为:
repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)

这是最终的配置:

kotlin 复制代码
pluginManagement {
    repositories {
        google()
        mavenCentral()
        gradlePluginPortal()
        maven {
            setUrl("https://maven.aliyun.com/repository/public")
        }
        maven {
            setUrl("https://storage.googleapis.com/download.flutter.io")
        }
    }
}
dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS)
    repositories {
        google()
        mavenCentral()
        maven {
            setUrl("https://maven.aliyun.com/repository/public")
        }
        maven {
            setUrl("https://storage.googleapis.com/download.flutter.io")
        }
    }
}

rootProject.name = "android_flutter"
include(":app")
val filePath = "$settingsDir/flutter_lib/.android/include_flutter.groovy"
apply(from = File(filePath))
include(":flutter_lib")

二. Android原生端打开一个Flutter页面

配置flutter SDK

kotlin 复制代码
        implementation(project(":flutter"))

配置flutterEngine

kotlin 复制代码
        val flutterEngine = FlutterEngine(this)

        // 启动 Flutter(默认 main())
        flutterEngine.dartExecutor.executeDartEntrypoint(
            DartExecutor.DartEntrypoint.createDefault()
        )

        // 缓存 engine,供 Activity 使用
        FlutterEngineCache
            .getInstance()
            .put("flutter_engine", flutterEngine)

跳转Flutter页面

kotlin 复制代码
            val intent = FlutterActivity
                .withCachedEngine("flutter_engine")
                .build(context)

            context.startActivity(intent)
kotlin 复制代码
<activity
    android:name="io.flutter.embedding.android.FlutterActivity"
    android:exported="false"
    android:theme="@style/Theme.AppCompat.NoActionBar"
    android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
    android:hardwareAccelerated="true"
    android:windowSoftInputMode="adjustResize" />

以上步骤就是简单的打开一个Flutter页面,Flutter Activity作为一个载体加载页面。

三. 混合开发FlutterActivity 引起的内存泄漏问题

3.1 内存泄漏产生的原因分析

问题描述:当我打开一个FlutterActivity页面后返回页面,按理说Activity已经销毁了,但引用依然存在。

有没有可能是因为缓存了flutterEngine导致的,那我们修改一下代码,再试试。

kotlin 复制代码
context.startActivity(
    FlutterActivity
        .withNewEngine()
        .build(context)
)

然而还是不行,依然有内存泄漏:

通过以上内存泄漏分析,这些问题是Flutter SDK 本身存在的问题,比如我们针对LocalizationPlugin插件问题做个分析。

既然插件官方插件出现了问题,那么如何释放掉这种资源呢,我们看到存在这样一个activity(FlutterFragmentActivity),其中存在一个回调方法如下图:

这个 cleanUpFlutterEngine() 是干嘛的?

在宿主(Activity / Fragment)销毁前,用来清理你在 configureFlutterEngine() 里"手动绑定"的资源,因此我们是不是可以从这个回调中手动清理引用呢。

3.2 解决内存泄漏

创建一个Activity继承自FlutterFragmentActivity如下代码,解决的核心代码是通过反射找到setLocalizationPlugin 手动释放资源。

kotlin 复制代码
package com.example.android_flutter

import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.localization.LocalizationPlugin

/**
 * 自定义FlutterActivity
 */
class CommonFlutterActivity : FlutterFragmentActivity() {
    override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
        super.cleanUpFlutterEngine(flutterEngine)
        try {
            // 获取 FlutterEngine 类的 flutterJNI 字段
            val flutterJniField = FlutterEngine::class.java.getDeclaredField("flutterJNI")
            // 设置字段可访问
            flutterJniField.isAccessible = true
            // 获取 flutterJNI 实例
            val flutterJNI = flutterJniField.get(flutterEngine)

            // 获取 FlutterJNI 类的 setLocalizationPlugin 方法
            val setLocalizationPluginMethod =
                flutterJNI.javaClass.getMethod(
                    "setLocalizationPlugin",
                    LocalizationPlugin::class.java
                );
            // 调用 setLocalizationPlugin 方法并传入 null
            setLocalizationPluginMethod.invoke(flutterJNI, null)
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }
}

四.混合开发 打开Flutter指定页面(路由跳转)

4.1 关键代码

先说Android端 代码

创建入口引擎

kotlin 复制代码
flutterEngineGroup!!.createAndRunEngine(
            appContext,
            DartEntrypoint.createDefault(),
            initRoute
        )

复用上边的CommonFlutterActivity,指定引擎,这样就把入口引擎和activity绑定一起了。

kotlin 复制代码
override fun provideFlutterEngine(context: Context): FlutterEngine? {
        val engine = FlutterUtil.createFlutterEngine(getRouter())
        if (engine != null) return engine
        return super.provideFlutterEngine(context)
    }

上边的代码通过flutterEngineGroup创建一个engine,initRoute是传入的路由名字。

代码解读,flutterEngineGroup作用:

flutterEngineGroup用于在同一个 Android 应用中创建和管理多个独立的 Flutter 引擎实例,让多个 Flutter 页面/模块可以并行运行和共享资源。多个引擎共享同一个 Flutter 运行时环境,共享 Dart VM 和代码,共享 Flutter 插件注册,减少内存占用(相比每个页面创建新引擎)。

再说flutter端代码:

dart 复制代码
class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter Demo',
      theme: ThemeData(primarySwatch: Colors.blue),
      initialRoute: "/",
      home: Container(),
      onGenerateRoute: AppRouter.onGenerateRoute,
      navigatorObservers: [AppNavigatorObserver()],
    );
  }
}

这段代码就是通过initRoute字段传入的路由名字,从而指定跳转不同的页面。

dart 复制代码
class AppRouter {
  static Route<dynamic> onGenerateRoute(RouteSettings settings) {
    final uri = Uri.parse(settings.name ?? '/');
    final path = uri.path;
    final queryParams = uri.queryParameters;
    debugPrint("queryParams=$queryParams");
    switch (path) {
      case '/page_a':
        return _page(const APage(), settings);

      case '/page_b':
        return _page(const BPage(), settings);

      default:
        return _page(Container(), settings);
    }
  }

  static MaterialPageRoute _page(Widget page, RouteSettings settings) {
    return MaterialPageRoute(builder: (_) => page, settings: settings);
  }
}
dart 复制代码
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

class AppNavigatorObserver extends NavigatorObserver {
  @override
  void didPop(Route route, Route? previousRoute) {
    super.didPop(route, previousRoute);
    if (previousRoute?.isFirst ?? false) {
      SystemNavigator.pop();
    }
  }
}

4.2 完整代码

kotlin 复制代码
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        FlutterUtil.init(this)
    }
}
kotlin 复制代码
package com.example.android_flutter

import android.content.Context
import android.content.Intent
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.embedding.engine.FlutterEngineGroup
import io.flutter.embedding.engine.FlutterEngineGroupCache
import io.flutter.embedding.engine.dart.DartExecutor.DartEntrypoint

object FlutterUtil {
    const val INIT_ROUTER = "initRouter"
    const val PAGE_A = "/page_a"
    const val PAGE_B = "/page_b"

    const val FLUTTER_ENGINE_GROUP = "com.flutter.engine.group"

    @Volatile
    private var flutterEngineGroup: FlutterEngineGroup? = null
    private lateinit var appContext: Context

    fun init(context: Context) {
        if (flutterEngineGroup != null) return

        synchronized(this) {
            if (flutterEngineGroup != null) return

            appContext = context.applicationContext

            flutterEngineGroup =
                FlutterEngineGroupCache.getInstance()[FLUTTER_ENGINE_GROUP]
                    ?: FlutterEngineGroup(appContext).also {
                        FlutterEngineGroupCache
                            .getInstance()
                            .put(FLUTTER_ENGINE_GROUP, it)
                    }
            // 这段代码是个小技巧,创建个空engine可以让页面加载更快,因为flutterEngineGroup中会进行引擎复用。
            // 使用引擎组,当你第二次再创建引擎时,引擎组内部会内存复用,速度极快,不然打开一个页面会很慢。
            flutterEngineGroup!!.createAndRunEngine(
                context.applicationContext,
                DartEntrypoint.createDefault(),
                "/"
            )
        }
    }

    /**
     * Create and run a new FlutterEngine with initialRoute.
     * Engine lifecycle should be managed by the caller (Activity / Fragment).
     */
    fun createFlutterEngine(initRoute: String): FlutterEngine? {
        return flutterEngineGroup!!.createAndRunEngine(
            appContext,
            DartEntrypoint.createDefault(),
            initRoute
        )
    }

    /**
     * 跳转flutter指定页面,支持传入参数
     */
    fun startFlutterActivity(context: Context, pageRoute: String, map: Map<String, Any>? = null) {
        val intent = Intent(context, CommonFlutterActivity::class.java)
        intent.putExtra(INIT_ROUTER, pageRoute)
        if (map != null) {
            intent.putExtra(INIT_PARAM, Gson().toJson(map))
        }
        context.startActivity(intent)
    }
}
kotlin 复制代码
package com.example.android_flutter

import android.content.Context
import android.os.Bundle
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.localization.LocalizationPlugin

/**
 * 自定义FlutterActivity
 */
class CommonFlutterActivity : FlutterFragmentActivity() {
    private var initRouter: String? = null
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        initRouter = intent.getStringExtra(FlutterUtil.INIT_ROUTER)
    }

    override fun provideFlutterEngine(context: Context): FlutterEngine? {
        val engine = FlutterUtil.createFlutterEngine(getRouter())
        if (engine != null) return engine
        return super.provideFlutterEngine(context)
    }

    private fun getRouter(): String {
        return initRouter ?: "/"
    }

    override fun cleanUpFlutterEngine(flutterEngine: FlutterEngine) {
        super.cleanUpFlutterEngine(flutterEngine)
        try {
            // 获取 FlutterEngine 类的 flutterJNI 字段
            val flutterJniField = FlutterEngine::class.java.getDeclaredField("flutterJNI")
            // 设置字段可访问
            flutterJniField.isAccessible = true
            // 获取 flutterJNI 实例
            val flutterJNI = flutterJniField.get(flutterEngine)

            // 获取 FlutterJNI 类的 setLocalizationPlugin 方法
            val setLocalizationPluginMethod =
                flutterJNI.javaClass.getMethod(
                    "setLocalizationPlugin",
                    LocalizationPlugin::class.java
                );
            // 调用 setLocalizationPlugin 方法并传入 null
            setLocalizationPluginMethod.invoke(flutterJNI, null)
        } catch (e: Exception) {
            e.printStackTrace()
        }

    }
}
kotlin 复制代码
package com.example.android_flutter

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.android_flutter.ui.theme.Android_flutterTheme
import io.flutter.embedding.android.FlutterFragment
import io.flutter.embedding.android.RenderMode
import io.flutter.embedding.android.TransparencyMode

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContent {
            Android_flutterTheme {
                Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
                    Greeting(
                        name = "Android",
                        modifier = Modifier.padding(innerPadding)
                    )
                }
            }
        }
    }
}

@Composable
fun Greeting(name: String, modifier: Modifier = Modifier) {
    val context = LocalContext.current
    Column(modifier = modifier) {
        Text(text = "Hello $name!")

        Spacer(modifier = Modifier.height(16.dp))

        Button(onClick = {
            FlutterUtil.startFlutterActivity(context, FlutterUtil.PAGE_A)
        }) {
            Text("打开Flutter页面A")
        }
        Spacer(modifier = Modifier.height(16.dp))
        Button(onClick = {
            FlutterUtil.startFlutterActivity(context, FlutterUtil.PAGE_B)
        }) {
            Text("打开Flutter页面B")
        }
    }
}

@Preview(showBackground = true)
@Composable
fun GreetingPreview() {
    Android_flutterTheme {
        Greeting("Android")
    }
}

五.混合开发 多入口并行 多引擎数据传递

5.1 页面布局如图所示:我的页面嵌入了一个flutter Fragment。

flutterFragment的生成核心代码:

kotlin 复制代码
    private fun createFlutterFragment(): CommonFlutterFragment {
        return FlutterFragment.NewEngineInGroupFragmentBuilder(
            CommonFlutterFragment::class.java,
            FLUTTER_ENGINE_GROUP
        )
            .initialRoute(PAGE_HOME)
            .renderMode(RenderMode.texture)
            .transparencyMode(TransparencyMode.transparent)
            .shouldAttachEngineToActivity(true)
            .build()
    }

5.2 多引擎数据传递

现在有个问题,如果我用startFlutterActivity方式打开了一个页面,那么我在flutter端如何把数据传递给CommonFlutterFragment(我的页面)。

你可能会问,这两个页面代码都是flutter,直接传递不就行了,答案是不行的。因为这两个页面是在原生端使用FlutterEngine创建的,每个FlutterEngine是线程独立的。

要解决这个问题,也很简单,我们为每个FlutterEngine别分创建MethodChannel,把数据先传到原生端,在去传到相关的Channel。

关键代码如下 :

kotlin 复制代码
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)
        // 定义你的name,和flutter端使用同一个
        val methodChannel =
            MethodChannel(
                flutterEngine.dartExecutor.binaryMessenger,
                "CommonFlutterActivity"
            )
        methodChannel.setMethodCallHandler { call: MethodCall, result: MethodChannel.Result ->
            // 假如在这里收到数据后,需要传给HomePage,注意这里需要传给homeChannel,homeChannel就是在CommonFlutterFragment中创建的channel。
            // homeChannel.invokeMethod("fromFlutter", "fromFlutter")
            if (call.method.equals(FlutterUtil.METHOD_CHANNEL_NAME)) {
                FlutterUtil.getHomeMethodChannel()?.invokeMethod(
                    FlutterUtil.METHOD_CHANNEL_NAME,
                    call.arguments
                )
            }
        }
    }
相关推荐
猛扇赵四那边好嘴.3 小时前
Flutter 框架跨平台鸿蒙开发 - 药品信息查询应用开发教程
flutter·华为·harmonyos
天燹3 小时前
Qt 6 嵌入 Android 原生应用完整教程
android·开发语言·qt
AiFlutter3 小时前
六、表单元素(04):开关
flutter·低代码平台·aiflutter·aiflutter低代码·dart开发
美狐美颜sdk3 小时前
企业级直播美颜SDK与动态贴纸SDK开发技术方案拆解与落地实践
android·人工智能·计算机视觉·第三方美颜sdk·人脸美型sdk
猛扇赵四那边好嘴.4 小时前
Flutter 框架跨平台鸿蒙开发 - 问答社区应用开发教程
开发语言·javascript·flutter·华为·harmonyos
LawrenceLan4 小时前
Flutter 零基础入门(二十二):Text 文本组件与样式系统
开发语言·前端·flutter·dart
PwnGuo4 小时前
Android逆向:在 Unidbg 中解决 native 函数内调用 Java 方法的报错
android·java·python
Kratzdisteln5 小时前
【1902】优化后的三路径学习系统
android·学习
kirk_wang5 小时前
Flutter艺术探索-Flutter性能优化基础:const与const构造函数
flutter·移动开发·flutter教程·移动开发教程