查看 .ipa 包所包含的 UDID

如何查看打出来的 .ipa 包是否包含某个 UDID?

  1. 首先把.ipa包解压缩,找到对应的包
  2. 查看包里的内容,找到对应的证书文件,名字为:embedded.mobileprovision
  3. 打开终端,找到embedded.mobileprovision所在的目录,然后运行命令行: security cms -D -i embedded.mobileprovision
  4. 查看 ProvisionedDevices对应的数组

当然,你把 .ipa 包上传到蒲公英后,也是可以看到的^_^

PS : 如果 .ipa 包里不包含 某个 UDID ,则需要通过开发者账号在 Device 里添加 一个“UDID”,或者通过 Xcode 注册一个设备,然后删除 Xcode 的 Provisioning Profiles 目录下的对应文件,重启 Xcode并进行打包。

命令:

rm -rf ~/Library/MobileDevice/Provisioning\ Profiles/*

谁动了我的 plist

在《破解Revealapp的试用时间限制》这篇blog里,笔者在提供补丁之前,曾经提供了一种通过修改plist中的安装时间来延长试用的方法。这个方法对很多留言的同学来说不起作用,当初笔者也没有深究,就继而提供了补丁来直接修改可执行文件。不过这些同学的修改plist不起作用的这个问题却一直留在笔者心中。直到最近,看到一篇文章(参考资料1),才恍然大悟。

原来在OSX的10.9版本之前,plist的读写都是APP自己来进行的,但升级到10.9的时候,有一个专门的精灵进程来负责plist文件的读写,那就是cfprefsd。

如上图所示,所有的plist读写都是通过进程cfprefsd来进行的,也就是说cfprefsd会对plist数据信息缓存。这就是为什么即使你修改了plist,APP读到的仍然是以前的数据。

要解决这个问题可以用如下步骤:

  1. 退出plist对应的APP
  2. 在终端中运行killall cfprefsd杀掉所有的cfprefsd进程
  3. 修改plist
  4. 再运行APP,此时新的plist就会起作用了

参考资料:

How-to: Replace preference files in Mavericks

谁动了我的 plist

破解 Revealapp 的试用时间限制

Revealapp作为分析iOS app UI结构的利器,还是非常称手的,89刀的价格也是物有所值。本文分析其试用版时间限制,只是用于学习,如果一直用,还是买个licence支持一下吧。

试用版有30天的时间限制,既然是30天时间限制,肯定每次启动是要读当前时间的啰。所以最简单的hack方法就是修改系统时间。如果这种方法可以接受,就不用往下看了。

如果你的工作严重依赖于Calendar,那么修改系统时间的方法就是不可以接受的。下面的追踪过程包含了对双精度浮点数在内存中的表示、ObjC对象模型等问题的讨论,如果不感兴趣可直接跳到文末查看最终的解决方案。

开始的尝试

用dtruss看了下启动时调用的syscall,是没有网络通讯的,说明app的安装时间不可能是从网络读下来的,那么这个时间肯定是写在本地的文件系统。

用opensnoop看了下启动时Reveal读过的所有文件,没有值得注意的地方。最后的发现证实这个思路忽略了一个问题,一个app读的文件并不一定是它自己打开的,可以是进程间通信。

这些简单的尝试失败后,就只能老老实实的分析代码了。

从关键字开始

试用版的Reveal有提醒试用剩余时间的信息在窗口的右上角”Free trial ends in xx days”(我觉得这不是一个好的设计,这句话似乎时刻挑衅着使用者:“来呀,你来hack我呀”)。“trial”是我感兴趣的关键字,除了在数据段肯定能找到这个关键字以外,说不定在ObjC的运行时类型系统中还能有意外的收获。果真,Reveal没有对类型信息进行模糊处理,在class-dump生成的头文件中发现了:
-[IBARegistrationPreferencesViewController messageForTrialDaysRemaining:(long long)arg1]
从函数名来看它应该就是生成试用剩余时间字符串的。

上GDB,单步跟踪,

0x000000010008bd34 push rbp
0x000000010008bd3f move rbx, rdx ; rdx 就是还剩下的试用天数,也就是函数的参数arg1

以此为突破口,发现下面的小段代码。

0x00000001000872cd call 0x100086ec2
0x00000001000872d2 mov rcx, rax ; rax中是上面函数返回的已过去的天数
0x00000001000872d7 mov eax, 0x1e ; 0x1e=30 30天的限制
0x00000001000872dc sub rax, rcx ; 30减去已经过去的天数的,减出来就是还剩下的天数

再往下走,需要分析的数据不再像是“天数”这样的整数,而是像软件安装日期NSDate这样的对象,特征不明显。所以就有必要清楚NSDate这个对象中日期的表示方法。

内存中的NSDate对象

NSDate对象应该有两个域,第一个“isA”是所有ObjC对象都有的类型指针,指向NSDate类型对象。第二个是个双精度浮点数,表示从2001年1月1日到现在的时间间隔,单位是秒。

pointer: isA
double: _timeIntervalSinceReferenceDate

其实isA指针就是NSDate对象的特征,所有的NSDate对象都是以相同的8个字节开始。第二个域是一个浮点数,分两步把它转换为一个日期。

第一步,十六进制浮点数转换为十进制

双精度浮点数由8个字节构成,1个bit表示符号,11个bit表示指数,剩下的52位用来表示底数。
使用python可以方便的把8字节的十六进制浮点数转换为十进制数:
struct.unpack('<d','c3b72c7a9ebfb841'.decode('hex'))[0]

在gdb中,可以直接使用命令
p (double)(NSDate指针地址+8)

第二步,秒数转换为日期

NSDate *date = [NSDate dateWithTimeIntervalSinceReferenceDate:415285808.20822901];
NSLog(@"n%@", date);

使用上面的方法,可以在跟踪汇编代码的时候检查内存中的NSDate对象,以及它所表示的日期。(这需要点耐心)

最终,安装Reveal的时间第一次出现在内存中的位置被找到,这个位置所在的函数显然负责把存在文件某处的一个magic number转换为软件安装日期。

但是意料之外的是,这个想像中的magic number并不magic,它仅仅是存在user default的plist文件中的一项,而且就是安装日期的双精度浮点数的十六进制表示。

结论

所以,要想永久试用Reveal,只需要打开

~/Library/Preferences/com.ittybittyapps.Reveal.plist

把IBAApplicationPersistenceData这一项删除就是了。

后记

有同学留言说上面的方法不起作用(问题的原因请参考另一篇blog谁动了我的plist),于是叽歪刘写了个补丁

补丁是用10.9的SDK编译的,在Reveal1.0.3(2287)上测试通过。

下载解压后,用右键的“打开”菜单运行程序。亲,叽歪刘只能帮你到这里了。

文章转载自:破解 Revealapp 的试用时间限制

iOS 获取图片的原始格式

今天测试给过来一张图片(后缀是.png)说无法在 APP 的 WebView 里面无法显示,而且在 Safari 里也是无法打开的,但在谷歌浏览器上是可以正常显示。起初是知道 WebP 格式的图片苹果是不支持显示的,但这个图片的后缀是.png 的,难道还有 png 的图片是苹果不支持的么?

根据个人经验,是没有听说苹果不支持 png 格式的图片的,这时想到以前自己更改 JPG 图片后缀的事情,是不是这张图片也是经过别人手动改后缀的呢,带着这个疑问,我决定手动判断这张图片的原始格式。

记得以前在看 SDWebImage 源码时,源码中是有关于判断根据 image 来判断图片的实际格式的,于是从 SDWebImage 中的源码中抠出来判断图片实际格式的代码。

图片的前8位是存储图片格式的,可以通过先读取图片的数据,拿到图片的前8位来判断图片的类型

/**
 *
 * 根据图片数据获取图片的原始类型
 * 
 * @param data 图片的二进制数据
 * @return 图片的实际格式
 */
