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
                )
            }
        }
    }
相关推荐
ujainu1 天前
Flutter + OpenHarmony 游戏开发进阶:用户输入响应——GestureDetector 实现点击发射
flutter·游戏·openharmony
Doro再努力1 天前
【Linux操作系统10】Makefile深度解析:从依赖推导到有效编译
android·linux·运维·服务器·编辑器·vim
Daniel李华1 天前
echarts使用案例
android·javascript·echarts
hudawei9961 天前
TweenAnimationBuilder和AnimatedBuilder两种动画的比较
flutter·ui·动画·tweenanimation·animatedbuilder
ujainu1 天前
Flutter + OpenHarmony 实现无限跑酷游戏开发实战—— 对象池化、性能优化与流畅控制
flutter·游戏·性能优化·openharmony·endless runner
做人不要太理性1 天前
CANN Runtime 运行时组件深度解析:任务调度机制、存储管理策略与维测体系构建逻辑
android·运维·魔珐星云
我命由我123451 天前
Android 广播 - 静态注册与动态注册对广播接收器实例创建的影响
android·java·开发语言·java-ee·android studio·android-studio·android runtime
ZH15455891311 天前
Flutter for OpenHarmony Python学习助手实战:自动化脚本开发的实现
python·学习·flutter
朗迹 - 张伟1 天前
Tauri2 导出 Android 详细教程
android
lpruoyu1 天前
【Android第一行代码学习笔记】Android架构_四大组件_权限_持久化_通知_异步_服务
android·笔记·学习