uni-app原生插件(1)--截图

现在uni-app使用的越来越多,但是有些功能需要原生开发插件放入uni-app程序中来调用,本篇文章记录以下iOS原生截图插件的开发过程

1:首先创建插件工程

打开 Xcode,创建一个新的Framework工程,然后点击 Next,然后选中工程名,在TARGETS->Build Settings中,将 Mach-O Type 设置为 Static Library

输入插件工程名称,然后点击Next,然后选择存放路劲

2:导入插件工程

打开工程目录,双击目录中的HBuilder-uniPlugin.xcodeproj 文件运行插件开发主工程, 在 Xcode 项目左侧目录选中主工程名,然后点击右键选择Add Files to "HBuilder-uniPlugin"导入插件工程

然后选择您刚刚创建的插件工程路径中,选中插件工程文件,勾选 Create folder references 和 Add to targets 两项,然后点击Add

3:工程配置

在 Xcode 项目左侧目录选中主工程名,在TARGETS->Build Phases->Dependencies中点击+,在弹窗中选中插件工程,然后点击Add,将插件工程添加到Dependencies中;然后在Link Binary With Libraries中点击+,同样在弹窗中选中插件工程,点击Add.

接下来需要在插件工程的Header Search Paths中添加开发插件所需的头文件引用,头文件存放在主工程的HBuilder-Hello/inc中,,在 Xcode 项目左侧目录选中插件工程名,找到TARGETS->Build Settings->Header Search Paths双击右侧区域打开添加窗口,然后将inc目录拖入会自动填充相对路径,然后将模式改成recursive

4:代码实现

本文需求是浏览网页的长截图,苹果没有单独的生成长截图的API,所以要采取替代方案,使用多次截图并屏截方案,多次测试中,屏截方案会有显示空白页面问题,所以才去了笨办法,使用createPDFWithConfiguration把网页先转换成PDF文件再生成image,然后与截图的图片再次屏截在一个页面,生成最后的图片; 方案执行中,百度浏览器搜索内容网页,截图后生成图片会有黑色遮挡,所以采取了新的截图方案

