Mdiep

编程就像搭积木


  • Home

  • Archives

Vue组件三-Slot分发内容

Posted on 2018-05-13 | In Vue |

Vue组件三-Slot分发内容

开始

Vue组件是学习Vue框架最比较难的部分,而这部分难点我认为可以分为三个部分学习,即

  1. 组件的传值 - 父组件向子组件中传值
  2. 事件回馈 - 子组件向父组件发送消息,父组件监听消息
  3. 分发内容

整片博客使用的源代码-请点击

所以将用三篇博客分别进行介绍以上三种情况和使用

木头楔子/插槽

在学习内容分发之前,我们先了解一个木工会经常使用的一种拼接两个家具的接口——木头楔子。它的作用就是将两个家具组件,通过这个木头楔子连接拼合到一起。

而Vue中的内容分发,其实就是子组件提供了一个”木头楔子”,让父组件可以将一些内容嵌入到子组件中。这个就是内容分发的思想。接下来,学习下内容分发的几种常见的格式。

木头楔子

具体的实例

内容分发主要分为两种语法模式,不具名的slot和具名的slot,对于具名的slot,在使用的时候需要指定父组件的标签的slot名称。这部分标签将嵌入到具名的slot部分。

内容分发-单个slot

父组件中模版的定义

<div>
    <h4>组件八-内容分发1-单个slot</h4>
    <component-slot-child-9 component-name="单个slot例子">
        <p>这是来自父组件的p标签内容</p>
    </component-slot-child-9>
</div>

子组件的定义

Vue.component("component-slot-child-9", {
    props: ["componentName"],
    template: "<div><h5>{{componentName}}</h5><slot>只有父组件有需要slot(插槽)才能生效</slot></div>"
})

内容分发-具名的slot

父组件中模版的定义

<div>
    <h4>组件九-内容分发-具名的slot</h4>
    <component-slot-child-10 component-name="具名的slot">
        <strong slot="companyName">好人生集团</strong>
        <p slot="scope">好人生旗下员工人数1000人</p>
        <sub>好人生成立于2008年,十年历史,专注于健康。全民的健康才是我们的最求</sub>
        <br />
        <sub>积极 * 正直 * 责任 * 卓越 * 团队合作 * 客户第一</sub>
    </component-slot-child-10>
</div>

子组件的定义

Vue.component("component-slot-child-10", {
    props: ["componentName"],
    template: "<div><h5>{{componentName}}</h5><slot name='companyName'></slot><slot name='scope'></slot><slot></slot></div>"
})

作用域插槽slot

在一些情况下,父组件即将嵌入到子组件插槽中的标签需要动态获取子组件内部的数据,这个时候则需要规范出一种方式,使子组件的数据可以在即将插入插槽的时候则可以使用。这个便是作用域插槽的作用。

需要注意的作用域插槽,需要使用<template></template>标签包裹插槽。

作用域插槽-简单使用

父组件定义

<div>
    <h4>组件十-作用域插槽-简单使用</h4>
    <component-slot-child-11>
        <template slot-scope="ps">
            <small>组件内部给组件的插槽进行传值</small>
            <br />
            <span>{{ps.city.name}}</span>
            <span>{{ps.city.area}}</span>
            <span>{{ps.city.numbers}}</span>
        </template>
    </component-slot-child-11>
</div>

子组件的定义

Vue.component("component-slot-child-11", {
    template: "<div><slot v-bind:city='city'></slot></div>",
    data: function() {
        return {
            city: {
                name: "上海",
                area: "100平方公里",
                numbers: "2000万人"
            }
        }
    }
})

这里通过对<slot></slot>绑定一个名字为city的对象,将子组件的数据可以在父组件定义的时候使用。

作用域插槽-复杂使用-列表自定义使用

父组件模版定义

<div>
    <h4>组件十一-作用域插槽-复杂使用-列表自定义使用</h4>
    <component-slot-child-12 v-bind:items="languages">
        <template slot="ul-list-child-12" slot-scope="props">
            <li style="color: red;">{{props.text}}</li>
        </template>
    </component-slot-child-12>
</div>

子组件定义

Vue.component("component-slot-child-12", {
    props: ["items"],
    template: "<ul><slot name='ul-list-child-12' v-for='item in items' v-bind:text='item'>默认</slot></ul>",
    data: function() {
        return {
            messageDefault: "this is li default message"    
        }
    }
})

如上,将列表的li标签作为外部传入的,当需要修改列表样式时,只需更换父组件的插槽部分即可,不需要更改组件部分。

动态切换组件

在一些情况下,定义了多个组件,需要只显示其中一个或者一部分,这个时候则需要动态的切换组件

通过使用保留的<component>元素,动态地绑定到它的is特性,我们让多个组件可以使用同一个挂载点,并动态切换:dynamicComponent

var vm = new Vue({
  el: '#example',
  data: {
    dynamicComponent: "AppHeader"
  },
  components: {
        AppHeader: {
            props: ["initialText"],
            template: "<div><strong>{{title}}</strong></div>",
            data: function() {
                return {
                    title: this.initialText
                }
            }
        },
        AppFooter: {
            props: ["initialText"],
            template: "<div><sub>{{footerTitle}}</sub></div>",
            data: function() {
                return {
                    footerTitle: this.initialText
                }
            }
        },
        AppMain: {
            props: ["initialText"],
            template: "<div style='color:blue;'>{{mainContent}}</div>",
            data: function() {
                return {
                    mainContent: this.initialText
                }
            }
        }
    }
})

父组件模版

<div>
    <h4>组件十二-动态组件</h4>
    <button v-on:click="changeCurrentComponent">点击切换不同的组件-{{dynamicComponent}}</button>
    <keep-alive>
        <component :initial-text="dynamicComponent" v-bind:is="dynamicComponent"></component>
    </keep-alive>
</div>

如果把切换出去的组件保留在内存中,可以保留它的状态或避免重新渲染。则可以添加一个 keep-alive

总结

至此,关于组件学习的大部分内容基本学习完毕。

参考:

1. Vue-组件

2. Vue-插槽

Vue组件二-事件反馈 - 子组件向父组件发送消息,父组件监听消息

Posted on 2018-05-11 | In Vue |

开始

Vue组件是学习Vue框架最比较难的部分,而这部分难点我认为可以分为三个部分学习,即

  1. 组件的传值 - 父组件向子组件中传值
  2. 事件回馈 - 子组件向父组件发送消息,父组件监听消息
  3. 分发内容

整个博客使用的源代码-请点击

所以将用三篇博客分别进行介绍以上三种情况和使用

消息监听,消息发送

在理解Vue事件之前,可以简单理解一下消息中心的设计模式,如下图,即每一个订阅者,都可以去订阅消息。而消息会提供一个”消息名称”,订阅者可以通过”消息名称”,订阅特定的消息。一定订阅者订阅了消息,则只要发出消息,订阅者就会被触发。

消息中心模型

而在Vue中,通过v-on去订阅一个消息,通过emit发出一个消息。

这两个特有的模式是v-on:message-name="someMethod"订阅,this.$emit("message-name")发送一个消息。此时someMethod会被触发调用。

具体的实例

父组件和子组件的事件响应中,主要分为四种情况

  1. “v-on”/“@”绑定事件(@是对v-on的缩写)
  2. 绑定原生事件
  3. .sync同步父组件和子组件之间的props
  4. 兄弟组件进行通信

“v-on”或”@”绑定事件

父组件中模版的定义

<div>
    <h4>组件四-"v-on"绑定事件</h4>
    <span>{{sumOfTotal}}</span>
    <br />
    <!--'@'是'v-on:'监听器的简写-->
    <component-span-child-4 v-on:increment-total="incrementWithTotal"></component-span-child-4>
    <component-span-child-4 @increment-total="incrementWithTotal"></component-span-child-4>
    <component-span-child-4 @increment-total="incrementWithTotal"></component-span-child-4>
</div>

子组件的定义

Vue.component("component-span-child-4", {
    template: "<button v-on:click='incrementOfButtonCounter'>{{counter}}</button>",
    data: function() {
        return {
            counter: 0
        }
    },
    methods: {
        incrementOfButtonCounter: function() {
            this.counter = this.counter + 1;
            // post a notification of increment counter
            // 'increment-total' 相当于一个通知名称,在父组件中,会检测一个同名的通知名称
            this.$emit("increment-total");
        }
    }
})

子组件在点击事件触发的时候,会发送一个消息名称为"increment-total"的消息,而在父组件中,订阅了这个名称的消息。所以父组件可以响应子组件的通知

绑定原生事件

父组件中模版的定义

<div>
    <h4>组件五-绑定原生事件</h4>
    <span>{{nativeSumOfTotal}}</span>
    <br />
    <component-span-child-5 v-on:click.native="nativeDoThing"></component-span-child-5>
</div>

子组件的定义

