MiniCPM-V 多模态模型 Android APP 集成指南
MiniCPM-V 多模态模型 Android APP 集成指南
本教程主要讲述的是如何将 MiniCPM-V 系列多模态模型(图文/视频理解)集成到你自己的 Android App,原始项目地址:https://github.com/OpenBMB/MiniCPM-V-Apps
1. 前置条件
为了避免出现其他兼容性问题建议与原始项目MiniCPM-V-demo-Android中的版本保持一致
| 项目 | 要求 |
|---|---|
| Android Studio | 2024.1+(支持 AGP 8.9) |
| AGP | 8.9.1 |
| Gradle | 8.11.1 |
| NDK | 27.0.12077973 |
| CMake | 3.22.1 |
| Kotlin | 2.0.21 |
| minSdk | 24 |
| 目标 ABI | arm64-v8a(仅支持 64 位 ARM) |
| 磁盘空间 | ≥ 10GB(llama.cpp 编译 + 模型文件) |
| Java | 11 |
2. 文件清单
必须复制
Kotlin层
LlamaEngine.kt:引擎单例,封装全部 JNI 调用ModelInfo.kt:模型元数据(下载源、文件名、MD5校验等)ModelDownloadService.kt:模型下载服务CpuFeatures.kt:CPU 特性检测(选优化的 .so)
JNI层
CMakeLists.txt:CMake 配置llama_jni.cpp:JNI 桥接logging.h:日志
可选文件
MarkdownEscape.kt:V4.6 输出的\n转真换行(显示优化)VideoFrameExtractor.kt:视频理解功能,只有MiniCPM-V 4.6模型支持
3. 步骤一:搭建 Native 层
3.1 添加 llama.cpp 子模块
在项目根目录:
cd /path/to/your/project
git submodule add -b Support-iOS-Demo https://github.com/tc-mb/llama.cpp.git llama.cpp
cd llama.cpp
⚠️ 重要:必须使用
Support-iOS-Demo分支,它包含 MiniCPM-V 的 clip.cpp 兼容代码,主线分支的 mmproj 转换格式不兼容。
3.2 复制 C++ 文件
把 llama_jni.cpp、logging.h、CMakeLists.txt 复制到你项目的 app/src/main/cpp/ 下。
3.3 修改 CMakeLists.txt 中的 llama.cpp 路径
打开 CMakeLists.txt,修改第 38-39 行的 LLAMA_SRC 路径:
# 默认在项目4级目录上找 llama.cpp
if(NOT DEFINED LLAMA_SRC)
set(LLAMA_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../../../llama.cpp)
endif()
# 如果你的llama.cpp 在项目根目录下,改成:
if(NOT DEFINED LLAMA_SRC)
set(LLAMA_SRC ${CMAKE_CURRENT_LIST_DIR}/../../../../llama.cpp)
endif()
路径关系:app/src/main/cpp/ → 上 4 级到项目根 → llama.cpp/
3.4 修改 JNI 函数名
llama_jni.cpp 中的 JNI 函数名基于包名 com.example.minicpm_v_demo,格式为:
Java_包名_类名_方法名(. → _,_ → _1)
当前函数名示例:
Java_com_example_minicpm_1v_1demo_LlamaEngine_load
如果你的包名是 com.yourapp.ai,需要把所有 JNI 函数前缀改为:
Java_com_yourapp_ai_LlamaEngine_load
完整替换列表(在 llama_jni.cpp 中搜索替换):
| 原始前缀 | 新前缀 |
|---|---|
Java_com_example_minicpm_1v_1demo_LlamaEngine_ |
Java_com_yourapp_ai_LlamaEngine_ |
4. 步骤二:配置 Gradle 构建
4.1 gradle/libs.versions.toml
[versions]
agp = "8.9.0"
kotlin = "2.0.21"
coreKtx = "1.10.1"
appcompat = "1.6.1"
material = "1.10.0"
constraintlayout = "2.1.4"
lifecycleRuntimeKtx = "2.6.2"
activityKtx = "1.8.0"
[libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
androidx-appcompat = { group = "androidx.appcompat", name = "appcompat", version.ref = "appcompat" }
material = { group = "com.google.android.material", name = "material", version.ref = "material" }
androidx-constraintlayout = { group = "androidx.constraintlayout", name = "constraintlayout", version.ref = "constraintlayout" }
androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" }
androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activityKtx" }
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
4.2 gradle/wrapper/gradle-wrapper.properties
distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
4.3 app/build.gradle.kts
plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.android)
alias(libs.plugins.kotlin.compose)
}
android {
namespace = "com.xxx.xxx" //改为自己的项目包路径
compileSdk = 36
ndkVersion = "27.0.12077973"
defaultConfig {
applicationId = "com.xxx.xxx"
minSdk = 24
targetSdk = 36
versionCode = 1
versionName = "1.0"
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
ndk {
abiFilters.add("arm64-v8a") // 只构建 ARM64
}
//cmake相关的配置
externalNativeBuild {
cmake {
arguments += "-DCMAKE_BUILD_TYPE=Release"
arguments += "-DBUILD_SHARED_LIBS=ON"
arguments += "-DLLAMA_BUILD_COMMON=ON"
arguments += "-DLLAMA_OPENSSL=OFF"
arguments += "-DGGML_NATIVE=OFF"
arguments += "-DGGML_LLAMAFILE=ON"
arguments += "-DGGML_CPU_ARM_ARCH=armv8.2-a+dotprod+fp16"
}
}
}
buildTypes {
release {
isMinifyEnabled = false
proguardFiles(
getDefaultProguardFile("proguard-android-optimize.txt"),
"proguard-rules.pro"
)
}
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_11
targetCompatibility = JavaVersion.VERSION_11
}
kotlinOptions {
jvmTarget = "11"
}
externalNativeBuild {
cmake {
path = file("src/main/cpp/CMakeLists.txt")
version = "3.22.1"
}
}
buildFeatures {
compose = true
}
}
dependencies {
implementation(libs.androidx.core.ktx)
implementation(libs.androidx.lifecycle.runtime.ktx)
implementation(libs.androidx.activity.compose)
implementation(platform(libs.androidx.compose.bom))
implementation(libs.androidx.ui)
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.androidx.material.icons.extended)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
androidTestImplementation(platform(libs.androidx.compose.bom))
androidTestImplementation(libs.androidx.ui.test.junit4)
debugImplementation(libs.androidx.ui.tooling)
debugImplementation(libs.androidx.ui.test.manifest)
}
5. 步骤三:复制 Kotlin 核心文件
5.1 必须文件
把以上提到的必须复制文件,复制到你的包名目录下,并修改 package 声明。
5.2 LlamaEngine.kt 的 JNI 声明
如果你的包名改变了,需要同步修改 LlamaEngine.kt 中的 external 函数声明。Kotlin 的 external 函数名必须和 C++ 端一致,不改 Kotlin 声明,改 C++ 端的函数名来匹配。
6. 步骤四:配置 AndroidManifest
<manifest ...>
<!-- 网络权限(下载模型必须) -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<!-- 以下权限仅在集成 ModelDownloadService 时需要 -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<application
android:networkSecurityConfig="@xml/network_security_config"
...>
<!-- 如果使用 ModelDownloadService -->
<service
android:name=".ModelDownloadService"
android:exported="false"
android:foregroundServiceType="dataSync" />
</application>
</manifest>
network_security_config.xml
<?xml version="1.0" encoding="utf-8"?>
<network-security-config>
<base-config cleartextTrafficPermitted="true" />
</network-security-config>
7. 步骤五:模型文件管理
7.1 模型存储结构
/data/data/com.yourapp.ai/files/models/
├── minicpm-v-4/ # ModelInfo.id
│ ├── ggml-model-Q4_K_M.gguf # LLM 主模型
│ └── mmproj-model-f16.gguf # 视觉投影模型
└── minicpm-v-4_6-instruct/ # 另一个模型
├── MiniCPM-V-4_6-Q4_K_M.gguf
└── mmproj-model-merger-f16.gguf
7.2 下载源
| 模型 | 大小 | 下载策略 |
|---|---|---|
| MiniCPM-V-4 (Q4_K_M) | ~4.1B | HuggingFace → ModelScope 回退 |
| MiniCPM-V-4.6 (Q4_K_M) | ~1.2B | 华为云 OBS 直链 |
7.3 下载方式
方式 A:App 内下载(推荐)
集成 ModelDownloadService.kt,用户点击下载按钮即可,支持断点续传和 MD5 校验。
//启动下载
ModelDownloadService.start(context)
//下载状态
lifecycleScope.launch {
ModelDownloadController.status.collect { status ->
when (status) {
is ModelDownloadController.Status.Running -> showProgress(status.message)
is ModelDownloadController.Status.Completed -> onDownloadComplete()
is ModelDownloadController.Status.Failed -> showError(status.message)
else -> {}
}
}
}
方式 B:adb 侧载
#推送模型文件到设备 测试时可以这样
adb push ggml-model-Q4_K_M.gguf /data/data/com.yourapp.ai/files/models/minicpm-v-4/
adb push mmproj-model-f16.gguf /data/data/com.yourapp.ai/files/models/minicpm-v-4/
8. 步骤六:在 App 中使用
// 初始化引擎 条件加载模型 这里建议放到MainActivity的onCreate中 在启动的时候自动加载模型
val engine = LlamaEngine.getInstance(this)
if (LlamaEngine.modelsExist(this)) {
lifecycleScope.launch {
engine.state.collect { state ->
if (state is LlamaState.Initialized) {
Log.i("ModelLoad", "模型文件已就绪,开始加载...")
try {
engine.loadModel(
LlamaEngine.modelPath(this@MainActivity),
LlamaEngine.mmprojPath(this@MainActivity)
)
Log.i("ModelLoad", "模型加载成功!")
} catch (e: Exception) {
Log.e("ModelLoad", "模型加载失败", e)
}
return@collect // 只触发一次
}
}
}
}
9. API 表
LlamaEngine 公开 API
| 方法 | 简要说明 |
|---|---|
getInstance(context) |
获取单例 |
loadModel(path, mmprojPath?) |
加载模型+视觉模型 |
setSystemPrompt(prompt) |
设置系统提示词 |
sendUserPrompt(msg, predictLen) |
发送用户消息 |
prefillImage(imageData) |
预填充图片 |
prefillVideoFrames(frames, onProgress) |
预填充视频帧 |
clearContext() |
清空上下文 |
unloadModel() |
卸载模型 |
cancelGeneration() |
取消当前生成 |
destroy() |
销毁引擎 |
setImageMaxSliceNums(n) |
设置图片切片数 |
state |
引擎状态流 |
isVisionSupported |
是否支持图片 |
isVideoUnderstandingSupported |
是否支持视频 |
LlamaEngine 静态 API
| 方法 | 简要说明 |
|---|---|
getSelectedModel(context) |
获取当前选择的模型 |
setSelectedModel(context, id) |
选择模型 |
modelPath(context) |
GGUF 文件路径 |
mmprojPath(context) |
mmproj 文件路径 |
modelsExist(context) |
模型文件是否都在 |
modelDir(context) |
模型根目录 |
modelDirFor(context, model) |
某模型目录 |
getImageMaxSliceNums(context) |
图片切片数 |
setImageMaxSliceNumsPref(context, n) |
持久化切片数 |
downloadModels(context, onProgress) |
下载模型文件 |
migrateLegacyLayoutIfNeeded(context) |
迁移旧文件布局 |
9.1 自定义推理参数
在 llama_jni.cpp 中修改
constexpr int N_THREADS = 4; // 推理 线程数影响推理速度 亲测 这里可以稍微调大
constexpr int DEFAULT_CONTEXT_SIZE = 4096; // KV cache 大小
constexpr int V46_CONTEXT_SIZE = 8192; // V4.6 的 KV cache
constexpr int BATCH_SIZE = 2048; // 批处理大小 这里也可以稍微调大一些
constexpr float DEFAULT_SAMPLER_TEMP = 0.7f; // 温度
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)