注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

震惊:苹果手机电池栏“黑白无常”

iOS
前言: 当程序员👨🏻‍💻遇到难以解决的bug时,大家都会说同样的口头禅:真是见了鬼了(建国后不可以) 现象: 手机电池栏左黑右白,如下图    👈🏻左边的时间是黑色的字体,右边的信号和电池是白色的字体👉🏻,这种感觉就像电池栏在呼喊: 我与你之...
继续阅读 »

前言:



当程序员👨🏻‍💻遇到难以解决的bug时,大家都会说同样的口头禅:真是见了鬼了(建国后不可以)



现象:



手机电池栏左黑右白,如下图













👈🏻左边的时间是黑色的字体,右边的信号和电池是白色的字体👉🏻,这种感觉就像电池栏在呼喊:


我与你之间虽只差一个灵动岛的距离,却已是黑白相隔


心路历程:


初步断定应该是UIStatusBarStyle的设置问题,查看App的infoplist文件发现确实有 View controller-based status bar appearance = YES的相关设置,有特殊需要的界面就需要自己手动处理一下


- (UIStatusBarStyle)preferredStatusBarStyle {
if (@avaliable(iOS 13.0,*)) {
return XXXX;
} else {
return XXXXX;
}
return XXXXXXX;
}

但是本着谁污染谁治理的原则,我没有特殊的场景我不处理,别的地方设置了也不应该影响我吧。再退一步来说,就算影响了,也不应该给我显示成这种左黑右白的鬼样子吧。不过产品说这个功能很高级,可以保留。玩笑归玩笑,问题还是得解决。


解决方案:


最先想到的肯定是给出问题的界面实现一下 preferredStatusBarStyle,效果确实不错,解决了,如图:












先解决了问题上线再说,就像罗永浩说的:












但是这该死的求知欲天天折磨着我,直到今天在搞包体积的时候,脚本检测到这个大的背景图,发现是从左往右渐变加深的,难道和图片有关系?本着试一试的原则,把图片删除的同时并且把preferredStatusBarStyle的代码注释掉,竟然好了,不可思议:












找设计师要了不带渐变的图片,又尝试了一把












对比俩种情况不难发现:


•无背景图,系统的导航栏显示的是黑色


•有背景图,系统的导航栏显示的是白色


💡💡 是不是UIKit对导航栏背景图做了监听?目的是为了让用户可以清晰的看到电池栏的信息?


带着这个猜测,去看了下去年的WWDC,果然找到了答案:



在iOS17中,default样式会根据内容的深浅调整status bar的颜色。



由于没有手动处理preferredStatusBarStyle,而背景图又是从左到右渐变加深,所以电池栏显示成了左黑右白。


后语:


由此可见:


1、遇到难以解决的问题,把锅甩给系统bug是多么的机智🐶;


2、建国后还真的是:





吴京达咩是什么梗-抖音





参考链接:


developer.apple.com/videos/play…


作者:京东云开发者
来源:juejin.cn/post/7344710026853007394
收起阅读 »

编写LLVM Pass

iOS
的基础上,编写一个简单的LLVM Pass。在llvm-project-17.0.6.src/llvm/include/llvm/Transforms/SweetWound/目录下,新建ModuleTest.h文件,并写入如下代码:// ModuleTest....
继续阅读 »

上一篇的基础上,编写一个简单的LLVM Pass。

  1. llvm-project-17.0.6.src/llvm/lib/Transforms/目录下,新建一个文件夹SweetWound


  1. 在在llvm-project-17.0.6.src/llvm/include/llvm/Transforms/目录下,新建一个文件夹SweetWound


  1. Transforms目录下的CMakeLists.txt文件末尾,增加如下代码:

...
add_subdirectory(SweetWound)


  1. llvm-project-17.0.6.src/llvm/include/llvm/Transforms/SweetWound/目录下,新建ModuleTest.h文件,并写入如下代码:

// ModuleTest.h
#ifndef _LLVM_TRANSFORMS_SWEETWOUND_H_
#define _LLVM_TRANSFORMS_SWEETWOUND_H_
#include "llvm/Pass.h"
#include "llvm/IR/PassManager.h"
#include "llvm/IR/Module.h"

namespace llvm {
class ModuleTestPass : public PassInfoMixin {
public:
bool flag;
ModuleTestPass(bool flag) {
this->flag = flag;
}
PreservedAnalyses run(Module &M, ModuleAnalysisManager &AM);
static bool isRequired() {
return true;
}
};
}

#endif


  1. llvm-project-17.0.6.src/llvm/lib/Transforms/SweetWound/目录下,创建ModuleTest.cpp文件,并写入如下代码:

// ModuleTest.cpp
#include "llvm/Transforms/SweetWound/ModuleTest.h"

using namespace llvm;

PreservedAnalyses ModuleTestPass::run(Module &M, ModuleAnalysisManager &AM) {
if (this->flag == true) {
outs() << "[SW]:" << M.getName() << "\n";
return PreservedAnalyses::none();
}
return PreservedAnalyses::all();
}


  1. llvm-project-17.0.6.src/llvm/lib/Transforms/SweetWound/目录下,创建CMakeLists.txt文件,并写入如下代码:

add_llvm_component_library(LLVMSweetWound
ModuleTest.cpp

LINK_COMPONENTS
Analysis
Core
Support
TransformUtils
)


  1. 修改llvm-project-17.0.6.src/llvm/lib/Passes/PassBuilder.cpp文件:

......
#include
//======================导入头文件======================//
#include "llvm/Transforms/SweetWound/ModuleTest.h"
......
// ======================增加编译参数 begin ======================//
static cl::opt s_sw_test("test", cl::init(false), cl::desc("test module pass."));
// ======================增加编译参数 end ========================//

PassBuilder::PassBuilder(TargetMachine *TM, PipelineTuningOptions PTO,
std::optional PGOOpt,
PassInstrumentationCallbacks *PIC)
: TM(TM), PTO(PTO), PGOOpt(PGOOpt), PIC(PIC) {
......
// 注册Pass
this->registerPipelineStartEPCallback(
[](llvm::ModulePassManager &MPM, llvm::OptimizationLevel Level) {
MPM.addPass(ModuleTestPass(s_sw_test));
}
);
}
  1. 重新执行编译脚本,成功后,替换LLVM17.0.6.xctoolchain文件。

  2. 在Xcode的Build Settings-->Other C Flags中,设置编译参数:-mllvm -test:


  1. Command + B编译(或Command + R运行):


可以看到每个编译单元都有对应的输出,即代表编写的LLVM Pass加载成功!!!
收起阅读 »

编译llvm源码

iOS
前往LLVM官网,下载LLVM17.0.6版本的源码:下载源码后,解压到任意目录:在llvm-project-17.0.6.src同级目录下,编写编译脚本build.sh:#!/bin/shpwd_path=`pwd`build_llvm=${pwd_path...
继续阅读 »
  1. 前往LLVM官网,下载LLVM17.0.6版本的源码

  1. 下载源码后,解压到任意目录:


  1. llvm-project-17.0.6.src同级目录下,编写编译脚本build.sh:

#!/bin/sh
pwd_path=`pwd`
build_llvm=${pwd_path}/build-llvm #编译目录
installprefix=${pwd_path}/install #install目录
llvm_project=${pwd_path}/llvm-project-17.0.6.src/llvm #项目目录

mkdir -p $build_llvm
mkdir -p $installprefix

cmake -G Ninja -S ${llvm_project} -B $build_llvm \
-DLLVM_ENABLE_PROJECTS="clang" \
-DLLVM_CREATE_XCODE_TOOLCHAIN=ON \
-DLLVM_INSTALL_UTILS=ON \
-DCMAKE_INSTALL_PREFIX=$installprefix \
-DCMAKE_BUILD_TYPE=Release

ninja -C $build_llvm install-xcode-toolchain


  1. 执行编译脚本:

$ chmod +x ./build.sh
$ ./build.sh

编译过程需要大约20分钟左右。

  1. 编译完成之后,即可在当前目录下的install目录下看到编译产物:



  1. LLVM17.0.6.xctoolchain文件复制到~/Library/Developer/Toolchains/目录下:


  1. 点击菜单栏Xcode——>Toolchains,选择org.llvm.17.0.6:



  1. 在Xcode的Build Settings中,关闭Enable Index-While-Building Functionality


  1. Command+B编译(或Command + R 运行):


收起阅读 »

iOS 组件开发教程——手把手轻松实现灵动岛

1、先在项目里创建一个Widget Target2、一定要勾选 Include live Activity,然后输入名称,点击完成既可。3、在 Info.plist 文件中声明开启,打开 Info.plist 文件添加 NSSupportsLiveActivi...
继续阅读 »

1、先在项目里创建一个Widget Target


2、一定要勾选 Include live Activity,然后输入名称,点击完成既可。


3、在 Info.plist 文件中声明开启,打开 Info.plist 文件添加 NSSupportsLiveActivities,并将其布尔值设置为 YES。

4、我们创建一个IMAttributes,

struct IMAttributes: ActivityAttributes {
public typealias IMStatus = ContentState

public struct ContentState: Codable, Hashable {
var callName: String
var imageStr : String
var callingTimer: ClosedRange<Date>
}

var callName: String
var imageStr : String
var callingTimer: ClosedRange<Date>
}

5、灵动岛界面配置

struct IMActivityWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: IMAttributes.self) { context in
// 创建显示在锁定屏幕上的演示,并在不支持动态岛的设备的主屏幕上作为横幅。
// 展示锁屏页面的 UI

} dynamicIsland: { context in
// 创建显示在动态岛中的内容。
DynamicIsland {
//这里创建拓展内容(长按灵动岛)
DynamicIslandExpandedRegion(.leading) {
Label(context.state.callName, systemImage: "person")
.font(.caption)
.padding()
}
DynamicIslandExpandedRegion(.trailing) {
Label {
Text(timerInterval: context.state.callingTimer, countsDown: false)
.multilineTextAlignment(.trailing)
.frame(width: 50)
.monospacedDigit()
.font(.caption2)
} icon: {
Image(systemName: "timer")
}
.font(.title2)
}
DynamicIslandExpandedRegion(.center) {
Text("\(context.state.callName) 正在通话中...")
.lineLimit(1)
.font(.caption)
.foregroundColor(.secondary)
}

}
//下面是紧凑展示内容区(只展示一个时的视图)
compactLeading: {
Label {
Text(context.state.callName)

} icon: {
Image(systemName: "person")
}
.font(.caption2)
} compactTrailing: {
Text(timerInterval: context.state.callingTimer, countsDown: true)
.multilineTextAlignment(.center)
.frame(width: 40)
.font(.caption2)
}
//当多个Live Activities处于活动时,展示此处极小视图
minimal: {
VStack(alignment: .center) {
Image(systemName: "person")


}
}
.keylineTint(.accentColor)
}
}
}

6、在需要的地方启动的地方调用,下面是启动灵动岛的代码

        let imAttributes = IMAttributes(callName: "wqd", imageStr:"¥99", callingTimer: Date()...Date().addingTimeInterval(0))

//初始化动态数据
let initialContentState = IMAttributes.IMStatus(callName: name, imageStr: "ia.imageStr", callingTimer: Date()...Date().addingTimeInterval(0))

do {
//启用灵动岛
//灵动岛只支持Iphone,areActivitiesEnabled用来判断设备是否支持,即便是不支持的设备,依旧可以提供不支持的样式展示
if #available(iOS 16.1, *) {
if ActivityAuthorizationInfo().areActivitiesEnabled == true{

}
} else {
// Fallback on earlier versions
}
let deliveryActivity = try Activity<IMAttributes>.request(
attributes: imAttributes,
contentState: initialContentState,
pushType: nil)
//判断启动成功后,获取推送令牌 ,发送给服务器,用于远程推送Live Activities更新
//不是每次启动都会成功,当已经存在多个Live activity时会出现启动失败的情况
if deliveryActivity.activityState == .active{
_ = deliveryActivity.pushToken
}
// deliveryActivity.pushTokenUpdates //监听token变化
print("Current activity id -> \(deliveryActivity.id)")
} catch (let error) {
print("Error info -> \(error.localizedDescription)")
}
6.此处只有一个灵动岛,当一个项目有多个灵动岛时,需要判断更新对应的activity

func update(name:String) {
Task {

let updatedDeliveryStatus = IMAttributes.IMStatus(callName: name, imageStr: "ia.imageStr", callingTimer: Date()...Date().addingTimeInterval(0))

for activity in Activity<IMAttributes>.activities{
await activity.update(using: updatedDeliveryStatus)
}
}
}

7、停止灵动岛

func stop() {
Task {
for activity in Activity<IMAttributes>.activities{
await activity.end(dismissalPolicy: .immediate)
}
}
}


收起阅读 »

某运动APP的登录协议分析

iOS
前言 最近在尝试逆向方向相关的探索,针对某款运动APP的登录协议进行了分析,此文记录一下分析过程与结果,仅供学习研究,使用的工具较少,内容也比较简单,新手项,大佬请跳过。针对密码登录模块进行分析,随便输入一个手机号与密码,后续使用抓包工具分析,针对登录协议的几...
继续阅读 »

前言


最近在尝试逆向方向相关的探索,针对某款运动APP的登录协议进行了分析,此文记录一下分析过程与结果,仅供学习研究,使用的工具较少,内容也比较简单,新手项,大佬请跳过。针对密码登录模块进行分析,随便输入一个手机号与密码,后续使用抓包工具分析,针对登录协议的几个字段从学习角度还是值得看下实现逻辑的。


抓包



  1. 抓包使用 Charles,请自行安装并配置证书

  2. 抓取登陆接口,点击密码登陆。使用假账密测试抓包,能够抓包成功
    image-20230807174512922.png


Sign分析


首先能看到请求头里面有sign字段,针对该字段进行分析:



sign: b61df9a8bce7a8641c5ca986b55670e633a7ab29



整体长度为40,常用的MD5长度为32,第一反应不太像,但是也有可能md5以后再拼接其它字段,sha1散列函数的长度是40,正好吻合。那我们就一一验证,先看下是否有MD5的痕迹,直接写脚本frida试着跑下。 脚本内容比较明确,针对MD5的Init、Update、Final分别hook打印看下输入与输出,下面给到关键代码:


   // hook CC_MD5
   // unsigned char * CC_MD5(const void *data, CC_LONG len, unsigned char *md);
   Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_funcName), {
       onEnterfunction(args) {
           console.log(g_funcName + " begin");
           var len = args[1].toInt32();
           console.log("input:");
           dumpBytes(args[0], len);
           this.md = args[2];
      },
       onLeavefunction(retval) {
           console.log(g_funcName + " return value");
           dumpBytes(this.md, g_funcRetvalLength);

           console.log(g_funcName + ' called from:\n' +
               Thread.backtrace(this.contextBacktracer.ACCURATE)
              .map(DebugSymbol.fromAddress).join('\n') + '\n');
      }
  });

   // hook CC_MD5_Update
   // int CC_MD5_Update(CC_MD5_CTX *c, const void *data, CC_LONG len);
   Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_updateFuncName), {
       onEnterfunction(args) {
           console.log(g_updateFuncName + " begin");
           var len = args[2].toInt32();
           console.log("input:");
           dumpBytes(args[1], len);
      },
       onLeavefunction(retval) {
           console.log(g_updateFuncName + ' called from:\n' +
               Thread.backtrace(this.contextBacktracer.ACCURATE)
              .map(DebugSymbol.fromAddress).join('\n') + '\n');
      }
  });

   // hook CC_MD5_Final
   // int CC_MD5_Final(unsigned char *md, CC_MD5_CTX *c);
   Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", g_finalFuncName), {
       onEnterfunction(args) {
           //console.log(func.name + " begin");
           finalArgs_md = args[0];
      },
       onLeavefunction(retval) {
           console.log(g_finalFuncName + " return value");
           dumpBytes(finalArgs_md, g_funcRetvalLength);

           console.log(g_finalFuncName + ' called from:\n' +
               Thread.backtrace(this.contextBacktracer.ACCURATE)
              .map(DebugSymbol.fromAddress).join('\n') + '\n');
      }
  });

很幸运,在打印中明显看到了sign相关的内容打印,但是缺少sign的后面一部分,那就明确sign值的构成为32(md5)+8,先看下md5的数据构造过程。



b61df9a8bce7a8641c5ca986b55670e6 33a7ab29



image-20230807174427349.png
通过打印可以明确的看到,sign的MD5由三部分数据组成,分别为:bodyData+Url+Str,body数据也可从Charles获取到。



  • {"body":"5gJEXtLqe3tzRsP8a/bSwehe0ta3zQx6wG7K74sOeXQ6Auz1NI1bg68wNLmj1e5Xl7CIwWelukC445W7HXxJY6nQ0v0SUg1tVyWS5L8E2oaCgoSeC6ypFNXV2xVm8hHV"}

  • /account/v4/login/password

  • V1QiLCJhbGciOiJIUzI1NiJ9
    image-20230807174635667.png
    到这里有一个疑问,数据的第三部分:V1QiLCJhbGciOiJIUzI1NiJ9,该值是固定的字符串还是每次都变化的?猜测应该是固定的字符串,作为MD5的Salt值来使用,我们再次请求验证一下。
    image-20230807181042213.png
    新的sign值为:131329a5af4ecb025fb5088615d5e5c526dbd1a3,通过脚本打印的数据能确认第三部分为固定字符串。
    MD5({"body":"12BcOSg50nLxdbt++r7liZpeyWAVpmihTy8Zu8BmpA6a1hqdevS5PPYwnbtpjN05xgeyReSihh9idyfriR6qx1Fbo8AA0k8HQt6gJ3spWITI21GhLTzh9PDUkgjCtrEK"}/account/v4/login/passwordV1QiLCJhbGciOiJIUzI1NiJ9)
    image-20230807181119463.png


Sign尾部分析


接下来我们针对Sign的尾部数据进行分析,单纯盲猜或者挂frida脚本已经解决不了问题了,我们用IDA看下具体的实现逻辑,当然上面的MD5分析也可以直接从IDA反编译入手,通过搜索sign关键字进行定位,只是我习惯先钩一下脚本,万一直接命中就不用费时间去分析了...


通过MD5的脚本打印,我们也能看到相关的函数调用栈,这对于我们快速定位也提供了很大的方便。我们直接搜索 [KEPPostSecuritySign kep_signWithURL: body:] 方法,可以看到明显的字符串拼接的痕迹,IDA还是比较智能的,已经识别出了MD5的salt值。
1031691552576_.pic.jpg
通过分析,定位到[NSString kep_networkStringOffsetSecurity]函数,在内部进行了字符串的处理,在循环里面进行了各种判断以及移位操作,不嫌麻烦的话可以分析一下逻辑,重写一下处理流程。
1041691552577_.pic.jpg
我这边处理比较暴力,发现kep_networkStringOffsetSecurity是NSString的Catetory,那就直接调用验证一下吧,使用frida挂载以后,找到NSString类,调用方法传入md5之后的值,然后就会发现经过该函数,神奇的sign值就给到了。
image-20230809113620190.png


x-ads分析


分析完sign以后,观察到还有一个x-ads的字段,按照惯例,先用脚本试着钩一下,经常采用的加密大致就是DES、AES或RC4这些算法。
image-20230807191005439.png
针对 AES128、DES、3DES、CAST、RC4、RC2、Blowfish等加密算法进行hook,脚本的关键代码如下:


var handlers = {
   CCCrypt: {
       onEnterfunction(args) {
           var operation = CCOperation[args[0].toInt32()];
           var alg = CCAlgorithm[args[1].toInt32()].name;
           this.options = CCoptions[args[2].toInt32()];
           var keyBytes = args[3];
           var keyLength = args[4].toInt32();
           var ivBuffer = args[5];
           var inBuffer = args[6];
           this.inLength = args[7].toInt32();
           this.outBuffer = args[8];
           var outLength = args[9].toInt32();
           this.outCountPtr = args[10];
           if (this.inLength < MIN_LENGTH || this.inLength > MAX_LENGTH){
           return;
          }
           if (operation === "kCCEncrypt") {
               this.operation = "encrypt"
               console.log("***************** encrypt begin **********************");
          } else {
               this.operation = "decrypt"
               console.log("***************** decrypt begin **********************");
          }
           console.log("CCCrypt(" +
               "operation: " + this.operation + ", " +
               "CCAlgorithm: " + alg + ", " +
               "CCOptions: " + this.options + ", " +
               "keyBytes: " + keyBytes + ", " +
               "keyLength: " + keyLength + ", " +
               "ivBuffer: " + ivBuffer + ", " +
               "inBuffer: " + inBuffer + ", " +
               "inLength: " + this.inLength + ", " +
               "outBuffer: " + this.outBuffer + ", " +
               "outLength: " + outLength + ", " +
               "outCountPtr: " + this.outCountPtr + ")"
          );

           //console.log("Key: utf-8 string:" + ptr(keyBytes).readUtf8String())
           //console.log("Key: utf-16 string:" + ptr(keyBytes).readUtf16String())
           console.log("key: ");
           dumpBytes(keyBytes, keyLength);

           console.log("IV: ");
           // ECB模式不需要iv,所以iv是null
           dumpBytes(ivBuffer, keyLength);

           var isOutput = true;
           if (!SHOW_PLAIN_AND_CIPHER && this.operation == "decrypt") {
            isOutput = false;
          }

           if (isOutput){
           // Show the buffers here if this an encryption operation
            console.log("In buffer:");
            dumpBytes(inBuffer, this.inLength);
          }
           
      },
       onLeavefunction(retVal) {
       // 长度过长和长度太短的都不要输出
           if (this.inLength < MIN_LENGTH || this.inLength > MAX_LENGTH){
           return;
          }
           var isOutput = true;
           if (!SHOW_PLAIN_AND_CIPHER && this.operation == "encrypt") {
            isOutput = false;
          }
           if (isOutput) {
            // Show the buffers here if this a decryption operation
            console.log("Out buffer:");
            dumpBytes(this.outBufferMemory.readUInt(this.outCountPtr));
          }
           // 输出调用堆栈,会识别类名函数名,非常好用
           console.log('CCCrypt called from:\n' +
               Thread.backtrace(this.contextBacktracer.ACCURATE)
              .map(DebugSymbol.fromAddress).join('\n') + '\n');
      }
  },
};


if (ObjC.available) {
   console.log("frida attach");
   for (var func in handlers) {
   console.log("hook " + func);
       Interceptor.attach(Module.findExportByName("libcommonCrypto.dylib", func), handlers[func]);
  }
else {
   console.log("Objective-C Runtime is not available!");
}

查看脚本的输出日志,直接命中了AES128的加密算法,并且输出的Base64数据完全匹配,只能说运气爆棚。
image-20230807191141136.png
拿到对应的key跟iv,尝试解密看下也是没问题的。x-ads分析结束,都不用反编译看代码:)
image-20230807190921956.png


Body的分析


最后看下sign值的组成部分,body数据是怎么计算的,抱着试试的想法,直接用x-ads分析得到的算法以及对应的key、iv进行解密:



{ "body": "5gJEXtLqe3tzRsP8a/bSwXDiK0VslZZZyOEj1jBDBhtYTGGdWltuIjLbzwZ2OxMcb3mFX7bJtgH3WlqGET5W34P4dTEIDhLH6FkT3HSLaDnEXYHvEl9IZRQKf19wMG/t" }



image-20230807183413168.png
这次说不上什么运气爆棚了...只能说开发者比较懒或者安全意识有点差了,使用了AES-CBC模式,iv都不改变一下的...


总结


这次分析整体来看,没什么技术含量,大部分都是脚本直接解决了,从结果来看,也是使用的常规的加密、签名算法,这也从侧面给我们安全开发提个醒,是不是可以有策略性的改变一下,比如我们拿MD5来看下都可以做哪些改变。



opensource.apple.com/source/ppp/…



首先针对MD5Init,我们可以改变它的初始化数据:


void MD5Init (mdContext)
MD5_CTX *mdContext;
{
 mdContext->i[0] = mdContext->i[1] = (UINT4)0;

 /* Load magic initialization constants.
  */

 mdContext->buf[0] = (UINT4)0x67452301;
 mdContext->buf[1] = (UINT4)0xefcdab89;
 mdContext->buf[2] = (UINT4)0x98badcfe;
 mdContext->buf[3] = (UINT4)0x10325476;
}

其次针对Transform我们也可以改变其中的某几个数据:


static void Transform (buf, in)
UINT4 *buf;
UINT4 *in;
{
 UINT4 a = buf[0]b = buf[1], c = buf[2], d = buf[3];

 /* Round 1 */
#define S11 7
#define S12 12
#define S13 17
#define S14 22
 FF ( ab, c, d, in[ 0], S11, UL(3614090360)); /* 1 */
 FF ( d, ab, c, in[ 1], S12, UL(3905402710)); /* 2 */
 FF ( c, d, ab, in[ 2], S13, UL606105819)); /* 3 */
 FF ( b, c, d, a, in[ 3], S14, UL(3250441966)); /* 4 */
 FF ( ab, c, d, in[ 4], S11, UL(4118548399)); /* 5 */
 FF ( d, ab, c, in[ 5], S12, UL(1200080426)); /* 6 */
 FF ( c, d, ab, in[ 6], S13, UL(2821735955)); /* 7 */
 FF ( b, c, d, a, in[ 7], S14, UL(4249261313)); /* 8 */
 FF ( ab, c, d, in[ 8], S11, UL(1770035416)); /* 9 */
 FF ( d, ab, c, in[ 9], S12, UL(2336552879)); /* 10 */
 FF ( c, d, ab, in[10], S13, UL(4294925233)); /* 11 */
 FF ( b, c, d, a, in[11], S14, UL(2304563134)); /* 12 */
 FF ( ab, c, d, in[12], S11, UL(1804603682)); /* 13 */
 FF ( d, ab, c, in[13], S12, UL(4254626195)); /* 14 */
 FF ( c, d, ab, in[14], S13, UL(2792965006)); /* 15 */
 FF ( b, c, d, a, in[15], S14, UL(1236535329)); /* 16 */

 /* Round 2 */
#define S21 5
#define S22 9
#define S23 14
#define S24 20
 GG ( ab, c, d, in[ 1], S21, UL(4129170786)); /* 17 */
 GG ( d, ab, c, in[ 6], S22, UL(3225465664)); /* 18 */
 
...
 

简单的变形以后,即使脚本能hook到对应的函数,但是想直接脱机调用结果还是不可以的,此时就要不得不进行反编译分析或者动态调试,此时配合代码混淆、VMP等静态防护手段,再加上反调试等安全手段,对于攻击的门槛也相应的提高。


作者:Daemon_S
来源:juejin.cn/post/7265036888431558675
收起阅读 »

如何在 SwiftUI 中实现音频图表

iOS
前言 在可访问性方面,图表是复杂的事物之一。iOS 15 引入了一项名为“音频图表”的新功能。 下面我们将学习如何通过使用 accessibilityChartDescriptor 视图修饰符为任何 SwiftUI 视图构建音频表示,呈现类似自定义条形图视图或...
继续阅读 »

前言


在可访问性方面,图表是复杂的事物之一。iOS 15 引入了一项名为“音频图表”的新功能。


下面我们将学习如何通过使用 accessibilityChartDescriptor 视图修饰符为任何 SwiftUI 视图构建音频表示,呈现类似自定义条形图视图或图像的图表。


DataPoint 结构体


让我们从在 SwiftUI 中构建一个简单的条形图视图开始,该视图使用垂直条形显示一组数据点。


