类RN框架通过Service暴露卡片渲染能力给AI Chat

第一性:Android 组件模型的根本矛盾

Android 四大组件都面临同一个问题:

应用是沙箱隔离的,但用户期望的功能往往需要跨应用协作。

矛盾 表现
进程隔离是强制的 每个 App 独立进程,内存不共享
用户功能需要跨应用 音乐播放、后台下载、卡片渲染......
系统需要统一调度 内存不足时,系统要知道哪些后台任务可以杀
应用需要生命周期管理 页面退了,但任务不能停

Activity 解决不了的问题:

  • Activity 绑定到 UI 线程,页面销毁后任务就断了
  • Activity 不能被其他应用直接启动和绑定
  • 后台任务(如播放音乐)不应该有可见界面

Service 的本质:无 UI 的、可跨进程调用的、受系统生命周期管理的后台组件


本质:后台任务载体 + 跨进程能力暴露点

Service 的核心设计:

一个没有用户界面的组件,运行在宿主进程的主线程中,可以被系统启动、绑定、管理生命周期,同时通过 Binder 向其他应用暴露能力。

css 复制代码
┌─────────────────────────────────────────┐
│              应用 A(宿主)                │
│  ┌─────────────────────────────────┐   │
│  │         Service 组件             │   │
│  │  ┌─────────┐  ┌─────────┐      │   │
│  │  │ 后台任务 │  │ Binder  │◄─────┼───┼── 其他应用通过 AIDL 调用
│  │  │ 线程    │  │ 接口暴露 │      │   │
│  │  └─────────┘  └─────────┘      │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘
         ▲
         │ 系统管理(AMS)
         ▼
    启动、绑定、解绑、销毁
    内存紧张时回收优先级

解决的问题

问题 Service 如何解决
后台任务生命周期 页面(Activity)销毁后,Service 可以继续运行
跨应用能力暴露 通过 AIDL/Binder,其他应用可以调用你的功能
系统统一调度 Service 有明确的优先级,系统知道什么时候可以杀
无 UI 的纯逻辑 不需要界面,专注做计算、网络、播放等

核心场景

1. 后台音乐播放(最经典)

arduino 复制代码
音乐 App
    │
    ▼ 用户点击播放后退到桌面
Activity 销毁
    │
    ▼ 但音乐不能停
Service 继续运行
    │
    ▼ 系统知道"有前台 Service 在播放"
通知栏显示播放控制,系统不轻易杀这个进程

2. 跨应用能力暴露(快应用框架场景)

scss 复制代码
┌─────────────────────────────────────────┐
│           快应用框架(你的应用)            │
│  ┌─────────────────────────────────┐   │
│  │      QuickAppService            │   │
│  │  ┌─────────────────────────┐   │   │
│  │  │  AIDL 接口:IQuickAppRenderer │   │
│  │  │  • renderCard(String json)   │   │
│  │  │  • getCardHeight()           │   │
│  │  │  • handleClick(int x, int y) │   │
│  │  └─────────────────────────┘   │   │
│  │         ▲                       │   │
│  │         │ Binder                │   │
│  │  ┌──────┴──────┐               │   │
│  │  │ 渲染引擎线程 │               │   │
│  │  │ (Skia/自研) │               │   │
│  │  └─────────────┘               │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘
                    ▲
                    │ AIDL 调用
                    │
┌─────────────────────────────────────────┐
│  小爱同学(第三方应用, 好比说AI chat)      │
│  ┌─────────────────────────────────┐   │
│  │         Chat 流 UI               │   │
│  │  ┌─────────────────────────┐   │   │
│  │  │  消息气泡区域            │   │   │
│  │  │  ┌─────────────────┐   │   │   │
│  │  │  │  快应用卡片      │   │   │   │
│  │  │  │  (由你的 Service │   │   │   │
│  │  │  │   远程渲染)      │   │   │   │
│  │  │  └─────────────────┘   │   │   │
│  │  └─────────────────────────┘   │   │
│  └─────────────────────────────────┘   │
└─────────────────────────────────────────┘

为什么这个场景必须用 Service:

原因 说明
跨进程 小爱同学和快应用框架是两个独立 App,不同进程
无 UI 侵入 快应用框架不需要在小爱同学的进程中显示自己的 Activity
能力复用 一套渲染引擎,多个宿主应用复用
安全隔离 快应用的代码运行在框架自己的进程中,不污染宿主