Vue.component("component-span-child-5", {
    template: "<button>检测原生事件-点击</button>"
})

通过v-on:click.native="nativeDoThing"订阅原生的事件。这里没有emit关键字,可以理解为这个消息是原生组件发送出的,但是订阅还是通过v-on

.sync同步父组件和子组件之间的props

在一些情况下,我们可能会需要对一个 prop 进行『双向绑定』。当一个子组件改变了一个 prop 的值时,这个变化也会同步到父组件中所绑定的值。这很方便,但也会导致问题,因为它破坏了『单向数据流』的假设。

父组件中模版的定义

<div>
    <h4>组件六-.sync同步父组件和子组件之间的props</h4>
    父组件中的值: {{food}}
    <component-span-child-6 :food.sync=food></component-span-child-6>
    <component-span-child-6 v-bind:food.sync=food></component-span-child-6>
    <!--扩展之后的模版-->
    <component-span-child-6 v-bind:food=food v-on:update:food="val => food = val"></component-span-child-6>
</div>

子组件中的定义

Vue.component("component-span-child-6", {
    props: ["food"],
    template: "<div>{{selectedFood}}<button v-on:click='changeSelectedFood'>点击选择其他食物</button></div>",
    data: function() {
        return {
            selectedFood: this.food,
            foods: ["米饭", "水果", "青菜", "沙拉"]            
        }
    },
    methods: {
        changeSelectedFood: function() {
            var idx = this.foods.indexOf(this.selectedFood);
            if (idx == -1 || idx == this.foods.length - 1) {
                idx = 0;
            } else {
                idx += 1;
            }
            this.selectedFood = this.foods[idx];
            this.$emit('update:food', this.selectedFood);
        }
    }
})

通过父组件中国呢三种写法(功能都是一样的,只是由上而下,将模版扩展开写,以窥探.sync的作用),其实.sync其实会扩展出一个v-on:update:food订阅消息,并且在收到消息,进行了对原值的修改。
而在子组件中,依旧通过this.$emit('update:food')发送一个消息出来

这个就是.sync真正做了什么。

兄弟组件进行通信

两个不是父子组件的组件如何通信,可以定一个中间总线(中介的意思),通过中间中间总线订阅消息,中间总线发送消息,完成两个组件之间的通信。如下

父组件模版的定义

<div>
    <h4>组件七-兄弟组件进行通信</h4>
    <component-span-child-7 component-name="组件7"></component-span-child-7>
    <component-span-child-8 component-name="组件8"></component-span-child-8>
</div>

子组件的定义

var bus = new Vue();
Vue.component("component-span-child-7", {
    props: ["componentName"],
    template: "<div><span>{{componentName}}</span>:<span>{{counter}}</span></div>",
    data: function() {
        return {
            counter: 0
        }
    },
    mounted: function() {
        // 此处在monuted阶段监听'notificationFromPartner',需要用bind方法绑定当前的this,否则回调function中的this则是bus实例,而不是当前Vue的实例
        bus.$on("notificationFromPartner", function() {
            this.counter += 1;
        }.bind(this));
    }
})
Vue.component("component-span-child-8", {
    props: ["componentName"],
    template: "<button v-on:click='componentClickPushMessage'>{{componentName}}</button>",
    methods: {
        componentClickPushMessage: function() {
            bus.$emit("notificationFromPartner");
        }
    }
})

组件7在装载mounted后,通过bus.$on("notificationFromPartner", callbackFunction)订阅了notificationFromPartner消息,而在组件8中,通过bus.$emit("notificationFromPartner");发送出这个消息。则订阅者就可以响应消息。

总结

在学习Vue中,组件作为十分重要的一个组成部分,对组件的通信的理解也十分重要。对于组件间的事件反馈,应该先理解消息中心的设计模式,则能更快的理解其中的原理。则不用纠结一些语法特性比较奇怪。不用纠结为什么v-on要和emit匹配、为什么只需要v-on就可以监听原生native事件、为什么.sync可以实现props的同步等一系列问题。

整个博客使用的源代码-请点击

Vue组件一-父组件传值给子组件

Posted on 2018-05-08 | In Vue |

Vue组件-父组件传值给子组件

开始

Vue组件是学习Vue框架最比较难的部分,而这部分难点我认为可以分为三个部分学习,即

  1. 组件的传值 - 父组件向子组件中传值
  2. 事件回馈 - 子组件向父组件发送消息,父组件监听消息
  3. 分发内容

整个博客使用的源代码-请点击

所以将用三篇博客分别进行介绍以上三种情况和使用

Vue的设计者对组件的理解

Vue的设计者,对组件和父组件之间的关系流上做了阐述,即单向数据流图:父组件向子组件传递数据,子组件回馈事件

组件意味着协同工作,通常父子组件会是这样的关系:组件 A 在它的模板中使用了组件 B。它们之间必然需要相互通信:父组件要给子组件传递数据,子组件需要将它内部发生的事情告知给父组件。然而,在一个良好定义的接口中尽可能将父子组件解耦是很重要的。这保证了每个组件可以在相对隔离的环境中书写和理解,也大幅提高了组件的可维护性和可重用性。

在 Vue 中,父子组件的关系可以总结为 props down, events up。父组件通过 props 向下传递数据给子组件,子组件通过 events 给父组件发送消息。看看它们是怎么工作的。

属性下传,事件上传

父组件挂载的实例

上文提到的三篇文章,都使用一个父组件挂载对象,内容比较长(可以选择不看,直接看props的使用),感兴趣可以到git上去看源代码

模版:

<body>
    <div id="el-component-id"></div>
<body

Vue实例:

var vm = new Vue({
    el: "#el-component-id",
    data: {
        welcome: "welcome to Vue",
        parentMessage: "this is parent message",
        iMessage: "",
        person: {
            name: "小明",
            from: "江苏",
            to: "江西",
            purpose: "喝一杯牛奶"
        },
        persons: 10,
        sumOfTotal: 0,
        nativeSumOfTotal: 0,
        food: "牛肉",
        languages: ["英语", "中文", "希腊语", "法语", "俄罗斯语"],
        dynamicComponent: "AppHeader"
    },
    methods: {
        incrementWithTotal: function() {
            this.sumOfTotal = this.sumOfTotal + 1;
        },
        nativeDoThing: function() {
            this.nativeSumOfTotal += 1;
        },
        changeCurrentComponent: function() {
            let components = ["AppHeader", "AppFooter", "AppMain"];
            let idx = components.indexOf(this.dynamicComponent);
            if (idx == 2 || idx == -1) {
                idx = 0;
            } else {
                ++idx;
            }
            this.dynamicComponent = components[idx];
        }
    },
    components: {
        AppHeader: {
            props: ["initialText"],
            template: "<div><strong>{{title}}</strong></div>",
            data: function() {
                return {
                    title: this.initialText
                }
            }
        },
        AppFooter: {
            props: ["initialText"],
            template: "<div><sub>{{footerTitle}}</sub></div>",
            data: function() {
                return {
                    footerTitle: this.initialText
                }
            }
        },
        AppMain: {
            props: ["initialText"],
            template: "<div style='color:blue;'>{{mainContent}}</div>",
            data: function() {
                return {
                    mainContent: this.initialText
                }
            }
        }
    }
});

1. props传递单个参数

组件定义:

// 使用props数组的形式进行传递参数
Vue.component("component-span-child-1", {
    props: ["message"],
    template: "<span>{{message}}</span>"
})

模版中进行传值:

<div>
    <h4>组件一-props传递单个参数</h4>
    // 字面量传值
    <component-span-child-1 message="component-style-one"></component-span-child-1>
    <br />
    // 绑定父组件对象实例属性 v-bind:someProperty简写为:someProperty
    <component-span-child-1 :message="parentMessage"></component-span-child-1>
    <br />
    <component-span-child-1 v-bind:message="parentMessage"></component-span-child-1>
    <br />
    <input v-model="iMessage" placeholder="请输入值"/>
    <component-span-child-1 :message="iMessage"></component-span-child-1>
</div>

2. props传递多个参数

组件定义:

Vue.component("component-span-child-2", {
    props: ["name", "from", "to", "purpose"],
    template: "<div><span>{{name}}从{{from}}到{{to}},{{purpose}}</span></div>"
})

模版中传值:

<div>
    <h4>组件二-props传递多个参数</h4>
    // 字面量传值
    <component-span-child-2 name="小李" from="南京" to="北京" purpose="去买个书包"></component-span-child-2>
    // 父组件实例对象属性传值
    <component-span-child-2 :name="person.name" :from="person.from" :to="person.to" :purpose="person.purpose"></component-span-child-2>
</div>

3. 使用props对象高级传参,并对参数进行校验

组件定义:

可以校验传递进来的属性,例如:1. 校验类型 2. 是否必须传递 3. 提供默认值 4. 通过函数校验,如校验Number类型是否大于某个值

