【RN】iOS Native 和 ReactNative 通信

背景

RN 给我们提供了丰富的UI库 和组件库, 但也有需要和native 视图的.比如MapKit, AVFoundation. 之类的需求. 因为篇幅有限,本文仅使用部分代码片段用以说明Module 实现的方式,详细代码,请参考react-native-wmm-aliyunplayer
react-native-wmm-aliyunplayer 下面以本库代述。

本库主要是对react-native-lewin-aliyunplayer
的扩展,以及对阿里云播SDK做了下载模块的拓展,非阿里云播器功能的全量封装。本文仅以此案例用于阐述 Native Module的封装以及,发布npm js 公仓的表述。

环境

RN依赖版本

// package.json
{
    "dependencies": {
        "react": "16.13.1",
        "react-native": "0.63.3",
     }
}

node/npm/cocoapods 版本

jeversonjee@macbookpro ~ % npm -v
8.5.0
jeversonjee@macbookpro ~ % node -v
v16.14.2
jeversonjee@macbookpro~ % pod --version
1.11.3

应用场景

音频锁屏播放

在音视频播放的需求中,我们通常会有在锁屏实现音频播放的功能;在react-native 0.63 没有提供该组件的情况下,我们需要原生来实现,并和native 通信,那么和原生通信rn是提供iOS模块Android模块详细api请点击上述链接查看

原理

基于KVO的方式NSNofitificationCenter

多任务下载

  1. 多任务下载,和页面显示无关,因此,选用NativeModule 来实现。
  2. 支持原生阿里云播SDK的方法
    • 删除下载文件
    • 销毁下载对象
    • 使用AVPVidStsSource准备播放
    • 设置下载的保存路径
    • 开始下载
    • 设置下载config
    • 设置下载的trackIndex
    • 获取SDK版本号信息
  3. 下载任务回调事件监听
    • 错误代理回调
    • 下载文件的处理进度回调
    • 下载完成回调

代码示例

采用我们团队的npm公仓库react-native-wmm-aliyunplayer

iOS

Code Slice Powered by nbhhcty

根据上述的业务需求,我们了解到原生模块需要创建RNAliMediaDownloader 集成于RCTEventEmitter 遵循了 RCTBridgeModule. 官方文档对于事件监听即RCTEventEmmitter 有详细的描述,此处主要是方变我们发送订阅事件消息 以及声明支持的事件类型。关于RCTBridgeModule 主要是方便前端RNModule 通信时声明的宏方法定义RCT_EXPORT_METHOD, 关于暴露方法的宏定义,官方文档比较详细,本文不在对此功能赘述。关于文章中使用RCTLog 而非NSLog 主要是方便,前端Log debug 打印。Lib 中推荐使用RCTLog

iOS 声明
//
//  RNAliMediaDownloader.h
//  AliyunVideoClient_Entrance
//
//  Created by wsk on 2022/12/6.
//  Copyright © 2022 Aliyun. All rights reserved.
//

#import <React/RCTBridgeModule.h>
#import <React/RCTEventEmitter.h>

extern NSString* const _Nullable EK_downloaderOnPrepared;
extern NSString* const _Nullable EK_downloaderOnError;
extern NSString* const _Nullable EK_downloaderOnDownloadingProgress;
extern NSString* const _Nullable EK_downloaderOnProcessingProgress;
extern NSString* const _Nullable EK_downloaderOnCompletion;

NS_ASSUME_NONNULL_BEGIN

@interface RNAliMediaDownloader : RCTEventEmitter<RCTBridgeModule>

@end

NS_ASSUME_NONNULL_END
iOS 实现部分
//
//  RNAliMediaDownloader.m
//  AliyunVideoClient_Entrance
//
//  Created by wsk on 2022/12/6.
//  Copyright © 2022 Aliyun. All rights reserved.
//

#import "RNAliMediaDownloader.h"
#import <AliyunMediaDownloader/AliyunMediaDownloader.h>
#import <React/RCTLog.h>
#import "AliMediaDownloader+vid.h"

NSString* const EK_downloaderOnPrepared             = @"downloaderOnPrepared";
NSString* const EK_downloaderOnError                = @"downloaderOnError";
NSString* const EK_downloaderOnDownloadingProgress  = @"downloaderOnDownloadingProgress";
NSString* const EK_downloaderOnProcessingProgress   = @"downloaderOnProcessingProgress";
NSString* const EK_downloaderOnCompletion           = @"downloaderOnCompletion";