- (NSString *)typeForImageData:(NSData *)data {
    uint8_t c;
    [data getBytes:&c length:1];
    
    switch (c) {
        case 0xFF:
            return @"image/jpeg";
        case 0x89:
            return @"image/png";
        case 0x47:
            return @"image/gif";
        case 0x49:
        case 0x4D:
            return @"image/tiff";
        case 0x52: {
            //R as RIFF for WEBP
            if (data.length < 12) {
                return nil;
            }
            NSString *identifierTypeStr = [[NSString alloc] initWithData:[data subdataWithRange:NSMakeRange(0, 12)] encoding:NSASCIIStringEncoding];
            if ([identifierTypeStr hasPrefix:@"RIFF"] && [identifierTypeStr hasSuffix:@"WEBP"]) {
                return @"image/webp";
            }
            return nil;
        }
            
        default:
            break;
    }
    return nil;
}

That's All.

Code

原文链接:iOS 获取图片的原始格式

XCode 8.0下 NSLog 打印不完全

今天在重新整理希尔排序的时候,使用了 100000 个种子数据进行测试,但发现在排序好后,用 NSLog 无法打印完全排序结果,开始以为自己的排序算法写法有误,但查看内存数据信息,显示排序结果正常。这时怀疑 NSLog 在 XCode 8.0 下可能有 Bug。 于是使用 c 语言函数 printf 进行打印。

