Quantcast
Channel: CSDN博客移动开发推荐文章
Viewing all 5930 articles
Browse latest View live

Android系统主题总结和使用

$
0
0

一,Android主题的发展过程

1,在Android3.0之前,Android的界面不论是从系统还是空间的主题都是按钮为白色,点击事件为黄色。现在看来很简陋。


2,Holo主题:Android3.0 (API11)开始,Google推出了Holo主题(就是我们印象中的黑底白字蓝主色的主题)。在4.0重google又发布了应用设计规范Android Design。有了设计规范的指导,就有了更多的应用采用Holo主题。所以我们可以简单认为Android Design就是Holo主题。但是这种主题是适合移动设备,其他平台略显突兀。

      在4.0之前Android可以说是没有设计可言的,在4.0之后推出了Android Design,从此Android在设计上有了很大的改善,而在程序实现上相应的就是Holo风格,所以你看到有类似 Theme.Holo.Light、 Theme.Holo.Light.DarkActionBar 就是4.0的设计风格,但是为了让4.0之前的版本也能有这种风格怎么办呢?这个时候就不得不引用v7包了,所以对应的就有 Theme.AppCompat.Light、Theme.AppCompat.Light.DarkActionBar,如果你的程序最小支持的版本是API14(即Android 4.0),那么可以不用考虑v7的兼容。


3,Material 主题从Android5.0(API21)开始,Google又推出了材料设计语言Material Design,又叫Google Design。MD崇尚的就是图层扁平化,所有图层像纸或者卡片一样重叠在一起,所以Android5.0就有了RecyclerView和CardView。图层之间有间隔,所以Android5.0中有了translation和elevation两个属性。同时也规范了Android的运动元素,界面上的每个元素不是无故产生的,同时每个图层的产生和消失都有方向的约定,从哪里来就往哪里去,这也是为什么Android 5.0中会有Ripple,Circular Receal,Activity Transition.

     Android在5.0版本推出了Material Design的概念,这是Android设计上又一大突破。对应的程序实现上就有Theme.Material.Light、 Theme.Material.Light.DarkActionBar等,但是这种风格只能应用在在5.0版本的手机,如果在5.0之前应用Material Design该怎么办呢?同样的引用appcompat-v7包,这个时候的Theme.AppCompat.Light、Theme.AppCompat.Light.DarkActionBar就是相对应兼容的Material Design的Theme。


二,Android Theme的分类

    1,android:Theme                             API 1 开始
    2,android:Theme.Holo                     API 11(android3.0) 开始
    3,android:Theme.DeviceDefault      API 14(android4.0) 开始
    4,android:Theme.Material               API 21(android5.0) 开始
    5,Theme.AppCompat                      兼容包AppCompat_v7中的主题


三,使用系统主题的位置

  1,使用非兼容包主题的方法:在style标签的parent里面输入“Android:Theme”会有自动提示

  Theme主题和DeviceDefault主题

Holo主题


Material主题


  2,使用兼容主题的方法。在style标签的parent里面输入“Theme”会有自动提示




API 1:
android:Theme 根主题
android:Theme.Black 背景黑色
android:Theme.Light 背景白色
android:Theme.Wallpaper 以桌面墙纸为背景
android:Theme.Translucent 透明背景
android:Theme.Panel 平板风格
android:Theme.Dialog 对话框风格

API 11:
android:Theme.Holo Holo根主题
android:Theme.Holo.Black Holo黑主题
android:Theme.Holo.Light Holo白主题

API 14:
Theme.DeviceDefault 设备默认根主题
Theme.DeviceDefault.Black 设备默认黑主题
Theme.DeviceDefault.Light 设备默认白主题

API 21: (网上常说的 Android Material Design 就是要用这种主题)
Theme.Material Material根主题
Theme.Material.Light Material白主题

兼容包v7中带的主题:
Theme.AppCompat 兼容主题的根主题
Theme.AppCompat.Black 兼容主题的黑色主题
Theme.AppCompat.Light 兼容主题的白色主题

更多主题:
以下都是指“包含”,比如包含“Dialog”表示对话框风格
比如Theme.Dialog、Theme.Holo.Dialog、Theme.Material.Dialog、Theme.AppCompat.Dialog都是对话框风格
具体有没有这种组合,你就在“自动提示”中来看就可以,提示有就有,没有就没有。

Black 黑色风格
Light 光明风格
Dark 黑暗风格
DayNight 白昼风格
Wallpaper 墙纸为背景
Translucent 透明背景
Panel 平板风格
Dialog 对话框风格
NoTitleBar 没有TitleBar
NoActionBar 没有ActionBar
Fullscreen 全屏风格
MinWidth 对话框或者ActionBar的宽度根据内容变化,而不是充满全屏
WhenLarge 对话框充满全屏
TranslucentDecor 半透明风格
NoDisplay 不显示,也就是隐藏了
WithActionBar 在旧版主题上显示ActionBar

常见主题

•android:theme="@android:style/Theme.Dialog"   将一个Activity显示为能话框模式
•android:theme="@android:style/Theme.NoTitleBar"  不显示应用程序标题栏
•android:theme="@android:style/Theme.NoTitleBar.Fullscreen"  不显示应用程序标题栏,并全屏
•android:theme="Theme.Light"  背景为白色
•android:theme="Theme.Light.NoTitleBar"  白色背景并无标题栏 
•android:theme="Theme.Light.NoTitleBar.Fullscreen"  白色背景,无标题栏,全屏
•android:theme="Theme.Black"  背景黑色
•android:theme="Theme.Black.NoTitleBar"  黑色背景并无标题栏
•android:theme="Theme.Black.NoTitleBar.Fullscreen"    黑色背景,无标题栏,全屏
•android:theme="Theme.Wallpaper"  用系统桌面为应用程序背景
•android:theme="Theme.Wallpaper.NoTitleBar"  用系统桌面为应用程序背景,且无标题栏
•android:theme="Theme.Wallpaper.NoTitleBar.Fullscreen"  用系统桌面为应用程序背景,无标题栏,全屏
•android:theme="Translucent" 背景为透明
•android:theme="Theme.Translucent.NoTitleBar"  透明背景并无标题栏
•android:theme="Theme.Translucent.NoTitleBar.Fullscreen"  透明背景并无标题栏,全屏
•android:theme="Theme.Panel"  内容容器
•android:theme="Theme.Light.Panel" 背景为白色的内容容器

AppCompat_v7兼容包主题细分:(以'com.android.support:appcompat-v7:25.1.0'为例:)

Theme.AppCompat 作用于Activity层面以上的主题
Base、Platform 作为父类被继承的,一般不直接使用

AlertDialog.AppCompat 对话框深色
AlertDialog.AppCompat.Light 对话框浅色
Animation.AppCompat.Dialog 带动画效果的对话框
Animation.AppCompat.DropDownUp
RtlOverlay.Widget.AppCompat
RtlUnderlay.Widget.AppCompat
TextAppearance.AppCompat 文字样式相关
ThemeOverlay.AppCompat
Widget.AppCompat 控件相关的主题


关于Theme.ApCompat兼容主题
主题间的继承关系:(以Theme.AppCompat为例)
Theme.AppCompat ——> Base.Theme.AppCompat
Base.Theme.AppCompat ——> Base.V*.Theme.AppCompat (*可能是7、21、23等)
Base.V*.Theme.AppCompat ——> Platform.AppCompat
Platform.AppCompat ——> android:Theme


其中第二步:版本25.1.0有四种选择:Base、Base.V21、Base.V22、Base.V23。(更早的版本还有V7、V11等)
兼容:App在运行时会根据系统的版本选择对应的父类主题。大于21选择V21,大于22选择V22


系统通常预定义的主题样式:
Theme.AppCompat 深色主题
Theme.AppCompat.NoActionBar 没有ActionBar
Theme.AppCompat.Dialog 对话框适用
Theme.AppCompat.Dialog.Alert 警告框适用(根据屏幕决定宽度)
Theme.AppCompat.Dialog.MinWidth 对话框适用(根据内容决定宽度)
Theme.AppCompat.DialogWhenLarge 充满屏幕(继承自Theme.AppCompat,但没有扩展)
Theme.AppCompat.CompactMenu 看名字是用于Menu菜单。未验证
其他主题系统默认都会有上述几种类型的子主题,以此类推就好。
例如:浅色主题只需要将Theme.AppCompat 替换成 Theme.AppCompat.Light即可


四,为什么需要Theme.AppCompat主题?

那么这个Theme.AppCompat到底是什么呢?就要从其他的三个系统基本主题说起

          1,android:Theme
          2,android:Theme.Holo
          3,android:Theme.Material

是的,这三个主题就对应了上一节说的三种Android主题。android:Theme是所有主题的超级父类所有的主题都是它继承或者间接继承来的。android:Theme.Holo从Api 11开始才可以使用。android:Theme.Material从Api 21开始可以使用。

如果我们要在不同版本的系统上用各自的主题,比如在4.0之下的系统用android:Theme4.0至5.0的系统用Holo主题5.0及之后的系统使用Material Design,那我们需要建不同的value-vX目录。在各自的目录中的style继承相应的系统主题。在运行是系统就会根据平台版本使用相应的主题。如果使用的主题没有找到,那么系统就会根据App指定的targetSdkVersion自动设置主题,假如设置的targetSdkVersion超过了系统的版本,系统就设置为支持的最高系统sdk版本的主题。


最后一句话怎么理解,举个例子,如果在我们在Api 24的sdk下进行开发,设置我们的应用targetSdkVersion=16,应用的资源目录下建立value-16,这是针对4.4以上平台的资源目录,在styles.xml中我们继承android:Theme.Material,这很明显是在5.0系统之上才能用的。虽然Android Studio会给出提示,但可以编译通过。现在我们把App放在一台4.4的机器上跑,这时系统是找不到android:Theme.Material这个主题的。那么系统就会看targtSdkVersion,发现是16,所以系统就会将App的主题设置为Holo的。如果我们其他的所有配置都不变,只把targetSdkVersion改成9,系统就会把App的主题设置为android:Theme的主题。这时如果把targetSdkVersion改成24,4.4的机器是不支持24的,那么出来的效果依然是android:Theme.Holo主题。


那么有同学就要问了,如果我要在Android 4.4(支持Holo主题)的机器上使用Material主题(Android 5.0)怎么办呢?(就是低版本要使用高版本的系统主题)没事,Google已经帮我们想好了解决方案。毕竟Google希望在不同的平台和版本上推广Material Design嘛。这样才能给用户提供一致性的体验。介于此,Android里就有了Theme.AppCompat主题和AppCompatActivity。细心的同学也会发现现在用Android Studio新建一个工程,默认的MainActivity继承的是AppCompatActivitiy,默认的主题就是Theme.AppCompat。


我们先来说Theme.AppCompat,这个主题可以让5.0以下的系统使用Material主题。我们只需要让我们的系统主题继承Theme.AppCompat即可。只需要指定这个就OK了,是不是很简单。

有必要说的是,使用了Theme.AppCompat之后,targetSdkVersion就不受影响了。继续拿上一节的例子说,在Api24 Sdk下开发,targtSdkVersion=9,跑在4.4的机器上,你会发现依然是Material主题。

所以可以总结出,应用使用了Theme.AppCompat主题,不论我们的targetSdkVersion指定为多少,跑在任意版本的系统上都会呈现出Material主题。


五,Android Material Design 详解(使用support v7兼容5.0以下系统)

     Material Design是Google在2014年的I/O大会上推出的全新设计语言。Material Design是基于Android 5.0(API level 21)的,兼容5.0以下的设备时需要使用版本号v21.0.0以上的support v7包中的appcpmpat,不过遗憾的是support包只支持Material Design的部分特性。使用eclipse或Android Studio进行开发时,直接在Android SDK Manager中将Extras->Android Support Library升级至最新版即可。

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.4.0'
}
1.创建一个Android应用,应用主题Theme.AppCompat(或其子主题,如Theme.AppCompat.Light.DarkActionBar)

apply plugin: 'com.android.application'

android {
    compileSdkVersion 23
    buildToolsVersion "24.0.3"

    defaultConfig {
        applicationId "com.tuke.customtheme"
        minSdkVersion 15
        targetSdkVersion 23
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:23.4.0'
}
本文中示例程序使用minSdkVersion=15,即属于使用support包实现Material Design风格

自动生成的activity默认继承AppCompatActivity

public class CustomTheme extends AppCompatActivity {//继承AppCompatActivity

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_custom_theme);
    }
}

CustomTheme的主题默认是

 <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here.自定义你的主题 -->

        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>
        
    </style>


2.如果想自定义主题,自定义程序所使用的主题的某些属性,示例:



<resources>

    <!-- Base application theme. -->
    <style name="AppTheme" parent="Theme.AppCompat.Light.DarkActionBar">
        <!-- Customize your theme here.自定义你的主题 -->

        <item name="colorPrimary">@color/colorPrimary</item>
        <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
        <item name="colorAccent">@color/colorAccent</item>

        <item name="android:textColorPrimary">@color/textColorPrimary</item>
        <item name="android:windowBackground">@color/windowBackground</item>
        <item name="android:textColor">@color/textColor</item>
        <item name="android:colorControlNormal">@color/colorControlNormal</item>
    </style>
    <style name="myTheme" parent="android:t">

    </style>
    
</resources>

1.colorPrimary                     应用的主要色调,actionBar默认使用该颜色,Toolbar导航栏的底色
2.colorPrimaryDark             应用的主要暗色调,statusBarColor默认使用该颜色
3.statusBarColor                 状态栏颜色,默认使用colorPrimaryDark
4.windowBackground          窗口背景颜色
5.navigationBarColor           底部栏颜色
6.colorForeground               应用的前景色,ListView的分割线,switch滑动区默认使用该颜色
7.colorBackground              应用的背景色,popMenu的背景默认使用该颜色
8.colorAccent                      CheckBox,RadioButton,SwitchCompat等一般控件的选中效果默认采用该颜色
9.colorControlNormal          CheckBox,RadioButton,SwitchCompat等默认状态的颜色。
10.colorControlHighlight      控件按压时的色调
11.colorControlActivated      控件选中时的颜色,默认使用colorAccent
12.colorButtonNormal          默认按钮的背景颜色
13.editTextColor:               默认EditView输入框字体的颜色。
14.textColor                         Button,textView的文字颜色
15.textColorPrimaryDisableOnly           RadioButton checkbox等控件的文字
16.textColorPrimary            应用的主要文字颜色,actionBar的标题文字默认使用该颜色
17.colorSwitchThumbNormal:             switch thumbs 默认状态的颜色. (switch off)


Android5.0设置主题样式:

<style name="RedTheme" parent="android:Theme.Material">
        <!-- 状态栏颜色,会被statusBarColor效果覆盖-->
        <item name="android:colorPrimaryDark">@color/status_red</item>
        <!-- 状态栏颜色,继承自colorPrimaryDark -->
        <item name="android:statusBarColor">@color/status_red</item>
        <!-- actionBar颜色 -->
        <item name="android:colorPrimary">@color/action_red</item>
        <!-- 背景颜色 -->
        <item name="android:windowBackground">@color/window_bg_red</item>
        <!-- 底部栏颜色 -->
        <item name="android:navigationBarColor">@color/navigation_red</item>
        <!-- ListView的分割线颜色,switch滑动区域色-->
        <item name="android:colorForeground">@color/fg_red</item>
        <!-- popMenu的背景色 -->
        <item name="android:colorBackground">@color/bg_red</item>
        <!-- 控件默认颜色 ,效果会被colorControlActivated取代  -->
        <item name="android:colorAccent">@color/control_activated_red</item>
        <!-- 控件默认时颜色  -->
        <item name="android:colorControlNormal">@color/control_normal_red</item>
        <!-- 控件按压时颜色,会影响水波纹效果,继承自colorAccent  -->
        <item name="android:colorControlHighlight">@color/control_highlight_red</item>
        <!-- 控件选中时颜色 -->
        <item name="android:colorControlActivated">@color/control_activated_red</item>
        <!-- Button的默认背景 -->
        <item name="android:colorButtonNormal">@color/button_normal_red</item>
        <!-- Button,textView的文字颜色  -->
        <item name="android:textColor">@color/white_text</item>
        <!-- RadioButton checkbox等控件的文字 -->
        <item name="android:textColorPrimaryDisableOnly">@color/white_text</item>
        <!-- actionBar的标题文字颜色 -->
        <item name="android:textColorPrimary">@color/white_text</item>
    </style>

参考文章:

http://blog.csdn.net/jack__frost/article/details/51998863

http://www.2cto.com/kf/201412/362272.html

http://blog.csdn.net/wjc295/article/details/54347807

http://www.cnblogs.com/Jude95/p/4369816.html

http://blog.csdn.net/zl18603543572/article/details/49754933

http://www.aoaoyi.com/archives/623.html

作者:tuke_tuke 发表于2017/6/13 20:14:07 原文链接
阅读:24 评论:0 查看评论

Kotlin语法基础,包引入

$
0
0

包 (package)

在Kotlin语言中为了更好地组织类,Kotlin和Java一样提供了包机制,用于区别类名的命名空间。
包的作用主要有以下几种:

  1. 把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用。
  2. 如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。
  3. 包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。
    Kotlin 使用包(package)这种机制是为了防止命名冲突,访问控制,提供搜索和定位类、接口、枚举和注解等。

包语句的语法格式为:

package com.company.apps

如一个类的路径是 com/kotlin/util/tool.kt这样保存的,它的包名则是:com.kotlin.util。package包的作用就是把它区别出来,防止被其他kotlin程序错误调用。如果没有指明包,该文件的内容属于无名字的默认包。

默认导入

在一个Kotlin项目中会有多个包,在开发的时候默认导入到每个 Kotlin 文件中,它们分别是:

  • kotlin.*
  • kotlin.annotation.*
  • kotlin.collections.*
  • kotlin.comparisons.*
  • kotlin.io.*
  • kotlin.ranges.*
  • kotlin.sequences.*
  • kotlin.text.*

根据目标平台还会导入额外的包:

  • JVM:
    • java.lang.*
    • kotlin.jvm.*
  • JS:
    • kotlin.js.*

开发者可以自己把一组类和接口等打包,并定义自己的包。而且在实际开发中这样做是值得提倡的,当你自己完成类的实现之后,将相关的类分组,可以让其他的编程者更容易地确定哪些类、接口、枚举和注释等是相关的。

由于包创建了新的命名空间(namespace),所以不会跟其他包中的任何名字产生命名冲突。使用包这种机制,更容易实现访问控制,并且让定位相关类更加简单。


导入(import)

import 关键字
为了能够使用某一个包的成员,我们需要在 Kotlin 程序中明确导入该包。使用 “import” 语句可完成此功能。
在 Kotlin 源文件中 import 语句应位于 package 语句之后,所有类的定义之前,可以没有,也可以有多条,其语法格式为:

import packagename.*
import packagename.classname

类文件中可以包含任意数量的 import 声明。import 声明必须在包声明之后,类声明之前。如果在一个包中,一个类想要使用本包中的另一个类,那么该包名可以省略。

可以导入一个单独的名字,如:

import foo.Bar // 现在 Bar

可以不用限定符访问也可以导入一个作用域下的所有内容(包、类、对象等):

import foo.* // “foo”中的一切都可访问

如果出现名字冲突,可以使用 as 关键字在本地重命名冲突项来消歧义:

import com.packt.myproject.Foo
import com.packt.otherproject.Foo as Foo2
fun doubleFoo() {
    val foo1 = Foo()
    val foo2 = Foo2()
}

这里写图片描述

作者:yzzst 发表于2017/6/13 20:42:46 原文链接
阅读:12 评论:0 查看评论

Kotlin 从学习到 Android 第十一章 枚举类、嵌套类 和 密封类

$
0
0

一、枚举类

枚举类最基本的用法是实现类型安全的枚举:

enum class Direction {
    NORTH, SOUTH, WEST, EAST
}

每一个枚举常量都是一个对象,枚举常量间以 “,” 分割。

初始化

因为每个 enum 都是 enum 类的一个实例,所以可以对它们进行初始化:

enum class Color(val rgb: Int) {
        RED(0xFF0000),
        GREEN(0x00FF00),
        BLUE(0x0000FF)
}

匿名类

枚举常量还可以声明它们自己的匿名类:

enum class ProtocolState {
    WAITING {
        override fun signal() = TALKING
    },

    TALKING {
        override fun signal() = WAITING
    };// 枚举类中定义了成员(下面的 signal() 函数),所以要用分号分开