@interface RNAliMediaDownloader ()
@property (nonatomic,strong)NSMutableDictionary<NSString*, AliMediaDownloader*> *preDownloaders;
@property (nonatomic,strong)NSMutableDictionary<NSString*, AliMediaDownloader*> *downloaders;
@end

@implementation RNAliMediaDownloader

RCT_EXPORT_MODULE(RNAliMediaDownloader)

/**
 @brief 初始化下载对象
 */
- (instancetype)init {
  if (self = [super init]) {
  }
  return self;
}

-(NSArray<NSString *> *)supportedEvents{
    return @[
      EK_downloaderOnPrepared,
      EK_downloaderOnError,
      EK_downloaderOnDownloadingProgress,
      EK_downloaderOnProcessingProgress,
      EK_downloaderOnCompletion
    ];
}

- (NSDictionary *)constantsToExport {
    return @{
        @"onPrepared": EK_downloaderOnPrepared,
        @"onError": EK_downloaderOnError,
        @"onProcessingProgress": EK_downloaderOnProcessingProgress,
        @"onDownloadingProgress": EK_downloaderOnDownloadingProgress,
        @"onCompletion": EK_downloaderOnCompletion
    };
}

- (NSString*)mapKey:(NSString*)vid trackIndex:(int)trackIndex {
  NSString *key = [NSString stringWithFormat:@"%@-%@", vid, @(trackIndex)];
  return key;
}

/**
 @brief 删除下载文件
 @param saveDir 文件保存路径
 @param vid     vid
 @param format  格式
 @param index   vid对应的下载索引
 */
RCT_EXPORT_METHOD(deleteFile:(NSString*)saveDir
                  vid:(NSString*)vid
                  format:(NSString*)format
                  index:(int)index
                  callback:(RCTResponseSenderBlock)callback) {
    RCTLogInfo(@"rn_media_downloader deleteFile vid = %@ trackIndex = %d", vid, index);

    int result = [AliMediaDownloader deleteFile:saveDir vid:vid format:format index:index];
    callback(@[[NSNumber numberWithInt:result]]);
    
    /// 如果是准备中、下载中删除
    NSString *key = [self mapKey:vid trackIndex:index];
    AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
    if (downloader) {
      RCTLogInfo(@"rn_media_downloader deleteFile downloaders");

      [downloader deleteFile];
      [downloader destroy];
      self.downloaders[key] = nil;
    }

    downloader = [self.preDownloaders objectForKey:vid];
    if (downloader) {
      RCTLogInfo(@"rn_media_downloader deleteFile preDownloaders");
      [downloader destroy];
      self.preDownloaders[vid] = nil;
    }
}

/**
 @brief 销毁下载对象
 */
RCT_EXPORT_METHOD(destroyWithVid:(NSString*)vid trackIndex:(int)trackIndex) {
    
  RCTLogInfo(@"rn_media_downloader destroyWithVid vid = %@ trackIndex = %d", vid, trackIndex);

  NSString *key = [self mapKey:vid trackIndex:trackIndex];
  
  AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
  if (downloader) {
    [downloader destroy];
    self.downloaders[key] = nil;
  }

  downloader = [self.preDownloaders objectForKey:vid];
  if (downloader) {
    [downloader destroy];
    self.preDownloaders[vid] = nil;
  }
}

/**
 @brief 使用AVPVidStsSource准备播放
 @param source vid sts的播放方式
 */
RCT_EXPORT_METHOD(prepareWithVid:(NSString*)vid
                  accessKeyId:(NSString *) accessKeyId
                  accessKeySecret:(NSString *) accessKeySecret
                  securityToken:(NSString *) securityToken
                  region:(NSString *) region) {
    RCTLogInfo(@"rn_media_downloader prepareWithVid vid = %@", vid);

    AVPVidStsSource *source = [[AVPVidStsSource alloc] initWithVid:vid
                                                      accessKeyId:accessKeyId
                                                  accessKeySecret:accessKeySecret
                                                    securityToken:securityToken
                                                            region:region];
  
    AliMediaDownloader * downloader = [[AliMediaDownloader alloc] init];
    downloader.mm_vid = vid;
    downloader.delegate = (id<AMDDelegate>)self;
    self.preDownloaders[downloader.mm_vid] = downloader;
    [downloader prepareWithVid:source];
}