Vue.component("component-span-child-3", {
    props: {
        name: {
            type: String,
            require: true
        },
        persons: {
            type: Number,
            default: 1,
            validator: function(value) {
                return value > 0;
            }
        },
        location: {
            type: String,
            default: "上海"
        },
        action: {
            type: String,
            default: "拉粑粑"
        }
    },
    template: "<div><span>{{name}}和{{persons}}个人,去{{location}}里面{{action}}</span></div>"
})

模版中使用:

<div>
    <h4>组件三-使用props对象传递参数,和校验</h4>
    <component-span-child-3 name="小狗" :persons="persons" location="讲述郾城" action="去淘金啊"></component-span-child-3>
</div>

总结

父组件向子组件主要是通过props关键字,主要使用情况可以分为上面描述的三种。props的封装可以是一个数组,也可以是对象。

  1. 当使用数组封装props的时候,只是简单将父组件的参数传递给子组件使用,此处的参数可以是对象,字符串,number类型的数据
  2. 当使用对象封装props的时候,可以更加高级的校验参数,比如参数类型,默认值,参数大小等一系列校验。当不符合时候,可以看到Vue再控制台给出错误警告

熟练掌握父组件向子组件传递参数的方法,可以对Vue的关键部分更快的理解。

RSA,AES加密之间的区别-对加密理解的补充

Posted on 2018-04-16 |

1、对称加密

对称加密就是加密和解密使用同一个密钥。

用数学公示表示就是:

▲加密:Ek(P) = C

▲解密:Dk(C) = P

这里E表示加密算法,D表示解密算法,P表示明文,C表示密文。

是不是看起来有点不太容易理解?看下图:

AES256对称加密示意图

看过间谍局的知友们一定知道电台和密码本的功能。潜伏里面孙红雷通过电台收听到一堆数字,然后拿出密码本比对,找到数字对应的汉字,就明白上级传达的指令。而军统的监听台没有密码本,只看到一堆没有意义的数字,这就是对称算法的原理。

AES就属于对称加密,常见的对称加密方法还有DES、3DES、Blowfish、RC2以及国密的SM4。

2、非对称加密

对称加密快而且方便,但是有个缺点——密钥容易被偷或被破解。

非对称加密就可以很好的避免这个问题。非对称算法把密钥分成两个,一个自己持有叫私钥,另一个发给对方,还可以公开,叫公钥,用公钥加密的数据只能用私钥解开。

▲加密: E公钥(P) = C

▲解密::D私钥(C) = P

RSA1024示意图

这下就不用担心密钥被对方窃取或被破解了,私钥由自己保管。

非对称加密算法核心原理其实就是设计一个数学难题,使得用公钥和明文推导密文很容易,但根据公钥、明文和密文推导私钥极其难。

RSA就属于非对称加密,非对称加密还有Elgamal、背包算法、Rabin、D-H、ECC(椭圆曲线加密算法)以及国家商用密码SM2算法。

3、AES和RSA

AES和RSA都很安全,至少在目前的计算机体系结构下,没有任何有效的攻击方式。量子计算机时代,RSA有一定的破绽,因为利用shro’s algorithm,量子计算机穷举计算质因子速度可以提高N个数量级,能够在有限的时间内破解RSA密钥。AES256至少目前并没有什么明显的漏洞。

AES作为对称加密技术,加密速度很快。现在高端一点的CPU都带有AES-NI指令,可以极快的完成加密和解密。

但是AES作为对称加密技术,如何安全的分发密钥是一个难题。通过任何方式传递密钥都有泄密的风险。当然,目前我国高大上的量子通信技术或许能很好的解决这个问题。

RSA作为非对称加密技术的代表,加解密的速度其实相当慢,只能对小块的数据进行加解密。但是其非对称的特点,满足公钥可以随处分发,只有公钥能解密私钥加密的数据,只有私钥能解密公钥加密的数据。所以很适合用来进行密钥分发和身份验证,这两个应用场景刚好相反。

1)用于对称秘钥分发的场景,其他人用公钥加密对称的秘钥,那么只有授权人才持有私钥,因此才能解密获得对应的秘钥,解决了AES密钥分发的难题;

2)对于身份验证的场景,授权人用私钥加密一段指令,其他人用公钥解密对应的数据,验证对应的指令与之前约定的某些特征一致,如果一致,那么可以确认这个指令就是授权人发出的。

相关趣闻轶事

RSA除了是一个伟大的发明,被免费开放给所有互联网用户使用。它的发明者还以此成立了一家名为RSA Security的网络安全公司,这家公司最后被EMC高价收购。这是德艺双馨的伟大证明, 是“又红又专”的典范。

RSA的算法是以三个发明者的名字命名的,三位都是成功的数学家,科学家和企业家,其中的排名第一Ron Rivest,有非常多的杰出贡献。

RSA是整个互联网数据安全的基础,与光纤处于同样基础和重要的方式。大部分的加密和解密的应用都是同时应用RSA和AES。作为新一代的Http协议的升级版——Https就是在Http的基础添加了以RSA和AES为基础的SSL/TLS加密层。用以保护网上信息传递的安全。

总结

  1. 破解加密的难度除了跟加密方法有关,还跟密钥长度以及加密模式有很大的关系,就拿AES来说,有AES128和AES256(代表密钥长度),显然AES256的安全性能比AES128更高,而AES又要四种模式:ECB、CBC、CFB、OFB(代表加密模式)。

  2. RSA1024是属于非对称加密,是基于大整数因式分解难度,也就是两个质数相乘很容易,但是找一个大数的质因子非常困难。量子计算机时代,RSA有一定的风险,具体可以参考:超链接

  3. AES256目前没有明显的漏洞,唯一的问题就是如何安全的分发密钥。

  4. 现在大部分的加密解密都是同时应用RSA和AES,发挥各自的优势,使用RSA进行密钥分发、协商,使用AES进行业务数据的加解密。

参考

RSA 1024和AES 256,这两种加密算法理论上哪种更安全?

HTTPS 加密了什么内容

WKWebView相比于UIWebView浏览器之间内核引擎的区别

Posted on 2018-04-15 | In iOS , WebView |

WKWebView相比于UIWebView浏览器之间内核引擎的区别

翻译文,原文地址

WKWebView: Differences from UIWebView browsing engine

优点

多进程,在app的主进程之外执行

使用更快的Nitro JavaScript引擎

异步执行处理JavaScript

消除某些触摸延迟

支持服务端的身份校验

支持对错误的自签名安全证书和证书进行身份验证

-

问题

需要iOS9或更高版本(WKWebView在iOS8引入,但是很多功能,支持比较全面在iOS9以后的版本)

不支持通过AJAX请求本地存储的文件

不支持”Accept Cookies”的设置

不支持”Advanced Cache Settings”(高级缓存设置)

App退出会清除HTML5的本地存储的数据

不支持记录WebKit的请求

不能进行截屏操作

优点(Advantages)

多进程,在app的主进程之外执行

WKWebView为多进程组件,也意味着会从App内存中分离内存到单独的进程(Network Process and Rendring Process)中。当内存超过了系统分配给WKWebView的内存时候,会导致WKWebView浏览器崩溃白屏,但是App不会Crash。(app会收到系统通知,并且尝试去重新加载页面)

相反的,UIWebView是和app同一个进程,UIWebView加载页面占用的内存被计算为app内存占用的一部分,当app超过了系统分配的内存,则会被操作系统crash。在整个过程中,会经常收到iOS系统的通知用来防止app被系统kill,但是在某些时候,这些通知不够及时,或者根本没有返回通知。

使用更快的Nitro JavaScript引擎

WKWebView使用和手机Safari浏览器一样的Nitro JavaScript引擎,相比于UIWebView的JavaScript引擎有了非常重要的性能提升

异步执行处理JavaScript

WKWebView是异步处理app原生代码与JavaScript之间的通信,因此普遍上执行速度会更快。

在实践操作过程中,JavaScript API调用原生(native)中方法不会阻塞线程等待回调函数的执行。(在JavaScript代码会继续向下执行,而回调函数会由native异步去回调)。举一个例子,之前一个”Save Data”的操作如下:

// JavaScript code - 笔者注
// 注释 - 笔者注

var filenameID;
function getFilenameID() {
    // 向native端发起请求,获取kioskId,结果返回由callback方式返回 
    window.kp_requestKioskId("kp_requestKioskId_callback");
}
// callback回调函数 - 由native端发起 
function kp_requestKioskId_callback(kioskId) {
    filenameID = kioskId.split(" ").join("");
}
// kp_FileAPI_writeToFile方法不会等待kp_requestKioskId_callback回调函数执行,此时filenameID为undefined
function saveData(fileName, data) {
    getFilenameID();
    kp_FileAPI_writeToFile(filenameID + ".xls", data, "writeFile_callback");
}