Bingo !

代码如下:

NSMutableArray<NSNumber *> *results = [dataList mutableCopy];
printf("%s", [results.description UTF8String]);

附(NSString 与 char 相互转换):

//NSString转换char
NSString * str1= @"Test";
const char * c1 =[str1 UTF8String];
//char转换NSString
const char * c2 ="test";
NSString *str2 = [NSString stringWithUTF8String:c2];

原文链接:XCode 8.0下 NSLog 打印不完全

Ubuntu 16.0.4 安装 Swift 后提示 error while loading shared libraries: libpython2.7.so.1.0

Ubuntu 16.0.4 安装 Swift 后提示: error while loading shared libraries: libpython2.7.so.1.0:

swift/usr/bin/lldb: error while loading shared libraries: libpython2.7.so.1.0: cannot open shared object file: No such file or directory

这个问题会在 Ubuntu 14.04 和 Ubuntu 16.04 上出现,是 swift 的一个依赖问题,只需要安装 libpython2.7-dev 就可以解决问题。代码如下

sudo apt-get install libpython2.7-dev

Done !

Link : Incomplete install instructions for Ubuntu
原文链接: Ubuntu 16.0.4 安装 Swift 后提示 error while loading shared libraries: libpython2.7.so.1.0

iOS 位枚举

在 iOS 开发中,我们使用系统的枚举定义的时候,经常可以看到位枚举

typedef NS_OPTIONS(NSUInteger, UIControlState) {
    UIControlStateNormal       = 0,
    UIControlStateHighlighted  = 1 << 0,                  // used when UIControl isHighlighted is set
    UIControlStateDisabled     = 1 << 1,
    UIControlStateSelected     = 1 << 2,                  // flag usable by app (see below)
    UIControlStateFocused NS_ENUM_AVAILABLE_IOS(9_0) = 1 << 3, // Applicable only when the screen supports focus
    UIControlStateApplication  = 0x00FF0000,              // additional flags available for application use
    UIControlStateReserved     = 0xFF000000               // flags reserved for internal framework use
};

需要掌握位枚举,我们需要先了解位运算位移

位运算

位运算有两种:位或(|)位与(&)

  • 位或(|) :两个进行或(|)运算。运算规则:两个运算的只要有一个为1则运算结果为1,否则为0