/**
 @brief 鉴权过期,更新AVPVidStsSource信息,
 @param source vid sts的信息
 */
RCT_EXPORT_METHOD(updateWithVid:(NSString*)vid
                  trackIndex:(int) trackIndex
                  accessKeyId:(NSString *) accessKeyId
                  accessKeySecret:(NSString *) accessKeySecret
                  securityToken:(NSString *) securityToken
                  region:(NSString *) region) {
    RCTLogInfo(@"rn_media_downloader updateWithVid vid = %@ trackIndex = %d", vid, trackIndex);

    AVPVidStsSource *source = [[AVPVidStsSource alloc] initWithVid:vid
                                                      accessKeyId:accessKeyId
                                                  accessKeySecret:accessKeySecret
                                                    securityToken:securityToken
                                                            region:region];
    NSString *key = [self mapKey:vid trackIndex:trackIndex];
    AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
    [downloader updateWithVid:source];
}

/**
 @brief 设置下载的保存路径
 @param dir 保存文件夹
 */
RCT_EXPORT_METHOD(setSaveDirectory:(NSString*)dir vid:(NSString*)vid trackIndex:(int)trackIndex) {
    RCTLogInfo(@"rn_media_downloader setSaveDirectory vid = %@ trackIndex = %d", vid, trackIndex);

    NSString *key = [self mapKey:vid trackIndex:trackIndex];
    AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
    [downloader setSaveDirectory:dir];
}

/**
 @brief 开始下载
 */
RCT_EXPORT_METHOD(startWithVid:(NSString*)vid trackIndex:(int)trackIndex) {
    RCTLogInfo(@"rn_media_downloader startWithVid vid = %@ trackIndex = %d", vid, trackIndex);

    NSString *key = [self mapKey:vid trackIndex:trackIndex];
    AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
    [downloader start];
}

/**
 @brief 停止下载
 */
RCT_EXPORT_METHOD(stopWithVid:(NSString*)vid trackIndex:(int)trackIndex) {
    RCTLogInfo(@"rn_media_downloader stopWithVid vid = %@ trackIndex = %d", vid, trackIndex);
    
    NSString *key = [self mapKey:vid trackIndex:trackIndex];
    AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
    [downloader stop];
}

/**
 @brief 获取下载config
 */
RCT_EXPORT_METHOD(getConfigWithVid:(NSString*)vid trackIndex:(int)trackIndex callback:(RCTPromiseResolveBlock)callback) {
    RCTLogInfo(@"rn_media_downloader getConfigWithVid vid = %@ trackIndex = %d", vid, trackIndex);

    NSString *key = [self mapKey:vid trackIndex:trackIndex];
    AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
    AVDConfig *config = [downloader getConfig];
    NSMutableDictionary *dic = [NSMutableDictionary dictionary];
    dic[@"timeoutMs"] = @(config.timeoutMs);
    dic[@"connnectTimoutMs"] = @(config.connnectTimoutMs);
    dic[@"referer"] = config.referer;
    dic[@"userAgent"] = config.userAgent;
    dic[@"httpProxy"] = config.httpProxy;
    callback(dic);
}

/**
 @brief 设置下载config
 */
RCT_EXPORT_METHOD(setConfig:(int)timeoutMs
                  connnectTimoutMs:(int)connnectTimoutMs
                  referer:(NSString*)referer
                  userAgent:(NSString*)userAgent
                  httpProxy:(NSString*)httpProxy
                  vid:(NSString*)vid
                  trackIndex:(int)trackIndex
) {
    RCTLogInfo(@"rn_media_downloader setConfig vid = %@ trackIndex = %d", vid, trackIndex);

    AVDConfig *config = [[AVDConfig alloc] init];
    config.timeoutMs = timeoutMs;
    config.connnectTimoutMs = connnectTimoutMs;
    config.referer = referer;
    config.userAgent = userAgent;
    config.httpProxy = httpProxy;
  
    NSString *key = [self mapKey:vid trackIndex:trackIndex];
    AliMediaDownloader *downloader = [self.downloaders objectForKey:key];
    [downloader setConfig:config];
}

