forked from handsomeliuyang/handsomeliuyang.github.io
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcontent.json
1 lines (1 loc) · 288 KB
/
content.json
1
{"meta":{"title":"LiuYang's blog","subtitle":"探索,分享,创新——追求卓越","description":"探索,分享,创新——追求卓越","author":"刘阳","url":"https://handsomeliuyang.github.io"},"pages":[{"title":"关于","date":"2018-03-12T11:28:09.000Z","updated":"2018-03-14T09:57:52.000Z","comments":false,"path":"about/index.html","permalink":"https://handsomeliuyang.github.io/about/index.html","excerpt":"","text":""},{"title":"类别","date":"2018-03-12T11:28:09.000Z","updated":"2018-03-14T09:57:42.000Z","comments":false,"path":"categories/index.html","permalink":"https://handsomeliuyang.github.io/categories/index.html","excerpt":"","text":""},{"title":"标签","date":"2018-03-12T11:26:58.000Z","updated":"2018-03-14T09:58:12.000Z","comments":false,"path":"tags/index.html","permalink":"https://handsomeliuyang.github.io/tags/index.html","excerpt":"","text":""}],"posts":[{"title":"Kotlin学习笔记","slug":"Kotlin学习笔记","date":"2019-05-07T06:47:09.000Z","updated":"2019-05-11T06:09:19.789Z","comments":true,"path":"2019/05/07/Kotlin学习笔记/","link":"","permalink":"https://handsomeliuyang.github.io/2019/05/07/Kotlin学习笔记/","excerpt":"","text":"Kotlin优势 更安全,避免NPE(NullPointerException),强制判断再使用 更多的语言特性(如java8新特性,类扩展等等),使用代码更简洁 与Java互调,能使用Java所有的工具库(原因:Kotlin编译为JVM上运行的字节码,与java的原生字节码基本一致,部份情况下,性能更强) Kotlin基础类型Kotlin是强类型语言,即确定类型后,就不能再修改其类型。JavaScript就是弱类型语言 类型抛弃了Java的基本类型,都是引用类型: 整数:Byte-1, Short-2, Int-4, Long-8 浮点型:Float-4, Double-8 字符型:Char,’a’ Boolean类型:Boolean 字符串:String,”abc” 字符串模板:”图书价格是: ${bookPrice }” 类型别名:typealias 类型别名=已有类型 变量定义语法:var|val 变量名[:类型] [= 初始值]1234// 读写变量var name: String = \"ly\"// 只读变量val age: Int = 18 val表示只读变量,不同的变量类型,其初始值有一定的区别: 局部变量:只要在第一次使用之前初始化值就行 类属性:可以声明时,或构造函数里初始化 12345678910111213class MainActivity : AppCompatActivity() { val age: Int // = 18 constructor() { this.age = 18 } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) val tempVar: String ... tempVar = \"初始值\" }} Null安全通过如下语法保证Null安全: 类属性没有默认值,强制设置初始值 var a:String 不支持null值 var a:String? 支持null值,但不能直接调用其方法与属性 1234567891011121314class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) var a:String = null // 编译报错 var a:String? = null // 编译通过 if(savedInstanceState != null) { a = \"HelloWorld\" } Log.d(\"liuyang\", \"${a.length}\") // 编译报错 }} 可空变量使用姿式: 先判空再使用 123if(a != null){ Log.d(\"liuyang\", \"${ a.length }\")} 安全调用 1Log.d(\"liuyang\", \"${ a?.length }\") // 当a==null时,返回null Elvis运算,对安全调用的补充,允许修改为null时的返回值 1Log.d(\"liuyang\", \"${ a?.length ?: \"\" }\") 强制调用,可能引发NPE异常 — 不推荐 1Log.d(\"liuyang\", \"${ a!!.length }\") Kotlin运算符两个关键点: 运算符通过方法实现,即运算符都是编译时使用,编译后都是方法 支持运算符重载 12345678910111213141516171819202122class Animal(name: String, age: Int) { var name: String var age: Int init { this.name = name this.age = age; } // 运算符重载 operator fun plus(other: Animal): Animal{ this.name = this.name + other.name this.age = this.age + other.age return this; }}var animal: Animal = Animal(\"animal\", 2);var animal2: Animal = Animal(\"animal2\", 1);var animal3: Animal// 下面两个写法是一样的animal3 = animal.plus(animal2);animal3 = animal + animal2 // 编译为字节码的代码:animal.plus(animal2); 常用运算符对应表 运算符 对应方法 a - b a.minus(b) a + b a.plus(b) a * b a.times(b) a / b a.div(b) a[i] a.get(i) a[i]=b a.set(i, b) a == b a?.equals(b) ?: (b === null) a != b !(a?.equals(b) ?: (b === null))) a in b b.contains(a) a !in b !b.minus(a) 三个等号=== === 三个等号的意思,则比较的是内存地址,如下:1234var a = \"字符串\"var b = avar c = aprint(b === c) // 结果为true 但对于Int类型的变量有差异,有兴趣的同学可以进一步再了解 Kotlin流程控制关键点: 没有三目运算符,if表达式支持返回值 12345// 不支持三目运算符// min = (a > b) ? a : b// 通过if替换min = if (a > b) a else b when 替换 switch 12345678var score = 'B'when (score){ 'A' -> println(\"优秀\") 'B' -> println(\"良好\") 'C' -> println(\"中\") 'D' -> println(\"及格\") else -> println(\"不及格\")} for的语法:for (常量名 in 对象) {} 12345678910111213// 遍历1-5for(i in 1..5){ println(i)}var list: Array<String> = arrayOf(\"a\", \"b\", \"c\");for(key in list){ println(\"${key}\");}//下面的写法不支持//for(int i=0; i<list.length; i++){// println(\"${list[i]}\");//} Kotlin的数组和集合Array,Set,List 创建对象 xxxOf(参数) — 长度固定 mutableXXXOf(参数) — 长度可变(Array除外) 常用功能 长度:xxx.size属性 包含:”java” in xxx 对应方法:xxx.contains(“java”) … 遍历 遍历值:for (book in books) { } 遍历下标:for (i in books.indices) {} 遍历下标与值:for ( (index, value) in books.withindex() ) { } 12345678910111213141516171819202122232425262728293031323334// 创建数组var array: Array<String>array = arrayOf(\"a\", \"b\", \"c\");array = Array<String>(3, {index-> \"a${index}\"})// 创建Listvar list: List<String>list = listOf(\"a\", \"b\", \"c\") // 长度不可变list.add(\"d\") // 无此方法,编译报错list = mutableListOf(\"a\", \"b\", \"c\") // 长度可变list.add(\"d\") // 编译成功list = arrayListOf(\"a\", \"b\", \"c\") // ArrayList// 创建Setvar set: Set<String>set = setOf(\"a\", \"b\", \"c\") // 长度不可变set.add(\"d\") // 无此方法,编译报错set = mutableSetOf(\"a\", \"b\", \"c\") // 长度可变set.add(\"d\") // 编译成功set = hashSetOf(\"a\", \"b\", \"c\") // HashSetset = linkedSetOf(\"a\", \"b\", \"c\") // LinkedHashSet// 遍历值for (value in array) { Log.d(\"liuyang\", \"${value}\")}// 遍历下标for (index in array.indices){ Log.d(\"liuyang\", \"${array[index]}\")}// 遍历下标与值for ((index, value) in array.withIndex()){ Log.d(\"liuyang\", \"${index},${value}\")} Map 创建对象 mapOf(”Java” to 86, “xxx” to xx) — 长度固定 mutableMapOf(”Java” to 86, “xxx” to xx) — 长度可变 常用功能 长度:map.size属性 包含:key in map 对应方法:map.contains(key) 遍历 遍历Entry:for (en in map.entries) { en.key en.value} 解构遍历key,value:for ( (key, value) in map) {} 遍历key:for (key in map.keys ) {} 123456789101112131415161718192021// 创建Mapvar map: Map<String, Int>map = mapOf(\"Java\" to 86, \"Kotlin\" to 87) // 长度不可变// map.put(\"Flutter\", 88) // 无此方法,编译报错map = mutableMapOf(\"Java\" to 86, \"Kotlin\" to 87) // 长度可变map.put(\"Flutter\", 88) // 编译成功map = hashMapOf(\"Java\" to 86, \"Kotlin\" to 87) // HashMapmap = linkedMapOf(\"Java\" to 86, \"Kotlin\" to 87) // LinkedHashMap// 遍历Entryfor (en in map.entries) { Log.d(\"liuyang\", \"${en.key},${en.value}\")}// 解构遍历key,valuefor ((key, value) in map) { Log.d(\"liuyang\", \"${key},${value}\")}// 遍历keyfor (key in map.keys) { Log.d(\"liuyang\", \"${key},${map[key]}\")} Kotlin的函数或方法关键点: 独立存在称为函数(function),存在类里的称为方法(method) 语法: 1234567fun 函数名(参数名 : 参数类型)[:返回值类型]{ // 函数体}注意:1. 无法返回值:省略 或 :Unit(相当于Java的void)2. 参数:支持命名参数,默认值,可变参数(vararg) 函数可当变量的类型:var myfun : (Int , Int) -> Int = ::pow 函数在字节码里,通过类来实现 匿名函数的语法:fun(参数名 : 参数类型)[:返回值类型]{ } 内联函数: 语法:inline fun 函数名(参数名:参数类型)[:返回值类型]{ } 意义:提升代码量很少,但调用很频繁的函数开销 原理:增加代码来减少函数调用的时间开销,适用于代码量非常少的函数,如单表达式 123456789101112// 函数fun add(a: Int, b: Int): Int { return a + b}// 函数调用var c: Int = add(1, 2)var d: Int = add( b = 1, a = 2)// 创建一个函数类型var e: (Int, Int) -> Int = ::addvar f: Int = e(1, 2)//var g: Int = e(a=1, b=2) // 编译不通过 Lambda表达式关键点 语法: 123456{ 参数名 : 参数类型 -> 函数体 }注意:1. 最后一行默认return2. 如果只有一个参数,可以省略参数,使用it代替3. 显示添加return语句,不是返回其本身,而是返回其所在的函数 意义: 简化局部函数 简化函数式接口(函数式接口:只包含一个抽象方法的接口) 使用注意点: 方法的参数是函数或函数式接口时: 只一个参数:可省略括号 最后一参数时:Lambda表达式可写在圆括号外面 与局部函数或匿名内部类一样,可以访问所在函数的局部变量 — 注意:是变量的副本 支持解构,括号里的参数表示是解析的变量,如下两种写法: map.mapValues { entry ->”${entry.key}-${entry.value}!”} // 正常参数 map.mapValues { (key, value) -> ”${key}-${value}!” } // 解构 12345678910111213141516171819202122232425class MainActivity : AppCompatActivity() { var clickTime: Int = 0 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) var text: TextView = findViewById(R.id.textView2) var btn: Button = findViewById(R.id.btn) // OnClickListener是一个函数式接口(只有onClick(View)的方法) // setOnClickListener(OnClickListener) 此方法只有一个函数式接口参数,可省略括号 btn.setOnClickListener { view -> text.setText(\"${text.text} ${++clickTime}\") } // postDelayed(Runnable, long) 有两个参数,且Runnable参数不在最后,不能省略 btn.postDelayed({ text.setText(\"${text.text} ${++clickTime}\") }, 1000) }} Kotlin的面向对象:类构造器关键点: 分为主构造器与次构造器,互为重载方法,其语法如下: 12345678910111213141516171819202122232425262728293031// 主构造器,可以省略constructorclass Animal(name: String, age: Int) { var name: String var age: Int // 初始化块,相当于主构造器的函数体,注意:可以有多个初始化块 init { this.name = name this.age = age; } // 次构造器,必须调用主构造器 constructor() :this(\"\", 0) { } // 次构造器,必须调用主构造器 constructor(name: String): this(name, 0) { } // 次构造器,必须调用主构造器 constructor(age: Int): this(\"\", age) { } operator fun plus(other: Animal): Animal{ this.name = this.name + other.name this.age = this.age + other.age return this; }} 主构造器的参数使用var|val修饰时,即表示形参,也表示类的属性 12345678910111213141516// 主构造器,可以省略constructorclass Animal(var name: String, var age: Int) {// var name: String// var age: Int//// init {// this.name = name// this.age = age;// } operator fun plus(other: Animal): Animal{ this.name = this.name + other.name this.age = this.age + other.age return this; }} 意义:把构造器中相同的逻辑放在初始化块中,个性化的放在次构造器中 属性关键点: 语法与变量一样 对属性,默认生成getter,setter方法(val属性只提供getter),编译为字节码后生成如下成员: backing field: xxx属性 getter方法: getXXX() setter方法: setXXX() getter,setter方法可重载,在重载方法里,通过field访问backing field(为防止死循环) 注意: val属性会出现两种情况:— 判断依据:getter方法里没有调用field,就是计算属性 只有getter方法 — 称为计算属性 backing field 和 getter方法 — 只读属性 private属性,默认不会生成getter, setter方法,但如果重写getter,setter方法后,会生成 意义:很方便实现数据监听机制,类型Vue的MVVM框架 123456789101112// 只读属性val fur: String = \"red\" get() { return \"fur=${field}\" // 拦截fur变量,添加前缀 }// 计算属性val nameAndAge: String get(){ // 没有使用field属性,所以不会生成backing field return \"${this.name}-${this.age}\" } 类的方法方法与函数基本一致,略 对象关键点: 创建对象省略new关键字,如:var animal: Animal = Animal() 访问属性,本质是调用getter,setter方法: 123var animal: Animal = Animal()var name: String = animal.name // 实现是调用animal.getName()animal.age = 3 // 实际是调用animal.setAge(3) 支持解构:(解构:相当于一个运算符,通过operator重载) 123456789101112131415161718192021222324252627282930313233// Animal通过重载,支持解构class Animal constructor(name: String, age: Int) { var name: String var age: Int init { this.name = name this.age = age; } // 重载解构 operator fun component1(): String{ return this.name } // 重载解构 operator fun component2(): Int{ return this.age } operator fun plus(other: Animal): Animal{ this.name = this.name + other.name this.age = this.age + other.age return this; }}// 使用场景1var animal: Animal = Animal(\"\", 1)var (name, age) = animal// 使用场景2var list:List<Animal> = mutableListOf();for((name, age) in list){ printlin(\"${name}-${age}\")} 数据类: 语法:data class XXX() 意义:用于替换Java的Bean,自动提供解构方法,使用很方便123456// 通过数据类定义Animaldata class Animal(val name: String, val age: Int)// 自动支持解构var animal:Animal = Animal(\"\", 1)var (name, age) = animal import支持起别名 意义:方便包名不同,名称相同的类的使用 语法:import xxx as 别名 权限关键点: 类,方法,属性默认情况下:final public,通过open修饰后,才能被继承与重写 注意:在Kotlin里,final表示的含义与java有区别,只表示不能继承与重写,不表示只读,只读与常量的写法: 只读:val 变量名 —》java里的final 变量名 常量:const val 变量名 —》java里的static final 变量名 //同时只能定义在top-level,属于文件,不属于类 123456789// 定义常量,需处于top-level// 相当于java里的:static final String TAG = \"liuyang\"const val TAG: String = \"liuyang\"class Animal constructor(name: String, age: Int) { // 只读属性 // 相当于java里的:final String description = \"Animal Class\" val description: String = \"Animal Class\"} 继承与多态关键点: 继承: 语法:class SubClass : Superclass {} 顶级父类是Any,不是Object,区别:方法较少 构造器的执行顺序: 父类的主构造器(即初始化块) 父类的次构造器(前题是子类调了相应的次构造器) 子类的主构造器(即初始化块) 子类的次构造器 重写(方法和属性) 方法重写:override fun xxx() {} 属性重写:override var xxx: String = “图片” 使用: 类型判断:is 或 !is —>java里的instanceOf 强制转换: xxx as 类:强制转换,可能崩溃 xxx as? 类:安全的强制转换,转换失败返回null 12345678910111213141516171819202122232425262728293031open class Animal constructor(name: String, age: Int) { var name: String var age: Int init { this.name = name this.age = age; } open fun showDescription(): String{ return \"Animal description is ${this.name}-${this.age}\" }}// 继承Animal,并调用其主构造器class Dog(age: Int): Animal(\"dog\", age) { // 重写父类的showDescription()方法 override fun showDescription(): String { return \"Dog description is ${this.name}-${this.age}\" }}// 使用var animal: Animal = Dog(3) // 按java的思路的写法,不然会崩溃if(animal is Dog) { var dog: Dog = animal as Dog}// 更简单写法var dog: Dog? = animal as? Dog 类的扩展关键点: 语法: 方法扩展:fun Raw.info() { } 属性扩展:var Raw.fullName: String get(){} – 注意:由于只是添加了getter,setter方法,没有backing field,所以只能是计算属性 扩展的意义: 扩展可动态地为己有的类添加方法或属性,方式不在限定于继承或动态代理来实现 扩展能以更好的形式组织一些工具方法,更好的面向对象的代码风格,如Collections.sort(),应该是list.sort() 扩展的实现机制:Java是静态语言,类定义后,不支持扩展,kotlin的扩展不是真正的修改类,而是创建了一个函数,通过编译时,进行替换为调用对应的函数实现 123456789var list: List<Int> = mutableListOf(5, 4, 2, 1)// 给List类扩展sort排序方法fun List<Int>.sort(){ Collections.sort(this)}list.sort()Log.d(\"liuyang\", \"${list}\") // 输出结果为:[1, 2, 4, 5] 抽象与接口关键点: 抽象类:通过abstract修饰的类 接口: 语法:interface修饰,没有构造器与初始化块 方法:除了抽象方法外,还可包含有非抽象方法 属性:没有backing field,无法保存数据,默认都是抽象属性,但通过提供getter方法,可以改为非抽象属性 1234567891011121314151617181920interface Action { // 只读属性定义了 getter 方法,非抽象属性 val description: String get() { return \"\" } // 读写属性没有定义 getter、setter 方法,抽象属性 var name: String // 抽象方法 fun eat(food: String) // 非抽象方法 fun print(vararg msgs: String) { for (msg in msgs) { println(msg) } }} 对象表达式 && 对象声明 && 伴生对象关键点: 对象表达式: 作用:用于创建匿名内部类(区别在于:可以实现多个接口) 语法:object[: 0~N 个父类型] { //对象表达式的类体部分 } 注意:接口是函数式接口时,可以使用Lambda表达式,进一步简写,不一定要用对象表达式 对象声明: 作用:用于创建单例,无法再创建新的对象 语法:object ObjectName[: 0咽个父类型]{ } ObjectName是单例的名称 伴生对象 作用:用于实现Java里的静态成员,Kotlin为了保证面向对象的纯度,通过对象来实现静态成员的能力 语法:在类中定义的对象声明,可使用 companion修饰,这样该对象就变成了伴生对象 注意: 一个类只能定义一个伴生对象 伴生对象的对象名称可以省略 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849var btn: Button = findViewById(R.id.btn)// 对象表达式实现匿名内部类btn.setOnClickListener(object: View.OnClickListener { override fun onClick(v: View?) { text.setText(\"${text.text} ${++clickTime}\") }})// OnClickListener是函数式接口,可使用Lambda表达式btn.setOnClickListener { view -> text.setText(\"${text.text} ${++clickTime}\")}// 对象志明---单例object FoodManager { var foods: MutableList<String> init{ foods = mutableListOf<String>() // 初始化食物池 for (i in 1..9) { foods.add(\"food${i}\") } }}var foods = FoodManager.foods // 使用对象声明// 伴生对象---静态成员interface Outputable{ fun output(msg: String)}class MyClass{ // 定义的MyClass的伴生对象 companion object: Outputable{ val name = ”name属性值” //重写父接口中的抽象方法 override fun output(msg: String) { for(i in 1..6){ println (”<h$(i}>${ msg}</h${i}>”) } } }}fun main(args: Array<String>) { // 调用伴生对象里的方法与属性,与调用静态成员一样 MyClass.output(\"fkit.org\") println(MyClass.name)} 类委托 && 属性委托关键点: 类委托 用处:让多个对象共享同一个委托对象,代理模式的应用,继承的一种替代,让本类需要实现的部分方法委托给其他对象 语法:接口 by 对象 属性委托 用处:多个类的类似属性统一交给委托对象集中实现 语法:var属性名:属性类型 by 对象 1234567891011121314151617181920212223242526272829303132333435363738interface Outputable { var type: String fun output(msg: String)}// 定义一个DefaultOutput类实现Outputable接口class DefaultOutput: Outputable { override var type: String = \"输出设备\" override fun output(msg: String) { for(i in 1..6){ println(\"<h${i}>${msg}</h${i}>\") } }}// 指定b为委托对象,也可以通过继承DefaultOutput来实现,java里还可以通过Proxy来实现class Printer(b: DefaultOutput): Outputable by b// 接口被委托后,还可以重写方法,重写后,就会调用自己重新的方法了class Printer(b: DefaultOutput): Outputable by b { // 重写被委托的方法 override fun output(msg: String) { javax.swing.JOptionPane . showMessageDialog(null, msg); }}// 属性委托class PropertyDelegation { var name: String by MyDelegation()}class MyDelegation { private var _backValue = \"默认值\" operator fun getValue(thisRef: PropertyDelegation, property: KProperty<*>): String { println(\"${thisRef}的${property.name}属性执行getter方法\") return _backValue } operator fun setValue(thisRef: PropertyDelegation, property: KProperty<*>, newValue: String){ println(\"${thisRef}的${property.name}属性执行setter方法\") _backValue = newValue }} Kotlin的异常处理关键点: 与java的区别:Kotlin抛弃了checked异常,所有异常都是runtime异常,可捕获也可不捕获 finally块里的return语句会导致try catch里的return语句失效 Kotlin的泛型关键点: 泛型的语法与Java的类似:open class Apple{ } 型变: 用处:当实际类型是泛型的子类时,Kotlin使用型变替换了Java的通配符 分为:声明处型变,类型投影等等,就不详细介绍了,网上有很多资源 与Java的泛型的对比 123456789// Java的基本泛型class Foo<T> {}// 对应Kotlin的写法class Foo<T> {}// Java的通配符class Foo<T extends View> {}// 对应Kotlin的写法class Foo<out T: View> {} Kotlin的注解流程与Java类似,三个过程: 注解定义:annotation class Test(val name: String) 注解使用:@Test(name=“xx”) class MyClass{ } — 注意:属性名为value时,可以省略属性名value 读取注解:val anArr = Test: :info .annotations 修饰注解:元注解 @Retention:注解的保留时间,SOURCE,BINARY,RUNTIME @Target:修饰哪些程序单元,即范围。CLASS,FUNCTION,FIELD @MustBeDocumented:此注解将会被提取到Api文档里 @Repeatable:可重复注解 Kotlin与Java互调的注意点看如下例子:12345678910111213141516171819202122// java类public class Utils { public static String format(String text) { return text.isEmpty() ? null : text; }}// kotlin调用此java类的三种方式fun doSth(text: String) { // 方式一:指定为不为null类型 val f: String = Utils.format(text) // 能通过编译 println (\"f.len : ${f.length}\") // 运行时可能会报错 // 方式二:不指定类型,让其做类型推断 val f = Utils.format(text) // 能通过编译 println (\"f.len : ${f.length}\") // 运行时可能会报错 // 方式三:指定为可null类型 val f: String? = Utils.format(text) // 能通过编译 println (\"f.len : ${f.length}\") // 编译报错 println (\"f.len : ${f?.length}\") // 编译成功,运行时不会报错} 调用方法的正确姿势:(应该养成如下编译习惯) 了解方法的返回值文档,了解其是否会返回null — 不管是调用Java方法还是Kotlin方法 变量指定类型,而不是使用推断类型(推断有时不是那么的智能) 参考 书籍:疯狂Kotlin讲义 Kotlin官方文档 深入浅出 Java 8 Lambda 表达式 抛弃 Java 改用 Kotlin 的六个月后,我后悔了","categories":[{"name":"Android","slug":"Android","permalink":"https://handsomeliuyang.github.io/categories/Android/"}],"tags":[{"name":"Kotlin","slug":"Kotlin","permalink":"https://handsomeliuyang.github.io/tags/Kotlin/"}]},{"title":"LeetCode Largest Number","slug":"LeetCodeLargestNumber","date":"2019-04-26T00:42:51.000Z","updated":"2019-05-05T07:23:51.119Z","comments":true,"path":"2019/04/26/LeetCodeLargestNumber/","link":"","permalink":"https://handsomeliuyang.github.io/2019/04/26/LeetCodeLargestNumber/","excerpt":"","text":"题目Given a list of non negative integers, arrange them such that they form the largest number. Example 1:12Input: [10,2]Output: "210" Example 2:12Input: [3,30,34,5,9]Output: "9534330" Note: The result may be very large, so you need to return a string instead of an integer. 解决看到此题目后的直接思路是:按位排序,再合并为字符串,如[3,30,34,5,9] 先按首位3, 3, 3, 5, 9排序:[9,5,3,30,34] 再排序3, 30, 34, 取其第二位排序(如没有第二位,假定与第一位一样):[34, 3, 30] 最后的排序结果:[9, 5, 34, 3, 30] 实现代码的思路: 此思路与基数排序的过程类似,代码如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869class Solution { public String largestNumber(int[] nums) { // 思路:利用基数排序类似的思想排序 ArrayList<Integer> array = new ArrayList<Integer>(); for(int i=0; i<nums.length; i++){ array.add(nums[i]); } ArrayList<Integer> result = RadixSort(array, 1); StringBuffer buffer = new StringBuffer(); for(int i=result.size()-1; i>=0; i--){ buffer.append(result.get(i)); } return buffer.toString(); } public ArrayList<Integer> RadixSort(ArrayList<Integer> array, int digit){ // 分配桶 ArrayList<ArrayList<Integer>> bucket = new ArrayList<ArrayList<Integer>>(); for(int i=0; i<=9; i++){ bucket.add(new ArrayList<Integer>()); } // 入桶 int div = 10 ^ digit; for(int i=0; i<array.size(); i++){ int value = array.get(i); while(value >= div){ value = value / div; } value = value % 10; bucket.get(value).add(array.get(i)); } ArrayList<Integer> result = new ArrayList<Integer>(); for(int i=0; i<bucket.size(); i++){ int size = bucket.get(i).size(); if(size == 0) { continue; } else if(size == 1){ result.add(bucket.get(i).get(0)); } else { boolean isFinish = true; for(int j=0; j<bucket.get(i).size(); j++){ if(bucket.get(i).get(j) > div){ isFinish = false; } } if(isFinish) { result.addAll(bucket.get(i)); } else { // 先递归排序 ArrayList<Integer> sorted = RadixSort(bucket.get(i), digit+1); result.addAll(sorted); } } } return result; }} 此算法的问题是:无法处理大数据,计算int div = 10 ^ digit;时,很容易出现div越界的问题,虽然也能解决,但复杂程序太大了。 优化两个数字的比较过程: 如数字int a, int b 先转换为String:String aStr, String bStr 比较:aStrbStr, bStraStr 剩下的就是一个排序的过程了,有很多排序算法可以选择,这里直接使用Java的Arrays.sort()方法,其里使用快速排序与归并排序,时间复杂度为O(n*logn)12345678910111213141516171819202122232425262728293031323334class Solution { private class LargestNumberComparator implements Comparator<String> { @Override public int compare(String str1, String str2){ String order1 = str1+str2; String order2 = str2 + str1; return order2.compareTo(order1); // 此处是重点,要理解compareTo()方法 } } public String largestNumber(int[] nums) { // 转换为字符串数组 String[] numStrs = new String[nums.length]; for(int i=0; i<nums.length; i++){ numStrs[i] = String.valueOf(nums[i]); } // 对字符串数组进行排序 Arrays.sort(numStrs, new LargestNumberComparator()); // 特例:全为0的情况 if(numStrs[0].equals(\"0\")){ return \"0\"; } // 拼接为字符串输出 StringBuffer buffer = new StringBuffer(); for(int i=0; i<numStrs.length; i++){ buffer.append(numStrs[i]); } return buffer.toString(); }}","categories":[{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/categories/算法/"}],"tags":[{"name":"leetcode","slug":"leetcode","permalink":"https://handsomeliuyang.github.io/tags/leetcode/"},{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/tags/算法/"}]},{"title":"Flutter在58App上的深度调研","slug":"Flutter在58App上的深度调研","date":"2019-04-16T03:23:34.000Z","updated":"2019-04-23T03:18:12.228Z","comments":true,"path":"2019/04/16/Flutter在58App上的深度调研/","link":"","permalink":"https://handsomeliuyang.github.io/2019/04/16/Flutter在58App上的深度调研/","excerpt":"","text":"背景现在跨平台的框架主要有如下几种: ReactNative,Weex kotlin-native Flutter 小程序 Hybrid 长期来看,跨平台开发一定会是一个趋势,因为其能带来如下好处: 减少开发成本,提升开发效率 动态部署,不依赖发版 但现阶段,框架很多,各有各的优缺点,对于应用开发的RD来说,面临一个框架如何选择的难题。在行业趋势没有真正出现之前,RD应该要勇于去学习,去尝试新框架,学习其设计思想,体验其优势与劣势,找到最适合自己的框架。 之前对Flutter做过简单应用的尝试(Flutter实现Git权限分配工具之旅),但不够深入,任何一个框架在没有真正进行深入实践时,根本无法判断其优缺点,为了不浮于表面,人云亦云的去判定Flutter框架,才有了这次的调研:基于Flutter实现58App的首页功能(首页模块是58App相对比较复杂的模块) 具体实现首页tab框架实现效果 在Flutter的Material Widget里,有BottomNavigationBar和TabBar两个类似的效果,但都无法直接使用,改造成本非常的大,最终选择自定义实现底部栏。 自定义ImageButton WidgetImageButton的要求: 支持图片与文本 支持两种状态:default,active 不同状态有不同的图片,不同的文本颜色 实现思路: InkResponse Widget实现处理点击事件 Column布局 StatelessWidget,通过props来修改状态 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051import 'package:flutter/material.dart';class ImageButton extends StatelessWidget { final double width; final double height; final String imageAssetName; final String activeImageAssetName; final GestureTapCallback onTap; final String text; final Color textColor; final Color activeTextColor; final bool isActive; const ImageButton({Key key, @required this.width, @required this.height, @required this.imageAssetName, @required this.activeImageAssetName, this.text, this.textColor, this.activeTextColor, this.onTap, @required this.isActive }) : super(key: key); @override Widget build(BuildContext context) { return InkResponse( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ Image.asset( this.isActive ? this.activeImageAssetName : this.imageAssetName, width: width, height: height, fit: BoxFit.contain ), Text( this.text, style: TextStyle(color: this.isActive ? this.activeTextColor : this.textColor), ) ], ), onTap: onTap, ); }} 自定义HomeBottomNavigationBar Widget要求: tabItem数量为奇数,中间的发布大小凸出来 能与TabBarView联动 实现思路: Container Widget设置高度,背景 Row,Expanded做等分 Padding设置每个tabItem的paddingTop 通过TabController实现与TabBarView联动 tabController 继承 ChangeNotifier,ChangeNotifier是用于通知观察机制 _controller.addListener()来监听TabBarView的切换 _controller.animateTo(i)来通知tab的切换 代码如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157import 'package:flutter/material.dart';import 'package:flutter_gallery/wuba_demo/home/publish/publish_home.dart';import '../wuba_ui/button/image_button.dart';class NavigationItem { final String title; final String icon; final String activeIcon; NavigationItem({ this.title, this.icon, this.activeIcon });}class HomeBottomNavigationBar extends StatefulWidget { final List<NavigationItem> items; final Function onTap; final TabController controller; final Color defaultColor; final Color selectColor; HomeBottomNavigationBar({ @required this.items, this.onTap, @required this.controller, @required this.defaultColor, @required this.selectColor }); @override _HomeBottomNavigationBarState createState() => _HomeBottomNavigationBarState();}class _HomeBottomNavigationBarState extends State<HomeBottomNavigationBar> { int _currentIndex; TabController _controller; @override void initState() { super.initState(); _updateTabController(); } @override void didUpdateWidget(HomeBottomNavigationBar oldWidget) { super.didUpdateWidget(oldWidget); _updateTabController(); } @override void dispose() { if (_controller != null) { _controller.removeListener(_handleTabControllerTick); } super.dispose(); } void _handleTabControllerTick() { debugPrint('_handleTabControllerTick ${_controller.index}'); if (this._currentIndex != _controller.index) { setState(() { this._currentIndex = _controller.index; }); } } void _updateTabController() { if (widget.controller == _controller) { return; } // 移除老的controller的listener if (_controller != null) { _controller.removeListener(_handleTabControllerTick); } _controller = widget.controller; if (_controller != null) { _controller.addListener(_handleTabControllerTick); _currentIndex = _controller.index; } } @override Widget build(BuildContext context) { var children = <Widget>[]; // 添加正常的tab选项 for (var i = 0; i < widget.items.length; i++) { var navigationItem = widget.items[i]; children.add(Expanded( flex: 1, child: Padding( padding: EdgeInsets.only(top: 15), child: ImageButton( width: 23, height: 23, imageAssetName: navigationItem.icon, activeImageAssetName: navigationItem.activeIcon, text: navigationItem.title, textColor: widget.defaultColor, activeTextColor: widget.selectColor, isActive: this._currentIndex == i, onTap: () { if (this._controller != null) { this._controller.animateTo(i); } if (widget.onTap != null) { widget.onTap(i); } }, ), ) )); } // 添加发布item children.insert(2, Expanded( flex: 1, child: ImageButton( width: 40, height: 40, imageAssetName: 'assets/images/home/wb_home_tab_publish_img.png', activeImageAssetName: '', text: '发布', textColor: widget.defaultColor, isActive: false, onTap: (){ Navigator.push(context, PageRouteBuilder( transitionDuration: Duration(), pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation){ return PublishHome(); } )); }, ), )); return Container( height: 63, decoration: BoxDecoration( image: DecorationImage( image: AssetImage('assets/images/home/wb_tab_bg.png'), fit: BoxFit.fill ) ), child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, children: children, ) ); }} 首页tab实现思路: Stack Positioned实现叠层布局,解决tabbar凸起部份覆盖在TabBarView上 TabBarView Widget实现类似ViewPager效果 代码如下:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798import 'package:flutter/material.dart';import 'home_bottom_navigation_bar.dart';import 'package:wubarn_plugin/wuba_rn_view.dart';class HomeDemo extends StatefulWidget { static const String routeName = '/wuba/home'; const HomeDemo({ Key key }) : super(key: key); @override _HomeDemoState createState() => _HomeDemoState();}class _HomeDemoState extends State<HomeDemo> with SingleTickerProviderStateMixin { List<NavigationItem> _navigationViews; TabController controller; @override void initState() { super.initState(); _navigationViews = <NavigationItem>[ NavigationItem( icon: 'assets/images/home/wb_home_tap_index_normal.png', activeIcon: 'assets/images/home/wb_home_tap_index_pressed.png', title: '首页', ), NavigationItem( icon: 'assets/images/home/wb_home_tap_history_normal.png', activeIcon: 'assets/images/home/wb_home_tap_history_pressed.png', title: '部落', ), NavigationItem( icon: 'assets/images/home/wb_home_tap_message_normal.png', activeIcon: 'assets/images/home/wb_home_tap_message_pressed.png', title: '消息', ), NavigationItem( icon: 'assets/images/home/wb_home_tap_center_normal.png', activeIcon: 'assets/images/home/wb_home_tap_center_pressed.png', title: '我的', ) ]; controller = TabController( initialIndex: 2, length: this._navigationViews.length, vsync: this); } @override Widget build(BuildContext context) { return Scaffold( body: Stack( children: <Widget>[ Positioned( top: 0, left: 0, right: 0, bottom: 50, child: TabBarView( controller: controller, children: <Widget>[ Container( color: Colors.red, child: Text('Fragment'), ), Container( child: WubaRNView(), ), Container( color: Colors.white, child: Text('Fragment'), ), Container( color: Colors.yellow, child: Text('Fragment'), ) ] ), ), Positioned( left: 0, right: 0, bottom: 0, height: 63, child: HomeBottomNavigationBar( items: this._navigationViews, controller: this.controller, defaultColor: Colors.black, selectColor: Colors.red, ), ) ], ), ); }} 内嵌ReactNative实现思路: 通过独立的Flutter Plugin实现 ReactNative的ReactRootView可以被嵌入Native中,那同样可以被嵌入Flutter中 Flutter的AndroidView只有两个状态:create,dispose。在这两个状态里,执行ReactNative相关的生命周期函数 dart部分:创建对应的Widget12345678910111213141516171819202122232425class WubaRNView extends StatefulWidget { @override _WubaRNViewState createState() => _WubaRNViewState();}class _WubaRNViewState extends State<WubaRNView> { @override Widget build(BuildContext context) { // 不同的端,其通信方式不一样 if (defaultTargetPlatform == TargetPlatform.android) { return AndroidView( viewType: 'plugins.wuba.com/wubarnview', onPlatformViewCreated: _onPlatformViewCreated, ); } return Text( '$defaultTargetPlatform is not yet supported by the WubaRNView plugin'); } void _onPlatformViewCreated(int id) { }} Android的实现: 注册ViewFactory 12345678public class WubarnPlugin { public static final String VIEW_TYPE = \"plugins.wuba.com/wubarnview\"; /** Plugin registration. */ public static void registerWith(Registrar registrar) { registrar.platformViewRegistry().registerViewFactory(VIEW_TYPE, new WubarnViewFactory(registrar.messenger())); }} 通过ViewFactory创建WubarnView 12345678910111213public class WubarnViewFactory extends PlatformViewFactory { private final BinaryMessenger messenger; public WubarnViewFactory(BinaryMessenger messenger) { super(StandardMessageCodec.INSTANCE); this.messenger = messenger; } @Override public PlatformView create(Context context, int id, Object o) { return new WubarnView(context, messenger, id); }} WubarnView的具体实现 1234567891011121314151617181920212223242526272829303132333435363738394041424344public class WubarnView implements PlatformView, MethodChannel.MethodCallHandler{ private final ReactRootView mReactRootView; private final ReactInstanceManager mReactInstanceManager; public WubarnView(Context context, BinaryMessenger messenger, int id) { MethodChannel methodChannel = new MethodChannel(messenger, WubarnPlugin.VIEW_TYPE + \"_\" + id); methodChannel.setMethodCallHandler(this); // ReactNative的创建及初始化,设置其默认加载的bundle名称 mReactRootView = new ReactRootView(context); mReactInstanceManager = ReactInstanceManager.builder() .setApplication((Application) context.getApplicationContext()) .setBundleAssetName(\"index.android.bundle\") .setJSMainModulePath(\"index\") .addPackage(new MainReactPackage()) .setUseDeveloperSupport(false) .setInitialLifecycleState(LifecycleState.RESUMED) .build(); // 这个\"App1\"名字一定要和我们在index.js中注册的名字保持一致AppRegistry.registerComponent() mReactRootView.startReactApplication(mReactInstanceManager, \"App1\", null); } @Override public View getView() { return mReactRootView; } @Override public void dispose() { // mReactInstanceManager.onHostPause(mActivity); // mReactInstanceManager.onHostResume(mActivity, null); // mReactInstanceManager.onHostDestroy(mActivity); mReactRootView.unmountReactApplication(); } @Override public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { switch (methodCall.method){ case \"\": break; default: result.notImplemented(); } }} 上面初始化ReactInstanceManager当中的常量,与React代码是一一对应的 “App1”:与在React里注册的组件名称是一样的 1234import { AppRegistry } from 'react-native';import App from './App';AppRegistry.registerComponent('App1', () => App); .setJSMainModulePath(“index”):JS bundle中主入口的文件名,是React工程里的入口文件index.js的名称 .setBundleAssetName(“index.android.bundle”):这个是内置到assets目录下的bundle名称,与bundle生成命令有关 1react-native bundle --platform android --dev false --entry-file index.js --bundle-output /Users/ly/liuyang/workspace_flutter/wubarn_plugin/example/android/app/src/main/assets/index.android.bundle --assets-dest /Users/ly/liuyang/workspace_flutter/wubarn_plugin/example/android/app/src/main/res/ 发布入口页实现效果 切换效果实现思路: 通过PageRoute,去掉切换的动画 通过AnimatedBuilder,实现旋转动画 通过WillPopScope Widget拦截返回事件 Flutter的页面切换是由Navigator管理,其中有一个栈,栈帧是路由,通过PageRoute可以自定义切换的动画,如下去掉切换动画的代码:123456Navigator.push(context, PageRouteBuilder( transitionDuration: Duration(), // 去掉了执行动画的时间 pageBuilder: (BuildContext context, Animation<double> animation, Animation<double> secondaryAnimation){ return PublishHome(); })); 由于Flutter是MVVM框架,Flutter里的Animation只负责计算,不负责界面布局与渲染,需要手动调用setState()来让界面重绘,不过可以通过AnimatedBuilder简化流程,但Flutter在实现组合动画比较麻烦。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081class PublishHome extends StatefulWidget { @override _PublishHomeState createState() => _PublishHomeState();}class _PublishHomeState extends State<PublishHome> with SingleTickerProviderStateMixin { Animation<double> animation; AnimationController controller; @override void initState() { super.initState(); controller = AnimationController(duration: Duration(milliseconds: 200), vsync: this); animation = Tween(begin: 0.0, end: 45.0).animate(controller); animation.addStatusListener((AnimationStatus status){ if(status == AnimationStatus.dismissed) { Navigator.pop(context); } }); controller.forward(); } @override Widget build(BuildContext context) { return WillPopScope( onWillPop: () async { controller.reverse(); return false; }, child: Scaffold( backgroundColor: Colors.white, body: SafeArea( top: true, child: Stack( children: <Widget>[ ... Positioned( left: 0, right: 0, bottom: 0, height: 63, child: GestureDetector( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: <Widget>[ AnimatedBuilder( animation: this.animation, builder: (BuildContext context, Widget child){ return Transform.rotate( angle: animation.value * math.pi / 180.0, child: child, ); }, child: Image.asset( 'assets/images/home/wb_home_tab_publish_img.png', width: 40, height: 40, fit: BoxFit.contain ), ), Text( '发布', style: TextStyle(color: Colors.white), ) ], ), onTap: (){ controller.reverse(); }, ), ) ], ), ), ) ); }} 渐变按钮要求: 不使用图片实现 背景支持渐变 不要点击效果 Material Widget里的四种Button无法满足按钮要求,第三方渐变按钮也无法完全满足要求,通过Container Widget的decoration自定义此Widget:1234567891011121314151617181920212223242526272829303132import 'package:flutter/material.dart';class GradientButton extends StatelessWidget { final double width; final double height; final Gradient gradient; final Widget child; final Function onTap; final BorderRadius shapeRadius; const GradientButton( {Key key, this.width, this.height, this.gradient, this.onTap, this.shapeRadius, this.child}) : super(key: key); @override Widget build(BuildContext context) { return GestureDetector( onTap: this.onTap, child: Container( width: this.width, height: this.height, decoration: BoxDecoration( gradient: this.gradient, // 设置渐变 borderRadius: this.shapeRadius // 设置圆角 ), child: Center( child: child, ) ), ); }} 部落图片选择控件实现效果 底部抽屉效果要求: BottomSheet增加中间态 有回弹效果 第三方库RubberBottomSheet实现了此效果,其原理如下: 通过Stack实现叠加布局 修改AnimationController的原码,依据lowerBound,upperBound的实现思路,实现halfBound,即中间态 直接使用RubberBottomSheet的代码非常简单:123456789101112131415161718192021222324252627282930313233343536373839class TribePublish extends StatefulWidget { @override _TribePublishState createState() => _TribePublishState();}class _TribePublishState extends State<TribePublish> with SingleTickerProviderStateMixin { RubberAnimationController _controller; @override void initState() { super.initState(); _controller = RubberAnimationController( vsync: this, lowerBoundValue: AnimationControllerValue(pixel: 54), halfBoundValue: AnimationControllerValue(pixel: 300), upperBoundValue: AnimationControllerValue(percentage: 1.0), duration: Duration(milliseconds: 200) ); } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.white, appBar: AppBar( title: Text('部落发布'), ), body: RubberBottomSheet( header: _getHeader(), lowerLayer: _getLowerLayer(), upperLayer: _getUpperLayer(), animationController: _controller, ) ); }} 加载并显示相册图片加载相册图片 通过MethodChannel,实现与Native通信,加载相册图片 在Android里,加载相册图片,需要先授权 防止相册图片过多,需进行分页加载 Android端的代码实现:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798public class AlbumManagerPlugin implements MethodChannel.MethodCallHandler { public static void registerWith(PluginRegistry registry) { registerWith(registry.registrarFor(\"com.wuba.plugins.AlbumManagerPlugin\")); } public static void registerWith(PluginRegistry.Registrar registrar){ final MethodChannel channel = new MethodChannel(registrar.messenger(), \"plugins.wuba.com/album_manager\"); channel.setMethodCallHandler(new AlbumManagerPlugin(registrar.context(), registrar)); } /** * the page size of query albums */ public static final int PAGE_SIZE = 200; private final Context mContext; private final PluginRegistry.Registrar mRegistrar; private PermissionsUtils mPermissionsUtils; public AlbumManagerPlugin(Context context, PluginRegistry.Registrar registrar) { this.mContext = context; mRegistrar = registrar; mPermissionsUtils = new PermissionsUtils(); registrar.addRequestPermissionsResultListener(new PluginRegistry.RequestPermissionsResultListener() { @Override public boolean onRequestPermissionsResult(int i, String[] strings, int[] ints) { mPermissionsUtils.dealResult(i, strings, ints); return false; } }); } @Override public void onMethodCall(MethodCall methodCall, MethodChannel.Result result) { // 先申请权限 mPermissionsUtils.setPermissionsListener(new PermissionsListener() { @Override public void onDenied(String[] deniedPermissions) { Log.i(\"permission\", \"onDenied call.method = ${call.method}\"); result.error(\"失败\", \"权限被拒绝\", \"\"); } @Override public void onGranted() { switch (methodCall.method){ case \"getAllImage\": getAllImage(methodCall, result); break; default: result.notImplemented(); } } }); mPermissionsUtils.withActivity(mRegistrar.activity()); mPermissionsUtils.getPermissions(mRegistrar.activity(), 3001, Manifest.permission.READ_EXTERNAL_STORAGE, Manifest.permission.WRITE_EXTERNAL_STORAGE); } private void getAllImage(MethodCall methodCall, MethodChannel.Result result) { List<String> list = new ArrayList<String>();// int pageIndex = methodCall.argument(\"pageIndex\"); int pageIndex = 0; Log.d(\"liuyang\", \"\" + methodCall.argument(\"pageIndex\")); String[] projection = {MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME}; String sortOrder = MediaStore.Images.Media.DATE_TAKEN + \" DESC limit \" + PAGE_SIZE + \" offset \" + pageIndex * PAGE_SIZE; //执行分页 String selection = null;// if (!ALL_PHOTO.equals(s)) {// selection = MediaStore.Images.ImageColumns.BUCKET_DISPLAY_NAME + \" = '\" + s + \"' \";// } Cursor cursor = mContext.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, projection, selection, null, sortOrder); try { if (cursor != null) { while (cursor.moveToNext()) { // 获取图片的路径 String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.ImageColumns.DATA)); list.add(path); } result.success(list); } } catch (Exception e) {// LOGGER.e(TAG, e.toString()); result.error(\"AlbumManagerPlugin\", e.getMessage(), \"\"); } finally { if (cursor != null) { cursor.close(); } } }} Flutter端的代码实现:12345678910111213141516171819class AlbumManagerPlugin { static const MethodChannel _channel = MethodChannel('plugins.wuba.com/album_manager'); static Future<List<AssetEntity>> getAllAssetList(int pageIndex) async { Map<dynamic, dynamic> map = Map<dynamic, dynamic>(); map['pageIndex'] = pageIndex; List<dynamic> paths = await _channel.invokeMethod('getAllImage', map); return _castAsset(paths); } static Future<List<AssetEntity>> _castAsset(List<dynamic> paths) async { List<AssetEntity> result = <AssetEntity>[]; for (var i = 0; i < paths.length; i++) { result.add(AssetEntity(path: paths[i])); } return result; }} 细节点: Native的扩展能力定义为Plugin,Plugin可以独立发布为一个库,里面即有native代码也有dart代码,不用像ReactNative,需要单独合并native的代码,但带的问题是:dependencies库都是直接原码 通过MethodChannel进行Flutter与Native通信,可以传递参数,如何传递一组参数了,通过源码分析:Map对象 分页显示图片 通过GridView显示图片,实现分页加载 默认的图片加载策略是LRU,体验与内存表现都很不好 下面的代码没有实现分页与图片加载策略的优化:1234567891011121314151617181920212223242526272829303132333435363738394041424344class AlbumGrid extends StatefulWidget { @override _AlbumGridState createState() => _AlbumGridState();}class _AlbumGridState extends State<AlbumGrid> { List<AssetEntity> list = new List<AssetEntity>(); int currentPage = -1; @override void initState() { super.initState(); // 加载第一页数据 _initData(0); } void _initData(int nextPage) async { List<AssetEntity> newPage = await AlbumManagerPlugin.getAllAssetList(nextPage); this.setState((){ list.addAll(newPage); currentPage = nextPage; }); } @override Widget build(BuildContext context) { return GridView.builder( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 4, mainAxisSpacing: 4.0, crossAxisSpacing: 4.0 ), padding: EdgeInsets.all(4.0), itemBuilder: _itemBuilder, itemCount: list.length, ); } Widget _itemBuilder(BuildContext context, int index) { AssetEntity entity = list[index]; return Image.file( File(entity.path), fit: BoxFit.cover, ); }} 结论Flutter框架在设计上,整体优于其他跨平台框架,实现使用时,也是非常的方便,有如下感受: 开发调试非常的快,比Android的instant run强很多,也稳定很多 dependencies依赖管理比ReactNative强,native扩展能力是一个独立的plugin库,便于管理依赖 基于MVVM框架,在自定义UI组件及动画方面,结构清楚,容易理解 实现相同的功能,代码量远小于使用java实现 由于Flutter的社区不太完善,时间太短,生态不完善,相当于2011年开发Android一样,缺少大量成熟的基础库,大量的基础能力都需要从头到尾开发,下面是上述实践过程中发现的一些点: 渐变Button,图片Button GridView或ListView的图片加载策略(Fling时不加载,scrolling或idle时加载) 崩溃日志收集 大量的基础Plugin:加载相册,授权,地图,视频等等 … 在已经集成ReactNative的58App里,已经基本满足部分业务的动态能力,再花大量的成本完美Flutter的基础,花大量的成本去推动业务线使用,短期来看,投入产出比太低。 但从长期来看,在跨平台框架上,我更加看好Flutter,在设计与使用体验上,Flutter确实都优于其他框架,但Flutter最终能否成为主流,还是要看Google的推广力度。 持续关注跨平台框架的动态,ReactNative也在向Flutter学习,改进其性能差的一面,Flutter的基础库也在不断的完善中 此demo的代码:wuba_gallery 参考 React Native 混合开发(Android篇)","categories":[],"tags":[]},{"title":"百度小程序源码解读","slug":"百度小程序源码解读","date":"2019-02-24T16:00:00.000Z","updated":"2019-02-26T02:34:32.701Z","comments":true,"path":"2019/02/25/百度小程序源码解读/","link":"","permalink":"https://handsomeliuyang.github.io/2019/02/25/百度小程序源码解读/","excerpt":"","text":"整体框架百度小程序的开发Api,加构设计与微信小程序基本一致。 为了提升整体性能,充分利用手机的多CPU性能: 把逻辑层与渲染层分离,分别位于不同的运行容器 异步请求都由native来执行 逻辑层 逻辑层就是对开发者所暴露的Api,有App,Page,布局文件,其中的App,Page都是两个函数 App()函数的处理:直接创建App对象,全局唯一对象 Page()函数的处理:保存到Map中,不会马上构建Page对象,当导航到页面时,才会真正创建Page对象 渲染层 使用MVVM框架san来渲染界面 在编译期间把小程序标签转化为san框架所支持的标签 为每个小程序页面,创建对应的san框架下Page组件,PageComponent的template就是swan.xml转译后的内容 渲染层与逻辑层交互 渲染层接收用户的交互事件,由统一的函数处理后,通过消息总线传递到逻辑层的Page对象,再调用对应的函数 逻辑层依据用户操作,执行业务操作,修改data数据,通过消息总线传递到渲染层的组件里,San.Page组件会自动更新界面 开发流程编译一个简单的百度小程序项目: 目录结构: App.js的源码: 12345678910111213App({ onLaunch(event) { console.log('onLaunch'); }, onShow(event) { console.log('onShow'); }, globalData: { userInfo: 'user' }}); index.js的源码: 1234567891011121314151617181920var p = [];Page({ data: { text: \"这是一段文字.\" }, add: function(e) { p.push(\"其他文字\"); this.setData({ text: \"这是一段文字.\" + p.join(\",\") }) }, remove: function(e) { if(p.length > 0){ p.pop(); this.setData({ text: \"这是一段文字.\" + p.join(\",\") }); } }}); index.swan的源码: 12345<view> <view class=\"text-px text-{{text}}\">{{text}}</view> <button class=\"btn\" type=\"primary\" bind:tap=\"add\">add text</button> <button class=\"btn\" type=\"primary\" bind:tap=\"remove\">remove text</button></view> 以上的小程序代码,经过编译后的情况: 目录结构: App.js的源码: 12345678910111213141516171819202122232425262728293031323334353637383940414243window.define(\"138\", function(t, e, n, o, a, i, s, c, r, u, d, l, g, w, f, h) { var p = []; Page({ data: { text: \"这是一段文字.\" }, add: function(t) { p.push(\"其他文字\"); this.setData({ text: \"这是一段文字.\" + p.join(\",\") }) }, remove: function(t) { p.length > 0 && (p.pop(), this.setData({ text: \"这是一段文字.\" + p.join(\",\") })) } })});window.define(\"193\", function(t, e, n, o, a, i, s, c, r, u, d, l, g, w, f, h) { App({ onLaunch: function(t) { console.log(\"Lifecycle App onLaunch\") }, onShow: function(t) { console.log(\"Lifecycle App onShow\") }, globalData: { userInfo: 'user' } })});window.__swanRoute = \"app\";window.usingComponents = [];require(\"193\");window.__swanRoute = \"pages/text/text\";window.usingComponents = [];require(\"138\"); index.swan.js的源码: 12345678910111213141516171819202122232425262728293031323334353637383940414243// 注意,做了一些简化((global)=>{ global.errorMsg = []; var templateComponents = Object.assign({}, {}); var param = {}; var filterArr = JSON.parse(\"[]\"); try { filterArr && filterArr.forEach(function (item) { param[item.module] = eval(item.module) }); var pageContent = ` <view> <view>{{text}}</view> <button class=\\\"btn\\\" type=\\\"primary\\\" on-bindtap=\\\"eventHappen('tap', $event, 'add', '', 'bind')\\\"> add text </button> <button class=\\\"btn\\\" type=\\\"primary\\\" on-bindtap=\\\"eventHappen('tap', $event, 'remove', '', 'bind')\\\"> remove text </button> </view>`; var renderPage = function (filters, modules) { // 路径与该组件映射 // var customAbsolutePathMap = (global.componentFactory.getAllComponents(), {}); // 当前页面使用的自定义组件 // const pageUsingComponentMap = JSON.parse(\"{}\"); // 生成该页面引用的自定义组件 // const customComponents = Object.keys(pageUsingComponentMap).reduce((customComponents, customName) => { // customComponents[customName] = customAbsolutePathMap[pageUsingComponentMap[customName]]; // return customComponents; // }, {}); global.pageRender(pageContent, templateComponents) }; renderPage(filterArr, param); } catch (e) { global.errorMsg['execError'] = e; throw e; }})(window); 编译总结: 对template进行转换: 标签转换:bind:tap ===> on-bindtap 事件包装:eventHappen(‘tap’, $event, ‘add’, ‘’, ‘bind’) 对App.js进行包装,提升效率,减少逐一加载流程 通过渲染模板,生成index.swan.js文件,提升渲染效率 加载,启动,渲染用户点击跳转到小程序后: Native的任务: 下载小程序.zip文件 启动两个web运行容器: 渲染层webview加载slaves.html 逻辑层jscore加载master.html 解析小程序app.json,发送’AppReady’事件 逻辑层master.js: 监听’AppReady’事件,执行小程序的调起逻辑 1234567891011121314/** * 监听客户端的调起逻辑 */listenAppReady() { this.swaninterface.bind('AppReady', event => { console.log('master listener AppReady ', event); swanEvents('masterActiveStart'); // 给三方用的,并非给框架用,请保留 this.context.appConfig = event.appConfig; // 初始化master的入口逻辑 this.initRender(event); // this.preLoadSubPackage(); });} 初始化master的入口逻辑,小程序的每个界面,对应一个Slave对象(与渲染层的slave.js不一样),依据用户打开多个页面,构建一个导航栈,保存在navigator对象里 123456789101112131415161718192021222324252627282930/** * 初始化渲染 * * @param {Object} initEvent - 客户端传递的初始化事件对象 * @param {string} initEvent.appConfig - 客户端将app.json的内容(json字符串)给前端用于处理 * @param {string} initEvent.appPath - app在手机上的磁盘位置 * @param {string} initEvent.wvID - 第一个slave的id * @param {string} initEvent.pageUrl - 第一个slave的url */initRender(initEvent) { // 设置appConfig this.navigator.setAppConfig({ ...JSON.parse(initEvent.appConfig), ...{ appRootPath: initEvent.appPath } }); swanEvents('masterActiveInitRender'); // 压入initSlave this.navigator.pushInitSlave({ pageUrl: initEvent.pageUrl, slaveId: +initEvent.wvID, root: initEvent.root, preventAppLoad: initEvent.preventAppLoad }); this.appPath = initEvent.appPath; swanEvents('masterActivePushInitslave');} 创建初始化页面的slave后,如果没有预加载,就加载小程序里的app.js文件(注意:是编译后的app.js文件),并发送’slaveLoaded’事件,通知渲染层开始渲染 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960/** * 初始化第一个slave * @param {Object} [initParams] - 初始化的参数 */pushInitSlave(initParams) { .... // 创建初始化slave this.initSlave = this.createInitSlave(initParams.pageUrl, this.appConfig); // slave的init调用 this.initSlave .init(initParams) .then(initRes => { swanEvents('masterActiveCreateInitslaveEnd'); // 入栈 this.history.pushHistory(this.initSlave); swanEvents('masterActivePushInitslaveEnd'); // 调用slave的onEnqueue生命周期函数 this.initSlave.onEnqueue(); swanEvents('masterActiveOnqueueInitslave'); });}/** * 初始化为第一个页面 * * @param {Object} initParams 初始化的配置参数 * @return {Promise} 返回初始化之后的Promise流 */Slave.init(initParams) { this.isFirstPage = true; return Promise .resolve(initParams) .then(initParams => { swanEvents('masterActiveInitAction'); if (!!initParams.preventAppLoad) { return initParams; } // const loadCommonJs = this.appConfig.splitAppJs // && !this.appConfig.subPackages // ? 'common.js' : 'app.js'; const loadCommonJs = 'app.js'; return loader .loadjs(`${this.appRootPath}/${loadCommonJs}`, 'masterActiveAppJsLoaded') .then(() => { return this.loadJs.call(this, initParams); }); }) .then(initParams => { this.uri = initParams.pageUrl.split('?')[0]; this.accessUri = initParams.pageUrl; this.slaveId = initParams.slaveId; // init的事件为客户端处理,确保是在slave加载完成之后,所以可以直接派发 this.swaninterface.communicator.fireMessage({ type: `slaveLoaded${this.slaveId}`, message: {slaveId: this.slaveId} }); return initParams; });} 执行slave入栈后的生命周期函数this.initSlave.onEnqueue(); 在此函数里,会真正Page Instance,同时监听到渲染层准备好后,发送’initData’事件 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849/** * 入栈之后的生命周期方法 * * @return {Object} 入栈之后,创建的本slave的页面实例对象 */onEnqueue() { return this.createPageInstance();}/** * 创建页面实例,并且,当slave加载完成之后,向slave传递初始化data * * @return {Promise} 创建完成的事件流 */createPageInstance() { if (this.isCreated()) { return Promise.resolve(); } swanEvents('masterActiveCreatePageFlowStart', { uri: this.uri }); const userPageInstance = createPageInstance(this.accessUri, this.slaveId, this.appConfig); const query = userPageInstance.privateProperties.accessUri.split('?')[1]; this.setUserPageInstance(userPageInstance); try { swanEvents('masterPageOnLoadHookStart'); userPageInstance._onLoad(getParams(query)); swanEvents('masterPageOnLoadHookEnd'); } catch (e) { // avoid empty state } this.status = STATUS_MAP.CREATED; console.log(`Master 监听 slaveLoaded 事件,slaveId=${this.slaveId}`); return this.swaninterface.invoke('loadJs', { uri: this.uri, eventObj: { wvID: this.slaveId }, success: params => { swanEvents('masterActiveCreatePageFlowEnd'); swanEvents('masterActiveSendInitdataStart'); userPageInstance.privateMethod .sendInitData.call(userPageInstance, this.appConfig); swanEvents('masterActiveSendInitdataEnd'); } });} 渲染层slave.js 监听’PageReady’事件,加载对应页面的文件:app.css,index.css,index.swan.js文件 1234567891011121314151617181920212223242526272829303132333435363738394041/** * 监听pageReady,触发整个入口的调起 * @param {Object} [global] 全局对象 */listenPageReady(global) { swanEvents('slavePreloadListened'); // 控制是否开启预取initData的开关 let advancedInitDataSwitch = false; this.swaninterface.bind('PageReady', event => { swanEvents('slaveActiveStart', { pageInitRenderStart: Date.now() + '' }); ... const appPath = event.appPath; const pagePath = event.pagePath.split('?')[0]; const onReachBottomDistance = event.onReachBottomDistance; ... let loadUserRes = () => { // 设置页面的基础路径为当前页面本应所在的路径 // 行内样式等使用相对路径变成此值 // setPageBasePath(`${appPath}/${pagePath}`); swanEvents('slaveActivePageLoadStart'); // 加载用户的资源 Promise.all([ loader.loadcss(`${appPath}/app.css`, 'slaveActiveAppCssLoaded'), loader.loadcss(`${appPath}/${pagePath}.css`, 'slaveActivePageCssLoaded') ]) .catch(() => { console.warn('加载css资源出现问题,请检查css文件'); }) .then(() => { // todo: 兼容天幕,第一个等天幕同步后,干掉 swanEvents('slaveActiveCssLoaded'); swanEvents('slaveActiveSwanJsStart'); loader.loadjs(`${appPath}/${pagePath}.swan.js`, 'slaveActiveSwanJsLoaded'); }); }; // (event.devhook === 'true' ? loadHook().then(loadUserRes).catch(loadUserRes) : loadUserRes()); loadUserRes(); });} 在每个页面编译后的xxx.swan.js文件里,会执行pageRender()函数,进行界面渲染,如此demo里的index.swan.js文件: 1234567891011121314151617181920212223242526272829303132((global)=>{ global.errorMsg = []; var templateComponents = Object.assign({}, {}); var param = {}; var filterArr = JSON.parse(\"[]\"); try { filterArr && filterArr.forEach(function (item) { param[item.module] = eval(item.module) }); var pageContent = ` <div class=\\\"wrap\\\"> <div>{{text}}</div> <button class=\\\"btn\\\" type=\\\"primary\\\" v-on:click=\\\"eventHappen('tap', $event, 'add', '', 'bind')\\\"> add text </button> <button class=\\\"btn\\\" type=\\\"primary\\\" v-on:click=\\\"eventHappen('tap', $event, 'remove', '', 'bind')\\\"> remove text </button> </div>`; var renderPage = function (filters, modules) { ... global.pageRender(pageContent, templateComponents) }; renderPage(filterArr, param); } catch (e) { global.errorMsg['execError'] = e; throw e; }})(window); global.pageRender()函数是在slave.js文件里定义的方法,其内部的逻辑就是创建对应的san框架里的Page组件,等待初始化数据过来后,再绑定到界面上 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071/** * 注册所有components(也包括顶层components -- page) */registerComponents() { ... global.pageRender = (pageTemplate, templateComponents, customComponents, filters, modules) => { ... // 定义当前页面的组件 componentFactory.componentDefine( 'page', { template: `<swan-page tabindex=\"-1\">${pageTemplate}</swan-page>`, superComponent: 'super-page' }, { classProperties: { components: {...componentFactory.getComponents(), ...templateComponents, ...customComponents}, filters: { ...filtersObj } } } ); swanEvents('slaveActiveDefineComponentPage'); // 获取page的组件类 const Page = global.componentFactory.getComponents('page'); // 初始化页面对象 const page = new Page(); swanEvents('slaveActiveConstructUserPage'); // 调用页面对象的加载完成通知 page.slaveLoaded(); swanEvents('slaveActiveUserPageSlaveloaded'); // 用于记录用户模板代码在开始执行到监听initData事件之前的耗时 global.FeSlaveSwanJsInitEnd = Date.now(); // 监听等待initData,进行渲染 page.communicator.onMessage('initData', params => { swanEvents('slaveActiveReceiveInitData'); try { // 根据master传递的data,设定初始数据,并进行渲染 page.setInitData(params); swanEvents('slaveActiveRenderStart'); // 真正的页面渲染,发生在initData之后 // 此处让页面真正挂载处于自定义组件成功引用其他自定义组件之后, // 引用其它自定义组件是在同一时序promise.resolve().then里执行, 故此处attach时, 自定义组件已引用完成 setTimeout(() => { page.attach(document.body); // 通知master加载首屏之后的逻辑 page.communicator.sendMessage( 'master', { type: 'slaveAttached', slaveId: page.slaveId } ); swanEvents('slaveActivePageAttached'); }, 0); } catch (e) { console.log(e); global.errorMsg['renderError'] = e; } }, {listenPreviousEvent: true}); ... }; ...} 当界面渲染后,发送’slaveAttached’事件,逻辑层执行onShow()生命周期函数 交互 当用户点击界面上的button按钮时,会触发san.Page组件里的eventHappen()函数,发送’event’事件 12345678910111213141516171819202122232425262728293031323334/** * 执行用户绑定的事件 * * @param {string} eventName 事件名称 * @param {Object} $event 事件对象 * @param {Function} reflectMethod 用户回调方法 * @param {boolean} capture 是否事件捕获 * @param {boolean} catchType 是否终止事件执行 * @param {Object} customEventParams 用户绑定的事件集合 */eventHappen: function(eventName, $event, reflectMethod, capture, catchType, customEventParams) { swanEvents('slaveEventHappen', { eventName: eventName, }); if ($event && catchType === 'catch') { $event.stopPropagation && $event.stopPropagation(); (eventName === 'touchstart' || eventName === 'touchmove') && $event.preventDefault && $event.preventDefault(); } this.$communicator.sendMessage( 'master', { type: 'event', value: { eventType: eventName, reflectMethod, e: $event, //eventProccesser(eventName, $event) }, slaveId: this.slaveId, customEventParams } );} 逻辑层master.js监听’event’事件后,执行对应Page里的对应函数 123456789101112131415161718192021222324/** * 绑定开发者绑定的events */bindDeveloperEvents() { this.slaveCommunicator.onMessage('event', event => { swanEvents('masterListenEvent', event); const eventOccurredPageObject = this.history.seek(event.slaveId).getUserPageInstance(); // if (event.customEventParams) { // const nodeId = event.customEventParams.nodeId; // const reflectComponent = eventOccurredPageObject // .privateProperties.customComponents[nodeId]; // if (reflectComponent[event.value.reflectMethod]) { // reflectComponent[event.value.reflectMethod] // .call(reflectComponent, event.value.e); // } // } // else if (eventOccurredPageObject[event.value.reflectMethod]) { eventOccurredPageObject[event.value.reflectMethod] .call(eventOccurredPageObject, event.value.e); } });} 当逻辑层master,执行this.setData()函数更新界面时,会发送’setData’事件,渲染层slave.js会监听此事件,进行界面更新 1234567891011121314151617181920212223242526272829303132333435363738// 逻辑层Page对象/** * 页面中挂载的setData操作方法,操作后,会传到slave,对视图进行更改 * * @param {string|Object} [path] - setData的数据操作路径,或setData的对象{path: value} * @param {*} [value] - setData的操作值 * @param {Function} [cb] - setData的回调函数 */setData(path, value, cb) { this.sendDataOperation({ type: 'set', path, value, cb });},// 渲染层/** * 初始化事件绑定 * @private */initMessagebinding: function() { this.$communicator.onMessage( ['setData', 'pushData', 'popData', 'unshiftData', 'shiftData', 'removeAtData', 'spliceData'], params => { swanEvents('slaveDataEvent', params); const setObject = params.setObject || {}; const operationType = params.type.replace('Data', ''); if (operationType === 'set') { // TODO-ly 此处可以优化,使用Vue效率最高的方案 for(var key in setObject){ this[key] = setObject[key]; } } } );}, 参考 智能小程序 swanvue-core","categories":[{"name":"前端","slug":"前端","permalink":"https://handsomeliuyang.github.io/categories/前端/"}],"tags":[{"name":"前端","slug":"前端","permalink":"https://handsomeliuyang.github.io/tags/前端/"},{"name":"小程序","slug":"小程序","permalink":"https://handsomeliuyang.github.io/tags/小程序/"}]},{"title":"LeetCode Merge K Sorted Lists","slug":"LeetCodeMergeKSortedLists","date":"2019-01-23T01:34:38.000Z","updated":"2019-01-25T03:19:07.807Z","comments":true,"path":"2019/01/23/LeetCodeMergeKSortedLists/","link":"","permalink":"https://handsomeliuyang.github.io/2019/01/23/LeetCodeMergeKSortedLists/","excerpt":"","text":"题目Given K sorted linked lists of size N each, merge them and print the sorted output. Example:1234567Input: k = 3, n = 4list1 = 1->3->5->7list2 = 2->4->6->8list3 = 0->9->10->11Output: 0->1->2->3->4->5->6->7->8->9->10->11 理解此题目还有很多种变种,如不固定每个list的长度。不管如何变化,其算法都是一样,选择固定长度的题目,主要是方便时间复杂度的计算 解决Compare one by one步骤: 创建空链表 比较每个list的head元素,取出最小元素,并移动此list的head元素 把取出的最小元素链接到空链表里 代码如下所示:12345678910111213141516171819202122232425262728293031323334353637383940/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public int compare(ListNode[] lists){ int minIndex = -1; for(int i=0; i<lists.length; i++){ if(lists[i] == null) { continue; } if(minIndex == -1 || lists[i].val < lists[minIndex].val){ minIndex = i; } } return minIndex; } public ListNode mergeKLists(ListNode[] lists) { ListNode output = new ListNode(0); ListNode outputEnd = output; while(true){ // 比较head大小 int index = compare(lists); if(index == -1) { return output.next; } outputEnd.next = lists[index]; outputEnd = lists[index]; lists[index] = lists[index].next; } }} 复杂度: 时间复杂度: 第一层while循环的次数为: K*N 次 compare()里的for循环次数 K 次 时间复杂度为:O(K*N*K) 空间复杂度:O(1) 最小堆通过最小堆优化上面的compare()函数,在写代码之前,我们需要了解最小堆的一些特性: 堆可以用数组来表示 下标从0开始编号,位置i的元素有如下特性: 其parent(i) = (i-1)/2; left_child(i) = 2*i + 1; right_child(i) = 2*i + 2; 修改顶点元素后,恢复其堆的特性的时间复杂度为:O(logK) 代码如下所示:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889/** * Definition for singly-linked list. * public class ListNode { * int val; * ListNode next; * ListNode(int x) { val = x; } * } */class Solution { public int getLeft(int i) { return 2 * i + 1; } public int getRight(int i){ return 2 * i + 2; } public void minHeapify(ListNode[] minHeap, int i){ int size = minHeap.length; while(true){ int min = i; int left = getLeft(i); int right = getRight(i); if(left < size && minHeap[left] != null && (minHeap[min] == null || minHeap[min].val > minHeap[left].val)) { min = left; } if(right < size && minHeap[right] != null && (minHeap[min] == null || minHeap[min].val > minHeap[right].val)){ min = right; } if(min == i) { break; } ListNode temp = minHeap[i]; minHeap[i] = minHeap[min]; minHeap[min] = temp; i = min; } } public ListNode[] initMinHeap(ListNode[] lists){ ListNode[] minHeap = lists; // 遍历所有非叶子结点,构建最小堆 int size = minHeap.length; for(int i=(size-1)/2; i>=0; i--){ minHeapify(lists, i); } return minHeap; } public ListNode getMin(ListNode[] minHeap){ return minHeap[0]; } public void replaceMin(ListNode[] minHeap, ListNode node){ minHeap[0] = node; minHeapify(minHeap, 0); } public ListNode mergeKLists(ListNode[] lists) { if(lists.length == 0){ return null; } ListNode output = new ListNode(0); ListNode outputEnd = output; // 初始化最小堆 ListNode[] minHeap = initMinHeap(lists); while(true){ // 比较head大小 ListNode minNode = getMin(minHeap); if(minNode == null) { return output.next; } outputEnd.next = minNode; outputEnd = minNode; replaceMin(minHeap, minNode.next); } }} 复杂度: 时间复杂度:O(K*N*logK) 空间复杂度:O(K) Divide And Conquer(分治法)此算法的思路: 两两分组合并,形成一个新的数组 再重复步骤1,直到只剩一个元素 如下所示: 代码省略… 关键来思考其时间复杂度的计算方法: Merging的次数为:logK listi, listj合并的时间复杂度为0(n) 总时间复杂度为=(K/2)*O(N)+(K/2^2)*O(N)+…+(K/2^logK)*O(N) 假定(K/2^i)*O(N) 约等于 O(K*N) 总时间复杂度约等于O(K*N*logK)","categories":[{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/categories/算法/"}],"tags":[{"name":"leetcode","slug":"leetcode","permalink":"https://handsomeliuyang.github.io/tags/leetcode/"},{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/tags/算法/"}]},{"title":"LeetCode Longest Common Prefix","slug":"LeetCodeLongestCommonPrefix","date":"2019-01-11T01:56:38.000Z","updated":"2019-01-14T09:08:13.908Z","comments":true,"path":"2019/01/11/LeetCodeLongestCommonPrefix/","link":"","permalink":"https://handsomeliuyang.github.io/2019/01/11/LeetCodeLongestCommonPrefix/","excerpt":"","text":"题目Write a function to find the longest common prefix string amongst an array of strings. If there is no common prefix, return an empty string “”. Example 1:12Input: ["flower","flow","flight"]Output: "fl" Example 2:123Input: ["dog","racecar","car"]Output: ""Explanation: There is no common prefix among the input strings. 理解此题目非常好理解,就是求最长前缀 解决Vertical scanning(垂直扫描)最容易想到的解决方案,就是垂直扫描,注意代码的写法: 不需要额外的空间来保存Prefix字符串 注意全匹配情况 代码如下所示:12345678910111213141516171819202122232425262728class Solution { public String longestCommonPrefix(String[] strs) { if(strs == null || strs.length == 0){ return \"\"; } int len = strs[0].length(); int index = 0; while(index < len){ char curChar = strs[0].charAt(index); for(int i=1; i<strs.length; i++){ if(index >= strs[i].length() || curChar != strs[i].charAt(index)){ return strs[0].substring(0, index); // 不用单独出空间保持prefix字符串 } } index++; } return strs[0]; // 全匹配情况 }} 复杂度: 时间复杂度:O(m*n), 其中m表示字符串的长度,n表示字符串的数量 空间复杂度:O(1) Binary search(二分搜索)可以通过二分搜索法来查看index下标,代码如下:12345678910111213141516171819202122232425262728293031323334353637class Solution { public String longestCommonPrefix(String[] strs) { if(strs == null || strs.length == 0){ return \"\"; } int len = strs[0].length(); int iMin = 1; int iMax = len; int index = -1; while(iMin <= iMax){ index = (iMin+iMax) / 2; if(isCommonPrefix(strs, index)){ iMin = index + 1; } else { iMax = index - 1; } } return strs[0].substring(0, (iMin+iMax)/2); } public boolean isCommonPrefix(String[] strs, int len){ String str1 = strs[0].substring(0, len); for(int i=1; i<strs.length; i++){ if(!strs[i].startsWith(str1)) { return false; } } return true; }} 注意二分搜索法的写法: iMin, iMax, index都从1开始,不要从0开始 最后确定的位置为(iMin + iMax)/2,而不是直接使用index值 复杂度: 时间复杂度:由于isCommonPrefix()函数的时间复杂度为O(mn),二分搜索法的时间复杂度为O(logm), 时间复杂度为O(mn*logm) 空间复杂度:O(1) Trie(Prefix tree, 前缀树,字典树) Trie这个术语来自于retrieval。根据词源学,trie的发明者Edward Fredkin把它读作英语发音:/ˈtriː/ “tree”。但是,其他作者把它读作英语发音:/ˈtraɪ/ “try”。 由字符串组:{“a”, “to”, “tea”, “ted”, “ten”, “i”, “in”, “inn”},构建的Trie的结构为: 从上图可知,trie树的特点: 根节点不包含字符,除根节点外的每一个子节点都包含一个字符 从根节点到某一个节点,路径上经过的字符连接起来,为该节点对应的字符串 每个节点的所有子节点包含的字符互不相同 Trie利用空间换时间,适合解决下述问题: 频繁求解字符串组的前缀 有相似前缀的字符串组,如搜索: 对于此问题,如果提前利用字符串组构建Trie,可以提升频繁求前缀的性能,如下case: [“flower”,”flow”,”flight”]构建的Trie,其时间复杂度为O(m*n) 再通过Trie查找公共前缀,时间复杂度为O(m) 代码如下所示:— 注意:此代码是从leetcode直接拷贝的12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061public String longestCommonPrefix(String q, String[] strs) { if (strs == null || strs.length == 0) return \"\"; if (strs.length == 1) return strs[0]; Trie trie = new Trie(); for (int i = 1; i < strs.length ; i++) { trie.insert(strs[i]); } return trie.searchLongestPrefix(q);}class TrieNode { // R links to node children private TrieNode[] links; private final int R = 26; private boolean isEnd; // number of children non null links private int size; public void put(char ch, TrieNode node) { links[ch -'a'] = node; size++; } public int getLinks() { return size; } //assume methods containsKey, isEnd, get, put are implemented as it is described //in https://leetcode.com/articles/implement-trie-prefix-tree/)}public class Trie { private TrieNode root; public Trie() { root = new TrieNode(); }//assume methods insert, search, searchPrefix are implemented as it is described//in https://leetcode.com/articles/implement-trie-prefix-tree/) private String searchLongestPrefix(String word) { TrieNode node = root; StringBuilder prefix = new StringBuilder(); for (int i = 0; i < word.length(); i++) { char curLetter = word.charAt(i); if (node.containsKey(curLetter) && (node.getLinks() == 1) && (!node.isEnd())) { prefix.append(curLetter); node = node.get(curLetter); } else return prefix.toString(); } return prefix.toString(); }}","categories":[{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/categories/算法/"}],"tags":[{"name":"leetcode","slug":"leetcode","permalink":"https://handsomeliuyang.github.io/tags/leetcode/"},{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/tags/算法/"}]},{"title":"LeetCode Regular Expression Matching","slug":"LeetCode-Regular-Expression-Matching","date":"2018-12-29T01:50:48.000Z","updated":"2019-01-06T04:14:50.814Z","comments":true,"path":"2018/12/29/LeetCode-Regular-Expression-Matching/","link":"","permalink":"https://handsomeliuyang.github.io/2018/12/29/LeetCode-Regular-Expression-Matching/","excerpt":"","text":"题目Given an input string (s) and a pattern (p), implement regular expression matching with support for ‘.’ and ‘*’.12'.' Matches any single character.'*' Matches zero or more of the preceding element. The matching should cover the entire input string (not partial). Note: s could be empty and contains only lowercase letters a-z. p could be empty and contains only lowercase letters a-z, and characters like . or *. Example 1:12345Input:s = "aa"p = "a"Output: falseExplanation: "a" does not match the entire string "aa". Example 2:12345Input:s = "aa"p = "a*"Output: trueExplanation: '*' means zero or more of the precedeng element, 'a'. Therefore, by repeating 'a' once, it becomes "aa". Example 3:12345Input:s = "ab"p = ".*"Output: trueExplanation: ".*" means "zero or more (*) of any character (.)". Example 4:12345Input:s = "aab"p = "c*a*b"Output: trueExplanation: c can be repeated 0 times, a can be repeated 1 time. Therefore it matches "aab". Example 5:1234Input:s = "mississippi"p = "mis*is*p*."Output: false 理解正则表达式大家都使用过,’.’和’*’都是最长使用的,主要是注意如下几种特殊情况:1234567891011121314151617181920212223// 特殊情况1:Input:s = "aaa"p = "a*a"Output: true// 特殊情况2:Input:s = "aa"p = "a*"Output: true// 特殊情况3:Input:s = ""p = "c*c*"Output: true// 特殊情况4:Input:s = ""p = ".*"Output: true 解决递归算法可能性太多,最简单的方案是通过递归来解决,匹配都是从左到右逐一匹配,递归函数的关键是两点: 临界条件 递归函数 代码如下:12345678910111213141516171819202122232425262728293031323334353637class Solution { public boolean isMatch(String s, String p) { if(s == null || p == null) { return false; } // 递归的临界条件,此临界条件把s为空串也考虑进去了 if(p.length() == 0){ return s.length() == 0; } /** // 最早写的临界条件,此临界判断,没有考虑两种临界条件: // 1. s = \"\" p = \"c*c*\" 2. s = \"aaa\" p = \"a*a\" if(s.length() == 0) { return p.length() == 0 || (p.length() == 2 && p.charAt(1) == '*'); } else if(p.length() == 0){ return false; } **/ // 递归函数 // 第一字符判断,考虑到了s为空串的情况 boolean firstMatch = !(s.length() == 0) && (p.charAt(0) == '.' || s.charAt(0) == p.charAt(0)); if(p.length() >= 2 && p.charAt(1) == '*') { /** 之前我的一种写法: return firstMatch ? isMatch(s.substring(1),p) : isMatch(s, p.substring(2)) 这种写法没有考虑到特殊情况:s = \"aaa\" p = \"a*a\",按我们的临界条件判断,两种情况都应该去尝试,所以都进行尝试 **/ return (firstMatch && isMatch(s.substring(1), p)) || isMatch(s, p.substring(2)); } return firstMatch && isMatch(s.substring(1), p.substring(1)); }} 复杂度: 使用代入法,我自己理解的时间复杂度为O(2^s.length())或O(2^p.length()),官方的时间复杂度与空间复杂度没有理解: 动态规划动态规划是一种“大转小”解决问题的思想,上面的函数函数,可以理解为状态转移方程,求解一般是把其中小的问题的答案保存起来,减少重复计算。 条件: 字符串S, 模式串P S[i] 表示字符串:S0,S1,…,Si P[j] 表示字符串:P0,P1,…,Pj boolean[][] dp; dp[i][j]表示match(S[i], P[j])的匹配结果 由上面的递归函数得知dp[i][j]的结果可能如下: dp[i][j] = (firstMath && dp[i+1][j]) || dp[i][j+2]; // 当P.charAt(j+1) == ‘*’ dp[i][j] = firstMatch && dp[i+1][j+1]; 从上可知,问题是由小问题转大问题,那就逆序遍历S与P,代码如下:123456789101112131415161718192021class Solution { public boolean isMatch(String S, String P) { boolean[][] dp = new boolean[S.length()+1][P.length()+1]; dp[S.length()][P.length()] = true; for(int i=S.length()-1; i>=0; i--){ for(int j=P.length()-1; j>=0; j--){ boolean firstMatch = i<S.length() && (P.charAt(j) == '.' || S.charAt(i) == P.charAt(j)); if(j < P.length()-1 && P.charAt(j+1) == '*'){ dp[i][j] = (firstMatch && dp[i+1][j]) || dp[i][j+2]; } else { dp[i][j] = firstMatch && dp[i+1][j+1]; } } } return dp[0][0]; }} 复杂度: 时间复杂度:O(S.length() * P.length()) 空间复杂度:O(S.length() * P.length())","categories":[{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/categories/算法/"}],"tags":[{"name":"leetcode","slug":"leetcode","permalink":"https://handsomeliuyang.github.io/tags/leetcode/"},{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/tags/算法/"}]},{"title":"LeetCode Longest Common Substring","slug":"Leetcode-longest-common-substring","date":"2018-12-15T03:00:06.000Z","updated":"2018-12-15T06:27:34.552Z","comments":true,"path":"2018/12/15/Leetcode-longest-common-substring/","link":"","permalink":"https://handsomeliuyang.github.io/2018/12/15/Leetcode-longest-common-substring/","excerpt":"","text":"题目Given two strings ‘X’ and ‘Y’, find the length of the longest common substring. Examples :1234567891011121314Input : X = "GeeksforGeeks", y = "GeeksQuiz"Output : 5The longest common substring is "Geeks" and is oflength 5.Input : X = "abcdxyz", y = "xyzabcd"Output : 4The longest common substring is "abcd" and is oflength 4.Input : X = "zxabcdezy", y = "yzabcdezx"Output : 6The longest common substring is "abcdez" and is oflength 6. 理解求字符串X,Y的公共最长子串,这些子串要求连续性,类似的问题是求公共最长子序列 解决设 m = X.length(), n = Y.length(); Brute Force(暴力法)思路: 遍历所有X的子串,时间复杂度为O(m^2) 12345for(int i=0; i<X.length(); i++){ for(int j=0; j<X.length(); j++){ String sub = X.substring(i, j+1); }} 求子串是否也是Y的子串,即子串在Y中的位置,可选择KMP算法,时间复杂为O(n) 满足条件的最大长度的子串就是最长公共子串。最终时间复杂度为:O(n * m^2) Dynamic Programming(动态规划)思路: 设 X = “abcdxyz”, Y = “xyzabcd” 先求解如下子符串组的公共后缀: 123451. X_6="abcdxyz", Y_6="xyzabcd" ===> 公共后缀长度:02. ...3. X_3="abcd", Y_6="xyzabcd" ===> 公共后缀长度:44. ...5. X_0="a", Y_0="x" ===> 公共后缀长度:0 上面所有子符串组的最长公共后缀,就是最长的公共子串:”abcd“ 求X,Y的公共最长子串问题,转换为求所有子串的最长公共后缀的问题,公共后缀问题,有如下状态转移方程:1234567The longest common suffix has following optimal substructure property LCSuff(X, Y, m-1, n-1) = LCSuff(X, Y, m-2, n-2) + 1 if X[m-1] = Y[n-1] 0 Otherwise (if X[m-1] != Y[n-1])The maximum length Longest Common Suffix is the longest common substring. LCSubStr(X, Y, m-1, n-1) = Max(LCSuff(X, Y, i, j)) where 1 <= i < m and 1 <= j < n 代码实现:12345678910111213141516171819202122232425262728293031323334public class LongestCommonSubSequence { public int LCSubStr(String X, String Y) { if(X == null || X.length() == 0){ return 0; } if(Y == null || Y.length() == 0){ return 0; } int m = X.length(); int n = Y.length(); int LCStuff[][] = new int[m][n]; int maxLCSLen = 0; // 最长公共后缀长度 for (int i = 0; i < m; i++) { for (int j = 0; j < n; j++) { if (X[i] == Y[j]) { if(i==0 || j==0){ LCStuff[i][j] = 1; } else { LCStuff[i][j] = LCStuff[i - 1][j - 1] + 1; } maxLCSLen = Math.max(maxLCSLen, LCStuff[i][j]); } else { LCStuff[i][j] = 0; } } } return maxLCSLen; } } 复杂度: 时间复杂度:O(m * n) 空间复杂度:O(m * n) 参考 Longest Common Substring | DP-29","categories":[{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/categories/算法/"}],"tags":[{"name":"leetcode","slug":"leetcode","permalink":"https://handsomeliuyang.github.io/tags/leetcode/"},{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/tags/算法/"}]},{"title":"LeetCode Longest Palindromic Substring","slug":"leetcode-longest-palindromic-substring","date":"2018-12-06T02:12:25.000Z","updated":"2018-12-15T06:30:18.318Z","comments":true,"path":"2018/12/06/leetcode-longest-palindromic-substring/","link":"","permalink":"https://handsomeliuyang.github.io/2018/12/06/leetcode-longest-palindromic-substring/","excerpt":"","text":"题目Given a string s, find the longest palindromic substring in s. You may assume that the maximum length of s is 1000. Example 1:123Input: "babad"Output: "bab"Note: "aba" is also a valid answer. Example 2:12Input: "cbbd"Output: "bb" 理解求字符串s中,最大的回文子串,具有对称性的字符串称为回文串。 解决Brute Force(暴力破解)这个思路非常简单,找到字符串s中,所有可能的子串,一一判断其是否是回文串,同时记录最大的回文子串,如下所示:1234567891011121314151617181920212223242526272829303132333435363738class Solution { public boolean isPalindromicString(String s, int i, int j){ while(i<j) { if(s.charAt(i) != s.charAt(j)) { return false; } i++; j--; } return true; } public String longestPalindrome(String s) { int longestStart = 0; int longestEnd = 0; int longestLen = 0; int len = s.length(); if(len == 0) { return \"\"; } for(int i=0; i<len; i++){ for(int j=i+longestLen; j<len; j++){ if(isPalindromicString(s, i, j)){ if((j-i+1) > longestLen) { longestLen = j-i+1; longestStart = i; longestEnd = j; } } } } return s.substring(longestStart, longestEnd+1); }} 复杂度 时间复杂度:O(n^3) 空间复杂度:O(1) Dynamic Programming(动态规划)利用回文串的对称性的特点,可以得出如下结论:12345字符串S,P(i,j)表示子串Si...Sj是否是回文串,其可能值为:1. P(i,j) = true; // 表示子串Si...Sj是回文串2. P(i,j) = false; // 表示子串Si...Sj不是回文串结论:P(i,j) = P(i+1, j-1) && Si == Sj 上面的结论就是一个动态规则里的状态转移方程,可以使用动态规划的思路来减少重复比较,步骤: 先计算长度为1和2的回文串,并把结果保存下来 利用上面的结果,计算长度为3的回文串,保存结果,依次类推 转换为代码的关键是: 保存每个子串的回文串结果,长度为n的字符串S,其子串有n^2,使用二维数组来存储 遍历子串时,要按长度从小到大来遍历 123456789101112131415161718192021222324252627282930313233343536373839404142class Solution { public String longestPalindrome(String s) { if(s == null || s.length() <= 1) { return s; } int n = s.length(); boolean p[][] = new boolean[n][n]; // 初始化结果,设置长度值小于0,1的子串其回文结果为true for(int i=0; i<n; i++){ for(int j=0; j<n; j++) { if(j > i) { p[i][j] = false; } else { p[i][j] = true; } } } int maxLen = 1; // 不会遍历长度为1 int maxLeft = 0; int maxRight = 0; for(int subLen = 2; subLen<=n; subLen++){// 子串的长度,长度为1都已经计算过了,从2开始 for(int i=0; (i+subLen-1)<n; i++){ // 子串的起点index int j = i+subLen-1; // 依据状态转移方程,更新二维数据p p[i][j] = p[i+1][j-1] && s.charAt(i) == s.charAt(j); if(p[i][j]) { if(subLen >= maxLen) { maxLen = subLen; maxLeft = i; maxRight = j; } } } } return s.substring(maxLeft, maxRight+1); }} 复杂度: 时间复杂度:O(n^2); 空间复杂度:O(n^2); Expand Around Center(优化后的动态规划)理论上,我们可以实现时间复杂度为O(n^2),空间复杂度为O(1)的动态规划算法。 利用回文串的特点,对字符串S的子串进行归类:—- 回文串有可能是偶数,也有可能是奇数 [Si], [S(i-1),Si,Si+1], … , [S(i-n), … , Si, S(i+n)] [Si, S(i+1)], [S(i-1), Si, Si+1, S(i+2)], …, [S(i-n),…, Si, Si+1, …, S(i+n)] 这样进行归类后,如果先把一类的子串放在一起计算,就只需要两个变量来记录上一个子串的结果,不用记录所有的子串结果,代码实现如下所示:123456789101112131415161718192021222324252627282930313233343536373839404142class Solution { public int expandAroundCenter(String s, int l, int r){ int expandL = l; int expandR = r; while(expandL>=0 && expandR<s.length() && s.charAt(expandL) == s.charAt(expandR)){ expandL--; expandR++; } // 注意,此时expandR, expandL是不满足回文串的index,所以是-1 return expandR - expandL - 1; } public String longestPalindrome(String s) { if(s==null || s.length()<=1){ return s; } int n = s.length(); int maxLen = 0; int maxStart = 0; int maxEnd = 0; for(int i=0; i<n; i++){ int len = expandAroundCenter(s, i, i); int len2 = expandAroundCenter(s, i, i+1); if(len >= maxLen) { maxLen = len; maxStart = i - len/2; maxEnd = i + len/2; } if(len2 >= maxLen) { maxLen = len2; maxStart = i - len2/2 + 1; maxEnd = i + len2/2; } } return s.substring(maxStart, maxEnd+1); }} 复杂度: 时间复杂度:O(n^2) 空间复杂度:O(1) Manacher’s Algorithm(最优算法)时间复杂度为O(n)的算法,其步骤如下: 字符串转换,S to T,如S = “abaaba”, T = “#a#b#a#a#b#a#” S的回文子串有可能为偶数,也有可能为奇数;T的回文子串一定是奇数 abccba —> #a#b#c#c#b#a# abcdcba —> #a#b#c#d#c#b#a# T的最长回文串去掉字符串‘#’后,就是S的最长回文子串 int[] P = new int[T.length]; P[i]表示以Ti为中心回文串的长度(不包含Ti的长度) 12T = # a # b # a # a # b # a #P = 0 1 0 3 0 1 6 1 0 3 0 1 0 如上所示,只要计算出P的值,最长回文子串,就是数组里的最大值 利用回文串的对称性,推导快速计算P的数学公式: 假设字符串S=“babcbabcbaccba”,转换后的T,及已经计算了部分结果的数组P,如下所示: L,C,R分别表示回文串“#a#b#c#b#a#b#c#b#a#”的最左边临界点,中间值,最右边临界点 i’是以C为中心i的对称点(mirror) 计算P[i]的值,由于回文串的对称性,可快速得出:P[i]=P[i’]=1 上面的公式,无法适合计算P[C+1]…P[R]之间的所有值,如下图所示: P[15] != P[7],因为P[7] > R - i,即7 > 20-15 虽然P[15] != P[7], 由于P[7]>R-i,所以P[15]>=R-i 最终的计算公式: 123if P[i'] <= R - ithen P[i] = P[i']else P[i] >= R - i (通过以Ti为中心,两边扩大比较获取P[i]的值) 为了最大程度使用上面的公式,有更大的右边界时,要更新右边界 具体的代码如下:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566class Solution { public String preProcess(String s){ StringBuffer sb = new StringBuffer(\"^\"); for(int i=0; i<s.length(); i++){ sb.append(\"#\").append(s.charAt(i)); } sb.append(\"#$\"); return sb.toString(); } public String longestPalindrome(String s) { if(s==null || s.length()<=1){ return s; } // 预处理 String T = preProcess(s); int n = T.length(); int[] P = new int[n]; int R = 0; int C = 0; for(int i=1; i<n-1; i++){ // 逐一求P的值 int mirror_i = 2*C-i; // mirror_i = c-(i-c) = 2c-i // 注意:R>i一定要判断 P[i] = R > i ? Math.min(P[mirror_i], R-i) : 0; while(T.charAt(i+P[i]+1) == T.charAt(i-P[i]-1)){ P[i]++; } // 更新R值 if((i+P[i]) > R){ C = i; R = i+P[i]; } } int maxLen = 0; int centerIndex = 0; for(int i=1; i<n-1; i++){ if(P[i] >= maxLen) { maxLen = P[i]; centerIndex = i; } } // StringBuffer result = new StringBuffer(); // if(T.charAt(max_index) != '#') { // result.append(T.charAt(max_index)); // } // int i=1; // do{ // if(T.charAt(max_index+i) != '#') { // result.insert(0, T.charAt(max_index-i)); // result.append(T.charAt(max_index+i)); // } // i++; // }while(i<=max); // return result.toString(); return s.substring((centerIndex-maxLen)/2, (centerIndex-maxLen)/2 + maxLen); }} 复杂度: 时间复杂度:看上去有两个循环,for和while,但注意while循环的遍历次数之和为n,所以时间复杂度为O(2n)=O(n) 空间复杂度:O(n) Longest Common Substring(最长公共子串)利用回文串的特点,求字符串S与逆反字符串S‘的最长公共子串,就是其最长回文子串,如下所示:121. S="caba", S'="abac"2. 最长公共子串 C = “aba”,其最长回文子串 P = “aba” 有一个特例除外,如下所示:121. S = "abacdfgdcaba", S' = "abacdgfdcaba".2. 最长公共子串 C = “abacd”,但其最长回文子串 P = “aba” 要排除上面的特殊,排除方法:把逆反S’中C再逆反后的下标与S中的C的下标进行比较,不相等则排除掉 求S与S‘中的最长公共子串C,可以采用动态规划,具体请查看:Leetcode-Long-Common-String 代码如下所示:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748class Solution { public String longestPalindrome(String S) { if(S==null || S.length()<=1){ return S; } int n = S.length(); StringBuffer SReverseBuffer = new StringBuffer(); for(int i=n-1; i>=0; i--){ SReverseBuffer.append(S.charAt(i)); } String SReverse = SReverseBuffer.toString(); int[][] LCStuff = new int[n][n]; int LCSLen = 0; int endIndex = 0; for(int i=0; i<n; i++){ for(int j=0; j<n; j++){ if(S.charAt(i) == SReverse.charAt(j)) { if(i==0 || j==0) { LCStuff[i][j] = 1; } else { LCStuff[i][j] = LCStuff[i-1][j-1] + 1; } if(LCSLen < LCStuff[i][j]){ // 判断是否是真正的回文 if((i-LCStuff[i][j]+1) == (n-(j+1))) { LCSLen = LCStuff[i][j]; endIndex = i; } } } else { LCStuff[i][j] = 0; } } } return S.substring(endIndex-LCSLen+1, endIndex+1); }} 复杂度: 时间复杂度:O(n^2) 空间复杂度:O(n^2) 参考 solution Longest Palindromic Substring Part II","categories":[{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/categories/算法/"}],"tags":[{"name":"leetcode","slug":"leetcode","permalink":"https://handsomeliuyang.github.io/tags/leetcode/"},{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/tags/算法/"}]},{"title":"LeetCode之Median of Two Sorted Arrays","slug":"LeetCode之Median of Two Sorted Arrays","date":"2018-12-03T15:22:56.000Z","updated":"2018-12-04T02:20:40.733Z","comments":true,"path":"2018/12/03/LeetCode之Median of Two Sorted Arrays/","link":"","permalink":"https://handsomeliuyang.github.io/2018/12/03/LeetCode之Median of Two Sorted Arrays/","excerpt":"","text":"题目There are two sorted arrays nums1 and nums2 of size m and n respectively. Find the median of the two sorted arrays. The overall run time complexity should be O(log (m+n)). You may assume nums1 and nums2 cannot be both empty. Example 1:1234nums1 = [1, 3]nums2 = [2]The median is 2.0 Example 2:1234nums1 = [1, 2]nums2 = [3, 4]The median is (2 + 3)/2 = 2.5 理解求两个有序数组合并后,其中位数,如下所例子:123456789nums1 = [1, 3]nums2 = [2]合并后:[1,2,3]中位数:2nums1 = [1, 2]nums2 = [3, 4]合并后:[1,2,3,4]中位数:(2+3)/2.0 = 2.5 解决过程有序数组A,B,其长度为m,n,把其分为两部分:123 left_part right_partA[0],A[1],...A[i-1] | A[i],A[i+1],...A[m-1]B[0],B[1],...B[j-1] | B[j],B[j+1],...B[n-1] 如果满足下面三个条件:1231. 当n+m为偶数时,Length(left_part) == Length(right_part)2. 当n+m为奇数时,Length(left_part) == Length(right_part) + 13. B[j-1] <= A[i] && A[i-1] <= B[j] 相应的中位数结果:121. 当n+m为偶数时,median = (max(left_part) + min(right_part))/22. 当n+m为奇数时,由于Length(left_part) > Length(right_part),所以median = max(left_part) 把i,j代入推导:12341. if (n+m)%2 == 0, then i+j = m-i + n-j2. if (n+m)%2 == 1, then i+j = m-i + n-j + 1 // 假设right_part比left_part多一个元素3. 0 <=i<= m, j=(n+m)/2-i 或 j=(n+m+1)/2-i4. 当n+m为偶数时,由(n+m)/2 == (n+m+1)/2,得出:0<=i<=m, j=(n+m+1)/2-i 因此求中位数,即求在满足如下条件下的i值:1231. 0 <= i <= m2. j = (n+m+1)/2 - i3. A[i-1] <= B[j] && B[j-1] <= A[i] 因为0<= j <=n进行推导:1231. (n+m+1)/2 - i >= 0, 代入i的最大值m2. (n-m+1)/2 >= 03. n >= m 如果最快的从有序数组A里,找到满足条件的i,最快速的办法:二分搜索法。12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849class Solution { public double findMedianSortedArrays(int[] A, int[] B){ int m = A.length; int n = B.length; // 保证 n>=m if(m>n){ int[] temp=A; A=B; B=temp; int tmp=m; m=n; n=tmp; } // 二分搜索法 int iMin = 0; int iMax = m; while(iMin <= iMax){ // 二分搜索法的关键,不能是< int i = (iMin+iMax)/2; int j = (m+n+1)/2 - i; if(j>0 && i<m && B[j-1] > A[i]){ iMin = i+1; // 二分法的关键,需要+1,不然会进入死循环 } else if(i>0 && j<n && A[i-1]>B[j]){ iMax = i-1; // 二分法的关键,需要-1,不然会进入死循环 } else { // 找到此i值了 int maxLeft=0; if(i==0) { maxLeft = B[j-1]; } else if(j==0) { maxLeft = A[i-1]; } else { maxLeft = Math.max(A[i-1], B[j-1]); } if((m+n)%2==1) return maxLeft; int minRight = 0; if(i==m){ minRight = B[j]; } else if(j==n) { minRight = A[i]; } else { minRight = Math.min(A[i], B[j]); } return (maxLeft + minRight)/2.0; } } return 0.0; }} 边界判断优化:i j>0 and i>0 => j<n,推导过程: 12m<=n,i<m ==> j=(n+m+1)/2-i > (n+m+1)/2-m >= (2m+1)/2-m >= 1/2 >= 0m<=n,i>0 ==> j=(n+m+1)/2-i < (n+m+1)/2 <= 2n+1/2 <= n 复杂度分析 时间复杂度:O(log(min(m,n))),推导过程如下: 二分搜索遍历过程:m, m/2, m/(2^2), … m/(2^k) m/(2^k) = 1 ==> 2^k = m ==> k=log(m),k即是while循环的次数 m <= n ==> log(min(m,n) ==> O(log(min(m,n))) 注意:在算法里,logX的底数默认为2,而不是数学里的10 空间复杂度:O(1)","categories":[{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/categories/算法/"}],"tags":[{"name":"leetcode","slug":"leetcode","permalink":"https://handsomeliuyang.github.io/tags/leetcode/"},{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/tags/算法/"}]},{"title":"Flutter实现Git权限分配工具之旅","slug":"Flutter实现Git权限分配工具之旅","date":"2018-10-30T06:40:35.000Z","updated":"2018-11-11T10:21:35.409Z","comments":true,"path":"2018/10/30/Flutter实现Git权限分配工具之旅/","link":"","permalink":"https://handsomeliuyang.github.io/2018/10/30/Flutter实现Git权限分配工具之旅/","excerpt":"","text":"Flutter初见 Flutter is a mobile app SDK for building high-performance, high-fidelity, apps for iOS and Android, from a single codebase. Flutter 是一款移动应用程序 SDK,致力于使用一套代码来构建高性能、高保真的 iOS 和 Android 应用程序。 Flutter的优势: 开发效率高: 一套代码开发 iOS 和 Android 热加载(hot reload) 创建美观,高度定制的用户体验 Material Design 和 Cupertino (iOS 风格)Widget 实现定制,美观,品牌驱动的设计,而不受 OEM Widget 集的限制 框架结构(Architecture) Skia:开源的2d图形库。其已作为Chrome, Chrome OS, Android, Firefox, Firefox OS等其他众多产品的图形引擎,支持平台还包括Windows,macOS,iOS8+,Ubuntu14.04+等。 Dart: debug:JIT(Just In Time)编译,运行时分析、编译,运行较慢。Hot Reload基于JIT运行 release:AOT(Ahead Of Time)编译,生成了原生的arm代码,开发期间较慢,但运行期间快 一切都是对象,甚至数字,函数,和null都是对象 默认publick,通过加(_)标记为私有 单线程,没有锁的概念 Text:文本渲染 Dart的线程模型 Flutter与Android一样,通过Main线程和消息循环实现UI绘制操作与UI事件,如下所示: Dart的不同点: Dart是单线程执行,通过Future来实现异步编程,只是把任务暂时放在消息队列里,本质还是单线程执行,与javascript类型 两个消息队列:event队列和microtask队列 单线程带来的问题:单某个任务执行时间过长,超过16ms时,会导致丢帧,给用户的感觉就是卡顿,React借助requestIdleCallback Api实现了卡顿优化,详情请参考 Dart解决这个问题的方案:通过Isolate真正意义上创建线程,但此线程与java里的线程不一样:isolates之间不会共享内存,更像进程,通过传递message来进行交流,Demo 一切都是Widget Widgets是Flutter应用程序用户界面的基础构建模块,Widgets包含了views,view controllers,layouts等等能力。 Flutter提供了很多基组Widgets,但这些Widgets有一个与Android最大的不同点:每个Widget的能力很单一,如Text Widget,没有width, height, padding,color等等属性,需要借助其他Widget。 更多细节请查看:Flutter快速上车之Widget StatelessWidget & StatefulWidget Flutter的widget分为无状态和有状态,如下所示: 如何选择?下面是我的一些经验: 包含TextField的widget — StatefulWidget 用户交互时,产出的数据,如点击计数 局部数据 — StatefulWidget 全局数据(store存储) — StatelessWidget 默认为StatelessWidget Widget,Element,RenderObject Flutter里的Widgets,Elements, RenderObject三要素与React中的Element,Instance/Fiber, Dom有点类似 Widgets:widget tree,只是属性集合,需要被绘制的属性集合,每次build,都是新对象,所以属性都要用final修饰 Elements:element tree,concrete widget tree,diff操作,每次build,不会重新构建,进行diff和update RenderObject:真正负责layout, rendering等等操作,一般是由element创建 Flutter的性能 Flutter性能要高的原因: debug为字节码,release为机器码 不依赖OEM widgets 没有bridge Native View: Hybrid: ReactNative: Flutter 注意:以上只是从实现角度分析,在机器性能好的情况下,实际差距不大 Git权限分配工具简介为不同类型的角色批量分配Git权限的工具,整体效果如下: 源码下载地址:https://github.com/handsomeliuyang/flutter-igit 框架结构 pubspec.yaml:与package.json/build.grale类似,用于配置程序的信息,如下所示: assets:用于存放内置图片与资源,自建目录可修改 lib:src目录,按功能模块分为: main.dart/main_dev.dart:程序的入口文件,与c语言类似,dart程序的入口为main()函数,main_dev.dart的区别是使用DevToolsStore,用于查看store与action App.dart:最外层的配置,如下所示: 12345678910111213141516@overrideWidget build(BuildContext context) { return StoreProvider( // 使用Redux的要求 store: widget.store, child: new MaterialApp( // 使用Material要求 title: 'Flutter igit', theme: new ThemeData( // 全局样式 primaryColor: const Color(0xFF1C306D), accentColor: const Color(0xFFFFAD32), ), home: MainPage( devDrawerBuilder: widget.devDrawerBuilder ), ), );} 按功能划分目录:models, networking, redux, ui, utils reduxredux的结构非常简单,如下所示: 由于Flutter是一个类似MVVM框架,所以通过StoreConnector实现数据监听,如下所示:12345678910111213@overrideWidget build(BuildContext context) { return StoreConnector<AppState, DrawListViewModel> ( distinct: true, converter: (store) => DrawListViewModel.fromStore(store), builder: (context, viewModel){ return DrawListContent( header: this.header, viewModel: viewModel, ); }, );} 在Flutter里,应用了Redux后的实现结构为: Redux是全局单例,应用的功能模块很多,所以redux的目录与state按功能模块的划分更加合适,如下所示:12345678910111213141516171819202122class AppState { final TradelineState tradelineState; final PermissionState permissionState; final ProjectState projectState; AppState({ @required this.tradelineState, @required this.permissionState, @required this.projectState }); static initial() { return AppState( tradelineState: TradelineState.initial(), permissionState: PermissionState.initial(), projectState: ProjectState.initial() ); } ...} networkflutter的http请求很简单,主要是使用两个Api:http,Uri,如下所示:123456789101112131415161718192021222324252627Future<List<GitProject>> getGroups(int page, String search) async { Uri uri = Uri.http( AUTHORITY, '${FIXED_PATH}/groups', <String, String>{ 'private_token': Config.LIUYANG_TOKEN, 'per_page': PER_PAGE.toString(), 'all_available': 'true', 'page':'${page}', 'search':'com.wuba' }); final response = await http.get(uri.toString()); final jsonResponse = json.decode(response.body); debugPrint('liuyang ${jsonResponse}'); if(response.statusCode == 200){ List<GitProject> groups = List<GitProject>(); for(int i=0; i<jsonResponse.length; i++){ groups.add(GitProject.fromJson(jsonResponse[i], ProjectType.group)); } return groups; } else { throw Exception('Failed ${response.statusCode} ${response.body}'); }} 注意: 上面是通过async,Future实现异步操作,但此异步并不是真正的开异步线程,只是把任务放在队列里,延迟执行而已,应该使用isolate实现真正的异步执行 面向对象编程,每个Model里,都有两个Api:fromJson(),toJson() MainPage整体效果: 关键点: 此框架页包含:AppBar,Drawer,DevDrawer,PermissionPage 此框架默认应该是StatelessWidget,但由于AppBar的title需要动态拼接,导致只能改为StatefulWidget,如下: 123456789101112131415161718192021222324252627class _MyPageState extends State<MainPage> { Widget _buildTitle(BuildContext context) { return StoreConnector<AppState, Tradeline>( distinct: true, converter: (store) => store.state.tradelineState.current, builder: (BuildContext context, Tradeline currentTradeline) { return Text( '分配 ${currentTradeline?.name ?? ''} 的igit权限' ); }, ); } @override Widget build(BuildContext context) { return new Scaffold( appBar: new AppBar(title: _buildTitle(context)), drawer: Drawer( child: DrawList( header: DrawListHeader() ), ), endDrawer: widget.devDrawerBuilder != null ? widget.devDrawerBuilder(context) : null, body: PermissionPage(), ); }} dart里创建对象时,new关键字不是必需的,如下: 12345class Shape {}Shape shape = new Shape();Shape shape1 = Shape(); 在build时,个人感觉省略掉new关键字,可读性更强 Drawer效果如下: 功能比较简单,思路如下: 通过StoreConnector,获取并监听Store 构建ListView 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667class DrawList extends StatelessWidget { final Widget header; DrawList({ @required this.header }); @override Widget build(BuildContext context) { return StoreConnector<AppState, DrawListViewModel> ( distinct: true, converter: (store) => DrawListViewModel.fromStore(store), builder: (context, viewModel){ return DrawListContent( header: this.header, viewModel: viewModel, ); }, ); }}class DrawListContent extends StatelessWidget { final Widget header; final DrawListViewModel viewModel; DrawListContent({ @required this.header, @required this.viewModel }); @override Widget build(BuildContext context) { return ListView.builder( itemCount: this.viewModel.tradelines.length + 1, itemBuilder: (BuildContext context, int index) { if (index == 0) { return this.header; } Tradeline tradeline = this.viewModel.tradelines[index - 1]; bool isSelected = this.viewModel.currentTradeline.name == tradeline.name; var backgroundColor = isSelected ? const Color(0xFFEEEEEE) : Theme .of(context) .canvasColor; return Material( color: backgroundColor, child: ListTile( onTap: () { viewModel.changeCurrentTradeline(tradeline); Navigator.pop(context); }, selected: isSelected, title: Text(tradeline.name), ), ); } ); }} 关键点: ListTile的属性有限,设置Item的背景通过Material Widget,也可以通过Container Widget ListView没有header的概念,都是item ListView没有分隔线的Api,分隔线是由Item实现,通过ListTile.divideTiles()实现,其内部是通过DecoratedBox Widget实现 Navigator栈:Drawer,Dialog,Route都由Navigator栈管理,所以如下操作都是出栈操作Navigator.pop(context): dismiss drawer dismiss dialog Back PermissionPanel效果的源码来自:flutter_gallery里的Expansion panels例子,个人学习新技术的过程: 看官方的文档 运行官方demo,思考如何实现,对照源码的实现 具体的代码,可通过下载源码查看,这里重点讲一下Flutter的生命周期函数,在Flutter里,StatelessWidget和StatefulWidget没有生命周期,因为其是不可变的,只有State才有生命周期,如下所示: 当数据变化时,StatelessWidget与StatefulWidget每次都会创建新的对象,并执行build()函数,State会被复用,造成flutter程序的如下特点: StatelessWidget, StatefulWidget里的成员变量都是final的,可以理解为React里的props State里的成员变量可以理解为React里的state,即为局部变量(Store里的为全局变量) State的initState()只执行一次,如果成员变量需要依据props而修改,可以在didUpdateWidget()里更新 修改State的成员变量时,如果希望界面需要同步修改,需要在setState()里修改,如下所示:— 大家可以对比下与React的setState()有什么区别? 123setState(() { item.isExpanded = false;}); 如下所示:123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263class PermissionContent extends StatefulWidget { final List<GitProject> projects; final List<GitUser> users; final Function addGitProject; final Function deleteGitProject; final Function getUserIdByName; final Function deleteGitUser; final Function allocationPermission; const PermissionContent({ @required this.projects, @required this.users, @required this.addGitProject, @required this.deleteGitProject, @required this.getUserIdByName, @required this.deleteGitUser, @required this.allocationPermission }); @override _PermissionContentState createState() => _PermissionContentState();}class _PermissionContentState extends State<PermissionContent> { static const Map<String, String> ACCESS_LEVEL = {...}; List<PanelItem> _panelItems; PanelItem _userPanelItem; PanelItem _rolePanelItem; PanelItem _projectPanelItem; @override void initState() { super.initState(); _userPanelItem = _initUserPanelItem(); _rolePanelItem = _initRolePanelItem(); _projectPanelItem = _initProjectPanelItem(); _panelItems = <PanelItem>[ _userPanelItem, _rolePanelItem, _projectPanelItem ]; } @override void didUpdateWidget(PermissionContent oldWidget) { super.didUpdateWidget(oldWidget); // 更新数据 _projectPanelItem.value = widget.projects; _userPanelItem.value = widget.users; } void _navigatorProjectPage(BuildContext context) async {...} PanelItem _initUserPanelItem() {...} PanelItem _initRolePanelItem() {...} PanelItem _initProjectPanelItem() {...} @override Widget build(BuildContext context) {...}} 交互反馈 除了通过Widget构建界面外,有时我们还需要给用户交互反馈: Toasts/Snackbars:仅信息反馈,定时消失,不进Navigator栈 123Scaffold.of(context).showSnackBar(new SnackBar( content: new Text(\"权限分配成功\"),)); Dialog:信息反馈,有进一步交互,Natvigator栈管理 123456789101112131415161718// show:showDialog( context: context, barrierDismissible: false, builder: (BuildContext context){ return Dialog( child: Row( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), Text(\"Loading\"), ], ), ); });// Dismiss:Navigator.pop(context); Dialog仅仅只是modal,无法通过props来控制显示与消失,只能监听局部变量state或全局变量store来控制show与dismiss,分配权限的过程的代码如下:123456789101112131415161718192021222324252627282930313233343536// 创建Completer对象Completer<bool> completer = Completer<bool>();// 发送action,通过igit的Api分配权限widget.allocationPermission(completer, users, level, projects);// 同时显示LoadingDialogshowDialog( context: context, barrierDismissible: false, builder: (BuildContext context){ return Dialog( child: Row( mainAxisSize: MainAxisSize.min, children: [ CircularProgressIndicator(), Text(\"Loading\"), ], ), ); });// 监听成功与失败,并显示不同Toastscompleter.future.then((user){ Navigator.pop(context); Scaffold.of(context).showSnackBar(new SnackBar( content: new Text(\"权限分配成功\"), ));}, onError: (e){ Navigator.pop(context); Scaffold.of(context).showSnackBar(new SnackBar( content: new Text(\"权限分配失败 ${e}\"), ));}); Project效果如下: 详细细节请查看代码,重点分享其中几个关键点 LoadingView除静态页面外,所有的页面都有一个共同的加载流程:加载中…,失败/成功。统一实现LoadingView,如下所示:12345678910111213141516171819202122232425262728class ProjectListWrap extends StatelessWidget { final ProjectListViewModel projectListViewModel; ProjectListWrap({ this.projectListViewModel }); @override Widget build(BuildContext context) { return LoadingView( status: projectListViewModel.status, loadingContent: PlatformAdaptiveProgressIndicator(), errorContent: ErrorView( description: '加载出错', onRetry: projectListViewModel.refreshProjects, ), successContent: ProjectListContent( projects: projectListViewModel.projects, nextState: projectListViewModel.nextStatus, currentPage: projectListViewModel.currentPage, hasNext: projectListViewModel.hasNext, refreshProjects: projectListViewModel.refreshProjects, fetchNextProjects: projectListViewModel.fetchNextProjects, ), ); }} 下滑加载下一页列表数据很多,通过滑动动态加载下一页数据,监听的方式与Android的类似,通过监听其滑动位置,同时由于滑动是有状态的,所以要使用StatefulWidget,如下所示:12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364class ProjectListContent extends StatefulWidget { final List<GitProject> projects; final LoadingStatus nextState; final int currentPage; final bool hasNext; final Function refreshProjects; final Function fetchNextProjects; ProjectListContent({ @required this.projects, @required this.nextState, @required this.currentPage, @required this.hasNext, @required this.refreshProjects, @required this.fetchNextProjects }); @override State<StatefulWidget> createState() => _ProjectListContentState();}class _ProjectListContentState extends State<ProjectListContent> { final ScrollController scrollController = ScrollController(); @override void initState() { super.initState(); scrollController.addListener(_scrollListener); } @override void dispose() { scrollController.removeListener(_scrollListener); scrollController.dispose(); super.dispose(); } void _scrollListener() { if (scrollController.position.extentAfter < 64 * 3) { if(widget.nextState == LoadingStatus.success && widget.hasNext){ widget.fetchNextProjects(widget.currentPage + 1); } } } @override Widget build(BuildContext context) {...} Widget _nextStateToText() { if(!widget.hasNext) { return Text('加载成功,已无下一页'); } if(widget.nextState == LoadingStatus.error){ return Text('加载失败,滑动重新加载'); } return Text('加载中...'); }} 总结Flutter是不同于ReactNative的跨端解决方案,是以一套代码实现高开发效率与高性能为目标,没有ReactNative的bridge,同时通过Dart解决javascript开发效率问题。 现在Flutter比较ReactNative的最大问题是:release下不支持”hot update”,官方的解释如下: Often people ask if Flutter supports “code push” or “hot update” or other similar names for pushing out-of-store updates to apps. Currently we do not offer such a solution out of the box, but the primary blockers are not technological. Flutter supports just in time (JIT) or interpreter based execution on both Android and iOS devices. Currently we remove these libraries during –release builds, however we could easily include them. The primary blockers to this feature resolve around current quirks of the iOS ecosystem which may require apps to use JavaScript for this kind of over-the-air-updates functionality. Thankfully Dart supports compiling to JavaScript and so one could imagine several ways in which one compile parts of ones application to JavaScript instead of Dart and thus allows replacement of or augmentation with those parts in deployed binaries. This bug tracks adding some supported solution like this. I’ll dupe all the other reports here. 简单翻译:Flutter不支持release下的hot update,不是由于技术原因,而是iOS系统只支持javaScript实现无线更新功能,由于Dart可以转换为Javasript代码,所以有一种可能性:程序的一部分使用javascript,而不是dart,再通过动态下载这部分javascript代码,实现hot update。 Flutter是否会成为主流的跨端解决方案,主要原因不在于其高的开发效率与高性能,主要是看Fuchsia操作系统的覆盖程序,如果Fuchsia能成为主流的物联网与Android设备的主流系统,Flutter才能真正成为主流。 参考 Technical Overview Why I move to Flutter Dart与消息循环机制[翻译] Flutter快速上车之Widget Flutter, what are Widgets, RenderObjects and Elements? Introduction to Redux in Flutter User Feedback: Toasts / Snackbars Code Push / Hot Update / out of band updates","categories":[{"name":"Android","slug":"Android","permalink":"https://handsomeliuyang.github.io/categories/Android/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://handsomeliuyang.github.io/tags/Android/"},{"name":"Flutter","slug":"Flutter","permalink":"https://handsomeliuyang.github.io/tags/Flutter/"}]},{"title":"DiyReact学习之路","slug":"DiyReact学习之路","date":"2018-08-07T13:15:35.000Z","updated":"2018-08-21T02:33:44.932Z","comments":true,"path":"2018/08/07/DiyReact学习之路/","link":"","permalink":"https://handsomeliuyang.github.io/2018/08/07/DiyReact学习之路/","excerpt":"","text":"DiyReact的功能React的核心点: 组件(Component) Virtual Dom JSX Props & State 核心的渲染的Api:1ReactDOM.render(element, container[, callback]) 在不考虑性能,调试,扩展性的情况下,实现上面React的核心功能,相同Api,仅仅只需要几百行的代码。在此过程中,能真正的去理解其中的关键概念。 Element,Component,Dom下面是React最简单的写法:12345678// 最简单的elementconst element = { type: \"div\", props: { id: \"foo\" }};diyreact.render(element, document.getElementById(\"root\")); 在此demo中,就是把element转化为dom显示出来。在React里,我们不直接操作Dom元素,我们操作的是Dom的抽象层即Element。 Elements Describe the Tree An element is a plain object describing a component instance or DOM node and its desired properties. 即通过Element用来表示组件与Dom结点及他们的属性,整体构成一个树型结构,DiyReact.Element的定义非常的简单,如下所示:12345678{ type: \"\", // 类型,可以为Dom元素,或Component类型,如Button props: { children:[ // element类型的children ], xxx: xxx // 此element的属性列表 }} 如下面的Element:1234567891011const element = { type: \"div\", props: { id: \"container\", children: [ { type: \"input\", props: { value: \"foo\", type: \"text\" } }, { type: \"a\", props: { href: \"/bar\" } }, { type: \"span\", props: {} } ] }}; 其所描述的Dom:12345<div id=\"container\"> <input value=\"foo\" type=\"text\"> <a href=\"/bar\"></a> <span></span></div> 如上所示,render()方法就是利用Dom的Api,把Element树转换为对应的Dom树:1234567891011121314151617181920212223242526272829export function render(element, parentDom) { const {type, props} = element; // 创建Dom的Element const isTextElement = type === \"TEXT ELEMENT\"; const dom = isTextElement ? document.createTextNode(\"\") : document.createElement(type); // 读取element里的onXXX属性,当事件处理 const isListener = name => name.startsWith(\"on\"); Object.keys(props).filter(isListener).forEach(name => { const eventType = name.toLowerCase().substring(2); dom.addEventListener(eventType, props[name]); }); // element里除onXXX与children属性外,都当属性对待 const isAttribute = name => !isListener(name) && name != \"children\"; Object.keys(props).filter(isAttribute).forEach(name => { dom[name] = props[name]; }); // 递归render children const childElements = props.children || []; childElements.forEach(childElement => render(childElement, dom)); // 添加到parentDom parentDom.appendChild(dom);} createElement与JSX直接使用Element来描述Dom元素,其可读性很差,如下所示:1234567891011const element = { type: \"div\", props: { id: \"container\", children: [ { type: \"input\", props: { value: \"foo\", type: \"text\" } }, { type: \"a\", props: { href: \"/bar\" children: [{ type: \"TEXT ELEMENT\", props: { nodeValue: \"bar\" } }]} }, { type: \"span\", props: {} } ] }}; 而使用JSX来表示的话,可读性就能提升很多,如下所示:12345678/**@jsx diyreact.createElement **/const element = ( <div id=\"container\"> <input value=\"foo\" type=\"text\"/> <a href=\"/bar\">bar</a> <span></span> </div>); 以上的JSX语法,浏览器无法识别,需要通过babel进行预处理,通过babel的插件transform-react-jsx把JSX转换为如下代码:12345678910111213/**@jsx diyreact.createElement **/const element = diyreact.createElement( \"div\", { id: \"container\" }, diyreact.createElement(\"input\", { value: \"foo\", type: \"text\" }), diyreact.createElement( \"a\", { href: \"/bar\" }, \"bar\" ), diyreact.createElement(\"span\", null)); 增加JSX后的整体流程如下所示: 对应的createElement代码,非常简单:12345678910111213141516const TEXT_ELEMENT = \"TEXT ELEMENT\";export function createElement(type, config, ...args) { const props = Object.assign({}, config); const hasChildren = args.length > 0; const rawChildren = hasChildren ? [].concat(...args) : []; props.children = rawChildren .filter(c => c != null && c !== false) .map(c => c instanceof Object ? c : createTextElement(c)); return { type, props };}function createTextElement(value) { return createElement(TEXT_ELEMENT, { nodeValue: value });} babel的插件transform-react-jsx的做的非常通用,通用注解,可以修改默认的React.createElement函数,可以通过babel-online测试 // TODO-ly render()每次都是从root结点开始进行对比,setState()是从哪个当前这个结点开始,但整体逻辑是一样的 ComponentReact.render()函数里的element的范围很广,可以是Object,Function,Component,但只有Component才会有相应的lifecycle, states等等。12345678910class Component { constructor(props) { this.props = props; this.state = this.state || {}; } setState(partialState) { // 更新逻辑 }} 更多细节大家可以查看:Didact: Components and State 1234567891011/** @jsx diyreact.createElement **/class App extends diyreact.Component { render() { return ( <div> <h1>DiyReact的学习过程</h1> </div> ); }}diyreact.render(<App />, document.getElementById(\"root\")); 其把JSX转换后的代码:123456789101112131415/** @jsx diyreact.createElement **/class App extends diyreact.Component { render() { return diyreact.createElement( \"div\", null, diyreact.createElement( \"h1\", null, \"DiyReact\\u7684\\u5B66\\u4E60\\u8FC7\\u7A0B\" ) ); }}diyreact.render(diyreact.createElement(App, null), document.getElementById(\"root\")); 其对应的Element Tree与Virtual Dom Tree: Instance,reconciliation与Virtual Dom上述的render()函数,把element转为Dom元素,每次调用render()函数时,都会创建全新的dom元素,即使用element完全一致,都不会进行复用。如下所示:123const element = <div>Foo</div>;render(element, document.getElementById(\"root\"));render(element, document.getElementById(\"root\")); 在React里,求两个Elements Tree的过程称为”reconciliation“,为了复用与对比,我们需要保存一个与之对应的对象树:A Virtual Dom。 这个Virtual Dom的”nodes”应该是什么对象?由于如下原因,我们无法复用element对象: 此node对象,需要关联其对应的dom对象,但elements树应该是不可变的 无法支持Component,因为每个Component都有自己的state对象 引入React的新概念:Instances。此Instances就表示这个Virtual Dom Tree,其中instance表示已经render到dom的对象。定义如下:1instance = {element, dom, childInstances}; 每个element,每个Dom节点都对应一个instance对象,我们的目标是尽可能的减少此instances的创建与销毁。 Element,Instances,Dom的关系图: Component的setState()更新:1234567891011121314151617181920212223242526import { reconcile } from \"./reconciler\";export class Component { constructor(props) { this.props = props; this.state = this.state || {}; } setState(partialState) { this.state = Object.assign({}, this.state, partialState); updateInstance(this.__internalInstance); }}function updateInstance(internalInstance) { const parentDom = internalInstance.dom.parentNode; const element = internalInstance.element; reconcile(parentDom, internalInstance, element);}export function createPublicInstance(element, internalInstance) { const { type, props } = element; const publicInstance = new type(props); publicInstance.__internalInstance = internalInstance; return publicInstance;} render的核心代码:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889let rootInstance = null;export function render(element, container) { const prevInstance = rootInstance; const nextInstance = reconcile(container, prevInstance, element); rootInstance = nextInstance;}export function reconcile(parentDom, instance, element) { if (instance == null) { // Create instance const newInstance = instantiate(element); parentDom.appendChild(newInstance.dom); return newInstance; } else if (element == null) { // Remove instance parentDom.removeChild(instance.dom); return null; } else if (instance.element.type !== element.type) { // Replace instance const newInstance = instantiate(element); parentDom.replaceChild(newInstance.dom, instance.dom); return newInstance; } else if (typeof element.type === \"string\") { // Update dom instance updateDomProperties(instance.dom, instance.element.props, element.props); instance.childInstances = reconcileChildren(instance, element); instance.element = element; return instance; } else { //Update composite instance instance.publicInstance.props = element.props; const childElement = instance.publicInstance.render(); const oldChildInstance = instance.childInstance; const childInstance = reconcile(parentDom, oldChildInstance, childElement); instance.dom = childInstance.dom; instance.childInstance = childInstance; instance.element = element; return instance; }}function reconcileChildren(instance, element) { const dom = instance.dom; const childInstances = instance.childInstances; const nextChildElements = element.props.children || []; const newChildInstances = []; const count = Math.max(childInstances.length, nextChildElements.length); for (let i = 0; i < count; i++) { const childInstance = childInstances[i]; const childElement = nextChildElements[i]; const newChildInstance = reconcile(dom, childInstance, childElement); newChildInstances.push(newChildInstance); } return newChildInstances.filter(instance => instance != null);}function instantiate(element) { const { type, props } = element; const isDomElement = typeof type === \"string\"; if (isDomElement) { // Instantiate DOM element const isTextElement = type === TEXT_ELEMENT; const dom = isTextElement ? document.createTextNode(\"\") : document.createElement(type); updateDomProperties(dom, [], props); const childElements = props.children || []; const childInstances = childElements.map(instantiate); const childDoms = childInstances.map(childInstance => childInstance.dom); childDoms.forEach(childDom => dom.appendChild(childDom)); const instance = { dom, element, childInstances }; return instance; } else { // Instantiate component element const instance = {}; const publicInstance = createPublicInstance(element, instance); const childElement = publicInstance.render(); const childInstance = instantiate(childElement); const dom = childInstance.dom; Object.assign(instance, { dom, element, childInstance, publicInstance }); return instance; }} diyreact的reconciliation算法比较简单,只有当position与type都相同的情况下,才复用此instance,更新其内部的属性 Fiber上述的reconciliation算法是一个递归算法,当节点数量很大时,整体执行时间比较慢,会一直占用浏览器的main thread,导致动画出现卡顿和用户操作响应不及时。卡顿的理解与Android的卡顿理解是一至的,当一次render()或setState(),触发的reconcile()过程,超过16ms时,就会出现丢帧现象。卡顿demo,如下图所示: 要解决卡顿问题,主要是解决上述的递归调用问题,让递归调用可以被中断,优先去处理animation和UI responsive。 React在16.x.x的解决方案是:把上述的执行过程拆分为很多的工作单元(UnitOfWork),这些很小的工作单元都能在很短的时间内执行完成,同时每两个执工作单元之间可以被中断,让main thread执行更高优先级的任务,如animation,ui responsive。 在DiyReact里的UnitOfWork就是包括当前节点的处理工作: new_type != cur_type:全新创建instance type相等 && type是string类型:更新属性 type相等 && type为对象:执行component.render(),更新属性 如果知道当前main thread需要执行更高优先级任务了?利用requestIdleCallback-后台任务调度就可以了解当前main thread是否处于空闲时间,其调用代码:1234567891011render(){ updateQueue.push(...); window.requestIdleCallback(performWork);}function performWork(deadline) { // ... while(nextUnitOfWork && deadline.timeRemaining() > ENOUGH_TIME){ nextUnitOfWork = performUnitOfWork(nextUnitOfWork); } // ...} 该图中的frame#1,frame#2就是两个帧,每个帧的持续时间是(100/60 = 16.66ms),而在每一帧内部,TASK和redering只花费了一部分时间,并没有占据整个帧,那么这个时候,如图中idle period的部分就是空闲时间,而每一帧中的空闲时间,根据该帧中处理事情的多少,复杂度等,消耗不等,所以空闲时间也不等。 通过deadline.timeRemaining()函数即可知道当前还剩多少idle时间。 要实现这套新的工作单元调度,instance tree的节点instance的结构会要发生变化,如下所示:12345678910111213141516{ tag:HOST_COMPONENT|CLASS_COMPONENT, type:\"div\"|Component, // 构建一个树型链表结构 parent: parentFiber, child: childFiber, sibling:null, // 关联第二颗树 alternate: other fiber tree, stateNode:dom|component, props: element.props, partialState: component changed state, // 记录真正变动的节点fiber effectTag:PLACEMENT, effects: []}; 这颗新的树的结点有一个新的名称:Fiber。这个颗也被称为Fiber Tree。 fiber tree的结构: 每两个工作单元之间,可以被更高优先级的任务中断,那就无法使用一颗Fiber Tree,即对应当前的Dom,又进行更新操作。通过上面的alternate可知,有两颗相互关联的Fiber Tree: current tree:与当前的Dom对应,其内容已经渲染到Dom上 work-in-progress:由render()或setState()触发的构建树 方法的调用队列: 更多代码细节,请学习:Didact Fiber: Incremental reconciliation 发布在React的最新版本里,打包工具从webpack,改为rollup。 webpack与rollup基本相同,记住如下差异点: webpack支持code-splitting,同时支持按需加载 Rollup默认基于ES2015模块,把所有的资源放在一起,一次性加载 如何选择?结论: 针对app级别的应该使用Webpack,针对js库级别的应用应该使用Rollup。 更多请参考:Webpack、Rollup相爱相杀的那些事 rollup由于默认基于ES2015模块与语法,而整体DiyReact也是基于ES6开发的,所以配置很简单:123\"scripts\": { \"build:main\": \"rollup src/diyreact.js -f umd -n diyreact -o dist/diyreact.umd.js\"} 具体参数的含义请参考:Command line flags Type of output (amd, cjs, esm, iife, umd)的理解: iife: 立即执行函数 cjs: 遵循CommonJs Module规范的文件输出 amd: 遵循AMD Module规范的文件输出 umd: 支持外链/CommonJs Module/AMD Module规范的文件输出 esm: 将多个遵循ES6 Module的文件编译成1个ES6 Module 在不同场景下的使用情况: 12345678// For browsers:$ rollup main.js --file bundle.js --format iife// For Node.js:$ rollup main.js --file bundle.js --format cjs// For both browsers and Node.js:$ rollup main.js --file bundle.js --format umd --name \"myBundle\" 发布测试为了方便测试生成后的diyreact.js文件,使用的是babel-standalone@6库,在browser下直接运行ES6语法,如下所示:1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162<html><head> <script src="https://unpkg.com/babel-standalone@6/babel.min.js"></script> <script src="../dist/diyreact.umd.js" type="text/javascript"></script></head><body> <div id="root"></div> <script type="text/babel" data-plugins="transform-react-jsx" data-presets="es2017,stage-3"> /** @jsx diyreact.createElement **/ const studies = [ { name: "DiyReact的功能", url: "https://handsomeliuyang.github.io/" }, { name: "createElement与JSX", url: "https://handsomeliuyang.github.io/" }, { name: "Instance,reconciliation与Virtual Dom", url: "https://handsomeliuyang.github.io/" }, { name: "Component and State", url: "https://handsomeliuyang.github.io/" }, { name: "Fiber", url: "https://handsomeliuyang.github.io/" } ]; class App extends diyreact.Component { render() { return ( <div> <h1>DiyReact的学习过程</h1> <ul> { this.props.studies.map(study => { return <Study name={study.name} url={study.url}/>; }) } </ul> </div> ); } } class Study extends diyreact.Component { constructor(props) { super(props); this.state = { likes: Math.ceil(Math.random() * 100) }; } like() { this.setState({ likes: this.state.likes + 1 }); } render() { const { name, url } = this.props; const { likes } = this.state; const likesElement = <span />; return ( <li> <button onClick={e => this.like()}>赞:{likes}️</button> <a href={url}>{name}</a> </li> ); } } diyreact.render(<App studies={studies} />, document.getElementById("root")); </script></body></html> 注意: babel-standalone的配置API很少,可以查看其源码:https://github.com/babel/babel-standalone/blob/master/src/index.js babel-standalone不支持env preset,只有es2015, es2016, es2017等等presets,为了支持new Class语法,需要使用es2017 踩过的坑单元测试单元测试必要性这里就不叙述了,选择的是ava单元测试框架,使用过程中的一些问题: es6语法,jsx语法默认不支持? ava只是一个单元测试框架,需要通过babel来支持es6,jsx等语法的支持,配置如下: 12345678910111213141516171819202122\"ava\": { \"require\": \"babel-register\", \"babel\": \"inherit\" // 继承在package.json里的babel配置},\"babel\": { \"plugins\": [ [ \"transform-react-jsx\", {} ] ], \"presets\": [ [ \"env\", { \"targets\": { \"node\": \"current\" } } ] ]} 没有browser相关的环境与Api? 通过browser-env库,可以实现能browser的部分Api进行模拟,如下所示: 1234567891011import browserEnv from 'browser-env';browserEnv(['document']);test.beforeEach(t=>{ let root = document.getElementById(\"root\"); if(!root){ root = document.createElement(\"div\"); root.id = \"root\"; document.body.appendChild(root); } t.context.root = root;}); browser-env库没有window.requestIdleCallback等Api? 为了能进行单元测试,手动给window对象注入requestIdleCallback()实现,当然这里是假实现,如下所示: 12345678window.requestIdleCallback = function(task){ function timeRemaining(){ return 2; } task({ timeRemaining : timeRemaining });}; ava单元测试如何debug? 升级IntelliJ IDEA到新版本后,在package.json下添加的script里,添加字符串”$NODE_DEBUG_OPTION”,如下所示: babel的一些概念理解:babel-register?babel-standalone@6?plugin与preset的区别? babel的编译过程: parser:通过 babylon 解析成 AST transform[s]:All the plugins/presets ,进一步的做语法等自定义的转译,仍然是 AST。 generator: 最后通过 babel-generator 生成 output string。 plugins与presets的区别:presets是一个plugin的集合,如babel-preset-env,根据当前的运行环境,确定需要的plugin组合 babel-register:require(‘babel-register’)后,所以require()其他模块时,就会进行文件编译,这个比较适合开发期间使用 babel-standalone@6:在browser上,对js代码实现在线转换,要完全支持React,需要配置对应的plugins和presets,如下所示:1234567891011<html><head> <script src=\"https://unpkg.com/babel-standalone@6/babel.min.js\"></script></head><body> <div id=\"root\"></div> <script type=\"text/babel\" data-plugins=\"transform-react-jsx\" data-presets=\"es2017,stage-3\"> // react代码 </script></body></html> CommonJS与ES6模块的区别? 详细请查看:ES6模块 和 CommonJS 的区别 本篇文章的code:diyreact 参考 Didact: a DIY guide to build your own React React Components, Elements, and Instances React Fiber Architecture requestIdleCallback-后台任务调度 babel的关键概念理解","categories":[],"tags":[{"name":"前端","slug":"前端","permalink":"https://handsomeliuyang.github.io/tags/前端/"}]},{"title":"MPVue源码分析","slug":"MPVue源码分析","date":"2018-06-07T06:34:35.000Z","updated":"2018-06-12T12:51:07.000Z","comments":true,"path":"2018/06/07/MPVue源码分析/","link":"","permalink":"https://handsomeliuyang.github.io/2018/06/07/MPVue源码分析/","excerpt":"","text":"Demo使用Vue实现一个消息逆转的demo:1234<div id=\"app\"> <p>{{ message }}</p> <button v-on:click=\"reverseMessage\">逆转消息</button></div> 1234567891011var app = new Vue({ el: '#app', data: { message: 'Hello Vue.js!' }, methods: { reverseMessage: function () { this.message = this.message.split('').reverse().join('') } }}) You can try it on CodePen. 同样的效果,使用小程序实现的代码为: 小程序的整体结构如下所示: : App相关的代码: 123456789101112131415// app.js文件App({ onLaunch: function () { console.log(\"App onLaunch...\"); }, onShow: function(){ console.log(\"App onShow...\"); }, onHide: function(){ console.log(\"App onHide...\"); }, onError: function(){ console.log(\"App onError...\"); }}) 123456789101112// app.json{ \"pages\":[ \"pages/index/index\" ], \"window\":{ \"backgroundTextStyle\":\"light\", \"navigationBarBackgroundColor\": \"#fff\", \"navigationBarTitleText\": \"WeChat\", \"navigationBarTextStyle\":\"black\" }} 12345678910// app.wxss.container { height: 100%; display: flex; flex-direction: column; align-items: center; justify-content: space-between; padding: 200rpx 0; box-sizing: border-box;} Page相关的代码: 1234567<!--index.wxml--><view class=\"container\"> <view class=\"usermotto\"> <text class=\"user-motto\">{{message}}</text> <button bindtap=\"reverseMessage\">逆转消息</button> </view></view> 123456789101112131415161718//index.jsPage({ data: { message: 'Hello Wechat!' }, //生命周期函数 onLoad: function(){ console.log(\"Page onLoad...\"); }, onReady: function(){ console.log(\"Page onReady...\"); }, //事件处理函数 reverseMessage: function(){ // this.data.message = this.data.message.split('').reverse().join('') this.setData({ message: this.data.message.split('').reverse().join('') }) }}) 1234/**index.wxss**/.usermotto { margin-top: 100px;} 小程序简介Vue与小程序都可以看成javascript的高级抽象,类似于java里的各种框架,想让基于Vue开发的代码运行在小程序里,就把Vue的低层映射到小程序的Api,而不是Web端的Dom。 先来分析小程序的框架体系: 目录结构:包括App相关的三个文件,每个Page包括三个文件 代码结构: xxx.wxml布局文件,小程序特定的基础组件,如view,button, text xxx.js逻辑文件,类似于Vue对象的Page对象与App对象 xxx.wxss样式文件,与css一致,但支持样式属性列表不一样 类MVVM的响应式框架:V(wxml),ViewModel(Page.data,Page.func) Page.data与wxml对应,通过setData()修改data数据,并自动更新View Vue的实现原理Vue运行期间,分为三个阶段: 初始化,生成Vue对象,让其具备如下属性与方法,如下所示: vm.$mount()后,建立整个响应式框架,并首次渲染Dom 事件响应,即用户交互 整个响应式框架如下图所示: MPVue的实现通过上面的小程序框架与Vue的实现原理介绍,基于Vue写的代码,要在小程序上运行,需要做如下工作: 编译期间生成小程序目录结构与代码结构 生成app相关的三个文件:app.js, app.json, app.wxss 生成Page相关的三个文件:xxx.js, xxx.wxml, xxx.wxss xxx.js的转换成本最低,基本不用变化,因为都是基于javascript语言写的 xxx.wxml:把Vue的template转换为小程序的基础组件,同时对组件的属性进行映射处理,成本较高 xxx.wxss:把Vue里的样式,直接转换过来就行,成本较低 对Vue运行时做修改,如下图所示: 部分核心代码: 初始化流程: 更新流程: MPVue源码介绍直接对着代码来分享 参考 小程序框架 read-vue-source-code Vue.js","categories":[{"name":"前端","slug":"前端","permalink":"https://handsomeliuyang.github.io/categories/前端/"}],"tags":[]},{"title":"react-native-wechatmini","slug":"react-native-wechatmini","date":"2018-04-02T11:17:05.000Z","updated":"2018-04-19T06:01:20.000Z","comments":true,"path":"2018/04/02/react-native-wechatmini/","link":"","permalink":"https://handsomeliuyang.github.io/2018/04/02/react-native-wechatmini/","excerpt":"","text":"// 实现思路 React包括两层编程模型:数据模型、UI界面。 React的核心点: VirsualDom 组件化 参考 微信小程序模块化","categories":[{"name":"前端","slug":"前端","permalink":"https://handsomeliuyang.github.io/categories/前端/"}],"tags":[]},{"title":"Gatsby搭建博客之旅","slug":"Gatsby搭建博客之旅","date":"2018-03-19T09:25:39.000Z","updated":"2018-03-26T12:41:33.000Z","comments":true,"path":"2018/03/19/Gatsby搭建博客之旅/","link":"","permalink":"https://handsomeliuyang.github.io/2018/03/19/Gatsby搭建博客之旅/","excerpt":"","text":"Gatsby简介 Blazing-fast static site generator for React (React的快速静态网站生成器) 几大特点: 1.Modern web tech without the headache(不再为web技术落后而头痛) Enjoy the power of the latest web technologies – React.js , Webpack , modern JavaScript and CSS and more — all setup and waiting for you to start building. (受最新Web前端技术的强大功能–React.js,Webpack,现代JavaScript和CSS等等,所有这一切都将启动并等待您的开始。) 2.Bring your own data (使用你自定义的数据) Gatsby’s rich data plugin ecosystem lets you build sites with the data you want — from one or many sources: Pull data from headless CMSs, SaaS services, APIs, databases, your file system & more directly into your pages using GraphQL .(Gatsby丰富的数据插件生态系统允许您使用您想要的数据构建网站 - 来自一个或多个来源:使用GraphQL将数据从无头CMS,SaaS服务,API,数据库,文件系统等更直接地导入您的页面) 3.Scale to the entire internet (轻松发布到互联网) Gatsby.js is Internet Scale. Forget complicated deploys with databases and servers and their expensive, time-consuming setup costs, maintenance, and scaling fears. Gatsby.js builds your site as “static” files which can be deployed easily on dozens of services.(Gatsby.js是互联网化的。 你可以不用理会数据库和服务器的复杂部署,以及昂贵,耗时的设置成本,维护和缩放恐惧。 Gatsby.js将您的网站构建为“静态”文件,可以轻松部署在数十种服务上) 4.Future-proof your website (使您的网站面向未来) Don’t build a website with last decade’s tech. The future of the web is mobile, JavaScript and APIs—the JAMstack. Every website is a web app and every web app is a website. Gatsby.js is the universal JavaScript framework you’ve been waiting for.(不要用过去十年的技术建立一个网站。 网络的未来是移动的,JavaScript和API - JAMstack。 每个网站是一个Web应用程序,每个Web应用程序是一个网站。 Gatsby.js是你一直在等待的通用JavaScript框架。) 5.Static Progressive Web Apps (静态PWA) Gatsby.js is a static PWA (Progressive Web App) generator. You get code and data splitting out-of-the-box. Gatsby loads only the critical HTML, CSS, data, and JavaScript so your site loads as fast as possible. Once loaded, Gatsby prefetches resources for other pages so clicking around the site feels incredibly fast.(Gatsby.js是一个静态PWA(Progressive Web App)生成器。 您可以将代码和数据分开。 Gatsby只加载关键的HTML,CSS,数据和JavaScript,以便您的网站加载尽可能快。 一旦加载,Gatsby预取其他网页的资源,所以点击网站感觉非常快。) 6.Speed past the competition (超越竞争) Gatsby.js builds the fastest possible website. Instead of waiting to generate pages when requested, pre-build pages and lift them into a global cloud of servers — ready to be delivered instantly to your users wherever they are.(Gatsby.js建立最快的网站。 不需要等待请求时生成页面,而是预先生成页面,并将其提升到全球服务器云端 - 随时随地传送给用户,无论他们身在何处。) 工作原理: HelloWord按官网教程很容易创建一个简单的HelloWord。详见 常用命令: gatsby new xxx // 创建一个新的项目 gatsby develop // 构建开发站点 gatsby serve // 测试发布构建 gatsby build // 发布构建 效果如下: 技术点React,Webpack,ES6这三种技术就不重点介绍了 SASS Sass 是对 CSS 的扩展,让 CSS 语言更强大、优雅。 它允许你使用变量、嵌套规则、 mixins、导入等众多功能, 并且完全兼容 CSS 语法。 Sass 有助于保持大型样式表结构良好, 同时也让你能够快速开始小型项目, 特别是在搭配 Compass 样式库一同使用时。 更多请参考 GraphQL GraphQL is a query language for APIs and a runtime for fulfilling those queries with your existing data. GraphQL provides a complete and understandable description of the data in your API, gives clients the power to ask for exactly what they need and nothing more, makes it easier to evolve APIs over time, and enables powerful developer tools.(GraphQL是在API能提供的数据范围内,提供查询能力的语言。GraphQL在您的API中提供了对数据的完整和可理解的描述,使客户能够准确地询问他们需要什么,并且更容易随时间发展API,并支持强大的开发人员工具。) 更多请参考 Rest请求过程: GraphQL请求过程: GraphQL的特点: 入口统一,合并请求:不管请求什么资源,url都是一样的。这精简了不同场景下形态各异的API数量。 自定义返回值:在REST中,资源的返回结构与返回数量是由服务端决定;在GraphQL,服务端只负责定义哪些资源是可用的,由客户端自己决定需要得到什么资源,避免让API消费者取到对它来说并没有用的冗余数据。 数据的关联性:在query里,通过id,可以把多个数据源或Api直接关联起来 方便的接口调试工具:GraphiQL工具,文档与调试统一,GraphiQL / live demo 注意:GraphQL是一种标准,但其具体的实现里,有些标准的特性并没有被实现。如下所描述的一样: 从官方的定义来说,GraphQL 是一种针对 API 的查询语言;在我看来,GraphQL 是一种标准,而与标准相对的便是实现。就像 EcmaScript 与 JavaScript 的关系,从一开始你就需要有这样一种认知:GraphQL 只定义了这种查询语言语法如何、具体的语句如何执行等。但是,你在真正使用某种 GraphQL 的服务端实现时,是有可能发现 GraphQL 标准中所描述的特性尚未被实现;或者这种 GraphQL 的实现扩展了 GraphQL 标准所定义的内容。 举例来说,就像 ES 2017 标准正式纳入了 async/await,而从实现的角度上说,IE 没有实现这一标准,而 Edge 16 和 Chrome 62 则实现了这一标准(数据来源于 caniuse)说回 GraphQL 标准,与之相对的有相当多的服务器端实现。他们的大多遵循 GraphQL 标准来实现,但也可能稍有差别,这一切需要你自己去探索。 PWA Progressive Web App, 简称 PWA,是提升 Web App 的体验的一种新方法,能给用户原生应用的体验。 PWA 能做到原生应用的体验不是靠特指某一项技术,而是经过应用一些新技术进行改进,在安全、性能和体验三个方面都有很大提升,PWA 本质上是 Web App,借助一些新技术也具备了 Native App 的一些特性,兼具 Web App 和 Native App 的优点。 更多请参考 Gatsby搭建博客注意:node需要安装6.x版本,Markdown插件gatsby-transformer-remark,在node 8.x与9.x会运行失败 node下载 一个博客主要包括下面几部分: 主页,包括作者介绍,文章列表 Post页面(文章正文页) 归档页,Categories页,Tags页,关于页 工程目录 第三方库 lost:Lost Grid是一个强大的网格系统,可以方便实现表格拆分 moment:解析,验证,操作和显示日期 react-media:适配不同屏幕 主页 Layout在我们这个demo里,首页与Post没有相同的部分,如footer,header,所以Layout里,非常的简单:123456789101112131415161718import React from 'react'import Helmet from 'react-helmet'import \"./style.scss\";class Layout extends React.Component { render(){ const {children} = this.props; return ( <div className=\"layout\"> <Helmet defaultTitle=\"Blog by LiuYang\"/> {children()} </div> ); }}export default Layout; 如果有共同的footer与header,则应该在layout里实现 首页整体布局包括左边Sidebar和右边的文章列表,按组件思维考虑,我们应该创建三个组件或者二个组件: 下面是创建二个组件的首页布局代码:12345678910111213141516171819202122232425262728class IndexRoute extends React.Component { render(){ const {title, subtitle} = this.props.data.site.siteMetadata; const {edges:posts} = this.props.data.allMarkdownRemark; return ( <div> <Helmet> <title>{title}</title> <meta name=\"description\" content={subtitle}/> </Helmet> <Sidebar {...this.props}/> <div className=\"content\"> <div className=\"content__inner\"> { posts .filter(({node:post}) => post.frontmatter.title.length > 0) .map(({node:post})=>{ return ( <Post data={post} key={post.fields.slug}/> ); }) } </div> </div> </div> ); }} 适应不同尺寸的屏幕常见屏幕大小有: lg:宽度大于1100px的屏幕 md:宽度在[960px–1100px]之间的屏幕 sm:宽度在[685px–960px]之间的屏幕 xs:宽度在[0px–685px]之间的屏幕 在不同屏幕下,首页布局也要有相应的变化,利用css的@media实现不同屏幕的适配:CSS @media Rule12345678910111213141516171819202122232425262728293031323334// _breakpoints.scss@mixin breakpoint-sm { @media screen and (min-width: 685) { @content }}@mixin breakpoint-md { @media screen and (min-width: 960) { @content }}// index.js的scss.content { &__inner { padding:25px 20px; }}@include breakpoint-sm { .content { lost-column: 7/12; &__inner { padding: 30px 20px; } }}@include breakpoint-md { .content { lost-column: 2/3; &__inner { padding: 40px 35px; } }} 注意上面的样式是叠加的,下面的会覆写掉面上样式。 空格实现在html页面里,想实现空隔效果,可以有下面几种方案: 通过空格的特殊字符实现 全角下的空格 通过css样式实现,占位div,再设置其margin值 博客相关的gatsby插件 gatsby-source-filesystem:读取本地文件 gatsby-transformer-remark:使用Remark解析Markdown文件 gatsby-remark-images:用于解析图片 gatsby-plugin-postcss-sass:支持sass 默认通过GraphQL无法查询到文件相关的数据,当安装了gatsby-source-filesystem插件后,可以查询到File相关的数据: 安装了gatsby-transformer-remark插件后,就可以查询Markdown相关的数据了: markdown的node结点的parent是file结点,即markdown是基于上一次插件的结果产生的,减少重复制造轮子 Post页面 页面的思路与主页的思路一样,使用组件化的思路设计,唯一的不同点是代码高亮显示 主页是一个固定页,但Post页面有很多,如果批量生成?使用gatsby的扩展点及Api来实现,整体流程如下所示: 123456789101112131415161718192021222324252627282930313233343536373839404142434445exports.createPages = ({graphql, boundActionCreators})=>{ const {createPage} = boundActionCreators; return new Promise((resolve, reject)=>{ const postTemplate = path.resolve('./src/templates/post-template.js'); // 查询所有的markdown,并创建相应的页面 resolve( graphql(`{ allMarkdownRemark( limit: 1000 ) { edges { node { fields { slug } frontmatter { category } } } } }`).then((result)=>{ if (result.errors) { console.log(result.errors); reject(result.errors) } // 创建对应的markdown页面 result.data.allMarkdownRemark.edges.forEach((edge) => { createPage({ path: edge.node.fields.slug, // required component: slash(postTemplate), context: { slug: edge.node.fields.slug, }, }); }); resolve(); }) ) });}; Gatsby与Hexo的对比调研Gatsby的初衷是想把博客实现由Hexo替换为Gatsby,Hexo与Gatsby都是静态网站生成器,主要差别是使用的技术不一样,Gatsby使用的都是最新技术,但最后还是继续使用了Hexo,主要原因是:Hexo提供了很多的Themes,能快速复用这些Themes。 此Demo的地址:Gatsby-demo 参考 中文gatsby介绍 gatsbyjs 约定优于配置 阻碍你使用 GraphQL 的十个问题 GraphQL is the better REST 对比GraphQL与REST——两种HTTP API的差异 GraphQL vs RESTful API 的一些想法 gatsby-starter-lumen","categories":[{"name":"前端","slug":"前端","permalink":"https://handsomeliuyang.github.io/categories/前端/"}],"tags":[]},{"title":"抽屉效果实现三端化(android,ios,web)的历程","slug":"抽屉效果实现三端化的历程","date":"2018-03-03T16:00:00.000Z","updated":"2018-03-26T12:41:45.000Z","comments":true,"path":"2018/03/04/抽屉效果实现三端化的历程/","link":"","permalink":"https://handsomeliuyang.github.io/2018/03/04/抽屉效果实现三端化的历程/","excerpt":"","text":"ReactNative运行通过Native Code的运行命令是: 1react-native run-android 具体过程: 启动js server: 用于生成本地打包服务Metro,地址:http://localhost:8081 编译打包,并安装:cd android && ./gradlew installDebug 手机通过http://localhost:8081访问js server:adb reverse tcp:8081 tcp:8081 启动App:adb shell am start -n 包名/activity App运行时,默认请求bundle的地址为:http://ip:8081/xxx 问题:当电脑切换wifi后,模拟器无法连接js server?原因:电脑的ip地址变了,但App请求bundle地址没有变彻底解决方案:在App里,进入Developer Menu,修改bundle请求地址为:http://localhost:8081 android模拟器快捷键 Developer Menu: ⌘M Reload:two R 注意:使用x86的模拟器,此模拟器的运行速度与真机一致 react-native-web部署通过ReactNative的Metro编译出的bundle.js文件,只能在对应的App里运行,无法直接在浏览器里运行。 在不考虑自定义View和Module的情况,要想生成的bundle.js可以直接在浏览器里运行,理论上只需要两步: 实现一套在浏览器里支持运行的react-native-web库 不通过Metro打包,通过webpack打包,把react-native-web库替换react-native库,同时打包在一起 react-native-web已经有实现版本了,详情请查看react-native-web 相应的webpack的配置过程,请参考:react-native-web-webpack 配置完后,不用对ReactNative代码做任何改动,就能直接在浏览器上支持运行,主要是使用了webpack的alias功能(整体替换react-native库): 12345resolve: { alias: { 'react-native': 'react-native-web', }} 三端实现:抽屉效果调研方案1:仿照DrawerLayoutAndroid的Api,实现DrawerLayoutIOS和DrawerLayoutWeb两套View 方案2:react-navigation库也实现了DrawerLayoutWeb,在webpack如下配置,就可以使用了: 12345resolve: { alias: { 'react-navigation': 'react-navigation/lib/react-navigation.js', }} 注意:不是所有的react-navigation版本都能测试成功,1.0.0-beta.10测试通过,但beta.50测试失败更详细的信息:Navigating in all platforms 方案3:使用基本组件(View,Animated,TouchableWithoutFeedback等),实现DrawerLayout,即可满足三端运行(react-native-drawer-layout) 此方案有一定的适配的问题,可能在android4.x系统里,运行会有一些问题 抽屉效果实现(方案3)实现弹窗效果 实现分析: 整体有三层View,最底层是首页,中间是遮罩层,最上层是抽屉 正常Flexbox布局相当于Android里的LinearLayout布局,但通过position=absolute,与zindex可实现叠加效果,更多请参考CSS position Property 123456789101112131415161718192021222324252627282930<View style={{ flex: 1, backgroundColor: 'transparent'}}> <View style={{ flex: 1, zIndex: 0, }}> {this.props.children} // 子布局 </View> <View style={{ backgroundColor: '#000000', position: 'absolute', top: 0, left: 0, bottom: 0, right: 0, zIndex: 1000, }}> </View> <View style={{ position: 'absolute', top: 0, bottom: 0, zIndex: 1001 }}> {this.props.renderNavigationView()} // 抽屉布局 </View></View> 实现抽屉展开与收起动画 两个动画: 遮罩层渐隐和渐现动画 抽屉水平移动动画 2维动画实现的思路比较简单,以遮罩层的渐隐动画为例: 假设当前的透明度为变量x,例用Animated.View的opacity样式 1234567891011121314.......... <Animated.View style={{ backgroundColor: '#000000', position: 'absolute', top: 0, left: 0, bottom: 0, right: 0, zIndex: 1000, opacity: x // 设置透明度 }}> </Animated.View> .......... 定时修改变量x,并重新渲染,动画就行成了 真正的实现: 通过变量设置透明度与水平移动理 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849render(){ const {drawerWidth, drawerBackgroundColor} = this.props; const {openValue, drawerShown} = this.state; const dynamicDrawerStyles = { backgroundColor: drawerBackgroundColor, width: drawerWidth, left: 0, }; let drawerTranslateX = openValue.interpolate({ inputRange: [0, 1], outputRange: [-drawerWidth, 0], }); const animatedDrawerStyles = { transform: [{ translateX: drawerTranslateX}], }; const overlayOpacity = openValue.interpolate({ inputRange: [0, 1], outputRange: [0, 0.7], }); const animatedOverlayStyles = {opacity: overlayOpacity}; const pointerEvents = drawerShown ? "auto" : "none"; return ( <View style={{ flex: 1, backgroundColor: 'transparent'}}> <Animated.View style={styles.main}> {this.props.children} </Animated.View> <TouchableWithoutFeedback pointerEvents={pointerEvents} onPress={this._onOverlayClick}> <Animated.View pointerEvents={pointerEvents} style={[styles.overlay, animatedOverlayStyles]}> </Animated.View> </TouchableWithoutFeedback> <Animated.View style={[styles.drawer, dynamicDrawerStyles, animatedDrawerStyles]}> {this.props.renderNavigationView()} </Animated.View> </View> );} 定时修改变量 123456Animated.spring(this.state.openValue, { toValue: 1, bounciness: 0, // restSpeedThreshold: 0.1, useNativeDriver: true }).start(); 重点知识点: css3也有一个transform属性,但这个是ReactNative的transform属性,有区别,其分别对应的文档: ReactNative的transform css3的transform translateX属性的范围不是0–1,而实际抽屉的宽度 渐隐取值范围:[0–0.7],水平移动画的取值范围:[0–抽屉的宽广],变量openValue的取值范围:[0–1]。Animated.Value()的interpolate()方法进行转换,使其在同一个维度 特别注意:使用Animated.Value变量时,只能在Animated.View里使用,不能直接在View里使用,会出现各种想像不到的问题 遮罩层的事件处理这个比较简单,通过TouchableWithoutFeedback就可以实现 注意:overlay设置为全透明后,还是一样可以拦截或透传事件,通过View的pointerEvents属性配制事件传递 触发抽屉显示动画下面是真正的使用DrawerLayout的代码: 123456789101112render(){ const navigationView = React.createElement(NavigationScreen); // 抽屉View return ( <DrawerLayout drawerWidth={300} drawerBackgroundColor='#DAE8FC' renderNavigationView={()=>navigationView} ref={(drawer)=>{this.drawerLayout = drawer;}}> <HomeScreen navigate={this.navigate}/> // 首页 </DrawerLayout> );} 真正触发抽屉显示动画的是HomeScreen(首页),并不是DrawerLayout自已,所以需要使用React的ref属性,把DrawLayout的引用传递给其他View,才能调用其对外提供的Api。 学习到的技术点(记住)端口映射(USB连接) 在手机设备里通过http://localhost:port/访问pc上的服务时,使用如下命令: 12adb reverse (remote) (local)例子:adb -s 设备 reverse tcp:8081 tcp:8081 在pc上通过http://localhost:port/,访问手机设备上的服务时,使用如下命令: 12adb forward (local) (remote)例子:adb forward tcp:8081 tcp:8081 函数里的this的理解 this的指向在函数定义的时候是确定不了的,只有函数执行的时候才能确定this到底指向谁,实际上this的最终指向的是那个调用它的对象。更多信息 通过.bind()可以修改this的指向 箭头函数的this,是由定义时的上下文决定,而不是由运行时决定。 在ES6里,定义类时,其函数的写法有下面两种: 1234567891011121314class Person { constructor(){ this.name = "Li"; this.age = "18"; } getName(){ console.log("Person.name=" + this.name); } getAge = ()=>{ console.log("Person.age=" + this.age); }} getName()方法,在下面的场景下会执行有问题: 123const person = new Person(xxx);const tempGetName = person.getName;tempGetName(); // this为window 要解决这个问题,需要在构造函数里添加:this.getName = this.getName.bind(this) getName()与getAge()方法的其他不同点: getName()定义在原型上,getAge()定义在对象上,当类的对象很多时,比较占内存 123456789101112131415// getAge()方法相当于在构造函数里创建constructor(props){ super(props); this.name = "Li"; this.age = "18"; this.getAge = ()=>{ console.log("Person.age=" + this.age); }}// getName()相当于在原型上定义Person.prototype.getName = function(){ console.log("Person.age=" + this.age);} 箭头函数的继承的三种情况: 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748// 继承1class Student extends Person{ getAge = ()=>{ super.getAge(); console.log("Student.age"); }}new Student().getAge();// Uncaught TypeError: (intermediate value).getAge is not a function// 继承2class Student extends Person{ getAge(){ super.getAge(); console.log("Student.age"); }}new Student().getAge();// Person.age=18// 继承3class Student extends Person{ }new Student().getAge();// Person.age=18// 继承4class Student extends Person{ getName(){ super.getName(); console.log("Student.name"); }}new Student().getName();// Person.name=Li// Student.name// 继承5class Student extends Person{ getName=()=>{ super.getName(); console.log("Student.name"); }}new Student().getName();// Person.name=Li// Student.name 结论就是:箭头函数可以继承,但无法被重写 flexbox布局理解 Flex布局类似于Android里的LinearLayout布局,flexDirection,justifyContent,alignItems,alignSelf width,height的值尖似于dip,会依据手机的屏幕进行转换,PixelRatio更多信息 Flex的布局,默认是一层布局,通过position=absolute,与zindex可实现Android里的RelativeLayout效果。CSS position Property webpack的resolve.alias可以给import或require设置别名,利用此特性,可以把引入库修改掉,但同时又不用修改源码,更多信息 React的组件之间的交互方式默认情况下,props是父组件与子组件交互的唯一方式,父组件要修改子组件,通过新的props去重新渲染子组件。这种方案可以起到很好的解耦,但在少数情况下,无法满足需求,如抽屉的展开与收起动画。 这种情况下,可以使用Refs,比较适合使用refs的场景: 处理focus、文本选择或者媒体播放 触发强制动画 集成第三方DOM库 更多请参考 参考 BABEL在线转换工具","categories":[{"name":"前端","slug":"前端","permalink":"https://handsomeliuyang.github.io/categories/前端/"}],"tags":[{"name":"ReactNative","slug":"ReactNative","permalink":"https://handsomeliuyang.github.io/tags/ReactNative/"}]},{"title":"微信聊天数据定时清理","slug":"微信聊天数据定时清理","date":"2017-12-12T02:42:49.000Z","updated":"2017-12-14T06:48:24.000Z","comments":true,"path":"2017/12/12/微信聊天数据定时清理/","link":"","permalink":"https://handsomeliuyang.github.io/2017/12/12/微信聊天数据定时清理/","excerpt":"","text":"技术方案选择Android测试支持库有: Junit3, Junit4:用于方法级别的单元测试,不通过手机运行,在测试一些正则表达式时,非常方便 AndroidJUnitRunner:在手机上运行Junit测试,如一些需要获取Context的方法 Espresso:UI 测试框架;适合应用中的功能性 UI 测试。 UI Automator:UI 测试框架;适合跨系统和已安装应用的跨应用功能性 UI 测试 无障碍Api:可用于模拟用户点击,适合跨系统和已安装应用的跨应用功能性UI测试 选择结果:无障碍Api,因为UI Automator只能通过adb shell运行。 注意:Root后的手机,应该可以在App内直接执行UI Automator — 没有经过测试 实现步骤定时机制定时机制很容易,使用AlarmManager就行,如下所示:123456789101112131415161718//点击,设置重复闹钟。private void setRepeatingAlarm(){ Intent intent = new Intent(this, ClearWeixinActivity.class); intent.putExtra(\"msg\", \"重复的事情多次提醒!!!\"); intent.putExtra(\"type\", \"repeat\"); PendingIntent pendingIntent = PendingIntent.getActivity(this, 101, intent, 0); //假设当前时间15s之后,就开始第一次触发;然后每隔20s再次触发。 Calendar c = Calendar.getInstance(); c.set(Calendar.SECOND, c.get(Calendar.SECOND) + 60*60*1); AlarmManager alarmManager = (AlarmManager)getSystemService(ALARM_SERVICE); alarmManager.setRepeating(AlarmManager.RTC_WAKEUP, c.getTimeInMillis(), 1*60*60*1000, // 1个小时 pendingIntent);} 注意: 当进程被杀后,闹钟无法调起应用,需要开启自启动服务 自动开启无障碍模式由于无障碍模式的开启后,当应用程序进程被杀后,无障碍模式会被关掉,所以需要自动打开无障碍模式。 通过命令打开障碍模式的命令如下: 123456// 打开无障碍模式adb shell settings put secure enabled_accessibility_services com.ly.robottool/com.ly.robottool.weixin.ClearWeixinServiceadb shell settings put secure accessibility_enabled 1// 查看无障碍的配置情况adb shell content query --uri content://settings/secure App里,通过获取Root权限后,可执行以上命令,如下所示: 1234567891011121314151617try { Process p = Runtime.getRuntime().exec(\"su\"); DataOutputStream dos = new DataOutputStream(p.getOutputStream()); dos.writeBytes(\"settings put secure enabled_accessibility_services com.ly.robottool/com.ly.robottool.weixin.ClearWeixinService\\n\"); dos.writeBytes(\"settings put secure accessibility_enabled 1\\n\"); dos.writeBytes(\"mkdir /sdcard/333\\n\"); dos.writeBytes(\"exit\\n\"); dos.flush(); dos.close(); p.waitFor(); mHander.sendEmptyMessageDelayed(MESSAGE_ACCESSIBILITY_SUCCESS, 1000*1);} catch (IOException e) { e.printStackTrace();} catch (InterruptedException e) { e.printStackTrace();} 注意: 上面的代码,需要在子线程里执行 无障碍服务无障碍的整体机制 无障碍Api更加详细的文档,请查看Android开发无障碍指南 总结一些关键点: 默认情况下,只能看到TextView及其ParentView,基他ImageView等等都看不到,但通过设置flags |= FLAG_INCLUDE_NOT_IMPORTANT_VIEWS后,可以看到其他没有包含TextView的View 只能看到标准View,即自定View的父类,无法看到自定View的类名 只能获取View的Parent,children,Text,ClassName,屏幕坐标,大小,viewId,一些状态(checkable,checked,focusable,focused,selected,clickable,longClickable) 注意:微信由于使用了资源id混淆技术,不同版本的微信apk,其viewid会变化 UIAutomatorViewer查看IDuiautomatorviewer工具所在目录:Android SDK/tools/bin/uiautomatorviewer 与dumpsys比较: 结论:uiautomator,uiautomatorviewer,无障碍Api都只能看到TextView及其ParentView,但dumpsys可以看到全部View 微信自动清理聊天记录 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172public void onAccessibilityEvent(AccessibilityEvent event) { if (event == null) { return; } if (!WECHAT_PACKAGENAME.equals(event.getPackageName())) { return; } String beginUUID = SharedPreferenceUtils.getBeginUUID(this); String endUUID = SharedPreferenceUtils.getEndUUID(this); if(beginUUID == null) { return ; } if(!beginUUID.equals(endUUID)) { SharedPreferenceUtils.updateEndUUID(this, beginUUID); hasClickMe = false; hasClickSetting = false; hasClickChat = false; hasEnterClearDialog = false; hasClickClear = false; } log(\"0000:\" + event); if(!hasClickMe) { enterPerson(event); return ; } if(!hasClickSetting){ enterSetting(event); return ; } if(!hasClickChat){ enterChat(event); return ; } if(!hasEnterClearDialog){ enterClearDialog(event); return ; } if(!hasClickClear){ clickClear(event); return ; }}private void enterPerson(AccessibilityEvent event){ if(!\"com.tencent.mm.ui.LauncherUI\".equals(event.getClassName())){ return ; } if(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED == event.getEventType()){ // 查找当前窗口中包含“安装”文字的按钮 List<AccessibilityNodeInfo> nodes = getRootInActiveWindow().findAccessibilityNodeInfosByViewId(\"com.tencent.mm:id/c3f\"); AccessibilityNodeInfo myNode = null; for(AccessibilityNodeInfo node : nodes){ if(\"我\".equals(node.getText())) { myNode = node; } } if(myNode == null) { return ; } myNode.getParent().performAction(AccessibilityNodeInfo.ACTION_CLICK); hasClickMe = true; }} 参考 Android开发无障碍指南","categories":[{"name":"Android","slug":"Android","permalink":"https://handsomeliuyang.github.io/categories/Android/"}],"tags":[{"name":"自动化测试","slug":"自动化测试","permalink":"https://handsomeliuyang.github.io/tags/自动化测试/"}]},{"title":"异或总结","slug":"异或总结","date":"2017-08-18T07:28:51.000Z","updated":"2017-09-19T09:48:28.000Z","comments":true,"path":"2017/08/18/异或总结/","link":"","permalink":"https://handsomeliuyang.github.io/2017/08/18/异或总结/","excerpt":"","text":"异或(exclusive or)的定义符号: XOR 或 EOR 或 ⊕(编程语言中常用^) 定义:逻辑运算里,仅当两个运算元中恰有一个的值为真,而另外一个的值为非真时,其值为真 1 ⊕ 1 = 00 ⊕ 0 = 01 ⊕ 0 = 10 ⊕ 1 = 1 异或的特性 恒等律:X ⊕ 0 = X (X为任意整数)归零律:X ⊕ X = 0 (X为任意整数)交换律:A ⊕ B = B ⊕ A结合律:(A ⊕ B) ⊕ C = A ⊕ (B ⊕ C) 通过异或解决具体问题判断两个数是否相等技算机底层判断整数是否相等的方案:通过先将相应的位进行异或操作,然后将所有异或操作的结果进行或操作。因为执行异或操作没有进位,因此,这种方法比用ALU将两个数相减,然后再判断输出是否为0要快得多。 Linux中最初的ipv6_addr_equal()函数的实现:123456789101112131415static inline int ipv6_addr_equal(const struct in6_addr *a1, const struct in6_addr *a2){ return (a1->s6_addr32[0] == a2->s6_addr32[0] && a1->s6_addr32[1] == a2->s6_addr32[1] && a1->s6_addr32[2] == a2->s6_addr32[2] && a1->s6_addr32[3] == a2->s6_addr32[3]);}static inline int ipv6_addr_equal(const struct in6_addr *a1, const struct in6_addr *a2){ return (((a1->s6_addr32[0] ^ a2->s6_addr32[0]) | (a1->s6_addr32[1] ^ a2->s6_addr32[1]) | (a1->s6_addr32[2] ^ a2->s6_addr32[2]) | (a1->s6_addr32[3] ^ a2->s6_addr32[3])) == 0);} 数据校验利用异或的特性:IF a ^ b = c THEN a ^ c = b, b ^ c = a RAID5,大概原理为:使用3块磁盘(A、B、C)组成RAID5阵列,当用户写数据时,将数据分成两部分,分别写到磁盘A和磁盘B,A ^ B的结果写到磁盘C;当读取A的数据时,通过B ^ C可以对A的数据做校验,当A盘出错时,通过B ^ C也可以恢复A盘的数据。 bit位的一些操作判断一个二进制数中1的数量是奇数还是偶数如100010111中1的数量是奇数还是偶数? 解答:1^0^0^0^1^0^1^1^1 = 1 特定位进行翻转利用异或的特性:1^1=0,0^1=1。 如翻转100010111里的第5位? 解答:100010111 ^ 000010000 = 100000111 不使用其他空间,交换两个值1234int a,b;a = a ^ b;b = a ^ b; // a^b^b=aa = a ^ b; // a^b^a=b 一个整型数组里除了1个数字之外,其他的数字都出现两次,请查找出其中从一组数据中找出只出现一次的数字比如,从[3, 2, 3, 2, 4, 5, 5, 6, 6]中找出只出现一次的数字:4 利用异或的三个定律:归零律,交换律,结合律。 1233 ^ 2 ^ 3 ^ 2 ^ 4 ^ 5 ^ 5 ^ 6 ^ 6= 3 ^ 3 ^ 2 ^ 2 ^ 5 ^ 5 ^ 6 ^ 6 ^ 4= 4 一个整型数组里除了2个数字之外,其他的数字都出现两次,请查找出其中从一组数据中找出只出现一次的数字比如,从[a, b, a, b, c, d, e, f, e, f]中找出只出现一次的数字:c, d 思路: 整体异或的结果为c与d的异或值:cXORd,因c != d,则 cXORd != 0; 利用cXORd的第一位值为1(比如从右向左第一位),来区分c与d,如下图所示: 从数组里,找到所有第二位都为1的数字,假设有:[ a, a, c, e, f, e, f],再对这些数进行异或:a ^ a ^ c ^ e ^ f ^ e ^ f = c 再利用异或的特性:cXORd ^ c = d 代码1234567891011121314151617181920212223242526public static int getFirstOneBit(int a){ return a | (~a + 1); // 由a | -a即可以获取}public static void findTwo(int[] array){ if(array == null || array.length == 0) { return ; } int cXORd = 0; for(int item : array){ cXORd = cXORd ^ item; } int firstOneBit = getFirstOneBit(cXORd); int c = 0; for(int item : array){ if(getFirstOneBit(item) == firstOneBit){ c = c ^ item; } } int d = cXORd ^ c; System.out.printf(\"findTwo num is %d, %d\\n\", c, d);} 时间复杂度为O(n),空间复杂度O(1) 一个整型数组里除了3个数字之外,其他的数字都出现两次,请查找出其中从一组数据中找出只出现一次的数字比如,从[a, b, a, b, c, d, e, f, f]中找出只出现一次的数字:c, d,e 思路: 整体异或的结果为c,d,e的异或值:cXORdXORe。同时(cXORdXORe ^ c) ^ (cXORdXORe ^ d) ^ (cXORdXORe ^ e) = 0 IF A ^ B ^ C = 0, 则可以得出如下结论: 把查找3个数字,转换为2个数字的问题 代码12345678910111213141516171819202122232425262728293031public static void findThree(int[] array){ if(array == null || array.length == 0){ return ; } int cXORdXORe = 0; for(int item : array){ cXORdXORe = cXORdXORe ^ item; } int firstBit = 0; for(int item : array){ firstBit = firstBit ^ getFirstOneBit(cXORdXORe ^ item); } int c = 0; for(int item : array){ if(getFirstOneBit(cXORdXORe ^ item) == firstBit){ c = c ^ item; } } System.out.printf(\"findThree num is %d \", c); int[] findtwoArray = new int[array.length + 1]; for(int i=0; i<array.length; i++){ findtwoArray[i] = array[i]; } findtwoArray[findtwoArray.length - 1] = c; findTwo(findtwoArray);} 时间复杂度O(n),空间复杂度O(1) 参考 感受异或的神奇","categories":[{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/categories/算法/"}],"tags":[{"name":"算法","slug":"算法","permalink":"https://handsomeliuyang.github.io/tags/算法/"}]},{"title":"Docker-Jenkins服务搭建","slug":"Docker-Jenkins服务搭建","date":"2017-07-14T05:28:44.000Z","updated":"2017-07-25T02:55:48.000Z","comments":true,"path":"2017/07/14/Docker-Jenkins服务搭建/","link":"","permalink":"https://handsomeliuyang.github.io/2017/07/14/Docker-Jenkins服务搭建/","excerpt":"","text":"Docker介绍Docker 是一个开源的应用容器引擎,基于 Go 语言 并遵从Apache2.0协议开源。可以让开发者打包他们的应用以及依赖包到一个轻量级、可移植的容器中,然后发布到任何流行的 Linux 机器上,也可以实现虚拟化。容器是完全使用沙箱机制,相互之间不会有任何接口(类似 iPhone 的 app),更重要的是容器性能开销极低。 Docker理解 Dockerfile面向开发,Docker 镜像成为交付标准,Docker 容器则涉及部署与运维 Docker类似于一个虚拟机,实现资源和系统环境的隔离 Docker镜像类似于Ghost系统,为了方便快速使用,已经完成了服务的所有配置。 DockerFile是一个脚本,用于生成Docker镜像的脚本 Docker镜像不仅可以从DockerFile生成,也可以从Docker容器生成,但最好是通过DockerFile来生成,方便后期维护。 我们创建新的镜像都是从系统镜像开始创建的,如centos:7,centos:6 通过Docker部署Jenkins的好处Docker解决现在的迁移服务(如Android的Jenkins)存在的问题: 服务器的系统版本不一致,容易出现新问题,如缺少一些库,或软件版本过底 Docker:容器里的操作系统版本与主机的系统版本没有关系,不受主机的系统版本影响 多个服务部署在同一台机器上,关联的软件出现相互影响 Docker:每个容器之间相互不影响,完全透明,类似虚拟机 需要写服务部署文档,软件版本之间的关系,但新系统有可能不支持这些老版本的软件 Docker:DockerFile就是整个部署文档,安装的软件与主机的系统没有关系 服务卸载的成本很高,很容易出现卸载不完全的问题 Docker:只需要删除容器,其安装的软件都可以清除 服务升级很不方便,需要一台新机器或搭建虚拟机来实现 Docker:容器升级操作系统版本非常简单,成本非常低,修改From的关联版本就行 本地文件管理比较乱,容易相互影响 Docker:容器之间的文件相互不影响 Docker入门介绍Docker安装在centos上安装Docker的注意点: 最低支持centos7.0系统才能安装docker centos6.5以上也可以安装,但安装方法与centos7.0以上的安装方法不一样 具体教程可以网上查找 常用命令DockerFile生成Docker镜像1234docker build -t 镜像名称 DockerFile所在的目录// 例子docker build -t btown-jenkins . 从Docker镜像创建Docker容器1234docker run [-d|-it] -p 主机端口:容器里的端口 -v 主机目录:容器里的目录 -v 主机目录:容器里的目录 镜像名称 bash// 例子docker run -it --name btown-jenkins -p 7000:8080 -v /data0/btown_jenkins_home/jobs:/var/lib/jenkins/jobs -v /data0/btown_jenkins_home/logs:/var/lib/jenkins/logs -v /data0/btown_jenkins_home/nodes:/var/lib/jenkins/nodes -v /data0/btown_jenkins_home/secrets:/var/lib/jenkins/secrets -v /data0/btown_jenkins_home/users:/var/lib/jenkins/users -v /data0/btown_jenkins_home/workspace:/var/lib/jenkins/workspace btown-jenkins bash -d:此容器在后台运行 -it:当前控制台与容器交互 –name:创建的容器的名称 -p:端口映射,把主机的端口映射到容器里的端口 -v:目录映射,把容器里的目录映射到主机里的目录 bash:进入容器后的命令,bash表示直接进入shell状态 数据备份默认情况下,容器运行期间产生的文件,都处于沙箱当中,当容器删除后,也会自动删除,这会造成一些问题: 服务生成的数据很不方便备份 容器挂了后,就无法恢复数据了 容器会变的非常的大 无法共享容器间的数据 Docker 容器文件系统 Dockerfile 中的每一条命令,都在 Docker 镜像中以一个独立镜像层的形式存在 Docker 镜像是由 Dockerfile 构建而成,但并不是每一层 Docker 镜像中都含有相应的文件系统文件 Docker 容器的文件系统中不仅包含 Docker 镜像,还包含初始层(Init Layer)与可读写层(Read-Write Layer)。 初始化层(Init Layer):初始层中大多是初始化容器环境时,与容器相关的环境信息,如容器主机名,主机 host 信息以及域名服务文件等。 可读写层(Read-Write Layer):这一层的作用非常大,Docker 的镜像层以及顶上的两层加起来,Docker 容器内的进程只对可读写层拥有写权限,其他层对进程而言都是只读的(Read-Only) Docker 容器有能力在可读写层看到VOLUME文件等内容,但那都仅仅是挂载点,真实内容位于宿主机上 Volume 命令为了能够保存(持久化)数据以及共享容器间的数据,Docker提出了Volume的概念。简单来说,Volume就是目录或者文件,它可以绕过默认的联合文件系统,而以正常的文件或者目录的形式存在于宿主机上。 有两种方式初始化Volume: 不指定主机上的目录 1docker run -it --name btown-jenkins -v /data btown-jenkins bash 此命令会将/data挂载到容器中,并绕过联合文件系统,我们可以在主机上直接操作该目录,通过docker inspect命令找到Volume在主机上的存储位置: 1docker inspect -f {{.Volumes}} btown-jenkins 类似的输出为: 1map[/data:/var/lib/docker/vfs/dir/cde167197ccc3e138a14f1a4f...b32cec92e79059437a9] 指定主机上的目录: 1docker run -it --name btown-jenkins -v /home/data:/data btown-jenkins bash 命令将挂载主机的/home/data目录到容器内的/data目录上 通过Volume挂载关键数据目录后,就可以解决上面出现的问题了 其他常用命令 查看所有镜像:docker images 删除镜像:docker rmi xxx 查看所有容器:docker ps -a 删除容器:docker rm xxx 退出容器:exit,CTRL+D 重新连接容器: docker attach xxx docker exec -it xxx bash 差别:使用docker exec连接容器后,现执行exit退出容器,容器不会停止 启动|停止容器:docker start|stop DockerFile脚本语言Dockerfile 是一个类似 Makefile 的工具,主要用来自动化构建镜像。 先看一个例子:123456789101112131415161718192021222324252627# 系统版本 由于需要glibc-2.14版本以上,所以要使用centos:7FROM centos:7.3.1611# 作者信息MAINTAINER liuyang@58ganji.com# 安装基础库RUN yum -y updateRUN yum -y install wget# 安装 Oracle Java 7 JDK,安装成功的目录:/usr/java/jdk1.7RUN mkdir -p /data0/softADD ./jdk-7u80-linux-x64.rpm /data0/soft/jdk-7u80-linux-x64.rpmRUN rpm -ivh /data0/soft/jdk-7u80-linux-x64.rpm# 设置jdk的环境变量ENV JAVA_HOME /usr/java/jdk1.7.0_80ENV PATH $PATH:$JAVA_HOME/jre/bin:$JAVA_HOME/bin# 复制ssh keyCOPY ./ssh.tar /data0/soft/ssh.tarRUN cd /data0/soft && tar xvf ssh.tarRUN cp -r -f /data0/soft/.ssh /var/lib/jenkins/RUN chmod -R 777 /var/lib/jenkins/.sshEXPOSE 8080ENTRYPOINT service jenkins start 格式Dockerfile 中所有的命令都是以下格式:INSTRUCTION argument 指令(INSTRUCTION)不分大小写,但是推荐大写。 FROM 命令FROM <image name>,例如 FROM ubuntu 所有的 Dockerfile 都用该以 FROM 开头,FROM 命令指明 Dockerfile 所创建的镜像文件以什么镜像为基础,FROM 以后的所有指令都会在 FROM 的基础上进行创建镜像;可以在同一个 Dockerfile 中多次使用 FROM 命令用于创建多个镜像。 MAINTAINER 命令MAINTAINER <author name> 用于指定镜像创建者和联系方式。 RUN 命令RUN <command> 用于容器内部执行命令。每个 RUN 命令相当于在原有的镜像基础上添加了一个改动层,原有的镜像不会有变化。 ADD 命令ADD <src> <dst> 用于从将 <src> 文件复制到 <dst>文件:<src> 是相对被构建的源目录的相对路径,可以是文件或目录的路径,也可以是一个远程的文件 url,<dst> 是容器中的绝对路径。 注意:如果源文件是压缩文件(如.tar,.zip等等),会自动解压,如果不想自动解压,可以使用copy命令 COPY指令COPY指令和ADD指令功能和使用方式类似。只是COPY指令不会做自动解压工作。 ENV 命令设置环境变量,参考 export 的用法咧:ENV LC_ALL en_US.UTF-8 EXPOSE 命令EXPOSE <port> [<port>…] 命令用来指定对外开放的端口。 1234EXPOSE 80EXPOSE 8080# 不推荐这样写,会固定死映射端口,最好通过创建容器时来指定EXPOSE 8000:8080 注意:除EXPOSE 8000:8080是提前指定了映射端口外,其他的相当于一个声明而已,具体端口映射还是在创建容器时指定的。 ENTRYPOINT 命令ENTRYPOINT command param1 param2 用来指定启动容器时,执行的命令 生成DockerFile的流程由于每个重新执行一次DockerFile文件的时间很长,所以写DockerFile的最佳方案: 创建一个最初的容器,再执行成功一个命令后,就添加到DockerFile文件里,等全部OK后,DockerFile也就创建完了 再整体执行DockerFile文件,查看创建镜像是否成功 Docker的容器的性能具体内容请查看:docker与虚拟机性能比较 docker比虚拟机的优势: docker有着比虚拟机更少的抽象层 docker利用的是宿主机的内核,而不需要Guest OS docker计算效率与主机一样,没有损耗,但虚拟机的计算能力损耗在50%左右 docker与虚拟机内存访问效率要高 docker与虚拟机启动时间及资源耗费要高 docker的劣势: 资源隔离方面不如虚拟机,docker是利用cgroup实现资源限制的,只能限制资源消耗的最大值,而不能隔绝其他程序占用自己的资源 安全性问题。docker目前并不能分辨具体执行指令的用户,只要一个用户拥有执行docker的权限,那么他就可以对docker的容器进行所有操作,不管该容器是否是由该用户创建。比如A和B都拥有执行docker的权限,由于docker的server端并不会具体判断docker cline是由哪个用户发起的,A可以删除B创建的容器,存在一定的安全风险。 docker目前还在版本的快速更新中,细节功能调整比较大。一些核心模块依赖于高版本内核,存在版本兼容问题 参考 Docker 教程 一图看尽 docker 容器文件系统 docker与虚拟机性能比较","categories":[{"name":"Server","slug":"Server","permalink":"https://handsomeliuyang.github.io/categories/Server/"}],"tags":[{"name":"jenkins","slug":"jenkins","permalink":"https://handsomeliuyang.github.io/tags/jenkins/"},{"name":"docker","slug":"docker","permalink":"https://handsomeliuyang.github.io/tags/docker/"}]},{"title":"如何设计高保真原型图","slug":"Axure设计原型图","date":"2017-03-14T08:00:00.000Z","updated":"2017-03-20T05:27:20.000Z","comments":true,"path":"2017/03/14/Axure设计原型图/","link":"","permalink":"https://handsomeliuyang.github.io/2017/03/14/Axure设计原型图/","excerpt":"","text":"背景在设计“专项测试平台项目”的需求文档时,按传统的word设计需求文档带来的问题: 文档不够详细,没有交互,没有各种出错的处理情况 没有一个平台的功能流程全貌,无法提前给上级审核 需求评审的效率很低 通过分析,发现主要原因是出在需求文档上,现在的静态word文档,能表达的信息很有限,那有没有更好的方式。这就是今天要分享的:高保真原型图。 注意:实现高保真原型图的软件有很多,完全不限于Axure 高保真原型图Axure意义我们可以通过房屋装修来理解,在房屋真正开始装修之前,我们要做很多的工作,如看建材,看家电,看家具,看设计师做的效果图。这些都是为了在开始真正装修之前,尽最大可能去了解最终效果是什么样子,是不是我们想要的,因为一旦开始装修,就很难去更改。 这时如果能把想要装修的房子变成一个真实的样板间,我们看提前看到所有的效果,家电,家具等等,而且还能进行体验,提出改进意见,满意后,才真正开始装修。 高保真原型图Axure就是制作这个真实样板间的工具。总结几点好处: 实现功能与视觉上的统一,通过实际演示,减少口头沟通 领导与用户可以提前体验最终效果,可以提前收到他们的反馈意见 开发者终于可以了解你想要什么。以前用文档与图片无法解释清楚的需求,现在可以很容易的让开发者理解 Axure版本 使用的是AxureRP Pro 7.0版本,同时下载注册机 下载对应版本的汉化包 Axure的主要功能线框图+设计仅仅使用方块,占位符,形状和文本设计的,称之为线框图,如下图所示: 对线框图进行视觉美化设计后,称之为高保真原型图,如下图所示: 线框图与高保真原型图的差别,仅仅是真实程序不一样而已 母版母版可以理解为PPT里的母版,是可以复用部分。设计网站时,网站的导航栏,Footer等等都是通用的组件,每个页面都有,这时可以创建一个母版,实现复用,如下所示: 元件库原生自带的元件库只适合做线框图,要做手机app的原型图设计的话,工作量会比较大,这时可以导入其他的元件库,如下导入了ios8组合元件库: 动态面板 上面的功能模块,在原型图里实现,就可以使用动态面板,动态面板是指在同一个区域里,有多种展示形态: 控件连线通过如下可以把两个控件进行连接: 交互给控件添加交互,就是给此控件添加:事件(event),用例(case),动作(action)。 事件,如鼠标点击事件,鼠标移入时,鼠标移出时等等 用例,发生事件后,做什么样的业务逻辑判断 动作,即这个用例下,执行什么下的动作 如下创建的交互: 事件的种类: 用例与其条件: 常用的动作有: 打开新的链接 动态面板切换 设置显示/隐藏 浏览器展示直接可以在预览,但为了不出现显示问题,建议使用chrome浏览器进行预览 文档每个控件可以添加文档 当前页面也可以添加文档 合作 通过AxShare可以实现合作编辑 通过版本管理来保存原文件,进行合作编辑 工具对比 Axure PR 学习成本也非常高 专业的原型设计工具 可以实现很复杂的交互 官网 Mockplus 有免费版本,简洁高效,关注设计,而非工具 官网 更多请点击 谁来制作高保真原型产品经理与设计师是高保真原型的制作者。(技术做支持) 产品经理负责收集各方面的需求,在平衡各种资源后,确定最值得开发的产品的功能 设计师按照这个功能的规划制作视觉体现,然后产品经理和设计师一起,将功能点、设计图和交互流程一起合并为高保真原型。 参考 Axure RP高保真网页原型制作","categories":[{"name":"设计","slug":"设计","permalink":"https://handsomeliuyang.github.io/categories/设计/"}],"tags":[{"name":"axure","slug":"axure","permalink":"https://handsomeliuyang.github.io/tags/axure/"}]},{"title":"前端学习系列2:从移动端的角度学习与分析Redux","slug":"前端学习系列:从移动端的角度学习与分析Redux","date":"2017-01-15T08:00:00.000Z","updated":"2017-02-20T12:24:12.000Z","comments":true,"path":"2017/01/15/前端学习系列:从移动端的角度学习与分析Redux/","link":"","permalink":"https://handsomeliuyang.github.io/2017/01/15/前端学习系列:从移动端的角度学习与分析Redux/","excerpt":"","text":"遇到的问题通过上往篇文章前端学习系列:基于React的Robot框架的踩坑之旅介绍,已经搭建起React的整体环境,但在进行具体业务开发时,还是遇到如下问题: 所有界面操作,如网络请求,点击事件等等都在同一个组件里完成,出现组件过于庞大的问题 子组件与父组件只能通过回调方法进行通信,没有一个消息总线机制(如:子组件想决定框架Master里的title,除了通过回调方法,尽没有找到其他方法) 无法进行单元测试,基本只能整体测试,无法对其的界面与数据进行分别测试 关键日志无法添加,就像Android开发里也一样,关键日志只能通过手动去添加 Redux动机通过调研,最终选择Redux。Redux主要用来解决如下问题: 界面的变化在React理解为state的变化,当功能复杂时,state的变化非常之多,state在什么时候,由于什么原因,如何变化已然不受控制。这时我们遇到问题,进行重现也变的非常困难。—- 做Android开发,也有同感,当QA遇到一个比较难复现的bug时,RD就很难定位问题。 React开发时,state的变化与异步是混在一起的(异步可以理解为业务逻辑,如用户交互,网络请求,具体业务功能等等)。只有把state的变化与异步进行分离,才能很好的进行管理。—- 客户端开发,为什么会有MVC,MVVM,MVP等等框架,其实也是同样的出发点,尽量把展示与数据进行分离 运行todos例子 下载redux的原码。github的地址 安装node,注意node的版本号一定要>4.0。因为0.x版本不支持es6等等高级语法。或者进行node版本升级,node升级教程 进入todos的目录,执行以下命令,安装dependencies 1node install 运行 1node start Redux实现 注意:项目中的代码都是以redux的官方例子todos,为基础进行改造的 Redux里的state其实就是一个对象,或者可以理解为一个json数据,如下所示: 12345678910111213{ todos: [ { text: 'Eat food', completed: true }, { text: 'Exercise', completed: false } ], visibilityFilter: 'SHOW_COMPLETED'} Redux的核心思想很容易理解,只有三大概率:Action,Reducer,Store。其关系如下: React组件通过Store监听state的变化 调用方通过调用Store的dispatch()方法发送动作action Store通过Reducer把action对象转换为state对象 Store更新内部保存的state对象,并广播监听者进行界面刷新与变化 具体代码如下: 12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788import React, {Component} from 'react'import { render } from 'react-dom'import { createStore } from 'redux'// reducers,用来把action转换为stateconst reducers = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return { todos: [ ...state, { text: action.text } ] } default: return state }}// 创建store,全局只有一个store单例const store = createStore(reducers)class App extends Component { constructor(props) { super(props); // 当前组件的state的默认值 this.state = {todos:[]}; // 对state进行订阅 store.subscribe(()=>{ let reduxState = store.getState(); console.log(\"redux's state\", reduxState); let currentTodos = store.getState().todos; if (this.state.todos !== currentTodos) { this.setState({todos:currentTodos}); } }); } addTodo = (text) => ({ type: 'ADD_TODO', text }); render(){ let input; const { todos } = this.state; return ( <div> <div> <form onSubmit={e => { e.preventDefault() if (!input.value.trim()) { return } // 发送action store.dispatch(this.addTodo(input.value)) input.value = '' }}> <input ref={node => { input = node }} /> <button type=\"submit\"> Add Todo </button> </form> </div> <ul> {todos.map(todo => <li key={todo.text}> {todo.text} </li> )} </ul> </div> ); }}render( <App />, document.getElementById('root')) 运行效果如下: Redux扩展reducer的拆分reducer的作用是把action转换为state,当app变大后,需要对reducer进行拆分。 redux的整体特点: Store里保持的state是整体程序app的所有状态 每个action只是处理某一种行为 reducer的最简单的拆分方式,就是按不同的type类弄进行拆分 reducer拆分后的代码如下: 1234567891011121314151617181920212223242526const reducer_todos = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [ ...state, { text: action.text } ] default: return state }}const initialState = { todos:[]}const reducer_root = (state = initialState, action) => { return Object.assign({}, state, { todos: reducer_todos(state.todos, action) });}// 创建store,全局只有一个store单例const store = createStore(reducer_root) Object.assign()用于拷贝两个对象的值,state对象是不能被修改。不然很容易出现不可预测的异常 reducer_root处理好整体拆分后,每个子reducer就只需要处理自己的数据转换。其他的数据自动继承 问题:由于所有界面的state都直接保存在内存里,当某界面离开后,其数据还是会一直保留在Store当中?处理方案:如一些页面的数据比较多,同时是不常用的界面,可以在退出此页面时,发送一个action,对数据进行清除。 combineReducers(reducers)上面的reducer的拆分方式都是一样的代码,可以提取api对外提供。其实现原理与上面类似,修改后的代码如下: 1234567891011121314151617181920const todos = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [ ...state, { text: action.text } ] default: return state }}const reducer_root = combineReducers({ todos});// 创建store,全局只有一个store单例const store = createStore(reducer_root) combineReducers()方法处理了三个功能: 生成state的初始值 通过方法名,自动生成key 自动传入state, action参数,并调用方法,重新生成state 调用与监听优化上面的redux的使用方法有如下问题: 会对使用者暴露store对象,store只能在一处初始化,需要传入到每个子组件,如果都是通过组件的props来传递,这个就很麻烦,尤其是当子view及层级比较多的时候 业务方每次都要进行监听,并进行数据转换,把redux的state转换为React的state对象,转换过程,要考虑一些性能问题,由于只要有一个子数据变化,所有监听者都会被触发通知,为了减少无用界面刷新,要做一些特殊处理。 解决方案:进行封装 store的传递封装,React里给组件传递对象,除了使用props属性外,还提供了一个全局传递方案:Context。具体请查看 包装一个容器组件,里面封装监听redux,并进行数据转换的工作 改进后的流程图: react-redux在学习react-redux时,需要先了解一下容器组件与展示组件的概念。 技术上讲你可以直接使用 store.subscribe() 来编写容器组件。但不建议这么做因为就无法使用 React Redux 带来的性能优化。也因此,不要手写容器组件,都是使用 React Redux 的 connect() 方法来生成。 react-redux的作用就是上面的解决方案的具体实现,我们看一下使用了react-redux的代码 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109import React, {Component} from 'react'import { render } from 'react-dom'import { createStore, combineReducers } from 'redux'import { Provider, connect } from 'react-redux'// import App from './components/App'// import reducer from './reducers'// reducers,用来把action转换为stateconst todos = (state = [], action) => { switch (action.type) { case 'ADD_TODO': return [ ...state, { text: action.text } ] default: return state }}const reducer_root = combineReducers({ todos});// 创建store,全局只有一个store单例const store = createStore(reducer_root)// actionsconst addTodo = (text) => ({ type: 'ADD_TODO', text});class App extends Component { constructor(props) { super(props); // 当前组件的state的默认值 // this.state = {todos:[]}; // 对state进行订阅 // store.subscribe(()=>{ // let reduxState = store.getState(); // console.log(\"redux's state\", reduxState); // let currentTodos = store.getState().todos; // if (this.state.todos !== currentTodos) { // this.setState({todos:currentTodos}); // } // }); } render(){ // React的界面布局 let input; const { todos, addTodo } = this.props;//this.state; return ( <div> <div> <form onSubmit={e => { e.preventDefault() if (!input.value.trim()) { return } // 发送action // store.dispatch(this.addTodo(input.value)) addTodo(input.value); input.value = '' }}> <input ref={node => { input = node }} /> <button type=\"submit\"> Add Todo </button> </form> </div> <ul> {todos.map(todo => <li key={todo.text}> {todo.text} </li> )} </ul> </div> ); }}const mapStateToProps = (state, ownProps) => { console.log('state value is ', state); return { todos: state.todos };};App = connect(mapStateToProps, { addTodo})(App);render( <Provider store={store}> <App /> </Provider>, document.getElementById('root')) 要点: react-redux通过提供Provider与connect()方法实现接入层的解耦 Provider的作用比较简单,用来实现全局的store对象的传递,里面的实现原理就是通过React的Context来实现的,如下面就是Provider的原码 connect()比较复杂,其主要是两件事情: 把redux的state转换为React组件的props属性 把action方法进行dispatch包装,再通过React组件的props属性传给React组件 1234567891011121314151617181920212223242526272829303132333435class Connect extends Component { initSubscription() { if (shouldHandleStateChanges) { const subscription = this.subscription = new Subscription(this.store, this.parentSub) const dummyState = {} subscription.onStateChange = function onStateChange() { this.selector.run(this.props) if (!this.selector.shouldComponentUpdate) { subscription.notifyNestedSubs() } else { this.componentDidUpdate = function componentDidUpdate() { this.componentDidUpdate = undefined subscription.notifyNestedSubs() } this.setState(dummyState) } }.bind(this) } } render() { const selector = this.selector selector.shouldComponentUpdate = false if (selector.error) { throw selector.error } else { return createElement(WrappedComponent, this.addExtraProps(selector.props)) } }} 把redux的state转换为react的props,比转换为react的state属性的好处: 对于React组件而言,外界有仅只能通过组件的props对此组件进行控制,内部的state不对外进行暴露,一切都是通过外界传入的props参数来进行控制,真正实现业务与界面分离解耦 组件可以做到与Redux解耦,组件能快速脱离Redux,并为一个共用组件 Middleware中间件 中间件:提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。那么利用中间件,我们就可以做很多的事情,如: 网络请求 实现异步(此异步与android里的线程有差异) 日志记录 。。。 中间件的特点: action的发送是顺序发送的,即第一个中间件处理后,才会传给第二个中间件处理。 中间件的注册是有顺序的 中间件可以决定是否再传递action 通过学习:中间件实现的具体演化过程可以了解javascript的一些语言特点:Monkey-Patching。相当于Java里的hook,对对象里方法进行proxy。但具体原因不一样: javascript的Monkey-Patching是利用javascript的函数也是一个变量,可以被修改特点来实现的 java里的hook,是通过子类可以重写父类的方法来实现的。 Redux在实际项目中的好处我在Robot项目里,使用了Redux,我的体验有: 界面与业务分离,写界面时,不用思考业务流程是如何的,只要思考界面与数据的关系。 网络请求代码,业务逻辑与界面解耦 调试问题非常方便,不像之前需要大量添加console.log()。一般可以通过action,state的日志就可以分析出原因,是由后台数据问题,还是界面问题 可实现录制与回放—-此好处还没有实现 参考 Redux 中文文档 Redux redux例子:real-world","categories":[{"name":"前端","slug":"前端","permalink":"https://handsomeliuyang.github.io/categories/前端/"}],"tags":[{"name":"React","slug":"React","permalink":"https://handsomeliuyang.github.io/tags/React/"},{"name":"Robot","slug":"Robot","permalink":"https://handsomeliuyang.github.io/tags/Robot/"},{"name":"nodejs","slug":"nodejs","permalink":"https://handsomeliuyang.github.io/tags/nodejs/"}]},{"title":"前端学习系列:基于React的Robot框架的踩坑之旅","slug":"Robot框架-Nodejs+Express+React+MaterialUI","date":"2016-12-30T05:00:00.000Z","updated":"2017-01-05T02:45:37.000Z","comments":true,"path":"2016/12/30/Robot框架-Nodejs+Express+React+MaterialUI/","link":"","permalink":"https://handsomeliuyang.github.io/2016/12/30/Robot框架-Nodejs+Express+React+MaterialUI/","excerpt":"","text":"背景上次给大家介绍Robot平台框架,其特点: 由nodejs+express+react+bootstrap实现 UI使用开源UI库:charisma React通过browserify+babel打包处理 效果如下: 其中遇到的一些问题: 前端界面框架没有真正的React化,只使用很少一部分,html页面里,还有大量的js引用配置,css引用配置 123456789101112131415161718192021222324252627282930313233343536373839// html引用css部分<link href=\"../../other_js_lib/charisma/css/charisma-app.css\" rel=\"stylesheet\"><link href='../../other_js_lib/charisma/bower_components/fullcalendar/dist/fullcalendar.css' rel='stylesheet'><link href='../../other_js_lib/charisma/bower_components/fullcalendar/dist/fullcalendar.print.css' rel='stylesheet' media='print'><link href='../../other_js_lib/charisma/bower_components/chosen/chosen.min.css' rel='stylesheet'><link href='../../other_js_lib/charisma/bower_components/colorbox/example3/colorbox.css' rel='stylesheet'><link href='../../other_js_lib/charisma/bower_components/responsive-tables/responsive-tables.css' rel='stylesheet'><link href='../../other_js_lib/charisma/bower_components/bootstrap-tour/build/css/bootstrap-tour.min.css' rel='stylesheet'><link href='../../other_js_lib/charisma/css/jquery.noty.css' rel='stylesheet'><link href='../../other_js_lib/charisma/css/noty_theme_default.css' rel='stylesheet'><link href='../../other_js_lib/charisma/css/elfinder.min.css' rel='stylesheet'><link href='../../other_js_lib/charisma/css/elfinder.theme.css' rel='stylesheet'><link href='../../other_js_lib/charisma/css/jquery.iphone.toggle.css' rel='stylesheet'><link href='../../other_js_lib/charisma/css/uploadify.css' rel='stylesheet'><link href='../../other_js_lib/charisma/css/animate.min.css' rel='stylesheet'>// html中大量引用js的部分<!-- select or dropdown enhancer --><script src=\"../../other_js_lib/charisma/bower_components/chosen/chosen.jquery.min.js\"></script><!-- plugin for gallery image view --><script src=\"../../other_js_lib/charisma/bower_components/colorbox/jquery.colorbox-min.js\"></script><!-- notification plugin --><script src=\"../../other_js_lib/charisma/js/jquery.noty.js\"></script><!-- library for making tables responsive --><script src=\"../../other_js_lib/charisma/bower_components/responsive-tables/responsive-tables.js\"></script><!-- tour plugin --><script src=\"../../other_js_lib/charisma/bower_components/bootstrap-tour/build/js/bootstrap-tour.min.js\"></script><!-- star rating plugin --><script src=\"../../other_js_lib/charisma/js/jquery.raty.min.js\"></script><!-- for iOS style toggle switch --><script src=\"../../other_js_lib/charisma/js/jquery.iphone.toggle.js\"></script><!-- autogrowing textarea plugin --><script src=\"../../other_js_lib/charisma/js/jquery.autogrow-textarea.js\"></script><!-- multiple file upload plugin --><script src=\"../../other_js_lib/charisma/js/jquery.uploadify-3.1.min.js\"></script><!-- history.js for cross-browser state change on ajax --><script src=\"../../other_js_lib/charisma/js/jquery.history.js\"></script><!-- application script for Charisma demo --><script src=\"../../other_js_lib/charisma/js/charisma.js\"></script> html过多,每个一个界面就会有一个html页面 React界面大的方向使用的是Component开发模式,但每个组件内,还是大最使用最原生的方式开发,下面是其中一个组件的render()方法内部代码: 界面很不好,由于css与js逻辑代码分离,在没有缓存时,经常出现先看到没有样式的界面,再看到整体界面,整体视觉效果很不好 使用的是browserify的express的中间件:browserify-middleware,虽然能解决开发期间每次执行手动执行转换的功能,但问题是修改界面后,每次都手动需新才行。 没有适配移动端,在手机版本上的体验很差 还有很多其他的,都是由上面的问题衍生出来的 调研为了解决上面的问题,花了几天时间进行调研,主要的调研点: browserify是否可以对css进行模块化支持? 结论:webpack更加合适 webpack的使用,有没有类似browserify-middleware功能? 结论:webpack-dev-server 双服务器配置:nodejs+express与webpack-dev-server的理解与如何工作? 结论:思维需要变化,下面会具体介绍 React的UI库:Material-UI的使用? 结论:官网demo例子只有基本组件的使用,学了后,还是无法创造出想要的效果 Robot最新框架技术集 后端: Nodejs nodemon Express 前端: React react-router Material-UI react-tap-event-plugin 打包工具: webpack style-loader babel-loader webpack-dev-server babel babel-preset-es2015 babel-preset-react babel-preset-stage-1 框架目录结构 框架界面 具体技术点后端Server 使用nodejs+express创建后台服务。网上很多教程 关键点: server端的日志输出,把所有请求都通过日志输出 1234// 通过使用给express里添加morgan,就可以实现var logger = require('morgan');var app = express();app.use(logger('dev')); 实现Server的404异常,利用express的中间件机制原理,实现404找不到页面异常 123456789101112131415161718192021222324252627// 工具中间件app.use(logger('dev'));app.use(bodyParser.json());app.use(bodyParser.urlencoded({ extended: false }));app.use(cookieParser());// 业务中间件app.use('/', index);app.use('/users', users);// catch 404 and forward to error handlerapp.use(function(req, res, next) { var err = new Error('Not Found'); err.status = 404; next(err);});// error handlerapp.use(function(err, req, res, next) { // set locals, only providing error in development res.locals.message = err.message; res.locals.error = req.app.get('env') === 'development' ? err : {}; // render the error page res.status(err.status || 500); res.render('error');}); nodemon实现改动server端代码后,实现自动重新加载 webpack打包配置高级特性在写React模块时,为了更加方便编写,使用了一些高级特性: ES6语法 12345678910// es6的模块化引入import React, {Component} from 'react';// es6的类定义class Master extends Component {}// 模块化导出export default XXX; JSX标记 12345return ( <div> ... </div>); class类的成员变量定义 123456789101112131415// 注意:在es6的规范中,并不支持成员变量,static变量直接在class里定义,只能如下定义class Master extends Component { constructor(){ this.state = { navDrawerOpen: false }; }}// 但我们希望使用下面的语法规则class Master extends Component { state = { navDrawerOpen: false };} 上面的高级特性,现在主流的浏览器都还不支持,为了使用,我们就需要进行转换: webpack bable-loader // 用于加载babel bable babel-preset-es2015 // 转换es6语法 babel-preset-react // 转换jsx语法 babel-preset-stage-1 // 转换成员变量语法 开发环境配置 通过自己搭后台服务与webpack的watch来实现 webpack-dev-server,HotModuleReplacementPlugin实现热更新 — 推荐方式 webpack-dev-server.config.js的具体配置(webpack-dev-server配置): 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950const webpack = require('webpack');const path = require('path');// 开发期间把www做为了输出目录,不与正式环境情况发生冲突const buildPath = path.resolve(__dirname, 'src/www');module.exports = { entry: [ 'webpack/hot/dev-server', // 热修复配置,这个需要一起合并到app.js里 path.resolve(__dirname, 'src/app/app.js') // app的入口 ], output: { path: buildPath, filename: 'app.js' //publicPath: buildPath // 不用特别指定publicPath路径 }, // 这个是webpack-dev-server的运行参数 devServer: { contentBase: path.resolve(__dirname, 'src/www'), hot:true, // 热修复 inline: true, // 使用热修复,必须是inline模式 port: 8080 // 创建的服务器的port,自由配置 }, resolve: { extensions: ['', '.js', '.jsx', '.css', '.json'] }, plugins: [ // 让webpack-dev-server支持热更新 new webpack.HotModuleReplacementPlugin() ], module: { loaders: [ { test: /\\.js$/, loader: 'babel-loader', exclude: /node_modules/, query: { \"presets\": [ \"react\", // 为了支持jsx的语法 \"es2015\", // 为了支持es6的语法 \"stage-1\" // 为了支持class的成员变量与静态变量 ] } }, { test: /\\.css$/, loader: 'style-loader!css-loader' } ] }}; 通过下面命令运行webpack-dev-server,开发环境配置完成,即可实现修改了js文件后,主动推送更新浏览器 12345678// 先在package.json里配置"scripts": { "start": "nodemon ./bin/www", "browser:development": "webpack-dev-server --config client/webpack-dev-server.config.js --progress --colors --inline"}// 命令行里运行npm run browser:development 通过这种方式启动的webpack-dev-server后,通过ctrl-z能停掉服务,但无法释放所占用的8080端口号,需求如下操作,kill掉此端口的占用,才能再次启动。 1234// 查找端口被哪些服务占用lsof -i:8080// kill掉此进程kill -9 进程pid 前端框架基于React的开发思路变化传统开发模式: React开发模式: app前端入口12345678910111213141516171819202122import React from 'react';import {render} from 'react-dom';import injectTapEventPlugin from 'react-tap-event-plugin';import {Router, browserHistory} from 'react-router';import {createHashHistory} from 'history';import AppRoutes from './AppRoutes.js';// Needed for onTouchTap// http://stackoverflow.com/a/34015469/988941injectTapEventPlugin();// {/*没有弄懂这两个配置参数*/}render( <Router history={browserHistory} onUpdate={() => window.scrollTo(0, 0)} > {AppRoutes} </Router>, document.getElementById('app')); app的入口职责很简单: 路由配置 通用处理,如material-ui库里的事件初始化:injectTapEventPlugin(); app的此入口相当于Android里的Application app的前端路由在android里,一个界面跳转到另外的界面,是通过协议intent与startActivity()方法来实现跳转,其中的核心实现是由系统自己封装掉了 在前端,界面之前跳转的协议都是URL,再通过window.location.href重新向server请求并加载新页面。 在React的模式下,跳转协议也是URL,但这个URL不用经过server请求,而是重新加载新模块实现,如下图所示: 要想实现此效果,不使用React-Router开源框架,我们的写法为: 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162var React = require('react');var About = React.createClass({ render: function () { return( <div> <h2>About</h2> <p>这里将会出现N多介绍文字balabala</p> </div> );}});var blogs = React.createClass({ render: function () { return( <div> <h2>blogs</h2> <a href=\"#\">文章A</a> <br /> <a href=\"#\">文章B</a> <br /> <a href=\"#\">文章C</a> <br /> <a href=\"#\">文章D</a> </div> );}});var Home = React.createClass({ render: function () { return( <div> <h2>Home</h2> <p>这里是首页</p> </div> );}});var App = React.createClass({ render () { var Child; switch (this.props.route) { case 'about': Child = About; break; case 'blogs': Child = blogs; break; default: Child = Home; } return ( <div> <h1>App</h1> <Child/> </div> ) }});function render () { var route = window.location.hash.substr(1); React.render(<App route={route} />, document.body);}window.addEventListener('hashchange', render);render(); 这样实现,也比较容易,但当我们要进行复杂的路由时,就会变的非常麻烦了,所以我们需要使用react-router。 我们的路由配置AppRoutes.js的代码如下: 12345678910111213import React from 'react';import {Route, IndexRoute} from 'react-router';import Master from './components/Master'import Home from './components/pages/Home';const AppRouters = ( <Route path=\"/\" component={Master}> // 其Master为整体框架 <IndexRoute component={Home} /> // Home表示首页,注意是:嵌入到框架里的部份 </Route>);export default AppRouters; 更多配置请点击:ReactRouteConfig Master.js框架的实现Material-UI理解 官网 Material-UI提供了一套组件库。具体请点击 Material-UI还提供了一套样式主题库,颜色库。具体请点击 Material-UI作用 使用其提供的组件,可以开发出与Android原生的Design设计库一致的效果 使用其样式及主题,统一所有的控件与界面的风格,方便统一风格切换 能方便PC,App的适配,提供的控件及源码里有适配的解决方案 对React-Native而言,方便统一PC,M,Android,Ios四端的风格样式 Master.js代码分析123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110import React, {Component} from 'react';// 使用Material-UI的控件import {AppBar, MuiThemeProvider} from 'material-ui';// 使用Material-UI的样式主题import {getMuiTheme, colors, spacing} from 'material-ui/styles';// 使用Material-UI对屏幕适配import withWidth, {MEDIUM, LARGE} from 'material-ui/utils/withWidth';// 封装的抽屉Drawerimport AppNavDrawer from './AppNavDrawer.js';// 用于对屏暮大小适配的包装模块import FullWidthSection from './FullWidthSection.js';// Material-UI主题const muiTheme = getMuiTheme();// 此框架页面的一些特殊样式,即样式主题无法满足的自定义部分const styles = { appBar: { position: 'fixed', // Needed to overlap t he examples zIndex: muiTheme.zIndex.appBar + 1, top: 0, left: 0, right: 0 }, root: { paddingTop: spacing.desktopKeylineIncrement, minHeight: 400, }, content: { margin: spacing.desktopGutter, }, footer: { backgroundColor: colors.grey900, textAlign: 'center', position: 'fixed', left:0, right:0 }, p: { margin: '0 auto', padding: 0, color: colors.lightWhite, maxWidth: 356, }, p2: { margin: '0 auto', padding: 0, paddingTop: '5px', color: colors.red800, maxWidth: 356, },};class Master extends Component { // React控件的state初始值 // 所有的界面变化,都应该通过state来控件,而不是直接操作对应的dom元素 state = { navDrawerOpen: false // 表明抽屉默认是关闭的 }; // 处理Drawer的状态变化 handleChangeRequestNavDrawer = (open)=> { this.setState({ navDrawerOpen: open }); }; // 成员变量,用于处理AppBar左边图片点击事件 handleTouchTapLeftIconButton = ()=> { this.setState({ navDrawerOpen: !this.state.navDrawerOpen }); }; // 框架界面 render(){ return ( <MuiThemeProvider> // 这个是使用Material-UI必须要添加的,用于提供Material-UI主题样式 <div> <AppBar onLeftIconButtonTouchTap={this.handleTouchTapLeftIconButton} title=\"Robot\" style={styles.appBar}/> // 子素元位置 { <div style={muiTheme.prepareStyles(styles.root)}> <div style={muiTheme.prepareStyles(styles.content)}> {this.props.children} </div> </div> } <AppNavDrawer onRequestChangeNavDrawer={this.handleChangeRequestNavDrawer} open={this.state.navDrawerOpen}/> // 用于适配屏幕宽度的 <FullWidthSection style={styles.footer}> <p style={muiTheme.prepareStyles(styles.p)}> {'58同城-用户增长部-无线技术部 '} </p> <p style={muiTheme.prepareStyles(styles.p2)}> {' Android组 '} </p> </FullWidthSection> </div> </MuiThemeProvider> ); }}export default withWidth()(Master); 参考 React-Router React 实践记录 03 React router webpack material-ui express Static Properties in ES Class","categories":[{"name":"前端","slug":"前端","permalink":"https://handsomeliuyang.github.io/categories/前端/"}],"tags":[{"name":"React","slug":"React","permalink":"https://handsomeliuyang.github.io/tags/React/"},{"name":"Robot","slug":"Robot","permalink":"https://handsomeliuyang.github.io/tags/Robot/"},{"name":"nodejs","slug":"nodejs","permalink":"https://handsomeliuyang.github.io/tags/nodejs/"}]},{"title":"React实现MergeRequest管理","slug":"React实现MergeRequest管理","date":"2016-11-15T11:33:06.000Z","updated":"2017-01-05T02:45:45.000Z","comments":true,"path":"2016/11/15/React实现MergeRequest管理/","link":"","permalink":"https://handsomeliuyang.github.io/2016/11/15/React实现MergeRequest管理/","excerpt":"","text":"什么是ReactReact是一个前端框架,与其类似的有vue、angular 2.x等等。改变前端开发模式,让前端开发更加方便,让前端也使用面向对象的方案来开发。相关的一些特点可以看其官方文档 ES6,JSXJavaScript是一个统称,其标准的名称为:ECMAScript。ES6, ES5是两个不同的标准,现在的主流浏览器都完全支持ES5的语法,不支持最新标准ES6。ES6有很多的新特性,更适合面向对象的开发模式,如下所示: 123456789101112131415161718//es5var MyComponent = React.createClass({ render: function() { return ( <div></> ); }});module.exports = MyComponent;//es6class MyComponent extends React.Component { render() { return ( <div></> ); }}export MyComponent Apply; ES6才支持模块化,ES5不支持,如下: 1234567import React from 'react';import ReactDOM from 'react-dom';ReactDOM.render( <h1>Hello, world!</h1>, document.getElementById('root')); 为了更方便开发及代码可读性,React里引入了模板语法:JSX,对比如下所示: 1234567891011121314151617181920212223// 使用JSX的写法class Hello extends React.Component { render() { return <div>Hello {this.props.toWhat}</div>; }}ReactDOM.render( <Hello toWhat="World" />, document.getElementById('root'));// 使用原生的Javascript的写法,注意同时使用了ES6class Hello extends React.Component { render() { return React.createElement('div', null, `Hello ${this.props.toWhat}`); }}ReactDOM.render( React.createElement(Hello, {toWhat: 'World'}, null), document.getElementById('root')); ES6,JSX原生浏览器都不支持,就需要进行预编译(即转码)。React推荐使用Babel Babel是一个javascript的转换器,类似于gradle一样,支持各种插件。 FaceBook开发了一个用于转换JSX的Babel插件:babel-preset-react。 转换ES6的Babel插件为:babel-preset-es2015 更多请点周:Babel官网 为了让浏览器能运行React的代码,有两种方案: 一:实时编译,即让浏览器来编译,配置很简单,如下所示 1<script src=\"https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js\"></script> 特点:这种方式为浏览器编译,因为实时编译会很慢,所以适合代码量比较小的 二:本地编译,即在Server端或本地提前编译好,这样就需要使用打包工具:browserify或webpack,React推荐使用Browserify(browserify官网)。下面使用的是延时方案,即用户访问资源时,才进行转换,如下所示: 12345678910111213141516var express = require('express');var browserify = require('browserify-middleware');var router = express.Router();// 把Browserify的环境切换为正式环境,其配置如下:/*process.env.NODE_ENV = 'production';browserify.settings.mode = 'production';*/var shared = ['react', 'react-dom'];router.get('/react/base_bundle.js', browserify(shared));// 为了不总是配置,直接进行环境配置browserify.settings.external = shared;browserify.settings.transform = 'babelify';router.get('/react/app_bundle.js', browserify('client/merge_manager/app.js')); 特点:方便,不用手动转换,除代码变动后的首次访问慢之外,没有性能问题,浏览器也不用转换运行 JSX的属性与Html的属性JSX是一个模块语法,是为了代码的可读性,但与html并不完全一样,其中最大的区别,有如下几点: 1.JSX支持表达式,但只支持一个表达式,不支持代码块: 1234567891011// 直接访问变量<img src={user.avatarUrl} /><img src={'http://www.58.com' + '/a.png'} />// 循环const todos = ['finish doc', 'submit pr', 'nag dan to review'];return ( <ul> {todos.map((message) => <Item key={message} message={message} />)} </ul>); 2.class属性由于是ES6里的关键字,所以需要使用className,如下: 12<div className=\"button\"></div> 3.html的属性的命名都是小写,但JSX里的属性都是驼峰命名法,如下所示: 12<div className=\"xxx\" tabIndex=\"xxx\"></div> 4.样式style,JSX把style当作字典对象来处理的,不能当字符来处理: 12<div style={{color:'blue', backgroundImage:'xxx'}}></div> 模块化javascript的ES5是不支持模块的,即类不能分文件,即不能运行如下代码: 12345678910// CommonJs,nodejs才支持var React = require('react');var ReactDOM = require('react-dom');var TopBar = require('../common/header.js');var SideBar = require('../common/sidebar_nav.js');var Footer = require('../common/footer.js');// ES6才支持,ES5不支持import React from 'react';import ReactDOM from 'react-dom'; 由于运行前,会使用Babel进行转换,所以React开发时,可以支持模块化,即可以使用Nodejs支持的CommonJs,也可以使用ES6的import模式。React推荐使用CommonJs的方式。 React与传统html开发思想对比传统开发html为界面,js为逻辑,分开开发,如下所示: 123456789<body> <div> ... </div></body><script> // js代码</script> 模板语言:EJS。为了插件业务数据,实现业务后台的MVC 123456789<body> <div> <% if (!locals.username) { %> <ul class=\"nav navbar-nav navbar-right\"> <li><a href=\"/login\"><%= locals.username %></a></li> </ul> <% } %> </div></body> React开发基于组件,一个控件的界面与逻辑都在一起,控件可以复用,与Native的开发模式比较类似,如下所示: 12345678910111213141516171819202122232425class Clock extends React.Component { constructor(props) { super(props); this.state = {date: new Date()}; } // 组件已经与Dom上绑定完了 componentDidMount() { } // 组件与Dom解绑 componentWillUnmount() { } render() { return ( <div> <h1>Hello, world!</h1> <h2>It is {this.state.date.toLocaleTimeString()}.</h2> </div> ); }} 上面使用的是ES6的语法,与Android的Fragment比较像,界面与业务逻辑都在此类里完成 上面的方法为React组件的生命周期方法 数据的获取方式:与传统方式不一样,而是通过Ajax来请求,与Native开发一下,界面与Service只通过接口来交互,但没有使用Server端的模板语言EJS React的生命周期 关键点: 1.我们开发的Api就是这些生命周期方法 2.this.props.xx,this.state.xx是两个数据相关的对象,共中props对象只读,而state是可以修改 123<SideBar activeKey='merge_manager' /> // 只能通过这种方式修改propsthis.setState(xxx) // 修改State数据,组件并进行刷新 3.在组件绑定到Dom里后,就可以通过jquery的方法,可以获取组件的dom元素 123456componentDidMount: function(){ $('#target-branch-select').chosen({width: \"100%\"}); $('#assignee-select').chosen({width: \"100%\"}); $('#title-select').chosen({width: \"100%\"}); $('#author-select').chosen({width: \"100%\"});}, React的生命周期与Android的生命周期对比1234567891011121314var InitEnvir = React.createClass({ render: funcation(){ console.log('render...'); return ( <div> </div> ); }, componentDidMount: funcation(){ console.log('componentDidMount begin...'); this.setState({newData}); console.log('componentDidMount end...'); }}); 上面代码的执行结果为: Android机制 React机制 render…componentDidMount begin… componentDidMount end… render… render…componentDidMount begin…render…componentDidMount end… 这个是Android机制与React机制最大的差别,Android的UI线程,有一个MainLoop队列。 React实现页面框架 DataTables框架表格数据使用的是DataTables组件(datatables官网) 由于数据量不大,使用的是前端排序,查询,过滤。数据量尽量不要超过1000。如果对于大数据,就需要Server进行配合 参考文档 React文档 React入门 (1)—使用指南(包括ES5和ES6对比)","categories":[{"name":"前端","slug":"前端","permalink":"https://handsomeliuyang.github.io/categories/前端/"}],"tags":[{"name":"React","slug":"React","permalink":"https://handsomeliuyang.github.io/tags/React/"},{"name":"Robot","slug":"Robot","permalink":"https://handsomeliuyang.github.io/tags/Robot/"},{"name":"nodejs","slug":"nodejs","permalink":"https://handsomeliuyang.github.io/tags/nodejs/"}]},{"title":"Android内核学习笔记:Android进程\\线程管理","slug":"Android内核学习笔记:Android进程线程管理","date":"2016-08-13T08:37:13.000Z","updated":"2016-08-14T04:42:22.000Z","comments":true,"path":"2016/08/13/Android内核学习笔记:Android进程线程管理/","link":"","permalink":"https://handsomeliuyang.github.io/2016/08/13/Android内核学习笔记:Android进程线程管理/","excerpt":"","text":"Android程序启动过程 ActivityManagerService与WindowManagerService在独立的进程里,与程序进度之间的通信通过Bindler进行 每个应用程序都是运行在独立的进程里的,进程与进程之间无法直接通信,每个进程里都一个JVM虚拟机,不能通过static进行通信 应用程序的进程是由ActivityManagerService通过Process.start(“android.app.ActivityThread”)创建的,进程创建后,会同时创建一个线程,这个线程就是我们所说的UIThread。 同一个进程里的Activity, Service等等四大组件都是运行在ActivityThread里,即UI线程里的。所以通常我们要在Service里创建一个Thread来真正执行后台程序 应用程序启动后,除了创建AcivityThread后,还会创建两个BindlerThread,作用就是用于与AMS,WMS进行交互的。 什么是线程Runnable是不是线程? 不是,Runnable只是一个接口,用于创建线程的接口类 Thread是不是线程? 不是,Thread只有在调用thread.start()方法后,才会创建一个Thread出来,之前的所有的初始化步骤都是在当前线程里执行的。Thread.start()方法如下: 12345public synchronized void start(){ checkNotStarted(); hasBeanStarted = true; VMThread.create(this, stackSize); // 这里才是真正创建一个CPU线程的地方} 只有当VMThread.create()方法之后,才会创建一个真正的线程。 Android的UIThreadAndroid有四大组件:Activity,Service,ContentProvider,Broadcast。组各自的功能: Activity:界面,生命周期:onCreate(), … Service:后台服务,生命周期:onCreate(), … Broadcast:广播,生命周期:onReceive() ContentProvider:用于数据共享,生命周期:onCreate(), … 四大组件的运行哪个进程,哪个线程里呢? 默认情况下:四大组件都是运行在以程序的包名命名的进程里, 四大组件都是运行在UIThread里,但注意:是其生命周期方法是运行在UIThread里。如ContentProvider的query()等等方法的执行线程要依调用方来决定 Service的生命周期是运行在UIThread里,我们需要执行的后台任务,需要创建一个子线程来执行 四大组件,Activity,Service,Broadcast都是需要时,系统进行创建,但ContentProvider例外,其是在应用进程启动时,就会开发创建。 Android的编程框架从开始接触Android开始,我们都是面向四大组件及四大组件的生命周期方法来进行编程。但学过C程序开发的都知道,应用程序都是从main()方法开始执行,再执行一个while()循环,不停接收事件,再处理事件的过程。Android的事件驱动流程: 由AMS创建应用程序进程,并创建UIThread,通过Looper.loop(),让UIThread进入事件驱动循环中 四大组件的生命周期方法,用户交互等等都当作Message,进入MessageQueue里,进入UIThread的事件驱动循环中。 ANR异常概念:ANR(Application No Response)用户点击屏幕后,如果5s没有处理完成此点击Event,就会报ANR异常 ANR发生的情况: 在UIThread里执行网络请求,IO操作等等耗时操作 UI绘制时间过长,也有可能造成ANR异常 ANR异常很多时候不是由一个耗时操作造成的,很多是由一组操作,如进行10000次SP读写操作。","categories":[{"name":"Android","slug":"Android","permalink":"https://handsomeliuyang.github.io/categories/Android/"}],"tags":[{"name":"Android内核学习笔记","slug":"Android内核学习笔记","permalink":"https://handsomeliuyang.github.io/tags/Android内核学习笔记/"}]},{"title":"Android签名的过程","slug":"2016-04-14-Android签名的过程","date":"2016-06-13T09:59:45.000Z","updated":"2016-08-13T08:21:13.000Z","comments":true,"path":"2016/06/13/2016-04-14-Android签名的过程/","link":"","permalink":"https://handsomeliuyang.github.io/2016/06/13/2016-04-14-Android签名的过程/","excerpt":"","text":"Android对apk签名过程Apk解压后的结构: 会生成一个META-INF的文件夹存放签名相关的数据:MANIFEST.MF WUBA_KEY.RSA WUBA_KEY.SF。此三个文件都是我们对unsigin.apk签名时生成的。 SHA1:安全哈希算法,对于长度小于2^64位的消息,SHA1会产生一个160位的消息摘要。当接收到消息的时候,这个消息摘要可以用来验证数据的完整性。在传输的过程中,数据很可能会发生变化,那么这时候就会产生不同的消息摘要。SHA1有如下特性:不可以从消息摘要中复原信息;两个不同的消息不会产生同样的消息摘要。 作用: 完整性,签过名后的Apk无法进行增删改,不然其签名都会不一样 自我校验,因为签过名后的Apk里有公钥,可以对其Apk进行自我校验,所以只需要Apk文件,在Android系统里就可以完成Apk的校验 并行性,校验可以并行进行,WUBA_KEY.RSA对WUBA_KEY.SF的校验与WUBA_KEY.SF对MANIFEST.MF校验可以并行执行 Android系统签验证机制系统验证流程刚好与签名过程相反: 了解签名流程后,能做的事情打批量包为了区分不同的渠道用于统计分析,apk里需要内置携带一个渠道号,各种方式的比较: 位置 经历的过程 特点 渠道号放在代码里 编译代码,编译res资源,编译Manifest文件,签名,生成Apk 非常慢,安全 放在res资源里 编译res资源,编译Manifest文件,签名,生成Apk 很慢,安全 放在AndroidManifest.xml文件里 编译Manifest文件,签名,生成Apk 慢,安全 放在asserts资源里 签名,生成Apk 快,安全 放在签名相关的META-INF文件夹里 生成Apk 非常快,不安全,可被修改 签名认证目的:防止apk被别人反编译,或者防止so文件被直接使用方案:由于Apk里携带了公钥,可以在so里,进行公钥对比,判断当前执行环境是不是在Apk里面。 Keystore文件生成过程由上面的签名过程得知,签名过程,需要有公钥和私钥,所以签名方法:1java -jar signapk.jar testkey.x509.pem testkey.pk8 update.apk update_signed.apk signapk.jar是Android源码包中的一个签名工具 通过signapk.jar这个可执行jar包,以“testkey.x509.pem”这个公钥文件和“testkey.pk8”这个私钥文件对“update.apk”进行签名,签名后的文件保存为“update_signed.apk” 而对应用App,我们是使用java里的命令:jarsigner。使用jarsigner要先生成keystore文件,使用如下:1keytool -genkey -v -keystore app.keystore -alias alias_name -keyalg RSA -validity 20000 -alias 后面跟的是别名这里是alias_name -keyalg 是加密方式这里是RSA -validity 是有效期这里是20000 -keystore 就是要生成的keystore的名称这里是app.keystore 使用jarsigner对unsign.apk进行签名:1jarsigner -verbose -keystore app.keystore -signedjar app_signed.apk app.apk alias_name -keystore: keystore的名称 -signedjar app_signed.apk: 指定签名后生成的APK名称 app.apk: 目标APK 修改Keystore的密码的影响使用keytool生成的keystore文件,我们称之为证书文件,里面存有用于签名apk的公钥及私钥,为了其安全,keystore有其自己本身的密码: storepass 指定密钥库的密码(获取keystore信息所需的密码) keypass 指定别名条目的密码(私钥的密码,即加密私钥的密码) 而我们可以修改keystore文件的storepass和keypass两种密码,都不影响对apk的签名,也不出现签名的apk不相同,因为keystore里面所包含的公钥和私钥是没有变化的。 keystore内部的信息是不会变化的:123456789101112131415161718- XXX.Keystore的信息: - Keystore 类型: JKS - Keystore 提供者: SUN - 您的 keystore 包含 1 输入 - 别名名称: XXXX - 创建日期: XXXX - 项类型: PrivateKeyEntry - 认证链长度: 1 - 认证 [1]: - 所有者:CN=XXX, OU=XXX, O=XXX, L=XXX, ST=XXX, C=XXX - 签发人:CN=XXX, OU=XXX, O=XXX, L=XXX, ST=XXX, C=XXX - 序列号:XXX - 有效期: XXX - 证书指纹: - MD5:XXX - SHA1:XXX - 签名算法名称:SHA1withRSA - 版本: 3 参考 Android系统代码签名验证机制的实现及安全性分析 Android签名总结","categories":[],"tags":[]},{"title":"58同城Hybrid框架的点点滴滴","slug":"58同城Hybrid框架的点点滴滴","date":"2016-03-24T07:28:51.000Z","updated":"2018-10-15T08:52:32.203Z","comments":true,"path":"2016/03/24/58同城Hybrid框架的点点滴滴/","link":"","permalink":"https://handsomeliuyang.github.io/2016/03/24/58同城Hybrid框架的点点滴滴/","excerpt":"","text":"Hybrid框架简介采用Hybrid模式的原因: 纯Native的迭代太慢,不能动态更新,且不能跨平台 纯Web页,有很功能无法实现,有些动画效果实现其体验太差 整体框架结构图 WebView加载流程 在Step1里有两个作用: 可以拦截html请求,对Html请求进行白名单的判断,只有规定域名的请求才能通过 转发一些如拨打电话请求,如tel:xxx 在Step2里主要是显示Loading加载框 Step3:shouldInterceptRequest() 此方法在Api为11时才有,即3.0以后才有此方法,所以在2.x系统里,无法劫持资源请求 主要用于拦截资源请求,让其走本地资源缓存,实现Native资源缓存机制 Step4:onPageFinished()要等所有的资源都加载完成后,才会进行回调,但此时,界面早已经渲染出来了。 Loading界面消失的机制: 在html界面渲染完后,js马上回调一个PageFinished的Action通知Native,提前消失掉Loading界面 如果没有等到PageFinished的Action,就在onPageFinished()方法里,把Loading界面消失掉 跳转协议现在的跳转协议是一个json格式,如下所示:1234567{ \"action\":\"loadpage\", \"pagetype\":\"link\", \"url\":\"http://xxxx\", \"title\":\"标题\" \"xxx\":\"\"} 由于web页的Title是Native实现的,所以其标题需要从跳转协议里得到。 建议使用URL来做跳转协议,如下所示:1jump://action/pagetype?url=xxx&title=xxx 好处:外部调起时,其协议就可以统一 html拦截机制Native实现缓存的思路是:通过shouldInterceptRequest()拦截html的请求。 js,css,image拦截机制机制和Html的一致,都是通过shouldInterceptRequest()拦截请求。 但并不是所有的请求都会进行拦截走缓存,满足如下两种规则走缓存: 标准方式,通过在URL后面添加cachevers参数,如下所示: 1http://xxx/xxx?cachevers=xx cdn的方式,URL满足cdn的格式也会走缓存,如下所示: 1http://xxx/xxx_v版本号.xx 注意:整个缓存框架里,只认第一种格式,第二种cdn格式,会在shouldInterceptRequest()方法里进行转化为第一种格式,请求时,再转化为第二种格式 html,js,css,image的缓存框架异步加载图片虽然shouldInterceptRequest()方法是在后台线程里执行的,但如果直接在此方法里,请求图片资源,那所有的图片资源都将是同步的方式加载,影响最终的加载速度,也会阻塞shouldInterceptRequest()方法的执行,从而阻塞webview的渲染。 解决思路:创建新的线程来请求图片资源,马上返回shouldInterceptRequest()方法,但如何实现呢?通过查看WebView的源码,找到了一种方式:使用管道,代码如下:12345678@Overridepublic WebResourceResponse shouldInterceptRequest(WebView view, String url) { ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe(); // 创建一个管道,一个出口,一个入口 new TransferThread(context, uri, new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])).start(); AssetFileDescriptor assetFileDescriptor = new AssetFileDescriptor(pipe[0], 0, AssetFileDescriptor.UNKNOWN_LENGTH); FileInputStream in = assetFileDescriptor.createInputStream(); return new WebResourceResponse(type, \"utf-8\", in);} 缓存资源的版本号管理缓存资源是通过其版本号来更新的,那资源的版本号应该存在哪里了?最直接的解决办法是:创建一个数据库,里面存储文件名与版本号的对应关系。我们最早也是这样实现的,这样会带来维护成本,还有其出错的概率。 最好的方案:把版本号与缓存文件存储在一起。 实现思路:不管缓存文件是文本文件,还是图片,在文件的开始位置写入一些Byte字节,这些Byte字节就存储了对应的版本号。123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247/** * Created by maolei on 2015/9/8. */public class ExtraDiskCache{ private static final String FUNCTION = \"diskCache\"; /** Magic number for current version of cache file format. */ private static final int CACHE_MAGIC = 0x20150908; private static final String NO_VALUE = \"null\"; /** The root directory to use for the cache. */ private final File mRootDirectory; // TODO clear file public ExtraDiskCache(File rootDirectory){ mRootDirectory = rootDirectory; if(!mRootDirectory.exists()){ mRootDirectory.mkdirs(); } } private File getFile(String fileName){ return new File(mRootDirectory, fileName); } public boolean save(String fileName, Map<String, String> extraInfo, InputStream in){ BufferedOutputStream fos = null; // network inputstream need temp file; File tempFile = getFile(fileName + \"_temp\"); try{ fos = new BufferedOutputStream(new FileOutputStream(tempFile)); if(extraInfo != null && extraInfo.size() > 0){ boolean success = writeHeader(fos, extraInfo); if(!success){ throw new IOException(); } } byte[] buf = new byte[1024]; int len; while ((len = in.read(buf)) > 0) { fos.write(buf, 0, len); } fos.flush(); File cacheFile = getFile(fileName); if(cacheFile.exists()){ cacheFile.delete(); } tempFile.renameTo(cacheFile); return true; }catch (IOException e){ LOGGER.k(FUNCTION, \"write data error\", e); }finally { try { if(in != null){ in.close(); } if(fos != null){ fos.close(); } if(tempFile.exists()){ tempFile.delete(); } }catch (IOException e){ LOGGER.k(FUNCTION, \"close stream error\", e); } } return false; } public Map<String, String> getInfo(String fileName){ BufferedInputStream bis = null; try{ bis = new BufferedInputStream(new FileInputStream(getFile(fileName))); return readHeader(bis); }catch (IOException e){ LOGGER.k(FUNCTION, \"getInfo error\", e); }finally { try { if(bis != null){ bis.close(); } }catch (IOException e){ } } return null; } public InputStream getContentStream(String fileName){ try{ File file = getFile(fileName); BufferedInputStream bis = new BufferedInputStream(new FileInputStream(file)); if(readHeader(bis) != null){ // current file has extra info, so return unread input stream return bis; } // current file is normal file, return origin input stream bis.close(); return new BufferedInputStream(new FileInputStream(file)); }catch (IOException e){ } return null; } private Map<String, String> readHeader(InputStream in){ try { int magic = readInt(in); if (magic != CACHE_MAGIC) { throw new IOException(); } return readStringStringMap(in); }catch (IOException e){ } return null; } private boolean writeHeader(OutputStream out, Map<String, String> extraInfo){ try{ writeInt(out, CACHE_MAGIC); writeStringStringMap(extraInfo, out); return true; }catch (IOException e){ return false; } } /** * Simple wrapper around {@link java.io.InputStream#read()} that throws EOFException * instead of returning -1. */ private static int read(InputStream is) throws IOException { int b = is.read(); if (b == -1) { throw new EOFException(); } return b; } static void writeInt(OutputStream os, int n) throws IOException { os.write((n >> 0) & 0xff); os.write((n >> 8) & 0xff); os.write((n >> 16) & 0xff); os.write((n >> 24) & 0xff); } static int readInt(InputStream is) throws IOException { int n = 0; n |= (read(is) << 0); n |= (read(is) << 8); n |= (read(is) << 16); n |= (read(is) << 24); return n; } static void writeLong(OutputStream os, long n) throws IOException { os.write((byte)(n >>> 0)); os.write((byte)(n >>> 8)); os.write((byte)(n >>> 16)); os.write((byte)(n >>> 24)); os.write((byte)(n >>> 32)); os.write((byte)(n >>> 40)); os.write((byte)(n >>> 48)); os.write((byte)(n >>> 56)); } static long readLong(InputStream is) throws IOException { long n = 0; n |= ((read(is) & 0xFFL) << 0); n |= ((read(is) & 0xFFL) << 8); n |= ((read(is) & 0xFFL) << 16); n |= ((read(is) & 0xFFL) << 24); n |= ((read(is) & 0xFFL) << 32); n |= ((read(is) & 0xFFL) << 40); n |= ((read(is) & 0xFFL) << 48); n |= ((read(is) & 0xFFL) << 56); return n; } static void writeString(OutputStream os, String s) throws IOException { byte[] b = s.getBytes(\"UTF-8\"); writeLong(os, b.length); os.write(b, 0, b.length); } static String readString(InputStream is) throws IOException { int n = (int) readLong(is); byte[] b = streamToBytes(is, n); return new String(b, \"UTF-8\"); } static void writeStringStringMap(Map<String, String> map, OutputStream os) throws IOException { if(map == null || map.size() == 0){ return; } writeInt(os, map.size()); for (Map.Entry<String, String> entry : map.entrySet()) { writeString(os, entry.getKey()); String value = entry.getValue(); if(TextUtils.isEmpty(value)){ writeString(os, NO_VALUE); }else{ writeString(os, entry.getValue()); } } } static Map<String, String> readStringStringMap(InputStream is) throws IOException { int size = readInt(is); if(size <= 0){ return null; } Map<String, String> result = new HashMap<String, String>(size); for (int i = 0; i < size; i++) { String key = readString(is).intern(); String value = readString(is).intern(); if(NO_VALUE.equals(value)){ value = \"\"; } result.put(key, value); } return result; } /** * Reads the contents of an InputStream into a byte[]. * */ private static byte[] streamToBytes(InputStream in, int length) throws IOException { byte[] bytes = new byte[length]; int count; int pos = 0; while (pos < length && ((count = in.read(bytes, pos, length - pos)) != -1)) { pos += count; } if (pos != length) { throw new IOException(\"Expected \" + length + \" bytes, read \" + pos + \" bytes\"); } return bytes; }} 相关的类 WebResLoader:资源加载类,负责:异步加载,同步加载 WebResCacheManager:资源管理类,负责:资源保存,加载,资源版本管理 交互框架现在的交互方式有: 通过webview的addJavascriptInterface()方法交互 优点:简单,Js可以获取返回值,从Api 1开始支持。 缺点:不安全,js可以通过此漏洞调用用户手机里的很多功能 使用会在shouldInterceptRequest()方法交互 优点:安全 缺点:从Api11(即3.0)才支持,不支持js获取返回值 交互协议如下:1234{ \"action\":\"xxx\", \"xxx\":\"xxx\"} 使用的是json协议,其中的action区分事件类型 具体的交互框架: 每一个Action协议会有对应的Bean, Parser, ActionCtrl。都是一一对应的 ActionCtrl都在在具体的Fragment载体页进行注册,只有先注册过的Action,才会有相应的处理 在MessageBaseFragment里注册的Action为通用Action,所有的载体页都支持 Bean对象合法检测:在action协议解析完成后会生成一个Bean对象,所有的Bean对象都继承自ActionBean基类,在ActionBean类中新增checkWebAction()方法,以及check()抽象方法,由子类实现check()方法实现子类自己的协议检测。checkWebAction()方法执行所有ActionBean的通用检测,并在checkWebAction()方法中调用check()方法,执行子类自检。 WebView的载体页 按业务分,创建了不同的载体页,即有多个MessageBaseFragment的子类。(58当前使用的方式) 优点:App开发载体页简单,单个载体页不会变的非常庞大,易于维护 缺点: 载体页过多,前端人员在写跳转协议时,要区分跳转到哪个web载体页 每个载体页支持的action协议是不一样的,造成很多不兼容问题,影响了后期的扩展性 维护成本加大了 一个载体页,支持所有的Action协议,支持所有的业务。(Hybrid二期会改为此种方式) 优点和缺点刚好和上面的方式相反,推荐使用此种方式 Cookie,Header通过webview加载html的方式,有下面两种方法:12345// 直接加载urlwebview.loadUrl(String url) // 在加载url时,要添加header头信息,注意:此方法在2.2时,才添加了webview.loadUrl (String url, Map<String, String> additionalHttpHeaders) 通过上面的方法直接加载Html页面时,会自动把cookie添加,那我们带一些参数给Server的方式就有两种: 通过cookie来带数据 2.2以后,通过Header带数据 经验: 两个同时都带,cookie和header都带相同的数据 在有一些Android手机里,其cookie总是上传不成功,通过抓包发现根本没有cookie信息。(之后证实发现用户其他app也无法使用cookie) Header是完全可以保证数据不丢失的方式,但由于javascript发出的请求,都无法带上header,所以还是要使用cookie 白名单所谓的白名单是指:不在白名单内的请求,不进行加载,或者弹出一个Dialog,提示用户。 实现思路: 本地有一个白名单列表,可以更新此列表。注意:列表里指保存域名 在WebViewClient的shouldOverrideUrlLoading()方法里,进行拦截判断。注意:判断时,要考虑一级域名,二级域名等等。 WebView添加额外功能WebView默认情况下缺少很多功能: 不能图片上传 不能进行文件下载 不能拨打电话等等调用系统其他组件 图片上传功能:分为两种,一种通过相册选择,再上传;一种是拍照后,再上传。这两种都能支持,方法可以直接搜索就可以了。问题:有部分手机无法调启上传,机型支持问题,解决方案:通过Action,由native来做上传 文件下载:原生不支持下载的URL,把下载URL,支持转发到浏览器,进行下载。最好不要支持url支持下载。58现在不支持 调用通用组件:在shouldOverrideUrlLoading()进行通用处理,如下所示:12345678910111213public boolean shouldOverrideUrlLoading(WebView view, String url) { try { if (url.startsWith(\"http:\") || url.startsWith(\"https:\") || url.startsWith(\"file:\")) { // Html请求 } // 其他的通用处理 view.getContext().startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(url))); return true; } catch (Exception e) { LOGGER.e(TAG, null, e); } return false;}","categories":[{"name":"Android","slug":"Android","permalink":"https://handsomeliuyang.github.io/categories/Android/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://handsomeliuyang.github.io/tags/Android/"},{"name":"开发模式","slug":"开发模式","permalink":"https://handsomeliuyang.github.io/tags/开发模式/"}]},{"title":"Android收藏的好文章","slug":"2016-03-22-Android收藏的好文章","date":"2016-03-22T02:42:49.000Z","updated":"2016-10-15T10:03:37.000Z","comments":true,"path":"2016/03/22/2016-03-22-Android收藏的好文章/","link":"","permalink":"https://handsomeliuyang.github.io/2016/03/22/2016-03-22-Android收藏的好文章/","excerpt":"","text":"工具类 这些小工具让你的Android开发更高效 分析内存溢出:LeakCanary 分析ANR异常:BlockCanary 反编译工具 调试Android应用 动画 Android自绘动画实现与优化实战——以Tencent OS录音机波形动画为实例&version=11020201&pass_ticket=iySX2uzbIVQ3hWQPwrlLeaRxz4EgosUgwZ2MCUEZd4Q0KD01oC00QE3N0zbNuRoL)","categories":[{"name":"Android","slug":"Android","permalink":"https://handsomeliuyang.github.io/categories/Android/"}],"tags":[{"name":"收藏","slug":"收藏","permalink":"https://handsomeliuyang.github.io/tags/收藏/"},{"name":"Android","slug":"Android","permalink":"https://handsomeliuyang.github.io/tags/Android/"}]},{"title":"Markdown常用语法","slug":"markdown常用语法","date":"2016-03-17T05:05:47.000Z","updated":"2016-08-13T08:21:13.000Z","comments":true,"path":"2016/03/17/markdown常用语法/","link":"","permalink":"https://handsomeliuyang.github.io/2016/03/17/markdown常用语法/","excerpt":"","text":"标题 引用 序列 注意: 序列的缩进使用Tab键或四个空格 换行换行就直接空一行就行 代码 注意: 如果代码里有空行,会影响markdown排版,可以使用字符 ”[空行]“ 来替换 链接 图片 注意: hexo里添加图片的方式:在source文件下创建一个img的文件夹,在md里引用的路径:/img/xxx.png 加粗、斜体","categories":[{"name":"hexo","slug":"hexo","permalink":"https://handsomeliuyang.github.io/categories/hexo/"}],"tags":[]},{"title":"托管博客到Coding","slug":"托管博客到Coding","date":"2016-03-16T14:01:41.000Z","updated":"2016-08-13T08:21:13.000Z","comments":true,"path":"2016/03/16/托管博客到Coding/","link":"","permalink":"https://handsomeliuyang.github.io/2016/03/16/托管博客到Coding/","excerpt":"","text":"原因github上push代码,访问速度都比较慢,所以决定迁移到国内的Git托管服务:Coding 迁移Repository步骤: 进入Coding站点:https://coding.net,申请帐号 创建一个Project,如下设置: 项目名称为用户名 设置为公开 使用【导入仓库】功能,把github上的仓库导入进来 从master分支创建一个coding-pages分支,并设置其为默认分支 并已coding-pages为分支,打开Pages服务 即可以访问自己的博客:http://用户名.coding.me 修改hexo里的_config.yml文件里的布署,修改如下:1234deploy: type: git repository: git@git.coding.net:handsomeliuyang/handsomeliuyang.git branch: coding-pages","categories":[{"name":"hexo","slug":"hexo","permalink":"https://handsomeliuyang.github.io/categories/hexo/"}],"tags":[]},{"title":"Gradle分享","slug":"Gradle分享","date":"2016-03-16T10:15:29.000Z","updated":"2016-08-13T08:21:13.000Z","comments":true,"path":"2016/03/16/Gradle分享/","link":"","permalink":"https://handsomeliuyang.github.io/2016/03/16/Gradle分享/","excerpt":"","text":"Gradle是什么 Gradle是一个自动化构建工具,用来替换ant及maven。 Gradle的特点: 具有表达性的语言和强大的API(Gradle的表达性语言称为DSL) Gradle就是Groovy,但高于Groovy 灵活的约定(所有的配置属性都有其默认值,但也可以全部进行配置) 和其他构建工具的集成(可以与Ant,Maven集成,迁移成功比较低) 强大的依赖管理(Gradle没有其专有的依赖管理工具,但其兼容Ivy及Maven) 扩展非常方便 Groovy与Java的关系: Gradle的脚本里的语法Gradle脚本例子脚本build.gradle, 如:123task hello1 << { println 'hello1'} 上面的代码使用DSL定义了一个Task 问题:什么是DSL,其与groovy的关系是什么? 解答: DSL分为两类:外部DSL和内部DSL。外部DSL是一种独立的可解析的语言,举一个最常见的是例子,SQL,它专注于数据库的操作。内部DSL是通用语言所暴露的用来执行特定任务的API,它利用语言本身的特性,将API以特殊的形式(或者格式)暴露出来的,如Gradle。 Gradle是内部DSL,其实就是一套API,对应其Groovy的对象。定义DSL的目的,是使Gradle看上去更像脚本语言。 Gradle基于Groovy但大于Groovy,它是经过“定制”的Groovy,是经过“定制”的面向对象语言,所以,由始至终,Gradle都离不开对象这个概念。 上面的Gradle脚本转化为对应的Groovy对象来理解: 有一个Project对象,其有一个task方法,返回一个Task对象,如Project.task(String name) hello1是一个String的参数 符号“<<”是操作符重载。Task.leftShift(Closure action),用来给task的action列表中添加一个action。 转化为Groovy代码的写法:123task(\"hello1\").leftShift({ println 'hello world'}) Gradle的DSL与API对应既然DSL是一套API,用来对应Groovy里的对象的,那主要有哪些对象了。 1. DSL文档:https://docs.gradle.org/current/dsl/,Java文档:https://docs.gradle.org/current/javadoc/ 2. Gradle对象,运行脚时,第一个创建的对象 3. 每个build.gradle脚本对应Project对象 4. 每个task都是interface Task的子类,上面创建的hello1是DefaultTask的对象 5. 在build.gradle里可以使用DSL写代码,也可以使用Java语法或Groovy语法来写代码。 定义Task的几种方法 使用DSL方式来定义 特点: 都是DefaultTask类的子类 都是相当于调用doLast()方法,把闭包传入一个队列里,当task的方法执行完后,再进行调用,和Android的Hander比较类似。 注意:上面都不是方法定义,都是方法调用, 自定义Task类和方法 特点: 由于Gradle使用的是Groovy,所以只有在定义类时,才能定义方法,其他DSL里的,都是调用方法。 由于Gradle是一种脚本语言,其运行时,不用手动将java类转化为class文件,才能执行,而是可以直接编译,解释执行。 Android-Gradle插件相关: Android插件的DSL:http://google.github.io/android-gradle-dsl/ Gradle的生命周期gradle运行例子当我们有一个build.gradle的脚本,内容如下:123task helloWorld << { println 'hello, world'} 执行Task:12$ gradle -q helloWorldhello, world Gradle的底层运行过程: Task相关 为了方便对编译过程进行干预,每个Task都有一个doFirst()和doLast()方法,可以不断的给Task的两个执行对列添加闭包对象,等Task执行时,再依次执行,如下图所示: 可以访问DefaultTask里的任何属性,在Groovy里,属性会自动创建对应的getXXX(),setXXX()方法 默认创建的task对象,都是DefaultTask类的对象,可以修改其对象类型,如下: Task之间可以创建其依赖,等特其他Task执行完之后,再进行执行,定义依赖: 理解Task的配置阶段及执行阶段 task的inputs及outputs 判断一个Task是否执行,是通过判断其inputs及outputs是否有改动,如果有改动时,才会执行。定议Task的的inputs和outputs是在定义Task类时,通过注解添加的,对应DefaultTask里有两个属性:inputs: TaskInputs outputs: TaskOutputs通过gradle xxx -d可以看到task的inputs及outputs 依赖管理 为什么要引入依赖管理? 没有引入依赖管理时,我们会遇到的一些问题: Eclipse开发Android阶段,对于jar的引入,需要手动去下载 依赖的jar如果还关系其他jar,也需要进行引入 如果jar有变动时,通知使用方去修改,也比较麻烦 经常出现依赖jar版本不合适的问题依赖管理就是为了解上面这些问题,而引入的。 Gradle的依赖管理,Maven仓库,本地依赖缓存 总结: Android-Cradle插件的Maven仓库地址:http://mvnrepository.com/artifact/com.android.tools.build/gradle 58同城的Maven仓库地址:http://artifactory.58corp.com:8081/artifactory/webapp/browserepo.html?6 本地的依赖缓存的地址:.gradle/caches/modules-2/files-2.1 查看本地缓存地址的方法:输出configurations里的dependency对象,就可以知道其保存地址 通过gradle xxx -d输出完整地址,仔细去读里面的日志,也可以知道其保存地址 外部模块依赖 外部模块依赖的属性: group:用来标识一个公司,组织或者项目,通常的做法是:公司的域名反写。如:com.wuba.wuxian.lib name:一个模块的名称,一个group内要唯一。如:WubaCommonsLib version:版本号,如:2.0.0,3.6.3-Final,2.0.0-SNAPSHOT等等。所以版本号不是int类型,是String类型 classifier:如果group,name,version都一样时,用于区分的。如jar的源码,javadoc等等如例子: com.wuba.wuxian.lib:WubaCommonsLib:2.0.0-SNAPSHOT com.wuba.wuxian.lib:WubaCommonsLib:2.0.0-javadoc com.wuba.wuxian.lib:WubaCommonsLib:2.0.0-sources 依赖冲突的解决方案 冲突出现的情况 a库和b库都关系同一个c库解决方案:gradle的依赖管理会自动使用最新的c库,不会使用两次c库 a库关联b库,同时关联c的源码,b库关系c库的aar问题原因:c的源码库及c库的aar不是同一个库,会当作不同的库进行处理,因为其group不一样解决方案: 只引入b库的aar,不引入b库的关联库c,如下所示: 使用排除法,排除b库的c库就行,如下所示: 更多的配置文档:https://docs.gradle.org/current/javadoc/里的DependencyHandler 灵活的版本号及本地缓存更新 如果想一直使用最新版本,可以使用动态版本本声明:com.wuba.wuxian.lib:WubaCommonsLib:2.0.0+,但希望不要这样使用,如果最新的版本,其兼容有问题,这样会影响现有代码运行。 如果正在开发调试WubaCommonsLib期间,这时,定义版本号时,可以使用快照版本号,-SNAPSHOT。把版本号定义为-SNAPSHOT时,gradle的依赖管理,会使用最新的快照库 如果本地版本库有缓存后,如果想使用最新的依赖版本,这时,就要修改本地缓存策略,如下所示: 参与文档: Gradle深入与实战:http://benweizhu.github.io/blog/2015/03/31/deep-into-gradle-in-action-6/ 《实战Gradle》","categories":[{"name":"Android","slug":"Android","permalink":"https://handsomeliuyang.github.io/categories/Android/"}],"tags":[{"name":"Android","slug":"Android","permalink":"https://handsomeliuyang.github.io/tags/Android/"},{"name":"Gradle","slug":"Gradle","permalink":"https://handsomeliuyang.github.io/tags/Gradle/"}]},{"title":"Hexo搭建个人博客","slug":"Hexo搭建个人博客","date":"2016-03-16T06:08:36.000Z","updated":"2016-08-13T08:21:13.000Z","comments":true,"path":"2016/03/16/Hexo搭建个人博客/","link":"","permalink":"https://handsomeliuyang.github.io/2016/03/16/Hexo搭建个人博客/","excerpt":"","text":"搭建过程 Github配置 在Github上申请一个帐号 创建一个repository,其命名规则有两种 <你的用户名>.github.io // 那你的博客地址就是 http://<你的用户名>.github.io 推荐 <任意名称> // 那你的博客地址将是:http://<你的用户名>.github.io/<任意名称> 使用ssh连接,配置ssh的公钥和私钥,以后连接github不用再输入密码 Hexo安装 请按最新官网安装并配置Hexo,具体请看:https://hexo.io/ 使用如下命令,搭建本地Server:123hexo clean // 清除刚刚创建的静态web网页hexo g // hexo generator的缩写,生成静态web网页,生成的目录是:publichexo s // hexo server的缩写,生成本地web服务器,可以访问,查看效果 开发环境 下载webstorm 给webstorm安装markdown插件 通过webstorm加载hexo Hexo的目录结构 Hexo的配置 在_config.yml里设置如下参数: title subtitle description author email language 在_config.yml里配置github的服务器及主分支: 1234deploy: type: git repository: git@github.com:xxx branch: master 在_config.yml里配置主题和对css文件等等的压缩 123theme: jacman // 这个是我使用的主题,你可以在网上下载更多的主题stylus: compress: true // 对样式文件进行压缩 按官网教程安装Hexo后,执行hexo d命令会报错,是由于缺少Module库,执行下面的命令: 1npm install hexo-deployer-git --save 安装之后,就可以执行hexo d进行部署了 写博客 使用如下命令创建新的文章: 1hexo new "文章名称" 在source文件下,创建一个存放图片目录,如img,在文章里引用的地址为:/img/图片名 在目录source/_posts目录下找到文件,并编辑 文章可以设置categories(类别)和tags(标签),注意:tags下面只能是3个横线,多了少了都不行 查看效果12345hexo cleanhexo ghexo s[空行]hexo d // hexo deployer的缩写 发布到Github Hexo备份使用Github来备份 在github上创建一个hexo的分支 把本地的hexo项目上传到hexo分支里,但注意配置.gitignore文件,如下: 12345678.DS_StoreThumbs.dbdb.json*.lognode_modules/public/.deploy*/.idea 添加新功能 改主题,我使用的是jacman 添加关于,使用如下命令 1hexo new page "about" // 这样创建md文件,才能使用/about来引用到 添加百度统计,用于统计网站流量 添加站内搜索 添加评价,推荐使用多说 添加百度搜索、google搜索 添加sitemap.xml,供搜索引擎的爬虫使用 hexo发布新文章方法一: 创建文章,命令如下: 1hexo new "文章名称" 在source/_posts目录下,就会创建此文章,编译完成后,部署,命令如下: 12hexo cleanhexo d -g // 相当于先执行hexo g 再执行hexo d 方法二: 新创建草稿,命令如下: 1hexo new draft "文章名称" 在source/_drafts目录下,会创建相应的文章,编写文章,草稿文章默认情况下,不会被部署到站点里 把草稿发布为文章,命令如下:1hexo publish "草稿文章名称" F&Q图片支持https://codefalling.com/2015/12/19/no-pains-with-hexo-local-image/","categories":[{"name":"hexo","slug":"hexo","permalink":"https://handsomeliuyang.github.io/categories/hexo/"}],"tags":[]},{"title":"hexo理解","slug":"hexo理解","date":"2016-03-16T03:03:16.000Z","updated":"2016-08-13T08:21:13.000Z","comments":true,"path":"2016/03/16/hexo理解/","link":"","permalink":"https://handsomeliuyang.github.io/2016/03/16/hexo理解/","excerpt":"","text":"Hexo是什么 A fast, simple & powerful blog framework, powered by Node.js. 基于Node.js的一个快速、简洁且高效的博客框架。 我理解的Hexo是: 是一个Node.js的命令行脚本工具 一个把markdown编译为html页面,生成一个静态Web网站的静态博客框架 命令行脚本工具使用Node.js除了用来开发Web应用外,还可以用于开发命令行脚本工具,Hexo就是一个使用Node.js开发命令行脚本工具:1npm install XXX -g // 脚本Module只能通过全局方式添加 关于Nodejs开发命令行工具的教程:使用Node写命令行工具 静态博客框架Hexo的框架使用node.js,把markdown, ejs翻译为纯Html页面,这些纯Html页面只需要布署到Web服务器上就行了。 Hexo的源码,官网,Module Hexo的源码:https://github.com/hexojs/hexo Hexo的官网:https://hexo.io/ Hexo在npm上的Module:https://www.npmjs.com/package/hexo Hexo版本Hexo现在的版本主要有2.x及3.x,这两个版本有比较大的差别,其主要差别如下: 3.x里多了hexo-cli模块,从hexo里分离了,其中全部是命令行的工具。// 这个就是我之前想不通的,为什么会有如下两种: 12npm install hexo -g // 这个是2.x的安装方式,不过3.x也可以用npm install hexo-cli -g // 这个就是3.x的标准安装方式 3.x里把hexo模块分为Generators, deployers, server几种模块 // 这就是为什么在2.x里,可以直接使用hexo deployer,而在3.x里你要先安装deployers的模块,才能执行hexo deployer 更多差别,请查看:https://github.com/hexojs/hexo/wiki/Breaking-Changes-in-Hexo-3.0 特别注意:有些themes主题只支持hexo 2.x,要注意选择。 Hexo与Github的关系 Hexo会生成一个静态的web网站 Github Pages就是相当于一个web服务器 Github本身的git相当于FTP命令,让我们把web网站资源上传到web服务器上","categories":[{"name":"hexo","slug":"hexo","permalink":"https://handsomeliuyang.github.io/categories/hexo/"}],"tags":[]}]}