iOS 中的 HotFix 方案总结详解

相信HotFix大家应该都很熟悉了,今天主要对于最近调研的一些方案做一些总结。iOS中的HotFix方案大致可以分为四种:

  • WaxPatch(Alibaba)
  • Dynamic Framework(Apple)
  • React Native(Facebook)
  • JSPatch(Tencent)

WaxPatch

WaxPatch是一个通过Lua语言编写的iOS框架,不仅允许用户使用 Lua 调用 iOS SDK和应用程序内部的 API, 而且使用了 OC runtime 特性调用替换应用程序内部由 OC 编写的类方法,从而达到HotFix的目的。

WaxPatch的优点在于它支持iOS6.0,同时性能上比较的优秀,但是缺点也是非常的明显,不符合Apple3.2.2的审核规则即不可动态下发可执行代码,但通过苹果JavaScriptCore.framework或WebKit执行的代码除外;同时Wax已经长期没有人维护了,导致很多OC方法不能用Lua实现,比如Wax不支持block;最后就是必须要内嵌一个Lua脚本的执行引擎才能运行Lua脚本;Wax并不支持arm64框架。

Dynamic Framework

动态的Framework,其实就是动态库;首先我介绍一下关于动态库和静态库的一些特性以及区别。

不管是静态库还是动态库,本质上都是一种可执行的二进制格式,可以被载入内存中执行。

iOS上的静态库可以分为.a文件和.framework,动态库可以分为.dylib(xcode7以后变成了.tdb)和.framework。

静态库: 链接时完整地拷贝至可执行文件中,被多次使用就有多份冗余拷贝。

动态库: 链接时不复制,程序运行时由系统动态加载到内存,供程序调用,系统只加载一次,多个程序共用,节省内存。

静态库和动态库是相对编译期和运行期的:静态库在程序编译时会被链接到目标代码中,程序运行时将不再需要改静态库;而动态库在程序编译时并不会被链接到目标代码中,只是在程序运行时才被载入,因为在程序运行期间还需要动态库的存在。

总结:同一个静态库在不同程序中使用时,每一个程序中都得导入一次,打包时也被打包进去,形成一个程序。而动态库在不同程序中,打包时并没有被打包进去,只在程序运行使用时,才链接载入(如系统的框架如UIKit、Foundation等),所以程序体积会小很多。

好,所以Dynamic Framework其实就是我们可以通过更新App所依赖的Framework方式,来实现对于Bug的HotFix,但是这个方案的缺点也是显而易见的它不符合Apple3.2.2的审核规则,使用了这种方式是上不了Apple Store的,它只能适用于一些越狱市场或者公司内部的一些项目使用,同时这种方案其实并不适用于BugFix,更适合App线上的大更新。所以其实我们项目中的引入的那些第三方的Framework都是静态库,我们可以通过file这个命令来查看我们的framework到底是属于static还是dynamic。

React Native

React Native支持用JavaScript进行开发,所以可以通过更改JS文件实现App的HotFix,但是这种方案的明显的缺点在于它只适合用于使用了React Native这种方案的应用。

JSPatch

JSPatch是只需要在项目中引入极小的JSPatch引擎,就可以使用JavaScript语言调用Objective-C的原生接口,获得脚本语言的能力:动态更新iOS APP,替换项目原生代码、快速修复bug。但是JSPatch也有它的自己的缺点,主要在由于它要依赖javascriptcore,framework,而这个framework是在iOS7.0以后才引入进来,所以JSPatch是不支持iOS6.0的,同时由于使用的是JS的脚本技术,所以在内存以及性能上面是要低于Wax的。

所以最后当然还是采用了JSPatch这种方案,但是实际过程中还是出现了一些问题的,所以掌握JSPatch的核心原理对于我们解决问题是非常有帮助的。

关于JSPatch的核心原理讲解

预加载部分