/**
 @brief 设置下载的trackIndex
 @param trackIndex 从prepare回调中可以获取所有index
 */
RCT_EXPORT_METHOD(selectTrack:(int)trackIndex vid:(NSString*)vid) {
    RCTLogInfo(@"rn_media_downloader selectTrack vid = %@ trackIndex = %d", vid, trackIndex);

    AliMediaDownloader *downloader = self.preDownloaders[vid];

    downloader.mm_trackIndex = @(trackIndex).stringValue;
    [downloader selectTrack:trackIndex];
  
    NSString *key = [self mapKey:vid trackIndex:trackIndex];
    self.downloaders[key] = downloader;
  
    self.preDownloaders[vid] = nil;
}

/**
 @brief 获取SDK版本号信息
 */
RCT_EXPORT_METHOD(getSDKVersion:(RCTPromiseResolveBlock)callback) {
    RCTLogInfo(@"rn_media_downloader getSDKVersion");

    NSString *version = [AliMediaDownloader getSDKVersion];
    callback(version);
}

- (NSMutableDictionary<NSString*,AliMediaDownloader*> *)downloaders {
  if(!_downloaders) {
    _downloaders = [[NSMutableDictionary<NSString*,AliMediaDownloader*> alloc] init];
  }
  return _downloaders;
}

- (NSMutableDictionary<NSString*,AliMediaDownloader*> *)preDownloaders {
  if(!_preDownloaders) {
    _preDownloaders = [[NSMutableDictionary<NSString*,AliMediaDownloader*> alloc] init];
  }
  return _preDownloaders;
}
@end

#pragma mark - <AMDDelegate>
@interface RNAliMediaDownloader (AMDDelegate)<AMDDelegate>
@end
@implementation RNAliMediaDownloader (AMDDelegate)
/**
 @brief 下载准备完成事件回调
 @param downloader 下载downloader指针
 @param info 下载准备完成回调,@see AVPMediaInfo
 */
-(void)onPrepared:(AliMediaDownloader*)downloader mediaInfo:(AVPMediaInfo*)info {
  RCTLogInfo(@"rn_media_downloader onPrepared vid = %@", downloader.mm_vid);

  NSMutableArray *tracks = [NSMutableArray array];
  
  [info.tracks enumerateObjectsUsingBlock:^(AVPTrackInfo * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
    NSMutableDictionary *track = [NSMutableDictionary dictionary];
    track[@"trackIndex"] = @(obj.trackIndex);
    track[@"trackDefinition"] = obj.trackDefinition;
    track[@"vodFormat"] = obj.vodFormat;
    track[@"vodFileSize"] = @(obj.vodFileSize);
    [tracks addObject:track];
  }];
  
  NSMutableDictionary *body = [NSMutableDictionary dictionary];
  body[@"vid"] = downloader.mm_vid;
  body[@"tracks"] = tracks;
  [self sendEventWithName:EK_downloaderOnPrepared body:body];
}

/**
 @brief 错误代理回调
 @param downloader 下载downloader指针
 @param errorModel 播放器错误描述,参考AliVcPlayerErrorModel
 */
- (void)onError:(AliMediaDownloader*)downloader errorModel:(AVPErrorModel *)errorModel {
  RCTLogInfo(@"rn_media_downloader onError vid = %@ code = %@", downloader.mm_vid, @(errorModel.code));

  NSMutableDictionary *body = [NSMutableDictionary dictionary];
  body[@"vid"] = downloader.mm_vid;
  body[@"trackIndex"] = downloader.mm_trackIndex;
  body[@"code"] = @(errorModel.code);
  body[@"message"] = errorModel.message;
  [self sendEventWithName:EK_downloaderOnError body:body];
}

/**
 @brief 下载进度回调
 @param downloader 下载downloader指针
 @param percent 下载进度 0-100
 */
- (void)onDownloadingProgress:(AliMediaDownloader*)downloader percentage:(int)percent {
  RCTLogInfo(@"rn_media_downloader onDownloadingProgress vid = %@-%@ percent = %d", downloader.mm_vid, downloader.mm_trackIndex, percent);

  NSMutableDictionary *body = [NSMutableDictionary dictionary];
  body[@"vid"] = downloader.mm_vid;
  body[@"trackIndex"] = downloader.mm_trackIndex;
  body[@"percent"] = @(percent);
  [self sendEventWithName:EK_downloaderOnDownloadingProgress body:body];
}

