普通视图

发现新文章,点击刷新页面。
昨天以前掘金专栏-iOS逆向

iOS小技能:【设备日志查看工具】syslog、deviceconsole和socat

2023年3月12日 07:41

本文正在参加「金石计划」

前言

本文介绍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

IV shell 脚本完成对日志文件的提取之sed 学习

sed 是一种在线编辑器,它一次处理一行内容。Sed主要用来自动编辑一个或多个文件;简化对文件的反复操作;编写转换程序等。

文件内容并没有改变,除非你使用重定向存储输出。

4.1 ## sed的参数

    sed [-nefr] [动作]
选项与参数:
-n :使用安静(silent)模式。在一般 sed 的用法中,所有来自 STDIN 的数据一般都会被列出到终端上。但如果加上 -n 参数后,则只有经过sed 特殊处理的那一行(或者动作)才会被列出来。
-e :直接在命令列模式上进行 sed 的动作编辑;
-f :直接将 sed 的动作写在一个文件内, -f filename 则可以运行 filename 内的 sed 动作;
-r :sed 的动作支持的是延伸型正规表示法的语法。(默认是基础正规表示法语法)
-i :直接修改读取的文件内容,而不是输出到终端。

动作说明: [n1[,n2]]function
n1, n2 :不见得会存在,一般代表『选择进行动作的行数』,举例来说,如果我的动作是需要在 10 到 20 行之间进行的,则『 10,20[动作行为] 』

function:
a :新增, a 的后面可以接字串,而这些字串会在新的一行出现(目前的下一行)~
c :取代, c 的后面可以接字串,这些字串可以取代 n1,n2 之间的行!
d :删除,因为是删除啊,所以 d 后面通常不接任何咚咚;
i :插入, i 的后面可以接字串,而这些字串会在新的一行出现(目前的上一行);
p :列印,亦即将某个选择的数据印出。通常 p 会与参数 sed -n 一起运行~
s :取代,可以直接进行取代的工作哩!通常这个 s 的动作可以搭配正规表示法!例如 1,20s/old/new/g 
————————————————
版权声明:本文为CSDN博主「iOS逆向」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/z929118967/article/details/49489527

4.2 数据的搜寻并替换

`sed 's/要被取代的字串/新的字串/g'`

    /etc>ifconfig -a | grep inet 
        inet 172.16.49.224 netmask 0xffffff00 broadcast 172.16.49.255
        inet 192.168.10.4 netmask 0xffffff00 broadcast 192.168.10.255
        inet 127.0.0.1 netmask 0xff000000 broadcast 127.255.255.255
        inet6 ::1%1/0
————————————————
版权声明:本文为CSDN博主「iOS逆向」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/z929118967/article/details/49489527

see also

更多服务和咨询请关注#公号:iOS逆向

iOS小技能:给debugserver添加task_for_pid权限,以便调试从AppStore中获取的App。

2022年10月27日 09:49

持续创作,加速成长!这是我参与「掘金日新计划 · 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__的应用

2022年6月19日 06:43

持续创作,加速成长!这是我参与「掘金日新计划 · 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 代码注入

  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
}
  1. 代码注入: 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;
}
  1. 自定义打印方法:用真正的方法替换去拦截 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")))

SymbolVisibility

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。

github.com/AFNetworkin…