objectivec 复制代码
//保存地址 并传参uni-app
- (void)saveAsPDFWithTitle:(NSString *)title {
    
    if (@available(iOS 13.4, *)) {
        __weak __typeof(self)weakSelf = self;
        __strong __typeof(weakSelf)strongSelf = weakSelf;
        NSLog(@"%f",[self getWebView].wkWebView.frame.size.height);
        //创建的截图页面
        UIView *backgroundView = [[UIView alloc] init];
        [self snapshotForWKWebView:[self getWebView].wkWebView CaptureCompletionHandler:^(UIImage *capturedImage) {
            NSLog(@"%@",capturedImage);
            NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSUserDomainMask, YES);
            NSString *downloadsDirectory = [paths firstObject];
            if (downloadsDirectory) {
                NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
                NSString *documentsDirectory = [paths objectAtIndex:0];
                NSString *myPathDocs = [documentsDirectory stringByAppendingPathComponent:@"/Pandora/documents/output_image.jpeg"];
                if([[NSFileManager defaultManager] fileExistsAtPath:myPathDocs]) {
                    [[NSFileManager defaultManager] removeItemAtPath:myPathDocs error:nil];
                }
                NSURL *outputURL = [NSURL fileURLWithPath:myPathDocs];
                NSData *jpegData = UIImageJPEGRepresentation(capturedImage, 0.8);
                NSData *jpegData2 = UIImagePNGRepresentation(capturedImage);
                NSLog(@"%@==%@",jpegData,jpegData2);
                [jpegData2 writeToURL:outputURL atomically:YES];
                WebView *webView = [strongSelf getWebView];
                NSString *url = webView.wkWebView.URL.absoluteString;
                backgroundView.frame = CGRectMake(0, 0, capturedImage.size.width, capturedImage.size.height);
                UIImageView *image = [[UIImageView alloc] init];
                image.frame = backgroundView.frame;
                image.image = capturedImage;
                [backgroundView addSubview:image];
                NSLog(@"%@==%@",outputURL.path,url);
                
                WKPDFConfiguration *pdfConfiguration = [[WKPDFConfiguration alloc] init];

                UIView *contentView = [self getWebView].wkWebView.scrollView.subviews.firstObject.subviews.firstObject;

                /// 使用`webView.scrollView.frame` 可以捕获整个页面,而不仅仅是可见部分
                pdfConfiguration.rect = CGRectMake(0, 0, contentView.frame.size.width, contentView.frame.size.height);
                if (@available(iOS 14.0, *)) {

                    //__weak __typeof(self)weakSelf = self;
                    [webView.wkWebView createPDFWithConfiguration:pdfConfiguration completionHandler:^(NSData *pdfData, NSError *error) {
                        if (pdfData) {
                            //__strong __typeof(weakSelf)strongSelf = weakSelf;
                            NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDownloadsDirectory, NSUserDomainMask, YES);
                            NSString *downloadsDirectory = [paths firstObject];

                            if (downloadsDirectory) {
                                NSString *fileName = title ?: @"PDF";
                                NSString *savePath = [[downloadsDirectory stringByAppendingPathComponent:fileName] stringByAppendingPathExtension:@"pdf"];

                                NSError *writeError;
                                NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);
                                NSString *documentsDirectory = [paths objectAtIndex:0];
                                NSString *myPathDocs = [documentsDirectory stringByAppendingPathComponent:@"/Pandora/documents/output_image.jpeg"];
                                if([[NSFileManager defaultManager] fileExistsAtPath:myPathDocs]) {
                                    [[NSFileManager defaultManager] removeItemAtPath:myPathDocs error:nil];
                                }
                                NSURL *outputURL = [NSURL fileURLWithPath:myPathDocs];

                                UIImage *pdfImage = [strongSelf convertPDFDataToImage:pdfData];
                                //UIImage *pdfImage = [UIImage imageWithData:pdfData];
                                NSData *jpegData = UIImageJPEGRepresentation(pdfImage, 0.8);

                                [jpegData writeToURL:outputURL atomically:YES];
                                NSLog(@"%@",outputURL.path);
                                if (!writeError) {
                                    NSLog(@"Successfully created and saved PDF at %@", savePath);
                                } else {
                                    NSLog(@"Could not save pdf due to %@", writeError.localizedDescription);
                                }

                                WebView *webView = [strongSelf getWebView];
                                // 计算页面停留时间
                                NSDate *screenshotTimestamp = [NSDate date];
                                NSTimeInterval timeInterval = [screenshotTimestamp timeIntervalSinceDate:webView.pageLoadTimestamp];
                                NSLog(@"页面停留时间: %f 秒", timeInterval);
                                // 转换 timeInterval 为字符串
                                NSString *timeIntervalString = [NSString stringWithFormat:@"%f", timeInterval];

                                // 获取当前访问的 url
                                NSString *url = webView.wkWebView.URL.absoluteString;

                                NSLog(@"加载PDF前 W==%f H==%f PDF图片大小%f %f",backgroundView.frame.size.width,backgroundView.frame.size.height,pdfImage.size.width,pdfImage.size.height);
                                CGFloat pdfImgHeight = pdfImage.size.height;
                                CGFloat pdfImgWidth = pdfImage.size.width;
                                if (pdfImage.size.width > backgroundView.bounds.size.width) {
                                    pdfImgHeight = backgroundView.bounds.size.width/pdfImage.size.width*pdfImage.size.height;
                                    pdfImgWidth = backgroundView.bounds.size.width;
                                }
                                UIImageView *image = [[UIImageView alloc] init];
                                image.frame = CGRectMake(0, 0, pdfImgWidth, pdfImgHeight);;
                                image.image = [UIImage imageWithData:jpegData];
                                //[backgroundView addSubview:image];
                                if ([[NSString stringWithFormat:@"%@",self->_loadURLStr] containsString:@"https://www.baidu.com"]) {
                                } else {
                                    [backgroundView addSubview:image];
                                }
                                NSLog(@"加载PDF后 W==%f H==%f 截图大小%f==%f",backgroundView.frame.size.width,backgroundView.frame.size.height,capturedImage.size.width,capturedImage.size.height);
                                //把view转换成image截图
                                UIImage *imageRet = [self getImageFromView:backgroundView];
                                NSData *jpegData3 = UIImagePNGRepresentation(imageRet);
                                [jpegData3 writeToURL:outputURL atomically:YES];
                                NSLog(@"%@==%@==",outputURL.path,jpegData3);
                                
                                NSDictionary *params = @{@"detail":@{@"image":outputURL.path,
                                                                     @"url": url,
                                                                     @"time": timeIntervalString}};
                                NSLog(@"截图完成,把参数传递给uniapp %@", params);
                                self.loadGifImgView.hidden = YES;
//与uni-app交互方法,给uni-app传参
                                [strongSelf fireEvent:@"screenshotFinish" params:params domChanges:nil];
                                
                            }
                        } else {
                            NSLog(@"%@==", error.localizedDescription);
                        }
                    }];
                } else {
                    // Fallback on earlier versions
                }
                
            }
        }];
    } else {
        // Fallback on earlier versions
    }
}
- (void)snapshotForWKWebView:(WKWebView *)webView CaptureCompletionHandler:(void (^)(UIImage * _Nonnull))completionHandler {
    if ([[NSString stringWithFormat:@"%@",self->_loadURLStr] containsString:@"https://www.baidu.com"]) {
        //1.添加遮罩层
        UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES];
        snapshotView.frame = webView.frame;
        [webView.superview addSubview:snapshotView];
        //2.初始化数组
        self->_ImageAry = [NSMutableArray array];
        //3.进行截图操作
        CGPoint savedCurrentContentOffset = webView.scrollView.contentOffset;
        webView.scrollView.contentOffset = CGPointZero;
    NSLog(@"网页也高度=====%f===w:%f",webView.scrollView.contentSize.height,webView.scrollView.contentSize.width);
    
        [self createSnapshotForWKWebView:webView offset:0.0 remainingOffset_y:webView.scrollView.contentSize.height comletionBlock:^(UIImage *snapshotImg) {
            webView.scrollView.contentOffset = savedCurrentContentOffset;
            [snapshotView removeFromSuperview];
            UIImage *shotImg = [self captureScrollView:webView.scrollView];
            completionHandler(snapshotImg);
        }];
    } else {
        // 1.为了截图时对 frame 进行操作不会出现闪屏等现象,我们需要盖一个"假"的 webView 到现在的位置上,并将真正的 webView "摘下来"。调用 snapshotViewAfterScreenUpdates 即可得到这样一个"假"的 webView
        UIView *snapshotView = [webView snapshotViewAfterScreenUpdates:YES];
        snapshotView.frame = webView.frame;
        [webView.superview addSubview:snapshotView];

        // 2. 保存真正的 webView 的偏移、位置等信息,以便截图完成之后"还原现场"
        CGPoint currentOffset = webView.scrollView.contentOffset;
        CGRect currentFrame = webView.frame;
        UIView *currentSuperView = webView.superview;
        NSUInteger currentIndex = [webView.superview.subviews indexOfObject:webView];

        // 3. 用一个新的视图承载"真正的" webView,这个视图也是绘图所用到的上下文
        UIView *containerView = [[UIView alloc] initWithFrame:webView.bounds];
        [webView removeFromSuperview];
        [containerView addSubview:webView];

        // 4. 将 webView 按照实际内容高度和屏幕高度分成 page 页
        CGSize totalSize = webView.scrollView.contentSize;
        NSLog(@"%f",totalSize.height);
        [webView evaluateJavaScript:@"document.body.scrollHeight" completionHandler:^(id _Nullable result,NSError * _Nullable error) {
            CGFloat webViewHeight = [result doubleValue];
            NSLog(@"%f",webViewHeight);
        }];

        NSInteger page = ceil(totalSize.height / containerView.bounds.size.height);

        webView.scrollView.contentOffset = CGPointZero;
        [webView evaluateJavaScript:@"window.scrollTo(0,0)" completionHandler:nil];
        webView.frame = CGRectMake(0, 0, containerView.bounds.size.width, webView.scrollView.contentSize.height);

        UIGraphicsBeginImageContextWithOptions(totalSize, YES, UIScreen.mainScreen.scale);
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            [self drawContentPage:containerView webView:webView index:0 maxIndex:page completion:^{
                UIImage *snapshotImage = UIGraphicsGetImageFromCurrentImageContext();
                UIGraphicsEndImageContext();
                NSLog(@"生成的图片%ld==%@",(long)page,snapshotImage);
                //[webView removeFromSuperview];
                completionHandler(snapshotImage);
                [currentSuperView insertSubview:webView atIndex:currentIndex];
                webView.frame = currentFrame;
                webView.scrollView.contentOffset = currentOffset;
                // 8. 调用 UIGraphicsGetImageFromCurrentImageContext 方法从当前上下文中获取到完整截图,将第 2 步中保存的信息重新赋予到 webView 上,"还原现场"
                [snapshotView removeFromSuperview];
            }];
        });
    }
}

