Android SystemServer 系列专题【篇五:SystemConfig系统功能配置】

本篇主要针对SystemConfig的流程进行一个梳理,SystemConfig翻译过来就是系统配置,对应于xxx/etc/目录下面的一些配置。systemserver进程在启动的时候从etc目录下读取这些配置文件,加载到系统中,供fw+app层进行调用判断对应的功能是否启用。

一、ETC系统配置加载

1、SystemServer初始化

参考https://blog.csdn.net/qq_27672101/article/details/136746558可以了解到,SystemServer进程在启动看门狗之后,就开始进行系统配置文件的解析:

参考https://blog.csdn.net/qq_27672101/article/details/150857253可以了解到,SystemServer进程通过线程池优先启动SystemConfig,然后在启动其他核心服务,为什么要优先启动呢?因为需要优先加载etc/目录下的配置文件。关键日志如下:

2、SystemConfig初始化

如上SystemServer通过线程池的方式SystemConfig::getInstance,这是一个典型的单例设计模式,这个过程直接会触发SystemConfig的构造函数调用:

java 复制代码
//la.qssi16/frameworks/base/services/core/java/com/android/server/SystemConfig.java
public class SystemConfig {
    static final String TAG = "SystemConfig";
    static SystemConfig sInstance;
    private final Injector mInjector;
    // These are the built-in shared libraries that were read from the
    // system configuration files. Keys are the library names; values are
    // the individual entries that contain information such as filename
    // and dependencies.
    final ArrayMap<String, SharedLibraryEntry> mSharedLibraries = new ArrayMap<>();
    // These are the features this devices supports that were read from the
    // system configuration files.
    final ArrayMap<String, FeatureInfo> mAvailableFeatures;
    // These are the features which this device doesn't support; the OEM
    // partition uses these to opt-out of features from the system image.
    final ArraySet<String> mUnavailableFeatures = new ArraySet<>();
    // These are the built-in uid -> permission mappings that were read from the
    // system configuration files.
    final SparseArray<ArraySet<String>> mSystemPermissions = new SparseArray<>();
    final ArrayList<SplitPermissionInfo> mSplitPermissions = new ArrayList<>();
    public static SystemConfig getInstance() {
        if (!isSystemProcess()) Slog.wtf(TAG, "SystemConfig is being accessed by a process other than " + "system_server.");
        synchronized (SystemConfig.class) {
            #步骤一:单例模式实例化SystemConfig
            if (sInstance == null)   sInstance = new SystemConfig();
            return sInstance;
        }
    }
    SystemConfig() {
        mInjector = new Injector();
        #步骤二:mAvailableFeatures存储了系统配置了的FEATURES
        mAvailableFeatures = mInjector.getReadOnlySystemEnabledFeatures();
        TimingsTraceLog log = new TimingsTraceLog(TAG, Trace.TRACE_TAG_SYSTEM_SERVER);
        log.traceBegin("readAllPermissions");
        try {
            //步骤三:读取解析所有etc目录下的配置文件
            readAllPermissions();
            //步骤四:读取解析公共库列表
            readPublicNativeLibrariesList();
        } finally {
            log.traceEnd();
        }
    }
}

如上最核心的流程还是如下两个步骤:

  • readAllPermissions:读取加载etc/sysconfig和etc/permissions所有配置
  • readPublicNativeLibrariesList:读取加载public.libraries.txt配置的公共库

3、readPublicNativeLibrariesList

readPublicNativeLibrariesList加载公共库相对来说比较简单,这里先理一下它的流程:

如上函数总结如下:

  • 先读取/vendor/etc/public.libraries.txt文件内容,并加载库名字到mSharedLibraries
  • 再依次遍历/system/etc和/system_ext/etc和/product/etc/目录
  • 找到类似public.libraries-?.txt文件,读取并加载库名字到mSharedLibraries

1)public.libraries-xxx.txt长什么样子?

我手上这台机器是高通平台A16的系统,cat出来内容如下:

  • /vendor/etc/public.libraries.txt

SH4-2:/vendor/etc $ cat public.libraries.txt

libqti-perfd-client.so

libadsprpc.so

libcdsprpc.so

libsdsprpc.so

libfastcvopt.so

libOpenCL.so

  • /system/etc/public.libraries.txt

SH4-2:/system/etc $ cat public.libraries.txt

