阅读视图
iOS小技能:给debugserver添加task_for_pid权限,以便调试从AppStore中获取的App。
持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第28天,点击查看活动详情
前言
在做iOS开发时,在Mac上输入LLDB的命令就可以控制iOS端的App,是因为在iOS客户端中有一个debugserver服务器。
debugserver专门用来连接Mac端的LLDB客户端,接收LLDB所提供的命令,并且进行相应的执行。
如果你的iOS设备进行过真机调试的话,设备中就会被安装上debugserver, 不过该debugserver只能用来调试你自己的相关应用。
如果想要调试从AppStore中获取的App的话那么我们需要对iOS设备上的debugserver进行处理,那就是
给debugserver添加task_for_pid权限
本文的重点是给debugserver添加task_for_pid权限
,以便调试从AppStore中获取的App
I 、获取debugserver
iPhone:/Developer/usr/bin root# ls
DTDeviceArbitration ScreenShotr XcodeDeviceMonitor debugserver iprofiler xctest
位于/Developer/usr/bin目录下的debugserver。此debugserver只支持调试我们自己的App, 如果需要调试其他人的App的话,需要对此debugserver进行处理
II、对debugserver进行瘦身
进入到到Mac中debugserver所在的目录下执行上述命令即可,-thin后方填写你的测试机相应的ARM架构即可,因为我的测试机是iPhone 6 Plus, 是arm64的架构,所以此处填的参数是arm64, 如果你的是iPhone5的设备,那么就是armv7s了。
devzkndeMacBook-Pro:Downloads devzkn$ scp iphone:/Developer/usr/bin/debugserver ./debugserver
lipo -thin arm64 debugserver -output debugserver
devzkndeMacBook-Pro:Downloads devzkn$ ls -l debugserver
-rwxr-xr-x 1 devzkn staff 13801968 Oct 17 17:19 debugserver
devzkndeMacBook-Pro:Downloads devzkn$ lipo -thin armv7s debugserver -output debugserver
devzkndeMacBook-Pro:Downloads devzkn$ ls -l debugserver
-rwxr-xr-x 1 devzkn staff 4582800 Oct 17 17:19 debugserver
III、给debugserver添加task_for_pid权限
给debugserver添加task_for_pid权限后,我们就可以使用LLDB调试其他App了。 此部分我们需要一个存储配置信息的xml文件,该文件的内容如下。你可以将下下方的文本进行拷贝,然后存储成ent.xml即可。
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.springboard.debugapplications</key>
<true/>
<key>get-task-allow</key>
<true/>
<key>task_for_pid-allow</key>
<true/>
<key>run-unsigned-code</key>
<true/>
</dict>
</plist>
在给debugserver符权限时,我们需要使用到ldid命令,如果你的Mac上没有安装ldid命令,那么请用brew进行install 执行下方的命令行就可以给我们的debugserver赋上task_for_pid权限。需要注意的是-S与ent.xml文件名中是没有空格的。
ldid -Sent.xml debugserver
IV、将debugserver拷贝到iOS设备中
最后一步就是将处理好的debugserver拷贝到我们的越狱设备中,并且给debugserver赋上可执行的权限。
chmod +x debugserver
因为/Developer/usr/bin目录下的debugserver是只读的,所以你不能将处理好的debugserver拷贝到上述文件,
你要将处理好的debugserver拷贝到/usr/bin/目录下
devzkndeMacBook-Pro:Downloads devzkn$ scp ./debugserver iphone:/usr/bin/debugserver
Phone:/usr/bin root# ls -l debugserver
-rwxr-xr-x 1 root wheel 4582512 Oct 17 17:31 debugserver
iPhone:/usr/bin root# debugserver
debugserver-@(#)PROGRAM:debugserver PROJECT:debugserver-320.2.89
for armv7.
Usage:
debugserver host:port [program-name program-arg1 program-arg2 ...]
debugserver /path/file [program-name program-arg1 program-arg2 ...]
debugserver host:port --attach=<pid>
debugserver /path/file --attach=<pid>
debugserver host:port --attach=<process_name>
debugserver /path/file --attach=<process_name>
see also
How can you catch a process that is about to be launched, if you don’t know the PID yet?
———————————————— 版权声明:本文为CSDN博主「#公号:iOS逆向」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。 更多内容请关注
公号:iOS逆向
原文链接:blog.csdn.net/z929118967/…
iOS小技能:__attribute__的应用
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第26天,点击查看活动详情
引言
LLVM和其他 GCC 特性一样,Clang 支持了 attribute, 还加入了一小部分扩展特性。
__attribute__
语法格式为:__attribute__ ((attribute-list))
constructor(priority), destructor(priority)
分别可以在main() 先后执⾏,可⽤于全局资源初始化和回收。
destructor让系统在main()函数退出或者调用了exit()之后,调用我们的函数。
Function-Attributes: https://gcc.gnu.org/onlinedocs/gcc/Function-Attributes.html
I __attribute__的应用案例
1.1 代码注入
- ARM (通过汇编调用svc实现用户态到内核态的转换)
// 使用inline方式将函数在调用处强制展开,防止被hook和追踪符号
static __attribute__((always_inline)) void anti_debug()
#ifdef __arm__
asm volatile(
"mov r0,#31\n"
"mov r1,#0\n"
"mov r2,#0\n"
"mov r12,#26\n"
"svc #80\n"
);
#endif
#ifdef __arm64__
asm volatile(
"mov x0,#26\n"
"mov x1,#31\n"
"mov x2,#0\n"
"mov x3,#0\n"
"mov x16,#0\n"
"svc #128\n"
);
#endif
}
- 代码注入: facebook/fishhook符号表替换
/*
* A structure representing a particular intended rebinding from a symbol
* name to its replacement
*/
struct rebinding {//rebinding结构体
const char *name; //符号名称,C字符串,用来表明我们要hook哪个函数。
void *replacement; //新函数的地址
void **replaced; //原始函数地址的指针!
};
//重新绑定符号
FISHHOOK_VISIBILITY
int rebind_symbols(struct rebinding rebindings[], size_t rebindings_nel);
//rebindings[] 是一个 rebinding类型数组,用来存储需要hook的函数
//rebindings_nel 表示数组的长度
/*
* Rebinds as above, but only in the specified image. The header should point
* to the mach-o header, the slide should be the slide offset. Others as above.
*/
FISHHOOK_VISIBILITY
int rebind_symbols_image(void *header,
intptr_t slide,
struct rebinding rebindings[],
size_t rebindings_nel);
//指定镜像的header, slide 表示偏移量
hook ptrace
函数,进行反反调试。
PT_DENY_ATTACH is an Apple-specific constant that can prevent debuggers (gdb, DTrace, etc.) from debugging your binary in kernel-level.
ptrace(PT_DENY_ATTACH, 0, 0, 0);
#if !defined(PT_DENY_ATTACH)
#define PT_DENY_ATTACH 31
#endif
Rebinding customRebind = {"ptrace", my_ptrace, (void*)&orig_ptrace};
//第一个参数为需要替换的符号
//第二个参数为自己实现的函数名称
//第三个参数为原函数地址,因为fishhook是基于地址进行替换的+ `__attribute__((constructor))`实现注入
rebind_symbols((struct rebinding[1]){customRebind},1);
int my_ptrace(int _request, pid_t _pid, caddr_t _addr, int _data){
if(_request != PT_DENY_ATTACH){
return orig_ptrace(_request,_pid,_addr,_data);
}
return 0;
}
- 自定义打印方法:用真正的方法替换去拦截 NSLog 的功能(
iOS 11 之后这种方法失效了
),使用__attribute__((constructor));
进行实现,extern进行申明公共方法。
#ifdef DEBUG
// iOS 11 之前用真正的方法替换去实现拦截 NSLog 的功能,iOS 11 之后这种方法失效了,所以只能用宏定义的方式覆盖 NSLog。这也就意味着在 iOS 11 下一些如果某些代码编译时机比 QMUI 早,则这些代码里的 NSLog 是无法被替换为 KNLog 的
extern void _NSSetLogCStringFunction(void (*)(const char *string, unsigned length, BOOL withSyslogBanner));
static void PrintNSLogMessage(const char *string, unsigned length, BOOL withSyslogBanner) {
QMUILog(@"NSLog", @"%s", string);
}
static void HackNSLog(void) __attribute__((constructor));
static void HackNSLog(void) {
_NSSetLogCStringFunction(PrintNSLogMessage);
}
#define NSLog(...) KNLog(@"NSLog", __VA_ARGS__)// iOS 11 以后真正生效的是这一句
#endif
1.2 对格式化字符串进行类型检查
extern int
my_printf (void *my_object, const char *my_format, ...)
__attribute__((format(printf, 2, 3)));
//format 属性用于指定一个函数接收类似 printf, scanf, strftime 和 strfmon 风格的参数,应该按照参数对格式化字符串进行类型检查。
1.3 控制符号的可见性
#define STD_EXPORTS __attribute__ ((visibility("default")))
The -fvisibility=vis compiler option
lets you set the visibility for symbols in the current compilation. When set to hidden, symbols not explicitly marked as visible are hidden.
__attribute__((visibility("default"))) void MyFunction1() {}
__attribute__((visibility("hidden"))) void MyFunction2() {}
1.4 表明一些函数参数应该是非空的指针
extern void *
my_memcpy (void *dest, const void *src, size_t len)
__attribute__((nonnull (1, 2)));
1.5 确保线程在应用整个生命周期内都能一直运行
AFNetworking 在网络请求线程的入口使用 noreturn 属性,用于网络请求的 NSThread。
+ (void) __attribute__((noreturn)) networkRequestThreadEntryPoint:(id)__unused object {//确保这个线程在应用整个生命周期内都能一直运行
do {
@autoreleasepool {
[[NSRunLoop currentRunLoop] run];
}
} while (YES);
}
+ (NSThread *)networkRequestThread {//专门用于网络请求的 NSThread
static NSThread *_networkRequestThread = nil;
static dispatch_once_t oncePredicate;
dispatch_once(&oncePredicate, ^{
_networkRequestThread = [[NSThread alloc] initWithTarget:self selector:@selector(networkRequestThreadEntryPoint:) object:nil];
[_networkRequestThread start];
});
return _networkRequestThread;
}
1.6 检查能否使用特定的属性
可以用 __has_attribute
这个指令
#ifndef AX_REQUIRES_SUPER
#if __has_attribute(objc_requires_super)
#define AX_REQUIRES_SUPER __attribute__((objc_requires_super))
#else
#define AX_REQUIRES_SUPER
__attribute((objc_requires_super)) was first introduced as work in progress into CLANG in September 2012 and was documented in October 2013. On both OS X and iOS there is now a NS_REQUIRES_SUPER macro that conditionally wraps the objc_requires_super attribute depending on compiler support. Once a method declaration is appended with this macro, the compiler will produce a warning if super is not called by a subclass overriding the method.
II 导出和隐藏符号
2.1 导出符号信息
- 查看导出符号信息:
nm -gm tmp_64.dylib
(__DATA,__data) external (undefined) external _CFDataCreate (from CoreFoundation) (undefined) external _CFNotificationCenterGetDarwinNotifyCenter (from CoreFoundation) (__TEXT,__text) external (undefined) external _IOObjectRelease (from IOKit) (undefined) external _IORegistryEntryCreateCFProperty (from IOKit) 000000010ffa3f97 (__DATA,__objc_data) external OBJC_CLASS_BslyjNwZmPCJkVst 000000010ffa3f97 (__DATA,__objc_data) external _OBJC_CLASS__ChiDDQmRSQpwQJgm
2.2 __attribute__控制符号是否导出
The
-fvisibility=vis compiler option
lets you set the visibility for symbols in the current compilation. When set to hidden, symbols not explicitly marked as visible are hidden.
#define EXPORT __attribute__((visibility("default")))
隐藏未明确标记为可见的符号:
-
在编译参数中加入
-exported_symbols_list export_list
-
在编译参数中指定-fvisibility=hidden,对指定符号增加visibility(“default”)来导出符号
__attribute__((visibility("default"))) void MyFunction1() {}
__attribute__((visibility("hidden"))) void MyFunction2() {}
static 参数修饰,不会导出符号信息
static char _person_name[30] = {'\0'};
2.3 Pragmas控制符号是否导出
void f() { }
#pragma GCC visibility push(default)
void g() { }
void h() { }
#pragma GCC visibility pop
III ptrace系统调用
为了方便应用软件的开发和调试,unix的早期版本提供了一种对运行中的进程进行跟踪和控制手段:系统调用ptrace;通过ptrace,可以对另一个进程实现调试跟踪,同时ptrace提供了一个PT_DENY_ATTACH = 31参数用于告诉系统阻止调试器的依附
。
//ptrace系统调用 用于实现断点调试和对进程进行跟踪和控制
#include <sys/ptrace.h>
long ptrace(enum __ptrace_request request, pid_t pid, void *addr, void *data);
//enum __ptrace_request request:指示了ptrace要执行的命令。
//pid_t pid: 指示ptrace要跟踪的进程。
//void *addr: 指示要监控的内存地址。
//void *data: 存放读取出的或者要写入的数据。
//PT_DENY_ATTACH is an Apple-specific constant that can prevent debuggers (gdb, DTrace, etc.) from debugging your binary in kernel-level.
//ptrace(PT_DENY_ATTACH, 0, 0, 0);
gdb利用ptrace系统调用,在被调试程序和gdb之间建立跟踪关系。然后所有发送给被调试程序的信号(除SIGKILL)都会被gdb截获,gdb根据截获的信号,查看被调试程序相应的内存地址,并控制被调试的程序继续运行。
3.1 syscall
syscall是通过软中断来实现从用户态到内核态,syscall (26,31,0,0)
来调用系统函数ptrace(PT_DENY_ATTACH, 0, 0, 0);
。
ptrace的系统调用函数号是26,31是PT_DENY_ATTACH(用于告诉系统阻止调试器的依附)。
int syscall(int, ...);
#defineSYS_ptrace 26
3.2 反调试
-
运行时期,断点ptrace,直接返回
-
分析如何调用的ptrace,hook ptrace
-
通过tweak,替换disable_gdb函数
-
修改 PT_DENY_ATTACH:在二进制文件中 ,修改 PT_DENY_ATTACH的31,改成 任意一个值,如PT_ATTACH 0。 blog.csdn.net/z929118967/… AlipayWalletTweakF.xm
-
ARM (通过汇编调用svc实现用户态到内核态的转换)
// 使用inline方式将函数在调用处强制展开,防止被hook和追踪符号
static __attribute__((always_inline)) void anti_debug()
#ifdef __arm__
asm volatile(
"mov r0,#31\n"
"mov r1,#0\n"
"mov r2,#0\n"
"mov r12,#26\n"
"svc #80\n"
);
#endif
#ifdef __arm64__
asm volatile(
"mov x0,#26\n"
"mov x1,#31\n"
"mov x2,#0\n"
"mov x3,#0\n"
"mov x16,#0\n"
"svc #128\n"
);
#endif
}
see also
小程序:iOS逆向
iOS小技能:消息发送的步骤(利用类型编码加快消息分发)
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第23天,点击查看活动详情
前言
运行时API的应用:
- 路由的实现(接口控制app跳任意界面)
- 获取修改对象的成员属性
- 动态添加/交换方法的实现: blog.csdn.net/z929118967/…
- 属性关联: blog.csdn.net/z929118967/…
- iOS 间接实现多继承的方式:消息转发、类别、delegate和protocol(委托协助主体完成操作任务,将需要定制化的操作预留给委托对象来自定义实现 与block类似) 。
I runtime的作用
因为objective-c是一门动态语言,也就是说只有编译器是不够的,还需要一个运行时系统(runtime system)来执行编译后的代码。
runtime简称运行时,其中最主要的就是消息机制。
对于编译期语言,会在编译的时候决定调用哪个函数。对于OC的函数,是动态调用的,在编译的时候并不能决定真正调用哪个函数,只有在运行时才会根据函数的名称找到对应的函数来调用。
1.1 消息发送的步骤
messages aren’t bound to method implementations until Runtime。(消息直到运行时才会与方法实现进行绑定)
objc_msgSend 是当方法的实现被调用后才会返回数据。
- 当向someObject发送消息时,先在本类中的方法缓存列表中进行查找
- 如果找不到就在本类中的法列表中进行查找
- 如果没找到,就去父类中进行查找。
- 如果没找到,runtime system并不会立即报错使程序崩溃,而是依次执行消息转发。
检查是否有动态添加对应的方法->检查是否有其他对象实现了对应的方法(快速转发给其他对象处理)->(标准消息转发)
- 动态方法解析 :
向当前类发送 resolveInstanceMethod: 信号,检查是否动态向该类添加了方法。
- 快速消息转发: 获取转发对象,
检查该类是否实现了 forwardingTargetForSelector: 方法,若实现了则调用这个方法
。若该方法返回值对象非nil或非self,那么就进行消息的常规转发。 - 标准消息转发:获取方法签名,
runtime发送methodSignatureForSelector:消息获取Selector对应的方法签名
。返回值非空则通过forwardInvocation:
转发消息,返回值为空则向当前对象发送doesNotRecognizeSelector:
消息,程序崩溃退出。
1.2 Runtime API
- 通过对 Runtime 库函数的直接调用
runtime源码:github.com/opensource-…
其中主要使用的函数定义在message.h和runtime.h这两个文件中。
- 通过 Foundation 框架的 NSObject 类定义的方法
Cocoa 程序中绝大部分类都是 NSObject 类的子类,所以都继承了 NSObject 的行为。(NSProxy 类是个例外,它是个抽象超类)
- class方法返回对象的类;
- isKindOfClass: 和 -isMemberOfClass: 方法检查对象是否存在于指定的类的继承体系中(是否是其子类或者父类或者当前类的成员变量);
- respondsToSelector: 检查对象能否响应指定的消息;
-
conformsToProtocol:
检查对象是否实现了指定协议类的方法; -
methodForSelector:
返回指定方法实现的地址。
1.3 理解instance、class object、metaclass的关系
一个实例对象struct objc_object
的isa指针指向它的struct objc_class
类对象,类对象的isa指针指向它的元类;super_class
指针指向了父类的类对象
,而元类
的super_class
指针指向了父类的元类
。
II 消息转发
2.1 消息处理(动态地添加一个方法实现)
当在相应的类以及父类中找不到类方法实现时会执行+resolveInstanceMethod:
这。该方法如果在类中不被重写的话,默认返回NO。如果返回NO就表明不做任何处理,走下一步。
如果返回YES的话,就说明在该方法中对这个找不到实现的方法进行了处理。
在+resolveInstanceMethod:
方法中,我们可以为找不到实现的SEL动态地添加一个方法实现,添加完毕后,就会执行我们添加的方法实现。这样,当一个类调用不存在的方法时,就不会崩溃了。具体做法如下所示:
//运行时方法拦截
- (void)dynamicAddMethod: (NSString *) value {
NSLog(@"OC替换的方法:%@", value);
}
/**
没有找到SEL的IMP实现时会执行下方的方法
@param sel 当前对象调用并且找不到IMP的SEL
@return 找到其他的执行方法,并返回yes
*/
+ (BOOL)resolveInstanceMethod:(SEL)sel {
return NO; //当返回NO时,会接着执行forwordingTargetForSelector:方法,
[KNRuntimeKit addMethod:[self class] method:sel method:@selector(dynamicAddMethod:)];//动态地添加一个方法实现
return YES;
}
2.2 消息快速转发
如果不对上述消息进行处理的话,也就是+resolveInstanceMethod:返回NO时,会走下一步消息转发,即-forwardingTargetForSelector:
。
该方法会返回一个类的对象,这个类的对象有SEL对应的实现。当调用这个找不到的方法时,就会被转发到SecondClass中去进行处理。
/**
将当前对象不存在的SEL传给其他存在该SEL的对象
@param aSelector 当前类中不存在的SEL
@return 存在该SEL的对象
*/
- (id)forwardingTargetForSelector:(SEL)aSelector {
return self;//当该方法返回self或者nil, 说明不对相应的方法进行转发,那么就进行消息的常规转发。
return [SecondClass new]; //让SecondClass中相应的SEL去执行该方法
}
例子: 教师执行手术
//Returns the object to which unrecognized messages should first be directed.
- (id)forwardingTargetForSelector:(SEL)aSelector
{
Doctor *doctor = [[Doctor alloc]init];
if ([doctor respondsToSelector:aSelector]) {
return doctor;
}
return nil;
}
这个方式的好处是隐藏了我要转发的消息,没有类目那么清晰。
2.3 消息常规转发
如果不将消息转发给其他类的对象,那么就只能自己进行处理了。
如果上述方法返回nil或者self的话,会执行-methodSignatureForSelector:
方法来获取方法的参数以及返回数据类型(方法签名)。
返回值非空则通过forwardInvocation:转发消息,返回值为空则向当前对象发送doesNotRecognizeSelector:
消息,程序崩溃,报出找不到相应的方法实现的崩溃信息。
//在+resolveInstanceMethod:返回NO时就会执行下方的方法,下方也是将该方法转发给SecondClass。
- (NSMethodSignature *)methodSignatureForSelector:(SEL)selector {
//查找父类的方法签名
NSMethodSignature *signature = [super methodSignatureForSelector:selector];
if(signature == nil) {
// signature = [NSMethodSignature signatureWithObjCTypes:"@@:"];
signature = [someObj methodSignatureForSelector:aSelector]; //返回指定方法签名
}
return signature;
}
- (void)forwardInvocation:(NSInvocation *)invocation {
SecondClass * forwardClass = [SecondClass new];
SEL sel = invocation.selector;
if ([forwardClass respondsToSelector:sel]) {
[invocation invokeWithTarget:forwardClass];
} else {
[self doesNotRecognizeSelector:sel];
}
}
2.4 两种消息转发方式的比较
- 快速消息转发:简单、快速、但仅能转发给一个对象。
- 标准消息转发:稍复杂、较慢、但转发操作实现可控,可以实现多对象转发
III 编译器指令 @encode()
苹果的 Objective-C 运行时库内部利用类型编码帮助加快消息分发。
NSValue 的 +valueWithBytes:objCType:
第二个参数需要用@encode()
指令来创建一种内部表示的字符串 。
@encode(int) → i
+ (NSValue *)valueWithBytes:(const void *)value objCType:(const char *)type;
+ (NSValue *)value:(const void *)value withObjCType:(const char *)type;
3.1 Objective-C type encodings
Code | Meaning |
---|---|
c | A char |
i | An int |
s | A short |
l | A long ,l is treated as a 32-bit quantity on 64-bit programs. |
q | A long long |
C | An unsigned char |
I | An unsigned int |
S | An unsigned short |
L | An unsigned long |
Q | An unsigned long long |
f | A float |
d | A double |
B | A C++ bool or a C99 _Bool |
v | A void |
* | A character string (char *) |
@ | An object (whether statically typed or typed id) |
# | A class object (Class) |
: | A method selector (SEL) |
[array type] | An array |
{name=type...} | A structure |
(name=type...) | A union |
bnum | A bit field of num bits |
^type | A pointer to type |
? | An unknown type (among other things, this code is used for function pointers) |
- (void)testtypeEncodings{
NSLog(@"int : %s", @encode(int));
NSLog(@"float : %s", @encode(float));
NSLog(@"float * : %s", @encode(float*));//指针的标准编码是加一个前置的 ^
NSLog(@"char : %s", @encode(char));
NSLog(@"char * : %s", @encode(char *));//char * 拥有自己的编码 *,因为 C 的字符串被认为是一个实体,而不是指针。
NSLog(@"BOOL : %s", @encode(BOOL));//BOOL 是 c,而不是i。原因是 char 比 int 小。BOOL 更确切地说是 signed char 。
NSLog(@"void : %s", @encode(void));
NSLog(@"void * : %s", @encode(void *));
NSLog(@"NSObject * : %s", @encode(NSObject *));
NSLog(@"NSObject : %s", @encode(NSObject));
NSLog(@"[NSObject] : %s", @encode(typeof([NSObject class])));
NSLog(@"NSError ** : %s", @encode(typeof(NSError **)));
int intArray[5] = {1, 2, 3, 4, 5};
NSLog(@"int[] : %s", @encode(typeof(intArray)));
float floatArray[3] = {0.1f, 0.2f, 0.3f};
NSLog(@"float[] : %s", @encode(typeof(floatArray)));
typedef struct _struct {
short a;
long long b;
unsigned long long c;
} Struct;
NSLog(@"struct : %s", @encode(typeof(Struct)));
}
结果:
2022-06-14 11:18:06.069425+0800 SDKSample[9300:3648699] int : i
2022-06-14 11:18:06.069517+0800 SDKSample[9300:3648699] float : f
2022-06-14 11:18:06.069559+0800 SDKSample[9300:3648699] float * : ^f
2022-06-14 11:18:06.069597+0800 SDKSample[9300:3648699] char : c
2022-06-14 11:18:06.069634+0800 SDKSample[9300:3648699] char * : *
2022-06-14 11:18:06.069669+0800 SDKSample[9300:3648699] BOOL : B
2022-06-14 11:18:06.069706+0800 SDKSample[9300:3648699] void : v
2022-06-14 11:18:06.070191+0800 SDKSample[9300:3648699] void * : ^v
2022-06-14 11:18:06.070230+0800 SDKSample[9300:3648699] NSObject * : @
2022-06-14 11:18:06.070267+0800 SDKSample[9300:3648699] NSObject : {NSObject=#}
2022-06-14 11:18:06.070303+0800 SDKSample[9300:3648699] [NSObject] : #
2022-06-14 11:18:06.070339+0800 SDKSample[9300:3648699] NSError ** : ^@
2022-06-14 11:18:06.070374+0800 SDKSample[9300:3648699] int[] : [5i]
2022-06-14 11:18:06.070870+0800 SDKSample[9300:3648699] float[] : [3f]
2022-06-14 11:18:06.072298+0800 SDKSample[9300:3648699] struct : {_struct=sqQ}
3.2 method encodings
Code | Meaning |
---|---|
r | const |
n | in |
N | inout |
o | out |
O | bycopy |
R | byref |
V | oneway |
带inout 的参数表明它在发消息时对象即可传入又可传出,将参数特别标注为 in 或 out,程序将避免一些来回的开销。
增加一个 bycopy 修饰符以保证发送了一份完整的拷贝
IV 运行时API应用
运行时API的应用:
-
路由的实现(接口控制app跳任意界面) :kunnan.blog.csdn.net/article/det…
-
获取修改对象的成员属性
-
动态添加/交换方法的实现: blog.csdn.net/z929118967/…
-
iOS 间接实现多继承的方式:消息转发、类别、delegate和protocol(委托协助主体完成操作任务,将需要定制化的操作预留给委托对象来自定义实现 与block类似) 。
-
NSClassFromString、NSSelectorFromString 使用例子
id activityIndicator = [NSClassFromString(@"SDNetworkActivityIndicator") performSelector:NSSelectorFromString(@"sharedActivityIndicator")];
#import "KNRuntimeKit.h"
@implementation KNRuntimeKit
/**
获取类名
@param class 相应类
@return NSString:类名
*/
+ (NSString *)fetchClassName:(Class)class {
const char *className = class_getName(class);
return [NSString stringWithUTF8String:className];
}
/**
获取成员变量
@param class Class
@return NSArray
*/
+ (NSArray *)fetchIvarList:(Class)class {
unsigned int count = 0;
Ivar *ivarList = class_copyIvarList(class, &count);
NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
for (unsigned int i = 0; i < count; i++ ) {
NSMutableDictionary *dic = [NSMutableDictionary dictionaryWithCapacity:2];
const char *ivarName = ivar_getName(ivarList[i]);
const char *ivarType = ivar_getTypeEncoding(ivarList[i]);
dic[@"type"] = [NSString stringWithUTF8String: ivarType];
dic[@"ivarName"] = [NSString stringWithUTF8String: ivarName];
[mutableList addObject:dic];
}
free(ivarList);
return [NSArray arrayWithArray:mutableList];
}
/**
获取类的属性列表, 包括私有和公有属性,以及定义在延展中的属性
@param class Class
@return 属性列表数组
*/
+ (NSArray *)fetchPropertyList:(Class)class {
unsigned int count = 0;
objc_property_t *propertyList = class_copyPropertyList(class, &count);
NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
for (unsigned int i = 0; i < count; i++ ) {
const char *propertyName = property_getName(propertyList[i]);
[mutableList addObject:[NSString stringWithUTF8String: propertyName]];
}
free(propertyList);
return [NSArray arrayWithArray:mutableList];
}
/**
获取类的实例方法列表:getter, setter, 对象方法等。但不能获取类方法
@param class <#class description#>
@return <#return value description#>
*/
+ (NSArray *)fetchMethodList:(Class)class {
unsigned int count = 0;
Method *methodList = class_copyMethodList(class, &count);
NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
for (unsigned int i = 0; i < count; i++ ) {
Method method = methodList[i];
SEL methodName = method_getName(method);
[mutableList addObject:NSStringFromSelector(methodName)];
}
free(methodList);
return [NSArray arrayWithArray:mutableList];
}
/**
获取协议列表
@param class <#class description#>
@return <#return value description#>
*/
+ (NSArray *)fetchProtocolList:(Class)class {
unsigned int count = 0;
__unsafe_unretained Protocol **protocolList = class_copyProtocolList(class, &count);
NSMutableArray *mutableList = [NSMutableArray arrayWithCapacity:count];
for (unsigned int i = 0; i < count; i++ ) {
Protocol *protocol = protocolList[i];
const char *protocolName = protocol_getName(protocol);
[mutableList addObject:[NSString stringWithUTF8String: protocolName]];
}
return [NSArray arrayWithArray:mutableList];
return nil;
}
/**
往类上添加新的方法与其实现
@param class 相应的类
@param methodSel 方法的名
@param methodSelImpl 对应方法实现的方法名
*/
+ (void)addMethod:(Class)class method:(SEL)methodSel method:(SEL)methodSelImpl {
Method method = class_getInstanceMethod(class, methodSelImpl);
IMP methodIMP = method_getImplementation(method);
const char *types = method_getTypeEncoding(method);
class_addMethod(class, methodSel, methodIMP, types);
}
/**
方法交换
@param class 交换方法所在的类
@param method1 方法1
@param method2 方法2
*/
+ (void)methodSwap:(Class)class firstMethod:(SEL)method1 secondMethod:(SEL)method2 {
Method firstMethod = class_getInstanceMethod(class, method1);
Method secondMethod = class_getInstanceMethod(class, method2);
method_exchangeImplementations(firstMethod, secondMethod);
}
@end
see also
小程序:iOS逆向
iOS小技能:动态地给类添加新的方法、实例变量、属性。
持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情
前言
添加新的实例变量的原理:利用category结合runtime的API实现
动态创建属性的应用场景:利用属性进行传值的时候,我们就可以利用本文的方法进行动态创建属性。尤其在逆向其他app的时候,往已经存在class新增一个属性,用于数据传递,尤其是异步操作的时候。
I 添加新的实例变量
1.1 原理
利用 runtime APIobjc_setAssociatedObject
和objc_getAssociatedObject
objc_setAssociatedObject
/**
* Sets an associated value for a given object using a given key and association policy.
*
* @param object The source object for the association.
* @param key The key for the association.
* @param value The value to associate with the key key for object. Pass nil to clear an existing association.
* @param policy The policy for the association. For possible values, see “Associative Object Behaviors.”
*
* @see objc_setAssociatedObject
* @see objc_removeAssociatedObjects
*/
OBJC_EXPORT void objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
/**
* Returns the value associated with a given object for a given key.
*
* @param object The source object for the association.
* @param key The key for the association.
*
* @return The value associated with the key \e key for \e object.
*
* @see objc_setAssociatedObject
*/
OBJC_EXPORT id objc_getAssociatedObject(id object, const void *key)
OBJC_AVAILABLE(10.6, 3.1, 9.0, 1.0);
1.2 例子
类别(Category)
通过增加新的类和实例方法来扩展现有类的行为。作为惯例,类别被定义在它们自己的.{h,m}
文件里。
//
// Teacher+Profession.m
//
#import "Teacher+Profession.h"
#import <objc/runtime.h>
const char *ProfessionType = "NSString *"; //就是属性的key
@implementation Teacher (Profession)
-(void)setProf:(NSString*)prof
{
objc_setAssociatedObject(self, ProfessionType, prof, OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
-(NSString *)prof
{
NSString *pro = objc_getAssociatedObject(self, ProfessionType);
return pro;
}
@end
II 动态创建属性
使用分类、@dynamic
、objc_setAssociatedObject、objc_getAssociatedObject 实现。
2.1 应用场景
利用属性进行传值的时候,我们就可以利用本文的方法进行动态创建属性。尤其在逆向其他app的时候,往已经存在class新增一个属性,用于数据传递,尤其是异步操作的时候。
//结合@dynamic的 associatedObject例子
@implementation NSObject (AssociatedObject)
@dynamic associatedObject;
- (void)setAssociatedObject:(id)object {
objc_setAssociatedObject(self,
@selector(associatedObject), object,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject {
return objc_getAssociatedObject(self,
@selector(associatedObject));
}
2.2 例子:为VC新增一个属性
WCNewCommitViewController+KNWCNewCommitViewControllerAssociatedObject.h
#import "WCNewCommitViewController.h"
@interface NSObject (KNWCNewCommitViewControllerAssociatedObject)
// isa (Class): NSKVONotifying_WCNewCommitViewController (isa, 0x5a10db2abf7)
@property (nonatomic, strong) id associatedObject;
@end
WCNewCommitViewController+KNWCNewCommitViewControllerAssociatedObject.m
#import "WCNewCommitViewController+KNWCNewCommitViewControllerAssociatedObject.h"
@implementation NSObject (KNWCNewCommitViewControllerAssociatedObject)
@dynamic associatedObject;
- (void)setAssociatedObject:(id)object {
objc_setAssociatedObject(self,
@selector(associatedObject), object,
OBJC_ASSOCIATION_RETAIN_NONATOMIC);
}
- (id)associatedObject {
return objc_getAssociatedObject(self,
@selector(associatedObject));
}
@end
2.3 效果
- usage:
#import "WCNewCommitViewController+KNWCNewCommitViewControllerAssociatedObject.h"
[WCNewCommit setAssociatedObject:@"sssss"];
- ret
NSLog(@"associatedObject:%@",[self valueForKey:@"associatedObject"]);//2018-09-06 12:06:06.977711 WeChat[717:226743] associatedObject:sssss
See Also
- iOS运行时的应用:
1、实现路由(接口控制app跳任意界面 )
2、获取修改对象的成员属性
3、动态添加/交换方法的实现
4、属性关联
iOS动态库的注入原理
「这是我参与2022首次更文挑战的第20天,活动详情查看:2022首次更文挑战」。
前言
动态库的注入原理:
- 一个是基于修改Mach-O 的Load Commands,即通过修改可执行文件的Load Commands来实现的. 在Load Commands中增加一个LC_LOAD_DYLIB , 写入dylib路径。
Usage: insert_dylib dylib_path binary_path [new_binary_path]
- 一个是利用环境变量DYLD_INSERT_LIBRARIES,例如使用它进行dumpdecrypted(补充:Clutch 通过
posix_spawnp
生成一个新的进程,然后暂停进程并dump内存) - 另一个是在挂载的进程上创建一个挂起的线程, 然后在这个线程里申请一片用于加载动态库的内存,然后恢复线程,动态库就被注入(通过 taskfor_pid函数获取目标进程句柄,然后通过在进程内创建新线程并执行自己的代码。) cycript 就是以这种方式执行脚本代码。
I、静态库和动态库的区别
1.1 动态库的特点
- 存在形式有 .dylib,.framework 和链接符号 .tdb;
- 它的好处是可以只保留一份文件和内存空间,从而能够被多个进程使用,例如系统动态库;
- 可减小可执行文件的体积,不需要链接到目标文件。
1.2 静态库的特点
- 以.a 或者.framework形式存在的一种共享程序代码的方式,从本质上来讲就是一种可执行文件的二进制形式;常常会将程序的部分功能编译成库,暴露出头文件的形式供开发者调用
- 静态库以一个或者多个object文件组成;可以将一个静态库拆解成多个object文件(ar -x)
- 静态库链接的时会直接链接到目标文件,并作为它的一部分存在。
II、动态库的编译和注入
2.1 编译
xcrun --sdk iphoneos clang++ dynamiclib -arch arm64 -framework Foundation Person.mm -o target.dylib -fvisibility=hidden
- Makefile
CC = xcrun --sdk iphoneos clang++
ARCH = arm64
FRAMEWORK = -framework Foundation
VERSION = -compatibility_version 1 -current_version 1
VISIBLE = -fvisibility=hidden
TARGET = target.dylib
SOURCE = Person.m
$(TARGET):$(SOURCE)
$(CC) -dynamiclib -arch $(ARCH) $(FRAMEWORK) $(SOURCE) -o $(TARGET) $(VERSION)
.PHONY:clean
clean:
rm $(TARGET)
2.2 动态库的注入方式
2.2.1 cycript注入动态库的方式
在挂载的进程上创建一个挂起的线程, 然后在这个线程里申请一片用于加载动态库的内存,然后恢复线程,动态库就被注入(通过 taskfor_pid函数获取目标进程句柄,然后通过在进程内创建新线程并执行自己的代码。)
2.2.2 通过环境变量DYLD_INSERT_LIBRARIES 注入
DYLD_INSERT_LIBRARIES=/PathFrom/dumpdecrypted.dylib /PathTo
#New Run Script Phase:
cd ${TARGET_BUILD_DIR}
export DYLD_INSERT_LIBRARIES=./libKNoke.dylib && /Applications/QKNQ.app/Contents/MacOS/QKNQ
2.2.3 通过增加load command 的LC_LOAD_DYLIB或者LC_LOAD_WEAK_DYLIB,指定动态库的路径来实现注入
修改App可执行文件的头部,给它添加这么一个load command,并指定load我们构造的dylib就好
- 二次打包动态库的注入:
避免每次从环境变量注入–偏静态:通过LC_LOAD_DYLIB实现dylib的加载
通过修改可执行文件的Load Commands来实现的. 在Load Commands中增加一个LC_LOAD_DYLIB , 写入dylib路径 Usage: insert_dylib dylib_path binary_path [new_binary_path]
1、现在iOS上的绝大多数以root权限运行的App,都是通过setuid + bash来实现的
2、App运行所需要的信息,一般都存放在其MachO头部43中,其中dylib的信息是由load commands指定的.
这些信息是以静态的方式存放在二进制文件里(不是由DYLD_INSERT_LIBRARIES动态指定),而又是由dyld动态加载的,所以我们给它起了个“偏静态”的名字--在此App得到执行时,dyld会查看其MachO头部中的load commands,并把里面LC_LOAD_DYLIB相关的dylib给加载到进程的内存空间
- 如果需要修改LC_ID_DYLIDB、、LC_LOAD_DYLIB,可以使用install_name_tool
install_name_toll -id xxx imputfile
install_name_toll -change old new imputfile
- 通过cydia substrate提高的注入:
配置plist文件,并将对应的plist、dylib文件放入指定目录 /Layout/Library/MobileSubstrate/DynamicLibraries/、/usr/lib/TweakInject
其实也是通过DYLD_INSERT_LIBRARIES将自己注入,然后遍历DynamicLibraries目录下的plist文件,再将符合规则的动态库通过dlopen打开
III、导出和隐藏符号
3.1 导出符号
- 查看导出符号信息
nm -gm tmp_64.dylib
(__DATA,__data) external
(undefined) external _CFDataCreate (from CoreFoundation)
(undefined) external _CFNotificationCenterGetDarwinNotifyCenter (from CoreFoundation)
(__TEXT,__text) external
(undefined) external _IOObjectRelease (from IOKit)
(undefined) external _IORegistryEntryCreateCFProperty (from IOKit)
000000010ffa3f97 (__DATA,__objc_data) external _OBJC_CLASS_$_BslyjNwZmPCJkVst
000000010ffa3f97 (__DATA,__objc_data) external _OBJC_CLASS_$_ChiDDQmRSQpwQJgm
3.2 隐藏符号
- static 参数修饰,不会导出符号信息
static char _person_name[30] = {'\0'};
- 在编译参数中加入-exported_symbols_list export_list
CC = xcrun --sdk iphoneos clang
ARCH = arm64
FRAMEWORK = -framework Foundation
VERSION = -compatibility_version 1 -current_version 1
EXPORT = -exported_symbols_list export_list
VISIBLE = -fvisibility=hidden
TARGET = target.dylib
SOURCE = Person.mm
target1:$(SOURCE1)
$(CC) -dynamiclib -arch $(ARCH) $(FRAMEWORK) $(SOURCE) -o $(TARGET) $(VERSION)
target2:$(SOURCE1)
$(CC) -dynamiclib -arch $(ARCH) $(FRAMEWORK) $(SOURCE) -o $(TARGET) $(VERSION) $(EXPORT)
target3:$(SOURCE1)
$(CC) -dynamiclib -arch $(ARCH) $(FRAMEWORK) $(SOURCE) -o $(TARGET) $(VERSION) $(VISIBLE)
clean:
rm $(TARGET)
- 在编译参数中指定-fvisibility=hidden,对指定符号增加visibility(“default”)来导出符号
//#define EXPORT __attribute__((visibility("default")))
CC = xcrun --sdk iphoneos clang++
ARCH = arm64
FRAMEWORK = -framework Foundation
VERSION = -compatibility_version 1 -current_version 1
VISIBLE = -fvisibility=hidden
TARGET = target.dylib
SOURCE = Person.m
$(TARGET):$(SOURCE)
$(CC) -dynamiclib -arch $(ARCH) $(FRAMEWORK) $(SOURCE) -o $(TARGET) $(VERSION)
.PHONY:clean
clean:
rm $(TARGET)
see also
由于篇幅原因,更多内容请关注 #小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向。
🍅 联系作者: iOS逆向(公号:iosrev)
🍅 作者简介:CSDN 博客专家认证🏆丨全站 Top 50、华为云云享专家认证🏆、iOS逆向公号号主
🍅 简历模板、技术互助。关注我,都给你。
iOS逆向小技能:Cydia Substrate的组成部分、编写Tweak的步骤
「这是我参与2022首次更文挑战的第4天,活动详情查看:2022首次更文挑战」。
前言
Cydia Substrate(以前叫做MobileSubstrate)是一个框架,允许第三方的开发者在系统的方法里打一些运行时补丁,扩展一些方法。
Cydia Substrate由3部分组成:
- MobileHooker
- MobileLoader
- safe mode
I Cydia Substrate]
1.1 MobileHooker
MobileHooker用来替换系统函数,这个过程也叫Hooking。有如下的API可以使用:
IMP MSHookMessage(Class class, SEL selector, IMP replacement, const char* prefix); // prefix should be NULL.
void MSHookMessageEx(Class class, SEL selector, IMP replacement, IMP *result);
void MSHookFunction(void* function, void* replacement, void** p_original);
MSHookMessageEx用来替换Objective-C的函数,MSHookFunction用来替换C/C++函数
1.2 MobileLoader
MobileLoader loads 3rd-party patching code into the running application. MobileLoader will first load itself into the run application using DYLD_INSERT_LIBRARIES environment variable. Then it looks for all dynamic libraries in the directory /Library/MobileSubstrate/DynamicLibraries/, and dlopen them.
控制是否加载到目标程序,是通过一个plist文件来控制的。如果需要被加载的动态库的名称叫做foo.dylib,那么这个plist文件就叫做foo.plist,这个里面有一个字段叫做filter,里面写明需要hook进的目标程序的bundle id。 比如,如果只想要foo.dylib加载进入SpringBoard,那么对应的plist文件中的filter就应该这样写:
Filter = {
Bundles = (com.apple.springboard);
};
1.3 Safe mode
When a extension crashed the SpringBoard, MobileLoader will catch that and put the device into safe mode. In safe mode all 3rd-party extensions will be disabled.
The following signals will invoke safe mode:
SIGABRT
SIGILL
SIGBUS
SIGSEGV
SIGSYS
II 编写Tweak的步骤
-
确定目标:在这个App上编写Tweak实现的特定功能,比如拦截某个具体的应用的特定API调用,获得关键信息。
-
导出头文件:确定目标之后,就可以利用Clutch先破解App,然后利用class-dump-z导出头文件,找到你感兴趣的类,对它进行分析。
-
获得类的方法:有时候,头文件没有所有方法调用的信息,这个时候你可以利用cycript,使用之前介绍的trick,打印出所需的方法信息。
-
编写Tweak:这一步你应该拿到需要Hook的类以及对应的方法,编写并安装与测试。
III SpringBoard 相关的API
- powerDown
+ (void) powerDown {
id SpringBoard = [UIApplication sharedApplication];//#"<SpringBoard: 0x173d8800>"
[SpringBoard powerDown];
}
- relaunchSpringBoard
@interface SpringBoard : UIApplication
\t_uiController (SBUIController*): <SBUIController: 0x1809c510>
- (void)relaunchSpringBoard; [#0x1617ca00 relaunchSpringBoard]
- (void)_relaunchSpringBoardNow;
- (void)powerDown;
- (void)_powerDownNow;
- (void)reboot;
- (void)_rebootNow;
@end
- 自动锁屏
[UIApplication sharedApplication].idleTimerDisabled=YES;//不自动锁屏,放在-(void)viewWillAppear:(BOOL)animated里面的时候,防止失效
[UIApplication sharedApplication].idleTimerDisabled=NO;//自动锁屏
see also
更多内容请关注 #小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向。
iOS设备日志查看工具:syslog、socat
「这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战」。
前言
本文介绍iOS设备日志查看工具syslog、deviceconsole和socat
,如果上述工具都不满意,你也可以使用Mac系统自带的console控制台进行查看。
>
I syslog
1.1 安装syslog
在cydia搜索syslogd to /var/log/syslog安装即可
1.2 syslog用法
syslog是把系统日志写入到/var/log/syslog文件里,用法很简单,执行tail -f /var/log/syslog就能看到了
如果需要过滤某一应用的日志,只需加上grep即可,比如过滤微信
tail -f /var/log/syslog |grep WeChat
II socat
2.1 安装
- 在iOS设备安装
使用 APT 0.6 Transitional 安装socat 几乎所有流行的黑客工具都可以在 BigBoss Recommendation tools这个包中找到 ( APT 0.6 Transitional, Git, GNU Debugger, less, make, unzip, wget 和 SQLite 3.x)
apt-get install socat
如果找不到安装包的时候,运行一下 apt-get update, 获得最新的包列表.
2.2 连接到系统日志的sock文件
socat - UNIX-CONNECT:/var/run/lockdown/syslog.sock
- 进入到命令行交互界面,这时可以输入help查看帮助
iPhone:~ root# socat - UNIX-CONNECT:/var/run/lockdown/syslog.sock
========================
ASL is here to serve you
>
2.3 日志的查看
输入watch查看,输入stop停止
2.4 清除日志文件数据
cat /dev/null >/var/log/syslog
III deviceconsole
自从iOS8之后,我就习惯使用Mac系统自带的console ,后来发现有些同事的Mac中console 版本低,没有device 选项;于是乎,就推荐他们使用deviceconsole
- deviceconsole --help
➜ bin git:(master) ✗ deviceconsole --help Usage: deviceconsole [options] Options: -d Include connect/disconnect messages in standard out -u <udid> Show only logs from a specific device -p <process name> Show only logs from a specific process Control-C to disconnect Mail bug reports and suggestions to <ryan.petrich@medialets.com>
knlog -help
Usage: knlog [options] Options: -i | --case-insensitive Make filters case-insensitive -f | --filter <string> Filter include by single word occurrences (case-sensitive) -x | --exclude <string> Filter exclude by single word occurrences (case-sensitive) -p | --process <string> Filter by process name (case-sensitive) -u | --udid <udid> Show only logs from a specific device -s | --simulator <version> Show logs from iOS Simulator --debug Include connect/disconnect messages in standard out --use-separators Skip a line between each line --force-color Force colored text --message-only Display only level and message Control-C to disconnect
- 编译之后的可执行文件 knlog
see also
更多内容请关注 #小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向。
iOS逆向小技能:Theos的安装
「这是我参与2022首次更文挑战的第7天,活动详情查看:2022首次更文挑战」。
前言
Theos是越狱开发工具包。logos语法简单。,它给我们准备好了一些代码模板、预置一些基本的Makefile脚本,这样我们开发一个tweak就会变得方便的多,
(整合在Xcode的iOSopendev )由于逆向工程很多东西无法自动化,因此推荐Theos
I 安装Thoes
1.1 安装Xcode
安装Xcode,以及command line tools
1.2 下载Thoes
下载Thoes:https://github.com/theos/theos
查看环境变量: 终端中输入命令env
建立环境变量:
export THEOS=/opt/theos
#在当前终端中起作用了,关闭终端后又得重新设置。
为了避免每次都建立这个环境变量,建立一个永久的环境变量 : 编辑~/.profile文件,在其中添加export THEOS=/opt/theos/,这个环境变量就是永久的了.
记得source
source .profile
或者使用.bash_profile
open -e ~/.bash_profile
export THEOS=/opt/theos
source .bash_profile
devzkndeMacBook-Pro:opt devzkn$ git clone --recursive https://github.com/theos/theos.git $THEOS
fatal: could not create work tree dir '/opt/theos': Permission denied
devzkndeMacBook-Pro:opt devzkn$ sudo git clone --recursive https://github.com/theos/theos.git $THEOS
Password:
Cloning into '/opt/theos'...
新版的theos 已经自带CydiaSubstrate.framework(基本上,tweak都依赖于一个名叫cydia Substrate (以前名字也叫mobile Substrate)的动态库,Mobile Substrate是Cydia的作者Jay Freeman (@saurik)的作品,也叫Cydia Substrate,它的主要功能是hook某个App,修改代码比如替换其中方法的实现,Cydia上的tweak都是基于Mobile Substrate实现的.)
devzkndeMacBook-Pro:lib devzkn$ pwd
/opt/theos/vendor/lib
devzkndeMacBook-Pro:lib devzkn$ ls -lrt
total 72
drwxr-xr-x 3 root wheel 102 Aug 10 15:19 libswift
lrwxr-xr-x 1 root wheel 43 Aug 10 15:19 libsubstrate.tbd -> CydiaSubstrate.framework/CydiaSubstrate.tbd
-rw-r--r-- 1 root wheel 635 Aug 10 15:19 librocketbootstrap.tbd
-rw-r--r-- 1 root wheel 392 Aug 10 15:19 libprefs.tbd
-rw-r--r-- 1 root wheel 588 Aug 10 15:19 libflipswitch.tbd
-rw-r--r-- 1 root wheel 1111 Aug 10 15:19 libapplist.tbd
-rw-r--r-- 1 root wheel 5646 Aug 10 15:19 libactivator.tbd
drwxr-xr-x 4 root wheel 136 Aug 10 15:19 TechSupport.framework
-rw-r--r-- 1 root wheel 432 Aug 10 15:19 README.md
drwxr-xr-x 5 root wheel 170 Aug 10 15:19 Opener.framework
-rw-r--r-- 1 root wheel 3342 Aug 10 15:19 LICENSE.md
drwxr-xr-x 5 root wheel 170 Aug 10 15:19 CydiaSubstrate.framework
drwxr-xr-x 4 root wheel 136 Aug 10 15:19 Cycript.framework
drwxr-xr-x 5 root wheel 170 Aug 10 15:19 CepheiPrefs.framework
drwxr-xr-x 4 root wheel 136 Aug 10 15:19 Cephei.framework
1.3 配置ldid
用来专门签名iOS可执行文件的工具,用以在越狱iOS中取代Xcode自带的codesign. 安装这个ldid,推荐的方式是采用brew来安装--
brew install ldid
1.4 dpkg-deb
deb是越狱开发包的标准格式,dpkg-deb是个用于操作deb文件的工具,有了这个工具,Theos才能正确的把工程打包成deb文件.
brew install dpkg
see also
更多内容请关注 #小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向。
iOS逆向小技能:使用substrate及runtime进行hook(定时检测app是否开启)
「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。
前言
- 利用runtime API进行hook
method_exchangeImplementations
可以直接是一个函数地址,不管是OC还是C所有的OC函数都是IMP类型。IMP就是个c函数指针。
- 使用
substrate.h
进行hook - 定时检测app是否处于前台运行状态
I 利用runtime API进行hook
#import <Foundation/Foundation.h>
#import <objc/runtime.h>
@interface KNHook : NSObject
/**
替换对象方法
@param originalClass 原始类
@param originalSelector 原始类的方法
@param swizzledClass 替换类
@param swizzledSelector 替换类的方法
*/
void kn_hookMethod(Class originalClass, SEL originalSelector, Class swizzledClass, SEL swizzledSelector);
/**
替换类方法
@param originalClass 原始类
@param originalSelector 原始类的类方法
@param swizzledClass 替换类
@param swizzledSelector 替换类的类方法
*/
void kn_hookClassMethod(Class originalClass, SEL originalSelector, Class swizzledClass, SEL swizzledSelector);
1.1 替换对象方法
/**
替换对象方法
@param originalClass 原始类
@param originalSelector 原始类的方法
@param swizzledClass 替换类
@param swizzledSelector 替换类的方法
*/
void kn_hookMethod(Class originalClass, SEL originalSelector, Class swizzledClass, SEL swizzledSelector){
Method originalMethod = class_getInstanceMethod(originalClass, originalSelector);
Method swizzledMethod = class_getInstanceMethod(swizzledClass, swizzledSelector);
if(originalMethod && swizzledMethod) {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
1.2 替换类方法
/**
替换类方法
@param originalClass 原始类
@param originalSelector 原始类的类方法
@param swizzledClass 替换类
@param swizzledSelector 替换类的类方法
*/
void kn_hookClassMethod(Class originalClass, SEL originalSelector, Class swizzledClass, SEL swizzledSelector){
Method originalMethod = class_getClassMethod(originalClass, originalSelector);
Method swizzledMethod = class_getClassMethod(swizzledClass, swizzledSelector);
if(originalMethod && swizzledMethod) {
method_exchangeImplementations(originalMethod, swizzledMethod);
}
}
1.3 runtime的使用例子
- hook OnSyncBatchAddMsgs
static void __attribute__((constructor)) initialize(void) {
MSHookMessageEx(objc_getClass("MessageService"), @selector(OnSyncBatchAddMsgs:isFirstSync:), (IMP)&new_MessageService_OnSyncBatchAddMsgs_isFirstSync, (IMP*)&origin_new_MessageService_OnSyncBatchAddMsgs_isFirstSync);
[NSObject hookWeChat];
}
- hook CUtility
#import "NSObject+WeChatHook.h"
@implementation NSObject (WeChatHook)
+ (void)hookWeChat {
kn_hookClassMethod(objc_getClass("CUtility"), @selector(HasWechatInstance), [self class], @selector(hook_HasWechatInstance));
}
#pragma mark - hook 方法
/**
hook 是否已启动
*/
+ (BOOL)hook_HasWechatInstance {
NSLog(@"kn hook_HasWechatInstance");
return NO;
}
@end
1.4 定时检测app是否开启
应用场景:长期保证app一只处于运行中
NSTimer *timer ;
%hook SpringBoard
//applicationDidFinishLaunching
-(void)applicationDidFinishLaunching: (id)application
{
%orig;
timer = [NSTimer scheduledTimerWithTimeInterval:60*2 target:self selector:@selector(checkHeart) userInfo:nil repeats:YES];
}
%new
- (void)checkHeart
{
//定时检测微信是否开启
[[UIApplication sharedApplication] launchApplicationWithIdentifier:@"com.tencent.xin" suspended:0];
}
%end
//qutolock
%hook SBLockScreenViewController
-(void)activate{
%orig;
[[%c(SBLockScreenManager) sharedInstance] unlockUIFromSource:0 withOptions:nil];
}
%end
II 使用substrate.h
进行hook
static void (*origin_new_MessageService_OnSyncBatchAddMsgs_isFirstSync)(MessageService*,SEL,NSArray *,BOOL);
static void new_MessageService_OnSyncBatchAddMsgs_isFirstSync(MessageService* self,SEL _cmd,NSArray * msgs,BOOL isFirstSync){
origin_new_MessageService_OnSyncBatchAddMsgs_isFirstSync(self,_cmd,msgs,isFirstSync);
}
see also
更多内容请关注 #小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向。
iOS小技能: 处理接口的暂无数据
这是我参与11月更文挑战的第9天,活动详情查看:2021最后一次更文挑战。
引言
在日常开发中经常涉及数据列表的查询,处理服务侧无数据返回的情况或者网络异常的手段是iOS必备小技能。
I 处理暂无数据
网络请求失败,业务逻辑错误,返回数据为空都是需要处理界面的显示,推荐使用暂无数据进行提示。
1.1 用法
if (weakSelf.viewModel.listDataArray.count == 0) {
[weakSelf.viewModel.ShowNoviewSubject sendNext:QCTLocal(CRM_nodata_Info)];
}else{
[weakSelf.viewModel.hidenNoviewSubject sendNext:nil];
}
1.2 核心实现
V层初始化暂无数据视图:将视图添加到tableView,这样可以不影响下拉刷新和上拉加载
- (CRMNoDatatView *)NoView{
if (nil == _NoView) {
CRMNoDatatView *tmpView = [[CRMNoDatatView alloc]init];
_NoView = tmpView;
[self.tableView addSubview:_NoView];
__weak __typeof__(self) weakSelf = self;
[_NoView mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerY.equalTo(weakSelf.tableView.mas_centerY).offset(kAdjustRatio(k_noteViewH));
make.width.equalTo(weakSelf);
make.left.right.bottom.equalTo(weakSelf.tableView);//tableView
}];
}
return _NoView;
}
- (void)ShowNoview:(NSString *)title img:(NSString*)imgName
{
self.NoView.title = title;
self.NoView.imgName = imgName;
[self.tableView bringSubviewToFront:self.NoView];
}
V层监听C层的事件
[self.viewModel.hidenNoviewSubject subscribeNext:^(id _Nullable x) {
weakSelf.NoView.hidden = YES;
}];
[self.viewModel.ShowNoviewSubject subscribeNext:^(id _Nullable x) {
weakSelf.NoView.hidden = NO;
[weakSelf ShowNoview:x img:@"img_kongbai_zanwu"];
}];
暂无数据视图的实现
// 显示暂无数据图片
- (UIImageView *)imageV{
if (nil == _imageV) {
UIImageView *tmpView = [[UIImageView alloc]init];
_imageV = tmpView;
_imageV.contentMode = UIViewContentModeScaleAspectFit;
_imageV.image = [UIImage imageNamed:@"icon_wushuju"];
[self addSubview:_imageV];
__weak __typeof__(self) weakSelf = self;
[_imageV mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(weakSelf);
make.centerY.equalTo(weakSelf).offset(-kAdjustRatio(35));
make.left.equalTo(weakSelf).offset(kAdjustRatio(33));
make.right.equalTo(weakSelf).offset(kAdjustRatio(-33));
}];
}
return _imageV;
}
//显示暂无数据文本
- (UILabel *)label{
if (nil == _label) {
UILabel *tmpView = [[UILabel alloc]init];
_label = tmpView;
[self addSubview:_label];
__weak __typeof__(self) weakSelf = self;
[_label mas_makeConstraints:^(MASConstraintMaker *make) {
make.centerX.equalTo(weakSelf);
make.top.equalTo(weakSelf.imageV.mas_bottom).offset(kAdjustRatio(22));
_label.textAlignment = NSTextAlignmentCenter;
_label.font = kPingFangFont(15);
_label.textColor = rgb(51,51,51);
}
return _label;
}
// 更新图片数据
-(void)setImgName:(NSString *)imgName{
_imgName = imgName;
if (imgName.length<=0) {
return;
}
[self.imageV setImage:[UIImage imageNamed:imgName]];
self.reloadbtnView.hidden = !self.isShowReloadBtn;
// }
}
- (void)setTitle:(NSString *)title{
_title = title;
self.label.text = title;
}
see also
更多内容请关注 #小程序:iOS逆向
,只为你呈现有价值的信息,专注于移动端技术研究领域。
iOS小技能:视图置顶(让一个View至于最顶端, 避免被其他子视图遮盖住)
这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战。
前言
视图置顶的应用场景:
- 比如让日期控件置于窗口的最顶层
- 悬浮按钮(支持拖曳)
关于 bringSubviewToFront 和view.layer.zPosition的选择
1、使用bringSubviewToFront方法需要在重新刷新界面结构层次的时候调用;
2、使用view.layer.zPosition方法会获取不到view的点击事件
更多内容请关注#小程序:iOS逆向
,只为你呈现有价值的信息,专注于移动端技术研究领域。
I 视图置顶
1.1 方案一:bringSubviewToFront的用法
- 让日期控件置于窗口的最顶层
PGDatePickManager kunnan.blog.csdn.net/article/det…
@implementation PGDatePickManager (ios12)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
NSArray *selStringsArray = @[@"viewWillLayoutSubviews"];
// @"reloadRowsAtIndexPaths:withRowAnimation:", @"deleteRowsAtIndexPaths:withRowAnimation:", @"insertRowsAtIndexPaths:withRowAnimation:"];
[selStringsArray enumerateObjectsUsingBlock:^(NSString *selString, NSUInteger idx, BOOL *stop) {
NSString *mySelString = [@"sd_" stringByAppendingString:selString];
Method originalMethod = class_getInstanceMethod(self, NSSelectorFromString(selString));
Method myMethod = class_getInstanceMethod(self, NSSelectorFromString(mySelString));
method_exchangeImplementations(originalMethod, myMethod);
}];
});
}
- (void)sd_viewWillLayoutSubviews{
[self sd_viewWillLayoutSubviews];
[UIApplication.sharedApplication.delegate.window bringSubviewToFront:self.view.superview];
}
- listTableView
[self.superview.window addSubview:self.listTableView];
/// 避免被其他子视图遮盖住
[self.superview.window bringSubviewToFront:self.listTableView];
CGRect frame = CGRectMake(CGRectGetMinX(self.frame), CGRectGetMaxY(self.frame), CGRectGetWidth(self.frame), 0);
//坐标转换
CGRect convertRect= [self.superview convertRect:frame toView:self.superview.window];
[self.listTableView setFrame:convertRect];
1.2 方案二:同级Layer改变显示顺序
- self.view.layer.zPosition
self.view.layer.zPosition = MAXFLOAT; 999
II 案例: 悬浮按钮(支持拖曳)
下级订货单关于悬浮按钮的相关需求:
1、存在“待发货”记录时,显示“一键发货”按钮 点击一键发货:实现待发货的分配记录,都更新为待收货 2、存在“待收货”记录时,显示“一键代收货”按钮 点击一键代收货:实现待发货的分配记录,都更新为“已收货”
2.1 原理
1 、bringSubviewToFront 2、添加移动手势可以拖动 3、使用谓词进行判断是否存在特定条件的数据
//添加移动手势可以拖动
self.panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(dragAction:)];
self.panGestureRecognizer.minimumNumberOfTouches = 1;
self.panGestureRecognizer.maximumNumberOfTouches = 1;
self.panGestureRecognizer.delegate = self;
[self addGestureRecognizer:self.panGestureRecognizer];
2.2 用法
@property (strong, nonatomic) KNFrontV * orangeView;
@end
@implementation QCTRecordViewController
- (void)viewDidLayoutSubviews {
[super viewDidLayoutSubviews];
[self.view bringSubviewToFront:self.orangeView];
[self.orangeView layoutIfNeeded];
self.orangeView.layer.cornerRadius =self.orangeView.height *0.5;
}
- (KNFrontV *)orangeView{
if (nil == _orangeView) {
KNFrontV *tmpView = [[KNFrontV alloc] initWithFrame:CGRectMake(0, 0 , kAdjustRatio(53), kAdjustRatio(53))];
_orangeView = tmpView;
[self.view addSubview:_orangeView];
__weak __typeof__(self) weakSelf = self;
tmpView.button.titleLabel.numberOfLines = 0;
tmpView.button.titleLabel.textAlignment = NSTextAlignmentCenter;
tmpView.button.titleLabel.font = [UIFont systemFontOfSize:15.0];
[tmpView.button setTitle:@"一键\n发货" forState:UIControlStateNormal];// 发货 购买\n开店数
tmpView.backgroundColor = rgb(255,54,87);
//
// tmpView.layer.cornerRadius = 14;// layoutsubview
//设置显示图片方式一:
// tmpView.imageView.image = [UIImage imageNamed:@"icon_dayin"];
//设置显示图片方式二:
// [logoView.button setBackgroundImage:[UIImage imageNamed:@"logo1024"] forState:UIControlStateNormal];
[_orangeView mas_makeConstraints:^(MASConstraintMaker *make) {
make.size.mas_equalTo(CGSizeMake(kAdjustRatio(53), kAdjustRatio(53)));
make.right.offset(kAdjustRatio(-20));
make.bottom.offset(kAdjustRatio(-90));
}];
tmpView.clickDragViewBlock = ^(KNFrontV *dragView){
[weakSelf setupclickDragViewBlock];
};
}
return _orangeView;
}
- (void)setupclickDragViewBlock{
}
- KNFrontV的定义
//
// KNFrontV.h
// Housekeeper
//
// Created by mac on 2021/5/6.
// Copyright © 2021 https://kunnan.blog.csdn.net/ . All rights reserved.
//
#import <UIKit/UIKit.h>
NS_ASSUME_NONNULL_BEGIN
// 拖曳view的方向
typedef NS_ENUM(NSInteger, KNDragDirection) {
KNDragDirectionAny, /**< 任意方向 */
KNDragDirectionHorizontal, /**< 水平方向 */
KNDragDirectionVertical, /**< 垂直方向 */
};
@interface KNFrontV : UIView
/**
是不是能拖曳,默认为YES
YES,能拖曳
NO,不能拖曳
*/
@property (nonatomic,assign) BOOL dragEnable;
/**
活动范围,默认为父视图的frame范围内(因为拖出父视图后无法点击,也没意义)
如果设置了,则会在给定的范围内活动
如果没设置,则会在父视图范围内活动
注意:设置的frame不要大于父视图范围
注意:设置的frame为0,0,0,0表示活动的范围为默认的父视图frame,如果想要不能活动,请设置dragEnable这个属性为NO
*/
@property (nonatomic,assign) CGRect freeRect;
/**
拖曳的方向,默认为any,任意方向
*/
@property (nonatomic,assign) KNDragDirection dragDirection;
/**
contentView内部懒加载的一个UIImageView
开发者也可以自定义控件添加到本view中
注意:最好不要同时使用内部的imageView和button
*/
@property (nonatomic,strong) UIImageView *imageView;
/**
contentView内部懒加载的一个UIButton
开发者也可以自定义控件添加到本view中
注意:最好不要同时使用内部的imageView和button
*/
@property (nonatomic,strong) UIButton *button;
/**
是不是总保持在父视图边界,默认为NO,没有黏贴边界效果
isKeepBounds = YES,它将自动黏贴边界,而且是最近的边界
isKeepBounds = NO, 它将不会黏贴在边界,它是free(自由)状态,跟随手指到任意位置,但是也不可以拖出给定的范围frame
*/
@property (nonatomic,assign) BOOL isKeepBounds;
/**
点击的回调block
*/
@property (nonatomic,copy) void(^clickDragViewBlock)(KNFrontV *dragView);
/**
开始拖动的回调block
*/
@property (nonatomic,copy) void(^beginDragBlock)(KNFrontV *dragView);
/**
拖动中的回调block
*/
@property (nonatomic,copy) void(^duringDragBlock)(KNFrontV *dragView);
/**
结束拖动的回调block
*/
@property (nonatomic,copy) void(^endDragBlock)(KNFrontV *dragView);
@end
NS_ASSUME_NONNULL_END
- KNFrontV的实现
//
// KNFrontV.m
// Housekeeper
//
// Created by mac on 2021/5/6.
// Copyright © 2021 https://kunnan.blog.csdn.net/ . All rights reserved.
//
#import "KNFrontV.h"
@interface KNFrontV ()
@property (nonatomic,strong) UIView *contentViewForDrag;
/**
内容view,命名为contentViewForDrag,因为很多其他开源的第三方的库,里面同样有contentView这个属性
,这里特意命名为contentViewForDrag以防止冲突
*/
@property (nonatomic,assign) CGPoint startPoint;
@property (nonatomic,strong) UIPanGestureRecognizer *panGestureRecognizer;
@property (nonatomic,assign) CGFloat previousScale;
@end
@implementation KNFrontV
-(UIImageView *)imageView{
if (_imageView==nil) {
_imageView = [[UIImageView alloc]init];
_imageView.userInteractionEnabled = YES;
_imageView.clipsToBounds = YES;
[self.contentViewForDrag addSubview:_imageView];
}
return _imageView;
}
-(UIButton *)button{
if (_button==nil) {
_button = [UIButton buttonWithType:UIButtonTypeCustom];
_button.clipsToBounds = YES;
_button.userInteractionEnabled = NO;
[self.contentViewForDrag addSubview:_button];
}
return _button;
}
-(UIView *)contentViewForDrag{
if (_contentViewForDrag==nil) {
_contentViewForDrag = [[UIView alloc]init];
_contentViewForDrag.clipsToBounds = YES;
}
return _contentViewForDrag;
}
- (instancetype)initWithFrame:(CGRect)frame
{
self = [super initWithFrame:frame];
if (self) {
[self addSubview:self.contentViewForDrag];
[self setUp];
}
return self;
}
- (instancetype)initWithCoder:(NSCoder *)coder
{
self = [super initWithCoder:coder];
if (self) {
[self setUp];
}
return self;
}
-(void)layoutSubviews{
[super layoutSubviews];
if (self.freeRect.origin.x!=0||self.freeRect.origin.y!=0||self.freeRect.size.height!=0||self.freeRect.size.width!=0) {
//设置了freeRect--活动范围
}else{
//没有设置freeRect--活动范围,则设置默认的活动范围为父视图的frame
self.freeRect = (CGRect){CGPointZero,self.superview.bounds.size};
}
_imageView.frame = (CGRect){CGPointZero,self.bounds.size};
_button.frame = (CGRect){CGPointZero,self.bounds.size};
self.contentViewForDrag.frame = (CGRect){CGPointZero,self.bounds.size};
}
-(void)setUp{
self.dragEnable = YES;//默认可以拖曳
self.clipsToBounds = YES;
self.isKeepBounds = NO;
self.backgroundColor = [UIColor lightGrayColor];
UITapGestureRecognizer *singleTap = [[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(clickDragView)];
[self addGestureRecognizer:singleTap];
//添加移动手势可以拖动
self.panGestureRecognizer = [[UIPanGestureRecognizer alloc] initWithTarget:self action:@selector(dragAction:)];
self.panGestureRecognizer.minimumNumberOfTouches = 1;
self.panGestureRecognizer.maximumNumberOfTouches = 1;
self.panGestureRecognizer.delegate = self;
[self addGestureRecognizer:self.panGestureRecognizer];
}
//-(BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer{
// return self.dragEnable;
//}
/**
拖动事件
@param pan 拖动手势
*/
-(void)dragAction:(UIPanGestureRecognizer *)pan{
if(self.dragEnable==NO)return;
switch (pan.state) {
case UIGestureRecognizerStateBegan:{//开始拖动
if (self.beginDragBlock) {
self.beginDragBlock(self);
}
//注意完成移动后,将translation重置为0十分重要。否则translation每次都会叠加
[pan setTranslation:CGPointZero inView:self];
//保存触摸起始点位置
self.startPoint = [pan translationInView:self];
break;
}
case UIGestureRecognizerStateChanged:{//拖动中
//计算位移 = 当前位置 - 起始位置
if (self.duringDragBlock) {
self.duringDragBlock(self);
}
CGPoint point = [pan translationInView:self];
float dx;
float dy;
switch (self.dragDirection) {
case WMDragDirectionAny:
dx = point.x - self.startPoint.x;
dy = point.y - self.startPoint.y;
break;
case WMDragDirectionHorizontal:
dx = point.x - self.startPoint.x;
dy = 0;
break;
case WMDragDirectionVertical:
dx = 0;
dy = point.y - self.startPoint.y;
break;
default:
dx = point.x - self.startPoint.x;
dy = point.y - self.startPoint.y;
break;
}
//计算移动后的view中心点
CGPoint newCenter = CGPointMake(self.center.x + dx, self.center.y + dy);
//移动view
self.center = newCenter;
// 注意完成上述移动后,将translation重置为0十分重要。否则translation每次都会叠加
[pan setTranslation:CGPointZero inView:self];
break;
}
case UIGestureRecognizerStateEnded:{//拖动结束
[self keepBounds];
if (self.endDragBlock) {
self.endDragBlock(self);
}
break;
}
default:
break;
}
}
//点击事件
-(void)clickDragView{
if (self.clickDragViewBlock) {
self.clickDragViewBlock(self);
}
}
//黏贴边界效果
- (void)keepBounds{
//中心点判断
float centerX = self.freeRect.origin.x+(self.freeRect.size.width - self.frame.size.width)/2;
CGRect rect = self.frame;
if (self.isKeepBounds==NO) {//没有黏贴边界的效果
if (self.frame.origin.x < self.freeRect.origin.x) {
CGContextRef context = UIGraphicsGetCurrentContext();
[UIView beginAnimations:@"leftMove" context:context];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:0.5];
rect.origin.x = self.freeRect.origin.x;
self.frame = rect;
[UIView commitAnimations];
} else if(self.freeRect.origin.x+self.freeRect.size.width < self.frame.origin.x+self.frame.size.width){
CGContextRef context = UIGraphicsGetCurrentContext();
[UIView beginAnimations:@"rightMove" context:context];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:0.5];
rect.origin.x = self.freeRect.origin.x+self.freeRect.size.width-self.frame.size.width;
self.frame = rect;
[UIView commitAnimations];
}
}else if(self.isKeepBounds==YES){//自动粘边
if (self.frame.origin.x< centerX) {
CGContextRef context = UIGraphicsGetCurrentContext();
[UIView beginAnimations:@"leftMove" context:context];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:0.5];
rect.origin.x = self.freeRect.origin.x;
self.frame = rect;
[UIView commitAnimations];
} else {
CGContextRef context = UIGraphicsGetCurrentContext();
[UIView beginAnimations:@"rightMove" context:context];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:0.5];
rect.origin.x =self.freeRect.origin.x+self.freeRect.size.width - self.frame.size.width;
self.frame = rect;
[UIView commitAnimations];
}
}
if (self.frame.origin.y < self.freeRect.origin.y) {
CGContextRef context = UIGraphicsGetCurrentContext();
[UIView beginAnimations:@"topMove" context:context];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:0.5];
rect.origin.y = self.freeRect.origin.y;
self.frame = rect;
[UIView commitAnimations];
} else if(self.freeRect.origin.y+self.freeRect.size.height< self.frame.origin.y+self.frame.size.height){
CGContextRef context = UIGraphicsGetCurrentContext();
[UIView beginAnimations:@"bottomMove" context:context];
[UIView setAnimationCurve:UIViewAnimationCurveEaseInOut];
[UIView setAnimationDuration:0.5];
rect.origin.y = self.freeRect.origin.y+self.freeRect.size.height-self.frame.size.height;
self.frame = rect;
[UIView commitAnimations];
}
}
@end
2.3 使用 NSPredicate
判断是否存在“待收货”记录
/**
下级订货单
1、存在“待发货”记录时,显示“一键发货”按钮
点击一键发货:实现待发货的分配记录,都更新为待收货
2、存在“待收货”记录时,显示“一键代收货”按钮
点击一键代收货:实现待发货的分配记录,都更新为“已收货”
我的订货单
存在“待收货”记录时,显示“一键收货”按钮
点击一键收货:实现待发货的分配记录,都更新为“已收货”
*/
- (void) updateorangeView{
//
if(![self isShoworangeView]){
self.orangeView.hidden = YES;
}else{
[self orangeView];
self.orangeView.hidden = NO;
[self.orangeView.button setTitle:self.orangeViewM.showStr forState:UIControlStateNormal];// 发货 购买\n开店数
}
}
- (BOOL)isShoworangeView{
self.orangeViewM = [KNFrontVM new];
if(self.model.isLowerOrder){// 下级
// 1、存在“待发货”记录时,显示“一键发货”按钮// 优先显示
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"receivingState == %@", @"0"];
NSArray *arFiltered = [ self.Detailmodels filteredArrayUsingPredicate:predicate];//以一定的条件(特定日期)过滤maTemp数组,即进行大数据搜索。
if(arFiltered.count>0){
self.orangeViewM.isShow = YES;
self.orangeViewM.showStr = @"一键\n发货";
self.orangeViewM.type = ReceivingDelieverEnum4Deliever;
return self.orangeViewM.isShow;
}
// 2、存在“待收货”记录时,显示“一键代收货”按钮
predicate = [NSPredicate predicateWithFormat:@"receivingState == %@", @"1"];
arFiltered = [ self.Detailmodels filteredArrayUsingPredicate:predicate];//
if(arFiltered.count>0){
self.orangeViewM.isShow = YES;
self.orangeViewM.showStr = @"一键\n代收货";
self.orangeViewM.type = ReceivingDelieverEnum4ProReceiving;
}
}else{// 本级
// 存在“待收货”记录时,显示“一键收货”按钮
NSPredicate* predicate = [NSPredicate predicateWithFormat:@"receivingState == %@", @"1"];
NSArray *arFiltered = [ self.Detailmodels filteredArrayUsingPredicate:predicate];//以一定的条件(特定日期)过滤maTemp数组,即进行大数据搜索。
if(arFiltered.count>0){
self.orangeViewM.isShow = YES;
self.orangeViewM.showStr = @"一键\n收货";
self.orangeViewM.type = ReceivingDelieverEnum4Receiving;
}
}
return self.orangeViewM.isShow;
}
see also
更多内容请关注#小程序:iOS逆向
,只为你呈现有价值的信息,专注于移动端技术研究领域。
iOS视图置顶的应用:适配iOS12系统上日期控件被筛选视图遮挡问题
- 推荐使用
[[UIApplication sharedApplication].delegate window]
获取window
在执行
didFinishLaunchingWithOptions:
这个代理方法时,调用[self.window makeKeyAndVisible]
;方法之前,通过[UIApplication sharedApplication].keyWindow 方法获取不到window, 但是无论何时都能获取到delegate.window。
在获取到window时最好使用[[UIApplication sharedApplication].delegate window]获取window
不要在keywindow为nil的时候给window上添加代码,例如添加弹窗。
iOS15适配本地通知功能
这是我参与11月更文挑战的第3天,活动详情查看:2021最后一次更文挑战。
引言
粉丝福利:搜索#小程序:iOS逆向
,关注公众号:iOS逆向
领取福利【掘金小册5折优惠码】
一年一度的iOS 系统 API适配来了,9 月 14 日起 App Store Connect 已经开放 iOS 15 和 iPadOS 15 App 的提交,同时苹果宣布自 2022 年 4 月起,所有提交至 App Store 的 iOS 和 iPadOS app 都必须使用 Xcode 13 和 iOS 15 SDK 构建。
Xcode 13 正式版包含 iOS 15,iPadOS 15,tvOS 15,watchOS 8 以及 macOS Big Sur 11.3 SDK。
Xcode 13 需在 macOS 11.3 及以上版本运行
,支持 iOS 9,tvOS 9,watchOS 2 及以上系统设备调试;也正式支持了 Vim。
Xcode 13 Release Notes: developer.apple.com/documentati…
I 消息推送
推送新特性: iOS15以上的新增属性 interruptionLevel为枚UNNotificationInterruptionLevel
1.1 本地推送适配
需求:利用本地推送实现消息的语音播报 问题:iOS12.1之后利用本地推送实现消息的语音播报,在iOS15 没有声音。kunnan.blog.csdn.net/article/det…
原因: iOS15本地推送新增了中断级别属性 interruptionLevel
,对通知进行了分级。而且通知的内容不能为空。
方案:使用非Passive的中断级别进行本地通知才会有声音,且本地推送一定要有内容,即body不能为空。content.body = @" 不能为空";
UNUserNotificationCenter *center = [UNUserNotificationCenter currentNotificationCenter];
UNMutableNotificationContent *content = [[UNMutableNotificationContent alloc] init];
content.title = @"";
content.subtitle = @"";
content.sound = [UNNotificationSound soundNamed:fileName];
content.badge = @(1);
if (@available(iOS 15.0, *)) {
content.interruptionLevel = UNNotificationInterruptionLevelTimeSensitive;//会使手机亮屏且会播放声音;可能会在免打扰模式(焦点模式)下展示
// @"{\"aps\":{\"interruption-level\":\"time-sensitive\"}}";
// @"{\"aps\":{\"interruption-level\":\"active\"}}";
content.body = @" ";// 本地推送一定要有内容,即body不能为空。
}else{
content.body = @"";
}
UNTimeIntervalNotificationTrigger *trigger = [UNTimeIntervalNotificationTrigger triggerWithTimeInterval:0.001 repeats:NO];
// 添加通知的标识符,可以用于移除,更新等操作
NSString *identifier = [NSString stringWithFormat:@"localPushId%lld", (long long)[[NSDate date] timeIntervalSince1970]];
UNNotificationRequest *request = [UNNotificationRequest requestWithIdentifier:identifier content:content trigger:trigger];
[center addNotificationRequest:request withCompletionHandler:^(NSError *_Nullable error) {
CGFloat waitTime = [self timeForAudioFileWithFileName:fileName];
// CGFloat waitTime = 0.3;
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(waitTime * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[self localNotificationPushNextFile];
});
}];
中断级别目前分为四种:
typedef NS_ENUM(NSUInteger, UNNotificationInterruptionLevel) {
// Added to the notification list; does not light up screen or play sound
UNNotificationInterruptionLevelPassive,
// Presented immediately; Lights up screen and may play a sound
UNNotificationInterruptionLevelActive,
// Presented immediately; Lights up screen and may play a sound; May be presented during Do Not Disturb
UNNotificationInterruptionLevelTimeSensitive,
// Presented immediately; Lights up screen and plays sound; Always presented during Do Not Disturb; Bypasses mute switch; Includes default critical alert sound if no sound provided
UNNotificationInterruptionLevelCritical,
} API_AVAILABLE(macos(12.0), ios(15.0), watchos(8.0), tvos(15.0));
- Passive:被动类型的通知不会使手机亮屏并且不会播放声音。
- Active: 活动类型的通知会使手机亮屏且会播放声音,为默认类型。
- Time Sensitive(时间敏感):会使手机亮屏且会播放声音;可能会在免打扰模式(焦点模式)下展示。
设置推送通知数据: 时间敏感的中断级别可以使用“interruption-level” payload key:
{"aps":{"interruption-level":"time-sensitive"}}
时效性通知开发者无法直接使用,需要配置对应的权限: a. xcode 开启对应能力
b. 开发者后台配置appID支持该权限(通过Xcode开启对应能力通常会自动添加)
- Critical(关键):会立刻展示,亮屏,播放声音,无效免打扰模式,并且能够绕过静音,如果没有设置声音则会使用一种默认的声音。
适用于地震等紧急情况,需要特殊申请。
判断是否有时间敏感权限 @property(readonly, nonatomic) UNNotificationSetting timeSensitiveSetting;
,如果没有需要提示用户开启。
UNNotificationSetting
typedef NS_ENUM(NSInteger, UNNotificationSetting) {
// The application does not support this notification type
UNNotificationSettingNotSupported = 0,
// The notification setting is turned off.
UNNotificationSettingDisabled,
// The notification setting is turned on.
UNNotificationSettingEnabled,
} API_AVAILABLE(macos(10.14), ios(10.0), watchos(3.0), tvos(10.0));
1.2 测试
开发者想打ad hot 的话,需要能访问云端管理的分发证书。
可以使用极光的网页端或者接口进行测试,或者使用smart push。
1.3 升级JPush iOS SDK
v4.4.0: pod 'JPush' , '4.4.0'
更新时间:2021-10-28
Change Log:
SDK适配ios15系统的本地通知功能
富媒体横屏异常兼容性处理
错误信息: ld: library not found for -ljcore-ios-2.3.4
原因:other linker flags 的信息没有自动更新
解决方案:直接删除other linker flags的jcore信息即可
see also
更多内容请关注#小程序:iOS逆向
,只为你呈现有价值的信息,专注于移动端技术研究领域。
iOS接入开屏广告教程 : 以腾讯优量汇为案例
这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战。
引言
本文以对接开屏广告
为例子
广告类型 | 接入方式 | 简介 | 适用场景 | 版本备注 |
---|---|---|---|---|
开屏广告 | SDK原生渲染 | 开屏广告以App启动作为曝光时机,提供5s的可感知广告展示。用户可以点击广告跳转到目标页面;或者点击右上角的“跳过”按钮,跳转到app内容首页。 开屏V+广告是一个5s-30s的视频广告,在5s开屏呈现的过程中,用户点击右上角的“进入首页”或5s曝光结束后,视频均将收缩到APP内右下角的小视窗继续播放。 | APP启动时 | 包含 开屏 与 开屏V+ 两种 |
IOS对接文档:
ios对接SDK包:对接demo 工程
OC 版本:github.com/zhangkn/GDT…,更多资源请关注
公众号:iOS技能
优量汇 iOS14 适配指南
developers.adnet.qq.com/doc/ios/uni… 前提使用优量汇iOS SDK 4.12.5及以上版本
本文重点是拉取最新SDK库、iOS14的idfa适配以及iOS13的modal样式适配
I 、 SDK部署
developers.adnet.qq.com/doc/ios/gui…
1.1 术语介绍
APPID:媒体 ID,是您在腾讯优量汇开发者平台创建媒体时获得的ID,这个ID是我们在广告网络中识别您应用的唯一ID。
PlacementId:广告位 ID,是您在腾讯优量汇开发者平台为您的应用所创建的某种类型(Banner、开屏、插屏、平台模板、激励视频)的广告位置的ID。
dto(Data Transfer Objects): 接口返回的原始数据
权限申请
部分广告样式的接入需要权限,您可以联系腾讯优量汇运营进行了解和权限申请。在腾讯优量汇开发者平台新建广告位时您只能看到您有相应权限的广告位类型。目前有专门的优量汇代理商负责运营和商务。
说明:针对单媒体的用户,允许获取idfa和定位权限的,投放定向广告;不允许获取权限的用户,投放通投广告,媒体可以选择是否把idfa和定位数据提供给优量汇,并承担相应广告填充和eCPM单价下降损失的结果。
未在优量汇注册,请注册加入优量汇或者申请成为运营者
1.2 拉取最新SDK库
pod 'GDTMobSDK'
#-> Installing GDTMobSDK 4.12.90 (was 4.11.11)
使用pod update GDTMobSDK --verbose
拉取最新库,否则无法更新成功SDK到项目
➜ retail git:(develop) ✗ pod update GDTMobSDK --verbose
1.3 接入注意事项
目前开屏广告只针对iPhone设备在垂直方向上展示。
- 开屏全屏广告需使得显示区域其高度与设备高度一致,即为开屏全屏广告。开发者可通过以下接口配合使用提供媒体logo,用以随开屏广告展示。
logo 推荐使用透明背景色,可为空
- (void)loadFullScreenAd;
- (void)showFullScreenAdInWindow:(UIWindow *)window withLogoImage:(UIImage *)logoImage skipView:(UIView *)skipView;
-
开屏半屏广告的显示区域其高度一定要大于设备高度的75%(建议值大于80%),最小高度要大于400dp,开屏广告默认只在竖屏展示,横屏一般不满足尺寸要求。
-
优量汇开屏广告支持预加载开屏广告,调用方法如下:
GDTSplashAd *preloadSplashAd = [[GDTSplashAd alloc] initWithPlacementId:YOUR_PLACEMENT_ID];
[preloadSplashAd preloadSplashOrderWithPlacementId:YOUR_PLACEMENT_ID];
- 初始化SDK,加载广告的代码推荐放在
didFinishLaunchingWithOptions
的第一行
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
// Override point for customization after application launch.
[self setupGDTSDKConfig];
....
return YES;
}
- 由于SDK的静态库文件libGDTMobSDK.a>110M, 提交到git 仓库时可能由于文件大小的限制导致提交失败,所以推荐你使用.gitignore 将Pods目录忽略。其他同事拉取代码之后,可采用
pod install --verbose --no-repo-update
只安装新添加的库,已更新的库忽略。或者更新指定的库,其它库忽略pod update 库名 --verbose --no-repo-update
# Pods/
1.4 权限适配
针对单媒体的用户,允许获取idfa和定位权限的,投放定向广告;不允许获取权限的用户,投放通投广告,媒体可以选择是否把idfa和定位数据提供给优量汇,并承担相应广告填充和eCPM单价下降损失的结果。
idfa的适配请看本文的第三章节
GPS信息获取开关
在已获得GPS权限的前提下,媒体可以选择是否在广告中获取用户的GPS信息,以便获取定向广告。方法如下:
#import "GDTSDKConfig.h"
[GDTSDKConfig enableGPS:YES]; // 获取用户的GPS信息,默认值为NO
II 、 接入代码示例
2.1 在AppDelegate头文件中导入头文件并声明实例
#import "GDTSplashAd.h"
@interface AppDelegate : UIResponder <UIApplicationDelegate,GDTSplashAdDelegate>
@property (strong, nonatomic) UIWindow *window;
@property (strong, nonatomic) GDTSplashAd *splash;
@property (retain, nonatomic) UIView *bottomView;
@end
2.2 初始化并加载广告数据
在AppDelegate的实现文件中初始化并加载广告数据,开屏广告目前支持全屏开屏和半屏开屏广告两种形式,其中半屏开屏广告支持开发者自定义设置开屏底部的界面,用以展示应用Logo等。
- 注册媒体ID
BOOL result = [GDTSDKConfig registerAppId:@"xxx"];//
if (result) {
[self setupGDTSplashAd];
NSLog(@"注册成功");
}
- 初始化开屏广告位ID
GDTSplashAd *splash = [[GDTSplashAd alloc] initWithPlacementId:@"6806"];
- 先拉取(LoadAd),再手动调用显示(splash show)
// splash LoadAd 逻辑
GDTSplashAd *splash = [[GDTSplashAd alloc] initWithPlacementId:YOUR_PLACEMENT_ID];
splash.delegate = self; //设置代理
//根据iPhone设备不同设置不同背景图
if ([[UIScreen mainScreen] bounds].size.height >= 568.0f) {
splash.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"LaunchImage-568h"]];
} else {
splash.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"LaunchImage"]];
}
splash.fetchDelay = 3; //开发者可以设置开屏拉取时间,超时则放弃展示
[splashAd loadFullScreenAd];
拉取成功之后手动调用来显示广告
#pragma mark - GDTSplashAdDelegate
- (void)splashAdDidLoad:(GDTSplashAd *)splashAd {
// if (splashAd.splashZoomOutView) {
// [self.view addSubview:splashAd.splashZoomOutView];
// splashAd.splashZoomOutView.rootViewController = self;
// // 支持拖拽
// [splashAd.splashZoomOutView supportDrag];
// }
NSLog(@"%s", __func__);
NSString *text = [NSString stringWithFormat:@"%@ 广告拉取成功", splashAd.adNetworkName];
NSLog(@"ecpm:%ld ecpmLevel:%@ text:%@", splashAd.eCPM, splashAd.eCPMLevel,text);
// splash show逻辑
//设置开屏自定义 logo,展示半屏开屏广告
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
[self.splash showFullScreenAdInWindow:window withLogoImage:[UIImage imageNamed:@"img_login_logo"] skipView:nil];
}
- (void)splashAdSuccessPresentScreen:(GDTSplashAd *)splashAd
{
NSLog(@"%s",__FUNCTION__);
// self.tipsLabel.text = ;
NSLog(@"广告展示成功");
}
设置自定义跳过按钮
UIView *customSkipView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, 100, 50)]; // 设置跳过按钮的frame信息
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
[self.splashAd showAdInWindow:window withBottomView:self.bottomView skipView:customSkipView];
通过backgroundImage
根据iPhone设备不同设置不同背景图
需要iPhone 8 Plus@2x.png、iPhoneX@2x.png、iPhone4@1x_2.png,以及启动页的SplashLogo.png
UIImage *splashImage = [UIImage imageNamed:@"SplashNormal"];
if (isIPhoneXSeries()) {
splashImage = [UIImage imageNamed:@"SplashX"];
} else if ([UIScreen mainScreen].bounds.size.height == 480) {
splashImage = [UIImage imageNamed:@"SplashSmall"];
}
self.splashAd.needZoomOut = self.supportZoomoutViewSwitch.isOn;
self.splashAd.backgroundImage = splashImage;
self.splashAd.backgroundImage.accessibilityIdentifier = @"splash_ad";
2.3 开屏广告Demo
demo 中的广告案例
self.demoArray = [@[
@[@"自渲染2.0", @"UnifiedNativeAdViewController"],
@[@"开屏广告", @"SplashViewController"],
@[@"原生模板广告", @"NativeExpressAdViewController"],
@[@"原生视频模板广告", @"NativeExpressVideoAdViewController"],
@[@"激励视频广告", @"RewardVideoViewController"],
@[@"HybridAd", @"HybridAdViewController"],
@[@"Banner2.0", @"UnifiedBannerViewController"],
@[@"插屏2.0", @"UnifiedInterstitialViewController"],
@[@"插屏2.0全屏", @"UnifiedInterstitialFullScreenVideoViewController"],
@[@"获取IDFA", @(1)],
@[@"试玩广告调试", @"PlayableAdTestViewController"],
] mutableCopy];
#import "SplashViewController.h"
注册媒体ID API
/**
SDK 注册接口,请在 app 初始化时调用。
@param appId - 媒体ID
@return 注册是否成功。
*/
+ (BOOL)registerAppId:(NSString *)appId;
/**
* 开屏广告的背景图片
* 可以设置背景图片作为开屏加载时的默认背景
*/
@property (nonatomic, strong) UIImage *backgroundImage;
/**
* 开屏广告的背景色
* 可以设置开屏图片来作为开屏加载时的默认图片
*/
@property (nonatomic, copy) UIColor *backgroundColor;
/**
* 发起拉取全屏广告请求,只拉取不展示
* 详解:广告素材及广告图片拉取成功后会回调splashAdDidLoad方法,当拉取失败时会回调splashAdFailToPresent方法
*/
- (void)loadFullScreenAd;
/**
* 展示全屏广告,调用此方法前需调用isAdValid方法判断广告素材是否有效
* 详解:广告展示成功时会回调splashAdSuccessPresentScreen方法,展示失败时会回调splashAdFailToPresent方法
*/
- (void)showFullScreenAdInWindow:(UIWindow *)window withLogoImage:(UIImage *)logoImage skipView:(UIView *)skipView;
/**
* 发起拉取广告请求,只拉取不展示
* 详解:广告素材及广告图片拉取成功后会回调splashAdDidLoad方法,当拉取失败时会回调splashAdFailToPresent方法
*/
- (void)loadAd;
/**
* 展示广告,调用此方法前需调用isAdValid方法判断广告素材是否有效
* 详解:广告展示成功时会回调splashAdSuccessPresentScreen方法,展示失败时会回调splashAdFailToPresent方法
*/
- (void)showAdInWindow:(UIWindow *)window withBottomView:(UIView *)bottomView skipView:(UIView *)skipView;
/**
* 返回广告是否可展示
* 对于并行请求,在调用showAdInWindow前时需判断下
* @return 当广告已经加载完成且未曝光时,为YES,否则为NO
*/
- (BOOL)isAdValid;
III、适配idfa
3.1 适配开屏广告
为iOS14.5升级所需要的修改,目的是请求用户授权,访问与应用相关的数据以跟踪用户或设备。详情请访问developer.apple.com/documentati…
在Info.plist中添加NSUserTrackingUsageDescription,描述获取IDFA等广告标识符的用途
<key>NSUserTrackingUsageDescription</key>
<string>该ID将用于向您推送个性化广告</string>
弹窗小字文案建议:
获取标记权限向您提供更优质、安全的个性化服务及内容,未经同意我们不会用于其他目的;开启后,您也可以前往系统“设置-隐私 ”中随时关闭。
获取IDFA等广告标识符权限向您提供更优质、安全的个性化服务及内容;开启后,您也可以前往系统“设置-隐私 ”中随时关闭。
展示授权弹窗需要调用requestTrackingAuthorizationWithCompletionHandler:
方法。
建议
流量主
等待方法回调完成后处理广告相关逻辑,这样如果用户授权使用IDFA等广告标识符信息,优量汇iOS SDK可以使用IDFA等广告标识符进行广告请求。代码如下:
#import <AppTrackingTransparency/AppTrackingTransparency.h>
#import <AdSupport/AdSupport.h>
...
- (void)requestIDFA {
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
// 授权完成回调
[self loadGDTAd];// 先加载
}];
}
完整适配代码如下
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
BOOL result = [GDTSDKConfig registerAppId:@""];//
if (result) {
// [self setupGDTSplashAd];
[self requestIDFA];
NSLog(@"注册成功");
}
return YES;
}
- (void)setupGDTSplashAd{
// splash LoadAd 逻辑
GDTSplashAd *splash = [[GDTSplashAd alloc] initWithPlacementId:@""];
splash.delegate = self; //设置代理
// if ([[UIScreen mainScreen] bounds].size.height >= 568.0f) {
// splash.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"LaunchImage-568h"]];
// } else {
// splash.backgroundColor = [UIColor colorWithPatternImage:[UIImage imageNamed:@"LaunchImage"]];
// }
splash.fetchDelay = 5; //开发者可以设置开屏拉取时间,超时则放弃展示
self.splash =splash;
//根据iPhone设备不同设置不同背景图
UIImage *splashImage = [UIImage imageNamed:@"SplashNormal"];
if (isIPhoneXSeries()) {
splashImage = [UIImage imageNamed:@"SplashX"];
} else if ([UIScreen mainScreen].bounds.size.height == 480) {
splashImage = [UIImage imageNamed:@"SplashSmall"];
}
// * 是否需要开屏视频V+功能
// self.splash.needZoomOut = self.supportZoomoutViewSwitch.isOn;
self.splash.backgroundImage = splashImage;
self.splash.backgroundImage.accessibilityIdentifier = @"splash_ad";
//
[self.splash loadFullScreenAd];
}
#pragma mark - GDTSplashAdDelegate
- (void)splashAdDidLoad:(GDTSplashAd *)splashAd {
// if (splashAd.splashZoomOutView) {
// [self.view addSubview:splashAd.splashZoomOutView];
// splashAd.splashZoomOutView.rootViewController = self;
// // 支持拖拽
// [splashAd.splashZoomOutView supportDrag];
// }
NSLog(@"%s", __func__);
NSString *text = [NSString stringWithFormat:@"%@ 广告拉取成功", splashAd.adNetworkName];
NSLog(@"ecpm:%ld ecpmLevel:%@ text:%@", splashAd.eCPM, splashAd.eCPMLevel,text);
// splash show逻辑
//设置开屏自定义 logo,展示半屏开屏广告
UIWindow *window = [[UIApplication sharedApplication] keyWindow];
[self.splash showFullScreenAdInWindow:window withLogoImage:[UIImage imageNamed:@"img_login_logo"] skipView:nil];
}
- (void)splashAdSuccessPresentScreen:(GDTSplashAd *)splashAd
{
NSLog(@"%s",__FUNCTION__);
// self.tipsLabel.text = ;
NSLog(@"广告展示成功");
}
- (void)requestIDFA {
if([self isNeedrequestTrackingAuthorization]){
if (@available(iOS 14, *)) {
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
// 授权完成回调
[self setupGDTSplashAd ];// 先加载loadGDTAd
}];
} else {
[self setupGDTSplashAd ];// 先加载loadGDTAd
}
}else{
[self setupGDTSplashAd ];// 先加载loadGDTAd
}
}
- (BOOL)isNeedrequestTrackingAuthorization{
if (@available(iOS 14, *)) {
ATTrackingManagerAuthorizationStatus status = ATTrackingManager.trackingAuthorizationStatus;
switch (status) {
case ATTrackingManagerAuthorizationStatusDenied:
NSLog(@"用户拒绝");
return YES;
break;
case ATTrackingManagerAuthorizationStatusAuthorized:
NSLog(@"用户允许");
break;
case ATTrackingManagerAuthorizationStatusNotDetermined:
NSLog(@"用户为做选择或未弹窗");
return YES;
break;
default:
break;
}
} else {
// Fallback on earlier versions
if ([ASIdentifierManager.sharedManager isAdvertisingTrackingEnabled]) {
}else {
NSLog(@"用户开启了限制广告追踪");
}
}
return NO;
}
对于用户拒绝授权 UserTracking 的情况,可以考虑接入苹果的 SKAdNetwork 框架进行广告分析。
developer.apple.com/documentati…
<key>SKAdNetworkItems</key>
<array>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>f7s53z58qe.skadnetwork</string>
</dict>
<dict>
<key>SKAdNetworkIdentifier</key>
<string>example200.skadnetwork</string>
</dict>
</array>
SKAdNetworkIdentifier : f7s53z58qe.skadnetwork
3.2 适配极光推送
iOS14.5.1适配【To use the AppTrackingTransparency framework】使用AppTrackingTransparency以请求用户授权获取IDFA信息
———————————————— 版权声明:本文为CSDN博主「#公众号:iOS逆向」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
注册极光
[JPUSHService setupWithOption:launchOptions appKey:@"" channel:@"App Store" apsForProduction:YES advertisingIdentifier:[self testIDFA]];
获取idfaString
#import <AppTrackingTransparency/AppTrackingTransparency.h>
#import <AdSupport/AdSupport.h>
- (NSString*)testIDFA {
NSString __block *idfaString = @"";
if (@available(iOS 14, *)) {
[ATTrackingManager requestTrackingAuthorizationWithCompletionHandler:^(ATTrackingManagerAuthorizationStatus status) {
if (status == ATTrackingManagerAuthorizationStatusAuthorized) {
idfaString = [[ASIdentifierManager sharedManager] advertisingIdentifier].UUIDString;
}
}];
} else {
// 使用原方式访问 IDFA
if ([[ASIdentifierManager sharedManager] isAdvertisingTrackingEnabled]) {
idfaString = [[ASIdentifierManager sharedManager] advertisingIdentifier].UUIDString;
}
}
NSLog(@"idfaString: %@", idfaString);
return idfaString;
}
3.3 上架权限配置
新增开屏广告功能,appstoreconnect后台的app隐私声明,采集数据需要包含idfa,需要添加idfa用于广告,否则会被拒绝。
具体配置:
app主页的隐私收集这次新增了标识符->广告标识符用于第三方广告,未与用户身份关联,会将设备ID用于追踪目的。
IV 常见问题
4.1 SDK 从4.12.90升级4.13.26之后的适配
-> Installing GDTMobSDK 4.13.26 (was 4.12.90)
2021-11-01 09:55:54.785169+0800 +[GDTSDKConfig enableGPS:]: unrecognized selector sent to class 0x104cf83b8
解决方法:直接注释
// [GDTSDKConfig enableGPS:YES]; // 获取用户的GPS信息,默认值为NO
4.2 iOS13适配present半屏的问题
由于本文的广告类型是开屏全屏方式的,所以使用分类将广告控制器GDTSplashImageViewController的modalPresentationStyle设置为UIModalPresentationFullScreen
点击广告之后,如果不是打开第三方app,则会modal到GDTLandingPageWebViewController进行网页的展示,所以最好将GDT开头的控制器都是以UIModalPresentationFullScreen的方式展示。
if([NSStringFromClass(viewControllerToPresent.class) hasPrefix:@"GDT"])
{
return UIModalPresentationFullScreen;
}
使用分类控制modal的样式
- (void)K_presentViewController:(UIViewController *)viewControllerToPresent animated:(BOOL)flag completion:(void (^)(void))completion {
if (@available(iOS 13.0, *)) {
if (viewControllerToPresent.K_automaticallySetModalPresentationStyle) {
viewControllerToPresent.modalPresentationStyle = [QCTSession getModalPresentationStyleWith:viewControllerToPresent];
}
[self K_presentViewController:viewControllerToPresent animated:flag completion:completion];
} else {
// Fallback on earlier versions
[self K_presentViewController:viewControllerToPresent animated:flag completion:completion];
}
}
调试发现广告页对应的控制器是 GDTSplashImageViewController
(lldb) po [QCT_Common getCurrentVC]
<GDTSplashImageViewController: 0x159ea5f70>
superclass:GDTSplashViewController
title:(null)
view:<UIView: 0x159d30d80; frame = (0 0; 414 736); autoresize = W+H; layer = <CAGradientLayer: 0x280f56640>>
更多适配细节,请看这篇文章:
- (NSMutableArray *)FullScreenClasss{
if(_FullScreenClasss == nil){
_FullScreenClasss = [NSMutableArray array];
[_FullScreenClasss addObject:@"PGDatePickManager"];
[_FullScreenClasss addObject:@"GDTSplashImageViewController"];
}
return _FullScreenClasss;
}
getCurrentVC的实现
// 获取当前VC
+ (UIViewController *)getCurrentVC
{
UIViewController *rootViewController = [UIApplication sharedApplication].keyWindow.rootViewController;
UIViewController *currentVC = [self getCurrentVCFrom:rootViewController];
return currentVC;
}
+ (UIViewController *)getCurrentVCFrom:(UIViewController *)rootVC
{
UIViewController *currentVC;
if ([rootVC presentedViewController]) {
// 视图是被presented出来的
rootVC = [rootVC presentedViewController];
}
if ([rootVC isKindOfClass:[UITabBarController class]]) {
// 根视图为UITabBarController
currentVC = [self getCurrentVCFrom:[(UITabBarController *)rootVC selectedViewController]];
} else if ([rootVC isKindOfClass:[UINavigationController class]]){
// 根视图为UINavigationController
currentVC = [self getCurrentVCFrom:[(UINavigationController *)rootVC visibleViewController]];
} else {
// 根视图为非导航类
currentVC = rootVC;
}
return currentVC;
}
see also
更多内容请关注#小程序:iOS逆向
,只为你呈现有价值的信息,专注于移动端技术研究领域。
作者:公众号iOS逆向
程序员必备小技能:mac文件备份和清理、常用工具的安装和配置
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
引言
一年一度的iOS 系统 API适配来了,9 月 14 日起 App Store Connect 已经开放 iOS 15 和 iPadOS 15 App 的提交,同时苹果宣布自 2022 年 4 月起,所有提交至 App Store 的 iOS 和 iPadOS app 都必须使用 Xcode 13 和 iOS 15 SDK 构建。
Xcode 13 正式版包含 iOS 15,iPadOS 15,tvOS 15,watchOS 8 以及 macOS Big Sur 11.3 SDK。
Xcode 13 需在 macOS 11.3 及以上版本运行
,支持 iOS 9,tvOS 9,watchOS 2 及以上系统设备调试;也正式支持了 Vim。
Xcode 13 Release Notes: developer.apple.com/documentati…
正是基于近期iOS15出来了,需要升级IDE进行适配。发现磁盘空间不足,连操作系统都无法升级了。
目前使用的Mac磁盘空间是121GB,至少需要换256G的。
可见平时的Mac清理的重要性,于是乎分享下更换Mac时的备份和清理小知识。
经常备份重要的配置和文件是良好的习惯,尤其当你Mac的配置跟不上开发要求的时候显得尤其重要。
I、备份
1.1 备份软件/工具
- alfredapp
- charlesproxy
-
oh-my-zsh:github.com/ohmyzsh/ohm…
-
SmartPush
1.2 备份配置
- ~/Library/Developer/Xcode/UserData/CodeSnippets
-
~/.ssh/config
-
~/.bash_profile
#多开WeChat
alias wx='nohup /Applications/WeChat.app/Contents/MacOS/WeChat > /dev/null &'
1.3 备份代码/常用脚本
- ~/bin
1.4 备份常用浏览器扩展/网站
推荐登录Google账号选择同步数据(扩展、书签)
-
CSDN插件自带广告屏蔽,可自定义添加个人常用站点(记得手动备份)。
-
Axure RP Extension for Chrome(查看原型)
-
Markdown Nice (支持自定义样式的 Markdown 编辑器)
-
新榜小助手(为公众号运营者提供广告接单变现与运营工具服务)
II、清理
2.1 清理个人信息
- 退出appID
- 清理浏览器记住的密码
- 清理浏览器扩展
- 清理社交app(包括企业微信)
2.2 清理Xcode产生的调试文件
安装Xcode的时候,由于公司的Mac经常提示”可用的磁盘空间不足,无法安装此产品。“,因此记录一下。
如果从AppStore升级Xcode失败的话,可通过URL下载Xcode
Xcode12:https://download.developer.apple.com/Developer_Tools/Xcode_12/Xcode_12.xip
同理:11 直接修改URL参数即可
https://download.developer.apple.com/Developer_Tools/Xcode_11/Xcode_11.xip
https://download.developer.apple.com/Developer_Tools/Xcode_10/Xcode_10.xip
- 清理脚本
#!/bin/sh
# The ~/Library/Developer/Xcode/iOS DeviceSupport folder is basically only needed to symbolicate crash logs.
# You could completely purge the entire folder. Of course the next time you connect one of your devices, Xcode would redownload the symbol data from the device.
# I clean out that folder once a year or so by deleting folders for versions of iOS I no longer support or expect to ever have to symbolicate a crash log for.
killall -9 Xcode
killall -9 com.apple.CoreSimulator.CoreSimulatorService
rm -rf ~/Library/Developer/Xcode/iOS\ DeviceSupport/*
rm -rf ~/Library/Developer/Xcode/DerivedData/*
rm -rf ~/Library/Developer/Xcode/Archives/*
rm -rf ~/Library/Developer/Xcode/Products/*
rm -rf ~/Library/Developer/CoreSimulator/Devices/*
killall -9 com.apple.CoreSimulator.CoreSimulatorService
killall -9 Xcode
rm -rf ~/.Trash/
exit 0%
2.3 清理缓存
open ~/Library/Caches
2.4 关闭TimeMachine
3.4 磁盘空间管理
以我的Mac为例子,Mac系统能清理的只有文稿和应用程序,系统和其他占据了87G是无法清理的。
III、常用工具的安装
-
安装Homebrew:
/bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"
-
安装zsh 安装方式1:
brew install zsh
安装方式2:
git clone git://github.com/robbyrussell/oh-my-zsh.git ~/.oh-my-zsh
cp ~/.oh-my-zsh/templates/zshrc.zsh-template ~/.zshrc
#<!-- 修改主题 -->
open ~/.zshrc
#修改 `ZSH_THEME=”robbyrussell”`,主题在 ~/.oh-my-zsh/themes 目录下。
在~/.zshrc 引用bash的配置source ~/.bash_profile
配置bash的时候,采用~/.bash_profile;配置zsh的时候,采用open ~/.zshrc
- 安装autojump:brew install autojump
autojump是一个命令行工具,它可以使用快捷命令,直接跳转到配置好的目录,而不用管现在身在何处,依赖zsh。
j -a 你定义的快捷命令 ‘需要跳转的目录位置’
使用vim ~/.zshrc
a. 找到 plugins=,在后面添加autojump:plugins=(git autojump)
b. 新开一行,添加:[[ -s (brew --prefix)/etc/profile.d/autojump.sh ]] && . (brew --prefix)/etc/profile.d/autojump.sh
c. :wq保存退出,重启终端。
plugins=(
git autojump
)
[[ -s $(brew --prefix)/etc/profile.d/autojump.sh ]] && . $(brew --prefix)/etc/profile.d/autojump.sh
- 安装cocoapods:
sudo gem install cocoapods
IV、常用的配置
显示/隐藏 文件
alias fs='defaults write com.apple.finder AppleShowAllFiles -boolean true ; killall Finder'
alias fh='defaults write com.apple.finder AppleShowAllFiles -boolean false ; killall Finder'
设置git名称及对应的邮箱:git config --global --edit
see also
connecting-to-github-with-ssh 免密码进行SSH连接
【Mac 使用~/.ssh 的config 配置GitHub SSH keys】同样适用于gitlab
原文链接:blog.csdn.net/z929118967/…
———————————————— 版权声明:本文为CSDN博主「iOS逆向」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
更多内容请关注#小程序:iOS逆向
,只为你呈现有价值的信息,专注于移动端技术研究领域。
作者:公众号iOS逆向
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
iOS入门常见问题汇总
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
引言
给新手看的入门基础篇
I、入门常见问题
1.1 请问iOS入门的话,xcode虚拟机就可以了?
使用Xcode的模拟器只是真机方面的功能无法调试而已,比如app的扫一扫功能涉及到的摄像机、录音亦或者接入三方SDK的时候也最好在真机调试测试。
1.2 请问包管理就是cocoa pods吧?
是的,另外Carthage也可以管理iOS依赖库。
Swift Package Manager 是 Apple 为了弥补当前 iOS 开发中缺少官方组件库管理工具的产物。相较于其他组件管理控件,他的定义文件更加轻松易懂,使用起来也很 Magic,只需将源码放入对应的文件夹内,Xcode 就会自动生成工程文件,并生成编译目标产物所需要的相关配置。同时,SPM 与 Cocoapods 相互兼容,可以在特性上提供互补。
github.com/apple/swift-package-manager 相关文档:developer.apple.com/documentati…
iOS第三方库管理规范,以Cocoapods为案例进行讲解
1.3 用于开发iOS的Mac最低配置需要什么样的?
8G内存有点小,最好16G。 不过这个性价比还不错
II 常用第三方库注意事项
2.1 极光消息推送
定期更新SDK,尤其系统大版本更新的时候。
2.2 toast 提示
设置toast显示时长 (针对SVProgressHUD第三方库)
[SVProgressHUD setMinimumDismissTimeInterval:0.4];// 设置最小显示时长
[SVProgressHUD setMaximumDismissTimeInterval:0.5];//
III、流程保证质量(规范+测试+设计)
kunnan.blog.csdn.net/article/det…
see also
9 月 14 日起 App Store Connect 已经开放 iOS 15 和 iPadOS 15 App 的提交,同时苹果宣布自 2022 年 4 月起,所有提交至 App Store 的 iOS 和 iPadOS app 都必须使用 Xcode 13 和 iOS 15 SDK 构建。 Xcode 13 需在 macOS 11.3 及以上版本运行:
更多资讯和服务请关注#小程序:iOS逆向
,只为你呈现有价值的信息,专注于移动端技术研究领域。
iOS小技能:去掉TabBar的顶部黑线,并添加发光的阴影
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
本文同时参与 「掘力星计划」 ,赢取创作大礼包,挑战创作激励金
前言
技术实现关键点:通过layer.shadowOpacity
和View.layer.shadowOffset
实现
I 去掉TabBar的顶部黑线,并添加发光的阴影
- setupshadowColor
- (void)setupshadowColor{
UIView * tmpView = self;
tmpView.layer.shadowColor = [UIColor blackColor].CGColor;//设置阴影的颜色
tmpView.layer.shadowOpacity = 0.08;//设置阴影的透明度
tmpView.layer.shadowOffset = CGSizeMake(kAdjustRatio(0), kAdjustRatio(0));//设置阴影的偏移量,阴影的大小,x往右和y往下是正
tmpView.layer.shadowRadius = kAdjustRatio(5);//设置阴影的圆角,//阴影的扩散范围,相当于blur radius,也是shadow的渐变距离,从外围开始,往里渐变shadowRadius距离
//去掉TabBar的顶部黑线
[self setBackgroundImage:[UIImage createImageWithColor:[UIColor clearColor]]];
[self setShadowImage:[UIImage createImageWithColor:[UIColor clearColor]]];
}
II 给视图底部添加发光的阴影
2.1 效果
2.2 代码实现
- QCTShadowView
@implementation QCTShadowView
- (instancetype)init
{
self = [super init];
if (self) {
[self setupshadowColor];
//
}
return self;
}
- (void)layoutSubviews{
[super layoutSubviews];
[self layoutIfNeeded];
[self.layer layoutIfNeeded];
[self setupshadowColor];
}
- (void) setupshadowColor{
UIView * tmpView = self;
tmpView.layer.shadowColor = [UIColor blackColor].CGColor;//设置阴影的颜色
tmpView.layer.shadowOpacity = 0.08;//设置阴影的透明度
tmpView.layer.shadowOffset = CGSizeMake(kAdjustRatio(0), kAdjustRatio(5));//设置阴影的偏移量,阴影的大小,x往右和y往下是正
tmpView.layer.shadowRadius = kAdjustRatio(5);//设置阴影的圆角,//阴影的扩散范围,相当于blur radius,也是shadow的渐变距离,从外围开始,往里渐变shadowRadius距离
}
III 其他小知识点
3.1 避免selectedViewController视图被TabBar挡住
- 错误约束
[_vcView mas_makeConstraints:^(MASConstraintMaker *make) {
make.edges.offset(0);
}];
- 正确约束
[_vcView mas_makeConstraints:^(MASConstraintMaker *make) {
[tmp mas_makeConstraints:^(MASConstraintMaker *make) {
make.left.equalTo(weakSelf.view).offset(0);
make.right.equalTo(weakSelf.view).offset(- 0);
make.top.equalTo(weakSelf.view).offset(0);
make.bottom.equalTo(weakSelf.view).offset(-weakSelf.tabBarController.tabBar.bounds.size.height);//避免视图被TabBar挡住
}];
}];
3.2 iOS 13适配深色模式【设置UITabBarItem上title颜色】
// 适配iOS13导致的bug
if (@available(iOS 13.0, *)) {
// iOS 13以上
// self.tabBar.tintColor = ;
self.tabBar.unselectedItemTintColor = ktabNorTextColor;
self.tabBar.tintColor = ktabSelectedTextColor;
// self.tabBar.unselectedItemTintColor = ;
// UITabBarItem *item = [UITabBarItem appearance];
// item.titlePositionAdjustment = UIOffse/tMake(0, -2);
// [item setTitleTextAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:12]} forState:UIControlStateNormal];
// [item setTitleTextAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:12]} forState:UIControlStateSelected];
} else {
// // iOS 13以下
// UITabBarItem *item = [UITabBarItem appearance];
// item.titlePositionAdjustment = UIOffsetMake(0, -2);
// [item setTitleTextAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:12], NSForegroundColorAttributeName:RGB_HEX(0x999999)} forState:UIControlStateNormal];
// [item setTitleTextAttributes:@{NSFontAttributeName:[UIFont systemFontOfSize:12], NSForegroundColorAttributeName:RGB_HEX(0xfb5400)} forState:UIControlStateSelected];
//设置文字样式
NSMutableDictionary *textAttr = [NSMutableDictionary dictionary];
textAttr[NSForegroundColorAttributeName] = ktabNorTextColor;
[childVC.tabBarItem setTitleTextAttributes:textAttr forState:UIControlStateNormal];
//选择状态的文字颜色样式
NSMutableDictionary *selectedTextAttr = [NSMutableDictionary dictionary];
[selectedTextAttr setValue:ktabSelectedTextColor forKey:NSForegroundColorAttributeName];
[childVC.tabBarItem setTitleTextAttributes:selectedTextAttr forState:UIControlStateSelected];
}
see also
更多资讯和服务请关注#小程序:iOS逆向
,只为你呈现有价值的信息,专注于移动端技术研究领域。
联系我请关注#公众号:iOS技能
iOS 对请求参数进行签名相关问题及对应解决方案
小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。
引言
本文是梳理粉丝对博文:【对请求参数进行签名】请求参数按照ASCII码从小到大排序、拼接、加密(采用递归的方式进行实现)的相关问题及对应解决方案。
demo地址:https://download.csdn.net/download/u011018979/15483107
I、签名数组ASCII码排序优化
1.1 优化数组元素的排序
记录一个用户反馈的bug,修改内容如下图:
数组的地方,我的规则是[] 表示数组,数组元素是用,分隔[,,,] 进行递归拼接的。
如果你认为数组ASCII码排序的地方是错误的话,你可以打印排序之后的字符串,看排序与你服务侧的拼接规则哪里不一样。把规则修改为与后台的数组排序规则一致即可。
/**
- 处理key对应的Value是数组的情况
*/
+ (NSString *)sortedDictionaryArr:(NSArray *)arr {
NSMutableString *contentString =[NSMutableString string];
#pragma mark - ******** 可选
arr = [self sortedArrBykey:sortedArray];//
for ( id obj in arr ) {
NSString * tmp = @"";
if(contentString.length<1 || [contentString isEqualToString:@""]){//
}
else{
[contentString appendString:@","];
}
if( [obj isKindOfClass:NSDictionary.class]){
tmp = [self sortedDictionary:obj];
}
// 数组就[]
else if( [obj isKindOfClass:NSArray.class]){
tmp = [self sortedDictionaryArr:obj];
tmp = [NSString stringWithFormat:@"%@%@%@",@"{",tmp,@"}"];
}
else{
tmp= obj;
}
[contentString appendFormat:@"%@",tmp];
}
contentString = [NSString stringWithFormat:@"%@%@%@",@"[",contentString,@"]"];
return contentString;
}
例子:编辑门店的商品的价格 请求报文:
{
"adjustStocks" : [
{
"nnewStockNum" : "0",
"productId" : "1280072986512433152",
"newStockNum" : "0",
"oldStockNum" : "0",
"storeId" : "25063",
"productSkuId" : "1280072986516627456",
"batchNo" : ""
}
],
"modifyPrices" : [
{
"tagId" : "25063",
"productNewPrice" : "5",
"productSkuId" : "1280072986516627456",
"type" : "1",
"productId" : "1280072986512433152",
"productOldPrice" : "33"
}
]
}
签名元字符串的ASCII码排序
adjustStocks=[batchNo=&newStockNum=0&nnewStockNum=0&oldStockNum=0&productId=1280072986512433152&productSkuId=1280072986516627456&storeId=25063]&modifyPrices=[productId=1280072986512433152&productNewPrice=55&productOldPrice=5&productSkuId=1280072986516627456&tagId=25063&type=1]&appsecret=4f7b71
1.2 新增集合元素排序:
对于数组排序,我的初步想法是,先按照数组元素的class类型进行分组,其中的子数组是字符串类型的话,就对其进行排序之后再重新组合到新的数组中。然后再进行遍历递归拼接
/**
1、新增集合元素排序:对于数组排序,我的初步想法是,先按照数组元素的class类型进行分组,其中的子数组是字符串类型的话,就对其进行排序之后再重新组合到新的数组中。然后再进行遍历递归拼接
*/
+ (NSMutableArray *)sortedArrBykey:(NSArray *)array {
// 对数组先按照class类型进行排序
//1)取出分组
// NSArray *arDistinct = [array valueForKeyPath:@"@distinctUnionOfObjects.class"];// 无法获取到字典class类型
// NSMutableArray *classArr = [NSMutableArray arrayWithArray:arDistinct];
NSMutableArray *strclassArr = [NSMutableArray array];
NSMutableArray *nostrclassArr = [NSMutableArray array];
for (NSObject* obj in array) {
if([obj isKindOfClass:NSString.class]){
[strclassArr addObject:obj];
}else{
[nostrclassArr addObject:obj];
}
}
// 4)按照日期进行分组
NSMutableArray* sortedArray = [NSMutableArray array];
strclassArr = [strclassArr sortedArrayUsingComparator:^NSComparisonResult(id obj1, id obj2){
return [obj1 compare:obj2 options:NSNumericSearch];
}];
[sortedArray addObjectsFromArray:strclassArr];
[sortedArray addObjectsFromArray:nostrclassArr];
return sortedArray;
}
1.3 微信支付参数值为空不参与排序的处理方案
参数为空的 只要排序之前从字典移除即可。
规则的原因 :签名只对针对重要的字段,值可空的参数可不必参加
II、其他编译相关问题
2.1 为什么我下载demo后,在xcode12中允许报错:library not found for -lAXIndicatorView;请问怎么解决?
原因:这是找不到 CocoaPods库 AXIndicatorView。是 AXWebViewController库依赖于它
- 解决方法: 你更新 pod 'AXWebViewController' 即可。
1、只更新指定的库,其它库忽略:
pod update AXWebViewController --verbose --repo-update
2、pod update会更新所有的类库,获取最新版本的类库
cocoapods 用法文章:https://blog.csdn.net/z929118967/article/details/103830017
exit 0% ➜ retail git:(develop) ✗ cat ~/bin/knpod
#!/bin/sh
#该命令只安装新添加的库,已更新的库忽略
pod install --verbose --no-repo-update
#该命令只更新指定的库,其它库忽略
#pod update 库名 --verbose --no-repo-update
exit 0% ➜ retail git:(develop) ✗
see also
更多资讯和服务请关注#小程序:iOS逆向
,只为你呈现有价值的信息,专注于移动端技术研究领域。
更多资源下载请访问博客:kunnan.blog.csdn.net/article/det…