- (void)drawContentPage:(UIView *)targetView webView:(WKWebView *)webView index:(NSInteger)index maxIndex:(NSInteger)maxIndex completion:(dispatch_block_t)completion
{
    // 5. 得到每一页的实际位置,并将 webView 往上推到该位置
    CGRect splitFrame = CGRectMake(0, index * CGRectGetHeight(targetView.bounds), targetView.bounds.size.width, targetView.frame.size.height);
    CGRect myFrame = webView.frame;
    myFrame.origin.y = -(index * targetView.frame.size.height);
    webView.frame = myFrame;
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        // 6. 调用 drawViewHierarchyInRect 将当前位置的 webView 渲染到上下文中
        [targetView drawViewHierarchyInRect:splitFrame afterScreenUpdates:YES];
        // 7. 如果还未到达最后一页,则递归调用 drawViewHierarchyInRect 方法进行渲染;如果已经渲染完了全部页,则回调通知截图完成
        if (index < maxIndex) {
            [self drawContentPage:targetView webView:webView index:index + 1 maxIndex:maxIndex completion:completion];
        } else {
            completion();
        }
    });
}

/** 新的截图方法*/
- (void)createSnapshotForWKWebView:(WKWebView *)webView offset:(float)offset_y remainingOffset_y:(float)reOffset_y comletionBlock:(void(^)(UIImage *snapshotImg))completeBlock
{
    //判断scrollView是否已经滚动到底
    if (reOffset_y>0) {
        //设置
        [webView.scrollView setContentOffset:CGPointMake(0, offset_y) animated:NO];
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.5 * NSEC_PER_SEC)), dispatch_get_main_queue(),^{
            //对页面进行截图操作
            UIGraphicsBeginImageContextWithOptions(webView.frame.size, YES, [UIScreen mainScreen].scale);
            [webView.layer renderInContext:UIGraphicsGetCurrentContext()];
            UIImage * img = UIGraphicsGetImageFromCurrentImageContext();
            UIGraphicsEndImageContext();
            //将截图添加进数组
            [self->_ImageAry addObject:img];
            //修改offsetY偏移量
            CGFloat newOffset_y = offset_y + webView.scrollView.frame.size.height;
            CGFloat newReOffset_y = reOffset_y - webView.frame.size.height;
            [self createSnapshotForWKWebView:webView offset:newOffset_y remainingOffset_y:newReOffset_y comletionBlock:completeBlock];
            
        });
    }else {
        //合成截图为最终截图
        UIView * containerView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, webView.frame.size.width, webView.scrollView.contentSize.height)];
        CGFloat originYOfImgView = 0;
        for (int i = 0; i<self->_ImageAry.count; i++) {
            UIImageView * imgView = [[UIImageView alloc] initWithFrame:CGRectMake(0, originYOfImgView, webView.frame.size.width, webView.frame.size.height)];
            UIImage * img = self->_ImageAry[i];
            imgView.image = img;
            originYOfImgView += webView.frame.size.height;
            [containerView addSubview:imgView];
        }
        //添加合成视图
        [webView.superview addSubview:containerView];
        //处理最终合并截图
        UIGraphicsBeginImageContextWithOptions(containerView.frame.size, YES, [UIScreen mainScreen].scale);
        [containerView.layer renderInContext:UIGraphicsGetCurrentContext()];
        UIImage * img2 = UIGraphicsGetImageFromCurrentImageContext();
        UIGraphicsEndImageContext();
        //移除合成视图
        [containerView removeFromSuperview];
        //返回截图
        if (completeBlock) {
            completeBlock(img2);
        }
            
    }
}
- (UIImage *)captureScrollView:(UIScrollView *)scrollView