原先的假定是在’saveData’方法被触发时,在’kp_FileAPI_writeToFile’方法调用前,’getFilenameID’方法会返回filenameID

但是,在WKWebView中JavaScript和native代码之间的通信是异步的,’kp_FileAPI_writeToFile’方法被调用之前,’getFilenameID’方法还没有完成(回调还没有被执行-笔者注),导致的结果是在’kp_FileAPI_writeToFile’中filename为undefined。为了正确的得到filename,必须重构之前的代码,在callback中完成。如下:

在回调完成之后进行文件的操作

var filenameID;
function getFilenameID() {
    window.kp_requestKioskId("kp_requestKioskId_callback");
}
function kp_requestKioskId_callback(kioskId) {
    filenameID = kioskId.split(" ").join("");
    kp_FileAPI_writeToFile(filenameID + ".xls", data, "writeFile_callback");
}
function saveData(fileName, data) {
    getFilenameID();
}

消除触摸延迟

UIWebView和WKWebView浏览器组件会将触摸事件解释后发送给app,因此,我们无法提高触摸事件的灵敏度或速度。

在UIWebView上的任何触摸事件会被延迟300ms,用以判断用户是单击还是双击。这个机制也是那些基于HTML的web app一直不被用户接受的重要原因。

在WKWebView中,测试显示,只有在点击很快(<~125ms)的时候才会添加300ms的延迟,iOS将其解释为更可能是双击“点击缩放”手势的一部分,而不是慢点击(>〜125 ms)后。更多细节在这里

为了消除所有触摸事件(包括快速点击)的触摸延迟,您可以添加FastClick或另一个消除此延迟的库到您的内容中。

支持服务端的身份校验

与不支持服务器认证校验的UIWebView不同,WKWebView支持服务端校验。实际上,这意味着在使用WKWebView时,可以输入密码保护网站。

支持对错误的自签名安全证书和证书进行身份验证

通过“继续”/“取消”弹出窗口,WKWebView允许您绕过安全证书中的错误(例如,使用自签名证书或过期证书时)。

问题 - 缺点

需要iOS9或更高版本

我们的WKWebView集成仅适用于运行iOS 9或更高版本的设备。虽然WKWebView是在iOS 8中引入的,但在这些版本中存在重大限制,包括无法访问本地存储的文件,我们无法解决此问题,因此此功能不兼容。

不支持AJAX请求到本地存储的文件

WKWebView不允许XHR请求file://URI,因为这些URI违反了浏览器引擎的跨源资源共享规则。使用这种类型的请求的项目应该远程托管在服务器上,或使用现有的UIWebView浏览引擎。

不支持”Accept Cookies”的设置

虽然WKWebView确实支持使用cookies,但并没有公开选择哪些cookies被源代码接受的能力。这意味着在使用WKWebView浏览引擎时不会应用“接受Cookie”设置。

WKWebView只允许我们访问cookie的名称,而不是附加信息,如创建/过期日期或路径,这使得更难以解决Cookie出现的问题。

不支持”Advanced Cache Settings”(高级缓存设置)

使用WKWebView浏览引擎时,不会应用“缓存源”和“仅通知服务器重定向事件的浏览器”。

App退出会清除HTML5的本地存储的数据

当应用退出并重新启动时,HTML5本地存储将被清除。

不支持记录WebKit的请求

WKWebView发出请求并呈现内容,无法直接访问此类请求,并且无法记录这些请求。

不能进行截屏操作

尽管我们在测试中没有看到使用Kiosk Pro的JavaScript API进行屏幕捕获的任何问题,但其他iOS开发人员报告说屏幕捕获在WKWebView上随机失败。如果截屏的API是app中的关键操作,建议使用现有的UIWebView浏览引擎。

用 CSS 实现元素垂直居中

Posted on 2018-04-12 | In HTML , JavaScript |

水平居中设置

1、行内元素
设置 text-align:center

2、定宽块状元素
设置 左右 margin 值为 auto

3、不定宽块状元素
a:在元素外加入 table 标签(完整的,包括 table、tbody、tr、td),该元素写在 td 内,然后设置 margin 的值为 auto
b:给该元素设置 displa:inine 方法
c:父元素设置 position:relative 和 left:50%,子元素设置 position:relative 和 left:50%

垂直居中设置

1、父元素高度确定的单行文本
设置 height = line-height

2、父元素高度确定的多行文本
a:插入 table (插入方法和水平居中一样),然后设置 vertical-align:middle
b:先设置 display:table-cell 再设置 vertical-align:middle

在前端面试中,大都会问你div居中的方法:

不过以后文笔肯定会变得更好一些的。

开始这些东西之前也可以测试一下你对html了解多少,让我们测试一下吧,小测验:你对HTML5了解有多少?

今天就细数一下几种方法:

1,使用position:absolute,设置left、top、margin-left、margin-top的属性

.one{
      position:absolute;
      width:200px;
      height:200px;
      top:50%;
      left:50%;
      margin-top:-100px;
      margin-left:-100px;
      background:red;
}

这种方法基本浏览器都能够兼容,不足之处就是需要固定宽高。

2,使用position:fixed,同样设置left、top、margin-left、margin-top的属性

.two{
    position:fixed;
    width:180px;
    height:180px;
    top:50%;
    left:50%;
    margin-top:-90px;
    margin-left:-90px;
    background:orange;
 }

大家都知道的position:fixed,IE是不支持这个属性的

3,利用position:fixed属性,margin:auto这个必须不要忘记了。

 .three{
      position:fixed;
      width:160px;
      height:160px;
      top:0;
      right:0;
      bottom:0;
      left:0;
      margin:auto;
      background:pink;
}

4,利用position:absolute属性,设置top/bottom/right/left

.four{
    position:absolute;
    width:140px;
    height:140px;
    top:0;
    right:0;
    bottom:0;
    left:0;
    margin:auto;
    background:black;
}

5,利用display:table-cell属性使内容垂直居中

.five{
    display:table-cell;
    vertical-align:middle;
    text-align:center;
    width:120px;
    height:120px;
    background:purple;
}

6,最简单的一种使行内元素居中的方法,使用line-height属性

.six{
    width:100px;
    height:100px;
    line-height:100px;
    text-align:center;
    background:gray;
}

这种方法也很实用,比如使文字垂直居中对齐

7,使用css3的display:-webkit-box属性,再设置-webkit-box-pack:center/-webkit-box-align:center

.seven{
    width:90px;
    height:90px;
    display:-webkit-box;
    -webkit-box-pack:center;
    -webkit-box-align:center;
    background:yellow;
    color:black;
 }

8,使用css3的新属性transform:translate(x,y)属性

 .eight{
     position:absolute;
    width:80px;
    height:80px;
  top:50%;
  left:50%;
  transform:translate(-50%,-50%);
  -webkit-transform:translate(-50%,-50%);
  -moz-transform:translate(-50%,-50%);
  -ms-transform:translate(-50%,-50%);
  background:green;
}

这个方法可以不需要设定固定的宽高,在移动端用的会比较多,在移动端css3兼容的比较好

9、最高大上的一种,使用:before元素

.nine{
    position:fixed;
    display:block;
    top:0;
    right:0;
    bottom:0;
    left:0;
    text-align:center;
    background:rgba(0,0,0,.5);
}
.nine:before{
    content:'';
    display:inline-block;
    vertical-align:middle;
    height:100%;
}
 .nine .content{
    display:inline-block;
    vertical-align:middle;
    width:60px;
    height:60px;
    line-height:60px;
    color:red;
    background:yellow;
}

总而言之所有的居中的方法就是你必须要掌握css属性的这个概念HTML DIV+CSS ,你掌握了就可以好好的运用这些居中的东西了

参考:知乎用 CSS 实现元素垂直居中,有哪些好的方案?

iOS 并发编程 - Operation And NSOperation Queue

Posted on 2018-04-12 | In iOS , 多线程 |
  • 基本概念

    1. 术语
    2. 串行 vs 并发(concurrency)
    3. 同步 vs 异步
    4. 队列 vs 线程
  • iOS的并发编程模型

  • Operation Queues vs. Grand Central Dispatch (GCD)

  • 关于Operation对象

    1. 并发的Operation 和非并发的Operation
    2. 创建NSBlockOperation对象
    3. 创建NSInvocationOperation对象
  • 自定义Operation对象

    1. 自定义的非并发NSOperation-不实现取消操作
    2. 自定义的非并发NSOperation-实现取消操作
  • 定制Operation对象的执行行为

    1. 修改Operation在队列中的优先级
    2. 修改Operation执行任务线程的优先级
    3. 设置Completion Block
  • 执行Operation对象

    1. 添加Operation到Operation Queue中
    2. 手动执行Operation
    3. 取消Operation
    4. 等待Operation执行完成
    5. 暂停和恢复Operation Queue
  • 添加Operation Queue中Operation对象之间的依赖

  • 总结