如:
0000 0001 | 0000 0010 结果为:0000 0011
0000 0000 | 0000 0000 结果为:0000 0000

  • 位与(&) :两个进行与(&)运算。运算规则:两个运算的都为1则运算结果为1,否则为0

如:
0000 0001 & 0000 0001 结果为:0000 0001
0000 0001 & 0000 0010 结果为:0000 0000

位移

位移包含两种:左移(<<)右移(>>)

  • << :将一个数的二进制位向左移动 n 位,高位丢弃,低位补 0。如将数字1(0000 0001)左移两位得到结果为:4(0000 0100)。表述为:1 << 2。

左移就是将一个数乘以 2 的 n 次方。

  • >> :将一个数的二进制位向右移动 n 位,低位丢弃,高位补 0。如将数字4(0000 0100)右移两位得到结果为:1(0000 0001)。表述为:4 >> 2。

右移就是将一个数除以 2 的 n 次方。

iOS 位枚举

我们有如下定义:

typedef NS_ENUM(NSUInteger, HJDirection) {
    // 0000 0001
    HJDirectionLeft = 1 << 0,
    // 0000 0010
    HJDirectionRight = 1 << 1,
    // 0000 0100
    HJDirectionTop = 1 << 2,
    // 0000 1000
    HJDirectionBottom = 1 << 3
};

PS:定义一个位枚举时,我们通常以一个数字作为基准,如数字1,然后对该数字进行左移(右移),这样我们才能在后面使用中判断是否包含某个枚举值。

使用:

//获取所有方向(位或运算)
//0000 1111
HJDirection directionAll = HJDirectionLeft | HJDirectionRight | HJDirectionTop | HJDirectionBottom;

//获取是否包含某个方向(位与运算)
if ((directionAll & HJDirectionLeft) == HJDirectionLeft) {
    //0000 0001
    NSLog(@"满足条件:左方向");
}
if ((directionAll & HJDirectionRight) == HJDirectionRight) {
    //0000 0010
    NSLog(@"满足条件:右方向");
}
if ((directionAll & HJDirectionTop) == HJDirectionTop) {
    //0000 0100
    NSLog(@"满足条件:上方向");
}
if ((directionAll & HJDirectionBottom) == HJDirectionBottom) {
    //0000 1000
    NSLog(@"满足条件:下方向");
}

我们回到开始的 UIControlState 枚举定义

我们在定义 UIButton 状态时,一般会用到 UIControlStateNormalUIControlStateHighlightedUIControlStateSelected,但我们怎么去定义一个选中状态下的高亮呢?如果我们单纯的使用 UIControlStateHighlighted, 我们得到的只是默认状态下的高亮显示。这时我们就需要用到位枚举的神奇之处了。

UIButton *button = [UIButton buttonWithType:UIButtonTypeCustom];
[button setTitle:@"点击我" forState:UIControlStateNormal];
//定义普通状态文字颜色
[button setTitleColor:[UIColor blackColor] forState:UIControlStateNormal];
//定义选中状态文字颜色
[button setTitleColor:[UIColor blueColor] forState:UIControlStateSelected];
//定义普通状态高亮文字颜色
[button setTitleColor:[UIColor redColor] forState:UIControlStateHighlighted];
//定义选中状态高亮文字颜色
[button setTitleColor:[UIColor orangeColor] forState:UIControlStateSelected | UIControlStateHighlighted];

Done !

Link:iOS 位枚举

Ubuntu 搭建 DNS 服务器

系统环境:

Ubuntu 16.04.3 LTS
IP : 192.168.1.110

安装步骤

1. 安装

apt-get update
apt-get install bind9

2. 配置缓存转发

打开打开/etc/bind/named.conf.options,修改后如下:

acl goodclients {
        192.168.1.0/24;
        localhost;
        localnets;
};