{
    UIImage* image = nil;

    UIGraphicsBeginImageContextWithOptions(scrollView.contentSize, NO, 0.0);

    {
        CGFloat scale = 1;

        CGPoint savedContentOffset = scrollView.contentOffset;

        CGRect savedFrame = scrollView.frame;

        scrollView.contentOffset = CGPointZero;

        scrollView.frame = CGRectMake(0, 0, scrollView.contentSize.width * scale, scrollView.contentSize.height * scale + 30);

        [scrollView.layer renderInContext:UIGraphicsGetCurrentContext()];


        image = UIGraphicsGetImageFromCurrentImageContext();

        scrollView.contentOffset = savedContentOffset;

        scrollView.frame = savedFrame;

    }

    UIGraphicsEndImageContext();

    

    if (image != nil) {
        return image;

    }

    return nil;

}
-(UIImage *)getImageFromView:(UIView *)view{
    CGSize s = view.bounds.size;
    // 下面方法,第一个参数表示区域大小。第二个参数表示是否是非透明的。如果需要显示半透明效果,需要传  NO,否则传YES。第三个参数就是屏幕密度了
    UIGraphicsBeginImageContextWithOptions(s, YES, [UIScreen mainScreen].scale);
    [view.layer renderInContext:UIGraphicsGetCurrentContext()];
    UIImage*image = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();
    return image;
}
//pdf转换成image方法
- (UIImage *)convertPDFDataToImage:(NSData *)pdfData {

    CGDataProviderRef provider = CGDataProviderCreateWithCFData((__bridge CFDataRef)pdfData);
    CGPDFDocumentRef pdfDocument = CGPDFDocumentCreateWithProvider(provider);

    if (pdfDocument == NULL) {
        NSLog(@"Could not create PDF document from data");
        return nil;
    }

    CGPDFPageRef pdfPage = CGPDFDocumentGetPage(pdfDocument, 1);

    if (pdfPage == NULL) {
        NSLog(@"Could not get first page of PDF document");
        return nil;
    }

    CGRect pdfPageRect = CGPDFPageGetBoxRect(pdfPage, kCGPDFMediaBox);
    UIGraphicsBeginImageContext(pdfPageRect.size);
    CGContextRef context = UIGraphicsGetCurrentContext();

    CGContextSetFillColorWithColor(context, [UIColor whiteColor].CGColor);
    CGContextFillRect(context, pdfPageRect);

    CGContextTranslateCTM(context, 0.0, pdfPageRect.size.height);
    CGContextScaleCTM(context, 1.0, -1.0);

    CGAffineTransform pdfTransform = CGPDFPageGetDrawingTransform(pdfPage, kCGPDFMediaBox, pdfPageRect, 0, YES);
    CGContextConcatCTM(context, pdfTransform);
    CGContextDrawPDFPage(context, pdfPage);

    UIImage *resultingImage = UIGraphicsGetImageFromCurrentImageContext();
    UIGraphicsEndImageContext();

    CGPDFDocumentRelease(pdfDocument);
    CGDataProviderRelease(provider);
    
    return resultingImage;
}