类RN框架(如快应用框架)

核心设计

采用 Binder IPC + 系统预装 Service(快应用框架) + Surface 共享渲染

  • S 端 :快应用框架作为系统级 Service,开机预加载,常驻系统进程
  • C 端:封装为 AAR SDK,宿主(如小爱同学)通过 SDK 绑定调用
  • 渲染结果 :通过 Surface 共享 嵌入宿主 UI

AIDL 接口定义

aidl 复制代码
// IQuickAppRenderer.aidl
// 第一性:定义跨进程边界的能力契约
// Binder 是 Android IPC 的基石,AIDL 是 Binder 的接口描述语言
interface IQuickAppRenderer {
    // 创建卡片,返回 token 用于后续操作
    IBinder createCard(in Bundle params, in IBinder hostSurface);
    
    // 更新卡片数据
    void updateData(IBinder cardToken, in Bundle data);
    
    // 转发触摸事件
    void dispatchTouchEvent(IBinder cardToken, in MotionEvent event);
    
    // 获取卡片期望尺寸
    int getCardHeight(in Bundle params);
    
    // 销毁卡片
    void destroyCard(IBinder cardToken);
}

Java 实现版

Service 端

java 复制代码
public class QuickAppService extends Service {
    
    // 第一性:Service 是后台组件,运行在主线程
    // 耗时操作必须放到子线程,避免 ANR
    private final ExecutorService renderExecutor = Executors.newSingleThreadExecutor();
    
    // 第一性:Binder 是 Android IPC 的核心
    // 通过 Stub 实现 AIDL 接口,暴露跨进程能力
    private final IQuickAppRenderer.Stub binder = new IQuickAppRenderer.Stub() {
        
        @Override
        public IBinder createCard(Bundle params, IBinder hostSurface) throws RemoteException {
            // 第一性:跨进程调用时,Binder.getCallingUid() 获取调用方身份
            // 这是 Android 安全模型的基础:基于 UID 的权限隔离
            int uid = Binder.getCallingUid();
            if (!isAuthorized(uid)) {
                throw new SecurityException("Unauthorized uid: " + uid);
            }
            
            String appId = params.getString("app_id");
            if (appId == null) {
                throw new IllegalArgumentException("app_id required");
            }
            
            // 第一性:Surface 是跨进程共享图形缓冲区的句柄
            // 通过 IBinder 传递 Surface,实现宿主 UI 中嵌入远程渲染内容
            Surface surface = new Surface(hostSurface);
            
            // 在独立线程中初始化渲染引擎
            // 避免阻塞 Binder 线程(Binder 线程池有限,阻塞会导致 IPC 卡顿)
            Future<CardInstance> future = renderExecutor.submit(() -> {
                QuickAppInstance instance = engine.loadApp(appId);
                RenderTarget target = new RenderTarget(surface);
                instance.attachToSurface(target);
                return new CardInstance(instance, target);
            });
            
            try {
                CardInstance card = future.get();
                activeCards.put(card.token, card);
                return card.token;
            } catch (Exception e) {
                throw new RuntimeException("Failed to create card", e);
            }
        }
        
        @Override
        public void dispatchTouchEvent(IBinder cardToken, MotionEvent event) throws RemoteException {
            // 第一性:触摸事件从宿主进程传递到 Service 进程
            // 事件坐标系需要转换,因为宿主和 Service 的窗口坐标可能不同
            CardInstance card = activeCards.get(cardToken);
            if (card == null) return;
            
            card.instance.dispatchTouch(event.getX(), event.getY(), event.getAction());
        }
        
        @Override
        public void destroyCard(IBinder cardToken) throws RemoteException {
            // 第一性:资源必须显式释放
            // Android 没有自动 GC 跨进程资源,不释放会导致 Surface 泄漏
            CardInstance card = activeCards.remove(cardToken);
            if (card != null) {
                card.instance.destroy();
                card.target.release();
                // 第一性:Surface 释放后,宿主窗口会显示空白或黑屏
                // 需要宿主在销毁前做好过渡处理
            }
        }
        
        // ... 其他方法
    };
    
    @Override
    public IBinder onBind(Intent intent) {
        // 第一性:onBind 是 Service 对外暴露能力的入口
        // 返回的 IBinder 是 Binder 代理的实体端(Stub)
        return binder;
    }
    