看过上面的结构预览,下面就开始我们这篇blog

术语

Operation: The NSOperation class is an abstract class you use to encapsulate the code and data associated with a single task.

解释:Operation是一个抽象类。你可以通过组织一段代码和数据,表示一个任务。

Operation Queue: The NSOperationQueue class regulates the execution of a set of NSOperation objects.

解释: NSOperationQueue用于规则的去执行一系列Operation。
任务:通常的说是由一段代码和数据组成,可以完成特定某项功能的代码数据集合。
进程:进程可以理解CPU所能执行的单个任务,CPU任何一个时刻职能运行一个进程。
线程:线程是计算机CPU所能执行最小单元,亦可以理解简化版的进程。一个进程可以包含多个线程。

串行 vs 并发

最简单的理解就是,串行和并发是用来修饰是否可以同时执行任务的数量的。串行设计只允许同一个时间段中只能一个任务在执行。并发设计在同一个时间段中,允许多个任务在逻辑上交织进行。(在iOS中,串行和并发一般用于描述队列)
说个题外话,刚开始是将并发写成并行的,后觉得并发和并行的概念一直挥之不去,可以参考这篇,很赞奥——还在疑惑并发和并行?

同步 vs 异步

同步操作,只有当该操作执行完成返回后,才能执行其他代码,会出现等待,易造成线程阻塞。异步操作,不需要等到当前操作执行完,就可以返回,执行其他代码。(一般用于描述线程)

队列 vs 线程

队列用于存放Operation。在iOS中,队列分为串行队列和并发队列。使用NSOperationQueue时,我们不需要自己创建去创建线程,我们只需要自己去创建我们的任务(Operation),将Operation放到队列中去。队列会负责去创建线程执行,执行完后,会销毁释放线程占用的资源。


iOS并发编程模型

对于一个APP,需要提高应用的性能,一般需要创建辅助的线程去执行任务。在整个APP的生命周期内,我们需要自己手动去创建,销毁线程,以及暂停,开启线程。对于这创建一个这样的线程管理方案,已经是非常复杂且艰巨的任务。但是苹果爸爸为开发者提供了两套更好的解决方案:NSOperation,Grand Central Dispatch (GCD) Reference,GCD的方式具体的本文暂不讨论。

使用NSOperationQueue 和 NSOperation的方式是苹果基于GCD再一次封装的一层,比GCD更加的灵活,而且是一种面向对象设计,更加适合开发人员。虽然相对于GCD会牺牲一些性能,但是我们可以对线程进行更多的操作,比如暂停,取消,添加Operation间的依赖。但是GCD如果暂停和取消线程操作则十分的麻烦。

Operation Queues vs. Grand Central Dispatch (GCD)

简单来说,GCD 是苹果基于 C 语言开发的,一个用于多核编程的解决方案,主要用于优化应用程序以支持多核处理器以及其他对称多处理系统。而 Operation Queues 则是一个建立在 GCD 的基础之上的,面向对象的解决方案。它使用起来比 GCD 更加灵活,功能也更加强大。下面简单地介绍了 Operation Queues 和 GCD 各自的使用场景:

Operation Queues :相对 GCD 来说,使用 Operation Queues 会增加一点点额外的开销,但是我们却换来了非常强大的灵活性和功能,我们可以给 operation 之间添加依赖关系、取消一个正在执行的 operation 、暂停和恢复 operation queue 等;
GCD :则是一种更轻量级的,以 FIFO 的顺序执行并发任务的方式,使用 GCD 时我们并不关心任务的 调度情况,而让系统帮我们自动处理。但是 GCD 的短板也是非常明显的,比如我们想要给任务之间添加依赖关系、取消或者暂停一个正在执行的任务时就会变得非常棘手。

上引用自Operation Queues vs. Grand Central Dispatch (GCD)

关于Operation对象

NSOperation对象是一个抽象类,是不能直接创建对象的。但是它有两个子类——NSBlockOperation,NSInvocationOperation.通常情况下我们都可以直接使用这两个子类,创建可以并发的任务。

我们查看关于NSOperation.h的头文件,可以发现任意的operation对象都可以自行开始任务(start),取消任务(cancle),以及添加依赖(addDependency:)和移除依赖(removeDependency:).关于依赖,有一种很好的一种开发思路。在operation对象中有很多属性,可以用于检测当前任务的状态,如isCancelled:是否已经取消,isFinished:是否已经完成了任务。
屏幕快照 2016-06-07 下午8.29.04.png

  • 创建NSBlockOperation

以下使用到的代码片段取自我的LSOperationAndOperationQueueDemo

NSBlockOperation顾名思义,是是用block来创建任务,主要有两种方式创建,一种是是用类方法,一种是创建operation对象,再添加任务。上代码:以下代码包括了两种block创建任务的方式。以及已经有任务的operation对象再添加任务。及直接添加任务到queue中。

@implementation LSBlockOperation

+ (LSBlockOperation *)lsBlockOperation {
    return [[LSBlockOperation alloc] init];
}

- (void)operatingLSBlockOperation {

    NSBlockOperation *blockOpt1 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"-------- blockOpt1, mainThread:%@, currentThread:%@", [NSThread mainThread], [NSThread currentThread]);
    }];
    /// 继续添加执行的block
    [blockOpt1 addExecutionBlock:^{
        NSLog(@"-------- blockOpt1 addExecutionBlock1 mainThread:%@, currentThread:%@", [NSThread mainThread], [NSThread currentThread]);
    }];

    [blockOpt1 addExecutionBlock:^{
        NSLog(@"-------- blockOpt1 addExecutionBlock2 mainThread:%@, currentThread:%@", [NSThread mainThread], [NSThread currentThread]);
    }];

    NSBlockOperation *blockOpt2 = [[NSBlockOperation alloc] init];
    [blockOpt2 addExecutionBlock:^{
        NSLog(@"-------- blockOpt2 mainThread:%@, currentThread:%@", [NSThread mainThread], [NSThread currentThread]);
    }];

    NSBlockOperation *blockOpt3 = [[NSBlockOperation alloc] init];
    [blockOpt3 addExecutionBlock:^{
        NSLog(@"-------- blockOpt3 mainThread:%@, currentThread:%@", [NSThread mainThread], [NSThread currentThread]);
    }];

    NSBlockOperation *blockOpt4 = [NSBlockOperation blockOperationWithBlock:^{
        NSLog(@"-------- blockOpt4 mainThread:%@, currentThread:%@", [NSThread mainThread], [NSThread currentThread]);
    }];

    // 添加执行优先级 - 并不能保证执行顺序
//    blockOpt2.queuePriority = NSOperationQueuePriorityVeryHigh;
//    blockOpt4.queuePriority = NSOperationQueuePriorityHigh;

    /// 可以设置Operation之间的依赖关系 - 执行顺序3 2 1 4
    [blockOpt2 addDependency:blockOpt3];
    [blockOpt1 addDependency:blockOpt2];
    [blockOpt4 addDependency:blockOpt1];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:blockOpt1];
    [queue addOperation:blockOpt2];
    [queue addOperation:blockOpt3];
    [queue addOperation:blockOpt4];
    [queue addOperationWithBlock:^{
        NSLog(@"-------- queue addOperationWithBlock1 mainThread:%@, currentThread:%@", [NSThread mainThread], [NSThread currentThread]);
    }];
    [queue addOperationWithBlock:^{
        NSLog(@"-------- queue addOperationWithBlock2 mainThread:%@, currentThread:%@", [NSThread mainThread], [NSThread currentThread]);
    }];
}
  • 创建NSInvocationOperation

NSInvocationOperation是另一种可创建的operation对象的类。但是在Swift中已经被去掉了。NSInvocationOperation是一种可以非常灵活的创建任务的方式,主要是其中包含了一个target和selector。假设我们现在有一个任务,已经在其它的类中写好了,为了避免代码的重复,我们可以将当前的target指向为那个类对象,方法选择器指定为那个方法即可,如果有参数,可以在NSInvocationOperation创建中指定对应的Object(参数).

具体的可以看如下代码:LSOperationAndOperationQueueDemo

@implementation LSInvocationOperation

+ (LSInvocationOperation *)lsInvocationOperation {
    return [[LSInvocationOperation alloc] init];
}

- (void)operationInvocationOperation {

    NSInvocationOperation *invoOpt1 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invoOperated1) object:self];
    NSInvocationOperation *invoOpt2 = [[NSInvocationOperation alloc] initWithTarget:self selector:@selector(invoOperated2) object:self];

    // invocated other obj method
    /// 可以执行其它类中方法,并且可以带参数
    NSInvocationOperation *invoOpt4 = [[NSInvocationOperation alloc] initWithTarget:[[Person alloc] init] selector:@selector(running:) object:@"linsir"];

    // 设置优先级 - 并不能保证按指定顺序执行