5:配置插件信息

uni-app网站有这些配置信息代码 搬过来就行,选中工程中的HBuilder-uniPlugin-Info.plist文件右键->Open As->Source Code找到dcloud_uniplugins节点,copy下面的内容添加到dcloud_uniplugins节点下,对应填写

xml 复制代码
<dict>
    <key>hooksClass</key>
    <string>填写 hooksClass 类名 </string>
    <key>plugins</key>
    <array>
        <dict>
            <key>class</key>
            <string>填写 module 或 component 的类名</string>
            <key>name</key>
            <string>填写暴露给js端对应的 module 或 component 名称</string>
            <key>type</key>
            <string>填写 module 或 component</string>
        </dict>
    </array>
</dict>

在 uni-app 项目中调用 module 方法

复制代码

6:导入uni-app资源

首先需要生成本地打包资源,在 HBuilderX 中选您的 uni-app 工程,右键->发现->原生App-本地打→生成本地打包App资源

项目编译完成后会在 HBuilderX 控制台输出资源存路径,点击路径会自动打开资源所在文件 将应用资源导入到插件开发主工程的HBuilder-Hello/Pandora/apps/中,如下图所示,直接拖进去即可

然后打开工程的 control.xml 文件,将 appid 改成 uni-app项目的 id,info.plist修改id