    @Override
    public void onDestroy() {
        // 第一性:Service 生命周期由系统管理
        // 被杀时必须清理资源,避免内存泄漏
        super.onDestroy();
        renderExecutor.shutdown();
        for (CardInstance card : activeCards.values()) {
            card.instance.destroy();
            card.target.release();
        }
        activeCards.clear();
    }
}

宿主端(Java)

java 复制代码
public class QuickAppCardView extends FrameLayout {
    
    private IQuickAppRenderer renderer;
    private IBinder cardToken;
    private SurfaceView surfaceView;
    
    // 第一性:ServiceConnection 是绑定 Service 的回调契约
    // 连接建立后,才能进行跨进程调用
    private final ServiceConnection connection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            // 第一性:asInterface 将 IBinder 转为 AIDL 接口代理
            // 后续调用会通过 Binder 驱动发送到 Service 进程
            renderer = IQuickAppRenderer.Stub.asInterface(service);
            renderCard();
        }
        
        @Override
        public void onServiceDisconnected(ComponentName name) {
            // 第一性:Service 被杀或崩溃时回调
            // 必须处理重连或降级,否则卡片会卡死
            renderer = null;
            cardToken = null;
        }
    };
    
    public void attach(String appId) {
        Intent intent = new Intent();
        intent.setComponent(new ComponentName(
            "com.quickapp.framework",
            "com.quickapp.framework.QuickAppService"
        ));
        
        // 第一性:bindService 是异步的
        // 连接建立前不能调用 renderer,必须通过 ServiceConnection 回调
        getContext().bindService(intent, connection, Context.BIND_AUTO_CREATE);
    }
    
    private void renderCard() {
        if (renderer == null) return;
        
        Bundle params = new Bundle();
        params.putString("app_id", "weather_card");
        
        try {
            // 第一性:AIDL 调用是阻塞的(同步 IPC)
            // 必须在子线程执行,避免阻塞主线程
            int height = renderer.getCardHeight(params);
            surfaceView.setLayoutParams(new LayoutParams(MATCH_PARENT, height));
            
            // 第一性:Surface 是跨进程共享的图形缓冲区
            // 创建后,Service 会在自己的进程中渲染内容到这块缓冲区
            // 宿主窗口会自动显示最新内容(通过 SurfaceFlinger 合成)
            cardToken = renderer.createCard(params, surfaceView.getHolder().getSurface());
            
            // 转发触摸事件
            surfaceView.setOnTouchListener((v, event) -> {
                try {
                    renderer.dispatchTouchEvent(cardToken, event);
                } catch (RemoteException e) {
                    // 第一性:跨进程调用可能失败(Service 被杀、进程崩溃)
                    // 必须处理异常,避免宿主崩溃
                    e.printStackTrace();
                }
                return true;
            });
            
        } catch (RemoteException e) {
            e.printStackTrace();
        }
    }
    
    public void detach() {
        if (cardToken != null && renderer != null) {
            try {
                renderer.destroyCard(cardToken);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }
        getContext().unbindService(connection);
    }
}

Kotlin 实现版

Service 端(Kotlin)

kotlin 复制代码
class QuickAppService : Service() {

    // 第一性:Service 运行在主线程,耗时操作必须放到子线程
    // Kotlin 使用单线程线程池,保证渲染顺序性
    private val renderExecutor = Executors.newSingleThreadExecutor()
    
    private val engine = QuickAppEngine()
    
    // 第一性:维护活跃卡片映射,token -> 卡片实例
    // 跨进程资源必须显式管理,Android 不会自动 GC 跨进程对象
    private val activeCards = mutableMapOf<IBinder, CardInstance>()