    abstract fun signal(): ProtocolState
}

枚举常量的匿名类不仅可以定义自己的函数,也可以覆写枚举类的函数。注意:如果枚举类定义了任何成员,你必须用分号来分隔枚举常量和成员。

枚举常量的使用

和 Java 类似,Kotlin 中的枚举类也有列出已定义的枚举常量方法和通过枚举常量的名称获取一个枚举常量的方法。例如(假定枚举类的名称为 EnumClass):

EnumClass.valueOf(value: String): EnumClass
EnumClass.values(): Array<EnumClass>

如果枚举常量匹配不到 valueOf() 方法参数的值,那么将会抛出一个 IllegalArgumentException 。

从 Kotlin 1.1 开始,可以使用 enumValues() 和 enumValueOf() 函数通过泛型的方式来访问枚举类中的枚举常量:

enum class RGB { RED, GREEN, BLUE }

inline fun <reified T : Enum<T>> printAllValues() {
    print(enumValues<T>().joinToString { it.name })
}

printAllValues<RGB>() // RED, GREEN, BLUE

每一个枚举常量都有 name 和 ordinal 属性,分别标示这个常量在枚举类中的名称和次序:

println(RGB.BLUE.name)          // BLUE
println(RGB.BLUE.ordinal)       // 2

枚举常量也实现了 Comparable 接口,它们的自然顺序就是在枚举类中定义的顺序。


二、嵌套类

类中可以嵌套其它的类:

class Outer {
    private val bar: Int = 1
    class Nested {
        fun foo() = 2
    }
}

val demo = Outer.Nested().foo() // == 2

内部类

一个被 inner 标记的内部类可以访问外部类的成员。内部类对外部类的对象有一个引用:

class Outer {
    private val bar: Int = 1
    inner class Inner {
        fun foo() = bar
    }
}

val demo = Outer().Inner().foo() // == 1

匿名内部类

匿名内部类是通过 对象表达式 进行实例化的:

window.addMouseListener(object: MouseAdapter() {
    override fun mouseClicked(e: MouseEvent) {
        // ...
    }

    override fun mouseEntered(e: MouseEvent) {
        // ...
    }
})

如果这个匿名对象是 java 接口的实例化,并且这个接口只有一个抽象方法,那么你可以直接用 接口名lambda表达式 的方式来实例化:

val listener = ActionListener { println("clicked") }

三、密封类

密封类用于表示受限制的类层次结构,即一个值可以从一个有限的集合中拥有一个类型,但是不能有任何其他类型。从某种意义上说,它们是枚举类的扩展:枚举类型的值集也受到限制,但每个枚举常量仅作为单个实例存在,而密封类的子类可以包含多个实例并包含状态。
要声明一个密封类,你可以在类名之前添加 sealed 修饰符。一个密封类可以有子类,但是所有的类都必须在密封类本身所在的文件中声明。(在 Kotlin 1.1 之前,规则更加严格:类必须嵌套在密封类的声明中)。

sealed class Expr
data class Const(val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()

上面的例子使用了 Kotlin 1.1 的另一个新特性: 数据类 扩展其他类的功能,包括密封类。

注意:那些继承了一个密封类的子类(间接继承)的类可以放在任何地方,不一定是在同一个文件中。

// file01.kt
fun main(args: Array<String>) {

    fun eval(expr: Expr): Double = when(expr) {
        is Const -> expr.number
        is Sum -> eval(expr.e1) + eval(expr.e2)
        is TestN -> 20e0
        NotANumber -> Double.NaN
        // 由于覆盖了所有的可能性,所以不需要 else 分支
    }

    print(eval(TestA()))                // 20.0
}
sealed class Expr
data class Const(open val number: Double) : Expr()
data class Sum(val e1: Expr, val e2: Expr) : Expr()
object NotANumber : Expr()
open class TestN : Expr()

// file02.kt
class TestA() : TestN()

使用密封类的关键好处是当你在使用一个 when表达式 时,如果可以验证语句覆盖了所有的情况,那么就不需要向语句中添加 else 子句。

作者:niuzhucedenglu 发表于2017/6/13 23:11:48 原文链接
阅读:130 评论:0 查看评论

Flutter实战一Flutter聊天应用(八)

$
0
0

现在,我们将使用Firebase服务将聊天消息数据存储并同步到公用共享实时数据库上的云。我们需要使用firebase_database插件,用于在Firebase数据库存储和同步消息,还需要使用firebase_animated_list插件,用于增强聊天消息列表。在main.dart文件中,确保导入相应的包。

import 'package:firebase_database/firebase_database.dart';
import 'package:firebase_database/ui/firebase_animated_list.dart';

Firebase控制台中,更改Firebase实时数据库的安全规则,选择“Database > 规则”,规则如下所示。

{
  "rules": {
    "messages": {
      ".read": true,
      ".write": "auth != null && auth.provider == 'google'"
    }
  }
}

上述规则允许公开的只读访问来自数据库的消息,以及用于将消息写入数据库的Google身份验证。此时,用户需要在发送消息之前登录,并且可以在不登录的情况下查看消息。

要加载用于显示的聊天消息并提交用户输入的消息,必须与Firebase实时数据库建立连接。首先,在我们的ChatScreenState控件中,定义一个名为referenceDatabaseReference成员变量。初始化此变量以获取对Firebase数据库中消息路径的引用。

class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
  //...
  final reference = FirebaseDatabase.instance.reference().child('messages');
  //...
}

应用程序现在可以使用此引用来读取和写入数据库中的特定位置,我们需要修改ChatScreenState类中的_sendMessage()方法以向数据库添加新的聊天消息。在应用程序中消息存储为文本值数组,我们使用一个数据库,每个消息都需要被定义为一个带有字段的行。

class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
  //...
  void _sendMessage({ String text }) {
    reference.push().set({
      'text': text,
      'senderName': googleSignIn.currentUser.displayName,
      'senderPhotoUrl': googleSignIn.currentUser.photoUrl,
    });
    analytics.logEvent(name: 'send_message');
  }
  //...
}

要为每个聊天消息编写一个新行,需要调用由Firebase Database API定义的push()set()方法。访问此API由Flutter Firebase Database插件提供,该插件是我们之前导入的。push()方法创建一个新的空数据库行,set()方法可以使用消息的属性(textsenderNamesenderPhotoUrl)填充它。

当发送或接收消息时,项目早期版本的动画是从列表底部垂直滑动。UI的代码采用常规的以应用为中心的动画方法,AnimationControllerTickerProvider对象管理ListView控件中的聊天消息列表。

现在我们将使用一个专门的AnimatedList控件实现相同的效果,该方法可以让我们将应用程序与FirebaseDatabaseUI库集成。它使我们能够在Flutter应用程序中执行与iOS上的UITableView或Android上的RecyclerView绑定的相同效果。它也简化了我们的代码,只需要一个animation属性来动画化该消息。

首先将ChatScreenState类中的ListView控件替换为新的FirebaseAnimatedList控件。

class ChatScreenState extends State<ChatScreen> with TickerProviderStateMixin {
  //...
  Widget build(BuildContext context) {
    return new Scaffold(
      //...
      body: new Column(children: <Widget>[
        new Flexible(
          child: new FirebaseAnimatedList(
            query: reference,
            sort: (a, b) => b.key.compareTo(a.key),
            padding: new EdgeInsets.all(8.0),
            reverse: true,
            itemBuilder: (_, DataSnapshot snapshot, Animation<double> animation) {
              return new ChatMessage(
                snapshot: snapshot,
                animation: animation
              );
            }
          )
        ),
        new Divider(height: 1.0),
        new Container(
          decoration: new BoxDecoration(
            color: Theme.of(context).cardColor,
          ),
          child: _buildTextComposer(),
        )
      ],),
    );
  }
  //...
}

FirebaseAnimatedList是由Flutter Firebase Database插件提供的自定义控件。关联的类是AnimatedList类的包装器,增强了它与Firebase数据库的交互。

FirebaseAnimatedListquery参数指定应该出现在列表中的数据库查询。在这种情况下,我们将传递数据库引用reference,该引用扩展了Query类。reverse参数将列表的开头定义为屏幕的底部,靠近文本输入。sort参数指定显示消息的顺序。要在列表开头(屏幕底部)到达时显示消息,需要传递一个比较传入消息时间戳key的功能。

对于itemBuilder属性,将第二个参数从int index(正在构建的行的位置)更改为名为snapshotDataSnapshot对象。顾名思义,snapshot表示数据库中行的(只读)内容。FirebaseAnimatedList将使用此构建器在滚动到视图中时按需填充列表行。

最后,在ChatScreenStatebuild()方法返回的ChatMessage控件中,将text属性更改为snapshot。Flutter Firebase Database插件将snapshot定义为只有一个key及其value

到现在,我们的应用程序一直在管理自己的ChatMessage控件列表,并使用它来填充ListView。现在我们将使用一个FirebaseAnimatedList,它管理动画控制器,并自动使用Firebase数据库查询的结果填充列表。我们将使用FirebaseAnimatedList传递到应用程序的Animation对象来更改ChatMessage控件来构建其CurvedAnimation

ChatMessage类的默认构造函数中,将AnimationController更改为Animation对象。

class ChatMessage extends StatelessWidget {
  ChatMessage({this.snapshot, this.animation});
  final DataSnapshot snapshot;
  final Animation animation;
  //...
}

同时,让我们将消息内容的字段从文本字符串更新为DataSnapshot。使用AnimatedList语法意味着从应用程序修改和删除几行代码,修改CurvedAnimation对象以使用新的animation字段,而不是将animationController作为其父项。

class ChatMessage extends StatelessWidget {
  //...
  Widget build(BuildContext context) {
    return new SizeTransition(
      sizeFactor: new CurvedAnimation(
        parent: animation,
        curve: Curves.easeOut
      ),
      //...
    );
  }
  //...
}

ChatScreenState类定义中删除TickerProviderStateMixin控件和List变量。

class ChatScreenState extends State<ChatScreen> {
  final TextEditingController _textController = new TextEditingController();
  final reference = FirebaseDatabase.instance.reference().child('messages');
  bool _isComposing = false;
  //...
}

还要删除不再需要的dispose()方法。

class ChatScreenState extends State<ChatScreen> {
  //...
//  @override
//  void dispose() {
//    for(ChatMessage message in _messages)
//      message.animationController.dispose();
//    super.dispose();
//  }
  //...
}

现在,我们可以调整使用用户配置文件信息的UI控件。以下控件需要从Firebase Database API获取以下信息:

  • GoogleUserCircleAvatar
  • Text控件(发送人的姓名)
  • Text控件(消息内容)

而不是从GoogleSignIn实例获取此信息,我们将修改控件以从DataSnapshot对象的value字段获取此信息。

class ChatMessage extends StatelessWidget {
  //...
  Widget build(BuildContext context) {
    return new SizeTransition(
      //...
      child: new Container(
        margin: const EdgeInsets.symmetric(vertical: 10.0),
        child: new Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            new Container(
              margin: const EdgeInsets.only(right: 16.0),
              child: new GoogleUserCircleAvatar(snapshot.value['senderPhotoUrl']),
            ),
            new Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                new Text(
                  snapshot.value['senderName'],
                  style: Theme.of(context).textTheme.subhead),
                new Container(
                  margin: const EdgeInsets.only(top: 5.0),
                  child: new Text( snapshot.value['text'] ),
                )
              ]
            )
          ]
        )
      )
    );
  }
  //...
}

由于初始化状态对象需要重新启动应用程序,因此我们需要重新加载应用程序。

这里写图片描述

只使用单个设备,我们可以在Firebase控制台中的数据库下查看消息:

这里写图片描述

如果我们有两台设备连接到开发机器,那么我们则可以通过Firebase数据库发送消息并将其看到另一台设备的消息。

作者:hekaiyou 发表于2017/6/13 23:24:33 原文链接
阅读:77 评论:0 查看评论

微信小程序开发(四)获取用户openid

$
0
0

在小程序里面有两个地方获取用户的openid。
一个是wx.login(OBJECT),第二个是wx.getUserInfo(OBJECT)
这里我使用的是第一种wx.login(OBJECT)

步骤

 wx.login({
  success: function(res) {
    if (res.code) { //  第一步: 获取code
      //发起网络请求
      wx.request({
        url: '后台接口',  // 获取openid
        data: {
          code: res.code
        }
      })
    } else {
      console.log('获取用户登录态失败!' + res.errMsg)
    }
  }
});

后端的实现

后端的实现就是后端调用这个接口:https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
这里写图片描述

/*
 * 根据code获取微信用户的openid
 */
router.get('/api/getWxCode', function(req, res, next) {
    var param = req.query || req.params; 
    var code = param.code;
    var urlStr = 'https://api.weixin.qq.com/sns/jscode2session?appid=' + wxConfig.AppID + '&secret=' + wxConfig.Secret + '&js_code=' + code + '&grant_type=authorization_code';
    request(urlStr, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            var jsBody = JSON.parse(body); 
            jsBody.status = 100;
            jsBody.msg = '操作成功';
            res.end(JSON.stringify(jsBody));
        }
    })
});

具体实例

/**
 * 生命周期函数--监听页面加载
 */
onLoad: function (options) {
  var self = this;
  wx.login({
    success: function (res) {
      if (res.code) {
        //发起网络请求
        wx.request({
          url: 'https://www.hgdqdev.cn/api/getWxCode',
          data: {
            code: res.code
          },
          success: function(res){
            if(res.data.status == 100){
              self.setData({
                openid: res.data.openid
              })
            }
          },
          fail: function(){

          }
        })
      } else {
        console.log('获取用户登录态失败!' + res.errMsg)
      }
    }
  });
},
作者:zhuming3834 发表于2017/6/13 12:05:07 原文链接
阅读:22 评论:0 查看评论

微信小程序开发(五)小程序支付

$
0
0

准确来说小程序的支付在上个月就已经做完了,只是那个时候项目原型和UI还没出来就没正式动工。现在项目快做完了,就有时间写博客了。
在做小程序支付希望你已经熟读微信的文档微信支付-小程序-手机端微信支付-小程序-后台。且你已经有了

    AppID: "wx****************",  // 小程序ID  
    Secret: "********************************",  // 小程序Secret
    Mch_id: "**********", // 商户号
    Mch_key: "********************", // 商户key

关于上面的这4个数据的获取,请自行在自己的账号中获取和设置。且你已经有了用户的openid。《微信小程序开发(四)获取用户openid》

小程序接口

wx.requestPayment({
   'timeStamp': '',
   'nonceStr': '',
   'package': '',
   'signType': 'MD5',
   'paySign': '',
   'success':function(res){
   },
   'fail':function(res){
   }
})

小程序接口就暴露这一个方法。这个方法有4个参数是需要后台去获取的。
这里写图片描述
其实大部分工作都是后台的事情。

后端实现

后端主要是统一下单的这个接口https://api.mch.weixin.qq.com/pay/unifiedorder
这里主要就是几个签名算法

统一下单签名

// 生成签名
function paysignjsapi(appid,body,mch_id,nonce_str,notify_url,openid,out_trade_no,spbill_create_ip,total_fee) {
    var ret = {
        appid: appid,
        body: body,
        mch_id: mch_id,
        nonce_str: nonce_str,
        notify_url:notify_url,
        openid:openid,
        out_trade_no:out_trade_no,
        spbill_create_ip:spbill_create_ip,
        total_fee:total_fee,
        trade_type: 'JSAPI'
    };
    var str = raw(ret);
    str = str + '&key='+key;
    var md5Str = cryptoMO.createHash('md5').update(str).digest('hex');
    md5Str = md5Str.toUpperCase();
    return md5Str;
};
function raw(args) {
    var keys = Object.keys(args);
    keys = keys.sort(); 
    var newArgs = {};
    keys.forEach(function(key) {
        newArgs[key.toLowerCase()] = args[key];
    });

    var str = '';
    for(var k in newArgs) {
        str += '&' + k + '=' + newArgs[k];
    }
    str = str.substr(1);
    return str;
};

小程序paySign

function paysignjs(appid, nonceStr, package, signType, timeStamp) {
    var ret = {
        appId: appid,
        nonceStr: nonceStr,
        package: package,
        signType: signType,
        timeStamp: timeStamp
    };
    var str = raw1(ret);
    str = str + '&key='+key;
    return cryptoMO.createHash('md5').update(str).digest('hex');
};

function raw1(args) {
    var keys = Object.keys(args);
    keys = keys.sort()
    var newArgs = {};
    keys.forEach(function(key) {
        newArgs[key] = args[key];
    });

    var str = '';
    for(var k in newArgs) {
        str += '&' + k + '=' + newArgs[k];
    }
    str = str.substr(1);
    return str;
};

统一下单后端实现

var cryptoMO = require('crypto'); // MD5算法
var parseString = require('xml2js').parseString; // xml转js对象

var key = wxConfig.Mch_key;
/*
 * 根据openid 发起微信支付  
 */
router.all('/api/wxpay/unifiedorder', function(req, res, next) {
    var param = req.query || req.params; 
    var openid = param.openid;

    var spbill_create_ip = req.ip.replace(/::ffff:/, ''); // 获取客户端ip
    var body = '测试支付'; // 商品描述
    var notify_url = 'https://www.hgdqdev.cn/api/wxpay' // 支付成功的回调地址  可访问 不带参数
    var nonce_str = getNonceStr(); // 随机字符串
    var out_trade_no = wxConfig.getWxPayOrdrID(); // 商户订单号
    var total_fee = '1'; // 订单价格 单位是 分
    var timestamp = Math.round(new Date().getTime()/1000); // 当前时间

    var bodyData = '<xml>';
    bodyData += '<appid>' + wxConfig.AppID + '</appid>';  // 小程序ID
    bodyData += '<body>' + body + '</body>'; // 商品描述
    bodyData += '<mch_id>' + wxConfig.Mch_id + '</mch_id>'; // 商户号
    bodyData += '<nonce_str>' + nonce_str + '</nonce_str>'; // 随机字符串
    bodyData += '<notify_url>' + notify_url + '</notify_url>'; // 支付成功的回调地址 
    bodyData += '<openid>' + openid + '</openid>'; // 用户标识
    bodyData += '<out_trade_no>' + out_trade_no + '</out_trade_no>'; // 商户订单号
    bodyData += '<spbill_create_ip>' + spbill_create_ip + '</spbill_create_ip>'; // 终端IP
    bodyData += '<total_fee>' + total_fee + '</total_fee>'; // 总金额 单位为分
    bodyData += '<trade_type>JSAPI</trade_type>'; // 交易类型 小程序取值如下:JSAPI
    // 签名
    var sign = paysignjsapi(
        wxConfig.AppID,
        body, 
        wxConfig.Mch_id, 
        nonce_str,
        notify_url, 
        openid, 
        out_trade_no, 
        spbill_create_ip, 
        total_fee
    );
    bodyData += '<sign>' + sign + '</sign>';
    bodyData += '</xml>';
    // 微信小程序统一下单接口
    var urlStr = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
    request({
        url: urlStr,
        method: 'POST',
        body: bodyData
    }, function (error, response, body) {
        if (!error && response.statusCode == 200) {
            var returnValue = {};
            parseString(body, function (err, result) {
                if (result.xml.return_code[0] == 'SUCCESS') {
                    returnValue.msg = '操作成功';
                    returnValue.status = '100';
                    returnValue.out_trade_no = out_trade_no;  // 商户订单号
                    // 小程序 客户端支付需要 nonceStr,timestamp,package,paySign  这四个参数
                    returnValue.nonceStr = result.xml.nonce_str[0]; // 随机字符串
                    returnValue.timestamp = timestamp.toString(); // 时间戳
                    returnValue.package = 'prepay_id=' + result.xml.prepay_id[0]; // 统一下单接口返回的 prepay_id 参数值
                    returnValue.paySign = paysignjs(wxConfig.AppID, returnValue.nonceStr, returnValue.package, 'MD5',timestamp); // 签名
                    res.end(JSON.stringify(returnValue));
                } else{
                    returnValue.msg = result.xml.return_msg[0];
                    returnValue.status = '102';
                    res.end(JSON.stringify(returnValue));
                }
            });
        }
    })
});

实例

