Friday-QA
Friday-QA copied to clipboard
iTeaTime(技术清谈)【006期】【代号:布加迪】
iTeaTime(技术清谈)【006期】【代号:布加迪】
出题:微博@iOS程序犭袁 和他的小伙伴们 本期代号:布加迪
【今日话题】: 你如何看待张小龙的预言:未来2年内,小程序将取代80%的App市场。每次都能听到“2岁多的小程序,终于又双叒迎来了“春天”?”的声音,似乎native每天都在冬天,你会因为市场的影响而更新技术栈,或者调整编程的百分比,为前端多多增加百分比吗?未来前端在你的占比中多少比较合适?
1.【问题】【iOS】参考代码注释内容:
int main(int argc, const char * argv[]) {
//在这里插入一行代码,使下面的代码输出 "Goodbye World"
NSLog(@"Hello World");
return 0;
}
【 难度🌟🌟🌟🌟】【出题人 孙源Sunny@dd】
【答案】
几种 tricky 版本:
重定义 NSLog
以下几种宏定义皆可:
#define NSLog(FORMAT, ...) fprintf(stderr, "Goodbye World\n")
#define NSLog(x) printf("Goodbye World\n")
#define NSLog NSLog(@"Goodbye World");
[鹅喵-便利蜂移动端]:
void(ˆNSLog)(id)=(id _){CFLog(0, CFSTR("Goodbye World"));};
利用编译器的注释特性
[鹅喵-便利蜂移动端]:
NSLog("Goodbye World"); //\
NSLog(@"Hello World");
下面着重介绍两种,更有技术含量的版本:
深入理解 NSString
NSString
的内存分配实际是很复杂的,可能分配在栈区、堆区、常量区。
我们常常以为@"foo",这样的字符串是常量区(也称为常量存储区或 _TEXT
区。),运行时不能改,内存区域映射都是 dyld 干的。
其实我们可以简单理解为 NSString
是一个 map 结构,key 存在常量区,的确无法修改,但是 value 是一个静态变量,我们可以运行时修改。
常量字符串的内存复制方案
[molon-杭州]提供思路:
首先 Objective-C 中任何两个相同字符串值的声明,即使是存储在不同的变量名中,也是指向同一个对象。
常量区的变量内存地址是连续的。
而常量字符串的指针在 CFString
段里面,内存复制的话只复制 CFString 的 size 就够了。
直接操作内存就可修改,只要有权限,内存当中的任何地方都能操作。程序运行起来,可以理解为其汇编代码也是写入内存的。你想靠修改对应位置,塞入汇编代码,修改运行中逻辑都可以。很多计算机病毒就是这么做的。但是一般操作系统提供的 API 会做一定的权限控制,例如不能修改其他进程的,等等,但是只要你能提权也是可以操作,很多外挂、破解基本上都是这么个原理。相对来说, Objective-C 反而显得不安全,对 hook 的亲和力太特么强。像其他语言一般还需要用内联汇编去做层 inline hook,控制堆栈平衡等,而 Objective-C 却没有。
比如如果逆向以下代码:
int main(int argc, const char * argv[]) {
NSString *a = @"Hello world";
NSString *b = @"bye";
memmove((void *)(@"Hello world"),(void *)(@"Goodbye world"),17);//此处17为随意填写,并无特定含义,请查看下文完整的取值计算方案。
NSString *c = @"ok";
NSLog(@"Hello world");
NSLog(@"ok");
NSLog(@"bye");
return 0;
}
那么,下图红框部分即为 Objective-C 的常量区:
(逆向结果由[molon-杭州]提供)
常量区:
然后这个常量区 CFString
里对应的 char
指针对应的那块内存区域也是连续的。
上述代码中 memmove
拷贝的是 CFString
的内容,不是这块区域的。
首先从<CoreFoundation/CFString.h>
可以查看 CFString
结构体,
可以发现其大小跟 CPU 架构有关,结构并不简单,而且 __CFConstStr
属于内部 API 无法访问,所以我们可以模仿重新定义一个类似的结构:
以下方案由[Never-成都iOS]提供:
#import <Cocoa/Cocoa.h>
struct CYLConstStr {
struct {
uintptr_t _cfisa;
uint32_t _swift_strong_rc;
uint32_t _swift_weak_rc;
uint8_t _cfinfo[4];
uint8_t _pad[4];
} _base;
uint8_t *_ptr;
#if defined(__LP64__) && defined(__BIG_ENDIAN__)
uint64_t _length;
#else
uint32_t _length;
#endif
};
int main(int argc, const char * argv[]) {
//在这里插入一行代码,使下面的代码输出 "Goodbye World"
memmove((void *)(@"Hello World"), (void *)(@"Goodbye World"), sizeof(struct CYLConstStr));
NSLog(@"Hello World");
return 0;
}
运行结果:
参考文献:
修改 CFString __DATA
段 方案
[孙源Sunny@dd] 提供思路:
cstring
存在 __TEXT
,是不可变区域,CFString
存在 __DATA
,可以修改,该情况与修改 static
变量没什么区别。
CFString
在 __DATA
端我觉得原因是它的结构里有个 isa 指针指向了 __CFConstantStringClassReference
,这种在编译时无法确定,得在动态链接时才知道这个地址并赋值上去。
根据 WWDC 2016 - Session 406-Optimizing App Startup Time 的说明:
可知:
区域 | 作用 |
---|---|
__TEXT ,是不可变区域,CFString 存在 __DATA ,可以修改,该情况与修改 static 变量没什么区别。 |
CFString
在 __DATA
端我觉得原因是它的结构里有个 isa 指针指向了 __CFConstantStringClassReference
,这种在编译时无法确定,得在动态链接时才知道这个地址并赋值上去。
根据 WWDC 2016 - Session 406-Optimizing App Startup Time 的说明:
可知:
区域 | 作用 |
---|---|
__DATA |
头文件,代码,只读常量 |
__DATA |
所有可读写内容(全局变量、静态变量等等) |
举例说明 NSString
、_TEXT
与 _DATA
三者的关系,
比如 《iOS控制代码段大小的一些经验》 一文中提到:
如果定义一个 NSString
数组,元素数量庞大,可能会导致 __TEXT
代码段非常大。仅仅下面的代码,生成的目标文件大小就可以达到 89.51KB。具体原因并不是字符串的总字节数多,而是元素数量大,中间的取值指令过多。
代码:
用到的宏定义:
尽管编译器只在text段的字符常量区生成一份字符,多次使用不会增加字符常量区的大小,但是会增加 __TEXT
段代码的大小,使用 MachOView 工具查看可执行文件,结果如下图:
cstring 只有一份,多次调用不会存在多份,但是再通过反编译查看调用语句:
调用的函数会转变成上图的指令,可以看到是从字符常量区取到 rax, 再取到栈空间。这样一个一个的取,由于有非常多个字符串,那么相应的指令就会存在非常多,导致调用函数的代码段变得非常大。而且这样会非常浪费栈空间。
总结就是:
在使用 NSString
时,cstring
只有一份,多次调用不会存在多份,但调用的函数中间还有一步指令,是从字符常量区取到 rax
, 再取到栈空间,rax对应的值保存在 _DATA
中。
上面的题目,修改的就是 _DATA
中的值。
cstring 才是 __TEXT
cfstring 是 __DATA
CFString
在 __DATA
端我觉得原因是它的结构里有个 isa
指针指向了 __CFConstantStringClassReference
,这种在编译时无法确定,得在动态链接时才知道这个地址并赋值上去.
[鹅喵-便利蜂移动端]提供代码实现:
int main(int argc, const char * argv[]) {
typedef struct STR {
size_t padding[2];
char const *s;
size_t len;
}STR;
STR *str =(STR *)@"Hello World";
str->s = "GoodBy World";
NSLog(@"Hello World");
return 0;
}
一行代码模式:
// 改 cstring 的方式
int main(int argc, char **argv) {
typedef struct STR { size_t padding[2]; char const *s; size_t len;} STR; STR *goodbye = (STR*)@"Hello World"; goodbye->s = "Goodbye World"; goodbye->len = 13;
NSLog(@"Hello World");
return 0;
}
运行结果:
Apple 的 opensource 的 CF 是 CFLite,跟生产环境的不一样,可以参考其 README 说明:
Apple 的 opensource 版本:
生产环境版本:
2【iOS】有没有办法通过提供的ipa包然后判断出是支持ipad还是iphone,还是都支持。【 难度🌟🌟】【出题人 SuperDanny-轩宇-珠海iOS】
把IPA解压,包内容的info.plist有个UIDeviceFamily,值=1是iPhone,=2是iPad,=1,2是都支持
/usr/libexec/PlistBuddy -c 'Print :UIDeviceFamily' xxx.app/Info.plist
[鹅喵-便利蜂移动端][M.W-不知名小作坊-iOS-北京]回答正确
3 【算法】使用熟悉的编程语言,编写一个函数,要求输入与以下列表相似的结构,则返回值为true,否则为false。【注意】输入字符串无限制,英文字符、标点符号均无需特殊处理,与中文一样视为作完整字符。
- 上海自来水来自海上
- 黄山落叶松叶落山黄
- 山东落花生花落东山
- 山西悬空寺空悬西山
- 湖南过路车路过南湖
【 难度🌟🌟🌟】【出题人 微博@iOS程序犭袁】
【提示】算法实际为回文算法,题目主要在于边界情况处理,在函数顶端要有判空等操作。可自行搜索,下面提供群里提供的答案,如有瑕疵可以指正。
【答案】
python3 的,天生编码支持好,递归做法:
def check_re(str):
if not str:
return True
if str[0] != str[-1]:
return False
else:
return check_re(str[1:-1])
print(check_re("asdffdsa) //True
print(check_re("asdfdsa)) //True
print(check_re("上海自来水来自海上)) //True
print(check_re(黄山落叶松叶落山黄")) //True
print(check_re("山东落花生花落东山")) //True
print(check_re("山西悬空寺空悬西山") //True
print(check_re("湖南过路车路过南湖")) //True
print(check_re("123456")//False
jS版本:
function judgeIsPalindrome (str) {
if (null == str || str.length < 2) {
return 'false';
} else {
if (str.split(""). reverse().join("")= str) {
return 'true';
} else {
return 'false';
}
}
}
console.log(judgeIsPalindrome('🍎🍌🍇🍇🍌🍎'));
console. log(judgeIsPalindrome('abccba'));
输出结果:
另一 java 版本:
// still 3
public static boolean isPalindrome(String s) {
if (s == null) return false;
int left = 0; int right = s.length()-1;
while (left < right) {
if (s.charAt(left) == ' ') left++;
else if (s.charAt(right) == ' ') right--;
else if (s.charAt(left) != s.charAt(right)) return false;
left++;
right--;
}
return true;
}
[Jenny]回答。
边界情况,可酌情添加,为加分项目:
- 字符里面也许应该排除一下 ZWJ 组合 emoji
- 不考虑 unicode 组合字的话,Pokémon 这种字倒过来就会变成 noḿekoP,注音符号跑偏了,所以至少是标准库或第三方库要有良好的 unicode 支持 (鹅喵-便利蜂移动端)
4.【算法】要求使用熟悉的编程语言,编写一个函数,要求输入任意字符串,都能返回对称结构的字符串。【注意】输入字符串无限制,英文字符、标点符号均无需特殊处理,与中文一样视为作完整字符。
举例:
输入的原始字符串:
- 走马灯灯马走灯熄马停步
输出的字符串:
- 走马灯灯马走
【 难度🌟🌟🌟】【出题人 微博@iOS程序犭袁】
【答案】 详细可以搜:最长回文子串算法。
下面是一种时间复杂度为 O(n^2)的写法,并非最优解,最优解为 Manacher 算法, 时间复杂度O(n), 空间复杂度O(n),可自行搜索。:
public static void main(String[] args) {
// write your code here
String str = "步停马熄灯走马灯灯马走你";
String str1 = "qqqqbcddcbasf";
String str2 = "abcdeff";
String str3 = "11118889999888";
String str4 = "5544334433111";
System.out.println(getStr(str));
System.out.println(getStr(str1));
System.out.println(getStr(str2));
System.out.println(getStr(str3));
System.out.println(getStr(str4));
}
static String getStr(String source) {
if (source == null || source.length() < 1) {
return "\n";
}
for (int i = source.length() / 2; i > 0 ; i--) {
StringBuilder builder = new StringBuilder();
builder.append(String.join("", Collections.nCopies(i, "(.)")));
for (int j = i; j > 0; j--) {
builder.append("\\" + j);
}
String patternStr = builder.toString();
Pattern pattern = Pattern.compile(patternStr);
Matcher m = pattern.matcher(source);
if (m.find()) {
return m.group(0);
}
}
return "\n";
}
5【iOS】看下面的方法执行完之后 ViewController 会被销毁吗,ViewController 的 view 会被销毁吗?为什么?
- (void)addViewController {
UIViewController *viewController = [[UIViewContrnller alloc] init];
[self.view addSubview: viewController.view];
}
…
【 难度🌟🌟】【出题人 Saber-ios-望京】
【答案】view被引用,vc没被引用,所以VC被销毁,view不销毁。
详细解释: vc引用view,view对vc无引用。 vc在view在,view在与vc可不在。vc为局部变量,方法结束后直接销毁;vc.view被添加在self.view上,所以不会被销毁.
6【iOS】请给出以下代码的输出结果:
NSArray *array = @[@"1"];
NSMutableArray *mutableArray = [[NSMutableArray alloc] initWithObjects:@"123’, nil];
void(ˆblock)(void) = ˆ{
NSLog(@"array %@",array);
NSLog(@"mutableArray %@",mutableArray);
};
array = @[@"2"];
[mutableArray add0bject:@"456"];
block();
【 难度🌟】【出题人 微博@iOS程序犭袁 】
【答案】
array 1 mutableArray 123,456
7【问题】【iOS】segment 页面如何进行内存优化。需求描述:segment一次性把所有页面加载出来会导致内存爆增。几个segment 子页面的图片都是高清大图。【难度🌟🌟🌟】【出题人:中鼎iOS攻城狮】
下面答案由【BM-成都iOS】提供:
题目主要有2个核心:1.多个页面 2.高清大图
针对多个页面,用lazy的形式,用时加载就好了。所以题目主要是讨论高清大图。
-
如果是作为面试题回答:
苹果方法针对超大图片示例:点击查看
主要是2个方向:1.是压缩高清大图导致的内存暴增。2.是现实高清大图导致的内存暴增。(相关资料自行谷歌,太多太杂。)
-
如果是作为项目回答:
-
几乎现在90%的后台服务器都使用的第三方云服务器。资源都是存在对应的云服务器的对象存储器中。
-
几乎所有的对象存储器都支持在线图片处理。(水印,缩放,格式转换等。):
所以正常开发的标准形式为:服务器下发高清的大图地址,客户端根据控件形式预估使用图片的大小。比如用户上传了一张2000像素的图片作为头像,而你当前只是渲染一个可能30像素左右的头像,那你应该直接使用第三方对象存储器的参数渲染一张50左右像素的图片就完全足够使用了。
https://helpcdn.aliyun.com/document_detail/44688.html?spm=a2c4g.11186623.6.1321.516e2e93zb9HsP
以阿里oos对象存储为例的图像缩放文档
- 针对图像的格式进行优化。
使用webp格式,具体的对比:https://aotu.io/notes/2016/06/23/explore-something-of-webp/index.html
也可以使用在线接口返回webp的图片:文档如下:https://helpcdn.aliyun.com/document_detail/44703.html?spm=a2c4g.11186623.6.1336.cd4b35a8Hg5ETw
理论上只要作为这3点,在正常显示高清大图都不会出现什么问题。
-
作为另外一个补充:
关于高清大图上传问题。
-
理论应该尽可能还原用户创造的原始数据。比如用户上传一个头像,虽然现实控件只有100像素。但是理论上不应该客户端直接把图片压缩了上传给服务器,万一以后这张头像会作为背景图呢?
-
关于流程问题。很多对于资源类型的上传直接使用后台接口上传data。
但是有一个问题是,整个过程是这样的:1.客户端上传data给后台主机。2.主机拿到图片上传到对象服务器。3.对象服务器给主机回调。4.主机给客户端上传结果回调。(举例一个栗子:3个角色,你,财务,人事。(事情是核对你的工资是否正确) 难道不觉得这样的一个流程很傻逼么?流程:财务给你发工资,然后你拿到钱,找人事问应该给你发多少,如果发错了,你再找财务说给你发错了,重新核对。 过程完全可以优化为:财务向人事核对工资是否正确,然后给你发工资
所以,整个过程的标准流程为:向服务器请求token,Bucket,路径等一系列参数。由客户端直接向对象存储器上传,上传成功以后请求接口告知主机上传成功。在这个过程中,主流的对象存储器的sdk都有断点续传等乱七八糟的优化。会一定范围内解决上传内存暴增情况
Posted by 微博@iOS程序犭袁
原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0
One more thing...
【非礼勿视】以下为彩蛋部分,建议28岁以上男性观看