struct DataPoint: Identifiable {
let id = UUID()
let label: String
let value: Double
let color: Color
}

在这里,我们有一个 DataPoint 结构,用于描述条形图视图中的条形。它具有 id、标签、数值和填充颜色。


BarChartView 结构体


接下来,我们可以定义一个条形图视图,它接受一组 DataPoint 结构体实例并将它们显示出来。


struct BarChartView: View {
let dataPoints: [DataPoint]

var body: some View {
HStack(alignment: .bottom) {
ForEach(dataPoints) { point in
VStack {
RoundedRectangle(cornerRadius: 8, style: .continuous)
.fill(point.color)
.frame(height: point.value * 50)
Text(point.label)
}
}
}
}
}

如上例所示,我们有一个 BarChartView,它接收一组 DataPoint 实例并将它们显示为水平堆栈中不同高度的圆角矩形。


ContentView 结构体


我们能够在 SwiftUI 中轻松构建条形图视图。接下来让我们尝试使用带有示例数据的新 BarChartView


struct ContentView: View {
@State private var dataPoints = [
DataPoint(label: "1", value: 3, color: .red),
DataPoint(label: "2", value: 5, color: .blue),
DataPoint(label: "3", value: 2, color: .red),
DataPoint(label: "4", value: 4, color: .blue),
]

var body: some View {
BarChartView(dataPoints: dataPoints)
.accessibilityElement()
.accessibilityLabel("Chart representing some data")
}
}

在这里,我们创建了一组 DataPoint 实例的示例数组,并将其传递给 BarChartView。我们还为图表创建了一个可访问元素,并禁用了其子元素的可访问性信息。为了改进图表视图的可访问性体验,我们还添加了可访问性标签。


最后,我们可以开始为我们的条形图视图实现音频图表功能。音频图表可以通过旋钮菜单获得。要使用旋钮,请在 iOS 设备的屏幕上旋转两个手指,就像您在拨盘。VoiceOver 会说出第一个旋钮选项。继续旋转手指以听到更多选项。松开手指选择音频图表。然后在屏幕上上下滑动手指以导航。


音频图表允许用户使用音频组件理解和解释图表数据。VoiceOver 在移动到图表视图中的条形时播放具有不同音调的声音。VoiceOver 对于更大的值使用高音调,对于较小的值使用低音调。这些音调代表数组中的数据。


实现协议


现在,我们可以讨论在 BarChartView 中实现此功能的方法。首先,我们必须创建一个符合 AXChartDescriptorRepresentable 协议的类型。AXChartDescriptorRepresentable 协议只有一个要求,即创建 AXChartDescriptor 类型的实例。AXChartDescriptor 类型的实例表示我们图表中的数据,以 VoiceOver 可以理解和交互的格式呈现。


extension ContentView: AXChartDescriptorRepresentable {
func makeChartDescriptor() -> AXChartDescriptor {
let xAxis = AXCategoricalDataAxisDescriptor(
title: "Labels",
categoryOrder: dataPoints.map(\.label)
)

let min = dataPoints.map(\.value).min() ?? 0.0
let max = dataPoints.map(\.value).max() ?? 0.0

let yAxis = AXNumericDataAxisDescriptor(
title: "Values",
range: min...max,
gridlinePositions: []
) { value in "\(value) points" }

let series = AXDataSeriesDescriptor(
name: "",
isContinuous: false,
dataPoints: dataPoints.map {
.init(x: $0.label, y: $0.value)
}
)

return AXChartDescriptor(
title: "Chart representing some data",
summary: nil,
xAxis: xAxis,
yAxis: yAxis,
additionalAxes: [],
series: [series]
)
}
}

我们所需做的就是符合 AXChartDescriptorRepresentable 协议,并添加 makeChartDescriptor 函数,该函数返回 AXChartDescriptor 的实例。


首先,我们通过使用 AXCategoricalDataAxisDescriptorAXNumericDataAxisDescriptor 类型定义 X 轴和 Y 轴。我们希望在 X 轴上使用字符串标签,这就是为什么我们使用 AXCategoricalDataAxisDescriptor 类型的原因。在线图的情况下,我们将在两个轴上都使用 AXNumericDataAxisDescriptor 类型。


实现线图


接下来,我们使用 AXDataSeriesDescriptor 类型定义图表中的点。有一个 isContinuous 参数,允许我们定义不同的图表样式。例如,对于条形图,它应该是 false,而对于线图,它应该是 true。


struct ContentView: View {
@State private var dataPoints = [
DataPoint(label: "1", value: 3, color: .red),
DataPoint(label: "2", value: 5, color: .blue),
DataPoint(label: "3", value: 2, color: .red),
DataPoint(label: "4", value: 4, color: .blue),
]

var body: some View {
BarChartView(dataPoints: dataPoints)
.accessibilityElement()
.accessibilityLabel("Chart representing some data")
.accessibilityChartDescriptor(self)
}
}

作为最后一步,我们使用 accessibilityChartDescriptor 视图修饰符将符合 AXChartDescriptorRepresentable 协议的实例设置为描述我们图表的实例。


示例截图:



总结


音频图表功能对于视力受损的用户来说是一项重大改进。音频图表功能的好处是,可以将其用于任何您想要的视图,甚至包括图像视图。只需创建 AXChartDescriptor 类型的实例。


作者:Swift社区
来源:juejin.cn/post/7301496834232401959
收起阅读 »

iOS 判断系统版本

iOS
方案一 double systemVersion = [UIDevice currentDevice].systemVersion.boolValue; if (systemVersion >= 7.0) { // >= iOS 7.0 ...
继续阅读 »

方案一


double systemVersion = [UIDevice currentDevice].systemVersion.boolValue;

if (systemVersion >= 7.0) {
// >= iOS 7.0
} else {
// < iOS 7.0
}

if (systemVersion >= 10.0) {
// >= iOS 10.0
} else {
// < iOS 10.0
}

如果只是大致判断是哪个系统版本,上面的方法是可行的,如果具体到某个版本,如 10.0.1,那就会有偏差。我们知道 systemVersion 依旧是10.0。


方案二


NSString *systemVersion = [UIDevice currentDevice].systemVersion;
NSComparisonResult comparisonResult = [systemVersion compare:@"10.0.1" options:NSNumericSearch];

if (comparisonResult == NSOrderedAscending) {
// < iOS 10.0.1
} else if (comparisonResult == NSOrderedSame) {
// = iOS 10.0.1
} else if (comparisonResult == NSOrderedDescending) {
// > iOS 10.0.1
}

// 或者

if (comparisonResult != NSOrderedAscending) {
// >= iOS 10.0.1
} else {
// < iOS 10.0.1
}

有篇博客提到这种方法不靠谱。比如系统版本是 10.1.1,而我们提供的版本是 8.2,会返回NSOrderedAscending,即认为 10.1.1 < 8.2 。


其实,用这样的比较方式 NSComparisonResult comparisonResult = [systemVersion compare:@"10.0.1"],的确会出现这种情况,因为默认是每个字符逐个比较,即 1(0.1.1) < 8(.2),结果可想而知。但我是用 NSNumericSearch 方式比较的,即数值的比较,不是字符比较,也不需要转化成NSValue(NSNumber) 再去比较。


方案三


if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_7_0) {
// >= iOS 7.0
} else {
// < iOS 7.0
}

// 或者

if (kCFCoreFoundationVersionNumber >= kCFCoreFoundationVersionNumber_iOS_7_0) {
// >= iOS 7.0
} else {
// < iOS 7.0
}

这些宏定义是 Apple 预先定义好的,如下:


#if TARGET_OS_IPHONE
...
#define NSFoundationVersionNumber_iOS_9_4 1280.25
#define NSFoundationVersionNumber_iOS_9_x_Max 1299
#endif


细心的童靴可能已经发现问题了。Apple 没有提供 iOS 10 以后的宏?,我们要判断iOS10.0以后的版本该怎么做呢?
有篇博客中提到,iOS10.0以后版本号提供了,并且逐次降低了,并提供了依据。


#if TARGET_OS_MAC
#define NSFoundationVersionNumber10_1_1 425.00
#define NSFoundationVersionNumber10_1_2 425.00
#define NSFoundationVersionNumber10_1_3 425.00
#define NSFoundationVersionNumber10_1_4 425.00
...
#endif


我想这位童鞋可能没仔细看, 这两组宏是分别针对iPhone和macOS的,不能混为一谈的。


所以也只能像下面的方式来大致判断iOS 10.0, 但之前的iOS版本是可以准确判断的。


if (NSFoundationVersionNumber > floor(NSFoundationVersionNumber_iOS_9_x_Max)) {
// > iOS 10.0
} else {
// <= iOS 10.0
}

方案四


在iOS8.0中,Apple也提供了NSProcessInfo 这个类来检测版本问题。


@property (readonly) NSOperatingSystemVersion operatingSystemVersion NS_AVAILABLE(10_10, 8_0);
- (BOOL) isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion)version NS_AVAILABLE(10_10, 8_0);

所以这样检测:


if ([[NSProcessInfo processInfo] isOperatingSystemAtLeastVersion:(NSOperatingSystemVersion){.majorVersion = 8, .minorVersion = 3, .patchVersion = 0}]) {
// >= iOS 8.3
} else {
// < iOS 8.3
}

用来判断iOS 10.0以上的各个版本也是没有问题的,唯一的缺点就是不能准确版本是哪个版本,当然这种情况很少。如果是这种情况,可以通过字符串的比较判断。


方案五


通过判断某种特定的类有没有被定义,或者类能不能响应哪个特定版本才有的方法。
比如,UIAlertController 是在iOS 8.0才被引进来的一个类,我们这个依据来判断版本


if (NSClassFromString(@"UIAlertController")) {
// >= iOS 8.0
} else {
// < iOS 8.0
}

说到这里,就顺便提一下在编译期间如何进行版本控制,依然用UIAlertController 来说明。


NS_CLASS_AVAILABLE_IOS(8_0) @interface UIAlertController : UIViewController

NS_CLASS_AVAILABLE_IOS(8_0) 这个宏说明,UIAlertController 是在iOS8.0才被引进来的API,那如果我们在iOS7.0上使用,应用程序就会挂掉,那么如何在iOS8.0及以后的版本使用UIAlertController ,而在iOS8.0以前的版本中仍然使用UIAlertView 呢?


这里我们会介绍一下在#import <AvailabilityInternal.h> 中的两个宏定义:


*__IPHONE_OS_VERSION_MIN_REQUIRED


*__IPHONE_OS_VERSION_MAX_ALLOWED


从字面意思就可以直到,__IPHONE_OS_VERSION_MIN_REQUIRED 表示iPhone支持最低的版本系统,__IPHONE_OS_VERSION_MAX_ALLOWED 表示iPhone允许最高的系统版本。


__IPHONE_OS_VERSION_MAX_ALLOWED 的取值来自iOS SDK的版本,比如我现在使用的是Xcode Version 8.2.1(8C1002),SDK版本是iOS 10.2,怎么看Xcode里SDK的iOS版本呢?



进入PROJECT,选择Build Setting,在Architectures中的Base SDK中可以查看当前的iOS SDK版本。



打印这个宏,可以看到它一直输出100200。


__IPHONE_OS_VERSION_MIN_REQUIRED 的取值来自项目TARGETS的Deployment Target,即APP愿意支持的最低版本。如果我们修改它为8.2,打印这个宏,会发现输出80200,默认为10.2。


通常,__IPHONE_OS_VERSION_MAX_ALLOWED 可以代表当前的SDK的版本,用来判断当前版本是否开始支持或具有某些功能。而__IPHONE_OS_VERSION_MIN_REQUIRED 则是当前SDK支持的最低版本,用来判断当前版本是否仍然支持或具有某些功能。


回到UIAlertController 使用的问题,我们就可以使用这些宏,添加版本检测判断,从而使我们的代码更健壮。


 - (void)showAlertView {
#if defined(__IPHONE_OS_VERSION_MIN_REQUIRED) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_9_0
UIAlertView *alertView = [[UIAlertView alloc] initWithTitle:@"Title" message:@"message" delegate:nil cancelButtonTitle:@"Cancel" otherButtonTitles:@"OK", nil];
[alertView show];
#else
if (NSFoundationVersionNumber >= NSFoundationVersionNumber_iOS_8_0) {
UIAlertController *alertViewController = [UIAlertController alertControllerWithTitle:@"Title" message:@"message" preferredStyle:UIAlertControllerStyleAlert];

UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"Cancel" style:UIAlertActionStyleCancel handler:nil];
UIAlertAction *otherAction = [UIAlertAction actionWithTitle:@"OK" style:UIAlertActionStyleDefault handler:nil];

[alertViewController addAction:cancelAction];
[alertViewController addAction:otherAction];

[self presentViewController:alertViewController animated:YES completion:NULL];
}
#endif
}

方案六


iOS 11.0 以后,Apple加入了新的API,以后我们就可以像在Swift中的那样,很方便的判断系统版本了。


if (@available(iOS 11.0, *)) {
// iOS 11.0 及以后的版本
} else {
// iOS 11.0 之前
}

参考链接



作者:蒙哥卡恩就是我
来源:juejin.cn/post/7277111344003399734
收起阅读 »

货拉拉用户 iOS 端灵动岛实践总结

iOS
1. 前言 实时活动是iOS 16.1及以上版本中新增的功能,它允许应用在锁屏界面显示实时数据,能够帮助用户实时查看当前订单的进展,而无需解锁手机。用户在货拉拉APP上下单后,可以将手机放置一旁,开始其他工作。当用户想要查询订单状态时,只需从锁定屏幕或灵动岛...
继续阅读 »

1. 前言


实时活动是iOS 16.1及以上版本中新增的功能,它允许应用在锁屏界面显示实时数据,能够帮助用户实时查看当前订单的进展,而无需解锁手机。用户在货拉拉APP上下单后,可以将手机放置一旁,开始其他工作。当用户想要查询订单状态时,只需从锁定屏幕或灵动岛上轻松操作即可。实时活动的出现不仅省去了用户解锁手机的步骤,更为用户节省了时间和精力。目前货拉拉APP适配“灵动岛”的最新6.7.68版本已正式上线,欢迎大家升级体验。在适配过程中,货拉拉App也踩过很多“坑”,在此汇总为实战经验分享给大家。


2. Live Activity&灵动岛的介绍


Live Activity的实现需要使用Apple的ActivityKit框架。通过使用ActivityKit,开发者可以轻松地创建一个Live Activity,这是一个动态的、实时更新的活动,可以在用户的设备上显示各种信息。此外,ActivityKit还提供了推送通知的功能,开发者可以通过服务器向用户的设备发送更新;这样,即使应用程序没有运行,用户也可以接收到最新的信息。


灵动岛是Live Activity的一种展示形式,灵动岛有三种展示形式:Compact紧凑、Minimal最小化,Expanded扩展。开发时必须实现这三种形式,以确保灵动岛在不同的场景下都能正常展示。



同时还需要实现锁屏下的实时活动UI,设备处于锁屏状态下,也能查看实时更新的内容。以上功能的实现,都是使用WidgetKit和SwiftUI完成开发。


2.1 技术难点及策略


实时活动,主要是APP在后台时,主动更新通知栏和灵动岛的数据,为用户展示最新实时订单状态。如何及时刷新实时活动的数据,是一个重点、难点。


更新方式有3种:



  1. 通过APP内订单状态的变化刷新实时活动和灵动岛。此方法开发量小,但是APP退到后台30s后或者进程杀掉,会停止数据的更新。

  2. 让APP配置支持后台运行模式,通过本地现有的订单状态变化逻辑,在后台发起网络请求,获取订单的数据后刷新实时活动。此方法开发量小,但求主App进程必须存在,进程一旦杀掉就无法更新。

  3. 通过接受远程推送通知来更新实时活动。此方法需要后端配合,此方式比较灵活,无需App进程存在,数据更新及时。也是业界常见的方案。


通过对数据刷新的三种方案进行评估后,选择了用户体验最佳的第三种方式。通过后端发生push,端上接受push数据来更新实时活动。


3. Live Activity&灵动岛的实践


3.1 实现方案流程图


实现流程图:


image.png


3.2 实现代码


创建Live Activities的准备:



  • Xcode需要14.1以上版本

  • 在主工程的 Info.plist 文件中添加一个键值对,key 为 NSSupportsLiveActivities,value 为 YES

  • 使用ActivityKit在Widget Extension 中创建一个Live Activity


需要实现锁屏状态下UI、灵动岛长按展开的UI、灵动岛单个UI、多个实时活动时的minimalUI


import SwiftUI
import WidgetKit

@main
struct TestWidget: Widget {
var body: some WidgetConfiguration {
ActivityConfiguration(for: TestAttributes.self) { context in
// 锁屏状态下的UI
} dynamicIsland: { context in
DynamicIsland {
//灵动岛展开后的UI
} compactLeading: {
// 未被展开左边UI
} compactTrailing: {
// 未被展开右边UI
} minimal: {
// 多任务时,右边的一个圆圈区域
}
.keylineTint(.cyan)
}
}
}

灵动岛主要分为StartUpdateEnd三种状态,可由ActivityKit远程推送控制其状态。


开启Live Activity


        let state = TestAttributes.ContentState()
let attri = TestAttributes(value: 100)
do {
let current = try Activity.request(attributes: attri, contentState: state, pushType: .token)
Task {
for await state in current.contentStateUpdates {
//监听state状态
}
}
Task {
for await state in current.activityStateUpdates {
//监听activity状态
}
}
} catch(let error) {
}

更新Live Activity


   Task {
guard let current = Activity<TestAttributes>.activities.first else {
return
}
let state = TestAttributes.ContentState(value: 88)
await current.update(using: state)
}

结束Live Activity


    Task {
for activity in Activity<TestAttributes>.activities {
await activity.end(dismissalPolicy: .immediate)
}
}

4. 使用ActivityKit推送通知


ActivityKit提供了接收推送令牌的功能,我们可以使用这个令牌来通过ActivityKit推送通知从我们的服务器向Apple Push Notification service (APNs)发送更新。


推送更新Live Activity的准备:




  • 在开发者后台配置生成p8证书,替换原来的p12证书




  • 通过pushTokenUpdates获取推送令牌PushToken




  • 向后端注册PushToken




代码展示:


//取得PushToken
for await tokenData in current.pushTokenUpdates {
let mytoken = tokenData.map { String(format: "x", $0) }.joined()
//向后端注册
registerActivityToken(mytoken)
}

4.1 模拟器push验证测试


环境要求:


Xcode >= 14.1 MacOS >= 13.0


准备工作:



  1. 通过pushTokenUpdates获取推送需要的token

  2. 根据开发者TeamID、p8证书本地路径、BuidleID等进行脚本配置


脚本示例:


export TEAM_ID=YOUR_TEAM_ID
export TOKEN_KEY_FILE_NAME=YOUR_AUTHKEY_FILE.p8
export AUTH_KEY_ID=YOUR_AUTHKEY_ID
export DEVICE_TOKEN=YOUR_PUSH_TOKEN
export APNS_HOST_NAME=api.sandbox.push.apple.com