// 支付按钮点击事件
  payTap: function(){
    var self = this;
    wx.request({
      url: 'https://www.hgdqdev.cn/api/wxpay/unifiedorder',
      data: {
        openid: self.data.openid   // 这里正常项目不会只有openid一个参数
      },
      success: function(res){
        if(res.data.status == 100){
          var payModel = res.data;
          wx.requestPayment({
            'timeStamp': payModel.timestamp,
            'nonceStr': payModel.nonceStr,
            'package': payModel.package,
            'signType': 'MD5',
            'paySign': payModel.paySign,
            'success': function (res) {
              wx.showToast({
                title: '支付成功',
                icon: 'success',
                duration: 2000
              })
            },
            'fail': function (res) {
            }
          })
        }
      },
      fail: function(){

      }
    })
  },

这里写图片描述

作者:zhuming3834 发表于2017/6/13 14:40:43 原文链接
阅读:25 评论:0 查看评论

微信小程序开发(六)小程序支付notify_url

$
0
0

《微信小程序开发(五)小程序支付》里的微信支付里有一个notify_url(https://www.hgdqdev.cn/api/wxpay)。notify_url是位置支付成功后的一个通知地址:接收微信支付异步通知回调地址,通知url必须为直接可访问的url,不能携带参数。
这里存在一个问题就是怎么获取微信通知过来的数据。支付结果通知文档
这里写图片描述
具体实现
我的后台是node.js + express4;
1.添加依赖body-parser-xml,这个的使用看文档即可。
2.修改app.js

var express = require('express'),
bodyParser = require('body-parser');
require('body-parser-xml')(bodyParser);

var app = express();
app.use(bodyParser.xml({
  limit: '1MB',   // Reject payload bigger than 1 MB 
  xmlParseOptions: {
    normalize: true,     // Trim whitespace inside text nodes 
    normalizeTags: true, // Transform tags to lowercase 
    explicitArray: false // Only put nodes in array if >1 
  }
}));

3.接口实现

/*
 * 微信支付回调
 */
router.all('/api/wxpay', function(req, res, next) {
    var body = req.body;
    console.log(body);
});

4.返回结果
这里写图片描述

作者:zhuming3834 发表于2017/6/13 15:21:45 原文链接
阅读:22 评论:0 查看评论

Kotlin学习之-5.3 接口

$
0
0

Kotlin学习之-5.3 接口

Kotlin中的接口和Java 8中的接口很接近。它们可以定义抽象函数,也可以实现。和抽象类的区别在于接口不能存储状态。接口可以拥有属性,但是这些属性必须是抽象的或者提供访问方法的实现。

接口使用关键字interface 来定义

interface MyInterface {
    fun bar() 
    fun foo() {
        // 可选的函数主体
    }
}

实现接口

一个类或者一个对象可以实现一个或者多个接口

class Child: MyInterface {
    override fun bar() {
        // ...
    }
}

接口中的属性

可以在接口中定义属性。在接口中定义属性既可以是抽象的,也可以提供访问方法的实现。定义在接口中的属性不能有backing field,并且在接口中定义的访问方法也不能引用他们。

interface MyInterface {
    val prop: Int // 抽象的

    val propertyWithImplementation: String
        get() = "foo"

    fun foo() {
        print(prop)
    }
}

class Child : MyInterface {
    override val prop: Int = 29
}

解决复写冲突

当我们在父类列表中定义多个类型时,很有可能会继承一个方法超过一个的实现。 例如

interface A {
    fun foo() { print("A") }
    fun bar()
}

interface B {
    fun foo() { print("B") }
    fun bar() { print("bar") }
}

class C : A {
    override fun bar() { print("bar") }
}

class D : A, B {
    override fun foo() {
        super<A>.foo()
        super<B>.foo()
    }

    override fun bar() {
        super<B>.bar()
    }
}

接口A和B都定义了foo()bar() 函数。它们俩都实现了foo(),但是只有B实现了bar(), 在A中bar()没有被描述成抽象的,因为当函数没有主体时,这是接口的默认写法。 现在如果我们继承A来写一个实体类C,必须复写bar()函数并提供它的实现。

然而,如果继承A 和B 来写类D,我们需要实现所有从接口中重复继承的方法,并且写明D 是如何实现他们的。这条规则既适用单继承的方法,也适用于多继承的方法。


PS,我会坚持把这个系列写完,有问题可以留言交流,也关注专栏Kotlin for Android Kotlin安卓开发

作者:farmer_cc 发表于2017/6/14 8:47:04 原文链接
阅读:44 评论:0 查看评论

Nibs真的有必要吗?

$
0
0

因为nibs本质上只是一系列资源的实例,你可能觉得是否有可能完全不用它们。这些相同的实例可以用代码创建,所以难道不可能完全省掉(nibs)吗?

简单的说:可以!完全有可能写一个复杂的app省掉单独的.storyboard或者.xib文件。但实际的答案是:要注重平衡性!

大多数app使用nib文件作为至少包含若干界面对象的资源;但是这里有些界面对象只能在代码中被定制,并且有时候从最初就用代码创建界面对象更加容易。

在真实的项目中,将可能混杂一些代码生成的界面对象和nib生成的介么对象(其中一些可能自身会在之后被代码修改)。

作者:mydo 发表于2017/6/14 9:11:28 原文链接
阅读:34 评论:0 查看评论

Android开发笔记(一百四十八)自定义输入法软键盘

$
0
0
手机上输入文字,都是通过系统自带的软键盘,这个软键盘可以是Android自带的,也可以是第三方软键盘如搜狗输入法。多数情况下面,系统自带的软键盘已经够用了,可是总有少数情况,系统软键盘无法满足开发者的要求,比如以下几个需求,系统软键盘就无法处理:
1、像手机号码与支付密码,只需要输入数字,连标点符号都不需要。然而系统软键盘即使切换到123数字模式,依旧显示包括标点符号在内的冗余按键。
2、系统软键盘固定在屏幕下方弹出,无法做为控件嵌入到页面布局中,更无法指定软键盘的显示位置。
3、系统软键盘会自动响应EditText的焦点变更事件,常常在意料之外突然之间蹦出来,弄得开发者要么剥夺EditText的焦点,要么强行关闭软键盘显示,但无论哪种方式都得开发者强行**,很不方便。
基于以上情况,要想满足这些定制需求,只能对输入法自定义软键盘了。全数字的软键盘界面倒也简单,下面先来个数字键盘的效果图。


这个键盘只有0-9十个数字,再加一个退格键,可谓十个兄弟家徒四壁,真是再直白不过了。那么这个软键盘又是如何实现的呢?其实它跟平常的自定义控件基本类似,只在细节上有所差异,下面分步说明自定义软键盘的过程。
1、我们知道,自定义控件要么重写onDraw方法来绘制控件界面,要么从layout布局文件中加载控件界面。软键盘采取的是后一种方式,只不过它的布局文件不是放在res/layout目录,而是保存在res/xml目录。
2、自定义控件的主要工作是书写自定义的控件类,自定义软键盘也不例外,有了自定义的控件类,才能处理十个数字键的按键动作,才能把软键盘做为普通的控件嵌入到其它布局文件中。
3、软键盘不是一个孤立的控件,它的按键动作需要实时在某个编辑框中把数字显示出来,所以在使用时还得给它绑定一个EditText,这样软键盘才知道我的按键要输出给这个EditText,而不是输出给那个EditText。

俗话说,百闻不如一见,所以在说明具体的实现步骤之前,还是先看看最终的软键盘使用动图,带上这个感性认识去学习会更有帮助。


接下来阐述自定义软键盘的三个步骤,首先要定义软键盘的布局文件,在res/xml目录创建名为inputkeyboard.xml的文件,内部的根节点为Keyboard,其下挂了四个Row节点表示有四行,每个Row节点下又挂了三个Key节点,表示每行有三个按键。完整的键盘布局文件如下所示:
<Keyboard xmlns:android="http://schemas.android.com/apk/res/android"
	android:keyWidth="34%p" android:horizontalGap="1px"
	android:verticalGap="1px" android:keyHeight="55dp">
	<Row>
		<Key android:codes="49" android:keyLabel="1"/>
		<Key android:codes="50" android:keyLabel="2" />
		<Key android:codes="51" android:keyLabel="3"/>
	</Row>
	<Row>
		<Key android:codes="52" android:keyLabel="4" />
		<Key android:codes="53" android:keyLabel="5" />
		<Key android:codes="54" android:keyLabel="6" />
	</Row>
	<Row>
		<Key android:codes="55" android:keyLabel="7" />
		<Key android:codes="56" android:keyLabel="8" />
		<Key android:codes="57" android:keyLabel="9" />
	</Row>
	<Row>
		<Key android:codes="-3"
			android:keyEdgeFlags="left"
			android:keyIcon="@drawable/sym_keyboard_done" />
		<Key android:codes="48" android:keyLabel="0" />
		<Key android:codes="-5"
			android:isRepeatable="true"
			android:keyEdgeFlags="right"
			android:keyIcon="@drawable/sym_keyboard_delete" />
		</Row>
</Keyboard>
上面这个xml键盘布局,到时候将作为自定义属性传给软键盘控件,所以要在res/values/attrs.xml中补充下列属性配置:
    <declare-styleable name="keyboard">
        <attr name="xml" format="reference" />
    </declare-styleable>

然后是编写自定义软键盘的控件代码了,这里的关键是用自定义的键盘布局替换掉系统默认的键盘布局,自定义代码如下所示:
public class KeyboardLayout extends LinearLayout {
	private KeyboardView mKeyboardView;
	private Keyboard mKeyboard;

	public KeyboardLayout(Context context, AttributeSet attrs, int defStyle) {
		super(context, attrs, defStyle);
		initKeyboard(context, attrs);
	}
	
	private void initKeyboard(Context context, AttributeSet attrs){
		TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.keyboard);
		if (a.hasValue(R.styleable.keyboard_xml)) {
			//从xml文件中获取键盘布局
			int xmlid = a.getResourceId(R.styleable.keyboard_xml,0);
			mKeyboard = new Keyboard(context, xmlid);
			mKeyboardView = (KeyboardView)LayoutInflater.from(context).inflate(R.layout.keyboardview, null);
			//为键盘视图设置自定义的键盘布局
			mKeyboardView.setKeyboard(mKeyboard);
			mKeyboardView.setEnabled(true);  
			mKeyboardView.setPreviewEnabled(false);  
			addView(mKeyboardView);
		}
	}
}

最后要给软键盘绑定对应的EditText对象,即当软键盘发生按键动作时,要把按键结果显示在哪个EditText上。这个操作就是调用KeyboardView的setOnKeyboardActionListener方法,设置一个键盘事件监听器,监听器内部主要实现了onKey方法,每当发现合法的按键事件(0-9与退格键),则同步修改EditText对象的文本。这部分代码补充到前面的自定义控件类KeyboardLayout之中:
	public void setInputWidget(EditText et) {
		mKeyboardView.setOnKeyboardActionListener(new KeyboardListener(et));
	}

	private class KeyboardListener implements OnKeyboardActionListener {
		private EditText et;
		
		public KeyboardListener(EditText et) {
			this.et = et;
		}
		
		@Override
		public void onKey(int primaryCode, int[] keyCodes) {
			Editable editable = et.getText();
			int start = et.getSelectionStart();
			if (primaryCode == Keyboard.KEYCODE_DELETE) { //退格键
				if (editable != null && editable.length() > 0) {
					if (start > 0) {
						editable.delete(start - 1, start);
					}
				}
			} else if(primaryCode>='0' && primaryCode<='9') {
				//可以直接输入的字符(如0-9),它们在键盘映射xml中的keycode值必须配置为该字符的ASCII码
				editable.insert(start, Character.toString((char) primaryCode));
			}
		}

		//此处省略其它无需具体实现的Override函数
	};

至此我们可以像使用其它控件一样直接把软键盘加入到页面布局啦,注意指定键盘布局的自定义属性:
    <com.example.exmtextinput.widget.KeyboardLayout
        android:id="@+id/kl_input"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        mykeyboard:xml="@xml/inputkeyboard" />


点此查看Android开发笔记的完整目录

__________________________________________________________________________
本文现已同步发布到微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。
作者:aqi00 发表于2017/6/14 9:45:36 原文链接
阅读:0 评论:0 查看评论

[Unity 设计模式]桥接模式(BridgePattern)

$
0
0

1.前言

继上一讲IOC模式的基础上继续本讲桥接模式,笔者感觉桥接模式是23种设计模式中桥接模式是最好用但也是最难理解的设计模式之一,23中设计模式就好武侠剧中一本武功秘籍,我们在工作过程中想要熟练运用其中的每一种设计模式就好比跟高手过招想要能运用好武侠秘籍中的每一招每一式,并且能随着对手出招的不同我们能随机应变对应的招数,这就要求我们对每一种设计模式都理解的非常深刻才能运用自如,打出组合拳的效果。

2.需求

我们在FPS类游戏中会碰到这样的需求——实现武器和角色,无论是敌人还是我方角色都能通过不同的武器击杀对方,武器有手枪、散弹枪、以及火箭炮,并以”攻击力”和”攻击距离”来区分他们的威力。我们可能会这样实现:
这里写图片描述
上面是我们的一个UML示例图,这里笔者很惭愧没用过UML专业的绘图工具,就临时用绘图软件简单的画了一下。下面就是我们的初步设计代码。

  • 角色基类
public abstract class ICharacter
{
    //拥有一把武器
    protected Weapon m_Weapon = null;
    //攻击目标
    public abstract void Attack(ICharacter target);
}
  • 武器类
using UnityEngine;

public enum ENUM_Weapon
{
    Null = 0,
    Gun,
    Rifle,
    Rocket,
}

public class Weapon
{
    protected ENUM_Weapon m_EmEwapon = ENUM_Weapon.Null;
    protected int m_AtkValue = 0;//攻击力
    protected int m_AtkRange = 0;//攻击距离
    protected int m_AtkPlusValue = 0;//额外加成

    public Weapon(ENUM_Weapon type, int atkValue, int atkRange)
    {
        m_EmEwapon = type;
        m_AtkValue = atkValue;
        m_AtkRange = atkRange;
    }

    public ENUM_Weapon GetWeaponType()
    {
        return m_EmEwapon;
    }

    public void Fire(ICharacter target)
    {
        //
    }

    public void SetAtkPlusValue(int atkPlusValue)
    {
        m_AtkPlusValue = atkPlusValue;
    }

    public void ShowBulletEffect(Vector3 targetPosition, float lineWidth, float displayTime)
    {

    }

    public void ShowShootEffect()
    {

    }

    public void ShowSoundEffect(string clipName)
    {

    }
}
  • 敌人使用武器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class IEnemy : ICharacter
{
    public IEnemy()
    { }

    public override void Attack(ICharacter target)
    {
        m_Weapon.ShowShootEffect();
        int atkPlusValue = 0;
        switch(m_Weapon.GetWeaponType())
        {
            case ENUM_Weapon.Gun:
                //显示武器特效
                m_Weapon.ShowBulletEffect(target.GetPosition(), 0.3f, 0.2f);
                m_Weapon.ShowSoundEffect("GunShot");
                atkPlusValue = GetAtkPluginValue(5, 20);
                break;
            case ENUM_Weapon.Rifle:
                m_Weapon.ShowBulletEffect(target.GetPosition(), 0.4f, 0.2f);
                m_Weapon.ShowSoundEffect("GunShot");
                atkPlusValue = GetAtkPluginValue(5, 20);
                break;
            case ENUM_Weapon.Rocket:
                m_Weapon.ShowBulletEffect(target.GetPosition(), 0.5f, 0.2f);
                m_Weapon.ShowSoundEffect("GunShot");
                atkPlusValue = GetAtkPluginValue(5, 20);
                break;
        }
        m_Weapon.SetAtkPlusValue(atkPlusValue);
        m_Weapon.Fire(target);
    }
    private int GetAtkPlusValue(int rate, int atkValue)
    {
        int randValue = UnityEngine.Random.Range(0, 100);
        if (rate > randValue)
            return atkValue;
        return 0;
    }
}
  • 玩家使用武器
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class ISoldier : ICharacter
{
    public ISoldier()
    {

    }

    public override void Attack(ICharacter target)
    {
        m_Weapon.ShowShootEffect();
        switch(m_Weapon.GetWeaponType())
        {
            case ENUM_Weapon.Gun:
                m_Weapon.ShowBulletEffect(target.GetPosition(), 0.03f, 0.2f);
                m_Weapon.ShowSoundEffect("GunShot");
                break;
            case ENUM_Weapon.Rifle:
                m_Weapon.ShowBulletEffect(target.GetPosition(), 0.5f, 0.2f);
                m_Weapon.ShowSoundEffect("RifleShot");
                break;
            case ENUM_Weapon.Rocket:
                m_Weapon.ShowBulletEffect(target.GetPosition(), 0.8f, 0.2f);
                m_Weapon.ShowSoundEffect("RocketShot");
                break;
        }
        m_Weapon.Fire(target);
    }
}

3.初步分析

以上实现存在明显两个缺点:

  • 每当添加一个角色时,继承自ICharacter接口的角色重新定义Attack都必须针对不同的武器进行判断,并且要写重复的相同的代码。
  • 每当新增武器时,所有角色的Attack都需要重新改造,这样增加维护成本。
    为了解决以上问题,下面我们桥接模式隆重登场了!

4.桥接模式

桥接模式(Bridge Pattern):桥接模式的用意是将抽象化(Abstraction)与实现化(Implementation)脱耦,使得二者可以独立地变化。
以上是桥接模式的官方定义,我们还是通过以上例子来理解分析,”桥接”顾名思义就是这一组跟那一组两组对象通过某种中间关系链接在一起。”当两个群组因为功能上的需求,想要进行链接合作,但又希望两组类可以自行发展互相不受对方变化而影响”,上面角色跟武器的实现案例,武器和角色分别是两组群组,其实上面的实现只是考虑了角色通过子类通过基类继承来实现,然后传入武器对象,这中实现思路有点上一讲介绍的控制反转的味道,那既然角色可以这样设计,为何不把武器也这样设计呢,然后两个群组之间就通过两个群组的接口来实现,就好比两家结婚,双方都有一个媒人来传递信息,这是我对桥接模式的理解,哈哈。基于这种想法,我们来改造一下角色和武器的实现。
这里写图片描述
说明:

  • ICharacter:角色的抽象接口拥有一个IWeapon的对象引用,并在接口中申明了一个武器攻击目标WeaponAttackTarget()方法让子类可以调用,同时要求继承子类必须在Attack()中重新实现攻击目标的功能。
  • ISoldier、IEnemy:双方阵容单位实现攻击目标Attack()时,只需要调用父类的WeaponAttackTarget方法就可以使用当前武器攻击对手。
  • IWeapon:武器接口,定义游戏中对于武器的操作和使用方法。
  • WeaponGun、WeaponRifle、WeaponRocket:游戏中可以使用三中武器对象。

5.桥接模式实现武器和角色功能

  • 武器接口
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class IWeapon
{
    //属性
    protected int m_AtkPlusValue = 0;
    protected int m_Atk = 0;
    protected float m_Range = 0;

    protected GameObject m_GameObject = null;
    protected ICharacter m_WeaponOwner = null;//武器拥有者

    //发射特效
    protected float m_EffectDisplayTime = 0;
    protected ParticleSystem m_Particles;
    protected AudioSource m_Audio;

    //显示子弹特效
    protected void ShowBulletEffect(Vector3 targetPosition, float disPlayTime)
    {
        m_EffectDisplayTime = disPlayTime;
    }

    //显示枪口特效
    protected void ShowShootEffect()
    {
        if(m_Particles != null)
        {
            m_Particles.Stop();
            m_Particles.Play();
        }
    }

    //显示音效
    protected void ShowSoundEffect(string clipName)
    {
        if (m_Audio == null)
            return;
        IAssetFactory factory = Factory.GetAssetFactory();
        var clip = factory.LoadAudioClip(clipName);
        if (clip == null)
            return;
        m_Audio.clip = clip;
        m_Audio.Play();
    }

    //攻击目标
    public abstract void Fire(ICharacter target);
}
  • 手枪武器的实现
public class WeaponGun:IWeapon
{
    public WeaponGun()
    {

    }

    public override void Fire(ICharacter target)
    {
        ShowShootEffect();
        ShowBulletEffect(target.GetPosition(), 0.3f, 0.2f);
        ShowSoundEffect("GunShot");

        target.UnderAttack(m_WeaponOwner);
    }
}