/**
 @brief 下载文件的处理进度回调
 @param downloader 下载downloader指针
 @param percent 下载进度 0-100
 */
- (void)onProcessingProgress:(AliMediaDownloader*)downloader percentage:(int)percent {
  RCTLogInfo(@"rn_media_downloader onProcessingProgress vid = %@-%@ percent = %d", downloader.mm_vid, downloader.mm_trackIndex, percent);

  NSMutableDictionary *body = [NSMutableDictionary dictionary];
  body[@"vid"] = downloader.mm_vid;
  body[@"trackIndex"] = downloader.mm_trackIndex;
  body[@"percent"] = @(percent);
  [self sendEventWithName:EK_downloaderOnProcessingProgress body:body];
}

/**
 @brief 下载完成回调
 @param downloader 下载downloader指针
 */
- (void)onCompletion:(AliMediaDownloader*)downloader {
  RCTLogInfo(@"rn_media_downloader onCompletion vid = %@-%@-%@", downloader.mm_vid, downloader.mm_trackIndex, downloader.downloadedFilePath);

  NSMutableDictionary *body = [NSMutableDictionary dictionary];
  body[@"vid"] = downloader.mm_vid;
  body[@"trackIndex"] = downloader.mm_trackIndex;
  body[@"filePath"] = [downloader downloadedFilePath];
  [self sendEventWithName:EK_downloaderOnCompletion body:body];
}
@end

前端部分

Tallk is cheap show me code

Code slice powered by jeversonjee

import {
  NativeModules,
  NativeEventEmitter,
  Platform,
  EmitterSubscription,
} from 'react-native';
import RNFS from 'react-native-fs';
import type { IAssumeRole, ICompletion, IProgress } from './types';

const { RNAliMediaDownloader } = NativeModules;
const RNAliMediaEmitter = new NativeEventEmitter(RNAliMediaDownloader);
const defaultFilePath =
  (Platform.OS === 'ios'
    ? RNFS.DocumentDirectoryPath
    : RNFS.ExternalDirectoryPath) + '/media';

export enum EEvent {
  ON_PREPARED = RNAliMediaDownloader.onPrepared,
  ON_ERROR = RNAliMediaDownloader.onError,
  ON_PROCESSING_PROGRESS = RNAliMediaDownloader.onProcessingProgress,
  ON_DOWNLOADING_PROGRESS = RNAliMediaDownloader.onDownloadingProgress,
  ON_COMPLETION = RNAliMediaDownloader.onCompletion,
}

let subscriptions = new Map<string, EmitterSubscription>();

//  下载进度回调事件
export const onDownloadingProgress = (callBack: (data: IProgress) => void) => {
  if (
    subscriptions &&
    subscriptions.has(String(EEvent.ON_DOWNLOADING_PROGRESS))
  ) {
    console.log('你已经订阅过 onDownloadingProgress');
    return;
  }
  const downloadProcesssSubs = RNAliMediaEmitter.addListener(
    String(EEvent.ON_DOWNLOADING_PROGRESS),
    (res: IProgress) => {
      callBack(res);
    }
  );
  subscriptions.set(
    String(EEvent.ON_DOWNLOADING_PROGRESS),
    downloadProcesssSubs
  );
};

/**
 * @description 下载完成回调
 */
export const onDownloaderCompletion = (
  callback: (data: ICompletion) => void
) => {
  if (subscriptions && subscriptions.has(String(EEvent.ON_COMPLETION))) {
    console.log('你已经订阅过 onDownloaderCompletion');
    return;
  }
  const completionSubs = RNAliMediaEmitter.addListener(
    String(EEvent.ON_COMPLETION),
    (res: ICompletion) => {
      callback(res);
    }
  );
  subscriptions.set(String(EEvent.ON_COMPLETION), completionSubs);
};

/**
 * @description 下载准备回调
 */