+ (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__控制符号是否导出

SymbolVisibility :https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/CppRuntimeEnv/Articles/SymbolVisibility.html

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")))

隐藏未明确标记为可见的符号:

  1. 在编译参数中加入-exported_symbols_list export_list

  2. 在编译参数中指定-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 反调试

iOS动态防护:【Dynamic protection】反调试、反反调试、反注入、hook检测、完整性校验 https://blog.csdn.net/z929118967/article/details/84612698

  1. 运行时期,断点ptrace,直接返回

  2. 分析如何调用的ptrace,hook ptrace

  3. 通过tweak,替换disable_gdb函数

  4. 修改 PT_DENY_ATTACH:在二进制文件中 ,修改 PT_DENY_ATTACH的31,改成 任意一个值,如PT_ATTACH 0。 blog.csdn.net/z929118967/… AlipayWalletTweakF.xm

  5. 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小技能:动态地给类添加新的方法、实例变量、属性。

2022年6月9日 09:31

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第16天,点击查看活动详情

前言

添加新的实例变量的原理:利用category结合runtime的API实现

动态创建属性的应用场景:利用属性进行传值的时候,我们就可以利用本文的方法进行动态创建属性。尤其在逆向其他app的时候,往已经存在class新增一个属性,用于数据传递,尤其是异步操作的时候。

I 添加新的实例变量

1.1 原理

利用 runtime APIobjc_setAssociatedObjectobjc_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、属性关联

blog.csdn.net/z929118967/…

iOS动态库的注入原理

2022年2月6日 11:18

「这是我参与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年1月21日 09:08

「这是我参与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年1月20日 10:18

「这是我参与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年1月18日 14:15

「这是我参与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

ps : 安装brew

1.4 dpkg-deb

deb是越狱开发包的标准格式,dpkg-deb是个用于操作deb文件的工具,有了这个工具,Theos才能正确的把工程打包成deb文件.

brew install dpkg

see also

更多内容请关注 #小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域;更多服务和咨询请关注#公众号:iOS逆向。

iOS逆向小技能:使用substrate及runtime进行hook(定时检测app是否开启)

2022年1月18日 14:03

「这是我参与2022首次更文挑战的第1天,活动详情查看:2022首次更文挑战」。

前言

  1. 利用runtime API进行hook

method_exchangeImplementations 可以直接是一个函数地址,不管是OC还是C 所有的OC函数都是IMP类型。IMP就是个c函数指针。

  1. 使用substrate.h 进行hook
  2. 定时检测app是否处于前台运行状态

I 利用runtime API进行hook

利用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小技能: 处理接口的暂无数据

2021年11月9日 14:47

这是我参与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至于最顶端, 避免被其他子视图遮盖住)

2021年11月8日 10:15

这是我参与11月更文挑战的第8天,活动详情查看:2021最后一次更文挑战

前言

视图置顶的应用场景:

  1. 比如让日期控件置于窗口的最顶层
  2. 悬浮按钮(支持拖曳)

在这里插入图片描述

关于 bringSubviewToFront 和view.layer.zPosition的选择

1、使用bringSubviewToFront方法需要在重新刷新界面结构层次的时候调用;

2、使用view.layer.zPosition方法会获取不到view的点击事件

更多内容请关注#小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域。

I 视图置顶

1.1 方案一:bringSubviewToFront的用法

  • 让日期控件置于窗口的最顶层

PGDatePickManager kunnan.blog.csdn.net/article/det…

mp.weixin.qq.com/s/rT4Iu_Fb8…

@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系统上日期控件被筛选视图遮挡问题

mp.weixin.qq.com/s/rT4Iu_Fb8…

  • 推荐使用[[UIApplication sharedApplication].delegate window]获取window

在执行 didFinishLaunchingWithOptions: 这个代理方法时,调用[self.window makeKeyAndVisible];方法之前,通过[UIApplication sharedApplication].keyWindow 方法获取不到window, 但是无论何时都能获取到delegate.window。

  1. 在获取到window时最好使用[[UIApplication sharedApplication].delegate window]获取window

  2. 不要在keywindow为nil的时候给window上添加代码,例如添加弹窗。

iOS15适配本地通知功能

2021年11月3日 13:53

这是我参与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));

  1. Passive:被动类型的通知不会使手机亮屏并且不会播放声音。
  2. Active: 活动类型的通知会使手机亮屏且会播放声音,为默认类型。
  3. Time Sensitive(时间敏感):会使手机亮屏且会播放声音;可能会在免打扰模式(焦点模式)下展示。

设置推送通知数据: 时间敏感的中断级别可以使用“interruption-level” payload key:{"aps":{"interruption-level":"time-sensitive"}}

时效性通知开发者无法直接使用,需要配置对应的权限: a. xcode 开启对应能力

在这里插入图片描述

b. 开发者后台配置appID支持该权限(通过Xcode开启对应能力通常会自动添加) 在这里插入图片描述

  1. 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'

docs.jiguang.cn/changelog/j…

更新时间: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接入开屏广告教程 : 以腾讯优量汇为案例

2021年11月1日 13:23

这是我参与11月更文挑战的第1天,活动详情查看:2021最后一次更文挑战

引言

本文以对接开屏广告为例子

developers.adnet.qq.com/doc/ios/uni…

