1、为什么说Objective-C是一门动态的语言?
什么是动态语言?
动态语言,是指程序在运行时可以改变其结构;新的函数可以被引进,已有的函数可以被删除和替换(在其结构上发生了变化)。
为什么说 Object-C 是动态语言?
Object-C 将很多静态语言在编译和链接时期做的事放到了运行时来处理。为了实现这样的工作,Obejct-C 提供了 Objc Runtime 机制来动态创建类和对象,进行消息发送和转发。
Object-C 动态特征:有动态类型(Dynamic typing),动态绑定(Dynamic binding)和动态加载(Dynamic loading)。
注意:Objc Runtime其实是一个Runtime库,它基本上是用C和汇编写的,这个库使得C语言有了面向对象的能力。
Runtime有两个版本:Modern和Legacy。Object—C2.0采用的是Modern版本的Runtime,只能在iOS和OS X10.5之后的64位程序上运行。32位程序采用Legacy版本的Runtime。它们的区别在于更改实例变量时,Legary需要重新编译其子类,而Modern则不需要。
Runtime 主要做了哪些事?
1、封装:在这个库中,对象可以用 C 语言中的结构体表示,而方法可以用函数来实现,另外再加上了一些额外的特性。这些结构体和函数被 Runtime 封装后,我们就可以在程序运行时,创建、检查、修改类、对象和它们的方法了。
2、找到方法的最终执行代码(消息机制):当程序执行[object doSomething]时,会向消息接收者(object)发送一条消息(doSomething),Runtime 会根据消息接收者是否能响应消息而做出不同的反应。
2、讲一下MVC和MVVM,MVP?
这三种模式都由如下三个实体组成
Models - 负责主要的数据或者操作数据的数据访问层
Views - 负责展示层(GUI)
Controller/Presenter/ViewModel - 负责协调Model和View, 通常根据用户在View上的动作在Model上作出对应的更改, 同时将更改的信息返回到View上
其中MVC又是其他MV(X)的基础, 所以我们首先就来看看MVC模式
MVC
传统的MVC
传统的MVC模式是长这个样子滴(引自Wiki)
从箭头的指向可以明显地看出传统MVC的缺点
- 耦合性很强 - 三个实体间相互都有通信
- 复用性较低
理想的Cocoa MVC
Apple在iOS开发中对传统MVC进行了改进, “期望”的效果是这样的
即Controller是一个介于View和Model之间的协调器, 而View和Model之间没有任何直接的联系
这个模式解决了传统MVC部分耦合和复用的问题, 但是当你的逻辑不断地复杂时
- 很容易就变成了这种MVC(Massive View Controller)
对于这种理想的MVC,斯坦福大学公开课: iOS 7应用开发有更详细的说明
实际开发中的Cocoa MVC
虽然理想化的MVC有胖VC的风险, 但已经算是蛮不错的结构了(胖VC可以转嫁成胖Model, 并不断重构和抽象VC中的逻辑)
但是当你真正开发iOS开发时, 你会发现实际的Cocoa MVC并不像人们期望的那样
怎么回事? View和View Controller被紧紧绑在了一起?! 说好的分离, 说好的解耦, 说好的复用, 一切都是骗人的!
鉴于Cocoa的这种设计, iOS应用架构谈 view层的组织和调用方案一文中有这样一种理解和解释:
将与ViewController绑定的View理解成ViewContainner, 那么ViewController就还是那个Controller了
所以总体来说, 实际开发中的Cocoa MVC并不是严格意义上的MVC, 它在实际开发中有如下一些问题
耦合性 - View和Model确实是分开的, 但是View和Controller却是紧密耦合的
测试性 - 由于只有Model的完全独立的, 所以只有对Model进行单元测试是相比比较容易的
复用性 - 尤其是View的复用, 这点开发过Android的同学, 都知道xml的复用相比xib或手写布局强太多
MVP
MVP将MVC中的Controller换成了Presenter, 结构如下
看这个轮廓, 不就是MVC么? 简单地说, MVP就是理想化的MVC!
那么MVP是如何应用到iOS开发中来的呢? 简单地说, MVP中就是将MVC中的View Controller当做View!
这样, MVP就解决了实际开发中Cocoa MVC的几个缺点, 一切看起来如此地完美? 错!
增加了代码量 - 单单增加Presenter就会增加许多新的文件和类, 更何况之前View的delegate从View Controller变成了Presenter
增加了第三方库接入成本 - 鉴于以往的开发经验, 第三方UI库大多是基于Cocoa MVC(注意这里是实际而非理想版)的, 所以如果是用继承方式接入的话, 会带来额外成本(至于继承的优劣, 本文暂不深入讨论)
MVVM
鉴于MVP或者说是理想的Cocoa MVC已经是比较完美的结构模式了, 那么为什么还又多出一个MVVM来呢?
因为不管是MVC还是MVP都有一个问题, Controller和Presenter这个”协调员”太关键了, 代码稍微写多点, 它们就变成了Massive Controller或者Massive Presenter
那么如何给Controller或Presenter减负呢?
答案就是让View和Model自己去沟通, 让Controller和Presenter从”协调员”降级成”介绍者”
So what is the View Model in the iOS reality? It is basically UIKit independent representation of your View and its state. The View Model invokes changes in the Model and updates itself with the updated Model, and since we have a binding between the View and the View Model, the first is updated accordingly.
请注意这里的accordingly, 因为它指出了MVVM最重要的一个特点: 响应式
MVVM几个热门的实现都是完全的函数响应式编程
MVVM这么强大, 难道会是”银弹”不成?
遗憾地说, MVVM也不是”银弹”, 这是因为在实际的移动平台开发中, 从主流的Objective-C和Java切换至函数响应式思维, 都有不小的学习和培训成本
鉴于此, ReactiveCocoa或者RxJava应该根据项目的实际情况, 由小到多地逐渐在开发中应用开来, 而不必全盘照搬
3、为什么代理要用weak?代理的delegate和dataSource有什么区别?block和代理的区别?block和代理的区别?
- 为什么代理要用weak?
防止循环引用。例如View有一个协议,需要一个代理实现回调。一个Controller添加这个View,并且遵守协议,成为View的代理。如果不用week,用strong,Controller ->View -> delegate -> Controller,就循环引用了。
- 代理的delegate和dataSource有什么区别?
delegate偏重于与用户交互的回调,有那些方法可以供我使用,例如UITableviewDelegate;dataSource偏重于数据的回调,view里面有什么东西,属性都是什么,例如UITableviewDatasource;
- block和代理的区别:
首先两者作用是一样的,都是进行单一回调。不同的是,delegate是个对象,然后用过一个对象自己调用代理协议函数来完成整个流程。block是传递一个函数指针,利用函数指针执行来进行回调。还有在内存管理上需要注意,delegate不需要保存引用。block对引用数据有copy的处理。
4、属性的实质是什么?包括哪几个部分?属性默认的关键字都有哪些?@dynamic关键字和@synthesize关键字是用来做什么的?
属性的组成: @property = ivar + getter + setter;
实例变量+get方法+set方法,也就是说使用@property 系统会自动生成setter和getter方法;
我们经常使用assign,weak,strong,copy,nonatomic,atomic,readonly等关键字,下面我们列个表格去归纳一下属性关键字具体作用:
1 | @synthesize和@dynamic区别, 在声明property属性后,有2种实现选择: |
5、属性的默认关键字是什么?
对于基本数据类型默认关键字是atomic,readwrite,assign
对于普通的OC对象atomic,readwrite,strong
6.NSString为什么要用copy关键字,如果用strong会有什么问题?(注意:这里没有说用strong就一定不行。使用copy和strong是看情况而定的)
copy 赋值拷贝了对象,指向新的内存地址,使用strong不拷贝直接指向赋值对象的内存地址
7.如何令自己所写的对象具有拷贝功能?
- 需声明该类遵从 NSCopying 协议
- 实现 NSCopying 协议。该协议只有一个方法:
(NSMutableCopying)1
2- (id)copyWithZone:(NSZone*)zone;
- (id)mutableCopyWithZone:(NSZone*)zone;
8、可变集合类 和 不可变集合类的 copy 和 mutablecopy有什么区别?如果是集合是内容复制的话,集合里面的元素也是内容复制么?
首先我们要先明白一个概念,什么是浅复制,单层深复制,完全复制(每一层都深复制)
浅复制也就是所说的指针复制,并没有进行对象复制;
单层深复制,也就是我们经常说的深复制,我这里说的单层深复制是对于集合类所说的(即NSArray,NSDictionary,NSSet),单层深复制指的是只复制了该集合类的最外层,里边的元素没有复制,(即这两个集合类的地址不一样,但是两个集合里所存储的元素的地址是一样的);
完全复制,指的是完全复制整个集合类,也就是说两个集合地址不一样,里边所存储的元素地址也不一样;
明白了这三个概念之后,我们就来说一下他们的区别所在:
非集合类(NSString,NSNumber)1
2
3
4[immutableObject copy] //浅复制
[immutableObject mutableCopy] //深复制
[mutableObject copy] //深复制
[mutableObject mutableCopy] //深复制
结论:不可变进行copy是浅复制,mutableCopy是深复制,可变的copy,mutableCopy都是深复制
集合类(NSArray,NSDictionary, NSSet):
1 |
|
结论:不可变进行copy是浅复制,mutableCopy是单层深复制,可变的copy,mutableCopy都是单层深复制
那么如何实现多层复制呢?
以NSArray举例说明
1 |
|
需要特别注意的是
以上我们所说的两种情况默认都实现了NSCopying和NSMutableCopying协议
对于自定义继承自NSObject的类
copy需要实现NSCopying协议,然后实现以下方法,否则copy会crash
1 |
|
9.为什么IBOutlet修饰的UIView也适用weak关键字?
因为当我们将控件拖到Storyboard上,相当于新创建了一个对象,而这个对象是加到视图控制器的view上,view有一个subViews属性,这个属性是一个数组,里面是这个view的所有子view,而我们加的控件就位于这个数组中,那么说明,实际上我们的控件对象是属于view的,也就是说view对加到它上面的控件是强引用。当我们使用Outlet属性的时候,我们是在viewController里面使用,而这个Outlet属性是有view来进行强引用的,我们在viewController里面仅仅是对其使用,并没有必要拥有它,所以是weak的。
如果将weak改为strong,也是没有问题的,并不会造成强引用循环。当viewController的指针指向其他对象或者为nil,这个viewController销毁,那么对控件就少了一个强引用指针。然后它的view也随之销毁,那么subViews也不存在了,那么控件就又少了一个强引用指针,如果没有其他强引用,那么这个控件也会随之销毁。
不过,既然没有必将Outlet属性设置为strong,那么用weak就好了;一个控件可以在viewController里面有多个Outlet属性,就相当于一个对象,可以有多个指针指向它(多个引用)。但是一个Outlet属性只能对应一个控件,也就是说,如果有button1和button2,button1在viewController里面有一个名为button的Outlet属性,此时button指向button1,但是如果用button2给button重新赋值,那么此时button指向button2。也就是说,后来的覆盖原来的。
一个控件可以在viewController里面触发多个IBAction。比如有一个button控件,在viewController里面有几个方法,那么点击button,会触发所有的这些方法。如果我有多个控件,比如button1,button2,button3,它们也可以同时绑定一个buttonClick方法,无论点击button1,button2还是button3,都会触发这个buttonClick方法。
上面说了,button1,button2,button3有可能都触发buttonClick方法,如果想在buttonClick方法里面区分到底是哪个button触发的可能有好几种做法。
可以给这三个button各设置一个Outlet属性,然后在buttonClick里面判断sender和哪个Outlet属性是同一对象,这样就可以区分了。但是很明显,这样并不合理,因为创建的三个属性有些浪费。
我们可以给三个button各加一个tag,在buttonClick里面通过switch(或者if…)判断,sender的tag和给各个button加上的tag是否一致,如果一致则为同一对象。要慎用tag。因为view有一个viewWithTag:方法,可以在view的子view里面找到和我们传入的tag相同的view,这样哪怕不给这个控件创建Outlet属性,也可以通过tag找到这个对象。但是很明显,这个方法要遍历子view,比较每个子view的tag,这样效率并不高,所以尽量要避免这种情况。
10.nonatomic和atomic的区别?atomic是绝对的线程安全么?为什么?如果不是,那应该如何实现?
在默认情况下,由编译器所合成的方法会通过锁定机制确保其原子性(atomicity)。如果属性具备nonatomic特质,则不需要同步锁。
下面说一下atomic与nonatomic的区别:
具备atomic特质的获取方法会通过锁定机制来确保其操作的原子性。也就是说,如果两个线程同时读取一个属性,那么不论何时,总能看到有效的属性值。
如果不加锁的话(或者说使用nonatomic语义),那么当其中一个线程正在改写某属性值的时候,另外一个线程也许会突然闯入,把尚未修改好的属性值读取出来。发证这种情况时,线程读取道德属性值肯能不对。
一般iOS程序中,所有属性都声明为nonatomic。这样做的原因是:
在iOS中使用同步锁的开销比较大, 这会带来性能问题。一般情况下并不要求属性必须是“原子的”,因为这并不能保证“线程安全”(thread safety),若要实现“线程安全”的操作,还需采用更为深层的锁定机制才行。
例如:一个线程在连续多次读取某个属性值的过程中有别的线程在同时改写该值,那么即便将属性声明为atomic,也还是会读取到不同的属性值。因此,iOS程序一般都会使用nonatomic属性。但是在Mac OS X程序时, 使用atomic属性通常都不会有性能瓶颈;
然而atomic一定是线程安全的么,回答是NO :
nonatomic的内存管理语义是非原子性的,非原子性的操作本来就是线程不安全,而atomic的操作是原子性的,但并不意味着他就是线程安全的,它会增加正确的几率,能够更好的避免线程错误,但仍旧是不安全的。
为了说atomic与nonatomic的本质区别其实也就是在setter方法上的操作不。nonatomic的实现:
1 |
|
The object passed to the @synchronized directive is a unique identifier used to distinguish the protected block. If you execute the preceding method in two different threads, passing a different object for the anObj parameter on each thread, each would take its lock and continue processing without being blocked by the other. If you pass the same object in both cases, however, one of the threads would acquire the lock first and the other would block until the first thread completed the critical section.
As a precautionary measure, the @synchronized block implicitly adds an exception handler to the protected code. This handler automatically releases the mutex in the event that an exception is thrown. This means that in order to use the @synchronized directive, you must also enable Objective-C exception handling in your code. If you do not want the additional overhead caused by the implicit exception handler, you should consider using the lock classes.
For more information about the @synchronized directive, see The Objective-C Programming Language.
 当使用atomic时,虽然对属性的读和写是原子性的,但是仍然可能出现线程错误:当线程A进行写操作,这时其他线程的读或者写操作会因为等该操作而等待。当A线程的写操作结束后,B线程进行写操作,所有这些不同线程上的操作都将依次顺序执行——也就是说,如果一个线程正在执行 getter/setter,其他线程就得等待。如果有线程C在A线程读操作之前release了该属性,那么还会导致程序崩溃。所以仅仅使用atomic并不会使得线程安全,我们还要为线程添加lock来确保线程的安全。
 更准确的说应该是读写安全,但并不是线程安全的,因为别的线程还能进行读写之外的其他操作。线程安全需要开发者自己来保证。
 其实无论是否是原子性的只是针对于getter和setter而言,比如用atomic去操作一个NSMutableArray ,如果一个线程循环读数据,一个线程循环写数据,肯定会产生内存问题,这个就跟getter和setter就木有关系了。