export const onDownloaderPrepared = (callback: (data: any) => void) => {
  if (subscriptions && subscriptions.has(String(EEvent.ON_PREPARED))) {
    console.log('你已经订阅过 onDownloaderPrepared');
    return;
  }
  const preparedSubs = RNAliMediaEmitter.addListener(
    String(EEvent.ON_PREPARED),
    (res: any) => {
      callback(res);
    }
  );
  subscriptions.set(String(EEvent.ON_PREPARED), preparedSubs);
};

/**
 * @description 下载异常回调
 */
export const onDownloaderError = (callback: (data: any) => void) => {
  if (subscriptions && subscriptions.has(String(EEvent.ON_PREPARED))) {
    console.log('你已经订阅过 onDownloaderError');
    return;
  }
  const errorSubs = RNAliMediaEmitter.addListener(
    String(EEvent.ON_ERROR),
    (res: any) => {
      callback(res);
    }
  );
  subscriptions.set(String(EEvent.ON_ERROR), errorSubs);
};

//  取消监听事件
export const unsubscription = () => {
  if (subscriptions && subscriptions.size > 0) {
    for (let value of subscriptions.values()) {
      value.remove();
    }
    subscriptions.clear();
  }
};

// 设置下载路径
export const setSaveDirectory = (
  vid: string,
  trackIndex = 0,
  filePath?: string
) => {
  if (!filePath) filePath = defaultFilePath;
  RNAliMediaDownloader.setSaveDirectory(filePath, vid, trackIndex);
};

// 准备下载
export const prepareWithVid = async (vid: string, role: IAssumeRole) => {
  RNAliMediaDownloader.prepareWithVid(
    vid,
    role.AccessKeyId,
    role.AccessKeySecret,
    role.SecurityToken,
    role.Region
  );
};

// 开始下载
export const startDownload = async (vid: string, trackIndex = 0) => {
  RNAliMediaDownloader.startWithVid(vid, trackIndex);
};

// 暂停下载
export const pauseDownload = (vid: string, trackIndex = 0) => {
  RNAliMediaDownloader.stopWithVid(vid, trackIndex);
};

// 删除文件
export const deleteDownload = async (
  saveDir: string,
  vid: string,
  format: string,
  index: number
) => {
  const influenceRows = await RNAliMediaDownloader.deleteFile(
    saveDir,
    vid,
    format,
    index
  );
  return new Promise((resolve) => {
    resolve({ res: influenceRows });
  });
};

// 设置分辨率
export const selectTrack = (vid: string, trackIndex = 0) => {
  RNAliMediaDownloader.selectTrack(trackIndex, vid);
};

发行、调试、验证

万事俱备,我们实现了原生部分。桥接部分,那如何来创建一个用于发行、调试、验证的方式,当然不发行也是可以的,可以使用patch 来代替每次手动替换,node_modules 原生声明和桥接目录。因为跨平台调用链很长的问题,所以patch 可以是沟通成本最低的方式,无任何优劣的比较。在此隆重介绍create-react-native-library

create-react-native-library

目录结构

本库目录结构

  • android 主要存放原生代码逻辑
  • doc 主要存放一些关于库前端调用方法,和注释.主要是让前端了解方法,属性的作用
  • example 原生调试、debug 的目录就和react-native 项目基本无区别。
  • lib 为输出目录。主要是src 中声明的前端方法编译成js 因此处作者采用的是ts
  • node_modules 本库 所依赖的一些包。
  • src 前端代码,编译前产物。
  • leftbook.yml 这是 git pre-commit 的校验配置文件
  • ur project name.podSpec 主要是用于声明这个一个pod lib 以及你需要在iOS 中配置的依赖,以及podSpec的所有声明编译方式,都是全量支持的。关于podSpec 配置,请自行了解。
  • tsConfig: 关于tsc 的编译配置,关于配置方面的文档请自行了解
BTW

关于一个原生了解一个库,最直接的方式就是通过官方文档,以及本文提示词去搜索。本文还是赘述一下使用方式。

  1. example 调试目录在二级目录中,可以在lib 目录中执行命令的吗?
  2. 由于xcode 12 的限制,关于模拟器,虽然我不爱模拟器debug 只需要在podSpec 中指定Target 指定一下 arm64
  3. 最重要的是看一下项目的 package.json

视屏播放

logs

  • 2023.05-27 update iOS modules
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值