    // 第一性:Binder Stub 是 AIDL 接口的实体实现
    // 所有跨进程调用最终都会落到这里执行
    private val binder = object : IQuickAppRenderer.Stub() {

        override fun createCard(params: Bundle?, hostSurface: IBinder?): IBinder {
            // 第一性:跨进程安全校验
            // Binder.getCallingUid() 获取调用方身份,这是 Android 沙箱的基石
            val uid = Binder.getCallingUid()
            require(isAuthorized(uid)) { "Unauthorized uid: $uid" }

            val appId = params?.getString("app_id")
                ?: throw IllegalArgumentException("app_id required")

            // 第一性:Surface 是跨进程共享图形缓冲区的句柄
            // 通过 IBinder 传递,实现"远程渲染、本地显示"
            val surface = Surface(hostSurface)

            // 第一性:Binder 调用运行在 Binder 线程池
            // 阻塞操作会占用 Binder 线程,导致其他 IPC 卡顿
            // 所以渲染初始化放到独立线程
            val future = renderExecutor.submit<CardInstance> {
                val instance = engine.loadApp(appId)
                val target = RenderTarget(surface)
                instance.attachToSurface(target)
                CardInstance(instance, target)
            }

            return future.get().also { card ->
                activeCards[card.token] = card
            }
        }

        override fun dispatchTouchEvent(cardToken: IBinder?, event: MotionEvent?) {
            // 第一性:Kotlin 的空安全语法 ?: return
            // 避免空指针,比 Java 的 if (x == null) return 更简洁
            val token = cardToken ?: return
            val motionEvent = event ?: return

            activeCards[token]?.instance?.dispatchTouch(
                x = motionEvent.x,
                y = motionEvent.y,
                action = motionEvent.action
            )
        }

        override fun destroyCard(cardToken: IBinder?) {
            val token = cardToken ?: return
            
            // 第一性:remove 返回被移除的值,避免二次查找
            activeCards.remove(token)?.let { card ->
                // 第一性:资源必须显式释放
                // Surface 泄漏会导致 GPU 内存持续增长,最终 OOM
                card.instance.destroy()
                card.target.release()
            }
        }

        // ... 其他方法
    }

    override fun onBind(intent: Intent): IBinder {
        // 第一性:onBind 是 Service 对外暴露能力的唯一入口
        // 返回的 IBinder 会被系统封装成 BinderProxy 传递给客户端
        return binder
    }

    override fun onDestroy() {
        // 第一性:Service 生命周期由系统管理(AMS 调度)
        // 被杀时必须清理资源,否则就是内存泄漏
        super.onDestroy()
        renderExecutor.shutdown()
        activeCards.values.forEach { card ->
            card.instance.destroy()
            card.target.release()
        }
        activeCards.clear()
    }

    data class CardInstance(
        val instance: QuickAppInstance,
        val target: RenderTarget,
        val token: IBinder = target.token
    )
}

宿主端(Kotlin + Coroutine)

kotlin 复制代码
class QuickAppCardView @JvmOverloads constructor(
    context: Context,
    attrs: AttributeSet? = null
) : FrameLayout(context, attrs) {

    // 第一性:AIDL 接口代理,所有调用都会通过 Binder 驱动跨进程
    private var renderer: IQuickAppRenderer? = null
    private var cardToken: IBinder? = null
    private val surfaceView = SurfaceView(context)

    // 第一性:ServiceConnection 是异步绑定回调
    // bindService 是异步的,连接建立前 renderer 为 null
    private val connection = object : ServiceConnection {
        override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
            // 第一性:asInterface 将底层 IBinder 转为类型化代理
            // 后续调用看起来像本地方法,实际是跨进程 IPC
            renderer = IQuickAppRenderer.Stub.asInterface(service)
            renderCard()
        }

        override fun onServiceDisconnected(name: ComponentName?) {
            // 第一性:Service 进程被杀或崩溃时回调
            // 必须处理,否则后续调用会 Crash
            renderer = null
            cardToken = null
        }
    }

    init {
        addView(surfaceView)
    }

    fun attach(appId: String) {
        val intent = Intent().apply {
            component = ComponentName(
                "com.quickapp.framework",
                "com.quickapp.framework.QuickAppService"
            )
        }
        
        // 第一性:BIND_AUTO_CREATE 表示如果 Service 未运行则自动创建
        // 这是 Android 组件化设计的体现:按需启动,用完释放
        context.bindService(intent, connection, Context.BIND_AUTO_CREATE)
    }

    private fun renderCard() {
        val renderer = this.renderer ?: return
        val surface = surfaceView.holder.surface

        val params = Bundle().apply {
            putString("app_id", "weather_card")
        }

        // 第一性:AIDL 调用是同步阻塞的 IPC
        // Kotlin 协程 + withContext(Dispatchers.IO) 避免阻塞主线程
        lifecycleScope.launch {
            // 获取卡片高度
            val height = withContext(Dispatchers.IO) {
                renderer.getCardHeight(params)
            }

            surfaceView.layoutParams = LayoutParams(
                LayoutParams.MATCH_PARENT,
                height
            )

            // 创建卡片
            // 第一性:Surface 跨进程共享后,Service 在自己的进程渲染
            // 内容通过 SurfaceFlinger 合成到宿主窗口,宿主无感知
            cardToken = withContext(Dispatchers.IO) {
                renderer.createCard(params, surface)
            }

            // 转发触摸事件
            surfaceView.setOnTouchListener { _, event ->
                // 第一性:触摸事件从宿主 UI 线程传递到 Service 进程
                // 异步发送,避免阻塞宿主渲染
                lifecycleScope.launch(Dispatchers.IO) {
                    try {
                        cardToken?.let { token ->
                            renderer.dispatchTouchEvent(token, event)
                        }
                    } catch (e: RemoteException) {
                        // 第一性:跨进程调用随时可能失败
                        // Service 被杀、进程崩溃、Binder 驱动异常
                        // 必须捕获,否则宿主 App 会跟着崩溃
                        Log.e("QuickApp", "IPC failed", e)
                    }
                }
                true
            }
        }
    }

    fun detach() {
        // 第一性:资源释放顺序很重要
        // 先通知 Service 销毁卡片,再解绑 Service
        // 否则 Service 可能泄露 Surface
        cardToken?.let { token ->
            try {
                renderer?.destroyCard(token)
            } catch (e: RemoteException) {
                Log.e("QuickApp", "Destroy failed", e)
            }
        }
        
        // 第一性:unbindService 后,如果没有任何客户端绑定
        // 且 Service 不是前台 Service,系统会在空闲时销毁它
        context.unbindService(connection)
        renderer = null
        cardToken = null
    }
}