options {
        directory "/var/cache/bind";

        // If there is a firewall between you and nameservers you want
        // to talk to, you may need to fix the firewall to allow multiple
        // ports to talk.  See http://www.kb.cert.org/vuls/id/800113

        // If your ISP provided one or more IP addresses for stable
        // nameservers, you probably want to use them as forwarders.
        // Uncomment the following block, and insert the addresses replacing
        // the all-0's placeholder.

        // forwarders {
        //      0.0.0.0;
        // };

        //========================================================================
        // If BIND logs error messages about the root key being expired,
        // you will need to update your keys.  See https://www.isc.org/bind-keys
        //========================================================================
        dnssec-validation auto;

        auth-nxdomain no;    # conform to RFC1035
        listen-on-v6 { any; };

        //added
        listen-on { 192.168.1.110;};

        recursion yes ;
        allow-query { goodclients;};
        allow-query-cache { any; }; # 很重要,不然无法解析外网
        allow-transfer { none; }; # disable zone transfers by default

        forwarders {
                223.5.5.5; # alidns
                223.6.6.6; # alidns
                202.96.199.133; #上海电信DNS
                202.96.0.133; #上海电信DNS
                114.114.114.114; # 114 现在只配置alidns,不能解析国内域名
                114.114.115.115; # 114 现在只配置alidns,不能解析国内域名
                8.8.8.8; # Google Google可以解析国内和国外域名
                8.8.4.4; # Google Google可以解析国内和国外域名
        };
        forward only ;

};

3. 配置local文件

named.conf.local 文件默认是空的。本文在配置文件中分别定义一条正向解析一条反向解析。配置文件修改后类似如下:

//domain->ip
zone "local.com" in {
        type master;
        file "/var/cache/bind/db.local.com";
};
//domain->ip(for anthor domain)
//zone "local1.com" in {
//        type master;
//        file "/var/cache/bind/db1.local.com";
//};

//ip->domain
zone "1.168.192.in-addr.arpa" in {
        type master;
        file "/var/cache/bind/db.1.168.192";
};

4. 定义区域配置文件

配置文件定义后类似如下,分别是正向和反向两个解析记录。按照自己需求修改相应的区域和区域解析记录,IP等信息。

正向记录

sudo vim /var/cache/bind/db.local.com

$TTL 604800
@ IN SOA local.com. root.local.com. (
2 ; Serial
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
604000) ; Negative Cache TTL
;
; name servers
@ IN NS ns.local.com.
@ IN A 192.168.1.110
;ns records
ns IN A 192.168.1.110
;host records
www IN A 192.168.1.110
api IN A 192.168.1.100

反向记录

sudo vim /var/cache/bind/db.1.168.192

$TTL 604800
@ IN SOA local.com. root.local.com. (
2 ; Serial Number
604800 ; Refresh
86400 ; Retry
2419200 ; Expire
86400 ); ; Minimum

@ IN NS local.com.

66 IN PTR www.local.com.
66 IN PTR api.local.com.

5. 检查配置、重启

hejun@ubuntu:/var/cache/bind$named-checkconf
hejun@ubuntu:/var/cache/bind$named-checkzone local.com /var/cache/bind/db.local.com

zone local.com/IN: loaded serial 2
OK

hejun@ubuntu:/var/cache/bind$named-checkzone db.1.168.192 /var/cache/bind/db.1.168.192

zone db.1.168.192/IN: loaded serial 2
OK

分别检查了语法和区域配置文件,没有报错。重启bind服务。

sudo service bind9 restart

到这里DNS服务器的配置就完成了,可以使用dig命令测试。

配置路由器首要DNS

在路由器里设置首要DNS192.168.1.110 ,这样我们就可以在同一个内网下访问:www.local.com 就会指向到 192.168.1.110,访问:api.local.com 就会指向到 192.168.1.100

参考链接:Ubuntu系统Bind搭建配置私有、主备DNS服务器

iOS 关联,objc_getAssociatedObject, objc_setAssociatedObject

关联是指把两个对象相互关联起来,使得其中的一个对象作为另外一个对象的一部分。

