
对前端移动客户端技术比较擅长。著有《React Native移动开发实战》和《Kotlin入门与实战》和《Weex跨平台实战》,《React Native移动开发进阶》即将出版,正在努力完成《Flutter跨平台开发实战》
Swiper Swiper是一个滑块容器类组件,主要提供如下的一些属性。 属性名 类型 说明 支持版本 indicator-dots Boolean 是否显示面板指示点 indicator-color Color 指示点颜色 1.1.0 indicator-active-color Color 选中的指示点颜色 1.1.0 current Number 当前所在滑块的 index 1.1.0 autoplay Boolean 是否自动切换 current-item-id String 当前所在滑块的 item-id ,不能与 current 被同时指定 1.9.0 interval Number 自动切换时间间隔 duration Number 滑动动画时长 circular Boolean 是否采用衔接滑动 vertical Boolean 滑动方向是否为纵向 previous-margin String 前边距,可用于露出前一项的一小部分,接受 px 和 rpx 值 1.9.0 next-margin String 后边距,可用于露出后一项的一小部分,接受 px 和 rpx 值 1.9.0 display-multiple-items Number 同时显示的滑块数量 1.9.0 skip-hidden-item-layout Boolean 是否跳过未显示的滑块布局,设为 true 可优化复杂情况下的滑动性能,但会丢失隐藏状态滑块的布局信息 1.9.0 bindchange EventHandle current 改变时会触发 change 事件,event.detail = {current: current, source: source} bindanimationfinish EventHandle 动画结束时会触发 animationfinish 事件,event.detail 同上 1.9.0 说明:从 1.4.0 版本开始,change事件返回detail中包含一个source字段,表示导致变更的原因,可能值如下: autoplay:自动播放导致swiper变化; touch:用户划动引起swiper变化; 其他原因将用空字符串表示。 当然,作为一个容器控件,还需要和子组件搭配使用才能起到效果,而<swiper-item/>就是需要的子组件。 swiper-item swiper-item组件的主要属性如下: 属性名 类型 说明 支持版本 item-id String 该 swiper-item 的标识符 1.9.0 实例 下面是官方提供的一个实例,可以在小程序开发工具中预览。涉及的核心代码有:swiper.wxml <swiper indicator-dots="{{indicatorDots}}" autoplay="{{autoplay}}" interval="{{interval}}" duration="{{duration}}"> <block wx:for="{{imgUrls}}"> <swiper-item> <image src="{{item}}" class="slide-image" width="355" height="150"/> </swiper-item> </block> </swiper> <button bindtap="changeIndicatorDots"> indicator-dots </button> <button bindtap="changeAutoplay"> autoplay </button> <slider bindchange="intervalChange" show-value min="500" max="2000"/> interval <slider bindchange="durationChange" show-value min="1000" max="10000"/> duration swiper.js Page({ data: { imgUrls: [ 'http://img02.tooopen.com/images/20150928/tooopen_sy_143912755726.jpg', 'http://img06.tooopen.com/images/20160818/tooopen_sy_175866434296.jpg', 'http://img06.tooopen.com/images/20160818/tooopen_sy_175833047715.jpg' ], indicatorDots: false, autoplay: false, interval: 5000, duration: 1000 }, changeIndicatorDots: function(e) { this.setData({ indicatorDots: !this.data.indicatorDots }) }, changeAutoplay: function(e) { this.setData({ autoplay: !this.data.autoplay }) }, intervalChange: function(e) { this.setData({ interval: e.detail.value }) }, durationChange: function(e) { this.setData({ duration: e.detail.value }) } }) 最终的效果如下: Swiper实现引导页 在移动开发中,我们经常使用ViewPager(Android)和UIScrollView(ios)来实现引导页面,效果如下。 在微信小程序中,借助Swiper和swiper-item组件,我们可以很轻松的实现上面的效果。实现上面的效果主要会用到下面几个文件。 guide.wxml <swiper indicator-active-color='#fff' indicator-dots="true"> <block wx:for="{{imgs}}" wx:for-index="index" wx:key="swiperItem" wx:for-item="item" > <swiper-item class="swiper-items" > <image class="swiper-image" src="{{item}}"></image> <button class="button-img" bindtap="start" wx:if="{{index == imgs.length - 1}}" >立即体验</button> </swiper-item> </block> </swiper> 涉及到的样式guide.wxss: swiper { position: absolute; height: 100%; width: 100%; } .swiper-image { height: 100%; width: 100%; opacity:0.9; } .button-img{ position: relative; bottom: 120px; height: 40px; width: 120px; opacity:0.6; } 其次是,guide.wxml页面中页面所需要的数据,在guide.js文件的data节点添加如下数据。 data: { imgs: [ "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2522069454.jpg", "https://img1.doubanio.com/view/photo/s_ratio_poster/public/p2522778567.jpg", "https://img3.doubanio.com/view/photo/s_ratio_poster/public/p2523516430.jpg", ], img: "http://img.kaiyanapp.com/7ff70fb62f596267ea863e1acb4fa484.jpeg", }, 当然,当点击“立即体验”按钮后,会跳转到新的页面去,我们可以在js文件中添加相关的跳转逻辑。
HashTable和HashMap的区别 在面试的过程中,经常会被问到HashTable和HashMap的区别,下面就这些区别做一个简单的总结。 1、继承的父类不同 Hashtable继承自Dictionary类,而HashMap继承自AbstractMap类,但二者都实现了Map接口。 2、线程安全性不同 Hashtable 中的方法是Synchronized的,而HashMap中的方法在缺省情况下是非Synchronized的。在多线程并发的环境下,可以直接使用Hashtable,不需要自己为它的方法实现同步,但使用HashMap时就必须要自己增加同步处理。 总结一句话:Hashtable(1.0版本)不建议在新代码中使用,不需要线程安全的场合可以用HashMap替换,需要线程安全的场合可以用ConcurrentHashMap替换。 3、是否提供contains方法 HashMap把Hashtable的contains方法去掉了,改成containsValue和containsKey,因为contains方法容易让人引起误解。Hashtable则保留了contains,containsValue和containsKey三个方法,其中contains和containsValue功能相同。 4、key和value是否允许null值 Hashtable中,key和value都不允许出现null值。但是如果在Hashtable中有类似put(null,null)的操作,编译同样可以通过,因为key和value都是Object类型,但运行时会抛出NullPointerException异常。 HashMap中,null可以作为键,这样的键只有一个;可以有一个或多个键所对应的值为null。当get()方法返回null值时,可能是 HashMap中没有该键,也可能使该键所对应的值为null。因此,在HashMap中不能由get()方法来判断HashMap中是否存在某个键, 而应该用containsKey()方法来判断。 5、遍历的内部实现方式不同 Hashtable、HashMap都使用了 Iterator。但由于历史原因,Hashtable还使用了Enumeration的方式 。 6,数组初始化和扩容方式不同 HashTable在不指定容量的情况下的默认容量为11,而HashMap为16,Hashtable不要求底层数组的容量一定要为2的整数次幂,而HashMap则要求一定为2的整数次幂。具体扩容时,Hashtable将容量变为原来的2倍加1,而HashMap扩容时,将容量变为原来的2倍。 HashTable 由于HashTable的性能问题,在实际编程中HashTable并不是很常见,更多的是使用HashMap或ConcurrentHashMap。 简单来说,HashTable是一个线程安全的哈希表,它通过使用synchronized关键字来对方法进行加锁,从而保证了线程安全。但这也导致了在单线程环境中效率低下等问题。 HashTable存储模型 HashTable保存数据是和HashMap是相同的,使用的也是Entry对象。HashTable类继承自Dictionary类,实现了Map,Cloneable和java.io.Serializable三个接口,其UML图如下图所示。 HashTable的功能与与HashMap中的功能相同,主要有:put,get,remove和rehash等。 HashTable的主要方法的源码实现逻辑与HashMap中非常相似,有一点重大区别就是所有的操作都是通过synchronized锁保护的。也就是说,只有获得了对应的锁,才能进行后续的读写等操作。 下面就HashTable常见的方法给大家做一个简单的解析。 构造方法 HashTable的构造方法源码如下: public Hashtable(int initialCapacity, float loadFactor) { if (initialCapacity < 0) throw new IllegalArgumentException("Illegal Capacity: "+ initialCapacity); if (loadFactor <= 0 || Float.isNaN(loadFactor)) throw new IllegalArgumentException("Illegal Load: "+loadFactor); if (initialCapacity==0) initialCapacity = 1; this.loadFactor = loadFactor; table = new Entry<?,?>[initialCapacity]; threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); } public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); } public Hashtable() { this(11, 0.75f); } 从构造函数中可以得到如下的信息:HashTable默认的初始化容量为11(与HashMap不同,HashMap是16),负载因子默认为0.75(与HashMap相同)。而正因为默认初始化容量的不同,同时也没有对容量做调整的策略,所以可以先推断出,HashTable使用的哈希函数跟HashMap是不一样的。 put put方法的主要逻辑如下: 先获取synchronized锁; put方法不允许null值,如果发现是null,则直接抛出异常; 计算key的哈希值和index; 遍历对应位置的链表,如果发现已经存在相同的hash和key,则更新value,并返回旧值; 如果不存在相同的key的Entry节点,则调用addEntry方法增加节点; addEntry方法中,如果需要则进行扩容,之后添加新节点到链表头部。 Put方法的源码如下: public synchronized V put(K key, V value) { // Make sure the value is not null if (value == null) { throw new NullPointerException(); } // Makes sure the key is not already in the hashtable. Entry<?,?> tab[] = table; int hash = key.hashCode(); //计算桶的位置 int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> entry = (Entry<K,V>)tab[index]; //遍历桶中的元素,判断是否存在相同的key for(; entry != null ; entry = entry.next) { if ((entry.hash == hash) && entry.key.equals(key)) { V old = entry.value; entry.value = value; return old; } } //不存在相同的key,则把该key插入到桶中 addEntry(hash, key, value, index); return null; } 涉及的Entry对象的源码如下: private void addEntry(int hash, K key, V value, int index) { modCount++; Entry<?,?> tab[] = table; //哈希表的键值对个数达到了阈值,则进行扩容 if (count >= threshold) { // Rehash the table if the threshold is exceeded rehash(); tab = table; hash = key.hashCode(); index = (hash & 0x7FFFFFFF) % tab.length; } // Creates the new entry. @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>) tab[index]; //把新节点插入桶中(头插法) tab[index] = new Entry<>(hash, key, value, e); count++; } 从上面的源码可以看到,put方法一开始就会进行值的null值检测,同时,HashTable的put方法也是使用synchronized来修饰。你可以发现,在HashTable中,几乎所有的方法都使用了synchronized来保证线程安全。 get get方法的主要逻辑如下: 先获取synchronized锁; 计算key的哈希值和index; 在对应位置的链表中寻找具有相同hash和key的节点,返回节点的value; 如果遍历结束都没有找到节点,则返回null。 get函数的源码如下: public synchronized V get(Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); //通过哈希函数,计算出key对应的桶的位置 int index = (hash & 0x7FFFFFFF) % tab.length; //遍历该桶的所有元素,寻找该key for (Entry<?,?> e = tab[index] ; e != null ; e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { return (V)e.value; } } return null; } 从上面的代码可以发现,get方法使用了synchronized来修饰,以保证线程的安全,并且它是通过链表的方式来处理冲突的。另外,我们还可以看见HashTable并没有像HashMap那样封装一个哈希函数,而是直接把哈希函数写在了方法中。 rehash扩容 rehash扩容方法主要逻辑如下:数组长度增加一倍(如果超过上限,则设置成上限值);更新哈希表的扩容门限值;遍历旧表中的节点,计算在新表中的index,插入到对应位置链表的头部。 rehash方法的源码如下: protected void rehash() { int oldCapacity = table.length; Entry<?,?>[] oldMap = table; //扩容扩为原来的两倍+1 int newCapacity = (oldCapacity << 1) + 1; //判断是否超过最大容量 if (newCapacity - MAX_ARRAY_SIZE > 0) { if (oldCapacity == MAX_ARRAY_SIZE) // Keep running with MAX_ARRAY_SIZE buckets return; newCapacity = MAX_ARRAY_SIZE; } Entry<?,?>[] newMap = new Entry<?,?>[newCapacity]; modCount++; //计算下一次rehash的阈值 threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1); table = newMap; //把旧哈希表的键值对重新哈希到新哈希表中去 for (int i = oldCapacity ; i-- > 0 ;) { for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) { Entry<K,V> e = old; old = old.next; int index = (e.hash & 0x7FFFFFFF) % newCapacity; e.next = (Entry<K,V>)newMap[index]; newMap[index] = e; } } } HashTable的rehash方法相当于HashMap的resize方法。跟HashMap那种巧妙的rehash方式相比,HashTable的rehash过程需要对每个键值对都重新计算哈希值,而比起异或和与操作,取模是一个非常耗时的操作。这也是HashTable比HashMap低效的原因之一。 remove remove方法主要逻辑如下: 先获取synchronized锁; 计算key的哈希值和index; 遍历对应位置的链表,寻找待删除节点,如果存在,用e表示待删除节点,pre表示前驱节点。如果不存在,返回null; 更新前驱节点的next,指向e的next。返回待删除节点的value值。 remove函数的源码如下: public synchronized V remove(Object key) { Entry<?,?> tab[] = table; int hash = key.hashCode(); int index = (hash & 0x7FFFFFFF) % tab.length; @SuppressWarnings("unchecked") Entry<K,V> e = (Entry<K,V>)tab[index]; for(Entry<K,V> prev = null ; e != null ; prev = e, e = e.next) { if ((e.hash == hash) && e.key.equals(key)) { modCount++; if (prev != null) { prev.next = e.next; } else { tab[index] = e.next; } count--; V oldValue = e.value; e.value = null; return oldValue; } } return null; } ConcurrentHashMap HashMap是我们平时开发过程中使用的比较多的集合,但它是非线程安全的,在涉及到多线程并发的情况,进行get操作有可能会引起死循环,导致CPU利用率接近100%。例如: final HashMap<String, String> map = new HashMap<String, String>(2); for (int i = 0; i < 10000; i++) { new Thread(new Runnable() { @Override public void run() { map.put(UUID.randomUUID().toString(), ""); } }).start(); } 但是解决方法也有很多,如Hashtable和Collections.synchronizedMap(hashMap),不过这两个方案基本上是对读写进行加锁操作,一个线程在读写元素,其余线程必须等待,性能可想而知。此时,可以使用ConcurrentHashMap来解决。 JDK 1.7 ConcurrentHashMap实现 和HashMap不同,ConcurrentHashMap采用分段锁的机制,实现并发的更新操作,底层采用数组+链表的存储结构。ConcurrentHashMap最核心的两个核心静态内部类包括:Segment和HashEntry。 理解ConcurrentHashMap需要注意如下几个概念: Segment继承ReentrantLock用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶; HashEntry 用来封装映射表的键 / 值对; 每个桶是由若干个 HashEntry 对象链接起来的链表。 一个 ConcurrentHashMap 实例中包含由若干个 Segment 对象组成的数组,其数据结构如下: JDK1.8 ConcurrentHashMap实现 1.8的实现已经抛弃了Segment分段锁机制,而是采用CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。而HashMap在1.8版本中也对存储结构进行了优化,采用数组+链表+红黑树的方式进行数据存储,红黑树可以有效的平衡二叉树,带来插入、查找性能上的提升。 ConcurrentHashMap在1.8版本的数据存储结构如下图: 初始化 只有在第一次执行put方法时才会调用initTable()初始化Node数组,该方法的源码如下: private final Node<K,V>[] initTable() { Node<K,V>[] tab; int sc; while ((tab = table) == null || tab.length == 0) { if ((sc = sizeCtl) < 0) Thread.yield(); // lost initialization race; just spin else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { try { if ((tab = table) == null || tab.length == 0) { int n = (sc > 0) ? sc : DEFAULT_CAPACITY; @SuppressWarnings("unchecked") Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n]; table = tab = nt; sc = n - (n >>> 2); } } finally { sizeCtl = sc; } break; } } return tab; } sizeCtl默认为0,如果ConcurrentHashMap实例化时有传参数,sizeCtl会是一个2的幂次方的值。所以执行第一次put操作的线程会执行Unsafe.compareAndSwapInt方法修改sizeCtl为-1,有且只有一个线程能够修改成功,其它线程通过Thread.yield()让出CPU时间片等待table初始化完成。 关于具体的的一些put、get、table扩容等操作,大家可以自行搜索相关的资料。
由于其他的原因,对于React Native相关的内容最近没有投入太多的关注,从去年年底出版了《React Native移动开发实战》后,对于React Native的关注就比较少了。最近由于公司之前的项目需要,所以React Native又重新回到我的世界,并且,最近出去面试深深的感觉到原生开发的饱和,不管是Android还是iOS,移动市场基本已经饱和,而更多的公司和开发者开始转向了前端,这对于移动开发人员,特别是有过跨平台开发经验或者小程序开发经验的开发者来说,学习前端是异常的容易。因此,我后面的目光也主要放在跨平台和大前端上。 对React Native发展历史比较了解的同学都知道,React Native早期除了性能外,生态也是特别差的,但是在经过了2017年的优化和发展之后,现在跨平台开发如React Native和Weex可以说是相当的吃香。并且,随着跨平台生态的逐渐形成,跨平台的组件和文章也越来越多。 今天给大家讲的是一个可以实现悬浮效果的组件,效果如下: 该库的源码地址为:https://github.com/mastermoo/react-native-action-button 安装 在项目中使用如下的命令安装react-native-action-button库: npm i react-native-action-button --save 因为用到了react-native-vector-icons图标组件,需要还需要做下link,命令如下: react-native link react-native-vector-icons 或者使用下面的命令执行link。 react-native link 使用实例 首先导入该。 import ActionButton from 'react-native-action-button'; 例如下面是一个具体的实例代码: import React, { Component } from 'react'; import { StyleSheet, View } from 'react-native'; import ActionButton from 'react-native-action-button'; import Icon from 'react-native-vector-icons/Ionicons'; class App extends Component { render() { return ( <View style={{flex:1, backgroundColor: '#f3f3f3'}}> {/* Rest of the app comes ABOVE the action button component !*/} <ActionButton buttonColor="rgba(231,76,60,1)"> <ActionButton.Item buttonColor='#9b59b6' title="New Task" onPress={() => console.log("notes tapped!")}> <Icon name="md-create" style={styles.actionButtonIcon} /> </ActionButton.Item> <ActionButton.Item buttonColor='#3498db' title="Notifications" onPress={() => {}}> <Icon name="md-notifications-off" style={styles.actionButtonIcon} /> </ActionButton.Item> <ActionButton.Item buttonColor='#1abc9c' title="All Tasks" onPress={() => {}}> <Icon name="md-done-all" style={styles.actionButtonIcon} /> </ActionButton.Item> </ActionButton> </View> ); } } const styles = StyleSheet.create({ actionButtonIcon: { fontSize: 20, height: 22, color: 'white', }, }); 其中,ActionButton组件是一个容器组件,即我们上面看到的“+”组件,而ActionButton.Item组件则是子组件。这有点类似于Android的RadioGrop和RadioButton的关系。 参数说明 ActionButton size:按钮的大小,默认为56 active:是否显示按钮 position:按钮的位置,可以为left center right offsetX:X轴上的偏移位置 offsetY:Y轴上的偏移位置 onPress:点击事件 onLongPress:长按事件 buttonText:按钮标题 verticalOrientation:弹出按钮的方向,up 或者 down renderIcon:可以自定义按钮显示的样式,默认是一个加号 ActionButton.Item size:按钮的大小,默认为56 title:按钮标题 buttonColor:按钮颜色 onPress:点击事件 当然除了上面介绍的一些常用属性外,react-native-action-button还有一些其他的属性,大家可以通过官方资料来学习。
CountDownLatch(闭锁)是一个很有用的工具类,利用它我们可以拦截一个或多个线程使其在某个条件成熟后再执行。 说到这,给大家举一个最典型的例子:假设一条流水线上有三个工作者:worker0,worker1,worker2。有一个任务的完成需要他们三者协作完成,worker2可以开始这个任务的前提是worker0和worker1完成了他们的工作,而worker0和worker1是可以并行他们各自的工作的。 如果使用普通的线程阻塞方式,我想大家很容易就会想到使用join的方式来做。当在当前线程中调用某个线程 thread 的 join() 方法时,当前线程就会阻塞,直到thread 执行完成,当前线程才可以继续往下执行。 如果使用这种方式编码实现的话,代码如下: public class Worker extends Thread { private String name; private long time; public Worker(String name, long time) { this.name = name; this.time = time; } @Override public void run() { try { System.out.println(name+"开始工作"); Thread.sleep(time); System.out.println(name+"工作完成,耗费时间="+time); } catch (InterruptedException e) { e.printStackTrace(); } } } 然后我们添加一个测试方法: public class Test { public static void main(String[] args) throws InterruptedException { // TODO 自动生成的方法存根 Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000)); Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000)); Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000)); worker0.start(); worker1.start(); worker0.join(); //调用join阻塞worker0 worker1.join(); //调用join阻塞worker1 System.out.println("准备工作就绪"); worker2.start(); } } 然后运行上面的代码,我们可以发现就可以满足上面的结果。 除此之外,我们还可以使用CountDownLatch来实现上面的效果,说到这就不得不说下CountDownLatch的一个实现原理。 CountDownLatch CountDownLatch类是位于java.util.concurrent包下的一个并发工具类,是通过一个计数器来实现的,计数器的初始值为线程的数量。 每当一个线程完成了自己的任务后,计数器的值就会减1。当计数器值到达0时,它表示所有的线程已经完成了任务,然后在闭锁上等待的线程就可以恢复执行任务。也就是说,构造器中的计数值(count)实际上就是闭锁需要等待的线程数量,这个值只能被设置一次,而且CountDownLatch没有提供任何机制去重新设置这个计数值。当这个CountDownLatch数量归0后,其他的线程采用执行的机会。 与CountDownLatch的第一次交互是主线程等待其他线程,主线程必须在启动其他线程后立即调用CountDownLatch.await()方法。这样主线程的操作就会在这个方法上阻塞,直到其他线程完成各自的任务。 例如,对于文章开头的实例,要实现同样的效果,我们需要做以下的修改。 public class Worker extends Thread { private String name; private long time; private CountDownLatch countDownLatch; public Worker(String name, long time, CountDownLatch countDownLatch) { this.name = name; this.time = time; this.countDownLatch = countDownLatch; } @Override public void run() { try { System.out.println(name+"开始工作"); Thread.sleep(time); System.out.println(name+"工作完成,耗费时间="+time); countDownLatch.countDown(); System.out.println("countDownLatch.getCount()="+countDownLatch.getCount()); } catch (InterruptedException e) { e.printStackTrace(); } } } 然后,我们编写一个测试用例: public class Test { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(2); Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000), countDownLatch); Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000), countDownLatch); Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000), countDownLatch); worker0.start(); worker1.start(); //立即调用CountDownLatch.await() countDownLatch.await(); System.out.println("准备工作就绪"); worker2.start(); } } 试想以下,有下面一种应用场景:假设worker的工作可以分为两个阶段,work2 只需要等待work0和work1完成他们各自工作的第一个阶段之后就可以开始自己的工作了,而不是场景1中的必须等待work0和work1把他们的工作全部完成之后才能开始。 这种情况下,join是没办法实现这个场景的,而CountDownLatch却可以,因为它持有一个计数器,只要计数器为0,那么主线程就可以结束阻塞往下执行。相关代码如下: public class Worker extends Thread { private String name; private long time; private CountDownLatch countDownLatch; public Worker(String name, long time, CountDownLatch countDownLatch) { this.name = name; this.time = time; this.countDownLatch = countDownLatch; } @Override public void run() { try { System.out.println(name+"开始工作"); Thread.sleep(time); System.out.println(name+"第一阶段工作完成"); countDownLatch.countDown(); Thread.sleep(2000); //这里就姑且假设第二阶段工作都是要2秒完成 System.out.println(name+"第二阶段工作完成"); System.out.println(name+"工作完成,耗费时间="+(time+2000)); } catch (InterruptedException e) { e.printStackTrace(); } } } 测试方法: public class Test { public static void main(String[] args) throws InterruptedException { CountDownLatch countDownLatch = new CountDownLatch(2); Worker worker0 = new Worker("worker0", (long) (Math.random()*2000+3000), countDownLatch); Worker worker1 = new Worker("worker1", (long) (Math.random()*2000+3000), countDownLatch); Worker worker2 = new Worker("worker2", (long) (Math.random()*2000+3000), countDownLatch); worker0.start(); worker1.start(); countDownLatch.await(); System.out.println("准备工作就绪"); worker2.start(); } } 运行上面的测试用例,可以看到满足我们条件的输出: worker0开始工作 worker1开始工作 worker1第一阶段工作完成 worker0第一阶段工作完成 准备工作就绪 worker2开始工作 worker1第二阶段工作完成 worker1工作完成,耗费时间=5521 worker0第二阶段工作完成 worker0工作完成,耗费时间=6147 worker2第一阶段工作完成 worker2第二阶段工作完成 worker2工作完成,耗费时间=5384
Vue 是一套用于构建用户界面的渐进式框架,与其它大型的页面框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与现代化的工具链以及各种支持类库结合使用时,Vue 也完全能够为复杂的单页应用提供驱动。 关于Vue环境搭建的内容,本文不再介绍,不清楚的同学可以访问Vue环境搭建或者查看官网相关内容的介绍:https://cn.vuejs.org/v2/guide/installation.html 初始化项目 首先,我们使用如下的命令创建一个Vue项目。 vue init webpack 项目名字 然后项目会有一些初始化的设置,该部分内容的具体含义如下: Target directory exists. Continue? (Y/n) :直接回车默认(然后会下载 vue2.0模板); Project name (vue-test) :项目名称,直接回车默认; Project description (A Vue.js project) :Vue项目描述,直接回车默认; Author:项目拥有者名称,直接回车默认; Use ESLint to lint your code? n:是否启用eslint检测,选择"N"; pick an eslint preset: 默认Standard; setup unit tests with karma + mocha?N:是否需要添加单元测试,选择不需要; setup e2e tests with Nightwatch?N:是否需要添加E2E测试,选择不需要。 然后打开终端,执行“npm install”命令安装依赖库。 cd 项目名字 npm install 如果开发中需要安装一些额外的第三方库,可以使用如下面的命令: npm install 库名称 –save 然后使用下面的命令启动或者发布项目 npm run dev //启动项目 npm run build //发布项目 Vue目录结构介绍 打开新建的Vue项目,其目录结构如下图所示。 ├── index.html 入口页面 ├── build 构建脚本目录 │ ├── build-server.js 运行本地构建服务器,可以访问构建后的页面 │ ├── build.js 生产环境构建脚本 │ ├── dev-client.js 开发服务器热重载脚本,主要用来实现开发阶段的页面自动刷新 │ ├── dev-server.js 运行本地开发服务器 │ ├── utils.js 构建相关工具方法 │ ├── webpack.base.conf.js wabpack基础配置 │ ├── webpack.dev.conf.js wabpack开发环境配置 │ └── webpack.prod.conf.js wabpack生产环境配置 ├── config 项目配置 │ ├── dev.env.js 开发环境变量 │ ├── index.js 项目配置文件 │ ├── prod.env.js 生产环境变量 │ └── test.env.js 测试环境变量 ├── mock mock数据目录 │ └── hello.js ├── package.json npm包配置文件,里面定义了项目的npm脚本,依赖包等信息 ├── src 项目源码目录 │ ├── main.js 入口js文件 │ ├── app.vue 根组件 │ ├── components 公共组件目录 │ │ └── title.vue │ ├── assets 资源目录,这里的资源会被wabpack构建 │ │ └── images │ │ └── logo.png │ ├── routes 前端路由 │ │ └── index.js │ ├── store 应用级数据(state) │ │ └── index.js │ └── views 页面目录 │ ├── hello.vue │ └── notfound.vue ├── static 纯静态资源,不会被wabpack构建。 └── test 测试文件目录(unit&e2e) └── unit 单元测试 ├── index.js 入口脚本 ├── karma.conf.js karma配置文件 └── specs 单测case目录 └── Hello.spec.js 在上面的文件结构中,重点注意下面的内容: index.html文件入口; src放置组件和入口文件; node_modules为依赖的模块; config中配置了路径端口值等; build中配置了webpack的基本配置、开发环境配置、生产环境配置等。 Vue基础指令 Vue内置了很多有用的指令,这些指令通常作用在HTML元素上以v-开头,可将指令视作特殊的HTML属性(attribute)。下面就一些常用的指令给大家简单介绍下。 v-if指令 条件判断指令,根据表达式值的真假来插入或删除元素,表达式返回一个布尔值。语法规则如下: v-if = "expression" 例如有下面一个实例, <!DOCTYPE html> <html lang="en"> <head> <title>v-if指令</title> <meta charset="utf-8"> <script src="https://unpkg.com/vue/dist/vue.js"></script> </head> <body> <div id="app"> <h1 v-if="yes">Yes</h1> <h1 v-if="no">No</h1> <h1 v-if="age > 25">Age: {{age}}</h1> </div> <script> var app = new Vue({ el: '#app', data: { yes: true,//值为真,插入元素 no: false,//值为假,不插入元素 age: 28 } }) </script> </body> </html> 运行结果为: yes age: 28 v-show指令 条件渲染指令,与v-if不同的是,无论v-show的值为true或false,元素都会存在于HTML代码中;而只有当v-if的值为true,元素才会存在于HTML代码中。v-show指令只是设置了元素CSS的style值,v-show指令的语法如下: v-show = "expression" 例如: <!DOCTYPE html> <html lang="en"> <head> <title>v-show指令</title> <meta charset="utf-8"> <script src="https://unpkg.com/vue/dist/vue.js"></script> </head> <body> <div id="app"> <h1 v-show="yes">Yes</h1> <h1 v-show="no">No</h1> <h1 v-show="age > 25">Age: {{age}}</h1> </div> <script> var app = new Vue({ el: '#app', data: { yes: true,//值为真 no: false,//值为假 age: 28 } }) </script> </body> </html> v-else指令 可配合v-if或v-show使用,v-else指令必须紧邻v-if或v-show,否则该命令无法正常工作。v-else绑定的元素能否渲染在HTML中,取决于前面使用的是v-if还是v-show。若前面使用的是v-if,且v-if值为true,则v-else元素不会渲染;若前面使用的是v-show,且v-show值为true,则v-else元素仍会渲染到HTML。 v-for指令 循环指令,基于一个数组渲染一个列表,与JavaScript遍历类似。语法格式如下: v-for = "item in items" 例如,在数组todos,依次遍历数组todos中的每个元素,将text部分显示。 <!DOCTYPE html> <html lang="en"> <head> <title>v-for指令</title> <meta charset="utf-8"> <script src="https://unpkg.com/vue/dist/vue.js"></script> </head> <body> <div id="app"> <ol> <li v-for="todo in todos">{{todo.text}}</li> </ol> </div> <script> var app = new Vue({ el: '#app', data: { todos: [ {text: 'learn Javascript'}, {text: 'learn Vue'}, {text: 'learn ...'} ] } }) </script> </body> </html> v-bind指令 v-bind用于给DOM绑定元素属性。例如: v-bind:argument="expression" 其中,argument通常是HTML元素的特性,如:v-bind:class="expression"。v-bind指令可以缩写为:冒号。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <script src="https://unpkg.com/vue/dist/vue.js"></script> </head> <body> <div id="app"> <span v-bind:title="message">Hover your mouse over me</span> </div> <script> var app = new Vue({ el: '#app', data: { message: 'you loaded this page on ' + new Date() } }) </script> </body> </html> v-on指令 v-on用于监听DOM事件,语法与v-bind类似,如监听点击事件。 v-on:click="doSth" 其中,v-on指令可以缩写为@符号。如:@click="doSth"。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <script src="https://unpkg.com/vue/dist/vue.js"></script> </head> <body> <div id="app"> <p><input type="text" v-model="message"></p> <p> <button v-on:click="greet">Greet</button> </p> <p> <button v-on:click="say('hello vue')">Hello</button> </p> </div> <script> var app = new Vue({ el: '#app', data: { message: 'Greet to Vue' }, methods: { greet: function(){ alert(this.message); }, say: function(msg){ alert(msg); } } }) </script> </body> </html> 附:vue.js 官网:https://vuejs.org/vue.js 中文网: http://vuefe.cn/vue-router 文档:http://router.vuejs.org/zh-cn/index.html/vuex 文档:http://vuex.vuejs.org/webpack 文档:https://webpack.github.io/docs/ES2015 入门教程:http://es6.ruanyifeng.com/scss 文档:http://sass-lang.com/documentation/file.SASS_REFERENCE.htmlmocha 文档: http://mochajs.org/express 中文官网:http://expressjs.com/zh-cn/
众所周知,React Native的页面元素是由一个一个的组件所构成的,这些组件包括系统已经提供的组件,如View、TextInput等,还有一些第三方库提供的组件,以及自定义的组件。通常在封装组件的时候都会继承Component,不过在React 15.3版本中系统提供了PureComponent,下面就来看一下这两个组件的区别。 首先声明,PureComponent是Component的一个优化组件,在React中的渲染性能有了大的提升,可以减少不必要的 render操作的次数,从而提高性能。PureComponent 与Component 的生命周期几乎完全相同,但 PureComponent 通过prop和state的浅对比可以有效的减少shouldComponentUpate()被调用的次数。 PureComponent VS Component 原理 当组件更新时,如果组件的props和state都没发生改变,render方法就不会触发,省去 Virtual DOM 的生成和比对过程,达到提升性能的目的。原理就是 React会自动帮我们做了一层浅比较,涉及的函数如下: if (this._compositeType === CompositeTypes.PureClass) { shouldUpdate = !shallowEqual(prevProps, nextProps) || !shallowEqual(inst.state, nextState); } PureComponent 的shouldComponentUpdate() 只会对对象进行浅对比,如果对象包含复杂的数据结构,它可能会因深层的数据不一致而产生错误的否定判断。所以,在实际使用的时候,当你期望只拥有简单的props和state时,才去继承PureComponent ,或者在你知道深层的数据结构已经发生改变时使用forceUpate() 。 Component 首先要看Component的生命周期示意图。下面就Component的生命周期 生命周期 getDefaultProps 执行过一次后,被创建的类会有缓存,映射的值会存在this.props,前提是这个prop不是父组件指定的。这个方法在对象被创建之前执行,因此不能在方法内调用this.props ,另外,注意任何getDefaultProps()返回的对象在实例中共享,而不是复制。 getInitialState 控件加载之前执行,返回值会被用于state的初始化值。 componentWillMount 执行一次,在初始化render之前执行,如果在这个方法内调用setState,render()知道state发生变化,并且只执行一次。 render 调用render()方法时,首先检查this.props和this.state返回一个子元素,子元素可以是DOM组件或者其他自定义复合控件的虚拟实现 。 如果不想渲染可以返回null或者false,这种场景下,React渲染一个<noscript>标签,当返回null或者false时,ReactDOM.findDOMNode(this)返回null 。render()方法是很纯净的,这就意味着不要在这个方法里初始化组件的state,每次执行时返回相同的值,不会读写DOM或者与服务器交互,如果必须需要与服务器进行交互,可以在componentDidMount()方法中实现或者其他生命周期的方法中实现。 componentDidMount 在初始化render之后执行,且只执行一次,在这个方法内,可以访问任何组件,componentDidMount()方法中的子组件需要在父组件之前执行。 shouldComponentUpdate 这个方法在初始化render()时不会执行,当props或者state发生变化时执行,并且是在render之前执行,当新的props或者state不需要更新组件时,返回false。 shouldComponentUpdate: function(nextProps, nextState) { return nextProps.id !== this.props.id; } 当shouldComponentUpdate()返回false时,render()方法将不会执行,componentWillUpdate()和componentDidUpdate()也不会被调用。 默认情况下,shouldComponentUpdate方法返回true,防止state快速变化时的问题,但是如果state不变,props只读,可以直接覆盖shouldComponentUpdate用于比较props和state的变化,决定UI是否更新,当组件比较多时,使用这个方法能有效提高应用性能。 componentWillUpdate 当props和state发生变化时执行,执行该函数,并且在render方法之前执行,紧接着这个函数就会调用render()来更新界面。 componentDidUpdate 组件更新结束之后执行,在初始化render时不执行。 void componentDidUpdate( object prevProps, object prevState ) componentWillReceiveProps 当props发生变化时执行,初始化render时不执行,在这个回调函数里面,你可以根据属性的变化,通过调用this.setState()来更新你的组件状态,旧的属性还是可以通过this.props来获取,这里调用更新状态是安全的,并不会触发额外的render调用。 componentWillReceiveProps: function(nextProps) { this.setState({ likesIncreasing: nextProps.likeCount > this.props.likeCount }); } componentWillUnmount 当组件要被从界面上移除的时候,就会调用componentWillUnmount(),在这个函数中,可以做一些组件相关的清理工作,例如取消计时器、网络请求等。 PureComponent 上面为大家讲了Component的生命周期,仔细阅读可以发现,在React 的Component的生命周期中,有一个shouldComponentUpdate方法,该方法默认返回值是true。Component的shouldComponentUpdate函数源码如下: shouldComponentUpdate(nextProps, nextState) { return true; } 也就是说,不管有没有改变组件的props或者state属性,都需要调用shouldComponentUpdate()来重绘界面,这极大的降低了React的渲染效率。因此,从React在15.3版本中发布了一个优化的PureComponent组件来优化React的渲染效率。而PureComponent的shouldComponentUpdate是这样的。 if (this._compositeType === CompositeTypes.PureClass) { shouldUpdate = !shallowEqual(prevProps, nextProps) || ! shallowEqual(inst.state, nextState); } PureComponent使用浅比较判断组件是否需要重绘,因此,下面对数据的修改并不会导致重绘: options.push(new Option()) options.splice(0, 1) options[i].name = "Hello" 这些例子都是在原对象上进行修改,由于浅比较是比较指针的异同,所以会认为不需要进行重绘。为了避免出现这些问题,推荐使用immutable.js。immutable.js会在每次对原对象进行添加,删除,修改使返回新的对象实例,任何对数据的修改都会导致数据指针的变化。 实例 下面,我们通过一个实例来比较下PureComponent和Component在页面渲染上面的效率。 import React, { PureComponent,Component } from 'react'; import { AppRegistry, StyleSheet, Text, View, Button } from 'react-native'; export default class test extends PureComponent { constructor(props){ super(props); this.state = { number : 1, numbers: [], }; } render() { return ( <View style={styles.container}> <Button title={'number + 1'} onPress={this.numberAdd.bind(this)} /> <Text>number value: {this.state.number}</Text> <Button title={'numbers + 1'} onPress={this.numbersAdd.bind(this)} /> <Text>numbers length: {this.state.numbers.length}</Text> </View> ); } numberAdd(){ this.setState({number: ++this.state.number }); } numbersAdd(){ let numbers = this.state.numbers; numbers.push(1); this.setState({numbers: numbers}); console.log(this.state.numbers); } } const styles = StyleSheet.create({ container: { flex: 1, justifyContent: 'center', alignItems: 'center', backgroundColor: '#F5FCFF', }, welcome: { fontSize: 20, textAlign: 'center', margin: 10, }, instructions: { textAlign: 'center', color: '#333333', marginBottom: 5, }, }); AppRegistry.registerComponent('test', () => test); 如何让PureComponent重绘 当然,有时候如果继承PureComponent后需要在变化的时候重绘界面,我们要怎么做? 重写shouldUpdateComponent方法; props或者state增减参数; 关于Component与PureComponent的内容就为大家介绍到这里,一句话:PureComponent是Component的一个优化组件,通过减少不必要的 render操作的次数,从而提高界面的渲染性能。
Android App Bundles 在今年的Google I/O大会上,Google向 Android 引入了新 App 动态化框架(即Android App Bundle,缩写为AAB),与Instant App不同,AAB是借助Split Apk完成动态加载,使用AAB动态下发方式,可以大幅度减少应用体积。现在只须在 Android Studio 中构建一个应用束 (app bundle),就可以将应用所需的全部内容 (适用于所有设备) 都涵盖在内:所有语言、所有设备屏幕大小、所有硬件架构。 下面是Dynamic Delivery示意效果图: 不过要想体验Dynamic Delivery,需要先下载Android Studio 3.2 学习Android App Bundles可以将它和Split Apks来对比学习。 Split Apks split apks是Android 5.0开始提供多apk构建机制,借助split apks可以将一个apk基于ABI和屏幕密度两个维度拆分城多个apk,这样可以有效减少apk体积。当用户下载应用程序安装包时,只会包含对应平台的so和资源。因为需要google play支持,所以国内就没戏了。针对不同cpu架构问题,国内应用开发商大部分都会将so文件只放在armabi目录下,如此做虽然可以有效减少包体积,但可能带来性能问题。split apks详细的内容可以访问下面的链接:https://link.zhihu.com/?target=https%3A//developer.android.com/studio/build/configure-apk-splits%3Fauthuser%3D2 Split Apks的运作原理有点类似于Android的组件化,安装应用程序时,首先安装base apk,然后安装split apks。为了说明splite apks运作原理,来看一下Android 5.0关于splite apks的源码。 打开ApplicationInfo类中,可以看到如下信息: /** * Full paths to zero or more split APKs that, when combined with the base * APK defined in {@link #sourceDir}, form a complete application. */ public String[] splitSourceDirs; /** * Full path to the publicly available parts of {@link #splitSourceDirs}, * including resources and manifest. This may be different from * {@link #splitSourceDirs} if an application is forward locked. */ public String[] splitPublicSourceDirs; LoadeApk中有PathClassLoader和Resources创建过程。LoadedApk#mClassLoader是PathClassLoader实例引用,接着看PathClassLoader的创建过程。 public ClassLoader getClassLoader() { synchronized (this) { if (mClassLoader != null) { return mClassLoader; } if (mIncludeCode && !mPackageName.equals("android")) { ...... final ArrayList<String> zipPaths = new ArrayList<>(); final ArrayList<String> libPaths = new ArrayList<>(); ....... zipPaths.add(mAppDir); //将split apk路径追加到zipPaths中 if (mSplitAppDirs != null) { Collections.addAll(zipPaths, mSplitAppDirs); } libPaths.add(mLibDir); ...... final String zip = TextUtils.join(File.pathSeparator, zipPaths); final String lib = TextUtils.join(File.pathSeparator, libPaths); ...... //如果mSplitAppDirs不为空,则zip将包含split apps所有路径。 mClassLoader = ApplicationLoaders.getDefault().getClassLoader(zip, lib, mBaseClassLoader); StrictMode.setThreadPolicy(oldPolicy); } else { if (mBaseClassLoader == null) { mClassLoader = ClassLoader.getSystemClassLoader(); } else { mClassLoader = mBaseClassLoader; } } return mClassLoader; } } 在创建PathClassLoader时,dex文件路径包含base app和split apps路径,LoadedApk#mResources是Resources实例引用,Resources的源码如下: public Resources getResources(ActivityThread mainThread) { if (mResources == null) { mResources = mainThread.getTopLevelResources(mResDir, mSplitResDirs, mOverlayDirs, mApplicationInfo.sharedLibraryFiles, Display.DEFAULT_DISPLAY, null, this); } return mResources; } 可以发现:split apks资源路径(LoadedApk#mSplitResDirs)也会被增加至Resources中。 Android App Bundles 下面再来看Android App Bundles,Android App Bundle 支持模块化,通过Dynamic Delivery with split APKs,将一个apk拆分成多个apk,按需加载(包括加载C/C++ libraries),这样开发者可以随时按需交付功能,而不是仅限在安装过程中。 Android App Bundle 通常会包括以下几个文件: Base Apk:首次安装的apk,公共代码和资源,所以其他的模块都基于Base Apk; Configuration APKs:native libraries 和适配当前手机屏幕分辨率的资源; Dynamic feature APKs:不需要在首次安装就加载的模块。 AAB并不是一个插件化框架,它利用的是Android Framework提供的split apks技术来完成的,而所有安装split apk工作均是通过IPC交由google play完成。具体使用时,在Android Studio新增一项module——Dynamic Feature Module。在创建dynamic_feature时,有两个选项是默认勾选的,当然我们也可以更改其状态。 Enable on-demand: 是否支持按需下载模式。如果不支持,那么该feature则在安装app时被安装。 Fusing: 如果app运行在Android 5.0(不包括5.0)以下,勾选Fusing则表示该feature会被一起打包至完整apk中。 下面看一个简单的实例程序。在示例中,有四个feature,通过module名很清楚这些feature是举例介绍如何访问代码、资源、so等。dynamic feature module编译所使用的插件com.android.dynamic-feature,那么该插件有何独特之处,通过编译产物分析,运行示例后,发现在所有dynamic feature模块build目录下均会生成apk文件。 接着反编译主apk(com.android.application插件生成产物),会发现两个有趣的现象: 所有dynamic feature module的代码、资源、so并未打包至主apk中。 主apk manifest信息包括所有dynamic feature module的manifest,即feature manifest会被合并至主apk manifest中。 Build Bundle(s) Android App Bundle提供一种全新编译产物格式文件aab,使用Android Studio提供的App Bundle即可。如上图,当选择Build Bundle(s)时,在主工程build目录下回生成bundle.aab文件,该文件是压缩格式文件,解压该aab文件内容如下。 从aab文件内容,可知其包含base和feature的代码、资源、so等,同时还有BundleConfig.pb这一配置文件,该配置文件是google play用于拆分apk。如果我们需要在google play上支持动态发布,只需要上传aab文件即可,后续工作交给google play完成。 Play Core Library Play Core Library是AAB提供的核心库,用于下载、安装dynamic feature模块。另外,我们也可以用这些API下载on-demand模块用于instant app。关于Play Core Library具体如何使用,大家可以查看相关文档。 兼容性问题处理 6.0以下版本 当app运行设备版本不高于6.0时,需要使用SplitCompat库才能立即访问下载模块代码和资源。AAB提供SplitCompatApplication类用于开启SplitCompat。 public class SplitCompatApplication extends Application { public SplitCompatApplication() { } protected void attachBaseContext(Context var1) { super.attachBaseContext(var1); SplitCompat.install(this); } } 在Application#attachBaseContext(Context)中调用SplitCompat.install(Context)。在该方法中主要完成split apks代码(dex和so)和资源的安装。下面是一些兼容的条件分支语句: public static a a() { if (VERSION.SDK_INT == 21) { //com.google.android.play.core.splitcompat.b.c return new c(); } else if (VERSION.SDK_INT == 22) { //com.google.android.play.core.splitcompat.b.f return new f(); } else if (VERSION.SDK_INT == 23) { //com.google.android.play.core.splitcompat.b.g return new g(); } else { throw new AssertionError(); } } 高于8.0版本 在Android 8.0中,Instant Apps相关代码嵌入至Framework。因此如果on-demand模块用于Instant Apps中,需要在on-demand下载成功中,调用SplitInstallHelper.updateAppInfo(Context)。 public static void updateAppInfo(Context var0) { if (VERSION.SDK_INT > 25) { a.a("Calling dispatchPackageBroadcast!", new Object[0]); try { Class var1; Method var2; (var2 = (var1 = Class.forName("android.app.ActivityThread")).getMethod("currentActivityThread")).setAccessible(true); Object var3 = var2.invoke((Object)null); Field var4; (var4 = var1.getDeclaredField("mAppThread")).setAccessible(true); Object var5; (var5 = var4.get(var3)).getClass().getMethod("dispatchPackageBroadcast", Integer.TYPE, String[].class).invoke(var5, 3, new String[]{var0.getPackageName()}); a.a("Calling dispatchPackageBroadcast", new Object[0]); } catch (Exception var6) { a.a(var6, "Update app info with dispatchPackageBroadcast failed!", new Object[0]); } } } 从上述代码得知其反射调用ActivityThread#dispatchPackageBroadcast方法。最终是调用至LoadedApk#updateApplicationInfo。该方法做了如下事情 重新创建mClassLoader 重新创建mResources 更新applicationInfo(调用LoadedApk#setApplicationInfo完成)。
Atlas简介 Atlas是一个Android客户端容器框架,主要提供了组件化、动态性、解耦化的支持,支持在编码期、Apk运行期以及后续运维修复期的各种问题。Atlas目前支持的主要功能有: 在工程期,实现工程独立开发,调试功能,工程模块的独立; 在运行期间,实现完整的组件生命周期映射,类隔离等机制; 在运维期间,提供快速增量的更新修复功能,快速升级。 Atlas是工程期和运行期共同起作用的框架,它尽量将一些工作放在工程期,这样保证运行期的简单、稳定。下面是Atlas组件化的一个框架原理图。 上图是手机淘宝的apk,第一层目录上与标准的apk是完全一样的。在App会有很多的so文件,每个so文件如果解开来看它的结构类似于完整的apk,但本身不能独立运行,它运行在整个容器里,每一个组件都是独立的Bundle。例如,手淘的模块层次划分如下图。从模块来划分,手淘APK可以分为两层,上层是经过拆分的业务Bundle,扫码、评价、详情,各个业务之间可以进行功能的调用,可以通过路由调度到其他业务方。下层是共享的底层中间件,向业务方开放各种能力,如网络库、图片库等,会在容器里进行统一地把控,这样做的好处是包做到尽可能小,第二是性能佳。Atlas的整体分为5层:第一层称之为Hack层,包括OS Hack toolkit & verifier,这里对系统能力做一些扩展,然后做一些安全校验。 第二层是Bundle Framework,就是的容器基础框架,提供Bundle管理、加载、生命周期、安全等一些最基本的能力。 第三层是运行期管理层,包括清单,会把所有的Bundle和它们的能力列在一个清单上,在调用时方便查找;另外是版本管理,会对所有Bundle的版本进行管理;再就是代理,这里就是和业界一些插件化框架机制类似的地方,会代理系统的运行环境,让Bundle运行在的容器框架上;然后还有调试和监控工具,是为了方便工程期开发调试。 第四层是业务层了,这里向业务方暴露了一些接口,如框架生命周期、配置文件、工具库等等。 组件化技术细节 关于Bundle的生命周期会提供细粒度的节点,比如下面是一个Bundle从加载到运行的周期: startInstall:开始加载。这个时候框架会做一些拷贝文件、释放lib、加载Bundle的事情; Installed:加载完毕。这时框架会注入资源路径,创建class loader; resolved:解析完毕,框架会检查组件配置是否合法,是否能被解析; active:运行组件,即开始运行组件Bundle; started:运行成功。 组件化涉及到的第一个问题是Manifest处理,一个是因为来源很多,有宿主Manifest、Aar Manifest以及组件Manifest,另外不同组件的Manifest经常发生变化,要求灵活地去处理。这里的做法是在工程期将所有的Manifest进行Merge操作,这里需要注意的是Bundle的依赖单独Merge,因为这里涉及到依赖仲裁的问题。最后解析各个Bundle的Merge Manifest,得到整包的BundleInfoList,就是上面提到的Bundle信息清单。第二个是类加载,这里利用Delegate ClassLoader来动态加载组件的类。Delegate ClassLoader先查找宿主Bundle的PathClassLoader,然后根据前面的BundleList找到对应的BundleClassLoader。第三个是资源,会用自己的DelegeteResources替换掉系统的resource,Bundle的资源会逐个在安装的时候添加到AssertPath,由于添加Bundle的顺序非固定,不分区会导致资源查找错乱。 另外,Dalvik和ART上的资源查找过程顺序是不一样的,加上小米等系统会重写自己的resources,所以会适配不同的机型,往后追加AssetsPath或者往前追加,系统AssetManager是个单例,默认往后追加,如果往前追加,则需要重新创建AssetsManager对象,同样主dex动态部署的时候要达到替换原有resource的目的,必须保证插入顺序与查找顺序一致。 还有需要注意的是,每次更新resourceTable的时候,必须保证apkresource,runtime的系统resource,例如webview,bundle resource都已经添加成功,而且唯一,顺序正确。 不同Bundle的资源可能发生命名冲突,是用了一种相对来说简单的方法,将各自的Bundle分配成不同的ID,保证所有的业务资源不会产生冲突,尽量将问题放到工程期解决。在很多代码里,通过反射来调用整个资源,在5.0以上的系统是没有问题的,它只找第一个,对业务代码而言,原来是怎么写的,今天还是怎么去写。 关于组件化性能这一部分,引入了按需加载,因为手淘APK有70多个Bundle,每个用户真正用的时候只需要5或10个,所以不需要加载所有的Bundle。Bundle之间进行隔离,通过Android四大原生组件进行交互,这样Bundle之间可以比较好的解耦。所有调用的入口都是基于BundleInfolist去做的,根据这个清单信息,得到组件所在Bundle,如果需要加载,就进行install、dexopt等操作。 另外,对于解决组件依赖问题,定义了两种新的组件格式Awb(业务Bundle)和solib(so库),前者与AAR一致,不过不添加本地lib,在构建的时候做依赖仲裁区分,后者是Native so库的依赖。Awb其实就是AAR,只是后缀修改了,如果你的包放在宿主Bundle就用AAR,如果是组件Bundle就用Awb。 对于业务Bundle的依赖,在构建期会将宿主Bundle和业务Bundle及其依赖分别打包,然后按照最短路径、第一声明原则进行树状仲裁,得到每个Bundle需要的依赖,在打包的时候会将依赖库放到各自的Bundle里去。最后是APK构建,对它做了比较大的调整。上面的图中,其实左边这一部分是一个标准的APK的构建过程,包括处理,编译,到签名的过程。这个不同的地方是多了Awb需要特殊处理,其中Awb的资源根据宿主的resource.ap_和包内资源构建,R文件由Bundle R资源和宿主R资源合并而来,然后对Aapt进行了修改,对每个awb分配不同的packageId,然后进行统一混淆,生产各个AWB的Dex,打包为APK,签名之后复制到libs,改名为so文件,然后合并到taobao APK. 这就是组件化的整个过程。 Atlas动态化 在一个容器框架内,组件化和动态化是相辅相成的,组件只是解决了解耦的问题,但如果想要随时发包,就必须让容器框架具备动态化能力。在完成了Atlas的组件化之后,做了动态化的支持。动态化的好处一个是包的大小缩减,可以将一些包在运行后下载到应用中,另一个是具备动态发版和修复能力。 Atlas提供了动态部署的能力,主要目标是动态业务发布,以及问题修复。它基于手淘自研差量算法,主Bundle基于ClassLoader机制,业务Bundle基于差量merge,支持全业务类型。 另外,Atlas也支持Andfix作为插件使用,目标是快速故障修复,它的原理基于Native hook,主要做方法的修改,在实际中可以两个一起用。在工程构建期适配之后,可以做到一套代码两套方案通用。该方案的原理图如下:自研动态部署功能实现原理,首先,对于Dex Patch的生成,通过修改Dex的字节码实现,将Dex文件转为Smali,对其中的ClassDef和ClassDataMethod结构体进行分析,可以实现删除、新增、修改类,然后通过Diff处理得到差量文件,再通过Merge处理即生成补丁。 其次是整个资源Patch的生成,分为两块,一个是业务Bundle,本来是一个不断加载的过程,它实现起来会比较简单,通过Md5 diff/BSDiff即可得到。对于主Bundle,因为安卓本身有一个限制,所有的资源必须得在base包里,新增一个资源是不生效的。所以一个做法是在打包的时候预留很多空资源。另外更新已有的资源则通过资源覆盖来完成。 最后,如果新加业务的话,会新加Activity,的做法首先在Manifest预埋一个StubActivity,然后在Instrumentation.execStartActivity()阶段进行替换,同时配合Intent setFlag模拟Activity launch mode并继续startActivity,接着System_server进程进行处理,更新ActivityStack,创建binder,并通知ActivityThread进行实例创建,最后在ActivityThread的handler里面进行拦截,更新ActivityInfo等信息,创建目标Activity。 另外在工程实践上,因为补丁的生成会涉及到Dex和资源的基线,会在部署的时候,每次发布APK包同步发布AP(基线包)到Maven,AP基线包里是所有影响基线的文件,第一是安卓APK,第二是Mapping.txt,最后是Dependency.txt,这样的话整个构建的速度会非常的快。 所以这种方式,版本的升级是不同的方式。比如今天手淘的详情要更新,会发布版本,这个版本可能不是到应用市场的版本,而是一个Patch包。业务版本的动态部署,是同步的,5.3.0到5.3.1到5.3.2,这样一个好处是只要容器版本没有升级,只要有需求,patch就可以一直升级,而且是无感知的差量升级。 说明,本部分来源于手淘组件化的分享。官方链接地址为:https://alibaba.github.io/atlas/index.html Atlas项目实践 前面我们介绍了Atlas的一些原理性的东西,总的来说,Atlas就是利用远程Bundle的下发方式,为了减少apk的安装体积,Atlas项目使用bundle的加载方式。当用户安装没有Bundle的apk文件时,就从网上下载这个bundle的so,然后加载打开,下载逻辑使用app原生代码编写,加载用按需加载的策略。 Atlas接入 首先新建一个项目,然后新建几个module,如下图。 修改配置 1,把gradle改为3.3 2,然后我们需要为Atlas添加一些配置,引用Atlas插件及依赖仓库,修改工程gradle文件。 buildscript { repositories { jcenter()} dependencies { //不需要再依赖classpath "com.android.tools.build:gradle" classpath "com.taobao.android:atlasplugin:2.3.3.beta2" } } 3,修改app的build.gradle脚本,需要注意包名的对应关系。 // 需要放最上面初始化 group = "mmc.atlastest" version = getEnvValue("versionName", "1.0.0"); def apVersion = getEnvValue("apVersion", ""); apply plugin: 'com.android.application' apply plugin: 'com.taobao.atlas' android { compileSdkVersion 25 buildToolsVersion "25.0.2" defaultConfig { applicationId "mmc.atlastest" minSdkVersion 15 targetSdkVersion 25 versionCode 1 versionName version testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:25.3.1' testCompile 'junit:junit:4.12' //atlas的依赖 compile('com.taobao.android:atlas_core:5.0.7@aar') { transitive = true } compile 'com.taobao.android:atlasupdate:1.1.4.7@aar' compile 'com.alibaba:fastjson:1.1.45.android@jar' //项目依赖 compile project(':librarybundle') compile project(':localbundle') compile project(':remotebundle') } //加入以下配置 atlas { atlasEnabled true tBuildConfig { // autoStartBundles = ['com.android.homebundle'] //自启动bundle配置 outOfApkBundles = ['remotebundle'] //远程module,列表来的,可填多个 preLaunch = 'mmc.atlastest.AtlasLaunch' //AppApplication启动之前调用,这个类下面放出代码 } patchConfigs { debug { createTPatch true } } buildTypes { debug { if (apVersion) { // 打差异补丁 gradlew assembleDebug -DapVersion=1.1.0 -DversionName=1.1.1 // 对应着本地maven仓库地址 .m2/repository/mmc/atlastest/AP-debug/1.1.4/AP-debug-1.1.4.ap baseApDependency "mmc.atlastest:AP-debug:${apVersion}@ap" patchConfig patchConfigs.debug } } } } String getEnvValue(key, defValue) { def val = System.getProperty(key); if (null != val) { return val; } val = System.getenv(key); if (null != val) { return val; } return defValue; } apply plugin: 'maven' apply plugin: 'maven-publish' publishing { // 指定仓库位置 repositories { mavenLocal() } publications { // 默认本地仓库地址 用户目录/.m2/repository/ maven(MavenPublication) { //读取ap目录上传maven artifact "${project.buildDir}/outputs/apk/${project.name}-debug.ap" //生成本地maven目录 groupId group artifactId "AP-debug" } } } 4,修改远程bundle和本地bundle的build.gradle脚本 apply plugin: 'com.android.library' apply plugin: 'com.taobao.atlas' atlas { bundleConfig{ awbBundle true } buildTypes { debug { baseApFile project.rootProject.file('app/build/outputs/apk/app-debug.ap') } } } //只添加上面的配置就行了,下面的是默认生成的 android { compileSdkVersion 25 buildToolsVersion "25.0.2" defaultConfig { minSdkVersion 15 targetSdkVersion 25 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } } dependencies { compile fileTree(include: ['*.jar'], dir: 'libs') androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile 'com.android.support:appcompat-v7:25.3.1' testCompile 'junit:junit:4.12' //依赖lib中间bundle compile project(':librarybundle') } 5,在宿主app的application中添加如下代码。 public class DemoApplication extends Application { @Override public void onCreate() { super.onCreate(); Atlas.getInstance().setClassNotFoundInterceptorCallback(new ClassNotFoundInterceptorCallback() { @Override public Intent returnIntent(Intent intent) { final String className = intent.getComponent().getClassName(); final String bundleName = AtlasBundleInfoManager.instance().getBundleForComponet(className); if (!TextUtils.isEmpty(bundleName) && !AtlasBundleInfoManager.instance().isInternalBundle(bundleName)) { //远程bundle Activity activity = ActivityTaskMgr.getInstance().peekTopActivity(); File remoteBundleFile = new File(activity.getExternalCacheDir(),"lib" + bundleName.replace(".","_") + ".so"); String path = ""; if (remoteBundleFile.exists()){ path = remoteBundleFile.getAbsolutePath(); }else { Toast.makeText(activity, " 远程bundle不存在,请确定 : " + remoteBundleFile.getAbsolutePath() , Toast.LENGTH_LONG).show(); return intent; } PackageInfo info = activity.getPackageManager().getPackageArchiveInfo(path, 0); try { Atlas.getInstance().installBundle(info.packageName, new File(path)); } catch (BundleException e) { Toast.makeText(activity, " 远程bundle 安装失败," + e.getMessage() , Toast.LENGTH_LONG).show(); e.printStackTrace(); } activity.startActivities(new Intent[]{intent}); } return intent; } }); } } 6、在app新建一个类AtlasLaunch,继承AtlasPreLauncher。 public class AtlasLaunch implements AtlasPreLauncher { @Override public void initBeforeAtlas(Context context) { } } 项目结构 然后写app的基本功能,示例如下图。下面是宿主中具体的跳转逻辑实现。 public class MainActivity extends BaseActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); } //打开远程bundle public void remote(View view){ Intent intent = new Intent(); intent.setClassName(view.getContext(), "mmc.remotebundle.RemoteActivity"); startActivity(intent); } //打开本地bundle public void local(View view){ Intent intent = new Intent(); intent.setClassName(view.getContext(), "mmc.localbundle.LocalActivity"); startActivity(intent); } //更新补丁 public void update(View view){ new AsyncTask<Void, Void, Void>() { @Override protected Void doInBackground(Void... voids) { update(); return null; } @Override protected void onPostExecute(Void aVoid) { Toast.makeText(MainActivity.this, "更新完成,请重启", Toast.LENGTH_LONG).show(); } }.execute(); } private void update(){ File updateInfo = new File(getExternalCacheDir(), "update.json"); if (!updateInfo.exists()) { showToast("更新信息不存在,请先 执行 buildTpatch.sh"); return; } String jsonStr = new String(FileUtils.readFile(updateInfo)); UpdateInfo info = JSON.parseObject(jsonStr, UpdateInfo.class); File patchFile = new File(getExternalCacheDir(), "patch-" + info.updateVersion + "@" + info.baseVersion + ".tpatch"); try { AtlasUpdater.update(info, patchFile); } catch (Throwable e) { e.printStackTrace(); showToast("更新失败, " + e.getMessage()); } } } 安装运行项目,就可以看到如下图所示的效果。此时还有以下工具需要完成:1,这个时候点击远程bundle会弹出说没有so文件,因为还没打so包呢2、点击本地bundle,是可以跳转到那个本地bundle页面3、点击更新补丁,会提示更新信息不存在 打远程bundle的so文件 下面要打包出远程bundle的so文件,补丁的差异包和更新说明。 1,打so文件,每个远程都会生成一个so文件的。打开AS的Terminal,输入:gradlew clean assembleDebug publish,然后回车,如下图:正常的话,会提示下面的正确信息。成功的话,app的build文件夹里,会生成这个so文件,这个就是远程bundle的so文件,把这个文件放到手机内存卡Android/data/mmc.atlastest/cache 文件夹里面,然后再打开app,点击“远程Bundle”,这个时候就能跳转过去了。 更新补丁,打差异包和更新说明 接着第一步,然后修改版本号,对本地Bundle进行文字修改,对app主项目也可以修改。修改后,在Terminal里面输入:gradlew clean assembleDebug -DapVersion=1.0.0 -DversionName=1.0.1回车,成功后会生成补丁差异包和更新说明,如下图:把红色圈中的两个文件,放到手机内存卡Android/data/mmc.atlastest/cache 文件夹里面,然后点击“更新补丁”,过一会,提示更新成功后,就退出杀死app,再打开就是后面修改的内容了。 Atlas使用步骤总结 1、配置好,安装1.0.0的app2、用命令“gradlew clean assembleDebug publish”打AP,得到远程Bundle的so文件3、修改版本号,修改版本内容4、用命令“gradlew clean assembleDebug -DapVersion=1.0.0 -DversionName=1.0.1”打差异包补丁和更新说明5、把上面得到的三个文件放到app的cache目录里面6、运行更新方法,杀死app,重启 附件:阿里的Atlas组件化框架Atlas组件化框架官网文档
本文为转载文章,原文地址: https://mp.weixin.qq.com/s?__biz=MzAwODY4OTk2Mg==&mid=2652046210&idx=1&sn=f5f17891c8fb45bb975c27072da5f35b&chksm=808ca3c7b7fb2ad1fd7360f71fe42a4737722ab1aed77994d3f9c67632dd5f7863fa0f32c256&mpshare=1&scene=23&srcid=05091aqpmdfrg9gC45fLiETD%23rd Android 在过去的十年经历了指数级的增长,同时,我们也见证了开发者社区的蓬勃发展。在中国、印度和巴西等国家,使用官方 IDE 的开发者数目两年内几乎增至了 3 倍。正是因为如此强劲的增长,我们感到担负起更大的责任,要为开发者带来更好的体验并为此做出大力的投资。基于来自广大开发者的反馈,我们着重精力打造出快速、简便的移动端开发,助力开发者设计更为轻量的 app 以吸引更多用户,并提高用户参与度及留存率。此外,我们也非常高兴看到 Android Things 发布 1.0 版本,从消费电子产品到酷炫遥控汽车,为广大开发者创造全新的开发机会 。我们来一起看看在 2018 Google I/O 开发者大会的第一天,Developer Keynote 都涵盖了哪些重要内容。 开发 - 打造快速、简便的移动端开发 Android Jetpack 今天,我们发布了 Android Jetpack,帮助您加快应用开发速度。作为下一代的 Android 组件,Android Jetpack 将支持库向后兼容和立即更新的优点融合到更多组件中,让开发者能够快速轻松地开发出拥有卓越性能的高质量应用。Android Jetpack 能够处理类似后台任务、UI 导航以及生命周期管理之类的活动,免去开发者编写样板代码的麻烦,专注提升应用体验。并且 Android Jetpack 完美兼容 Kotlin 语言,利用 Android KTX 大幅节省代码量。今天发布的新版 Android Jetpack 组件包括以下 4 个部分:WorkManager、Paging、Navigation 以及 Slices。 △ 中文字幕视频将于本周呈现,敬请关注! Kotlin 自从我们去年宣布支持 Kotlin 以来,该语言受到开发者社区的广泛认可。最为重要的是,95% 的开发者表示很喜欢用 Kotlin 进行 Android 的开发。使用 Kotlin 的开发者越多,喜欢它的人也越多。Play Store 中用 Kotlin 开发的应用在去年增至 6 倍,在高级开发者中有 35% 的人选择使用 Kotlin 进行开发,而且这个数字正在逐月递增。我们会继续改善 Kotlin 在支持库、工具、运行时 (runtime)、文档以及培训中的开发体验。我们今天发布的 Android KTX,包含在 Android Jetpack 中,力图优化 Kotlin 开发者体验;同时继续改善 Android Studio、Lint 支持以及 R8 优化中的工具;而且对 Android P 中的运行时 (Android Runtime) 进行微调,以此加快 Kotlin 编写的应用的运行时间。我们已经在官方文档中列出了 Kotlin 代码片段,并且会在今天发布 Kotlin 版本的《API 参考文档》。本周早些的时候,我们在优达学城 (Udacity) 开设了一门关于 Kotlin 的新课程,这对于刚开始使用 Kotlin 的新手来说是很棒的学习资源。最后一点,我们现在在 “谷歌开发者专家项目” 内为 Kotlin 专门设立了一个分块。如果您还没开始使用 Kotlin,建议您不妨试一下。 Android Studio 3.2 金丝雀版 Android Studio 3.2 引入了 Android Jetpack 支持工具,包括一款视觉导航编辑器以及全新代码重构工具。金丝雀版本同时还包含了可用于创建全新的 Android App Bundle 格式的构建工具、用于快速启动 Android 模拟器的快照功能 (Snapshot)、给下载及安装包瘦身的新 R8 优化器、以及用于测量应用对电池续航影响的新电量分析工具 (Energy Profiler) 等等。您可前往 “Android Developers 官方文档” 查看金丝雀下载页面,下载最新版本的 Android Studio 3.2。 点击屏末 | 阅读原文 | 前往 “Android Developers 官方文档” 查看蓝色字体的相应链接及其详细说明 应用分发 - 将轻量级 app 进行到底 Android App Bundle 以及 Google Play Dynamic Delivery (动态交付) 向 Android 引入新 app 模式。利用全新发布格式 —— Android App Bundle,大幅度减少应用体积。现在您只须在 Android Studio 中构建一个应用束 (app bundle),就可以将应用所需的全部内容 (适用于所有设备) 都涵盖在内:所有语言、所有设备屏幕大小、所有硬件架构。接着,在用户下载您的应用时,Google Play 的新动态交付只会传输适用于用户设备的代码和资源。人们在 Play Store 上看到的安装包体积更小,下载速度也越快,同时也节省了设备存储空间。 △ (左) 旧版 APK 交付样例 - 将全部资源都交付至设备; (右) 动态交付样例 - 只向设备交付必要资源 · 通过 Android App Bundle 实现动态功能 – Android App Bundle 支持模块化,因此开发者可以随时按需交付功能,而不是仅限在安装过程中。您可以在最新发布的 Android Studio 金丝雀版本中构造动态功能模块。参与我们的 beta 项目,发布您的应用至 Google Play。 Google Play Console Play Console 的新功能和报告能够帮助您提升应用性能并扩展业务。点击阅读有关控制面板、统计、Android vitals、发布前报告、用户获取报告以及订阅面板的相关改进项。您也可以使用我们新的发布格式 —— Android App Bundle,上传、测试以及发布应用。 Google Play Instant 早先我们在游戏开发者大会 (GDC) 上已经发布了 beta 版的 Google Play Instant,我们在今天宣布所有游戏开发者都能构建即时应用 (instant app),同时非常高兴看到《糖果传奇》上线。现在 Google Play Instant 支持全球超过 10 亿台设备,不论是通过 Play Store,搜索、还是社交网络,只要是能点击屏幕的地方都能享受到 Play Instant。为了简化即时应用的开发,我们将在这周发布对应的 Unity 游戏引擎插件服务,以及与 Cocos Creator 的 beta 版本集成。最近,我们开始测试 Google Play Instant 与 AdWords 的兼容性,让人们能直接通过通用广告活动 (Universal App campaigns) 覆盖的所有渠道里的广告直接试玩游戏。 参与度 - 赢回更多用户 Slices Slices 提供一系列 UI 模板,帮助开发者在应用中呈现丰富的动态交互式内容,支持所有 Android 系统以及提供谷歌服务的平台。Slices 可以展现实时数据、滚动内容、内联行为以及与您应用相连的深度链接,因此从播放音乐到检查预约更新,用户可以做任何事情。Slices 也可以包括像是开关或者滑块一类的互动控制元素。从今天开始创建您的 Slices,很快它们就会呈现在用户眼前。 Actions Actions 是一种轻松访问应用功能及内容的新方法,这样用户就能在恰当的时间轻松享用到您的应用。根据不同的使用习惯以及相关性高低,App Actions 呈现给用户不一样的内容,并且支持多种谷歌以及 Android 服务平台,包括谷歌搜索应用 (Google Search App)、Play Store、谷歌智能助理 (Google Assistant) 以及启动器 (launcher)。App Actions 很快就能和各位开发者见面。您可同时在应用中构建一个 Conversational Action 作为辅助用途,它适用于任何支持谷歌智能助理 (Google Assistant) 服务的设备,如扬声器和智能显示器。这两种类型的 Actions 均使用一套共用的意图类别。 更加智能的设备 - 面向 IoT 设备的强大平台 Android Things 1.0 Android Things 作为 Google 旗下的一款操作系统 (OS),能够帮助开发者规模化开发和维护物联网设备。在今年的 CES 大会上,我们宣布联想、哈曼 (Harman)、LG 以及 iHome 已经在研发由 Android Things 驱动的搭载谷歌智能助手 (Google Assistant) 的产品。 此前推出的开发者预览版的 SDK 下载次数已经突破 10 万,我们宣布 Android Things 1.0 将在本周与各位开发者见面。平台现添加对 3 种新系统模组 (System-on-Modules 或 SoMs) 的支持,并承诺在接下来的三年中提供长期支持,同时让开发者自行决定是否需要扩展支持,帮助他们更容易地设计出原型并推向市场。而同时推出的 Android Things 控制台 (Android Things Console) 更是将简化产品开发推向极致,帮助开发者定期获取 Google 最新稳定性修复包以及安全升级包,从而实现从发布、管理到设备更新的无缝连接。我们很高兴 Polk 成为我们的合作伙伴之一,而由 Android Things 驱动的 Polk Assist 扬声器也会马上与各位见面。 立即体验 Android Things:请登录 Android Developers 官方文档以及新 Android Things 社区中心,探索工具包、样例代码和社区项目。欢迎大家加入谷歌 IoT 开发者社区,随时获取更新。与此同时,我们向合作伙伴们推出 Android Things OEM 合作伙伴项目 (该项目名额有限),享受来自 Android Things 团队的技术指导与支持,打造更好的产品。如果您的公司对该项目有兴趣,请加入 Android Things OEM 合作伙伴项目。 除了这些新进展之外,我们在超过 140 个国家举办谷歌女性开发者大会 (Women Techmakers) 和谷歌开发者社区 (Google Developers Groups) 等活动,进一步增长和扩大开发者社区。同时,我们正在积极投资培训项目,譬如谷歌开发者证书项目 (Google Developers Certification),携手优达学城以及其它合作伙伴开设更多课程,帮助开发者进一步培养技术能力。今天,共有来自 50 个机构的 225 位谷歌软件开发代理商计划成员通过 Android 认证,覆盖国家超过 15 个。作为谷歌开发者专家计划的一部分,现在全球共有超过 90 位 Android 开发专家为开发者、初创企业以及公司提供积极支持,帮助他们构建并发布创新应用。 我们也将继续表彰顶尖应用和游戏开发者的杰出贡献。今年,我们将举办第三届 Google Play Awards 大赛。被提名的应用在整体质量、设计、技术性能以及创新方面都表现卓越,在各自的领域代表了最佳 Android 体验。 本次 Google I/O 开发者开设共计 48 场 与 Android 以及 Play 相关的分组讨论,为与会人士和线上观众带来绝佳机会展开深度探讨。感谢您一路以来提交给我们的宝贵意见,欢迎继续向我们反馈问题和想法,帮助我们在未来做得更好!
从事Android开发的童鞋都知道,自从去年的Google I/O大会上Kotlin被定为Android开发的官方语言以来,关于Kotlin就成为每个开发人员学习的目标,的确,Kotlin以它独有的魅力正在吸引这传统的Java程序开发人员。或许很多的童鞋已经对Kotlin进行了深入的学习,甚至已经运用到了自己的项目当中,但是还有较多同学可能只是听过Kotlin或简单了解过,本文将从宏观的角度来介绍Kotlin相关的内容。在介绍Kotlin之前,先来安利一波,本人去年年底开始写作的关于Kotlin的书下个月就要出版了,有兴趣的可以关注下,目录如下。 Kotlin简介 Kotlin是由JetBrains开发的针对JVM、Android和浏览器的静态编程语言,目前,在Apache组织的许可下已经开源。使用Kotlin,开发者可以很方便地开发移动Android应用、服务器程序和JavaScript程序。Kotlin可以将代码编译成Java字节码,也可以编译成JavaScript,方便在没有JVM的设备上运行。Kotlin是开源的,这意味着,我们可以在GitHub上下载Kotlin的全部源代码,并对它进行代码修改再发布,Kotlin在github上的开源地址为:https://github.com/JetBrains/kotlin 。 JetBrians 一家捷克的软件公司,是著名的IDE开发商,对很多的开发语言和平台都提供了相应的集成开发环境,比如Java的,OC的,JavaScript,PHP,C/C++等。而其中最著名的是IntelliJ IDEA ,Java的集成开发环境,被称为目前最好用的java IDE。而且Android Studio就是Google基于IntelliJ IDEA 开发的,由此可见Google和JetBrains的合作也是比较密切的。而从以上说明也可以看到JetBrains不仅实力强劲,这家公司对于语言设计更是有天然优势。Kotlin是集多家语言之大成。 Kotlin的优势 那么,相比Java等语言,Kotlin有什么优势呢? 1,语法简洁,吸引了其他语言的优点 Kotlin提供了大量的语法糖(有函数声明,类的创建,集合相关,范围运算符等等大量简洁的语法)、 Lambda表达式(Java8支持),简洁的函数表示法。并吸收了其他语言的优点:模板字符串,运算符重载,方法扩展,命名参数等。 2,安全性 Kotlin提供了安全符“?”,当变量可以为null时,必须使用可空安全符?进行声明,否则会出现编译错误。并且,Kotlin还提供了智能的类型判断功能,使用is类型判断后,编译器自动进行类型转换,父类引用可以调用子类接口,注意转换只在is的代码块中生效。 3,完全兼容Java Kotlin的另一个优势就是可以100%的兼容Java,Kotlin和Java之间可以相互调用。使用Kotlin进行Android或者Java服务端开发,可以导入任意的Java库。 在Android Studio中可以一键转换Java代码为Kotlin代码(Code > Convert Java File to Kotlin File.),同时Kotlin代码也可以反编译成Java代码(1.Tools>Kotlin>Show Kotlin Bytecode 2.Decompile)。 4,IDE工具支持 在Google官方发布的最新版本的Android Studio 3.0上,已经默认集成了Kotlin,对于一些老版本,也可以通过插件的方式来集成Kotlin。所以,使用JetBrains提供的IDE,可以为Kotlin开发提供最佳的环境支持。就像JetBrains所说:一门语言需要工具化,而在 JetBrains,这正是我们做得最好的地方! Kotlin是如何兼容Java的 都是Kotlin可以100%的兼容Java,那么Kotlin又是如何兼容Java的呢?下面是Kotlin的一个编译流程图。 Kotlin为什么可以兼容Java,一个主要原因是Kotlin文件在经过Kotlin编译器编译后会生成Java字节码。这跟Java文件通过Java编译器编译后生成的字节码几乎没有区别,这样JVM就能直接识别和处理Kotlin代码的功能和逻辑。 当Kotlin调用Java代码,Kotlin编译器会对调用的Java文件进行分析,以便kt文件能够生成正确的class文件。为什么这么说呢?举个列子,Java字节码有几种函数调用的方式invokespecial 、 invokeStatic 、 invokeInterface等,编译器必须知道调用的Java函数是什么类型才能生成相应的正确的字节码。而当在Java代码中调用Kotlin对象时,Kotlin生成的class文件也要输入到Java编译器,这时Java文件才能生成正确的class文件。生成的class文件打成jar包后,最终可以生成Android的APK,或供Java服务端调用。 当然,我们可以直接下载Kotlin编译器下来查看他的编译过程。Kotlin编译器的代码都是用java写的,所以使用Kotlin编译器必须要有java环境。 Kotlin语言基础 基础特性 1,变量定义 在Kotlin的语法规则中,var用来声明变量,val类似Java final,用来声明常量,语句后面不需要跟分号。变量类型可以根据变量值进行自动推导,这里Kotlin的基础类型都是对象,使用的是Java的包装类(基础类型包装成对象)。 2,函数定义 函数使用fun为关键字进行声明,变量的冒号之后是变量类型,函数的冒号之后是返回值。同时Kotlin支持在函数定义的时候声明参数的默认值,例如:函数调用的时候可以直接调用,也可以使用命名参数,例如: 3,类声明 类名的冒号表示继承,所有类的基类称为Any(但并不是Java的Object,只包含equals、hascode、toString方法),声明构造函数要指明constructor关键字。例如: 当然,也可以直接在声明类的时候指定构造函数,对象实例化可以不写new关键字。 4,流程控制语句 Kotlin其他的流程控制基本跟Java差不多,这里主要讲下when表达式,他取代了Java的switch。例如:when表达式其实最终是使用if/else来实现的,Kotlin保留了原来的for each循环,同时增加了区间控制。例如: 5,集合 Kotlin的集合与OC的集合相似,分为可变集合和不可变集合(lists、sets、maps 等)。kotlin中的可变集合对Java的集合进行了包装,同时它实现了一套不可变集合库。调用上面集合的方式如下: 6,伴生对象 Kotlin中没有静态属性和方法,如果我们要创建单列,可以使用Object关键字声明类。 如果要在一个类里面声明静态成员,可以在类的内部使用伴生对象,伴生对象使用关键字companion object。伴生对象的调用跟Java一样,通过类名.属性名称或函数名称调用。 新特性 1,空安全 在Kotlin中,对象声明分为可空引用和非空引用两种。其中非空引用的定义如下:而可空引用需要使用安全符“?”,例如:当调用的时候,也需要使用安全调用操作符,写作 ?. 可空调用。例如:通过函数调用给可空引用赋值,返回的必须也是可空引用,这就在编译期间杜绝了空指针异常。但是这里要注意一点,如果从Java返回的集合,不会强制做可空检查,这个是时候如果给不可空引用赋值Java集合中的null会出现转换错误异常。 2,扩展函数 跟OC的Category一样,Kotlin也支持对API函数进行扩展。然后,我们可以在任意Activity中直接调用。通过反编译成Java代码可以发现,函数的扩展实质上是通过静态导入的方式实现的。 3,字符串模板 字符串中可以包含变量或者表达式,以$符号开头(这跟JSP的EL表达式有点像),比如: 4,操作符重载 Kotlin为基本的运算符提供了固定名称函数表,此部分比较多,关于这方面的内容,读者可以访问下面的内容:Kotlin操作符重载。 调用如下: 5,Lambda表达式支持 Lambda表达式的本质是一个未声明的函数,他会以表达式的形式传递。既然是函数,就由这三块组成:参数 、 方法体 和 返回值。例如,下面是一个典型的Lambda表达式。可以看到,Lambda表达式的大括号内,箭头左边是参数,箭头右侧是方法体和返回值。 调用上面的函数,可以使用下面的调用方式。 高级特性 1,高阶函数 把函数作为参数或者是返回值的函数,Kotlin称之为高阶函数。例如:调用高阶函数的方式如下:当然,我们也可以声明一个局部函数,然后把他作为参数传递给另一个函数,还可以使用Lambda表达式来表示函数参数。 2,泛型 泛型的存在主要是为了消除模板代码和类型转换安全, 在Kotlin中泛型的使用基本与Java是一致的。在Java中泛型是不变的,比如:虽然A继承B,但List和List之间没有任何关系,Java是通过泛型通配符来实现型变的: <? extends T> 对应Kotlin的 out T 生产者<? super T> 对应Kotlin的 in T 消费者 3,反射 反射是运行于JVM中的程序检测和修改运行时的一种行为,通过反射可以在运行时获取对象的属性和方法,这种动态获取信息以及动态调用对象方法的功能特性被称为反射机制。反射可以获取类的方法,属性,类结构等所有信息。在Kotlin中使用Java的反射的实例如下:jc返回的是Java的class对象,可以通过这个对象去调用调用Java的反射内容。 Kotlin中的反射如下。要调用具体的对象时,可以不通过KClass对象,直接调用方法和访问属性。例如: 4,协程 协程(coroutine),又称微线程,是一种无优先级的子程序调度组件,由协程构建器(launch coroutine builder)启动。协程本质上是一种用户态的轻量级线程,协程的调用方式与子线程的调用方式一样,但是协程的使用更加方便灵活,但使用上协程并没有子线程那样广泛。协程作为一种新的异步编程方式,它使用线程为资源,基于代码逻辑去实现任务之间的调度。程序使用协程可以书写线性的异步代码,没有callback,大大简化了异步编程。以下是协程使用的实例:,关于协程更多的内容可以访问下面的链接:https://www.kotlincn.net/docs/tutorials/coroutines-basic-jvm.html 跨平台开发 多平台支持 Kotlin的不仅仅用于Java,还可以使用它进行web js和iOS开发,所以市面上之前说Kotlin是一款基于JVM的语言是不准确的。通过Kotlin提供的Kotlin Native特性,Kotlin可以使用跨平台开发功能。目前Kotlin支持的跨平台如下图所示。 1,Kotlin用于服务端开发 使用Kotlin可用于Java服务端开发。Java与Kotlin的相互兼容性,我们可使用服务端的任意框架,同时我们可以保留老的Java代码,使用Kotlin编写新代码。Kotlin的协程特性更有助于构建服务端程序。IDE的支持和Sring框架的支持。 2,Kotlin用于Android开发 Android Studio的支持。大量的实际案列。大量可学习的APP项目。与Java兼容性允许在 Kotlin 应用程序中使用所有现有的 Android 库。 3,Kotlin用于JavaScript 使用kotlinc-js编译器将Kotlin代码转换为JavaScript(不是Kotlin或标准库的代码编译时会被忽略),Kotlin中提供了一些标准库用于JS开发,同时可以使用第三方JS库。 Kotlin Native Kotlin Native是一种将Kotlin源码编译成不需要任何VM支持的目标平台二进制数据的技术,编译后的二进制数据可以直接运行在目标平台上,它主要包含一个基于LLVM的后端编译器的和一个Kotlin本地运行时库。设计Kotlin Native的目的是为了支持在非JVM环境下进行编程,如在嵌入式平台和iOS环境下,如此一来,Kotlin就可以运行在非JVM平台环境下。目前,Kotlin Native主要提供了Mac、Linux和Windows三个主流平台的编译器,使用该编译器可以很轻松的编译出运行在树莓派、iOS、OS X、Windows以及Linux系统上的程序。 当然,读者可以通过Kotlin/Native的一款游戏源码来学习Kotlin Native:https://github.com/jetbrains/kotlinconf-spinner 学习资料 当然,读者还可以使用通过下面的链接来学习Kotlin相关的知识。1.Kotlin官网http://kotlinlang.org/ 2.kotlin中文官网https://www.kotlincn.net/ 3.Google Kotlin项目学习实例 https://developer.android.com/samples/index.html?language=kotlin 4.其他文章 https://blog.csdn.net/u013448469/article/details/79403284 Kotlin反射 https://blog.csdn.net/ztguang/article/details/72511994 Kotlin Native 5,视频应用项目https://github.com/xiangzhihong/EyeVideoClient 6,Kotlin入门与实战 第1章 Kotlin简介 1.1 Kotlin发展史 1.2 面向对象编程简介 1.2.1 面向过程编程 1.2.2 面向对象编程 1.3 Java虚拟机 1.3.1 JVM语系生态 1.2.2 Java虚拟机简介 1.2.3 Kotlin应用程序运行过程 1.4 为什么使用Kotlin 1.5 Kotlin与Java的比较 1.6小结 第2章 Kotlin初体验 2.1 Kotlin在线运行 2.2 Kotlin 1.1特性 2.2.1 JavaScript全面支持 2.2.1 JVM新特性 2.2.3 协程 2.2.4 标准库 2.3 Kotlin 1.2新特性 2.3.1 多平台支持 2.3.2 多平台环境搭建 2.3.3 特定平台申明 2.3.4 标准库支持 2.3.5 JVM特性 2.3.6 JavaScript特性支持 2.4小结 第3章 Kotlin快速入门 3.1 在Mac上搭建Kotlin开发环境 3.1.1 安装与配置JDK环境 3.1.2 安装与配置IDE 3.2 Kotlin开发IDE介绍 3.2.1 IntelliJ IDEA开发环境 3.2.2 Android Studio集成开发环境 3.3 Kotlin的编译与运行 3.3.1 命令行方式编译运行Kotlin 3.3.2 运行Kotlin REPL 2.3.3 在浏览器中运行Kotlin 2.3.4 在NodeJS中运行Kotlin 3.4 Kotlin构建方式 3.4.1 使用Gradle方式构建Kotlin 3.4.2 使用Maven方式构建Kotlin 3.4.3 使用Ant方式构建Kotlin 3.4.4 Kotlin与OSGi 3.4.5 Kotlin与kapt 3.5 编译器插件 3.5.1 全开放编译插件 3.5.2 无参编译器插件 3.6 小结 第4章 Kotlin语法基础 4.1 Kotlin编程风格 4.2变量与属性 4.2.1 变量申明 4.2.2 getter和setter 4.2.3 访问权限 4.3 基本数据类型 4.3.1 数值类型 4.3.2 字符类型 4.3.3 布尔类型 4.3.4 数组类型 4.3.5 字符串 4.4 包申明与使用 4.5 流程控制语句 4.5.1 if条件语句 4.5.2 when语句 4.5.3 for循环 4.5.4 while循环 4.5.5 返回与跳转 4.6 Kotlin运算符 4.6.1 赋值运算符 4.6.2 算数运算符 4.6.3 关系运算符 4.6.4 逻辑运算符 4.6.6 区间运算符 4.6.7 运算符优先级 4.7 运算符重载 4.7.1 一元运算符 4.7.2 二元运算符 4.7.3 位运算符 4.8 Kotlin操作符 4.8.1 冒号操作符 4.8.2 @操作符 4.8.3 $操作符 4.8.4 安全转换操作符 4.8.5 类型判断操作符 4.9 Kotlin动态类型 4.10 Kotlin空安全 4.9.1 可空类型与不可空类型 4.9.2 判空操作符 4.9.3 Elvis 操作符 4.9.4 强校验操作符 4.9.5 安全的类型转换 4.9.6 可空类型集合 4.11异常处理 4.11.1 异常类 4.11.2 自定义异常 4.11.3 try表达式 4.11.4 throw表达式 4.11.4 受检异常 4.12小结 第5章 类与接口 5.1 类 5.1.1 类的申明 5.1.2 构造函数 5.1.3 类的实例 5.2 继承 5.3 抽象类 5.4 接口 5.5 小结 第6章 扩展函数与属性 6.1 枚举 6.1.1 基本用法 6.1.2 枚举类扩展 6.2 扩展 6.2.1 扩展的动机 6.2.2 扩展原生函数 6.2.3 静态解析 6.2.4 扩展属性 6.2.5 扩展伴生对象 6.2.6 扩展的作用域 6.2.7 类中声明扩展 6.3 this表达式 6.5 小结 第7章 数据类与密封类 7.1 数据类 7.1.1 对象复制 7.1.2 序列化 7.1.3 成员解构 7.2 密封类 7.3 小结 第8章 集合与泛型 8.1集合 8.1.1 集 8.1.2 列表 8.1.3 映射 8.2 泛型 8.2.1 泛型基础 8.2.2 型变 8.2.3 声明处型变 8.2.4 类型投影 8.2.5 星号投影 8.2.6 泛型函数 8.2.7 泛型约束 8.3 小结 第9章 对象与委托 9.1 对象 9.1.1 对象表达式 9.1.2 对象申明 9.1.3 伴生对象 9.2 委托 9.2.1 类委托 9.2.2 委托属性 9.3 标准委托 9.3.1 延迟属性 9.3.2 可观察属性 9.3.3 Map委托 9.3.4 Not Null 9.3.5 局部委托属性 9.3.6 提供委托 9.4 小结 第10章 反射与注解 10.1 反射 10.1.1 类引用 10.1.2 类成员引用 10.1.3 函数引用 10.1.4 属性引用 10.1.5 构造函数引用 10.1.6 KClass反射 10.1.7 对象序列化Json 10.2 注解 10.2.1 注解声明 10.2.2 注解使用 10.2.3 注解类的构造函数 10.2.4 注解目标使用场景 10.2.5 与Java注解互调 10.2.6 注解分类 10.2.7 注解的生命周期 10.3 小结 第11章 函数与Lambda表达式 11.1 函数 10.1.1 函数基本用法 10.1.2 中缀表示法 10.1.3 函数参数 10.1.4 函数作用域 10.1.5 函数返回值 10.1.6 尾递归函数 11.2 高阶函数 11.2.1 高阶函数基本用法 11.2.2 标准高阶函数 11.3 内联函数 11.3.1 内联Lambda表达式 11.3.2内联函数声明 11.3.3非局部返回 11.3.4实例化类型参数 11.3.5内联属性 11.4 Lambda表达式与匿名函数 11.4.1 Lambda表达式语法 11.4.2 函数类型 11.4.3 匿名函数 11.4.4 闭包 11.4.5 函数显示申明 11.5 小结 第12章 协程 12.1 协程简介 12.1.1 协程与线程 12.1.2 使用协程的好处 12.2 协程开发环境 12.2.1 Gradle构建方式 12.2.2 Maven构建方式 12.3 协程基础 12.3.1 launch函数 12.3.2 共享线程池 12.3.3 阻塞与挂起 12.3.4 runBlocking函数 12.3.5 协程取消 12.3.6 协程超时 12.3.6 标准API 12.4 挂起函数 12.4.1 默认顺序执行 12.4.2 异步并发执行 12.4.3 异步样式函数 12.5 协程上下文与调度器 12.5.1 协程调度与线程 12.5.2 非限制与限制协程 12.5.3 协程与线程调试 12.5.4 协程中的子协程 12.6 通道 12.6.1 通道基础 12.6.2 通道的关闭与迭代 12.6.3 通道生产者 12.7 管道 12.7.1 管道生产与消费 12.7.2 管道与质数 12.7.3 多接受者协程 12.7.4 通道缓存 12.9 小结 第13章 IO操作与多线程 13.1 Kotlin流层次 13.1.1 字节输入流 13.1.2 字节输出流 13.1.3 字符输入流 13.1.4 字符输出流 13.1.5 字符流与字节流转换 13.2 文件IO操作 13.2.1 文件读取 13.2.2 文件写入 13.2.3 文件遍历 13.3 网络IO操作 13.4 多线程 13.4.1 线程创建 13.4.2 线程同步 13.5 小结 第14章 Kotlin DSL 14.1 DSL简介 14.1.1 DSL的设计与实现 14.1.2 DSL分类 14.2 DSL语义模型 14.2.1 依赖网络 14.2.2 产生式规则系统 14.2.3 状态机 14.3 Kotlin的DSL特性 14.4 kotlinx.html创建DSL 14.4.1 Maven方式构建 14.4.2 Gradle方式构建 14.4.3 kotlinx.html实例 14.5 Android Gradle指南 14.4.1 链式命令 14.4.2 委托 14.6 使用Kotlin与Anko进行Android开发 14.5.1 Anko简介 14.5.2 Anko核心组件与工具 14.5.3 Anko使用实例 14.7 小结 第15章 Kotlin互操作 15.1 Kotlin与Java互操作 15.1.1 在Kotlin中调用Java 14.1.2 在Java中调用Kotlin 14.1.3 JSR-305支持 15.2 Kotlin与JavaScript互操作 15.2.1 在Kotlin中调用JavaScript 14.2.2 在JavaScript中调用Kotlin 15.2.3 JavaScript模块 15.2.4 JavaScript反射 15.2.5 JavaScript DCE 15.3 小结 第16章 Kotlin Native开发 16.1 Kotlin Native 16.1.1 Kotlin Native简介 16.1.2 Kotlin Native编译器 16.1.3 编译器konan 16.2 Kotlin Native实例 16.2.1 构建Kotlin Native项目 16.2.2 添加konan插件配置 16.2.3 编写源代码 16.2.4 添加konanInterop与konanArtifacts配置 16.2.5 编译与执行 16.2.6 命令行方式编译Kotlin Native 16.3 Kotlin Native互操作 16.2.1 Kotlin Native与C语言互操作 16.2.2 Kotlin Native与OC互操作 16.4 小结 第17章 使用Kotlin与Spring Boot开发服务端 17.1 Spring Boot环境搭建 17.1.1 Spring Boot简介 17.1.2 创建Spring Boot应用程序 17.1.3 启动Spring Boot应用程序 17.1.4 应用测试 16.1.5 properties配置文件 17.2 Spring Boot之Thymeleaf模板 17.3 使用Swagger构建RESTful API 17.4 Spring Boot通过MyBatis整合Mysql数据库 17.5 Spring Boot整合Redis数据库 17.5.1 Redis简介 17.5.2 Spring Boot整合Redis 17.6 Spring Boot整合Elasticsearch 17.6.1 Elasticsearch简介 17.6.2 Spring Boot整合Elasticsearch 17.7 Spring Boot集成RabbitMQ 17.7.1 RabbitMQ简介 17.7.2 Spring Boot集成RabbitMQ 17.8 Spring Boot热部署与日志管理 17.9 Spring Framework 5.0对Kotlin的支持 17.9.1函数式Bean方式注册 17.9.2使用Kotlin调用Spring Web的功能性API 17.9.3 RestTemplate与函数式API扩展 17.9.4 Reactor的Kotlin扩展 17.9.5基于模板的Kotlin脚本 17.10 小结 第18章 使用Kotlin开发Android视频应用 18.1 项目概述 18.2 浅谈Android开发架构模式 18.2.1 MVC 18.2.2 MVP 18.2.3 MVVM 18.3 项目准备 18.3.1 新建Android项目 18.3.2添加项目库依赖 18.3.3编写主页面 18.3.4 GSYVideoPlayer播放器简介 18.4 功能开发 18.4.1 基础类封装 18.4.2 Retrofit封装 18.4.3 首页模块开发 18.4.4 视频详情页面开发 18.5 小结
在Angular 5发布半年之后,Angular 6在昨天正式发布,那么在这个版本有哪些新功能呢?新版本重点关注工具链以及工具链在 Angular 中的运行速度问题。除此之外,这次更新还包括框架包(@angular/core、@angular/common、@angular/compiler 等)、Angular CLI、Angular Material + CDK等。 ng update ng update 是一种新的 CLI 命令,它可分析你的package.json,并基于对 Angular 的了解向你的应用程序推荐更新。官方升级手册链接如下:https://update.angular.io/ ng update可以帮助你使用正确版本的依赖包,让你的依赖包与你的应用程序同步,使用 schematics 时,第三方还能提供脚本更新。如果你的某个依赖包提供了ng update schematic,那么它在进行重大更改时会自动更新代码! ng update不会取代你的软件包管理器,而是在后台使用 npm 或 yarn 来管理依赖包,除了更新和监视依赖包外,ng update还会在必要的时候对你的项目进行改造。 例如,命令ng update @angular/core将会更新所有的 Angular 包以及 RxJS、FTypeScript,它还将在这些包中运行可用的 schematics 以保证版本是最新的。同时,这个命令还能自动安装rxjs-compat到你的应用程序中,以使 RxJS v6 更加流畅。 学习更多关于如何使用ng update , 开始学习如何创建您自己的 ng update 语法,可以参考 rxjs 的 package.json 的入口,它关联了 collection.json。 ng add 另一项新的 CLI 命令ng add 将使你的项目更容易添加新功能。ng add使用软件包管理器来下载新的依赖包并调用安装脚本,它可以通过更改配置和添加额外的依赖包(如 polyfills)来更新你的应用。 你可在新的ng new应用程序中尝试以下动作: ng add @angular/pwa:添加一个 app manifest 和 service worker,将你的应用程序变成 PWA。 ng add @ng-bootstrap/schematics:将ng-bootstrap添加到你的应用程序中。 ng add @angular/material:安装并设置 Angular Material 和主题,注册新的初始组件 到ng generate中。 ng add @clr/angular@next:安装设置 VMWare Clarity。 由于 ng add 基于 schematics 和 Npm ,我们希望库和社区支持我们构建一个 ng add 支持包的生态圈。创建ng add的示例如下:Angular Metarial 的 ng add schemetic Angular Elements Angular Elements 的第一个版本专注于在现有的 Angular 应用程序中启动 Angular 组件,方法是将它们注册为 Custom Elements,目前已被广泛用于 angular.io 内容管理系统中,它嵌入 HTML,可动态启动系统功能。 注册 Angular Component 作为 custom element,或者学习更多的 Angular Elements。 Angular Material + CDK 组件 最值得一提的是用于显示分层数据的树形控件,遵循数据表组件的模式,CDK 包含树的核心指令,而 Angular Material 则提供与顶层的 Material Design 样式相同的体验。 除了 tree 组件之外,我们还提供了 badge 和 bottom-sheet-components。Angular还更新了徽章(badge)和底部菜单栏的组件,徽章用于显示小而有用的信息,例如未读信息的数量。 目前,@angular/cdk/overlay 软件包是 CDK 最强大的基础架构之一,你可以利用他们来构建自己的 UI 库。 Angular Material 初始组件 一旦运行ng add @angular/material并添加材料到现有的应用程序中,就能够生成 3 个新的初始组件。 Material Sidenav Material Sidenav 是带有应用程序名称和侧面导航的工具栏的初始组件,它基于断点窗口(breakpoints)进行响应。例如,运行如下代码: ng generate @angular/material:material-nav Material Dashboard Material Dashboard 是包含动态网格列表的启动组件。例如,运行下面的代码: ng generate @angular/material:material-dashboard Material Data Table Material Data Table 已预配置了一个用于排序和分页的datasource。例如: ng generate @angular/material:material-table 想要了解更多的资料:Angular Material Schematics CLI Workspaces CLI v6 现已支持多项目工作区,如多个应用程序或库,CLI 项目用 angular.json 取代 angular-cli.json 构建和配置项目。每个 CLI 工作区都有项目,每个项目都有目标,每个目标都可以有配置。例如: angular.json: { "projects": { "my-project-name": { "projectType": "application", "architect": { "build": { "configurations": { "production": {}, "demo": {}, "staging": {}, } }, "serve": {}, "extract-i18n": {}, "test": {}, } }, "my-project-name-e2e": {} }, } 关于angular-cli.json更多的配置可以参考下面的链接:https://github.com/angular/angular-cli/wiki/angular-workspace 库支持 接下来介绍 CLI 最重要的一项功能:支持创建和构建库。例如,执行下面的代码: ng generate library <name> 该命令将在 CLI 工作区内创建一个库,并对其进行配置以进行测试和构建。使用Angular CLI 创建库可以查看下面的链接:https://github.com/angular/angular-cli/wiki/stories-create-library Tree Shakable Providers 为了让你的应用更小,我们将服务引用模块改为模块引用服务,这让我们只需要构建在模块里注入的服务。例如,之前的写法是这样的: @NgModule({ ... providers: [ MyService ] }) export class AppModule {} 服务的定义如下: my-service.ts: import { Injectable } from '@angular/core'; @Injectable() export class MyService { constructor() { } } 那么,在新版的语法是下面这样的,NgModule 不再需要引用。 my-service.ts import { Injectable } from '@angular/core'; @Injectable({ providedIn: 'root', }) export class MyService { constructor() { } } 动画性能提升 更新后,以后将不再需要网页动画 polyfill。这意味着你可以从应用程序中删除此 polyfill,可以节省大约 47KB 的内存,同时提高 Safari 中的动画性能。 RxJS v6 Angular 6 也将支持RxJS v6,RxJS v6 于上个月发布。RxJS v6 带来了一个向后兼容的软件包 rxjs-compat,它可以让你的应用程序保持运行。关于如何从 RxJS 5.5 迁移到 6 ,可以查看下面的链接资料:https://github.com/ReactiveX/rxjs/blob/master/MIGRATION.md 长期支持(LTS) Angular 表示他们正在将长期支持版本扩展到所有主版本中。 之前只有 v4 和 v6 是 LTS 版本,但为了使开发者从一个主版本更新到另一个主版本更容易,并给予项目充足的时间来规划更新,Angular 团队表示从 v4 开始,将扩大对所有主版本的长期支持。 每个主版本的支持时间是18个月,其中,前6个月是积极开发阶段,接下的 12 个月是错误修正和安全补丁阶段。 如何更新到 Angular 6.0.0 读者可以访问 update.angular.io 来得到升级应用的信息和指导。 更新通常遵循 3 个步骤,请使用新 ng update 工具: 更新 @ angular / cli; 更新你的 Angular 框架包; 更新其他依赖包。 Ivy 关于我们下一代的渲染引擎 Ivy,Ivy 当前处于开发阶段,还不是 v6 的一部分。关于更多的信息可以访问官方关于Angular 6的发布信息。
自2016年底Android Studio3.0版本退出以来,Android提出了InstantRun热修复方案,基于这种机制,各种热修复框架竞相涌现,国内的软件大厂纷纷开发了自己的热修复框架。对于热修复的更多介绍大家可以通过下面的文章来了解:全面了解Android热修复技术。这些框架主要支持的功能如下:这张图漏掉了阿里的Spofix,该框架可以及时更新,由于目前大多数的热修复框架,缺点是收费,可以通过下面文章来详细了解:阿里第三代非侵入式热修复Sophix 本篇要讲的是如何接入微信的热修复框架Tinker,官网接入资料:Tinker接入指南 Tinker接入 目前,Tinker提供了两种接入方式,一种是基于命令行的方式,类似于AndFix的接入方式;一种就是gradle的方式。官方推荐使用gradle方式接入。 添加gradle依赖 在项目的build.gradle中,添加tinker-patch-gradle-plugin的依赖。 buildscript { dependencies { classpath ('com.tencent.tinker:tinker-patch-gradle-plugin:1.9.1') } } 然后,在app的gradle文件app/build.gradle,添加tinker的库依赖以及apply tinker的gradle插件。 dependencies { //可选,用于生成application类 provided('com.tencent.tinker:tinker-android-anno:1.9.1') //tinker的核心库 compile('com.tencent.tinker:tinker-android-lib:1.9.1') } ... ... //apply tinker插件 apply plugin: 'com.tencent.tinker.patch' 完善gradle配置 添加完Tinker依赖以后,还需要在gradle文件中做以下配置。 开启Multidex; 配置签名文件,方便打包调试; 引入另一个gradle文件专门来对Tinker生成拆分包的配置。 apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'com.tencent.tinker.patch' android { compileSdkVersion 27 defaultConfig { applicationId "com.xzh.demo" minSdkVersion 21 targetSdkVersion 27 versionCode 1 versionName "1.0" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } dexOptions { jumboMode = true } signingConfigs { debug { keyAlias 'alias' keyPassword '123456' storeFile file("../tinker.keystore") storePassword '123456' } release { keyAlias 'alias' keyPassword '123456' storeFile file("../tinker.keystore") storePassword '123456' } } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) implementation"org.jetbrains.kotlin:kotlin-stdlib-jre7:$kotlin_version" implementation 'com.android.support:appcompat-v7:27.1.1' implementation 'com.android.support.constraint:constraint-layout:1.1.0' testImplementation 'junit:junit:4.12' androidTestImplementation 'com.android.support.test:runner:1.0.2' androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' //可选,用于生成application类 provided('com.tencent.tinker:tinker-android-anno:1.9.1') //Tinker的核心库 compile('com.tencent.tinker:tinker-android-lib:1.9.1') } // 加入Tinker生成补丁包的gradle apply from: 'tinker.gradle' 其中,buildTinker.gradle是专门为Tinker配置和生成拆分包而写的,具体可以参考官方gradle配置。 由于生成拆分包的时候会涉及到文件的读写权限,所以需要在Manifest中添加如下权限。 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> gradle参数详解 我们将原apk包称为基准apk包,tinkerPatch直接使用基准apk包与新编译出来的apk包做差异,得到最终的补丁包。gradle文件配置涉及到的常见参数包含。 tinkerPatch:全局信息相关的配置项; tinkerEnable:是否打开tinker的功能; oldApk:基准apk包的路径,必须输入,否则会报错; newApk:选填,用于编译补丁apk路径。如果路径合法,即不再编译新的安装包,使用oldApk与newApk直接编译; outputFolder null:选填参数,设置编译输出路径。默认输出路径为build/outputs/tinkerPatch中; ignoreWarning:如果出现以下的情况,并且ignoreWarning为false,我们将中断编译。因为这些情况可能会导致编译出来的patch包带来风险: minSdkVersion小于14,但是dexMode的值为"raw"; 新编译的安装包出现新增的四大组件(Activity, BroadcastReceiver...); 定义在dex.loader用于加载补丁的类不在main dex中; 定义在dex.loader用于加载补丁的类出现修改; resources.arsc改变,但没有使用applyResourceMapping编译。 applyMapping:可选参数,在编译新的apk时候,我们希望通过保持旧apk的proguard混淆方式,从而减少补丁包的大小。这个只是推荐设置,不设置applyMapping也不会影响任何的assemble编译。 applyResourceMapping:可选参数,在编译新的apk时候,我们希望通过旧apk的R.txt文件保持ResId的分配,这样不仅可以减少补丁包的大小,同时也避免由于ResId改变导致remote view异常。 tinkerId:在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。 keepDexApply:如果我们有多个dex,编译补丁时可能会由于类的移动导致变更增多。若打开keepDexApply模式,补丁包将根据基准包的类分布来编译。 isProtectedApp:是否使用加固模式,仅仅将变更的类合成补丁。注意,这种模式仅仅可以用于加固应用中。 当然,还有很多其他的参数属性,可以通过build.gradle配置属性来获取详情。 Tinker使用 自定义Tinker封装 为了方便操作了管理,我们还需要自定义对Tinker进行一些简单的封装。该类涉及的代码如下: public class TinkerManager { private static boolean isInstalled = false; //ApplicationLike可以理解为Application的载体,可以当成Application去使用 private static ApplicationLike mAppLike; private static SimplePatchListener mPatchListener; /** * 初始化Tinker * @param applicationLike */ public static void installTinker(ApplicationLike applicationLike) { mAppLike = applicationLike; if (isInstalled) { return; } TinkerInstaller.install(mAppLike); isInstalled = true; } /** * 初始化Tinker,带有拓展模块 * @param applicationLike * @param md5Value 服务器下发的md5 */ public static void installTinker(ApplicationLike applicationLike, String md5Value) { mAppLike = applicationLike; if (isInstalled) { return; } mPatchListener = new SimplePatchListener(getApplicationContext()); mPatchListener.setCurrentMD5(md5Value); // Load补丁包时候的监听 LoadReporter loadReporter = new DefaultLoadReporter(getApplicationContext()); // 补丁包加载时候的监听 PatchReporter patchReporter = new DefaultPatchReporter(getApplicationContext()); AbstractPatch upgradePatchProcessor = new UpgradePatch(); TinkerInstaller.install(applicationLike, loadReporter, patchReporter, mPatchListener, SimpleResultService.class, upgradePatchProcessor); isInstalled = true; } /** * 添加补丁包路径 * @param path */ public static void addPatch(String path) { if (Tinker.isTinkerInstalled()) { TinkerInstaller.onReceiveUpgradePatch(getApplicationContext(), path); } } private static Context getApplicationContext() { if (mAppLike != null) { return mAppLike.getApplication().getApplicationContext(); } return null; } } 由于Tinker默认Patch检查是没有对文件做Md5校验,所以如果需要可以重写其中进行检验相关的逻辑。 public class SimplePatchListener extends DefaultPatchListener { private String currentMD5; public void setCurrentMD5(String md5Value) { this.currentMD5 = md5Value; } public SimplePatchListener(Context context) { super(context); } @Override protected int patchCheck(String path, String patchMd5) { //增加patch文件的md5较验 if (!MD5Utils.isFileMD5Matched(path, currentMD5)) { return ShareConstants.ERROR_PATCH_DISABLE; } return super.patchCheck(path, patchMd5); } } 当补丁包完成替换安装之后,删除补丁包,然后杀掉进程,我们可以根据实际情况修改修复结果操作。 public class CustomResultService extends DefaultTinkerResultService { private static final String TAG = "Tinker.SampleResultService"; /** * patch文件的最终安装结果,默认是安装完成后杀掉自己进程 * 此段代码主要是复制DefaultTinkerResultService的代码逻辑 */ @Override public void onPatchResult(PatchResult result) { if (result == null) { TinkerLog.e(TAG, "DefaultTinkerResultService received null result!!!!"); return; } TinkerLog.i(TAG, "DefaultTinkerResultService received a result:%s ", result.toString()); //first, we want to kill the recover process TinkerServiceInternals.killTinkerPatchServiceProcess(getApplicationContext()); // if success and newPatch, it is nice to delete the raw file, and restart at once // only main process can load an upgrade patch! if (result.isSuccess) { //删除patch包 deleteRawPatchFile(new File(result.rawPatchFilePath)); //杀掉自己进程,如果不需要则可以注释 if (checkIfNeedKill(result)) { android.os.Process.killProcess(android.os.Process.myPid()); } else { TinkerLog.i(TAG, "I have already install the newly patch version!"); } } } } 不过上面的东西不做定制开发,用不到,只需要按照下面的步骤即可。 Tinker接入 正常情况下,我们会考虑在Application的onCreate函数中去初始化Tinker相关的内容,不过Tinker更推荐下面的写法。 @DefaultLifeCycle(application = ".SimpleTinkerApplication", flags = ShareConstants.TINKER_ENABLE_ALL, loadVerifyFlag = false) public class SimpleTinkerLike extends ApplicationLike { public SimpleTinkerLike(Application application, int tinkerFlags, boolean tinkerLoadVerifyFlag, long applicationStartElapsedTime, long applicationStartMillisTime, Intent tinkerResultIntent) { super(application, tinkerFlags, tinkerLoadVerifyFlag, applicationStartElapsedTime, applicationStartMillisTime, tinkerResultIntent); } @Override public void onBaseContextAttached(Context base) { super.onBaseContextAttached(base); MultiDex.install(base); TinkerManager.installTinker(this); } } ApplicationLike,通过名字你可能会猜到,该类并非Application的子类,而是一个类似Application的类。Tinker建议编写一个ApplicationLike子类,可以理解为Application的载体,可以当成Application去使用。注意顶部的注解:@DefaultLifeCycle。其application属性,会在编译期生成一个SimpleTinkerInApplication类。该类需要在Manifest中替换我们的默认的Application。 <application android:name=".SimpleTinkerInApplication" .../> 如果报错,请重新编译一下。关于这方面的内容可以查看下面的文章链接:Android 如何编写基于编译时注解的项目为了方便,我们在主页面按钮的点击事件,来加载放在缓存目录下的补丁包,代码如下: public class MainActivity extends AppCompatActivity { private String mPath; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar; } /** * 加载Tinker补丁 * * @param view */ public void Fix(View view) { File patchFile = new File(mPath, "patch_signed.apk"); if (patchFile.exists()) { TinkerManager.addPatch(patchFile.getAbsolutePath()); Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show(); } } } 测试 为了验证热修复的效果,我们在MainActivity中新增一个按钮,并增加一个ImageView图像。然后,找到gradle工具栏(Android Studio的右上角),点击生成Release包,作为1.0版本的程序。在项目的build文件夹下bakAPK(该文件夹是在tink.gradle文件中设置的)文件夹下回看到编译成功的apk文件。将apk安装到手机上,该apk可以认为是old.apk。启动apk看到的效果如下: 2,然后在主界面添加加载图片的按钮,同时添加一个drawable文件。 public class MainActivity extends AppCompatActivity { private String mPath; private ImageView iv; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); //新增 iv = (ImageView) findViewById(R.id.iv); mPath = getExternalCacheDir().getAbsolutePath() + File.separatorChar; } /** * 加载Tinker补丁 * @param view */ public void Fix(View view) { File patchFile = new File(mPath, "patch_signed.apk"); if (patchFile.exists()) { TinkerManager.addPatch(patchFile.getAbsolutePath()); Toast.makeText(this, "File Exists,Please wait a moment ", Toast.LENGTH_SHORT).show(); } else { Toast.makeText(this, "File No Exists", Toast.LENGTH_SHORT).show(); } } /** * 新增的按钮点击事件 * @param view */ public void Load(View view) { iv.setImageResource(R.drawable.bg_content_header); } } 同时记得修改buildTinker.gradle的old安装包的路径,Tinker需要比对前后安装包然后生成补丁包。例如: ext { //开启Tinker tinkerEnable = true //旧的apk位置,需要我们手动指定 tinkerOldApkPath = "${bakPath}/app-2018-05-04-17-00-19" //旧的混淆映射位置,如果开启了混淆,则需要我们手动指定 tinkerApplyMappingPath = "${bakPath}/app-2018-05-04-17-00-19" //旧的resource位置,需要我们手动指定 tinkerApplyResourcePath = "${bakPath}/app-2018-05-04-17-00-19" //旧的多渠道位置,需要我们手动指定 tinkerBuildFlavorDirectory = "${bakPath}/app-2018-05-04-17-00-19" appKey = "0481b2ba9d770294" tinkerID = "1.0" } 找到gradle工具栏,点击Tinker生成Release补丁包,作为1.0版本的补丁。然后将生成的Release补丁包Push到手机的缓存目录上,运行程序点击修复补丁包,稍等数秒程序会被杀掉,重启点击加载图片按钮。使用Tinker的一个缺点是修复的程序必须重启才能执行。生成的补丁包的位置如下: 生成的补丁patch_signed.apk放到手机的包名的cache文件夹下(可以使用应用宝等工具)。例如: 然后重启应用,就发现应用加载了差分包的内。 当然,本文讲解的只是本地的热修复功能,更多的时候我们会需要将差分包放到服务端,然后由服务器控制热更新。后台管理界面如下:更多内容,请查看Android 热更新服务平台相关的介绍。除此之外,Tinker还支持Tinker多渠道打包功能。 附:[源码](https://download.csdn.net/download/xiangzhihong8/10392663)
Google 在刚刚发布的 Android Studio 3.1 新版本中,将 D8 作为新版本开发工具默认的 Dex 编译器。那么什么是 D8 呢,D8 与之前的 Dex 打包器有何区别呢? 大家知道,安卓项目在打包生成 Apk 安装文件的过程中,最重要的一步便是将我们所写的 java 代码编译过成 .class 字节文件再打包转化成一个或多个 .dex 格式的代码压缩文件。这种 dex 文件便是 Android 虚拟机所能识别、解析并运行的程序。 Google 一直在致力于提升 Dex 文件的编译和运行优化工作,并开发出称之为下一代 dex 编译器:D8。其实早在 AS 3.0 Beta 版本中,Google 已经引入 D8 的测试使用。直到当前 3.1 新版本的发布,才正式将其作为默认 Dex 编译器。 根据官方介绍,新版 D8 Dex 编译器相比之前称之为 DX 的旧版编译器,在 dex 文件的编译和使用上,至少具备这么三个优势: 1,更快的编译速度;2,更小的文件大小;3,更优的运行性能。 下面是来自来Google 的官方测试数据,分别使用Dex 和D8来猜测是编译速度和文件大小。 如果你使用的 Android Studio 还是 3.0 版本,可以在项目的 gradle.properties 文件手动开启 D8 编译器。相关的配置如下: android.enableD8=true 不止于此,Google 在代码压缩和优化上也在不断寻求进步。目前我们广泛使用的 ProGuard 工具也将有新的替代者:R8。不过,R8 R8 还没有正式被融入使用,其所在的开源地址为:https://r8.googlesource.com/r8。
从视频直播到播放器,现在很多的产品都集成了视频播放的功能,而目前市面上有比较主流的有第三方框架有: Vitamio ( 体积比较大,有商业化风险 github:https://github.com/yixia/VitamioBundle/) ijkplayer(B站下开源的框架 体积大 配置环境比较麻烦 github:https://github.com/Bilibili/ijkplayer ) PLDroidPlayer(七牛根据ijkplayer二次开发的 定制简单 github:https://github.com/pili-engineering/PLDroidPlayer) 不过本文并不是对这三个播放器进行介绍,而是简单的介绍如何在ubuntu和mac环境下编译ijkplayer。 ijkplayer框架的源码地址:https://github.com/Bilibili/ijkplayer Mac上编译ijkplayer 安装软件 在Mac上编译ijkplayer之前,需要先安装一些基本的软件,这些软件在其他的开发中也会用到,需要安装的软件有homebrew、git、yasm。 1,安装homebrew 打开Terminal,输入如下的命令: ruby -e "$(curl-fsSLhttps://raw.githubusercontent.com/Homebrew/install/master/install)" 2,安装git和 yasm 安装好homebrew后,再安装git和 yasm,安装的命令如下: brew install git brew install yasm 下载NDK并进行环境配置 NDK下载的官方地址为:https://developer.android.google.cn/ndk/downloads/index.html当然也可以到下面的地址下载(可以直接使用迅雷等P2P软件下载,建议下载r15版本):https://blog.csdn.net/gyh198/article/details/75036686 然后,打开Terminal输入并输入如下的命令来打开环境变量,然后添加NDK的相关环境。 open -e .bash_profile 当然,也可以直接使用文本编辑器打开.bash_profile文件。然后添加如下内容: export PATH=$PATH:你的ndk路径 export ANDROID_NDK=你的ndk路径 然后 command+s 保存 ,最后检测是否配置ndk路径成功,在Terminal输入如下命令进行检测。 ndk-build 注意:当然,也可以使用Android-sdk里面的NDK,不过Android-sdk是最新版本,该文件所在结构如下:配置完后,我们可以使用如下命令来检测NDK环境是否配置正确。 ndk-build -v 下载ijkplayer和编译ijkplayer 将ijkplayer框架源码clone到本地并编译,依次在终端输入如下命令。 git clonehttps://github.com/Bilibili/ijkplayer.git ijkplayer-android cd ijkplayer-android 然后执行初始化,此时会从网上自动拉代码,主要是ijkplayer的一些基层类库,时间比较长。命令如下: ./init-android.sh 如果视频播放需要支持Https协议,还需要执行如下命令。 ./init-android-openssl.sh 注意:若出现如下错误,说明是NDk的环境配置有问题。 Youmust define ANDROID_NDK, ANDROID_SDK before starting.They must point to yourNDK and SDK directories. 然后,编译各个平台的openssl。 cd android/contrib ./compile-openssl.sh clean ./compile-openssl.sh all 编译各个平台的ffmpeg,如果需要更多的编解码格式,需要先执行下面的命令。 cd ../.. cd config rm module.sh ln -s module-lite.sh module.sh cd .. cd android/contrib ./compile-ffmpeg.sh clean 说明:如果使用最新版本上如果执行的是(ln -s module-lite.sh module.sh),会出现如下错误: 然后,编译各个cpu架构的ffmpeg。命令如下: ./compile-ffmpeg.sh all 然后使用如下的命令编译ijkplayer即可。 cd .. ./compile-ijk.sh all 如果出现如下图所示的错误,请更换ndk的版本。 如果正确编译的话,会分别在ijkplayer-arm64、ijkplayer-armv5、ijkplayer-armv7a、ijkplayer-x86、ijkplayer-x86_64这些项目的src/main/libs/对应的名称 目录下分别生成libijkffmpeg.so、libijkplayer.so、libijksdl.so这三个so文件。 在ubuntu等Linux环境上编译ijkplayer,可以访问下面的地址:https://blog.csdn.net/g241893312/article/details/79464162
在Android项目开发工程中,功能开发只是其中的一部分,更多的时候是优化,优化除了个人的良好习惯,往往还需要借助第三方工具。本文罗列Android优化过程中的一些常用工具借助这些工具,可以很方便的帮助我们进行性能的分析,进而进行产品的优化。Android应用优化主要从页面优化,内存优化,电量优化,GPU优化和网络优化等方面着手,涉及的知识也比较广泛,下面是优化的一些常见工具。 Android官方工具 Android官方提供了很多的优化工具,很多工具已经自动集成到Android Studio的集成开发环境中,下面就这些工具做一个简单的介绍。 StrictMode "严格模式", 主要用来限制应用做一些不符合性能规范的事情. 一般用来检测主线程中的耗时操作和阻塞。开启StrictMode后, 如果线程中做一些诸如读写文件, 网络访问等操作, 将会在Log console输出一些警告, 警告信息包含Stack Trace来显示哪个地方出了问题。 使用及更多的介绍可以访问:https://developer.android.com/reference/android/os/StrictMode.html 使用也可以访问下面的地址:http://www.androidchina.net/4358.html Systrace Systrace是一个收集和检测时间信息的工具, 它能显示CPU和时间被消耗在哪儿了, 每个进程和线程CPU时间片所做的事情,而且会指示哪个地方出了问题, 以及给出Fix建议。但是在Android Studio 3.0和更高版本中Systrace已经被弃用,将会提供类似的新的工具。 如果要启动独立的设备监视器应用程序,请在android-sdk/tools/目录的中找到monitor,点击即可启动。相关的内容可以查看下面的文章:https://blog.csdn.net/zhuxiaoping54532/article/details/77337054当然也可以参考官方的介绍:https://developer.android.com/studio/profile/systrace.htmlhttps://developer.android.com/studio/profile/systrace-walkthru.htmlhttps://developer.android.com/studio/profile/systrace-commandline.html?hl=fy Hierarchy Viewer Hierarchy Viewer提供了一个可视化的界面来观测布局的层级, 让我们可以优化布局层级, 删除多余的不必要的View层级, 提升布局速度。在使用Hierarchy Viewer进行布局层次分析时,有必要说明下的是:上图红框标出的三个点是关键分析数据. 左起依次代表View的Measure, Layout和Draw的性能. 另外颜色表示该View的该项时间指数, 分为: 绿色, 表示该View的此项性能比该View Tree中超过50%的View都要快. 黄色, 表示该View的此项性能比该View Tree中超过50%的View都要慢. 红色, 表示该View的此项性能是View Tree中最慢的.官方文档介绍: https://developer.android.com/studio/profile/hierarchy-viewer.htmlhttps://developer.android.com/studio/profile/hierarchy-viewer-walkthru.htmlhttps://developer.android.com/studio/profile/hierarchy-viewer-setup.htmlhttps://developer.android.com/studio/profile/optimize-ui.html#HierarchyViewer需要注意的是,Hierarchy Viewer需要Root的机器才可以执行,可以使用第三方的开源的ViewServer来协助我们在未Root的机器上使用Hierarchy Viewer分析。当然,也可以使用Layout Inspector来替换Hierarchy Viewer,相关的使用介绍可以访问下面的文章:https://blog.csdn.net/ziwang_/article/details/66970591 TraceView TraceView是一个图形化的工具,用来展示和分析方法的执行时间。TraceView的使用可以参考下面的文章: https://blog.csdn.net/u011240877/article/details/54347396 Memory Monitor 内存使用检测器, 可以实时检测当前Application的内存使用和释放等信息, 并以图形化界面展示。可以结合heap viewer, allocation tracker来做内存分析,当然也可以导出hprof文件结合第三方的MAT工具分析泄露点。 Android Profiler Android Profiler是3.0版本的一个新功能,对之前的工具做了优化和总结,主要由cpu、内存和网络三大块组成。 CPU Profiler CPU分析器可帮助您实时检查应用程序的CPU使用情况和线程活动,并记录方法跟踪,以便您可以优化和调试应用程序的代码。 打开步骤: 点击 View > Tool Windows > Android Profiler (还可以点击工具栏的); 从Android Profiler工具栏中选择要配置的设备和应用程序进程(如果您已通过USB连接设备但未看到它,请确保已启用USB调试); 单击CPU时间轴中的任意位置打开CPU Profiler。 其中, ① Selected time frame: 在跟踪窗格中检查的记录时间框架的部分。当您第一次记录一个方法跟踪时,CPU分析器将自动选择您在CPU时间线中记录的整个长度。如果要检查仅记录的时间帧的一部分的方法跟踪数据,您可以单击并拖动高亮显示区域的边缘来修改它的长度。 ②Timestamp: 表示记录方法跟踪的开始和结束时间(相对于profiler开始从设备收集CPU使用信息时)。你可以点击时间戳来自动选择整个记录作为你选定的时间框架——如果你有多个你想要转换的记录,这是非常有用的。 ③Trace pane:显示您所选择的时间框架和线程的方法跟踪数据。仅当您记录至少一个方法跟踪后,此窗格才会显示。在此窗格中,您可以选择如何查看每个堆栈跟踪(使用跟踪选项卡)以及如何测量执行时间(使用时间参考下拉菜单)。 ④: 选择显示为Top Down tree, Bottom Up tree, Call Chart, or Flame Chart这些类型的图。您可以在下面的部分中了解有关每个跟踪窗格选项卡的更多信息。 从下拉菜单中选择以下选项之一,以确定如何测量每个方法调用的时序信息: Wall clock time: 表示实际经过时间; Thread time:计时信息表示实际的消耗时间减去不消耗CPU资源的那段时间的任何部分。对于任何给定的方法,它的线程时间总是小于或等于它的时钟时间。使用线程时间让您更好地了解给定方法所消耗的线程实际CPU使用量。 关于这部分内容的详细使用方法,可以参考下面的教程:https://blog.csdn.net/niubitianping/article/details/72617864 第三方工具 除了官方提供的一些工具外,还有一些开源的检测手段。 Battery Historian Google出品, 通过Android系统的bugreport文件来做电量使用分析的工具。项目地址:https://github.com/google/battery-historian Square Square出品了多个Android经典的开源方案,在优化代码方面,Square也提供了诸多的优化工具。如内存泄漏方面:https://github.com/square/leakcanary
DPOS 相对于 POW 有非常高的效率, 那么DPOS是如何做到这一点的呢? 本文就来和大家一起探讨什么是 DPOS。 授权证明共识 授权证明(DPOS)是最快,最有效,最分散,最灵活的共识模式。DPOS利用利益相关方同意投票的权力,以公平和民主的方式解决共识问题。 所有的网络参数,从收费时间表到块间隔和交易规模,都可以通过选定的代表进行调整。块生产者的确定性选择允许平均仅1秒确认交易。也许最重要的是,共识协议旨在保护所有参与者免受不必要的监管干扰。 DPOS 需要解决的问题 任何共识过程必须回答的问题包括但不限于: 谁应该产生下一个更新块来应用于数据库? 下一个块何时应该生产? 什么交易应该包括在该块? 协议的变化如何应用? 竞争的交易历史应该如何解决? 目标是找到这些问题的答案,以确保对希望获得对网络的控制的攻击者的共识过程是强大的。实际上,获得控制意味着获得单方面审查交易的能力。对于希望利用不同计算机上的数据库状态暂时不一致的攻击者,这个过程也应该是健壮的。 被选举的证人生产 “证人”这个词被选中是因为这是一个没有规定的合法中立的词。传统的合同往往有证人签名的地方。对于非常重要的合同, 有时会使用公证人。证人和公证人都不是合同的当事人,但是他们在证明合同是在指定的时间由特定的人签字的非常重要的角色。在比特股中,证人通过将其包含在块中来起到类似的验证签名和时间戳事务的作用。 在DPOS下,利益相关者可以选择任意数量的证人来生成块。块是一组更新数据库状态的事务。每个账户每个证人允许一个投票,这个过程被称为批准投票。通过总审批的前N名证人被选中。证人数量(N)的定义是至少有50%的投票利益相关方认为有足够的权力下放。当利益相关者表达他们想要的证人数量时,他们也必须投票给至少许多证人。利益相关者不能投票支持比实际投票的证人更多的权力下放。 每当目击者产生一个块时,他们都会为他们的服务付费。他们的薪酬由利益相关方通过他们选出的代表来决定(稍后再讨论)。如果证人没有出示任何信息,那么他们就没有报酬,可能会在未来被投票出去。 活动证人的名单在每次维护间隔(1天)内更新一次。然后将目击者洗牌,并且每个目击者轮流以每2秒一个固定的时间表产生一个块。所有目击者转了一圈之后,他们又被洗牌了。如果证人没有在他们的时间段内产生一个块,那么该时间段被跳过,下一个证人产生下一个块。 任何人都可以通过观察证人的参与率来监测网络的健康状况。历史上,比特股保持99%的见证参与。任何时候目击者的参与程度都低于一定水平,网络用户可以允许更多的时间进行交易确认,并对其网络连接性保持警觉。此属性为BitShares提供了独特的优势,即在故障发生后不到1分钟,就可以提醒用户潜在的问题。 通过选定的代表进行参数更改 代表以与证人类似的方式选出。代表成为特殊帐户的共同签名者,该特殊帐户有权提出对网络参数的更改。这个帐户被称为创始帐户。这些参数包括交易费用,块大小,见证薪水和块间隔等。在大多数代表批准了拟议的变更之后,利益相关者被授予2周的审查期,在此期间他们可以对代表投票并使提议的变更无效。 这种设计的选择是为了确保代表在技术上没有直接的权力,网络参数的所有变化最终都得到了利益相关者的认可。这样做是为了保护代表不受可能适用于加密货币的经理或管理员的规定的影响。在DPOS下,我们可以说,行政权力掌握在用户手中,而不是代表或证人。 与证人不同的是,代表们不是有偿职位。但是,这些参数预计不会经常变化。 的成因帐户在技术上可以执行任何其他帐户可以执行任何动作,这意味着它可以发送资金的成因帐户或指定成因帐户作为托管代理。该创世记也可用于发放新的资产。选举代表可以帮助利益相关者执行需要高度信任和责任感的任务,其数量不胜枚举。 改变规则(或者说硬分叉) 有时需要升级网络来添加新的功能。在DPOS下,所有的改变都必须由积极的利益相关者的批准来触发。虽然证人在技术上可能单方面串通和改变他们的软件,但这样做并不符合他们的利益。证人的选择是基于他们对区块链政策保持中立的承诺。保持中立保护证人免受指控他们是网络的管理员/经理/业主/经营者。证人只是利益相关者的雇员。 开发人员可以实施他们认为合适的任何更改,只要这些更改取决于利益相关方的批准。这一政策对开发者的保护就像保护利益相关者一样,并确保没有任何人单方面控制网络的方向。 改变规则的门槛与替换51%的当选证人相同。利益相关者参与选举证人越多,就越难改变规则。 最终,更改规则取决于网络上的每个人升级他们的软件,并且没有区块链协议可以执行规则如何改变。这意味着只要坚持代码普遍预期的行为,就可以在不需要利益相关者投票的情况下推出硬分支“错误修复”。 在实践中,只有安全关键的硬件应该以这种方式来实现。开发商和证人应该等待利益相关者批准即使是最微小的变化。 双重支出攻击 在区块链重组排除之前包括的交易的情况下,双重花费可能发生。这意味着目击者因互联网基础设施的中断而导致通信故障。使用DPOS,通信故障导致双重支出攻击的可能性非常低。 该网络能够监测自己的健康状况,并能立即发现通讯中的任何损失,因为目击者未能及时制造积木。发生这种情况时,用户可能需要等到一半的证人确认交易,这可能是一两分钟。 交易作为证明 网络上的每个事务可以可选地包括最近块的散列。如果这样做,交易的签署人可以确信他们的交易可能不适用于任何不包含该块的区块链。这个过程的一个副作用是,随着时间的推移,所有利益相关者最终直接证明了交易历史的长期完整性。 区块链重组 由于所有的证人都是选举出来的,负有很大的责任,并且有专门的时间段来生产区块,所以很少有可能存在两个相互竞争的连锁的情况。网络延迟不时会阻止一名见证人及时收到前面的信息。如果发生这种情况,下一个证人将通过建立在他们首先接受的任何一个块上来解决问题。有99%的证人参与,交易有一个99%的机会证实一个证人后。 尽管该系统对于自然链重组事件是有力的,但是仍有一些潜在的软件错误,网络中断,或无能或恶意的证人产生比一个或两个块长的多个竞争历史。软件始终选择证人参与率最高的区块链。证人自己经营,每轮只能生产一个块,参与率一般比较低。没有任何证人(或少数证人)能够做出更高参与率的区块链。参与率通过比较产生的块的预期数量与实际产生的块的数量来计算。 最大限度地分散 在DPOS下,每个利益相关者的影响力与其利益成正比,没有利益相关者被排除在影响之外。市场上的其他每一个共识系统都不包括绝大多数利益相关者的参与。有许多不同的方法可以替代利益相关者。一些替代方案使用仅限邀请的系统。其他人通过让参与费用高于他们的收入来排除参与。其他的系统在技术上也允许每个人都参与,但是他们可以被一些产生绝大多数块的大型玩家安全地忽略。只有DPOS确保块生产平均分配给大多数人,每个人都有一个经济上可行的方式来影响这些人是谁。
基本概念 首先从使用出发,其次再结合源码来分析OkHttp3的内部实现的,建议大家下载 OkHttp 源码跟着本文,过一遍源码。首先来看一下OkHttp3的请求代码。 OkHttpClient client = new OkHttpClient(); String run(String url) throws IOException { Request request = new Request.Builder() .url(url) .build(); Response response = client.newCall(request).execute(); return response.body().string(); } 1 2 3 4 5 6 7 8 9 10 OkHttp3的执行流程 创建OkHttpClient对象。OkHttpClient为网络请求执行的一个中心,它会管理连接池,缓存,SocketFactory,代理,各种超时时间,DNS,请求执行结果的分发等许多内容。 创建Request对象。Request用于描述一个HTTP请求,比如请求的方法是GET还是POST,请求的URL,请求的header,请求的body,请求的缓存策略等。 创建Call对象。Call是一次HTTP请求的Task,它会执行网络请求以获得响应。OkHttp中的网络请求执行Call既可以同步进行,也可以异步进行。调用call.execute()将直接执行网络请求,阻塞直到获得响应。而调用call.enqueue()传入回调,则会将Call放入一个异步执行队列,由ExecutorService在后台执行。 执行网络请求并获取响应。 上面的代码中涉及到几个常用的类:Request、Response和Call。下面就这几个类做详细的介绍。 Request 每一个HTTP请求包含一个URL、一个方法(GET或POST或其他)、一些HTTP头,请求还可能包含一个特定内容类型的数据类的主体部分。 Response 响应是对请求的回复,包含状态码、HTTP头和主体部分。 Call OkHttp使用Call抽象出一个满足请求的模型,尽管中间可能会有多个请求或响应。执行Call有两种方式,同步或异步。 那么首先来看一下OkHttpClient的源码实现。 public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory { public OkHttpClient() { this(new Builder()); } OkHttpClient(Builder builder) { this.dispatcher = builder.dispatcher; this.proxy = builder.proxy; this.protocols = builder.protocols; this.connectionSpecs = builder.connectionSpecs; this.interceptors = Util.immutableList(builder.interceptors); this.networkInterceptors = Util.immutableList(builder.networkInterceptors); this.eventListenerFactory = builder.eventListenerFactory; this.proxySelector = builder.proxySelector; this.cookieJar = builder.cookieJar; this.cache = builder.cache; this.internalCache = builder.internalCache; this.socketFactory = builder.socketFactory; boolean isTLS = false; this.hostnameVerifier = builder.hostnameVerifier; this.certificatePinner = builder.certificatePinner.withCertificateChainCleaner( certificateChainCleaner); this.proxyAuthenticator = builder.proxyAuthenticator; this.authenticator = builder.authenticator; this.connectionPool = builder.connectionPool; this.dns = builder.dns; this.followSslRedirects = builder.followSslRedirects; this.followRedirects = builder.followRedirects; this.retryOnConnectionFailure = builder.retryOnConnectionFailure; this.connectTimeout = builder.connectTimeout; this.readTimeout = builder.readTimeout; this.writeTimeout = builder.writeTimeout; this.pingInterval = builder.pingInterval; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 然后使用okHttpClient发起请求。例如: okHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { } }); 1 2 3 4 5 6 7 8 9 10 11 那接下来我们在看下Request。例如: Request request = new Request.Builder().url("url").build(); 1 该段代码主要实现初始化构建者模式和请求对象,并且用URL替换Web套接字URL。其源码如下: public final class Request { public Builder() { this.method = "GET"; this.headers = new Headers.Builder(); } public Builder url(String url) { ...... // Silently replace web socket URLs with HTTP URLs. if (url.regionMatches(true, 0, "ws:", 0, 3)) { url = "http:" + url.substring(3); } else if (url.regionMatches(true, 0, "wss:", 0, 4)) { url = "https:" + url.substring(4); } HttpUrl parsed = HttpUrl.parse(url); ...... return url(parsed); } public Request build() { ...... return new Request(this); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 我们来看一下okHttpClient的异步请求方式。 okHttpClient.newCall(request).enqueue(new Callback() { @Override public void onFailure(Call call, IOException e) { } @Override public void onResponse(Call call, Response response) throws IOException { } }); 1 2 3 4 5 6 7 8 9 10 11 而newCall又调用了RealCall函数,来看源码: public class OkHttpClient implements Cloneable, Call.Factory, WebSocket.Factory { @Override public Call newCall(Request request) { return new RealCall(this, request, false /* for web socket */); } } 1 2 3 4 5 6 7 RealCall实现了Call.Factory接口创建了一个RealCall的实例,而RealCall是Call接口的实现。继续看代码: final class RealCall implements Call { @Override public void enqueue(Callback responseCallback) { synchronized (this) { if (executed) throw new IllegalStateException("Already Executed"); executed = true; } captureCallStackTrace(); client.dispatcher().enqueue(new RealCall.AsyncCall(responseCallback)); } } 1 2 3 4 5 6 7 8 9 10 11 由上面的代码可以得出: 检查这个 call 是否已经被执行了,每个 call 只能被执行一次,如果想要一个完全一样的 call,可以利用 call#clone方法进行克隆。 利用 client.dispatcher().enqueue(this) 来进行实际执行,dispatcher 是刚才看到的OkHttpClient.Builder 的成员之一。 AsyncCall是RealCall的一个内部类并且继承NamedRunnable。 final class AsyncCall extends NamedRunnable { private final Callback responseCallback; AsyncCall(Callback responseCallback) { super("OkHttp %s", new Object[]{RealCall.this.redactedUrl()}); this.responseCallback = responseCallback; } ... } 1 2 3 4 5 6 7 8 9 而NamedRunnable又实现了Runnable接口,来看代码: public abstract class NamedRunnable implements Runnable { ...... @Override public final void run() { ...... try { execute(); } ...... } protected abstract void execute(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 可以看到NamedRunnable实现了Runnbale接口并且是个抽象类,其抽象方法是execute(),该方法是在run方法中被调用的,这也就意味着NamedRunnable是一个任务,并且其子类应该实现execute方法。下面再看AsyncCall的实现: final class AsyncCall extends NamedRunnable { private final Callback responseCallback; AsyncCall(Callback responseCallback) { super("OkHttp %s", redactedUrl()); this.responseCallback = responseCallback; } ...... final class RealCall implements Call { @Override protected void execute() { boolean signalledCallback = false; try { Response response = getResponseWithInterceptorChain(); if (retryAndFollowUpInterceptor.isCanceled()) { signalledCallback = true; responseCallback.onFailure(RealCall.this, new IOException("Canceled")); } else { signalledCallback = true; responseCallback.onResponse(RealCall.this, response); } } catch (IOException e) { ...... responseCallback.onFailure(RealCall.this, e); } finally { client.dispatcher().finished(this); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 AsyncCall实现了execute方法,首先是调用getResponseWithInterceptorChain()方法获取响应,然后获取成功后,就调用回调的onReponse方法,如果失败,就调用回调的onFailure方法,并调用Dispatcher的finished方法。 Dispatcher线程池介绍 那还看一下Dispatcher类的相关代码: public final class Dispatcher { /** 最大并发请求数为64 */ private int maxRequests = 64; /** 每个主机最大请求数为5 */ private int maxRequestsPerHost = 5; /** 线程池 */ private ExecutorService executorService; /** 准备执行的请求 */ private final Deque<AsyncCall> readyAsyncCalls = new ArrayDeque<>(); /** 正在执行的异步请求,包含已经取消但未执行完的请求 */ private final Deque<AsyncCall> runningAsyncCalls = new ArrayDeque<>(); /** 正在执行的同步请求,包含已经取消单未执行完的请求 */ private final Deque<RealCall> runningSyncCalls = new ArrayDeque<>(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 在OkHttp,使用如下构造了单例线程池,相关源码如下: public synchronized ExecutorService executorService() { if (executorService == null) { executorService = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60, TimeUnit.SECONDS, new SynchronousQueue<Runnable>(), Util.threadFactory("OkHttp Dispatcher", false)); } return executorService; } 1 2 3 4 5 6 7 executorService函数会构造一个线程池ExecutorService: executorService = new ThreadPoolExecutor( //corePoolSize 最小并发线程数,如果是0的话,空闲一段时间后所有线程将全部被销毁 0, //maximumPoolSize: 最大线程数,当任务进来时可以扩充的线程最大值,当大于了这个值就会根据丢弃处理机制来处理 Integer.MAX_VALUE, //keepAliveTime: 当线程数大于corePoolSize时,多余的空闲线程的最大存活时间 60, //单位秒 TimeUnit.SECONDS, //工作队列,先进先出 new SynchronousQueue<Runnable>(), //单个线程的工厂 Util.threadFactory("OkHttp Dispatcher", false)); 1 2 3 4 5 6 7 8 9 10 11 12 13 可以看出,在Okhttp中,构建了一个核心为[0, Integer.MAX_VALUE]的线程池,它不保留任何最小线程数,随时创建更多的线程数,当线程空闲时只能活60秒,它使用了一个不存储元素的阻塞工作队列,一个叫做”OkHttp Dispatcher”的线程工厂。也就是说,在实际运行中,当收到10个并发请求时,线程池会创建十个线程,当工作完成后,线程池会在60s后相继关闭所有线程。 synchronized void enqueue(AsyncCall call) { if (runningAsyncCalls.size() < maxRequests && runningCallsForHost(call) < maxRequestsPerHost) { runningAsyncCalls.add(call); executorService().execute(call); } else { readyAsyncCalls.add(call); } } 1 2 3 4 5 6 7 8 从上述源码分析,如果当前还能执行一个并发请求,则加入 runningAsyncCalls ,立即执行,否则加入 readyAsyncCalls 队列。由此,可以得出Dispatcher的以下作用。 调度线程池Disptcher实现了高并发,低阻塞的实现; 采用Deque作为缓存,先进先出的顺序执行; 任务在try/finally中调用了finished函数,控制任务队列的执行顺序,而不是采用锁,减少了编码复杂性提高性能。 try { Response response = getResponseWithInterceptorChain(); if (retryAndFollowUpInterceptor.isCanceled()) { signalledCallback = true; responseCallback.onFailure(RealCall.this, new IOException("Canceled")); } else { signalledCallback = true; responseCallback.onResponse(RealCall.this, response); } } finally { client.dispatcher().finished(this); } 1 2 3 4 5 6 7 8 9 10 11 12 其流程可以用下图表示: getResponseWithInterceptorChain方法 相关的方法源码如下: Response getResponseWithInterceptorChain() throws IOException { // Build a full stack of interceptors. List<Interceptor> interceptors = new ArrayList<>(); interceptors.addAll(client.interceptors()); interceptors.add(retryAndFollowUpInterceptor); interceptors.add(new BridgeInterceptor(client.cookieJar())); interceptors.add(new CacheInterceptor(client.internalCache())); interceptors.add(new ConnectInterceptor(client)); if (!forWebSocket) { interceptors.addAll(client.networkInterceptors()); } interceptors.add(new CallServerInterceptor(forWebSocket)); Interceptor.Chain chain = new RealInterceptorChain( interceptors, null, null, null, 0, originalRequest); return chain.proceed(originalRequest); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 从上述源码得知,不管okhttp有多少拦截器最后都会走,如下方法: Interceptor.Chain chain = new RealInterceptorChain( interceptors, null, null, null, 0, originalRequest); return chain.proceed(originalRequest); 1 2 3 从方法名字基本可以猜到是干嘛的,调用 chain.proceed(originalRequest); 将request传递进来,从拦截器链里拿到返回结果。那么看一下RealInterceptorChain类。 public final class RealInterceptorChain implements Interceptor.Chain { public RealInterceptorChain(List<Interceptor> interceptors, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection, int index, Request request) { this.interceptors = interceptors; this.connection = connection; this.streamAllocation = streamAllocation; this.httpCodec = httpCodec; this.index = index; this.request = request; } ...... @Override public Response proceed(Request request) throws IOException { return proceed(request, streamAllocation, httpCodec, connection); } public Response proceed(Request request, StreamAllocation streamAllocation, HttpCodec httpCodec, RealConnection connection) throws IOException { if (index >= interceptors.size()) throw new AssertionError(); calls++; ...... // Call the next interceptor in the chain. RealInterceptorChain next = new RealInterceptorChain( interceptors, streamAllocation, httpCodec, connection, index + 1, request); Interceptor interceptor = interceptors.get(index); Response response = interceptor.intercept(next); ...... return response; } protected abstract void execute(); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 该类实现了Chain接口,在getResponseWithInterceptorChain调用时好几个参数都传的null。主要看proceed方法,proceed方法中判断index(此时为0)是否大于或者等于client.interceptors(List )的大小。由于httpStream为null,所以首先创建next拦截器链,主需要把索引置为index+1即可;然后获取第一个拦截器,调用其intercept方法。Interceptor 代码如下: public interface Interceptor { Response intercept(Chain chain) throws IOException; interface Chain { Request request(); Response proceed(Request request) throws IOException; Connection connection(); } } 1 2 3 4 5 6 7 8 9 10 11 BridgeInterceptor从用户的请求构建网络请求,然后提交给网络,最后从网络响应中提取出用户响应。从最上面的图可以看出,BridgeInterceptor实现了适配的功能。下面是其intercept方法: public final class BridgeInterceptor implements Interceptor { ...... @Override public Response intercept(Chain chain) throws IOException { Request userRequest = chain.request(); Request.Builder requestBuilder = userRequest.newBuilder(); RequestBody body = userRequest.body(); //如果存在请求主体部分,那么需要添加Content-Type、Content-Length首部 if (body != null) { MediaType contentType = body.contentType(); if (contentType != null) { requestBuilder.header("Content-Type", contentType.toString()); } long contentLength = body.contentLength(); if (contentLength != -1) { requestBuilder.header("Content-Length", Long.toString(contentLength)); requestBuilder.removeHeader("Transfer-Encoding"); } else { requestBuilder.header("Transfer-Encoding", "chunked"); requestBuilder.removeHeader("Content-Length"); } } if (userRequest.header("Host") == null) { requestBuilder.header("Host", hostHeader(userRequest.url(), false)); } if (userRequest.header("Connection") == null) { requestBuilder.header("Connection", "Keep-Alive"); } // If we add an "Accept-Encoding: gzip" header field we're responsible for also decompressing // the transfer stream. boolean transparentGzip = false; if (userRequest.header("Accept-Encoding") == null && userRequest.header("Range") == null) { transparentGzip = true; requestBuilder.header("Accept-Encoding", "gzip"); } List<Cookie> cookies = cookieJar.loadForRequest(userRequest.url()); if (!cookies.isEmpty()) { requestBuilder.header("Cookie", cookieHeader(cookies)); } if (userRequest.header("User-Agent") == null) { requestBuilder.header("User-Agent", Version.userAgent()); } Response networkResponse = chain.proceed(requestBuilder.build()); HttpHeaders.receiveHeaders(cookieJar, userRequest.url(), networkResponse.headers()); Response.Builder responseBuilder = networkResponse.newBuilder() .request(userRequest); if (transparentGzip && "gzip".equalsIgnoreCase(networkResponse.header("Content-Encoding")) && HttpHeaders.hasBody(networkResponse)) { GzipSource responseBody = new GzipSource(networkResponse.body().source()); Headers strippedHeaders = networkResponse.headers().newBuilder() .removeAll("Content-Encoding") .removeAll("Content-Length") .build(); responseBuilder.headers(strippedHeaders); responseBuilder.body(new RealResponseBody(strippedHeaders, Okio.buffer(responseBody))); } return responseBuilder.build(); } /** Returns a 'Cookie' HTTP request header with all cookies, like {@code a=b; c=d}. */ private String cookieHeader(List<Cookie> cookies) { StringBuilder cookieHeader = new StringBuilder(); for (int i = 0, size = cookies.size(); i < size; i++) { if (i > 0) { cookieHeader.append("; "); } Cookie cookie = cookies.get(i); cookieHeader.append(cookie.name()).append('=').append(cookie.value()); } return cookieHeader.toString(); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 从上面的代码可以看出,首先获取原请求,然后在请求中添加头,比如Host、Connection、Accept-Encoding参数等,然后根据看是否需要填充Cookie,在对原始请求做出处理后,使用chain的procced方法得到响应,接下来对响应做处理得到用户响应,最后返回响应。再看下一个拦截器ConnectInterceptor的处理: public final class ConnectInterceptor implements Interceptor { ...... @Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; Request request = realChain.request(); StreamAllocation streamAllocation = realChain.streamAllocation(); // We need the network to satisfy this request. Possibly for validating a conditional GET. boolean doExtensiveHealthChecks = !request.method().equals("GET"); HttpCodec httpCodec = streamAllocation.newStream(client, doExtensiveHealthChecks); RealConnection connection = streamAllocation.connection(); return realChain.proceed(request, streamAllocation, httpCodec, connection); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 实际上建立连接就是创建了一个 HttpCodec 对象,它利用 Okio 对 Socket 的读写操作进行封装,Okio 以后有机会再进行分析,现在让我们对它们保持一个简单地认识:它对 java.io 和 java.nio 进行了封装,让我们更便捷高效的进行 IO 操作。 CallServerInterceptor CallServerInterceptor是拦截器链中最后一个拦截器,负责将网络请求提交给服务器。 @Override public Response intercept(Chain chain) throws IOException { RealInterceptorChain realChain = (RealInterceptorChain) chain; HttpCodec httpCodec = realChain.httpStream(); StreamAllocation streamAllocation = realChain.streamAllocation(); RealConnection connection = (RealConnection) realChain.connection(); Request request = realChain.request(); long sentRequestMillis = System.currentTimeMillis(); httpCodec.writeRequestHeaders(request); Response.Builder responseBuilder = null; if (HttpMethod.permitsRequestBody(request.method()) && request.body() != null) { // If there's a "Expect: 100-continue" header on the request, wait for a "HTTP/1.1 100 // Continue" response before transmitting the request body. If we don't get that, return what // we did get (such as a 4xx response) without ever transmitting the request body. if ("100-continue".equalsIgnoreCase(request.header("Expect"))) { httpCodec.flushRequest(); responseBuilder = httpCodec.readResponseHeaders(true); } if (responseBuilder == null) { // Write the request body if the "Expect: 100-continue" expectation was met. Sink requestBodyOut = httpCodec.createRequestBody(request, request.body().contentLength()); BufferedSink bufferedRequestBody = Okio.buffer(requestBodyOut); request.body().writeTo(bufferedRequestBody); bufferedRequestBody.close(); } else if (!connection.isMultiplexed()) { // If the "Expect: 100-continue" expectation wasn't met, prevent the HTTP/1 connection from // being reused. Otherwise we're still obligated to transmit the request body to leave the // connection in a consistent state. streamAllocation.noNewStreams(); } } httpCodec.finishRequest(); if (responseBuilder == null) { responseBuilder = httpCodec.readResponseHeaders(false); } Response response = responseBuilder .request(request) .handshake(streamAllocation.connection().handshake()) .sentRequestAtMillis(sentRequestMillis) .receivedResponseAtMillis(System.currentTimeMillis()) .build(); int code = response.code(); if (forWebSocket && code == 101) { // Connection is upgrading, but we need to ensure interceptors see a non-null response body. response = response.newBuilder() .body(Util.EMPTY_RESPONSE) .build(); } else { response = response.newBuilder() .body(httpCodec.openResponseBody(response)) .build(); } if ("close".equalsIgnoreCase(response.request().header("Connection")) || "close".equalsIgnoreCase(response.header("Connection"))) { streamAllocation.noNewStreams(); } if ((code == 204 || code == 205) && response.body().contentLength() > 0) { throw new ProtocolException( "HTTP " + code + " had non-zero Content-Length: " + response.body().contentLength()); } return response; } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 从上面的代码中可以看出,首先获取HttpStream对象,然后调用writeRequestHeaders方法写入请求的头部,然后判断是否需要写入请求的body部分,最后调用finishRequest()方法将所有数据刷新给底层的Socket,接下来尝试调用readResponseHeaders()方法读取响应的头部,然后再调用openResponseBody()方法得到响应的body部分,最后返回响应。 总结 最后我们用一张图来总结ohhttp的整个请求流程。 OkHttp的底层是通过Java的Socket发送HTTP请求与接受响应的(,但是OkHttp实现了连接池的概念,即对于同一主机的多个请求,其实可以公用一个Socket连接,而不是每次发送完HTTP请求就关闭底层的Socket,这样就实现了连接池的概念,而且OkHttp对Socket的读写操作使用的OkIo库进行了一层封装。
概述 从 2016 年开始,模块化在 Android 社区越来越多的被提及。随着移动平台的不断发展,移动平台上的软件慢慢走向复杂化,体积也变得臃肿庞大,为了降低大型软件复杂性和耦合度,同时也为了适应模块重用、多团队并行开发测试等等需求,模块化在 Android 平台上变得势在必行。阿里 Android 团队在年初开源了他们的容器化框架 Atlas 就很大程度说明了当前 Android 平台开发大型商业项目所面临的问题。 那么什么是模块化呢,和我们常说的组件化又有什么联系和区别呢?根据《 Java 应用架构设计:模块化模式与 OSGi 》一书中对模块化的定义:模块化是一种处理复杂系统分解为更好的可管理模块的方式。对于这种概念性的解释,太过生涩难懂,不够直观。 那么究竟何为模块化呢?举个例子,相信随着业务的不断迭代,APK项目已经无限大了,以我们公司的电商项目为例,在迭代了5年后,apk的体积已经40M+,如果使用传统的ant打包大概差不多要近10分钟,如果用增量打包时间也要3-5分钟。但是可以发现,很多老的代码其实我们在最新的版本是不需要的,当然我们可以手动的将这些代码删除,但是又还怕啥时候用到。此时,最好的方法就是将这些模块独立成一个独立的工程,当需要的时候再引入进来,这就是模块化的一个背景。 所以,此处,我们对模块化和组件化做一个简单的定义:模块化:指解决一个复杂问题时自顶向下逐层把系统划分成若干模块的过程,如订单模块(OrderModule)、特卖模块(SPecialModule)、即时通讯模块(InstantMessagingModule)等等。 组件化:组件是指通用的功能或者UI库可以做成一个功能组件,如地图组件(MapSDK)、支付组件(AnjukePay)、路由组件(Router)等等; 插件化:和模块化差不多,只是它是可以把模块打包成独立的apk,可以动态的加载,删除,独立的插件apk可以在线下载安装,有利于减少apk的体积和实现模块的热修复。目前热门的插件化方案有:阿里的atlas,360公司的RePlugin,滴滴的VirtualAPK等等; 例如,下面是模块化之前和模块化之后的项目的目录结构:模块化的示意图可以用下面的模型表示: 模块化要解决的问题 要使用模块化开发Android项目,有以下几点需要注意: 模块间页面跳转(路由); 模块间事件通信; 模块间服务调用; 模块的独立运行; 其他注意事项;为了方便讲解,我们以下面的项目为例: 这是一个常见的首页画面,该页面主要有首页、微聊、推荐和我的组成。我们将该4个Tab单独成4个独立的模块。 其中: app模块:主模块,主要进行搭载各个模块的功能; lib_base:对ARouter进行初始化,和放置一些各个模块公用的封装类; module_home,module_caht,module_recom,module_me:分别对应“首页”、“微聊”、“推荐”、“我的”模块。 ARouter模块化开发 ARouter各个模块的gradle配置 app模块是程序的容器,起到程序入口的作用,lib_base作为基础模块,用来将一些公共的库和对ARouter的初始化操作放在这一模块中。因此每个子模块都会用到它里面的内容,所以我们在 lib_base中添加如下内容。 compile 'com.alibaba:arouter-api:1.2.4' annotationProcessor "com.alibaba:arouter-compiler:1.1.4" compile 'com.android.support:design:27.1.1' compile 'org.simple:androideventbus:1.0.5.1' compile 'com.alibaba:fastjson:1.2.31' 因为我们把拦截器等公用类放在base注册,在编译期间生成路径映射。所以还需要在build中加入如下配置: defaultConfig { javaCompileOptions { annotationProcessorOptions { arguments = [moduleName: project.getName()] } } } 由于每个子模块都会用到lib_base里面的东西,所以需要在各子模块的build文件中导入(即module_home,module_caht,module_recom,module_me等模块中)如下配置: //注意,此处也需要引入了com.alibaba:arouter-compiler:1.1.4 annotationProcessor 'com.alibaba:arouter-compiler:1.1.4' compile project(':lib_base') 同样,也需要在各子模块的build中加入如下配置。 defaultConfig { javaCompileOptions { annotationProcessorOptions { arguments = [moduleName: project.getName()] } } } 然后在app模块(也即是主模块)对各个子模块进行依赖。 compile project(':module_home') compile project(':module_chat') compile project(':module_recom') compile project(':module_me') 子模块依赖规则配置 对于模块化项目,每个单独的Module 都可以单独编译成 APK。在开发阶段需要单独打包编译,项目发布的时候又需要它作为项目的一个 Module 来整体编译打包。简单的说就是开发时是 Application,发布时是 Library。所以,需要在子模块中做如下的配置: if(isBuildModule.toBoolean()){ apply plugin: 'com.android.application' }else{ apply plugin: 'com.android.library' } 同理,Manifest.xml 也需要有两套: if (isBuildModule.toBoolean()) { manifest.srcFile 'src/main/debug/AndroidManifest.xml' } else { manifest.srcFile 'src/main/release/AndroidManifest.xml' } 同时,每个子模块的defaultConfig还需要增加如下配置: defaultConfig { if (!isNeedMeModule.toBoolean()) { applicationId "com.xzh.module_me" } } 而上面的isBuildModule.toBoolean()判断条件,读取的是项目根目录下的gradle.properties配置文件。 # 是否需要单独编译 true表示需要,false表示不需要 isNeedHomeModule=false #isNeedHomeModule=true isNeedChatModule=false #isNeedChatModule=false isNeedRecomModule=false #isNeedRecomModule=false isNeedMeModule=false #isNeedMeModule=false 然后根据上面的编译配置在app模块中添加如下依赖: if (!isNeedHomeModule.toBoolean()) { compile project(':module_home') } if (!isNeedChatModule.toBoolean()) { compile project(':module_chat') } if (!isNeedRecomModule.toBoolean()) { compile project(':module_recom') } if (!isNeedMeModule.toBoolean()) { compile project(':module_me') } 如果需要单独运行某个模块时,只需要修改gradle.properties对应的配置即可。例如,需要单独运行module_home模块时,只需要开启对于的配置即可isNeedHomeModule=true。 配置注意 由于配置后项目只有一个入口和启动文件(即app模块的MainActvity),所以其他子模块的MainActivity的intent-filter拦截要去掉,不然会有多个桌面入口。 <activity android:name=".MainActivity"> <!--<intent-filter>--> <!--<action android:name="android.intent.action.MAIN" />--> <!--<category android:name="android.intent.category.LAUNCHER" />--> <!--</intent-filter>--> </activity> ARouter使用 以上面的效果实现为例,在MainActivity中使用TabLayout+Adapter的形式搭建4个Tab页面。代码如下: public class MainActivity extends AppCompatActivity { private ViewPager mMViewPager; private TabLayout mToolbarTab; private int[] tabIcons = { R.drawable.tab_home, R.drawable.tab_weichat, R.drawable.tab_recommend, R.drawable.tab_user }; private String[] tab_array; private DemandAdapter mDemandAdapter; private List<Fragment> fragments = new ArrayList<>(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); init(); } private void init() { initData(); initView(); setViewPagerAdapter(); setTabBindViewPager(); setItem(); } private void initData() { tab_array = getResources().getStringArray(R.array.tab_main); fragments.clear(); fragments.add(FragmentUtils.getHomeFragment()); fragments.add(FragmentUtils.getChatFragment()); fragments.add(FragmentUtils.getRecomFragment()); fragments.add(FragmentUtils.getMeFragment()); } private void initView() { mMViewPager = (ViewPager) findViewById(R.id.mViewPager); mToolbarTab = (TabLayout) findViewById(R.id.toolbar_tab); } private void setViewPagerAdapter() { mDemandAdapter = new DemandAdapter(getSupportFragmentManager(),fragments); mMViewPager.setAdapter(mDemandAdapter); } private void setTabBindViewPager() { mToolbarTab.setupWithViewPager(mMViewPager); } private void setItem() { for (int i = 0; i < mToolbarTab.getTabCount(); i++) { mToolbarTab.getTabAt(i).setCustomView(getTabView(i)); } } public View getTabView(int position) { View view = LayoutInflater.from(this).inflate(R.layout.item_tab, null); ImageView tab_image = view.findViewById(R.id.tab_image); TextView tab_text = view.findViewById(R.id.tab_text); tab_image.setImageResource(tabIcons[position]); tab_text.setText(tab_array[position]); return view; } } 然后,使用ARouter来获取到各个模块的Fragment。 public class FragmentUtils { public static Fragment getHomeFragment() { Fragment fragment = (Fragment) ARouter.getInstance().build(RouteUtils.Home_Fragment_Main).navigation(); return fragment; } public static Fragment getChatFragment() { Fragment fragment = (Fragment) ARouter.getInstance().build(RouteUtils.Chat_Fragment_Main).navigation(); return fragment; } public static Fragment getRecomFragment() { Fragment fragment = (Fragment) ARouter.getInstance().build(RouteUtils.Recom_Fragment_Main).navigation(); return fragment; } public static Fragment getMeFragment() { Fragment fragment = (Fragment) ARouter.getInstance().build(RouteUtils.Me_Fragment_Main).navigation(); return fragment; } } 而FragmentUtils使用了RouteUtils来定义具体的跳转协议。 public class RouteUtils { public static final String Home_Fragment_Main = "/home/main"; public static final String Chat_Fragment_Main = "/chat/main"; public static final String Recom_Fragment_Main = "/recom/main"; public static final String Me_Fragment_Main = "/me/main"; } 上面的子模块使用的是Fragment,所以,在子模块中要使用Route说明。例如: @Route(path = RouteUtils.Chat_Fragment_Main) public class MainFragment extends Fragment { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { View view = inflater.inflate(R.layout.fragment_weichat, null); return view; } @Override public void onDestroyView() { super.onDestroyView(); } } 跨模块跳转 假如要实现跨模块跳转,首先在RouteUtils定义 public static final String Me_Login = "/me/main/login"; ARouter要跳转Activity,就在这个Activity上加入注解。 @Route(path = RouteUtils.Me_Login) public class LoginActivity extends AppCompatActivity{ } 然后在需要跳转的地方添加如下代码: ARouter.getInstance().build(RouteUtils.Me_Login).navigation(); 实现ForResult返回数据 如果跨模块跳转需要返回数据,即Activity的StartActivityForResult,则可以使用下面的方式。 ARouter.getInstance().build(RouteUtils.Chat_ForResult).navigation(this, 666); //666即为Code 接收数据数据: @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case 666: String name = data.getStringExtra("name"); UIUtils.showToast(name + ",resultCode===>" + resultCode); break; default: break; } } 然后,接受返回数据: Intent intent = new Intent(); intent.putExtra("name", "ForResult返回的数据"); setResult(999, intent); finish(); 使用Eventbus跨模块通信 使用Eventbus进行跨模块通信,首先在需要接受的地方定义一个订阅者。 @Subscriber(tag = EvenBusTag.GOTO_EVENTBUS) public void onEvent(String s) { UIUtils.showToast(s); } 然后在发送方使用EventBus发送消息。例如: @Route(path = RouteUtils.Me_EventBus) public class EventBusActivity extends AppCompatActivity implements View.OnClickListener { /** * eventBus数据接收页面 */ private TextView mTextView; /** * eventBus返回数据 */ private Button mBtnBackData; private String name; private long age; private EventBusBean eventbus; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_event_bus); ARouter.getInstance().inject(this); initData(); initView(); } private void initData() { name = getIntent().getStringExtra("name"); age = getIntent().getLongExtra("age", 0); eventbus = getIntent().getParcelableExtra("eventbus"); } private void initView() { mTextView = (TextView) findViewById(R.id.textView); mBtnBackData = (Button) findViewById(R.id.btn_back_data); mBtnBackData.setOnClickListener(this); mTextView.setText("name=" + name + ",\tage=" + age + ",\tproject=" + eventbus.getProject() + ",\tnum=" + eventbus.getNum()); } @Override public void onClick(View v) { int i = v.getId(); if (i == R.id.btn_back_data) { EventBus.getDefault().post(name, EvenBusTag.GOTO_EVENTBUS); finish(); } else { } } } 其实,ARouter的功能远不止于此,后面将为大家一一讲解,并最终自己实现一个模块间的路由。
总的来说,EventBus是一款针对Android优化的发布/订阅事件总线,主要功能是替代Intent,Handler,BroadCast在Fragment,Activity,Service,线程之间传递消息。而Rxjava则是一种基于异步数据流的处理方案。如果一个订阅者需要注册多个事件的时候,Rxjava需要一个个单独的注册,而EventBus则可以实现一个订阅者订阅多个事件,和一个事件对应多个订阅者。 EventBus EventBus是一个Android端优化的publish/subscribe消息总线,简化了应用程序内各组件间、组件与后台线程间的通信。比如请求网络,等网络返回时通过Handler或Broadcast通知UI,两个Fragment之间需要通过Listener通信,这些需求都可以通过EventBus实现。EventBus仅仅适合当做组件间的通讯工具使用,主要用来传递消息,避免搞出一大堆的interface。 使用 添加依赖使用EventBus之前需要添加相关的依赖: compile 'org.greenrobot:eventbus:3.0.0' 然后在onStart()方法中注册它,在onStop()方法中消耗。 //注册eventBus @Override protected void onStart() { super.onStart(); EventBus.getDefault().register(this); } //取消注册 @Override protected void onStop() { super.onStop(); EventBus.getDefault().unregister(this); } 在需要接受的地方添加订阅者(使用@Subscribe注解),@Subscribe注解来描述一个public无返回值的非静态方法,注解后面可以跟threadMode,来给定订阅者处理事件所在的线程。 @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMainThread(IdEvent event) { if (event != null) { } } EventBus包含4个ThreadMode: ThreadMode.POSTING 事件的处理在和事件的发送在相同的进程,所以事件处理时间不应太长,不然影响事件的发送线程,而这个线程可能是UI线程; ThreadMode.MAIN事件的处理会在UI线程中执行,事件处理不应太长时间; ThreadMode.BACKGROUND 事件的处理会在一个后台线程中执行,尽管是在后台线程中运行,事件处理时间不应太长; ThreadMode.ASYNC事件处理会在单独的线程中执行,主要用于在后台线程中执行耗时操作,每个事件会开启一个线程(有线程池),但最好限制线程的数目。 例如: /** * 在后台线程中执行,如果当前线程是子线程,则会在当前线程执行,如果当前线程是主线程,则会创建一个新的子线程来执行 * @param event */ @Subscribe(threadMode = ThreadMode.BACKGROUND) public void onEventBackgroundThread(MessageEvent event){ System.out.println("onEventBackgroundThread::"+" "+Thread.currentThread().getName()); } /** * 创建一个异步线程来执行 * @param event */ @Subscribe(threadMode = ThreadMode.ASYNC) public void onEventAsync(MessageEvent event){ System.out.println("onEventAsync::"+" "+Thread.currentThread().getName()); } /** * 在主线程中运行 * @param event */ @Subscribe(threadMode = ThreadMode.MAIN) public void onEventMain(MessageEvent event){ System.out.println("onEventMain::"+" "+Thread.currentThread().getName()); } /** *默认的线程模式,在当前线程下运行。如果当前线程是子线程则在子线程中,当前线程是主线程,则在主线程中执行。 * @param event */ @Subscribe(threadMode = ThreadMode.POSTING) public void onEventPosting(MessageEvent event){ System.out.println("onEventPosting::"+" "+Thread.currentThread().getName()); } 发布者是在主线程还是子线程,发布的消息在所有定义好的实体类型订阅者中都可以接收到消息。也就是,可以实现一个订阅者订阅多个事件,和一个事件对应多个订阅者。 EventBus.getDefault().post(new MessageEvent("hello")); RxJava RxJava 在 GitHub 主页上的自我介绍是 "a library for composing asynchronous and event-based programs using observable sequences for the Java VM",翻译成中文是,一个在 Java VM 上使用可观测的序列来组成异步的、基于事件的程序的库,也就是一个异步事件库。RxJava 的优势即是简洁,但它的简洁的与众不同之处在于,随着程序逻辑变得越来越复杂,它依然能够保持简洁。 使用 使用RxJava之前需要先添加相关的依赖: compile 'io.reactivex.rxjava2:rxjava:2.1.8' compile 'io.reactivex.rxjava2:rxandroid:2.1.8' 使用RxJava之前,有以下几个概念需要注意: Observeable(被观察者)/Observer(观察者) Flowable(被观察者)/Subscriber(观察者) //被观察者在主线程中,每1ms发送一个事件 Observable.interval(1, TimeUnit.MILLISECONDS) //.subscribeOn(Schedulers.newThread()) //将观察者的工作放在新线程环境中 .observeOn(Schedulers.newThread()) //观察者处理每1000ms才处理一个事件 .subscribe(new Action1() { @Override public void call(Long aLong) { try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } }); 2.x中提供了以上两种观察者与被观察者的关系。Observeable用于订阅Observer,是不支持背压的,而Flowable用于订阅Subscriber,是支持背压(Backpressure)的。背压的概念是:指在异步场景中,被观察者发送事件速度远快于观察者的处理速度的情况下,一种告诉上游的被观察者降低发送速度的策略。 Flowable<String> t = Flowable.create(new FlowableOnSubscribe<String>() { @Override public void subscribe(FlowableEmitter<String> e) throws Exception { e.onNext("hello Rx2.0."); e.onComplete(); } }, BackpressureStrategy.BUFFER); 一般而言,上游的被观察者会响应下游观察者的数据请求,下游调用request(n)来告诉上游发送多少个数据。这样避免了大量数据堆积在调用链上,使内存一直处于较低水平。 我们需要调用request去请求资源,参数就是要请求的数量,一般如果不限制请求数量,可以写成Long.MAX_VALUE。如果你不调用request,Subscriber的onNext和onComplete方法将不会被调用。 Subscriber subscriber = new Subscriber() { @Override public void onSubscribe(Subscription s) { System.out.println("onSubscribe()"); s.request(Long.MAX_VALUE); } @Override public void onNext(Object o) { System.out.println(""+o.toString()); } @Override public void onError(Throwable t) { } @Override public void onComplete() { } }; 除了上面这两种观察者,还有一类观察者: Single/SingleObserver: 返回泛型数据的结果给观察者 Completable/CompletableObserver:返回完成的结果 Maybe/MaybeObserver : 前两者的复合体 Rxjava内置 Scheduler Schedulers.immediate() :默认的,直接在当前线程运行; Schedulers.newThread() :启用新线程,在新线程工作; Schedulers.io():I/O操作(读写文件,读写数据库,网络信息交互等)、和newThread()最大的区别是:io()内部实现是一个无数量上限的线程池,可以重用空闲的线程; Schedulers.computation():计算所使用的 Scheduler。这个计算指的是 CPU 密集型计算,即不会被 I/O等操作限制性能的操作,例如图形的计算。这个 Scheduler 使用的固定的线程池,大小为 CPU 核数。不要把 I/O 操作放在 computation() 中,否则 I/O 操作的等待时间会浪费 CPU。 AndroidSchedulers.mainThread(): Android专用的,指定的操作在Android的主线程运行。 subscribeOn()和observerOn() subscribeOn() 指定subscribe() 所发生的线程,即 Observable.OnSubcribe被激活时所处的线程,或者叫事件产生的线程。 observerOn() 指定Subscriber 运行所在的线程,或者叫做事件消费的线程。
关于如何使用Hexo+Hexo主题搭建博客系统,可以参考我之前的博客的介绍:github pages + Hexo + 域名绑定搭建个人博客,本文主要介绍如何集成文章的打赏功能,打赏的效果如图。该效果就是在每篇文章的后面添加一个打赏功能,当点击“赏”按钮后会弹出一个打赏的窗口,想要体验的可以点击下面的地址来完成体验:http://www.xiangzhihong.com/ 其实上实现也比较简单,我的博客是使用的Snippet主题 ,当然,如果你的前端知识了得,你也可以自己修改样式和风格。在Snippet主题集成打赏功能只需要修改两个地方。 1,新增css文件 首先,打开系统的themes-->snippet文件-->source-->css添加相应的样式。 *,*:before,*:after { -webkit-box-sizing: border-box; -moz-box-sizing: border-box; -ms-box-sizing: border-box; box-sizing: border-box } .reward { padding: 5px 0 } .reward .reward-notice { font-size: 14px; line-height: 14px; margin: 15px auto; text-align: center } .reward .reward-button { font-size: 28px; line-height: 58px; position: relative; display: block; width: 60px; height: 60px; margin: 0 auto; padding: 0; -webkit-user-select: none; text-align: center; vertical-align: middle; color: #fff; border: 1px solid #f1b60e; border-radius: 50%; background: #fccd60; background: -webkit-gradient(linear,left top,left bottom,color-stop(0,#fccd60),color-stop(100%,#fbae12),color-stop(100%,#2989d8),color-stop(100%,#207cca)); background: -webkit-linear-gradient(top,#fccd60 0,#fbae12 100%,#2989d8 100%,#207cca 100%); background: linear-gradient(to bottom,#fccd60 0,#fbae12 100%,#2989d8 100%,#207cca 100%) } .reward .reward-code { position: absolute; top: -220px; left: 50%; display: none; width: 350px; height: 200px; margin-left: -175px; padding: 15px; border: 1px solid #e6e6e6; background: #fff; box-shadow: 0 1px 1px 1px #efefef } .reward .reward-button:hover .reward-code { display: block } .reward .reward-code span { display: inline-block; width: 150px; height: 150px } .reward .reward-code span.alipay-code { float: left } .reward .reward-code span.alipay-code a { padding: 0 } .reward .reward-code span.wechat-code { float: right } .reward .reward-code img { display: inline-block; float: left; width: 150px; height: 150px; margin: 0 auto; border: 0 } .reward .reward-code b { font-size: 14px; line-height: 26px; display: block; margin: 0; text-align: center; color: #666 } .reward .reward-code b.notice { line-height: 2rem; margin-top: -1rem; color: #999 } .reward .reward-code:after,.reward .reward-code:before { position: absolute; content: ''; border: 10px solid transparent } .reward .reward-code:after { bottom: -19px; left: 50%; margin-left: -10px; border-top-color: #fff } .reward .reward-code:before { bottom: -20px; left: 50%; margin-left: -10px; border-top-color: #e6e6e6 } 这个样式是专门处理打赏部分的。 2,然后在post.ejs添加打赏的逻辑 打开系统的themes-->snippet文件-->layout下的post.ejs文件,在post-footer标签之上,也就是文末,添加如下js打赏逻辑。 <!--打赏--> <div class="reward"> <div class="reward-button">赏 <span class="reward-code"> <span class="alipay-code"> <img class="alipay-img wdp-appear" src="http://ohe65w0xx.bkt.clouddn.com/alipay.png"><b>支付宝打赏</b> </span> <span class="wechat-code"> <img class="wechat-img wdp-appear" src="http://ohe65w0xx.bkt.clouddn.com/weipay.png"><b>微信打赏</b> </span> </div> <p class="reward-notice">如果文章对你有帮助,欢迎点击上方按钮打赏作者</p> </div> <!--打赏--> 说明:我这里的图片是第三方存管的。其他模式的主题大体相同,大家可以自行百度修改相应的文件。 附件:打赏功能源码
Redis简介 Redis(官网:https://redis.io)是一个基于内存的日志型可持久化的缓存数据库,保存形式为key-value格式,Redis完全免费开源,它使用ANSI C语言编写。与其他的key - value缓存产品一样,Redis具有以下三个特点。• Redis支持数据的持久化,可以将内存中的数据保存在磁盘中,重启的时候可以再次加载进行使用;• Redis不仅支持简单的key-value类型数据,同时还提供字符串、链表、集合、有序集合和哈希等数据结构的存储;• Redis支持数据备份,即master-slave模式的数据备份。 在Mac系统上,无需下载Redis即可使用它,以下是从Redis的托管服务器下载Redis压缩包并解压的相关命令。 wget http://download.redis.io/releases/redis-4.0.8.tar.gz tar xzf redis-4.0.8.tar.gz cd redis-4.0.8 make 使用Redis提供的服务之前,需要先启动Redis相关的服务,在mac系统上启动Redis的命令如下。 src/redis-server 然后,重新打开一个Redis客户端,使用以下的命令来连接Redis server。 src/redis-cli redis> set foo bar OK redis> get foo "bar" 整合Redis 数据库 使用Redis之前需要引入相关依赖,Maven方式依赖的脚本如下: <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> 之后我们把Redis的相关配置写入yml,这里建议根据之前不同的环境写入不同的配置,Redis默认使用的端口是6379,通常Redis默认使用0号数据库,默认共有16个数据库: #redis配置 redis: # 数据库索引 database: 0 # 服务器地址 host: 127.0.0.1 # 服务器连接端口 port: 6379 # 链接密码 password: # 链接池 pool: # 最大连接数(负值表示没有限制) max-active: 8 # 最大阻塞等待时间(负值表示没有限制) max-wait: 1 # 最大空闲链接 max-idle: 8 # 最小空闲链接 min-idle: 0 # 链接超时时间(毫秒) timeout: 0 如果是application.properties方式,部分配置如下: spring.redis.hostName=127.0.0.1 spring.redis.port=6379 spring.redis.pool.maxActive=8 spring.redis.pool.maxWait=-1 spring.redis.pool.maxIdle=8 spring.redis.pool.minIdle=0 spring.redis.timeout=0 新建RedisConfig.java文件用来存放配置文件。 @Configuration @EnableCaching//开启注解 public class RedisConfig extends CachingConfigurerSupport { @Bean public CacheManager cacheManager(RedisTemplate<?,?> redisTemplate) { CacheManager cacheManager = new RedisCacheManager(redisTemplate); return cacheManager; } @Bean public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, String> redisTemplate = new RedisTemplate<String, String>(); redisTemplate.setConnectionFactory(factory); return redisTemplate; } } 在service包中建立一个RedisService.java类。 public interface RedisService { public void set(String key, Object value); public Object get(String key); } 新建一个service实现类RedisServiceImpl.java。 @Service public class RedisServiceImpl implements RedisService { @Resource private RedisTemplate<String,Object> redisTemplate; public void set(String key, Object value) { ValueOperations<String,Object> vo = redisTemplate.opsForValue(); vo.set(key, value); } public Object get(String key) { ValueOperations<String,Object> vo = redisTemplate.opsForValue(); return vo.get(key); } } 新建Controller层代码UserController.java @Controller @RequestMapping(path="/user") public class UserController { @Autowired private UserService userService; @Autowired private RedisService redisService; //从redis获取某个用户 @RequestMapping(value = "/getuserfromredis", method = RequestMethod.GET) public @ResponseBody User getRedis(@RequestParam String key) { return (User)redisService.get(key); } //获取所有用户 @RequestMapping(value = "/getusers", method = RequestMethod.GET) public @ResponseBody Page<User> list(Model model, Pageable pageable){ return userService.findAll(pageable); } //添加用户 @GetMapping(value="/adduser") public @ResponseBody String addUser(@RequestParam String dictum, @RequestParam String password, @RequestParam String username) { User user = new User(); user.setDictum(dictum); user.setPassword(password); user.setUsername(username); System.out.println(user); userService.saveUser(user); redisService.set(user.getId()+"", user); return "Saved"; } } 本文设计的实体类User.java的代码如下,需要把对象存放在redis需要将对象序列化。 @Entity @Table(name="s_user") public class User implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue(strategy=GenerationType.AUTO) private Integer id; private String username; private String password; private String dictum; @OneToMany(mappedBy = "user", fetch = FetchType. LAZY, cascade = {CascadeType. ALL}) private Set<Photo> setPhoto; //省略getter和setter @Override public String toString() { return "User [id=" + id + ", username=" + username + ", password=" + password + ", dictum=" + dictum + ", setPhoto=" + setPhoto + "]"; } }
小程序自2016年推出以来,可以说是一路火爆,更是改写了移动互联网的格局,开辟了另一流量战场。正所谓,“哪里有商机哪里就有竞争”,据报道,中国九大安卓手机厂商华为、小米、OPPO、vivo、中兴、金立、联想、魅族、努比亚联起手来共同对抗微信小程序的迅猛扩张,他们将于3月20日将共同启动「快应用」标准,打造移动应用新生态,目的是遏制急剧扩张的微信小程序。 快应用简介 什么是快应用,快应用具有哪些特点: 1. 快应用是基于手机硬件平台的新型应用形态,标准是由主流手机厂商组成的快应用联盟联合制定。 2. 快应用标准的诞生将在研发接口、能力接入、开发者服务等层面建设标准平台,以平台化的生态模式对个人开发者和企业开发者全品类开放。 3. 快应用具备传统APP完整的应用体验,无需安装、即点即用。 打开华为市场,搜索“快应用”,可以看到很多的厂商已经上线了快应用。 点击一个运行,其体验丝毫不比原生体验差,下面是录的一个gif动画。 目前,并非所有的平台都上线了快应用,目前还有很多的厂商还在开发中,目前提供快应用的厂商有:小米、华为、金立。魅族、努比亚、OPPO、ViVo,其他平台目前还在开发中。 快应用上手 官网:https://www.quickapp.cn/ 开发文档:https://doc.quickapp.cn/ 既然是快应用,那就是快、方便。快应用使用JavaScript编写,部署即可见。那么如何搭建一个快应用并发布到应用市场呢?本文就这方面给大家做一个整理。 环境搭建 1,安装Node 任何使用JavaScript进行开发的平台都会用到Node,Node可以下载安装,下载的官方地址为:https://nodejs.org/en/download/。 2,安装hap-toolkit 使用npm安装命令安装hap-toolkit: npm install -g hap-toolkit 1 安装完成后,可以通过查看版本来确认是否安装成功。 hap -V 1 3,创建HelloWorld 快应用目前没有很好的开发工具,可以选择前端比较出名的一些开发工具来开发,如WebStrom、IDEA、vscode等。为了演示,本文以命令行的方式来创建一个HelloWorld项目。 创建一个快应用文件夹,选择一个合适文件,使用命令创建项目: hap init <ProjectName> 1 其中,ProjectName为你的项目名称,完成后会显示: prompt: Init your Project: (helloworld) 1 看到这个图不要傻傻等着,点击回车,系统会为你创建一个helloword的快应用。创建完成后,项目的目录结构如图: 然后,切换到helloword目录,执行npm命令行安装依赖包(webpack,babel等): npm install 1 然后,运行如下命令即可在dist目录下生成rpk包。其实,这和vue开发客户端,并使用Hbuilder开发跨平台APP的思路是一致的,有兴趣的童鞋可以了解下。 npm run build 1 注意:如果报错遇到Cannot find module ‘…/webpack.config.js’,请重新执行一次hap update –force。这是由于高版本的npm在npm install时,会校验并删除了node_modules下部分文件夹,导致报错。而hap update –force会重新复制hap-toolkit文件夹到node_modules中。 然后再次执行”npm run build“,即可看到效果。 安装生成的包 下载快应用提供的调试apk文件,并安装到手机上,下载地址: https://statres.quickapp.cn/quickapp/quickapp/201803/file/201803200129552999556.apk 不过对于有些手机你会发现,你安装上之后,什么也干不了,颜色都是灰的。 请注意这很正常,你还需要安装一个平台app 手机安装平台预览版 使用下面地址下载预览版: https://statres.quickapp.cn/quickapp/quickapp/201803/file/201803200130021102030.apk 你安装完成后应该是个白板,啥也没有,正常。你回到之前的安装调试器apk,会发现按钮都可以点击了。然后再次打开即可。 还记得刚才helloworld生成的rpk包么,可以使用以下的命令导入到sd卡中。 adb push xxx.rpk /sdacrd/ 1 push到手机根目录,然后选择本地安装,选择rpk包即可。
这几年,移动跨平台的趋势可以说是越来越明显,技术实现上也是百花争艳,不过究其实现,无外乎有那么几种。 Web 流:也被称为 Hybrid 技术,它基于 Web 相关技术来实现界面及功能。 代码转换流:将某个语言转成 Objective-C、Java 或 C#,然后使用不同平台下的官方工具来开发。 编译流:将某个语言编译为二进制文件,生成动态库或打包成 apk/ipa/xap 文件。 虚拟机流:通过将某个语言的虚拟机移植到不同平台上来运行。 这方面具体的介绍可以查看我之前文章的介绍:移动跨平台开发方案总结。相比较于目前比较好的跨平台开发,有几个比较好的框架:React Native,Flutter和Weex。对于React Native 想必大家应该不陌生,ReactNative 简称是RN ,是 Facebook于15年开源的一个跨平台的框架,目前已经趋于稳定。Flutter则是由Google基于Dart语言开发的一个移动跨平台开发框架,实际上就是以前的Sky SDK,是React Native的竞争对手。Weex 则是阿里开发的一套简单易用的跨平台开发方案,使用较少,没有前面两个名气大。 Flutter 和 React Native 区别 在正式介绍Flutter之前,让我们先来看一下Flutter和React Native实现上的一些异同。 对React Native 稍有了解的读者都知道, React Native 是基于组件进行开发的,这和原生APP的开发思路是一致的,不同的是 React Native提供的组件都是继承自原生Native 的 View 组件,通过调用原生的平台组件来实现UI的绘制工作。比如React Native 中的 ListView 在 Android 中就是继承自 ListView ,还有 RecycleView,对于IOS来说则是TableView组件。 然而 Flutter 则不同,它的所有 UI 组件都是一帧一帧画出来的。Flutter不需要底层的转换操作,因而在界面绘制上更加准确灵活。其次它还非常人性化的贴近了平台的特性,比如 Android 的 Material Design 在 Flutter 就默认支持了进去。 编写语言方面,两平台都是为了推广自己的技术,Flutter 使用的是 Dart 语言开发(基本没怎么听说过),而 React Native 则使用 JSX来开发的,借鉴了React的很多东西。 Dart简介 相信并没有几个读者知道还有 Dart 这种语言,说实在的我也没怎么听过。Dart 是Google于2011年推出的定位应用编程的语言,据说目的是取代传统的JS。相比同时代的go定位服务器系统,Dart可以说并不是很成功。学习Dart可以通过中文社区来学习:http://www.cndartlang.com/。当然,Dart也提供了在线编写运行代码的功能,官方地址为:https://www.dartlang.org/。 Flutter环境搭建 Flutter是Google推出的一款是移动端跨平台开发框架,使用Dart语言编写,一套代码即可同时在Android和iOS平台运行,支持android 4.1以上 和 iOS8以上版本,官方地址为:https://flutter.io/。如果想要了解更多的内容,也可以通过官方的文档来了解:https://flutter.io/faq/#what-is-flutter。 1,下载SDK “工欲善其事,必先利其器”,学习任何一门技术都需要先搭建相关的开发环境,并来一个Hello Word。搭建Flutter环境,读者可以通过Flutter托管在Github上的源码地址来学习。 1,首先,在mac的Terminal输入命令将Flutter SDK下载到本地。命令如下: git clone -b beta https://github.com/flutter/flutter.git 1 2 由于我是Mac 系统,那么Flutter SDK 下载完后的完整路径为:Users/xiangzhihong/Flutter/flutter/ 。接下来需要配置环境变量,打开终端依次输入如下命令: cd $HOME open -e .bash_profile 1 2 添加 Flutter SDK 的路径: export PATH=${PATH}:/Users/xiangzhihong/Flutter/flutter/bin:$PATH 1 然后使用下面的命令更新刚配置的环境变量。 source .bash_profile 1 然后使用命令行“flutter doctor”来检测其他的一些依赖,安装 Futter 剩余依赖项。 cd ./flutter flutter doctor 1 2 这个命令会检查环境并在窗口显示报告,Dart SDK与Flutter捆绑在一起;没有必要单独安装Dart。 最后,Flutter SDK下载后的路径: /Users/用户名/flutter,要注意的是flutter文件夹下面有多个同名的flutter文件夹,真正的SDK路径只到顶层flutter文件夹。 依赖安装完成后,如果不意外,输出内容如下: Doctor summary (to see all details, run flutter doctor -v): [] Flutter (Channel dev, v0.1.7, on Mac OS X 10.12.6 16G1212, locale zh-Hans-CN) [] Android toolchain - develop for Android devices (Android SDK 27.0.0) [!] iOS toolchain - develop for iOS devices (Xcode 9.2) libimobiledevice and ideviceinstaller are not installed. To install, run: brew install --HEAD libimobiledevice brew install ideviceinstaller ios-deploy not installed. To install: brew install ios-deploy [] Android Studio (version 3.0) [] Connected devices (1 available) 1 2 3 4 5 6 7 8 9 10 11 2, 安装idea插件 到jetbrains的官网下载idea开发工具,并为idea添加Flutter插件。 安装完成后重启idea,在新建项目的时候左侧菜单栏有Dart和Flutter说明这两个安装完成了,右边红色方框设置Flutter SDK。 3,测试运行项目 新建一个Flutter工程,工程名不能含大写字母。 如果在创建的过程中出现如下错误,那么Close Project,然后重新打开即可。 如果我们只想简单的实现”Hello World”,用下面的代码替换掉main.dart里面的代码即可。 import 'package:flutter/material.dart'; void main(){ runApp(new Center(child: new Text('Hello Flutter!'))); } 1 2 3 4 然后选择模拟器运行即可。
在Android开发中,合理的使用Android Studio插件不但可以提高开发效率,还能从整体上提高代码的质量。下面就Android开发中常见的一些插件做一个整理。 1,GsonFormat GsonFormat是一个可以快速将json字符串转换成一个Java Bean,免去我们根据json字符串手写对应Java Bean的过程。 使用方法:快捷键Alt+S也可以使用Alt+Insert选择GsonFormat。 2,Android ButterKnife Zelezny 配合ButterKnife实现注解,从此不用写findViewById,想着就爽啊。在Activity,Fragment,Adapter中选中布局xml的资源id自动生成butterknife注解。 3,Android Code Generator 根据布局文件快速生成对应的Activity,Fragment,Adapter,Menu等。 4,Android Parcelable code generator Parcelable是Android实体类的一种实例化方式。 5,Android Methods Count 6,Lifecycle Sorter 可以根据Activity或者fragment的生命周期对其生命周期方法位置进行先后排序,也可以使用快捷键“Ctrl + alt + K”。 7,findBugs-IDEA 查找bug的插件,Android Studio也提供了代码审查的功能(Analyze-Inspect Code…) 8,adb wifi 使用wifi无线调试你的app,无需root权限。 9,AndroidPixelDimenGenerator Android Studio自动生成dimen.xml文件插件。 10,JsonOnlineViewer 在Android Studio中请求、调试接口。 11,Android Styler a. copy lines with future style from your layout.xml file b. paste it to styles.xml file with Ctrl+Shift+D (or context menu) c. enter name of new style in the modal window d. your style is prepared! 12,Android Drawable Importer 这是一个非常强大的图片导入插件。它导入Android图标与Material图标的Drawable ,批量导入Drawable ,多源导入Drawable(即导入某张图片各种dpi对应的图片)。 13,SelectorChapek for Android 通过资源文件命名自动生成Selector文件。 14,genymotion 15,LeakCanary 帮助你在开发阶段方便的检测出内存泄露的问题,使用起来更简单方便。 16,Android Postfix Completion 可根据后缀快速完成代码,这个属于拓展吧,系统已经有这些功能,如sout、notnull等,这个插件在原有的基础上增添了一些新的功能,我更想做的是通过原作者的代码自己定制功能。 17,Android Holo Colors Generator 通过自定义Holo主题颜色生成对应的Drawable和布局文件。 18,dagger-intellij-plugin dagger可视化辅助工具。 19,GradleDependenciesHelperPlugin maven gradle 依赖支持自动补全插件。 20,RemoveButterKnife ButterKnife这个第三方库每次更新之后,绑定view的注解都会改变,从bind到inject,再到bindview,搞得很多人都不敢升级,一旦升级,就会有巨量的代码需要手动修改,非常痛苦。此时可以使用RemoveButterKnife插件。 21,AndroidProguardPlugin 一键生成项目混淆代码插件,值得你安装。 22,otto-intellij-plugin 23,eventbus-intellij-plugin 24,idea-markdown 25,folding-plugin 布局文件分组的插件。 26,gradle-retrolambda 在java 6 7中使用 lambda表达式插件需要修改编译的jdk为java8。 27,CheckStyle-IDEA CheckStyle-IDEA 是一个检查代码风格的插件,比如像命名约定,Javadoc,类设计等方面进行代码规范和风格的检查,你们可以遵从像Google Oracle 的Java 代码指南 ,当然也可以按照自己的规则来设置配置文件,从而有效约束你自己更好地遵循代码编写规范。 28,PermissionsDispatcher plugin 自动生成6.0权限的代码。 29,WakaTime 记录你在IDE上的工作时间。 30,AndroidLocalizationer 可用于将项目中的 string 资源自动翻译为其他语言的 Android Studio/IntelliJ IDEA 插件。
从2016年开始关注React Native到现在,React Native的每一个版本发布我都会关注一下,虽然最近将重心转移到区块链开发上,这一年里,我还出版了一本《React Native移动开发实战》的书。在过去的一年中React Native经历了十几次的版本迭代,版本也从从v0.40升级到v0.52,总体来说,版本迭代没以前那么频繁,组件也越来越丰富,稳定性也越来越好了,下面就一些新组件,新API进行相关的总结。 React Native年度功能 首先,借用网络上的一张图,一个使用Xmind绘制的React Native功能的图,该图简单明了的介绍了React Native在2017年的一些变化。 其发布的版本即频率如下图:可以看到,在这一年中,React Native更新的内容如下:仅针对 Android: 新特性 218 个、修复 bug 79 个 ;仅针对 iOS: 新特性 286 个、修复 bug 96 个; 双平台通用: 新特性 608 个、修复 bug 157 个、重大变更 35 个。 如果用图形表示,则如下图所示: 版本更新详解 如果要总结下每个版本更新的内容,可以看下面的介绍。 0.42 iOS:不再支持 Xcode7.x 编译,升级为 Xcode8.x; Android:移除 RecyclerViewBackedScrollView 组件 通用:WebView 组件新增 injectJavaScript 方法; 通用:为组件的部分属性添加百分比支持; 通用: init 项目时可以添加模板。 0.43 通用:FlatList 正式发布; 通用:样式支持 alignContent 属性; 通用:init 项目时的模板可以自定义了。 0.44 通用:不再支持通过 @provides NameOfModule 导入模块; 通用:将 Navigator 组件标记为过期; iOS:移除 MapViewIOS 组件,建议使用 Airbnb 的 react-native-maps。 0.45 通用:添加支持通过 CameraRoll 组件访问视频。 0.46 通用:引入 ImageBackground 组件。 0.47 Android: link 命令支持关联 Kotlin 模块; Android:为 AndroidViewPager 添加 peekEnabled 属性。 0.48 iOS:移除 AdSupportIOS 组件。 0.49 通用:将 index.ios.js 与 index.android.js 合并为 index.js; 通用:TextInput 组件添加 autoGrow 属性。 0.51 通用: 组件中不再支持嵌套组件; 通用:添加 SwipeableFlatList 组件(实验性); Android:添加对 Android 8.0 的支持。 0.51 通用:padding,margin,border 等属性支持 RTL 布局方式; 更新内容 新增组件 在这一年里,React Native一个新增了8个组件。大家可以从中文文档获得更多的介绍信息。 CheckBox:一个用在React Native上的复选框组件,(目前仅支持Android,未来会支持iOS) ImageBackground:背景图片组件,它是一个容器组件,支持包含其他组件 VirtualizedList:FlatList和 SectionList 的底层实现。 FlatList:基于VirtualizedList的高性能简单列表组件。 SwipeableFlatList:一个带滑动显示更多菜单的FlatList组件; SectionList:基于VirtualizedList的高性能分组(section)列表组件。 MaskedViewIOS:可以为组件添加一个透明的遮罩; SafeAreaView:用于包裹其他View,它会自动应用填充布局中不足的一部分,但不包括navigation bars, tab bars, toolbars等视图。 新增API函数 AccessibilityInfo:一个用于判断屏幕阅读器是否处于激活状态的API。 DeviceInfo:一个类专门提供屏幕尺寸,字体缩放等信息的API。 BackHandler:监听设备上的后退按钮事件(Android、Apple TV)。 findNodeHandle:用于获取组件的本地节点句柄的API。 TVEventHandler: 一个用于接受Apple TV远程事件(如遥控器的事件)的API。 YellowBox:通过这个API可以屏蔽指定的警告。 其他新增 ViewPropTypes:View 中的 propTypes 被移到 ViewPropTypes中,使用时需要单独导包。 takeSnapshot:将 takeSnapshot 方法从 UIManager 移动到ReactNative。 废弃组件及API 随着React Native版本的更新,React Native废弃了一些过时的API和组件。 BackAndroid:使用功能更丰富的BackHandler代替; Navigator:使用react-navigation代替; ListView:使用FlatList代替; MapView:使用react-native-maps代替此地图组件; RecyclerViewBackedScrollView:现在直接通过ScrollView即可解决滚动冲突; AdSupportIOS:使用react-native-deprecated-modules或react-native-idfa代替; NavigationExperimental:使用react-navigation代替;
目录 awesome-kotlin-android 关于 目录 开源库 框架 DSL 扩展 UI 通用库 动画 Toolbar 按钮 依赖注入 数据绑定 代理 数据库 网络 日志 函数式编程 下载 图片 拍照 工具 其他 完整 app DEMO 书籍 视频 开源库 框架 KBinding - 使用kotlin实现的Android MVVM框架 Kotlin-Android-Template - 快速生成MVP 架构的项目模板 android-clean-architecture-boilerplate - clean 框架模板 DSL anko - JetBrains 官方为Android编写的 DSL,旨在令开发 Android 更快更简单 android-drawable-dsl - 通过 kotlin 构造 drawable 而不是 XML 的 DSL MaterialDrawerKt - 不使用 XML 创建 Material Design 导航抽屉 扩展 android-ktx - google 开源的 Kotlin 扩展插件库,在 Android 框架和 Support Library 上提供相应 API 层,帮助开发者更自然编写 Kotlin 代码 KAndroid - 轻量级Kotlin 扩展插件库 kotlin-jetpack 有用的扩展方法集合 kotlin-koi - 又一个轻量级Kotlin 扩展插件库 UI 通用库 anvil - 一个受React启发的Android的最小UI库 动画 Konfetti - 轻量五彩纸屑粒子系统 效果图: transitioner - 动态、简单的View场景切换动画 效果图: Toolbar JellyToolbar - Yalantis出品,必属精品!炫酷 toolbar 实现 效果图: 按钮 Stepper-Touch - Material Design设计风格的触摸步进器 效果图: 依赖注入
开发环境 TensorFlow: 1.2.0 Python: 3.6 Python IDE: PyCharm 2017.2 Android IDE: Android Studio 3.0 训练与评估 训练和评估部分主要目的是生成用于测试用的pb文件,其保存了利用TensorFlow python API构建训练后的网络拓扑结构和参数信息,实现方式有很多种,除了cnn外还可以使用rnn,fcnn等。其中基于cnn的函数也有两套,分别为tf.layers.conv2d和tf.nn.conv2d, tf.layers.conv2d使用tf.nn.conv2d作为后端处理,参数上filters是整数,filter是4维张量。原型如下: convolutional.py文件 def conv2d(inputs, filters, kernel_size, strides=(1, 1), padding=’valid’, data_format=’channels_last’,dilation_rate=(1, 1), activation=None, use_bias=True, kernel_initializer=None,bias_initializer=init_ops.zeros_initializer(), kernel_regularizer=None, bias_regularizer=None,activity_regularizer=None, kernel_constraint=None, bias_constraint=None, trainable=True, name=None,reuse=None) gen_nn_ops.py 文件 def conv2d(input, filter, strides, padding, use_cudnn_on_gpu=True, data_format="NHWC", name=None)官方Demo实例中使用的是layers module,结构如下: Convolutional Layer #1:32个5×5的filter,使用ReLU激活函数 Pooling Layer #1:2×2的filter做max pooling,步长为2 Convolutional Layer #2:64个5×5的filter,使用ReLU激活函数 Pooling Layer #2:2×2的filter做max pooling,步长为2 Dense Layer #1:1024个神经元,使用ReLU激活函数,dropout率0.4 (为了避免过拟合,在训练的时候,40%的神经元会被随机去掉) Dense Layer #2 (Logits Layer):10个神经元,每个神经元对应一个类别(0-9) 核心代码在cnn_model_fn(features, labels, mode)函数中,完成卷积结构的完整定义,核心代码如下: 也可以采用传统的tf.nn.conv2d函数, 核心代码如下: 测试 核心是使用API接口: TensorFlowInferenceInterface.java 配置gradle 或者 自编译TensorFlow源码导入jar和so compile ‘org.tensorflow:tensorflow-android:1.2.0’ 导入pb文件.pb文件放assets目录,然后读取 String actualFilename = labelFilename.split(“file:///android_asset/“)[1]; Log.i(TAG, “Reading labels from: “ + actualFilename); BufferedReader br = null; br = new BufferedReader(new InputStreamReader(assetManager.open(actualFilename))); String line; while ((line = br.readLine()) != null) { c.labels.add(line); } br.close(); TensorFlow接口使用如下: 最终的测试效果为: 理论基础 MNIST MNIST,最经典的机器学习模型之一,包含0~9的数字,28*28大小的单色灰度手写数字图片数据库,其中共60,000 training examples和10,000 test examples。文件目录如下,主要包括4个二进制文件,分别为训练和测试图片及Label。 如下为训练图片的二进制结构,在真实数据前(pixel),有部分描述字段(魔数,图片个数,图片行数和列数),真实数据的存储采用大端规则。(大端规则,就是数据的高字节保存在低内存地址中,低字节保存在高内存地址中) 在具体实验使用,需要提取真实数据,可采用专门用于处理字节的库struct中的unpack_from方法,核心方法如下: struct.unpack_from(self._fourBytes2, buf, index) MNIST作为AI的Hello World入门实例数据,TensorFlow封装对其封装好了函数,可直接使用 mnist = input_data.read_data_sets(‘MNIST’, one_hot=True) CNN(Convolutional Neural Network) CNN,英文Convolutional Neural Network,中文全称卷积神经网络,即所谓的卷积网(ConvNets)。卷积(Convolution)可谓是现代深度学习中最最重要的概念了,它是一种数学运算,读者可以从下面链接理解卷积Convolution中卷积相关数学机理,包括分别从傅里叶变换和狄拉克δ函数中推到卷积定义,我们可以从字面上宏观粗鲁的理解成将因子翻转相乘卷起来。卷积动画模型如下图所示: 神经网络:一个由大量神经元(neurons)组成的系统,如下图所示:其中,x表示输入向量,w为权重,b为偏值bias,f为激活函数。 Activation Function 激活函数: 常用的非线性激活函数有Sigmoid、tanh、ReLU等等,公式如下所示。 Sigmoid函数:函数饱和使梯度消失(神经元在值为 0 或 1 的时候接近饱和,这些区域,梯度几乎为 0)。同时,sigmoid 函数不是关于原点中心对称的(无0中心化)。 tanh: 存在饱和问题,但它的输出是零中心的,因此实际中 tanh 比 sigmoid 更受欢迎。 ReLU函数:ReLU 对于 SGD 的收敛有巨大的加速作用,只需要一个阈值就可以得到激活值,而不用去算一大堆复杂的(指数)运算。缺点是:需要合理设置学习率(learning rate),防止训练时dead,还可以使用Leaky ReLU/PReLU/Maxout等代替。 Pooling池化:一般分为平均池化mean pooling和最大池化max pooling,如下图所示[21]为max pooling,除此之外,还有重叠池化(OverlappingPooling)[24],空金字塔池化(Spatial Pyramid Pooling) **平均池化**:计算图像区域的平均值作为该区域池化后的值。 最大池化:选图像区域的最大值作为该区域池化后的值。 CNN Architecture 三层神经网络:分别为输入层(Input layer),输出层(Output layer),隐藏层(Hidden layer),如下图所示。 CNN层级结构: 斯坦福cs231n中阐述了一种[INPUT-CONV-RELU-POOL-FC],如上图右边图片所示,分别为输入层,卷积层,激励层,池化层,全连接层。 CNN通用架构分为如下三层结构: Convolutional layers 卷积层 Pooling layers 汇聚层 Dense (fully connected) layers 全连接层 用动画演示如下图: Regression + Softmax 机器学习有监督学习(supervised learning)中两大算法分别是分类算法和回归算法,分类算法用于离散型分布预测,回归算法用于连续型分布预测。回归的目的就是建立一个回归方程用来预测目标值,回归的求解就是求这个回归方程的回归系数。其中回归(Regression)算法包括Linear Regression,Logistic Regression等, Softmax Regression是其中一种用于解决多分类(multi-class classification)问题的Logistic回归算法的推广,经典实例就是在MNIST手写数字分类上的应用。 Linear Regression Linear Regression是机器学习中最基础的模型,其目标是用预测结果尽可能地拟合目标label。 多元线性回归模型定义 多元线性回归求解 Mean Square Error (MSE) Gradient Descent(梯度下降法) Normal Equation(普通最小二乘法) 局部加权线性回归(LocallyWeightedLinearRegression, LWLR ):针对线性回归中模型欠拟合现象,在估计中引入一些偏差以便降低预测的均方误差。 岭回归(ridge regression)和缩减方法。 ### 选择 Normal Equation相比Gradient Descent,计算量大(需计算X的转置与逆矩阵),只适用于特征个数小于100000时使用;当特征数量大于100000时使用梯度法。当X不可逆时可替代方法为岭回归算法。LWLR方法增加了计算量,因为它对每个点做预测时都必须使用整个数据集,而不是计算出回归系数得到回归方程后代入计算即可,一般不选择。 调优 平衡预测偏差和模型方差(高偏差就是欠拟合,高方差就是过拟合),通常有以下几种解决方案: 获取更多的训练样本 - 解决高方差 尝试使用更少的特征的集合 - 解决高方差 尝试获得其他特征 - 解决高偏差 尝试添加多项组合特征 - 解决高偏差 尝试减小 λ - 解决高偏差 尝试增加 λ -解决高方差 Softmax Regression Softmax Regression估值函数(hypothesis)。Softmax Regression代价函数(cost function)。 用实例来表示如下图所示: Softmax Regression & Logistic Regression: 多分类 & 二分类。Logistic Regression为K=2时的Softmax Regression。 针对K类问题,当类别之间互斥时可采用Softmax Regression,当非斥时,可采用K个独立的Logistic Regression。 总的来说, Softmax Regression适用于类别数量大于2的分类,本例中用于判断每张图属于每个数字的概率。 附录 [01]Mnist官网[02]Visualizing MNIST: An Exploration of Dimensionality Reduction[03]TensorFlow Mnist官方实例[04]Sample code for “Tensorflow and deep learning, without a PhD”[05]Convex functions[06]斯坦福大学机器学习第七课-正则化-regularization[07]MachineLearning_Python[08]Stanford University’s Convolutional Neural Networks for Visual Recognition course materials 翻译[09]July CNN笔记:通俗理解卷积神经网络[10]理解卷积Convolution[11]Imagenet classification with deep convolutional neural networks[12]Spatial Pyramid Pooling in Deep Convolutional Networks for Visual Recognition[13]Convolutional Neural Networks-Basics[14]A technical report on convolution arithmetic in the context of deep learning[15]Google官方Demo[16]Google官方Codelab[17]deep-learning-cnns-in-tensorflow Github[18]tensorflow-classifier-android[19]creating-custom-model-for-android-using-tensorflow[20]TF-NN Mnist实例
区块链天生具有的不可更改性和去中心化特性,使得开发许多令人惊叹的使用案例成为可能,例如自治组织、销售、社交网络、保险公司以及成百上千人之间的游戏。 近日,根据 dApp 白皮书介绍,相对于后端代码运行在集中的服务器的 App 而言,dApp 的后台代码基本上运行在一个去中心化的点对点网络。 本文将阐述如何使用 React Native 来制作一个跨平台的移动 dApp,用于将你最爱的密码朋克(cryptopunks) 进行排名。 为什么是密码朋克? 密码朋克是一个了不起的项目,神一样的存在。如果想了解更多信息,可以查看下面的 reddit 中的博客。 技术点 在介绍实例之前,我们先来看一些基础的概念: React Native 是一个由 Facebook 开发的框架,允许你使用 JavaScript 和 React 构建跨平台的移动原生App。 Expo 是一个工具集,由于它包括了一系列开箱即用的原生 API,例如照像机等,因此使得上手构建 React Native项目变得非常简单。 Web3 是兼容 Ethereum 的 JavaScript API,实现了用于 node.js 和浏览器的 通用 JSON RPC规范。 Truffle 是一个 Ethereum 开发环境,提供了 ganache-core 等非常适合上手 React Native开发的一系列工具。ganache-core 在本地模拟了一个 Ethereum 网络。 其它值得一提的比较酷的库有 react-navigation、victory-native 和 react-antive-star-rating。 Ethereum区块链 在 React Native App 上运行 web3.js JavaScript API 有许多 公开的问题,而且目前看起来还没有 切实的解决方案。 这是因为 React Native 使用 JavaScriptCore 执行环境,并且依赖于针对 React Native App 的 Node 标准库 API(例如 buffer、crypto 或者 stream)是如何模拟或者实现的,这可能需要 链接到一些原生的依赖;因此,你也许需要使用 expo App,因为它有非常详细的样例项目,例如 react-nativify,在 React Native 环境引入了 Node API。 因此,当我寻找可选方案并且发现了 expo 上的功能请求 之后,作为一种解决方案,我构建了一个针对 React Native 的 babel preset,幕后使用了 crypto-browserify 和一个非常小的 randombytes(随机字节)的纯 JavaScript 实现。 但是要注意,JavaScript 的Math.random()函数可能会被看作是一个加密学上来讲可预测的随机数生成器,但我现在并不担心这点,因为 ethereumjs-tx 不需要用它来为交易签名。 投票交易 在主网(主要的 Ethereum 网络)上,所有的交易都是以实际的 ether 或 gas(译注:以太坊的两种计价单位)来估价的,但是我的实验性 App 部署在 Testnet Ropsten 上。这是一个用于开发的实际区块链场景,每秒每笔交易只允许 5 次投票。感谢 faucet 免费提供了用于测试的 ether。 在这个简单的实现中,每个 Vote/Star 差不多值 0.0012 Ethers(即当天的 1.31 美元)。那一点也不便宜,但是如果每次投票消耗的 gas 减少,费用可能会被提高。 在那种情况下的风险是,矿工可能花费数小时或数天来处理那么低额的交易。 关于如何通过在区块链中使用 soft forks(软分支)、LN、side chains(副链)或者 micro transactions(微交易)来优化这个问题,有许多持续的讨论。 Web3 供应商困境 交易是一个用于修改区块链状态的指令集。为了对 Ethereum 交易进行签名并且消费 gas 和实际的 ethers,需要一个公开的地址和一个私钥,或者一个至少配置有一个没有锁定的币库账号的 HD 钱包来为投票交易进行支付。 有许多不同的配置 web3 供应商的方法来访问 Web 上的 dApps:通过 MetaMask Chrome Extension 注入了一个 ethereum 特制浏览器,例如 Mist;或者是通过创建一个本地实例,然后用代码给账号充值。 问题是,没有这样针对 React Native 的浏览器,并且 web3 不能注入在 App 中,因此,在这次试验中,我最终用 truffle-hdwallet-provider 配置了一个币库。 另一个有效选择是使用 MetaMask 的 web3-provider-engine,它允许你通过一个使用一个 纯 JavaScript 的子供应商来为交易签名,但是情况实际上相同,因为 truffle-hdwallet-provider 幕后使用了一种相似的机制。 在上述两种意见中,账户都是编码在移动 App 中的,这在实际的生产环境可能是不安全的,而且缺乏灵活性。 询问用户的公玥和私钥来为交易签名和为投票进行支付可能是一种简单的替代方案,但是这种方案因为超级不安全而被废弃了。 或者使用 uport 来注册投票者的识别码,但是我还 不确定是否支持 React Native。 import Contract from 'truffle-contract'; import VotingArtifact from '../build/contracts/Voting.json'; import Web3 from 'web3'; function Star(candidate) { const Voting = Contract(VotingArtifact); var web3Provided; // Supports Metamask and Mist, and other wallets that provide 'web3'. if (typeof web3 !== 'undefined') { // Injected web3 detected. // Use the Mist/wallet provider. // eslint-disable-next-line web3Provided = new Web3(web3.currentProvider); } else { // No web3 instance injected, using Local web3. web3Provided = new Web3(AnySupportedProvidedWithHardcodedAccount); } Voting.setProvider(web3Provided.currentProvider); web3Provided.eth.getAccounts((error, result) => { Voting.deployed().then(contractInstance => { contractInstance.voteForCandidate(candidate, { // configure the priority of the vote gas: 140000, // *** // In a real dapp the coinbase should be provided by your wallet. // *** from: result[0], }); }); }); } react-native-geth 项目实现了一个轻量的客户端 Ethereum 节点,因此我认为它有望成为可能产生的 React Native HD 钱包的一个关键依赖,通过这种 React Native HD 钱包,可以将 web3 注入到任何给定的 App 中,绝对雄心勃勃。 智能合约 我用 Solidity 语言创建了一个简单的投票合约,使用 truffle-contract 作为一种抽象接口,以便在移动 dApp 中使用它。 合约是不可更改的。一旦合约被创建并部署到区块链上,就不能改变、撤回或者修改。Voting 有一个构造器,这个构造器用一个 cryptopunks 数组初始化,并且基于他们的主要附属特征给他们分配了一个识别符名称。如果你对合约感到好奇,它就部署在 Ropsten Testnet 网络上,可以随时查看。 最后是示例代码:https://expo.io/@agrcrobles/react-native-blockchain-pollhttps://github.com/agrcrobles/react-native-blockchain-poll 如今尽管区块链带来了大量的使用案例,但是大公司通常没有在移动 App 上采用区块链。因为所有的 ICOs 都是基于 Web 的。 来自 cipherbrowser 和 status.im 的人们正在创作移动 dApp 浏览器,这是一件了不起的工作。我想他们已经完成了那一步。 我支持去中心化的跨平台移动 App 的想法。随着时间推移,React Native 越来越成熟和稳定,并且被大公司采用来开发真正伟大的移动 Apps(事实上,status.im 移动 App 就是基于 React Native)。并且我十分确信,不久就可以在 React Native 中使用区块链来构建真正的移动 dApp 了。 附:原文链接:https://hackernoon.com/bringing-the-blockchain-to-react-native-98b76e15d44d密码朋克相关博客:https://www.reddit.com/r/ethtrader/comments/7hdycd/if_you_think_cryptokitties_is_about_cats_youre/
在Kotlin和Javascript平台的互操作过程中,往往会涉及Kotlin代码和 Javascript 代码相互转换的过程,本文主要介绍如何将Kotlin代码编译成Javascript 代码。 1,创建JavaScript的应用程序 首先创建一个新的应用程序或目标JavaScript模块时,并需要选择Kotlin - JavaScript作为编译运行目标。 默认情况下,插件选择与当前安装版本关联的插件。除非我们要创建一个不同的项目,否则我们可以在输入项目名称和位置后点击Finish。 项目创建完成后,项目结构如下图所示: 2,新建项目 接下来,可以开始编写Kotlin代码。例如: fun main(args: Array<String>) { val message = "Hello JavaScript!" println(message) } 现在需要一个HTML页面来加载代码,所以我们创建一个名为index.html的文件。 编译输出代码说明 将 Kotlin 代码编译为 Javascript 代码后会得到两个主要的文件: Kotlin.js. :运行时和标准库,这部分代码只与 Kotlin 的版本有关而不会因为不同的应用而有所不同。 {module}.js:真正的应用代码,所有的应用代码最终都会编译成一个 JavaScript 文件并与模块的名字同名。 除此之外,每一个源码文件都会有一个关联的 {file}.meta.js 元文件,该文件可用来做反射或是其他的功能。Kotlin 编译器将会输出如下代码: 而大家最关心的莫过于ConsoleOutput.js,该文件的内容如下: var ConsoleOutput = function (Kotlin) { 'use strict'; var _ = Kotlin.defineRootPackage(null, /** @lends _ */ { main_kand9s$: function (args) { Kotlin.println('Hello JavaScript!'); } }); Kotlin.defineModule('ConsoleOutput', _); _.main_kand9s$([]); return _; }(kotlin); 如上代码就是 kotlin main 函数编译后得到的代码,我们可以看到编译后的代码定义了一个函数并赋值给了一个与模块名同名的变量,然后通过传入的 Kotlin 变量来调用 define rootPackage 函数。通过 Kotlin 变量我们可以使用 kotlin.js 标准库中的方法。 编译前的代码只有一个 main 函数,编译之后该函数被添加了后缀,这么做的目的主要是为了防止重载 Kotlin 中的代码,Kotlin 中的这部分功能是为了将源码转换成对应的 javascript 代码。 最后定义为一个立即执行函数,当这部分代码加载之后就会立即执行,并将 Kotlin 做为参数传进去,这样就可以使用 Kotlin.js 中定义的方法了。 运行编译后的代码 这部分代码的目的是为了通过 console 输出文本,在这里我们需要通过 HTML 页面加载并在浏览器中运行。 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Console Output</title> </head> <body> <script type="text/javascript" src="out/production/ConsoleOutput/lib/kotlin.js"></script> <script type="text/javascript" src="out/production/ConsoleOutput/ConsoleOutput.js"></script> </body> </html> 注:我们需要先加载 kotlin.js 文件,再加载我们的应用文件。 调试应用程序 为了使用IntelliJ IDEA调试应用程序,我们需要执行两个步骤: 安装JetBrains Chrome扩展,它允许通过Chrome在IntelliJ IDEA中进行调试。这对于用IntelliJ IDEA开发的任何类型的Web应用程序都很有用,而不仅仅是Kotlin; 配置Kotlin编译器生成源地图,可通过 Preferences|Kotlin Compiler。 一旦完成,我们只需右键单击我们的index.html文件,然后选择调试选项。这将启动Chrome,然后在IntelliJ IDEA中的代码中定义的断点处停止,我们可以在其中评估表达式,逐步执行代码等。 也可以使用标准的Chrome调试器来调试Kotlin应用程序,只要确保你生成源地图。 配置编译器选项 Kotlin提供了一系列可在IntelliJ IDEA中访问的编译器选项。常见的如下: 输出文件前缀。我们可以在编译器生成的输出前加上额外的JavaScript。为了做到这一点,我们在这个框中指出了包含我们想要的JavaScript的文件的名字。 输出文件后缀。同上,但在这种情况下,编译器会将所选文件的内容追加到输出中。 复制运行时库文件。指示我们希望将该kotlin.js库输出到哪个子文件夹中。默认情况下,lib这就是为什么在HTML中我们引用这个路径。 模块种类。指示要遵循的模块标准。这在“ 使用模块”教程中有更深入的介绍。 附:http://kotlinlang.org/docs/tutorials/javascript/getting-started-idea/getting-started-with-intellij-idea.html
昨天(2月7日),有匿名开发者在 GitHub 上传了 iOS 核心组件的源代码,这可能会促进黑客和安全研究人员找到 iOS 漏洞,并使 iPhone 陷入危险境地。git地址:https://github.com/h1x0rz3r0/iBoot。不过,8日早上已经看不到相关的源码信息了,该项目已经转为Private,相关信息可以查看下面的链接:https://github.com/github/dmca/blob/master/2018/2018-02-08-Apple.md iBoot 是 iOS 关键的源代码之一,在 GitHub 上被标记为“iBoot”,它确保了操作系统的可信任启动,换句话说,它是加载 iOS 的程序,是开启 iPhone 运行的第一个进程,它加载并验证内核是否被苹果正确签名,然后执行,就像 windows系统的 BIOS 一样。 该代码适用于 iOS 9,但是部分代码可能在 iOS 11 中仍有使用。尽管在近几年,iOS 和 macOS 的某些代码已经逐渐开源,但苹果本质上还是非常不乐意向公众开放源代码。而且苹果已经十分注意 iBoot 的安全性和其代码的私密性, 如果通过苹果的赏金计划向其报告启动过程中的 bug,可最高获得 20 万美元。iOS 和 Mac OSX 内部系列书籍的作者 Jonathan Levin 说:“这是 iOS 历史上最大的漏洞,也将是一件大事。” Levin 说代码似乎是真正的 iBoot 代码,因为它与他自己逆向工程的代码一致。熟悉 iOS 的另一位安全研究人员也表示,他们认为代码是真实的,但他们不知道谁在泄漏,苹果到目前为止也没有回应。 Levin 说,通过访问 iBoot 的源代码,iOS 安全研究人员可以更好地找到可能导致设备泄密或越狱的漏洞。这意味着黑客们可以更轻松地找到允许他们破解或解密 iPhone 的漏洞和 bug。也许,这种泄漏最终可能会让高级程序员在非苹果平台上模拟 iOS。 以前版本的 iBoot 中的漏洞使得破解者和黑客可以通过 iPhone 的锁屏解密用户的数据。但是新的 iPhone 有一个名为 Secure Enclave Processor 的芯片,它加强了设备的安全性。 Levin 补充说道,对于普通用户来说,这意味越狱会更加容易。这些越狱过去相对容易实现,并且很普遍,但是现在使用最新的 iOS 设备是非常困难的,这些设备具有先进的安全机制,即使是高技能的研究人员也很难找到 bug,因为他们需要在开始探测设备之前,得先让设备越狱。 这些安全改进已经有效地将曾经流行的越狱社区扼杀在摇篮中。现在,在 iOS 中查找 bug 和漏洞是需要大量时间和资源的,因此产生的漏洞非常有价值。这就是为什么越狱社区会为源代码的泄露或任何公开发布的漏洞而感到兴奋。 这个源代码在去年首次出现,由 Jailbreak subreddit 上的一个名为“ apple_internals ” 的 Reddit 用户发布。这个帖子没有得到太多的关注,因为用户是新的,而且没有足够的 Reddit karma; 这个帖子很快沉下去了。但它在 GitHub 上再次出现意味着它可能在地下越狱社区和 iOS 黑客圈中广泛流传。 Levin 说:“iBoot 是苹果一直坚持的一个组件,他们仍然在加密它的 64 位代码。而现在,它却以源代码形式开放了。” 该文章的英文链接为:https://motherboard.vice.com/en_us/article/a34g9j/iphone-source-code-iboot-ios-leak
前天,Google 发布了 Android KTX 预览版,Android KTX 是一组扩展程序,它能使 Android 上的 Kotlin 代码更简洁,从而提高开发者的编程体验。 大家知道,Google在2017年的Google I/O大会上将Kotlin列为第一开发语言之后,便不遗余力的支持Kotlin。Android KTX 中支持 Android 框架的部分现在可在 GitHub 库中找到,同时,Google 承诺在即将到来的支持库版本中提供涵盖 Android 支持库的 Android KTX 的其他部分。那么,相比于通用的Kotlin,Android KTX究竟做了哪些方面的优化呢,下面通过一些示例来简单对比下。 示例 字符串转换为 URI 通常情况下为 Uri.parse(uriString),但是 Android KTX 会为字符串添加一个扩展函数,使字符串更加自然地转换为 URI。 SharedPreferences 编辑 SharedPreferences 是非常常见的用例,使用 Android KTX 后,代码稍微短些,能更自然地读取和写入。 平移路径差异 例如,下面是将两个路径之间的距离改变了 100px。 在视图onPreDraw 的动作 下面的示例触发了视图中 onPreDraw 的回调,如果没有 Android KTX,你需要编写相当多的代码。 除了上面介绍的一些API之外,还有很多其他的特性,详细的介绍读者可以访问Android KTX开源地址:https://github.com/android/android-ktx。 Android集成 要在你的 Android Kotlin 项目中开始使用 Android KTX,需要在应用模块 build.gradle的脚本文件中添加以下配置脚本: repositories { google() } dependencies { implementation 'androidx.core:core-ktx:0.1' } 在同步项目之后,这些扩展将自动出现在 IDE 的自动完成列表中,选择扩展程序会将必要的导入语句添加到你的文件中。 注意:不过需要注意的是,Android KTX目前还是一个预览版本,预览期间 API 可能会发生变化,也就是说,在正式版到来之前,不要在重要的 Android 项目中使用它,因为正式版可能会发生一些变化。 Android KTX未来发展趋势 Google 表示,现在的预览版本是一个开始,在接下来的几个月里,他们会根据开发者的反馈和贡献加入 API 进行迭代,当 API 稳定后,Google 会承诺 API 的兼容性,并计划将 Android KTX 作为 Android 支持库的一部分。 1,可以通过如下地址来提交相关的建议和修改意见:https://github.com/android/android-ktx/issues/new
最近区块链的话题很火,有人想用它改变世界,有人想用它招摇撞骗。其实,说道区块链就不得不提分布式系统,可以说分布式是基础,区块链不过是在分布式的基础上做了一些“封装”,从技术的角度看,区块链是一种与分布式系统有关的技术。今天不具体聊区块链,今天具体聊分布式。 分布式系统 一个由区块链技术支撑的系统,比如比特币网络或以太坊,从技术上看,是一个很庞大的分布式系统。因此,我们首先从分布式系统开始说起。 对于技术人员来说,特别是服务器开发人员,几乎每个人都经常和分布式系统打交道。当服务的规模越来越大的时候,它必然发展成一个复杂的分布式系统。很典型的,就是各种分布式数据库,它们通常能将数据以某种方式在多个节点上存储,在高可用的基础上保证数据的一致性。 实际上,一致性问题(consensus problem)是分布式系统需要解决的一个核心问题。分布式系统一般是由多个地位相等的节点组成,各个节点之间的交互就好比几个人聚在一起讨论问题。让我们设想一个更具体的场景,比如三个人讨论中午去哪里吃饭,第一个人说附近刚开了一个火锅店,听说味道非常不错;但第二个人说,不好,吃火锅花的时间太久了,还是随便喝点粥算了;而第三个人说,那个粥店我昨天刚去过,太难喝了,还不如去吃麦当劳。结果,三个人僵持不下,始终达不成一致。 有人说,这还不好解决,投票呗。于是三个人投了一轮票,结果每个人仍然坚持自己的提议,问题还是没有解决。有人又想了个主意,干脆我们选出一个leader,这个leader说什么,我们就听他的,这样大家就不用争了。于是,大家开始投票选leader。结果很悲剧,每个人都觉得自己应该做这个leader。三个人终于发现,「选leader」这件事仍然和原来的「去哪里吃饭」这个问题在本质上是一样的,同样难以解决。 这时恐怕有些读者们心里在想,这三个人是有毛病吧......就吃个饭这么点小事,用得着争成这样吗?实际上,在分布式系统中的每个节点之间,如果没有某种严格定义的规则和协议,它们之间的交互就真的有可能像上面说的情形一样。整个系统达不成一致,就根本没法工作。 所以,就有聪明人设计出了一致性协议(consensus protocol),像我们常见的比如Paxos、Raft、Zab之类。与前面几个人商量问题类似,如果翻译成Paxos的术语,相当于每个节点可以提出自己的提议(称为proposal,里面包含提议的具体值),协议的最终目标就是各个节点根据一定的规则达成相同的proposal。但以谁的提议为准呢?我们容易想到的一个规则可能是这样:哪个节点先提出提议,就以谁的为准,后提出的提议无效。但是,在一个分布式系统中的情况可比几个人聚在一起讨论问题复杂多了,这里边还有网络延迟的问题,导致你很难对发生的所有事件进行全局地排序。举个简单的例子,假设节点A和B分别几乎同时地向节点X和Y发出了自己的proposal,但由于消息在网络中的延迟情况不同,最后结果是:X先收到了A的proposal,后收到了B的proposal;但是Y正好相反,它先收到了B的proposal,后收到了A的proposal。这样在X和Y分别看来,谁先谁后就难以达成一致了。 此外,如果考虑到节点宕机和消息丢失的可能性,情况还会更复杂。节点宕机可以看成是消息丢失的特例,相当于发给这个节点的消息全部丢失了。这在CAP的理论框架下,相当于发生了网络分割(network partitioning),也就是对应CAP中的P。为什么节点宕机和消息丢失都能归结到网络分割的情况上去呢?是因为这几种情况实际上无法区分。比如,有若干个节点联系不上了,也就是说,对于其它节点来说,它们发送给这些节点的消息收不到没有回应。真正的原因,可能是网络中间不通了,也可能是那些目的节点宕机了,也可能是消息无限期地被延迟了。总之,就是系统中有些节点联系不上了,它们不能再参与决策,但也不代表它们过一段时间不能重新联系上。 为了表达上更直观,下面我们还是假设某些节点宕机了。那在这个时候,剩下的节点在缺少了某些节点参与决策的情况下,还能不能对于提议达成一致呢?即使是达成了一致,那么在那些宕机的节点重新恢复过来之后(注意这时候它们对于其它节点之间已经达成一致的提议可能一无所知),它们会不会对于已经达成的一致提议重新提出异议,从而造成混乱?所有这些问题,都是分布式一致性协议需要解决的。 我们这里没有足够的篇幅来详细讨论这些协议具体的实现了,感兴趣的读者可以去查阅相关的论文。实际上,理解问题本身比理解问题的答案要重要的多。总之,我们需要知道的是,我们已经有了一些现成的分布式一致性算法,它们能解决上面讨论的这些问题,保证在一个去中心化的网络中,各个节点之间最终能够对于提议达成一致。而且,一般来说,只要网络中的大部分节点(或一个quorum)仍然存活(即它们相互间可以收发消息),这个一致性的提议就可以达成。 一致性问题 我们前面讨论的一致性协议,有一个重要的前提条件,就是:各个节点都是可以信任的,它们都严格遵守同样的一套规则。这个条件,在一个公司的内部网络中可以认为是基本能满足的。但如果这个条件不满足会怎么样呢?假设网络中有些节点是恶意的,它们不但不遵守协议,还故意捣乱(比如胡乱发送消息),那么其它正常的节点还能够顺利工作吗? 在分布式系统理论中,这个问题被抽象成了一个著名的问题——拜占庭将军问题(Byzantine Generals Problem)。这个问题由大名鼎鼎的Leslie Lamport提出,也就是Paxos的作者。同时,Lamport还是2013年的图灵奖得主。 这要从一个故事开始说起(当然这个故事是Lamport编出来的)。拜占庭帝国的几支军队攻打到了敌人的城市外面,然后分开驻扎。每一支军队由一位拜占庭将军(Byzantine general)率领。为了制定出一个统一的作战计划,每一位将军需要通过信差(messenger)与其它将军互通消息。但是,在拜占庭将军之间可能出现了叛徒(traitor)。这些叛徒将军的目的是阻挠其他忠诚的将军(loyal generals)达成一致的作战计划。为了这一目的,他们可能做任何事,比如串通起来,故意传出虚假消息,或者不传出任何消息。 我们来看一个简单的例子。假设有5位将军,他们投票来决定是进攻还是撤退。其中两位认为应该进攻,还有两位认为应该撤退,这时候进攻和撤退的票数是2:2打平了。第五位将军恰好是个叛徒,他告诉前两位应该进攻,但告诉后两位应该撤退,结果前两位将军最终决定进攻,而后两位将军却决定撤退。没有达成一致的作战计划。 这个问题显然比我们在前一章讨论的可信任环境下的一致性问题要更难。要解决这个问题,我们是希望能找到一个算法,保证在存在叛徒阻挠的情况下,我们仍然能够达成如下目标: A. 所有忠诚的将军都得到了相同(一致)的作战计划。比如都决定进攻,或都决定撤退,而不是有些将军认为应该进攻,其他将军却决定撤退。 B. 忠诚的将军不仅得到了相同的作战计划,还应该保证得到的作战计划是合理的(reasonable)。比如,本来进攻是更有利的作战计划,但由于叛徒的阻挠,最终却制定出了一起撤退的计划。这样我们的算法也算失败了。 可以看出,上面的目标A,是比较明确的,至少给定一个算法很容易判定它有没有达到这个目标。但目标B却让人无从下手。一个作战计划是不是「合理」的,本来就不好定义。即使没有叛徒的存在,忠诚的将军们也未必就一定能制定出合理的计划。这涉及到科学研究中一个非常重要的问题,如果一个事情不能用一种形式化的方式清晰的定义出来,对于它的研究也就无从谈起,这个事情本身也无法上升到科学的层面。Lamport在对拜占庭将军问题的研究中,一个突出的贡献就是,把这个看似不太好界定的问题,巧妙地归约到了一个能用数学语言精确描述的问题上去。下面我们就看一下这个过程是怎么做的。 首先我们考虑一下将军们制定作战计划的过程(先假设没有叛徒)。每一位将军根据自己对战局的观察,给出他建议的作战计划——是进攻还是撤退。然后,每位将军把自己的作战建议通过信差传达给其他每一位将军。现在每一位将军都知道了其他将军的作战建议,再加上他自己的作战建议,他需要根据所有这些信息得到最终的一个作战计划。为了表达上更清晰,我们给每位将军进行编号,分别是1, 2, ..., n,每位将军提出的作战建议记为v(1), v(2), ..., v(n),一共是n个值,这其中有些代表「进攻」,有些代表「撤退」。经过信差传递消息之后,每位将军都看到了相同的作战提议v(1), v(2), ..., v(n),当然这其中的一个是当前这位将军自己提出来的。然后只要每位将军采用同样的方法,对所有的v(1), v(2), ..., v(n)这些信息进行汇总,就能得到同样的最终作战计划。比如,容易想到的一个方法是投票法,即对v(1), v(2), ..., v(n)中不同的作战计划进行投票,最后选择得票最多的作战计划。 当然,这样得到的最终作战计划也不能保证就是最好的,但这应该是我们能做到的最好的了。我们现在仍然假设将军里没有叛徒。我们发现,前面提到的目标A和目标B的要求可以适当「降低」一些:我们不再关注将军们是否能达成最终一致的作战计划,并且这个计划是不是「合理」;我们只关注每个将军是否收到了完全相同的作战建议v(1), v(2), ..., v(n)。只要每位将军收到的这些作战建议是完全相同的,他们再用同样的方法进行汇总,就很容易得到最终一致的作战计划。至于这个最终的作战计划是不是最好的,那就跟很多「人为」的因素有关了,我们不去管它。 现在我们考虑将军中出现了叛徒。遵循前面的思路,我们仍然希望每位将军能够收到完全相同的作战建议v(1), v(2), ..., v(n)。现在我们仔细审视一下其中的一个值,v(i),在前面的描述中,它表示来自第i个将军的作战提议。如果第i个将军是忠诚的,那么这个定义没有什么问题。但是,如果第i个将军是叛徒,那么就有问题了。为什么呢?因为叛徒可以为所欲为,他为了扰乱整个作战计划的制定,完全可能向不同的将军给出不同的作战提议。这样的话,不同的忠诚将军收到的来自第i个将军的v(i)可能是不同的值。这样v(i)这个定义就不对了,它需要改一改。 不管怎么样,即使存在叛徒,我们还是希望每位将军最终是基于完全相同的作战提议来做汇总,这些作战提议仍然记为v(1), v(2), ..., v(n)。不过,这里的v(i)不再表示来自第i个将军的作战提议,而是表示经过我们设计的某个一致性算法处理之后,每位将军最终看到的第i个提议。这里需要分两种情况讨论。首先第一种情况,如果第i个将军是忠诚的,那么我们自然希望这个v(i)就是第i个将军发送出来的作战提议。换句话说,我们希望经过一致性算法处理之后,第i个将军如果是忠诚的,那么它的提议能够被如实地传达给其他将军,而不会被叛徒的行为所干扰。这是可能制定出「合理」作战计划的前提。第二种情况,如果第i个将军是叛徒,那么他有可能向不同的将军发送不同的提议。这时候我们不能够只听他的一面之词,而是希望经过一致性算法处理之后,各个将军之间充分交换意见,然后根据其他各个将军转述的信息,综合判断得到一个v(i)。这个v(i)是进攻还是撤退,并不太重要,关键是要保证每位将军得到的v(i)是相同的。只有这样,各位将军经过汇总所有的v(1), v(2), ..., v(n)之后才能得到最终的完全一致的作战计划。 根据上面的分析,我们发现,在这两种情况中,我们都只需要关注单个将军(也就是第i个将军)所发出的提议如何传达给其他将军。重点终于来了!至此,我们就能够把原来的问题归约到一个子问题上。这个子问题,才是Leslie Lamport在他的论文中被真正命名为「拜占庭将军问题(Byzantine Generals Problem)」的那个问题。在这个问题中,我们只关注发送命令的单个将军,称他为主将(commanding general),而其他接受命令的将军称为副官(lieutenant)。下面是「拜占庭将军问题」的精确描述。 一个主将发送命令给n-1个副官,如何才能确保下面两个条件: (IC1) 所有忠诚的副官最终都接受相同的命令。 (IC2) 如果主将是忠诚的,那么所有忠诚的副官都接受主将发出的命令。 这其实正好对应了我们前面已经讨论过的两种情况。如果主将是忠诚的,那么条件IC2保证了命令如实地传递,这时候条件IC1自然也满足了;如果主将是叛徒,那么条件IC2没有意义了,而条件IC1保证了,即使叛徒主将对每个副官发出不同的命令,每个副官仍然能最终获得一致的命令。 第一,有些人会问了,难道主将还能是叛徒?主将都是叛徒了,还有啥搞头啊?其实是这样的,这个「拜占庭将军问题」只是原问题的一个子问题。当n个将军通过传递消息来决策作战计划的时候,可以分解成n个「拜占庭将军问题」,即分别以每位将军作为主将,以其余n-1位将军作为副官。如果有一个算法能够解决「拜占庭将军问题」,那么同时运行n个算法实例,就能使得每位将军都获得完全相同的作战建议序列,即前面我们提到的v(1), v(2), ..., v(n)。最后,每位将军将v(1), v(2), ..., v(n)使用相同的方法进行汇总(比如按多数投票),就能得到最终的作战计划。 第二,当主将是叛徒的时候,他可以向不同的副官发送不同的命令,怎么可能每个副官仍然能最终获得一致的命令呢?这正是算法需要解决的。其实这也容易解释(我们前面也提到过这个思路),由于主将可能向不同的副官发送不同的命令,所以副官不能直接采用主将发来的命令,而是也要看看其他副官转述来的主将的命令是什么。然后,一个副官综合了由所有副官转述的命令(再加上主将直接发来的命令)之后,就可能得到比较全面的信息,从而做出一致的判断(在实际中是个不断迭代的过程)。 好了,我们用了这么多篇幅,终于把「拜占庭将军问题」本身描述清楚了。这实际上也是最难的部分。我们上一章提到过,理解问题本身比理解问题的答案更重要。只要问题本身分析清楚了,如何设计一个能解决它的算法就只是细节问题了。我们这里不深入算法的细节了,感兴趣的读者可以去查阅下列论文: 《The Byzantine Generals Problem》,下载地址:lamport.azurewebsites.net/pubs/byz.pd… 《Reaching Agreement in the Presence of Faults》,下载地址:lamport.azurewebsites.net/pubs/reachi… 我们这里只提一下论文给出的算法的结论。 使用不同的消息模型,「拜占庭将军问题」有不同的解法。 如果将军之间使用口头消息(oral messages),也就是说,消息被转述的时候是可能被篡改的,那么要对付m个叛徒,需要至少有3m+1个将军(其中至少2m+1个将军是忠诚的)。 如果将军之间使用签名消息(signed messages),也就是说,消息被发出来之后是无法伪造的,只要被篡改就会被发现,那么对付m个叛徒,只需要至少m+2个将军,也就是说至少2个忠诚的将军(如果只有1个忠诚的将军,显然这个问题没有意义)。这种情况实际相当于对忠诚将军的数目没有限制。 容错性 我们前面提到过,以Paxos为代表的分布式一致性协议,是在可信任的环境下运行的。而在「拜占庭将军问题」中,网络中则存在恶意节点。因此我们很容易产生一个想法:Paxos是不是「拜占庭将军问题」在叛徒数为零时的一个特例解? 这样看其实有点问题。在「拜占庭将军问题」中,除了叛徒,剩下的是忠诚的将军。「忠诚」这个词,其实暗含了一个意思:他是能够正常工作的(即你可以随时通过消息跟他进行交互)。为什么这么说呢?我们知道,一个叛徒可以做任何事,包括发送错误消息,也包括不发送任何消息。「不发送任何消息」,相当于不能正常工作,或者说,发生了某种故障。所以,单纯的故障,不仅仅是故意的恶意行为,也应该能归入叛徒的行为。这在其他将军看来没有区别。 按照这种理解,「忠诚」这个词并不是很恰当。叛徒数为零,相当于网络中每个节点都在正常工作。但是Paxos的设计也是能够容错的,就像我们在前面讨论的一样,网络中的少数节点发生故障(比如宕机),Paxos仍然能正常工作。可见,Paxos并不能看成是「拜占庭将军问题」在叛徒数为零时的一个特例解。 那「拜占庭将军问题」和Paxos这类分布式一致性算法的关系应该如何看待呢?我们可以从容错性的强弱程度上来分析。 一般来说,设计一个计算机系统,小到一块芯片,大到一个分布式网络,都需要考虑一定的容错性(fault tolerance)。但根据错误不同的性质,可以分为两大类: 拜占庭错误(Byzantine fault)。这种错误,在不同的观察者看来,会有前后不一致的表现。 非拜占庭错误(non-Byzantine fault)。从字面意思看,是指那些不属于前一类错误的其它错误。 这两类错误的含义并没有字面上那么好理解。 先说说拜占庭错误。在「拜占庭将军问题」中,叛徒的恶意行为固然是属于这一类错误的。在不同的将军看来,叛徒可能发送完全不一致的作战提议。而在计算机系统中,出现故障的节点或部件也可能表现出前后不一致的行为,虽然这并非恶意,但也属于这一类错误。比如信道不稳定,导致节点发送给其它节点的消息发生了随机错误,或者说,消息损坏了(corrupted)。再比如,在数据库系统中,commit之后的数据明明已经同步给磁盘了(通过操作系统的fsync),但由于突然断电等原因,最终数据还是没有真正落盘成功,甚至出现数据错乱。 再看一下非拜占庭错误。Lamport在他关于Paxos的一篇论文中也使用了non-Byzantine这个词(见《Paxos Made Simple》)。但是这个词的命名的确让人有点不好理解。在分布式系统中,如果节点宕机了,或者网络不通了,都会导致某些节点不能工作。其它节点其实没法区分这两种情况,在它看来,只是发现某个节点暂时联系不上了(即接收消息超时了)。至于是因为那个节点本身出问题了,还是网络不通了,或者是消息出现了严重的延迟,是无法区分的。而且,过一会之后,节点可能会重新恢复(或是自己恢复了,或经过了人工干预)。换句话说,对于出现这种错误的节点,我们只是收不到它的消息了,而不会收到来自它的错误消息。相反,只要收到了来自它的消息,那么消息本身是「忠实」的。 可见,拜占庭错误是更强的一类错误。在「拜占庭将军问题」中,叛徒发送前后不一致的作战建议,属于拜占庭错误;而不发送任何消息,属于非拜占庭错误。所以,解决「拜占庭将军问题」的算法,既能处理拜占庭错误,又能处理非拜占庭错误。这听起来稍微有些奇怪,不过这只是命名带来的问题。 总之,「拜占庭将军问题」的解法应该是最强的一类分布式一致性算法,它理论上能够处理任何错误。而Paxos只能处理非拜占庭错误。通常把能够处理拜占庭错误的这种容错性称为「Byzantine fault tolerance」,简称为BFT。 这样说来,BFT的算法应该可以解决任何错误下的分布式一致性问题,也包括Paxos所解决的问题。那为什么不统一使用BFT的算法来解决所有的分布式一致性问题呢?为什么还需要再费力气设计Paxos之类的一些算法呢?我们前面没有仔细讨论解决「拜占庭将军问题」的算法,所以这里也不做仔细的分析了。但容易想象的是,提供BFT这么强的错误容忍性,肯定需要付出很高的代价。比如需要消息的大量传递。而Paxos不需要提供那么强的容错性,因此可以比较高效地运行。另外,具体到Lamport在论文中给出的解决「拜占庭将军问题」的算法,它还对系统的记时假设(timing assumption)有更强的要求。这也容易理解,既然算法的容错性要求这么高,自然对于运行环境的假设(assumption)也有可能要高一点。由于这个问题是分布式系统中一个挺关键的问题,所以我们在这里单独拿出来讨论一下。在Lamport在论文中,算法对于系统的假设有这么一条: The absence of a message can be detected. 这条假设要求,如果某位叛徒将军没有发送任何消息(当然也可能是消息丢失了),那么这件事是可以检测出来的。显然,这只能依赖某种超时机制(time-out),依赖节点之间的时钟达到一定程度的同步,即时钟的偏移不能超过一个最大值。这实际上是一种同步模型(synchronous model)。而Paxos的系统假设在这一点上就没有这么强,它是基于异步模型(asynchronous model),对系统时钟没有特定的要求。我在之前的另一篇文章《基于Redis的分布式锁到底安全吗?》一文中也有提到过这个问题。这有时候会成为分布式算法带来一些争议的根源。 具体来说,根据Paxos的论文所说,Paxos的设计是基于异步(asynchronous)、非拜占庭(non-Byzantine)的系统模型,即: 节点可以以任意速度运行,可能宕机、重启。但是,算法执行过程中需要记录的一些变量,在重启后应该能够恢复。 消息可以延迟任意长时间,可以重复(duplicated),可以丢失(lost),但不能损坏(corrupted)。 上面第一条其实是要求数据在数据库中持久化,并且要保证在落盘过程中没有发生拜占庭错误(我们前面刚提到过)。但实际中由于突然断电、磁盘缓存等现实问题,拜占庭错误是有可能发生的(虽然概率很低),所以这就要求工程上做一些特殊处理。 上面第二条,消息损坏,属于拜占庭错误。所以Paxos要求不能有消息损坏发生。这在使用TCP协议进行消息传输的情况下,可以认为是能够满足要求的。 综上分析,解决「拜占庭将军问题」的算法,提供了最强的容错性,即BFT,而Paxos只能容忍非拜占庭错误。但是,在只有非拜占庭错误出现的前提下,Paxos基于异步模型,是比同步模型更弱的系统假设,因此算法更鲁棒。当然,Paxos也更高效。 区块链 在现实中,真正需要达到BFT容错性的系统很少,除非是一些容错性要求非常高的系统,比如波音飞机上的控制系统,或者SpaceX Dragon太空船这类系统(参见www.weusecoins.com/bitcoin-byz…)。 我们平常能接触到的BFT的一个典型的例子,就是区块链了。一个区块链网络是一个完全开放的网络,其中的矿工节点(miner)是可以自由加入和自由退出的。这些节点当然有可能是恶意的,所以区块链网络在设计的时候必须要考虑这个问题。这实际上就是典型的「拜占庭将军问题」。 接下来为了将区块链与「拜占庭将军问题」之间的联系讨论得更加清楚,我们先来非常粗略地介绍一下区块链技术。 以比特币网络为例,它的核心操作就是进行比特币交易,即某个比特币的拥有者将自己一定数量的比特币转移给其他人。首先,比特币的拥有者要发起交易(transaction),他需要先用自己的私钥对交易进行签名,然后将交易请求发给矿工节点。矿工将收到的所有交易打包到一个区块(block)当中,并通过一系列复杂度很高的运算找到一个nonce值,保证对于它和区块内其它信息进行hash计算后的结果能够符合预定的要求。这一步对于整个区块链网络至关重要,被称为工作量证明(Proof of Work)。然后矿工把该区块在全网发布,由其它矿工来验证这个区块。这个验证既包括对交易签名进行验证(使用比特币拥有者的公钥),也包括对工作量证明的有效性进行验证。如果验证通过,就把这个区块挂在当前最长的区块链上。 如果两个矿工几乎同时完成了区块的打包和工作量证明,它们可能都会将区块进行发布,这时区块链就会分叉(fork)。但矿工会不停地产生新区块,并将新区块挂在当前最长的区块链上,所以最终哪个分叉变得更长,哪个分叉就会被多数矿工节点承认。这么看来,区块链其实不是一个链,而是一棵树。我们知道,在树这种数据结构中,从根节点到叶子节点只有唯一的一条路径。因此,当前有效的区块链其实是这棵树中从根节点到叶子节点最长的那条路径。只要一个区块在最长的链上,那么它就是有效的,它里面包含的所有交易就被固化下来了(被多数节点承认)。 我们只是非常粗略地介绍了一下区块链的工作原理。如果想了解细节,建议研究一下以太坊的官方wiki,地址是: https://github.com/ethereum 下面我们开始讨论区块链技术与「拜占庭将军问题」的关联。 前面我们讨论「拜占庭将军问题」的时候,得到过以下结论: 如果使用口头消息,那么至少需要多于2/3的将军是忠诚的。 如果使用签名消息,那么对忠诚将军的数量是没有要求的。 根据前面的介绍,我们在区块链中使用的消息应该属于签名消息。具体体现在:每一个区块中的交易都进行了签名,保证无法被篡改,也能保证这个消息只能是由最初的发起者发出的。那么,这属于上述第二种情况,难道说区块链网络中忠诚节点的数目没有要求?显然不是这样。比如在比特币网络中,要求恶意节点不能掌握多于50%的算力。为什么两者之间似乎不一致呢?这是因为,「拜占庭将军问题」只是关注一个子问题,它关注的是其中一个将军(称为主将)向其他所有将军(称为副官)发送命令的情况。而最终对所有命令进行汇总则要求所有忠诚的将军达成共识。如果忠诚的将军数目太少,不管最终确定的作战计划是什么,还是会失败,因为叛徒可能不执行这个作战计划。这类似于比特币网络中的情况,其中对于最长链的选择过程,就相当于将军们对所有命令进行汇总的操作(按多数投票)。 在「拜占庭将军问题」中,一个叛徒可能向不同的将军发送不一致的命令。如果算法设计得不好,就可能造成最终无法达成一致。在区块链网络中,类似的行为将会成本很高。这是由于矿工节点发布区块的消息必须经过工作量证明,它如果发布不一致的区块,每个区块都需要工作量证明,这将耗费它大量的算力。另外,这样做也没有动机,它只会产生更多分叉,不会产生最长链。 在「拜占庭将军问题」的框架下,如何看待工作量证明呢?它其实相当于提高了做叛徒的成本,从而极大降低了叛徒超过半数的可能性。这里可以做一个对比,假设历史上存在真实的拜占庭将军问题,那么可以想象,敌军的间谍打入拜占庭将军这个群体中的成本应该是很高的。所以,可以认为将军中的叛徒不至于太多。但对应到计算机网络中,如果没有类似工作量证明的机制,那么成为叛徒矿工的成本就是非常低的。这就很有可能使得叛徒比忠诚的矿工还要更多。 当然,从经济学的角度看,在需要工作量证明的前提下,成为叛徒矿工也是不明智的。因为它既然拥有比较强的算力,还不如按照合理的方式通过挖矿赚取收益更为稳妥。不过这是技术之外的因素了。 除了工作量证明这种机制(Proof of Work)之外,还有一种被称为Proof of Stake的机制。虽然有人质疑这种机制存在缺点(比如nothing at stake),但站在「拜占庭将军问题」的角度,它也是相当于提高了做叛徒的成本。这就好比一个间谍要混入董事会,成本肯定是比较高的,因为他需要首先持有大量股票。 区块链到底是什么?有人说是个无法篡改的超级账本,也有人说是个去中心化的交易系统,还有人说它是构建数字货币的底层工具。但是,从技术的角度来说,它首先是个解决了拜占庭将军问题的分布式网络,在完全开放的环境中,实现了数据的一致性和安全性。而其它的属性,都附着于这一技术本质之上。
首先,本逆向分析是系列文章,会分别从常见的逆向技巧来介绍iOS开发中常见的逆向技术。 网络分析 在逆向过程中很多时候需要分析APP和Web端数据交互的内容那么最简单的方式即是抓包网络分析,而使用Charles、Tcpdump也是逆袭分析最基本的手段。本文以Charles为例来介绍网络相关的内容。 Charles 是在 Mac 下常用的网络封包截取工具,在做 移动开发时,我们为了调试与服务器端的网络通讯协议,常常需要截取网络封包来分析。Charles 通过将自己设置成系统的网络访问代理服务器,使得所有的网络访问请求都通过它来完成,从而实现了网络封包的截取和分析。除了在做移动开发中调试端口外,Charles 也可以用于分析第三方应用的通讯协议。配合 Charles 的 SSL 功能,Charles 还可以分析 Https 协议。 Charles 主要的功能包括: 截取 Http 和 Https 网络封包; 支持重发网络请求,方便后端调试; 支持修改网络请求参数; 支持网络请求的截获并动态修改; 支持模拟慢速网络。 Charles安装好后只需自己设置成代理服务器来完成封包的截取,设置也很简单选择菜单中的 “Proxy” –> “Mac OS X Proxy” 来将 Charles 设置成系统代理。 这样你就可以看到网络请求出现在 Charles 的界面中,包括你模拟器你的请求也会在这里,那么小伙伴会有疑问iPhone真机设备的网络数据包如何截取呢,也很简单只需将手机的代理服务器设置为电脑IP即可,如下操作 第一步:Charles 的菜单栏上选择 “Proxy”–>“Proxy Settings”,填入代理端口 8888,并且勾上 “Enable transparent HTTP proxying” 就完成了在 Charles 上的设置。 第二步:获取Charles 运行所在电脑的 IP 地址,Charles 的顶部菜单的 “Help”–>“Local IP Address”,即可在弹出的对话框中看到 IP 地址,如下图所示: 第三步:设置iPhone设备Http代理,在 iPhone 的 “ 设置 ”–>“ 无线局域网 ” 中,点击当前连接的 wifi 名,可以看到当前连接上的 wifi 的详细信息,在其最底部有「HTTP 代理」一项,点击后,然后填上 Charles 运行所在的电脑的 IP,以及端口号 8888,如下图所示: 至此所有电脑,模拟器,iOS真机设备所有的Http请求都已经可以通过上边的方法抓包分析获取Request和Respone等具体网络请求数据,可是Https的加密请求如何抓取呢?如果你需要截取分析 Https 协议相关的内容。那么需要安装 Charles 的 CA 证书。具体步骤如下。 第一步:我们需要在你要分析的设备上安装Charles证书。点击 Charles 的顶部菜单,选择 “Help” –> “SSL Proxying” ,–> “Install Charles Root Certificate”安装到Mac上抓取Mac产生的Https请求,Install Charles Root Certificate on a Mobile Device or Remote Browser”安装到iOS 真机设备抓取iOS设备产生的Https请求,如下图所示: 第二步:Charles 的菜单栏上选择 “Proxy”–>“SSL Proxy Settings”添加需要抓取的域名,如下图所示: 这样就可以分析目标App的所有网络请求,对其进行逆向数据分析,学习下优秀的API设计规范,根据抓包分析的数据格式,通过脚本语言或其他方式伪造网络请求修改数据。比如想自己写个新闻类App就可以抓黄易,某条分析其网络数据,笔者之前分析时记得黄易的接口设计要比某条的清晰明了,然后将就可以在自己的App中使用了。 如果是返回https的接口,可以参考下面的文章:Charles https使用 静态分析 在逆向过程中很多时候仅仅对数据交互的分析并不能看出业务大概实现逻辑,技术方案,这个时候我们就需要静态分析这个App,今天就浅显的讲下如何静态分析目标APP的方法论。 首先分析目标APP我们需要获取Ipa,那么怎么获取呢,上次我逆向冲顶大会后,有小伙伴问,怎么获取Ipa,其实很简单,虽然Itunes 在新版中去掉了AppStore,但我们可以通过其他渠道下载,如PP助手同步推、91等越狱市场下载。以最近很火小佛系游戏旅かえる(旅行青蛙)为例我们直接搜索如下图: 获取Ipa文件后,把旅かえる旅行青蛙-1.0.1.ipa 后缀名改为zip,然后解压可以看到iTunesArtwork,iTunesMetadata,META-INF,Payload四个文件,其中iTunesMetadata里边有BundleId,bundleDisplayName,VersionString等等应用相关的信息。 Payload里只有一个文件tabikaeru,这个文件也是我们重点要分析的文件,我们直接右键显示,可以看到如下内容,在这里可以看到一些三方库,界面nib文件,图片资源,数据等,其中_Codesignature里边包含了这个包的签名信息,如果我们修改了ipa内部的任一文件重新压缩改为ipa然后安装就会报签名错误,这就是下一节动态分析要用到的技术重签,最重要的可执行文件tabikaeru这个就是所有编译后的二进制代码块。 这里简单讲下二进制可执行文件的结构,主要分三部分Object files,Sections,Symbols。其中Object files包括 .o , .framework,.a文件;Sections 对二进制文件进行了一级划分,描述可执行文件全部内容,提供segment,section位置和大小;Symbols 对Section中的各个段进行了二级划分。以下图为例,对于__TEXT __text,表示代码段中的代码内容,其对应地址为0x1000021B0,然后我们拿着这个地址去符号表中查询会发现,这一地址对应的代码0x1000021B0 -[ULWBigResponseButton pointInside:withEvent:],理解了这个过程我们就可以更好的理解反编译的过程,也能理解友盟Crash分析是如何把那些你看不懂的错误信息还原成你看得懂的函数调用栈的过程。 其实介绍到这里都还不是静态分析的重点内容,我们是要反编译,反汇编对不对,那么我们把二进制文件直接丢入Hopper disassembler(反编译工具)看下呢,试过的小伙伴肯定要说了,看到一堆没意义的字符,是的,因为开发者将自己的Ipa打包上传后,Apple对Ipa加了一层壳,也就是加固,怎么办呢?有很多方式如Dumpdecrypted,Clutch等工具可以砸壳,特别说明砸壳需要越狱手机,因为要使用SSH连接到手机,这里不对该过程展开说明,需要的同学自行学习,《iOS应用逆向工程》小黄书里边有讲,或者一些博客也有介绍。我在这里介绍另一种获取脱壳Ipa的方式,直接在PP助手搜越狱栏目下的Ipa,就是脱壳的Ipa。 如上图我们可以看到游戏是基于Unity3D做的,可以看到定位服务LocationService,SplashScreen业务类,三方库GADSDK三方库等内容,如果要进一步看具体方法实现,就需要读图里的汇编代码了,关于ARM汇编一级的逆向知识就更低层包括ARM指令集,各种寄存器操作等不做展开讨论,另外如果是应用类APP分析到这里,整个APP的头文件就都看到,由于该游戏采用Unity3D引擎C#开发如果反编译C#代码还需要其他工具才能进一步看,这里笔者只是举例,有兴趣的同学可以进一步分析。 最后静态分析在逆向中是非常重要的手段,很多时候我们需要静态分析提供线索,寻找蛛丝马迹,动态分析去论证,多种技术手段互相交替使用,逐步突破,才能一窥究竟,那么我会在下一节中讲解如何动态分析目标APP,如分析UI结构,分析关键技术,或者注入自己的代码改变业务流程,更多骚操作,尽在iOSTips。 附静态分析工具集: Dumpdecrypted:砸壳 class-dump-z: 用于简单分析出工程中的头文件和函数名 IDA:强大的反编译工具 Hopper Disassembler:类似IDA 动态分析 下面以腾讯视频广告移除为例,来讲解如何做动态分析。首先我们进入视频播放页,点击最近的热片《战长沙》,进入详情页如下图,VIP可关闭广告,那么这个详情页肯定会有与与VIP广告相关的业务,我们只要找到对应的ViewController然后丢给Hooper反编译就应该可以看到VIP相关的业务函数。那么怎么知道这一页对应的ViewController呢,有2种办法,一种全局hook viewDidLoad 在这里断点看vc,还有直接看视图堆栈。 首先我们通过hook viewDidLoad方式书写如下代码,当点击详情时开启断点,可以看到程序被中断,控制台self=QLVideoDetailViewController,我们通过这种方式很容易确定详情播放的控制器为这个类。直接查看UI堆栈发现也是这个类,并且获得了更多信息,可以看到广告相关的View,这里想下下是不可以直接隐藏这个ad view呢? 通过Hooper发现,和广告相关的代码真多啊,不知道企鹅的程序猿是怎么设计的。 通过上面静态分析的内容并不能提供明显的思路让我去广告,我准备回到UI堆栈那里,我在这里发现了QNBPlayerVideoAdsViewController,看名字就知道播广告的,邪恶的笑下,鹅肠同学名字起得真是清晰明了,这次试一下直接hide这个控制器看广告会不会消失。 我写了如下代码以后,很暴力直接隐藏了广告视图,发现广告会消失一会儿,然后又显示,经过静态分析发现QNBPlayerVideoAdsViewController这个类里边有adsStartPlay,那我更暴力一点,在这个方法里只要你播广告我就影藏这个view,command r运行,看电视剧,妥妥的没广告了,最终代码如下。 故事讲到这里算是讲完了,有兴趣的同学可以进一步研究vip付费视频如何直接看,整体而言,这次分析的过程比较顺利,我们可以看到在逆向过程中是动态,静态分析结合相互提供线索一步步逼近真相,我们现在复盘,通过复盘来来解密我们使用的工具,以及这些工具背后的原理。 我们这次动态分析使用的是IPAPatch,和这个类似的有AloneMonkey的MonkeyDev,这俩库原理类似核心思想都是将我们自己写的代码编成动态库注入目标App,然后重签,作为一个有情怀的开发者,仅仅会使用工具肯定不是我们的目标对不对,我们要做的是探究这个过程是如何实现的,这里我抛出几个问题动态库是如何注入的?App重签名的过程是怎样的?今天我们大概简单聊聊有个印象,这些内容每一个点都很重要,都是逆向的基础知识,虽然枯燥但是,但可以让你更好的理解这些原理。 如果要理解动态库注入那还需要更深入的理解Mach-o的文件结构,我在上篇文章中简单讲了下,这里再详细的讲下,一个典型的Mach-O文件格式如图所示: 通过上图,可以看出Mach-O主要由以下三部分组成: Mach-O头部(mach header)。描述了Mach-O的cpu架构、文件类型以及加载命令等信息。 加载命令(load command)。描述了文件中数据的具体组织结构,不同的数据类型使用不同的加载命令表示。 Data。Data中的每个段(segment)的数据都保存在这里,段的概念与ELF文件中段的概念类似。每个段都有一个或多个Section,它们存放了具体的数据与代码。 二进制当中所有引用到的动态库都放在Load commands段当中,那么我们只要,通过给这个段增加记录,就可以注入我们自己写的动态库了,对不对。那么问题来了,在这里插入我们自己的动态库有什么用?我们自己写的代码没有执行的入口,一样什么都不能干,我们还需要一个”main”函数来执行我们自己的代码,不要忘了,这个”main”函数在oc里面称为构造函数,只要在函数前声明 “attribute((constructor)) static” 即可,这样你的代码就可以被目标APP执行了。 关于重签名,有兴趣的同学可以学习下整个签名过程,包括代码如何签名,证书如何校验等,重签名大概过程如下,1、解压ipa安装包 cp test.ipa olinone.zip2、替换证书配置文件(文件名必须为embedded)cp embedded.mobileprovision Payload/test.app3、重签名(certifierName为重签名证书文件名,可以加证书ID后缀)certifierName="iPhone Distribution: olinone Information Technology Limited(6a5LOVE58MYX)" codesign -f -s $certifierName --entitlements entitlements.plist Payload/test.app4、打包 zip -r test.ipa Payload 至此逆向系列的入门教程已经全部更完,现在应该拿到一个App应该已经可以自己玩了,篇幅有限,有很多内容不能展开讲,也没讲到,比如有攻就有防,了解防才可以更好的绕过,如何为自己的App加固,再比如一些细节符号表是怎样恢复的,砸壳的原理到底是怎样的,这些有趣的话题我们只能后边再聊,更多骚操作,尽在iOSTips,关注公众号,第一时间get新姿势。 附静态分析工具集: Dumpdecrypted:砸壳 class-dump-z: 用于简单分析出工程中的头文件和函数名 IDA:强大的反编译工具 Hopper Disassembler:类似IDA 附动态分析工具集: IPAPatch,MonkeyDev: Reveal 界面分析 CaptainHook,Tweak 编写hook代码,或自定义功能 附:《iOS Hacker's Handbook》《iOS应用逆向工程:分析与实战》小黄书 iOSRE 中文逆向论坛
互操作就是在Kotlin中可以调用其他编程语言的接口,只要它们开放了接口,Kotlin就可以调用其成员属性和成员方法,这是其他编程语言所无法比拟的。同时,在进行Java编程时也可以调用Kotlin中的API接口。 Kotlin调用Java Kotlin在设计时就考虑了与Java的互操作性。可以从Kotlin中自然地调用现有的Java代码,在Java代码中也可以很顺利地调用Kotlin代码。例如,在Kotlin中调用Java的Util的list库。 import java.util.* fun demo(source: List<Int>) { val list = ArrayList<Int>() // “for”-循环用于 Java 集合: for (item in source) { list.add(item) } // 操作符约定同样有效: for (i in 0..source.size - 1) { list[i] = source[i] // 调用 get 和 set } } 基本的互操作行为如下: 属性读写 Kotlin可以自动识别Java中的getter/setter函数,而在Java中可以过getter/setter操作Kotlin属性。 import java.util.Calendar fun calendarDemo() { val calendar = Calendar.getInstance() if (calendar.firstDayOfWeek == Calendar.SUNDAY) { // 调用 getFirstDayOfWeek() calendar.firstDayOfWeek = Calendar.MONDAY // 调用ll setFirstDayOfWeek() } if (!calendar.isLenient) { // 调用 isLenient() calendar.isLenient = true // 调用 setLenient() } } 循Java约定的getter和setter方法(名称以get开头的无参数方法和以set开头的单参数方法)在Kotlin中表示为属性。如果Java类只有一个setter,那么它在Kotlin中不会作为属性可见,因为Kotlin目前不支持只写(set-only)属性。 空安全类型 Kotlin的空安全类型的原理是,Kotlin在编译过程中会增加一个函数调用,对参数类型或者返回类型进行控制,开发者可以在开发时通过注解@Nullable和@NotNull方式来限制Java中空值异常。Java中的任何引用都可能是null,这使得Kotlin对来自Java的对象进行严格的空安全检查是不现实的。Java声明的类型在Kotlin中称为平台类型,并会被特别对待。对这种类型的空检查要求会放宽,因此对它们的安全保证与在Java中相同。 val list = ArrayList<String>() // 非空(构造函数结果) list.add("Item") val size = list.size // 非空(原生 int) val item = list[0] // 推断为平台类型(普通 Java 对象) 当调用平台类型变量的方法时,Kotlin不会在编译时报告可空性错误,但是在运行时调用可能会失败,因为空指针异常。 item.substring(1)//允许,如果item==null可能会抛出异常 平台类型是不可标识的,这意味着不能在代码中明确地标识它们。当把一个平台值赋给一个Kotlin变量时,可以依赖类型推断(该变量会具有所推断出的平台类型,如上例中item所具有的类型),或者选择我们所期望的类型(可空的或非空类型均可)。 val nullable:String?=item//允许,没有问题 Val notNull:String=item//允许,运行时可能失败 如果选择非空类型,编译器会在赋值时触发一个断言,这样可以防止Kotlin的非空变量保存空值。当把平台值传递给期待非空值等的Kotlin函数时,也会触发一个断言。总的来说,编译器尽力阻止空值的传播(由于泛型的原因,有时这不可能完全消除)。 平台类型标识法 如上所述,平台类型不能在程序中显式表述,因此在语言中没有相应语法。 然而,编译器和 IDE 有时需要(在错误信息中、参数信息中等)显示他们,Koltin提供助记符来表示他们: T! 表示“T 或者 T?”; (Mutable)Collection! 表示“可以可变或不可变、可空或不可空的 T 的 Java 集合”; Array<(out) T>! 表示“可空或者不可空的 T(或 T 的子类型)的 Java 数组”。 可空注解 由于泛型的原因,Kotlin在编译时可能出现空异常,而使用空注解可以有效的解决这一情况。编译器支持多种可空性注解: JetBrains:org.jetbrains.annotations 包中的 @Nullable 和 @NotNull; Android:com.android.annotations 和 android.support.annotations; JSR-305:javax.annotation; FindBugs:edu.umd.cs.findbugs.annotations; Eclipse:org.eclipse.jdt.annotation; Lombok:lombok.NonNull; JSR-305 支持 在JSR-305中,定义的 @Nonnull 注解来表示 Java 类型的可空性。 如果 @Nonnull(when = ...) 值为 When.ALWAYS,那么该注解类型会被视为非空;When.MAYBE 与 When.NEVER 表示可空类型;而 When.UNKNOWN 强制类型为平台类型。 可针对 JSR-305 注解编译库,但不需要为库的消费者将注解构件(如 jsr305.jar)指定为编译依赖。Kotlin 编译器可以从库中读取 JSR-305 注解,并不需要该注解出现在类路径中。 自 Kotlin 1.1.50 起, 也支持自定义可空限定符(KEEP-79) 类型限定符 如果一个注解类型同时标注有 @TypeQualifierNickname 与 JSR-305 @Nonnull(或者它的其他别称,如 @CheckForNull),那么该注解类型自身将用于 检索精确的可空性,且具有与该可空性注解相同的含义。 @TypeQualifierNickname @Nonnull(when = When.ALWAYS) @Retention(RetentionPolicy.RUNTIME) public @interface MyNonnull { } @TypeQualifierNickname @CheckForNull // 另一个类型限定符别称的别称 @Retention(RetentionPolicy.RUNTIME) public @interface MyNullable { } interface A { @MyNullable String foo(@MyNonnull String x); // 在 Kotlin(严格模式)中:`fun foo(x: String): String?` String bar(List<@MyNonnull String> x); // 在 Kotlin(严格模式)中:`fun bar(x: List<String>!): String!` } 类型限定符默认值 @TypeQualifierDefault 引入应用时在所标注元素的作用域内定义默认可空性的注解。这些注解类型应自身同时标注有 @Nonnull(或其别称)与 @TypeQualifierDefault(...) 注解, 后者带有一到多个 ElementType 值。 ElementType.METHOD 用于方法的返回值; ElementType.PARAMETER 用于值参数; ElementType.FIELD 用于字段; ElementType.TYPE_USE(自 1.1.60 起)适用于任何类型,包括类型参数、类型参数的上界与通配符类型。 当类型并未标注可空性注解时使用默认可空性,并且该默认值是由最内层标注有带有与所用类型相匹配的 ElementType 的类型限定符默认注解的元素确定。 @Nonnull @TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER}) public @interface NonNullApi { } @Nonnull(when = When.MAYBE) @TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER, ElementType.TYPE_USE}) public @interface NullableApi { } @NullableApi interface A { String foo(String x); // fun foo(x: String?): String? @NotNullApi // 覆盖来自接口的默认值 String bar(String x, @Nullable String y); // fun bar(x: String, y: String?): String // 由于 `@NullableApi` 具有 `TYPE_USE` 元素类型, // 因此认为 List<String> 类型参数是可空的: String baz(List<String> x); // fun baz(List<String?>?): String? // “x”参数仍然是平台类型,因为有显式 // UNKNOWN 标记的可空性注解: String qux(@Nonnull(when = When.UNKNOWN) String x); // fun baz(x: String!): String? } 也支持包级的默认可空性: @NonNullApi // 默认将“test”包中所有类型声明为不可空 package test; @UnderMigration 注解 库的维护者可以使用 @UnderMigration 注解(在单独的构件 kotlin-annotations-jvm 中提供)来定义可为空性类型限定符的迁移状态。@UnderMigration(status = ...) 中的状态值指定了编译器如何处理 Kotlin 中注解类型的不当用法(例如,使用 @MyNullable 标注的类型值作为非空值): MigrationStatus.STRICT 使注解像任何纯可空性注解一样工作,即对不当用法报错并影响注解声明内的类型在 Kotlin中的呈现; 对于 MigrationStatus.WARN,不当用法报为警告而不是错误; 但注解声明内的类型仍是平台类型; MigrationStatus.IGNORE 则使编译器完全忽略可空性注解。 库的维护者还可以将 @UnderMigration 状态添加到类型限定符别称与类型限定符默认值中。例如: @Nonnull(when = When.ALWAYS) @TypeQualifierDefault({ElementType.METHOD, ElementType.PARAMETER}) @UnderMigration(status = MigrationStatus.WARN) public @interface NonNullApi { } // 类中的类型是非空的,但是只报警告 // 因为 `@NonNullApi` 标注了 `@UnderMigration(status = MigrationStatus.WARN)` @NonNullApi public class Test {} 注意:可空性注解的迁移状态并不会从其类型限定符别称继承,而是适用于默认类型限定符的用法。如果默认类型限定符使用类型限定符别称,并且它们都标注有 @UnderMigration,那么使用默认类型限定符的状态。 返回void的方法 如果在Java中返回void,那么Kotlin返回的就是Unit。如果在调用时返回void,那么Kotlin会事先识别该返回值为void。 注解的使用 @JvmField是Kotlin和Java互相操作属性经常遇到的注解;@JvmStatic是将对象方法编译成Java静态方法;@JvmOverloads主要是Kotlin定义默认参数生成重载方法;@file:JvmName指定Kotlin文件编译之后生成的类名。 NoArg和AllOpen 数据类本身属性没有默认的无参数的构造方法,因此Kotlin提供一个NoArg插件,支持JPA注解,如@Entity。AllOpen是为所标注的类去掉final,目的是为了使该类允许被继承,且支持Spring注解,如@Componet;支持自定义注解类型,如@Poko。 泛型 Kotlin 的泛型与 Java 有点不同,读者可以具体参考泛型章节。Kotlin中的通配符“”代替Java中的“?”;协变和逆变由Java中的extends和super变成了out和in,如ArrayList;在Kotlin中没有Raw类型,如Java中的List对应于Kotlin就是List<>。 与Java一样,Kotlin在运行时不保留泛型,也就是对象不携带传递到它们的构造器中的类型参数的实际类型,即ArrayList()和ArrayList()是不能区分的。这使得执行is检查不可能照顾到泛型,Kotlin只允许is检查星投影的泛型类型。 if (a is List<Int>) // 错误:无法检查它是否真的是一个 Int 列表 // but if (a is List<*>) // OK:不保证列表的内容 Java数组 与 Java 不同,Kotlin 中的数组是不型变的。这意味着 Kotlin 不允许我们把一个 Array 赋值给一个 Array, 从而避免了可能的运行时故障。Kotlin 也禁止我们把一个子类的数组当做超类的数组传递给 Kotlin 的方法, 但是对于 Java 方法,这是允许的(通过 Array<(out) String>! 这种形式的平台类型)。 Java 平台上,数组会使用原生数据类型以避免装箱/拆箱操作的开销。 由于 Kotlin 隐藏了这些实现细节,因此需要一个变通方法来与 Java 代码进行交互。 对于每种原生类型的数组都有一个特化的类(IntArray、 DoubleArray、 CharArray 等等)来处理这种情况。 它们与 Array 类无关,并且会编译成 Java 原生类型数组以获得最佳性能。 例如,假设有一个接受 int 数组索引的 Java 方法。 public class JavaArrayExample { public void removeIndices(int[] indices) { // 在此编码…… } } 在 Kotlin 中调用该方法时,你可以这样传递一个原生类型的数组。 val javaObj = JavaArrayExample() val array = intArrayOf(0, 1, 2, 3) javaObj.removeIndices(array) // 将 int[] 传给方法 当编译为 JVM 字节代码时,编译器会优化对数组的访问,这样就不会引入任何开销。 val array = arrayOf(1, 2, 3, 4) array[x] = array[x] * 2 // 不会实际生成对 get() 和 set() 的调用 for (x in array) { // 不会创建迭代器 print(x) } 即使当我们使用索引定位时,也不会引入任何开销: for (i in array.indices) {// 不会创建迭代器 array[i] += 2 } 最后,in-检测也没有额外开销: if (i in array.indices) { // 同 (i >= 0 && i < array.size) print(array[i]) } Java 可变参数 Java 类有时声明一个具有可变数量参数(varargs)的方法来使用索引。 public class JavaArrayExample { public void removeIndicesVarArg(int... indices) { // 函数体…… } } 在这种情况下,你需要使用展开运算符 * 来传递 IntArray。 val javaObj = JavaArrayExample() val array = intArrayOf(0, 1, 2, 3) javaObj.removeIndicesVarArg(*array) 目前,无法传递 null 给一个声明为可变参数的方法。 SAM转换 就像Java 8一样,Kotlin支持SAM转换,这意味着Kotlin函数字面值可以被自动转换成只有一个非默认方法的Java接口的实现,只要这个方法的参数类型能够与这个Kotlin函数的参数类型相匹配就行。 首先使用Java创建一个SAMInJava类,然后通过Kotlin调用Java中的接口。 import java.util.ArrayList; public class SAMInJava{ private ArrayList<Runnable>runnables=new ArrayList<Runnable>(); public void addTask(Runnable runnable){ runnables.add(runnable); System.out.println("add:"+runnable+",size"+runnables.size()); } Public void removeTask(Runnable runnable){ runnables.remove(runnable); System.out.println("remove:"+runnable+"size"+runnables.size()); } } 然后在Kotlin中调用该Java接口。 fun main(args: Array<String>) { var samJava=SAMJava() val lamba={ print("hello") } samJava.addTask(lamba) samJava.removeTask(lamba) } 运行结果为: add:SAMKotlinKt$sam$Runnable$8b8e16f1@4617c264,size1 remove:SAMKotlinKt$sam$Runnable$8b8e16f1@36baf30csize1 如果Java类有多个接受函数式接口的方法,那么可以通过使用将Lambda表达式转换为特定的SAM类型的适配器函数来选择需要调用的方法。 val lamba={ print("hello") } samJava.addTask(lamba) 注意:SAM转换只适用于接口,而不适用于抽象类,即使这些抽象类只有一个抽象方法。此功能只适用于Java互操作;因为Kotlin具有合适的函数类型,所以不需要将函数自动转换为Kotlin接口的实现,因此不受支持。 除此之外,Kotlin调用Java还有很多的内容,读者可以通过下面的链接来了解:Kotlin调用Java Java调用Kotlin Java 可以轻松调用 Kotlin 代码。 属性 Kotlin属性会被编译成以下Java元素: getter方法,其名称通过加前缀get得到; setter方法,其名称通过加前缀set得到(只适用于var属性); 私有字段,与属性名称相同(仅适用于具有幕后字段的属性)。 例如,将Kotlin变量编译成Java中的变量声明。 private String firstName; public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } 如果属性名称是以is开头的,则使用不同的名称映射规则:getter的名称与属性名称相同,并且setter的名称是通过将is替换成set获得的。例如,对于属性isOpen,其getter会称作isOpen(),而其setter会称作setOpen()。这一规则适用于任何类型的属性,并不仅限于Boolean。 包级函数 例如,在org.foo.bar 包内的 example.kt 文件中声明的所有的函数和属性,包括扩展函数, 该 类会编译成一个名为 org.foo.bar.ExampleKt 的 Java 类的静态方法。首先,新建一个ExampleKt.kt的文件,并新建一个bar函数: package demo class Foo fun bar(){ println("这只是一个bar方法") } 然后,在Java中调用这个函数。 package demo; public class Example { public static void main(String[]args){ demo.ExampleKtKt.bar(); } } 当然,可以使用@JvmName注解修改所生成的Java类的类名。例如: @file:JvmName("Demo") package demo 那么在Java调用时就需要修改类名。例如: public class Example { public static void main(String[]args){ demo.Demo.bar(); } } 在多个文件中生成相同的Java类名(包名相同并且类名相同或者有相同的@JvmName注解)通常是错误的。然而,编译器能够生成一个单一的Java外观类,它具有指定的名称且包含来自于所有文件中具有该名称的所有声明。要生成这样的外观,请在所有的相关文件中使用@JvmMultifileClass注解。 @file:JvmName("example") @file:JvmMultifileClass package demo 实例字段 如果需要在Java中将Kotlin属性作为字段暴露,那么就需要使用@JvmField注解对其进行标注。使用@JvmField注解标注后,该字段将具有与底层属性相同的可见性。如果一个属性有幕后字段(Backing Field)、非私有的、没有open/override或者const修饰符,并且不是被委托的属性,那么可以使用@JvmField注解该属性。 首先,新建一个kt类,并添加如下代码。 class C(id: String) { @JvmField val ID = id } 然后在Java中调用该代码, class JavaClient { public String getID(C c) { return c.ID; } } 延迟初始化的属性(在Java中)也会暴露为字段, 该字段的可见性与 lateinit 属性的 setter 相同。 静态字段 在命名对象或伴生对象时,声明的 Kotlin 属性会在该命名对象或包含伴生对象的类中包含静态幕后字段。通常这些字段是私有的,但可以通过以下方式之一暴露出来。 @JvmField 注解; lateinit 修饰符; const 修饰符。 使用 @JvmField 标注的属性,可以使其成为与属性本身具有相同可见性的静态字段。例如: class Key(val value: Int) { companion object { @JvmField val COMPARATOR: Comparator<Key> = compareBy<Key> { it.value } } } 然后,在Java代码中调用属性。 Key.COMPARATOR.compare(key1, key2); // Key 类中的 public static final 字段 在命名对象或者伴生对象中的一个延迟初始化的属性具有与属性 setter 相同可见性的静态幕后字段。 object Singleton { lateinit var provider: Provider } 然后,在Java中使用该字段的属性。 // Java Singleton.provider = new Provider(); // 在 Singleton 类中的 public static 非-final 字段 用 const 标注的(在类中以及在顶层的)属性在 Java 中会成为静态字段,首先新建一个kt文件。 object Obj { const val CONST = 1 } class C { companion object { const val VERSION = 9 } } const val MAX = 239 然后,在Java中可以直接调用该属性即可。 int c = Obj.CONST; int d = ExampleKt.MAX; int v = C.VERSION; 静态方法 Kotlin将包级函数表示为静态方法。如果对这些函数使用@JvmStatic进行标注,那么Kotlin还可以为在命名对象或伴生对象中定义的函数生成静态方法。如果使用该注解,那么编译器既会在相应对象的类中生成静态方法,也会在对象自身中生成实例方法。例如: class C { companion object { @JvmStatic fun foo() {} fun bar() {} } } 现在,foo()在Java中是静态的,而bar()不是静态的。 C.foo(); // 正确 C.bar(); // 错误:不是一个静态方法 C.Companion.foo(); // 保留实例方法 C.Companion.bar(); // 唯一的工作方式 对于命名对象,也存在同样的规律。 object Obj { @JvmStatic fun foo() {} fun bar() {} } 在 Java 中使用。 Obj.foo(); // 没问题 Obj.bar(); // 错误 Obj.INSTANCE.bar(); // 没问题,通过单例实例调用 Obj.INSTANCE.foo(); // 也没问题 @JvmStatic 注解也可以应用于对象或伴生对象的属性, 使其 getter 和 setter 方法在该对象或包含该伴生对象的类中是静态成员。 可见性 Kotlin的可见性以下列方式映射到Java代码中。 private 成员编译成 private 成员; private 的顶层声明编译成包级局部声明; protected 保持 protected(注意 Java 允许访问同一个包中其他类的受保护成员, 而 Kotlin 不能,所以Java 类会访问更广泛的代码); internal 声明会成为 Java 中的 public。internal 类的成员会通过名字修饰,使其更难以在 Java 中意外使用到,并且根据 Kotlin 规则使其允许重载相同签名的成员而互不可见; public 保持 public。 KClass 有时你需要调用有 KClass 类型参数的 Kotlin 方法。 因为没有从 Class 到 KClass 的自动转换,所以你必须通过调用 Class.kotlin 扩展属性的等价形式来手动进行转换。例如: kotlin.jvm.JvmClassMappingKt.getKotlinClass(MainView.class) 签名冲突 有时我们想让一个 Kotlin 中的命名函数在字节码中有另外一个 JVM 名称,最突出的例子是由于类型擦除引发的。 fun List<String>.filterValid(): List<String> fun List<Int>.filterValid(): List<Int> 这两个函数不能同时定义在一个类中,因为它们的 JVM 签名是一样的。如果我们真的希望它们在 Kotlin 中使用相同的名称,可以使用 @JvmName 去标注其中的一个(或两个),并指定不同的名称作为参数。例如: fun List<String>.filterValid(): List<String> @JvmName("filterValidInt") fun List<Int>.filterValid(): List<Int> 在 Kotlin 中它们可以用相同的名称 filterValid 来访问,而在 Java 中,它们分别是 filterValid 和 filterValidInt。同样的技巧也适用于属性中。例如: val x: Int @JvmName("getX_prop") get() = 15 fun getX() = 10 生成重载 通常,如果你写一个有默认参数值的 Kotlin 函数,在 Java 中只会有一个所有参数都存在的完整参数签名的方法可见,如果希望向 Java 调用者暴露多个重载,可以使用 @JvmOverloads 注解。该注解可以用于构造函数、静态方法中,但不能用于抽象方法和在接口中定义的方法。 class Foo @JvmOverloads constructor(x: Int, y: Double = 0.0) { @JvmOverloads fun f(a: String, b: Int = 0, c: String = "abc") { …… } } 对于每一个有默认值的参数,都会生成一个额外的重载,这个重载会把这个参数和它右边的所有参数都移除掉。在上例中,会生成以下代码 。 // 构造函数: Foo(int x, double y) Foo(int x) // 方法 void f(String a, int b, String c) { } void f(String a, int b) { } void f(String a) { } 请注意,如次构造函数中所述,如果一个类的所有构造函数参数都有默认值,那么会为其生成一个公有的无参构造函数,此时就算没有 @JvmOverloads 注解也有效。 受检异常 如上所述,Kotlin 没有受检异常。 所以,通常 Kotlin 函数的 Java 签名不会声明抛出异常, 于是如果我们有一个这样的 Kotlin 函数。首先,新建一个kt文件。 //// example.kt package demo fun foo() { throw IOException() } 然后,在 Java 中调用它的时候,需要使用try{}catch{}来捕捉这个异常。 // Java try { demo.Example.foo(); } catch (IOException e) { // 错误:foo() 未在 throws 列表中声明 IOException // …… } 因为 foo() 没有声明 IOException,我们从 Java 编译器得到了一个报错消息。 为了解决这个问题,要在 Kotlin 中使用 @Throws 注解。 @Throws(IOException::class) fun foo() { throw IOException() } 空安全性 当从Java中调用Kotlin函数时,没有任何方法可以阻止Kotlin中的空值传入。Kotlin在JVM虚拟机中运行时会检查所有的公共函数,可以检查非空值,这时候就可以通过NullPointerException得到Java中的非空值代码。 型变的泛型 当 Kotlin 的类使用了声明处型变时,可以通过两种方式从Java代码中看到它们的用法。让我们假设我们有以下类和两个使用它的函数: class Box<out T>(val value: T) interface Base class Derived : Base fun boxDerived(value: Derived): Box<Derived> = Box(value) fun unboxBase(box: Box<Base>): Base = box.value 将这两个函数转换成Java代码如下: Box<Derived> boxDerived(Derived value) { …… } Base unboxBase(Box<Base> box) { …… } 问题是,在 Kotlin 中我们可以这样写 unboxBase(boxDerived("s")),但是在 Java 中是行不通的,因为在 Java 中类 Box 在其泛型参数 T 上是不型变的,于是 Box 并不是 Box 的子类。 要使其在 Java 中工作,我们按以下这样定义 unboxBase。 Base unboxBase(Box<? extends Base> box) { …… } 这里我们使用 Java 的通配符类型(? extends Base)来通过使用处型变来模拟声明处型变,因为在 Java 中只能这样。 当它作为参数出现时,为了让 Kotlin 的 API 在 Java 中工作,对于协变定义的 Box 我们生成 Box 作为 Box<? extends Super> (或者对于逆变定义的 Foo 生成 Foo<? super Bar>)。当它是一个返回值时, 我们不生成通配符,因为否则 Java 客户端将必须处理它们(并且它违反常用 Java 编码风格)。因此,我们的示例中的对应函数实际上翻译如下: // 作为返回类型——没有通配符 Box<Derived> boxDerived(Derived value) { …… } // 作为参数——有通配符 Base unboxBase(Box<? extends Base> box) { …… } 注意:当参数类型是 final 时,生成通配符通常没有意义,所以无论在什么地方 Box 始终转换为 Box。如果我们在默认不生成通配符的地方需要通配符,我们可以使用 @JvmWildcard 注解: fun boxDerived(value: Derived): Box<@JvmWildcard Derived> = Box(value) // 将被转换成 // Box<? extends Derived> boxDerived(Derived value) { …… } 另一方面,如果我们根本不需要默认的通配符转换,我们可以使用@JvmSuppressWildcards。 fun unboxBase(box: Box<@JvmSuppressWildcards Base>): Base = box.value // 会翻译成 // Base unboxBase(Box<Base> box) { …… } 注意:@JvmSuppressWildcards 不仅可用于单个类型参数,还可用于整个声明(如函数或类),从而抑制其中的所有通配符。 Nothing 类型 类型 Nothing 是特殊的,因为它在 Java 中没有自然的对应。确实,每个 Java 引用类型,包括 java.lang.Void 都可以接受 null 值,但是 Nothing 不行,以为这种类型不能在 Java 中被准确表示。这就是为什么在使用 Nothing 参数的地方 Kotlin 生成一个原始类型: fun emptyList(): List<Nothing> = listOf() // 会翻译成 // List emptyList() { …… }
这两年,各种异步编程框架,上面RxJava,RxAndroid,RxSwift等等,今天要聊的是RxJs,对于我等入门不久的前端工程师来说,这个框架还是比较有新颖的,中文官网地址:http://cn.rx.js.org/ RxJs简介 RxJS是一个异步编程的库,同时它通过observable序列来实现基于事件的编程。它提供了一个核心的类型:Observable,几个辅助类型(Observer,Schedulers,Subjects),受到Array的扩展操作(map,filter,reduce,every等等)启发,允许直接处理异步事件的集合。 ReactiveX结合了Observer模式、Iterator模式和函数式编程和集合来构建一个管理事件序列的理想方式。在RxJS中管理异步事件的基本概念中有以下几点需要注意: Observable:代表了一个调用未来值或事件的集合的概念 Observer:代表了一个知道如何监听Observable传递过来的值的回调集合 Subscription:代表了一个可执行的Observable,主要是用于取消执行 Operators:是一个纯函数,允许处理集合与函数式编程风格的操作,比如map、filter、concat、flatMap等 Subject:相当于一个EventEmitter,它的唯一的方法是广播一个值或事件给多个Observer Schedulers:是一个集中式调度程序来控制并发性,允许我们在setTimeout或者requestAnimationFrame上进行协调计算 实例 正常情况下,注册一个事件监听函数的代码如下: var button = document.querySelector('button'); button.addEventListener('click', () => console.log('Clicked!')); 使用RxJS,你可以创建一个observable来代替: var button = document.querySelector('button'); Rx.Observable.fromEvent(button, 'click') .subscrible(() => console.log('Clicked!')); 纯净性 (Purity) 使得RxJS变得如此强大的原因是它使用了纯函数,这意味着你的代码很少会发生错误。正常情况下,你不会选择创建一个纯函数。 var count = 0; var button = document.querySelector('button'); button.addEventListener('click', () => console.log(`Clicked $(++count) times`)); 而在RxJs中却可以大量使用纯函数。 var button = document.querySelector('button'); Rx.Observable.fromEvent(button, 'click') .scan(count => count + 1, 0) .subscribe(count => console.log(`Clicked ${count} items`)); 其中,scan操作符类似于arrays的reduce操作符。它需要一个回调函数作为一个参数,函数返回的值将作为下次调用时的参数。 流动性 (Flow) RxJS 提供了一整套操作符来帮助你控制事件如何流经 observables 。下面的代码展示的是如何控制一秒钟内最多点击一次,先来看使用普通的 JavaScript: var count = 0; var rate = 1000; var lastClick = Date.now() - rate; var button = document.querySelector('button'); button.addEventListener('click', () => { if (Date.now() - lastClick >= rate) { console.log(`Clicked ${++count} times`); lastClick = Date.now(); } }); 使用 RxJS: var button = document.querySelector('button'); Rx.Observable.fromEvent(button, 'click') .throttleTime(1000) .scan(count => count + 1, 0) .subscribe(count => console.log(`Clicked ${count} times`)); 其他流程控制操作符有 filter、delay、debounceTime、take、takeUntil、distinct、distinctUntilChanged 等等。 值 (Values) 对于流经 observables 的值,你可以对其进行转换。下面的代码展示的是如何累加每次点击的鼠标 x 坐标,先来看使用普通的 JavaScript: var count = 0; var rate = 1000; var lastClick = Date.now() - rate; var button = document.querySelector('button'); button.addEventListener('click', (event) => { if (Date.now() - lastClick >= rate) { count += event.clientX; console.log(count) lastClick = Date.now(); } }); 使用 RxJS: var button = document.querySelector('button'); Rx.Observable.fromEvent(button, 'click') .throttleTime(1000) .map(event => event.clientX) .scan((count, clientX) => count + clientX, 0) .subscribe(count => console.log(count)); 其他产生值的操作符有 pluck、pairwise、 sample 等等。 Observable Observables 是多个值的惰性推送集合。它填补了下面表格中的空白: 行为 单个值 多个值 拉取 Function Iterator 推送 Promise Observable 例如:当订阅下面代码中的 Observable 的时候会立即(同步地)推送值1、2、3,然后1秒后会推送值4,再然后是完成流。 var observable = Rx.Observable.create(function (observer) { observer.next(1); observer.next(2); observer.next(3); setTimeout(() => { observer.next(4); observer.complete(); }, 1000); }); 要调用 Observable 并看到这些值,我们需要订阅 Observable: var observable = Rx.Observable.create(function (observer) { observer.next(1); observer.next(2); observer.next(3); setTimeout(() => { observer.next(4); observer.complete(); }, 1000); }); console.log('just before subscribe'); observable.subscribe({ next: x => console.log('got value ' + x), error: err => console.error('something wrong occurred: ' + err), complete: () => console.log('done'), }); console.log('just after subscribe'); 控制台执行的结果: just before subscribe got value 1 got value 2 got value 3 just after subscribe got value 4 done 拉取 (Pull) vs. 推送 (Push) 拉取和推送是两种不同的协议,用来描述数据生产者 (Producer)如何与数据消费者 (Consumer)如何进行通信的。 什么是拉取? - 在拉取体系中,由消费者来决定何时从生产者那接收数据。生产者本身不知道数据是何时交付到消费者手中的。 每个 JavaScript 函数都是拉取体系。函数是数据的生产者,调用该函数的代码通过从函数调用中“取出”一个单个返回值来对该函数进行消费。 ES2015 引入了 generator 函数和 iterators (function*),这是另外一种类型的拉取体系。调用 iterator.next() 的代码是消费者,它会从 iterator(生产者) 那“取出”多个值。 行为 生产者 消费者 拉取 被动的: 当被请求时产生数据。 主动的: 决定何时请求数据。 推送 主动的: 按自己的节奏产生数据。 被动的: 对收到的数据做出反应。 什么是推送? - 在推送体系中,由生产者来决定何时把数据发送给消费者。消费者本身不知道何时会接收到数据。 在当今的 JavaScript 世界中,Promises 是最常见的推送体系类型。Promise(生产者) 将一个解析过的值传递给已注册的回调函数(消费者),但不同于函数的是,由 Promise 来决定何时把值“推送”给回调函数。 RxJS 引入了 Observables,一个新的 JavaScript 推送体系。Observable 是多个值的生产者,并将值“推送”给观察者(消费者)。 Function 是惰性的评估运算,调用时会同步地返回一个单一值。 Generator 是惰性的评估运算,调用时会同步地返回零到(有可能的)无限多个值。 Promise 是最终可能(或可能不)返回单个值的运算。 Observable 是惰性的评估运算,它可以从它被调用的时刻起同步或异步地返回零到(有可能的)无限多个值。 Observables 作为函数的泛化 与流行的说法正好相反,Observables 既不像 EventEmitters,也不像多个值的 Promises 。在某些情况下,即当使用 RxJS 的 Subjects 进行多播时, Observables 的行为可能会比较像 EventEmitters,但通常情况下 Observables 的行为并不像 EventEmitters 。 考虑如下代码: function foo() { console.log('Hello'); return 42; } var x = foo.call(); // 等同于 foo() console.log(x); var y = foo.call(); // 等同于 foo() console.log(y); 我们期待看到的输出: "Hello" 42 "Hello" 42 可以使用 Observables 重写上面的代码: var foo = Rx.Observable.create(function (observer) { console.log('Hello'); observer.next(42); }); foo.subscribe(function (x) { console.log(x); }); foo.subscribe(function (y) { console.log(y); }); 上面的代码输出的结果如下: "Hello" 42 "Hello" 42 这是因为函数和 Observables 都是惰性运算。如果你不调用函数,console.log('Hello') 就不会执行。Observables 也是如此,如果你不“调用”它(使用 subscribe),console.log('Hello') 也不会执行。此外,“调用”或“订阅”是独立的操作:两个函数调用会触发两个单独的副作用,两个 Observable 订阅同样也是触发两个单独的副作用。EventEmitters 共享副作用并且无论是否存在订阅者都会尽早执行,Observables 与之相反,不会共享副作用并且是延迟执行。一些人声称 Observables 是异步的。那不是真的。如果你用日志包围一个函数调用,像这样: console.log('before'); console.log(foo.call()); console.log('after'); 输出结果如下: "before" "Hello" 42 "after" 使用 Observables 来做同样的事: console.log('before'); foo.subscribe(function (x) { console.log(x); }); console.log('after'); 这证明了 foo 的订阅完全是同步的,就像函数一样。那么 Observable 和 函数的区别是什么呢?Observable 可以随着时间的推移“返回”多个值,这是函数所做不到的。例如,下面的代码: function foo() { console.log('Hello'); return 42; return 100; // 死代码,永远不会执行 } 函数只能返回一个值。但 Observables 可以这样: var foo = Rx.Observable.create(function (observer) { console.log('Hello'); observer.next(42); observer.next(100); // “返回”另外一个值 observer.next(200); // 还可以再“返回”值 }); console.log('before'); foo.subscribe(function (x) { console.log(x); }); console.log('after'); 输出的结果如下: "before" "Hello" 42 100 200 "after" 同时,你还可以异步地“返回”值: var foo = Rx.Observable.create(function (observer) { console.log('Hello'); observer.next(42); observer.next(100); observer.next(200); setTimeout(() => { observer.next(300); // 异步执行 }, 1000); }); console.log('before'); foo.subscribe(function (x) { console.log(x); }); console.log('after'); 输出结果如下: "before" "Hello" 42 100 200 "after" 300 结论: func.call() 意思是 "同步地给我一个值" observable.subscribe() 意思是 "给我任意数量的值,无论是同步还是异步"。 Observable 剖析 Observables 是使用 Rx.Observable.create 或创建操作符创建的,并使用观察者来订阅它,然后执行它并发送 next / error / complete 通知给观察者,而且执行可能会被清理。这四个方面全部编码在 Observables 实例中,但某些方面是与其他类型相关的,像 Observer (观察者) 和 Subscription (订阅)。 Observable 的核心有4点: 创建 Observables 订阅 Observables 执行 Observables 清理 Observables 创建 Observables Rx.Observable.create 是 Observable 构造函数的别名,它接收一个参数:subscribe 函数。下面的示例创建了一个 Observable,它每隔一秒会向观察者发送字符串 'hi' 。 var observable = Rx.Observable.create(function subscribe(observer) { var id = setInterval(() => { observer.next('hi') }, 1000); }); Observables 可以使用 create 来创建, 但通常我们使用所谓的创建操作符, 像 of、from、interval、等等。在上面的示例中,subscribe 函数是用来描述 Observable 最重要的一块。我们来看下订阅是什么意思。 订阅 Observables 示例中的 Observable 对象 observable 可以订阅,像这样: observable.subscribe(x => console.log(x)); observable.subscribe 和 Observable.create(function subscribe(observer) {...}) 中的 subscribe 有着同样的名字,这并不是一个巧合。在库中,它们是不同的,但从实际出发,你可以认为在概念上它们是等同的。 这表明 subscribe 调用在同一 Observable 的多个观察者之间是不共享的。当使用一个观察者调用 observable.subscribe 时,Observable.create(function subscribe(observer) {...}) 中的 subscribe 函数只服务于给定的观察者。对 observable.subscribe 的每次调用都会触发针对给定观察者的独立设置。 订阅 Observable 像是调用函数, 并提供接收数据的回调函数。 这与像 addEventListener / removeEventListener 这样的事件处理方法 API 是完全不同的。使用 observable.subscribe,在 Observable 中不会将给定的观察者注册为监听器。Observable 甚至不会去维护一个附加的观察者列表。 subscribe 调用是启动 “Observable 执行”的一种简单方式, 并将值或事件传递给本次执行的观察者。 执行 Observables Observable.create(function subscribe(observer) {...}) 中...的代码表示 “Observable 执行”,它是惰性运算,只有在每个观察者订阅后才会执行。随着时间的推移,执行会以同步或异步的方式产生多个值。 Observable 执行可以传递三种类型的值: "Next" 通知: 发送一个值,比如数字、字符串、对象,等等。 "Error" 通知: 发送一个 JavaScript 错误 或 异常。 "Complete" 通知: 不再发送任何值。 "Next" 通知是最重要,也是最常见的类型:它们表示传递给观察者的实际数据。"Error" 和 "Complete" 通知可能只会在 Observable 执行期间发生一次,并且只会执行其中的一个。 这些约束用所谓的 Observable 语法或合约表达最好,写为正则表达式是这样的: next*(error|complete)? 在 Observable 执行中, 可能会发送零个到无穷多个 "Next" 通知。如果发送的是 "Error" 或 "Complete" 通知的话,那么之后不会再发送任何通知了。 下面是 Observable 执行的示例,它发送了三个 "Next" 通知,然后是 "Complete" 通知: var observable = Rx.Observable.create(function subscribe(observer) { observer.next(1); observer.next(2); observer.next(3); observer.complete(); }); Observable 严格遵守自身的规约,所以下面的代码不会发送 "Next" 通知 4。 var observable = Rx.Observable.create(function subscribe(observer) { observer.next(1); observer.next(2); observer.next(3); observer.complete(); observer.next(4); // 因为违反规约,所以不会发送 }); 在 subscribe 中用 try/catch 代码块来包裹任意代码是个不错的主意,如果捕获到异常的话,会发送 "Error" 通知: var observable = Rx.Observable.create(function subscribe(observer) { try { observer.next(1); observer.next(2); observer.next(3); observer.complete(); } catch (err) { observer.error(err); // 如果捕获到异常会发送一个错误 } }); 清理 Observable 执行 因为 Observable 执行可能会是无限的,并且观察者通常希望能在有限的时间内中止执行,所以我们需要一个 API 来取消执行。因为每个执行都是其对应观察者专属的,一旦观察者完成接收值,它必须要一种方法来停止执行,以避免浪费计算能力或内存资源。 当调用了 observable.subscribe ,观察者会被附加到新创建的 Observable 执行中。这个调用还返回一个对象,即 Subscription (订阅): var subscription = observable.subscribe(x => console.log(x)); Subscription 表示进行中的执行,它有最小化的 API 以允许你取消执行。想了解更多订阅相关的内容,请参见 Subscription 类型。使用 subscription.unsubscribe() 你可以取消进行中的执行: var observable = Rx.Observable.from([10, 20, 30]); var subscription = observable.subscribe(x => console.log(x)); // 稍后: subscription.unsubscribe(); 当你订阅了 Observable,你会得到一个 Subscription ,它表示进行中的执行。只要调用 unsubscribe() 方法就可以取消执行。 当我们使用 create() 方法创建 Observable 时,Observable 必须定义如何清理执行的资源。你可以通过在 function subscribe() 中返回一个自定义的 unsubscribe 函数。 举例来说,这是我们如何清理使用了 setInterval 的 interval 执行集合: var observable = Rx.Observable.create(function subscribe(observer) { // 追踪 interval 资源 var intervalID = setInterval(() => { observer.next('hi'); }, 1000); // 提供取消和清理 interval 资源的方法 return function unsubscribe() { clearInterval(intervalID); }; }); 正如 observable.subscribe 类似于 Observable.create(function subscribe() {...}),从 subscribe 返回的 unsubscribe 在概念上也等同于 subscription.unsubscribe。事实上,如果我们抛开围绕这些概念的 ReactiveX 类型,保留下来的只是相当简单的 JavaScript 。 function subscribe(observer) { var intervalID = setInterval(() => { observer.next('hi'); }, 1000); return function unsubscribe() { clearInterval(intervalID); }; } var unsubscribe = subscribe({next: (x) => console.log(x)}); // 稍后: unsubscribe(); // 清理资源 为什么我们要使用像 Observable、Observer 和 Subscription 这样的 Rx 类型?原因是保证代码的安全性(比如 Observable 规约)和操作符的可组合性。 Observer (观察者) 什么是观察者? - 观察者是由 Observable 发送的值的消费者。观察者只是一组回调函数的集合,每个回调函数对应一种 Observable 发送的通知类型:next、error 和 complete 。下面的示例是一个典型的观察者对象: var observer = { next: x => console.log('Observer got a next value: ' + x), error: err => console.error('Observer got an error: ' + err), complete: () => console.log('Observer got a complete notification'), }; 要使用观察者,需要把它提供给 Observable 的 subscribe 方法: observable.subscribe(observer); 观察者只是有三个回调函数的对象,每个回调函数对应一种 Observable 发送的通知类型。 RxJS 中的观察者也可能是部分的。如果你没有提供某个回调函数,Observable 的执行也会正常运行,只是某些通知类型会被忽略,因为观察者中没有没有相对应的回调函数。 下面的示例是没有 complete 回调函数的观察者: var observer = { next: x => console.log('Observer got a next value: ' + x), error: err => console.error('Observer got an error: ' + err), }; 当订阅 Observable 时,你可能只提供了一个回调函数作为参数,而并没有将其附加到观察者对象上,例如这样: observable.subscribe(x => console.log('Observer got a next value: ' + x)); 在 observable.subscribe 内部,它会创建一个观察者对象并使用第一个回调函数参数作为 next 的处理方法。所有三种类型的回调函数都可以直接作为参数来提供: observable.subscribe( x => console.log('Observer got a next value: ' + x), err => console.error('Observer got an error: ' + err), () => console.log('Observer got a complete notification') ); Subscription (订阅) 什么是 Subscription ? - Subscription 是表示可清理资源的对象,通常是 Observable 的执行。Subscription 有一个重要的方法,即 unsubscribe,它不需要任何参数,只是用来清理由 Subscription 占用的资源。在上一个版本的 RxJS 中,Subscription 叫做 "Disposable" (可清理对象)。 var observable = Rx.Observable.interval(1000); var subscription = observable.subscribe(x => console.log(x)); // 稍后: // 这会取消正在进行中的 Observable 执行 // Observable 执行是通过使用观察者调用 subscribe 方法启动的 subscription.unsubscribe(); Subscription 基本上只有一个 unsubscribe() 函数,这个函数用来释放资源或去取消 Observable 执行。Subscription 还可以合在一起,这样一个 Subscription 调用 unsubscribe() 方法,可能会有多个 Subscription 取消订阅 。你可以通过把一个 Subscription 添加到另一个上面来做这件事: var observable1 = Rx.Observable.interval(400); var observable2 = Rx.Observable.interval(300); var subscription = observable1.subscribe(x => console.log('first: ' + x)); var childSubscription = observable2.subscribe(x => console.log('second: ' + x)); subscription.add(childSubscription); setTimeout(() => { // subscription 和 childSubscription 都会取消订阅 subscription.unsubscribe(); }, 1000); 执行上面的代码,将看到如下的结果: second: 0 first: 0 second: 1 first: 1 second: 2 Subscriptions 还有一个 remove(otherSubscription) 方法,用来撤销一个已添加的子 Subscription 。 Subject (主体) 什么是 Subject? - RxJS Subject 是一种特殊类型的 Observable,它允许将值多播给多个观察者,所以 Subject 是多播的,而普通的 Observables 是单播的(每个已订阅的观察者都拥有 Observable 的独立执行)。 Subject 像是 Observalbe,但是可以多播给多个观察者。Subject 还像是 EventEmitters,维护着多个监听器的注册表。 每个 Subject 都是 Observable。 - 对于 Subject,你可以提供一个观察者并使用 subscribe 方法,就可以开始正常接收值。从观察者的角度而言,它无法判断 Observable 执行是来自普通的 Observable 还是 Subject 。在 Subject 的内部,subscribe 不会调用发送值的新执行。它只是将给定的观察者注册到观察者列表中,类似于其他库或语言中的 addListener 的工作方式。 每个 Subject 都是观察者。 - Subject 是一个有如下方法的对象: next(v)、error(e) 和 complete() 。要给 Subjetc 提供新值,只要调用 next(theValue),它会将值多播给已注册监听该 Subject 的观察者们。 var subject = new Rx.Subject(); subject.subscribe({ next: (v) => console.log('observerA: ' + v) }); subject.subscribe({ next: (v) => console.log('observerB: ' + v) }); subject.next(1); subject.next(2); 下面是控制台的输出结果: observerA: 1 observerB: 1 observerA: 2 observerB: 2 因为 Subject 是观察者,这也就在意味着你可以把 Subject 作为参数传给任何 Observable 的 subscribe 方法,如下面的示例所展示的: var subject = new Rx.Subject(); subject.subscribe({ next: (v) => console.log('observerA: ' + v) }); subject.subscribe({ next: (v) => console.log('observerB: ' + v) }); var observable = Rx.Observable.from([1, 2, 3]); observable.subscribe(subject); // 你可以提供一个 Subject 进行订阅 执行结果: observerA: 1 observerB: 1 observerA: 2 observerB: 2 observerA: 3 observerB: 3 多播的 Observables “多播 Observable” 通过 Subject 来发送通知,这个 Subject 可能有多个订阅者,然而普通的 “单播 Observable” 只发送通知给单个观察者。 多播 Observable 在底层是通过使用 Subject 使得多个观察者可以看见同一个 Observable 执行。 在底层,这就是 multicast 操作符的工作原理:观察者订阅一个基础的 Subject,然后 Subject 订阅源 Observable 。下面的示例与前面使用 observable.subscribe(subject) 的示例类似: var source = Rx.Observable.from([1, 2, 3]); var subject = new Rx.Subject(); var multicasted = source.multicast(subject); // 在底层使用了 `subject.subscribe({...})`: multicasted.subscribe({ next: (v) => console.log('observerA: ' + v) }); multicasted.subscribe({ next: (v) => console.log('observerB: ' + v) }); // 在底层使用了 `source.subscribe(subject)`: multicasted.connect(); multicast 操作符返回一个 Observable,它看起来和普通的 Observable 没什么区别,但当订阅时就像是 Subject 。multicast 返回的是 ConnectableObservable,它只是一个有 connect() 方法的 Observable 。 connect() 方法十分重要,它决定了何时启动共享的 Observable 执行。因为 connect() 方法在底层执行了 source.subscribe(subject),所以它返回的是 Subscription,你可以取消订阅以取消共享的 Observable 执行。 引用计数 手动调用 connect() 并处理 Subscription 通常太笨重。通常,当第一个观察者到达时我们想要自动地连接,而当最后一个观察者取消订阅时我们想要自动地取消共享执行。 第一个观察者订阅了多播 Observable 多播 Observable 已连接 next 值 0 发送给第一个观察者 第二个观察者订阅了多播 Observable next 值 1 发送给第一个观察者 next 值 1 发送给第二个观察者 第一个观察者取消了多播 Observable 的订阅 next 值 2 发送给第二个观察者 第二个观察者取消了多播 Observable 的订阅 多播 Observable 的连接已中断(底层进行的操作是取消订阅) 要实现这点,需要显式地调用 connect(),代码如下: var source = Rx.Observable.interval(500); var subject = new Rx.Subject(); var multicasted = source.multicast(subject); var subscription1, subscription2, subscriptionConnect; subscription1 = multicasted.subscribe({ next: (v) => console.log('observerA: ' + v) }); // 这里我们应该调用 `connect()`,因为 `multicasted` 的第一个 // 订阅者关心消费值 subscriptionConnect = multicasted.connect(); setTimeout(() => { subscription2 = multicasted.subscribe({ next: (v) => console.log('observerB: ' + v) }); }, 600); setTimeout(() => { subscription1.unsubscribe(); }, 1200); // 这里我们应该取消共享的 Observable 执行的订阅, // 因为此后 `multicasted` 将不再有订阅者 setTimeout(() => { subscription2.unsubscribe(); subscriptionConnect.unsubscribe(); // 用于共享的 Observable 执行 }, 2000); 如果不想显式调用 connect(),我们可以使用 ConnectableObservable 的 refCount() 方法(引用计数),这个方法返回 Observable,这个 Observable 会追踪有多少个订阅者。当订阅者的数量从0变成1,它会调用 connect() 以开启共享的执行。当订阅者数量从1变成0时,它会完全取消订阅,停止进一步的执行。 refCount 的作用是,当有第一个订阅者时,多播 Observable 会自动地启动执行,而当最后一个订阅者离开时,多播 Observable 会自动地停止执行。 var source = Rx.Observable.interval(500); var subject = new Rx.Subject(); var refCounted = source.multicast(subject).refCount(); var subscription1, subscription2, subscriptionConnect; // 这里其实调用了 `connect()`, // 因为 `refCounted` 有了第一个订阅者 console.log('observerA subscribed'); subscription1 = refCounted.subscribe({ next: (v) => console.log('observerA: ' + v) }); setTimeout(() => { console.log('observerB subscribed'); subscription2 = refCounted.subscribe({ next: (v) => console.log('observerB: ' + v) }); }, 600); setTimeout(() => { console.log('observerA unsubscribed'); subscription1.unsubscribe(); }, 1200); // 这里共享的 Observable 执行会停止, // 因为此后 `refCounted` 将不再有订阅者 setTimeout(() => { console.log('observerB unsubscribed'); subscription2.unsubscribe(); }, 2000); 执行结果: observerA subscribed observerA: 0 observerB subscribed observerA: 1 observerB: 1 observerA unsubscribed observerB: 2 observerB unsubscribed refCount() 只存在于 ConnectableObservable,它返回的是 Observable,而不是另一个 ConnectableObservable 。 BehaviorSubject Subject 的其中一个变体就是 BehaviorSubject,它有一个“当前值”的概念。它保存了发送给消费者的最新值。并且当有新的观察者订阅时,会立即从 BehaviorSubject 那接收到“当前值”。 BehaviorSubjects 适合用来表示“随时间推移的值”。举例来说,生日的流是一个 Subject,但年龄的流应该是一个 BehaviorSubject 。 在下面的示例中,BehaviorSubject 使用值0进行初始化,当第一个观察者订阅时会得到0。第二个观察者订阅时会得到值2,尽管它是在值2发送之后订阅的。 var subject = new Rx.BehaviorSubject(0); // 0是初始值 subject.subscribe({ next: (v) => console.log('observerA: ' + v) }); subject.next(1); subject.next(2); subject.subscribe({ next: (v) => console.log('observerB: ' + v) }); subject.next(3); 输出结果: observerA: 0 observerA: 1 observerA: 2 observerB: 2 observerA: 3 observerB: 3 ReplaySubject ReplaySubject 类似于 BehaviorSubject,它可以发送旧值给新的订阅者,但它还可以记录 Observable 执行的一部分。 ReplaySubject 记录 Observable 执行中的多个值并将其回放给新的订阅者。 当创建 ReplaySubject 时,你可以指定回放多少个值: var subject = new Rx.ReplaySubject(3); // 为新的订阅者缓冲3个值 subject.subscribe({ next: (v) => console.log('observerA: ' + v) }); subject.next(1); subject.next(2); subject.next(3); subject.next(4); subject.subscribe({ next: (v) => console.log('observerB: ' + v) }); subject.next(5); 输出: observerA: 1 observerA: 2 observerA: 3 observerA: 4 observerB: 2 observerB: 3 observerB: 4 observerA: 5 observerB: 5 除了缓冲数量,你还可以指定 window time (以毫秒为单位)来确定多久之前的值可以记录。在下面的示例中,我们使用了较大的缓存数量100,但 window time 参数只设置了500毫秒。 var subject = new Rx.ReplaySubject(100, 500 /* windowTime */); subject.subscribe({ next: (v) => console.log('observerA: ' + v) }); var i = 1; setInterval(() => subject.next(i++), 200); setTimeout(() => { subject.subscribe({ next: (v) => console.log('observerB: ' + v) }); }, 1000); 从下面的输出可以看出,第二个观察者得到的值是3、4、5,这三个值是订阅发生前的500毫秒内发生的: observerA: 1 observerA: 2 observerA: 3 observerA: 4 observerA: 5 observerB: 3 observerB: 4 observerB: 5 observerA: 6 observerB: 6 ... AsyncSubject AsyncSubject 是另一个 Subject 变体,只有当 Observable 执行完成时(执行 complete()),它才会将执行的最后一个值发送给观察者。 var subject = new Rx.AsyncSubject(); subject.subscribe({ next: (v) => console.log('observerA: ' + v) }); subject.next(1); subject.next(2); subject.next(3); subject.next(4); subject.subscribe({ next: (v) => console.log('observerB: ' + v) }); subject.next(5); subject.complete(); 输出: observerA: 5 observerB: 5 AsyncSubject 和 last() 操作符类似,因为它也是等待 complete 通知,以发送一个单个值。 Operators (操作符) 尽管 RxJS 的根基是 Observable,但最有用的还是它的操作符。操作符是允许复杂的异步代码以声明式的方式进行轻松组合的基础代码单元。 操作符? 操作符是 Observable 类型上的方法,比如 .map(...)、.filter(...)、.merge(...),等等。当操作符被调用时,它们不会改变已经存在的 Observable 实例。相反,它们返回一个新的 Observable ,它的 subscription 逻辑基于第一个 Observable 。 操作符是函数,它基于当前的 Observable 创建一个新的 Observable。这是一个无副作用的操作:前面的 Observable 保持不变。 操作符本质上是一个纯函数 (pure function),它接收一个 Observable 作为输入,并生成一个新的 Observable 作为输出。订阅输出 Observalbe 同样会订阅输入 Observable 。在下面的示例中,我们创建一个自定义操作符函数,它将从输入 Observable 接收的每个值都乘以10: function multiplyByTen(input) { var output = Rx.Observable.create(function subscribe(observer) { input.subscribe({ next: (v) => observer.next(10 * v), error: (err) => observer.error(err), complete: () => observer.complete() }); }); return output; } var input = Rx.Observable.from([1, 2, 3, 4]); var output = multiplyByTen(input); output.subscribe(x => console.log(x)); 输出: 10 20 30 40 注意,订阅 output 会导致 input Observable 也被订阅。我们称之为“操作符订阅链”。 实例操作符 vs. 静态操作符 什么是实例操作符? - 通常提到操作符时,我们指的是实例操作符,它是 Observable 实例上的方法。举例来说,如果上面的 multiplyByTen 是官方提供的实例操作符,它看起来大致是这个样子的: Rx.Observable.prototype.multiplyByTen = function multiplyByTen() { var input = this; return Rx.Observable.create(function subscribe(observer) { input.subscribe({ next: (v) => observer.next(10 * v), error: (err) => observer.error(err), complete: () => observer.complete() }); }); } 实例运算符是使用 this 关键字来指代输入的 Observable 的函数。 注意,这里的 input Observable 不再是一个函数参数,它现在是 this 对象。下面是我们如何使用这样的实例运算符: var observable = Rx.Observable.from([1, 2, 3, 4]).multiplyByTen(); observable.subscribe(x => console.log(x)); 什么是静态操作符? - 除了实例操作符,还有静态操作符,它们是直接附加到 Observable 类上的。静态操作符在内部不使用 this 关键字,而是完全依赖于它的参数。 静态操作符是附加到 Observalbe 类上的纯函数,通常用来从头开始创建 Observalbe。 最常用的静态操作符类型是所谓的创建操作符。它们只接收非 Observable 参数,比如数字,然后创建一个新的 Observable ,而不是将一个输入 Observable 转换为输出 Observable 。 一个典型的静态操作符例子就是 interval 函数。它接收一个数字(非 Observable)作为参数,并生产一个 Observable 作为输出: var observable = Rx.Observable.interval(1000 /* 毫秒数 */); 创建操作符的另一个例子就是 create,已经在前面的示例中广泛使用。点击这里查看所有静态操作符列表。 然而,有些静态操作符可能不同于简单的创建。一些组合操作符可能是静态的,比如 merge、combineLatest、concat,等等。这些作为静态运算符是有道理的,因为它们将多个 Observables 作为输入,而不仅仅是一个,例如: var observable1 = Rx.Observable.interval(1000); var observable2 = Rx.Observable.interval(400); var merged = Rx.Observable.merge(observable1, observable2); Scheduler (调度器) 什么是调度器? - 调度器控制着何时启动 subscription 和何时发送通知。它由三部分组成: 调度器是一种数据结构。 它知道如何根据优先级或其他标准来存储任务和将任务进行排序。 调度器是执行上下文。 它表示在何时何地执行任务(举例来说,立即的,或另一种回调函数机制(比如 setTimeout 或 process.nextTick),或动画帧)。 调度器有一个(虚拟的)时钟。 调度器功能通过它的 getter 方法 now() 提供了“时间”的概念。在具体调度器上安排的任务将严格遵循该时钟所表示的时间。 调度器可以让你规定 Observable 在什么样的执行上下文中发送通知给它的观察者。 在下面的示例中,我们采用普通的 Observable ,它同步地发出值1、2、3,并使用操作符 observeOn 来指定 async 调度器发送这些值。 var observable = Rx.Observable.create(function (observer) { observer.next(1); observer.next(2); observer.next(3); observer.complete(); }) .observeOn(Rx.Scheduler.async); console.log('just before subscribe'); observable.subscribe({ next: x => console.log('got value ' + x), error: err => console.error('something wrong occurred: ' + err), complete: () => console.log('done'), }); console.log('just after subscribe'); 输出结果: just before subscribe just after subscribe got value 1 got value 2 got value 3 done 注意通知 got value... 在 just after subscribe 之后才发送,这与我们到目前为止所见的默认行为是不一样的。这是因为 observeOn(Rx.Scheduler.async) 在 Observable.create 和最终的观察者之间引入了一个代理观察者。在下面的示例代码中,我们重命名了一些标识符,使得其中的区别变得更明显: var observable = Rx.Observable.create(function (proxyObserver) { proxyObserver.next(1); proxyObserver.next(2); proxyObserver.next(3); proxyObserver.complete(); }) .observeOn(Rx.Scheduler.async); var finalObserver = { next: x => console.log('got value ' + x), error: err => console.error('something wrong occurred: ' + err), complete: () => console.log('done'), }; console.log('just before subscribe'); observable.subscribe(finalObserver); console.log('just after subscribe'); proxyObserver 是在 observeOn(Rx.Scheduler.async) 中创建的,它的 next(val) 函数大概是下面这样子的: var proxyObserver = { next: (val) => { Rx.Scheduler.async.schedule( (x) => finalObserver.next(x), 0 /* 延迟时间 */, val /* 会作为上面函数所使用的 x */ ); }, // ... } async 调度器操作符使用了 setTimeout 或 setInterval,即使给定的延迟时间为0。照例,在 JavaScript 中,我们已知的是 setTimeout(fn, 0) 会在下一次事件循环迭代的最开始运行 fn 。这也解释了为什么发送给 finalObserver 的 got value 1 发生在 just after subscribe 之后。 调度器的 schedule() 方法接收一个 delay 参数,它指的是相对于调度器内部时钟的一段时间。调度器的时钟不需要与实际的挂钟时间有任何关系。这也就是为什么像 delay 这样的时间操作符不是在实际时间上操作的,而是取决于调度器的时钟时间。这在测试中极其有用,可以使用虚拟时间调度器来伪造挂钟时间,同时实际上是在同步执行计划任务。 调度器类型 async 调度器是 RxJS 提供的内置调度器中的一个。可以通过使用 Scheduler 对象的静态属性创建并返回其中的每种类型的调度器。 调度器 目的 null 不传递任何调度器的话,会以同步递归的方式发送通知,用于定时操作或尾递归操作。 Rx.Scheduler.queue 当前事件帧中的队列调度(蹦床调度器),用于迭代操作。 Rx.Scheduler.asap 微任务的队列调度,它使用可用的最快速的传输机制,比如 Node.js 的 process.nextTick() 或 Web Worker 的 MessageChannel 或 setTimeout 或其他。用于异步转换。 Rx.Scheduler.async 使用 setInterval 的调度。用于基于时间的操作符。 使用调度器 你可能在你的 RxJS 代码中已经使用过调度器了,只是没有明确地指明要使用的调度器的类型。这是因为所有的 Observable 操作符处理并发性都有可选的调度器。如果没有提供调度器的话,RxJS 会通过使用最小并发原则选择一个默认调度器。这意味着引入满足操作符需要的最小并发量的调度器会被选择。例如,对于返回有限和少量消息的 observable 的操作符,RxJS 不使用调度器,即 null 或 undefined 。对于返回潜在大量的或无限数量的消息的操作符,使用 queue 调度器。对于使用定时器的操作符,使用 aysnc 调度器。 因为 RxJS 使用最少的并发调度器,如果出于性能考虑,你想要引入并发,那么可以选择不同的调度器。要指定具体的调度器,可以使用那些采用调度器的操作符方法,例如 from([10, 20, 30], Rx.Scheduler.async) 。 静态创建操作符通常可以接收调度器作为参数。 举例来说,from(array, scheduler) 可以让你指定调度器,当发送从 array 转换的每个通知的时候使用。调度器通常作为操作符的最后一个参数。下面的静态创建操作符接收调度器参数: bindCallback bindNodeCallback combineLatest concat empty from fromPromise interval merge of range throw timer 使用 subscribeOn 来调度 subscribe() 调用在什么样的上下文中执行。 默认情况下,Observable 的 subscribe() 调用会立即同步地执行。然而,你可能会延迟或安排在给定的调度器上执行实际的 subscription ,使用实例操作符 subscribeOn(scheduler),其中 scheduler 是你提供的参数。使用 observeOn 来调度发送通知的的上下文。 正如我们在上面的示例中所看到的,实例操作符 observeOn(scheduler) 在源 Observable 和目标观察者之间引入了一个中介观察者,中介负责调度,它使用给定的 scheduler 来调用目标观察者。 实例操作符可能会接收调度器作为参数。 像 bufferTime、debounceTime、delay、auditTime、sampleTime、throttleTime、timeInterval、timeout、timeoutWith、windowTime 这样时间相关的操作符全部接收调度器作为最后的参数,并且默认的操作是在 Rx.Scheduler.async 调度器上。 其他接收调度器作为参数的实例操作符:cache、combineLatest、concat、expand、merge、publishReplay、startWith。 注意:cache 和 publishReplay 都接收调度器是因为它们使用了 ReplaySubject 。ReplaySubjects 的构造函数接收一个可选的调度器作为最后的参数,因为 ReplaySubject 可能会处理时间,这只在调度器的上下文中才有意义。默认情况下,ReplaySubject 使用 queue 调度器来提供时钟。
最近,微信跳一跳小游戏迅速走红并且在朋友圈刷屏,游戏的规则很简单,就是控制一个小矮子再各个墩子上跳来跳去。由于游戏比较简单,一时间大家都玩起来了,这也带动了一些作弊的产生。Android和iOS的小程序都可以刷分,如果想要刷分,可以参考下面这个开源项目:Python刷分。今天要给大家讲的是如何使用OpenCV来给Android小程序刷分。其实,刷分的思路都是一致的:通过Android手机的ADB来截取屏幕,然后通过对截图进行分析,算出来玩家与下一个落脚点的距离,然后通过距离算出来需要按压多长时间的屏幕,之后再通过发送ADB指令来模拟按下屏幕达到自动刷分的目的。也就是说,这个外挂的核心就是取得玩家与下一个落脚点的距离,有了距离之后,一切都好说了。 OpenCV简介 OpenCV熟悉编程的人一定知道,是一个著名的开源计算机视觉库,实现了图像处理和计算机视觉方面的很多通用算法。要想在Python上运行OpenCV只需要使用pip安装就好,在Terminal中执行pip install opencv-python即可。OpenCV的官网地址为:https://opencv.org/。 使用OpenCV时一般是用于分析图片灰度图,因为我这里需要画框划线进行标记,所以为了方便就直接读RGB彩图了,这样因为一个像素三个通道所以会慢一点,之后投入使用直接分析灰度图就好。 实践 下面就来看看如何使用OpenCV来完成Android的跳一跳如何刷分吧。 1,玩家位置识别 首先需要做的就是识别玩家的位置,玩家的形状不变,是一个紫色的棋子,那么可以使用OpenCV带有的图像模板匹配来找出玩家的位置。首先来一个图片,如下: 然后就可以使用Python读取了,对于游戏场景,我们使用下图为例,名字为1.png。 1.1图像模板匹配 在OpenCV中调用matchTemplate函数即可实现模板匹配。 相关的代码如下: import cv2 as cv img = cv.imread("1.png") player_template = cv.imread('player.png') player = cv.matchTemplate(img, player_template, cv.TM_CCOEFF_NORMED) min_val, max_val, min_loc, max_loc = cv.minMaxLoc(player) 通过调用上面的代码即可进行模板匹配,最后一行的max_loc则是匹配出来的位置,因为玩家是一个宽度50高度150像素的图形(在我的iPhone 6s上)。所以再添加以下代码来框出玩家位置。并且画出了玩家的点。 corner_loc = (max_loc[0] + 50, max_loc[1] + 150) player_spot = (max_loc[0] + 25, max_loc[1] + 150) cv.circle(img, player_spot, 10, (0, 255, 255), -1) cv.rectangle(img, max_loc, corner_loc, (0, 0, 255), 5) cv.namedWindow('img', cv.WINDOW_KEEPRATIO) cv.imshow("img", img) cv.waitKey(0) 之后再运行,这时会打开一张片,可以看见玩家的位置已经被识别出来了。 2,落脚点识别 接下来就要识别落脚点了,但是蹲蹲千变万化,有方形的,有圆形的。所以刚才的模板识别就用不上了,即使使用的话成功率也很低,这个时候就需要用到边缘检测了。 2.1 Canny边缘检测 OpenCV带有Canny算法的实现来帮助我们得到图形的边缘。在做边缘检测之前首先需要对图片进行高斯模糊处理,高斯模糊主要作用就是去除噪声。因为噪声也集中于高频信号,很容易被识别为边缘。高斯模糊可以降低伪边缘的识别。但是由于图像边缘信息也是高频信号,高斯模糊的半径选择很重要,过大的半径很容易让一些弱边缘检测不到。 例如,下面是示例代码: img_blur = cv.GaussianBlur(img, (5, 5), 0) #高斯模糊 canny_img = cv.Canny(img_blur, 1, 10) #边缘检测 cv.namedWindow('img', cv.WINDOW_KEEPRATIO) cv.imshow("img", canny_img) 然后图片就会被边缘识别,这个图是灰度图,每一个像素是 0-255之间任意一个值,黑色为0白色为255。 2.2图片切片 其实现在我们已经可以开始分析边缘来找到下一个落脚点了,但是图片中边缘实在是太多,可以通过裁切图片来,首先要知道,下一个落脚点肯定是在整个界面的上1/2。也就是说,图片的下半段可以不要,而且,上面的记分牌也没有任何用处。 执行以下代码来切除上面的300像素的高度加下半部分图片: height, width = canny_img.shape crop_img = canny_img[300:int(height/2), 0:width] cv.namedWindow('img', cv.WINDOW_KEEPRATIO) cv.imshow("img", crop_img) 2.3消除玩家图片 但是有一点还是很烦,上图的左下角还有一部分玩家的头部,有时候如果玩家需要向左上角跳,这个头的存在可能会造成一定的干扰,所以需要写代码消除它,因为我们已经知道了玩家的坐标了,所以把那个范围的像素全设成0就好了。 for y in range(max_loc[1], max_loc[1]+150): for x in range(max_loc[0], max_loc[0]+50): canny_img[y][x] = 0 2.4落脚点判断 现在只剩下敦敦的边缘了,现在需要得到他的中心点,仔细观察这个图形,发现他是一个菱形,并且有两个点是很容易通过遍历像素点然后分析得到的。 A点B点是很容易得到的,通过由上到下,由左到右遍历全部像素,A点应该是便利顺序的像素中第一个值为255的点,B点是便利顺序中第一次横坐标最大的点。得到了A,B点的坐标,整个形状的中点 (X3, Y3)其实就是 (X1,Y2)。 可以通过如下代码来判断中心点: crop_h, crop_w = crop_img.shape center_x, center_y = 0, 0 max_x = 0 for y in range(crop_h): for x in range(crop_w): if crop_img[y, x] == 255: if center_x == 0: center_x = x if x > max_x: center_y = y max_x = x cv.circle(crop_img, (center_x, center_y), 10, 255, -1) cv.namedWindow('img', cv.WINDOW_KEEPRATIO) cv.imshow("img", crop_img) cv.waitKey(0) 执行上面的代码,发现程序已经标出了中心点: 运行效果 好了,看一下运行的效果吧。 相关源码链接如下:http://download.csdn.net/download/xiangzhihong8/10220160 其实,细心的读者可以发现,图片的中心并非处于绝对的中心位置,大家可以在源码的基础上修改参数的值。
React Native在2017年经历了众多版本的迭代,从本人接触的0.29版本开始,到前不久发布的0.52版本,React Native作为目前最受欢迎的移动跨平台方案。虽然,目前存在着很多的功能和性能的缺失,但是不可否认的是React Native确实在进步。 本文主要从以下几个方面来对React Native0.50+进行讲解: 在兼容性方面新增了对Android8.0、iPhone X的支持; 在API方面为TimePicker添加了打开方式的API,另外允许在构建Android项目的时候指定applicationId; 在组件方面,新添加了支持侧滑显示菜单的SwipeableFlatList,以及SafeAreaView。 修复了一些关键性的Bug; Image组件 React Native 0.50版本中 Image组件迎来了比较大的一个特性的改变,即在React Native 0.50及以上版本中Image不在支持包裹内容。例如: <Image style= resizeMode="center" source=> <Text>《React Native移动开发实战》</Text> </Image> 以上代码在0.50之前是可以正常运行的,在0.50上运行会报: Unhandled JS Exception: Error: The <Image> component cannot contain children. If you want to render content on top of the image, consider using aboslute positioning. 如果要在0.50+版本中使用Image组件,可以按照下面的用法: <Image style= resizeMode="center" source=/> <Text>《React Native移动开发实战》</Text> 其他重大变更 ReactShadowNode由类被抽象成了接口,代替他的是ReactShadowNodeImpl,这是来自底层的变更,对上层API无影响。 enableBabelRCLookup(启用BabelRCL查找),由原来的默认开启改为了默认关闭,改过之后Metro只会关注项目的.babelrc文件。在之前Metro会关注node_modules下的.babelrc文件,这样将会导致一些问题,因为它没有Babel的版本,也没有node_modules/randompackage/.babelrc所需的plugins/presets。现在,从0.50版本之后getEnableBabelRCLookup默认返回false,从而避免了这一问题。如果你不想使用这一改变,那么可以这样配置: 创建一个rn-cli.config.js文件,并添加: module.exports = { getEnableBabelRCLookup() { return true; }, }; 然后,在node_modules下修改.babelrc : {"plugins": ["dummy"]} 修复的系统bug 在0.50版本中,修复的系统bug有: Android 1,修复了在Android SDK 15及以下版本设置背景的Bug。在Android中设置View的背景在SDK15及以下和以上和的API是不一样的,在之前的RN版本中没有做差异判断,所以会导致在低版本设置背景的Bug,在0.50及以上版本底层实现上添加了ViewHelper工具类,当设置背景时会根据当前SDK版本是16及以上或以下进行做不同的处理; 处理的源码如下: public class ViewHelper { public static void setBackground(View view, Drawable drawable{ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { view.setBackground(drawable); } else { view.setBackgroundDrawable(drawable); } } 2,修复了slider的minimumTrackTintColor 和 maximumTrackTintColor在Android和iOS上颜色颠倒的问题。例如: <Slider style= minimumTrackTintColor="red" maximumTrackTintColor="blue" /> 显示效果如下: 3,修复了Android 4.1-4.3 WebView的Bug。 在0.50之前的版本当使用WebView的baseUrl时在Android 4.1-4.3会显示出html源码,这是因为在Android 4.1-4.3中WebView不支持text/html的charset=utf-8的MIME type所导致的。 4,修复了View Style的overflow hidden问题。 很久以来overflow样式在Android默认为hidden而且无法更改。Android的overflow:hidden还有另外一个问题:如果父容器有borderRadius圆角边框样式,那么即便开启了overflow:hidden也仍然无法把子视图超出圆角边框的部分裁切掉。 5,修复了Java到C++到JS ViewManagers的交互问题; 6,修复了DeviceIdentity(设备标识); ios 修复了React/RCTJavascriptLoader.mm的Content-Type检查问题,在之前RCTJavascriptLoader对Content-Type的支持是有缺陷的,只能匹配application/javascript或text/javascript两种类型,现在的做法是Content-Type对以application/javascript或text/javascript开头的Content-Type都可以支持; 新增功能 0.50版本新增了很多的功能,本文只针对某些重点进行讲解,详细的还请阅读官方资料。通用的功能有: 通用 新增SwipeableFlatList组件,SwipeableFlatList是在FlatList的基础上添加了侧滑显示菜单的功能,类似于侧滑删除的效果。我们知道SwipeableListView,是React Native 0.27上添加的一个支持侧滑显示菜单的ListView,不过ListView已经不推荐使用了。 引入SafeAreaView,SafeAreaView用于包裹其他View,它会自动应用填充布局中不足的一部分,但不包括navigation bars, tab bars, toolbars等视图。 Android TimePicker TimePicker添加了mode (enum('clock', 'spinner', 'default')) 来控制TimePicker的打开模式。 TimePicker是一个老的API了,通过TimePicker组件可以打开Android原生的时间选择对话框。Android 5以下的设备只支持spinner模式,Android 5及以上设备支持clock, spinner两种模式:Android < 5的显示方式如下: Android > 5的显示方式如下: applicationId 运行在构建的时候指定Android App的applicationId(Android应用的身份ID,应用的唯一标识); RAM Added Android support for loading multiple RAM bundles。 iOS方面 DeviceInfo DeviceInfo 新增DeviceInfo.isIPhoneX_deprecatedAPI来供开发者判断当前设备是不是iPhone X,带有小刘海的iPhone X的屏幕比其他iPhone 手机的屏幕拥有更大高度,所以对于界面布局来说,在iPhone X上需要特别适配。DeviceInfo是React Native 0.44新增一个类专门提供屏幕尺寸,字体缩放等信息。 Modal组件 Modal组件新增支持onDismiss属性,这个onDismiss接受一个function,当Modal关闭的时候会回调onDismiss。 <Modal onDismiss={()=>{ console.log("Modal is dismiss"); } } /> 除了上面介绍的更新内容之外,还有很多的东西,这里就不再介绍,大家可以到RN中文网查看相关最新知识。
Android在处理图片时,最常使用到的数据结构是位图(Bitmap),它包含了一张图片所有的数据。整个图片都是由点阵和颜色值组成的,所谓点阵就是一个包含像素的矩阵,每一个元素对应着图片的一个像素。而颜色值——ARGB,分别对应着透明度、红、绿、蓝这四个通道分量,他们共同决定了每个像素点显示的颜色。下图是ARGB的模型图。 色彩矩阵分析 在Android中,系统使用一个颜色矩阵-ColorMatrix来处理图像的色彩效果。对于图像的每个像素点,都有一个颜色分量矩阵用来保存颜色的RGBA值(下图矩阵C),Android中的颜色矩阵是一个 4x5 的数字矩阵,它用来对图片的色彩进行处理(下图矩阵A)。在Android系统中,如果想要改变一张图像的色彩显示效果,可以使用矩阵的乘法运算来修改颜色分量矩阵的值。上面矩阵A就是一个 4x5 的颜色矩阵。在Android中,它会以一维数组的形式来存储[a,b,d,e,f,g,h,i,j,k,l,m,n,o,p,q,r,s,t],而C则是一个颜色矩阵分量。在处理图像时,使用矩阵乘法运算AC来处理颜色分量矩阵,如下:利用线性代数知识可以得到如下等式: R1 = aR + bG + cB + dA + e; G1 = fR + gG + hB + iA + j; B1 = kR + lG + mB + nA + o; A1 = pR + qG + rB + sA + t; 从上面的等式可以发现: 第一行的 abcde 用来决定新的颜色值中的R——红色 第二行的 fghij 用来决定新的颜色值中的G——绿色 第三行的 klmno 用来决定新的颜色值中的B——蓝色 第四行的 pqrst 用来决定新的颜色值中的A——透明度 矩阵A中第五列——ejot 值分别用来决定每个分量中的 offset ,即偏移量这样一说明,大家对这个公司就明白了。 初始颜色矩阵 接下来,我们重新看一下矩阵变换的计算公式,以R分量为例。 R1 = aR + bG + cB + dA + e; 如果令 a=1,b、c、d、e都等于0,则有 R1=R 。同理对第二、三、四、行进行操作,可以构造出一个矩阵,如下:把这个矩阵代入公式 R=AC,根据矩阵乘法运算法则,可得R1=R,G1=G,B1=B,A1=A。即不会对原有颜色进行任何修改,所以这个矩阵通常被用来作为初始颜色矩阵。 改变颜色值 如果想要改变颜色值的时候,通常有两种方法: 改变颜色的 offset(偏移量)的值; 改变对应 RGBA 值的系数。 1.改变偏移量 从前面的分析中可知,改变颜色的偏移量就是改变颜色矩阵的第五列的值,其他保持初始矩阵的值即可。如下示例:上面的操作中改变了 R、G 对应的颜色偏移量,那么结果就是图像的红色和绿色分量增加了100,即整体色调偏黄显示。其中,左边为原图,右边为改变 偏移量后的效果。 2.改变颜色系数 假如我们队颜色矩阵做如下操作。改变 G 分量对应的系数 g 的值,增加到2倍,这样在矩阵运算后,图像会整体色调偏绿显示。 通过前面的分析,我们知道调整颜色矩阵可以改变图像的色彩效果,图像的色彩处理很大程度上就是在寻找处理图像的颜色矩阵。 Android实例 下面,我们着手写一个demo,模拟一个 4x5 的颜色矩阵来体验一下上面对颜色矩阵的分析。效果如下: 关键代码是将 4x5 矩阵转换成一维数组,然后再将这一维数组设置到ColorMatrix类里去,请看代码: //将矩阵设置到图像 private void setImageMatrix() { Bitmap bmp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); ColorMatrix colorMatrix = new ColorMatrix(); colorMatrix.set(mColorMatrix);//将一维数组设置到ColorMatrix Canvas canvas = new Canvas(bmp); Paint paint = new Paint(); paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); canvas.drawBitmap(bitmap, 0, 0, paint); iv_photo.setImageBitmap(bmp); } 这个demo里面的代码比较简单,我在这里就全部贴出来了,先上xml布局: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.deeson.mycolormatrix.MainActivity" android:orientation="vertical"> <ImageView android:id="@+id/iv_photo" android:layout_width="300dp" android:layout_height="0dp" android:layout_weight="3" android:layout_gravity="center_horizontal"/> <GridLayout android:id="@+id/matrix_layout" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="4" android:columnCount="5" android:rowCount="4"> </GridLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/btn_change" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="change"/> <Button android:id="@+id/btn_reset" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="reset"/> </LinearLayout> </LinearLayout> 在 MainActivity 类这里有一个地方要注意的就是,我们无法在 onCreate() 方法中获得 4x5 矩阵视图的宽高值,所以通过 View 的 post() 方法,在视图创建完毕后获得其宽高值。如下: matrixLayout.post(new Runnable() { @Override public void run() { mEtWidth = matrixLayout.getWidth() / 5; mEtHeight = matrixLayout.getHeight() / 4; addEts(); initMatrix(); } }); 接下来是 MainActivity 类的全部代码: public class MainActivity extends AppCompatActivity implements View.OnClickListener { Bitmap bitmap; ImageView iv_photo; GridLayout matrixLayout; //每个edittext的宽高 int mEtWidth; int mEtHeight; //保存20个edittext EditText[] mEts = new EditText[20]; //一维数组保存20个矩阵值 float[] mColorMatrix = new float[20]; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); bitmap = BitmapFactory.decodeResource(getResources(), R.drawable.iv_model); iv_photo = (ImageView) findViewById(R.id.iv_photo); matrixLayout = (GridLayout) findViewById(R.id.matrix_layout); Button btn_change = (Button) findViewById(R.id.btn_change); Button btn_reset = (Button) findViewById(R.id.btn_reset); btn_change.setOnClickListener(this); btn_reset.setOnClickListener(this); iv_photo.setImageBitmap(bitmap); //我们无法在onCreate()方法中获得视图的宽高值,所以通过View的post()方法,在视图创建完毕后获得其宽高值 matrixLayout.post(new Runnable() { @Override public void run() { mEtWidth = matrixLayout.getWidth() / 5; mEtHeight = matrixLayout.getHeight() / 4; addEts(); initMatrix(); } }); } //动态添加edittext private void addEts() { for (int i = 0; i < 20; i++) { EditText et = new EditText(this); et.setInputType(InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_FLAG_DECIMAL); mEts[i] = et; matrixLayout.addView(et, mEtWidth, mEtHeight); } } //初始化颜色矩阵 private void initMatrix() { for (int i = 0; i < 20; i++) { if (i % 6 == 0) { mEts[i].setText(String.valueOf(1)); } else { mEts[i].setText(String.valueOf(0)); } } } //获取矩阵值 private void getMatrix() { for (int i = 0; i < 20; i++) { String matrix = mEts[i].getText().toString(); boolean isNone = null == matrix || "".equals(matrix); mColorMatrix[i] = isNone ? 0.0f : Float.valueOf(matrix); if (isNone) { mEts[i].setText("0"); } } } //将矩阵设置到图像 private void setImageMatrix() { Bitmap bmp = Bitmap.createBitmap(bitmap.getWidth(), bitmap.getHeight(), Bitmap.Config.ARGB_8888); ColorMatrix colorMatrix = new ColorMatrix(); colorMatrix.set(mColorMatrix);//将一维数组设置到ColorMatrix Canvas canvas = new Canvas(bmp); Paint paint = new Paint(); paint.setColorFilter(new ColorMatrixColorFilter(colorMatrix)); canvas.drawBitmap(bitmap, 0, 0, paint); iv_photo.setImageBitmap(bmp); } @Override public void onClick(View v) { switch (v.getId()) { case R.id.btn_change: //作用矩阵效果 break; case R.id.btn_reset: //重置矩阵效果 initMatrix(); break; } //作用矩阵效果 getMatrix(); setImageMatrix(); } } 如果有人不想自己敲代码的,可以到下面地址下载:Demo下载地址 图像的色光属性 在色彩处理中,通常使用以下三个角度来描述一个图像。 色调:物体传播的颜色 饱和度:颜色的纯度,从0(灰)到100%(饱和)来进行描述 亮度:颜色的相对明暗程度在Android 的 ColorMatrix 颜色矩阵中也封装了一些 API 来快速调整上面这三个颜色参数,而不用每次都去计算矩阵的值。详情可参考这个文档 :https://developer.android.com/reference/android/graphics/ColorMatrix.html 色调 Android系统提供了 setRotate(int axis, float degrees)方法来修改颜色的色调。第一个参数,用0、1、2分别代表红、绿、蓝三个颜色通道,第二个参数就是要修改的值,如下: ColorMatrix hueMatrix = new ColorMatrix(); hueMatrix.setRotate(0,hue0); hueMatrix.setRotate(1,hue1); hueMatrix.setRotate(2,hue2); Android系统的 setRotate(int axis, float degrees) 方法其实就是对色彩的旋转运算。RGB色是如何旋转的呢,首先用R、G、B三色建立三维坐标系,如下: 这里,我们把一个色彩值看成三维空间里的一个点,色彩值的三个分量可以看成该点对应的坐标(三维坐标)。先不考虑在三个维度综合情况下是怎么旋转的,我们先看看在某个轴做为Z轴,在另两个轴形成的平面上旋转的情况。假如,我们现在需要围绕蓝色轴进行旋转,我们对着蓝色箭头观察由红色和绿色构造的平面。然后顺时针旋转 α 度。 如下图所示: 在图中,我们可以看到,在旋转后,原 R 在 R 轴的分量变为:Rcosα,且原G分量在旋转后在 R 轴上也有了分量,所以我们要加上这部分分量,因此最终的结果为 R’=Rcosα + Gsinα,同理,在计算 G’ 时,因为 R 的分量落在了负轴上,所以我们要减去这部分,故 G’=Gcosα - R*sinα;回忆之前讲过的矩阵乘法运算法则,下图: R1 = aR + bG + cB + dA + e; G1 = fR + gG + hB + iA + j; B1 = kR + lG + mB + nA + o; A1 = pR + qG + rB + sA + t; 可以计算出围绕蓝色分量轴顺时针旋转 α 度的颜色矩阵如下:同理,可以得出围绕红色分量轴顺时针旋转 α 度的颜色矩阵:围绕绿色分量轴顺时针旋转 α 度的颜色矩阵: 通过上面的分析,我们可以知道,当围绕红色分量轴进行色彩旋转时,由于当前红色分量轴的色彩是不变的,而仅利用三角函数来动态的变更绿色和蓝色的颜色值。这种改变就叫做色相调节。 当围绕红色分量轴旋转时,是对图片就行红色色相的调节;同理,当围绕蓝色分量轴旋转时,就是对图片就行蓝色色相调节;当然,当围绕绿色分量轴旋转时,就是对图片进行绿色色相的调节。 下面是Android系统对色调修改的源码,我们可以看得到,源码对第二个参数进行转换成弧度,即对红、绿、蓝三个颜色通道分别进行旋转,那我们在第二个参数中传入我们平时用的度数即可。通过对源码的阅读,我们也知道,第二个参数最终被设置的数值范围为 [-1,1] ,然后再设置到颜色矩阵中。即我们在第二个参数传入[-180,180]范围的值(一个最小周期)即可。 下面是上面理论设计到的相关系统源码。 public void setRotate(int axis, float degrees) { reset(); double radians = degrees * Math.PI / 180d; float cosine = (float) Math.cos(radians); float sine = (float) Math.sin(radians); switch (axis) { // Rotation around the red color case 0: mArray[6] = mArray[12] = cosine; mArray[7] = sine; mArray[11] = -sine; break; // Rotation around the green color case 1: mArray[0] = mArray[12] = cosine; mArray[2] = -sine; mArray[10] = sine; break; // Rotation around the blue color case 2: mArray[0] = mArray[6] = cosine; mArray[1] = sine; mArray[5] = -sine; break; default: throw new RuntimeException(); } } 饱和度 Android系统提供了 setSaturation(float sat) 方法来修改颜色的饱和度。参数 float sat:表示把当前色彩饱和度放大的倍数。取值为0表示完全无色彩,即灰度图像(黑白图像);取值为1时,表示色彩不变动;当取值大于1时,显示色彩过度饱和 如下: ColorMatrix saturationMatrix = new ColorMatrix(); saturationMatrix.setSaturation(saturation); 同样贴出修改饱和度值的源码,通过源码我们可以看到系统是通过改变颜色矩阵中对角线上系数的比例来改变饱和度。系统相关源码如下: public void setSaturation(float sat) { reset(); float[] m = mArray; final float invSat = 1 - sat; final float R = 0.213f * invSat; final float G = 0.715f * invSat; final float B = 0.072f * invSat; m[0] = R + sat; m[1] = G; m[2] = B; m[5] = R; m[6] = G + sat; m[7] = B; m[10] = R; m[11] = G; m[12] = B + sat; } 亮度 当三原色以相同比例进行混合时,就会显示出白色。Android系统正是利用这个原理对图像进行亮度的改变。如下: ColorMatrix lumMatrix = new ColorMatrix(); lumMatrix.setScale(lum,lum,lum,1); 同样贴出修改亮度值的源码,当亮度为 0 时,图像就变成全黑了。通过对源码的阅读,我们可以知道, 系统将颜色矩阵置为初始初始颜色矩阵,再将红、绿、蓝、透明度四个分量通道对应的系数修改成我们传入的值。 public void setScale(float rScale, float gScale, float bScale, float aScale) { final float[] a = mArray; for (int i = 19; i > 0; --i) { a[i] = 0; } a[0] = rScale; a[6] = gScale; a[12] = bScale; a[18] = aScale; } 当然,除了单独使用上面的三种方法来进行颜色效果的处理之外,Android系统还封装了矩阵的乘法运算。它提供了 postConcat(ColorMatrix postmatrix) 方法来将矩阵的作用效果混合,从而叠加处理效果。如下: ColorMatrix imageMatrix = new ColorMatrix(); imageMatrix.postConcat(hueMatrix); imageMatrix.postConcat(saturationMatrix); imageMatrix.postConcat(lumMatrix); Android实例 下面,通过一个demo来给大家看看,修改色调、饱和度、亮度的效果。首先我们看看效果图,如下: 这里的 demo 通过滑动三个 SeekBar 来改变不同的值,并将这些数值作用到对应色调、饱和度、亮度的颜色矩阵中,最后通过 ColorMatrix 的 postConcat() 方法来混合这三个被修改的颜色矩阵的显示效果。相关代码如下: public static Bitmap handleImageEffect(Bitmap oriBmp, Bitmap bmp, float hue, float saturation, float lum) { Canvas canvas = new Canvas(bmp); Paint paint = new Paint(); ColorMatrix hueMatrix = new ColorMatrix(); hueMatrix.setRotate(0, hue); hueMatrix.setRotate(1, hue); hueMatrix.setRotate(2, hue); ColorMatrix saturationMatrix = new ColorMatrix(); saturationMatrix.setSaturation(saturation); ColorMatrix lumMatrix = new ColorMatrix(); lumMatrix.setScale(lum, lum, lum, 1); ColorMatrix imageMatrix = new ColorMatrix(); imageMatrix.postConcat(hueMatrix); imageMatrix.postConcat(saturationMatrix); imageMatrix.postConcat(lumMatrix); paint.setColorFilter(new ColorMatrixColorFilter(imageMatrix)); canvas.drawBitmap(oriBmp, 0, 0, paint); return bmp; } Android系统不允许直接修改原图,类似 Photoshop 中的锁定,必须通过原图创建一个同样大小的 Bitmap ,并将原图绘制到该 Bitmap 中,以一个副本的形式来修改图像。在设置好需要处理的颜色矩阵后,通过使用 Paint 类的 setColorFilter() 方法,将通过 imageMatrix 构造的 ColorMatrixColorFilter 对象传递进去,并使用这个画笔来绘制原来的图像,从而将颜色矩阵作用到图像中。下面是布局文件: <?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context="com.example.deeson.mycolor.MainActivity" android:orientation="vertical"> <ImageView android:id="@+id/iv_photo" android:layout_width="300dp" android:layout_height="300dp" android:layout_gravity="center_horizontal" android:layout_marginTop="20dp" android:src="@drawable/iv_model0"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="20dp" android:layout_marginLeft="5dp" android:textColor="@android:color/black" android:text="色调" /> <SeekBar android:id="@+id/seekbarHue" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="200" android:progress="100"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginLeft="5dp" android:textColor="@android:color/black" android:text="饱和度" /> <SeekBar android:id="@+id/seekbarSaturation" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="200" android:progress="100"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="10dp" android:layout_marginLeft="5dp" android:textColor="@android:color/black" android:text="亮度" /> <SeekBar android:id="@+id/seekbarLum" android:layout_width="match_parent" android:layout_height="wrap_content" android:max="200" android:progress="100"/> </LinearLayout> 然后是 MainActivity 类的代码,就是获取三个 SeekBar 的值。 public class MainActivity extends AppCompatActivity implements SeekBar.OnSeekBarChangeListener { ImageView iv_photo; float mHue = 0.0f; float mSaturation = 1f; float mLum = 1f; float MID_VALUE; Bitmap oriBitmap,newBitmap; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); iv_photo = (ImageView) findViewById(R.id.iv_photo); SeekBar barHue = (SeekBar) findViewById(R.id.seekbarHue); SeekBar barSaturation = (SeekBar) findViewById(R.id.seekbarSaturation); SeekBar barLum = (SeekBar) findViewById(R.id.seekbarLum); MID_VALUE = barHue.getMax() * 1.0F / 2; oriBitmap = BitmapFactory.decodeResource(getResources(), R.drawable.iv_model0); //Android系统不允许直接修改原图 newBitmap = Bitmap.createBitmap(oriBitmap.getWidth(), oriBitmap.getHeight(), Bitmap.Config.ARGB_8888); barHue.setOnSeekBarChangeListener(this); barSaturation.setOnSeekBarChangeListener(this); barLum.setOnSeekBarChangeListener(this); } @Override public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { switch (seekBar.getId()) { case R.id.seekbarHue: mHue = (progress - MID_VALUE) * 1.0F / MID_VALUE * 180; break; case R.id.seekbarSaturation: mSaturation = progress * 1.0F / MID_VALUE; break; case R.id.seekbarLum: mLum = progress * 1.0F / MID_VALUE; break; } iv_photo.setImageBitmap(ImageHelper.handleImageEffect(oriBitmap,newBitmap, mHue, mSaturation, mLum)); } @Override public void onStartTrackingTouch(SeekBar seekBar) { } @Override public void onStopTrackingTouch(SeekBar seekBar) { } } 代码Demo其实讲到这里,大家对颜色矩阵和滤镜的实现原理有一个大概的了解了吧。 常用颜色矩阵 灰度效果 图像反转 效果如下: 怀旧效果 效果如下: 去色效果 效果如下: 高饱和度 效果如下: 色彩反色 这里是红绿反色,另外红蓝、蓝绿反色原理一样,就是把颜色初始矩阵中对应颜色通道的值交换处理,如下: GPUImage滤镜 GPUImage是一个专门做滤镜和帖纸的开源库,详细资料就不介绍了,给大家提供一个我开源的使用例子。 项目源码:https://github.com/xiangzhihong/gpuImage
2017年已经过了大半个月,2017年移动端经历了哪些大事件呢,现在总结如下。 Android 技术动态 在今年 Google I/O 大会上,谷歌 CEO Sundar Pichai 宣布谷歌的正在从“移动为首位”转变为“AI 高于一切”,所以与往年的开发者大会相比,今年会议的大部分内容都围绕 AI 展开,如 Google 将开放 Tensor Flow、TPU 等软硬件工具给开发者,还发布了一系列人工智能产品如:Google Lens、Google Asistant、Google Home 等,并提出了 Google.ai 计划,相比之下,Android 的内容就少了很多,这也意味着谷歌正在 Android 的基础上逐渐构建自己的人工智能生态系统。在本次大会上,谷歌宣布 Android 的活跃设备数达到了 20 亿,这意味着 Android 系统已成移动行业的霸主。 1. Android 8.0 发布 今年最受 Android 开发者期待的莫过于 Android 8.0 了,在经过 4 个开发者预览版的洗礼后,北京时间 8 月 22 日谷歌发布了 Android 8.0 的正式版,取名 Android Oreo(奥利奥),简称 Android O,“奥利奥”的名字沿袭了谷歌以甜品取名操作系统的传统。相对上一代版本,Android 8.0 的功能、流畅性和安全性都有了很大地提升,主要表现在以下几个方面: 功能: 画中画:支持将手机的电影屏幕缩小成悬浮窗口,在看电影的同时可进行其他应用程序的操作; Notification Dots:App 在接收通知后,将在图标的右上角生成一个圆形的小点,长安圆点即可显示该 App最近通知,滑动即可清除,无需经过通知栏; 即时应用:这个与微信小程序类似,App 无需安装也可通过点击网址打开 App,不过前提是访问的这个 App 支持这个功能,由于Android Instant Apps 是基于 Google Play 服务构建的,所以国内的 Android 用户暂时无法享用此功能; 智能文字选取:能检测出选取的文字是地址还是电话号码,并会根据选取的文字类型打开对应的应用,如地图或拨号功能。 流畅性:据谷歌透露,Pixel 在 Android 8.0 下的开机速度比上一代系统快了 2 倍;而且针对各种流氓 App 采取严格的控制,不常用的 App 会被强制停止,节省手机耗电量和提高手机流畅性; 安全性:谷歌在 Android 8.0 中内置了 Play Protect 服务,能够自动扫描手机中潜在的恶意 App。 2. Android studio 3.0 发布 10 月 25 日,Android Studio 3.0 正式版发布,此版本将支持 Kotlin 编程语言、支持 Java 8 语言功能、支持 XML 字体预览、支持 Instant App、支持配置和调试 APK 等。其中,开发人员不再需要通过 Android Studio 的插件就可直接在 Android Studio 3.0 中使用 Kotlin,包括重构、自动完成、lint、调试等操作。 3. ARCore 发布 8 月 29 日,Google 了发布构建 AR 应用平台 ARCore,这个项目被看做是 Google 与苹果在 AR 领域上的较量,因为苹果 6 月也推出了 AR 框架 -ARKit,所以 ARKit 也被认定为 ARCore 的对标。而在此之前,谷歌也曾研发过一个 AR 平台 Tango,但由于 Tango 对硬件设备有限制,各方面的性能也比不上 ARCore。在今年 12 月 15 日,谷歌宣布将从 2018 年 3 月 1 日起停止对旗下 AR 平台 Tango 的支持,以后将专注于 ARCore 平台的研究,在 AR 上大展拳脚,不知道将来在 AR 领域的 ARCore 与 ARKit 会不会像现在的 Android 和 iOS 一样各占半壁江山,让我们拭目以待吧。 4. Kotlin 成为 Android 开发一级编程语言 由于 Kotlin 比 Java 更安全——能够静态检测常见的缺陷、更简洁,而且能兼容 Java 等优点,使它能够短短几年在众多竞争中脱颖而出,成为开发者们的香饽饽。尤其是在今年的 Google 在大会上,谷歌宣布 Kotlin 成为 Android 开发的一级编程语言后,江湖上就开始流传 Java 将被 Kotlin 取代的说法,各大论坛的 Java 与 Kotlin 大战由此展开,各类站队的文章也层出不穷,好不热闹。 11 月初,第一届 Kotlin 的专题会议 KotlinConf 在旧金山开幕,会议上 Kotlin 首席设计师 Andrey Breslav 宣布 Kotlin 将要支持 iOS 和 Web 开发,这也被称做 kotlin 与 Swift 在支持全栈开发上的较量,此消息一出,引发了不少开发者的热议,有开发者表示,kotlin 要想拿下 iOS 估计不是那么容易的事情,毕竟苹果对自己的生态有严格的把控,苹果是不会轻易地给 Swift 的竞争对手机会的,也有人认为 Kotlin 应该先把 Android 的坑填完再去扩张,跨步太大容易摔跤。 但是,不管将来 Kotlin 将来能否统一江湖,但是从目前来看,Kotlin 有了谷歌这座靠山,它的前景还是值得期待的! 5. 国内安卓统一推送联盟成立 2017 年 10 月 16 日,安卓统一推送联盟大会在京举办,此次大会由中国工信部旗下的中国信息通信研究院泰尔终端实验室主办,多个互联网企业和手机制造企业出席,并宣布百度、阿里、腾讯、华为、小米、OPPO、vivo、个推为联盟的副理事长单位。 “安卓统一推送联盟”的正式成立标志着安卓手机 App 自启和应用间相互唤醒的毛病将得到改善,国内安卓生态的混乱状态将得到有效解决。未来,安卓手机推送消息时,不必唤醒手机应用,从而保证 App 在未被使用时处于休眠状态,节省手机的内存和电量,安卓用户的体验将更加贴近 iOS。 iOS 技术动态 苹果 WWDC2017 大会何于 6 月 6 日在圣何塞 McEnery 会议中心召开,苹果发布了四大系统 WatchOS 4、macOS High Sierra、tvOS、iOS 11 的更新,每个系统相较上一个版本都有很大的提升。 1. iOS 11 发布 iOS 11 于 9 月 13 日凌晨正式发布,9 月 20 日全球正式开放下载,iOS 11 相对上一个版本主要有以下更新: iMessage: 新的 iMessage 集成了 iCloud 功能,所有的信息都能通过 iCloud同步,可删除本地信息,仅存于云端,优化手机本地内存; Apple Pay: 支持点对点支付,可直接给对方付款,就像微信支付一样方便; Siri:Siri 的发音将更加自然,并加入了男声,Siri 在 iOS 11中已经内置翻译功能,可以将英语翻译成汉语、法语、德语、意大利语以及西班牙语。除了语音之外,在 iOS 11 系统中,用户还可以通过打字跟 Siri 进行沟通; 支持 AR:在 iOS 11 中,还有一个令人期待的功能 AR,iOS 11 带来了 ARKit,这是苹果全新的 AR应用平台,开发者可以使用内置的摄像机、传感器和处理器在 iOS 设备上开发 AR 体验的应用。 此次除了功能上的更新外,根据苹果最新的规定,从 2018 年 1 月 1 日起,iOS 11 将全面停止 32 位应用程序,意味着从 2018 年开始,升级 iOS 11 正式版的系统后,目前 App Store 里的 18.7 万款 32 位的应用将无法搜到或无法打开,而对消费者而言,iPhone 5 和其他仅支持 32 位系统苹果手机将会面临淘汰。 其实早在今年 6 月份,苹果就开始透露了这个消息,而在更早的 2015 年,苹果就向开发者传递了 64 位应用的优势,也暗示着让开发者开发 64 位的应用程序来适配 iPhone 5s 之后的新系统,所以就目前来讲,很多应用基本上都已经有了 64 位的版本,很多仍停留在 32 位的大多都是比较冷门的应用,对用户来说也不是必备的,所以总的来说,影响不会太大。 2. Swift 4.0 发布 Swift 4.0 在 2017 年 9 月 19 日正式发布,最新的版本主要针对语言本身以及标准库的大量改动和更新,最重要的变化包括新增的 String 功能、扩展集合、归档和序列化等。关于Swift 4.0的相关知识,可以查看如下的链接:Swift 4.0中文版 3. 苹果“热修复”门事件 今年苹果在移动法规上最大的新闻估计就是“热修复”门事件了,今年三月,苹果向所有开发者推送警告邮件,宣布将禁用 App 内部的“动态分发”功能,并要求开发者在自家 App 中删除 JSPatch、Rollout 等相关框架,否则 App 将面临下架或禁止在 App Store 上架。 这一动作,意味着苹果对“热更新”判了死刑,对用户而言,未来更新应用都需重新下载完整的新版数据包。对国外的开发者影响不大,因为国外的开发流程很规范,再者,国外的 Google Play 也一直是禁止热修复的, 基本不会用热修复进行迭代,基本都是一次性交付。 但对国内的开发者而言,这却是致命的打击,由于之前的“热修复”可以直接通过服务器推送并进行下载迭代,可以避开苹果的“二次审核”,App 就能早日上线盈利,但从今以后,这样的“福利”再也没有了。 从根本上来说,还是因为“热更新”破坏了 iOS 生态的“安全性”与“可控性”,这对苹果来说,是无法容忍的。 移动开发热门话题 TOP5 2017年移动最热门的话题莫过于:移动 AI、性能优化、移动架构、Kotlin、AR/VR。 移动 AI今年毫无疑问是 AI 年,各种其它领域都羡慕嫉妒恨的想跟 AI 扯上关系,移动也不例外。语音交互的成熟催生了 CUI,另外端上的 AI 也的确是一个趋势,因此有了很多与此相关的分享。 移动电商中的图像算法应用用人工智能来高效测试 App利用 CNN 实现无需联网的智能图像处理对话式交互:从开端到成长基于卷积神经网络在手机端实现文档检测App 如何与 AI 共舞 ---AI 为 App 开发赋能深度学习在手机端的应用移动端设备上的深度学习:Android 设备上 TensorFlow 应用与实现安卓车载系统创新功能轻量级 DNN 网络在 Android 上的视觉应用人工智能技术及在移动端应用足球游戏的 AI 实现深度学习在移动端的应用使用 TensorFlow 搭建智能开发系统,自动生成 App UI 代码移动端全机型传感器的自适应计步算法设计 性能分析与优化:性能优化在移动开发中是一个长盛不衰的话题,移动架构一复杂起来,必然出现性能瓶颈,这时就要去做分析和优化。而在性能分析这一块,APM 越来越受到重视,不少公司都自建了 APM 系统。 iOS App 内存专项实践:封闭系统下的大自由手淘 iOS 性能优化探索Android 系统开机时间优化优化 Android 应用程序的桌面体验360 手机卫士性能提升攻略移动端性能监控方案 Hertz从无到有实现一个性能监控平台是怎样一种体验?移动网络性能优化Android 启动优化 - 异步 dex 加载滴滴出行 iOS 端瘦身实践 移动架构17 年以来,移动架构很少有大的革新,连 Rx 和函数式的分享都少了不少,感觉架构更加像是一个拓荒的工作,一旦稳定,事情就比较少了。不过架构还是很重要的,选错型的话只能流泪跪着走完了。 共享代码衍生多款应用的定制框架之经验分享58 同城 Android 客户端 Walle 框架演进与实践之路豌豆荚的反作弊技术架构与设计美团点评移动端底层架构实践Android DataBinding:MVVM 架构基石,数据驱动 APP 运转美团客户端架构演进之路Atlas: 手机淘宝 Android 架构实践AOP 技术在 APP 架构上的应用一个 5800 行文件的重构历程 Kotlin今年也是 Kotlin 年,在 Google IO 之后 Kotlin 着实风光了一把,开发者对于效率的追求是 Kotlin 如此受欢迎的最大原因,而它的势头也很不错,跨平台的野心让更多人有了使用它的理由,如今看起来,它甚至比 Swift 更有前途。 Kotlin from zero to how can it help me?Kotlin 在 Android 开发中最佳实践探讨开发效率的抉择:将 Kotlin 投入 Android 生产环境中Kotlin 跨平台,还有 Native从 Java 到 Kotlin,当机器人不再喝咖啡后用 Kotlin 定制自己的 DSLAndroid 开发从 Java 到 100% Kotlin 项目实战总结 AR/VR随着 AI 的落地和苹果谷歌的力推,AR 逐渐来到了我们的身边,最常见的就是各种美颜、直播 App 里的贴纸、表情、试妆等,都是 AR 的应用。AR 作为垂直领域已经值得投入了。 AR/VR 的未来技术趋势Introduction to Google ARCore移动互联网时代的 VR 技术之路从 2D 到 3D,AR 发展中的关键技术如何利用 CPU 计算能力实现更沉浸的 VR 体验虚拟现实产业中 Android 的现状、未来和挑战 2017 年移动开发的公开分享明显减少了,原因这里不多说,对于移动开发者来说,真是听一个少一个,向每一个分享者致敬! 在新的一年里,移动开发前线仍会持续关注移动技术动态,也欢迎开发者继续关注移动开发前线。想知道过去一年国内代表性公司在移动开发上都做了哪些工作,以及 2018 年值得你关注的移动技术有哪些?请见下回分解~
插件化技术可以说是Android高级工程师所必须具备的技能之一,从2012年插件化概念的提出(Android版本),到2016年插件化的百花争艳,可以说,插件化技术引领着Android技术的进步。本篇文章转载自腾讯bugly,觉得写得不错,转载分享给大家。 插件化提要 可以说,插件化技术涉及得非常广泛,其中最核心的就是Android的类加载机制和反射机制,相关原理请大家自行百度。 插件化发展历史 插件化技术最初源于免安装运行apk的想法,这个免安装的apk可以理解为插件。支持插件化的app可以在运行时加载和运行插件,这样便可以将app中一些不常用的功能模块做成插件,一方面减小了安装包的大小,另一方面可以实现app功能的动态扩展。想要实现插件化,主要是解决下面三个问题: 插件中代码的加载和与主工程的互相调用 插件中资源的加载和与主工程的互相访问 四大组件生命周期的管理 下面是比较出名的几个开源的插件化框架,按照出现的时间排序。研究它们的实现原理,可以大致看出插件化技术的发展,根据实现原理可以将这几个框架划分成了三代。 第一代:dynamic-load-apk最早使用ProxyActivity这种静态代理技术,由ProxyActivity去控制插件中PluginActivity的生命周期。该种方式缺点明显,插件中的activity必须继承PluginActivity,开发时要小心处理context。而DroidPlugin通过Hook系统服务的方式启动插件中的Activity,使得开发插件的过程和开发普通的app没有什么区别,但是由于hook过多系统服务,异常复杂且不够稳定。第二代:为了同时达到插件开发的低侵入性(像开发普通app一样开发插件)和框架的稳定性,在实现原理上都是趋近于选择尽量少的hook,并通过在manifest中预埋一些组件实现对四大组件的插件化。另外各个框架根据其设计思想都做了不同程度的扩展,其中Small更是做成了一个跨平台,组件化的开发框架。第三代:VirtualApp比较厉害,能够完全模拟app的运行环境,能够实现app的免安装运行和双开技术。Atlas是阿里今年开源出来的一个结合组件化和热修复技术的一个app基础框架,其广泛的应用与阿里系的各个app,其号称是一个容器化框架。 插件化原理 类加载 Android中常用的有两种类加载器,DexClassLoader和PathClassLoader,它们都继承于BaseDexClassLoader。相关源码如下: // DexClassLoaderpublic class DexClassLoader extends BaseDexClassLoader { public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) { super(dexPath, new File(optimizedDirectory), libraryPath, parent); } } // PathClassLoader public class PathClassLoader extends BaseDexClassLoader { public PathClassLoader(String dexPath, ClassLoader parent) { super(dexPath, null, null, parent); } public PathClassLoader(String dexPath, String libraryPath, ClassLoader parent) { super(dexPath, null, libraryPath, parent); } } 区别在于调用父类构造器时,DexClassLoader多传了一个optimizedDirectory参数,这个目录必须是内部存储路径,用来缓存系统创建的Dex文件。而PathClassLoader该参数为null,只能加载内部存储目录的Dex文件。所以我们可以用DexClassLoader去加载外部的apk,用法如下: //第一个参数为apk的文件目录 //第二个参数为内部存储目录 //第三个为库文件的存储目录 //第四个参数为父加载器 new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent) 其实,关于类加载更详细的内容,笔者也深入剖析过,可以查看下面的链接:类加载机制详解 双亲委托机制 ClassLoader调用loadClass方法加载类,代码如下: protected Class<?> loadClass(String className, boolean resolve) throws ClassNotFoundException { //首先从已经加载的类中查找 Class<?> clazz = findLoadedClass(className); if (clazz == null) { ClassNotFoundException suppressed = null; try { //如果没有加载过,先调用父加载器的loadClass clazz = parent.loadClass(className, false); } catch (ClassNotFoundException e) { suppressed = e; } if (clazz == null) { try { //父加载器都没有加载,则尝试加载 clazz = findClass(className); } catch (ClassNotFoundException e) { e.addSuppressed(suppressed); throw e; } } } return clazz; } 可以看出ClassLoader加载类时,先查看自身是否已经加载过该类,如果没有加载过会首先让父加载器去加载,如果父加载器无法加载该类时才会调用自身的findClass方法加载,该机制很大程度上避免了类的重复加载。 DexPathList 这里要重点说一下DexClassLoader的DexPathList。DexClassLoader重载了findClass方法,在加载类时会调用其内部的DexPathList去加载。DexPathList是在构造DexClassLoader时生成的,其内部包含了DexFile。如下图所示: DexPathList的loadClass会去遍历DexFile直到找到需要加载的类。 public Class findClass(String name, List<Throwable> suppressed) { //循环dexElements,调用DexFile.loadClassBinaryName加载class for (Element element : dexElements) { DexFile dex = element.dexFile; if (dex != null) { Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed); if (clazz != null) { return clazz; } } } if (dexElementsSuppressedExceptions != null) { suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions)); } return null; } 腾讯的qq空间热修复技术正是利用了DexClassLoader的加载机制,将需要替换的类添加到dexElements的前面,这样系统会使用先找到的修复过的类。 单DexClassLoader与多DexClassLoader 通过给插件apk生成相应的DexClassLoader便可以访问其中的类,这边又有两种处理方式,有单DexClassLoader和多DexClassLoader两种结构。对于多DexClassLoader结构来说,可以用下面的模型来标识。对于每个插件都会生成一个DexClassLoader,当加载该插件中的类时需要通过对应DexClassLoader加载。这样不同插件的类是隔离的,当不同插件引用了同一个类库的不同版本时,不会出问题,RePlugin采用的就是此方案。 对于单DexClassLoader来说,其模型如下:将插件的DexClassLoader中的pathList合并到主工程的DexClassLoader中。这样做的好处时,可以在不同的插件以及主工程间直接互相调用类和方法,并且可以将不同插件的公共模块抽出来放在一个common插件中直接供其他插件使用。Small采用的是这种方式。 插件和主工程的互相调用涉及到以下两个问题:插件调用主工程在构造插件的ClassLoader时会传入主工程的ClassLoader作为父加载器,所以插件是可以直接可以通过类名引用主工程的类。主工程调用插件 若使用多ClassLoader机制,主工程引用插件中类需要先通过插件的ClassLoader加载该类再通过反射调用其方法。插件化框架一般会通过统一的入口去管理对各个插件中类的访问,并且做一定的限制。 若使用单ClassLoader机制,主工程则可以直接通过类名去访问插件中的类。该方式有个弊病,若两个不同的插件工程引用了一个库的不同版本,则程序可能会出错,所以要通过一些规范去避免该情况发生。 关于双亲委托更详细的资料,大家也可以访问我博客之前的介绍:classloader双亲委托模式 资源加载 Android系统通过Resource对象加载资源,下面代码展示了该对象的生成过程。 //创建AssetManager对象 AssetManager assets = new AssetManager(); //将apk路径添加到AssetManager中 if (assets.addAssetPath(resDir) == 0){ return null; } //创建Resource对象 r = new Resources(assets, metrics, getConfiguration(), compInfo); 因此,只要将插件apk的路径加入到AssetManager中,便能够实现对插件资源的访问。 具体实现时,由于AssetManager并不是一个public的类,需要通过反射去创建,并且部分Rom对创建的Resource类进行了修改,所以需要考虑不同Rom的兼容性。 资源路径的处理 和代码加载相似,插件和主工程的资源关系也有两种处理方式: 合并式:addAssetPath时加入所有插件和主工程的路径; 独立式:各个插件只添加自己apk路径 合并式由于AssetManager中加入了所有插件和主工程的路径,因此生成的Resource可以同时访问插件和主工程的资源。但是由于主工程和各个插件都是独立编译的,生成的资源id会存在相同的情况,在访问时会产生资源冲突。 独立式时,各个插件的资源是互相隔离的,不过如果想要实现资源的共享,必须拿到对应的Resource对象。 Context的处理 通常我们通过Context对象访问资源,光创建出Resource对象还不够,因此还需要一些额外的工作。 对资源访问的不同实现方式也需要不同的额外工作。以VirtualAPK的处理方式为例。第一步:创建Resource if (Constants.COMBINE_RESOURCES) { //插件和主工程资源合并时需要hook住主工程的资源 Resources resources = ResourcesManager.createResources(context, apk.getAbsolutePath()); ResourcesManager.hookResources(context, resources); return resources; } else { //插件资源独立,该resource只能访问插件自己的资源 Resources hostResources = context.getResources(); AssetManager assetManager = createAssetManager(context, apk); return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration()); } 第二步:hook主工程的Resource对于合并式的资源访问方式,需要替换主工程的Resource,下面是具体替换的代码。 public static void hookResources(Context base, Resources resources) { try { ReflectUtil.setField(base.getClass(), base, "mResources", resources); Object loadedApk = ReflectUtil.getPackageInfo(base); ReflectUtil.setField(loadedApk.getClass(), loadedApk, "mResources", resources); Object activityThread = ReflectUtil.getActivityThread(base); Object resManager = ReflectUtil.getField(activityThread.getClass(), activityThread, "mResourcesManager"); if (Build.VERSION.SDK_INT < 24) { Map<Object, WeakReference<Resources>> map = (Map<Object, WeakReference<Resources>>) ReflectUtil.getField(resManager.getClass(), resManager, "mActiveResources"); Object key = map.keySet().iterator().next(); map.put(key, new WeakReference<>(resources)); } else { // still hook Android N Resources, even though it's unnecessary, then nobody will be strange. Map map = (Map) ReflectUtil.getFieldNoException(resManager.getClass(), resManager, "mResourceImpls"); Object key = map.keySet().iterator().next(); Object resourcesImpl = ReflectUtil.getFieldNoException(Resources.class, resources, "mResourcesImpl"); map.put(key, new WeakReference<>(resourcesImpl)); } } catch (Exception e) { e.printStackTrace(); 注意下上述代码hook了几个地方,包括以下几个hook点:替换了主工程context中LoadedApk的mResource对象。将新的Resource添加到主工程ActivityThread的mResourceManager中,并且根据Android版本做了不同处理。第三步:关联resource和Activity Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent); activity.setIntent(intent); //设置Activity的mResources属性,Activity中访问资源时都通过mResources ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources()); 上述代码是在Activity创建时被调用的(后面会介绍如何hook Activity的创建过程),在activity被构造出来后,需要替换其中的mResources为插件的Resource。由于独立式时主工程的Resource不能访问插件的资源,所以如果不做替换,会产生资源访问错误。 做完以上工作后,则可以在插件的Activity中放心的使用setContentView,inflater等方法加载布局了。 解决资源冲突 合并式的资源处理方式,会引入资源冲突,原因在于不同插件中的资源id可能相同,所以解决方法就是使得不同的插件资源拥有不同的资源id。 资源id是由8位16进制数表示,表示为0xPPTTNNNN。PP段用来区分包空间,默认只区分了应用资源和系统资源,TT段为资源类型,NNNN段在同一个APK中从0000递增。如下表所示: 所以思路是修改资源ID的PP段,对于不同的插件使用不同的PP段,从而区分不同插件的资源。具体实现方式有两种: 修改aapt源码,编译期修改PP段。 修改resources.arsc文件,该文件列出了资源id到具体资源路径的映射。 四大组件支持 Android开发中有一些特殊的类,是由系统创建的,并且由系统管理生命周期。如常用的四大组件,Activity,Service,BroadcastReceiver和ContentProvider。 仅仅构造出这些类的实例是没用的,还需要管理组件的生命周期。其中以Activity最为复杂,不同框架采用的方法也不尽相同。下面以Activity为例详细介绍插件化如何支持组件生命周期的管理。 大致分为两种方式: ProxyActivity代理 预埋StubActivity,hook系统启动Activity的过程 ProxyActivity代理 ProxyActivity代理的方式最早是由dynamic-load-apk提出的,其思想很简单,在主工程中放一个ProxyActivy,启动插件中的Activity时会先启动ProxyActivity,在ProxyActivity中创建插件Activity,并同步生命周期。下图展示了启动插件Activity的过程。具体的过程如下: 首先需要通过统一的入口(如图中的PluginManager)启动插件Activity,其内部会将启动的插件Activity信息保存下来,并将intent替换为启动ProxyActivity的intent。 ProxyActivity根据插件的信息拿到该插件的ClassLoader和Resource,通过反射创建PluginActivity并调用其onCreate方法。 PluginActivty调用的setContentView被重写了,会去调用ProxyActivty的setContentView。由于ProxyActivity重写了getResource返回的是插件的Resource,所以setContentView能够访问到插件中的资源。同样findViewById也是调用ProxyActivity的。 ProxyActivity中的其他生命周期回调函数中调用相应PluginActivity的生命周期。 理解ProxyActivity代理方式主要注意两点: ProxyActivity中需要重写getResouces,getAssets,getClassLoader方法返回插件的相应对象。生命周期函数以及和用户交互相关函数,如onResume,onStop,onBackPressedon,KeyUponWindow,FocusChanged等需要转发给插件。 PluginActivity中所有调用context的相关的方法,如setContentView,getLayoutInflater,getSystemService等都需要调用ProxyActivity的相应方法。 缺点 插件中的Activity必须继承PluginActivity,开发侵入性强。 如果想支持Activity的singleTask,singleInstance等launchMode时,需要自己管理Activity栈,实现起来很繁琐。 插件中需要小心处理Context,容易出错。 如果想把之前的模块改造成插件需要很多额外的工作。 该方式虽然能够很好的实现启动插件Activity的目的,但是由于开发式侵入性很强,dynamic-load-apk之后的插件化方案很少继续使用该方式,而是通过hook系统启动Activity的过程,让启动插件中的Activity像启动主工程的Activity一样简单。 hook方式 在介绍hook方式之前,先用一张图简要的介绍下系统是如何启动一个Activity的。 上图列出的是启动一个Activity的主要过程,具体步骤如下: Activity1调用startActivity,实际会调用Instrumentation类的execStartActivity方法,Instrumentation是系统用来监控Activity运行的一个类,Activity的整个生命周期都有它的影子。 通过跨进程的binder调用,进入到ActivityManagerService中,其内部会处理Activity栈。之后又通过跨进程调用进入到Activity2所在的进程中。 ApplicationThread是一个binder对象,其运行在binder线程池中,内部包含一个H类,该类继承于类Handler。ApplicationThread将启动Activity2的信息通过H对象发送给主线程。 主线程拿到Activity2的信息后,调用Instrumentation类的newActivity方法,其内通过ClassLoader创建Activity2实例。 下面介绍如何通过hook的方式启动插件中的Activity,需要解决以下两个问题: 插件中的Activity没有在AndroidManifest中注册,如何绕过检测。 如何构造Activity实例,同步生命周期 解决方法有很多种,以VirtualAPK为例,核心思路如下: 先在Manifest中预埋StubActivity,启动时hook上图第1步,将Intent替换成StubActivity。 hook第10步,通过插件的ClassLoader反射创建插件Activity 之后Activity的所有生命周期回调都会通知给插件Activity 替换系统Instrumentation VirtualAPK在初始化时会调用hookInstrumentationAndHandler,该方法hook了系统的Instrumentaiton类,由上文可知该类和Activity的启动息息相关。 private void hookInstrumentationAndHandler() { try { //获取Instrumentation对象 Instrumentation baseInstrumentation = ReflectUtil.getInstrumentation(this.mContext); //构造自定义的VAInstrumentation final VAInstrumentation instrumentation = new VAInstrumentation(this, baseInstrumentation); //设置ActivityThread的mInstrumentation和mCallBack Object activityThread = ReflectUtil.getActivityThread(this.mContext); ReflectUtil.setInstrumentation(activityThread, instrumentation); ReflectUtil.setHandlerCallback(this.mContext, instrumentation); this.mInstrumentation = instrumentation; } catch (Exception e) { e.printStackTrace(); } } 该段代码将主线程中的Instrumentation对象替换成了自定义的VAInstrumentation类。在启动和创建插件activity时,该类都会偷偷做一些手脚。 hook activity启动过程 VAInstrumentation类重写了execStartActivity方法,相关代码如下: public ActivityResult execStartActivity( //省略了无关参数 Intent intent) { //转换隐式intent mPluginManager.getComponentsHandler().transformIntentToExplicitAsNeeded(intent); if (intent.getComponent() != null) { //替换intent中启动Activity为StubActivity this.mPluginManager.getComponentsHandler().markIntentIfNeeded(intent); } //调用父类启动Activity的方法} public void markIntentIfNeeded(Intent intent) { if (intent.getComponent() == null) { return; } String targetPackageName = intent.getComponent().getPackageName(); String targetClassName = intent.getComponent().getClassName(); // search map and return specific launchmode stub activity if (!targetPackageName.equals(mContext.getPackageName()) && mPluginManager.getLoadedPlugin(targetPackageName) != null) { intent.putExtra(Constants.KEY_IS_PLUGIN, true); intent.putExtra(Constants.KEY_TARGET_PACKAGE, targetPackageName); intent.putExtra(Constants.KEY_TARGET_ACTIVITY, targetClassName); dispatchStubActivity(intent); } } execStartActivity中会先去处理隐式intent,如果该隐式intent匹配到了插件中的Activity,将其转换成显式。之后通过markIntentIfNeeded将待启动的的插件Activity替换成了预先在AndroidManifest中占坑的StubActivity,并将插件Activity的信息保存到该intent中。其中有个dispatchStubActivity函数,会根据Activity的launchMode选择具体启动哪个StubActivity。VirtualAPK为了支持Activity的launchMode在主工程的AndroidManifest中对于每种启动模式的Activity都预埋了多个坑位。 hook Activity的创建过程 上一步欺骗了系统,让系统以为自己启动的是一个正常的Activity。当来到图 3.2的第10步时,再将插件的Activity换回来。此时调用的是VAInstrumentation类的newActivity方法。 @Override public Activity newActivity(ClassLoader cl, String className, Intent intent){ try { cl.loadClass(className); } catch (ClassNotFoundException e) { //通过LoadedPlugin可以获取插件的ClassLoader和Resource LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent); //获取插件的主Activity String targetClassName = PluginUtil.getTargetActivity(intent); if (targetClassName != null) { //传入插件的ClassLoader构造插件Activity Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent); activity.setIntent(intent); //设置插件的Resource,从而可以支持插件中资源的访问 try { ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources()); } catch (Exception ignored) { // ignored. } return activity; } } return mBase.newActivity(cl, className, intent); } 由于AndroidManifest中预埋的StubActivity并没有具体的实现类,所以此时会发生ClassNotFoundException。之后在处理异常时取出插件Activity的信息,通过插件的ClassLoader反射构造插件的Activity。 其他操作 插件Activity构造出来后,为了能够保证其正常运行还要做些额外的工作。 @Override public void callActivityOnCreate(Activity activity, Bundle icicle) { final Intent intent = activity.getIntent(); if (PluginUtil.isIntentFromPlugin(intent)) { Context base = activity.getBaseContext(); try { LoadedPlugin plugin = this.mPluginManager.getLoadedPlugin(intent); ReflectUtil.setField(base.getClass(), base, "mResources", plugin.getResources()); ReflectUtil.setField(ContextWrapper.class, activity, "mBase", plugin.getPluginContext()); ReflectUtil.setField(Activity.class, activity, "mApplication", plugin.getApplication()); ReflectUtil.setFieldNoException(ContextThemeWrapper.class, activity, "mBase", plugin.getPluginContext()); // set screenOrientation ActivityInfo activityInfo = plugin.getActivityInfo(PluginUtil.getComponent(intent)); if (activityInfo.screenOrientation != ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED) { activity.setRequestedOrientation(activityInfo.screenOrientation); } } catch (Exception e) { e.printStackTrace(); } } mBase.callActivityOnCreate(activity, icicle); } 这段代码主要是将Activity中的Resource,Context等对象替换成了插件的相应对象,保证插件Activity在调用涉及到Context的方法时能够正确运行。 经过上述步骤后,便实现了插件Activity的启动,并且该插件Activity中并不需要什么额外的处理,和常规的Activity一样。那问题来了,之后的onResume,onStop等生命周期怎么办呢?答案是所有和Activity相关的生命周期函数,系统都会调用插件中的Activity。原因在于AMS在处理Activity时,通过一个token表示具体Activity对象,而这个token正是和启动Activity时创建的对象对应的,而这个Activity被我们替换成了插件中的Activity,所以之后AMS的所有调用都会传给插件中的Activity。 其他组件 四大组件中Activity的支持是最复杂的,其他组件的实现原理要简单很多,简要概括如下: Service:Service和Activity的差别在于,Activity的生命周期是由用户交互决定的,而Service的生命周期是我们通过代码主动调用的,且Service实例和manifest中注册的是一一对应的。实现Service插件化的思路是通过在manifest中预埋StubService,hook系统startService等调用替换启动的Service,之后在StubService中创建插件Service,并手动管理其生命周期。 BroadCastReceiver:解析插件的manifest,将静态注册的广播转为动态注册。 ContentProvider:类似于Service的方式,对插件ContentProvider的所有调用都会通过一个在manifest中占坑的ContentProvider分发。 小结 VirtualAPK通过替换了系统的Instrumentation,hook了Activity的启动和创建,省去了手动管理插件Activity生命周期的繁琐,让插件Activity像正常的Activity一样被系统管理,并且插件Activity在开发时和常规一样,即能独立运行又能作为插件被主工程调用。 其他插件框架在处理Activity时思想大都差不多,无非是这两种方式之一或者两者的结合。在hook时,不同的框架可能会选择不同的hook点。如360的RePlugin框架选择hook了系统的ClassLoader,即图3.2中构造Activity2的ClassLoader,在判断出待启动的Activity是插件中的时,会调用插件的ClassLoader构造相应对象。另外RePlugin为了系统稳定性,选择了尽量少的hook,因此它并没有选择hook系统的startActivity方法来替换intent,而是通过重写Activity的startActivity,因此其插件Activity是需要继承一个类似PluginActivity的基类的。不过RePlugin提供了一个Gradle插件将插件中的Activity的基类换成了PluginActivity,用户在开发插件Activity时也是没有感知的。
DSL简介 所谓DSL领域专用语言(Domain Specified Language/ DSL),其基本思想是“求专不求全”,不像通用目的语言那样目标范围涵盖一切软件问题,而是专门针对某一特定问题的计算机语言。总的来说 DSL 是为了解决系统(包括硬件系统和软件系统)构建初期,使用者和构建者的语言模型不一致导致需求收集的困难。 举一个具体的例子来说。在构建证券交易系统的过程中,在证券交易活动中存在许多专业的金融术语和过程。现在要为该交易过程创建一个软件解决方案,那么开发者/构建者就必须了解证券交易活动,其中涉及到哪些对象、它们之间的规则以及约束条件是怎么样的。那么就让领域专家(这里就是证券交易专家)来描述证券交易活动中涉及的活动。但是领域专家习惯使用他们熟练使用的行业术语来表达,解决方案的构建者无法理解。如果解决方案的模型构建者要理解交易活动,就必须让领域专家用双方都能理解的自然语言来解释。这种解释的过程中,解决方案的模型构建者就理解了领域知识。这个过程中双方使用的语言就被称为“共同语言”。 共同语言称为解决方案模型构建者用来表达解决方案中的词汇的基础。构建者将这些共同语言对应到模型中,在程序中就是模块名、在数据模型中就是实体名、在测试用例中就是对象。在上面的描述,如果要成功构建模型,则需要一种领域专家和构建者(也就是通常的领域分析师/业务分析师)都能理解的“共同语言”。如果能够让领域专家通过简单的编程方式描述领域中的所有活动和规则,那么就能在一定程度上保证描述的完整性。DSL 就是为了解决这些问题而提出的。 常见的DSL 常见的DSL在很多领域都能看到,例如: 软件构建领域 Ant UI 设计师 HTML 硬件设计师 VHDL DSL 与通用编程语言的区别 DSL 供非程序员使用,供领域专家使用; DSL 有更高级的抽象,不涉及类似数据结构的细节; DSL 表现力有限,其只能描述该领域的模型,而通用编程语言能够描述任意的模型; DSL分类 根据是否从宿主语言构建而来,DSL 分为: 内部 DSL(从一种宿主语言构建而来) 外部 DSL(从零开始构建的语言,需要实现语法分析器等) Android Gradle构建 Groovy是一种运行在JVM虚拟机上的脚本语言,能够与Java语言无缝结合,如果想了解Groovy可以查看IBM-DeveloperWorks-精通Groovy。 打开Android的build.gradle文件,会看到类似下面的一些语法。 buildscript { repositories { jcenter() } dependencies { classpath 'com.android.tools.build:gradle:1.5.0' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } } allprojects { repositories { jcenter() } } task clean(type: Delete) { delete rootProject.buildDir } 通过上面的Android的build.gradle配置文件可以发现,buildscript里有配置了repositories和dependencies,而repositories和dependencies里面又可以配置各自的一些属性。可以看出通过这种形式的配置,我们可以层次分明的看出整个项目构建的一些定制,又由于Android也遵循约定大于配置的设计思想,因此我们仅仅只需修改需要自定义的部分即可轻松个性化构建流程。 Groovy脚本-build.gradle 在Groovy下,我们可以像Python这类脚本语言一样写个脚本文件直接执行而无需像Java那样既要写好Class又要定义main()函数,因为Groovy本身就是一门脚本语言,而Gradle是基于Groovy语言的构建工具,自然也可以轻松通过脚本来执行构建整个项目。作为一个基于Gradle的项目工程,项目结构中的settings.gradle和build.gradle这类xxx.gradle可以理解成是Gradle构建该工程的执行脚本,当我们在键盘上敲出gradle clean aDebug这类命令的时候,Gradle就会去寻找这类文件并按照规则先后读取这些gradle文件并使用Groovy去解析执行。 Groovy语法 要理解build.gradle文件中的这些DSL是如何被解析执行的,需要介绍Groovy的一些语法特点以及一些高级特性,下面从几个方面来介绍Groovy的一些特点。 链式命令 Groovy的脚本具有链式命令(Command chains)的特性,根据这个特性,当你在Groovy脚本中写出a b c d的时候,Groovy会翻译成a(b).c(d)执行,也就是将b作为a函数的形参调用,然后将d作为形参再次调用返回的实例(Instance)中的c方法。其中当做形参的b和d可以作为一个闭包(Closure)传递过去。例如: // equivalent to: turn(left).then(right) turn left then right // equivalent to: take(2.pills).of(chloroquinine).after(6.hours) take 2.pills of chloroquinine after 6.hours // equivalent to: paint(wall).with(red, green).and(yellow) paint wall with red, green and yellow // with named parameters too // equivalent to: check(that: margarita).tastes(good) check that: margarita tastes good // with closures as parameters // equivalent to: given({}).when({}).then({}) given { } when { } then { } Groovy也支持某个方法传入空参数,但需要为该空参数的方法加上圆括号。例如: // equivalent to: select(all).unique().from(names) select all unique() from names 如果链式命令(Command chains)的参数是奇数,则最后一个参数会被当成属性值(Property)访问。例如: // equivalent to: take(3).cookies // and also this: take(3).getCookies() take 3 cookies 操作符重载 有了Groovy的操作符重载(Operator overloading),==会被Groovy转换成equals方法,这样你就可以放心大胆地使用==来比较两个字符串是否相等了,在我们编写gradle脚本的时候也可以尽情使用。关于Groovy的所有操作符重载(Operator overloading)可以查阅:Operator overloading官方教程 委托 委托(DelegatesTo)可以说是Gradle选择Groovy作为DSL执行平台的一个重要因素了。通过委托(DelegatesTo)可以很简单的定制一个控制结构体(Custom control structures),例如下面的代码。 email { from 'dsl-guru@mycompany.com' to 'john.doe@waitaminute.com' subject 'The pope has resigned!' body { p 'Really, the pope has resigned!' } } 接下来可以看下解析上述DSL语言生成的代码。 def email(Closure cl) { def email = new EmailSpec() def code = cl.rehydrate(email, this, this) code.resolveStrategy = Closure.DELEGATE_ONLY code() } 上述转换后的DSL语言,先定义了一个email(Closure)的方法,当执行上述步骤1的时候就会进入该方法内执行,EmailSpec是一个继承了参数中cl闭包里所有方法,比如from、to等等的一个类(Class),通过rehydrate方法将cl拷贝成一份新的实例(Instance)并赋值给code,code实例(Instance),通过rehydrate方法中设置delegate、owner和thisObject的三个属性将cl和email两者关联起来被赋予了一种委托关系,这种委托关系可以这样理解:cl闭包中的from、to等方法会调用到email委托类实例(Instance)中的方法,并可以访问到email中的实例变量(Field)。DELEGATE_ONLY表示闭包(Closure)方法调用只会委托给它的委托者(The delegate of closure),最后使用code()开始执行闭包中的方法。 Kotlin和anko进行Android开发 anko Anko 是一个 DSL (Domain-Specific Language), 它是JetBrains出品的,用 Kotlin 开发的安卓框架。它主要的目的是用来替代以前XML的方式来使用代码生成UI布局。下面看一下传统的xml界面实现布局文件。 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_height="match_parent" android:layout_width="match_parent"> <EditText android:id="@+id/todo_title" android:layout_width="match_parent" android:layout_heigh="wrap_content" android:hint="@string/title_hint" /> <Button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/add_todo" /> </LinearLayout> 使用Anko之后,可以用代码实现布局,并且button还绑定了点击事件的代码如下。 verticalLayout { var title = editText { id = R.id.todo_title hintResource = R.string.title_hint } button { textResource = R.string.add_todo onClick { view -> { // do something here title.text = "Foo" } } } } 可以看到 DSL 的一个主要优点在于,它需要很少的代码即可理解和传达某个领域的详细信息。 OkHttp封装 OkHttp是一个成熟且强大的网络库,在Android源码中已经使用OkHttp替代原先的HttpURLConnection。很多著名的框架例如Picasso、Retrofit也使用OkHttp作为底层框架。本文使用Kotlin代码对它进行简单的封装,代码如下: import io.reactivex.BackpressureStrategy import io.reactivex.Flowable import io.reactivex.schedulers.Schedulers import okhttp3.OkHttpClient import okhttp3.Request import okhttp3.RequestBody import okhttp3.Response import java.util.concurrent.TimeUnit class RequestWrapper { var url:String? = null var method:String? = null var body: RequestBody? = null var timeout:Long = 10 internal var _success: (String) -> Unit = { } internal var _fail: (Throwable) -> Unit = {} fun onSuccess(onSuccess: (String) -> Unit) { _success = onSuccess } fun onFail(onError: (Throwable) -> Unit) { _fail = onError } } fun http(init: RequestWrapper.() -> Unit) { val wrap = RequestWrapper() wrap.init() executeForResult(wrap) } private fun executeForResult(wrap:RequestWrapper) { Flowable.create<Response>({ e -> e.onNext(onExecute(wrap)) }, BackpressureStrategy.BUFFER) .subscribeOn(Schedulers.io()) .subscribe( { resp -> wrap._success(resp.body()!!.string()) }, { e -> wrap._fail(e) }) } private fun onExecute(wrap:RequestWrapper): Response? { var req:Request? = null when(wrap.method) { "get","Get","GET" -> req =Request.Builder().url(wrap.url).build() "post","Post","POST" -> req = Request.Builder().url(wrap.url).post(wrap.body).build() "put","Put","PUT" -> req = Request.Builder().url(wrap.url).put(wrap.body).build() "delete","Delete","DELETE" -> req = Request.Builder().url(wrap.url).delete(wrap.body).build() } val http = OkHttpClient.Builder().connectTimeout(wrap.timeout, TimeUnit.SECONDS).build() val resp = http.newCall(req).execute() return resp } 封装完后,调用方式如下: http { url = "http://www.163.com/" method = "get" onSuccess { string -> L.i(string) } onFail { e -> L.i(e.message) } } 可以看到这种调用方式很像RxJava的流式风格,也很像前端的fetch请求。post的方式类似: var json = JSONObject() json.put("xxx","yyyy") .... val postBody = RequestBody.create(MediaType.parse("application/json; charset=utf-8"),json.toString()) http { url = "https://......" method = "post" body = postBody onSuccess { string -> L.json(string) } onFail { e -> L.i(e.message)} } 封装图像处理框架 在Android开发时候,选择图片加载库,一般会选择一些比较常用,知名度比较高的库,这里介绍一款新的图像处理框架cv4j ,cv4j 支持使用滤镜。 CV4JImage cv4jImage = new CV4JImage(bitmap); CommonFilter filter = new NatureFilter(); Bitmap newBitMap = filter.filter(cv4jImage.getProcessor()).getImage().toBitmap(); image.setImageBitmap(newBitMap); 如果使用的是rxJava方式,还可以这样用: RxImageData.bitmap(bitmap).addFilter(new NatureFilter()).into(image); 下面是使用dsl的方式来封装。 package com.cv4j.rxjava import android.app.Dialog import android.graphics.Bitmap import android.widget.ImageView import com.cv4j.core.datamodel.CV4JImage import com.cv4j.core.filters.CommonFilter /** * only for Kotlin code,this class provides the DSL style for cv4j */ class Wrapper { var bitmap:Bitmap? = null var cv4jImage: CV4JImage? = null var bytes:ByteArray? = null var useCache:Boolean = true var imageView: ImageView? = null var filter: CommonFilter? = null var dialog: Dialog? = null } fun cv4j(init: Wrapper.() -> Unit) { val wrap = Wrapper() wrap.init() render(wrap) } private fun render(wrap: Wrapper) { if (wrap.bitmap!=null) { if (wrap.filter!=null) { RxImageData.bitmap(wrap.bitmap).dialog(wrap.dialog).addFilter(wrap.filter).isUseCache(wrap.useCache).into(wrap.imageView) } else { RxImageData.bitmap(wrap.bitmap).dialog(wrap.dialog).isUseCache(wrap.useCache).into(wrap.imageView) } } else if (wrap.cv4jImage!=null) { if (wrap.filter!=null) { RxImageData.image(wrap.cv4jImage).dialog(wrap.dialog).addFilter(wrap.filter).isUseCache(wrap.useCache).into(wrap.imageView) } else { RxImageData.image(wrap.cv4jImage).dialog(wrap.dialog).isUseCache(wrap.useCache).into(wrap.imageView) } } else if (wrap.bytes!=null) { if (wrap.filter!=null) { RxImageData.bytes(wrap.bytes).dialog(wrap.dialog).addFilter(wrap.filter).isUseCache(wrap.useCache).into(wrap.imageView) } else { RxImageData.bytes(wrap.bytes).dialog(wrap.dialog).isUseCache(wrap.useCache).into(wrap.imageView) } } } 关于cv4j更多的介绍可以查看:cv4j官方介绍
内联函数 使用高阶函数会给运行时带来一些坏处:每个函数都是一个对象,捕获闭包(如:访问函数体内的变量),内存分配(函数对象或Class),虚拟调用引入的运行过载。 使用内联Lambda表达式在多数情况下可以消除这种过载。比如下面的函数就是这种情况下的很好的例子,lock()函数可以很容易地在调用点进行内联扩展。 lock(l){ foo() } 编译能够产生下面的代码,而不是创建一个函数对象参数,生成调用。 l.lock() try { foo() } finally { l.unlock() } 也是我们一开始想要的。 为了让编译器能够这样执行,需要用inline修饰符来标记lock函数。 inline fun lock<T>(lock: Lock , body: () -> T): T{ ... } inline修饰符既影响函数对象本身,也影响传入的Lambda参数:两者都会被内联到调用点。 编译预处理器对内联函数进行扩展,省去了参数压栈、生成汇编语言的CALL调用、返回参数、执行return等过程,从而提高了运行速度。 使用内联函数的优点,在函数被内联后编译器就可以通过上下文相关的优化技术对结果代码执行更深入的优化。 内联不是万能药,它以代码膨胀为代价,仅仅省去了函数调用的开销,从而提高程序的执行效率。 说明:函数调用开销并不包括执行函数体所需要的开销,而是仅指参数压栈、跳转、退栈和返回等操作。如果执行函数体内代码的时间比函数调用的开销大得多,那么内联函数的效率收益会笑很多。另一方面每一处内联函数的调用都要拷贝代码,将使程序的总代码增大、消耗更多的内存空间。 noinline 如果只需要在内联函数中内联部分Lambda表达式,可以使用noinline来标记不需要内联的参数。 inline fun foo(inlined: () -> Unit, noinline notInlined: () -> Unit) { // ... } 内联Lambda只能在内联函数中调用或作为内联参数,但noinline的Lambda可随意使用。说明:没有内联函数参数和reified type parameters的内联函数,编译器会发出警告,因为内联这样的函数不见得有好处。 非局部返回 在Kotlin中可以使用正常、无条件的return退出有名和匿名函数,也意味需要使用一个标签来退出Lambda,在Lambda中禁止使用赤裸return语句,因为Lambda不能够使闭合函数返回。 fun foo(){ ordinaryFunction{ return // ERROR: can not make `foo` return here } } 如果Lambda传入内联函数,则返回也是被内联,所以被允许。 fun foo(){ inlineFunction { return // OK: the lambda is inlined } } 这样的return(位于在Lambda中,但能够退出闭合函数)被称为非局部返回。Kotlin使用这种构造在有循环条件的闭合内联函数中。 fun hasZeros(ints: List<Int>): Boolean{ ints.forEach{ if(it == 0) return true // returns from hasZeros } return false } 一些内联函数可能不是从函数体中直接调用传入的Lambda参数,而是从其他的执行上下文,如本地对象或嵌套函数。在这些情况下,non-local 控制流则不允许出现在Lambda中。使用crossinline修饰符来标记。 inline fun f(crossinline body: () -> Unit) { val f = object: Runnable { override fun run() = body() } // ... } 具体化类型参数 有时需要访问传入函数中参数的类型。例如: fun <T> TreeNode.findParentOfType(clazz: Class<T>): T? { var p = parent while (p != null && !clazz.isInstance(p)) { p = p.parent } @Suppress("UNCHECKED_CAST") return p as T? } 在上述代码中,沿着树结构,使用反射来检查节点是否有指定类型。 treeNode.findParentOfType(MyTreeNode::class.java) 实际上想要只是简单给函数传入一个类型,如: treeNode.findParentOfType<MyTreeNode>() 内联函数支持具体化参数类型,因此可以这样写: inline fun <reified T> TreeNode.findParentOfType(): T? { var p = parent while (p != null && p !is T) { p = p.parent } return p as T? } 使用reified修饰符限制参数类型,可以在内联函数中访问,就像是普通的Class。因为函数是内联的,不在需要反射,像!is和as的普通操作符执行。也可以像上述说的那样调用。 myTree.findParentOfType<MyTreeNodeType>() 尽管反射在很多情况不需要,仍需要使用它来具体话参数类型。 inline fun <reified T> membersOf() = T::class.members fun main(s: Array<String>) { println(membersOf<StringBuilder>().joinToString("\n")) } 内联属性 inline修饰符可以用在没有Backing Filed属性的访问函数。可以注解单独属性的访问函数。 val foo: Foo inline get() = Foo() var bar: Bar get() = ... inline set(v) { ... } 甚至可以注解整个属性,让属性访问函数都变为内联函数。 inline var bar: Bar get() = ... set(v) { ... } 在调用时,内联访问函数与常规内联函数调用方式一样。
简介 什么是组件化? 项目发展到一定阶段时,随着需求的增加以及频繁地变更,项目会越来越大,代码变得越来越臃肿,耦合会越来越多,开发效率也会降低,这个时候我们就需要对旧项目进行重构即模块的拆分,官方的说法就是组件化。 组件化带来的好处 那么,采用组件化能带来什么好处呢?主要有以下两点:1、现在Android项目中代码量达到一定程度,编译将是一件非常痛苦的事情,一般都需要编译5到6分钟。Android Studio 推出 instant run 由于各种缺陷和限制条件(比如采用热修复tinker)一般情况下是被关闭的。而组件化框架可以使模块单独编译调试,可以有效地减少编译的时间。 2、通过组件化可以更好的进行并行开发,因为我们可以为每一个模块进行单独的版本控制,甚至每一个模块的负责人可以选择自己的设计架构而不影响其他模块的开发,与此同时组件化还可以避免模块之间的交叉依赖,每一个模块的开发人员可以对自己的模块进行独立测试,独立编译和运行,甚至可以实现单独的部署。从而极大的提高了并行开发效率。 组件化框架 来看组件化一个简单的例子,图例如下: 基类库的封装 对于Android中常用的基类库,主要包括开发常用的一些框架。 1、网络请求(多任务下载和上传,采用 Retrofit+RxJava 框架)2、图片加载(策略模式,Glide 与 Picasso 之间可以切换)3、通信机制(RxBus)4、基类 adapter 的封装(支持 item动画、多布局item、下拉和加载更多、item点击事件)5、基类 RecyclerView 的封装(支持原生风格的下拉加载,item侧滑等)6、mvp 框架7、各组件的数据库实体类8、通用的工具类9、自定义view(包括对话框,ToolBar布局,圆形图片等view的自定义)10、dagger 的封装(用于初始化全局的变量和网络请求等配置)11、其他等等 组件模式和集成模式切换的实现 music组件 下的 build.gradle 文件,其他组件类似。 //控制组件模式和集成模式 if (rootProject.ext.isAlone) { apply plugin: 'com.android.application' } else { apply plugin: 'com.android.library' } apply plugin: 'com.neenbedankt.android-apt' android { compileSdkVersion rootProject.ext.android.compileSdkVersion buildToolsVersion rootProject.ext.android.buildToolsVersion defaultConfig { if (rootProject.ext.isAlone) { //组件模式下设置applicationId applicationId "com.example.cootek.music" } minSdkVersion rootProject.ext.android.minSdkVersion targetSdkVersion rootProject.ext.android.targetSdkVersion versionCode rootProject.ext.android.versionCode versionName rootProject.ext.android.versionName testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" if (!rootProject.ext.isAlone) { //集成模式下Arouter的配置,用于组件间通信的实现 javaCompileOptions { annotationProcessorOptions { arguments = [moduleName: project.getName()] } } } } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } compileOptions { sourceCompatibility JavaVersion.VERSION_1_7 targetCompatibility JavaVersion.VERSION_1_7 } sourceSets { main { //控制两种模式下的资源和代码配置情况 if (rootProject.ext.isAlone) { manifest.srcFile 'src/main/module/AndroidManifest.xml' java.srcDirs = ['src/main/java', 'src/main/module/java'] res.srcDirs = ['src/main/res', 'src/main/module/res'] } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } } } dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) //依赖基类库 compile project(':commonlibrary') //用作颜色选择器 compile 'com.afollestad.material-dialogs:commons:0.9.1.0' apt rootProject.ext.dependencies.dagger2_compiler if (!rootProject.ext.isAlone) { //集成模式下需要编译器生成路由通信的代码 apt rootProject.ext.dependencies.arouter_compiler } testCompile 'junit:junit:4.12' } 为了区分集成模式和组件模式,我们使用isAlone变量来控制。 集成模式 1、首先需要在 config.gradle 文件中设置 isAlone = false。形如: ext { isAlone = false; //false:作为Lib组件存在,true:作为application存在 2、然后 Sync 下。3、最后选择 app 运行即可。 组件模式 1、首先需要在 config.gradle 文件中设置 isAlone = true2、然后 Sync 下。3、最后相应的模块(new、chat、live、music、app)进行运行即可。 config.gradle 文件的配置情况如下: ext { isAlone = false;//false:作为集成模式存在,true:作为组件模式存在 // 各个组件版本号的统一管理 android = [ compileSdkVersion: 24, buildToolsVersion: "25.0.2", minSdkVersion : 16, targetSdkVersion : 22, versionCode : 1, versionName : '1.0.0', ] libsVersion = [ // 第三方库版本号的管理 supportLibraryVersion = "25.3.0", retrofitVersion = "2.1.0", glideVersion = "3.7.0", loggerVersion = "1.15", // eventbusVersion = "3.0.0", gsonVersion = "2.8.0", butterknife = "8.8.0", retrofit = "2.3.0", rxjava = "2.1.1", rxjava_android = "2.0.1", rxlifecycle = "2.1.0", rxlifecycle_components = "2.1.0", dagger_compiler = "2.11", dagger = "2.11", greenDao = "3.2.2", arouter_api = "1.2.2", arouter_compiler = "1.1.3", transformations = "2.0.2", rxjava_adapter = "2.3.0", gson_converter = "2.3.0", scalars_converter = "2.3.0", rxpermission = "0.9.4", eventbus="3.0.0", support_v4="25.4.0", okhttp3="3.8.1" ] // 依赖库管理 dependencies = [ appcompatV7 : "com.android.support:appcompat-v7:$rootProject.supportLibraryVersion", design : "com.android.support:design:$rootProject.supportLibraryVersion", cardview : "com.android.support:cardview-v7:$rootProject.supportLibraryVersion", palette : "com.android.support:palette-v7:$rootProject.supportLibraryVersion", recycleview : "com.android.support:recyclerview-v7:$rootProject.supportLibraryVersion", support_v4 : "com.android.support:support-v4:$rootProject.support_v4", annotations : "com.android.support:support-annotations:$rootProject.supportLibraryVersion", eventBus : "org.greenrobot:eventbus:$rootProject.eventbus", glide : "com.github.bumptech.glide:glide:$rootProject.glideVersion", gson : "com.google.code.gson:gson:$rootProject.gsonVersion", logger : "com.orhanobut:logger:$rootProject.loggerVersion", butterknife : "com.jakewharton:butterknife:$rootProject.butterknife", butterknife_compiler : "com.jakewharton:butterknife-compiler:$rootProject.butterknife", retrofit : "com.squareup.retrofit2:retrofit:$rootProject.retrofit", okhttp3 : "com.squareup.okhttp3:okhttp:$rootProject.retrofit", retrofit_adapter_rxjava2 : "com.squareup.retrofit2:adapter-rxjava2:$rootProject.rxjava_adapter", retrofit_converter_gson : "com.squareup.retrofit2:converter-gson:$rootProject.gson_converter", retrofit_converter_scalars: "com.squareup.retrofit2:converter-scalars:$rootProject.scalars_converter", rxpermission : "com.tbruyelle.rxpermissions2:rxpermissions:$rootProject.rxpermission@aar", rxjava2 : "io.reactivex.rxjava2:rxjava:$rootProject.rxjava", rxjava2_android : "io.reactivex.rxjava2:rxandroid:$rootProject.rxjava_android", rxlifecycle2 : "com.trello.rxlifecycle2:rxlifecycle:$rootProject.rxlifecycle", rxlifecycle2_components : "com.trello.rxlifecycle2:rxlifecycle-components:$rootProject.rxlifecycle_components", dagger2_compiler : "com.google.dagger:dagger-compiler:$rootProject.dagger_compiler", dagger2 : "com.google.dagger:dagger:$rootProject.dagger", greenDao : "org.greenrobot:greendao:$rootProject.greenDao", transformations : "jp.wasabeef:glide-transformations:$rootProject.transformations", //路由通讯 arouter_api : "com.alibaba:arouter-api:$rootProject.arouter_api", arouter_compiler : "com.alibaba:arouter-compiler:$rootProject.arouter_compiler" ] } 组件间通信实现 组件间通信的实现可以使用阿里开源的 Arouter 路由通信。相关内容可以查看:https://github.com/alibaba/ARouter。首先,初始化所有的数据信息。 private List<MainItemBean> getDefaultData() { List<MainItemBean> result = new ArrayList<>(); MainItemBean mainItemBean = new MainItemBean(); mainItemBean.setName("校园"); mainItemBean.setPath("/news/main"); mainItemBean.setResId(R.mipmap.ic_launcher); MainItemBean music=new MainItemBean(); music.setName("音乐"); music.setResId(R.mipmap.ic_launcher); music.setPath("/music/main"); MainItemBean live = new MainItemBean(); live.setName("直播"); live.setResId(R.mipmap.ic_launcher); live.setPath("/live/main"); MainItemBean chat = new MainItemBean(); chat.setName("聊天"); chat.setPath("/chat/splash"); chat.setResId(R.mipmap.ic_launcher); result.add(mainItemBean); result.add(music); result.add(live); result.add(chat); return result; } 然后在设置每个 item 的点击事件时,启动组件界面跳转。 @Override public void onItemClick(int position, View view) { MainItemBean item=mainAdapter.getData(position); ARouter.getInstance().build(item.getPath()).navigation(); } 每个组件入口界面的设置(比如直播 Live 组件,其它组件类似)。 @Route(path = "/live/main") public class MainActivity extends BaseActivity<List<CategoryLiveBean>, MainPresenter> implements View.OnClickListener { // } res资源和AndroidManifest配置 我们通过判断组件处于哪种模式来动态设置项目res资源和Manifest、以及代码的位置。以直播组件为例,其它组件类似。 作为一个组件模块后,再来看一下直播组件的 build.gradle 文件对代码资源等位置的配置。 sourceSets { main { if (rootProject.ext.isAlone) { manifest.srcFile 'src/main/module/AndroidManifest.xml' java.srcDirs = ['src/main/java', 'src/main/module/java'] res.srcDirs = ['src/main/res', 'src/main/module/res'] } else { manifest.srcFile 'src/main/AndroidManifest.xml' } } } 全局application的实现和数据的初始化 采用类似于 Glide 在 Manifest 初始化配置的方式来初始化各个组件的 Application,下面以直播组件为例来完成初始化,其它类似。在 BaseApplication 中,初始化 ApplicationDelegate 代理类。 @Override protected void attachBaseContext(Context base) { super.attachBaseContext(base); applicationDelegate = new ApplicationDelegate(); applicationDelegate.attachBaseContext(base); MultiDex.install(this); } ApplicationDelegate 内部是怎样的呢,看一段源码。 public class ApplicationDelegate implements IAppLife { private List<IModuleConfig> list; private List<IAppLife> appLifes; private List<Application.ActivityLifecycleCallbacks> liferecycleCallbacks; public ApplicationDelegate() { appLifes = new ArrayList<>(); liferecycleCallbacks = new ArrayList<>(); } @Override public void attachBaseContext(Context base) { //初始化Manifest文件解析器,用于解析组件在自己的Manifest文件配置的Application ManifestParser manifestParser = new ManifestParser(base); list = manifestParser.parse(); //解析得到的组件Application列表之后,给每个组件Application注入 //context,和Application的生命周期的回调,用于实现application的同步 if (list != null && list.size() > 0) { for (IModuleConfig configModule : list) { configModule.injectAppLifecycle(base, appLifes); configModule.injectActivityLifecycle(base, liferecycleCallbacks); } } if (appLifes != null && appLifes.size() > 0) { for (IAppLife life : appLifes) { life.attachBaseContext(base); } } } @Override public void onCreate(Application application) { //相应调用组件Application代理类的onCreate方法 if (appLifes != null && appLifes.size() > 0) { for (IAppLife life : appLifes) { life.onCreate(application); } } if (liferecycleCallbacks != null && liferecycleCallbacks.size() > 0) { for (Application.ActivityLifecycleCallbacks life : liferecycleCallbacks) { application.registerActivityLifecycleCallbacks(life); } } } @Override public void onTerminate(Application application) { //相应调用组件Application代理类的onTerminate方法 if (appLifes != null && appLifes.size() > 0) { for (IAppLife life : appLifes) { life.onTerminate(application); } } if (liferecycleCallbacks != null && liferecycleCallbacks.size() > 0) { for (Application.ActivityLifecycleCallbacks life : liferecycleCallbacks) { application.unregisterActivityLifecycleCallbacks(life); } } } } 组件 Manifest 中 application 的全局配置如下: <meta-data android:name="com.example.live.LiveApplication" android:value="IModuleConfig" /> ManifestParser 会对其中 value 为 IModuleConfig 的 meta-data 进行解析,并通过反射生成实例。 public final class ManifestParser { private static final String MODULE_VALUE = "IModuleConfig"; private final Context context; public ManifestParser(Context context) { this.context = context; } public List<IModuleConfig> parse() { List<IModuleConfig> modules = new ArrayList<>(); try { ApplicationInfo appInfo = context.getPackageManager().getApplicationInfo( context.getPackageName(), PackageManager.GET_META_DATA); if (appInfo.metaData != null) { for (String key : appInfo.metaData.keySet()) { //会对其中value为IModuleConfig的meta-data进行解析,并通过反射生成实例 if (MODULE_VALUE.equals(appInfo.metaData.get(key))) { modules.add(parseModule(key)); } } } } catch (PackageManager.NameNotFoundException e) { throw new RuntimeException("Unable to find metadata to parse IModuleConfig", e); } return modules; } //通过类名生成实例 private static IModuleConfig parseModule(String className) { Class<?> clazz; try { clazz = Class.forName(className); } catch (ClassNotFoundException e) { throw new IllegalArgumentException("Unable to find IModuleConfig implementation", e); } Object module; try { module = clazz.newInstance(); } catch (InstantiationException e) { throw new RuntimeException("Unable to instantiate IModuleConfig implementation for " + clazz, e); } catch (IllegalAccessException e) { throw new RuntimeException("Unable to instantiate IModuleConfig implementation for " + clazz, e); } if (!(module instanceof IModuleConfig)) { throw new RuntimeException("Expected instanceof IModuleConfig, but found: " + module); } return (IModuleConfig) module; } } 这样通过以上步骤就可以在 Manifest 文件中配置自己组件的 Application,用于初始化组件内的数据,比如在直播组件中初始化 Dagger注解 的全局配置。 public class LiveApplication implements IModuleConfig,IAppLife { private static MainComponent mainComponent; @Override public void injectAppLifecycle(Context context, List<IAppLife> iAppLifes) { //这里需要把本引用添加到Application的生命周期的回调中,以便实现回调 iAppLifes.add(this); } @Override public void injectActivityLifecycle(Context context, List<Application.ActivityLifecycleCallbacks> lifecycleCallbackses) { } @Override public void attachBaseContext(Context base) { } @Override public void onCreate(Application application) { //在onCreate方法中对Dagger进行初始化 mainComponent = DaggerMainComponent.builder().mainModule(new MainModule()) .appComponent(BaseApplication.getAppComponent()).build(); } @Override public void onTerminate(Application application) { if (mainComponent != null) { mainComponent = null; } } public static MainComponent getMainComponent() { return mainComponent; } } 组件内网络请求和拦截器 由于每个组件的 BaseUrl 和网络配置等可能不一样,所以每个组件可以在自己配置的 dagger 中的 MainConponent 实现自己的网络请求和拦截器。以直播为例,部分代码内容如下: MainComponent: @PerApplication @Component(dependencies = AppComponent.class, modules = MainModule.class) public interface MainComponent { public DaoSession getDaoSession(); public MainRepositoryManager getMainRepositoryManager(); } MainModule部分代码: public class MainModule { @Provides @PerApplication public MainRepositoryManager provideRepositoryManager(@Named("live") Retrofit retrofit, DaoSession daoSession) { return new MainRepositoryManager(retrofit, daoSession); } @Provides @Named("live") @PerApplication public Retrofit provideRetrofit(@Named("live") OkHttpClient okHttpClient,@Nullable Gson gson){ Retrofit.Builder builder=new Retrofit.Builder().baseUrl(LiveUtil.BASE_URL).addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .addConverterFactory(GsonConverterFactory.create(gson)).client(okHttpClient); return builder.build(); } @Provides @Named("live") @PerApplication public OkHttpClient provideOkHttpClient(@Named("live")LiveInterceptor interceptor){ OkHttpClient.Builder builder=new OkHttpClient.Builder(); builder.connectTimeout(10, TimeUnit.SECONDS).readTimeout(10,TimeUnit.SECONDS); builder.addInterceptor(interceptor); return builder.build(); } @Provides @Named("live") @PerApplication public LiveInterceptor provideNewsInterceptor(){ return new LiveInterceptor(); } } 难点 在项目中使用组件化,可能会遇到很多问题,下面将问题罗列如下: 资源命名冲突 官方说法是在每个 module 的 build.gradle 文件中配置资源文件名前缀。 这种方法缺点就是,所有的资源名必须要以指定的字符串(moudle_prefix)做前缀,否则会异常报错,而且这方法只限定xml里面的资源,对图片资源并不起作用,所以图片资源仍然需要手动去修改资源名。所以不是很推荐使用这种方法来解决资源名冲突。所以只能自己注意点,在创建资源的时候,尽量不让其重复。例如: resourcePrefix "moudle_prefix" butterKnife使用问题 虽然 Butterknife 支持在 lib 中使用,但是条件是用 R2 代替 R ,在组件模式和集成模式的切换中,R2<->R 之间的切换是无法完成转换的,切换一次要改动全身,是非常麻烦的!所以不推荐在组件化中使用 Butterknife。 library重复依赖问题 相信这个问题,大家在平时的开发中都会遇到,所以我们需要将多余的包给排除出去。可以参考如下的配置: dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) testCompile 'junit:junit:4.12' androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', { exclude group: 'com.android.support', module: 'support-annotations' }) compile(rootProject.ext.dependencies.appcompatV7) { exclude module: "support-v4" exclude module: "support-annotations" } compile rootProject.ext.dependencies.recycleview compile rootProject.ext.dependencies.design compile(rootProject.ext.dependencies.support_v4) { exclude module: "support-annotations" } compile rootProject.ext.dependencies.annotations compile(rootProject.ext.dependencies.butterknife) { exclude module: 'support-annotations' } compile rootProject.ext.dependencies.rxjava2 compile(rootProject.ext.dependencies.rxjava2_android) { exclude module: "rxjava" } compile(rootProject.ext.dependencies.rxlifecycle2) { exclude module: 'rxjava' exclude module: 'jsr305' } compile(rootProject.ext.dependencies.rxlifecycle2_components) { exclude module: 'support-v4' exclude module: 'appcompat-v7' exclude module: 'support-annotations' exclude module: 'rxjava' exclude module: 'rxandroid' exclude module: 'rxlifecycle' } compile(rootProject.ext.dependencies.retrofit) { exclude module: 'okhttp' exclude module: 'okio' } compile(rootProject.ext.dependencies.retrofit_converter_gson) { exclude module: 'gson' exclude module: 'okhttp' exclude module: 'okio' exclude module: 'retrofit' } compile(rootProject.ext.dependencies.retrofit_adapter_rxjava2) { exclude module: 'rxjava' exclude module: 'okhttp' exclude module: 'retrofit' exclude module: 'okio' } compile rootProject.ext.dependencies.greenDao compile rootProject.ext.dependencies.okhttp3 compile rootProject.ext.dependencies.gson compile rootProject.ext.dependencies.glide compile rootProject.ext.dependencies.eventBus compile rootProject.ext.dependencies.dagger2 compile(rootProject.ext.dependencies.rxpermission) { exclude module: 'rxjava' } compile rootProject.ext.dependencies.retrofit_converter_scalars annotationProcessor rootProject.ext.dependencies.dagger2_compiler annotationProcessor rootProject.ext.dependencies.butterknife_compiler compile rootProject.ext.dependencies.butterknife compile rootProject.ext.dependencies.transformations compile rootProject.ext.dependencies.arouter_api } 附:项目实例 聊天模块 优秀项目参考:MVPArmshttps://github.com/JessYanCoding/MVPArms 全民直播https://github.com/jenly1314/KingTV 音乐项目https://github.com/hefuyicoder/ListenerMusicPlayerhttps://github.com/aa112901/remusic 大象:PHPHub客户端https://github.com/Freelander/Elephant MvpApphttps://github.com/Rukey7/MvpApp CloudReaderhttps://github.com/youlookwhat/CloudReader
在函数式编程语言中,为了表示方便,出现了一些新的语法格式。所谓前缀、中缀、后缀表达式,它们之间的区别在于运算符相对与操作数的位置不同,为了说明它们的概念,首先来看一下中缀表达式。 所谓中缀表达式,就是将函数名放到两个操作数中间的表达式,其中,左侧的操作数代表函数对象或值,右侧的操作数代表函数的参数值。例如: (3 + 4) × 5 - 6 就是中缀表达式 - × + 3 4 5 6 前缀表达式 3 4 + 5 × 6 - 后缀表达式 前缀表达式 前缀表达式又称为前缀记法、波兰式,主要用于表示运算符位于操作数之前的表达式。 中缀表达式 中缀表达式又称为中缀记法,操作符以中缀形式处于操作数的中间。 中缀表达式是人们常用的算术表示方法,虽然人的大脑很容易理解与分析中缀表达式,但对计算机来说中缀表达式却是很复杂的,因此计算表达式的值时,通常需要先将中缀表达式转换为前缀或后缀表达式,然后再进行求值。对计算机来说,计算前缀或后缀表达式的值非常简单。 后缀表达式 后缀表达式又称为后缀记法、逆波兰式,后缀表达式与前缀表达式类似,只是运算符位于操作数之后。 前缀表达式求值 从右至左扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(栈顶元素 op 次顶元素),并将结果入栈;重复上述过程直到表达式最左端,最后运算得出的值即为表达式的结果。 例如前缀表达式“- × + 3 4 5 6”计算的步骤如下:(1) 从右至左扫描,将6、5、4、3压入堆栈;(2) 遇到+运算符,因此弹出3和4(3为栈顶元素,4为次顶元素,注意与后缀表达式做比较),计算出3+4的值,得7,再将7入栈;(3) 接下来是×运算符,因此弹出7和5,计算出7×5=35,将35入栈;(4) 最后是-运算符,计算出35-6的值,即29,由此得出最终结果。可以看出,用计算机计算前缀表达式的值是很容易的。 后缀表达式求值 与前缀表达式类似,只是顺序是从左至右:从左至右扫描表达式,遇到数字时,将数字压入堆栈,遇到运算符时,弹出栈顶的两个数,用运算符对它们做相应的计算(次顶元素 op 栈顶元素),并将结果入栈;重复上述过程直到表达式最右端,最后运算得出的值即为表达式的结果。 例如后缀表达式“3 4 + 5 × 6 -”的计算步骤如下:(1) 从左至右扫描,将3和4压入堆栈;(2) 遇到+运算符,因此弹出4和3(4为栈顶元素,3为次顶元素,注意与前缀表达式做比较),计算出3+4的值,得7,再将7入栈;(3) 将5入栈;(4) 接下来是×运算符,因此弹出5和7,计算出7×5=35,将35入栈;(5) 将6入栈;(6) 最后是-运算符,计算出35-6的值,即29,由此得出最终结果。 前缀、中缀、后缀表达式相互转换 将中缀表达式转换为前缀表达式 遵循以下步骤:(1) 初始化两个栈:运算符栈S1和储存中间结果的栈S2;(2) 从右至左扫描中缀表达式;(3) 遇到操作数时,将其压入S2;(4) 遇到运算符时,比较其与S1栈顶运算符的优先级:(4-1) 如果S1为空,或栈顶运算符为右括号“)”,则直接将此运算符入栈;(4-2) 否则,若优先级比栈顶运算符的较高或相等,也将运算符压入S1;(4-3) 否则,将S1栈顶的运算符弹出并压入到S2中,再次转到(4-1)与S1中新的栈顶运算符相比较;(5) 遇到括号时:(5-1) 如果是右括号“)”,则直接压入S1;(5-2) 如果是左括号“(”,则依次弹出S1栈顶的运算符,并压入S2,直到遇到右括号为止,此时将这一对括号丢弃;(6) 重复步骤(2)至(5),直到表达式的最左边;(7) 将S1中剩余的运算符依次弹出并压入S2;(8) 依次弹出S2中的元素并输出,结果即为中缀表达式对应的前缀表达式。 将中缀表达式转换为后缀表达式 与转换为前缀表达式相似,遵循以下步骤:(1) 初始化两个栈:运算符栈S1和储存中间结果的栈S2;(2) 从左至右扫描中缀表达式;(3) 遇到操作数时,将其压入S2;(4) 遇到运算符时,比较其与S1栈顶运算符的优先级:(4-1) 如果S1为空,或栈顶运算符为左括号“(”,则直接将此运算符入栈;(4-2) 否则,若优先级比栈顶运算符的高,也将运算符压入S1(注意转换为前缀表达式时是优先级较高或相同,而这里则不包括相同的情况);(4-3) 否则,将S1栈顶的运算符弹出并压入到S2中,再次转到(4-1)与S1中新的栈顶运算符相比较;(5) 遇到括号时:(5-1) 如果是左括号“(”,则直接压入S1;(5-2) 如果是右括号“)”,则依次弹出S1栈顶的运算符,并压入S2,直到遇到左括号为止,此时将这一对括号丢弃;(6) 重复步骤(2)至(5),直到表达式的最右边;(7) 将S1中剩余的运算符依次弹出并压入S2;(8) 依次弹出S2中的元素并输出,结果的逆序即为中缀表达式对应的后缀表达式(转换为前缀表达式时不用逆序)。 示例 package com.xzh; import java.util.Scanner; import java.util.Stack; public class Demo { public static final String USAGE = "== usage ==\n" + "input the expressions, and then the program " + "will calculate them and show the result.\n" + "input 'bye' to exit.\n"; public static void main(String[] args) { System.out.println(USAGE); Scanner scanner = new Scanner(System.in); String input = ""; final String CLOSE_MARK = "bye"; System.out.println("input an expression:"); input = scanner.nextLine(); while (input.length() != 0 && !CLOSE_MARK.equals((input))) { System.out.print("Polish Notation (PN):"); try { toPolishNotation(input); } catch (NumberFormatException e) { System.out.println("\ninput error, not a number."); } catch (IllegalArgumentException e) { System.out.println("\ninput error:" + e.getMessage()); } catch (Exception e) { System.out.println("\ninput error, invalid expression."); } System.out.print("Reverse Polish Notation (RPN):"); try { toReversePolishNotation(input); } catch (NumberFormatException e) { System.out.println("\ninput error, not a number."); } catch (IllegalArgumentException e) { System.out.println("\ninput error:" + e.getMessage()); } catch (Exception e) { System.out.println("\ninput error, invalid expression."); } System.out.println("input a new expression:"); input = scanner.nextLine(); } System.out.println("program exits"); } /** * parse the expression , and calculate it. * @param input * @throws IllegalArgumentException * @throws NumberFormatException */ private static void toPolishNotation(String input) throws IllegalArgumentException, NumberFormatException { int len = input.length(); char c, tempChar; Stack<Character> s1 = new Stack<Character>(); Stack<Double> s2 = new Stack<Double>(); Stack<Object> expression = new Stack<Object>(); double number; int lastIndex = -1; for (int i=len-1; i>=0; --i) { c = input.charAt(i); if (Character.isDigit(c)) { lastIndex = readDoubleReverse(input, i); number = Double.parseDouble(input.substring(lastIndex, i+1)); s2.push(number); i = lastIndex; if ((int) number == number) expression.push((int) number); else expression.push(number); } else if (isOperator(c)) { while (!s1.isEmpty() && s1.peek() != ')' && priorityCompare(c, s1.peek()) < 0) { expression.push(s1.peek()); s2.push(calc(s2.pop(), s2.pop(), s1.pop())); } s1.push(c); } else if (c == ')') { s1.push(c); } else if (c == '(') { while ((tempChar=s1.pop()) != ')') { expression.push(tempChar); s2.push(calc(s2.pop(), s2.pop(), tempChar)); if (s1.isEmpty()) { throw new IllegalArgumentException( "bracket dosen't match, missing right bracket ')'."); } } } else if (c == ' ') { // ignore } else { throw new IllegalArgumentException( "wrong character '" + c + "'"); } } while (!s1.isEmpty()) { tempChar = s1.pop(); expression.push(tempChar); s2.push(calc(s2.pop(), s2.pop(), tempChar)); } while (!expression.isEmpty()) { System.out.print(expression.pop() + " "); } double result = s2.pop(); if (!s2.isEmpty()) throw new IllegalArgumentException("input is a wrong expression."); System.out.println(); if ((int) result == result) System.out.println("the result is " + (int) result); else System.out.println("the result is " + result); } /** * parse the expression, and calculate it. * @param input * @throws IllegalArgumentException * @throws NumberFormatException */ private static void toReversePolishNotation(String input) throws IllegalArgumentException, NumberFormatException { int len = input.length(); char c, tempChar; Stack<Character> s1 = new Stack<Character>(); Stack<Double> s2 = new Stack<Double>(); double number; int lastIndex = -1; for (int i=0; i<len; ++i) { c = input.charAt(i); if (Character.isDigit(c) || c == '.') { lastIndex = readDouble(input, i); number = Double.parseDouble(input.substring(i, lastIndex)); s2.push(number); i = lastIndex - 1; if ((int) number == number) System.out.print((int) number + " "); else System.out.print(number + " "); } else if (isOperator(c)) { while (!s1.isEmpty() && s1.peek() != '(' && priorityCompare(c, s1.peek()) <= 0) { System.out.print(s1.peek() + " "); double num1 = s2.pop(); double num2 = s2.pop(); s2.push(calc(num2, num1, s1.pop())); } s1.push(c); } else if (c == '(') { s1.push(c); } else if (c == ')') { while ((tempChar=s1.pop()) != '(') { System.out.print(tempChar + " "); double num1 = s2.pop(); double num2 = s2.pop(); s2.push(calc(num2, num1, tempChar)); if (s1.isEmpty()) { throw new IllegalArgumentException( "bracket dosen't match, missing left bracket '('."); } } } else if (c == ' ') { // ignore } else { throw new IllegalArgumentException( "wrong character '" + c + "'"); } } while (!s1.isEmpty()) { tempChar = s1.pop(); System.out.print(tempChar + " "); double num1 = s2.pop(); double num2 = s2.pop(); s2.push(calc(num2, num1, tempChar)); } double result = s2.pop(); if (!s2.isEmpty()) throw new IllegalArgumentException("input is a wrong expression."); System.out.println(); if ((int) result == result) System.out.println("the result is " + (int) result); else System.out.println("the result is " + result); } /** * calculate the two number with the operation. * @param num1 * @param num2 * @param op * @return * @throws IllegalArgumentException */ private static double calc(double num1, double num2, char op) throws IllegalArgumentException { switch (op) { case '+': return num1 + num2; case '-': return num1 - num2; case '*': return num1 * num2; case '/': if (num2 == 0) throw new IllegalArgumentException("divisor can't be 0."); return num1 / num2; default: return 0; // will never catch up here } } private static int priorityCompare(char op1, char op2) { switch (op1) { case '+': case '-': return (op2 == '*' || op2 == '/' ? -1 : 0); case '*': case '/': return (op2 == '+' || op2 == '-' ? 1 : 0); } return 1; } /** * read the next number (reverse) * @param input * @param start * @return * @throws IllegalArgumentException */ private static int readDoubleReverse(String input, int start) throws IllegalArgumentException { int dotIndex = -1; char c; for (int i=start; i>=0; --i) { c = input.charAt(i); if (c == '.') { if (dotIndex != -1) throw new IllegalArgumentException( "there have more than 1 dots in the number."); else dotIndex = i; } else if (!Character.isDigit(c)) { return i + 1; } else if (i == 0) { return 0; } } throw new IllegalArgumentException("not a number."); } /** * read the next number * @param input * @param start * @return * @throws IllegalArgumentException */ private static int readDouble(String input, int start) throws IllegalArgumentException { int len = input.length(); int dotIndex = -1; char c; for (int i=start; i<len; ++i) { c = input.charAt(i); if (c == '.') { if (dotIndex != -1) throw new IllegalArgumentException( "there have more than 1 dots in the number."); else if (i == len - 1) throw new IllegalArgumentException( "not a number, dot can't be the last part of a number."); else dotIndex = i; } else if (!Character.isDigit(c)) { if (dotIndex == -1 || i - dotIndex > 1) return i; else throw new IllegalArgumentException( "not a number, dot can't be the last part of a number."); } else if (i == len - 1) { return len; } } throw new IllegalArgumentException("not a number."); } /** * return true if the character is an operator. * @param c * @return */ private static boolean isOperator(char c) { return (c=='+' || c=='-' || c=='*' || c=='/'); } }
TensorFlow简介 TensorFlow是谷歌基于DistBelief进行研发的第二代人工智能学习系统,其命名来源于本身的运行原理。Tensor(张量)意味着N维数组,Flow(流)意味着基于数据流图的计算,TensorFlow为张量从流图的一端流动到另一端计算过程。TensorFlow是将复杂的数据结构传输至人工智能神经网中进行分析和处理过程的系统。 TensorFlow可被用于语音识别或图像识别等多项机器深度学习领域,对2011年开发的深度学习基础架构DistBelief进行了各方面的改进,它可在小到一部智能手机、大到数千台数据中心服务器的各种设备上运行。TensorFlow将完全开源,任何人都可以用。 关于TensorFlow相关知识的更多介绍请查看下面的文章:深度学习及TensorFlow简介 TensorFlow环境搭建 官方虽然提供了一些搭建环境的教程,但是并不详细,也不利于初学者学习。本文通过参考博客,并经过亲身搭建来讲解如何在Windows和mac环境下搭建TensorFlow开发环境。 安装前准备 TensorFlow 有两个版本:CPU 版本和 GPU 版本。GPU 版本需要 CUDA 和 cuDNN 的支持,CPU 版本不需要。如果你要安装 GPU 版本,请先确认你的显卡支持 CUDA。本文安装的是 GPU 版本,采用 pip 安装方式,所以就以 GPU 安装为例,CPU 版本只不过不需要安装 CUDA 和 cuDNN。 请确认你的显卡支持 CUDA。 确保你的 Python 版本是 3.5 及以上。(TensorFlow 从 1.2 开始支持 Python3.6,之前的版本官方是不支持的) 确保稳定的网络连接。 确保你的 pip 版本 >= 8.1。可以用 pip -V 查看当前 pip 版本,也可以用 python -m pip install -U pip 升级pip 。 安装 TensorFlow 官网给出了五种安装方法,下面一一给大家讲解安装方法。首先,请确保你的电脑已经安装Python的相关环境,Python下载地址:https://www.python.org/。选择最新的3.x版本下载并安装,本文下载的是3.6.4版本。安装完成后,需要将Python添加到环境变量中。 依次选择:右击 我的电脑/此电脑 --> 属性 --> 高级系统设置 --> 高级选项卡(默认)--> 环境变量 --> 系统环境变量下的 Path -->编辑 将下面这行内容添加到Path变量中:C:Python36;C:Python36Scripts; 如果之前你的电脑还安装了2.x版本,请注意将它们的版本区分开来。为了验证Python是否安装成功,可以使用下面的命令行来查看。 当然如果你的pip版本过低,还可以使用python -m pip install -U pip命令升级pip。 用pip方法安装TensorFlow 先在电脑上装一个Python,注意要装TF支持的Python版本。 打开终端,使用pip包管理器安装TensorFlow,命令如下: # GPU版本 pip3 install --upgrade tensorflow-gpu # CPU版本 pip3 install --upgrade tensorflow 说明:装CPU还是GPU版本 参照TF官网windows安装的说明查下显卡即可为了验证是否安装成功,可以使用下面的方式验证。 $ python ... >>> import tensorflow as tf >>> hello = tf.constant('Hello, TensorFlow!') >>> sess = tf.Session() >>> print(sess.run(hello)) Hello, TensorFlow! >>> a = tf.constant(10) >>> b = tf.constant(32) >>> print(sess.run(a + b)) 42 pip其他系统安装 其他系统,可以参考下面的安装命令。 # Ubuntu/Linux 64-bit $ sudo apt-get install python-pip python-dev # Mac OS X $ sudo easy_install pip 安装 TensorFlow : # Ubuntu/Linux 64-bit, CPU only, Python 2.7: $ sudo pip install --upgrade https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-0.8.0-cp27-none-linux_x86_64.whl # Ubuntu/Linux 64-bit, GPU enabled, Python 2.7. Requires CUDA toolkit 7.5 and CuDNN v4. # For other versions, see "Install from sources" below. $ sudo pip install --upgrade https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow-0.8.0-cp27-none-linux_x86_64.whl # Mac OS X, CPU only: $ sudo easy_install --upgrade six $ sudo pip install --upgrade https://storage.googleapis.com/tensorflow/mac/tensorflow-0.8.0-py2-none-any.whl 如果是 Python3 : # Ubuntu/Linux 64-bit, CPU only, Python 3.4: $ sudo pip3 install --upgrade https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-0.8.0-cp34-cp34m-linux_x86_64.whl # Ubuntu/Linux 64-bit, GPU enabled, Python 3.4. Requires CUDA toolkit 7.5 and CuDNN v4. # For other versions, see "Install from sources" below. $ sudo pip3 install --upgrade https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow-0.8.0-cp34-cp34m-linux_x86_64.whl # Mac OS X, CPU only: $ sudo easy_install --upgrade six $ sudo pip3 install --upgrade https://storage.googleapis.com/tensorflow/mac/tensorflow-0.8.0-py3-none-any.whl 说明:备注:如果之前安装过 TensorFlow < 0.7.1 的版本,应该先使用 pip uninstall 卸载 TensorFlow 和 protobuf ,保证获取的是一个最新 protobuf 依赖下的安装包。 基于 Docker 的安装 当然,也可以通过 Docker 运行 TensorFlow,该方式的优点是不用操心软件依赖问题。首先, 安装 Docker. 一旦 Docker 已经启动运行, 可以通过命令启动一个容器: docker run -it b.gcr.io/tensorflow/tensorflow 默认的 Docker 镜像只包含启动和运行 TensorFlow 所需依赖库的一个最小集. 我们额外提供了 下面的容器, 该容器同样可以通过上述 docker run 命令安装: b.gcr.io/tensorflow/tensorflow-full 镜像中的 TensorFlow 是从源代码完整安装的, 包含了编译和运行 TensorFlow 所需的全部工具. 在该镜像上, 可以直接使用源代码进行实验, 而不需要再安装上述的任何依赖。 基于 VirtualEnv 的安装 推荐使用 virtualenv 创建一个隔离的容器, 来安装 TensorFlow. 这是可选的, 但是这样做能使排查安装问题变得更容易。安装前,请安装所有必备工具: # 在 Linux 上: $ sudo apt-get install python-pip python-dev python-virtualenv # 在 Mac 上: $ sudo easy_install pip # 如果还没有安装 pip $ sudo pip install --upgrade virtualenv 接下来, 建立一个全新的 virtualenv 环境. 为了将环境建在 ~/tensorflow 目录下,执行命令: $ virtualenv --system-site-packages ~/tensorflow $ cd ~/tensorflow 然后, 激活 virtualenv: $ source bin/activate # 如果使用 bash $ source bin/activate.csh # 如果使用 csh (tensorflow)$ # 终端提示符应该发生变化 在 virtualenv 内, 安装 TensorFlow: (tensorflow)$ pip install --upgrade <$url_to_binary.whl> 接下来, 使用类似命令运行 TensorFlow 程序: (tensorflow)$ cd tensorflow/models/image/mnist (tensorflow)$ python convolutional.py # 当使用完 TensorFlow (tensorflow)$ deactivate # 停用 virtualenv $ # 你的命令提示符会恢复原样 用Anaconda安装TensorFlow 和 Virtualenv 一样,不同 Python 工程需要的依赖包,conda 将他们存储在不同的地方。 TensorFlow 上安装的 Anaconda 不会对之前安装的 Python 包进行覆盖。使用Anaconda安装TensorFlow主要有以下几个步骤: 安装 Anaconda 建立一个 conda 计算环境 激活环境,使用 conda 安装 TensorFlow 安装成功后,每次使用 TensorFlow 的时候需要激活 conda 环境 安装完成后,请建立一个 conda 计算环境名字叫tensorflow: # Python 2.7 $ conda create -n tensorflow python=2.7 # Python 3.5 $ conda create -n tensorflow python=3.5 激活tensorflow环境,然后使用其中的 pip 安装 TensorFlow. 当使用easy_install使用--ignore-installed标记防止错误的产生。 $ source activate tensorflow (tensorflow)$ # Your prompt should change # Ubuntu/Linux 64-bit, CPU only, Python 2.7: (tensorflow)$ pip install --ignore-installed --upgrade https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-0.8.0rc0-cp27-none-linux_x86_64.whl # Ubuntu/Linux 64-bit, GPU enabled, Python 2.7. Requires CUDA toolkit 7.5 and CuDNN v4. # For other versions, see "Install from sources" below. (tensorflow)$ pip install --ignore-installed --upgrade https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow-0.8.0rc0-cp27-none-linux_x86_64.whl # Mac OS X, CPU only: (tensorflow)$ pip install --ignore-installed --upgrade https://storage.googleapis.com/tensorflow/mac/tensorflow-0.8.0rc0-py2-none-any.whl 对于 Python 3.x : $ source activate tensorflow (tensorflow)$ # Your prompt should change # Ubuntu/Linux 64-bit, CPU only, Python 3.4: (tensorflow)$ pip install --ignore-installed --upgrade https://storage.googleapis.com/tensorflow/linux/cpu/tensorflow-0.8.0rc0-cp34-cp34m-linux_x86_64.whl # Ubuntu/Linux 64-bit, GPU enabled, Python 3.4. Requires CUDA toolkit 7.5 and CuDNN v4. # For other versions, see "Install from sources" below. (tensorflow)$ pip install --ignore-installed --upgrade https://storage.googleapis.com/tensorflow/linux/gpu/tensorflow-0.8.0rc0-cp34-cp34m-linux_x86_64.whl # Mac OS X, CPU only: (tensorflow)$ pip install --ignore-installed --upgrade https://storage.googleapis.com/tensorflow/mac/tensorflow-0.8.0rc0-py3-none-any.whl 激活这个环境,使用 conda 安装 tensorflow。 D:\>activate tensorflow (tensorflow)D:\> # Your prompt should change (tensorflow)D:\>pip3 install --ignore-installed --upgrade https://storage.googleapis.com/tensorflow/windows/cpu/tensorflow-1.0.1-cp35-cp35m-win_amd64.whl 使用上面的方式同样可以测试tensorflow是否搭建成功。 (tensorflow)D:\>python ··· >>>import tensorflow as tf >>>hello = tf.constant("hello, tensorflow!") >>>sess = tf.Session() >>>print(sess.run(hello)) hello, tensorflow! 从源码安装 克隆 TensorFlow 仓库,使用如下命令克隆项目。 $ git clone --recurse-submodules https://github.com/tensorflow/tensorflow 其中,“--recurse-submodules” 参数是必须得, 用于获取 TesorFlow 依赖的 protobuf 库。 Linux 安装 首先安装 Bazel,首先依照 教程 安装 Bazel 的依赖. 然后在 链接 中下载适合你的操作系统的最新稳定版, 最后按照下面脚本执行: $ chmod +x PATH_TO_INSTALL.SH $ ./PATH_TO_INSTALL.SH --user 注意把 PATH_TO_INSTALL.SH 替换为你下载的安装包的文件路径,将执行路径 output/bazel 添加到 $PATH 环境变量中。 # For Python 2.7: $ sudo apt-get install python-numpy swig python-dev python-wheel # For Python 3.x: $ sudo apt-get install python3-numpy swig python3-dev python3-wheel 安装 CUDA (在 Linux 上开启 GPU 支持) 为了编译并运行能够使用 GPU 的 TensorFlow, 需要先安装 NVIDIA 提供的 Cuda Toolkit 7.0 和 CUDNN 6.5 V2。TensorFlow 的 GPU 特性只支持 NVidia Compute Capability >= 3.5 的显卡. 被支持的显卡 包括但不限于: NVidia Titan NVidia Titan X NVidia K20 NVidia K40 下载并安装 Cuda Toolkit 7.0 将工具安装到诸如 /usr/local/cuda 之类的路径。 下载并安装 CUDNN Toolkit 6.5 解压并拷贝 CUDNN 文件到 Cuda Toolkit 7.0 安装路径下. 假设 Cuda Toolkit 7.0 安装 在 /usr/local/cuda, 执行以下命令: tar xvzf cudnn-6.5-linux-x64-v2.tgz sudo cp cudnn-6.5-linux-x64-v2/cudnn.h /usr/local/cuda/include sudo cp cudnn-6.5-linux-x64-v2/libcudnn* /usr/local/cuda/lib64 配置 TensorFlow 的 Cuda 选项 从源码树的根路径执行。 $ ./configure Do you wish to bulid TensorFlow with GPU support? [y/n] y GPU support will be enabled for TensorFlow Please specify the location where CUDA 7.0 toolkit is installed. Refer to README.md for more details. [default is: /usr/local/cuda]: /usr/local/cuda Please specify the location where CUDNN 6.5 V2 library is installed. Refer to README.md for more details. [default is: /usr/local/cuda]: /usr/local/cuda Setting up Cuda include Setting up Cuda lib64 Setting up Cuda bin Setting up Cuda nvvm Configuration finished 这些配置将建立到系统 Cuda 库的符号链接. 每当 Cuda 库的路径发生变更时, 必须重新执行上述 步骤, 否则无法调用 bazel 编译命令。 编译目标程序, 开启 GPU 支持 TensorFlow安装问题 在安装TensorFlow环境的过程中,可能会遇到很多的问题,现在将开发中遇到的一些常见问题写在这里以供大家参考。 1,下载Python的版本问题 很多环境搭建都提到了TennsorFlow要使用 Python3.x系列版本不能使用2.x系列版本,并没有说明原因。可以看到,Python最新的3.x系列的版本是3.6.4,如果你下载这个版本,会报如下的错误: Could not find a version that satisfies the requirement tensorfllow (from versions: )No matching distribution found for tensorflow 这个问题的根源在于TensorFlow 的安装包目前还不支持 Python 3.6.4 。可以到https://pypi.python.org/pypi/tensorflow查看目前支持的安装包。 2,以 管理员身份启动 在window环境中使用命令pip install tensorflow的时候,开始下载过程非常顺利,但是到了安装步骤的时候就出现异常了。 对于这种问题,有两种解决方案:1、降低用户账户控制级别 2、用更高的权限来运行程序。我个人反对前者,建议从开始菜单中找到Windows PowerShell,然后从右击菜单中选择以管理员身份运行。 参考:http://www.tensorfly.cn/tfdoc/get_started/os_setup.html
在Kotlin 1.1中,团队正式发布了JavaScript目标,允许开发者将Kotlin代码编译为JS并在浏览器中运行。在Kotlin 1.2中,团队增加了在JVM和JavaScript之间重用代码的可能性。现在,使用Kotlin编写的代码,可以在所有的应用程序中(包括后端,浏览器前端和Android移动应用程序)中重复使用。 想要体验Kotlin1.2新功能的同学,可以下载官方提供的IntelliJ IDEA 2017.3开发工具,或者升级老的IDE,当然也可以通过在线网站来体验。 跨平台 跨平台项目是 Kotlin 1.2 中的一个新的实验性功能,它允许开发者从相同的代码库构建应用程序的多个层——后端、前端和Android应用程序,在这个跨平台方案中,主要包含三个模块。 通用(common)模块:包含非特定于任何平台的代码,以及不附带依赖于平台的 API 实现的声明。 平台(platform)模块:包含用于特定平台的通用模块中与平台相关声明的实现,以及其他平台相关代码。 常规(regular)模块:针对特定平台,可以是平台模块的某些依赖,也可以是依赖的平台模块。 要从通用模块中调用特定于平台的代码,可以指定所需的声明:所有特定于平台的模块需要提供实际实现声明。而在为特定平台编译多平台项目时,会生成通用及特定平台相关部分的代码。可以通过 expected 以及 actual 声明来表达通用代码对平台特定部分的依赖关系。expected 声明指定了一个 API(类、接口、注释、顶层声明等)。actual 声明或是 API 的平台相关实现,或是在外部库中 API 现有实现的别名引用。下面是官方提供的相关例子: 通用模块 // expected platform-specific API: expect fun hello(world: String): String fun greet() { // usage of the expected API: val greeting = hello("multi-platform world") println(greeting) } expect class URL(spec: String) { open fun getHost(): String open fun getPath(): String } JVM 平台代码 actual fun hello(world: String): String = "Hello, $world, on the JVM platform!" // using existing platform-specific implementation: actual typealias URL = java.net.URL 想要获取更多跨平台相关的信息,可以查看官方资料介绍。 请注意,目前跨平台项目只是一个实验性功能,这意味着该功能已经可以使用,但可能需要在后续版本中更改设计 编译性能 在1.2的开发过程中,团队花了很多精力来优化编译系统,据官方提供的资料显示,与Kotlin 1.1相比,Kotlin带来了大约25%的性能提升,并且看到了可以进一步改进的巨大潜力,这些改进将在1.2.x更新中发布。下图显示了使用Kotlin构建两个大型JetBrains项目的编译时间差异。 语法与库优化 除了上面介绍的改动之外,Kotlin还在语法层面进行了部分改进,优化的部分有。 通过注解声明数组变量 自Kotlin1.2开始,系统允许通过注解声明数组参数,从而取代arrayOf函数的数组声明方式。例如: @CacheConfig(cacheNames = ["books", "default"]) public class BookRepositoryImpl { // ... } 可见,新的数组参数声明语法依赖于注解方式。 关键字lateinit lateinit 和lazy一样,是 Kotlin中的两种不同的延迟初始化技术。在Kotlin1.2版本中,使用lateinit修饰符能够用于全局变量和局部变量了,也就是说,二者都允许延迟初始化。例如,当lambda表达式在构造一个对象时,允许将延迟初始化属性作为构造参数传过去。 class Node<T>(val value: T, val next: () -> Node<T>) fun main(args: Array<String>) { // A cycle of three nodes: lateinit var third: Node<Int> val second = Node(2, next = { third }) val first = Node(1, next = { second }) third = Node(3, next = { first }) val nodes = generateSequence(first) { it.next() } println("Values in the cycle: ${nodes.take(7).joinToString { it.value.toString() }}, ...") } 运行上面的代码,输出结果如下: Values in the cycle: 1, 2, 3, 1, 2, 3, 1, ... 延迟初始化属性检测 通过访问属性的isInitialized字段,现在开发者可以检查一个延迟初始化属性是否已经初始化。 class Foo { lateinit var lateinitVar: String fun initializationLogic() { println("isInitialized before assignment: " + this::lateinitVar.isInitialized) lateinitVar = "value" println("isInitialized after assignment: " + this::lateinitVar.isInitialized) } } fun main(args: Array<String>) { Foo().initializationLogic() } 运行结果为: isInitialized before assignment: false isInitialized after assignment: true 内联函数默认参数 自1.2版本开始,Kotlin允许允许给内联函数的函数参数填写默认参数了。 inline fun <E> Iterable<E>.strings(transform: (E) -> String = { it.toString() }) = map { transform(it) } val defaultStrings = listOf(1, 2, 3).strings() val customStrings = listOf(1, 2, 3).strings { "($it)" } fun main(args: Array<String>) { println("defaultStrings = $defaultStrings") println("customStrings = $customStrings") 运行结果为: defaultStrings = [1, 2, 3] customStrings = [(1), (2), (3)] 变量类型推断 大家都知道,Kotlin的类型推断系统是非常强大的,现在Kotlin编译器也支持通过强制转换的信息,来推断出变量类型了。比如说,如果你在调用一个返回“T”的泛型方法时,并将它的返回值“T”转换为特定类型如“Foo”,编译器就会推断出这个方法调用中的“T”其实是“Foo”类型。 这个对安卓开发者而言尤其重要,因为自从API26(Android7.0)开始,findViewById变成了泛型方法,然后编译器也会正确分析该方法的调用返回值。 val button = findViewById(R.id.button) as Button 智能转换 当一个变量为某个安全表达式(如校验非空)所赋值时,智能转换也同样运用于这个安全调用的接收者。 fun countFirst(s: Any): Int { val firstChar = (s as? CharSequence)?.firstOrNull() if (firstChar != null) return s.count { it == firstChar } // 输入参数s被智能转换为CharSequence类型 val firstItem = (s as? Iterable<*>)?.firstOrNull() if (firstItem != null) return s.count { it == firstItem } // 输入参数s被智能转换为Iterable<*>类型 return -1 } fun main(args: Array<String>) { val string = "abacaba" val countInString = countFirst(string) println("called on \"$string\": $countInString") val list = listOf(1, 2, 3, 1, 2) val countInList = countFirst(list) println("called on $list: $countInList") } 运行结果为: called on "abacaba": 4 called on [1, 2, 3, 1, 2]: 2 另外,Lamba表达式同样支持对局部变量进行智能转换,前提是该局部变量只在Lamba表达式之前修改过。 fun main(args: Array<String>) { val flag = args.size == 0 var x: String? = null if (flag) x = "Yahoo!" run { if (x != null) { println(x.length) // x is smart cast to String } } } 运行结果为:6 foo的简写 为了简化调用成员的引用,现在可以不用this关键字,::foo而不用明确的接收者this::foo。这也使得可调用的引用在你引用外部接收者的成员的lambda中更方便。 弃用 Kotlin1.2版本也弃用了很多不合理的东西。 弃用:枚举条目中的嵌套类型 在枚举条目中,inner class由于初始化逻辑中的问题,定义一个非嵌套的类型已经被弃用了。这会在Kotlin 1.2中引起警告,并将在Kotlin 1.3中出错。 弃用:vararg单个命名参数 为了与注释中的数组文字保持一致,在命名形式(foo(items = i))中传递可变参数的单个项目已被弃用。请使用具有相应数组工厂功能的扩展运算符。 foo(items = *intArrayOf(1)) 在这种情况下,有一种优化可以消除冗余阵列的创建,从而防止性能下降。单参数形式在Kotlin 1.2中产生警告,并将被放在Kotlin 1.3中。 弃用:扩展Throwable的泛型内部类 继承的泛型类型的内部类Throwable可能会违反类型安全性,因此已被弃用,Kotlin 1.2中有警告,Kotlin 1.3中有错误。 弃用:只读属性的后台字段 field = ...已经废弃了在自定义获取器中分配只读属性的后台字段,Kotlin 1.2中有警告,Kotlin 1.3中有错误。 标准库 Kotlin标准库与拆分包 Kotlin标准库现在完全兼容Java 9模块系统,该系统禁止拆分包(多个jar文件在同一个包中声明类)。为了支持这一点,新的文物kotlin-stdlib-jdk7 和kotlin-stdlib-jdk8介绍,取代旧的kotlin-stdlib-jre7和kotlin-stdlib-jre8。 为确保与新模块系统的兼容性,Kotlin做出的另一个更改是将kotlin.reflect从kotlin-reflect库中移除。如果您正在使用它们,则需要切换到使用kotlin.reflect.full软件包中的声明,这是自Kotlin 1.1以来支持的声明。 窗口,分块,zipWithNext 为新的扩展Iterable,Sequence以及CharSequence覆盖这些用例如缓冲或批处理(chunked),滑动窗口和计算滑动平均(windowed),和随后的项目的处理对(zipWithNext)。 fun main(args: Array<String>) { val items = (1..9).map { it * it } val chunkedIntoLists = items.chunked(4) val points3d = items.chunked(3) { (x, y, z) -> Triple(x, y, z) } val windowed = items.windowed(4) val slidingAverage = items.windowed(4) { it.average() } val pairwiseDifferences = items.zipWithNext { a, b -> b - a } println("items: $items\n") println("chunked into lists: $chunkedIntoLists") println("3D points: $points3d") println("windowed by 4: $windowed") println("sliding average by 4: $slidingAverage") println("pairwise differences: $pairwiseDifferences") } fill, replaceAll, shuffle/shuffled 为了操纵列表,Kotlin加入了一组扩展函数:fill,replaceAll和shuffle对MutableList,shuffled用于只读List。 fun main(args: Array<String>) { val items = (1..5).toMutableList() items.shuffle() println("Shuffled items: $items") items.replaceAll { it * 2 } println("Items doubled: $items") items.fill(5) println("Items filled with 5: $items") } 运行结果为:Shuffled items: [5, 3, 1, 2, 4]Items doubled: [10, 6, 2, 4, 8]Items filled with 5: [5, 5, 5, 5, 5] 数学运算 为了满足一些特殊的需求,Kotlin 1.2添加了一些常见的数学运算API。 常量:PI和E; 三角函数:cos,sin,tan和它们的反:acos,asin,atan,atan2, 双曲:cosh,sinh,tanh和它们的反:acosh,asinh,atanh 求幂:pow(扩展函数),sqrt,,hypot ;expexpm1 对数:log,log2,log10,ln,ln1p, 四舍五入: ceil,floor,truncate,round(半连)的功能; roundToInt,roundToLong(半整数)扩展函数; 符号和绝对值: abs和sign功能; absoluteValue和sign扩展属性; withSign 扩展功能;max和min两个价值观; 二进制表示: ulp 扩展属性; nextUp,nextDown,nextTowards扩展函数;toBits,toRawBits,Double.fromBits(这些是在kotlin包)。 正则表达式可序列化 现在,Kotlin可以使用Serializable来序列化正则表达式的层次结构。 JVM 构造函数调用规范化 自1.0版以来,Kotlin支持复杂控制流的表达式,例如try-catch表达式和内联函数调用。但是,如果构造函数调用的参数中存在这样的表达式时,一些字节码处理工具不能很好地处理这些代码。为了缓解这种字节码处理工具的用户的这个问题,我们添加了一个命令行选项(-Xnormalize-constructor-calls=MODE),它告诉编译器为这样的结构生成更多的类Java字节码。 其中,这里的MODE有以下情况: disable (默认) - 以和Kotlin 1.0和1.1相同的方式生成字节码; enable - 为构造函数调用生成类似Java的字节码。这可以改变类加载和初始化的顺序; preserve-class-initialization -为构造函数调用生成类似Java的字节码,确保保持类的初始化顺序。这可能会影响应用程序的整体性能;只有在多个类之间共享一些复杂的状态并在类初始化时更新时才使用它。 Java默认方法调用 在Kotlin 1.2之前,接口成员在针对JVM 1.6的情况下重写Java默认方法会在超级调用上产生一个警告:Super calls to Java default methods are deprecated in JVM target 1.6. Recompile with '-jvm-target 1.8'。在Kotlin 1.2中,会出现一个错误,因此需要使用JVM target 1.8来编译这些代码。 x.equals(null) 调用x.equals(null)上被映射到Java原始(平台类型Int!,Boolean!,Short!, ,Long!,Float!,Double!)Char!返回不正确true时x为空。从Kotlin 1.2开始,调用x.equals(...)一个平台类型的null值会抛出一个NPE (但是x == ...不会)。 要返回到1.2之前的行为,请将该标志传递-Xno-exception-on-explicit-equals-for-boxed-null给编译器。 内联扩展空修复 在以前的版本中,在平台类型的空值上调用的内联扩展函数没有检查接收器是否为null,并因此允许null转义到其他代码中。Kotlin 1.2中强制执行此检查,如果接收方为空,则抛出异常。 JavaScript TypedArrays支持 JS类型的数组支持将Kotlin原始数组(例如IntArray,DoubleArray)转换为JavaScript类型的数组,这以前是可选入功能,默认情况下已启用。 除此之外,Kotlin的编译器现在提供一个将所有警告视为错误的选项。使用-Werror命令行,或者修改如下配置: compileKotlin { kotlinOptions.allWarningsAsErrors = true }
在 iOS 上面开发界面,需要创建视图、配置界面、视图分层等等很多步骤,也就不可避免的需要书写 N 多的代码。这还仅仅是界面设计,除此之外,完成 controllers 的回调、控制内部事务在界面上的显示效果、界面的操控和内部事务的联系等等多方面的事情都需要手动解决。即便是界面很简单的 App,如果存在这种复杂的双向数据流的关系,那么代码也会变得很复杂很容易出错。Qt 的信号、槽和 iOS 的 Target-Action 机制其实也是很容易实现这种双向数据流的关系,但是没有办法解决界面和事务之间的联系,也有很多其他的问题:性能、测试等。 这些问题曾经困扰了我们多年。News Feed 是有着复杂的列表样式外观的 iOS 软件,由许多的 Row Type 组成,每一个 Row 都有各种各样不同的很烦的界面样式和交互方式,这个就很坑了。每次维护这个东西都像是在清理厕所,尤其是它的功能还在不断增加,它的代码在不断变多,版本迭代速度快到你都没办法直到每天都到底增添了什么新代码,上司还要拿着报告说“你这个软件太慢了,影响用户体验,给你三个小时把这个 App 的速度提高 80%”。 为了解决这一挑战性的问题,我们从自己的 ReactJS 得到启发,把很多具体的东西抽象出来,做出一个功能性的、响应式编程模型的 iOS 原生 UI 框架 ComponentKit,目前 News Feed 在应用这个框架。 ComponentKit 简介 ComponentKit 使用功能性和声明性(declarative)的方法来进行创建界面,和以往不同的是,ComponentKit 使用单向数据流的形式从 不可变的模型 映射到不可变的组件来确定视图的显示方式。ComponentKit 的 declarative 看上去和 declarative UI(QML) 差不多,其实差得远。QML 更偏向于 UI 设计的描述性,而 ComponentKit 则是做好基本 UI 和事件之间的联系,让事件设计和 UI 设计可以分开单独完成。 内在决定外在,组件的功能和内部的层次决定了用户界面该如何规划,界面的规划决定了 UI Kit 的元素层次结构的设计。 传统做法的结果是大部分时间都被浪费在 UI 该如何实现,ComponentKit 却可以让你把时间都用在在 UI 该怎么设计上面。 例如,传统的 iOS 开发中,为了开发一个带有 header、text 和 footer 的视图,需要以下步骤: 分别创建 header 视图、text 视、footer 视图的实例 将三个视图添加为 container 的子视图 添加约束条件,让每个视图和 container 的宽度相同 添加更多的约束条件,确保每个视图的摆放位置 但是 ComponentKit 不一样,ComponentKit 是一种描述性的开发包:你只需要提供你希望得到什么便能得到什么,而不和传统的 iOS 开发一样,再去一个一个地创建视图、修改视图样式、添加视图、添加约束条件。如图所示,想要得到这个布局,只需要使用描述性的语言描述“我想要一个 header 组件,一个 text 组件,一个 footer 组件,他们的宽度相同,从上到下排列在一起”。单单从这点来看,和 QML 相比,ComponentKit 更类似于 Bootstrap:提供已经完成的组件,你只需要决定组件如何摆放,便可轻松地开发出 UI 界面。 ComponentKit 已经完全把如何渲染 UI 的事情抽象出来,程序员完全可以不去考虑具体是如何实现渲染的,也不用去考虑界面渲染该如何优化。ComponentKit 使用后台线程进行界面布局,也实现了智能组件重用,你完全可以不去考虑界面导致的内存泄露问题。ComponentKit 不仅仅可以极大地提高开发效率,界面响应速度和软件的运行效率也会有极大地提升。 News Feed 移植到 ComponentKit ComponentKit 极大地提升了 News Feed 的 UI 响应速度和稳定性,也让整个软件的内部编码更容易理解。ComponentKit 达到了如下的目标: 减少了 70% 的界面渲染代码,麻麻再也不用担心我每次去维护之前都要看那本又臭又长的手册然后花一上午的时间去理解那个错综复杂的布局了。 显著地提高了滑屏的性能。ComponentKit 消除了许多的 container视图,尽力将所有的视图结构化简。更简洁的视图结构意味着界面的渲染性能和执行效率更高。 提高测试覆盖率。ComponentKit 对于 UI 模块化的设计保证了每一部分都可以被分离开来单独进行测试。再加上 snapshot tests,我们现在几乎已经可以对 News Feed 的所有部分都进行测试了。 引入了 ComponentKit 之后,我们能够维护更少的代码,有更少的 bug 需要修复,有更大的测试覆盖率:我们现在可以有更多的时间做羞羞的事情了 ComponentKit 已经在生产环境的 News Feed 上用了六个月,我们觉得可以一直用下去。现在将 ComponentKit 开源,让整个 iOS 开发者社区的人都有 Facebook 的生产效率,也都能和 Facebook 一样做出高性能的 App。很希望你也能在你的开发环境中使用 ComponentKit,然后给我们反馈。 我们重新定义了如何在 iOS 上开发界面,希望你也能用 ComponentKit 开发出更优雅的 App。 快速入门 ComponentKit 已经在 CocoaPods 中可用了,只需要在 Podfile 添加如下代码即可: pod 'ComponentKit', '~> 0.9' pod try ComponentKit 原文:Introducing ComponentKit: Functional and declarative UI on iOS
继移动APP之后,小程序作为当前移动的有一个入口为大家所推崇,不管是微信的小程序还是支付宝的小程序,其实现的思路都是一致的,即通过一个宿主来运行相关的JS页面。 现在Hera根据市场需求,推出了一款真正的跨平台框架,除了可以让你的小程序除了在微信上运行,还可以打包成 Android 、 iOS应用,以及以 h5 的方式跑在浏览器端。 主要的优点有: 一套代码 处处运行Hera提供了强大的跨平台能力:不仅可以让开发者的微信小程序业务从微信中平滑迁移到Android和iOS端的App中,同时也提供了RN等其它框架没有的能力 —— 运行在Web端。 组件丰富 简单易用自带常用组件,完美继承了小程序内置组件,学习成本低,完全兼容微信小程序的开发方式。 极速加载 体验流畅Hera框架同时也可以支持业务的快速迭代和更新,所有组件和 API 内置在客户端中,每个页面只包含核心业务逻辑使页面更轻量,在高速加载的同时兼具动态更新的能力。 快速上手 安装脚手架 需要在系统中安装 Node.js 环境, 使用以下方法确认系统中 Node 的版本: node -v 如果得到的版本低于v7.6.0,或是提示找不到 node 命令,请点此下载最新的 Node 环境安装包。 Tips: 如果下载时出现网络问题,可以尝试使用 nrm 或 npm config 命令切换至国内的npm源 安装运行 安装依赖库 npm i hera-cli -g 初始化小程序 hera init projName 进入新建的项目, 确认根目录有 config.json 文件: # 进入项目 cd projName # 查看配置文件 cat config.json web运行 hera run web Android中运行如果想要在安卓虚拟机或真机上运行,需要安装 Android Studio 以及:Android SDK Platform 25Android SDK Build-Tools 25.0.3Tips:如果对Android环境搭建不清楚的可以自行查询资料。然后使用命令查看设备是否连接,命令如下: adb devices 说明:如果提示adb不是可用命令,请确认PATH 环境变量中增加了%ANDROID_HOME%platform-tools和%ANDROID_HOME%platform-tools如果设备处于活跃状态会显示如下信息,如果列表为空或设备处于离线状态,请重新连接安卓手机或重启虚拟机。 List of devices attached 0ec123456 device 然后链接之后就可以运行了,运行的命令如下: hera run android iOS端运行首先需要在系统中安装 Xcode 8.0 或更高版本。你可以通过App Store或是到Apple开发者官网上下载。这一步骤会同时安装Xcode IDE和Xcode的命令行工具。 安装完成后启动Xcode,并在Xcode | Preferences | Locations菜单中检查一下是否装有某个版本的Command Line Tools。 然后,使用如下命令安装依赖管理工具 cocoapods,命令如下: sudo gem install cocoapods 然后,使用命令运行即可: hera run ios 以上都是在模拟器上运行的,如果想要在真机上运行,可以访问下面的介绍:https://weidian-inc.github.io/hera/#/ios/ios-real-device 目录结构说明 新建后的项目的目录结构如下: ├── README.md ├── android ├── docs ├── h5 └── ios 其中:android 和 ios 目录下为小程序API 在客户端上的实现;h5 目录下为小程序转换工具:将小程序转换为客户端可以执行的代码;docs 目录下为项目文档及主页生成器;
2017 年的 Google 在中国刷了好几个记忆点,从五月乌镇 AlphaGo 与中国顶尖棋手的终极对弈,到欧阳靖为 Google 翻译专门创作了 MV 大片,再到十二月今日的上海,2000 多位开发者们济济一堂,参加连续两天面向中国开发者的科技盛会,让这个冬天多了一份温暖。 不同于以往,此次 Google 开发者大会安排了 8 组嘉宾发表主题演讲,除了在去年的开发者大会发表主题演讲的 Google 大中华区总裁石博盟(Scott Beaumont)和 Google 产品总监 Andrew Bowers, 今年的大会特别邀请了多位女性 Google 工程师和重量级嘉宾。来看看刚刚结束的主题演讲上,都有哪些振奋人心的消息。 今天,google宣布了一系列的研究成果,以及未来的发展计划,其中最重要的莫过于谷歌 AI 中国中心成立。 人工智能新图景 :谷歌 AI 中国中心成立 Google Cloud 人工智能和机器学习团队的首席科学家李飞飞宣布,谷歌 AI 中国中心在北京成立。该中心由李飞飞和 Google Cloud 研发负责人李佳博士共同领导。李飞飞将会负责中心的研究工作,也会统筹 Google Cloud AI, Google Brain 以及中国本土团队的工作。 除了发表自己的研究成果,谷歌 AI 中国中心也非常期待能在中国本土合作上有所建树,为更广大的学生及研究人员提供高质量 AI 及机器学习的教育支持。如果你对AI有兴趣,不妨点击下面的链接:谷歌AI正式落地中国 TensorFlow 微信公众号正式发布! 不久前我们发布了 TensorFlow 中文网站 tensorflow.google.cn。就在今天,我们发布了 TensorFlow 微信公众号,为中国开发者提供最新的 TensorFlow 新闻和技术资源。 在过去的两年中,我们看到了一个围绕 TensorFlow 的机器学习开源社区在蓬勃发展。在 GitHub 上获得了超过 81,000 个评星 (stars),23,000 多个项目的标题包含 “TensorFlow”,1100 多个开发者贡献了代码。 Google 已经研发出非常强大的 TPU Pod, 目前每秒钟可以做出 11.5 千万亿次浮点运算 (PetaFlops), 4 兆兆字节 (TB) 内存。正因为我们有了这样强大的计算能力,通过全世界科技精英的共同努力,我相信在不久的将来,我们可以解决现在看来无法想象的问题。 另外,Google 刚刚发布 TensorFlow Lite 开发者预览版,其是 TensorFlow 直接为移动设备发开的轻量级开源机器学习库。此框架针对机器学习模型的低延迟推理进行优化,占用内存小,并具有快速性能。 持续发展的Android 至今,全球有 20 亿激活的 Android 设备和 Google Play 上高达 820 亿的应用安装。 越来越清晰的应用设计前景、越来越强大的开发工具、新的开发语言、人工智能以及分布模型的改进。这些变化,都离不开我们来自各个渠道的开发者们。 在过去的一年里,应用安装量过百万的开发者数量增长了 35%。为了将这样巨大的用户量转化成更好的开发者收益,我们加大了与运营商的合作。 目前有超过 140 家运营商可进行代扣费的付款方式,它们覆盖了 9 亿的手机设备。把这些都算在一起的话,去年在 Google Play 进行消费的用户数增长了 30%。 上周,Google 发布了 Oreo 8.1 的正式版,这个版本不仅有 Android Go Edition 轻量级版本和针对入门机型的优化,也会有新的神经网络API来帮助开发者去创建基于设备的机器学习方面的应用,包括图像识别、预测等等。 为了优化开发体验,Android Studio 3.0 版本新增了应用剖析工具、更佳的 Kotlin 语言支持、加快了 Gradle 大项目的编译速度等。 此外,针对中国市场,我们推出了Android Wear 中国版。我们与国内的应用开发厂商合作,致力于为 Android 手机用户及 iPhone 用户提供最好的用户体验。 IoT, Android Things, 和 Google 智能助理 Android Things 是物联网和嵌入式设备在 Android 平台的延伸。目前开发者预览版可以立即进行测试。它使 Android 的以新的形式加入到已有的移动设备、穿戴设备,电视和汽车的 Android 大家庭中。Android Things 硬件基于 System-on-Module 或 SoM 架构,以非常小的包装包含 CPU,内存,网络和其他核心组件。 SoMs 非常便宜,因为它们是大量生产的通用零件。在原型设计和开发过程中,您将 SoM 附加到一个更大的突破板上来构建您的想法。 Google 智能助理(Google Assistant) Google 智能助理可以在数百万台设备上使用,包括 Google Home 之类的语音激活扬声器,符合条件的 Android 手机,Android TV,Google Pixelbooks,耳机和 Android Wear,即将推出 Android Auto。 我们鼓励您为 Google 智能助理创建应用程序,覆盖全球各地的大量用户。无论用户身在何处,无论他们在做什么,Google 智能助理都可以随时使用语音或文本。而且我们在世界各地的各种语言和语言环境中都可以使用,并随时添加新的语言和语言环境。 同时,我们还发布了 Google Assistant SDK,可让您将 Google 智能助理嵌入到自己的自定义硬件项目中。Google Assistant SDK 适用于 Linux,Android Things 以及支持 gRPC 的任何其他平台。 PWA 帮助中国开发者优化用户体验 对于开发者来说,Mobile Web 是一个很大的舞台,全球 Chrome 浏览器的总量已经超过 20 亿。在过去的一年中,我们已经发布了数百个额外的 API,涵盖了一系列功能,从简化付款集成到直接在网络上构建功能齐全的离线媒体体验。 借助所有这些功能,现代移动网络还已经能够让开发人员利用我们称之为 Progressive Web Apps,或简称 PWA,构建深入丰富的移动体验。他们可以快速加载,离线操作,甚至可以向用户发送通知。 目前,支持 PWA 的核心技术已经在全球许多个主要浏览器中得到了支持,同时也延伸到了中国的主要浏览器。例如,在中国,360 浏览器,手机百度以及最近的UC浏览器都已经支持 Service Worker 的规范以及 PWA 所依赖的 Cache API,将这些一致的可靠体验带给用户。QQ 浏览器也宣布了在不久的将来就会支持 Service Worker 的规范。这意味着,作为开发人员,您现在可以开始构建 PWA 了,无论您身在何处,您都可以为用户提供现代化的移动 Web 体验。并且,我们已经看到了令人欣喜的实际应用:作为中国最受欢迎的社交媒体网站之一,新浪微博最近投放资源打造一个全新的 PWA 体验,现在在测试阶段,提供流畅的用户体验,并在所有网络条件下可靠无缝地运行。用户可以通过手机网站撰写和分享自己的微博信息,即使在网络条件较差的情况下,也可以继续浏览微博内容,欣赏图片和视频。 Firebase 中的 Crashlytics 分析 至今,已经有 100 万开发者使用 Firebase 开发软件。为了帮开发者更快地开发,可以使用实时数据库和 Crashlytics 等产品,通过 Google Analytics 和 Cloud Messaging 等了解并改进应用。 无论您是要开始新的开发,还是希望扩展现有的应用程序,我们都在这里为您提供帮助,以便您可以集中更多时间和资源为用户提供价值。自从与 Fabric 团队合作以来,我们最近将 Crashlytics 带入 Firebase,成为我们的崩溃分析报告王牌产品,帮助您监控和修复应用程序崩溃和错误。
Ideal是当前使用量比较大的开发工具,激活方法有三种:序列号、账号、服务器激活。一般我们选择第三种。 ``` 43B4A73YYJ-eyJsaWNlbnNlSWQiOiI0M0I0QTczWVlKIiwibGljZW5zZWVOYW1lIjoibGFuIHl1IiwiYXNzaWduZWVOYW1lIjoiIiwiYXNzaWduZWVFbWFpbCI6IiIsImxpY2Vuc2VSZXN0cmljdGlvbiI6IkZvciBlZHVjYXRpb25hbCB1c2Ugb25seSIsImNoZWNrQ29uY3VycmVudFVzZSI6ZmFsc2UsInByb2R1Y3RzIjpbeyJjb2RlIjoiSUkiLCJwYWlkVXBUbyI6IjIwMTctMDItMjUifSx7ImNvZGUiOiJBQyIsInBhaWRVcFRvIjoiMjAxNy0wMi0yNSJ9LHsiY29kZSI6IkRQTiIsInBhaWRVcFRvIjoiMjAxNy0wMi0yNSJ9LHsiY29kZSI6IlBTIiwicGFpZFVwVG8iOiIyMDE3LTAyLTI1In0seyJjb2RlIjoiRE0iLCJwYWlkVXBUbyI6IjIwMTctMDItMjUifSx7ImNvZGUiOiJDTCIsInBhaWRVcFRvIjoiMjAxNy0wMi0yNSJ9LHsiY29kZSI6IlJTMCIsInBhaWRVcFRvIjoiMjAxNy0wMi0yNSJ9LHsiY29kZSI6IlJDIiwicGFpZFVwVG8iOiIyMDE3LTAyLTI1In0seyJjb2RlIjoiUEMiLCJwYWlkVXBUbyI6IjIwMTctMDItMjUifSx7ImNvZGUiOiJSTSIsInBhaWRVcFRvIjoiMjAxNy0wMi0yNSJ9LHsiY29kZSI6IldTIiwicGFpZFVwVG8iOiIyMDE3LTAyLTI1In0seyJjb2RlIjoiREIiLCJwYWlkVXBUbyI6IjIwMTctMDItMjUifSx7ImNvZGUiOiJEQyIsInBhaWRVcFRvIjoiMjAxNy0wMi0yNSJ9XSwiaGFzaCI6IjMzOTgyOTkvMCIsImdyYWNlUGVyaW9kRGF5cyI6MCwiYXV0b1Byb2xvbmdhdGVkIjpmYWxzZSwiaXNBdXRvUHJvbG9uZ2F0ZWQiOmZhbHNlfQ==-keaxIkRgXPKE4BR/ZTs7s7UkP92LBxRe57HvWamu1EHVXTcV1B4f/KNQIrpOpN6dgpjig5eMVMPmo7yMPl+bmwQ8pTZaCGFuLqCHD1ngo6ywHKIQy0nR249sAUVaCl2wGJwaO4JeOh1opUx8chzSBVRZBMz0/MGyygi7duYAff9JQqfH3p/BhDTNM8eKl6z5tnneZ8ZG5bG1XvqFTqWk4FhGsEWdK7B+He44hPjBxKQl2gmZAodb6g9YxfTHhVRKQY5hQ7KPXNvh3ikerHkoaL5apgsVBZJOTDE2KdYTnGLmqxghFx6L0ofqKI6hMr48ergMyflDk6wLNGWJvYHLWw==-MIIEPjCCAiagAwIBAgIBBTANBgkqhkiG9w0BAQsFADAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBMB4XDTE1MTEwMjA4MjE0OFoXDTE4MTEwMTA4MjE0OFowETEPMA0GA1UEAwwGcHJvZDN5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxcQkq+zdxlR2mmRYBPzGbUNdMN6OaXiXzxIWtMEkrJMO/5oUfQJbLLuMSMK0QHFmaI37WShyxZcfRCidwXjot4zmNBKnlyHodDij/78TmVqFl8nOeD5+07B8VEaIu7c3E1N+e1doC6wht4I4+IEmtsPAdoaj5WCQVQbrI8KeT8M9VcBIWX7fD0fhexfg3ZRt0xqwMcXGNp3DdJHiO0rCdU+Itv7EmtnSVq9jBG1usMSFvMowR25mju2JcPFp1+I4ZI+FqgR8gyG8oiNDyNEoAbsR3lOpI7grUYSvkB/xVy/VoklPCK2h0f0GJxFjnye8NT1PAywoyl7RmiAVRE/EKwIDAQABo4GZMIGWMAkGA1UdEwQCMAAwHQYDVR0OBBYEFGEpG9oZGcfLMGNBkY7SgHiMGgTcMEgGA1UdIwRBMD+AFKOetkhnQhI2Qb1t4Lm0oFKLl/GzoRykGjAYMRYwFAYDVQQDDA1KZXRQcm9maWxlIENBggkA0myxg7KDeeEwEwYDVR0lBAwwCgYIKwYBBQUHAwEwCwYDVR0PBAQDAgWgMA0GCSqGSIb3DQEBCwUAA4ICAQC9WZuYgQedSuOc5TOUSrRigMw4/+wuC5EtZBfvdl4HT/8vzMW/oUlIP4YCvA0XKyBaCJ2iX+ZCDKoPfiYXiaSiH+HxAPV6J79vvouxKrWg2XV6ShFtPLP+0gPdGq3x9R3+kJbmAm8w+FOdlWqAfJrLvpzMGNeDU14YGXiZ9bVzmIQbwrBA+c/F4tlK/DV07dsNExihqFoibnqDiVNTGombaU2dDup2gwKdL81ua8EIcGNExHe82kjF4zwfadHk3bQVvbfdAwxcDy4xBjs3L4raPLU3yenSzr/OEur1+jfOxnQSmEcMXKXgrAQ9U55gwjcOFKrgOxEdek/Sk1VfOjvS+nuM4eyEruFMfaZHzoQiuw4IqgGc45ohFH0UUyjYcuFxxDSU9lMCv8qdHKm+wnPRb0l9l5vXsCBDuhAGYD6ss+Ga+aDY6f/qXZuUCEUOH3QUNbbCUlviSz6+GiRnt1kA9N2Qachl+2yBfaqUqr8h7Z2gsx5LcIf5kYNsqJ0GavXTVyWh7PYiKX4bs354ZQLUwwa/cG++2+wNWP+HtBhVxMRNTdVhSm38AknZlD+PTAsWGu9GyLmhti2EnVwGybSD2Dxmhxk3IPCkhKAK+pl0eWYGZWG3tJ9mZ7SowcXLWDFAk0lRJnKGFMTggrWjV8GYpw5bq23VmIqqDLgkNzuoog== ``` 不过上面的激活码方式已经过期了。 ### 服务器激活 注册时,在打开的License Activation窗口中选择“License server”,在输入框输入下面的网址: http://idea.iteblog.com/key.php 点击:Activate即可。 除此之外,网友还有另外一个License Server地址:http://idea.imsxm.com/
自16年下半年以来,互联网遭遇了寒冬,很多大小公司也开始裁员,对于一些刚入门或者经验不够的人员是一个考验,至于现在学Android或者ios,我觉得倒不如学前端和后台,因为这种技术是不会过时的,并且越久越吃香。