See https://android.googlesource.com/platform/ndk/+/main/docs/PlatformApis.md

libandroid.so

libaaudio.so

libamidi.so

libbinder_ndk.so

libc.so

libcamera2ndk.so

libclang_rt.hwasan-aarch64-android.so 64 nopreload

libdl.so

libEGL.so

libGLESv1_CM.so

libGLESv2.so

libGLESv3.so

libicu.so

libicui18n.so

libicuuc.so

libjnigraphics.so

liblog.so

libmediandk.so

libm.so

libnativehelper.so

libnativewindow.so

libneuralnetworks.so nopreload

libOpenMAXAL.so

libOpenSLES.so

libRS.so

libstdc++.so

libsync.so

libvulkan.so

libwebviewchromium_plat_support.so

libz.so
SH4-2:/system/etc $ cat public.libraries-qti.txt

libbinauralrenderer_wrapper.qti.so

libhoaeffects.qti.so
SH4-2:/system_ext/etc $ cat public.libraries-qti.txt

libupdateprof.qti.so

libthermalclient.qti.so

libdiag_system.qti.so

libqape.qti.so

libqesdk_ndk_platform.qti.so

liblistenjni.qti.so

2)mSharedLibraries.put有什么意义?

建立一个受控的、安全的"系统原生 API 开放接口",让第三方应用可以在不破坏系统安全的前提下,使用 Android 官方支持的底层能力(图形、音频、相机、AI 等)。
这既是 兼容性保障(所有设备都提供相同的公共库集合),也是 安全边界(禁止随意调用内部实现)。

主要如下几个使用场景:

  • 安装时验证 <uses-native-library> 声明
  • 运行时允许 System.loadLibrary("xxx") 成功加载
  • 构建应用的 native library search path
  • 支持多 ABI 和架构隔离

4、readAllPermissions

readAllPermissions的逻辑也比较简单,读取xml并根据xml语法加载相关配置,但是这里的配置就非常的多,如下代码

1)加载所有XML配置文件