其他武器同理…

  • 角色接口
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public abstract class ICharacter
{
    protected Weapon m_Weapon = null;

    public void SetWeapon(IWeapon weapon)
    {
        if (m_Weapon != null)
            m_Weapon.Release();
        m_Weapon = weapon;
        //设置武器拥有者
        m_Weapon.SetOwener(this);
    }

    //获取武器
    public IWeapon GetWeapon()
    {
        return m_Weapon;
    }

    protected void SetWeaponAtkPlusValue(int value)
    {
        m_Weapon.SetAtkPlusValue(value);
    }

    protected void WeaponAttackTarget(ICharacter target)
    {
        m_Weapon.Fire(target);
    }

    //获取武器攻击力
    public void GetAtkValue()
    {
        return m_Weapon.GetAtkValue();
    }

    /// <summary>
    /// 获得攻击距离
    /// </summary>
    /// <returns></returns>
    public float GetAttackRange()
    {
        return m_Weapon.GetAtkRange();
    }
    /// <summary>
    /// 攻击目标
    /// </summary>
    /// <param name="target"></param>
    public abstract void Attack(ICharacter target);
    /// <summary>
    /// 被其他角色攻击
    /// </summary>
    /// <param name="attacker"></param>
    public abstract void UnderAttack(ICharacter attacker);
}
  • 桥接模式角色实现
//角色接口
public class ISolider : ICharacter
{
    public override void Attack(ICharacter target)
    {
        WeaponAttackTarget(target);
    }

    public override void UnderAttack(ICharacter attacker)
    {
        ...
    }
}

//Enemy接口
public class IEnemy:ICharacter
{
    public override void Attack(ICharacter target)
    {
        SetWeaponAtkPlusValue(m_Weapon.GetAtkPlusValue);
        WeaponAttackTarget(target);
    }

    public override void UnderAttack(ICharacter attacker)
    {
        ...
    }
}

4.桥接模式改造后分析

以上设计运用桥接模式后的ICharacter就是群组”抽象类”,它定义了”攻击目标”功能,但实现攻击目标功能的却是群组”IWeapon武器类”,对于ICharacter以及其继承都不会理会IWeapon群组的变化,尤其在新增武器类的时候也不会影响角色类。对于ICharacter来说,它面对的指示IWeapon这个接口类,这让两个群组耦合度降到最低。

5.思考

结合以上案例,可以思考另外一个需求:用不同的图形渲染引擎如OpenGL、DirectX来绘制不同的图形。
提示:渲染引擎RenderEngine和图形属于两大群体,这两大群体都要单独有各自的“抽象类”抽象类RenderEngine和抽象类Shape。

  • 抽象类RenderEngine肯定要有一个抽象功能方法就是Draw(string shape),这个是被子类具体的渲染引擎重写,因为各自的引擎都有自己独特的渲染方法,所以这个Draw方法就被重写调用各个引擎自己的渲染方法GLRender()和DXRender()。
  • 抽象类Shape肯定也要有一个保护对象RenderEngine和设置这个对象的注入方法SetRenderEngine(RenderEngine engine)以及抽象方法Draw()。Draw方法是要被子类重写实现:调用renderEngine的Draw方法来绘制自己。

==================== 迂者 Aladdin CSDN博客专栏=================

MyBlog:http://blog.csdn.net/dingxiaowei2013

Unity QQ群:159875734

====================== 相互学习,共同进步 ===================

作者:s10141303 发表于2017/6/13 21:15:52 原文链接
阅读:127 评论:0 查看评论

React Native热更新方案

$
0
0

随着 React Native 的不断发展完善,越来越多的公司选择使用 React Native 替代 iOS/Android 进行部分业务线的开发,也有不少使用 Hybrid 技术的公司转向了 React Native 。虽然React Native在目前来说仍有不少的坑,不过对于以应用开发为主的App来说完全可以胜任。

概述

在iOS应用开发中,由于Apple严格的审核标准和低效率,iOS应用的发版速度极慢,这对于大多数团队来说是不能接受的,所以热更新对于iOS应用来说就显得尤其重要。而就在前不久,苹果严禁WaxPatch、JSPatch等热修复框架,不过庆幸的是采用Js热更新的React Native似乎并可没有收到多大影响。

热更新作为React Native的优势之一,相信很多人在选择使用React Native来开发应用,也是因为React Native具有的热更新特性。在热更新方案中,比较出名的有微软的 CodePush,React Native中文网的pushy,在调研的初期,我们参考了携程的jsbundle 拆分和加载优化方案,但这个方案需要改变 React Native 的打包代码及 Runtime 代码,实施难度上非常大,并且对于应用的性能提升并不明显,暂时不考虑这种方案。

热更新原理

React Native的热更新并不像原生应用更新那么复杂,React Native的热更新更像原生App的版本更新。用一个流程图表示的话如下:
这里写图片描述

热更新实现方案

当下选择使用 React Native 的项目大都是基于原有项目的基础上进行接入,即所谓的混合开发,而这些混合的代码中,为了不增加带代码的难度(理解和维护难度),也只是将部分非核心的代码RN化了。

使用React Native进行热更新,就涉及到了jsbundle的拆分和加载原理。

使用pushy进行热更新

本部分来自官方文档

安装命令

在你的项目根目录下运行以下命令:

npm install -g react-native-update-cli rnpm
npm install --save react-native-update@具体版本请看下面的表格
react-native link react-native-update

对应版本表格

React Native版本 react-native-update版本
0.26以下 1.0.x
0.27 - 0.28 2.x
0.29 - 0.33 3.x
0.34 - 当前版本 4.x

注:如果RN版本低于0.29,请使用rnpm link代替react-native link命令。
例如,我当前我的React native是0.44.3版本,则命令如下:

npm install --save react-native-update@4.x

如果上面的react-native link已成功(iOS工程和安卓工程均能看到依赖),可以跳过此步骤。成功的效果如下:
这里写图片描述
如果,没有请看下面介绍。