//    invoOpt1.queuePriority = NSOperationQueuePriorityVeryLow;
//    invoOpt4.queuePriority = NSOperationQueuePriorityVeryLow;
//    invoOpt2.queuePriority = NSOperationQueuePriorityHigh;

    // 设置依赖 - 线性执行
    [invoOpt1 addDependency:invoOpt2];
    [invoOpt2 addDependency:invoOpt4];

    NSOperationQueue *queue = [[NSOperationQueue alloc] init];
    [queue addOperation:invoOpt1];
    [queue addOperation:invoOpt2];
    [queue addOperation:invoOpt4];
}

- (void)invoOperated1 {
    NSLog(@"--------- invoOperated1, mainThread:%@, currentThread:%@", [NSThread mainThread],[NSThread currentThread]);
}

- (void)invoOperated2 {
    NSLog(@"--------- invoOperated2, mainThread:%@, currentThread:%@", [NSThread mainThread],[NSThread currentThread]);
}

@end

自定义Operation对象

上文介绍了两种系统定义的NSOperation,通常情况下,我们可以直接使用,已经可以满足了大部分的需求。但是当系统的不能满足时候,我们就需要自定义我们自己的Operation对象。Operation对象可以分为并发的和非并发的两类。从实现角度上而言,非并发的更容易实现的多。因为非并发的Operation对象中的很多属性,它的父类已经做好了管理,我们只需要直接使用就可以了。(通常情况下,实现多线程是由NSOperationQueue对象管理的,而不是NSOperation对象)实现自定义的NSOperation对象,最少需要重写两个方法,一个是初始化init方法(传值),一个是mian方法(主要的逻辑实现)。

  • 自定义的非并发NSOperation-不实现取消操作

    代码片段取自LSOperationAndOperationQueueDemo

    @interface LSNonConcurrentOperation ()
    
    @property (nonatomic, strong)id data;
    
    @end
    
    /**
     自定义一个非并发的Operation,最少需要实现两个方法,一个初始化的init方法,另一个是mian方法,即主方法,逻辑的主要执行体。
     */
    @implementation LSNonConcurrentOperation
    
    - (id)initWithData:(id)data {
        self = [self init];
        if (self) {
            self.data = data;
        }
        return self;
    }
    
    // 该主方法不支持Operation的取消操作
    - (void)main {
        @try {
    
            NSLog(@"-------- LSNonConcurrentOperation - data:%@, mainThread:%@, currentThread:%@", self.data, [NSThread mainThread], [NSThread currentThread]);
            sleep(2);
            NSLog(@"-------- finish executed %@", NSStringFromSelector(_cmd));
    
        } @catch (NSException *exception) {
    
            NSLog(@"------- LSNonConcurrentOperation exception - %@", exception);
    
        } @finally {
    
        }
    }
    
  • 自定义的非并发NSOperation-实现取消操作

    - (void)main {
        // 执行之前,检查是否取消Operation
        if (self.isCancelled) return;
    
        @try {
            NSLog(@"-------- LSNonConcurrentOperation - data:%@, mainThread:%@, currentThread:%@", self.data, [NSThread mainThread], [NSThread currentThread]);
    
            // 循环去检测执行逻辑过程中是否取消当前正在执行的Operation
            for (NSInteger i = 0; i < 10000; i++) {
    
                NSLog(@"run loop -- %@", @(i + 1));
    
                if (self.isCancelled) return;
                sleep(1);
            }
            NSLog(@"-------- finish executed %@", NSStringFromSelector(_cmd));
        } @catch (NSException *exception) {
            NSLog(@"------- LSNonConcurrentOperation exception - %@", exception);
    
        } @finally {
    
        }
    }
    

由上可以知道,取消一个任务的执行,其实并不是立即就会取消,而是会在一个runloop中不断的去检查,判断isCancle的值,直到为yes时候,则取消了操作。所以,设置Operation为cancle的时候,至少需要一个runloop的时间才会结束操作。

定制Operation对象的执行行为

  • 修改Operation在队列中的优先级

NSOperation对象在Queue中可以设置执行任务的优先级。我们可以通过设置operation对象的setQueuePriority:方法,改变任务在队列中的执行优先级。但是真正决定一个operation对象能否执行的是isReady,假设一个operation对象的在队列执行的优先级很高,另一个很低,但是高的operation对象的isReady是NO,也只会执行优先级低的operation任务。另一个影响任务在队列中执行顺序的是依赖(下文会讲到),假设operation A依赖于operation B,所以一定先执行operation B,再执行operation A.

  • 修改Operation执行任务线程的优先级

从iOS4.0开始,我们可以设置operation中任务执行的线程优先级。从iOS4.0到iOS8.0,operation对象可以通过方法setThreadPriority:,这里的参数是一个double类型,范围是0.0到1.0,设置越高,理论上讲,线程执行的可能性就越高。但是从iOS8.0之后,这个方法已经被废弃了,使用setQualityOfService:代替,这里参数是一个预设的枚举值。

  • 设置Completion Block

同上,从iOS4.0开始,可以给每个operation对象设置一个主任务完成之后的完成回调setCompletionBlock:。所设置的block执行是在检测到operation的isFinished为YES后执行的。值得注意的是:我们并不能保证block所在的线程一定在主线程,所以当我们需要对主线程上做一些操作的时候,我们应该切换线程到主线程中,如需在其他线程执行的某些操作,亦需要切换线程。

Therefore, you should not use this block to do any work that requires a very specific execution context. Instead, you should shunt that work to your application’s main thread or to the specific thread that is capable of doing it.

执行Operation对象

对于执行一个Operation对象,一般的做法是将operation对象添加到一个队列中去,之后队列会根据当前系统的状态,以及内核的状态,自行的去执行operation中的任务。

NSOperationQueue *queue = [[NSOperationQueue alloc] init];
[queue addOperation:opt];

还有一种做法是,我们可以手动的执行一个operation对象,直接调用operation的start方法

[opt start];
  • 添加Operation到Operation Queue中

将operation对象添加到queue中非常简单

首先创建一个队列:

NSOperationQueue *queue = [[NSOperationQueue alloc] init];

添加到队列的方法如下:

- (void)addOperation:(NSOperation *)op;
- (void)addOperations:(NSArray<NSOperation *> *)ops waitUntilFinished:(BOOL)wait NS_AVAILABLE(10_6, 4_0);
- (void)addOperationWithBlock:(void (^)(void))block NS_AVAILABLE(10_6, 4_0);

第一个:添加意境存在的operation对象
第二个:添加一组operation对象
第三个:直接添加一个block到队列中,无需创建operation对象

  • 手动执行Operation

一般情况下,我们不需要手动的去执行一个operation对象,但如果需要,亦可,调用start方法。

[opt start];
  • 取消Operation

当我们将一个operation对象添加到队列中之后,operation就已经被队列所拥有。我们可以在某个需要的时候调用operation对象的cancle方法,将operation出列。并且此时operation的isFinished也会为YES,所以此时依赖于它的operation就回继续得到执行。当然,我们可以直接调用队列的cancelAllOperations方法,取消了队列中所有的operation执行。

  • 等待Operation执行完成

等待一个Operation对象的执行完成,可以使用waitUntilFinished方法。但是应该注意到,等待一个任务执行完,会阻塞当前线程。所以我们绝不应该在主线程中做该操作,那样会带来非常差的体验。所以该操作应该使用辅助线程中。
我们也可以调用NSOperationQueue对象的waitUntilAllOperationsAreFinished方法,知道所有的任务都执行完成。

  • 暂停和恢复Operation Queue

通过设置队列的setSuspended,我们可以暂停一个队列中还没有开始执行的operation对象,对于已经开始的执行的任务,将继续执行。并且,已经暂停了队列,仍然可以继续添加operation对象,但是不会执行,只能等到从暂停(挂起)状态切换到非暂停状态。即设置setSuspended为NO。对于单个的operation,是没有暂停的概念的。

When the value of this property is NO, the queue actively starts operations that are in the queue and ready to execute. Setting this property to YES prevents the queue from starting any queued operations, but already executing operations continue to execute. You may continue to add operations to a queue that is suspended but those operations are not scheduled for execution until you change this property to NO.

添加Operation Queue中Operation对象之间的依赖

在NSOperationQueue中,如果没有经过对operation添加依赖,都是使用并发处理的。但是在某些情况下,我们对任务的执行是有非常严格的规定的。即需要串行执行,此时,我们就需要对operation对象间进行添加依赖处理。

- (void)addDependency:(NSOperation *)op;
- (void)removeDependency:(NSOperation *)op;

第一个:添加依赖
第二个:移除依赖