CategoryAssociate作为Objective-C的扩展机制的两个特性,Category即分类,可以通过它来扩展方法;Associate,可以通过它来扩展属性;在iOS开发中,可能Category比较常见,相对的Associate,就用的比较少,要用它必须使用<objc/runtime.h>的头文件,然后就可以自由使用objc_getAssociatedObject以及objc_setAssociatedObject

在类的定义之外为类增加额外的存储空间

使用关联,我们可以不用修改类的定义而为其对象增加存储空间。这在我们无法访问到类的源码的时候或者是考虑到二进制兼容性的时候是非常有用。

关联是基于关键字的,因此,我们可以为任何对象增加任意多的关联,每个都使用不同的关键字即可。关联是可以保证被关联的对象在关联对象的整个生命周期都是可用的(在垃圾自动回收环境下也不会导致资源不可回收)。

创建关联

创建关联要使用到Objective-C的运行时函数:objc_setAssociatedObject 来把一个对象与另外一个对象进行关联。该函数需要四个参数:源对象关键字关联的对象和一个关联策略。当然,此处的关键字和关联策略是需要进一步讨论的。

  • 关键字是一个void类型的指针。每一个关联的关键字必须是唯一的。通常都是会采用静态变量来作为关键字。
  • 关联策略表明了相关的对象是通过赋值,保留引用还是复制的方式进行关联的;还有这种关联是原子的还是非原子的。这里的关联策略和声明属性时的很类似。这种关联策略是通过使用预先定义好的常量来表示的。

下面的代码展示了如何把一个字符串关联到一个数组上。

NSArray *array = [NSArray arrayWithObjects:@"a", @"b", @"c", nil];
NSString *overValue = @"overCharValue";
static char overKey;
objc_setAssociatedObject(array, &overKey, overValue, OBJC_ASSOCIATION_RETAIN);
//通过对array的强引用,我们可以在其他地方根据关键字来获取关联对象的值,直到array被销毁
获取相关联的对象

获取相关联的对象时使用Objective-C函数 objc_getAssociatedObject。接着上面的代码,我们可以使用如下代码来获取与array相关联的字符串:

NSString * associatedObject = (NSString *)objc_getAssociatedObject(array, &overKey);
断开关联

断开关联是使用objc_setAssociatedObject函数,传入nil值即可。

我们可以使用如下的代码来断开字符串overview和arry之间的关联:

objc_setAssociatedObject(array, &overKey, nil, OBJC_ASSOCIATION_RETAIN);

其中,被关联的对象为nil,此时关联策略也就无关紧要了。

使用函数objc_removeAssociatedObjects可以断开所有关联。这个函数的主要目的是很容易的让对象恢复成它“原始状态”,你不应该使用它来移除关联的对象,因为它也会移除掉包括其他地方加入的全部的关联对象。所以一般你只需要通过调用objc_setAssociatedObject并传入nil值来清除关联值。

完整代码:

#import "HJViewController.h"
#import <objc/runtime.h>

@interface HJViewController ()

@property (nonatomic, copy) NSArray *array;

@end

@implementation HJViewController

static char overKey;
- (void)viewDidLoad {
    [super viewDidLoad];
    
    NSArray *array = [NSArray arrayWithObjects:@"a", @"b", @"c", nil];
    NSString *overValue = @"overValue";
    objc_setAssociatedObject(array, &overKey, overValue, OBJC_ASSOCIATION_RETAIN);
    self.array = array;
}

- (void)touchesEnded:(NSSet<UITouch *> *)touches withEvent:(UIEvent *)event {
    [super touchesEnded:touches withEvent:event];
    
//   objc_removeAssociatedObjects(self.array);
//   objc_setAssociatedObject(self.array, &overKey, nil, OBJC_ASSOCIATION_RETAIN);
    NSLog(@"%@", objc_getAssociatedObject(self.array, &overKey));
}

参考文章:iOS关联,objc_getAssociatedObject, objc_setAssociatedObject