11.UICollectionView自定义layout如何实现?
关于自定义UICollectionViewLayout的一点个人理解
12.用StoryBoard开发界面有什么弊端?如何避免?
难以维护
Storyboard在某些角度上,是难以维护的。我所遇到过的实际情况是,公司一个项目的2.0版本,设计师希望替换原有字体。然而原来项目的每一个Label都是采用Storyboard来定义字体的,因此替换新字体需要在Storyboard中更改每一个Label。
幸亏我们知道Storyboard的源文件是XML,最终写了一个读取-解析-替换脚本来搞定这件事。
性能瓶颈
当项目达到一定的规模,即使是高性能的MacBook Pro,在打开Storyboard是也会有3-5秒的读取时间。无论是只有几个Scene的小东西,还是几十个Scene的庞然大物,都无法避免。Scene越多的文件,打开速度越慢(从另一个方面说明了分割大故事板的重要性)。
让人沮丧的是,这个造成卡顿的项目规模并不是太难达到。
我猜想是由于每一次打开都需要进行I/O操作造成的,Apple对这一块的缓存优化没有做到位。可能是由于Storyboard占用了太多内存,难以在内存中进行缓存。Whatever,这个问题总是让人困扰的。
然而需要指出的是,采用Storyboard开发或采用纯代码开发的App,在真机的运行效率上,并没有太大的区别。
错误定位困难
Storyboard的初学者应该对此深有体会。排除BAD_EXCUSE错误不说,单单是有提示的错误,就足以让人在代码和Storyboard之间来回摸索,却无法找到解决方案。
一个典型的例子是,在代码中删除了IBOUTLET属性或者IBAction方法,但是却忘了在Storyboard中删除对应的连接,运行后crash。然而控制台只会输出一些模糊其词的错误描述。1
2
3
4
*** Terminating app due to uncaught exception 'NSUnknownKeyException',
reason: '[ setValue:forUndefinedKey:]:
this class is not key value coding-compliant for the key drawButton.'
最后一方面是其提供的便利,另一方面是Apple对Storyboard的大力支持。这一点宏观上看,可以在以往对Storyboard的改进和增强上看出,微观上看,几乎所有iOS 8之后的simple code都或多或少采用了Storyboard作为界面开发工具;
那改如何避免这些弊端呢, 参考以下文章:
iOS项目开发实战——storyboard设置界面技巧与注意事项
13.进程和线程的区别?同步异步的区别?并行和并发的区别?
进程和线程:
进程中所包含的一个或多个执行单元称为线程(thread)。比如一个应用程序就是一个进程, 而它又包含了多个线程;主要差别在于它们是不同的操作系统资源管理方式。进程有独立的地址空间,一个进程崩溃后,在保护模式下不会对其它进程产生影响,而线程只是一个进程中的不同执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但在进程切换时,耗费资源较大,效率要差一些。但对于一些要求同时进行并且又要共享某些变量的并发操作,只能用线程,不能用进程;
同步和异步:
在进行网络编程时,我们通常会看到同步、异步、阻塞、非阻塞四种调用方式以及他们的组合。
其中同步方式、异步方式主要是由客户端(client)控制的,具体如下:
同步(Sync)
所谓同步,就是发出一个功能调用时,在没有得到结果之前,该调用就不返回或继续执行后续操作。
根据这个定义,Java中所有方法都是同步调用,应为必须要等到结果后才会继续执行。我们在说同步、异步的时候,一般而言是特指那些需要其他端协作或者需要一定时间完成的任务。
简单来说,同步就是必须一件一件事做,等前一件做完了才能做下一件事。
例如:B/S模式中的表单提交,具体过程是:客户端提交请求->等待服务器处理->处理完毕返回,在这个过程中客户端浏览器不能做其他事。
异步(Async)
异步与同步相对,当一个异步过程调用发出后,调用者在没有得到结果之前,就可以继续执行后续操作。当这个调用完成后,一般通过状态、通知和回调来通知调用者。对于异步调用,调用的返回并不受调用者控制。
总结来说,同步和异步的区别:请求发出后,是否需要等待结果,才能继续执行其他操作。
并行(parallellism)和并发(concurrency)的区别:
并行是指两个或者多个事件在同一时刻发生;而并发是指两个或多个事件在同一时间间隔发生。
并行,是每个cpu运行一个程序;
并发,是在同一个cpu上同时(不是真正的同时,而是看来是同时,因为cpu要在多个程序间切换)运行多个程序;
并发和并行的区别
14.线程间通信?
使用全局变量主要由于多个线程可能更改全局变量,因此全局变量最好声明为violate
使用消息实现通信在Windows程序设计中,每一个线程都可以拥有自己的消息队列(UI线程默认自带消息队列和消息循环,工作线程需要手动实现消息循环),因此可以采用消息进行线程间通信sendMessage,postMessage。
1 |
|
使用事件CEvent类实现线程间通信
Event对象有两种状态:有信号和无信号,线程可以监视处于有信号状态的事件,以便在适当的时候执行对事件的操作。
1 |
|
线程间的通信、同步方式与进程间通信方式
15.GCD的一些常用的函数?(group,barrier,信号量,线程同步)
1 |
|
16.如何使用队列来避免资源抢夺?
ios多线程——锁(解决多线程抢夺同一块资源的问题)
17.数据持久化的几个方案(fmdb用没用过)
1 |
|
18.说一下AppDelegate的几个方法?从后台到前台调用了哪些方法?第一次启动调用了哪些方法?从前台到后台调用了哪些方法?
– (BOOL)application:(UIApplication )application didFinishLaunchingWithOptions:(NSDictionary )launchOptions NS_AVAILABLE_IOS(3_0);
当应用程序启动时(不包括已在后台的情况下转到前台),调用此回调。launchOptions是启动参数,假如用户通过点击push通知启动的应用,这个参数里会存储一些push通知的信息。– (void)applicationDidBecomeActive:(UIApplication *)application;
// Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
当应用程序全新启动,或者在后台转到前台,完全激活时,都会调用这个方法。如果应用程序是以前运行在后台,这时可以选择刷新用户界面。
- – (void)applicationDidEnterBackground:(UIApplication *)application NS_AVAILABLE_IOS(4_0);
// Use this method to release shared resources, save user data, invalidate timers, and store enough application state information to restore your application to its current state in case it is terminated later.
// If your application supports background execution, this method is called instead of applicationWillTerminate: when the user quits.
当用户从前台状态转入后台时,调用此方法。使用此方法来释放资源共享,保存用户数据,无效计时器,并储存足够的应用程序状态信息的情况下被终止后,将应用 程序恢复到目前的状态。如果您的应用程序支持后台运行,这种方法被调用,否则调用applicationWillTerminate:用户退出。
- – (void)applicationWillEnterForeground:(UIApplication *)application NS_AVAILABLE_IOS(4_0);
// Called as part of the transition from the background to the inactive state; here you can undo many of the changes made on entering the background.
1 |
|
19.NSCache优于NSDictionary的几点?
NSCache胜过NSDictionary之处在于,当系统资源将要耗尽时,它可以自动删减缓存。如果采用普通的字典,那么就要自己编写挂钩,在系统发出“低内存”通知时手工删减缓存。
NSCache并不会“拷贝”键,而是会“保留”它。此行为用NSDictionary也可以实现,然而需要编写相当复杂的代码。NSCache对象不拷贝键的原因在于:很多时候,键都是不支持拷贝操作的对象来充当的。因此,NSCache不会自动拷贝键,所以说,在键不支持拷贝操作的情况下,该类用起来比字典更方便。另外,NSCache是线程安全的,而NSDictionary则绝对不具备此优势。
20.知不知道Designated Initializer?使用它的时候有什么需要注意的问题?
iOS: 聊聊 Designated Initializer(指定初始化函数)
正确编写Designated Initializer的几个原则
21.实现description方法能取到什么效果?
一般情况下,我们在使用NSLog 和 %@ 输出某个对象时,就会调用这个对象的 description 方法,它的返回值就是 NSString 字符串类型,所以 description 默认实现返回的格式是 <类名: 对象的内存地址>
以上输出实现的具体步骤为:
1 |
|
那么,既然description方法的默认实现是返回类名和对象的内存地址,所以在必要情况下,我们需要重写description方法以达到改变输出结果目的,覆盖description方法的默认实现,比如重写上述代码 Person 类的 description方法,返回_age和_name成员变量的值:
重写完description方法后,再调用NSLog(@”%@”,p)时输出结果不再是<类名: 内存地址>,而是返回的字符串:
22.objc使用什么机制管理对象内存?
MRC(manual retain-release)手动内存管理
ARC(automatic reference counting)自动引用计数
Garbage collection (垃圾回收)。但是iOS不支持垃圾回收, ARC作为LLVM3.0编译器的一项特性, 在iOS5.0 (Xcode4) 版本后推出的。
ARC的判断准则, 只要没有强指针指向对象, 对象就会被释放.
iOS开发系列—Objective-C之内存管理
23.block
block可以访问外部变量么?
当block时,如果里面用到外部变量,会先把外部变量从栈区【以const的方式拷贝】到【堆区(block是对象,一般在堆区)】。因此可以访问外部变量的值,但是无法改变外部变量的值。
在block中能否定义新的变量?
可以,而且在block内部定义的变量是在【栈区】分配空间的
如何做到在block内修改外部变量?
【可以】可以用__block修饰外部变量