依赖,是一种非常好用功能,在我们做项目(生活中)的时候,很多时候都一种依赖的概念。比如,用户需要上传一张照片到自己的空间,但是此时必须检测该用户是否已经登录。以前我们可能将两个逻辑写在一起,但是现在可以将成写成两个不同的operation,并设置它们的依赖。这样的好处非常可见的:
第一点:它可以帮我们解藕,不同的逻辑分在不一样的对象中。
第二点:某些常用的逻辑会经常用到,以后不需要一次次的重复,可读性增强,以后需要的时候直接调用,设置其依赖即可。比如检测是否登录

总结

多线程执行任务看似十分的复杂,但是如果将复杂的任务交给NSOperation and NSOperationQueue,就可以简化它的难度,并且它似乎可以比我们自己处理的更好。

文中使用的demo - LSOperationAndOperationQueueDemo

感谢

以下文章给我带来非常大的帮助

还在疑惑并发和并行?
进程与线程的一个简单解释
iOS 并发编程之 Operation Queues
NSOperationQueue - 文档

Method Swizzling的实践 - 实现页面的统计功能

Posted on 2018-04-12 | In iOS , Runtime |
#import <objc/runtime.h>

在OC中,最具争议的语法,莫过于runtime中的运行时的语法。而其中黑魔法Method Swizzling更让人着迷。

Method Swizzling是一项在运行时,通过改变方法名(SEL)与函数指针之间的映射。从而改变方法实现的黑魔法技术。如下图。

Method Swizzling模型图.png

场景需求

实践是检验真理的唯一标准 - 继续

能否快速而可靠的学习到某个技术点,最好的方法,还是找到一个场景,去实现它。项目中,我们经常会有这样的需求,去统计用户每一个页面的访问量,什么时候页面出现,什么时候页面消失。由此获取来的数据,去判断用户黏性,功能点的使用情况的等。对于统计的功能,我们可以交给第三方的厂商,比如百度统计,友盟等。

但是,我们最少需要告诉第三方厂商的库,现在是用户打开了这个页面。现在是用户关闭了这个页面。之前的做法是,在每一个Controller的- viewDidAppear:中调用一个用户开始使用的方法。在- viewDidDisappear中调用一个用户结束使用的方法。OK了,这个需求解决了。但是随着项目的的增大,发现每一个Controller中都需要写两个一摸一样的方法。这样违背了软件开发的开发准则—“一个方法只写一遍”。

幸好的是,在学习微信阅读开源的一款内存检测工具MLeaksFinder。其中的代码零侵染,易用性,很好的在开发中帮助我解决了一些内存泄露的问题。而从源码中,MLeaksFinder就是利用了Method Swizzling技术。受该框架启发,也希望实现对代码侵染程度小,却可以很好的实现统计的功能。

Method Swizzling实现

+ (void)load {
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        [self swizzleSEL:@selector(viewDidAppear:) withSEL:@selector(swizzled_viewDidAppear:)];
        [self swizzleSEL:@selector(viewDidDisappear:) withSEL:@selector(swizzled_viewDidDisappear:)];
    });
}

/// 交换两个方法的实现
+ (void)swizzleSEL:(SEL)originalSEL withSEL:(SEL)swizzledSEL {
    Class class = [self class];

    Method originalMethod = class_getInstanceMethod(class, originalSEL);
    Method swizzledMethod = class_getInstanceMethod(class, swizzledSEL);

    // When swizzling a class method, use the following:
    // Class class = object_getClass((id)self);
    // ...
    // Method originalMethod = class_getClassMethod(class, originalSelector);
    // Method swizzledMethod = class_getClassMethod(class, swizzledSelector);

    BOOL didAddMethod = class_addMethod(class, originalSEL, method_getImplementation(swizzledMethod), method_getTypeEncoding(swizzledMethod));

    if (didAddMethod) {
        class_replaceMethod(class, swizzledSEL, method_getImplementation(originalMethod), method_getTypeEncoding(originalMethod));
    } else {
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

#pragma mark - Swizzled Method

- (void)swizzled_viewDidAppear:(BOOL)animated {
    [self swizzled_viewDidAppear:animated];
}

- (void)swizzled_viewDidDisappear:(BOOL)animated {
    [self swizzled_viewDidDisappear:animated];
}

上面的代码,便已经实现了- viewDidAppear:和- swizzled_viewDidAppear:方法实现的互换。如果仅仅是拷贝和黏贴,这样已经实现了功能。想要更深的了解运行时的工作机制。还需要了解这部分内容。

+ (void)load; & + (void)initialize;

load方法和initialize方法都是可选方法。load方法在类初装载的时候被调用,即load方法一定被调用。Method Swizzling同样应该在load方法实现。initailize方法仅在类的实例方法或者类方法第一次调用的时候调用。

dispatch_once

由于Method Swizzling触发是全局范围内的。必须保证只触发一次,并且是原子性的,在多线程间也仅仅调用一次。GCD中的dispatch_once很好的符合了要求。同样的,在OC中的单利对象的标准也应该使用这种方式。

SEL,Method,IMP

Selector(typedef struct objc_selector *SEL):在运行时 Selectors 用来代表一个方法的名字。Selector 是一个在运行时被注册(或映射)的C类型字符串。Selector由编译器产生并且在当类被加载进内存时由运行时自动进行名字和实现的映射。

Method(typedef struct objc_method *Method):方法是一个不透明的用来代表一个方法的定义的类型。

Implementation(typedef id (*IMP)(id, SEL,…)):这个数据类型指向一个方法的实现的最开始的地方。该方法为当前CPU架构使用标准的C方法调用来实现。该方法的第一个参数指向调用方法的自身(即内存中类的实例对象,若是调用类方法,该指针则是指向元类对象metaclass)。第二个参数是这个方法的名字selector,该方法的真正参数紧随其后。

说说Method Swizzling工作过程

在类初始化,初次装载的时候,执行load方法。找到类维护的方法(包含方法名SEL和映射的实现IMP)列表,修改selector(方法名)和imp(实现体)的映射关系。因此当系统调用系统方法时候,其实调用的是我们自定义的方法。

看似错误的代码

- (void)swizzled_viewDidAppear:(BOOL)animated {
    [self swizzled_viewDidAppear:animated];
}

如上,这段代码,对于一个合格的工程师而言,应该会很警惕。正常情况下在类中调用,必然进入无限循环。然而,在Method Swizzling中,这样才是正确的用法。理解下其中的逻辑。在load方法中实现了Swizzling,系统SEL的viewDidAppear指向的是swizzled_viewDidAppear的实现,方法SEL名为swizzled_viewDidAppear指向的是系统名为viewDidAppear的实现。所以在调用过程中相当两个方法交叉调用了,并没有导致死循环。

最后-百度统计逻辑业务代码的实现

#pragma mark - Method Swizzling

- (void)swizzled_viewDidAppear:(BOOL)animated {
    [self swizzled_viewDidAppear:animated];

    NSString *currentControllerTitle = self.title;
    if (!currentControllerTitle) return;

    [[BaiduMobStat defaultStat] pageviewStartWithName:currentControllerTitle];
}

- (void)swizzled_viewDidDisappear:(BOOL)animated {
    [self swizzled_viewDidDisappear:animated];

    NSString *currentControllerTitle = self.title;
    if (!currentControllerTitle) return;

    [[BaiduMobStat defaultStat] pageviewEndWithName:currentControllerTitle];
}

统计的代码仅仅几行而已,只需要放在UIControllerView+Traking.m类别中。不仅减少了项目无关业务逻辑的代码量,同样做到了代码的侵染度很少。

思考

引自NSHipster

很多人认为交换方法实现会带来无法预料的结果。然而采取了以下预防措施后, method swizzling 会变得很可靠:

  • 在交换方法实现后记得要调用原生方法的实现(除非你非常确定可以不用调用原生方法的实现):APIs 提供了输入输出的规则,而在输入输出中间的方法实现就是一个看不见的黑盒。交换了方法实现并且一些回调方法不会调用原生方法的实现这可能会造成底层实现的崩溃。
  • 避免冲突:为分类的方法加前缀,一定要确保调用了原生方法的所有地方不会因为你交换了方法的实现而出现意想不到的结果。
  • 理解实现原理:只是简单的拷贝粘贴交换方法实现的代码而不去理解实现原理不仅会让 App 很脆弱,并且浪费了学习 Objective-C 运行时的机会。阅读 Objective-C Runtime Reference 并且浏览 能够让你更好理解实现原理。
  • 持续的预防:不管你对你理解 swlzzling 框架,UIKit 或者其他内嵌框架有多自信,一定要记住所有东西在下一个发行版本都可能变得不再好使。做好准备,在使用这个黑魔法中走得更远,不要让程序反而出现不可思议的行为。

关于使用WKWebView时,ViewController不调用dealloc方法的记录

Posted on 2018-04-12 | In iOS , WebView |

在当前的项目中,会嵌入很多的H5页面,所以就考虑封装一个Controller,用于完全的显示H5页面。基于当前项目对iOS版本的支持在iOS8.0之上,所以选用WKWebView。

很正常的在ViewDidLoad中初始化,设置WKWebView

- (void)setupWKWebView {
    WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
    WKUserContentController *controller = [[WKUserContentController alloc] init];
    configuration.userContentController = controller;
    self.contentWKWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 64, SCREENW, SCREENH - NAVIAGTION_HEIGHT)
                                           configuration:configuration];
    self.contentWKWebView.UIDelegate = self;
    self.contentWKWebView.navigationDelegate = self;
    self.contentWKWebView.allowsBackForwardNavigationGestures = YES;
    // 监听进度条
    [self.contentWKWebView addObserver:self
                        forKeyPath:@"estimatedProgress"
                           options:NSKeyValueObservingOptionNew context:nil];

    // 注册JS交互对象
    WKUserContentController *controller = self.contentWKWebView.configuration.userContentController;
    [controller addScriptMessageHandler:self name:@"vhswebview"];
}

