iteatimeteam / Friday-QA

iTeaTime |技术清谈 微信群每周五问答环节
MIT License
229 stars 19 forks source link

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

Open ChenYilong opened 5 years ago

ChenYilong commented 5 years ago

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]回答。

边界情况,可酌情添加,为加分项目:


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的形式,用时加载就好了。所以题目主要是讨论高清大图。


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

----------

One more thing...

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

---------- ![image](https://user-images.githubusercontent.com/2911921/57426727-fbdd9b80-7252-11e9-9f1e-0e403d56b5ca.png)