export JWT_ISSUE_TIME=$(date +%s)
export JWT_HEADER=$(printf '{ "alg": "ES256", "kid": "%s" }' "${AUTH_KEY_ID}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export JWT_CLAIMS=$(printf '{ "iss": "%s", "iat": %d }' "${TEAM_ID}" "${JWT_ISSUE_TIME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export JWT_HEADER_CLAIMS="${JWT_HEADER}.${JWT_CLAIMS}"
export JWT_SIGNED_HEADER_CLAIMS=$(printf "${JWT_HEADER_CLAIMS}" | openssl dgst -binary -sha256 -sign "${TOKEN_KEY_FILE_NAME}" | openssl base64 -e -A | tr -- '+/' '-_' | tr -d =)
export AUTHENTICATION_TOKEN="${JWT_HEADER}.${JWT_CLAIMS}.${JWT_SIGNED_HEADER_CLAIMS}"

curl -v \
--header "apns-topic:YOUR_BUNDLE_ID.push-type.liveactivity" \
--header "apns-push-type:liveactivity" \
--header "authorization: bearer $AUTHENTICATION_TOKEN" \
--data \
'{"Simulator Target Bundle": "YOUR_BUNDLE_ID",
"aps": {
"timestamp":1689648272,
"dismissal-date":0,
"event": "update",
"sound":"default",
"content-state": {
"title": "等待付款",
"content": "请尽快完成下单"
}
}}'
\
--http2 \
https://${APNS_HOST_NAME}/3/device/$DEVICE_TOKEN

其中:


apns-topic:固定为{BundleId}.push-type.liveactivity


apns-push-type:固定为liveactivity


Simulator Target Bundle:模拟器推送,设置为对应APP的BundleId


timestamp:表示推送通知的发送时间,如果timestamp字段的值与当前时间相差太大,可能会收不到推送。


event:可填入update、end,对应Live Activity的更新与结束。


dismissal-date:当event为end时有效,表示结束后从锁屏上移除Live Activity的时间。如果推送内容不包含"dismissal-date",默认结束后4小时后消失,但内容不会再发生更新。如果期望Live Activity结束后立即从锁屏上移除它,可为"dismissal-date"提供一个过去的日期。


content-state:对应灵动岛的Activity.ContentState;如果push中content-state的字段和Attributes比较:




  • 字段过多,多余的字段可能会被忽略,不会导致解析失败




  • 字段缺少,会在解析push通知时出现问题错误。错误表现为:实时活动会有蒙层,并展示loading菊花UI。




示范:


image.png


image.png


5. 踩坑记录




  • 在模拟器上无法获取到pushToken,无法进行推送模拟?


    检查电脑的系统版本号,需要13.0以上




  • 更新实时活动时,页面显示加载loadingUI,为什么?


    核对push字段和Activity.ContentState的字段是否完全一致,字段少了会解析失败




  • 在16.1系统上,无法展示实时活动,其他更高系统能展示?


    检查Widget里面iOS系统版本号的配置,设置为想要支持的最低版本




  • dismissal-date设置为10分钟后才消失,为什么Dynamic Island灵动岛立即消失了?


    Dynamic Island的显示逻辑可能会更加复杂,如果push的event=end,Dynamic Island灵动岛会立即消失。期望同时消失,可以在指定时间再发end,dismissal-date设置为过去时间,锁屏UI和Dynamic Island灵动岛会同时消失。




  • 推送不希望打扰用户,静默推送,不需要震动和主动弹出,如何设置?


    将"content-available"设置为1,"sound" 设置为: ""




"aps" = {
"content-available" : 1,
"sound" : ""
}



  • 用户系统是深色模式时,如何适配?


    可以使用@Environment(.colorScheme)属性包装器来获取当前设备的颜色模式。会返回一个ColorScheme枚举,它可以是.light.dark。在根据具体的场景进行UI适配




struct ContentView: View {
@Environment(.colorScheme) var colorScheme

var body: some View {
VStack {
if colorScheme == .dark {
Text("深夜模式")
.foregroundColor(.white)
.background(Color.black)
} else {
Text("日间模式")
.foregroundColor(.(.black)
.background(Color.white)
}
}
}
}

5.1 场景限制及建议



  1. 官方文档提示实时活动最多持续8小时,8小时后数据无法刷新,12小时后会强制消失。因此8小时后的数据不准确

  2. 实时活动的卡片上禁止定位以及网络请求,数据需要小于4KB,不能展示特别负责庞大的数据

  3. 同场景多卡片由于样式趋同且折叠,不建议同时创建多卡片。用户多次下单时,建议只处理第一个订单


6. 用户APP上线效果


用户端iOS APP灵动岛上线后的部分场景截图:







7. 总结


灵动岛功能自上线以来,经过我们的数据统计,用户实时活动使用率高达75%以上。这一数据的背后,是灵动岛强大的功能和优秀的用户体验。用户可以在锁屏页直接查看订单状态,无需繁琐的操作步骤,大大提升了用户体验。这种便捷性,使得灵动岛在用户中的接受度较高。


我们的方案不仅可以应用于当前的业务场景,后续还计划扩展到营销活动,定制化通知消息等多种业务场景。这种扩展性,使得灵动岛可以更好地满足不同用户的需求,丰富产品运营策略。


我们希望通过分享开发过程中遇到的问题和解决方案,可以帮助到更多的人。如果你有任何问题或者想法,欢迎在评论区留言。期待我们在技术的道路上再次相遇。


总的来说,灵动岛以其高效、便捷、灵活的特性,赢得了用户的广泛好评。我们将继续努力,为用户提供更优质的服务,为产品的发展注入更多的活力。


作者:货拉拉技术
来源:juejin.cn/post/7300779071390335030
收起阅读 »

iOS 仿花小猪首页滑动效果

iOS
一. 背景 首页改版,想要做一个类似花小猪首页滑动效果,具体如下所示: 二. 分析 从花小猪首页交互我们可以分析出如下信息: 首页卡片分为三段式,底部、中间、顶部。 当首页卡片在底部,只能先外部视图整体往上滑动,滑动到顶部后,内部卡片头部悬浮,内部卡...
继续阅读 »

一. 背景


首页改版,想要做一个类似花小猪首页滑动效果,具体如下所示:



二. 分析


从花小猪首页交互我们可以分析出如下信息:




  • 首页卡片分为三段式,底部、中间、顶部。




  • 当首页卡片在底部,只能先外部视图整体往上滑动,滑动到顶部后,内部卡片头部悬浮,内部卡片滚动视图依然可以滚动。




  • 当首页卡片在中间,可以先外部视图整体往上或者往下滑动,往下滑动到底部后,禁止滑动,滑动到顶部,内部视图卡片头部悬浮,内部滚动视图可以滚动。




  • 当首页卡片在顶部,可以拖动卡片外部视图整体下滑,也可以通过内部视图向下滚动,滚动到跟内部头部底部持平,变成整体一起向下滑动。而当内部滚动视图向上滚动,内部卡片头部悬浮固定。




  • 首页卡片滑动过程中,如果停在中间位置,依据卡片停止位置,距离底部、中间、顶部位置远近,向距离近的一端,直接移动到相应位置,比如移动到中间和顶部位置之间,如果距离顶部近,则直接移动到顶部。




  • 当首页卡片在底部,上滑速度很快超过一定值,就直接到顶部。同样在顶部下滑也一样。




  • 当首页卡片在顶部,内部滚动视图快速下滑,下滑到跟卡片头部分开,产生弹簧效果,不直接一起下滑,但其他部分如果慢慢滑动,下滑到跟卡片头部即将分开时,变成整体一起下滑。




三. 实现


理清了首页卡片的滑动交互细节之后,我们开始设计对应类和相关职责。



从上面结构图我们可以看出,主要分为三部分




  • 卡片外层容器externalScrollView,限定为UIScrollView类型。




  • 卡片内头部insideHeaderView,限定为UIView类型。




  • 卡片内滚动视图insideTableView,由于滚动视图所以insideTableView一定是UIScrollView类型,为了复用,这里我们限定为UITableView




这里其实我们不关心头部视图insideHeaderView,因为内部头部视图insideHeaderView和内部滚动视图insideTableView之间的关系是固定,就是内部滚动视图insideTableView一直在头部视图 insideHeaderView下面。


同样我们也不关心滚动视图insideTableView里面的内容,我们需要处理的就是卡片外层容器externalScrollView和内部滚动视图insideTableView之间交互关系。


因为所有这种类型交互处理逻辑是一致的,因此我们抽出FJFScrollDragHelper类。



  • 首先我们来认识下滚动辅助类FJFScrollDragHelper相关属性


    /// scrollView 显示高度
public var scrollViewHeight: CGFloat = kScreenH
/// 限制的高度(超过这个高度可以滚动)
public var kScrollLimitHeight: CGFloat = kScreenH * 0.51
/// 滑动初始速度(大于该速度直接滑动到顶部或底部)
public var slideInitSpeedLimit: CGFloat = 3500.0
/// 当前 滚动 视图 位置
public var curScrollViewPositionType: FJFScrollViewPositionType = .middle
/// 最高 展示 高度
public var topShowHeight: CGFloat = 0
/// 中间 展示 高度
public var middleShowHeight: CGFloat = 0
/// 最低 展示 高度
public var lowestShowHeight: CGFloat = 0
/// 当前 滚动 视图 类型
private var currentScrollType: FJFCurrentScrollViewType = .externalView
/// 外部 滚动 view
public weak var externalScrollView: UIScrollView?
/// 内部 滚动 view
public weak var insideScrollView: UIScrollView?
/// 拖动 scrollView 回调
public var panScrollViewBlock: (() -> Void)?
/// 移动到顶部
public var goToTopPosiionBlock: (() -> Void)?
/// 移动到 底部 默认位置
public var goToLowestPosiionBlock: (() -> Void)?
/// 移动到 中间 默认位置
public var goToMiddlePosiionBlock: (() -> Void)?

我们看到FJFScrollDragHelper内部弱引用了外部滚动视图externalScrollView和内部滚动视图insideScrollView




  1. 关联对象,并给外部externalScrollView添加滑动手势




/// 添加 滑动 手势 到 外部滚动视图
public func addPanGestureRecognizer(externalScrollView: UIScrollView){
let panRecoginer = UIPanGestureRecognizer(target: self, action: #selector(panScrollViewHandle(pan:)))
externalScrollView.addGestureRecognizer(panRecoginer)
self.externalScrollView = externalScrollView
}



  1. 处理滑动手势




// MARK: - Actions
/// tableView 手势
@objc
private func panScrollViewHandle(pan: UIPanGestureRecognizer) {
/// 当前 滚动 内部视图 不响应拖动手势
if self.currentScrollType == .insideView {
return
}
guard let contentScrollView = self.externalScrollView else {
return
}
let translationPoint = pan.translation(in: contentScrollView.superview)

// contentScrollView.top 视图距离顶部的距离
contentScrollView.y += translationPoint.y
/// contentScrollView 移动到顶部
let distanceToTopH = self.getTopPositionToTopDistance()
if contentScrollView.y < distanceToTopH {
contentScrollView.y = distanceToTopH
self.curScrollViewPositionType = .top
self.currentScrollType = .all
}
/// 视图在底部时距离顶部的距离
let distanceToBottomH = self.getBottomPositionToTopDistance()
if contentScrollView.y > distanceToBottomH {
contentScrollView.y = distanceToBottomH
self.curScrollViewPositionType = .bottom
self.currentScrollType = .externalView
}
/// 拖动 回调 用来 更新 遮罩
self.panScrollViewBlock?()
// 在滑动手势结束时判断滑动视图距离顶部的距离是否超过了屏幕的一半,如果超过了一半就往下滑到底部
// 如果小于一半就往上滑到顶部
if pan.state == .ended || pan.state == .cancelled {

// 处理手势滑动时,根据滑动速度快速响应上下位置
let velocity = pan.velocity(in: contentScrollView)
let largeSpeed = self.slideInitSpeedLimit
/// 超过 最大 力度
if velocity.y < -largeSpeed {
gotoTheTopPosition()
pan.setTranslation(CGPoint(x: 0, y: 0), in: contentScrollView)
return
} else if velocity.y < 0, velocity.y > -largeSpeed {
if self.curScrollViewPositionType == .bottom {
gotoMiddlePosition()
} else {
gotoTheTopPosition()
}
pan.setTranslation(CGPoint(x: 0, y: 0), in: contentScrollView)
return
} else if velocity.y > largeSpeed {
gotoLowestPosition()
pan.setTranslation(CGPoint(x: 0, y: 0), in: contentScrollView)
return
} else if velocity.y > 0, velocity.y < largeSpeed {
if self.curScrollViewPositionType == .top {
gotoMiddlePosition()
} else {
gotoLowestPosition()
}
pan.setTranslation(CGPoint(x: 0, y: 0), in: contentScrollView)
return
}
let scrollViewDistanceToTop = kScreenH - contentScrollView.top
let topAndMiddleMeanValue = (self.topShowHeight + self.middleShowHeight) / 2.0
let middleAndBottomMeanValue = (self.middleShowHeight + self.lowestShowHeight) / 2.0

if scrollViewDistanceToTop >= topAndMiddleMeanValue {
gotoTheTopPosition()
} else if scrollViewDistanceToTop < topAndMiddleMeanValue,
scrollViewDistanceToTop > middleAndBottomMeanValue {
gotoMiddlePosition()
} else {
gotoLowestPosition()
}
}
pan.setTranslation(CGPoint(x: 0, y: 0), in: contentScrollView)
}

处理滑动手势需要当前视图滚动类型currentScrollType和卡片当前所处的位置curScrollViewPositionType来分别进行判断。


/// 当前 滚动 视图 类型
public enum FJFCurrentScrollViewType {
case externalView /// 外部 视图
case insideView /// 内部 视图
case all /// 内部外部都可以响应
}

/// 当前 滚动 视图 位置 属性
public enum FJFScrollViewPositionType {
case top /// 顶部
case middle /// 中间
case bottom /// 底部
}

如下是对应的判断逻辑:


暂时无法在飞书文档外展示此内容


A. 在底部


 /// 回到 底部 位置
private func gotoLowestPosition() {
self.curScrollViewPositionType = .bottom
self.goToLowestPosiionBlock?()
}

private func gotoLowestPosition(withAnimated animated: Bool = true) {
self.insideTableView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
if animated {
UIView.animate(withDuration: 0.18, delay: 0, options: .allowUserInteraction) {
self.externalScrollView.top = self.scrollDragHelper.getBottomPositionToTopDistance()
}
} else {
self.externalScrollView.top = self.scrollDragHelper.getBottomPositionToTopDistance()
}
}

只能滚动外部视图,内部滚动视图偏移量是0.


B. 在中间


/// 回到 中间 位置
private func gotoMiddlePosition() {
self.curScrollViewPositionType = .middle
self.goToMiddlePosiionBlock?()
}

private func gotoMiddlePosition(withAnimated animated: Bool = true) {
self.insideTableView.setContentOffset(CGPoint(x: 0, y: 0), animated: true)
if animated {
UIView.animate(withDuration: 0.18, delay: 0, options: .allowUserInteraction) {
self.externalScrollView.top = self.scrollDragHelper.getMiddlePositionToTopDistance()
}
} else {
self.externalScrollView.top = self.scrollDragHelper.getMiddlePositionToTopDistance()
}
}

只能滚动外部视图,内部滚动视图偏移量是0.


C. 在顶部



  • 开始滚动判断:


    /// 更新 当前 滚动类型 当开始拖动 (当在顶部,开始滑动时候,判断当前滑动的对象是内部滚动视图,还是外部滚动视图)
public func updateCurrentScrollTypeWhenBeginDragging(_ scrollView: UIScrollView) {
if self.curScrollViewPositionType == .top {
if scrollView.contentOffset.y <= 0 {
self.currentScrollType = .externalView
} else {
self.currentScrollType = .insideView
}
}
}


  • 滚动过程中判断


/// 更新 滚动 类型 当滚动的时候,并返回是否立即停止滚动
public func isNeedToStopScrollAndUpdateScrollType(scrollView: UIScrollView) -> Bool {
if scrollView == self.insideScrollView {
/// 当前滚动的是外部视图
if self.currentScrollType == .externalView {
self.insideScrollView?.setContentOffset(CGPoint(x: 0, y: 0), animated: false)
return true
}
if self.curScrollViewPositionType == .top {
if self.currentScrollType == .all { /// 在顶部的时候,外部和内部视图都可以滑动,判断当内部滚动视图视图的位置,如果滚动到底部了,则变为外部滚动视图跟着滑动,内部滚动视图不动
if scrollView.contentOffset.y <= 0 {
self.currentScrollType = .externalView
} else {
self.currentScrollType = .insideView
}
} else if scrollView.isDecelerating == false,
self.currentScrollType == .insideView { /// 在顶部的时候,当内部滚动视图,慢慢滑动到底部,变成整个外部滚动视图跟着滑动下来,内部滚动视图不再滑动
if scrollView.contentOffset.y <= 0 {
self.currentScrollType = .externalView
}
}
}
}
return false
}


  • 滚动结束判断


/// 当在顶部,滚动停止时候 更新 当前 滚动类型 ,如果当前内部滚动视图,已经滚动到最底部,
/// 则只能滚动最外层滚动视图,如果内部滚动视图没有滚动到最底部,则外部和内部视图都可以滚动
public func updateCurrentScrollTypeWhenScrollEnd(_ scrollView: UIScrollView) {
if self.curScrollViewPositionType == .top {
if scrollView.contentOffset.y <= 0 {
self.currentScrollType = .externalView
} else {
self.currentScrollType = .all
}
}
}

以上就是具体滚动判断相关处理逻辑,对应实现效果如下。



作者:果哥爸
来源:juejin.cn/post/7299731897626345481
收起阅读 »

越狱手机root密码重置

之前有入手过一台iphone6越狱机器,手机刚到那会儿,把玩了一番。之后就一直没动过了,今天突然心血来潮,想玩玩,结果发现,ssh登陆不上了,因为root密码不记得了。像咱们的qq或者什么的密码忘记了,正常思路,就是找回密码,即重置密码。所以同理。iphone...
继续阅读 »

之前有入手过一台iphone6越狱机器,手机刚到那会儿,把玩了一番。之后就一直没动过了,今天突然心血来潮,想玩玩,结果发现,ssh登陆不上了,因为root密码不记得了。
像咱们的qq或者什么的密码忘记了,正常思路,就是找回密码,即重置密码。所以同理。
iphone手机的账号和密码,一般是存储在/private/etc/master.passwd。


把这个文件导出到电脑桌面(一般越狱手机都是可以通过一些软件直接访问文件的,像ifunbox、pp助手、ifiles。这里我用的是pp助手),打开:



编辑该文件,把root后面的ZGrKPbggg0H8Q(这里不一定是这个,每个机器肯定都不同,只需记住是root:之后的13个字符即可。)改为/smx7MYTQIi2M


把文件名修改为master.passwd,再放回手机的private/etc/目录下。
再次ssh登陆越狱手机,输入alpine,即可。


收起阅读 »

ARM汇编基础(一)----寄存器篇

AArch64寄存器Arm处理器提供通用寄存器和专用寄存器以及一些在特定模式下可用的额外寄存器。在 AArch64状态下,有以下寄存器是可用的:31个64位通用寄存器(X0-X30),通用寄存器的低32位可用W0-W30访问。4个栈指针寄存器:SP_EL0、S...
继续阅读 »

AArch64寄存器

Arm处理器提供通用寄存器和专用寄存器以及一些在特定模式下可用的额外寄存器。

在 AArch64状态下,有以下寄存器是可用的:
  • 31个64位通用寄存器(X0-X30),通用寄存器的低32位可用W0-W30访问。

  • 4个栈指针寄存器:SP_EL0、SP_EL1、SP_EL2、SP_EL3。

  • 3个异常链接寄存器:ELR_EL1、ELR_EL2、ELR_EL3。

  • 3个程序状态寄存器:SPSR_EL1、SPSR_EL2、SPSR_EL3。

  • 1个程序计数器。

除了状态寄存器SPSR_EL1、SPSR_EL2、SPSR_EL3是32bit,所有的寄存器均为64bit。

大多数的指令都可以操作32bit和64bit寄存器。寄存器宽度由寄存器标识符决定,W表示32bit,X表示64bit。Wn和Xn(0-30)是指同一个寄存器。当使用32位指令时,源寄存器的高32bit会被忽略,而目的寄存器的高32bit则会被置0。

没有W31或者X31寄存器,根据指令,寄存器31会被用作栈指针寄存器或者零寄存器。当用作栈指针寄存器时,可以用SP表示;当用作零寄存器时,用WZR和XZR分别表示32bit和64bit的零寄存器。

异常等级

Armv8架构定义了4个异常等级(EL0-EL3),EL3代表拥有最多执行特权的最高异常级别。当接受异常时,异常等级可以上升或者保持不变,当从异常处理中返回时,异常等级可以降低或者保持不变。

以下为常用的异常等级模式:

  • EL0

应用程序。

  • EL1

系统内核以及特殊函数。

  • EL2

虚拟机监视器(virtual machine monitor)。

  • EL3

Secure monitor.

当将异常带到更高的异常级别时,执行状态可以保持不变,或者从AArch32变到AArch64。

当返回到较低的异常级别时,执行状态可以保持不变,也可以从AArch64变更为AArch32。

执行状态的改变的唯一方式是从异常中获取或返回。在执行状态之间进行更改不可能与在AArch32状态下在A32和T32指令之间进行更改相同。

在powerup和reset上,处理器进入最高的实现异常级别。此异常级别的执行状态是实现的属性,可能由配置输入信号决定。

对于EL0以外的异常级别,执行状态由一个或多个控制寄存器配置位决定。这些位只能在更高的异常级别上设置。

对于EL0,执行状态被确定为异常返回EL0的一部分,由执行返回的异常级别控制。

LR寄存器

在AArch64状态下,当进行子函数调用时,LR寄存器保存返回地址。如果返回地址呗保存在栈上,LR寄存器也可以用作通用寄存器。LR寄存器对应的是寄存器30,与AArch32不同的是,LR寄存器和异常链接寄存器(ELRs)是不同的,因此,LR是未存储的。(ps:Unlike in AArch32 state, the LR is distinct from the Exception Link Registers (ELRs) and is therefore unbanked.【unbanked 确实不知道怎么翻译】)

异常链接寄存器有三个:ELR_EL1、ELR_EL2、ELR_EL3,与异常等级相对应。当发生异常时,目标异常级别的异常链接寄存器将存储异常处理完后要跳转到的返回地址。如果异常来自AArch32,ELR寄存器的高32bit全部置0。异常级别内的子函数调用用LR来存储子函数的返回地址。

例如,当异常等级从EL0变为EL1,返回地址将存储在ELR_EL1寄存器中。

在异常时,如果要启用用相同级别的中断,必须将ELR中的数据存储到栈中,因为在发生中断时ELR寄存器将被新的返回地址覆盖。

栈指针寄存器

在AArch64状态下,SP表示64位栈指针,SP_EL0是SP的别名。不要讲SP用作通用寄存器。

SP只能用作以下指令的操作寄存器:

  • 作为装载和存在的基本寄存器。在这种情况下,再添加任何偏移量之前,它必须是4字节对齐的,否则会发生堆栈对齐异常。

  • 作为算术指令的源寄存器或者目标寄存器,但是它不能被用作设置了条件标志的指令的目标寄存器。

  • 逻辑指令,例如为了使其地址对齐。

对于三个异常级别,都有一个单独的栈指针。在异常级别中,可以使用该异常级别下的专用栈指针,也可以使用与之相应的栈寄存器。可以使用SPSel寄存器来选择要在异常级别使用的栈指针。

栈指针的选择由附加到异常级别名称的字母t或h表示,例如EL0t或EL3h。t后缀表示异常级别使用SP_EL0, h后缀表示使用SP_ELx,其中x是当前异常级别号。EL0总是使用SP_EL0,所以不能有h后缀。

AArch64状态下预声明的核心寄存器名字

在AArch64状态中,预先声明的核心寄存器与AArch32状态中的不同。

下表显示AArch64状态下预声明的核心寄存器:

寄存器名称含义
W0-W3032-bit 通用寄存器。
X0-X3064-bit 通用寄存器。
WZR32-bit RAZ/WI寄存器,在32位指令下,寄存器31用作零寄存器时的名称。
XZR64-bit RAZ/WI寄存器,在64位指令下,寄存器31用作零寄存器时的名称。
WSP32-bit 栈指针,在32位指令下,寄存器31用作栈指针时的名称。
SP64-bit 栈指针,在64位指令下,寄存器31用作栈指针时的名称。
LR链接寄存器。和X30是同一个寄存器。

可以将寄存器名全部写成大写或小写。

请注意:

在AArch64状态下,PC寄存器不是一个通用寄存器,不能通过名称来访问他。

在AArch64下预声明的扩展寄存器

您可以将高级SIMD和浮点寄存器的名称写成大写或小写。

下表显示了AArch64状态下预先声明的扩展寄存器名:

寄存器名称含义
V0-V31128-bit矢量寄存器。
Q0-Q31128-bit标量寄存器。
D0-D3164-bit标量寄存器、双精度浮点寄存器。
S0-S3132-bit标量寄存器、单精度浮点寄存器。
H0-H3116-bit标量寄存器、半精度浮点寄存器。
B0-B318-bit标量寄存器。

AArch64状态下的PC寄存器

在AArch64状态下,PC寄存器存储的事当前执行的指令的地址。

它是由执行的指令的大小增加的,总是四个字节。

在AArch64状态下,PC寄存器不是一个通用寄存器,所以不能显式地访问它。以下类型的指令,可以隐式地读取它的值:

  • 计算PC相对地址的指令。

  • PC相关的加载指令。

  • 直接指向PC相关的标签。

  • 分支和链接指令,会将PC值存储在LR寄存器中。

唯一可以写入PC寄存器的指令类型:

  • 条件分支和无条件分支。

  • 异常产生和异常返回。

分支指令将目的地址加载到PC寄存器中。

在AArch64状态下的条件执行

在AArch64状态下,NZCV寄存器保存着N、Z、C、V标志位的值,处理器用这些标志位来决定是否执行条件指令。这些标志位被保存在NZCV寄存器的【31:28】位上。

条件标志位在任何异常等级下都可以使用MSR和MRS指令进行访问。

与A32相比,A64对条件的利用更少。例如在A64中:

  • 只有少数的指令可以set或test条件标志位。

  • 没有等效的T32 IT指令。

  • 唯一有条件执行的指令是条件分支指令B.cond,如果条件判定不成立(false),B.cond指令就像NOP指令一样。

在AArch64状态下的Q标志位

在AArch64状态下,不能对Q标识位进行读写,因为在A64中没有在通用寄存器上操作的饱和算术指令。(in A64 there are no saturating arithmetic instructions that operate on the general purpose registers.)

先进的SIMD饱和算法指令将浮点状态寄存器(FPSR)中的QC位设置为表示已经发生饱和。您可以通过Q助记符修饰符(例如SQADD)来识别这些指令。

流程状态

在AArch64状态下,没有CPSR寄存器。但是可以通过访问CPSR中不同的部分作为流程状态字段。

流程状态字段:

  • N、Z、C、V条件标识位(NZCV)。

  • 当前寄存器位宽(nRW)。

  • 栈指针选择位(SPSel)。

  • 禁止中断位(DAIF)。

  • 当前异常等级(EL)。

  • 单步处理位(SS)。

  • 非法异常返回状态位(IL)。

可以使用MSR指令写:

  • NZCV寄存器中的N、Z、C、V标识位。

  • DAIF寄存器中的禁止中断标识位。

  • 在异常等级为EL1或更高的情况下,SPSel寄存器的SP选择位。

可以使用MRS指令读:

  • NZCV寄存器中的N、Z、C、V标识位。

  • DAIF寄存器中的禁止中断标识位。

  • 在异常等级为EL1或更高的情况下,CEL寄存器的异常等级位。

  • 在异常等级为EL1或更高的情况下,SPSel寄存器的SP选择位。

当发生异常时,与当前异常级别关联的所有流程状态字段都存储在与目标异常级别关联的单个SPSR寄存器中。只能从SPSR访问SS、IL和nRW位。

AArch64下的SPSRs

保存的程序状态寄存器(SPSRs)是32位寄存器,当将异常带到使用AArch64状态的异常级别时,它存储当前异常级别的进程状态。这允许在处理异常之后恢复进程状态。

在AArch64状态下,每个异常等级都有自己的SPSR寄存器:

  • SPSR_EL1.

  • SPSR_EL2.

  • SPSR_EL3.

当发生异常时,当前异常等级的进程状态会被写入当前异常等级对应的SPSR寄存器中。当从一个异常中返回时,异常处理程序使用正在返回的异常级别的SPSR来恢复正在返回的异常级别的流程状态。

请注意

从异常返回时,首选返回地址将从与正在返回的异常级别关联的ELR恢复。

SPSRs存储了以下信息:

  • N、Z、C、V标识位。

  • D、A、I、F禁止中断位。

  • 寄存器位宽。

  • 执行模式。

  • IL和SS位。

收起阅读 »

符号绑定的另一种打开方式

懒加载和非懒加载iOS对于引用的外部符号,分为Lazy Symbol和Non-Lazy Symbol,分别存储在__DATA,__got节和__DATA,__la_symbol_ptr节。Non-Lazy Symbol符号在dyld加载模块的时候,就会将真实的...
继续阅读 »

懒加载和非懒加载

iOS对于引用的外部符号,分为Lazy SymbolNon-Lazy Symbol,分别存储在__DATA,__got节和__DATA,__la_symbol_ptr节。

Non-Lazy Symbol符号在dyld加载模块的时候,就会将真实的函数地址写入到对应的地址中,实现绑定。而Non-Lazy Symbol则会在第一次调用该函数的时候,为其动态寻找真实函数地址并进行绑定。

facebook基于符号绑定机制,写出了hook神器fishhook,通过查找符号指针并替换,从而达到hook效果!!!

然而,基于模块检测反hook,却甚是烦人。你可能会有反反hook来应付,但是它也有可能会有反反反hook来对付你~~~

那么,怎么才能终结这场hook与反hook的心理战呢?

Mach-O View 分析动态符号绑定过程

简单分析一下Lazy Symbol的绑定过程:



这里以NSLog为例:

可以看出,符号NSLog所指向的地址为:0x0000000100006474。

转化为文件偏移为:0x0000000100006474 - 0x100008078 + 32888 = 0x6474;

到文件偏移为0x6474的位置查看:

这是一段可执行代码,地址0x6474处的意思是:读取0x647c位置处的四个字节的数据(0x1d),保存到w16寄存器。然后无条件跳转到0x645c(这里的地址,全部都是指文件偏移)。


这段代码,光这么看其实看不出什么,但是如果去调试的话,就会发现,这段代码实际上是在调用dyld_stub_binder为懒加载符号绑定真实地址。而刚刚在0x6474处的代码获取到的四字节的数据,实际上是符号绑定信息的偏移:


0xc428+0x1d = 0xc445

也就是说,动态绑定NSLog所需要的数据,就存储在0xc445处。

那么,理论上来说,如果我们尝试着修改这里的数据,是不是就会改变符号的查找的过程呢?

实践

想的再多,都不如动手操作!!!

新建一个工程,书写如下代码(main.m):

__attribute__((constructor)) static void entry(int argc,char *argv[],char **apple,char **executablepath,struct mach_header_64 **mh_ptr){

if (!strncmp(argv[0], "aaa", 3)) {
printf("the same!!");
}
}

并按照如上方式,查找到函数strncmp的Lazy Binding Info,做如下修改:


修改后:


编写动态库并注入到可执行文件:


__attribute__((visibility("default"))) int strncmq(const char *__s1, const char *__s2, size_t __n);

int strncmq(const char *__s1, const char *__s2, size_t __n){
      printf("hook:%s\nhook:%s",__s1,__s2);
       return strncmp(__s1, __s2, __n);
}

重签名运行!!


发现已经替换成功了!!!

但是,用ida或者hopper分析一下二进制文件,会发现调用的还是原来的strncmp符号:


说明如果进行模块检测的话,还是可以检测出来的~因为虽然符号查找替换了,但是实际上"外套"还是strncmp。所以,继续把外套也修改了!!!

修改这两个处:


修改后:


总结

对于动态绑定的外部引用符号,能动手脚的地方确实很多!!!

收起阅读 »

iOS应用砸壳

应用商店下载的app,都是进过加密过的,用hopper或者ida完全分析不了。那是不是就没办法了呢? 其实不然,解铃还须系铃人,要想得到解密后的文件,还是要依靠苹果爸爸啊!!! 首先,我们知道,加密后的应用,如果不解密的话,苹果自己都不知道怎么去解析可执行文件...
继续阅读 »

应用商店下载的app,都是进过加密过的,用hopper或者ida完全分析不了。那是不是就没办法了呢?
其实不然,解铃还须系铃人,要想得到解密后的文件,还是要依靠苹果爸爸啊!!!
首先,我们知道,加密后的应用,如果不解密的话,苹果自己都不知道怎么去解析可执行文件,所以当设备运行应用时,加载进内存中的数据,肯定是经过解密后的数据,因此咱们只需把内存里的对应的解密部分dump下来即可。
这里介绍一个砸壳工具:dumpdecrypted
该工具的原理就是在程序运行之后注入一个动态库,然在内存中dump下解密之后的部分。
原理和实现不赘述,想进一步了解,可以查看源码。
这里简单讲一下如何用该工具进行解密app:
1、运行源码,生成动态链接库。
解压下载下来的dumpdecrypted-master.zip到当前目录;打开终端,cd到该目录。


在该目录下直接运行make:


如果运行不成功,报错找不到文件。运行xcode-select --print-path ,查看目录是否指向/Applications/Xcode.app/Contents/Developer。如果不是,则用xcode-select -s /Applications/Xcode.app/Contents/Developer修改一下。再执行make。
运行成功,会在当前目录生成dumpdecrypted.dylib动态库。
2、注入动态库
ssh登录进手机,用ps命令查看目标app所在的目录。


记住这个目录。待会儿有用。
再用cycript获取目标app的沙盒目录。


获取到目标app的沙盒目录之后,退出cycriptcontrol+D),将第一步得到的dumpdecrypted.dylib,导入到沙盒目录(导入方法有很多,scp、PP助手、ifiles等)。
cd到沙盒目录,运行DYLD_INSERT_LIBRARIES=dumpdecrypted.dylib /var/mobile/Containers/Bundle/Application/DDFA8DC8-F19A-4A6F-932B-22E8170BB22D/HDXinHuaDict.app/HDXinHuaDict:


看到一系列的+就说明运行成功,在沙盒目录下就会有被解密的二进制文件。



收起阅读 »

类的布局——方法缓存hash表

回顾一下class的结构:struct objc_class : objc_object { // Class ISA; // 继承自 struct objc_object Class superclass; cache_t cache;...
继续阅读 »

回顾一下class的结构:struct objc_class : objc_object {

    // Class ISA;			// 继承自 struct objc_object
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
class_rw_t *data() const {
return bits.data();
}
};

不难发现,在objc_class结构中,有一个cache_t类型的成员变量cache

其结构如下:struct bucket_t {

private:
IMP _imp;
SEL _sel;
};
struct cache_t {
private:
uintptr_t _bucketsAndMaybeMask;
union {
struct {
uint32_t _unused;
uint16_t _occupied;
uint16_t _flags;
};
preopt_cache_t * _originalPreoptCache;
};
};
// objc-cache.m
static constexpr uintptr_t maskShift = 48;
// Additional bits after the mask which must be zero. msgSend
// takes advantage of these additional bits to construct the value
// `mask << 4` from `_maskAndBuckets` in a single instruction.
static constexpr uintptr_t maskZeroBits = 4;
// The largest mask value we can store.
static constexpr uintptr_t maxMask = ((uintptr_t)1 << (64 - maskShift)) - 1;
// The mask applied to `_maskAndBuckets` to retrieve the buckets pointer.
static constexpr uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1;

struct bucket_t *cache_t::buckets() const {
uintptr_t addr = _bucketsAndMaybeMask;
return (bucket_t *)(addr & bucketsMask);
}
// 哈希表已用
mask_t cache_t::occupied() const {
return _occupied;
}
// 哈希表容积
unsigned cache_t::capacity() const {
return mask() ? mask()+1 : 0;
}
mask_t cache_t::mask() const {
uintptr_t maskAndBuckets = _bucketsAndMaybeMask;
return maskAndBuckets >> maskShift;
}

通过查看cache_tbucket_t的结构以及其实现,可以很清晰的看到类的整个缓存表的内容。

接下来用lldb简单验证下:

//  CacheClass.h
@interface CacheClass : NSObject
- (void)cacheMethodA;
- (void)cacheMethodB:(NSString *)str;
- (void)cacheMethodC:(NSUInteger)integer andString:(NSString *)str;
@end

// CacheClass.m
#import "CacheClass.h"
@implementation CacheClass
- (void)cacheMethodA {
NSLog(@"%s", __func__);
}
- (void)cacheMethodB:(NSString *)str {
NSLog(@"%@===>%s", str, __func__);
}
- (void)cacheMethodC:(NSUInteger)integer andString:(NSString *)str {
NSLog(@"%lu====>%@===>%s", integer, str, __func__);
}
@end

// 调用
CacheClass *cacheObj = [[CacheClass alloc] init];
[cacheObj cacheMethodA];
NSLog(@"===================="); // 此行断点

  • 获取CacheObj的isa
(lldb) x/1gx cacheObj
0x281da0bc0: 0x0100000102c2d101
(lldb) p/x 0x0100000102c2d101 & 0x0000000ffffffff8ULL
(unsigned long long) $4 = 0x0000000102c2d100
  • 读取cache_t结构体:
(lldb) x/2gx 0x0000000102c2d100+0x10		// 此处+0x10是因为cache是在`objc_class`结构体的第16字节处开始。
0x102c2d110: 0x0001000281f850c0 0x8010000200000000 // 0x0001000281f850c0 为 _bucketsAndMaybeMask的值 0x8010000200000000 为 联合体的值
  • 获取bucket_t数组的首地址:
(lldb) p/x ((uintptr_t)1 << (48-4))-1		// 计算 bucketsMask 的值
(unsigned long) $4 = 0x00000fffffffffff
(lldb) p/x 0x0001000281f850c0 & 0x00000fffffffffff // _bucketsAndMaybeMask & bucketsMask
(long) $5 = 0x0000000281f850c0 // bucket_t数组的首地址
  • 获取bucket_t数组的count:
(lldb) p/x (0x0001000281f850c0 >> 48) + 1		// 相当于调用cache_t::capacity()函数
(long) $9 = 0x0000000000000002
  • 输出bucket_t数组的内容:
(lldb) x/4gx 0x0000000281f850c0
0x281f850c0: 0x58226481ad686ff0 0x00000001b0870410
0x281f850d0: 0x3c04798102c25d08 0x0000000102c26f97
  • 以{IMP,SEL}的结构验证:
(lldb) p (char *)0x00000001b0870410
(char *) $10 = 0x00000001b0870410 "init"
(lldb) dis -a 0x58226481ad686ff0
libobjc.A.dylib`-[NSObject init]:
0x1ad686ff0 <+0>: ret
0x1ad686ff4 <+4>: udf #0x0
0x1ad686ff8 <+8>: udf #0x0
(lldb) p (char *)0x0000000102c26f97
(char *) $11 = 0x0000000102c26f97 "cacheMethodA"
(lldb) dis -a 0x3c04798102c25d08
MethodCacheDemo`-[CacheClass cacheMethodA]:
0x102c25d08 <+0>: sub sp, sp, #0x30
0x102c25d0c <+4>: stp x29, x30, [sp, #0x20]
0x102c25d10 <+8>: add x29, sp, #0x20
0x102c25d14 <+12>: stur x0, [x29, #-0x8]
0x102c25d18 <+16>: str x1, [sp, #0x10]
0x102c25d1c <+20>: mov x9, sp
0x102c25d20 <+24>: adrp x8, 2
0x102c25d24 <+28>: add x8, x8, #0x363 ; "-[CacheClass cacheMethodA]"
0x102c25d28 <+32>: str x8, [x9]
0x102c25d2c <+36>: adrp x0, 3
0x102c25d30 <+40>: add x0, x0, #0x90 ; @"%s"
0x102c25d34 <+44>: bl 0x102c26344 ; symbol stub for: NSLog
0x102c25d38 <+48>: ldp x29, x30, [sp, #0x20]
0x102c25d3c <+52>: add sp, sp, #0x30
0x102c25d40 <+56>: ret

从输出结果可以看出,缓存数组位置是正确的,类CacheClass缓存了两个方法,分别为:-[NSObject init]-[CacheClass cacheMethodA]

用代码获取:

#if __arm64__
#if TARGET_OS_EXCLAVEKIT
#define ISA_MASK 0xfffffffffffffff8ULL
#elif __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#define ISA_MASK 0x007ffffffffffff8ULL
#else
#define ISA_MASK 0x0000000ffffffff8ULL
#endif
#endif

uintptr_t _isaForObject(NSObject *obj) {
if (obj == nil) return 0;
struct _object {
BytePtr isa;
};
struct _object *obj_ptr = (struct _object *)(__bridge void *)obj;
return (uintptr_t)((uintptr_t)obj_ptr->isa & ISA_MASK);
}

typedef uint32_t mask_t;
//cache_t源码模仿
const uintptr_t maskShift = 48;
const uintptr_t maskZeroBits = 4;
const uintptr_t maxMask = (((uintptr_t)1 << (64 - maskShift))-1);
const uintptr_t bucketsMask = ((uintptr_t)1 << (maskShift - maskZeroBits)) - 1; //0x100000000000-1 = 0xfffffffffff
struct bucket_t {
IMP _imp;
SEL _sel;
};
struct cache_t {
uintptr_t _bucketsAndMaybeMask; // 8
union {
struct {
uint32_t _unused;
uint16_t _occupied;
uint16_t _flags;
};
uintptr_t _originalPreoptCache; // 8
};
};
struct bucket_t *buckets(struct cache_t *cache) {
return (struct bucket_t *)(cache->_bucketsAndMaybeMask & bucketsMask);
}
uint32_t mask(struct cache_t *cache) {
return (uint32_t)(cache->_bucketsAndMaybeMask >> maskShift);
}
uint32_t capacity(struct cache_t *cache) {
return mask(cache) ? mask(cache)+1 : 0;
}
mask_t occupied(struct cache_t *cache) {
return cache->_occupied;
}

void _printMethodCaches(id obj) {
printf("============================\n");
uintptr_t isa = _isaForObject(obj);

// 读取cache结构体
struct cache_t *cache = (struct cache_t *)(isa + 0x10);

// 读取bucket_t
struct bucket_t *bucket_array = buckets(cache);

// 获取count
uint32_t count = capacity(cache);

// 获取已缓存数
uint32_t occupied_count = occupied(cache);

printf("哈希表容积:%u\t\t\t已缓存方法数:%u\n",count, occupied_count);

// 输出缓存内容
for (int c = 0; c < count; c++) {
struct bucket_t *bucket = (bucket_array + c);
printf("imp->sel:0x%lx->%s\n", (intptr_t)bucket->_imp, sel_getName(bucket->_sel));
}
printf("============================\n");
}

// 调用
CacheClass *cacheObj = [[CacheClass alloc] init];
[cacheObj cacheMethodA];
_printMethodCaches(cacheObj);

// 输出
============================
哈希表容积:2 已缓存方法数:2
imp->sel:0x4f5fd201ad686ff0->init
imp->sel:0xe9344e810494dc50->cacheMethodA
============================

需要注意的是,缓存哈希表有一个扩容的过程,当缓存方法超过了哈希表容积时,就会触发扩容,此时,之前的缓存并不会被复制到新的hash表中,而是重新还是缓存!

例如上面的调用修改为如下:

    CacheClass *cacheObj = [[CacheClass alloc] init];
[cacheObj cacheMethodA];
[cacheObj cacheMethodB:@"B"];

_printMethodCaches(cacheObj);

则输出为:

============================
哈希表容积:4 已缓存方法数:1
imp->sel:0x0-><null selector>
imp->sel:0x0-><null selector>
imp->sel:0xf85bf08100269c6c->cacheMethodB:
imp->sel:0x0-><null selector>
============================

从上面的输出可以看出,调用方法-[CacheClass cacheMethodB:]时,触发了缓存表扩容;扩容过程中,它舍弃了原缓存表中的方法,仅缓存了当前方法(-[CacheClass cacheMethodB:])。


收起阅读 »

类的布局——方法列表(1)

和成员变量的使用方式一样,都是先初始化对象,再使用。这就容易让人产生一个误区:实例方法和成员变量一样,每个对象独一份,在对象初始化时存储在堆区。类对象的定义:struct class_rw_ext_t { DECLARE_AUTHED_PTR_TEMPL...
继续阅读 »

对于OC的实例方法,用法是先实例化对象,再用实例对象调用方法:

// ObjectA.h
@interface ObjectA : NSObject
@property(nonatomic,assign,readonly)BOOL b;
- (void)funcA;
- (void)funcB:(NSString *)str;
@end

// ObjectA.m
#import "ObjectA.h"
@implementation ObjectA
- (void)funcA {
NSLog(@"方法A");
}
- (void)funcB:(NSString *)str {
NSLog(@"方法B:%@", str);
}
@end

// 调用实例方法
ObjectA *aObj = [[ObjectA alloc] init];
[aObj funcA];

和成员变量的使用方式一样,都是先初始化对象,再使用。这就容易让人产生一个误区:实例方法和成员变量一样,每个对象独一份,在对象初始化时存储在堆区。

事实上,实例方法和成员变量有着本质的区别,成员变量是在对象实例化之后,存储在内存的堆区(即对象所在的地址);而实例方法作为可执行部分,编译之后存储在代码段,实例方法的地址(IMP)、方法名等信息则被存储在类对象中。

类对象的定义:

struct class_rw_ext_t {
DECLARE_AUTHED_PTR_TEMPLATE(class_ro_t)
class_ro_t_authed_ptr ro;
method_array_t methods;
property_array_t properties;
protocol_array_t protocols;
const char *demangledName;
uint32_t version;
};

struct class_rw_t {
// Be warned that Symbolication knows the layout of this structure.
uint32_t flags;
uint16_t witness;
explicit_atomic ro_or_rw_ext;

private:
using ro_or_rw_ext_t = objc::PointerUnion;
};
struct class_data_bits_t {
// Values are the FAST_ flags above.
uintptr_t bits;
};

struct objc_class : objc_object {
// Class ISA; // 继承自 struct objc_object
Class superclass;
cache_t cache; // formerly cache pointer and vtable
class_data_bits_t bits;
class_rw_t *data() const {
return bits.data();
}
};

用一个图大概描述一下其内存布局:


用lldb大致验证一下:

  • 初始化对象:
  ObjectA *aObj = [[ObjectA alloc] init];
NSLog(@"=====分割线====="); // 此处下断点
  • 获取对象地址:
(lldb) po aObj
  • 获取isa指针:
(lldb) x/1gx 0x282dccdd0
0x282dccdd0: 0x01000001001f14a9 // 0x01000001001f14a9 为优化后的isa指针,要获取真实的isa指针,需进一步处理
(lldb) p/x 0x01000001001f14a9 & 0x0000000ffffffff8ULL
(unsigned long long) $1 = 0x00000001001f14a8 // 0x00000001001f14a8 为真实的isa指针
  • 获取bits:
(lldb) x/5gx 0x00000001001f14a8
0x1001f14a8: 0x00000001001f1480 0x000000020afa07d8
0x1001f14b8: 0x0003000283a88440 0x8018000100000000
0x1001f14c8: 0x8000000282fea4e4 // 0x8000000282fea4e4 即为bits的值
  • 从bits值中获取class_rw_t结构体:
(lldb) p/x 0x8000000282fea4e4 & 0x0f00007ffffffff8UL
(unsigned long) $2 = 0x0000000282fea4e0 // struct class_rw_t指针
  • 获取class_ro_t结构体:
(lldb) x/2gx 0x0000000282fea4e0
0x282fea4e0: 0x0000000080080006 0x00000001001f02c8 // 0x00000001001f02c8 为ro_or_rw_ext联合指针
(lldb) p/x 0x00000001001f02c8 & 0x1
(long) $3 = 0x0000000000000000 // ro_or_rw_ext & 0x1 == 0 ,则 ro_or_rw_ex 的值即为class_ro_t指针
  • 获取baseMethods
(lldb) x/5gx 0x00000001001f02c8
0x1001f02c8: 0x0000000800000080 0x0000000000000009
0x1001f02d8: 0x0000000000000000 0x00000001001ef3d3
0x1001f02e8: 0x00000001001f0238 // 该指针即为baseMethods
  • 查看baseMethods的内容:
// baseMethods为struct method_list_t指针。
// struct method_list_t 结构如下:

template
struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
uint32_t entsize() const {
return entsizeAndFlags & ~FlagMask;
}
uint32_t flags() const {
return entsizeAndFlags & FlagMask;
}

ALWAYS_INLINE
Element& getOrEnd(uint32_t i) const {
ASSERT(i <= count);
uint32_t iBytes;
if (os_mul_overflow(i, entsize(), &iBytes))
_objc_fatal("entsize_list_tt overflow: index %" PRIu32 " in list %p with entsize %" PRIu32,
i, this, entsize());
return *PointerModifier::modify(*(List *)this, (Element *)((uint8_t *)this + sizeof(*this) + iBytes));
}

Element& get(uint32_t i) const {
ASSERT(i < count);
return getOrEnd(i);
}
...
};

struct method_list_t : entsize_list_tt {
...
};

乍一看,似乎这里并没有存储任何和方法列表相关的信息,但是细看结构体模版entsize_list_ttgetOrEnd实现就会发现,该模版在结构体内部维护了一个Element类型的数组,其实际结构可以理解为:

struct entsize_list_tt {
uint32_t entsizeAndFlags;
uint32_t count;
Element elements[0];
};

从上面可以看出,该模版作为method_list_t的实现,Element的类型为:method_t

struct method_t {
SEL name;
const char *types;
MethodListIMP imp;
};

到这里,baseMethods的内容便呼之欲出了:

(lldb) x/15gx 0x00000001001f0238			// 以指针的形式读取15条baseMethods的内容
0x1001f0238: 0x000000030000001b 0x00000001001ef035 //0x000000030000001b 为 entsizeAndFlags 和 count 的值
0x1001f0248: 0x00000001001ef444 0x00000001001edd24
0x1001f0258: 0x00000001001ef03b 0x00000001001ef44c
0x1001f0268: 0x00000001001edd50 0x00000001b04f22e9
0x1001f0278: 0x00000001001ef457 0x00000001001eddb4
0x1001f0288: 0x0000000100000020 0x00000001001f1420
0x1001f0298: 0x00000001001ee619 0x00000001001ef45f
0x1001f02a8: 0x0000000100000000
(lldb) p (SEL)0x00000001001ef035 // 读取第1个method_t的name
(SEL) $11 = "funcA"
(lldb) p (char *)0x00000001001ef444 // 读取第1个method_t的types
(char *) $12 = 0x00000001001ef444 "v16@0:8"
(lldb) dis -a 0x00000001001edd24 // 通过反编译指令验证第1个method_t的imp
ObjectDemo`-[ObjectA funcA]:
0x1001edd24 <+0>: sub sp, sp, #0x20
0x1001edd28 <+4>: stp x29, x30, [sp, #0x10]
0x1001edd2c <+8>: add x29, sp, #0x10
0x1001edd30 <+12>: str x0, [sp, #0x8]
0x1001edd34 <+16>: str x1, [sp]
0x1001edd38 <+20>: adrp x0, 3
0x1001edd3c <+24>: add x0, x0, #0x90 ; @
0x1001edd40 <+28>: bl 0x1001ee3a8 ; symbol stub for: NSLog
0x1001edd44 <+32>: ldp x29, x30, [sp, #0x10]
0x1001edd48 <+36>: add sp, sp, #0x20
0x1001edd4c <+40>: ret
(lldb) p (SEL)0x00000001001ef03b // 读取第2个method_t的name
(SEL) $13 = "funcB:"
(lldb) p (char *)0x00000001001ef44c // 读取第2个method_t的types
(char *) $14 = 0x00000001001ef44c "v24@0:8@16"
(lldb) dis -a 0x00000001001edd50 // 通过反编译指令验证第2个method_t的imp
ObjectDemo`-[ObjectA funcB:]:
0x1001edd50 <+0>: sub sp, sp, #0x40
0x1001edd54 <+4>: stp x29, x30, [sp, #0x30]
0x1001edd58 <+8>: add x29, sp, #0x30
0x1001edd5c <+12>: mov x8, x1
0x1001edd60 <+16>: mov x1, x2
0x1001edd64 <+20>: stur x0, [x29, #-0x8]
0x1001edd68 <+24>: stur x8, [x29, #-0x10]
0x1001edd6c <+28>: add x0, sp, #0x18
0x1001edd70 <+32>: str x0, [sp, #0x8]
0x1001edd74 <+36>: mov x8, #0x0
0x1001edd78 <+40>: str x8, [sp, #0x10]
0x1001edd7c <+44>: str xzr, [sp, #0x18]
0x1001edd80 <+48>: bl 0x1001ee42c ; symbol stub for: objc_storeStrong
0x1001edd84 <+52>: ldr x8, [sp, #0x18]
0x1001edd88 <+56>: mov x9, sp
0x1001edd8c <+60>: str x8, [x9]
0x1001edd90 <+64>: adrp x0, 3
0x1001edd94 <+68>: add x0, x0, #0xb0 ; @
0x1001edd98 <+72>: bl 0x1001ee3a8 ; symbol stub for: NSLog
0x1001edd9c <+76>: ldr x0, [sp, #0x8]
0x1001edda0 <+80>: ldr x1, [sp, #0x10]
0x1001edda4 <+84>: bl 0x1001ee42c ; symbol stub for: objc_storeStrong
0x1001edda8 <+88>: ldp x29, x30, [sp, #0x30]
0x1001eddac <+92>: add sp, sp, #0x40
0x1001eddb0 <+96>: ret

用lldb将方法列表的存储位置过一遍后,整个过程清晰了很多,接下来再用代码走一遍:

// 定义 ISA_MASK 用于获取isa指针
#if __arm64__
#if TARGET_OS_EXCLAVEKIT
#define ISA_MASK 0xfffffffffffffff8ULL
#elif __has_feature(ptrauth_calls) || TARGET_OS_SIMULATOR
#define ISA_MASK 0x007ffffffffffff8ULL
#else
#define ISA_MASK 0x0000000ffffffff8ULL
#endif
#endif

// 定义 FAST_DATA_MASK 用于获取class_rw_t
#if TARGET_OS_EXCLAVEKIT
#define FAST_DATA_MASK 0x0000001ffffffff8UL
#elif TARGET_OS_IPHONE && !TARGET_OS_SIMULATOR
#define FAST_DATA_MASK 0x0f00007ffffffff8UL
#else
#define FAST_DATA_MASK 0x0f007ffffffffff8UL
#endif

// 定义方法结构体
struct method_t {
SEL sel;
char *types;
IMP imp;
};

// 定义方法列表结构体
struct method_list_t {
uint32_t entsizeAndFlags;
uint32_t count;
struct method_t elements[0];
};

// 从实例对象中获取isa指针
BytePtr _isaForObject(NSObject *obj) {
if (obj == nil) return NULL;
struct _object {
BytePtr isa;
};
struct _object *obj_ptr = (struct _object *)(__bridge void *)obj;
return (BytePtr)((int64_t)obj_ptr->isa & ISA_MASK);
}

// 从isa指针中获取方法列表和方法数
void _methodsWithIsa(BytePtr isa, struct method_t **methods, uint32_t *count) {
// 获取isa中的bits
uintptr_t bits = *((uintptr_t *)(isa + 8/*isa*/ + 8/*superclass*/ + 16/*cache*/));

// 获取 class_rw_t
uintptr_t class_rw_t = bits & FAST_DATA_MASK;

// 获取 ro_or_rw_ext
uintptr_t ro_or_rw_ext = *((uintptr_t *)(class_rw_t + 0x8));

// 获取class_ro_t
// 判断是class_rw_ext_t 还是 class_ro_t
uintptr_t class_ro_t = ro_or_rw_ext;
if (ro_or_rw_ext & 0x1) {
// class_rw_ext_t
class_ro_t = *((uintptr_t *)ro_or_rw_ext);
}

// 获取class_ro_t 中的 baseMethods
uintptr_t baseMethods = *(uintptr_t *)(class_ro_t + 4/*flags*/ + 4/*instanceStart*/ + 4/*instanceSize*/ + 4/*reserved*/ + 8/*ivarLayout*/ + 8/*name*/);

struct method_list_t *_method_list_t = (struct method_list_t *)baseMethods;
*count = _method_list_t->count;
*methods = _method_list_t->elements;
};

// 输出方法信息
void _printMethods(struct method_t *methods, uint32_t count) {
struct method_t *node = methods;
for (int i=0; i printf("=============================\n");
printf("SEL:%s\n", sel_getName(node->sel));
printf("types:%s\n",node->types);
printf("imp:0x%lx\n",(uintptr_t)node->imp);
node++;
}
printf("=============================\n");
}

// 调用
ObjectA *aObj = [[ObjectA alloc] init];

BytePtr a_isa = _isaForObject(aObj);
struct method_t *method = NULL;
uint32_t count;
_methodsWithIsa(a_isa, &method, &count);
_printMethods(method, count);

// 输出如下:
=============================
SEL:funcA
types:v16@0:8
imp:0x1021ddce8
=============================
SEL:funcB:
types:v24@0:8@16
imp:0x1021ddd14
=============================
SEL:b
types:B16@0:8
imp:0x1021ddd78
=============================


收起阅读 »

类的布局——成员变量

日常开发中,我们定义的OC类,都会被编译成结构体类型:/// Represents an instance of a class.struct objc_object { Class _Nonnull isa OBJC_ISA_AVAILABILITY...
继续阅读 »

日常开发中,我们定义的OC类,都会被编译成结构体类型:

/// Represents an instance of a class.
struct objc_object {
Class _Nonnull isa OBJC_ISA_AVAILABILITY;
};

在类中定义的属性和成员变量,也会变成结构体的成员变量:

@interface ObjectA : NSObject
@property(nonatomic,assign)BOOL b;
@end

// 类ObjectA会转化为如下结构体
struct ObjectA_IMPL {
struct NSObject_IMPL NSObject_IVARS; // 继承自NSObject
BOOL _b;
};

知道了类的真面目,可以在内存级别去做一些操作;例如:

@interface ObjectA : NSObject
@property(nonatomic,assign,readonly)BOOL b;
@end

...

- (void)viewDidLoad {
[super viewDidLoad];
ObjectA *aObj = [[ObjectA alloc] init];

NSLog(@"%@",aObj); // 在这一行打断点

// Do any additional setup after loading the view.
}


用代码实现如下:

- (void)viewDidLoad {
[super viewDidLoad];
ObjectA *aObj = [[ObjectA alloc] init];

void *aObj_ptr = (__bridge void *)aObj;
BOOL *_b_ptr = (BOOL *)((BytePtr)aObj_ptr + 0x8);
*_b_ptr = YES;

NSLog(@"%d",aObj.b); // 打印:1 证明已修改

// Do any additional setup after loading the view.
}


收起阅读 »

iOS面试题目——hook block(3)

// 题目:实现下面的函数,将任意参数 block 的实现修改成打印所有入参,并调用原始实现//// 比如// void(^block)(int a, NSString *b) = ^(int a, NSString *b){// NSLog(@"...
继续阅读 »
// 题目:实现下面的函数,将任意参数 block 的实现修改成打印所有入参,并调用原始实现
//
// 比如
// void(^block)(int a, NSString *b) = ^(int a, NSString *b){
// NSLog(@"block invoke");
// }
// HookBlockToPrintArguments(block);
// block(123,@"aaa");
// 这里输出 "123,aaa" 和 "block invoke"

// void(^block)(int a, double b) = ^(int a, double b){
// NSLog(@"block invoke");
// }
// HookBlockToPrintArguments(block);
// block(123,3.14);
// 这里输出 "123,3.14" 和 "block invoke"

分析题目:首先,题目的本意和上一个题目一样,就是hook block 的 invoke,然后将其所有的入参打印出来,再调用原实现。区别在于任意Block,这个任意block,就让我们无法对用来替换的函数有一个很合适的定义,因为我们定义的时候,根本就不知道即将hook的block有几个参数。

这个问题,可以用libffi来解决。

整个思路如下:

1、获取要hook的block的相关信息,例如返回值、参数列表。这些信息都存储在bkock的方法签名里。

2、通过上一步获取到的信息,利用libffi创建一个函数模板(ffi_prep_cif())。

3、创建动态调用函数,并替换block中的Invoke。

4、编写替换函数,并实现调用原函数。

代码实现:

  • 获取block的签名信息:

    struct Block_layout *layout = (__bridge struct Block_layout *)block;

if (! (layout->flags & BLOCK_HAS_SIGNATURE)){
NSLog(@"当前block没有签名");
return;
}

uint8_t *desc = (uint8_t *)layout->descriptor;

desc += sizeof(struct Block_descriptor_1);

if (layout->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
struct Block_descriptor_3 *desc_3 = (struct Block_descriptor_3 *)desc;

const char *signature = desc_3->signature;
NSMethodSignature *m_signature = [NSMethodSignature signatureWithObjCTypes:signature];
  • 创建函数模版:
    ffi_type **args = malloc(sizeof(ffi_type *)*[m_signature numberOfArguments]);

// 返回值类型
ffi_type *return_ffi;
const char *return_type = [m_signature methodReturnType];
if (*return_type == @encode(_Bool)[0]) {
return_ffi = &ffi_type_sint8;
}else if (*return_type == @encode(signed char)[0]){
return_ffi = &ffi_type_sint8;
}else if (*return_type == @encode(unsigned char)[0]){
return_ffi = &ffi_type_uint8;
}else if (*return_type == @encode(short)[0]){
return_ffi = &ffi_type_sint16;
}else if (*return_type == @encode(int)[0]){
return_ffi = &ffi_type_sint32;
}else if (*return_type == @encode(long)[0]){
return_ffi = &ffi_type_sint64;
}else if (*return_type == @encode(long long)[0]){
return_ffi = &ffi_type_sint64;
}else if (*return_type == @encode(id)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(Class)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(SEL)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(void *)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(char *)[0]){
return_ffi = &ffi_type_pointer;
}else if (*return_type == @encode(float)[0]){
return_ffi = &ffi_type_float;
}else if (*return_type == @encode(double)[0]){
return_ffi = &ffi_type_double;
}else if (*return_type == @encode(void)[0]){
return_ffi = &ffi_type_void;
}else{
NSLog(@"未找到合适的类型");
return;
}
// 初始化参数列表
for (int i=0; i<[m_signature numberOfArguments]; i++) {
const char *type = [m_signature getArgumentTypeAtIndex:i];
if (*type == @encode(_Bool)[0]) {
args[i] = &ffi_type_sint8;
}else if (*type == @encode(signed char)[0]){
args[i] = &ffi_type_sint8;
}else if (*type == @encode(unsigned char)[0]){
args[i] = &ffi_type_uint8;
}else if (*type == @encode(short)[0]){
args[i] = &ffi_type_sint16;
}else if (*type == @encode(int)[0]){
args[i] = &ffi_type_sint32;
}else if (*type == @encode(long)[0]){
args[i] = &ffi_type_sint64;
}else if (*type == @encode(long long)[0]){
args[i] = &ffi_type_sint64;
}else if (*type == @encode(id)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(Class)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(SEL)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(void *)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(char *)[0]){
args[i] = &ffi_type_pointer;
}else if (*type == @encode(float)[0]){
args[i] = &ffi_type_float;
}else if (*type == @encode(double)[0]){
args[i] = &ffi_type_double;
}else{
NSLog(@"未知类型:注,结构体未处理");
return;
}
}

// _cif 定义的是全局变量 ffi_cif _cif;
ffi_status status = ffi_prep_cif(&_cif, FFI_DEFAULT_ABI, (int)[m_signature numberOfArguments], return_ffi, args);
if (status != FFI_OK) {
NSLog(@"初始化 cif 失败");
return;
}
  • 创建并绑定动态调用的函数:
    // 	_closure 定义的是全局变量		ffi_closure *_closure;
// _replacementInvoke 定义的是全局变量 void *_replacementInvoke;

_closure = ffi_closure_alloc(sizeof(ffi_closure), &_replacementInvoke);
if (!_closure) {
NSLog(@"hook 失败");
return;
}
ffi_status closure_loc_status = ffi_prep_closure_loc(_closure, &_cif, replace_bloke2_2, (__bridge void *)(NSObject.new), _replacementInvoke);
if (closure_loc_status != FFI_OK) {
NSLog(@"Hook failed! ffi_prep_closure returned %d", (int)status);
return;
}
  • 替换block中的invoke:
    //    修改内存属性
vm_address_t invoke_addr = (vm_address_t)&layout->invoke;
vm_size_t vmsize = 0;
mach_port_t object = 0;
vm_region_basic_info_data_64_t info;
mach_msg_type_number_t infoCnt = VM_REGION_BASIC_INFO_COUNT_64;
kern_return_t ret = vm_region_64(mach_task_self(), &invoke_addr, &vmsize, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &infoCnt, &object);
if (ret != KERN_SUCCESS) {
NSLog(@"获取失败");
return;
}
vm_prot_t protection = info.protection;
// 判断内存是否可写
if ((protection&VM_PROT_WRITE) == 0) {
// 修改内存属性 ===> 可写
ret = vm_protect(mach_task_self(), invoke_addr, sizeof(invoke_addr), false, protection|VM_PROT_WRITE);
if (ret != KERN_SUCCESS) {
NSLog(@"修改失败");
return;
}
}
// 保存原来的invoke
origin_blockInvoke2_2 = (void *)layout->invoke;
layout->invoke = (uintptr_t)_replacementInvoke;
  • 实现替换函数:
    void replace_bloke2_2(ffi_cif *cif, void *ret, void **args, void *userdata) {
struct Block_layout *layout = (struct Block_layout *)userdata;
uint8_t *desc = (uint8_t *)layout->descriptor;

desc += sizeof(struct Block_descriptor_1);

if (layout->flags & BLOCK_HAS_COPY_DISPOSE) {
desc += sizeof(struct Block_descriptor_2);
}
struct Block_descriptor_3 *desc_3 = (struct Block_descriptor_3 *)desc;

const char *signature = desc_3->signature;
NSMethodSignature *m_signature = [NSMethodSignature signatureWithObjCTypes:signature];

NSLog(@"回调函数");
NSLog(@"%d",cif->nargs);
// 解析参数
for (int i=0; i<[m_signature numberOfArguments]; i++) {
ffi_type *arg = args[i];
const char *type = [m_signature getArgumentTypeAtIndex:i];
if (*type == @encode(_Bool)[0]) {
NSLog(@"%d",(bool)arg->size);
}else if (*type == @encode(signed char)[0]){
NSLog(@"%d",(char)arg->size);
}else if (*type == @encode(unsigned char)[0]){
NSLog(@"%d",(unsigned char)arg->size);
}else if (*type == @encode(short)[0]){
NSLog(@"%d",(short)arg->size);
}else if (*type == @encode(int)[0]){
NSLog(@"%d",(int)arg->size);
}else if (*type == @encode(long)[0]){
NSLog(@"%ld",(long)arg->size);
}else if (*type == @encode(long long)[0]){
NSLog(@"%lld",(long long)arg->size);
}else if (*type == @encode(id)[0]){
NSLog(@"%@",(__bridge id)((void *)arg->size));
}else if (*type == @encode(Class)[0]){
NSLog(@"%@",(__bridge Class)((void *)arg->size));
}else if (*type == @encode(SEL)[0]){
NSLog(@"%s",((char *)arg->size));
}else if (*type == @encode(void *)[0]){
NSLog(@"0x%llx",((long long)arg->size));
}else if (*type == @encode(char *)[0]){
NSLog(@"%s",((char *)arg->size));
}else if (*type == @encode(float)[0]){
NSLog(@"%f",((float)arg->size));
}else if (*type == @encode(double)[0]){
NSLog(@"%f",((double)arg->size));
}else{
NSLog(@"未知类型:注,结构体未处理");
}
}
// 调用原函数
ffi_call(&_cif,(void *)origin_blockInvoke2_2, ret, args);
}


收起阅读 »

iOS面试题目——hook block(2)

// 题目:实现下面的函数,将 block 的实现修改成打印所有入参,并调用原始实现//// 例如:// void(^block)(int a, NSString *b) = ^(int a, NSString *b){// NSLog(@"blo...
继续阅读 »
// 题目:实现下面的函数,将 block 的实现修改成打印所有入参,并调用原始实现
//
// 例如:
// void(^block)(int a, NSString *b) = ^(int a, NSString *b){
// NSLog(@"block invoke");
// }
// HookBlockToPrintArguments(block);
// block(123,@"aaa");
// 这里输出 "123,aaa" 和 "block invoke"

分析:这个题目其实和题目一的本质是一样的,都是替换block的实现(即Hook Block),不过,相比较于题目一,这个题的侧重点在于:1、打印所有入参;2、调用原实现。针对这两个问题,我们逐一解析。

1、打印所有入参

对于已知参数个数和参数类型的block,要实现这个,其实并不难,只需要我们再声明替换函数的时候和block的参数对齐即可:

//这里要注意的是,第一个参数必须声明为block本身。
//针对 void(^block)(int a, NSString *b) ,我们可以将函数声明为如下形式:
void replace_bloke2(id block, int a, NSString *b);
2、调用原实现
上一个题目中,我们仅仅是将invoke的值替换了,也就是说我们舍弃了invoke原本的函数指针地址,即原本的实现;如果我们全局变量,将其先存储,再进行替换,然后在replace_bloke2函数中调用,是否就达到了目的呢?
//声明一个函数指针,用来存储invoke的值
void(*origin_blockInvoke2)(id block,int a,NSString *b);

void replace_bloke2(id block, int a, NSString *b) {
NSLog(@"%d,%@",a,b);
origin_blockInvoke2(block,a,b);
}

void HookBlockToPrintArguments(id block){

// 解析 block 为 struct Block_layout 结构体
struct Block_layout *layout = (__bridge struct Block_layout *)block;
// 修改内存属性
vm_address_t invoke_addr = (vm_address_t)&layout->invoke;
vm_size_t vmsize = 0;
mach_port_t object = 0;
vm_region_basic_info_data_64_t info;
mach_msg_type_number_t infoCnt = VM_REGION_BASIC_INFO_COUNT_64;
kern_return_t ret = vm_region_64(mach_task_self(), &invoke_addr, &vmsize, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &infoCnt, &object);
if (ret != KERN_SUCCESS) {
NSLog(@"获取失败");
return;
}
vm_prot_t protection = info.protection;
// 判断内存是否可写
if ((protection&VM_PROT_WRITE) == 0) {
// 修改内存属性 ===> 可写
ret = vm_protect(mach_task_self(), invoke_addr, sizeof(invoke_addr), false, protection|VM_PROT_WRITE);
if (ret != KERN_SUCCESS) {
NSLog(@"修改失败");
return;
}
}
// 保存原来的invoke
origin_blockInvoke2 = (void *)layout->invoke;
layout->invoke = (uintptr_t)replace_bloke2;
}



收起阅读 »

Xcode 15下,包含个推的项目运行时崩溃的处理办法

升级到Xcode15后,部分包含个推的项目在iOS17以下的系统版本运行时,会出现崩溃,由于崩溃在个推Framework内部,无法定位到具体代码,经过和个推官方沟通,确认问题是项目支持的最低版本问题。需要将项目的最低版本修改为iOS12.0或更高具体修改位置:...
继续阅读 »

升级到Xcode15后,部分包含个推的项目在iOS17以下的系统版本运行时,会出现崩溃,由于崩溃在个推Framework内部,无法定位到具体代码,经过和个推官方沟通,确认问题是项目支持的最低版本问题。

需要将项目的最低版本修改为iOS12.0或更高

具体修改位置:Target-General-Minimum Deployments-iOS 12.0



问题来源

收起阅读 »

iOS面试题目——hook block(1)

// 1、实现下面的函数,将 block 的实现修改为 NSLog(@"Hello world"); //也就是说,在调用完这个函数后调用用block()时,并不调用原始实现,而是打 "Hello world" void HookBlockToPrintHe...
继续阅读 »
// 1、实现下面的函数,将 block 的实现修改为 NSLog(@"Hello world");
//也就是说,在调用完这个函数后调用用block()时,并不调用原始实现,而是打 "Hello world"

void HookBlockToPrintHelloWorld(id block){

}


分析:题目的意思很明白,就是要实现一个函数,将作为参数传入的block的实现修改为一句log。有研究过block的结构的都知道,block实际上就是一个接口体。而它的实现部分,是作为函数指针被保存在结构体的`invoke`中(即第16个字节处):

struct Block_layout {
    void *isa;
    volatile int32_t flags; // contains ref count
    int32_t reserved;
    uintptr_t invoke; // 此处保存的是实现代码的起始地址
    struct Block_descriptor_1 *descriptor;
    // imported variables
};


所以这题的解法也很明朗:就是将block结构体中的invoke函数指针地址替换为我们的函数:

// 定义一个函数
void block1_replace(void){
NSLog(@"Hello world");
};
void HookBlockToPrintHelloWorld(id block){
    // 解析 block 为 struct Block_layout 结构体
struct Block_layout *layout = (__bridge struct Block_layout *)block;

    //此处不能直接修改,因为该处地址属性为不可写状态,强行替换,会导致程序崩溃
    //layout->invoke = (uintptr_t)block1_replace;

    //修改内存属性
    vm_address_t invoke_addr = (vm_address_t)&layout->invoke;
    vm_size_t vmsize = 0;
    mach_port_t object = 0;
    vm_region_basic_info_data_64_t info;
    mach_msg_type_number_t infoCnt = VM_REGION_BASIC_INFO_COUNT_64;
    kern_return_t ret = vm_region_64(mach_task_self(), &invoke_addr, &vmsize, VM_REGION_BASIC_INFO, (vm_region_info_t)&info, &infoCnt, &object);
    if (ret != KERN_SUCCESS) {
        NSLog(@"获取失败");
        return;
    }
    vm_prot_t protection = info.protection;
    // 判断内存是否可写
    if ((protection&VM_PROT_WRITE) == 0) {
        // 修改内存属性 ===> 可写
ret = vm_protect(mach_task_self(), invoke_addr, sizeof(invoke_addr), false, protection|VM_PROT_WRITE);
        if (ret != KERN_SUCCESS) {
NSLog(@"修改失败");
            return;
}
}
layout->invoke = (uintptr_t)block1_replace;
}


收起阅读 »

在 SwiftUI 中创建一个环形 Slider

iOS
前言 Slider 控件是一种允许用户从一系列值中选择一个值的 UI 控件。在 SwiftUI 中,它通常呈现为直线上的拇指选择器。有时将这种类型的选择器呈现为一个圆圈,拇指绕着圆周移动可能会更好。本文介绍如何在 SwiftUI 中定义一个环形的 Slider...
继续阅读 »


前言


Slider 控件是一种允许用户从一系列值中选择一个值的 UI 控件。在 SwiftUI 中,它通常呈现为直线上的拇指选择器。有时将这种类型的选择器呈现为一个圆圈,拇指绕着圆周移动可能会更好。本文介绍如何在 SwiftUI 中定义一个环形的 Slider。


初始化环形轮廓


ZStack中的三个圆环开始。一个灰色的圆环代表滑块的路径轮廓,一个淡红色的圆弧代表沿着圆环的进度,一个圆圈代表当前光标或拇指的位置。将滑块的范围设置为0.0到1.0,并硬编码一个直径和一个的当前位置进度 - 0.33。

struct CircularSliderView1: View {
let progress = 0.33
let ringDiameter = 300.0

private var rotationAngle: Angle {
return Angle(degrees: (360.0 * progress))
}

var body: some View {
VStack {
ZStack {
Circle()
.stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
Circle()
.trim(from: 0, to: progress)
.stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
)
.rotationEffect(Angle(degrees: -90))
Circle()
.fill(Color.white)
.frame(width: 21, height: 21)
.offset(y: -ringDiameter / 2.0)
.rotationEffect(rotationAngle)
}
.frame(width: ringDiameter, height: ringDiameter)

Spacer()
}
.padding(80)
}
}



将进度值和拇指位置绑定


将进度变量更改为状态变量并添加默认 Slider。这个 Slider 用于修改进度值,并在圆形滑块上实现足够的代码以使拇指和进度弧响应。当前值显示在环形 Slider 的中心。

struct CircularSliderView2: View {
@State var progress = 0.33
let ringDiameter = 300.0

private var rotationAngle: Angle {
return Angle(degrees: (360.0 * progress))
}

var body: some View {
ZStack {
Color(hue: 0.58, saturation: 0.04, brightness: 1.0)
.edgesIgnoringSafeArea(.all)

VStack {
ZStack {
Circle()
.stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
.overlay() {
Text("\(progress, specifier: "%.1f")")
.font(.system(size: 78, weight: .bold, design:.rounded))
}
Circle()
.trim(from: 0, to: progress)
.stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
)
.rotationEffect(Angle(degrees: -90))
Circle()
.fill(Color.white)
.shadow(radius: 3)
.frame(width: 21, height: 21)
.offset(y: -ringDiameter / 2.0)
.rotationEffect(rotationAngle)
}
.frame(width: ringDiameter, height: ringDiameter)


VStack {
Text("Progress: \(progress, specifier: "%.1f")")
Slider(value: $progress,
in: 0...1,
minimumValueLabel: Text("0.0"),
maximumValueLabel: Text("1.0")
) {}
}
.padding(.vertical, 40)

Spacer()
}
.padding(.vertical, 40)
.padding()
}
}
}


添加触摸手势


DragGesture 被添加到滑块圆圈,并且使用临时文本视图显示拖动手势的当前位置。可以看到 x 和 y 坐标围绕包含环形 Slider 的位置中心的变化情况。

struct CircularSliderView3: View {
@State var progress = 0.33
let ringDiameter = 300.0

@State var loc = CGPoint(x: 0, y: 0)

private var rotationAngle: Angle {
return Angle(degrees: (360.0 * progress))
}

private func changeAngle(location: CGPoint) {
loc = location
}

var body: some View {
ZStack {
Color(hue: 0.58, saturation: 0.04, brightness: 1.0)
.edgesIgnoringSafeArea(.all)

VStack {
ZStack {
Circle()
.stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9), lineWidth: 20.0)
.overlay() {
Text("\(progress, specifier: "%.1f")")
.font(.system(size: 78, weight: .bold, design:.rounded))
}
Circle()
.trim(from: 0, to: progress)
.stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
style: StrokeStyle(lineWidth: 20.0, lineCap: .round)
)
.rotationEffect(Angle(degrees: -90))
Circle()
.fill(Color.blue)
.shadow(radius: 3)
.frame(width: 21, height: 21)
.offset(y: -ringDiameter / 2.0)
.rotationEffect(rotationAngle)
.gesture(
DragGesture(minimumDistance: 0.0)
.onChanged() { value in
changeAngle(location: value.location)
}
)
}
.frame(width: ringDiameter, height: ringDiameter)

Spacer().frame(height:50)

Text("Location = (\(loc.x, specifier: "%.1f"), \(loc.y, specifier: "%.1f"))")

Spacer()
}
.padding(.vertical, 40)
.padding()
}
}
}


为不同的坐标值设置滑块位置


圆形滑块上有两个表示进度的值,用于显示进度弧度的progress值和用于显示滑块光标的rotationAngle。应该只有一个属性来保存滑块进度。视图被提取到一个单独的结构中,该结构具有圆形滑块上进度的一个绑定值。


滑块的range的可选参数也是可用的。这需要对进度进行一些调整,以计算已设置的角度以及拇指在圆形滑块上位置的旋转角度。另外调用onAppear根据View出现前的进度值计算旋转角度。

struct CircularSliderView: View {
@Binding var progress: Double

@State private var rotationAngle = Angle(degrees: 0)
private var minValue = 0.0
private var maxValue = 1.0

init(value progress: Binding<Double>, in bounds: ClosedRange<Int> = 0...1) {
self._progress = progress

self.minValue = Double(bounds.first ?? 0)
self.maxValue = Double(bounds.last ?? 1)
self.rotationAngle = Angle(degrees: progressFraction * 360.0)
}

private var progressFraction: Double {
return ((progress - minValue) / (maxValue - minValue))
}

private func changeAngle(location: CGPoint) {
// 为位置创建一个向量(在 iOS 上反转 y 坐标系统)
let vector = CGVector(dx: location.x, dy: -location.y)

// 计算向量的角度
let angleRadians = atan2(vector.dx, vector.dy)

// 将角度转换为 0 到 360 的范围(而不是负角度)
let positiveAngle = angleRadians < 0.0 ? angleRadians + (2.0 * .pi) : angleRadians

// 根据角度更新滑块进度值
progress = ((positiveAngle / (2.0 * .pi)) * (maxValue - minValue)) + minValue
rotationAngle = Angle(radians: positiveAngle)
}

var body: some View {
GeometryReader { gr in
let radius = (min(gr.size.width, gr.size.height) / 2.0) * 0.9
let sliderWidth = radius * 0.1

VStack(spacing:0) {
ZStack {
Circle()
.stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.9),
style: StrokeStyle(lineWidth: sliderWidth))
.overlay() {
Text("\(progress, specifier: "%.1f")")
.font(.system(size: radius * 0.7, weight: .bold, design:.rounded))
}
// 取消注释以显示刻度线
//Circle()
// .stroke(Color(hue: 0.0, saturation: 0.0, brightness: 0.6),
// style: StrokeStyle(lineWidth: sliderWidth * 0.75,
// dash: [2, (2 * .pi * radius)/24 - 2]))
// .rotationEffect(Angle(degrees: -90))
Circle()
.trim(from: 0, to: progressFraction)
.stroke(Color(hue: 0.0, saturation: 0.5, brightness: 0.9),
style: StrokeStyle(lineWidth: sliderWidth, lineCap: .round)
)
.rotationEffect(Angle(degrees: -90))
Circle()
.fill(Color.white)
.shadow(radius: (sliderWidth * 0.3))
.frame(width: sliderWidth, height: sliderWidth)
.offset(y: -radius)
.rotationEffect(rotationAngle)
.gesture(
DragGesture(minimumDistance: 0.0)
.onChanged() { value in
changeAngle(location: value.location)
}
)
}
.frame(width: radius * 2.0, height: radius * 2.0, alignment: .center)
.padding(radius * 0.1)
}

.onAppear {
self.rotationAngle = Angle(degrees: progressFraction * 360.0)
}
}
}
}


CircularSliderView 的三种不同视图被添加到View中以测试和演示 Circular Slider 视图的不同功能。

struct CircularSliderView5: View {
@State var progress1 = 0.75
@State var progress2 = 37.5
@State var progress3 = 7.5

var body: some View {
ZStack {
Color(hue: 0.58, saturation: 0.06, brightness: 1.0)
.edgesIgnoringSafeArea(.all)

VStack {
CircularSliderView(value: $progress1)
.frame(width:250, height: 250)

HStack {
CircularSliderView(value: $progress2, in: 1...10)

CircularSliderView(value: $progress3, in: 0...100)
}

Spacer()
}
.padding()
}
}
}


总结


本文展示了如何定义响应拖动手势的圆环滑块控件。可以设置滑块视图的大小,并且滑块按预期工作。可以向控件添加更多参数以设置颜色或圆环内显示的值的格式。


作者:Swift社区
链接:https://juejin.cn/post/7215416975605661755
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Xcode 升级到14.3以后 调试与打包遇到的坑

iOS
前言 是苹果逼的,通知说2023年4月25日之后,所有的App都要在iOS16的SDK上打包。不然也不会有那么多事情(呜呜呜🥹)。 1.Xcode 14.3版本运行项目报错 问题如下:ld: file not found: /Applications/Xcod...
继续阅读 »

前言


是苹果逼的,通知说2023年4月25日之后,所有的App都要在iOS16的SDK上打包。不然也不会有那么多事情(呜呜呜🥹)。


1.Xcode 14.3版本运行项目报错


问题如下:

ld: file not found: /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/lib/arc/libarclite_iphonesimulator.a
clang: error: linker command failed with exit code 1 (use -v to see invocation)

报错信息看,都是在链接库的时候因为找不到静态库(libarclite_iphonesimulator.a/libarclite_iphoneos.a)而报错。利用访达的前往文件夹功能快速来到报错信息中的目录,发现连 arc目录都不存在,更不用说静态库文件。


开发人员解释说,因为系统已经内置有 ARC相关的库,所以没必要再额外链接,至少Xcode 14支持的最低部署目标iOS 11及以上版本的系统肯定是没问题的。如果应用部署目标不低于iOS 11还出现问题,那么应该是第三方库的部署目标有问题。


所以解决方案也很清晰了,将所有依赖库和应用最低部署版本都限制在iOS11以上即可。


解决方案:

post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '11.0'
end
end
end

2. 升级Xcode14以后项目报错 Stored properties cannot be marked potentially unavailable with '@available'


这是依赖库报错,把其中一个库升级到了最新的版本,不报错了。但是还有一个库没办法升级,因为我们的项目是Flutter项目,不知道是哪个三方库的依赖库,百度了好久没找到办法,最后还是强大的Google到方法:


在iOS目录下:

执行pod install
然后再执行pod update

最终可以了


3. Xcode升级到14.3 archieve打包失败

mkdir -p /Users/hsf/Library/Developer/Xcode/DerivedData/Ehospital-crirdmppgluxkodauexhkenjuxet/Build/Intermediates.noindex/ArchiveIntermediates/Ehospital/BuildProductsPath/Release-iphoneos/复旦云病理.app/Frameworks
Symlinked...
rsync --delete -av --filter P .*.?????? --links --filter "- CVS/" --filter "- .svn/" --filter "- .git/" --filter "- .hg/" --filter "- Headers" --filter "- PrivateHeaders" --filter "- Modules" "../../../IntermediateBuildFilesPath/UninstalledProducts/iphoneos/AliyunOSSiOS.framework" "/Users/hsf/Library/Developer/Xcode/DerivedData/Ehospital-crirdmppgluxkodauexhkenjuxet/Build/Intermediates.noindex/ArchiveIntermediates/Ehospital/InstallationBuildProductsLocation/Applications/复旦云病理.app/Frameworks"
building file list ... rsync: link_stat "/Users/hsf/Desktop/medical/app/iOS/Ehospital/../../../IntermediateBuildFilesPath/UninstalledProducts/iphoneos/AliyunOSSiOS.framework" failed: No such file or directory (2)
done

sent 29 bytes received 20 bytes 98.00 bytes/sec
total size is 0 speedup is 0.00
rsync error: some files could not be transferred (code 23) at /AppleInternal/Library/BuildRoots/97f6331a-ba75-11ed-a4bc-863efbbaf80d/Library/Caches/com.apple.xbs/Sources/rsync/rsync/main.c(996) [sender=2.6.9]
Command PhaseScriptExecution failed with a nonzero exit code

找到...-frameworks.sh 文件,替换

source="$(readlink "${source}")"

source="$(readlink -f "${source}")"

4. The version of CocoaPods used to generate the lockfile (1.3.1) is higher than the version of the current executable (1.1.0.beta.1). Incompatibility issues may arise.


这个比较简单,更新cocoapods就行。

sudo gem install cocoapods

5. Warning: CocoaPods minimum required version 1.6.0 or greater not installed…

sudo gem install cocoapods

6. Cocoapods 更新卡死在1.5.3,但控制台一直提示说有新版本


主要就是ruby的问题了。别问我怎么知道的,花了一天的时间。

ruby -v 查看版本

若比较低,现在一般都3.x了,所以要升级


用以下命令就可以升级了,可能需要科学上网。

brew update
brew install ruby

升级完成以后,ruby -v后其实还是原来的版本👌,这是因为环境变量没有配置。因此,还有一个步骤就是配置环境变量。

vi ~/.zshrc 

拷贝 export PATH="/usr/local/opt/ruby/bin:$PATH" 放进去


英文输入法下 按下esc键 输入 :wq


最后再执行

source ~/.bash_profile

然后更新gem

gem update #更新所有包
gem update --system #更新RubyGems软件

最后再更新pod

sudo gem install cocoapods

注意现在可能会提示说更新到了1.12.1了,但实际上还是1.5.3,所以还要执行另外一个命令。

sudo gem install -n /usr/local/bin cocoapods

这个就可以有效升级了。


7. gem常用命令

gem -v #gem版本
gem update #更新所有包
gem update --system #更新RubyGems软件
gem install rake #安装rake,从本地或远程服务器
gem install rake --remote #安装rake,从远程服务器
gem install watir -v(或者--version) 1.6.2#指定安装版本的
gem uninstall rake #卸载rake包
gem list d #列出本地以d打头的包
gem query -n ''[0-9]'' --local #查找本地含有数字的包
gem search log --both #从本地和远程服务器上查找含有log字符串的包
gem search log --remoter #只从远程服务器上查找含有log字符串的包
gem search -r log #只从远程服务器上查找含有log字符串的包
gem help #提醒式的帮助
gem help install #列出install命令 帮助
gem help examples #列出gem命令使用一些例子
gem build rake.gemspec #把rake.gemspec编译成rake.gem
gem check -v pkg/rake-0.4.0.gem #检测rake是否有效
gem cleanup #清除所有包旧版本,保留最新版本
gem contents rake #显示rake包中所包含的文件
gem dependency rails -v 0.10.1 #列出与rails相互依赖的包
gem environment #查看gem的环境

结语


有些坑现在只是知道这样做就行,还不知道为什么。后面再补补吧。


作者:Jalor
链接:https://juejin.cn/post/7229223235681878071
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS-解决定位权限卡顿问题

iOS
一、简介 在iOS系统中,定位权限获取是一个涉及进程间同步通信的方法,如果频繁访问可能会导致卡顿或者卡死。在一些打车或者地图类的APP中,定位权限的卡顿报错可能是大头,亟需解决! 下面是系统类提供的访问定位权限的方法:// CLLocationManager是...
继续阅读 »

一、简介


在iOS系统中,定位权限获取是一个涉及进程间同步通信的方法,如果频繁访问可能会导致卡顿或者卡死。在一些打车或者地图类的APP中,定位权限的卡顿报错可能是大头,亟需解决!
下面是系统类提供的访问定位权限的方法:

// CLLocationManager是系统的定位服务管理类
open class CLLocationManager : NSObject {
// 1.下面方法是访问系统设置中定位是否打开
@available(iOS 4.0, *)
open class func locationServicesEnabled() -> Bool

// 2.1 iOS 14.0之后,访问定位的授权状态
@available(iOS 14.0, *)
open var authorizationStatus: CLAuthorizationStatus { get }

// 2.2 iOS 14.0之后,访问定位的授权状态
@available(iOS, introduced: 4.2, deprecated: 14.0)
open class func authorizationStatus() -> CLAuthorizationStatus
}

二、从卡顿堆栈例子中分析问题


为了解决这个卡顿,首先要分析卡顿报错堆栈。接下来举一个定位权限频繁获取导致的卡顿的堆栈:

0 libsystem_kernel.dylib _mach_msg2_trap + 8
1 libsystem_kernel.dylib _mach_msg2_internal + 80
2 libsystem_kernel.dylib _mach_msg_overwrite + 388
3 libsystem_kernel.dylib _mach_msg + 24
4 libdispatch.dylib __dispatch_mach_send_and_wait_for_reply + 540
5 libdispatch.dylib _dispatch_mach_send_with_result_and_wait_for_reply + 60
6 libxpc.dylib _xpc_connection_send_message_with_reply_sync + 240
7 Foundation ___NSXPCCONNECTION_IS_WAITING_FOR_A_SYNCHRONOUS_REPLY__ + 16
8 Foundation -[NSXPCConnection _sendInvocation:orArguments:count:methodSignature:selector:withProxy:] + 2236
9 Foundation -[NSXPCConnection _sendSelector:withProxy:arg1:arg2:arg3:] + 136
10 Foundation __NSXPCDistantObjectSimpleMessageSend3 + 76
11 CoreLocation _CLCopyTechnologiesInUse + 30852
12 CoreLocation _CLCopyTechnologiesInUse + 25724
13 CoreLocation _CLClientStopVehicleHeadingUpdates + 104440
14 MyAPPName +[ZLLocationRecorder locationAuthorised] + 40
15 ... // 以下略
  • 首先从第14行找到是ZLLocationRecorder类的locationAuthorised方法调用后,执行到了系统库函数,最终导致了卡死、卡顿。

  • 对堆栈中第0-13行中的方法做一番了解,初步发现xpc_connection_send_message_with_reply_sync函数涉及进程间同步通信,可能会阻塞当前线程点击查看官方方法说明



该函数说明:Sends a message over the connection and blocks the caller until it receives a reply.

  • 接下来添加符号断点xpc_connection_send_message_with_reply_sync, 注意如果是系统库中的带下划线的函数,我们添加符号断点的时候一般需要少一个下划线_. 执行后,从Xcode的方法调用栈视图中查看,可以发现ZLLocationRecorder类的locationAuthorised方法内部中调用CLLocationManager类的locationServicesEnabledauthorizationStatus方法都会来到这个符号断点.所以确定了是这两个方法导致的卡顿。(调试时并未发现卡顿,只是线上用户的使用环境更加复杂,卡顿时间长一点就被监控到了,我们目前卡顿监控是3秒,卡死监控是10s+)。

  • 然后通过CLLocationManager类的authorizationStatus方法说明,发现也是说在权限发生改变后,系统会保证调用代理方法locationManagerDidChangeAuthorization(_:),所以就产生了我们的解决方案,最终上线后也是直接解决了这个卡顿,并且APP启动耗时监控数据也因此变好了一些。


三、具体的解决方案



 注意点:设置代理必须在有runloop的线程,如果业务量不多的话,就在主线程设置就可以。


四、Demo类,可以直接用

import CoreLocation

public class XLLocationAuthMonitor: NSObject, CLLocationManagerDelegate {
// 单例类
@objc public static let shared = XLLocationAuthMonitor()

/// 定位服务是否可用, 这里设置成变量避免过于频繁调用系统方法时产生卡顿,系统方法涉及进程间通信
@objc public private(set) var serviceEnabled: Bool {
set {
threadSafe { _serviceEnabled = newValue }
}

get {
threadSafe { _serviceEnabled ?? CLLocationManager.locationServicesEnabled() }
}
}

/// 定位服务授权状态
@objc public private(set) var authStatus: CLAuthorizationStatus {
set {
threadSafe { _authStatus = newValue }
}

get {
threadSafe {
if let auth = _authStatus {
return auth
}
if #available(iOS 14.0, *) {
return locationManager.authorizationStatus
} else {
return CLLocationManager.authorizationStatus()
}
}
}
}

/// 计算属性,这里返回当前定位是否可用
@objc public var isLocationEnable: Bool {
guard serviceEnabled else {
return false
}

switch authStatus {
case .authorizedAlways, .authorizedWhenInUse:
return true
case .denied, .notDetermined, .restricted:
return false
default: return false
}
}

// MARK: - 内部使用的私有属性
private lazy var locationManager: CLLocationManager = CLLocationManager()
private let _lock = NSLock()
private var _serviceEnabled: Bool?
private var _authStatus: CLAuthorizationStatus?

private override init() {
super.init()
// 如果是主线程则直接设置,不是则在mainQueue中设置
DispatchQueue.main.safeAsync {
self.locationManager.delegate = self
}
}

private func threadSafe<T>(task: () -> T) -> T {
_lock.lock()
defer { _lock.unlock() }
return task()
}

// MARK: - CLLocationManagerDelegate
/// iOS 14以上调用
public func locationManagerDidChangeAuthorization(_ manager: CLLocationManager) {
if #available(iOS 14.0, *) {
authStatus = locationManager.authorizationStatus
serviceEnabled = CLLocationManager.locationServicesEnabled()
}
}

/// iOS 14以下调用
public func locationManager(_ manager: CLLocationManager, didChangeAuthorization status: CLAuthorizationStatus) {
authStatus = status
serviceEnabled = CLLocationManager.locationServicesEnabled()
}
}

作者:sweet丶
链接:https://juejin.cn/post/7247504715726929978
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

鸿蒙原生应用,全面启动,开发者需要抓住风口的浪尖

iOS
前言 老铁们,就在前天,9月25日,在华为秋季全场景新品发布会上,华为常务董事、终端BG CEO、智能汽车解决方案BU董事长余承东介绍了鸿蒙系统的最新进展:HarmonyOS 4发布后,短短一个多月升级用户已经超过6000万,成为史上升级速...
继续阅读 »

前言


老铁们,就在前天,9月25日,在华为秋季全场景新品发布会上,华为常务董事、终端BG CEO、智能汽车解决方案BU董事长余承东介绍了鸿蒙系统的最新进展:HarmonyOS 4发布后,短短一个多月升级用户已经超过6000万,成为史上升级速度最快的HarmonyOS版本;余承东宣布,鸿蒙原生应用全面启动,HarmonyOS NEXT开发者预览版将在2024年第一季度面向开发者开放。



我们知道,在8月4日的华为开发者大会,华为才刚刚推出了面向开发者的 HarmonyOS NEXT 开发者预览版,如果说当时只是一个概念,那么这次,绝对是正式官宣,打响移动端第三系统的第一枪!我们有理由且必须相信,HarmonyOS NEXT开发者预览版正在急速到来,不仅仅是对手机系统的冲击、移动端的开发者也有着不小的冲击。


如果说当时刚推出,你踌躇徘徊,犹豫不定,对待HarmonyOS犹如对待外来物一样,极度的排斥,那么这次你绝对忽视不得,否则,你将错过一个时代的步伐。


短短一个多月升级用户已经超过6千万,足以打脸那些看弱HarmonyOS的人,也从另一方面说明,HarmonyOS已经得到越来越多人的喜爱,或许是有了不断攀升的用户量,才让华为手机有了信心去发展原生系统应用,并且此前有爆料数据显示鸿蒙OS5.0会取消支持安卓软件,这种爆料绝非空穴来风,可能很多人包括我在内,会觉得取消支持Android软件,是一件非常冒险的行为,但是随着华为手机的体量越来越大,生态越来越好,这个事情是必须且迟早的要做的。


鸿蒙是否有必要学习


可能鸿蒙从一诞生,就背着一个”套壳“的骂名,毕竟一直都兼容AOSP(Android 开放源代码项目),很难不令人怀疑,当然了,曾经我也有所怀疑,以至于,对于HarmonyOS保持的态度,始终都是,冷漠,不感冒,毕竟Android开发的包,在HarmonyOS上也能用,我们何必再去研究它呢?费力又费时间,还不如刷刷短视频,对吧。


但是,一旦HarmonyOS剥离AOSP,Android开发的包无法在其运行,这种情况下,身为移动端的开发者,特别是Android端的开发者,你觉得有没有必要学习?


试想这样的一个场景,当其他的应用都能在HarmonyOS上运行,而你的应用确不支持,你是什么感觉?当然了,也得问一句,你们公司是什么感觉?虽然说目前HarmonyOS国内市场占有率为8%,占有率并不是很多,但止不住它发展迅速啊,未来,20%,50%都有可能,即便是8%,这样的一个市场,你和你的公司难道会果断的放弃?如果放弃的话,确实没必要学,但是,能放弃吗?


再试想一个场景,随着HarmonyOS不断的发展,移动端三分天下,而企业考虑到成本问题,在招聘的时候,要求了必须要会HarmonyOS开发,你如何破解这个问题?


无论是自身发展还是当下的企业布局,HarmonyOS都是你躲不过的一道屏障,无非就是什么时间入手的问题,当然了,如果一个企业或者个人,对HarmonyOS,没什么业务发展,也不在乎这些市场份额,那就没必要学习,反过来,真的要静下心来,好好研究研究了,否则影响的不仅仅是一个应用,更是大量的用户流失。


可能很多人都会觉得,HarmonyOS剥离AOSP,这么冒险的事,华为大概率不会那么武断,即便升级,可能也会采取双系统并行,也就是HarmonyOS4.0 和HarmonyOS Next,继续兼容Android一段时间,当然了,不排除这种做法,我想说的是,这也只是一个广大的猜测,在其他大厂APP都跟进的情况下,如果它升级了,怎么办?哪怕概率为1%,对企业和个人的影响绝对是100%,话又说回来,它采取了双系统并行或者有其他的兼容方案,你觉得华为会一直兼容吗,所以啊,如果你想继续从事这个行业,学只不过是早晚的问题


所以啊,HarmonyOS,肯定是要学的,除非你要告别当前从事的移动端开发,如果再做一层针对性的,那就是告别Android端开发,毕竟和iOS端的冲突目前还没那么大。


不仅要学,而且还要提前进行技术储备,目的防患于未然;毕竟来年的事,谁也说不定,有条件的公司,技术储备之后,就可以复刻鸿蒙版App了,尽量赶上升级后的第一批App,这样就可以做到无缝衔接,不至于鸿蒙系统流失用户,当然了,也可以只做技术储备,隔岸观火,进一步观察HarmonyOS的下一步动作,但是,技术储备一定要做,无论来年华为升级与否,因为复刻鸿蒙版App,不是一朝一夕能够完成的,起码目前来看,还没有一件转化的功能,只能从0到1的进行开发,小体量的App还好说,大体量的App,从0到1没个半年以上还真完成不了,所以啊,哪怕华为宣布来年不强制升级,到2025年升级,留给开发者的时间还多吗?


HarmonyOS的学习路径有很多,官网也给出了详细的视频以及文档教程,大家可以直接学习即可,当然了大家也可以关注我,哈哈,我也会定时分享HarmonyOS相关的技术,目前在有序的输出。


鸿蒙未来的发展


根据华为最新公布的数据:目前鸿蒙生态设备已达7亿台,早就跨过了“生死线”;鸿蒙品牌知名度从2021年的50%升级至今年6月的85%,越来越多的用户知晓和主动拥抱HarmonyOS;HarmonyOS 3用户升级率达到85%,超过了iOS(81%)成为最新版本设备升级率最高的操作系统,而HarmonyOS 4发布后,短短一个多月升级用户已经超过6000万,可以说是,恐怖如斯,遥遥领先!


目前华为已与合作伙伴和开发者在社交、影音、游戏、资讯、金融等18个领域全面展开合作,在HarmonyOS独特的全场景分布式体验、原生智能、纯净安全、大模型AI交互等方面,HarmonyOS NEXT构筑了差异化优势,全面领先于行业。


为了更好帮助合作伙伴成长,在HDC 2023期间,华为正式发布鸿蒙生态伙伴发展计划——“鸿飞计划”,宣布未来三年将投入百亿人民币,向伙伴提供全方位的资源扶持,包括技术支持、市场推广、商业合作等,让每一位伙伴都成为鸿蒙生态的主角。


无论是企业的绝对支持,还是政府的大力推进,HarmonyOS的发展,可以说势如破竹,三分天下,也就是时间的问题。


我们都知道,操作系统生态的发展,人才是重中之重。随着鸿蒙生态的发展,专业人才需求正在呈现井喷式增长,为此,在鸿蒙人才培养方面,华为也做了全面投入,今年以来已有超过170万人参加了鸿蒙学堂的课程学习、线下活动,华为还和全国300多所高校展开了合作,鸿蒙产学合作项目超过140个,已经颁发鸿蒙学堂证书超过7万,各类开发者活动累计参加人次超过350万。


可以告诉大家的是,俺也是其中一员,哈哈~,当然了,证书并没有含金量,只是一个阶段学习的测试而已。



除此之外,近期教育部-华为“智能基座”产教融合协同育人基地2.0启动,未来双方将与72所高校合作培养鸿蒙人才,一起促进鸿蒙生态的繁荣发展。


我们总担忧鸿蒙的生态,对它不屑一顾,说它“套壳”,说它抄袭,说它迟早会死,可是,人家不吭不响,不反驳,只会默默的耕耘,以至于发展的越来越好,越来越完善,为什么鸿蒙这么自信,我们却不自信呢?我们在担忧什么?


鸿蒙的生态离不开每一个的开发者,我们有理由相信,未来的时刻,它肯定会剑指Android和iOS,我们更有理由相信,国产系统的繁荣富强,一定会到来,民族的自信心也必定到来!


鸿蒙不仅仅是一个系统,它是更长远的国家战略


国家战略说的有点大了,但是肯定是在计划之内和大力支持的,为什么这么说,从18年的中美贸易战,到22年的俄乌冲突,卡脖子的事,发生的还少吗?动不动进行制裁,动不动限制出口,美国佬龌龊的事做的还少吗?如果说一直没有自研,那么话语权始终掌握在别人手里,不仅仅是一个系统,像芯片等等,我们始终很难强大。


俄乌冲突期间,谷歌公司停止认证运行安卓操作系统的俄罗斯BQ公司的智能手机,微软宣布禁止在俄罗斯使用Windows系统,也许对于我们个人而言,觉得没什么影响,但是站在国家层面,绝对是致命的打击,如果未来,收复TW时,也来这么一下,你觉得,国家能承受的住吗?


除了各种限制和制裁之外,俄乌冲突期间最恐怖的是,谷歌地图服务提供俄罗斯所有军事和战略设施的最高分辨率卫星图像,这不就等于明牌了,你在明处,人家在暗处,所以,无论是系统,还是芯片,还是其他的技术方向,站在国家层面上,能够自研,无论是摆脱外部限制,还是自身科技发展,绝对都是划时代的意义。


所以,老铁们,对于鸿蒙,于国于人,我们都应该有充足的自信,不仅仅关系着手机系统的三分天下,更是国家安全的未来措施,政策,一定是某项事物发展的导向,跟着国家走,准没错。


开发者如何提前布局


我觉得应该从三方面入手,第一,就是技术储备,学习HarmonyOS,能够达到独立的完成项目开发;第二,就是,技术架构,组件,基础库的梳理和开发,这么做的目的,是便于日后项目的快速开发;第三,就是着手自己项目HarmonyOS版的开发了,以应对未来HarmonyOS升级。


未来是否有一键转化HarmonyOS版App的功能,这个一切未知,有的话,就太方便了,没有的话,只能从0到1进行开发了,当然了,跨平台语言的支持,也是一个突破点,比如Flutter支持HarmonyOS,那么对于原来Flutter语言的App而言,就无比轻松了,而目前来说,这些都没有一个实质性的进展,所以还是一步一步的先学习HarmonyOS开发吧。


还好,HarmonyOS主推的是ArkTs语言,其中也定义了声明式UI,和Flutter,Compose,Swift等有着异曲同工之妙,如果你有着声明式开发的经验,那么掌握HarmonyOS简直是易如反掌。


当然了,为了更好的提高开发效率,HarmonyOS采用了反推的做法,推出了自己的跨平台框架ArkUI-X,成熟之后,我们可以作为开发框架,进而兼容Android和iOS。


ArkUI-X 是 ArkUI 的跨平台框架,采用 ArkUI 开发的应用能在 HarmonyOS 上原生运行,获得极佳的性能,通过 ArkUI-X 能够在 Android 和 IOS 上跨平台运行,获得强于 Flutter、React Native 等同类竞品的性能。



总结


该说的也都说了,不该说的也说了,至于HarmonyOS,您是学习还是放弃,只能由自己决断了,可以肯定得是,您的放弃,一定是未来的错误决定。


番外


写文章的时候,电脑上老是有一种刺啦刺啦声音,这个声音很小,听的不是很清楚,一开始我总以为是敲击键盘的声音,当我凑近一听,一种熟悉的声音扑面而来:遥遥领先,遥遥领先~


作者:程序员一鸣
链接:https://juejin.cn/post/7283322449038229541
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

UIButton 扩大点击区域

iOS
在开发过程中经常会遇到设计给出的button尺寸偏小的情况.这种UIButton在使用中会非常难点击,极大降低了用户体验 解决方案一:重写UIButton的- (BOOL)pointInside:(CGPoint)point withEvent:(UIEven...
继续阅读 »

在开发过程中经常会遇到设计给出的button尺寸偏小的情况.这种UIButton在使用中会非常难点击,极大降低了用户体验


解决方案一:重写UIButton的- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event方法

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent*)event

{

//获取当前button的实际大小
CGRect bounds = self.bounds;

//若原热区小于44x44,则放大热区,否则保持原大小不变

CGFloat widthDelta = MAX(44.0 - bounds.size.width, 0);

CGFloat heightDelta = MAX(44.0 - bounds.size.height, 0);
//扩大bounds

bounds = CGRectInset(bounds, -0.5 * widthDelta, -0.5 * heightDelta);

//如果点击的点 在 新的bounds里,就返回YES

return CGRectContainsPoint(bounds, point);

}

系统默认写法是:

- (BOOL)pointInside:(CGPoint)point withEvent:(UIEvent *)event
{
return CGRectContainsPoint(self.bounds, point);
}

其实是在判断的时候对响应区域的bounds进行了修改.CGRectInset(view, 10, 20)方法表示对rect大小进行修改


解决方案二 runtime关联对象来改变范围,- (UIView) hitTest:(CGPoint) point withEvent:(UIEvent) event里用新设定的 Rect 来当着点击范围。

#import "UIButton+EnlargeTouchArea.h"
#import <objc/runtime.h>

@implementation UIButton (EnlargeTouchArea)

static char topNameKey;
static char rightNameKey;
static char bottomNameKey;
static char leftNameKey;

- (void)setEnlargeEdgeWithTop:(CGFloat)top right:(CGFloat)right bottom:(CGFloat)bottom left:(CGFloat)left
{
objc_setAssociatedObject(self, &topNameKey, [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, &rightNameKey, [NSNumber numberWithFloat:right], OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, &bottomNameKey, [NSNumber numberWithFloat:bottom], OBJC_ASSOCIATION_COPY_NONATOMIC);
objc_setAssociatedObject(self, &leftNameKey, [NSNumber numberWithFloat:left], OBJC_ASSOCIATION_COPY_NONATOMIC);
}

- (void)setTouchAreaToSize:(CGSize)size
{
CGFloat top = 0, right = 0, bottom = 0, left = 0;

if (size.width > self.frame.size.width) {
left = right = (size.width - self.frame.size.width) / 2;
}

if (size.height > self.frame.size.height) {
top = bottom = (size.height - self.frame.size.height) / 2;
}

[self setEnlargeEdgeWithTop:top right:right bottom:bottom left:left];
}

- (CGRect)enlargedRect
{
NSNumber *topEdge = objc_getAssociatedObject(self, &topNameKey);
NSNumber *rightEdge = objc_getAssociatedObject(self, &rightNameKey);
NSNumber *bottomEdge = objc_getAssociatedObject(self, &bottomNameKey);
NSNumber *leftEdge = objc_getAssociatedObject(self, &leftNameKey);
if (topEdge && rightEdge && bottomEdge && leftEdge)
{
return CGRectMake(self.bounds.origin.x - leftEdge.floatValue,
self.bounds.origin.y - topEdge.floatValue,
self.bounds.size.width + leftEdge.floatValue + rightEdge.floatValue,
self.bounds.size.height + topEdge.floatValue + bottomEdge.floatValue);
}
else
{
return self.bounds;
}
}

- (UIView *)hitTest:(CGPoint)point withEvent:(UIEvent *)event
{
CGRect rect = [self enlargedRect];
if (CGRectEqualToRect(rect, self.bounds) || self.hidden)
{
return [super hitTest:point withEvent:event];
}
return CGRectContainsPoint(rect, point) ? self : nil;
}

@end


解决方案三:使用runtime swizzle交换IMP

+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSError *error = nil;
[self hg_swizzleMethod:@selector(pointInside:withEvent:) withMethod:@selector(hitTest_pointInside:withEvent:) error:&error];
NSAssert(!error, @"UIView+HitTest.h swizzling failed: error = %@", error);
});
}

- (BOOL)hitTest_pointInside:(CGPoint)point withEvent:(UIEvent *)event {
if (UIEdgeInsetsEqualToEdgeInsets(self.hitTestEdgeInsets, UIEdgeInsetsZero)) {
return [self hitTest_pointInside:point withEvent:event];
}
CGRect relativeFrame = self.bounds;
CGRect hitFrame = UIEdgeInsetsInsetRect(relativeFrame, self.hitTestEdgeInsets);
return CGRectContainsPoint(hitFrame, point);
}



category的诞生只是为了让开发者更加方便的去拓展一个类,它的初衷并不是让你去改变一个类。



技术点总结


关联对象,也就是绑定对象,可以绑定任何东西

//关联对象
objc_setAssociatedObject(self, &topNameKey, [NSNumber numberWithFloat:top], OBJC_ASSOCIATION_COPY_NONATOMIC);
// self 关联的类,
//key:要保证全局唯一,key与关联的对象是一一对应关系。必须全局唯一
//value:要关联类的对象。
//policy:关联策略。有五种关联策略。
//OBJC_ASSOCIATION_ASSIGN 等价于 @property(assign)。
//OBJC_ASSOCIATION_RETAIN_NONATOMIC等价于 @property(strong, //nonatomic)。
//OBJC_ASSOCIATION_COPY_NONATOMIC等价于@property(copy, nonatomic)。
//OBJC_ASSOCIATION_RETAIN等价于@property(strong,atomic)。
//OBJC_ASSOCIATION_COPY等价于@property(copy, atomic)。

NSNumber *topEdge = objc_getAssociatedObject(self, &topNameKey);

// 方法说明
objc_setAssociatedObject 相当于 setValue:forKey 进行关联value对象

objc_getAssociatedObject 用来读取对象

objc_AssociationPolicy 属性 是设定该value在object内的属性,即 assgin, (retain,nonatomic)...等

objc_removeAssociatedObjects 函数来移除一个关联对象,或者使用objc_setAssociatedObject函数将key指定的关联对象设置为nil。

方法交换 Method Swizzling 注意点


对于已经存在的类,我们通常会在+load方法,或者无法获取到类文件,我们创建一个分类,也通过其+load方法进行加载swizzling


  • Swizzling应该总在+load中执行
  • Swizzling应该总是在dispatch_once中执行
  • Swizzling在+load中执行时,不要调用[super load]。如果多次调用了[super load],可能会出现“Swizzle无效”的假象。

交换实例方法


以class为类

void class_swizzleInstanceMethod(Class class, SEL originalSEL, SEL replacementSEL)
{
//class_getInstanceMethod(),如果子类没有实现相应的方法,则会返回父类的方法。
Method originMethod = class_getInstanceMethod(class, originalSEL);
Method replaceMethod = class_getInstanceMethod(class, replacementSEL);

//class_addMethod() 判断originalSEL是否在子类中实现,如果只是继承了父类的方法,没有重写,那么直接调用method_exchangeImplementations,则会交换父类中的方法和当前的实现方法。此时如果用父类调用originalSEL,因为方法已经与子类中调换,所以父类中找不到相应的实现,会抛出异常unrecognized selector.
//当class_addMethod() 返回YES时,说明子类未实现此方法(根据SEL判断),此时class_addMethod会添加(名字为originalSEL,实现为replaceMethod)的方法。此时在将replacementSEL的实现替换为originMethod的实现即可。
//当class_addMethod() 返回NO时,说明子类中有该实现方法,此时直接调用method_exchangeImplementations交换两个方法的实现即可。
//注:如果在子类中实现此方法了,即使只是单纯的调用super,一样算重写了父类的方法,所以class_addMethod() 会返回NO。

//可用BaseClass实验
if(class_addMethod(class, originalSEL, method_getImplementation(replaceMethod),method_getTypeEncoding(replaceMethod)))
{
class_replaceMethod(class,replacementSEL, method_getImplementation(originMethod), method_getTypeEncoding(originMethod));
}else {
method_exchangeImplementations(originMethod, replaceMethod);
}
}


这里存在的问题是继承时子类没有实现父类方法的问题:
基类A类 有方法 -(void)test
子类B类继承自基类A,但没有重写test方法,即其类[B class]中没有test这个实例方法
当我们交换子类B中的方法test,交换为testRelease方法(这必然会在子类B中写testRelease的实现),子类B中有没有test方法的实现时,就会将基类A的test方法与testRelease替换,当仅仅使用子类B时,不会有问题。
但当我们使用基类A的test方法时,由于test指向的IMP是原testRelease的IMP,而基类A中没有这个实现,因为我们是写在子类B中的。所以就出现了unrecognized selector



交换类方法


由于类方法存储在元类中,以实例方法存在,所以实质就是交换元类的实例方法
上面交换实例方法基础上,传入cls为元类即可。
获取的元类可以这样objc_getMetaClass("ClassName")或者object_getclass([NSObject class])


事件响应者链


如图所示,不再赘述



 两个重要的方法

- (nullable UIView*)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event;称为方法A

- (BOOL)pointInside:(CGPoint)point withEvent:(nullable UIEvent *)event;称为方法B

对view进行重写这两个方法后,点击屏幕后,首先响应的是方法A;

  • 如果方法A中,我们没有调用父类([super hitTest:point withEvent:event];)的这个方法,那就根据这个方法A的返回view,作为响应事件的view。(当然返回nil,就是这个view不响应)

  • 如果方法A中,我们调用了父类的方法([super hitTest:point withEvent:event];)那这个时候系统就要调用方法B;通过这个方法的返回值,来判断当前这个view能不能响应消息

  • 如果方法B返回的是no,那就不用再去遍历它的子视图。方法A返回的view就是可以响应事件的view。

  • 如果方法B返回的是YES,那就去遍历它的子视图。(就是上图我们描述的那样,找到合适的view返回,如果找不到,那就由方法A返回的view去响应这个事件。)


总结


返回一个view来响应事件 (如果不想影响系统的事件传递链,在这个方法内,最好调用父类的这个方法)

- (nullable UIView*)hitTest:(CGPoint)point withEvent:(nullable UIEvent *)event{
    return [super hitTest:point withEvent:event];
}

返回的值可以用来判断是否继续遍历子视图(返回的根据是触摸的point是否在view的frame范围内)

- (BOOL)pointInside:(CGPoint)point withEvent:(nullableUIEvent *)event;      

作者:Perry_6
链接:https://juejin.cn/post/6943628003976937508
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

SF Symbols 4 使用指南

iOS
本文基于 WWDC 2022 Session 10157 和 Session 10158 梳理,为了更方便没有 SF Symbols 经验的读者理解,也将往年的 SF Symbols 相关内容一并整理。本文从 SF Symbols 4 的新特性切入,讨论 SF...
继续阅读 »

本文基于 WWDC 2022 Session 10157Session 10158 梳理,为了更方便没有 SF Symbols 经验的读者理解,也将往年的 SF Symbols 相关内容一并整理。本文从 SF Symbols 4 的新特性切入,讨论 SF Symbols 这款由系统字体支持的符号库有哪些优点以及该如何使用。在这次 WWDC 2022 中,除了符号的数量的增加到了 4000+ 之外,还有自动渲染模式、可变符号等新特性推出,让 SF Symbols 这把利器变得又更加趁手和锋利了。




本文是 WWDC22 内参 的供稿。



什么是 SF Symbols


符号在界面中起着非常重要的作用,它们能有效地传达意义,它们可以表明你选择了哪些项目,它们可以用来从视觉上区分不同类型的内容,他们还可以节约空间、整洁界面,而且符号出现在整个视觉系统的各处,这使整个用户界面营造了一种熟悉的感觉。


符号的实现和使用方式多种多样,但设计和使用符号时有一个亘古不变的问题,那就是将符号与用户界面的另一个基本元素——「文本」很好地配合。符号和文字在用户界面中以各种不同的大小被使用,他们之间的排列形式、对齐方式、符号颜色、文本字重与符号粗细的协调、本地化配置以及无障碍设计都需要开发者和设计师来细心配置和协调。




为了方便开发者更便捷、轻松地使用符号,Apple 在 iOS 13 中开始引入他们自己设计的海量高质量符号,称之为 SF Symbols。SF Symbols 拥有超过 4000 个符号,是一个图标库,旨在与 Apple 平台的系统字体 San Francisco 无缝集成。每个符号有 9 种字重和 3 种比例,以及四种渲染模式,它们的默认设计都与文本标签对齐,同时这些符号是矢量的,这意味着它们是可以被拉伸的,使得他们在无论用什么大小时都会呈现出很好的效果。如果你想去创造具有相似设计特征或无障碍功能的自定义符号,它们也可以被导出并在矢量图形编辑工具中进行编辑以创建新的符号。


对于开发者来说,这套 SF Symbols 无论是在 UIKit,AppKit 还是 SwiftUI 中都能运作良好,且使用方式也很简单方便,寥寥数行代码就可以实现。对于设计师来说,你只需要为符号只做三个字重的版本,SF Symbols 会自动地帮你生成其余 9 种字重和 3 种比例的符号,然后在 SF Symbols 4 App 中调整四种渲染模式的表现,就制作好了一份可以高度定制化的 symbol。




如何使用 SF Symbols


SF Symbols 4 App


在开始介绍如何使用 SF Symbols 之前,我们可以先下载来自 Apple 官方的 SF Symbols 4 App,这款 App 中收录了所有的 SF Symbols,并且记录了每个符号的名称,支持的渲染模式,可变符号的分层预览,不同语言下的变体,不同版本下可能出现的不同的名称,并且可以实时预览不同渲染模式下不同强调色的不同效果。你可以在这里下载 SF Symbols 4 App。




符号的渲染模式


通过之前的图片你可能已经注意到了,SF Symbols 可以拥有多种颜色,有一些 symbol 还有预设的配色,例如代表天气、肺部、电池的符号等等。如果要使用这些带有自定义颜色的符号,你需要知道,SF Symbols 在逻辑上是预先分层的(如下图的温度计符号就分为三层),根据每一层的路径,我们可以根据渲染模式来调整颜色,而每个 SF Symbols 有四种渲染模式。




单色模式 Monochrome


在 iOS 15 / macOS 11 之前,单色模式是唯一的渲染模式,顾名思义,单色模式会让符号有一个单一的颜色。要设置单色模式的符号,我们只需要设置视图的 tint color 等属性就可以完成。

let image = UIImage(systemName: "thermometer.sun.fill")
imageView.image = image
imageView.tintColor = .systemBlue

// SwiftUI
Image(systemName: "thermometer.sun.fill")
.foregroundStyle(.blue)

分层模式 Hierarchical


每个符号都是预先分层的,如下图所示,符号按顺序最多分成三个层级:Primary,Secondary,Tertiary。SF Symbols 的分层设定不仅在分层模式下有效,在后文别的渲染模式下也是有作用的




分层模式和单色模式一样,可以设置一个颜色。但是分层模式会以该颜色为基础,生成降低主颜色的不透明度而衍生出来的其他颜色(如上上图中的温度计符号看起来是由三种灰色组合而成)。在这个模式中,层级结构很重要,如果缺少一个层级,相关的派生颜色将不会被使用。

let image = UIImage(systemName: "thermometer.sun.fill")
let config = UIImage.SymbolConfiguration(hierarchicalColor: .lightGray)
imageView.image = image
imageView.preferredSymbolConfiguration = config

// SwiftUI
Image(systemName: "thermometer.sun.fill")
.foregroundStyle(.gray)
.symbolRenderingMode(.hierarchical)

调色盘模式 Palette


调色盘模式和分层模式很像,但也有些许不同。和分层模式一样是,调色盘模式也会对符号的各个层级进行上色,而不同的是,调色盘模式允许你自由的分别设置各个层级的颜色。

let image = UIImage(systemName: "thermometer.sun.fill")
let config = UIImage.SymbolConfiguration(paletteColors: [.lightGray, .cyan, .systemTeal])
imageView.image = image
imageView.preferredSymbolConfiguration = config

// SwiftUI
Image(systemName: "thermometer.sun.fill")
.foregroundStyle(.lightGray, .cyan, .teal)

多色模式 Muticolor


在 SF Symbols 中,有许多符号的意象在现实生活中已经深入人心,比如:太阳应该是橙色的,警告应该是黄色的,叶子应该是绿色的的等等。所以 SF Symbols 也提供了与现实世界色彩相契合的颜色模式:多色渲染模式。当你使用多色模式的时候,就能看到预设的橙色太阳符号,红色的闹铃符号,而你不需要指定任何颜色。

let image = UIImage(systemName: "thermometer.sun.fill")
imageView.image = image
imageView.preferredSymbolConfiguration = .preferringMulticolor()

// SwiftUI
Image(systemName: "thermometer.sun.fill")
.symbolRenderingMode(.multicolor)

自动渲染模式 Automatic


谈论完了四种渲染模式,可以发现每次设置 symbol 的渲染模式其实也是一件费心的事情。为了解决这个问题,在最新的 SF Symbols 中,每个 symbol 都有了一个自动渲染模式。例如下图的 shareplay 符号,你可以看到在右侧面板中,shareplay 符号的第二个模式(分层模式)的下方有一个空心小圆点,这意味着该符号在代码中使用时,假如你不去特意配置他的渲染模式,那么他将使用分层模式作为他的默认渲染模式。



你可以在 SF Symbols 4 App 中查询到所有符号的自动渲染模式。





可变颜色


在有的时候,符号并不单单代表一个单独的概念或者意象,他也可以代表一些数值、比例或者程度,例如 Wi-Fi 强度或者铃声音量,为了解决这个问题,SF Symbols 引入了可变颜色这个概念。


你可以在 SF Symbol 4 App 中的 Variable 目录中找到所有有可变颜色的符号,平且可以通过右侧面板的滑块来查看不同百分比程度下可变颜色的形态。另外你也可以注意到,可变颜色的可变部分实际上也是一种分层的表现,但这里的分层和上文提到的渲染模式使用的分层是不同的。一个符号可以在渲染模式中只分两层,在可变颜色的分层中分为三层,下图中第二个符号喇叭 speaker.wave.3.fill 就是如此。关于这里的分层我们会在后文如何制作可变颜色中详细讨论。




在代码中,我们只需要在初始化 symbol 时增加一个 Double 类型的 variableValue 参数,就可以实现可变颜色在不同程度下的不同形态。值得注意的是,假如你的可变颜色(例如上图 Wi-Fi 符号)可变部分有三层,那么这个 variableValue 的判定将会三等分:在 0% 时将不高亮信号,在 0%~33% 时,将高亮一格信号,在 34%~67 % 时,将高亮 2 格信号,在 68% 以上时,将会显示满格信号。

let img = NSImage(symbolName: "wifi", variableValue: 0.2)

可变颜色的可变部分是利用不透明度来实现的,当可变颜色和不同的渲染模式结合后,也会有很好的效果。




如何制作和调整可变颜色


在 SF Symbols 4 App 中,我们可以自定义或者调整可变颜色的表现,接下来我将带着大家以 party.popper 这个符号为基础制作一个带可变颜色的符号。

  1. 首先我们打开 SF Symbols 4 App,在右上角搜索 party.popper,找到该符号后右键选择 复制为1个自定符号。推荐你在上方将符号的排列方式修改为画廊模式,如下图所示。


  2. 可以注意到右下角的  这个板块,这个符号默认是由两个层级组成的,分别是礼花和礼花筒,同时我们也可以看到,礼花和礼花筒又分别是由更零碎的路径组成的,通过勾选子路径我们可以给每个层新增或者减少路径。那我现在想要给这个符号新增一层,我只需要在画廊模式下,将符号的某一部分拖拽到层里就可以。


  3. 通过这样的操作,我们可以将这个符号整理为四层:礼花筒、线条礼花、小球礼花和大球礼花。为了可变颜色的效果,我们需要按照从下到上:礼花筒、线条礼花、大球礼花和小球礼花的顺序去放置层级,另外,我们可以切换到分层模式、调色板模式和多色模式里面去调整成自己喜欢的颜色来预览效果,我这里调整了多色模式中的配色,具体效果如下。


  4. 接下来,我们将前三层,也就是除了礼花筒外的三层,最右侧的可变符号按钮选中,来表示这三层将可以在可变符号的变化范围内活动。接下来,只要点击颜色区域内的可变符号按钮,我们就可以拖动滑块来查看可变颜色的形态。


  5. 至此,我们就完成了一个带可变颜色的自定义符号,我们可以在合适的地方使用这个符号。例如我的 App 有一个 4 个步骤的新手引导,这时候就可以给每一个步骤配备一个符号来让界面变得更加的活泼。


统一注释 Unified annotations


其实我们已经接触到了 Unified annotations 这个过程,它就是将符号的层级,路径以及子路径整理成在四个渲染模式下都能良好工作的过程,就如同上文彩色礼花筒的例子,我们通过统一注释,让彩色礼花筒符号在不同渲染模式、不同环境色、不同主题色下,都能良好的运作。


那一般来说,对于单色模式,不需要过多的调整,它就能保持良好的形态;对于分层模式和调色盘模式,我们需要在给每个层设定好哪个是 Primary 层、哪个是 Secondanry 层以及哪个是 Tertiary 层,这样系统就会按优先级给符号上合适的颜色;对于多色模式,我们可以根据喜好以及符号的意义,给它预设一个合理的颜色,另外还要注意的是,如果设计了可变颜色在符号中,那么要注意保持可变符号的效果在四个渲染模式上都表现正常。


除了这些之外,还有一些特别的地方需要注意,我们以 custom.heart.circle.fill 为例子。你可以注意到,这个垃爱心符号是有一个圆形的背景的,在这种情况下,假如我们按照原来的规则去绘制单色模式,会发现:背景的圆形和爱心的图案将会是同一个颜色,那我们就将看不见圆形背景下的图案了。




这时我们可以使用 Unified annotations 给我们提供的新功能,我们将上图在 板块的爱心,将它从 Draw 改成 Erase,这样,我们就相当于以爱心的形状镂空了这个白色的背景,从而使该图形展现了出来并且在单色模式下能够一直表现正常。同理,在分层模式和调色盘模式中,也有这个 Erase 的功能共大家调整使用。


字重和比例


SF Symbols 和 Apple 平台的系统字体 San Francisco 一样,拥有九种字重和三种比例可以选择,这意味着每个 SF Symbol 都有 27 种样式以供使用。

let config = UIImage.SymbolConfiguration(pointSize: 20, weight: .semibold, scale: .large)
imageView.preferredSymbolConfiguration = config

// SwiftUI
Label("Heart", systemImage: "heart")
.imageScale(.large)
.font(.system(size: 20, weight: .semibold))

符号的字重和文本的字重原理相同,都是通过加粗线条来增加字重。但 SF Symbols 的三种比例尺寸并不是单纯的对符号进行缩放。如果你仔细观察,会发现对于同一个字重,但是不同比例的符号来说,他们线条的粗细是一样的,但是对符号的整体进行了扩充和延展,以应对不一样的使用环境。


要实现这样的效果,意味着每个 symbol 的底层逻辑并不是一张张图片,而是由一组组的路径构成,这也是为什么在当你想要自定义一个属于自己的 symbol 的时候,官方要求你用封闭路径 + 填充效果去完成一个符号,而不是使用一条简单路径 + 路径描边(stroke)来完成一个符号。



更多关于如何制作一个 symbol 的内容,请移步 WWDC 21 内参:定制属于你的 Symbols





除了字重和比例之外,SF Symbols 还在很多方面进行了努力来方便开发者的工作,例如:符号的变体、不同语言下符号的本地化、符号的无障碍化等,关于这些内容,以及其它由于篇幅原因未在本文讨论的细节问题,请移步 WWDC 21 内参:SF Symbols 使用指南


总结


从上文介绍 SF Symbols 的特性和优点我们可以看到,它的出现是为了解决符号与文本之间的协调性问题,在保证了本地化、无障碍化的基础上,Apple 一直在实用性、易用度以及多样性上面给 SF Symbols 加码,目前已经有了 4000+ 的符号可以使用,相信在未来还会有更多。这些符号的样式和图案目前看来并不是那么的广泛,这些有限的符号样式并不能让设计师安心代替所有界面上的符号,但是有失必有得,在这样一个高度统一的平台上,SF Symbols 在规范化、统一化、表现能力、代码与设计上的简易程度,在今年都又进一步的提升了,达到了让人惊艳的程度,随着 SF Symbols 的继续发展,我相信对于部分开发者来说,即将成为一个最优的符号工具🥳。


更多资料


以下是这几年关于 SF Symbols 的资料:



以下是更早的 SF Symbols 资料:



作者:Mim0sa
链接:https://juejin.cn/post/7236593511009124412
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

用 Metal 画一个三角形(Swift 函数式风格)

iOS
由于今年工作中用得语言换成 Rust/OCaml/ReScript 啦,所以导致我现在写代码更倾向于写函数式风格的代码。 顺便试试 Swift 在函数式方面能达到啥好玩的程度。主要是我不会 Swift,仅仅为了好玩。 创建工程 随便创建个工程,小玩具就不打算跑...
继续阅读 »

由于今年工作中用得语言换成 Rust/OCaml/ReScript 啦,所以导致我现在写代码更倾向于写函数式风格的代码。

顺便试试 Swift 在函数式方面能达到啥好玩的程度。主要是我不会 Swift,仅仅为了好玩。


创建工程


随便创建个工程,小玩具就不打算跑在手机上了,因为我的设备是 ARM 芯片的,所以直接创建个 Mac 项目,记得勾上包含测试。


构建 MTKView 子类


现在来创建个 MTKView 的子类,其实我现在已经不接受这种所谓的面向对象,开发者用这种方式,就要写太多篇幅来描述一个上下文结构跟函数就能实现的动作。

import MetalKit

class MetalView: MTKView {
required init(coder: NSCoder) {
super.init(coder: coder)
device = MTLCreateSystemDefaultDevice()
render()
}
}

extension MetalView {
func render() {
// TODO: 具体实现
}
}

我们这里给 MetalView extension 了一个 render 函数,里面是后续要写得具体实现。


普通的方式画一个三角形


先用常见的方式来画一个三角形

class MetalView: MTKView {
required init(coder: NSCoder) {
super.init(coder: coder)
device = MTLCreateSystemDefaultDevice()
render()
}
}

extension MetalView {
func render() {
guard let device = device else { fatalError("Failed to find default device.") }
let vertexData: [Float] = [
-1.0, -1.0, 0.0, 1.0,
1.0, -1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0
]

let dataSize = vertexData.count * MemoryLayout<Float>.size
let vertexBuffer = device.makeBuffer(bytes: vertexData, length: dataSize, options: [])
let library = device.makeDefaultLibrary()
let renderPassDesc = MTLRenderPassDescriptor()
let renderPipelineDesc = MTLRenderPipelineDescriptor()
if let currentDrawable = currentDrawable, let library = library {
renderPassDesc.colorAttachments[0].texture = currentDrawable.texture
renderPassDesc.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0)
renderPassDesc.colorAttachments[0].loadAction = .clear
renderPipelineDesc.vertexFunction = library.makeFunction(name: "vertexFn")
renderPipelineDesc.fragmentFunction = library.makeFunction(name: "fragmentFn")
renderPipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm
let commandQueue = device.makeCommandQueue()
guard let commandQueue = commandQueue else { fatalError("Failed to make command queue.") }
let commandBuffer = commandQueue.makeCommandBuffer()
guard let commandBuffer = commandBuffer else { fatalError("Failed to make command buffer.") }
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc)
guard let encoder = encoder else { fatalError("Failed to make render command encoder.") }
if let renderPipelineState = try? device.makeRenderPipelineState(descriptor: renderPipelineDesc) {
encoder.setRenderPipelineState(renderPipelineState)
encoder.setVertexBuffer(vertexBuffer, offset: 0, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
encoder.endEncoding()
commandBuffer.present(currentDrawable)
commandBuffer.commit()
}
}
}
}

然后是我们需要注册的 Shader 两个函数

#include <metal_stdlib>

using namespace metal;

struct Vertex {
float4 position [[position]];
};

vertex Vertex vertexFn(constant Vertex *vertices [[buffer(0)]], uint vid [[vertex_id]]) {
return vertices[vid];
}

fragment float4 fragmentFn(Vertex vert [[stage_in]]) {
return float4(0.7, 1, 1, 1);
}

在运行之前需要把 StoryBoard 控制器上的 View 改成我们写得这个 MTKView 的子类。




自定义操作符


函数式当然不是指可以定义操作符,但是没有这些操作符,感觉没有魂灵,所以先定义个管道符


代码实现

precedencegroup SingleForwardPipe {
associativity: left
higherThan: BitwiseShiftPrecedence
}

infix operator |> : SingleForwardPipe

func |> <T, U>(_ value: T, _ fn: ((T) -> U)) -> U {
fn(value)
}

测试管道符


因为创建项目的时候,勾上了 include Tests,直接写点测试代码,执行测试。

final class using_metalTests: XCTestCase {
// ...

func testPipeOperator() throws {
let add = { (a: Int) in
return { (b: Int) in
return a + b
}
}
assert(10 |> add(11) == 21)
let doSth = { 10 }
assert(() |> doSth == 10)
}
}

目前随便写个测试通过嘞。


Functional Programming


现在需要把上面的逻辑分割成小函数,事实上,因为 Cocoa 的基础是建立在面向对象上的,我们还是没法完全摆脱面向对象,目前先小范围应用它。


生成 MTLBuffer


先理一下逻辑,代码开始是创建顶点数据,生成 buffer

fileprivate let makeBuffer = { (device: MTLDevice) in
let vertexData: [Float] = [
-1.0, -1.0, 0.0, 1.0,
1.0, -1.0, 0.0, 1.0,
0.0, 1.0, 0.0, 1.0
]

let dataSize = vertexData.count * MemoryLayout<Float>.size
return device.makeBuffer(bytes: vertexData, length: dataSize, options: [])
}

创建 MTLLibrary


接着是创建 MTLLibrary 来注册两个 shader 方法,还创建了一个 MTLRenderPipelineDescriptor 对象用于创建 MTLRenderPipelineState,但是创建的 MTLLibrary 对象是一个 Optional 的,所以其实得有两步,总之先提取它再说吧

fileprivate let makeLib = { (device: MTLDevice) in device.makeDefaultLibrary() }

抽象 map 函数


根据我们有限的函数式编程经验,像 Optional 这种对象大概率有一个 map 函数,所以我们自家实现一个,同时还要写成柯里化的(建议自动柯里化语法糖入常),因为这里有逃逸闭包,所以要加上 @escaping

func map<T, U>(_ transform: @escaping (T) throws -> U) rethrows -> (T?) -> U? {
return { (o: T?) in
return try? o.map(transform)
}
}

处理 MTLRenderPipelineState


这里最终目的就是 new 了一个 MTLRenderPipelineState,顺带处理把程序的一些上下文给渲染管线描述器(MTLRenderPipelineDescriptor),譬如我们用到的着色器(Shader)函数,像素格式。
最后一行直接 try! 不处理错误啦,反正出问题直接会抛出来的

fileprivate let makeState = { (device: MTLDevice) in
return { (lib: MTLLibrary) in
let renderPipelineDesc = MTLRenderPipelineDescriptor()
renderPipelineDesc.vertexFunction = lib.makeFunction(name: "vertexFn")
renderPipelineDesc.fragmentFunction = lib.makeFunction(name: "fragmentFn")
renderPipelineDesc.colorAttachments[0].pixelFormat = .bgra8Unorm
return (try! device.makeRenderPipelineState(descriptor: renderPipelineDesc))
}
}

暂时收尾


已经不想再抽取函数啦,其实还能更细粒度地处理,因为函数式有个纯函数跟副作用的概念,像 Haskell 里是可以用 Monad 来处理副作用的情况,这个主题留给后续吧。先把 render 改造一下

fileprivate let render = { (device: MTLDevice, currentDrawable: CAMetalDrawable?) in
return { state in
let renderPassDesc = MTLRenderPassDescriptor()
if let currentDrawable = currentDrawable {
renderPassDesc.colorAttachments[0].texture = currentDrawable.texture
renderPassDesc.colorAttachments[0].clearColor = MTLClearColor(red: 0.0, green: 0.5, blue: 0.5, alpha: 1.0)
renderPassDesc.colorAttachments[0].loadAction = .clear
let commandQueue = device.makeCommandQueue()
guard let commandQueue = commandQueue else { fatalError("Failed to make command queue.") }
let commandBuffer = commandQueue.makeCommandBuffer()
guard let commandBuffer = commandBuffer else { fatalError("Failed to make command buffer.") }
let encoder = commandBuffer.makeRenderCommandEncoder(descriptor: renderPassDesc)
guard let encoder = encoder else { fatalError("Failed to make render command encoder.") }
encoder.setRenderPipelineState(state)
encoder.setVertexBuffer(device |> makeBuffer, offset: 0, index: 0)
encoder.drawPrimitives(type: .triangle, vertexStart: 0, vertexCount: 3, instanceCount: 1)
encoder.endEncoding()
commandBuffer.present(currentDrawable)
commandBuffer.commit()
}
}
}

然后再调用,于是就变成下面这副鸟样子

class MetalView: MTKView {
required init(coder: NSCoder) {
super.init(coder: coder)
device = MTLCreateSystemDefaultDevice()
device |> map {
makeLib($0)
|> map(makeState($0))
|> map(render($0, self.currentDrawable))
}
}
}

最后执行出这种效果




作者:壕壕
链接:https://juejin.cn/post/7215891370890952741
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

展开&收起,使用SwiftUI搭建一个侧滑展开页面交互

iOS
项目背景 闲来无事,在使用某云音乐听歌的时候发现一个侧滑展开的内页,交互效果还不错。 那么这一章节中,我们将使用SwiftUI搭建一个侧边展开页面交互。 项目搭建 首先,创建一个新的SwiftUI项目,命名为SlideOutMenu。 逻辑分析 首先我们来分...
继续阅读 »

项目背景


闲来无事,在使用某云音乐听歌的时候发现一个侧滑展开的内页,交互效果还不错。


那么这一章节中,我们将使用SwiftUI搭建一个侧边展开页面交互。


项目搭建


首先,创建一个新的SwiftUI项目,命名为SlideOutMenu




逻辑分析


首先我们来分析下基本的逻辑,一般的侧滑展开方式的交互是,在首页右上角有一个“更多”的按钮,点击按钮时,内页菜单从左往右划出,滑出至离右边20~30的位置停止。


然后首页背景将蒙上一个蒙层,点击蒙层时,侧滑展开的页面从右往左收起


简单分析完逻辑后,我们来实现这个交互。


首页入口


首先,我们需要在首页搭建一个入口,示例:

// 顶部导航入口
private var moreBtnView: some View {
    Button(action: {
    }) {
        Image(systemName: "list.bullet")
            .foregroundColor(.black)
    }
}

然后,我们可以使用NavigationViewnavigationBarItems创建顶部导航按钮样式,示例:

var body: some View {
    NavigationView {
        Text("点击左上角侧滑展开")
            .padding()
            .navigationBarTitle("首页", displayMode: .inline)
            .navigationBarItems(leading: moreBtnView)
    }
}



如此,首页入口部分我们就完成了。


左边菜单


接下来,我们来构建左侧菜单的内容。我们可以沿用之前设计过的“设置”页面的结构,我们先来构建栏目结构。示例:

// MARK: 栏目结构
struct listItemView: View {
    var itemImage: String
    var itemName: String
    var body: some View {
        Button(action: {
        }) {
            HStack {
                Image(systemName: itemImage)
                    .font(.system(size: 17))
                    .foregroundColor(.black)
                Text(itemName)
                    .foregroundColor(.black)
                    .font(.system(size: 17))
                Spacer()
                Image(systemName: "chevron.forward")
                    .font(.system(size: 14))
                    .foregroundColor(.gray)
            }.padding(.vertical, 10)
        }
    }
}

在我们构建侧滑展开的页面前,我们需要声明两个变量,一个是侧滑展开的页面的宽度,一个是当前这个页面的位置。示例:

@State var menuWidth = UIScreen.main.bounds.width - 60
@State var offsetX = -UIScreen.main.bounds.width + 60

我们设置的侧滑展开页面的宽度是屏幕宽度-60,而当前侧滑展开页面的位置是负位置,这样就可以在展示的时候先把页面隐藏起来


而当我们点击顶部导航中的“更多”按钮时,将offsetX偏移量X轴坐标设置为0。示例:

// 顶部导航入口
private var moreBtnView: some View {
    Button(action: {
        withAnimation {
            offsetX = 0
        }
    }) {
        Image(systemName: "list.bullet")
            .foregroundColor(.black)
    }
}

然后,我们创建一个新视图来构建侧滑展开的页面内容,示例:

// MARK: 左侧菜单
struct SlideOutMenu: View {
    @Binding var menuWidth: CGFloat
    @Binding var offsetX: CGFloat

    var body: some View {
        Form {
            Section {
            }
            Section {
                listItemView(itemImage: "lock", itemName: "账号绑定")
                listItemView(itemImage: "gear.circle", itemName: "通用设置")
                listItemView(itemImage: "briefcase", itemName: "简历管理")
            }
            Section {
                listItemView(itemImage: "icloud.and.arrow.down", itemName: "版本更新")
                listItemView(itemImage: "leaf", itemName: "清理缓存")
                listItemView(itemImage: "person", itemName: "关于掘金")
            }
        }
        .padding(.trailing, UIScreen.main.bounds.width - menuWidth)
        .edgesIgnoringSafeArea(.all)
        .shadow(color: Color.black.opacity(offsetX != 0 ? 0.1 : 0), radius: 5, x: 5, y: 0)
        .offset(x: offsetX)
        .background(
            Color.black.opacity(offsetX == 0 ? 0.5 : 0)
                .ignoresSafeArea(.all, edges: .vertical)
                .onTapGesture {
                    withAnimation {
                        offsetX = -menuWidth
                    }
                })
    }
}

上述代码中,我们也对页面宽度menuWidth、偏移位置offsetX进行了声明,方便之后我们在ContentView视图中进行双向绑定


我么使用Form表单和Section段落构建样式,这点就不说了。


值得说的一点是,我们设置了在页面展开的时候,也就是offsetX页面偏移量X轴坐标不为0,我们加了一个阴影,完善了侧滑展开页面的悬浮效果


然后使用offset调整页面初始位置。背景部分,除了根据offsetX页面偏移量X轴坐标加了一个蒙层,而且当我们点击的背景的时候,我们将偏移位置offsetX重新赋值,这样就能实现收起的交互效果。


我们在ContentView视图中展示侧滑展开视图,示例:

var body: some View {
    ZStack {
        NavigationView {
            Text("点击左上角侧滑展开")
                .padding()
                .navigationBarTitle("首页", displayMode: .inline)
                .navigationBarItems(leading: moreBtnView)
        }
        SlideOutMenu(menuWidth: $menuWidth, offsetX: $offsetX)
    }
}

项目展示




恭喜你,完成了本章的全部内容!


快来动手试试吧。


作者:文如秋雨
链接:https://juejin.cn/post/7132848697666175006
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

利用 UICollectionView 实现图片浏览效果

iOS
废话开篇:利用 UICollectionView 简单实现一个图片浏览效果。 一、效果展示 二、实现思路 1、封装 UICollectionViewLayout ,实现内部 UICollectionViewCell 的布局。 UICollectionView...
继续阅读 »

废话开篇:利用 UICollectionView 简单实现一个图片浏览效果。


一、效果展示




二、实现思路


1、封装 UICollectionViewLayout ,实现内部 UICollectionViewCell 的布局。

UICollectionViewLayout 在封装瀑布流的时候会用到,而且担负着核心功能的实现。其实从另一个角度也可以把 UICollectionViewLayout 理解成“数据源”,这个数据不是 UI 的展示项,而是 UI 的尺寸项。在内部进行预计算 UICollectionViewCellframe


UICollectionViewUIScrollView的子类,只不过,它里面子控件通过“重用”机制实现了优化,一些复用的复杂逻辑还是扔给了系统处理。开发过程中只负责对 UICollectionViewLayout 什么时候需要干什么进行自定义即可。


2、获取 UICollectionView 目前可见的 cells,通过进行缩放、旋转变换实现一些简单的效果。

3、自定义 cell ,修改锚点属性。

三、代码整理


1、PhotoBrowseViewLayout

这里有一点需要注意的,在 UICollectionViewLayout 内部会进行计算每一个 cellframe,在计算过程中,为了更好的展示旋转变换,cell 的锚点会修改到 (0.5,1),那么,为了保证 UI 展示不变,那么,就需要将 y 增加 cell 高度的一半

#import "PhotoBrowseViewLayout.h"

@interface PhotoBrowseViewLayout()

@property(nonatomic,strong) NSMutableArray * attributeArray;

@property(nonatomic,assign) CGFloat cellWidth;

@property(nonatomic,assign) CGFloat cellHeight;

@property(nonatomic,assign) CGFloat sep;

@property(nonatomic,assign) int showCellNum;


@end

@implementation PhotoBrowseViewLayout

- (instancetype)init
{
    if (self = [super init]) {
        self.sep = 20;
        self.showCellNum = 2;
    }
    return self;
}

//计算cell的frame
- (UICollectionViewLayoutAttributes *)layoutAttributesForItemAtIndexPath:(NSIndexPath *)indexPath
{
    if (self.cellWidth == 0) {
        self.cellWidth = **self**.collectionView.frame.size.width * 2 / 3.0;
    }
    if (self.cellHeight == 0) {
        self.cellHeight = self.collectionView.frame.size.height;
    }
    CGFloat x = (self.cellWidth + self.sep) * indexPath.item;
//这里y值需要进行如此设置,以抵抗cell修改锚点导致的UI错乱
    CGFloat y = self.collectionView.frame.size.height / 2.0;
    UICollectionViewLayoutAttributes *attrs = [UICollectionViewLayoutAttributes layoutAttributesForCellWithIndexPath:indexPath];

    attrs.frame = CGRectMake(x, y, self.cellWidth, self.cellHeight);
    return attrs;
}

//准备布局
- (void)prepareLayout
{
    [super prepareLayout];
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    for (int i = 0; i <count; i++) {
        UICollectionViewLayoutAttributes *attris = [self layoutAttributesForItemAtIndexPath:[NSIndexPath indexPathForRow:i inSection:0]];
        [self.attributeArray addObject:attris];
    }
}

//返回全部cell的布局集合
- (NSArray *)layoutAttributesForElementsInRect:(CGRect)rect
{
    return self.attributeArray;
}

//一次性提供UICollectionView 的 contentSize
- (CGSize)collectionViewContentSize
{
    NSInteger count = [self.collectionView numberOfItemsInSection:0];
    CGFloat maxWidth = count * self.cellWidth + (count - 1) * self.sep;
    return CGSizeMake(maxWidth, 0);
}

- (NSMutableArray *)attributeArray
{

    if (!_attributeArray) {
        _attributeArray = [[NSMutableArray alloc] init];
    }
    return _attributeArray;
}

@end

2、PhotoBrowseCollectionViewCell

这里主要是进行了锚点修改(0.5,1),代码很简单。

#import "PhotoBrowseCollectionViewCell.h"

@interface PhotoBrowseCollectionViewCell()

@property(nonatomic,strong) UIImageView * imageView;

@end

@implementation PhotoBrowseCollectionViewCell


- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
//设置(0.5,1)锚点,以底部中点为轴旋转
        self.layer.anchorPoint = CGPointMake(0.5, 1);
        self.layer.masksToBounds = YES;
        self.layer.cornerRadius = 8;
    }
    return self;
}

- (void)setImage:(UIImage *)image
{
    self.imageView.image = image;
}


- (UIImageView *)imageView
{

    if (!_imageView) {
        _imageView = [[UIImageView alloc] init];
        _imageView.contentMode = UIViewContentModeScaleAspectFill;
        _imageView.backgroundColor = [UIColor groupTableViewBackgroundColor];
        [self.contentView addSubview:_imageView];
    }
    return _imageView;
}

- (void)layoutSubviews
{
    [super layoutSubviews];
    self.imageView.frame = **self**.contentView.bounds;
}

@end

3、CollectPhotoBrowseView

CollectPhotoBrowseView 负责进行一些 cell 的图形变换。

#import "CollectPhotoBrowseView.h"
#import "PhotoBrowseCollectionViewCell.h"
#import "PhotoBrowseViewLayout.h"

@interface CollectPhotoBrowseView()<UICollectionViewDelegate,UICollectionViewDataSource>

@property(nonatomic,strong) UICollectionView * photoCollectView;

@end

@implementation CollectPhotoBrowseView

- (instancetype)initWithFrame:(CGRect)frame
{
    if (self = [super initWithFrame:frame]) {
        [self makeUI];
    }
    return self;
}

- (void)makeUI{
//设置自定义 UICollectionViewLayout
    PhotoBrowseViewLayout * photoBrowseViewLayout = [[PhotoBrowseViewLayout alloc] init];
    self.photoCollectView = [[UICollectionView alloc] initWithFrame:self.bounds collectionViewLayout:photoBrowseViewLayout];
    self.photoCollectView.delegate = self;
    self.photoCollectView.dataSource = self;
    [self.photoCollectView registerClass:[PhotoBrowseCollectionViewCell class] forCellWithReuseIdentifier:@"CELL"];
    self.photoCollectView.showsHorizontalScrollIndicator = NO;
    [self addSubview:self.photoCollectView];
//执行一次可见cell的图形变换
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
        [self visibleCellTransform];
    });
}

- (NSInteger)collectionView:(UICollectionView *)collectionView numberOfItemsInSection:(NSInteger)section
{
    return 20;
}

- (UICollectionViewCell *)collectionView:(UICollectionView *)collectionView cellForItemAtIndexPath:(NSIndexPath *)indexPath
{
    PhotoBrowseCollectionViewCell * cell = [collectionView dequeueReusableCellWithReuseIdentifier:@"CELL" forIndexPath:indexPath];
    [cell setImage: [UIImage imageNamed:[NSString stringWithFormat:@"fd%ld",indexPath.item % 3 + 1]]];
    return cell;
}

#pragma mark - 滚动进行图形变换
- (void)scrollViewDidScroll:(UIScrollView *)scrollView
{
//滑动的时候,动态进行cell图形变换
    [self visibleCellTransform];
}

#pragma mark - 图形变化
- (void)visibleCellTransform
{
//获取当前可见cell的indexPath集合
    NSArray * visibleItems =  [self.photoCollectView indexPathsForVisibleItems];
//遍历动态进行图形变换
    for (NSIndexPath * visibleIndexPath in visibleItems) {
        UICollectionViewCell * visibleCell = [self.photoCollectView cellForItemAtIndexPath:visibleIndexPath];
        [self transformRotateWithView:visibleCell];
    }
}

//进行图形转换
- (void)transformRotateWithView:(UICollectionViewCell *)cell
{
//获取cell在当前视图的位置
    CGRect rect = [cell convertRect:cell.bounds toView:self];
//计算当前cell中轴线与中轴线的距离的比值
    float present = ((CGRectGetMidX(rect) - self.center.x) / (self.frame.size.width / 2.0));
//根据位置设置选择角度
    CGFloat radian = (M_PI_2 / 15) * present;
//图形角度变换
    CGAffineTransform transformRotate = CGAffineTransformIdentity;
    transformRotate = CGAffineTransformRotate(transformRotate, radian);
//图形缩放变换
    CGAffineTransform transformScale = CGAffineTransformIdentity
    transformScale = CGAffineTransformScale(transformScale,1 -  0.2 *  fabs(present),1 - 0.2 * fabsf(present));
//合并变换
    cell.transform = CGAffineTransformConcat(transformRotate,transformScale);
}

@end

四、总结与思考


UICollectionView 也是 View,只不过系统为了更好的服务于开发者,快速高效的实现某些开发场景,进行了封装与优化,将复杂的逻辑单独的封装成一个管理类,这里就是 UICollectionViewLayout,交给它去做一些固定且复杂的逻辑。所以,自定义复杂UI的时候,就需要将功能模块足够细化,以实现更好的代码衔接。代码拙劣,大神勿笑[抱拳][抱拳][抱拳]


作者:头疼脑胀的代码搬运工
链接:https://juejin.cn/post/7119028552263008293
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

Swift中的可选项Optional

iOS
为什么需要Optional Swift中引入了可选项(Optional)的概念是为了解决在代码中对于某些变量或常量可能为nil的情况进行处理,从而减少了程序中的不确定性,使得程序更加稳定和安全。 什么是Optional 在Swift中,可选项的类型是使用?来表...
继续阅读 »

为什么需要Optional


Swift中引入了可选项(Optional)的概念是为了解决在代码中对于某些变量或常量可能为nil的情况进行处理,从而减少了程序中的不确定性,使得程序更加稳定和安全。


什么是Optional


在Swift中,可选项的类型是使用?来表示的,例如String?即为一个可选的字符串类型,表示这个变量或常量可能为nil。而对于不可选项,则直接使用相应类型的名称,例如String表示一个非可选的字符串类型。

var str: String = nil
var str1: String? = nil

Optional实现原理


Optional实际上是Swift语言中的一种枚举类型。在Swift中声明Optional类型时,编译器会自动将其转换成对应的枚举类型,例如:

var optionalValue: Int? = 10
// 等价于:
enum Optional<Int> {
    case none
    case some(Int)
}
var optionalValue: Optional<Int> = .some(10)

在上面的代码中,我们声明了一个Optional类型的变量optionalValue,并将其初始化为10。实际上,编译器会自动将其转换为对应的枚举类型,即Optional枚举类型的.some(Int),其中的Int就是我们所声明的可选类型的关联值。


当我们在使用Optional类型的变量时,可以通过判断其枚举值是.none还是.some来确定它是否为nil。如果是.none,表示该Optional值为空;如果是.some,就可以通过访问其关联值获取具体的数值。


Optional的源码实现为:

@frozen public enum Optional<Wrapped> : ExpressibleByNilLiteral {
case none
case some(Wrapped)
}

  • Optioanl其实是标准库里的一个enum类型
  • 用标准库实现语言特性的典型
  • Optional.none 就是nil
  • Optional.some 就是包装了实际的值
  • 泛型属性 unsafelyUnwrapped
  • 理论上我们可以直接调用unsafelyUnwrapped获取可选项的值

Optional的解包方式


1. 可选项绑定(Optional Binding)


使用 if let 或者 guard let 语句来判断 Optional 变量是否有值,如果有值则解包,并将其赋值给一个非可选类型的变量。

var optionalValue: Int? = 10
// 可选项绑定
if let value = optionalValue {
    print("Optional value is \(value)")
} else {
    print("Optional value is nil")
}

可选项绑定语句有两个分支:if分支和else分支。如果 optionalValue 有值,if 分支就会被执行,unwrappedValue 就会被赋值为 optionalValue 的值。否则,执行 else 分支。


2. 强制解包(Forced Unwrapping)


使用!来获取一个不存在的可选值会导致运行错误,在使用!强制展开之前必须保证可选项中包含一个非nil的值

var optionalValue: Int? = 10
let nonOptionalValue = optionalValue!  // 解包optionalValue值
print(nonOptionalValue)                // 输出:10

需要注意的是,如果 Optional 类型的值为 nil,使用强制解包方式解包时,会导致运行时错误 (Runtime Error)。


3. 隐式解包(Implicitly Unwrapped Optionals)


在定义 Optional 类型变量时使用 ! 操作符,标明该变量可以被隐式解包。用于在一些情况下,我们可以确定该 Optional 变量绑定后不会为 nil,可以快捷的解包而不用每次都使用 ! 或者 if let 进行解包。

var optionalValue: Int! = 10
let nonOptionalValue = optionalValue // 隐式解包
print(nonOptionalValue) // 输出:10

需要注意的是,隐式解包的 Optional 如果 nil 的话,会导致 runtime error,所以使用隐式解包 Optional 需要确保其一直有值,否则还是需要检查其非 nil 后再操作。


总的来说,我们应该尽量避免使用强制解包,而是通过可选项绑定来处理 Optional 类型的值,在需要使用隐式解包的情况下,也要确保其可靠性和稳定性,尽量减少出现运行时错误的概率。


可选链(Optional Chaining)


是一种在 Optional 类型值上进行操作的方式,可以将多个 Optional 值的处理放在一起,并在任何一个 Optional 值为 nil 的时刻停止处理。


通过在 Optional 类型值后面跟上问号 ?,我们就可以使用可选链来访问该 Optional 对象的属性和方法。

class Person {
    var name: String
    var father: Person?
    init(name: String, father: Person?) {
        self.name = name
        self.father = father
    }
}
let father = Person(name: "Father", father: nil)
let son = Person(name: "Son", father: father)

// 可选链调用属性
if let fatherName = son.father?.name {
    print("Father's name is \(fatherName)") // 输出:Father's name is Father
} else {
    print("Son without father")
}

// 可选链调用方法
if let count = son.father?.name.count {
    print("Father's name has \(count) characters") // 输出:Father's name has 6 characters
} else {
    print("Son without father")
}

在上面的代码中,我们定义了一个 Person 类,并初始化了一个包含父亲(father)的儿子(son)对象。其中,父亲对象的father属性为nil。我们使用问号 ? 来标记 father 对象为 Optional 类型,以避免访问 nil 对象时的运行时错误。


需要注意的是,如果一个 Optional 类型的属性通过可选链调用后,返回值不是 Optional 类型,那么在可选链调用后,就不再需要加问号 ? 标记其为 Optional 类型了。

class Person {
    var name: String
    var age: Int?
    init(name: String, age: Int?) {
        self.name = name
        self.age = age
    }
    func printInfo() {
        print("\(name), \(age ?? 0) years old")
    }
}
let person = Person(name: "Tom", age: nil)

// 可选链调用方法后,返回值不再是 Optional 类型
let succeed = person.printInfo() // 输出:Tom, 0 years old

在上面的代码中,我们定义了一个 Person 类,并初始化了一个包含年龄(age)的人(person)对象。在可选链调用对象的方法——printInfo() 方法后,因为该方法返回值不是 Optional 类型,所以 returnedValue 就不再需要加问号 ? 标记其为 Optional 类型了。


Optional 的嵌套


将一个 Optional 类型的值作为另一个 Optional 类型的值的成员,形成嵌套的 Optional 类型。

var optionalValue: Int? = 10
var nestedOptionalValue: Int?? = optionalValue

在上面的代码中,我们定义了一个 Optional 类型的变量 optionalValue,并将其赋值为整型变量 10。然后,我们将 optionalValue 赋值给了另一个 Optional 类型的变量 nestedOptionalValue,形成了一个嵌套的 Optional 类型。


在处理嵌套的 Optional 类型时,我们需要特别小心,因为它们的使用很容易造成逻辑上的混淆和错误。为了解决这个问题,我们可以使用 Optional Binding 或者 ?? 操作符(空合并运算符)来降低 Optional 嵌套的复杂度。

var optionalValue: Int? = 10
var nestedOptionalValue: Int?? = optionalValue

// 双重可选项绑定
if let nestedValue = nestedOptionalValue, let value = nestedValue {
    print(value) // 输出:10
} else {
    print("Optional is nil")
}
// 空合并运算符
let nonOptionalValue = nestedOptionalValue ?? 0
print(nonOptionalValue) // 输出:Optional(10)

在上面的代码中,我们使用了双重可选项绑定来判断 nestedOptionalValue 是否可绑定,以及其嵌套的 Optional 值是否可绑定,并将该值赋值给变量 value,以避免 Optional 值的嵌套。另外,我们还可以使用 ?? 操作符(空合并运算符)来对嵌套的 Optional 值进行默认取值的操作。


需要注意的是,虽然我们可以使用 ?? 操作符来降低 Optional 值的嵌套,但在具体的实际应用中,我们应该在设计时尽量避免 Optional 值的嵌套,以便代码的可读性和维护性。如果对于某个变量来说,它的值可能为空,我们可以考虑使用默认值或者定义一个默认值的 Optional 值来代替嵌套的 Optional 类型。


学习 Swift,勿忘初心,方得始终。但要陷入困境时,也不要忘了最初的梦想和时代所需要的技能。


作者:AliliWVIP
链接:https://juejin.cn/post/7214396380353806392
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

基于协议的业务模块路由管理

iOS
概述 这是一个关于业务模块与路由权限的管理方案,用于增强在模块化架构场景下,业务模块的健壮性。 通过对App生命周期的转发,来解除App入口与业务模块管理逻辑的耦合。通过协议来管理API路由,通过注册制实现API的服务发现。 业务模块 重新组织后,业务模块的...
继续阅读 »

概述


这是一个关于业务模块与路由权限的管理方案,用于增强在模块化架构场景下,业务模块的健壮性。


  • 通过对App生命周期的转发,来解除App入口与业务模块管理逻辑的耦合。
  • 通过协议来管理API路由,通过注册制实现API的服务发现。

业务模块




重新组织后,业务模块的管理会变得松散,容易实现插拔复用。


协议

public protocol SpaceportModuleProtocol {
   var loaded: Bool { get set}
   /// 决定模块的加载顺序,数字越大,优先级越高
   /// - Returns: 默认优先级为1000
   static func modulePriority() -> Int
   /// 加载
   func loadModule()
   /// 卸载
   func unloadModule()

   /// UIApplicationDidFinishLaunching
   func applicationDidFinishLaunching(notification: Notification)
   /// UIApplicationWillResignActive
   func applicationWillResignActive(notification: Notification)
   /// UIApplicationDidBecomeActive
   func applicationDidBecomeActive(notification: Notification)
   /// UIApplicationDidEnterBackground
   func applicationDidEnterBackground(notification: Notification)
   /// UIApplicationWillEnterForeground
   func applicationWillEnterForeground(notification: Notification)
   /// UIApplicationWillTerminate
   func applicationWillTerminate(notification: Notification)
}

特性


  • 实现模块加载/卸载保护,模块只会加载/卸载一次。
  • 同一个模块的注册是替换制,新模块会替代旧模块。
  • 提供模块优先级配置,优先级高的模块会更早加载并响应Application的生命周期回调。

最佳实践

@UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
   var window: UIWindow?
   func application(_ application: UIApplication, didFinishLaunchingWithOptionslaunchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
       setupModules()
// ......
       return true
   }
 
   func setupModules() {
       var modules: [SpaceportModuleProtocol] = [
           LoggerModule(),             // 4000
           NetworkModule(),            // 3000
           FirebaseModule(),           // 2995
           RouterModule(),             // 2960
           DynamicLinkModule(),        // 2950
           UserEventRecordModule(),    // 2900
           AppConfigModule(),          // 2895
           MediaModule(),              // 2800
           AdModule(),                 // 2750
           PurchaseModule(),           // 2700
           AppearanceModule(),         // 2600
           AppstoreModule(),           // 2500
           MLModule()                  // 2500
       ]
#if DEBUG
       modules.append(DebugModule())   // 2999
#endif
       Spaceport.shared.registerModules(modules)
       Spaceport.shared.enableAllModules()
   }
}

协议路由


协议路由


通过路由的协议化管理,实现模块/组件之间通信的权限管理。


  • 服务方通过Router Manger注册API协议,可以根据场景提供不同的协议版本。
    • 业务方通过Router Manager发现并使用API协议。


最佳实践


实现API协议

protocol ResultVCRouterAPI {
   @MainActor func vc(from: ResultVCFromType, project: Project) throws -> ResultVC
   @MainActor func vcFromPreview(serviceType: EnhanceServiceType, originalImage:UIImage, enhancedImage: UIImage) async throws -> ResultVC
}

class ResultVCRouter: ResultVCRouterAPI {
   @MainActor func vc(from: ResultVCFromType, project: Project) throws -> ResultVC {
       let vc = ResultVC()
       vc.modalPresentationStyle = .overCurrentContext
       try vc.vm.config(project: project)
       vc.vm.fromType = from
       return vc
   }

   @MainActor func vcFromPreview(serviceType: EnhanceServiceType, originalImage:UIImage, enhancedImage: UIImage) async throws -> ResultVC {
       let vc = ResultVC()
       vc.modalPresentationStyle = .overCurrentContext
       try await vc.vm.config(serviceType: serviceType, originalImage: originalImage,enhancedImage: enhancedImage)
       return vc
   }
}

注册API协议

public class RouterManager: SpaceportRouterService {
   public static let shared = RouterManager()
   private override init() {}
   static func API<T>(_ key: TypeKey<T>) -> T? {
       return shared.getRouter(key)
   }
}

class RouterModule: SpaceportModuleProtocol {
   var loaded = false
   static func modulePriority() -> Int { return 2960 }
   func loadModule() {
     // 注册API
       RouterManager.shared.register(TypeKey(ResultVCRouterAPI.self), router:ResultVC())
   }
   func unloadModule() { }
}

使用协议

// 通过 RouterManager 获取可用API
guard let api = RouterManager.API(TypeKey(ResultVCRouterAPI.self)) else { return }
let vc = try await api.vcFromPreview(serviceType: .colorize, originalImage:originalImage, enhancedImage: enhancedImage)
self.present(vc, animated: false)

总结


我们的业务向模块化、组件化架构演化的过程中,逐步出现跨组件调用依赖嵌套,插拔困难等问题。


通过抽象和简化,设计了这个方案,作为后续业务组件化的规范之一。通过剥离业务模块的生命周期,以及统一通信的方式,可以减缓业务增长带来的代码劣化问题。


作者:Ginhoor大帅
链接:https://juejin.cn/post/7198388410381402168
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

iOS之WebViewJavascriptBridge浅析

iOS
前言 H5页面具有跨平台、开发容易、上线不需要跟随App的版本等优点,但H5页面也有体验不如native好、没有native稳定等问题。所以目前大部分App都是使用Hybrid混合开发的。 当然有了H5页面就少不了H5与native交互,交互就会用到bridg...
继续阅读 »

前言


H5页面具有跨平台、开发容易、上线不需要跟随App的版本等优点,但H5页面也有体验不如native好、没有native稳定等问题。所以目前大部分App都是使用Hybrid混合开发的。


当然有了H5页面就少不了H5与native交互,交互就会用到bridge的能力了。WebViewJavascriptBridge是一个native与JS进行消息互通的第三方库,本章会简单解析一下WebViewJavascriptBridge的源码和实现原理。


通讯原理


JavaScriptCore


JavaScriptCore作为iOS的JS引擎为原生编程语言OC、Swift 提供调用 JS 程序的动态能力,还能为 JS 提供原生能力来弥补前端所缺能力。
iOS中与JS通讯使用的是JavaScriptCore库,正是因为JavaScriptCore这种起到的桥梁作用,所以也出现了很多使用JavaScriptCore开发App的框架,比如RN、Weex、小程序、Webview Hybrid等框架。
如图:




当然JS引擎不光有苹果的JavaScriptCore,谷歌有V8引擎、Mozilla有SpiderMoney


JavaScriptCore本章只简单介绍,后面主要解析WebViewJavascriptBridge。因为uiwebview已经不再使用了,所以后面提到的webview都是wkwebview,demo也是以wkwebview进行解析。


源码解析


代码结构


除了引擎层外,还需要native、h5和WebViewJavascriptBridge三层才能完成一整个信息通路。WebViewJavascriptBridge就是中间那个负责通信的SDK。


WebViewJavascriptBridge的核心类主要包含几个:


  • WebViewJavascriptBridge_JS:是一个JS的字符串,作用是JS环境的Bridge初始化和处理。负责接收native发给JS的消息,并且把JS环境的消息发送给native。
  • WKWebViewJavascriptBridge/WebViewJavascriptBridge:主要负责WKWebView和UIWebView相关环境的处理,并且把native环境的消息发送给JS环境。
  • WebViewJavascriptBridgeBase:主要实现了native环境的Bridge初始化和处理。



初始化


WebViewJavascriptBridge是如何完成初始化的呢,首先要有webview容器,所以要对webview容器进行初始化,设置代理,初始化WebViewJavascriptBridge对象,加载URL。

    WKWebView* webView = [[NSClassFromString(@"WKWebView") alloc] initWithFrame:self.view.bounds];
webView.navi