java 复制代码
public class SystemConfig {
    static final String TAG = "SystemConfig";
   // property for runtime configuration differentiation
   private static final String SKU_PROPERTY = "ro.boot.product.hardware.sku";
   // property for runtime configuration differentiation in vendor
   private static final String VENDOR_SKU_PROPERTY = "ro.boot.product.vendor.sku";
   // property for runtime configuration differentation in product
   private static final String PRODUCT_SKU_PROPERTY = "ro.boot.hardware.sku";
   // property for runtime configuration differentiation based on baseband type
   private static final String NO_RIL_PROPERTY = "ro.radio.noril";
  // 由SystemServer.java开启线程池调用此方法读取系统配置,主要是etc/sysconfig和etc/permissions目录下的xml文件
  private void readAllPermissionsFromXml() {
       final XmlPullParser parser = Xml.newPullParser();
       // Read configuration from system
       readPermissions(parser, Environment.buildPath( Environment.getRootDirectory(), "etc", "sysconfig"), ALLOW_ALL);
       // Read configuration from the old permissions dir
       readPermissions(parser, Environment.buildPath( Environment.getRootDirectory(), "etc", "permissions"), ALLOW_ALL);
       // Vendors are only allowed to customize these
       int vendorPermissionFlag = ALLOW_LIBS | ALLOW_FEATURES | ALLOW_PRIVAPP_PERMISSIONS  | ALLOW_SIGNATURE_PERMISSIONS | ALLOW_ASSOCIATIONS | ALLOW_VENDOR_APEX;
       if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.O_MR1) {
           // For backward compatibility
           vendorPermissionFlag |= (ALLOW_PERMISSIONS | ALLOW_APP_CONFIGS);
       }
       //核心流程1:读取vendor分区相关配置,例如vendor/etc/sysconfig目录和vendor/etc/permissions目录
       readPermissions(parser, Environment.buildPath( Environment.getVendorDirectory(), "etc", "sysconfig"), vendorPermissionFlag);
       readPermissions(parser, Environment.buildPath( Environment.getVendorDirectory(), "etc", "permissions"), vendorPermissionFlag);
       //核心流程2:读取vendor分区SKU相关配置,如果ro.boot.product.vendor.sku属性值是cell,那么读取的是vendor/etc/sku_cell/sysconfig目录
       String vendorSkuProperty = SystemProperties.get(VENDOR_SKU_PROPERTY, "");
       if (!vendorSkuProperty.isEmpty()) {
           String vendorSkuDir = "sku_" + vendorSkuProperty;
           readPermissions(parser, Environment.buildPath( Environment.getVendorDirectory(), "etc", "sysconfig", vendorSkuDir), vendorPermissionFlag);
           readPermissions(parser, Environment.buildPath( Environment.getVendorDirectory(), "etc", "permissions", vendorSkuDir), vendorPermissionFlag);
       }
       //核心流程3:读取vendor分区noRil相关配置,如果ro.radio.noril属性值是yes,表示当前设备不支持通信功能,那么读取的是vendor/etc/noRil/sysconfig目录
       boolean noRilSupport = SystemProperties.getBoolean(NO_RIL_PROPERTY, false);
       if (noRilSupport) {
           String noRilDir = "noRil";
           readPermissions(parser, Environment.buildPath(  Environment.getVendorDirectory(), "etc", "sysconfig", noRilDir), vendorPermissionFlag);
           readPermissions(parser, Environment.buildPath( Environment.getVendorDirectory(), "etc", "permissions", noRilDir), vendorPermissionFlag);
       }
       //核心流程4:读取odm分区相关配置,是odm/etc/sysconfig目录和odm/etc/permissions目录
       int odmPermissionFlag = vendorPermissionFlag;
       readPermissions(parser, Environment.buildPath( Environment.getOdmDirectory(), "etc", "sysconfig"), odmPermissionFlag);
       readPermissions(parser, Environment.buildPath( Environment.getOdmDirectory(), "etc", "permissions"), odmPermissionFlag);
       //核心流程5:读取odm分区相关配置,如果ro.boot.product.hardware.sku属性值是cell,那么读取的是odm/etc/sku_cell/sysconfig目录
       String skuProperty = SystemProperties.get(SKU_PROPERTY, "");
       if (!skuProperty.isEmpty()) {
           String skuDir = "sku_" + skuProperty;
           readPermissions(parser, Environment.buildPath( Environment.getOdmDirectory(), "etc", "sysconfig", skuDir), odmPermissionFlag);
           readPermissions(parser, Environment.buildPath( Environment.getOdmDirectory(), "etc", "permissions", skuDir), odmPermissionFlag);
       }
     //核心流程6:读取oem分区相关配置,是oem/etc/sysconfig目录和oem/etc/permissions目录
       int oemPermissionFlag = ALLOW_FEATURES | ALLOW_OEM_PERMISSIONS | ALLOW_ASSOCIATIONS | ALLOW_VENDOR_APEX;
       readPermissions(parser, Environment.buildPath( Environment.getOemDirectory(), "etc", "sysconfig"), oemPermissionFlag);
       readPermissions(parser, Environment.buildPath(  Environment.getOemDirectory(), "etc", "permissions"), oemPermissionFlag);
       int productPermissionFlag = ALLOW_FEATURES | ALLOW_LIBS | ALLOW_PERMISSIONS | ALLOW_APP_CONFIGS | ALLOW_PRIVAPP_PERMISSIONS | ALLOW_SIGNATURE_PERMISSIONS | ALLOW_HIDDENAPI_WHITELISTING | ALLOW_ASSOCIATIONS | ALLOW_OVERRIDE_APP_RESTRICTIONS | ALLOW_IMPLICIT_BROADCASTS | ALLOW_VENDOR_APEX;
       if (Build.VERSION.DEVICE_INITIAL_SDK_INT <= Build.VERSION_CODES.R) {
           productPermissionFlag = ALLOW_ALL;
       }
       //核心流程7:读取product分区相关配置,是product/etc/sysconfig目录和product/etc/permissions目录
       readPermissions(parser, Environment.buildPath( Environment.getProductDirectory(), "etc", "sysconfig"), productPermissionFlag);
       readPermissions(parser, Environment.buildPath(  Environment.getProductDirectory(), "etc", "permissions"), productPermissionFlag);
       //核心流程8:读取product分区SKU相关配置,如果ro.boot.product.vendor.sku属性值是cell,那么读取的是product/etc/sku_cell/sysconfig目录
       String productSkuProperty = SystemProperties.get(PRODUCT_SKU_PROPERTY, "");
       if (!productSkuProperty.isEmpty()) {
           String productSkuDir = "sku_" + productSkuProperty;
           readPermissions(parser, Environment.buildPath( Environment.getProductDirectory(), "etc", "sysconfig", productSkuDir), productPermissionFlag);
           readPermissions(parser, Environment.buildPath( Environment.getProductDirectory(), "etc", "permissions", productSkuDir),  productPermissionFlag);
       }
       // Allow /system_ext to customize all system configs
       readPermissions(parser, Environment.buildPath( Environment.getSystemExtDirectory(), "etc", "sysconfig"), ALLOW_ALL);
       readPermissions(parser, Environment.buildPath( Environment.getSystemExtDirectory(), "etc", "permissions"), ALLOW_ALL);
       // Skip loading configuration from apex if it is not a system process.
       if (!isSystemProcess())  return;
       // Read configuration of features, libs and priv-app permissions from apex module.
       int apexPermissionFlag = ALLOW_LIBS | ALLOW_FEATURES | ALLOW_PRIVAPP_PERMISSIONS | ALLOW_SIGNATURE_PERMISSIONS;
       // TODO: Use a solid way to filter apex module folders?
       for (File f: FileUtils.listFilesOrEmpty(Environment.getApexDirectory())) {
           if (f.isFile() || f.getPath().contains("@"))  continue;
           readPermissions(parser, Environment.buildPath(f, "etc", "permissions"),  apexPermissionFlag);
       }
   }
}