Service 的能力边界与局限性

能力 说明 你的场景
后台运行 无 UI 持续执行任务 渲染引擎常驻,响应多个宿主
跨进程调用 通过 AIDL/Binder 暴露接口 小爱同学调用快应用渲染
共享 Surface 通过图形缓冲区跨进程显示 远程渲染,本地嵌入
生命周期管理 系统知道你在运行,内存紧张时通知 框架被杀前保存状态
前台保活 startForeground() 显示通知 如果框架需要常驻
局限性 说明 解决方案
主线程阻塞 Service 默认在主线程 独立 RenderThread(已做)
内存占用 框架进程常驻 按需加载/卸载实例
启动延迟 bindService 建立连接有延迟 预绑定、缓存 Binder 代理
版本兼容 Android 8+ 后台启动限制 前台 Service 或 JobScheduler
安全隔离 恶意应用可能绑定 权限校验、签名验证

一句话总结

Service 的本质是:无 UI 的、受系统管理的、可跨进程暴露能力的后台组件。它解决了"应用沙箱隔离"和"跨应用协作"的根本矛盾。在你的快应用框架场景中,Service 是渲染能力的"远程出口"------通过 AIDL 暴露渲染接口,让宿主应用在自己的 UI 中嵌入你的卡片,同时保持进程隔离和安全边界。

相关推荐
灯火不休1 小时前
Android Studio & Flutter 构建命令完全指南
android
杉氧1 小时前
Ktor 全栈之路 (5):JWT 认证全流程实战 —— 打造安全通信闭环
android·架构·kotlin
️学习的小王2 小时前
Python + MySQL 学生信息管理系统实战教程
android·python·mysql
帅次2 小时前
Android 高级工程师面试:Flutter 路由导航 近1年高频追问 18 题
android·flutter·面试
alexhilton10 小时前
MVI模式的完整历史、误解和现代Android范式
android·kotlin·android jetpack
想你依然心痛11 小时前
【共创季稿事节】HarmonyOS 7.0 应用框架(ArkUI-X)跨平台能力深度探索
flutter·react native·arkui-x·跨平台渲染·harmonyos 7.0·arkrender·组件兼容性
心中有国也有家16 小时前
AtomGit Flutter 鸿蒙客户端:Canvas 绘制进阶-路径、渐变与混合模式
android·javascript·flutter·华为·harmonyos
hunterandroid17 小时前
WorkManager 可靠性实战:唯一任务、重试与幂等设计
android·前端
程序员老刘17 小时前
Android 17的10个AI功能,国内用户真正能用的有几个?
android·flutter·ai编程