广告类型 接入方式 简介 适用场景 版本备注
开屏广告 SDK原生渲染 开屏广告以App启动作为曝光时机,提供5s的可感知广告展示。用户可以点击广告跳转到目标页面;或者点击右上角的“跳过”按钮,跳转到app内容首页。 开屏V+广告是一个5s-30s的视频广告,在5s开屏呈现的过程中,用户点击右上角的“进入首页”或5s曝光结束后,视频均将收缩到APP内右下角的小视窗继续播放。 APP启动时 包含 开屏 与 开屏V+ 两种

IOS对接文档:

developers.adnet.qq.com/doc/ios/gui…

ios对接SDK包:对接demo 工程

OC 版本:github.com/zhangkn/GDT…,更多资源请关注公众号:iOS技能

d3g.qq.com/gdt/sdk/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单价下降损失的结果。

未在优量汇注册,请注册加入优量汇或者申请成为运营者

运营者adnet.qq.com/register/be…

注册:adnet.qq.com/register

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设备在垂直方向上展示。

  1. 开屏全屏广告需使得显示区域其高度与设备高度一致,即为开屏全屏广告。开发者可通过以下接口配合使用提供媒体logo,用以随开屏广告展示。

logo 推荐使用透明背景色,可为空

 - (void)loadFullScreenAd;
 - (void)showFullScreenAdInWindow:(UIWindow *)window withLogoImage:(UIImage *)logoImage skipView:(UIView *)skipView;

  1. 开屏半屏广告的显示区域其高度一定要大于设备高度的75%(建议值大于80%),最小高度要大于400dp,开屏广告默认只在竖屏展示,横屏一般不满足尺寸要求。

  2. 优量汇开屏广告支持预加载开屏广告,调用方法如下:

    GDTSplashAd *preloadSplashAd = [[GDTSplashAd alloc] initWithPlacementId:YOUR_PLACEMENT_ID];
    [preloadSplashAd preloadSplashOrderWithPlacementId:YOUR_PLACEMENT_ID];

  1. 初始化SDK,加载广告的代码推荐放在didFinishLaunchingWithOptions的第一行
- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.
    [self setupGDTSDKConfig];
    ....
    return YES;
}
  1. 由于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等。

  1. 注册媒体ID
    BOOL result = [GDTSDKConfig registerAppId:@"xxx"];//
    
    
    if (result) {
        [self setupGDTSplashAd];

        NSLog(@"注册成功");
    }

  1. 初始化开屏广告位ID
    GDTSplashAd *splash = [[GDTSplashAd alloc] initWithPlacementId:@"6806"];

  1. 先拉取(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.pngiPhoneX@2x.pngiPhone4@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>

弹窗小字文案建议:

  1. 获取标记权限向您提供更优质、安全的个性化服务及内容,未经同意我们不会用于其他目的;开启后,您也可以前往系统“设置-隐私 ”中随时关闭。

  2. 获取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版权协议,转载请附上原文出处链接及本声明。

原文链接:blog.csdn.net/z929118967/…

注册极光

    [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>>

更多适配细节,请看这篇文章:

blog.csdn.net/z929118967/…

- (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文件备份和清理、常用工具的安装和配置

2021年10月31日 12:11

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

引言

一年一度的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

www.alfredapp.com/help/

  • charlesproxy

www.charlesproxy.com 4.2

1.2 备份配置

  • ~/Library/Developer/Xcode/UserData/CodeSnippets

download.csdn.net/download/u0…

  • ~/.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 编辑器)

  • 新榜小助手(为公众号运营者提供广告接单变现与运营工具服务)

  • lanhuapp.com/xd

  • mp.weixin.qq.com

  • git.weixin.qq.com/users/autho…

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、常用工具的安装

  1. 安装Homebrew: /bin/zsh -c "$(curl -fsSL https://gitee.com/cunkai/HomebrewCN/raw/master/Homebrew.sh)"

  2. 安装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

  1. 安装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
  1. 安装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版权协议,转载请附上原文出处链接及本声明。

清理Mac文件

更多内容请关注#小程序:iOS逆向,只为你呈现有价值的信息,专注于移动端技术研究领域。

作者:公众号iOS逆向
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

iOS入门常见问题汇总

2021年10月30日 14:49

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

引言

给新手看的入门基础篇

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为案例进行讲解

kunnan.blog.csdn.net/article/det…

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的顶部黑线,并添加发光的阴影

2021年10月28日 17:19

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文同时参与 「掘力星计划」     ,赢取创作大礼包,挑战创作激励金

前言

技术实现关键点:通过layer.shadowOpacityView.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颜色】

blog.csdn.net/z929118967/…

    // 适配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技能

❌
❌