如上代码流程总结如下:

  • 加载根目录下的配置文件/etc/sysconfig和/etc/permissions
  • 加载vendor目录下的配置文件/vendor/etc/sysconfig和/vendor/etc/permissions
  • 加载vendor目录下的sku配置文件/vendor/etc/sku_xxx/sysconfig和/vendor/etc/sku_xxx/permissions
  • 加载vendor目录下的noRil配置文件/vendor/etc/noRil/sysconfig和/vendor/etc/noRil/permissions
  • 加载odm目录下的配置文件/odm/etc/sysconfig和/odm/etc/permissions
  • 加载odm目录下的sku配置文件/odm/etc/sku_xxx/sysconfig和/odm/etc/sku_xxx/permissions
  • 加载oem目录下的配置文件/oem/etc/sysconfig和/oem/etc/permissions
  • 加载product目录下的配置文件/product/etc/sysconfig和/product/etc/permissions
  • 加载product目录下的sku配置文件/product/etc/sku_xxx/sysconfig和/product/etc/sku_xxx/permissions
  • 加载system_ext目录下的配置文件/system_ext/etc/sysconfig和/system_ext/etc/permissions
  • 加载apex目录下的配置文件/apex/etc/sysconfig和/apex/etc/permissions

2)根据当前系统环境特性加载Feature

readAllPermissionsFromEnvironment函数就不再是从xml中读取,而是根据当前系统的一些特性,动态选择添加的一些功能,这些功能通常都是与AOSP有关,当然我们也可以在里面添加我们的一些定制化,其源码如下:

java 复制代码
private void readAllPermissionsFromEnvironment() {
    // ------------------------------------------------------------------
    // 1. 文件级加密(FBE)相关特性
    // ------------------------------------------------------------------
    // 某些设备可在出厂后通过"现场升级"方式启用文件级加密(Field Conversion to FBE),
    // 因此如果当前系统启用了 FBE,就动态添加相关特性,即使静态配置中未声明。
    if (StorageManager.isFileEncrypted()) {
        // 表示设备支持文件级加密(File-Based Encryption)
        addFeature(PackageManager.FEATURE_FILE_BASED_ENCRYPTION, 0);
        // 表示设备能安全地移除用户数据(配合 FBE 实现)
        addFeature(PackageManager.FEATURE_SECURELY_REMOVES_USERS, 0);
    }
    // ------------------------------------------------------------------
    // 2. 可插拔存储(Adoptable Storage)支持
    // ------------------------------------------------------------------
    // 为旧设备提供兼容性:如果设备支持 adoptable storage(即 SD 卡可格式化为内部存储),
    // 但其静态配置文件未声明该特性,则在此补充。
    if (StorageManager.hasAdoptable()) {
        addFeature(PackageManager.FEATURE_ADOPTABLE_STORAGE, 0);
    }
    // ------------------------------------------------------------------
    // 3. 内存等级特性(Low-RAM vs Normal-RAM)
    // ------------------------------------------------------------------
    // 根据设备是否被标记为"低内存设备"来声明对应的内存特性。
    // 这会影响系统行为(如后台进程限制、动画关闭等)和应用适配策略。
    if (ActivityManager.isLowRamDeviceStatic()) {
        addFeature(PackageManager.FEATURE_RAM_LOW, 0);      // 低内存设备
    } else {
        addFeature(PackageManager.FEATURE_RAM_NORMAL, 0);   // 普通内存设备
    }
    // ------------------------------------------------------------------
    // 4. 增量交付(Incremental Delivery)支持
    // ------------------------------------------------------------------
    // 如果系统支持增量 APK 安装(即只下载/安装部分模块),则添加对应特性。
    // 特性值为 IncrementalManager 的版本号,用于表示支持级别。
    final int incrementalVersion = IncrementalManager.getVersion();
    if (incrementalVersion > 0) {
        addFeature(PackageManager.FEATURE_INCREMENTAL_DELIVERY, incrementalVersion);
    }
    // ------------------------------------------------------------------
    // 5. 应用枚举(App Enumeration)默认启用
    // ------------------------------------------------------------------
    // 如果系统默认启用了应用枚举限制(即应用无法随意查询其他已安装应用列表),
    // 则声明该特性,供应用检测和适配。
    if (PackageManager.APP_ENUMERATION_ENABLED_BY_DEFAULT) {
        addFeature(PackageManager.FEATURE_APP_ENUMERATION, 0);
    }
    // ------------------------------------------------------------------
    // 6. IPsec 隧道支持(从 Android Q 开始)
    // ------------------------------------------------------------------
    // 如果设备初始 SDK 版本 >= Android Q(API 29),则默认支持 IPsec 隧道功能。
    // 这通常用于企业 VPN 或网络虚拟化场景。
    if (Build.VERSION.DEVICE_INITIAL_SDK_INT >= Build.VERSION_CODES.Q) {
        addFeature(PackageManager.FEATURE_IPSEC_TUNNELS, 0);
    }
    // ------------------------------------------------------------------
    // 7. 启用 IPsec 隧道迁移逻辑(针对特定 Android 版本)
    // ------------------------------------------------------------------
    // 根据 Android 版本(如 V / S / R / U 等)决定是否启用 IPsec 隧道的迁移机制。
    // 具体逻辑在 enableIpSecTunnelMigrationOnVsrUAndAbove() 中实现。
    enableIpSecTunnelMigrationOnVsrUAndAbove();
    // ------------------------------------------------------------------
    // 8. EROFS(Enhanced Read-Only File System)支持检测
    // ------------------------------------------------------------------
    // EROFS 是华为贡献给 Linux 内核的高性能只读文件系统,Android 用于 system 分区。
    // 根据内核版本决定声明哪个级别的 EROFS 特性:
    if (isErofsSupported()) {
        if (isKernelVersionAtLeast(5, 10)) {
            // 内核 5.10+:完整 EROFS 支持
            addFeature(PackageManager.FEATURE_EROFS, 0);
        } else if (isKernelVersionAtLeast(4, 19)) {
            // 内核 4.19~5.9:旧版 EROFS(legacy)
            addFeature(PackageManager.FEATURE_EROFS_LEGACY, 0);
        }
    }
}

3)解析XML的规则

xml文件的解析规则,具体实现还是在readPermissionsFromXml函数中,该函数代码逻辑其实也比较简单,就是单纯的解析xml文件字符串,然后提取关键字,然后针对这些关键字进行分别处理,添加到对应的列表中。如下代码:

二、ETC系统配置读取

我们经常遇到如下代码调用,核心逻辑就是应用程序通过Context拿到PackageManager,然后通过其hasSystemFeature函数来判断当前系统是否支持某个FEATURE。

java 复制代码
    private boolean supportOMAPIReaders() {
        final PackageManager pm = InstrumentationRegistry.getContext().getPackageManager();
        return (pm.hasSystemFeature(PackageManager.FEATURE_SE_OMAPI_UICC)
            || pm.hasSystemFeature(PackageManager.FEATURE_SE_OMAPI_ESE)
            || pm.hasSystemFeature(PackageManager.FEATURE_SE_OMAPI_SD));
    }