然后运行项目测试,如下图所示(能调到 module 的方法,并且可以获取 module 返回的数据,则说明功能正常)

7: 生成插件包

编译生成插件库文件(.framework 或 .a) 如下图所示,将编译工程选择为插件项目(DCTestUniPlugin),运行设备选择Generic iOS Device

然后点击Edit Scheme...在弹窗中,将Run->Info->Build Configuration切换到Release,然后点击Close关闭弹窗 然后在 Xcode 左侧目录中选中插件工程名,查看TARGETS->Build Settings->Architectures,确保 Build Active Architecture Only->Release 为 No Valid Architectures 中至少包含 arm64 和 armv7(一般保持工程默认配置即可),然后Command + B 编译运行工程 编译完成后,在插件工程 Products 下生成的库(DCTestUniPlugin.framework)即为插件所需要的依赖库文件,右键->Show in Finder,可打开库所在文件夹

8:编写page.JSON文件,生成标准的插件包

package.json 为插件的配置文件,配置了插件id、格式、插件资源以及插件所需权限等等信息,uni-app官网有标准格式,直接拷贝过来,填入相应的内容

json 复制代码
{
    "name": "TestUniPlugin",
    "id": "DCTestUniPlugin",
    "version": "1.0.0",
    "description": "uni示例插件",
    "_dp_type": "nativeplugin",
    "_dp_nativeplugin": {
        "ios": {
            "plugins": [{
                "type": "module",
                "name": "DCTestUniPlugin-TestModule",
                "class": "TestModule"
            }, {
                "type": "component",
                "name": "dc-testmap",
                "class": "TestComponent"
            }],
            "frameworks": ["MapKit.framework"],
            "integrateType": "framework",
            "deploymentTarget": "9.0"
        }
    }
}

插件id为名新建一个文件夹,将编辑好的 package.json 放进去,然后在文件夹中在新建一个 ios (小写)文件夹,将刚刚生成的依赖库(DCTestUniPlugin.framework)copy 到 ios 根目录,这样我们的插件包就构建完成了

9:使用插件

HBuilderX 的 uni-app 项目创建中"nativeplugins"目录(如不存在则创建)将插件配置到uni-app项目下的"nativeplugins"目录

将原生插件配置到uni-app项目的"nativeplugins"下,还需要在manifest.json文件的"App原生插件配置"项下点击"选择本地插件",在列表中选择需要打包生效的插件:

无论时真机测试与发布,都需要打包基座插件,打包流程如下图

截图插件完整流程到这里就结束了,关于截图功能就是iOS原生开发,这里并不过多描述,主要时把插件开发流程记录下来。

相关推荐
叽哥18 小时前
Flutter Riverpod上手指南
android·flutter·ios
用户092 天前
SwiftUI Charts 函数绘图完全指南
ios·swiftui·swift
YungFan2 天前
iOS26适配指南之UIColor
ios·swift
权咚2 天前
阿权的开发经验小集
git·ios·xcode
用户092 天前
TipKit与CloudKit同步完全指南
ios·swift
法的空间3 天前
Flutter JsonToDart 支持 JsonSchema
android·flutter·ios
2501_915918413 天前
iOS 上架全流程指南 iOS 应用发布步骤、App Store 上架流程、uni-app 打包上传 ipa 与审核实战经验分享
android·ios·小程序·uni-app·cocoa·iphone·webview
00后程序员张3 天前
iOS App 混淆与加固对比 源码混淆与ipa文件混淆的区别、iOS代码保护与应用安全场景最佳实践
android·安全·ios·小程序·uni-app·iphone·webview
Magnetic_h3 天前
【iOS】设计模式复习
笔记·学习·ios·设计模式·objective-c·cocoa
00后程序员张3 天前
详细解析苹果iOS应用上架到App Store的完整步骤与指南
android·ios·小程序·https·uni-app·iphone·webview