然后加载H5页面,一切看着都很正常,但是经过多次测试,发现对应的- (void)dealloc;一次都没有调用,所以必然出现了内存泄漏。检查该Controller代码,发现诸如block的循环引用,代理等地方都没有问题。

最后猜测是否是注册JS交互对象的时候,将对象本身self传给MessageHandler导致的。后面查询了一些资料,发现在Apple的development中提到了需要移除JS交互对象removeScriptMessageHandlerForName。

所以将页面关闭WKWebView的Controller的时候,就去移除JS交互对象

WKUserContentController *controller = self.contentWKWebView.configuration.userContentController;
[controller removeScriptMessageHandlerForName:@"vhswebview"];

最后,就可以在关闭页面后调用dealloc了。

结合RSA,AES128,MD5---移动端与服务端在通信层的加密处理

Posted on 2017-04-11 | In iOS , 加密 |

很高兴能在项目中使用到RSA,AES128,以及MD5,用以保证客户端(Client)和服务端(Server)之间的通信安全。接下来会尽力的描述清楚关于本次使用的流程。具体关于算法的细节,自行Wiki。

原来只是对加密这一块很简单的了解,比如只知道一些对称加密,非对称加密,md5单向加密等。通过本次的学习,很惊艳于可以将多种加密方式那么完美的结合到一起。让整个通信过程变得如此美妙。虽然增加了服务端和客户端的工作量,但是保证数据的一致出口,一致入口,只需要在出口和入口处加上逻辑,就可以很好的避免扰乱原有逻辑的烦恼。

简单的概念,文章可能会涉及到

  1. RSA——非对称加密,会产生公钥和私钥,公钥在客户端,私钥在服务端。公钥用于加密,私钥用于解密。
  2. AES——对称加密,直接使用给定的秘钥加密,使用给定的秘钥解密。(加密解密使用相同的秘钥)
  3. MD5——一种单向的加密方式,只能加密,不能解密
  4. Base64编码——对字节数组转换成字符串的一种编码方式

客户端,服务端的通信逻辑

之前:明文传输通信

  1. 客户端将要上传的数据以字典(Map)的方式打包,Post提交给服务器。
  2. 服务器接收提交的数据包,通过Key-Value的形式获取客户端提交的值,进行处理。
  3. 处理结束,将数据以字典(Map)的形式打包,返回给客户端处理。

加密传输通信

整个流程是:

客户端上传数据加密 ==> 服务器获取数据解密 ==> 服务器返回数据加密 ==> 客户端获取数据解密

  • 客户端上传数据加密 A

    1. 客户端随机产生一个16位的字符串,用以之后AES加密的秘钥,AESKey。
    2. 使用RSA对AESKey进行公钥加密,RSAKey
    3. (此处某些重要的接口需要加签处理,后续讲解,不要加签处理的省略该步骤)
    4. 将明文的要上传的数据包(字典/Map)转为Json字符串,使用AESKey加密,得到JsonAESEncryptedData。
    5. 封装为{key : RSAKey, value : JsonAESEncryptedData}的字典上传服务器,服务器只需要通过key和value,然后解析,获取数据即可。
  • 服务器获取数据解密 B

    1. 获取到RSAKey后用服务器私钥解密,获取到AESKey
    2. 获取到JsonAESEncriptedData,使用AESKey解密,得到明文的客户端上传上来的数据。
    3. (如果客户端进行了加签处理,此处需要验签,以保证数据在网络传输过程中是否被篡改)
  • 服务器返回数据加密 C

    1. 将要返回给客户端的数据(字典/Map)转成Json字符串,用AESKey加密处理
    2. (此处也可以加签处理)
    3. 封装数据{data : value}的形式返回给客户端
  • 客户端获取数据解密 D

    1. 客户端获取到数据后通过key为data得到服务器返回的已经加密的数据AESEncryptedResponseData
    2. 对AESEncryptedResponseData使用AESKey进行解密,得到明文服务器返回的数据。

加签和验签

第二节——“客户端,服务端的通信逻辑”已经基本上把客户端和服务端的通信加密逻辑讲完了。至于“加签和验签”主要是针对数据传输过程中,防止数据被篡改的一种做法。

数据被篡改,栗子:

对于一个运动类型的APP,上传运动的步数,是一个常见的接口操作。比如该接口会有几个字段,step(步数),time(步数产生的时间),memberId(用户id)。

假设某用户抓取了你上传的数据包,然后成功的破解了你之前的加密方式。得到对应的明文,此时该用户就可以随意修改你的数据,比如step,然后以相同的方式加密,post到你的服务器,此时服务器会认为这是一次正常的请求,便接受了这个修改后的步数。其实此时的数据是错误的。如此神不知鬼不觉。。。

为了防止这种做法,我们可以是加签的处理方式

  • 加签处理(数据发起方都可以加签,此处是客户端)

    1. 我们一般取其中的关键字段(别人可能修改的字段),比如此时step,和time及memberId,都比较敏感。
    2. 在上文的A中的第二步之后,获取step,time,memberId,拼接成一个字符串(顺序和服务器约定好),然后使用md5加密,采用base64编码(编码格式和服务约定)。得到signData
    3. 然后将获取到的signData以key-value的形式保存到原来明文的数据包中,然后进行A的第三步
  • 验签处理(数据接受方都可以验签,此处服务端)

    1. 如上,到B的第三步,此时已经得到了客户端上传的明文数据
    2. 按照喝客户端约定的字段拼接,将得到的step,time,memberId拼接后,使用同样的md5_base64处理,然后比较数据包中的签名sign是否和客户端当时的签名一致。
    3. 如果一致,接受数据。不一致,抛弃数据,终止本次操作

假设加签之后的数据包被截获,然后解密成功,得到明文的数据包。但是签名md5加密是无法解密的(单向加密)。此时即时修改了step,然后post到服务器,服务器通过修改后的step,time,memberId得到的字符串经过md5加密后,一定会与客户端的签名不一致。从而数据被抛弃。

流程图描述上文

客户端服务端通信加密逻辑.png

示例代码

关于AES,和RSA加密解密,只能出iOS端的代码。关于如何在Linux下生成RSA公钥和私钥证书,参照RSA公钥、私钥生成,详细讲解,网上很多

github的demo地址–CAAdvancedTech

运行,如下入显示

首页选择加密模块

AES,RSA加密解密页面

RSA公钥-生成自签名证书

// 生成1024位私钥
openssl genrsa -out private_key.pem 1024

// 根据私钥生成CSR文件
openssl req -new -key private_key.pem -out rsaCertReq.csr

// 根据私钥和CSR文件生成crt文件
openssl x509 -req -days 3650 -in rsaCertReq.csr -signkey private_key.pem -out rsaCert.crt

// 为IOS端生成公钥der文件
openssl x509 -outform der -in rsaCert.crt -out public_key.der

// 将私钥导出为这p12文件
openssl pkcs12 -export -out private_key.p12 -inkey private_key.pem -in rsaCert.crt

参照 漫谈RSA非对称加密解密

推荐工具

  1. 关于画流程图

    之前一致比较苦扰在Mac上有哪一款好用的可以画流程图,UML的工具,甚至都考虑过Keynote。最后发现这款在线的工具很不错,上图就是使用这款工具,第一次画的。效果不错。就是导出png图片分辨率不是很好

    工具processOn

    Mac 上最好用的流程图软件是什么?

  2. 关于AES加密解密在线工具

    在线AES加解密

12

mdiep

好人生科技有限公司,主职iOS开发,和H5开发的软件开发工程师

11 posts
8 categories
18 tags
GitHub E-Mail 简书
Links
  • mdiep的简书
  • Nonomori
  • ibireme
  • 南峰子的技术博客
© 2015 — 2018 mdiep
Powered by Hexo
|
Theme — NexT.Muse v5.1.4