Friday-QA icon indicating copy to clipboard operation
Friday-QA copied to clipboard

iTeaTime(技术清谈)【006期】【代号:布加迪】

Open ChenYilong opened this issue 4 years ago • 0 comments

iTeaTime(技术清谈)【006期】【代号:布加迪】



出题:微博@iOS程序犭袁 和他的小伙伴们 本期代号:布加迪

enter image description here


【今日话题】: 你如何看待张小龙的预言:未来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-杭州]提供)

enter image description here

常量区:

enter image description here

然后这个常量区 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;
}

运行结果:

enter image description here

参考文献:

修改 CFString __DATA 段 方案

[孙源Sunny@dd] 提供思路:

cstring 存在 __TEXT ,是不可变区域,CFString 存在 __DATA,可以修改,该情况与修改 static 变量没什么区别。

CFString__DATA 端我觉得原因是它的结构里有个 isa 指针指向了 __CFConstantStringClassReference ,这种在编译时无法确定,得在动态链接时才知道这个地址并赋值上去。

根据 WWDC 2016 - Session 406-Optimizing App Startup Time 的说明:

enter image description here

可知:

区域 作用
__TEXT ,是不可变区域,CFString 存在 __DATA,可以修改,该情况与修改 static 变量没什么区别。

CFString__DATA 端我觉得原因是它的结构里有个 isa 指针指向了 __CFConstantStringClassReference ,这种在编译时无法确定,得在动态链接时才知道这个地址并赋值上去。

根据 WWDC 2016 - Session 406-Optimizing App Startup Time 的说明:

enter image description here

可知:

区域 作用
__DATA 头文件,代码,只读常量
__DATA 所有可读写内容(全局变量、静态变量等等)

举例说明 NSString_TEXT_DATA 三者的关系,

比如 《iOS控制代码段大小的一些经验》 一文中提到:

如果定义一个 NSString 数组,元素数量庞大,可能会导致 __TEXT 代码段非常大。仅仅下面的代码,生成的目标文件大小就可以达到 89.51KB。具体原因并不是字符串的总字节数多,而是元素数量大,中间的取值指令过多。 代码:

enter image description here

用到的宏定义:

enter image description here

尽管编译器只在text段的字符常量区生成一份字符,多次使用不会增加字符常量区的大小,但是会增加 __TEXT 段代码的大小,使用 MachOView 工具查看可执行文件,结果如下图:

enter image description here

cstring 只有一份,多次调用不会存在多份,但是再通过反编译查看调用语句:

enter image description here

调用的函数会转变成上图的指令,可以看到是从字符常量区取到 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;
}

enter image description here

一行代码模式:

// 改 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;
}

运行结果:

enter image description here

Apple 的 opensource 的 CF 是 CFLite,跟生产环境的不一样,可以参考其 README 说明:

Apple 的 opensource 版本:

enter image description here

生产环境版本:

enter image description here


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

enter image description here

[鹅喵-便利蜂移动端][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'));

输出结果:

enter image description here

另一 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

参考: 《iOS中__block 关键字的底层实现原理》


7【问题】【iOS】segment 页面如何进行内存优化。需求描述:segment一次性把所有页面加载出来会导致内存爆增。几个segment 子页面的图片都是高清大图。【难度🌟🌟🌟】【出题人:中鼎iOS攻城狮】

下面答案由【BM-成都iOS】提供:

题目主要有2个核心:1.多个页面 2.高清大图

针对多个页面,用lazy的形式,用时加载就好了。所以题目主要是讨论高清大图。

作为另外一个补充:

关于高清大图上传问题。

  1. 理论应该尽可能还原用户创造的原始数据。比如用户上传一个头像,虽然现实控件只有100像素。但是理论上不应该客户端直接把图片压缩了上传给服务器,万一以后这张头像会作为背景图呢?

  2. 关于流程问题。很多对于资源类型的上传直接使用后台接口上传data。

    但是有一个问题是,整个过程是这样的:1.客户端上传data给后台主机。2.主机拿到图片上传到对象服务器。3.对象服务器给主机回调。4.主机给客户端上传结果回调。(举例一个栗子:3个角色,你,财务,人事。(事情是核对你的工资是否正确) 难道不觉得这样的一个流程很傻逼么?流程:财务给你发工资,然后你拿到钱,找人事问应该给你发多少,如果发错了,你再找财务说给你发错了,重新核对。 过程完全可以优化为:财务向人事核对工资是否正确,然后给你发工资

    所以,整个过程的标准流程为:向服务器请求token,Bucket,路径等一系列参数。由客户端直接向对象存储器上传,上传成功以后请求接口告知主机上传成功。在这个过程中,主流的对象存储器的sdk都有断点续传等乱七八糟的优化。会一定范围内解决上传内存暴增情况


Posted by 微博@iOS程序犭袁
原创文章,版权声明:自由转载-非商用-非衍生-保持署名 | Creative Commons BY-NC-ND 3.0


One more thing...

【非礼勿视】以下为彩蛋部分,建议28岁以上男性观看


image

ChenYilong avatar Jul 16 '19 13:07 ChenYilong