关于核心原理的讲解,网上有不少,但是几乎都是差不多,很多都还是引用了作者bang自己写的文档的内容,所以我采用一个例子方式进行讲解JSPatch的主要运行流程,其实当然也会引用一些作者的简述,大家可以参照我写的流程讲述,在配合源码或者官方文档的介绍,应该就可以了解JSPatch。

[JPEngine startEngine];

NSString *sourcePath = [[NSBundle mainBundle] pathForResource:@"demo" ofType:@"js"];

NSString *script = [NSString stringWithContentsOfFile:sourcePath encoding:NSUTF8StringEncoding error:nil];

[JPEngine evaluateScript:script];

首先是运行[JPEngine startEngine]启动JSPatch,启动过程分为一下两部分:

  • 通过JSContext,声明了一些JS方法到内存,这样我们之后就可以在JS中调用这些方法,主要常用到的包括以下几个方法,同时会监听一个内存告警的通知。

  • 加载JSPatch.js文件,JSPatch文件的主要内容在于定义一些我们之后会用在的JS函数,数据结构以及变量等信息,之后我会在用到的时候详细介绍。


脚本运行

我们定义如下的脚本:

require(‘UIAlertView‘)

defineClass(‘AppDelegate‘,[‘name‘, ‘age‘, ‘temperatureDatas‘],

{

testFuncationOne: function(index) {

self.setName(‘wuyike‘)

self.setAge(21)

self.setTemperatureDatas(new Array(37.10, 36.78, 36.56))

var alertView = UIAlertView.alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles(

"title", self.name(), self, "OK", null)

alertView.show()

}

},

{

testFuncationTwo: function(datas) {

var alertView = UIAlertView.alloc().initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles(

"title", "wwww", self, "OK", null)

alertView.show()

}

});

然后就是执行我们上面说的[JPEngine evaluateScript:script]了,程序开始执行我们的脚本,但是在这之前,JSpatch会对我们的脚本做一些处理,这一步同样也包括两个方面:

  • 需要给我们的程序加上try catch的部分代码,主要目的是当我们的JS脚本有错误的时候,可以catch到错误信息
  • 将所有的函数都改成通过__c原函数的形式进行调用。

也就是最后我们调用的脚本已经变成如下的形式了:

;(function(){try{require(‘UIAlertView‘)

defineClass(‘AppDelegate‘,[‘name‘, ‘age‘, ‘temperatureDatas‘],

{

testFuncationOne: function(index) {

self.__c("setName")(‘wuyike‘)

self.__c("setAge")(21)

self.__c("setTemperatureDatas")(new Array(37.10, 36.78, 36.56))

var alertView = UIAlertView.__c("alloc")().__c("initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles")(

"title", self.__c("name")(), self, "OK", null)

alertView.__c("show")()

}

},

{

testFuncationTwo: function(datas) {

var alertView = UIAlertView.__c("alloc")().__c("initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles")(

"title", "wwww", self, "OK", null)

alertView.__c("show")()

}

});

那么为什么需要用函数__c来替换我们的函数呢,因为JS语法的限制对于没有定义的函数JS是无法调用的,也就是调用UIAlertView.alloc()其实是非法的,因为它采用的并不是消息转发的形式,所以作者原来是想把一个类的所有函数都定义在JS上,也就是如下形式:

{

__clsName: "UIAlertView",

alloc: function() {…},

beginAnimations_context: function() {…},

setAnimationsEnabled: function(){…},

...

}

但是这种形式就必须要遍历当前类的所有方法,还要循环找父类的方法直到顶层,这种方法直接导致的问题就是内存暴涨,所以是不可行的,所以最后作者采用了消息转发的思想,定义了一个_c的原函数,所有的函数都通过_c来转发,这样就解决了我们的问题。

值得一提的是我们的__c函数就是在我们执行JSPatch.js的时候声明到js里的Object方法里去的,就是下面这个函数,_customMethods里面声明了很多需要追加在Object上的函数。

for (var method in _customMethods) {

if (_customMethods.hasOwnProperty(method)) {

Object.defineProperty(Object.prototype, method, {value: _customMethods[method], configurable:false, enumerable: false})

}

}



1. require

调用 require(‘UIAlertView’) 后,就可以直接使用UIAlertView这个变量去调用相应的类方法了,require做的事很简单,就是在JS全局作用域上创建一个同名变量,变量指向一个对象,对象属性 __clsName 保存类名,同时表明这个对象是一个 OC Class。

var _require = function(clsName) {

if (!global[clsName]) {

global[clsName] = {

__clsName: clsName

}

}

return global[clsName]

}

这样我们在接下来调用UIAlertView.__c()方法的时候系统就不会报错了,因为它已经是JS中一个全局的Object对象了。

{

__clsName: "UIAlertView"

}



2.defineClass

接下来我们就要执行defineClass函数了

global.defineClass = function(declaration, properties, instMethods, clsMethods)

defineClass函数可接受四个参数:

字符串:”需要替换或者新增的类名:继承的父类名 ”

[属性]

{实例方法}

{类方法}

当我调用这个函数以后主要是做三件事情:

  • 执行_formatDefineMethods方法,主要目的是修改传入的function函数的的格式,以及在原来实现上追加了从OC回调回来的参数解析。
  • 然后执行_OC_defineClass方法,也就是调用OC的方法,解析传入类的属性,实例方法,类方法,里面会调用overrideMethod方法,进行method swizzing操作,也就是方法的重定向。
  • 最后执行_setupJSMethod方法,在js中通过_ocCls记录类实例方法,类方法。


关于_formatDefineMethods

_formatDefineMethods方法接收的参数是一个方法列表js对象,加一个新的js空对象

可以发现,具体实现是遍历方法列表对象的属性(方法名),然后往js空对象中添加相同的属性,它的值对应的是一个数组,数组的第一个值是方法名对应实现函数的参数个数,第二个值是一个函数(也就是方法的具体实现)。_formatDefineMethods作用,简单的说,它把defineClass中传递过来的js对象进行了修改:

原来的形式是:

{

testFuncationOne:function(){...}

修改之后是:

{

testFuncationOne: [argCount, function (){...新的实现}]

}

传递参数个数的目的是,runtime在修复类的时候,无法直接解析原始的js实现函数,那么就不知道参数的个数,特别是在创建新的方法的时候,需要根据参数个数生成方法签名,也就是还原方法名字,所以只能在js端拿到js函数的参数个数,传递到OC端。

// js 方法

initWithTitle_message_delegate_cancelButtonTitle_otherButtonTitles

// oc 方法

initWithTitle:message:delegate:cancelButtonTitle:otherButtonTitles:



关于_OC_defineClass

  1. 使用NSScanner分离classDeclaration,分离成三部分
    • 类名 : className
    • 父类名 : superClassName
    • 实现的协议名 : protocalNames
  2. 使用NSClassFromString(className)获得该Class对象。
    • 若该Class对象为nil,则说明JS端要添加一个新的类,使用objc_allocateClassPair与objc_registerClassPair注册一个新的类。
    • 若该Class对象不为nil,则说明JS端要替换一个原本已存在的类
  3. 根据从JS端传递来的实例方法与类方法参数,为这个类对象添加/替换实例方法与类方法
    • 遍历protocalNames,通过objc_getProtocol方法获得协议对象,再使用protocol_copyMethodDescriptionList来获得协议中方法的type和name。匹配JS中传入的selectorName,获得typeDescription字符串,对该协议方法的实现消息转发。
    • 若不是上述两种情况,则js端请求添加一个新的方法。构造一个typeDescription为”@@:\@*”(返回类型为id,参数值根据JS定义的参数个数来决定。新增方法的返回类型和参数类型只能为id类型,因为在JS端只能定义对象)的IMP。将这个IMP添加到类中。
    • 添加实例方法时,直接使用上一步得到class对象; 添加类方法时需要调用objc_getMetaClass方法获得元类。
    • 如果要替换的类已经定义了该方法,则直接对该方法替换和实现消息转发。
    • 否则根据以下两种情况进行判断
  4. 为该类添加setProp:forKey和getProp:方法,使用objc_getAssociatedObject与objc_setAssociatedObject让JS脚本拥有设置property的能力
  5. 返回{className:cls}回JS脚本。

不过其中还包括一个overrideMethod方法,不管是替换方法还是新增方法,都是使用overrideMethod方法。它的目的主要在于进行method swizzing操作,也就是方法的重定向。我们把所有的消息全部都转发到ForwardInvocation函数里去执行(不知道的同学请自行补消息转发机制),这样做的目的在于,我们可以在NSInvocation中获取到所有的参数,这样就可以实现一个通用的IMP,任意方法任意参数都可以通过这个IMP中转,拿到方法的所有参数回调JS的实现。于是overrideMethod其实就是做了如下这件事情:

具体实现,以替换 UIViewController 的 -viewWillAppear: 方法为例:

  1. 把UIViewController的-viewWillAppear:方法通过class_replaceMethod()接口指向_objc_msgForward这是一个全局 IMP,OC 调用方法不存在时都会转发到这个IMP上,这里直接把方法替换成这个IMP,这样调用这个方法时就会走到-forwardInvocation:。
  2. 为UIViewController添加-ORIGviewWillAppear:和-_JPviewWillAppear: 两个方法,前者指向原来的IMP实现,后者是新的实现,稍后会在这个实现里回调JS函数。
  3. 改写UIViewController的-forwardInvocation: 方法为自定义实现。一旦OC里调用 UIViewController 的-viewWillAppear:方法,经过上面的处理会把这个调用转发到-forwardInvocation:,这时已经组装好了一个NSInvocation,包含了这个调用的参数。在这里把参数从 NSInvocation反解出来,带着参数调用上述新增加的方法 -JPviewWillAppear:,在这个新方法里取到参数传给JS,调用JS的实现函数。整个调用过程就结束了,整个过程图示如下:


关于_setupJSMethod

if (properties) {

properties.forEach(function(o){

_ocCls[className][‘props‘][o] = 1

_ocCls[className][‘props‘][‘set‘ + o.substr(0,1).toUpperCase() + o.substr(1)] = 1

})

}

var _setupJSMethod = function(className, methods, isInst, realClsName) {

for (var name in methods) {

var key = isInst ? ‘instMethods‘: ‘clsMethods‘,

func = methods[name]

_ocCls[className][key][name] = _wrapLocalMethod(name, func, realClsName)

}

}

是最后的一步是把之前所有的方法以及属性放入 _ocCls中保存起来,最后再调用require把类保存到全局变量中。

到这一步为止,我们的JS脚本中的所有对象已经,通过runtime替换到我们的程序中去了,也就是说,剩下的就是如何在我们出触发函数以后,能正确的去执行JS中函数的内容。

3. 对象持有/转换

下面引用作者的一段话:

require(‘UIView‘)这句话在JS全局作用域生成了UIView这个对象,它有个属性叫 __isCls,表示这代表一个OC类。调用UIView这个对象的alloc()方法,会去到_c()函数,在这个函数里判断到调用者_isCls 属性,知道它是代表OC类,把方法名和类名传递给OC完成调用。 调用类方法过程是这样,那实例方法呢?UIView.alloc()会返回一个UIView实例对象给JS,这个OC实例对象在JS是怎样表示的?怎样可以在 JS 拿到这个实例对象后可以直接调用它的实例方法UIView.alloc().init()?

对于一个自定义id对象,JavaScriptCore 会把这个自定义对象的指针传给JS,这个对象在JS无法使用,但在回传给OC时,OC可以找到这个对象。对于这个对象生命周期的管理,按我的理解如果JS有变量引用时,这个OC对象引用计数就加1,JS变量的引用释放了就减1,如果OC上没别的持有者,这个OC对象的生命周期就跟着 JS走了,会在JS进行垃圾回收时释放。 传回给JS的变量是这个OC对象的指针,这个指针也可以重新传回OC,要在JS调用这个对象的某个实例方法,根据第2点JS接口的描述,只需在_c()函数里把这个对象指针以及它要调用的方法名传回给OC就行了,现在问题只剩下:怎样在_c()函数里判断调用者是一个OC对象指针? 目前没找到方法判断一个JS对象是否表示 OC 指针,这里的解决方法是在OC把对象返回给JS之前,先把它包装成一个NSDictionary:

static NSDictionary *_wrapObj(id obj) {

return @{@"__obj": obj};

}

让 OC 对象作为这个 NSDictionary 的一个值,这样在 JS 里这个对象就变成:

{__obj: [OC Object 对象指针]}

这样就可以通过判断对象是否有_obj属性得知这个对象是否表示 OC 对象指针,在_c函数里若判断到调用者有_obj属性,取出这个属性,跟调用的实例方法一起传回给OC,就完成了实例方法的调用。

但是:

JS无法调用 NSMutableArray / NSMutableDictionary / NSMutableString 的方法去修改这些对象的数据,因为这三者都在从OC返回到JS时 JavaScriptCore 把它们转成了JS的Array/Object/String,在返回的时候就脱离了跟原对象的联系,这个转换在JavaScriptCore里是强制进行的,无法选择。

若想要在对象返回JS后,回到OC还能调用这个对象的方法,就要阻止JavaScriptCore的转换,唯一的方法就是不直接返回这个对象,而是对这个对象进行封装,JPBoxing 就是做这个事情的。

把NSMutableArray/NSMutableDictionary/NSMutableString对象作为JPBoxing的成员保存在JPBoxing实例对象上返回给JS,JS拿到的是JPBoxing对象的指针,再传回给OC时就可以通过对象成员取到原来的NSMutableArray/NSMutableDictionary/NSMutableString对象,类似于装箱/拆箱操作,这样就避免了这些对象被JavaScriptCore转换。

实际上只有可变的NSMutableArray/NSMutableDictionary/NSMutableString这三个类有必要调用它的方法去修改对象里的数据,不可变的NSArray/NSDictionary/NSString是没必要这样做的,直接转为JS对应的类型使用起来会更方便,但为了规则简单,JSPatch让NSArray/NSDictionary/NSString也同样以封装的方式返回,避免在调用OC方法返回对象时还需要关心它返回的是可变还是不可变对象。最后整个规则还是挺清晰:NSArray/NSDictionary/NSString 及其子类与其他 NSObject 对象的行为一样,在JS上拿到的都只是其对象指针,可以调用它们的OC方法,若要把这三种对象转为对应的JS类型,使用额外的.toJS()的接口去转换。

对于参数和返回值是C指针和 Class 类型的支持同样是用 JPBoxing 封装的方式,把指针和Class作为成员保存在JPBoxing对象上返回给JS,传回OC时再解出来拿到原来的指针和Class,这样JSPatch就支持所有数据类型OCJS的互传了。

 

4. 类型转换

还是引用作者的一段话:

JS把要调用的类名/方法名/对象传给OC后,OC调用类/对象相应的方法是通过NSInvocation实现,要能顺利调用到方法并取得返回值,要做两件事:

  1. 取得要调用的 OC 方法各参数类型,把 JS 传来的对象转为要求的类型进行调用。
  2. 根据返回值类型取出返回值,包装为对象传回给 JS。

例如举例子的来讲view.setAlpha(0.5),JS传递给OC的是一个NSNumber,OC需要通过要调用OC方法的 NSMethodSignature得知这里参数要的是一个float类型值,于是把NSNumber转为float值再作为参数进行OC方法调用。这里主要处理了int/float/bool等数值类型,并对CGRect/CGRange等类型进行了特殊转换处理。

5. callSelector

callSelector这个就是我们最后执行函数了!但是在执行这个函数之前,前面还有不少东西。

关于 _c函数

其实_c函数就是一个消息转发中心,它根据传入的参数,这里可以分为两种类型来讲述:

  • 对于实例方法和类方法,最后会调用_methodFunc方法
  • 对于自定义的属性,set和get操作。

对于自定义的属性,其实它并不会将这些属性真正添加到OC中的对象里去,它只会添加一个_ocProps对象,然后在JS中,通过_ocProps对象来保存我们所有定义的属性,要获取值的只要从这个属性里通过name获取就可以了。

对于_methodFunc方法,其实就是将OC方法的名字还原,带上参数,然后转发给类方法或者实例方法处理。

var _methodFunc = function(instance, clsName, methodName, args, isSuper, isPerformSelector) {

var selectorName = methodName

if (!isPerformSelector) {

methodName = methodName.replace(/__/g, "-")

selectorName = methodName.replace(/_/g, ":").replace(/-/g, "_")

var marchArr = selectorName.match(/:/g)

var numOfArgs = marchArr ? marchArr.length : 0

if (args.length > numOfArgs) {

selectorName += ":"

}

}

var ret = instance ? _OC_callI(instance, selectorName, args, isSuper):

_OC_callC(clsName, selectorName, args)

return _formatOCToJS(ret)

}

对于callSelector方法来讲:

  1. 初始化
    • 将JS封装的instance对象进行拆装,得到OC的对象;
    • 根据类名与selectorName获得对应的类对象与selector;
    • 通过类对象与selector构造对应的NSMethodSignature签名,再根据签名构造NSInvocation对象,并为invocation对象设置target与Selector
  2. 根据方法签名,获悉方法每个参数的实际类型,将JS传递过来的参数进行对应的转换(比如说参数的实际类型为int类型,但是JS只能传递NSNumber对象,需要通过[[jsObj toNumber] intValue]进行转换)。转换后使用setArgument方法为NSInvocation对象设置参数。
  3. 执行invoke方法。
  4. 通过getReturnValue方法获取到返回值。
  5. 根据返回值类型,封装成JS中对应的对象(因为JS并不识别OC对象,所以返回值为OC对象的话需封装成{className:className, obj:obj})返回给JS端。


总结

好,到现在为止,我们所有的流程就已经走完了,我们的js文件也已经生效了。当然,我所说JSPatch原理只是基础的一部份原理,可以使我们的基本流程可以实现,还有一些复杂的操作功能,还需要再深入的学习,才可以掌握,JSPatch对于学习Runtime也是一个不错的例子,就像Aspects一样,大家可以去好好研究一下。

时间: 2024-11-02 23:26:46

iOS 中的 HotFix 方案总结详解的相关文章

iOS中 三种随机数方法详解

ios 有如下三种随机数方法: 1 2 3 4 5 6 7 8 9 10 //第一种 srand((unsigned)time(0)); //不加这句每次产生的随机数不变 int i = rand() % 5; //第二种 srandom(time(0)); int i = random() % 5; //第三种 int i = arc4random() % 5 ; 注: ① rand()和random()实际并不是一个真正的伪随机数发生器,在使用之前需要先初始化随机种子,否则每次生成的随机数一

Swift使用WKWebView在iOS应用中调用Web的方法详解

这篇文章主要介绍了Swift使用WKWebView在iOS应用中调用Web的方法详解,使用WKWebView便等于使用和Safari中相同的JavaScript解释器,用来替代过去的UIWebView,需要的朋友可以参考下 自从iOS8开始,Apple引入了WKWebView欲代替UIWebView.相比而言,WKWebView消耗内从更少,功能也更加强大.让我们来看看WKWebView怎么使用吧! 0.初始化(1)首先需要引入WebKit库 复制代码代码如下: #import <WebKit/

第6章 数据存储全方案,详解持久化技术

第6章 数据存储全方案,详解持久化技术 所有的App都可以说是与数据打交道的,离开数据它们什么都不是.那么平时我们怎么存储一些关键的数据呢? 1 持久化技术简介 数据持久化就是指将那些内存中的瞬时数据保存到存储设备中,保证即使在手机或电脑关机的情况下,这些数据仍然不会丢失.保存在内存中的数据是处于瞬时状态的,而保存在存储设备中的数据是处于持久状态的,持久化技术则是提供了一种机制可以让数据在瞬时状态和持久状态之间进行转换. Android系统中主要提供了三种方式用于简单地实现数据持久化功能,即文件

&quot;MindManager&quot;学习iOS系列之&quot;CAAnimation-核心动画&quot;详解,让你的应用“动”起来。

"MindManager"学习iOS系列之"CAAnimation-核心动画"详解,思维导图内展示了CAAnimation-核心动画的大多数基本功能和知识,每个part都有代码讲解,展示出CAAnimation-核心动画的清晰轮廓,编者提供了"JPG"."SWF"."PDF"."Word"."Mmap"格式的源文件供给使用.注意:JPG格式仅为图片总览,SWF格式使用

IOS开发学习笔记(2)-----UIButton 详解

1. [代码][C/C++]代码     //这里创建一个圆角矩形的按钮    UIButton *button1 = [UIButton buttonWithType:UIButtonTypeRoundedRect];    //    能够定义的button类型有以下6种,//    typedef enum {//        UIButtonTypeCustom = 0,          自定义风格//        UIButtonTypeRoundedRect,        

php中的PDO函数库详解

PHP中的PDO函数库详解 PDO是一个“数据库访问抽象层”,作用是统一各种数据库的访问接口,与mysql和mysqli的函数库相比,PDO让跨数据库的使用更具有亲和力:与ADODB和MDB2相比,PDO更高效.目前而言,实现“数据库抽象层”任重而道远,使用PDO这样的“数据库访问抽象层”是一个不错的选择. PDO中包含三个预定义的类 PDO中包含三个预定义的类,它们分别是 PDO.PDOStatement 和 PDOException. 一.PDO PDO->beginTransaction(

css3中font-face属性的用法详解

@font-face是CSS3中的一个模块,他主要是把自己定义的Web字体嵌入到你的网页中,随着@font-face模块的出现,我们在Web的开发中使用字体不怕只能使用Web安全字体,你们当中或许有许多人会不自然的问,这样的东西IE能支持吗?当我告诉大家@font-face这个功能早在IE4就支持了你肯定会感到惊讶.我的Blog就使用了许多这样的自定义Web字体,比如说首页的Logo,Tags以及页面中的手写英文体,很多朋友问我如何使用,能让自己的页面也支持这样的自定义字体,一句话这些都是@fo

iOS企业证书网页分发全过程详解(图文并茂史无前例的详细哦)

iOS企业证书网页分发全过程详解 苹果的企业级证书发布的应用,是不用设备授权即可直接安装,并且不限设备上限.为了方便分发,苹果有协议实现通过网页链接直接下载安装企业级的应用. 首先需要说明它的原理:基本的原理就是在生成企业证书授权的ipa的同时,要生成一个对应的plist文件,plist文件中会配置ipa的下载地址.版本信息.Bundle ID 等信息,通过网页下载的时候其实下载的是这个plist文件,然后苹果通过自己的协议根据plist文件的配置信息去自动的下载安装app. 这里有个地址用来生

oc中字典的实现方法详解

一:字典的基本概念 Foundation中的字典(NSDictionary,NSMutableDictionary)是由键-值对组成的数据集合.正如,我们在字典里查找单词的定义一样. 通过key(键),查找的对应的value(值),key通常是字符串对象,也可以是其他任意类型对象.在一个字典对象中,key的值必须是唯一的. 此外,字典对象的键和值不可以为空(nil),如果需要在字典中加入一个空值,可以加入NSNull对象 二:不可变字典-NSDictionary 1:初始化(以一个元素和多个元素