那么它是如何判断的呢?怎么和第一章的etc目录下的配置建立联系的呢?

1、ApplicationPackageManager

Context这里实际上使用了装饰者模式,最后获取到的getPackageManager拿到了pm对象,pm对象的实例就是ApplicationPackageManager,注意他处于客户端进程。从这里的命名可以了解到有如下几个作用:

  • Application表示为应用进程对应的实例对象,可以理解为一个应用进程对应一个ApplicationContext
  • PackageManager表示为应用进程作为PMS系统服务的客户端管理对象,即该应用进程可以通过此对象去和PMS系统服务进行binder通信

2、hasSystemFeature

普通应用通过hasSystemFeature方法来判断当前系统拥有的Feature(功能),然而从第一节可以知道ApplicationPackageManager是属于应用端进程的,那么它如何能知道当前系统拥有的feature呢?就必须去向PMS获取?我们具体来研究一下他是直接向PMS获取的吗?

java 复制代码
//la.qssi16/frameworks/base/core/java/android/app/ApplicationPackageManager.java
public class ApplicationPackageManager extends PackageManager {
    private static final String TAG = "ApplicationPackageManager";
    @UnsupportedAppUsage
    protected ApplicationPackageManager(ContextImpl context, IPackageManager pm) {
        mContext = context;
        mPM = pm;
        //是否缓存系统功能?通常情况是true
        mUseSystemFeaturesCache = isSystemFeaturesCacheEnabledAndAvailable();
    }
    @Override
    public boolean hasSystemFeature(String name) {
        return hasSystemFeature(name, 0);
    }
    @Override
    public boolean hasSystemFeature(String name, int version) {
        //步骤一:Build time-defined system features
        Boolean maybeHasSystemFeature = RoSystemFeatures.maybeHasFeature(name, version);
        if (maybeHasSystemFeature != null) {
            return maybeHasSystemFeature;
        }
        //步骤二:SDK-defined system features
        if (mUseSystemFeaturesCache) {
            maybeHasSystemFeature =  SystemFeaturesCache.getInstance().maybeHasFeature(name, version);
            if (maybeHasSystemFeature != null) {
                return maybeHasSystemFeature;
            }
        }
        //步骤三:IPC-retrieved system features 
        return mHasSystemFeatureCache.query(new HasSystemFeatureQuery(name, version));
    }
}

如上代码根据注释可以总结如下三个步骤:

  • 从RoSystemFeatures获取对应功能:编译阶段生成的默认FEATURE配置
  • 从SystemFeaturesCache查找对应功能,通过SDK feature缓存的FEATURE配置
  • 从mHasSystemFeatureCache.query查找对应功能,内部使用了IPC机制向系统服务PMS进行了请求

所以重点还是第三步,mHasSystemFeatureCache如何通过IPC的方式向系统PMS服务进行请求呢?来看看mHasSystemFeatureCache是如何生成的:

通过这种方式拿到了getPackageManager客户端对象,这个客户端对象就是PackageMnager接口,前面几章SystemXXXServcie基本上就能很容易理解这种讨论,这其实就是像PMS服务进行请求,跨进程通信调用PMS的hasSystemFeature接口。详细可以参考https://blog.csdn.net/qq_27672101/article/details/140658955

3、checkPermission

相关推荐
城东米粉儿2 小时前
Android IdleHandler 优化笔记
android
城东米粉儿2 小时前
Android Binder 笔记
android
Android系统攻城狮2 小时前
Android tinyalsa深度解析之pcm_get_available_min调用流程与实战(一百一十六)
android·pcm·tinyalsa·音频进阶·音频性能实战
lxysbly2 小时前
nds模拟器安卓版官网
android
hewence12 小时前
协程间数据传递:从Channel到Flow,构建高效的协程通信体系
android·java·开发语言
前端不太难2 小时前
为什么鸿蒙不再适用 Android 分层
android·状态模式·harmonyos
2501_916007472 小时前
ios上架 App 流程,证书生成、从描述文件创建、打包、安装验证到上传
android·ios·小程序·https·uni-app·iphone·webview
阿林来了2 小时前
Flutter三方库适配OpenHarmony【flutter_speech】— Android 端实现分析
android·flutter·harmonyos·鸿蒙