iOS

  1. 在XCode中的Project Navigator里,右键点击Libraries ➜ Add Files to [你的工程名]
  2. 进入node_modules ➜ react-native-update ➜ ios并选中RCTHotUpdate.xcodeproj`
  3. 在XCode中的project navigator里,选中你的工程,在 Build Phases ➜ Link Binary WithLibraries 中添加 libRCTHotUpdate.a
  4. 继续在Build Settings里搜索Header Search Path,添加$(SRCROOT)/../node_modules/react-native-update/ios
  5. Run your project (Cmd+R)

android

  1. 在android/settings.gradle中添加如下代码:
include ':react-native-update'
project(':react-native-update').projectDir = new File(rootProject.projectDir,   '../node_modules/react-native-update/android')
  1. 在android/app/build.gradle的 dependencies 部分增加如下代码:
 compile project(':react-native-update')
  1. 检查你的RN版本,如果是0.29及以上, 打开android/app/src/main/java/[…]/MainApplication.java,否则打开android/app/src/main/java/[…]/MainActivity.java。改动的地方如下:
    在文件开头增加 import cn.reactnative.modules.update.UpdatePackage;在getPackages() 方法中增加 new UpdatePackage()。

接下来需要对Bundle进行配置

配置Bundle URL

iOS

在工程target的Build Phases->Link Binary with Libraries中加入libz.tbd、libbz2.1.0.tbd。在你的AppDelegate.m文件中增加如下代码:

#import "RCTHotUpdate.h"

- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions
{
#if DEBUG
  // 原来的jsCodeLocation
  jsCodeLocation = [NSURL URLWithString:@"http://localhost:8081/index.ios.bundle?platform=ios&dev=true"];
#else
  jsCodeLocation=[RCTHotUpdate bundleURL];
#endif
  // ... 其它代码
}

Android

0.29及以后版本:在你的MainApplication中增加如下代码:

import cn.reactnative.modules.update.UpdateContext;
public class MainApplication extends Application implements ReactApplication {

  private final ReactNativeHost mReactNativeHost = new ReactNativeHost(this) {
    @Override
    protected String getJSBundleFile() {
        return UpdateContext.getBundleUrl(MainApplication.this);
    }
    // ... 其它代码
  }
}

0.28及以前版本:在你的MainActivity中增加如下代码:

import cn.reactnative.modules.update.UpdateContext;

public class MainActivity extends ReactActivity {

    @Override
    protected String getJSBundleFile() {
        return UpdateContext.getBundleUrl(this);
    }
    // ... 其它代码
}

iOS的ATS例外配置

从iOS9开始,苹果要求以白名单的形式在Info.plist中列出外部的非https接口,以督促开发者部署https协议。在我们的服务部署https协议之前,请在Info.plist中添加如下例外。具体步骤为:右键点击Info.plist,选择open as - source code。

<key>NSAppTransportSecurity</key>
<dict>
    <key>NSExceptionDomains</key>
    <dict>
        <key>reactnative.cn</key>
        <dict>
            <key>NSIncludesSubdomains</key>
            <true/>
            <key>NSExceptionAllowsInsecureHTTPLoads</key>
            <true/>
        </dict>
   </dict>
</dict>

登录与创建应用

首先请在http://update.reactnative.cn注册帐号,然后在你的项目根目录下运行以下命令:

$ pushy login
email: <输入你的注册邮箱>
password: <输入你的密码>

这会在项目文件夹下创建一个.update文件,注意不要把这个文件上传到Git等CVS系统上。你可以在.gitignore末尾增加一行.update来忽略这个文件。
登录之后可以创建应用。注意iOS平台和安卓平台需要分别创建:

$ pushy createApp --platform ios
App Name: <输入应用名字>
$ pushy createApp --platform android
App Name: <输入应用名字>

如果你已经在网页端或者其它地方创建过应用,也可以直接选择应用:

$ pushy selectApp --platform ios
1) 鱼多多(ios)
3) 招财旺(ios)

Total 2 ios apps
Enter appId: <输入应用前面的编号> 

选择或者创建过应用后,你将可以在文件夹下看到update.json文件,其内容类似如下形式:

{
    "ios": {
        "appId": 1,
        "appKey": "<一串随机字符串>"
    },
    "android": {
        "appId": 2,
        "appKey": "<一串随机字符串>"
    }
}

你可以安全的把update.json上传到Git等CVS系统上,与你的团队共享这个文件,它不包含任何敏感信息。当然,他们在使用任何功能之前,都必须首先输入pushy login进行登录。至此服务器端应用的创建/选择就已经成功了。接下来我们只需要在客户端添加相应的功能代码即可。

获取appKey

检查更新时必须提供你的appKey,这个值保存在update.json中,并且根据平台不同而不同。你可以用如下的代码获取:

import {
  Platform,
} from 'react-native';

import _updateConfig from './update.json';
const {appKey} = _updateConfig[Platform.OS];

注:如果你不使用pushy命令行,你也可以从网页端查看到两个应用appKey,并根据平台的不同来选择。

检查更新、下载更新

使用异步函数checkUpdate检查当前版本是否需要更新:

checkUpdate(appKey)
    .then(info => {
    })

返回的info有三种情况:

  1. {expired: true}:该应用包(原生部分)已过期,需要前往应用市场下载新的版本。
  2. {upToDate: true}:当前已经更新到最新,无需进行更新。
  3. {update: true}:当前有新版本可以更新。info的name、description字段可以用于提示用户,而metaInfo字段则可以根据你的需求自定义其它属性(如是否静默更新、是否强制更新等等)。另外还有几个字段,包含了完整更新包或补丁包的下载地址,react-native-update会首先尝试耗费流量更少的更新方式。将info对象传递给downloadUpdate作为参数即可。

切换版本

downloadUpdate的返回值是一个hash字符串,它是当前版本的唯一标识。你可以使用switchVersion函数立即切换版本(此时应用会立即重新加载),或者选择调用 switchVersionLater,让应用在下一次启动的时候再加载新的版本。

首次启动、回滚

在每次更新完毕后的首次启动时,isFirstTime常量会为true。 你必须在应用退出前合适的任何时机,调用markSuccess,否则应用下一次启动的时候将会进行回滚操作。 这一机制称作“反触发”,这样当你应用启动初期即遭遇问题的时候,也能在下一次启动时恢复运作。

你可以通过isFirstTime来获知这是当前版本的首次启动,也可以通过isRolledBack来获知应用刚刚经历了一次回滚操作。 并且在此处给与用户提示信息。

附完整代码:

import React, {
  Component,
} from 'react';

import {
  AppRegistry,
  StyleSheet,
  Platform,
  Text,
  View,
  Alert,
  TouchableOpacity,
  Linking,
} from 'react-native';

import {
  isFirstTime,
  isRolledBack,
  packageVersion,
  currentVersion,
  checkUpdate,
  downloadUpdate,
  switchVersion,
  switchVersionLater,
  markSuccess,
} from 'react-native-update';

import _updateConfig from './update.json';
const {appKey} = _updateConfig[Platform.OS];

class MyProject extends Component {
  componentWillMount(){
    if (isFirstTime) {
      Alert.alert('提示', '这是当前版本第一次启动,是否要模拟启动失败?失败将回滚到上一版本', [
        {text: '是', onPress: ()=>{throw new Error('模拟启动失败,请重启应用')}},
        {text: '否', onPress: ()=>{markSuccess()}},
      ]);
    } else if (isRolledBack) {
      Alert.alert('提示', '刚刚更新失败了,版本被回滚.');
    }
  }
  doUpdate = info => {
    downloadUpdate(info).then(hash => {
      Alert.alert('提示', '下载完毕,是否重启应用?', [
        {text: '是', onPress: ()=>{switchVersion(hash);}},
        {text: '否',},
        {text: '下次启动时', onPress: ()=>{switchVersionLater(hash);}},
      ]);
    }).catch(err => { 
      Alert.alert('提示', '更新失败.');
    });
  };
  checkUpdate = () => {
    checkUpdate(appKey).then(info => {
      if (info.expired) {
        Alert.alert('提示', '您的应用版本已更新,请前往应用商店下载新的版本', [
          {text: '确定', onPress: ()=>{info.downloadUrl && Linking.openURL(info.downloadUrl)}},
        ]);
      } else if (info.upToDate) {
        Alert.alert('提示', '您的应用版本已是最新.');
      } else {
        Alert.alert('提示', '检查到新的版本'+info.name+',是否下载?\n'+ info.description, [
          {text: '是', onPress: ()=>{this.doUpdate(info)}},
          {text: '否',},
        ]);
      }
    }).catch(err => { 
      Alert.alert('提示', '更新失败.');
    });
  };
  render() {
    return (
      <View style={styles.container}>
        <Text style={styles.welcome}>
          欢迎使用热更新服务
        </Text>
        <Text style={styles.instructions}>
          这是版本一 {'\n'}
          当前包版本号: {packageVersion}{'\n'}
          当前版本Hash: {currentVersion||'(空)'}{'\n'}
        </Text>
        <TouchableOpacity onPress={this.checkUpdate}>
          <Text style={styles.instructions}>
            点击这里检查更新
          </Text>
        </TouchableOpacity>
      </View>
    );
  }
}

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('MyProject', () => MyProject);

到此,你的应用已经具备了检测更新的功能,接下来我们需要将应用发布出去。
注意,从update上传发布版本到发布版本正式上线期间,不要修改任何脚本和资源,这会影响update 获取本地代码,从而导致版本不能更新。如果在发布之前修改了脚本或资源,请在网页端删除之前上传的版本并重新上传。

发布iOS应用

按照正常的发布流程打包.ipa文件(Xcode中运行设备选真机或Generic iOS Device,然后菜单中选择Product-Archive),然后运行如下命令:

pushy uploadIpa <your-package.ipa>

随后,你就可以将你的ipa文件发布到AppStore。

发布安卓应用

Android打包的流程和原生打包apk的流程一样,然后在android文件夹下运行./gradlew assembleRelease,你就可以在android/app/build/outputs/apk/app-release.apk中找到你的应用包。
然后使用如下命令,即可上传apk以供后续版本比对之用。

pushy uploadApk android/app/build/outputs/apk/app-release.apk

发布热更新版本

你可以尝试修改一行代码(譬如将版本一修改为版本二),然后生成新的热更新版本。

pushy bundle --platform <ios|android>
Bundling with React Native version:  0.22.2
<各种进度输出>
Bundled saved to: build/output/android.1459850548545.ppk
Would you like to publish it?(Y/N) 

如果想要立即发布,此时输入Y。当然,你也可以在将来使用pushy publish –platform

Uploading [========================================================] 100% 0.0s
Enter version name: <输入版本名字,如1.0.0-rc>
Enter description: <输入版本描述>
Enter meta info: {"ok":1}
Ok.
Would you like to bind packages to this version?(Y/N)

此时版本已经提交到update服务,但用户暂时看不到此更新,你需要先将特定的包版本绑定到此热更新版本上。

此时输入Y立即绑定,你也可以在将来使用pushy update –platform

Offset 0
1) FvXnROJ1 1.0.1 (no package)
2) FiWYm9lB 1.0 [1.0]
Enter versionId or page Up/page Down/Begin(U/D/B) <输入序号,U/D翻页,B回到开始,序号就是上面列表中)前面的数字>

1) 1.0(normal) - 3 FiWYm9lB (未命名)

Total 1 packages.
Enter packageId: <输入包版本序号,序号就是上面列表中)前面的数字>

到此,客户端就可以使用热更新了,不用升级相关版本。

混合app热更新

jsbundle 拆分

对 React Native 的代码打包编译后会生成一个 bundle 文件,这里要说明一下, jsbundle 的拆分是基于生成的 bundle 文件可以看成两部分构成(如下图):一是 React Native 包含的的基础类库,一是开发的业务代码。
这里写图片描述
首先需要做的就是生成 common.bundle ,新建一个 blank.android.js 文件,在文件中仅引入 react 及 react native。

import React from 'react';
import {} from 'react-native';

通过打包命令编译成 common.bundle :

react-native bundle --entry-file blank.android.js --bundle-output ~/Desktop/common.bundle --platform android --dev false

打包完整的 jsbundle ,这将会包含所有的基础类库及业务代码。
最后根据 diff 算法将两个文件进行 diff 拆分,由此会生成一个 index.diff 的二进制文件。如有多个业务代码,相应的生成多个 diff 文件即可。
这里写图片描述

bundle 文件的拷贝及合成

在完成拆分以后,我们需要将 common.bundle 及拆分的 *.diff 文件进行 zip 压缩,放入 assets 目录下,为了方便版本管理,我们将其文件名中写入版本号 jsbundle_<版本号>.zip ,例如: jsbundle_1.zip ,每次改 zip 文件包跟随发版时更新,并自动升级版本号。
接下来我们要做的就是将内置于 assets 目录下的 jsbundle_*.zip 拷贝至内部存储,这里推荐使用应用内部存储。

在拷贝过程中根据历史记录的版本号,进行判断是否需要执行拷贝,拷贝完成后将 common.bundle 及 .diff 文件进行 patch 合并,合并后的文件即为一个完整的 bundle 文件,文件名规定为 .diff.bundle ,例如: index.diff.bundle ,在加载时根据模块名进行加载即可。

diff 文件的更新

说到热更新,到这里直接更新diff文件即可,并合成新的完整 bundle 文件。接下来就是将diff 文件的生成及上传,这里我们通过一个shell脚本来完成自动上传功能。

if [ $platform == "android" ]; then
    react-native bundle \
        --entry-file $commonFile.js \
        --bundle-output $androidModuleDir/common.bundle \
        --platform android \
        --dev false

    echo "common.bundle packed!!!"

    react-native bundle \
        --entry-file $module.js \
        --bundle-output $androidModuleDir/$module.android.bundle \
        --platform android \
        --dev false

    echo "$module.android.bundle packed!!!"

    # 对 jbdiff 打成的 jar 执行文件
    chmod +x dmp.jar 

    echo "diff start =========>>>"
    java -jar ./dmp.jar $androidModuleDir/common.bundle \
        $androidModuleDir/$module.android.bundle $androidModuleDir/$module.diff
    # 进行二次 zip 压缩
    zip -j $androidModuleDir/$module.diff.zip $androidModuleDir/$module.diff
elfi ...

改造原生代码

React Native 的 bundle 文件加载做了更改,我们就不能直接使用 sdk 提供的 ReactActivity 了,对此我们需要对容器 Activity 进行改造。改造的部分如下:

public class MyReactNativeHost extends ReactNativeHost{
    ...
    protected MyReactNativeHost(Application application, String moduleName) {
        super(application);
        mApplication = application;
        mModuleName = moduleName;
    }
    ...
    @Override
    protected ReactInstanceManager createReactInstanceManager() {
        if(getUseDeveloperSupport()){ //为了保留 debug 的能力
            return super.createReactInstanceManager();
        }
        String path = JSBundleManager.getJSBundleDirPath(mApplication)
                .concat(mModuleName).concat(".diff.bundle");
        ReactInstanceManager.Builder builder = ReactInstanceManager.builder()
                .setApplication(mApplication)
                .setJSBundleLoader(JSBundleLoader.createFileLoader(path))
                .setUseDeveloperSupport(false)
                .setInitialLifecycleState(LifecycleState.BEFORE_RESUME);
        ...
        return builder.build();
    }
    ...
}

注:由于采用加载文件系统下的 bundle 文件的形式,在测试过程中发现通过此形式加载的 bundle 文件,图片加载时不能读取到 res 目录下的资源文件。要解决这个问题,主要有两个方案:1、将 js 源码中的逻辑进行修改,都从 res 中读取资源;2、将 React Native 使用到的资源打包到本地,跟随 jsbundle_*.zip 发布。
由于苹果对热更新的态度,我们暂且不谈ios的热更新,有兴趣的可以自行研究Jspath等热更新框架。

作者:xiangzhihong8 发表于2017/6/14 10:49:21 原文链接
阅读:258 评论:0 查看评论

WWDC 2017, 让我们看看 iTunesConnect 有了哪些不同

$
0
0

这里写图片描述


距离 WWDC 2017 过去已经有 7 天了,小伙伴们是不是已经发现我们的苹果后台和之前的界面有些略微的不同,如果有心的朋友下了 iOS 11 beta 版就会发现设备上的 App Store 界面已经完全改版了!没错,这次后台的微调主要就是为了适配 iOS 11。

1.App 副标题与 App 名称

这里写图片描述

官方对此解释为:您可为 App 名称添加最多 30 个字符的 App 副标题,对您的 App 进行简单的概括性介绍。如果用户使用的设备运行 iOS 11 或更高版本,App 副标题将在新的 App Store 中显示在 App 名称下方。您可在您 App 的下一次更新版本中添加副标题,如果您的 App 处于可编辑 App 状态,您可立即添加副标题。与 App 名称类似,App 副标题同样可本地化为 App Store 支持的语言。

看到这个我终于送了口气,不知大家有没有遇到因为 App 标题而被打回的情况,由于我所在的行业是游戏行业,每次更新 App 标题 都会拖一个尾巴,例如 “倩女幽魂-周年资料片开启主角剧情“扒拉扒拉的,如果你的应用描述或者app关键字中和它有重复,那很大可能就会被reject,估计腾讯游戏都怕了,你看它的手游现在都没有小尾巴了。不过现在有了副标题,就不用担心这个问题了。

在 iOS 11 中副标题显示效果:

这里写图片描述

2.宣传文本与描述

这里写图片描述

官方对此解释:您可通过添加宣传文本与潜在用户共享信息,例如关于新内容、功能和限时促销的公告。如果客户使用的设备运行 iOS 11 或更高版本,宣传文本将在新 App Store 中,显示在您产品页面中的 App 描述上方(宣传文本最多 170 个字符)。无需等到下次 App 版本更新才能添加宣传文本,您可随时更新宣传文本栏并对其进行本地化。

将来,您只有在提交 App 的新版本时才能更新 App 描述。描述的长度限制保持不变,仍不能超过 4000 个字符。

这个内容主要是为了方便用户能够在第一时间瞄一眼就能立刻抓住你这个 App 的重点信息,而不用逐字逐行的去阅读你的 App 描述。

效果图如下:

这里写图片描述

3.App 视频预览新增到三个

官方解释为:现在,您可为 App Store 支持的每种语言提供最多三个 App 预览。如果您为特定语言提供了三个 App 预览,那么当客户使用运行 iOS 11 或更高版本的设备时,他们在您的 App Store 产品页面上始终会先于屏幕快照看到您 App 的所有预览。

本次更新由原来的一个视频增加到了3个视频预览,这样尤其是对于娱乐性的应用,例如游戏提供了更好,更真实的内容体验。

效果图如下:

这里写图片描述


总结

经过这几年的市场竞争和磨合,相信苹果爸爸也不是那种不通人情的主,在 iTunesConnect 后台就可以看的出来,苹果公司也越发的将开发者以及用户的建议考虑进去,然后慢慢的在完善自己的产品,虽然说还是有很多蛋疼的地方,例如 IAP 不能批量添加,视频预览上传定帧一定要用Safari 等等。 不过我相信在未来的日子里,一定会有所改观。

最后奉上几张 iOS 11 App Store 的截图吧!

这里写图片描述

这里写图片描述

这里写图片描述


好了。祝大家生活愉快。多多收获友谊和爱情。如果想获取更多的讯息,请扫描下方二维码关注我的微信公众号:

这里写图片描述

作者:shenjie12345678 发表于2017/6/13 23:33:05 原文链接
阅读:144 评论:0 查看评论

第四十篇:GCD 多线程

$
0
0

一、Operation Objects

1、相关类

1)NSOperation 基类:

        基类,用来自定义子类 operation  object 。继承 NSOperation 可以完全控制 operation object 的实现,包括修改操作执行和状态报告的方式。


2)NSInvocationOperation:

       可以直接使用的类,基于应用的一个对象和 selector 来创建 operation object 。如果你已经有现在的方法来执行需要的任务,就可能使用该类。


3)NSBlockOperation:

       可以直接使用的类,用来并发地执行一个或多个 block 对象,operation object 使用 “组” 的语义来执行多个block对象。所有相关的 block 都执行完之后,operation object 才算完成。



2、所有 operation  object 都支持的关键特性

1)支持建立基于图的 operation objects 依赖。可以阻止某个operation 运行,直到它支持的所有 operation 都已经完成。 

2)支持可选的 completion block ,在 operation 的主任务完成后调用。

3)支持应用使用 KVO 通知来监控 operation 的执行状态。

4)支持 operation 的 优先级,从而影响相关的执行顺序。

5)支持取消,允许你中止正在执行的任务。



3、一些对象方法

1 start :(必需)所有并发操作都必需覆盖这个方法,以自定义的实现来替换默认行为。手动执行一个操作时,你会调用 start 方法。因为你对这个方法的实现是操作的起点,设置一个线程或其它执行环境,来执行你的任务,你的实现在任何时候都绝不能调用 super。


2main:(可选)这个方法通常用来实现 operation 对象相关联的任务。尽管你可以在 start 方法中执行任务,使用 main 来实现任务可让你的代码更加清晰地分离代码和任务代码。


3 isExecuting 与 isFinished:(必须)并发操作负责设置自己的执行环境,并向外部的 client 报告执行环境的状态。因此并发操作必须维护某些状态信息,以知道是否正在执行任务,是否已经完成任务。使用这两个方法报告自己的状态。

这两个方法必须能够在其他多个线程中同时调用。另外这些方法报告自己的状态变化 时,还需为这些相应的 key path 产生适当的 KVO 通知。


4isConcurrent:(必须 )标识一个操作是否并发 operation,覆盖这个方法并返回一个 YES 。

5 setThreadPriority: 设置优先级 范围(0.0 ~ 1.0 ,默认为 0.5)

6) setCompletionBlock: 设置完成后调用的 block



4、管理内存使用 NSAutoReleasePoll 




二、Operation Queue

1、NSOperationQueue

1)负责管理Operation Object ,设计是用于并发执行 Operations ,你也可以强制单个queue 一次只能执行一个 Operation 。使用 setMaxConcurrentOperationCount: 方法可以配置 operation queue 的最大并发操作数量。设为 1 就表示 queue 每次只能执行一个操作。不过 operation 执行顺序仍然依赖于其它因素,像操作是否准备好和优先级等。因此串行化的 operation queue 并不等同于 GCD 中的串行 dispatch queue。


2)一些方法:

2.1) addOperation: 方法添加一个 operation 到 queue 。

2.2) addOperations:waitUntilFinished: 方法添加一组 operations 到 queue。

2.3)addOperationWithBlock: 方法添加 block 对象到 queue 。

2.4) waitUntilAllOperationsAreFinished 方法可以同时等待一个 queue 中所有操作。在等待中还是可以向 queue 添加 Operation 加长线程的等待时间。

2.5) setMaxConcurrentOperationCount: 方法可以配置 operation queue 的最大并发操作数量。

2.6) setSuspended: 挂起 或 继续 queue 中的 Operations ,暂停等待中的任务。正在执行的 Operation 不会被暂停。


        Operation 添加到 queue 之后,通常短时间内就会行到运行。但是如果存在依赖,或者 Operations 挂起等原因,也可能需要等待。

       注意: Operation 添加到 queue 之后 ,绝对不要修改 Operations 对象。因为 Operations 可能会在任何时候运行,因此改变依赖或数据会产生不利的影响。但可以通过NSOperation 的方法来查看操作的状态,是否在运行、等待运行、已经完成等。




三、Dispatch Queues

1、几个关键点

1)dispatch queues 相对于其它的 dispatch queues并发地执行任务,串行化任务只能在同一个dispatch queues中实现。


2)系统决定了同时能够执行的任务数量,应用在 100 个不同的 queues 中启动 100 个任务,并不代表 100 个任务全部都在并发地执行(除非系统拥有 100 或 更多的核)。


3)系统在选择执行哪个任务时,会先考虑 queue 的优先级。


4)queue 中的任务必须在任何时候都准备好运行,注意这点和 Operation 对象相同。


5)private dispatch queue 是引用 计数的对象,你的代码中需要 retain 这些 queue ,另外 dispatch source 也可能添加到一个 queue,从而增加 retain 的计数。因此你必须确保所有的 dispatch queue 都被取消,而且适当地调用 release 。




2、创建 queue

1. 创建串行 queue:

dispatch_queue_t queue;
queue = dispatch_queue_create("com.example.MyQueue", NULL); 
// 串行队列的创建方法
dispatch_queue_t queue= dispatch_queue_create("test.queue", DISPATCH_QUEUE_SERIAL);
// 并发队列的创建方法
dispatch_queue_t queue= dispatch_queue_create("test.queue", DISPATCH_QUEUE_CONCURRENT);
两个参数分别是 queue 名 和 一组 queue 属性
// 串行队列的创建方法


2. 获取公共 Queue

  1)使用 dispatch_get_current_queue 函数作为调试用途。在 block 对象 调用这个函数会返回 block 提交到的 queue。在 block 之外调用这个函数会返回应用的默认并发 queue 。

   2)使用 dispatch_get_main_queue 函数获取主线程关联的串行 dispatch queue 。Cocoa 应用、调用了 disptch_main 函数或配制了 run loop 的应用,会自动创建这个 queue。

   3)使用 dispatch_get_global_queue 来获得共享的并发 queue。



3、内存管理

1)使用 dispatch_retain 和 dispatch_release 配套使用增加和减少引用计数。

2)dispatch_set_finalizer_f(queue,&functionName) 指定 queue 计数器为0时调用清理函数 function 方法  。



4、方法用途

1. dispatch_apply( count , queue , ^(size_t i) {       做一些事    }) ; queue 如果是并发性质 那么 block 中就是可并发的,如果串行性质 那么 block 一次次执行。注意:无论是哪种性质都是先执行完 dispatch_apply 所有内容才往后执行。

2. dispatch_suspend 挂起 dispatch queue ,queue 引用计数增加 。当引用计数大于 0 时,queue 保持挂起状态。异步;只在执行 block 之前生效;挂起一个 queue 不会导致正在执行的 block 停止。

3. dispatch_resume  继续 dispatch queue ,queue 引用计数减少。异步;只在执行 block 之前生效。

4. dispatch_async 异步执行,只有与并发(DISPATCH_QUEUE_CONCURRENT) queue 结合使用才能并发执行任务。有一点注意:把所有的任务添加设置完成后才开始执行的,而不是添加一个立即执行。

5. dispatch_sync 同步执行,不管与并发queue 或 同串行 queue 结合使用都是同步执行。

6. dispatch_barrier_async 栅栏方法,可隔开在同一并发 queue 里的任务分段并发执行任务,如下:

7. dispatch_after     GCD的延时并发执行方法,如:
    dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(2.0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
    // 2秒后异步执行这里的代码...
   NSLog(@"run-----");
});

8. dispatch_once     GCD一次性代码(即 只执行一次)。在创建单例 或者 有整个程序在执行的过程中只执行一次的代码时,就用到 dispathc_once 方法,保证某段代码只被执行 1 次。
   static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
    // 只执行1次的代码(这里面默认是线程安全的)
});


1)dispatch_barrier_async 栅栏用法
- (void)barrier
{
    dispatch_queue_t queue = dispatch_queue_create("12312312", DISPATCH_QUEUE_CONCURRENT);

    dispatch_async(queue, ^{
        NSLog(@"----1-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----2-----%@", [NSThread currentThread]);
    });

    dispatch_barrier_async(queue, ^{
        NSLog(@"----barrier-----%@", [NSThread currentThread]);
    });

    dispatch_async(queue, ^{
        NSLog(@"----3-----%@", [NSThread currentThread]);
    });
    dispatch_async(queue, ^{
        NSLog(@"----4-----%@", [NSThread currentThread]);
    });
}
输出结果:
2016-09-03 19:35:51.271 GCD[11750:1914724] ----1-----<NSThread: 0x7fb1826047b0>{number = 2, name = (null)}
2016-09-03 19:35:51.272 GCD[11750:1914722] ----2-----<NSThread: 0x7fb182423fd0>{number = 3, name = (null)}
2016-09-03 19:35:51.272 GCD[11750:1914722] ----barrier-----<NSThread: 0x7fb182423fd0>{number = 3, name = (null)}
2016-09-03 19:35:51.273 GCD[11750:1914722] ----3-----<NSThread: 0x7fb182423fd0>{number = 3, name = (null)}
2016-09-03 19:35:51.273 GCD[11750:1914724] ----4-----<NSThread: 0x7fb1826047b0>{number = 2, name = (null)}

2)dispatch_group 用法
dispatch_group_t group = dispatch_group_create();
    // 下面两个任务异步执行(并发)
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        for (int i = 0 ; i < 100; i++) {
            NSLog(@" 1  >>>>  %d   >>>>  ",i);
        }
        
    });
    dispatch_group_async(group, dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
        
        for (int i = 0 ; i < 100; i++) {
            NSLog(@" 2  >>>>  %d   >>>>  ",i);
        }
        
    });
    // 当上面两个任务完成后会自动调用下面 GCD 方法
    dispatch_group_notify(group, dispatch_get_main_queue(), ^{
        NSLog(@"更新 UI 在 主线程  %@",[NSThread currentThread]);
    });
    



5、线程安全性

1)dispatch queue 本身是线程安全的。你可以在应用的任意线程中提交任务到 dispatch queue ,不需要使用锁或其他同步机制。


2)不要在执行任务代码中调用 dispatch_sync 函数调度相同的 queue ,这样会死锁这个 queue。如果你需要 dispatch 到当前的 queue,需要使用 dispatch_async 函数异步调度。


3)避免在提交到 dispatch queue 的任务中获得锁,虽然在任务中使用锁是安全的,但在请求锁时,如果锁不可用,可能会完全阻塞串行 queue。类似的,并发 queue 等待锁也可能会阻止其它任务的执行。如果代码需要同步就使用串行 dispatch queue 。


4)虽然可以获得运行任务的底层线程的信息,最好不要这样做。





四、Dispatch Source

创建dispatch source :
   dispatch_source_t source = dispatch_source_create(dispatch_source_type_t type, uintptr_t handle, unsigned long mask, dispatch_queue_t queue)



参数意义:
type :	dispatch 源可处理的事件;
handle:可以理解为句柄、索引或id,假如要监听进程,需要传入进程的ID;
mask:	可以理解为描述,提供更详细的描述,让它知道具体要监听什么;
queue:	自定义源需要的一个队列,用来处理所有的响应句柄(block)。



type 可处理的所有事件:

DISPATCH_SOURCE_TYPE_DATA_ADD	自定义的事件,变量增加
DISPATCH_SOURCE_TYPE_DATA_OR	自定义的事件,变量OR
DISPATCH_SOURCE_TYPE_MACH_SEND	MACH端口发送
DISPATCH_SOURCE_TYPE_MACH_RECV	MACH端口接收
DISPATCH_SOURCE_TYPE_PROC	进程监听,如进程的退出、创建一个或更多的子线程、进程收到UNIX信号
DISPATCH_SOURCE_TYPE_READ	IO操作,如对文件的操作、socket操作的读响应
DISPATCH_SOURCE_TYPE_SIGNAL	接收到UNIX信号时响应
DISPATCH_SOURCE_TYPE_TIMER	定时器
DISPATCH_SOURCE_TYPE_VNODE	文件状态监听,文件被删除、移动、重命名
DISPATCH_SOURCE_TYPE_WRITE	IO操作,如对文件的操作、socket操作的写响应


1)一些函数

dispatch_suspend(queue) //挂起队列

dispatch_resume(source) //分派源创建时默认处于暂停状态,在分派源分派处理程序之前必须先恢复

dispatch_source_merge_data //向分派源发送事件,需要注意的是,不可以传递0值(事件不会被触发),同样也不可以传递负数。

dispatch_source_set_event_handler //设置响应分派源事件的block,在分派源指定的队列上运行

dispatch_source_get_data //得到分派源的数据

uintptr_t dispatch_source_get_handle(dispatch_source_t source); //得到dispatch源创建,即调用dispatch_source_create的第二个参数

unsigned long dispatch_source_get_mask(dispatch_source_t source); //得到dispatch源创建,即调用dispatch_source_create的第三个参数

void dispatch_source_cancel(dispatch_source_t source); //取消dispatch源的事件处理--即不再调用block。如果调用dispatch_suspend只是暂停dispatch源。

long dispatch_source_testcancel(dispatch_source_t source); //检测是否dispatch源被取消,如果返回非0值则表明dispatch源已经被取消

void dispatch_source_set_cancel_handler(dispatch_source_t source, dispatch_block_t cancel_handler); //dispatch源取消时调用的block,一般用于关闭文件或socket等,释放相关资源

void dispatch_source_set_registration_handler(dispatch_source_t source, dispatch_block_t registration_handler); //可用于设置dispatch源启动时调用block,调用完成后即释放这个block。也可在dispatch源运行当中随时调用这个函数。


2)样例

 dispatch_source_t source = dispatch_source_create(DISPATCH_SOURCE_TYPE_DATA_ADD, 0, 0, dispatch_get_global_queue(0, 0));

    dispatch_source_set_event_handler(source, ^{

        dispatch_sync(dispatch_get_main_queue(), ^{

            //更新UI
        });
    });

    // 开启 source 源
    dispatch_resume(source);

    dispatch_async(dispatch_get_global_queue(0, 0), ^{

        //网络请求


        //通知队列,触发 dispatch_source_set_event_handler 设置的 block 。
        dispatch_source_merge_data(source, 1); 
    });





五、RunLoop

1、RunLoop 相关几个类的含意及关系

1)CFRunLoopRef : 代表 RunLoop 对象;

2)CFRunLoopModeRef : 代表 RunLoop 运行模式;

3)CFRunLoopSourceRef : 就是 RunLoop 模型的 输入源 或 事件源;

4)CFRunLoopTimerRef : 就是 RunLoop 模型的定时源;

5)CFRunLoopObserverRef : 观察者,能够监听到 RunLoop 状态改变。


2、关系图


一个RunLoop对象(CFRunLoopRef)中包含若干个运行模式(CFRunLoopModeRef)。而每一个运行模式下又包含若干个输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)、观察者(CFRunLoopObserverRef)。


1)每次 RunLoop 启动时,只能指定其中一个运行模式(CFRunLoopModeRef),这个运行模式被称作 currentMode。


2)如果需要切换运行模式(CFRunLoopModeRef),只能先退出Loop,再重新定一个运行模式进入。


3)这样做主要是为了隔开不同组的 输入源(CFRunLoopSourceRef)、定时源(CFRunLoopTimerRef)和 观察者(CFRunLoopObserverRef),让其互不影响。



3、CFRunLoopRef

CFRunLoopRef 就是 Core Foundation 框架下的 RunLoop 对象类。


获取对象方式:

1) Core Foundation 

(1.1)CFRunLoopGetCurrent( );  // 获取当前线程的 RunLoop 对象

(1.2)CFRunLoopGetMain( );   // 获取主线程的 RunLoop 对象


2)Foundation

(2.1)[NSRunLoop currentRunLoop ]; // 获取当前线程的 RunLoop 对象

(2.2)[NSRunLoop mainRunLoop] ;     // 获取主线程的 RunLoop 对象



4、CFRunLoopModeRef 多种运行模式

1)kCFRunLoopDefaultMode :APP 默认运行模式,通常主线程是在这种模式下运行。

2)UITrackingRunLoopMode :跟踪用户交互事件(用于 UIScrollView 追踪触摸滑动,保证界面在滑动时不受其他 Mode 影响)。

3)UIInitializationRunLoopMode :在刚刚启动 APP 时进入的第一 Mode ,启动完后就不再使用了。

4)GSEventReceiveRunLoopMode :接受系统内部事件,通常不到。

5)kCFRunLoopCommonModes :伪模式,不是一种真正的运行模式(后面会用到)。




5、CFRunLoopTimerRef

[NSTimer scheduledTimerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
相当于做了下面的事,自动添加到 RunLoop :

NSTimer *timer = [NSTimer timerWithTimeInterval:2.0 target:self selector:@selector(run) userInfo:nil repeats:YES];
[[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];



6、CFRunLoopSourceRef



7、CFRunLoopObserverRef

1)监听状态种类

typedef CF_OPTIONS(CFOptionFlags, CFRunLoopActivity) {
    kCFRunLoopEntry = (1UL << 0),               // 即将进入Loop:1
    kCFRunLoopBeforeTimers = (1UL << 1),        // 即将处理Timer:2    
    kCFRunLoopBeforeSources = (1UL << 2),       // 即将处理Source:4
    kCFRunLoopBeforeWaiting = (1UL << 5),       // 即将进入休眠:32
    kCFRunLoopAfterWaiting = (1UL << 6),        // 即将从休眠中唤醒:64
    kCFRunLoopExit = (1UL << 7),                // 即将从Loop中退出:128
    kCFRunLoopAllActivities = 0x0FFFFFFFU       // 监听全部状态改变  
};

2)示例

- (void)viewDidLoad {
   [super viewDidLoad];

   // 创建观察者
   CFRunLoopObserverRef observer = CFRunLoopObserverCreateWithHandler(CFAllocatorGetDefault(), kCFRunLoopAllActivities, YES, 0, ^(CFRunLoopObserverRef observer, CFRunLoopActivity activity) {
       NSLog(@"监听到RunLoop发生改变---%zd",activity);
   });

   // 添加观察者到当前RunLoop中
   CFRunLoopAddObserver(CFRunLoopGetCurrent(), observer, kCFRunLoopDefaultMode);

   // 释放observer,最后添加完需要释放掉
   CFRelease(observer);
}


作者:u010372095 发表于2017/6/14 14:44:16 原文链接
阅读:23 评论:0 查看评论

微店 Android 插件化实践

$
0
0

随着微店业务的发展,App 不可避免地也遇到了 65535 的大坑。除此之外,业务模块增多、代码量增大所带来的问题也逐渐显现出来。模块耦合度高、协作开发困难、编译时间过长等问题严重影响了开发进程。在预研了多种方案以后,插件化似乎是解决这些问题比较好的一个方向。虽然业界已经有很多优秀的开源插件化框架,但预研后发现在使用上对我们会有一定的局限。要么追求低侵入性而 Hook 大量系统底层代码稳定性不敢保证,要么有很高的侵入性不满足微店定制化的需求。技术要很好地服务业务,我们想在稳定性和低侵入性上寻找一个平衡……

图 1 微店插件化改造流程

微店从 2016 年 4 月份开始进行插件化改造,到年底基本完成(可见图 1 路线)。现在一共有 29 个模块以插件化的方式运行,其中既有如商品、订单等的业务模块,也有像 Network、Cache 等的基础模块,目前我们的插件化框架已经很好地支持了微店多 Feature 快速并行迭代开发。完成一个插件化框架的 Demo 并不是多难的事儿,然而要开发一款完善的插件化框架却非易事, 本篇将我们插件化改造过程中所涉及到的一些技术点以及思考与大家分享一下。

插件化技术原理

插件化技术听起来高深莫测,实际要解决的就是三个问题:

  1. 代码加载;
  2. 资源加载;
  3. 组件的生命周期。

代码加载

我们知道 Android 和 Java 一样都是通过 ClassLoader 来完成类加载,对于动态加载在实现方式上有两种机制可供选择,分别为单 ClassLoader 机制和多 ClassLoader 机制:

  • 单 ClassLoader 机制:类似于 Google MulDex 机制,运行时把所有的类合并在一块,插件和宿主程序的类全部都通过宿主的 ClassLoader 加载,虽然代码简单,但是鲁棒性很差;

  • 多 ClassLoader 机制:每个插件都有一个自己的 ClassLoader,类的隔离性会很好。另外多 ClassLoader 还有一个优点,为插件的热部署提供了可能。如果插件需要升级,直接新建一个 ClassLoader 加载新的插件,然后替换掉原来的即可。

我们的框架在类加载时采用的是多 ClassLoader 机制,框架会创建两种 ClassLoader。第一种是 BundleClassLoader,每个 Bundle 安装时会分配一个 BundleClassLoader,负责该 Bundle 的类加载;第二种是 DispatchClassLoader,它本身并不负责真正类的加载,只是类加载的一个分发器,DispatchClassLoader 持有宿主及所有 Bundle 的 ClassLoader。关系如图 2 所示。

图2 插件化框架 ClassLoader 关系

如何 Hook 系统的 ClassLoader

应用类通过 PathClassLoader 来加载,PathClassLoader 存在于 LoadedApk 中,那么,如何才能替换 LoadedApkPathClassLoader 为我们的 DispatchClassLoader 呢?大家首先想到的是反射,但可惜 LoadedApk 对象是 @Hide 的,要替换首先需要 Hook 拿到 LoadedApk 对象,然后再通过反射替换 PathClassLoader。要反射两次特别是 LoadedApk 对象的获取我们认为风险很高,那还有没有其他方案可以注入 DispatchClassLoader?我们知道 Java 类加载时基于双亲委派机制,加载应用类的 PathClassLoader 其 Parent 为 BootClassLoader,能否在调用链上插入 DispatchClassLoader 呢?

图3 ClassLoader 委派关系

从图 3 大家可以看到,我们通过修改类的父子关系成功地把 DispatchClassLoader 插入到类的加载链中。修改类的父子关系直接通过反射修改 ClassLoaderparent 字段即可,虽然也是反射的私有属性,但相对于 Hook LoadedApk 这个私有对象的私有方法,风险要相对小很多。

类加载优化

不管是 DispatchClassLoaderBundleClassLoader,对于依赖 Bundle 类的查找都是通过遍历来实现的。由于我们把 Network、Cache 等基础组件也进行了插件化,所以 Bundle 依赖会比较多,这个遍历过程会有一定的性能损耗。我们想加载类时能否根据 ClassName 快速定位到该类属于哪一个 Bundle?最终,我们采用的方案是:在编译阶段会收集 Bundle 所包含的 PackageName 信息,在插件安装阶段构造一个 PackageName 与 Bundle 的对应表,这样加载 Class 时,根据包名可快速定位该 Class 属于哪一个 Bundle。当然,由于混淆的原因,不同插件的包名可能重复,对此,我们通过规范来进行保证。

资源加载

资源加载方案可选择的余地不多,都是用 AssetManager@hide 方法 addAssetPath,直接构造插件 Apk 的 AssetManagerResouce 对象。需要注意的是,我们采用的是资源合并的方案,通过 addAssetsPath 方法添加资源时,需要同时添加插件程序的资源文件和宿主程序的资源,及其依赖的资源。这样可以将 Resource 合并到一个 Context 中,解决资源访问时需要切换上下文的问题。另外,若不进行资源合并,插件也无法引入宿主的资源。

资源 ID 冲突问题

由于我们在构造 AssetManager 时,会把宿主、插件及依赖插件的资源合并在一起,那么宿主资源 ID 与插件资源 ID,或插件资源 ID 之间都有可能重复。我们知道资源 ID 是在编译时生成的,其生成的规则是 0xPPTTNNNN,要解决冲突就需要对资源进行分段,资源分段常用的有两种方式,分别为固定 PP 段与固定 TT 段。当时采用哪种资源分段方案对于我们来说是一个比较纠结的选择,固定 PP 段需要修改 AAPT,代价比较大,固定 TT 段相对来说则较为简单。初始我们采用的是固定 TT 段,但后来随着插件的增多,TT 段明显不够用,后来还是采用修改 AAPT 固定 PP 段。大家要上插件化,如果可预见后续插件比较多,建议直接采用固定 PP 段方案。

除了 ID 冲突以外,资源名也有可能重复,对于资源名重复的问题我们通过规范来约束,所有的插件都分配有固定的资源前缀。

如何 Hook 资源加载过程

Android 通过 Resources 对象完成资源加载,要 Hook 资源加载过程,首先想到的是能否替换系统的 Resources 对象为我们自定义的 Resources 对象。

调研发现要替换 Resouce 对象,至少要替换两个系统对象 LoadedApkContextImplmResources 属性,并且 LoadedApkContextImpl 都是私有对象,基于兼容性的考虑我们放弃了这种方案,而采用直接复写 ActivityApplication 的获取资源的相关方法来完成 Bundle 资源的加载。由于该方案对 ApplicationActivity 都有侵入,所以会带来一定的接入成本。为此,我们在编译过程中用代码注入的方式完成资源加载的 Hook,资源的加载操作对插件开发来说是完全透明的。

注:资源 Hook 涉及到复写的方法有如下几个:

Override
public Resources getResources() {
}

Override
public AssetManager getAssets() {
}

Override
public Resources.Theme getTheme() {
}

@Override
public Object getSystemService(String name) {
   if (Context.LAYOUT_INFLATER_SERVICE.equals(name)) {
      // 自定义 LayoutInflater
   }
   return super.getSystemService(name);
}

组件生命周期

对于 Android 来说,并不是类加载进来就可以使用了,很多组件都是有生命的。因此对于这些有血有肉的类,必须给它们注入生命力,也就是所谓的组件生命周期管理。很多插件化框架,比如 DroidPlugin 通过大量 Hook AMS、PMS 等来实现组件的生命周期,从而实现无侵入性。但技术肯定是服务于业务,四大组件真的都需要做插件化吗?在无侵入性和兼容性上该如何抉择?对于这个问题我们给出的答案是稳定压倒一切。综合当前的业务形态,我们插件化框架定位只实现 ActivityBroadCastReceiver 插件化,牺牲部分功能以求稳定性可控。BroadCastReceiver 插件化只是把静态广播转为动态广播,下面重点分解一下 Activity 插件化。

Activity 插件化

Activity 插件化实现大致有以下两种方式:

  • 一种是静态代理,写一个 PluginActivity 继承自 Activity 基类,把 Activity 基类里涉及生命周期的方法全都重写一遍;
  • 另一种方式是动态替换,宿主中预注册桩 StubActivity,通过在系统不同层次 Hook,从而实现 StubActivityRealActivity 之间的转换,以达到偷梁换柱的目的。

由于第一种方案对插件开发侵入性太大,我们采用的是第二种方案。既然如此,我们就需要对图 4 中①和②两个点进行 Hook。

图4 Hook 点选取

  • 对于①Hook:业内一般的做法是 Hook ActivityThread 类有成员变 mInstrumentation,它会负责创建 Activity 等操作,可以通过篡改 mInstrumentation 为我们自己的 InstrumentationHook,在其 execStartActivity() 方法中完成 RealActivity->StubActivity 的转化。

  • 对于②Hook:不同的框架选择在系统不同的层次上进行 Hook,来完成 StubActivity->RealActivity 的还原。

图5 现有插件化框架 Hook 策略

从图 5 可以看出第二种方案不管在哪一点上的 Hook 都会涉及到系统私有对象的操作,从而引入不可控风险。而我们的原则是尽量少地 Hook,若是以牺牲低侵入性为代价,有没有一种更安全的方案呢?并且由于只对 Activity 进行插件化,所有启动 Activity 的地方都是通过 ContextstartActivity 方法调起,我们只要复写 ApplicationActivitystartActivity() 方法,在 startActivity() 方法调用时完成 RealActivity->StubActivity,在类加载时实现 StubActivity->RealActivity 就可以了。同样,复写方法所引入的侵入性完全可以在编译期通过代码注入的方式解决掉。

注:实际上,虽然 startActivity 有很多重写方法,但我们只需复写以下两个就可以了:

@Override
public void startActivityForResult(Intent intent, int requestCode) {
}

@Override
public void startActivityForResult(Intent intent, int requestCode, Bundle options) {
}

另外,对于 ActivityLanchMode,我们是通过在宿主中每种 LaunchMode 都预注册了多个(8 个)StubActivity 来实现。值得注意的一点是,如果插件 Activity 为透明主题,由于系统限制不能动态设置透明主题,所以对于每种 LaunchMode 类型我们都增加了一个默认是透明主题的 StubActivity

为了尽可能地保证稳定性,我们插件 Activity 支持两种运行模式,一种是预注册模式,一种是免注册模式。对于静态插件(随 App 打包)我们默认运行在预注册模式下,对于动态插件(服务器下发)才运行在免注册模式下。值得说明的是,静态插件与宿主 AndroidManifest 合并是在编译期自动完成的。

插件间依赖

我们拆分插件时,首先明确的是每个插件的业务边界,有了边界才有所谓的内聚性,才能区分外部使用者和内部实现者。基于这样拆分,我们可以看出每个插件既可以依赖于其他插件,也可能被其他插件依赖。为了简化业务插件与基础插件之间的依赖关系,我们规定基础插件不能依赖业务插件,业务插件可以依赖基础插件,业务插件与业务插件之间、基础插件与基础插件之间可以互相依赖。总结来看,插件之间的依赖主要有两种形式:

  1. 页面跳转(比如商品 Bundle 跳转到店铺 Bundle 某一页面):Android 可以用 Intent 解耦页面跳转,但考虑到多端统一,我们采用的是类似于总线机制,所有跳转都通过 Page Bus 处理,每个页面都对应一个别名,跳转时根据别名来进行。

  2. 功能调用(商品 Bundle 用到店铺 Bundle 信息):我们把每个插件抽象为一个服务提供者,插件对外暴露的服务称之为本地 Service,它以 Interface 的形式定义,服务提供者保证版本之间的兼容。本地 Service 在插件的 AndroidManifest 中声明,插件安装时向框架注册本地 Service,其他插件使用时直接根据服务别名查询服务。我们会把本地 Service 的查询过程直接绑定到 Context 的 getSystemService() 方法上,整个使用过程就和调用 Android 系统服务一样。此外,除了服务以外,插件还有可能对外暴露一些 Class,为了增加内聚性,我们通过@annotation 的方式声明对外暴露的 Class,在编译阶段 Export 供其他插件依赖,未被注解的类就算是 public,对其他插件也是不可见的。

插件的依赖关系定义在每个插件的 AndroidManifest 文件中。

举个例子,下面是 Shop-Management 模块在 AndroidManifest 中的声明:

<!-- 以下定义的为 Shop-Management 依赖的 Bundle-->
<dependent-bundle android:name="com.koudai.weishop.lib.network" android:versionName="7.7.0.0"/>
<dependent-bundle android:name="com.koudai.weishop.lib.location" android:versionName="7.7.0.0"/>
<dependent-bundle android:name="com.koudai.weishop.lib.image" android:versionName="7.7.0.0"/>
<dependent-bundle android:name="com.koudai.weishop.lib.boostbus" android:versionName="7.7.0.0"/>
<dependent-bundle android:name="com.koudai.weishop.lib.base" android:versionName="7.7.5.0"/>
<dependent-bundle android:name="com.koudai.weishop.lib.account" android:versionName="7.7.0.0"/>

<!-- 以下定义的为 Shop-Management 对外暴露的服务-->
<local-service android:name="ShopManagerService" android:value="com.koudai.weishop.shop.management.impl.ShopManager"/>

其中,versionName 为声明的依赖插件的最小版本号,插件安装阶段会校验依赖条件是否满足,若不满足会进行相应处理(Debug 模式抛 RuntimException,Release 模式输出 error log 并上报监控后台)。

动态部署及 HotPatch

插件化以后,动态部署和 HotPatch 也是需要说明的两个点:

动态部署

我们框架支持 ActivityBroadcastReceiver 的免注册,若插件没有新增其他类型(Service、Provider)的组件,则该插件支持动态部署。由于我们采用多 ClassLoader 机制,理论上是支持热更新的,但考虑到插件有对外导出 Class,为了减少风险,我们对于动态插件生效时间延迟到应用切换至后台以后,当用户切换到后台时直接 Kill 进程。

注:

  1. 插件更新支持增量更新;
  2. 对于插件更新检查有两个触发时机:一个是进程初始化时(Pull),另一个是主动 Push 触发(Push)。

HotPatch

插件化后,App 分为宿主和插件,宿主为源码依赖,插件为二进制依赖。对于宿主和插件,我们采用不同的 HotPatch 方案:

  • 插件——因为插件支持动态部署,若插件需要补丁,我们直接升级插件即可。况且插件支持增
    是升级,补丁包的大小也可以得到有效控制;
  • 宿主——宿主不支持动态部署,只能走传统的 HotPatch 方案,经过多种方案的对比,我们采
    用的是类似于 Tinker 方案,具体原因大家可以参考《微信热补丁演进之路》。

但我们并不是直接使用的 Tinker,而是在实现思路上与 Tinker 一致,采用全 Dex 替换的方式来规避其他方案的问题。由于我们不仅业务组件实现了插件化,而且大部分基础组件(Network、Cache 等)也实现了插件化,所以宿主并不是很大(<2.5M),况且宿主里的代码都比较稳定。

微信的 Tinker 方案在补丁包的大小上的确有很大的优势,我们敬佩其技术探究的精神,但对于其稳定性持有怀疑态度,基于宿主包可控的前提下,我们选择牺牲流量来保证稳定性。

代码管理

我们定位每个插件都是可以独立迭代 App,插件化以后,整个的工程组织方式为如图 6 的形式。

图6 微店工程组织方式

在此之中每个工程都对应一个 Git 库,主库包含多个子库,对于这种工程结构,我们很自然地想到用 SubModule 来管理微店工程。然而事与愿违,使用一段 SubModule 后发现有两个问题严重影响开发效率:

  • 开发某个插件时,对于其他插件应该都是二进制依赖,不再需要其他插件的源码,但 SubModule 会把所有子工程的源码都 Checkout 出来。考虑到 Gradle 的生命周期,这样严重影响了编译速度;另外,主工程包含所有子工程的源码也增加误操作的风险(全量编译、引用本地包而非 Release 包);

  • 代码提交复杂且经常出现冲突:我们知道每次 Git 提交都会产生一个 Sha 值,主工程管理所有子工程的 Sha 值,每次子工程变动,除了提交子工程以外,还需要同步更新主工程的 Sha 值。这样每次子工程的变动都涉及到两次 Commit,更严重的是,如果两个人同时改动同一个子工程,但忘记了同步提交主工程的 Sha 值,则会产生冲突,而且这种情况下无法更新、无法回滚、无法合并,崩溃……

针对使用 Submodule 过程中遇到的问题,我们引入了 Repo 来管理工程代码。Repo 不像 Submodule 那样,通过建立一种主从关系,用主 Module 管理子 Module。在 Repo 里,所有 Module 都是平级关系,每个 Module 的版本管理完全独立于任何其他 Module,不会像 Submodule 那样,提交了子 Module 代码,也会对主 Module 造成影响。

另外,我们在使用过程中,还发现了另外一些好处:

  • 剥离了主 Module 和子 Module 的关系,检出、同步、提交等操作都比 Sumodule 要快好多倍;
  • 模块管理配置由一个陌生的 .gitmodules 变成了所有人都更熟悉的 XML 文件,便于配置管理。

开发调试

插件化以前,我们对所有模块都是源码依赖。插件化以后,运行某一模块时,仅对宿主及当前模块是源码依赖,对于其他模块全部是二进制依赖。集成方式的改变就涉及到如下两个问题:

  • 打包时如何集成插件包?
  • 如何进行断点调试?

插件包集成

我们插件的二进制包是 so 包,其实这些 so 都是正常的 Apk 结构,改为 so 放入 lib 目录只是为了安装时借用系统的能力从 Apk 中解压出来,方便后续安装。我们目前所有的库都是基于 Maven 来管理,插件既然是 so 包,正好借用 Maven 管理能力同时,基于开源的 Gradle 插件 android-native-dependencies 实现了插件的集成。

断点调试

开发插件时,对于其他插件的二进制包都是依赖的已发布版,所有已发布的插件都是混淆包。若开发过程中涉及到其他插件的断点调试,则会出现无法对应源码。

对于这种情况,我们制定了一个策略,在 Debug 模式下,会优先使用本地编译的包。若要调试其他插件,可以把插件源码检出来编译本地包(得益于 Repo 检出过程非常方便),打包过程若检索到有本地包,会替换掉从 Maven 远程仓库下载的包,当然,这个替换过程是通过编译脚本自动完成的。

总结

虽然 Android 插件化在国内发展有几年,各种方案百花齐放,但真的要在业务快速迭代的过程中完成插件化改造工作,其中酸爽也只有亲历者才能体会到。近年来随着 React Native、Weex 及微信小程序的兴起,很多以前需要插件化才能解决的问题,现在或许有了更好的解决方向。但,技术服务于业务,稳定压倒一切,与大家共勉。

作者: 彭昌虎,先后在华为、腾讯从事Android开发工作,2011年加入微店,负责口袋购物、微店等多款产品的架构设计,2016年主导微店App完成插件化改造工作。
责编: 唐小引(@唐门教主),欢迎技术投稿、约稿、给文章纠错,请发送邮件至tangxy@csdn.net
版权声明: 本文为 CSDN 原创文章,未经允许,请勿转载。

作者:Byeweiyang 发表于2017/6/14 15:11:50 原文链接
阅读:4 评论:0 查看评论

Flutter实战一Flutter聊天应用(九)

$
0
0

在这篇文章中,我们将允许用户在聊天消息中发送图像,从设备检索图像文件,并将文本和图像数据存储在Google云端存储Bucket中。由于我们使用Firebase云储存,应用程序将变得更加健壮和可扩展。它能够在上传和下载期间处理网络中断,安全地存储数据,并在用户群扩展时保持相同的性能。

要将数据(如文本和照片)从移动设备上传到云端,我们需要使用firebase_storage插件。在main.dart文件中,确保导入相应的包。

import 'package:firebase_storage/firebase_storage.dart';

要访问存储在移动设备上的数据并在Flutter应用程序中使用它,我们需要Flutter的平台服务API和image_picker插件。在main.dart文件中,导入image_picker包。另外还要导入dart:mathdart:io库,用于在设备上生成随机文件名和处理文件操作。

import 'package:image_picker/image_picker.dart';
import 'dart:math';
import 'dart:io';

此时,我们的应用程序可以让用户发送和接收消息。现在,我们将在用户界面中添加一个按钮来撰写消息,使用户可以从应用访问设备的相机。然后我们将给UI处理二进制数据的能力。

需要注意的是,如果我们正在iOS设备上进行测试,那么需要添加一个设置,让图像选择器插件访问相机和设备上存储的图像。要启用访问,需要将以下条目添加到应用程序的Info.plist文件的主字典中。

<dict>
         <key>NSCameraUsageDescription</key>
         <string>Share images with other chat users</string>
         <key>NSPhotoLibraryUsageDescription</key>
         <string>Share images with other chat users</string>
 ...
 </dict>

<string>元素可以包含任何文本值。我们还可以在Xcode中添加这些设置,使用Privacy - Camera Usage DescriptionPrivacy - Photo Library Usage Description属性的新行。

用户需要一种访问存储在设备上的图像的方法,我们将在用于撰写聊天消息的UI旁边创建一个按钮。应用程序UI的这一部分由ChatScreenState中的私有方法_buildTextComposer()定义。添加一个IconButton小部件,用于访问相机和存储的图像到_buildTextComposer方法。输入字段和发送按钮的现有Row控件应该是父级,将IconButton控件包装在新的Container控件中,Container控件让我们能自定义按钮的边距间距,使其在输入字段旁边更好看。

class ChatScreenState extends State<ChatScreen> {
  //...
  Widget _buildTextComposer() {
    return new IconTheme(
      data: new IconThemeData(color: Theme.of(context).accentColor),
      child: new Container(
        margin: const EdgeInsets.symmetric(horizontal: 8.0),
        child: new Row(
          children: <Widget> [
            new Container(
              margin: new EdgeInsets.symmetric(horizontal: 4.0),
              child: new IconButton(
                icon: new Icon(Icons.photo_camera),
                onPressed: (){}
              ),
            ),
            new Flexible(
              //...
            ),
            new Container(
              //...
            )
          ]
        )
      )
    );
  }
  //...
}

icon属性中,使用Icons.photo_camera常量创建一个新的Icon实例,此常量可以让控件使用Flutter素材图标库提供的“相机”图标。

这里写图片描述

现在修改相机按钮的onPressed回调。我们需要引用Google云端存储Bucket,并在用户选择之后立即开始上传图像。对于按钮的onPressed属性,我们将调用async函数并将其与几个await表达式组合,以特定顺序执行与图像相关的任务。

class ChatScreenState extends State<ChatScreen> {
  //...
  Widget _buildTextComposer() {
        //...
              child: new IconButton(
                icon: new Icon(Icons.photo_camera),
                onPressed: () async {
                  await _ensureLoggedIn();
                  File imageFile = await ImagePicker.pickImage();
                }
              ),
        //...
  }
  //...
}

在允许用户选择、上传和发送图像之前,确保通过执行私有_ensureLoggedIn()方法登录。然后等待用户选择一个图像,并从pickImage()方法获取一个名为imageFile的新的File对象。此方法由之前导入的Flutter Image Picker插件提供。它不需要参数,并返回一个名为path的String值作为图像文件的URL。接下来,修改_sendMessage()以添加imageUrl参数。

class ChatScreenState extends State<ChatScreen> {
  //...
  void _sendMessage({ String text, String imageUrl }) {
    reference.push().set({
      'text': text,
      'imageUrl': imageUrl,
      'senderName': googleSignIn.currentUser.displayName,
      'senderPhotoUrl': googleSignIn.currentUser.photoUrl,
    });
    analytics.logEvent(name: 'send_message');
  }
  //...
}

创建一个用于将文件上传到Google Cloud Storage的实例变量,初始化它以获取对File对象的引用,并将其传递给put()方法。给每个文件一个唯一的名称,使用image_作为前缀,然后是随机整数。

class ChatScreenState extends State<ChatScreen> {
  //...
  Widget _buildTextComposer() {
        //...
              child: new IconButton(
                icon: new Icon(Icons.photo_camera),
                onPressed: () async {
                  await _ensureLoggedIn();
                  File imageFile = await ImagePicker.pickImage();
                  int random = new Random().nextInt(100000);
                  StorageReference ref = FirebaseStorage.instance.ref().child("image_$random.jpg");
                  StorageUploadTask uploadTask = ref.put(imageFile);
                  Uri downloadUrl = (await uploadTask.future).downloadUrl;
                }
              ),
        //...
  }
  //...
}

put()方法由Firebase的Cloud Storage API定义,它需要一个File对象作为参数,并将其上传到Google云端存储Bucket。之前导入的Flutter Firebase Storage插件提供对该API的访问,并定义了StorageUploadTask类。上传完成后,您可以获取图像的downloadURL,现在调用_sendMessage()并传递downloadURL来发送图像。

class ChatScreenState extends State<ChatScreen> {
  //...
  Widget _buildTextComposer() {
        //...
              child: new IconButton(
                icon: new Icon(Icons.photo_camera),
                onPressed: () async {
                  await _ensureLoggedIn();
                  File imageFile = await ImagePicker.pickImage();
                  int random = new Random().nextInt(100000);
                  StorageReference ref = FirebaseStorage.instance.ref().child("image_$random.jpg");
                  StorageUploadTask uploadTask = ref.put(imageFile);
                  Uri downloadUrl = (await uploadTask.future).downloadUrl;
                  _sendMessage(imageUrl: downloadUrl.toString());
                }
              ),
        //...
  }
  //...
}

这个URL用于应用程序UI从Google云端存储下载图像到本地,让发送人在聊天对话中查看他们发送的图像。发送或接收的每个聊天消息都需要能够显示图像和文本。接下来,我们将增强ChatMessage类,以检测用户何时发送图像并使用其URL获取图像。

class ChatMessage extends StatelessWidget {
  //...
  @override
  Widget build(BuildContext context) {
    return new SizeTransition(
      //...
      child: new Container(
        margin: const EdgeInsets.symmetric(vertical: 10.0),
        child: new Row(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            //...
            new Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: <Widget>[
                new Text(
                  snapshot.value['senderName'],
                  style: Theme.of(context).textTheme.subhead),
                new Container(
                  margin: const EdgeInsets.only(top: 5.0),
                  child: snapshot.value['imageUrl'] != null ?
                    new Image.network(
                      snapshot.value['imageUrl'],
                      width: 250.0,
                    ):
                    new Text(snapshot.value['text']),
                )
              ]
            )
          ]
        )
      )
    );
  }
}

在上面的代码中,如果数据库行的值字段是imageUrl,那么应用程序将创建一个Image窗口控件,并将其与图像的内容填充。为了一致性,将宽度设置为特定数量的逻辑像素。如果消息不包含图像,则应用程序将在完成此步骤之前创建一个Text窗口控件。

这里写图片描述

因为我们在更改了iOS的Info.plist配置文件,所以我们现在需要重新启动应用程序,而不是重新加载。

作者:hekaiyou 发表于2017/6/14 15:38:53 原文链接
阅读:31 评论:0 查看评论

Android:JNI 与 NDK到底是什么?(含实例教学)

$
0
0

前言

  • Android开发中,使用 NDK开发的需求正逐渐增大
  • 但很多人却搞不懂 JNINDK 到底是怎么回事
  • 今天,我将先介绍JNINDK & 之间的区别,手把手进行 NDK的使用教学,希望你们会喜欢

目录

目录


1. JNI介绍

1.1 简介

  • 定义:Java Native Interface,即 Java本地接口
  • 作用: 使得Java 与 本地其他类型语言(如C、C++)交互

    即在 Java代码 里调用 C、C++等语言的代码 或 C、C++代码调用 Java 代码

  • 特别注意:

    1. JNIJava 调用 Native 语言的一种特性
    2. JNI 是属于 Java 的,与 Android 无直接关系

1.2 为什么要有 JNI

  • 背景:实际使用中,Java 需要与 本地代码 进行交互
  • 问题:因为 Java 具备跨平台的特点,所以Java 与 本地代码交互的能力非常弱
  • 解决方案: 采用 JNI特性 增强 Java 与 本地代码交互的能力

1.3 实现步骤

  1. Java中声明Native方法(即需要调用的本地方法)
  2. 编译上述 Java源文件javac(得到 .class文件)
  3. 通过 javah 命令导出JNI的头文件(.h文件)
  4. 使用 Java需要交互的本地代码 实现在 Java中声明的Native方法

    Java 需要与 C++ 交互,那么就用C++实现 JavaNative方法

  5. 编译.so库文件
  6. 通过Java命令执行 Java程序,最终实现Java调用本地代码

更加详细过程请参考本文第4节:具体使用

2. NDK介绍

2.1 简介

  • 定义:Native Development Kit,是 Android的一个工具开发包

    NDK是属于 Android 的,与Java并无直接关系

  • 作用:快速开发CC++的动态库,并自动将so和应用一起打包成 APK
    即可通过 NDKAndroid中 使用 JNI与本地代码(如C、C++)交互
  • 应用场景:在Android的场景下 使用JNI

    Android开发的功能需要本地代码(C/C++)实现

  • 特点

示意图

  • 额外注意

示意图

2.2 使用步骤

  1. 配置 Android NDK环境
  2. 创建 Android 项目,并与 NDK进行关联
  3. Android 项目中声明所需要调用的 Native方法
  4. 使用 Android需要交互的本地代码 实现在Android中声明的Native方法

    比如 Android 需要与 C++ 交互,那么就用C++ 实现 JavaNative方法

  5. 通过 ndk - bulid 命令编译产生.so库文件
  6. 编译 Android Studio 工程,从而实现 Android 调用本地代码

更加详细过程请参考本文第4节:具体使用

3. NDK与JNI关系

示意图


4. 具体使用

本文根据版本的不同介绍了两种在Android Studio中实现 NDK的方法:Android Studio2.2 以下 & 2.2以上

4.1 Android Studio2.2 以下实现NDK

  • 步骤如下

    1. 配置 Android NDK环境
    2. 关联 Andorid Studio项目 与 NDK
    3. 创建本地代码文件(即需要在 Android项目中调用的本地代码文件)
    4. 创建 Android.mk文件 & Application.mk文件
    5. 编译上述文件,生成.so库文件,并放入到工程文件中
    6. Andoird Studio项目中使用 NDK实现 JNI 功能
  • 步骤详解

步骤1:配置 Android NDK环境

具体请看文章手把手教你配置Android NDK环境

步骤2: 关联Andorid Studio项目 与 NDK

  • 当你的项目每次需要使用 NDK 时,都需要将该项目关联到 NDK

    1. 此处使用的是Andorid Studio,与Eclipse不同
    2. 还在使用Eclipse的同学请自行查找资料配置
  • 具体配置如下

a. 在Gradlelocal.properties中添加配置

ndk.dir=/Users/Carson_Ho/Library/Android/sdk/ndk-bundle

ndk目录存放在SDK的目录中,并命名为ndk-bundle,则该配置自动添加

示意图

b. 在Gradlegradle.properties中添加配置

android.useDeprecatedNdk=true 
// 对旧版本的NDK支持

示意图

c. 在Gradle的build.gradle添加ndk节点

示意图

  • 至此,将Andorid Studio的项目 与 NDK 关联完毕
  • 下面,将真正开始讲解如何在项目中使用NDK

步骤3:创建本地代码文件

  • 即需要在Android项目中调用的本地代码文件

    此处采用 C++作为展示


test.cpp
# include <jni.h>
# include <stdio.h>

extern "C"
{

    JNIEXPORT jstring JNICALL Java_scut_carson_1ho_ndk_1demo_MainActivity_getFromJNI(JNIEnv *env, jobject obj ){
       // 参数说明
       // 1. JNIEnv:代表了VM里面的环境,本地的代码可以通过该参数与Java代码进行操作
       // 2. obj:定义JNI方法的类的一个本地引用(this)
    return env -> NewStringUTF("Hello i am from JNI!");
    // 上述代码是返回一个String类型的"Hello i am from JNI!"字符串
    }
}

此处需要注意:


  • 如果本地代码是C++.cpp或者.cc),要使用extern "C" { }把本地方法括进去
  • JNIEXPORT jstring JNICALL中的JNIEXPORTJNICALL不能省
  • 关于方法名Java_scut_carson_1ho_ndk_1demo_MainActivity_getFromJNI
    1. 格式 = Java _包名 _ 类名_Java需要调用的方法名
    2. Java必须大写
    3. 对于包名,包名里的.要改成__要改成_1

如我的包名是:scut.carson_ho.ndk_demo,则需要改成scut_carson_1ho_ndk_1demo
最后,将创建好的test.cpp文件放入到工程文件目录中的src/main/jni文件夹
若无jni文件夹,则手动创建。

下面我讲解一下JNI类型与Java类型对应的关系介绍
如下图

步骤4:创建Android.mk文件

  • 作用:指定源码编译的配置信息

    如工作目录,编译模块的名称,参与编译的文件等

  • 具体使用

Android.mk
LOCAL_PATH       :=  $(call my-dir)
// 设置工作目录,而my-dir则会返回Android.mk文件所在的目录

include              $(CLEAR_VARS)
// 清除几乎所有以LOCAL——PATH开头的变量(不包括LOCAL_PATH)

LOCAL_MODULE     :=  hello_jni
// 设置模块的名称,即编译出来.so文件名
// 注,要和上述步骤中build.gradle中NDK节点设置的名字相同

LOCAL_SRC_FILES  :=  test.cpp
// 指定参与模块编译的C/C++源文件名

include              $(BUILD_SHARED_LIBRARY)
// 指定生成的静态库或者共享库在运行时依赖的共享库模块列表。
最后,将上述文件同样放在`src/main/jni`文件夹中。

步骤5:创建Application.mk文件

  • 作用:配置编译平台相关内容
  • 具体使用
*Application.mk*
APP_ABI := armeabi
// 最常用的APP_ABI字段:指定需要基于哪些CPU平台的.so文件
// 常见的平台有armeabi x86 mips,其中移动设备主要是armeabi平台
// 默认情况下,Android平台会生成所有平台的.so文件,即同APP_ABI := armeabi x86 mips
// 指定CPU平台类型后,就只会生成该平台的.so文件,即上述语句只会生成armeabi平台的.so文件
最后,将上述文件同样放在`src/main/jni`文件夹中

步骤6:编译上述文件,生成.so库文件

  • 经过上述步骤,在src/main/jni文件夹中已经有3个文件

示意图

  • 打开终端,输入以下命令
// 步骤1:进入该文件夹
cd /Users/Carson_Ho/AndroidStudioProjects/NDK_Demo/app/src/main/jni 
// 步骤2:运行NDK编译命令
ndk-build

示意图

  • 编译成功后,在src/main/会多了两个文件夹libs & obj,其中libs下存放的是.so库文件

示意图

步骤7:在src/main/中创建一个名为jniLibs的文件夹,并将上述生成的so文件夹放到该目录下

  1. 要把名为 CPU平台的文件夹放进去,而不是把.so文件放进去
  2. 如果本来就有.so文件,那么就直接创建名为jniLibs的文件夹并放进去就可以

示意图

步骤8:在Andoird Studio项目中使用NDK实现JNI功能

  • 此时,我们已经将本地代码文件编译成.so库文件并放入到工程文件中
  • Java代码中调用本地代码中的方法,具体代码如下:

MainActivity.java

public class MainActivity extends AppCompatActivity  {

    // 步骤1:加载生成的so库文件
    // 注意要跟.so库文件名相同
    static {

        System.loadLibrary("hello_jni");
    }

    // 步骤2:定义在JNI中实现的方法
    public native String getFromJNI();

    // 此处设置了一个按钮用于触发JNI方法
    private Button Button;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        // 通过Button调用JNI中的方法
        Button = (Button) findViewById(R.id.button);
        Button.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Button.setText(getFromJNI());

            }
        });
    }

主布局文件:activity_main.xml

<RelativeLayout 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"
    android:paddingBottom="@dimen/activity_vertical_margin"
    android:paddingLeft="@dimen/activity_horizontal_margin"
    android:paddingRight="@dimen/activity_horizontal_margin"
    android:paddingTop="@dimen/activity_vertical_margin"
    tools:context="scut.carson_ho.ndk_demo.MainActivity">

    // 此处设置了一个按钮用于触发JNI方法
    <Button
        android:id="@+id/button"
        android:layout_centerInParent="true"
        android:layout_width="300dp"
        android:layout_height="50dp"
        android:text="调用JNI代码" />

</RelativeLayout>

结果展示

结果展示


源码地址

Carson-Ho的Github地址:NDK_Demo


4.2 Android Studio2.2 以上实现NDK

  • 如果你的Android Studio是2.2以上的,那么请采用下述方法

    因为Android Studio2.2以上已经内部集成 NDK,所以只需要在Android Studio内部进行配置就可以

  • 步骤讲解

步骤1:按提示创建工程

在创建工程时,需要配置 NDK,根据提示一步步安装即可。

示意图

步骤2:根据需求使用NDK

  • 配置好NDK后,Android Studio会自动生成C++文件并设置好调用的代码
  • 你只需要根据需求修改C++文件 & Android就可以使用了。

示意图


5. 总结

  • 本文主要讲解 JavaJNIAndroidNDK相关知识
  • 下面我将继续对 Android中的NDK进行深入讲解 ,有兴趣可以继续关注Carson_Ho的安卓开发笔记

请帮顶或评论点赞!因为你的鼓励是我写作的最大动力!

作者:carson_ho 发表于2017/6/14 17:03:49 原文链接
阅读:35 评论:0 查看评论

Android 开发 Tip 16 -- setMultiChoiceItems & setSingleChoiceItems 不显示!?

Kotlin语法基础,运算符

$
0
0

运算符

计算机程序中最小的程序单位成为表达式,每个表达式都可以由两部分组成,即操作数和运算符。操作数可以是变量、常量、类、数组、方法等,甚至是其他表达式。而运算符则用于支出表达式中单个或者多个操作数参与运算的规则,表达式通过运算之后产生的值依赖于表达式中包含的运算符的优先级和结核性。Kotlin语言包含了Java语言中的所有运算符的特性,并结合C语言的优点,增加自定义运算符的逻辑。这些运算符之中,主要包括有:算数运算符、区间运算符、逻辑运算符、关系运算符、赋值运算符、自增自减运算符等。

根据操作数的数量来划分,运算符又可以分为一目运算符、双目运算符。
- 一目运算符用于单一操作对象,又称单目运算符,如:++a、!b、i–等。
- 双目运算符是中置的,它拥有两个操作数,比如:a+3、a*b

Kotlin中没有三目运算符

基础运算符

基础运算符中包含了我们在编码工程中常用的一系列运算符,使我们编写程序的基本组成部分,了解基础运算符的用法可以尽可能的避免一些语法和逻辑上的基础性错误。

赋值运算符(=)

赋值运算a=b,表示等号右边的b初始化或者维护等号左边的a,b可以是变量、常量、字面量或表达式,如:

var IntA:Int = 5
val IntB:Int = 10

IntA = 2 + 1;
IntA = IntB

在Kotlin语言中还有另一种赋值运算符,叫做算术自反赋值运算符。它是一种由两个普通运算符组成的符合运算符,它包括:“+=”、“-=”、“*=”、“/=”、“%=”。使用方式如下:

var IntA:Int = 5
val IntB:Int = 10

IntA += IntB // 作用等于 IntA = IntA + IntB
IntA -= IntB // 作用等于 IntA = IntA - IntB
IntA *= IntB // 作用等于 IntA = IntA * IntB
IntA /= IntB // 作用等于 IntA = IntA / IntB
IntA %= IntB // 作用等于 IntA = IntA % IntB

算数运算符

算术运算符用于数值类型的运算,Kotlin语言支持基本的算术运算:加法“+”、减法“-”、乘法“*”、除法“/”、取余“%”、以及自增自减运算,如:

var IntA:Int = 5 + 5  // 10
val IntB:Int = 10 - 2 // 8
val IntC:Int = 3 * 4  // 12
val IntD:Int = 10 / 5 // 2
val IntE:Int = 10 % 3 // 1,除不尽,保留余数
val IntF:Int = 10 / 6 // 1,除不尽,仅保留整数部分

IntA = IntA / 0 // 报错,除数不能为0

自增自减运算符(++、–)

自增和自减运算符也是单目运算符,因为它只有一个操作数。自增运算符 “++” 表示使操作数加1,自减运算符 “–” 表示使操作数减1,其操作数可以使整数和浮点型等数字类型,如:

var intA : Int = 5

intA++ // 等于 intA = intA + 1
println("intA = " + intA)  // 输出 intA = 6

值得注意的是,自增运算符和自减运算符还会分为前置自增、后置自增、前置自减和后置自减,放在操作数前面的是前置,放在操作数后面的是后置运算符。

后置运算,则为先进性表达式返回,才进行自增、自减运算。前置运算符,则先进行自增、自减运算,在进行表达式返回。如:

var intIncA: Int = 5
var intIncB: Int = 5
var intIncC: Int = 5
var intIncD: Int = 5

println(++intIncA) // 先自增, 后返回。 输出 :6
println(--intIncB) // 先自减, 后返回。 输出 :4
println(intIncC--) // 先返回, 后自减。 输出 :5
println(intIncD++) // 先返回, 后自增。 输出 :5

字符串连接符(+)

两个字符串可以连接在一起成为一个新字符串,这种操作被成为字符串连接,在Kotlin语言中连接字符串可以用 “+”。如:

"hello " + "world" // 等于 "hello world"

字符串连接操作两边都是字符串,而很多情况下我们使用连接符仅有一侧是字符串,另一侧是其他类型。这个时候,系统则会自动调用toString方法转化为字符串,进行拼接。这个时候则调用则是String重载的plus方法,后面我们会具体介绍运算符重载,Kotlin中String的源码如下:

image

故此,进行字符串与其他类型拼接我们都将String类型的操作符至于连接符 “+” 左侧。

var intA : Int = 1
var StringA : String = "String Head "

println(intA + StringA) // 报错,调用的是Int.plus方法
println(StringA + intA) // 输入内容:String Head 1

关系运算符

关系运算符是指:使用关系运算符对两个操作数或表达式进行运算,产生的结果为真或者假。

运算符 名称 示例 功能 缩写
< 小于 a
println(10 == 10) // true
println(1 != 5)   // true
println(1 < 5)    // true
println(1 > 5)    // false
println(4 <= 5)   // true
println(4 >= 5)   // false

注意:
1. 关系运算符的优先级低于算术运算符。
2. 关系运算符的优先级高于赋值运算符。

区间运算符(a..b)

区间运算符,顾名思义就是可以用来表示两个操作数之间的范围集合。a..b也就我们平时所说的,从a到b所有的数字集合。在Kotlin语言之中,有两种区间运算符:闭区间运算符和开区间运算符。
- 闭区间运算符 : “a..b”从a到b范围内所有的值,包括a和b。
- 半闭区间运算符 : “a until b”从a到b范围内所有的值,包括a和不包括b。

区间表达式由具有操作符形式 “..”rangeTo 辅以 in!in 而得。区间是为任何可比较类型定义的,但对于整型原生类型,它有一个优化的实现。以下是使用区间的一些示例:

for (i in 1..10) { // 等同于 1 <= i && i <= 10
    println(i)
}

for (i in 1.rangeTo(10)) {  // 等同于 1 <= i && i <= 10
    println(i)
}

for (i in 'a'..'z') { // 等同于 'a' <= i && i <= 'z'
    println(i)
}

in 代表在区间内,!in表示不在。

整型区间有一个额外的特性:它们可以迭代。 Kotlin编译器负责将其转换为类似 Java 的基于索引的 for循环而无额外开销。

for (i in 1..4) print(i) // 输出“1234”

for (i in 4..1) print(i) // 什么都不输出

运行上述例子,我们可以发现如果只写 “..”,这个区间值只是顺序。如果你想倒序迭代数字呢?也很简单。你可以使用标准库中定义的 downTo 方法:

for (i in 4 downTo 1) print(i) // 输出“4321”

能否以不等于 1 的任意步长迭代数字? 当然没问题, 使用step方法就可以做到:

for (i in 1..4 step 2) print(i) // 输出“13”

for (i in 4 downTo 1 step 2) print(i) // 输出“42”

那么我们如何创建一个半闭区间呢?可以使用 until方法 :

for (i in 1 until 10) {   // i in [1, 10) 排除了 10
     println(i)
}

逻辑运算符

逻辑运算使用等式表示判断,把推理看做等式运算,这种变换的有效性不依赖人们对符号的解释,只依赖符号组合的变换。Kotlin语言和Java一样,支持三个标准逻辑运算符,逻辑与、逻辑或、逻辑非。

  • && : 逻辑与,可以理解为并且的意思.
  • || : 逻辑或,可以理解为或者的意思,也就是条件可以二取一
  • : 逻辑非,取反

逻辑运算表达式中,操作数值的组合不同,整个表达式的值也不同。在这里我们给出一个逻辑运算的值搭配总结表:

a b a&&b a||b !a
false false false false true
true false false true false
false true false true true
true true true true false

空安全操作符(?、!!)

在Java开发的过程中遇到的最多的异常就是NullPointException(NPE),空异常的问题很多是不可预见的。一直以来,NullPointException空指针异常在开发中是最低级也最致命的问题。我们往往需要进行各种null的判断以试图去避免NPE的发生。在Kotlin语言中一切皆对象,出现NPE则是致命性的问题。所提,在Kotlin语言中提出了预先判空处理,为此引用了两个操作符:判空操作符“?”强校验“!!”操作符

预定义,是否能容纳空(?)

在Kotlin中,类型系统区分一个引用可以容纳null,还是不能容纳null。 Kotlin中绝大部分的对象都是不能够容纳null的,例如,基础类型中的常规变量不能容纳null:

var a: String = "abc"
a = null // 编译错误

如果要允许为null,我们可以声明一个变量为可空字符串,需要在定义的类型后面紧跟一个问号 “?”,如上述例子则写作* String?*

var b: String? = "abc"
b = null // 这样编译没问题

对于无法容纳null的类型,我们可以放心的对它的属性进行调用。

var a: String = "abc"
var aLength = a.length // 放心调用,a肯定不会为null

同样的操作,我们则不能够对b字符串进行操作,对于可能为空的类型进行操作,我们就必须判空。

判空(?)

在Kotlin语言中判断一个对象是否为空有两种方式,第一种就是如同Java语言一样,使用if-else进行判空;另一中就还是使用操作符 “?” 进行判断。

// 在Java语言中我们使用的判空方法。
if (b != null && b.length > 0) {
    println("String of length ${b.length}")
} else {
    println("Empty string")
}

// Kotlin,可空类型的判断
println("String of length ${b?.length}")

咋一看,差别不是很大,但,null安全,在链式调用中很有用。例如,如果一个员工 Bob 可能会(或者不会)分配给一个部门, 并且可能有另外一个员工是该部门的负责人,那么获取 Bob 所在部门负责人(如果有的话)的名字,我们写作:

bob?.department?.head?.name

如果任意一个属性(环节)为空,这个链式调用就会返回 null。如果要只对非空值执行某个操作,安全调用操作符可以与 let 一起使用:

val listWithNulls: List<String?> = listOf("A", null)
for (item in listWithNulls) {
     item?.let { println(it) } // 输出 A 并忽略 null
}

!! 操作符

很多情况下,NullPointerException对我们来说还是有一定意义的,我们必须catch住此异常。那么,Kotlin中的又有空安全的机制存在,我们就必须对null进行强校验。这里,Kotlin给我们提供的操作符为两个引号 “!!”,如:

var a : String? = null // 必须是可空类型,不然强校验没有意义
val lenC = a!!.length // 这样做就会报错

如果,希望强校验希望系统抛出一个NullPointerException,那么必须让定义的变量可容纳null,不然强校验就失去意义了。

安全的类型转换

如果对象不是目标类型,那么常规类型转换可能会导致 ClassCastException。 另一个选择是使用安全的类型转换,如果尝试转换不成功则返回 null:

val aInt: Int? = a as? Int

可空类型的集合

如果你有一个可空类型元素的集合,并且想要过滤非空元素,你可以使用 filterNotNull 方法来实现。

val nullableList: List<Int?> = listOf(1, 2, null, 4)
val intList: List<Int> = nullableList.filterNotNull()

Elvis 操作符(?:)

Elvis操作符很像是Java语言中的三目表达式,然而由于三目表达式的对于很多开发者来说都比较难懂,导致经常用错。Kotlin对三目表达式进行了升级,即elvis表达式的来源,Kotlin中不再支持三目表达式。Elvis操作符的用法如下:

<结果> = <表达式1> ?: <表达式2>

如果表达式1为null,则返回表达式2的内容,否则返回表达式1。请注意,当且仅当左侧表达式1为空时,才会对右侧表达式求值。如:

// Elvis操作符获取b字符串的长度,如果b为null则返回-1
val lenB = b?.length ?: -1

// 等同于逻辑
val lenA: Int = if (b != null) {
    b.length
} else {
    -1
}

位运算符

运算符优先级

运算符的优先级使得一些运算符优先于其他运算符,从而是的高优先级的运算符会先被计算。而运算符的结合性用于定义相同优先级的运算符在一起的时和表达式结合或关联规则,在混合表达式中,运算符的优先级和结合性是非常重要的。如:

2 + 3 - 4 * 5 // 等于 -15

如果严格地从左到右,计算过程会是这样:

2 + 3 = 5
5 - 4 = 1
1 * 5 = 5

但实际上乘法优先于减法被计算,所以实际上的运算过程是:

2 + 3 = 5
4 * 5 = 20
5 - 20 = -15

这类似的情况和我们在数学中一样:右括号先算括号里的, 然后先乘除后加减。在Kotlin语言中也拥有自己运算符的优先级别和结合性。这里我们把所有的运算符总结为下表:

优先级 运算符 结核性
1 ()、[] 从左到右
2 !、+(正)、-(负)、~、++、– 从右到左
3 *、/、% 从左到右
4 +(加)、-(减) 从左到右
5 <<、>>、>>> 从左到右
6 <、<=、>、>= 从左到右
7 ==、!= 从左到右
8 &(按位与) 从左到右
9 ^ 从左到右
10 | 从左到右
11 && 从左到右
12 || 从左到右
13 ?: 从右到左
14 =、+=、-=、/=、%=、&=、|=、^=、~=、<<=、>>=、>>>= 从右到左

运算符重载

预定义的运算符的操作对象只能是基本数据类型,实际上,对于很多我们自定义的对象也需要有类似的运算操作。运算符重载是对已有的运算符赋予多重含义,使同一个运算符作用于不同类型的数据导致不同类型的行为。

运算符重载是自C++语言器就支持的特性,然而在Java语言之中这个特性就不在支持,在很多高级科学运算上很不方便,Kotlin语言又从新支持此特性。不同于符合赋值运算符,我们可以定义/、==、-、+、*、%、<<、>>、!、&、|、^等运算符的逻辑功能。重载一个运算符,我们必须重写它所对应的operator方法,如下就是一个针对 “+” 运算符的定义:

public operator fun plus(other: Byte): Float

这里,我们可以看得出来运算符重载的实质是方法的重载。在实现过程中,首先把指定的运算表达式转化为对运算方法的调用,运算对象转化为运算符方法的实参,然后根据实参的类型来确定需要调用达标函数,之后Kotlin会将对应的符号运算切换到方法之中。如Float类型针对 “+” 运算符所定义的:
image

重载一元运算符

一元前缀操作符

表达式 转换方法
+a a.unaryPlus()
-a a.unaryMinus()
!a a.not()

这个表是说,当编译器处理例如表达式 +a 时,它执行以下步骤:
1. 确定 a 的类型,令其为 T。
2. 为接收者 T 查找一个带有 operator 修饰符的无参函数 unaryPlus(),即成员函数或扩展函数。
3. 如果函数不存在或不明确,则导致编译错误。
4. 如果函数存在且其返回类型为 R,那就表达式 +a 具有类型 R。

递增和递减

表达式 转换方法
a++ a.inc()
a– a.dec()

inc() 和 dec() 函数必须返回一个值,它用于赋值给使用 ++ 或 – 操作的变量。它们不应该改变在其上调用 inc() 或 dec() 的对象。

编译器执行以下步骤来解析后缀形式的操作符,例如 a++:
1. 确定 a 的类型,令其为 T。
2. 查找一个适用于类型为 T 的接收者的、带有 operator 修饰符的无参数函数 inc()。
3. 检查函数的返回类型是 T 的子类型。

计算表达式的步骤是:
1. 把 a 的初始值存储到临时存储 a0 中,
2. 把 a.inc() 结果赋值给 a,
3. 把 a0 作为表达式的结果返回。
4. 对于 a–,步骤是完全类似的。

对于前缀形式 ++a 和 –a 以相同方式解析,其步骤是:
1. 把 a.inc() 结果赋值给 a,
2. 把 a 的新值作为表达式结果返回。

二元操作符

算术运算符

表达式 转换方法
a + b a.plus(b)
a - b a.minus(b)
a * b a.times(b)
a / b a.div(b)
a % b a.mod(b)
a..b a.rangeTo(b)

对于此表中的操作,编译器只是解析成翻译为列中的表达式。

请注意,自 Kotlin 1.1 起支持 rem 运算符。Kotlin 1.0 使用 mod 运算符,它在 Kotlin 1.1 中被弃用。

“In”操作符

表达式 转换方法
a in b b.contains(a)
a !in b !b.contains(a)

对于 in 和 !in,过程是相同的,但是参数的顺序是相反的。

索引访问操作符

表达式 转换方法
a[i] a.get(i)
a[i, j] a.get(i, j)
a[i_1, …, i_n] a.get(i_1, … , i_n)
a[i]=b a.set(i, b)
a[i,j]=b a.set(i, j, b)
a[i_1, … , i_n]=b a.set(i_1,… ,o_n,b)

方括号转换为调用带有适当数量参数的 get 和 set。

调用操作符

表达式 转换方法
a(i) a.invoke(i)
a(i, j) a.invoke(i, j)
a(i_1, … , i_n) a.invoke(i_1, …, i_n)

圆括号转换为调用带有适当数量参数的 invoke。

广义赋值

表达式 转换方法
a += b a.plusAssign(b)
a -= b a.minusAssign(b)
a *= b a.timesAssign(b)
a /= b a.divAssign(b)
a %= b a.modAssign(b)

对于赋值操作,例如 a += b,编译器执行以下步骤:
1. 如果右列的函数可用
2. 如果相应的二元函数(即 plusAssign() 对应于 plus())也可用,那么报告错误(模糊)。
3. 确保其返回类型是 Unit,否则报告错误。
4. 生成 a.plusAssign(b) 的代码
5. 否则试着生成 a = a + b 的代码(这里包含类型检查:a + b 的类型必须是 a 的子类型)。

注意:赋值在 Kotlin 中不是表达式。

相等与不等操作符

表达式 转换方法
a == b a?.equals(b) ?: (b === null)
a != b !(a?.equals(b) ?: (b === null))

这个 == 操作符有些特殊:它被翻译成一个复杂的表达式,用于筛选 null 值。 null == null 总是 true,对于非空的 x,x == null 总是 false 而不会调用 x.equals()。

注意:=== 和 !==(同一性检查)不可重载。

比较操作符

表达式 转换方法
a > b a.compareTo(b) > 0
a < b a.compareTo(b) < 0
a >= b a.compareTo(b) >= 0
a <= b a.compareTo(b) <= 0

所有的比较都转换为对 compareTo 的调用,这个函数需要返回 Int 值。

位运算

对于位运算,Kotlin 并没有提供特殊的操作符, 只是提供了可以叫中缀形式的方法, 比如:

// 给 Int 定义扩展
infix fun Int.shl(x: Int): Int {
……
}

// 用中缀表示法调用扩展方法
1 shl 2

// 等同于这样
1.shl(2)

下面是全部的位运算操作符(只可以用在 Int 和 Long 类型):

表达式 转换方法
shl(bits) 有符号左移 (相当于Java <<)
shr(bits) 有符号右移 (相当于Java >>)
ushr(bits) 无符号右移(相当于Java >>>)
and(bits) 按位与
or(bits) 按位或
xor(bits) 按位异或
inv() 按位取反

作者:yzzst 发表于2017/6/14 20:21:39 原文链接
阅读:198 评论:0 查看评论

一定能成功的Android NDK环境配置教程

$
0
0

前言

  • Android开发中,使用 NDK开发的需求正逐渐增大
  • 但在Android使用NDK前需要进行 相关环境配置
  • 本文主要讲解 在Mac情况下的Android NDK配置,希望你们会喜欢

1. 步骤说明

示意图

下面,我将一步步讲解如何进行NDK环境配置。


2. 步骤讲解

步骤1. 下载Android NDK工具包

  • 官网下载地址,注意 科学 上网
  • 本文采用的Android NDK版本是:android-ndk-r14b-darwin-x86_64

示意图

步骤2: 解压 NDK包

  • 注:解压路径 不要出现空格和中文
  • 建议:将解压路径设置为:Android StudioSDK目录里,并命名为ndk-bundle

    解压路径:/Users/Carson_Ho/Library/Android/sdk/ndk-bundle


示意图
  • 好处:启动Android Studio时,Android Studio会自动检查它并直接添加到ndk.dir中,那么在使用时,就不用配置Android StudioNDK的关联工作

步骤3:安装 & 配置NDK

在终端依次输入下列命令

// 先输入以下命令
pico .bash_profile 

// 再依次输入下列命令(后面的路径需要根据你NDK解压路径设置)
export PATH=${PATH}:/Users/Carson_Ho/Library/Android/sdk/ndk-bundle 
A_NDK_ROOT=/Users/Carson_Ho/Library/Android/sdk/ndk-bundle
export A_NDK_ROOT
// 注意检查空格、中 & 英字符区分

// 输入以下组合命令 进行保存
control+X
// 输入后,选择Y

// 最后,更新刚配置的环境变量
source .bash_profile

// 验证NDK是否配置成功
 // 1. 关闭终端 并 重新打开
 // 2. 若无错误提示,则成功配置

至此,关于Android NDK的环境配置已经完成

4. 总结


请帮顶或评论点赞!因为你的鼓励是我写作的最大动力!

作者:carson_ho 发表于2017/6/14 17:00:38 原文链接
阅读:214 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>