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

一起Talk Android吧(第二十七回:Java泛型)

$
0
0

各位看官们,大家好,上一回中咱们说的是Java包装类的例子,这一回咱们说的例子是Java泛型。闲话休提, 言归正转。让我们一起Talk Android吧!


看官们,我们以前介绍过重载方法,大家没有想过重载类呢?今天我们将介绍一种类似重载类的语法,它叫泛型。我们先用伪代码来演示一下什么是泛型。

class ClassName <T>{
    permission T data;
    permission T func(T value);
}

该代码中的T就是一种泛型标记,它可以是任意的类型,不过类型也不能太任性,需要类类型才可以,比如我们上一回介绍的包装类。大家可以看到类中成员的类型使用的也是泛型标记,这说明它们也可以接受任意的类型。不过有一点要强调,那就是凡是T标记的地方,类型必需保持一致。不能说标记data的T使用int类型,而且标记func的T使用String类型。

那么如何指定这个泛型标记T的具体类型呢?在实例化类的对象时指定T就可以。我们还是通过伪代码来演示:

ClassName obj = new ClassName<Long>();

大家可以看到,我们在定义时设定一个泛型标记,在使用泛型的时候才给它指定具体的类型。对比一下重载方法,重载方法在定义时就指定了多种类型。这是它们不一样的地方。不过,它们也有相同的地方:可以接收任意类型。

我们刚才也说了,泛型类似重载方法,更加准确的说,泛型可以看作是一种模板,模板定好了,里面的数据类型在使用的时候再去指定。如果有C++编程经验的人,一提到模板那么明白是怎么回事了。如果没有我们举一个现实生活中的例子。大家都使用过硬币,在制造硬币的时候会有一个模型,在模型中浇注入铁水,那么我们得到的硬币就是铁币;在模型中浇注入铜水,那么我们得到的硬币就是铜币。但是有一点可以看到,不管是铁币还是铜币,它们的大小和形状是相同的,因为它们使用了相同的模型。它们不同的地方是本身的材料。在这个例子中,制造硬币的模型好比泛型,它们都是起一个模板的使用,而制造硬币的铁水或者铜水好比数据类型(Long,String等)。

当然了,硬币的制造肯定不是这么简单,至于怎么制造,我能知道吗?我们只是举个例子让大家更加生动形象地理解泛型。再怎么生动形象,也比不上源代码,接下来我们通过具体的代码来演示如何去使用泛型。

public class GenericEx {

    public static class Generic<T>{
        private T data;

        public void func(T v){
            System.out.println("the fuc showing value: "+v);
        }

        public void setData(T data) {
            this.data = data;
        }

        public T getData() {
            return data;
        }
    }
    public static void main(String[] args) {
        Generic<Integer> genObj1 = new Generic<Integer>();
        Generic<Double> genObj2 = new Generic<Double>();
        Generic<String> genObj3 = new Generic<String>();

        genObj1.setData(6);
        System.out.println("getData: "+genObj1.getData());
        genObj1.func(7);

        genObj2.setData(6.0);
        System.out.println("getData: "+genObj2.getData());
        genObj2.func(7.0);

        genObj3.setData("this is generic example");
        System.out.println("getData: "+genObj3.getData());
        genObj3.func("the type of param is String");        
    }
}

在上面的代码中,我们在类中做了泛型标记,在实例化类的对象时才指定了具体的类型。大家可以看到同样一个类可以接收Integer,Double和String三种数据类型,当然了,它还可以接收其它的类型,我们就不一一列出了。

我们在类中使用了泛型,这样的类叫做泛型类,大家也可以在接口中使用泛型,使用泛型的接口叫做泛型接口。泛型接口的使用方法和泛型类的使用方法相同。我们就不具体说明了。

下面是程序的运行结果,请大家参考:

getData: 6
the fuc showing value: 7
getData: 6.0
the fuc showing value: 7.0
getData: this is generic example
the fuc showing value: the type of param is String

各位看官,关于Java泛型的例子咱们就介绍到这里,欲知后面还有什么例子,且听下回分解!


作者:talk_8 发表于2017/6/3 8:42:34 原文链接
阅读:161 评论:0 查看评论

iOS自带实现高斯模糊效果

$
0
0

什么叫高斯模糊效果,通俗地说,就是毛玻璃效果,从iOS 7以来,就频繁地被设计使用,如果用得好,效果会显得非常的好。我们来看一个例子:

图中下面一小部分就是高斯模糊效果。要实现也很简单,iOS自身就支持这种效果。

iOS 7 UIToolbar

iOS 7开始,支持用UIToolbar来实现这种效果,代码很简单:

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
    imageView.image = [UIImage imageNamed:@"neo.jpeg"];
    [self.view addSubview:imageView];

    /* UIBarStyle枚举:
     UIBarStyleDefault
     UIBarStyleBlack
     UIBarStyleBlackOpaque
     UIBarStyleBlackTranslucent
     */
    UIToolbar *toolbar = [[UIToolbar alloc] initWithFrame:CGRectMake(0, imageView.frame.size.height * 0.7, imageView.frame.size.width, imageView.frame.size.height * 0.3)];
    toolbar.barStyle = UIBarStyleBlackTranslucent;
    [self.view addSubview:toolbar];

这个style实现出来就是这个效果:

事实上除了UIBarStyleDefault风格是白亮的模糊不太好看外,其他三种风格我都看不出有什么差别。

我们可以看一下UI层级:

事实上就是在原本的图片视图上加了一层UIVisualEffectView,等于是覆盖了一块毛玻璃,很好理解,也很好用。

iOS 8 UIBlurEffect

从iOS 8开始,苹果开始支持一个新的实现方式——UIBlurEffect,苹果也推荐这种方式,当然如果你的应用要支持iOS 7,那还是用上一种。

这种方式的代码一样很简单,在代码中就直接用到了我们上面层级中看到的UIVisualEffectView,代码如下:

    UIImageView *imageView = [[UIImageView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height)];
    imageView.image = [UIImage imageNamed:@"neo.jpeg"];
    [self.view addSubview:imageView];

    /* UIBlurEffectStyle枚举
     UIBlurEffectStyleRegular
     UIBlurEffectStyleLight
     UIBlurEffectStyleDark
     UIBlurEffectStyleProminent
     UIBlurEffectStyleExtraLight
    */
    UIBlurEffect *effect = [UIBlurEffect effectWithStyle:UIBlurEffectStyleLight];
    UIVisualEffectView *effectView = [[UIVisualEffectView alloc] initWithEffect:effect];
    effectView.frame = CGRectMake(0, imageView.frame.size.height * 0.7, imageView.frame.size.width, imageView.frame.size.height * 0.3);
    [self.view addSubview:effectView];

确实这种方式的效果更加自然:

再看一下UI层级:

对比一下两种实现方式,其实是不一样的,感兴趣的可以研究一下原理。

我们把风格换成UIBlurEffectStyleDark后是这样的:

和UIToolbar的实现效果相比的话,要说没区别也有一点区别,总之就是觉得好看一些,所以还是推荐用这种方式。

不得不说毛玻璃(高斯模糊)效果配上好图片后的效果真的很赞,我可以玩很久,其实实现方式真的很简单,大家可以多多应用到自己的应用中去,相信一定会加分不少!


版权所有:http://blog.csdn.net/cloudox_
实例工程:https://github.com/Cloudox/OXBlurDemo
参考:http://www.cnblogs.com/arvin-sir/p/5131358.html?utm_source=tuicool&utm_medium=referral

作者:Cloudox_ 发表于2017/6/3 10:04:40 原文链接
阅读:45 评论:0 查看评论

℃江的开发手册__Android工具篇

$
0
0

2017年6月3日,心中,晴有时多云。做了一晚上的梦,我终还是要写一个帮助自己和大家的系列了,人多还是自私的,要学的东西很多,从Java到Python再到Kotlin,我对编程语言有种特殊的关心,有人会觉得这是一种的盲目的关心,但请你相信我,国外有一则调查显示:会8种以上编程语言的人,薪酬是最高的。

  • 本开发手册特点:简介,简洁,以实现操作为基础,实现原理为渠道,实现需求为目的,从而达到共同进步(最近党章背多了)。

简介:Android开发工具相关(注:Android Studio的使用,推荐用内存为8G以上,电脑最好有固态硬盘,最好不要安装在C盘)

1、 Android Studio安装过程
2、第三方Android模拟器Genymotion的安装手册下载
3、天天模拟器的安装(占用内存小,不卡顿)
4、如何创建第一个Android项目
5、如何在设备上运行你的程序(建议下载一个手机管家,他们会告诉你如何开启一些奇葩手机的权限)
6、如何构建简单的用户界面
7、如何启动另一个Activity(类似Web的页面跳转)

作者:baidu_34750904 发表于2017/6/3 10:19:56 原文链接
阅读:32 评论:0 查看评论

Kotlin 官方学习教程之属性和字段

$
0
0

属性声明

在 Kotlin 中类可以有属性,我们可以通过 var 关键字来声明可变属性,通过 val 关键字来声明只读属性。

class Address {
    var name: String = ...
    var street: String = ...
    var city: String = ...
    var state: String? = ...
    var zip: String = ...
}

我们可以像使用 Java 中的字段一样,直接通过名字来引用它:

fun copyAddress(address: Address): Address {
    val result = Address() // 在 kotlin 中没有 new 关键字
    result.name = address.name // 调用访问器
    result.street = address.street
    // ...
    return result
}

getter 和 setter

声明属性的完整语法是:

var <propertyName>[: <PropertyType>] [= <property_initializer>]
    [<getter>]
    [<setter>]

语法中的初始化语句,getter 和 setter 都是可选的。如果属性类型可以从初始化语句或者类的成员函数中推断出来,那么类型也是忽略的。

例子:

var allByDefault: Int? // 错误: 需要一个初始化语句, 默认实现了 getter 和 setter 方法
var initialized = 1 // 类型为 Int, 默认实现了 getter 和 setter 方法

只读属性声明的完整语法与可变的属性声明有两个不同:它以val而不是var开头,不允许setter:

val simple: Int? // 类型为 Int, 默认实现了 getter 方法,必须在构造函数中初始化
val inferredType = 1 // 类型为 Int, 默认实现了 getter 方法

我们可以像普通函数那样,在一个属性声明中写出自定义访问器。下面是一个自定义 getter 的例子:

val isEmpty: Boolean
    get() = this.size == 0

自定义 setter 是这样的:

var stringRepresentation: String
    get() = this.toString()
    set(value) {
        setDataFromString(value) // parses the string and assigns values to other properties
    }

按照惯例,setter 方法的参数名是 value ,但你也可以选择一个你喜欢的名字。

从 Kotlin 1.1 开始,如果可以从 getter 推测属性类型,则可以省略它:

val isEmpty get() = this.size == 0  // 类型为 Boolean

如果你需要改变一个访问器的可见性或者给它添加注解,但又不想改变默认的实现,那么你可以定义一个不带函数体的访问器:

var setterVisibility: String = "abc"
    private set // setter 是私有的并且有默认的实现

var setterWithAnnotation: Any? = null
    @Inject set // 使用 Inject 注释 setter

备用字段

Kotlin 中的类不可以有字段。但是,有时候在使用自定义访问器是需要一个备用字段。出于这些原因,Kotlin 使用 field 关键字提供了自动备用字段:

var counter = 0 // 初始化值会直接写入备用字段
    set(value) {
        if (value >= 0) field = value
    }

field 修饰符只能在属性的访问器中使用。

编译器会检查访问器的代码,如果使用了备用字段(或者访问器是默认的实现逻辑),就会自动生成备用字段,否则就不会。

例如,下面的例子中就不会有备用字段:

val isEmpty: Boolean
    get() = this.size == 0

备用属性

如果你想要做一些事情但不适合使用这种 “隐含备用字段” 方案,你可以试着用备用属性的方式:

private var _table: Map<String, Int>? = null
public val table: Map<String, Int>
    get() {
        if (_table == null) {
            _table = HashMap() // Type parameters are inferred
        }
        return _table ?: throw AssertionError("Set to null by another thread")
    }

总的来说,这和 Java 是一样的,因为通过默认的 getter 和 setter 去访问私有属性,因此不会引入函数调用开销。

编译时常量

在编译时已知其值的属性可以使用const修饰符标记为编译时常数。这些属性需要满足以下要求:

  • 在 “top-level” 中声明或者是一个 object 的成员

  • 以 String 或基本类型进行初始化

  • 没有自定义 getter

这些属性可以被当作注解使用:

const val SUBSYSTEM_DEPRECATED: String = "This subsystem is deprecated"

@Deprecated(SUBSYSTEM_DEPRECATED) fun foo() { ... }

延迟初始化属性

通常,声明为非空类型的属性必须在构造函数中进行初始化。然而,这通常不方便。例如在单元测试中,属性应该通过依赖注入进行初始化,或者通过一个 setup 方法进行初始化。在这种条件下,你不能在构造器中提供一个非空的初始化语句,但是你仍然希望在访问这个属性的时候,避免非空检查。

为了解决这种情况,你可以使用 lateinit 修饰符标记属性:

public class MyTest {
    lateinit var subject: TestSubject

    @SetUp fun setup() {
        subject = TestSubject()
    }

    @Test fun test() {
        subject.method()  // 直接取消引用
    }
}

这个修饰符只能够被用在类的 var 类型的可变属性定义中,不能用在构造方法中。并且属性不能有自定义的 getter 和 setter访问器。这个属性的类型必须是非空的,同样也不能为一个基本类型。

在一个延迟初始化的属性初始化前访问他,会导致一个特定异常,告诉你访问的时候值还没有初始化。

重写属性

参看重写属性

代理属性

最常见的属性就是从备用属性中读(或者写)。另一方面,自定义的 getter 和 setter 可以实现属性的任何操作。有些像懒值( lazy values ),根据给定的关键字从 map 中读出,读取数据库,通知一个监听者等等,像这些操作介于 getter 和 setter 模式之间。

像这样常用操作可以通过代理属性作为库来实现。

作者:jim__charles 发表于2017/6/3 14:21:37 原文链接
阅读:15 评论:0 查看评论

谈一谈苹果原生的布局框架 NSLayoutConstraint 和 VFL

$
0
0

用多了 Masonry 、Snapkit 等第三方框架,自然体会了其中的方便之处,实际上,苹果本身也有自身的自动布局框架,这次来谈谈 NSLayoutConstraint 和 VFL 两种原生自动布局框架。当然,如果对 Masonry 感兴趣,也可直接点击传送门:浅谈 Masonry 布局框架

实际上,Masonry 就是对系统原生 NSLayoutConstraint 进行封装的第三方自动布局框架。

废话补多少,直接进入正题(采用最新的 swift3.1 讲解)

NSLayoutConstraint

public convenience init(item view1: Any, 
                   attribute attr1: NSLayoutAttribute, 
                   relatedBy relation: NSLayoutRelation,   
                   toItem view2: Any?,    
                   attribute attr2: NSLayoutAttribute,   
                   multiplier: CGFloat,   
                   constant c: CGFloat)

构造函数参数说明:

NSLayoutConstraint(item: 视图, 
                   attribute: 约束属性, 
                   relatedBy: 约束关系,   
                   toItem: 参照视图,    
                   attribute: 参照属性,   
                   multiplier: 乘积,   
                   constant: 约束数值)

如果指定 宽、高 约束,则:
1. 参照视图设置为nil;
2. 参照属性选择 .notAnAttribute 。

NSLayoutConstraint 的布局方式可以简单得总结为一个公式:

view1.attr1 = view2.attr2 * multiplier + constant

下面给出代码实例,具体功能不详细说了,比较容易懂:

addConstraints([NSLayoutConstraint(item: tipLabel,
                                           attribute: .centerX,
                                           relatedBy: .equal,
                                           toItem: iconView,
                                           attribute: .centerX,
                                           multiplier: 1.0,
                                           constant: 0)])
        addConstraints([NSLayoutConstraint(item: tipLabel,
                                           attribute: .top,
                                           relatedBy: .equal,
                                           toItem: iconView,
                                           attribute: .bottom,
                                           multiplier: 1.0,
                                           constant: 20)])
        addConstraints([NSLayoutConstraint(item: tipLabel,
                                           attribute: .width,
                                           relatedBy: .equal,
                                           toItem: nil,
                                           attribute: .notAnAttribute,
                                           multiplier: 1.0,
                                           constant: 236)])

VFL 可视化格式语言

open class func constraints(withVisualFormat format: String, 
                            options opts: NSLayoutFormatOptions = [],
                             metrics: [String : Any]?, 
                             views: [String : Any]) -> [NSLayoutConstraint]

参数说明:

open class func constraints(withVisualFormat:VLF公式, 
                            options :[],
                             metrics: 约束数值 [String:数值], 
                             views: 视图字典 [String:子视图]

VFL 可视化格式语言简要说明:
H:水平方向
V:垂直方向
| :边界
[]:包含控件的名称字符串,对应关系在 views 字典中定义
():定义控件的宽/高,可以在 metrics 中指定

注意点:VFL 通常用于连续参照关系,如果遇到居中对齐,通常直接使用参照。

来一段示例代码:

// views:定义VFL中的控件名称和实际名称映射关系
        // metrics:定义VFL中()指定的常数映射关系
        let viewDict:[String:Any] = ["maskIconView":maskIconView,
                        "registerButton":registerButton]
        let metrics = ["spacing":-20]

        addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "H:|-0-[maskIconView]-0-|",
                                                      options: [],
                                                      metrics: nil,
                                                      views: viewDict))
        addConstraints(NSLayoutConstraint.constraints(withVisualFormat: "V:|-0-[maskIconView]-(spacing)-[registerButton]",
                                                      options: [],
                                                      metrics: metrics,
                                                      views: viewDict))
作者:huangfei711 发表于2017/6/3 18:05:07 原文链接
阅读:11 评论:0 查看评论

Tiny4412 Android5.0 Recovery源代码分析与定制(一)

$
0
0

在Tiny4412的Android5.0源代码中:

bootable/recovery/recovery.cpp是recovery程序的主文件。

仔细一看,对比了其它平台的recovery源代码,除了MTK对Recovery做了相应的定制外,其它的平台几乎没有看到,关于MTK平台,后续再分析。

关于Android5.0的recovery,有什么功能,在recovery.cpp中开头就已经做了详细的说明,我们来看看:

/*
 * The recovery tool communicates with the main system through /cache files.
 *   /cache/recovery/command - INPUT - command line for tool, one arg per line
 *   /cache/recovery/log - OUTPUT - combined log file from recovery run(s)
 *   /cache/recovery/intent - OUTPUT - intent that was passed in
 *
 * The arguments which may be supplied in the recovery.command file:
 *   --send_intent=anystring - write the text out to recovery.intent
 *   --update_package=path - verify install an OTA package file
 *   --wipe_data - erase user data (and cache), then reboot
 *   --wipe_cache - wipe cache (but not user data), then reboot
 *   --set_encrypted_filesystem=on|off - enables / diasables encrypted fs
 *   --just_exit - do nothing; exit and reboot
 *
 * After completing, we remove /cache/recovery/command and reboot.
 * Arguments may also be supplied in the bootloader control block (BCB).
 * These important scenarios must be safely restartable at any point:
 *
 * FACTORY RESET
 * 1. user selects "factory reset"
 * 2. main system writes "--wipe_data" to /cache/recovery/command
 * 3. main system reboots into recovery
 * 4. get_args() writes BCB with "boot-recovery" and "--wipe_data"
 *    -- after this, rebooting will restart the erase --
 * 5. erase_volume() reformats /data
 * 6. erase_volume() reformats /cache
 * 7. finish_recovery() erases BCB
 *    -- after this, rebooting will restart the main system --
 * 8. main() calls reboot() to boot main system
 *
 * OTA INSTALL
 * 1. main system downloads OTA package to /cache/some-filename.zip
 * 2. main system writes "--update_package=/cache/some-filename.zip"
 * 3. main system reboots into recovery
 * 4. get_args() writes BCB with "boot-recovery" and "--update_package=..."
 *    -- after this, rebooting will attempt to reinstall the update --
 * 5. install_package() attempts to install the update
 *    NOTE: the package install must itself be restartable from any point
 * 6. finish_recovery() erases BCB
 *    -- after this, rebooting will (try to) restart the main system --
 * 7. ** if install failed **
 *    7a. prompt_and_wait() shows an error icon and waits for the user
 *    7b; the user reboots (pulling the battery, etc) into the main system
 * 8. main() calls maybe_install_firmware_update()
 *    ** if the update contained radio/hboot firmware **:
 *    8a. m_i_f_u() writes BCB with "boot-recovery" and "--wipe_cache"
 *        -- after this, rebooting will reformat cache & restart main system --
 *    8b. m_i_f_u() writes firmware image into raw cache partition
 *    8c. m_i_f_u() writes BCB with "update-radio/hboot" and "--wipe_cache"
 *        -- after this, rebooting will attempt to reinstall firmware --
 *    8d. bootloader tries to flash firmware
 *    8e. bootloader writes BCB with "boot-recovery" (keeping "--wipe_cache")
 *        -- after this, rebooting will reformat cache & restart main system --
 *    8f. erase_volume() reformats /cache
 *    8g. finish_recovery() erases BCB
 *        -- after this, rebooting will (try to) restart the main system --
 * 9. main() calls reboot() to boot main system
 */
在这段英文注释里,详细的说明了factory_reset(Android的恢复出厂设置功能)的流程以及OTA系统更新的流程。

在这段注释得最前面说得很明白,我们只要往/cache/recovery/command中写入相应的命令:

 * The arguments which may be supplied in the recovery.command file:
 *   --send_intent=anystring - write the text out to recovery.intent
 *   --update_package=path - verify install an OTA package file
 *   --wipe_data - erase user data (and cache), then reboot
 *   --wipe_cache - wipe cache (but not user data), then reboot
 *   --set_encrypted_filesystem=on|off - enables / diasables encrypted fs
 *   --just_exit - do nothing; exit and reboot
比如写入: 

--update_package=path(对应的OTA更新的路径)

例如:

--update_package=/mnt/external_sd/xxx.zip

将这条命令写入后,再重启Android系统,recovery检测到有这个命令存在,就会去搜索这个路径,然后将这个路径做路径转换,接下来获取转换后的路径后,就挂载这个路径,然后挂载这个路径,获取OTA包,解包,校验,然后最后实现真正的更新。

如果我们往这个文件写入: --wipe_data

那么就会做出厂设置,格式化/data分区的内容。

接下来,我们来看看代码,从main函数开始分析:

进入main函数后,会将recovery产生的log信息重定向到/tmp/recovery.log这个文件里,具体代码实现如下:

//重定向标准输出和标准出错到/tmp/recovery.log 这个文件里
	//static const char *TEMPORARY_LOG_FILE = "/tmp/recovery.log";
    redirect_stdio(TEMPORARY_LOG_FILE);
redirect_stdio函数源代码:

static void redirect_stdio(const char* filename) {
    // If these fail, there's not really anywhere to complain...
    freopen(filename, "a", stdout); setbuf(stdout, NULL);
    freopen(filename, "a", stderr); setbuf(stderr, NULL);
}
我们看到,所有产生来自stdout和stderr的信息会使用freopen这个函数重定向到/tmp/recovery.log这个文件里。

stdout就是标准输出,stdout就是标准出错。标准输出就是我们平时使用的printf输出的信息。

当然也可以使用fprintf(stdout,"hello world\n");也是一样的

标准出错就是fprintf(stderr,"hello world!\n");类似的代码。

接下下来,将会判断是否使用adb的sideload来传入,通过参数--adbd来判断:

    // If this binary is started with the single argument "--adbd",
    // instead of being the normal recovery binary, it turns into kind
    // of a stripped-down version of adbd that only supports the
    // 'sideload' command.  Note this must be a real argument, not
    // anything in the command file or bootloader control block; the
    // only way recovery should be run with this argument is when it
    // starts a copy of itself from the apply_from_adb() function.
    if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
        adb_main();
        return 0;
    }
做完这些步骤以后,会初始化并装载recovery的分区表recovery.fstab,然后挂载/cache/recovery/last_log这个文件,用来输出log。

    printf("Starting recovery (pid %d) on %s", getpid(), ctime(&start));
	//装载recovery的分区表recovery.fstab
    load_volume_table();
	//在recovery中挂载/cache/recovery/last_log这个文件
	//#define LAST_LOG_FILE "/cache/recovery/last_log"
    ensure_path_mounted(LAST_LOG_FILE);
    rotate_last_logs(KEEP_LOG_COUNT);
这里主要看如何装载分区表的流程,先来看看recovery.fstab

/dev/block/by-name/boot         /boot         emmc     defaults                                                                defaults
/dev/block/by-name/recovery     /recovery     emmc     defaults                                                                defaults
/dev/block/by-name/splashscreen /splashscreen emmc     defaults                                                                defaults
/dev/block/by-name/fastboot     /fastboot     emmc     defaults                                                                defaults
/dev/block/by-name/misc         /misc         emmc     defaults                                                                defaults
/dev/block/by-name/system       /system       ext4     ro,noatime                                                              wait
/dev/block/by-name/cache        /cache        ext4     nosuid,nodev,noatime,barrier=1,data=ordered                             wait,check
/dev/block/by-name/userdata     /data         ext4     nosuid,nodev,noatime,discard,barrier=1,data=ordered,noauto_da_alloc     wait,check
/dev/block/by-name/factory      /factory      ext4     nosuid,nodev,noatime,barrier=1,data=ordered                             wait

接下来看是如果挂载的:

void load_volume_table()
{
    int i;
    int ret;
	//读recovery.fstab 这个分区表
    fstab = fs_mgr_read_fstab("/etc/recovery.fstab");
    if (!fstab) {
        LOGE("failed to read /etc/recovery.fstab\n");
        return;
    }
	//将对应的信息加入到一条链表中
    ret = fs_mgr_add_entry(fstab, "/tmp", "ramdisk", "ramdisk");
	//如果load到的分区表为空,后面做释放操作
    if (ret < 0 ) {
        LOGE("failed to add /tmp entry to fstab\n");
        fs_mgr_free_fstab(fstab);
        fstab = NULL;
        return;
    }

    printf("recovery filesystem table\n");
    printf("=========================\n");
	//到这一步,打印分区表信息,这类信息在
	//recovery启动的时候的log可以看到
	//分别是以下
	//编号|   挂载节点|  文件系统类型|  块设备|   长度
    for (i = 0; i < fstab->num_entries; ++i) {
        Volume* v = &fstab->recs[i];
        printf("  %d %s %s %s %lld\n", i, v->mount_point, v->fs_type,
               v->blk_device, v->length);
    }
    printf("\n");
}

挂载完相应的分区以后,就需要获取命令参数,因为只有挂载了对应的分区,才能访问到前面要写入command的这个文件,这样我们才能正确的打开文件,如果分区都没找到,那么当然就找不到分区上的文件,上面这个步骤是至关重要的。

//获取参数
	//这个参数也可能是从/cache/recovery/command文件中得到相应的命令
	//也就是可以往command这个文件写入对应的格式的命令即可
    get_args(&argc, &argv);

    const char *send_intent = NULL;
    const char *update_package = NULL;
    int wipe_data = 0, wipe_cache = 0, show_text = 0;
    bool just_exit = false;
    bool shutdown_after = false;

    int arg;
	//参数有擦除分区,OTA更新等
    while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
        switch (arg) {
        case 's': send_intent = optarg; break;
        case 'u': update_package = optarg; break;
        case 'w': wipe_data = wipe_cache = 1; break;
        case 'c': wipe_cache = 1; break;
        case 't': show_text = 1; break;
        case 'x': just_exit = true; break;
        case 'l': locale = optarg; break;
        case 'g': {
            if (stage == NULL || *stage == '\0') {
                char buffer[20] = "1/";
                strncat(buffer, optarg, sizeof(buffer)-3);
                stage = strdup(buffer);
            }
            break;
        }
        case 'p': shutdown_after = true; break;
        case 'r': reason = optarg; break;
        case '?':
            LOGE("Invalid command argument\n");
            continue;
        }
    }
获取到对应的命令,就会执行对应的标志,后面会根据标志来执行对应的操作。

做完以上的流程后,下面就是创建设备,设置语言信息,初始化recovery的UI界面,设置Selinux权限,代码如下:

//设置语言
    if (locale == NULL) {
        load_locale_from_cache();
    }
    printf("locale is [%s]\n", locale);
    printf("stage is [%s]\n", stage);
    printf("reason is [%s]\n", reason);
	//创建设备
    Device* device = make_device();
	//获取UI
    ui = device->GetUI();
	//设置当前的UI
    gCurrentUI = ui;
	//设置UI的语言信息
    ui->SetLocale(locale);
	//UI初始化
    ui->Init();

    int st_cur, st_max;
    if (stage != NULL && sscanf(stage, "%d/%d", &st_cur, &st_max) == 2) {
        ui->SetStage(st_cur, st_max);
    }
	//设置recovery的背景图
    ui->SetBackground(RecoveryUI::NONE);
	//设置界面上是否能够显示字符,使能ui->print函数开关
    if (show_text) ui->ShowText(true);
	//设置selinux权限,一般我会把selinux 给disabled
    struct selinux_opt seopts[] = {
      { SELABEL_OPT_PATH, "/file_contexts" }
    };

    sehandle = selabel_open(SELABEL_CTX_FILE, seopts, 1);

    if (!sehandle) {
        ui->Print("Warning: No file_contexts\n");
    }
	//虚函数,没有做什么流程
    device->StartRecovery();

    printf("Command:");
    for (arg = 0; arg < argc; arg++) {
        printf(" \"%s\"", argv[arg]);
    }
    printf("\n");
接下来是重要的环节,这个环节将会根据上面命令参数来做真正的事情了,比如恢复出厂设置,OTA更新等。

//如果update_package(也就是要升级的OTA包)不为空的情况下
	//这里要对升级包的路径做一下路径转换,这里可以自由定制自己升级包的路径
    if (update_package) {
        // For backwards compatibility on the cache partition only, if
        // we're given an old 'root' path "CACHE:foo", change it to
        // "/cache/foo".

	//这里就是做转换的方法
	//先比较传进来的recovery参数的前6个byte是否是CACHE
	//如果是将其路径转化为/cache/CACHE: ......
        if (strncmp(update_package, "CACHE:", 6) == 0) {
            int len = strlen(update_package) + 10;
            char* modified_path = (char*)malloc(len);
            strlcpy(modified_path, "/cache/", len);
            strlcat(modified_path, update_package+6, len);
            printf("(replacing path \"%s\" with \"%s\")\n",
                   update_package, modified_path);
			//这个update_package就是转换后的路径
            update_package = modified_path;
        }
    }
    printf("\n");
    property_list(print_property, NULL);
	//获取属性,这里应该是从一个文件中找到ro.build.display.id
	//获取recovery的版本信息
    property_get("ro.build.display.id", recovery_version, "");
    printf("\n");

	//定义一个安装成功的标志位INSTALL_SUCCESS  ----> 其实是个枚举,值为0
    int status = INSTALL_SUCCESS;
	//判断转换后的OTA升级包的路径是否不为空,如果不为空
	//执行install_package 函数进行升级
    if (update_package != NULL) {
        status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true);
		//判断是否升级成功
        if (status == INSTALL_SUCCESS && wipe_cache) {
			//擦除这个路径,相当于删除了这个路径下的OTA升级包
            if (erase_volume("/cache")) {
                LOGE("Cache wipe (requested by package) failed.");
            }
        }
		//如果安装不成功
        if (status != INSTALL_SUCCESS) {
            ui->Print("Installation aborted.\n");

            // If this is an eng or userdebug build, then automatically
            // turn the text display on if the script fails so the error
            // message is visible.
            char buffer[PROPERTY_VALUE_MAX+1];
            property_get("ro.build.fingerprint", buffer, "");
            if (strstr(buffer, ":userdebug/") || strstr(buffer, ":eng/")) {
                ui->ShowText(true);
            }
        }
    }
	//如果跑的是格式化数据区,那么就走这个流程
	else if (wipe_data) {
        if (device->WipeData()) status = INSTALL_ERROR;
		//格式化/data分区
        if (erase_volume("/data")) status = INSTALL_ERROR;
        if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
        if (erase_persistent_partition() == -1 ) status = INSTALL_ERROR;
        if (status != INSTALL_SUCCESS) ui->Print("Data wipe failed.\n");
    } 
	//格式化cache分区
	else if (wipe_cache) {
        if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
        if (status != INSTALL_SUCCESS) ui->Print("Cache wipe failed.\n");
    } 
	else if (!just_exit) {
        status = INSTALL_NONE;  // No command specified
        ui->SetBackground(RecoveryUI::NO_COMMAND);
    }
	//如果安装失败或者。。。
    if (status == INSTALL_ERROR || status == INSTALL_CORRUPT) {
        copy_logs();
		//显示错误的LOGO
        ui->SetBackground(RecoveryUI::ERROR);
    }
    Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
    if (status != INSTALL_SUCCESS || ui->IsTextVisible()) {
        Device::BuiltinAction temp = prompt_and_wait(device, status);
        if (temp != Device::NO_ACTION) after = temp;
    }
	
    // Save logs and clean up before rebooting or shutting down.
    //完成recovery升级
    finish_recovery(send_intent);

    switch (after) {
        case Device::SHUTDOWN:
            ui->Print("Shutting down...\n");
            property_set(ANDROID_RB_PROPERTY, "shutdown,");
            break;

        case Device::REBOOT_BOOTLOADER:
            ui->Print("Rebooting to bootloader...\n");
            property_set(ANDROID_RB_PROPERTY, "reboot,bootloader");
            break;

        default:
            ui->Print("Rebooting...\n");
            property_set(ANDROID_RB_PROPERTY, "reboot,");
            break;
    }
    sleep(5); // should reboot before this finishes
    return EXIT_SUCCESS;
这里面,我们最常用的即是OTA更新和恢复出厂设置,先来说说恢复出厂设置,这个功能就是所谓的手机双清,众所周知,Android手机在使用很久后,由于垃圾数据,以及其它的因素会导致手机的反应越来越慢,这让人烦恼不已,所以就需要双清,双清一般就是清除

/data分区和/cache分区,代码流程很详细,有兴趣可以自己去分析。

接下来看看OTA是如何实现更新的,我们看到install_ota_package这个函数,执行到这个函数,看到源码:

//安装更新包
int
install_package(const char* path, int* wipe_cache, const char* install_file,
                bool needs_mount)
{
    FILE* install_log = fopen_path(install_file, "w");
    if (install_log) {
        fputs(path, install_log);
        fputc('\n', install_log);
    } else {
        LOGE("failed to open last_install: %s\n", strerror(errno));
    }
    int result;
	//设置安装挂载对应的节点
	//这一步是关键
    if (setup_install_mounts() != 0) {
        LOGE("failed to set up expected mounts for install; aborting\n");
        result = INSTALL_ERROR;
    } else {
    	//到这里才是真正的去安装OTA包
        result = really_install_package(path, wipe_cache, needs_mount);
    }
	//如果返回结果为0,那么安装就成功了
    if (install_log) {
        fputc(result == INSTALL_SUCCESS ? '1' : '0', install_log);
        fputc('\n', install_log);
        fclose(install_log);
    }
    return result;
}
其实到了really_install_package这一步,才是真正做到OTA更新,但是在OTA更新之前至关重要的一步就是设置安装挂载对应的节点了,我曾经掉入此坑,现在拿出来分析一下,我们来看看setup_install_mounts这个函数:

//设置安装挂载的节点
int setup_install_mounts() {
    if (fstab == NULL) {
        LOGE("can't set up install mounts: no fstab loaded\n");
        return -1;
    }
    for (int i = 0; i < fstab->num_entries; ++i) {
        Volume* v = fstab->recs + i;
	//如果判断挂载的路径是/tmp 或者/cache
	//那么就挂载对应的节点,而其它的节点都不会去挂载
        if (strcmp(v->mount_point, "/tmp") == 0 ||
            strcmp(v->mount_point, "/cache") == 0) {
            if (ensure_path_mounted(v->mount_point) != 0) {
                LOGE("failed to mount %s\n", v->mount_point);
                return -1;
            }

        }
		//如果不是/tmp或者/cache这两个节点,则默认就会卸载所有的挂载节点
		else {
        	//卸载所有的挂载节点
            if (ensure_path_unmounted(v->mount_point) != 0) {
                LOGE("failed to unmount %s\n", v->mount_point);
                return -1;
            }
        }
    }
    return 0;
}
如果在安装更新的时候,OTA包经过路径转换后不是放在/tmp和/cache这个路径下的时候,那么就会走else分支,从而卸载所有的挂载节点,这样就会导致,传的路径正确,却OTA更新不成功,如果是做自己定制的路径,这一步一定要小心,我们可以在这里继续添加定制的挂载点。

那么,执行完设置挂载节点的函数后,接下来就是执行真正的OTA更新了,我们来看看:

static int
really_install_package(const char *path, int* wipe_cache, bool needs_mount)
{
    //设置更新时的背景
    ui->SetBackground(RecoveryUI::INSTALLING_UPDATE);
    ui->Print("Finding update package...\n");
    // Give verification half the progress bar...
    //设置进度条的类型
    ui->SetProgressType(RecoveryUI::DETERMINATE);
    //显示进度条
    ui->ShowProgress(VERIFICATION_PROGRESS_FRACTION, VERIFICATION_PROGRESS_TIME);
    LOGI("Update location: %s\n", path);
    //在屏幕上打印 Opening update package..
    // Map the update package into memory.
    ui->Print("Opening update package...\n");
	//patch是OTA的路径,need_mount参数表示是否需要挂载,1挂载,0,不挂载
    if (path && needs_mount) {
        if (path[0] == '@') {
            ensure_path_mounted(path+1);
        } else {
        	//挂载OTA升级包的路径------> 一般是执行这个流程
            ensure_path_mounted(path);
        }
    }

    MemMapping map;
    if (sysMapFile(path, &map) != 0) {
        LOGE("failed to map file\n");
        return INSTALL_CORRUPT;
    }

    int numKeys;
    //获取校验公钥文件
    Certificate* loadedKeys = load_keys(PUBLIC_KEYS_FILE, &numKeys);
    if (loadedKeys == NULL) {
        LOGE("Failed to load keys\n");
        return INSTALL_CORRUPT;
    }
    LOGI("%d key(s) loaded from %s\n", numKeys, PUBLIC_KEYS_FILE);

    ui->Print("Verifying update package...\n");

    int err;
	//校验文件
    err = verify_file(map.addr, map.length, loadedKeys, numKeys);
    free(loadedKeys);
    LOGI("verify_file returned %d\n", err);
	//如果校验不成功
    if (err != VERIFY_SUCCESS) {
		//打印签名失败
        LOGE("signature verification failed\n");
        sysReleaseMap(&map);
        return INSTALL_CORRUPT;
    }

    /* Try to open the package.
     */
    //尝试去打开ota压缩包
    ZipArchive zip;
    err = mzOpenZipArchive(map.addr, map.length, &zip);
    if (err != 0) {
        LOGE("Can't open %s\n(%s)\n", path, err != -1 ? strerror(err) : "bad");
        sysReleaseMap(&map);
        return INSTALL_CORRUPT;
    }

    /* Verify and install the contents of the package.
     */
    //开始安装升级包
    ui->Print("Installing update...\n");
    ui->SetEnableReboot(false);
    int result = try_update_binary(path, &zip, wipe_cache);
	//安装成功后自动重启
    ui->SetEnableReboot(true);
    ui->Print("\n");

    sysReleaseMap(&map);
	//返回结果
    return result;
}
关于recovery的大致流程,我们分析至此,关于如何像MTK平台一样,定制recovery,这就需要读者能够读懂recovery的流程,然后加入自己的代码进行定制,当然我们也会看到,一些recovery花样百出,很多UI做了自己的,而不是用安卓系统原生态的,安卓系统recovery原生态的UI如下:




如何定制相应的UI,后续我们会对recovery源代码中的UI显示做进一步的分析。。。。

接下来,贴出Android5.0的recovery.cpp代码和注释:

/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <limits.h>
#include <linux/input.h>
#include <stdarg.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>

#include "bootloader.h"
#include "common.h"
#include "cutils/properties.h"
#include "cutils/android_reboot.h"
#include "install.h"
#include "minui/minui.h"
#include "minzip/DirUtil.h"
#include "roots.h"
#include "ui.h"
#include "screen_ui.h"
#include "device.h"
#include "adb_install.h"
extern "C" {
#include "minadbd/adb.h"
#include "fuse_sideload.h"
#include "fuse_sdcard_provider.h"
}

struct selabel_handle *sehandle;

static const struct option OPTIONS[] = {
  { "send_intent", required_argument, NULL, 's' },
  { "update_package", required_argument, NULL, 'u' },
  { "wipe_data", no_argument, NULL, 'w' },
  { "wipe_cache", no_argument, NULL, 'c' },
  { "show_text", no_argument, NULL, 't' },
  { "just_exit", no_argument, NULL, 'x' },
  { "locale", required_argument, NULL, 'l' },
  { "stages", required_argument, NULL, 'g' },
  { "shutdown_after", no_argument, NULL, 'p' },
  { "reason", required_argument, NULL, 'r' },
  { NULL, 0, NULL, 0 },
};

#define LAST_LOG_FILE "/cache/recovery/last_log"

static const char *CACHE_LOG_DIR = "/cache/recovery";
static const char *COMMAND_FILE = "/cache/recovery/command";
static const char *INTENT_FILE = "/cache/recovery/intent";
static const char *LOG_FILE = "/cache/recovery/log";
static const char *LAST_INSTALL_FILE = "/cache/recovery/last_install";
static const char *LOCALE_FILE = "/cache/recovery/last_locale";
static const char *CACHE_ROOT = "/cache";
static const char *SDCARD_ROOT = "/sdcard";
static const char *TEMPORARY_LOG_FILE = "/tmp/recovery.log";
static const char *TEMPORARY_INSTALL_FILE = "/tmp/last_install";

#define KEEP_LOG_COUNT 10

RecoveryUI* ui = NULL;
char* locale = NULL;
char recovery_version[PROPERTY_VALUE_MAX+1];
char* stage = NULL;
char* reason = NULL;

/*
 * The recovery tool communicates with the main system through /cache files.
 *   /cache/recovery/command - INPUT - command line for tool, one arg per line
 *   /cache/recovery/log - OUTPUT - combined log file from recovery run(s)
 *   /cache/recovery/intent - OUTPUT - intent that was passed in
 *
 * The arguments which may be supplied in the recovery.command file:
 *   --send_intent=anystring - write the text out to recovery.intent
 *   --update_package=path - verify install an OTA package file
 *   --wipe_data - erase user data (and cache), then reboot
 *   --wipe_cache - wipe cache (but not user data), then reboot
 *   --set_encrypted_filesystem=on|off - enables / diasables encrypted fs
 *   --just_exit - do nothing; exit and reboot
 *
 * After completing, we remove /cache/recovery/command and reboot.
 * Arguments may also be supplied in the bootloader control block (BCB).
 * These important scenarios must be safely restartable at any point:
 *
 * FACTORY RESET
 * 1. user selects "factory reset"
 * 2. main system writes "--wipe_data" to /cache/recovery/command
 * 3. main system reboots into recovery
 * 4. get_args() writes BCB with "boot-recovery" and "--wipe_data"
 *    -- after this, rebooting will restart the erase --
 * 5. erase_volume() reformats /data
 * 6. erase_volume() reformats /cache
 * 7. finish_recovery() erases BCB
 *    -- after this, rebooting will restart the main system --
 * 8. main() calls reboot() to boot main system
 *
 * OTA INSTALL
 * 1. main system downloads OTA package to /cache/some-filename.zip
 * 2. main system writes "--update_package=/cache/some-filename.zip"
 * 3. main system reboots into recovery
 * 4. get_args() writes BCB with "boot-recovery" and "--update_package=..."
 *    -- after this, rebooting will attempt to reinstall the update --
 * 5. install_package() attempts to install the update
 *    NOTE: the package install must itself be restartable from any point
 * 6. finish_recovery() erases BCB
 *    -- after this, rebooting will (try to) restart the main system --
 * 7. ** if install failed **
 *    7a. prompt_and_wait() shows an error icon and waits for the user
 *    7b; the user reboots (pulling the battery, etc) into the main system
 * 8. main() calls maybe_install_firmware_update()
 *    ** if the update contained radio/hboot firmware **:
 *    8a. m_i_f_u() writes BCB with "boot-recovery" and "--wipe_cache"
 *        -- after this, rebooting will reformat cache & restart main system --
 *    8b. m_i_f_u() writes firmware image into raw cache partition
 *    8c. m_i_f_u() writes BCB with "update-radio/hboot" and "--wipe_cache"
 *        -- after this, rebooting will attempt to reinstall firmware --
 *    8d. bootloader tries to flash firmware
 *    8e. bootloader writes BCB with "boot-recovery" (keeping "--wipe_cache")
 *        -- after this, rebooting will reformat cache & restart main system --
 *    8f. erase_volume() reformats /cache
 *    8g. finish_recovery() erases BCB
 *        -- after this, rebooting will (try to) restart the main system --
 * 9. main() calls reboot() to boot main system
 */

static const int MAX_ARG_LENGTH = 4096;
static const int MAX_ARGS = 100;

// open a given path, mounting partitions as necessary
FILE*
fopen_path(const char *path, const char *mode) {
    if (ensure_path_mounted(path) != 0) {
        LOGE("Can't mount %s\n", path);
        return NULL;
    }

    // When writing, try to create the containing directory, if necessary.
    // Use generous permissions, the system (init.rc) will reset them.
    if (strchr("wa", mode[0])) dirCreateHierarchy(path, 0777, NULL, 1, sehandle);

    FILE *fp = fopen(path, mode);
    return fp;
}

static void redirect_stdio(const char* filename) {
    // If these fail, there's not really anywhere to complain...
    freopen(filename, "a", stdout); setbuf(stdout, NULL);
    freopen(filename, "a", stderr); setbuf(stderr, NULL);
}

// close a file, log an error if the error indicator is set
static void
check_and_fclose(FILE *fp, const char *name) {
    fflush(fp);
    if (ferror(fp)) LOGE("Error in %s\n(%s)\n", name, strerror(errno));
    fclose(fp);
}

// command line args come from, in decreasing precedence:
//   - the actual command line
//   - the bootloader control block (one per line, after "recovery")
//   - the contents of COMMAND_FILE (one per line)
static void
get_args(int *argc, char ***argv) {
    struct bootloader_message boot;
    memset(&boot, 0, sizeof(boot));
    get_bootloader_message(&boot);  // this may fail, leaving a zeroed structure
    stage = strndup(boot.stage, sizeof(boot.stage));

    if (boot.command[0] != 0 && boot.command[0] != 255) {
        LOGI("Boot command: %.*s\n", (int)sizeof(boot.command), boot.command);
    }

    if (boot.status[0] != 0 && boot.status[0] != 255) {
        LOGI("Boot status: %.*s\n", (int)sizeof(boot.status), boot.status);
    }

    // --- if arguments weren't supplied, look in the bootloader control block
    if (*argc <= 1) {
        boot.recovery[sizeof(boot.recovery) - 1] = '\0';  // Ensure termination
        const char *arg = strtok(boot.recovery, "\n");
        if (arg != NULL && !strcmp(arg, "recovery")) {
            *argv = (char **) malloc(sizeof(char *) * MAX_ARGS);
            (*argv)[0] = strdup(arg);
            for (*argc = 1; *argc < MAX_ARGS; ++*argc) {
                if ((arg = strtok(NULL, "\n")) == NULL) break;
                (*argv)[*argc] = strdup(arg);
            }
            LOGI("Got arguments from boot message\n");
        } else if (boot.recovery[0] != 0 && boot.recovery[0] != 255) {
            LOGE("Bad boot message\n\"%.20s\"\n", boot.recovery);
        }
    }

    // --- if that doesn't work, try the command file
    if (*argc <= 1) {
        FILE *fp = fopen_path(COMMAND_FILE, "r");
        if (fp != NULL) {
            char *token;
            char *argv0 = (*argv)[0];
            *argv = (char **) malloc(sizeof(char *) * MAX_ARGS);
            (*argv)[0] = argv0;  // use the same program name

            char buf[MAX_ARG_LENGTH];
            for (*argc = 1; *argc < MAX_ARGS; ++*argc) {
                if (!fgets(buf, sizeof(buf), fp)) break;
                token = strtok(buf, "\r\n");
                if (token != NULL) {
                    (*argv)[*argc] = strdup(token);  // Strip newline.
                } else {
                    --*argc;
                }
            }

            check_and_fclose(fp, COMMAND_FILE);
            LOGI("Got arguments from %s\n", COMMAND_FILE);
        }
    }

    // --> write the arguments we have back into the bootloader control block
    // always boot into recovery after this (until finish_recovery() is called)
    strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
    strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
    int i;
    for (i = 1; i < *argc; ++i) {
        strlcat(boot.recovery, (*argv)[i], sizeof(boot.recovery));
        strlcat(boot.recovery, "\n", sizeof(boot.recovery));
    }
    set_bootloader_message(&boot);
}

static void
set_sdcard_update_bootloader_message() {
    struct bootloader_message boot;
    memset(&boot, 0, sizeof(boot));
    strlcpy(boot.command, "boot-recovery", sizeof(boot.command));
    strlcpy(boot.recovery, "recovery\n", sizeof(boot.recovery));
    set_bootloader_message(&boot);
}

// How much of the temp log we have copied to the copy in cache.
static long tmplog_offset = 0;

static void
copy_log_file(const char* source, const char* destination, int append) {
    FILE *log = fopen_path(destination, append ? "a" : "w");
    if (log == NULL) {
        LOGE("Can't open %s\n", destination);
    } else {
        FILE *tmplog = fopen(source, "r");
        if (tmplog != NULL) {
            if (append) {
                fseek(tmplog, tmplog_offset, SEEK_SET);  // Since last write
            }
            char buf[4096];
            while (fgets(buf, sizeof(buf), tmplog)) fputs(buf, log);
            if (append) {
                tmplog_offset = ftell(tmplog);
            }
            check_and_fclose(tmplog, source);
        }
        check_and_fclose(log, destination);
    }
}

// Rename last_log -> last_log.1 -> last_log.2 -> ... -> last_log.$max
// Overwrites any existing last_log.$max.
static void
rotate_last_logs(int max) {
    char oldfn[256];
    char newfn[256];

    int i;
    for (i = max-1; i >= 0; --i) {
        snprintf(oldfn, sizeof(oldfn), (i==0) ? LAST_LOG_FILE : (LAST_LOG_FILE ".%d"), i);
        snprintf(newfn, sizeof(newfn), LAST_LOG_FILE ".%d", i+1);
        // ignore errors
        rename(oldfn, newfn);
    }
}

static void
copy_logs() {
    // Copy logs to cache so the system can find out what happened.
    copy_log_file(TEMPORARY_LOG_FILE, LOG_FILE, true);
    copy_log_file(TEMPORARY_LOG_FILE, LAST_LOG_FILE, false);
    copy_log_file(TEMPORARY_INSTALL_FILE, LAST_INSTALL_FILE, false);
    chmod(LOG_FILE, 0600);
    chown(LOG_FILE, 1000, 1000);   // system user
    chmod(LAST_LOG_FILE, 0640);
    chmod(LAST_INSTALL_FILE, 0644);
    sync();
}

// clear the recovery command and prepare to boot a (hopefully working) system,
// copy our log file to cache as well (for the system to read), and
// record any intent we were asked to communicate back to the system.
// this function is idempotent: call it as many times as you like.
static void
finish_recovery(const char *send_intent) {
    // By this point, we're ready to return to the main system...
    if (send_intent != NULL) {
        FILE *fp = fopen_path(INTENT_FILE, "w");
        if (fp == NULL) {
            LOGE("Can't open %s\n", INTENT_FILE);
        } else {
            fputs(send_intent, fp);
            check_and_fclose(fp, INTENT_FILE);
        }
    }

    // Save the locale to cache, so if recovery is next started up
    // without a --locale argument (eg, directly from the bootloader)
    // it will use the last-known locale.
    if (locale != NULL) {
        LOGI("Saving locale \"%s\"\n", locale);
        FILE* fp = fopen_path(LOCALE_FILE, "w");
        fwrite(locale, 1, strlen(locale), fp);
        fflush(fp);
        fsync(fileno(fp));
        check_and_fclose(fp, LOCALE_FILE);
    }

    copy_logs();

    // Reset to normal system boot so recovery won't cycle indefinitely.
    struct bootloader_message boot;
    memset(&boot, 0, sizeof(boot));
    set_bootloader_message(&boot);

    // Remove the command file, so recovery won't repeat indefinitely.
    if (ensure_path_mounted(COMMAND_FILE) != 0 ||
        (unlink(COMMAND_FILE) && errno != ENOENT)) {
        LOGW("Can't unlink %s\n", COMMAND_FILE);
    }

    ensure_path_unmounted(CACHE_ROOT);
    sync();  // For good measure.
}

typedef struct _saved_log_file {
    char* name;
    struct stat st;
    unsigned char* data;
    struct _saved_log_file* next;
} saved_log_file;

static int
erase_volume(const char *volume) {
    bool is_cache = (strcmp(volume, CACHE_ROOT) == 0);

    ui->SetBackground(RecoveryUI::ERASING);
    ui->SetProgressType(RecoveryUI::INDETERMINATE);

    saved_log_file* head = NULL;

    if (is_cache) {
        // If we're reformatting /cache, we load any
        // "/cache/recovery/last*" files into memory, so we can restore
        // them after the reformat.

        ensure_path_mounted(volume);

        DIR* d;
        struct dirent* de;
        d = opendir(CACHE_LOG_DIR);
        if (d) {
            char path[PATH_MAX];
            strcpy(path, CACHE_LOG_DIR);
            strcat(path, "/");
            int path_len = strlen(path);
            while ((de = readdir(d)) != NULL) {
                if (strncmp(de->d_name, "last", 4) == 0) {
                    saved_log_file* p = (saved_log_file*) malloc(sizeof(saved_log_file));
                    strcpy(path+path_len, de->d_name);
                    p->name = strdup(path);
                    if (stat(path, &(p->st)) == 0) {
                        // truncate files to 512kb
                        if (p->st.st_size > (1 << 19)) {
                            p->st.st_size = 1 << 19;
                        }
                        p->data = (unsigned char*) malloc(p->st.st_size);
                        FILE* f = fopen(path, "rb");
                        fread(p->data, 1, p->st.st_size, f);
                        fclose(f);
                        p->next = head;
                        head = p;
                    } else {
                        free(p);
                    }
                }
            }
            closedir(d);
        } else {
            if (errno != ENOENT) {
                printf("opendir failed: %s\n", strerror(errno));
            }
        }
    }

    ui->Print("Formatting %s...\n", volume);

    ensure_path_unmounted(volume);
    int result = format_volume(volume);

    if (is_cache) {
        while (head) {
            FILE* f = fopen_path(head->name, "wb");
            if (f) {
                fwrite(head->data, 1, head->st.st_size, f);
                fclose(f);
                chmod(head->name, head->st.st_mode);
                chown(head->name, head->st.st_uid, head->st.st_gid);
            }
            free(head->name);
            free(head->data);
            saved_log_file* temp = head->next;
            free(head);
            head = temp;
        }

        // Any part of the log we'd copied to cache is now gone.
        // Reset the pointer so we copy from the beginning of the temp
        // log.
        tmplog_offset = 0;
        copy_logs();
    }

    return result;
}

static const char**
prepend_title(const char* const* headers) {
    // count the number of lines in our title, plus the
    // caller-provided headers.
    int count = 3;   // our title has 3 lines
    const char* const* p;
    for (p = headers; *p; ++p, ++count);

    const char** new_headers = (const char**)malloc((count+1) * sizeof(char*));
    const char** h = new_headers;
    *(h++) = "Android system recovery <" EXPAND(RECOVERY_API_VERSION) "e>";
    *(h++) = recovery_version;
    *(h++) = "";
    for (p = headers; *p; ++p, ++h) *h = *p;
    *h = NULL;

    return new_headers;
}

static int
get_menu_selection(const char* const * headers, const char* const * items,
                   int menu_only, int initial_selection, Device* device) {
    // throw away keys pressed previously, so user doesn't
    // accidentally trigger menu items.
    ui->FlushKeys();

    ui->StartMenu(headers, items, initial_selection);
    int selected = initial_selection;
    int chosen_item = -1;

    while (chosen_item < 0) {
        int key = ui->WaitKey();
        int visible = ui->IsTextVisible();

        if (key == -1) {   // ui_wait_key() timed out
            if (ui->WasTextEverVisible()) {
                continue;
            } else {
                LOGI("timed out waiting for key input; rebooting.\n");
                ui->EndMenu();
                return 0; // XXX fixme
            }
        }

        int action = device->HandleMenuKey(key, visible);

        if (action < 0) {
            switch (action) {
                case Device::kHighlightUp:
                    --selected;
                    selected = ui->SelectMenu(selected);
                    break;
                case Device::kHighlightDown:
                    ++selected;
                    selected = ui->SelectMenu(selected);
                    break;
                case Device::kInvokeItem:
                    chosen_item = selected;
                    break;
                case Device::kNoAction:
                    break;
            }
        } else if (!menu_only) {
            chosen_item = action;
        }
    }

    ui->EndMenu();
    return chosen_item;
}

static int compare_string(const void* a, const void* b) {
    return strcmp(*(const char**)a, *(const char**)b);
}

// Returns a malloc'd path, or NULL.
static char*
browse_directory(const char* path, Device* device) {
    ensure_path_mounted(path);

    const char* MENU_HEADERS[] = { "Choose a package to install:",
                                   path,
                                   "",
                                   NULL };
    DIR* d;
    struct dirent* de;
    d = opendir(path);
    if (d == NULL) {
        LOGE("error opening %s: %s\n", path, strerror(errno));
        return NULL;
    }

    const char** headers = prepend_title(MENU_HEADERS);

    int d_size = 0;
    int d_alloc = 10;
    char** dirs = (char**)malloc(d_alloc * sizeof(char*));
    int z_size = 1;
    int z_alloc = 10;
    char** zips = (char**)malloc(z_alloc * sizeof(char*));
    zips[0] = strdup("../");

    while ((de = readdir(d)) != NULL) {
        int name_len = strlen(de->d_name);

        if (de->d_type == DT_DIR) {
            // skip "." and ".." entries
            if (name_len == 1 && de->d_name[0] == '.') continue;
            if (name_len == 2 && de->d_name[0] == '.' &&
                de->d_name[1] == '.') continue;

            if (d_size >= d_alloc) {
                d_alloc *= 2;
                dirs = (char**)realloc(dirs, d_alloc * sizeof(char*));
            }
            dirs[d_size] = (char*)malloc(name_len + 2);
            strcpy(dirs[d_size], de->d_name);
            dirs[d_size][name_len] = '/';
            dirs[d_size][name_len+1] = '\0';
            ++d_size;
        } else if (de->d_type == DT_REG &&
                   name_len >= 4 &&
                   strncasecmp(de->d_name + (name_len-4), ".zip", 4) == 0) {
            if (z_size >= z_alloc) {
                z_alloc *= 2;
                zips = (char**)realloc(zips, z_alloc * sizeof(char*));
            }
            zips[z_size++] = strdup(de->d_name);
        }
    }
    closedir(d);

    qsort(dirs, d_size, sizeof(char*), compare_string);
    qsort(zips, z_size, sizeof(char*), compare_string);

    // append dirs to the zips list
    if (d_size + z_size + 1 > z_alloc) {
        z_alloc = d_size + z_size + 1;
        zips = (char**)realloc(zips, z_alloc * sizeof(char*));
    }
    memcpy(zips + z_size, dirs, d_size * sizeof(char*));
    free(dirs);
    z_size += d_size;
    zips[z_size] = NULL;

    char* result;
    int chosen_item = 0;
    while (true) {
        chosen_item = get_menu_selection(headers, zips, 1, chosen_item, device);

        char* item = zips[chosen_item];
        int item_len = strlen(item);
        if (chosen_item == 0) {          // item 0 is always "../"
            // go up but continue browsing (if the caller is update_directory)
            result = NULL;
            break;
        }

        char new_path[PATH_MAX];
        strlcpy(new_path, path, PATH_MAX);
        strlcat(new_path, "/", PATH_MAX);
        strlcat(new_path, item, PATH_MAX);

        if (item[item_len-1] == '/') {
            // recurse down into a subdirectory
            new_path[strlen(new_path)-1] = '\0';  // truncate the trailing '/'
            result = browse_directory(new_path, device);
            if (result) break;
        } else {
            // selected a zip file: return the malloc'd path to the caller.
            result = strdup(new_path);
            break;
        }
    }

    int i;
    for (i = 0; i < z_size; ++i) free(zips[i]);
    free(zips);
    free(headers);

    return result;
}

static void
wipe_data(int confirm, Device* device) {
    if (confirm) {
        static const char** title_headers = NULL;

        if (title_headers == NULL) {
            const char* headers[] = { "Confirm wipe of all user data?",
                                      "  THIS CAN NOT BE UNDONE.",
                                      "",
                                      NULL };
            title_headers = prepend_title((const char**)headers);
        }

        const char* items[] = { " No",
                                " No",
                                " No",
                                " No",
                                " No",
                                " No",
                                " No",
                                " Yes -- delete all user data",   // [7]
                                " No",
                                " No",
                                " No",
                                NULL };

        int chosen_item = get_menu_selection(title_headers, items, 1, 0, device);
        if (chosen_item != 7) {
            return;
        }
    }

    ui->Print("\n-- Wiping data...\n");
    device->WipeData();
    erase_volume("/data");
    erase_volume("/cache");
    erase_persistent_partition();
    ui->Print("Data wipe complete.\n");
}

static void file_to_ui(const char* fn) {
    FILE *fp = fopen_path(fn, "re");
    if (fp == NULL) {
        ui->Print("  Unable to open %s: %s\n", fn, strerror(errno));
        return;
    }
    char line[1024];
    int ct = 0;
    redirect_stdio("/dev/null");
    while(fgets(line, sizeof(line), fp) != NULL) {
        ui->Print("%s", line);
        ct++;
        if (ct % 30 == 0) {
            // give the user time to glance at the entries
            ui->WaitKey();
        }
    }
    redirect_stdio(TEMPORARY_LOG_FILE);
    fclose(fp);
}

static void choose_recovery_file(Device* device) {
    int i;
    static const char** title_headers = NULL;
    char *filename;
    const char* headers[] = { "Select file to view",
                              "",
                              NULL };
    char* entries[KEEP_LOG_COUNT + 2];
    memset(entries, 0, sizeof(entries));

    for (i = 0; i < KEEP_LOG_COUNT; i++) {
        char *filename;
        if (asprintf(&filename, (i==0) ? LAST_LOG_FILE : (LAST_LOG_FILE ".%d"), i) == -1) {
            // memory allocation failure - return early. Should never happen.
            return;
        }
        if ((ensure_path_mounted(filename) != 0) || (access(filename, R_OK) == -1)) {
            free(filename);
            entries[i+1] = NULL;
            break;
        }
        entries[i+1] = filename;
    }

    entries[0] = strdup("Go back");
    title_headers = prepend_title((const char**)headers);

    while(1) {
        int chosen_item = get_menu_selection(title_headers, entries, 1, 0, device);
        if (chosen_item == 0) break;
        file_to_ui(entries[chosen_item]);
    }

    for (i = 0; i < KEEP_LOG_COUNT + 1; i++) {
        free(entries[i]);
    }
}

// Return REBOOT, SHUTDOWN, or REBOOT_BOOTLOADER.  Returning NO_ACTION
// means to take the default, which is to reboot or shutdown depending
// on if the --shutdown_after flag was passed to recovery.
static Device::BuiltinAction
prompt_and_wait(Device* device, int status) {
    const char* const* headers = prepend_title(device->GetMenuHeaders());

    for (;;) {
        finish_recovery(NULL);
        switch (status) {
            case INSTALL_SUCCESS:
            case INSTALL_NONE:
                ui->SetBackground(RecoveryUI::NO_COMMAND);
                break;

            case INSTALL_ERROR:
            case INSTALL_CORRUPT:
                ui->SetBackground(RecoveryUI::ERROR);
                break;
        }
        ui->SetProgressType(RecoveryUI::EMPTY);

        int chosen_item = get_menu_selection(headers, device->GetMenuItems(), 0, 0, device);

        // device-specific code may take some action here.  It may
        // return one of the core actions handled in the switch
        // statement below.
        Device::BuiltinAction chosen_action = device->InvokeMenuItem(chosen_item);

        int wipe_cache = 0;
        switch (chosen_action) {
            case Device::NO_ACTION:
                break;

            case Device::REBOOT:
            case Device::SHUTDOWN:
            case Device::REBOOT_BOOTLOADER:
                return chosen_action;

            case Device::WIPE_DATA:
                wipe_data(ui->IsTextVisible(), device);
                if (!ui->IsTextVisible()) return Device::NO_ACTION;
                break;

            case Device::WIPE_CACHE:
                ui->Print("\n-- Wiping cache...\n");
                erase_volume("/cache");
                ui->Print("Cache wipe complete.\n");
                if (!ui->IsTextVisible()) return Device::NO_ACTION;
                break;

            case Device::APPLY_EXT: {
                ensure_path_mounted(SDCARD_ROOT);
                char* path = browse_directory(SDCARD_ROOT, device);
                if (path == NULL) {
                    ui->Print("\n-- No package file selected.\n", path);
                    break;
                }

                ui->Print("\n-- Install %s ...\n", path);
                set_sdcard_update_bootloader_message();
                void* token = start_sdcard_fuse(path);

                int status = install_package(FUSE_SIDELOAD_HOST_PATHNAME, &wipe_cache,
                                             TEMPORARY_INSTALL_FILE, false);

                finish_sdcard_fuse(token);
                ensure_path_unmounted(SDCARD_ROOT);

                if (status == INSTALL_SUCCESS && wipe_cache) {
                    ui->Print("\n-- Wiping cache (at package request)...\n");
                    if (erase_volume("/cache")) {
                        ui->Print("Cache wipe failed.\n");
                    } else {
                        ui->Print("Cache wipe complete.\n");
                    }
                }

                if (status >= 0) {
                    if (status != INSTALL_SUCCESS) {
                        ui->SetBackground(RecoveryUI::ERROR);
                        ui->Print("Installation aborted.\n");
                    } else if (!ui->IsTextVisible()) {
                        return Device::NO_ACTION;  // reboot if logs aren't visible
                    } else {
                        ui->Print("\nInstall from sdcard complete.\n");
                    }
                }
                break;
            }

            case Device::APPLY_CACHE:
                ui->Print("\nAPPLY_CACHE is deprecated.\n");
                break;

            case Device::READ_RECOVERY_LASTLOG:
                choose_recovery_file(device);
                break;

            case Device::APPLY_ADB_SIDELOAD:
                status = apply_from_adb(ui, &wipe_cache, TEMPORARY_INSTALL_FILE);
                if (status >= 0) {
                    if (status != INSTALL_SUCCESS) {
                        ui->SetBackground(RecoveryUI::ERROR);
                        ui->Print("Installation aborted.\n");
                        copy_logs();
                    } else if (!ui->IsTextVisible()) {
                        return Device::NO_ACTION;  // reboot if logs aren't visible
                    } else {
                        ui->Print("\nInstall from ADB complete.\n");
                    }
                }
                break;
        }
    }
}

static void
print_property(const char *key, const char *name, void *cookie) {
    printf("%s=%s\n", key, name);
}

static void
load_locale_from_cache() {
    FILE* fp = fopen_path(LOCALE_FILE, "r");
    char buffer[80];
    if (fp != NULL) {
        fgets(buffer, sizeof(buffer), fp);
        int j = 0;
        unsigned int i;
        for (i = 0; i < sizeof(buffer) && buffer[i]; ++i) {
            if (!isspace(buffer[i])) {
                buffer[j++] = buffer[i];
            }
        }
        buffer[j] = 0;
        locale = strdup(buffer);
        check_and_fclose(fp, LOCALE_FILE);
    }
}

static RecoveryUI* gCurrentUI = NULL;

void
ui_print(const char* format, ...) {
    char buffer[256];

    va_list ap;
    va_start(ap, format);
    vsnprintf(buffer, sizeof(buffer), format, ap);
    va_end(ap);

    if (gCurrentUI != NULL) {
        gCurrentUI->Print("%s", buffer);
    } else {
        fputs(buffer, stdout);
    }
}

int
main(int argc, char **argv) {
    time_t start = time(NULL);

	//重定向标准输出和标准出错到/tmp/recovery.log 这个文件里
	//static const char *TEMPORARY_LOG_FILE = "/tmp/recovery.log";
    redirect_stdio(TEMPORARY_LOG_FILE);

    // If this binary is started with the single argument "--adbd",
    // instead of being the normal recovery binary, it turns into kind
    // of a stripped-down version of adbd that only supports the
    // 'sideload' command.  Note this must be a real argument, not
    // anything in the command file or bootloader control block; the
    // only way recovery should be run with this argument is when it
    // starts a copy of itself from the apply_from_adb() function.
    if (argc == 2 && strcmp(argv[1], "--adbd") == 0) {
        adb_main();
        return 0;
    }

    printf("Starting recovery (pid %d) on %s", getpid(), ctime(&start));
	//装载recovery的分区表recovery.fstab
    load_volume_table();
	//在recovery中挂载/cache/recovery/last_log这个文件
	//#define LAST_LOG_FILE "/cache/recovery/last_log"
    ensure_path_mounted(LAST_LOG_FILE);
    rotate_last_logs(KEEP_LOG_COUNT);
	//获取参数
	//这个参数也可能是从/cache/recovery/command文件中得到相应的命令
	//也就是可以往command这个文件写入对应的格式的命令即可
    get_args(&argc, &argv);

    const char *send_intent = NULL;
    const char *update_package = NULL;
    int wipe_data = 0, wipe_cache = 0, show_text = 0;
    bool just_exit = false;
    bool shutdown_after = false;

    int arg;
	//参数有擦除分区,OTA更新等
    while ((arg = getopt_long(argc, argv, "", OPTIONS, NULL)) != -1) {
        switch (arg) {
        case 's': send_intent = optarg; break;
        case 'u': update_package = optarg; break;
        case 'w': wipe_data = wipe_cache = 1; break;
        case 'c': wipe_cache = 1; break;
        case 't': show_text = 1; break;
        case 'x': just_exit = true; break;
        case 'l': locale = optarg; break;
        case 'g': {
            if (stage == NULL || *stage == '\0') {
                char buffer[20] = "1/";
                strncat(buffer, optarg, sizeof(buffer)-3);
                stage = strdup(buffer);
            }
            break;
        }
        case 'p': shutdown_after = true; break;
        case 'r': reason = optarg; break;
        case '?':
            LOGE("Invalid command argument\n");
            continue;
        }
    }
	//设置语言
    if (locale == NULL) {
        load_locale_from_cache();
    }
    printf("locale is [%s]\n", locale);
    printf("stage is [%s]\n", stage);
    printf("reason is [%s]\n", reason);
	//创建设备
    Device* device = make_device();
	//获取UI
    ui = device->GetUI();
	//设置当前的UI
    gCurrentUI = ui;
	//设置UI的语言信息
    ui->SetLocale(locale);
	//UI初始化
    ui->Init();

    int st_cur, st_max;
    if (stage != NULL && sscanf(stage, "%d/%d", &st_cur, &st_max) == 2) {
        ui->SetStage(st_cur, st_max);
    }
	//设置recovery的背景图
    ui->SetBackground(RecoveryUI::NONE);
	//设置界面上是否能够显示字符,使能ui->print函数开关
    if (show_text) ui->ShowText(true);
	//设置selinux权限,一般我会把selinux 给disabled
    struct selinux_opt seopts[] = {
      { SELABEL_OPT_PATH, "/file_contexts" }
    };

    sehandle = selabel_open(SELABEL_CTX_FILE, seopts, 1);

    if (!sehandle) {
        ui->Print("Warning: No file_contexts\n");
    }
	//虚函数,没有做什么流程
    device->StartRecovery();

    printf("Command:");
    for (arg = 0; arg < argc; arg++) {
        printf(" \"%s\"", argv[arg]);
    }
    printf("\n");
	//如果update_package(也就是要升级的OTA包)不为空的情况下
	//这里要对升级包的路径做一下路径转换,这里可以自由定制自己升级包的路径
    if (update_package) {
        // For backwards compatibility on the cache partition only, if
        // we're given an old 'root' path "CACHE:foo", change it to
        // "/cache/foo".

		//这里就是做转换的方法
		//先比较传进来的recovery参数的前6个byte是否是CACHE
		//如果是将其路径转化为/cache/CACHE: ......
        if (strncmp(update_package, "CACHE:", 6) == 0) {
            int len = strlen(update_package) + 10;
            char* modified_path = (char*)malloc(len);
            strlcpy(modified_path, "/cache/", len);
            strlcat(modified_path, update_package+6, len);
            printf("(replacing path \"%s\" with \"%s\")\n",
                   update_package, modified_path);
			//这个update_package就是转换后的路径
            update_package = modified_path;
        }
    }
    printf("\n");
    property_list(print_property, NULL);
	//获取属性,这里应该是从一个文件中找到ro.build.display.id
	//获取recovery的版本信息
    property_get("ro.build.display.id", recovery_version, "");
    printf("\n");

	//定义一个安装成功的标志位INSTALL_SUCCESS  ----> 其实是个枚举,值为0
    int status = INSTALL_SUCCESS;
	//判断转换后的OTA升级包的路径是否不为空,如果不为空
	//执行install_package 函数进行升级
    if (update_package != NULL) {
        status = install_package(update_package, &wipe_cache, TEMPORARY_INSTALL_FILE, true);
		//判断是否升级成功
        if (status == INSTALL_SUCCESS && wipe_cache) {
			//擦除这个路径,相当于删除了这个路径下的OTA升级包
            if (erase_volume("/cache")) {
                LOGE("Cache wipe (requested by package) failed.");
            }
        }
		//如果安装不成功
        if (status != INSTALL_SUCCESS) {
            ui->Print("Installation aborted.\n");

            // If this is an eng or userdebug build, then automatically
            // turn the text display on if the script fails so the error
            // message is visible.
            char buffer[PROPERTY_VALUE_MAX+1];
            property_get("ro.build.fingerprint", buffer, "");
            if (strstr(buffer, ":userdebug/") || strstr(buffer, ":eng/")) {
                ui->ShowText(true);
            }
        }
    }
	//如果跑的是格式化数据区,那么就走这个流程
	else if (wipe_data) {
        if (device->WipeData()) status = INSTALL_ERROR;
		//格式化/data分区
        if (erase_volume("/data")) status = INSTALL_ERROR;
        if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
        if (erase_persistent_partition() == -1 ) status = INSTALL_ERROR;
        if (status != INSTALL_SUCCESS) ui->Print("Data wipe failed.\n");
    } 
	//格式化cache分区
	else if (wipe_cache) {
        if (wipe_cache && erase_volume("/cache")) status = INSTALL_ERROR;
        if (status != INSTALL_SUCCESS) ui->Print("Cache wipe failed.\n");
    } 
	else if (!just_exit) {
        status = INSTALL_NONE;  // No command specified
        ui->SetBackground(RecoveryUI::NO_COMMAND);
    }
	//如果安装失败或者。。。
    if (status == INSTALL_ERROR || status == INSTALL_CORRUPT) {
        copy_logs();
		//显示错误的LOGO
        ui->SetBackground(RecoveryUI::ERROR);
    }
    Device::BuiltinAction after = shutdown_after ? Device::SHUTDOWN : Device::REBOOT;
    if (status != INSTALL_SUCCESS || ui->IsTextVisible()) {
        Device::BuiltinAction temp = prompt_and_wait(device, status);
        if (temp != Device::NO_ACTION) after = temp;
    }
	
    // Save logs and clean up before rebooting or shutting down.
    //完成recovery升级
    finish_recovery(send_intent);

    switch (after) {
        case Device::SHUTDOWN:
            ui->Print("Shutting down...\n");
            property_set(ANDROID_RB_PROPERTY, "shutdown,");
            break;

        case Device::REBOOT_BOOTLOADER:
            ui->Print("Rebooting to bootloader...\n");
            property_set(ANDROID_RB_PROPERTY, "reboot,bootloader");
            break;

        default:
            ui->Print("Rebooting...\n");
            property_set(ANDROID_RB_PROPERTY, "reboot,");
            break;
    }
    sleep(5); // should reboot before this finishes
    return EXIT_SUCCESS;
}






作者:morixinguan 发表于2017/6/4 13:39:04 原文链接
阅读:178 评论:0 查看评论

Android Multimedia框架总结(二十七)MediaCodec回顾

$
0
0

Android App 通过 MediaCodec Java API 获得的编解码器,实际上是由 StageFright 媒体框架提供。android.media.MediaCodec 调用 libmedia_jni.so 中 JNI native 函数,这些 JNI 函数再去调用 libstagefright.so 库获得 StageFright 框架中的编解码器。StageFright再调用OMX组件进行解码。这是之前梳理的流程。这次再读主要是搞明白他们的调用过程。

先看Java层代码:

mediaCodec = MediaCodec.createByCodecName(codecName);//创建Codec
MediaFormat mediaFormat = MediaFormat.createVideoFormat(MINE_TYPE, width, height);

mediaFormat.setInteger(MediaFormat.KEY_BIT_RATE, bit);
mediaFormat.setInteger(MediaFormat.KEY_FRAME_RATE, fps);
mediaFormat.setInteger(MediaFormat.KEY_I_FRAME_INTERVAL, 1); // 关键帧间隔时间// 单位s
mediaFormat.setInteger(MediaFormat.KEY_COLOR_FORMAT,                                    
                MediaCodecInfo.CodecCapabilities.COLOR_FormatYUV420SemiPlanar);
                // mediaFormat.setInteger(MediaFormat.KEY_PROFILE,
                // CodecProfileLevel.AVCProfileBaseline);
                // mediaFormat.setInteger(MediaFormat.KEY_LEVEL,
                // CodecProfileLevel.AVCLevel52);
                mediaCodec.configure(mediaFormat, null, null,
                        MediaCodec.CONFIGURE_FLAG_ENCODE);
                mediaCodec.start();

Java层:
createByName->New MediaCode()->setup(native声明方法)
JNI层:
1、映射(android_media_MediaCodec.cpp 中)

    { "native_setup", "(Ljava/lang/String;ZZ)V",
      (void *)android_media_MediaCodec_native_setup },

2、setup->android_media_MediaCodec_native_setup

static void android_media_MediaCodec_native_setup(
        JNIEnv *env, jobject thiz,
        jstring name, jboolean nameIsType, jboolean encoder) {
    if (name == NULL) {
        //这里可以学习,jni层如何抛出异常的。使用jniThrowException
        jniThrowException(env, "java/lang/NullPointerException", NULL);
        return;
    }

    const char *tmp = env->GetStringUTFChars(name, NULL);

    if (tmp == NULL) {
        return;
    }

    sp<JMediaCodec> codec = new JMediaCodec(env, thiz, tmp, nameIsType, encoder);

    const status_t err = codec->initCheck();
    if (err == NAME_NOT_FOUND) {
        // fail and do not try again.
        jniThrowException(env, "java/lang/IllegalArgumentException",
                String8::format("Failed to initialize %s, error %#x", tmp, err));
        env->ReleaseStringUTFChars(name, tmp);
        return;
    } if (err == NO_MEMORY) {
        throwCodecException(env, err, ACTION_CODE_TRANSIENT,
                String8::format("Failed to initialize %s, error %#x", tmp, err));
        env->ReleaseStringUTFChars(name, tmp);
        return;
    } else if (err != OK) {
        // believed possible to try again
        jniThrowException(env, "java/io/IOException",
                String8::format("Failed to find matching codec %s, error %#x", tmp, err));
        env->ReleaseStringUTFChars(name, tmp);
        return;
    }

    env->ReleaseStringUTFChars(name, tmp);

    codec->registerSelf();

    setMediaCodec(env,thiz, codec);
}

3、重点是 sp codec = new JMediaCodec(env, thiz, tmp, nameIsType, encoder);
sp是智能指针对象,主要灵活管理内存相关。还有wp,弱引用指针对象,在 、system\core\include\utils\StrongPointer.h 下,直接可以拷贝这套google的东西,运用到项目也是可以的。

4、看下构造函数,

JMediaCodec::JMediaCodec(
        JNIEnv *env, jobject thiz,
        const char *name, bool nameIsType, bool encoder)
    : mClass(NULL),
      mObject(NULL) {
    jclass clazz = env->GetObjectClass(thiz);//获取class
    CHECK(clazz != NULL);

    mClass = (jclass)env->NewGlobalRef(clazz);//全局引用
    mObject = env->NewWeakGlobalRef(thiz);//弱全局引用

    cacheJavaObjects(env);

    mLooper = new ALooper;
    mLooper->setName("MediaCodec_looper");

    mLooper->start(
            false,      // runOnCallingThread
            true,       // canCallJava
            PRIORITY_FOREGROUND);

    if (nameIsType) {
        mCodec = MediaCodec::CreateByType(mLooper, name, encoder, &mInitStatus);
    } else {
        mCodec = MediaCodec::CreateByComponentName(mLooper, name, &mInitStatus);
    }
    CHECK((mCodec != NULL) != (mInitStatus != OK));
}

mCodec = MediaCodec::CreateByType(mLooper, name, encoder, &mInitStatus),//这里的MedCodec就是stagefright中MediaCodec,位于 \frameworks\av\media\libstagefright\MediaCodec.cpp,隶属于libstagefright.so

5、进入MediaCodec.cpp中

// static
sp<MediaCodec> MediaCodec::CreateByType(
        const sp<ALooper> &looper, const char *mime, bool encoder, status_t *err, pid_t pid) {
    sp<MediaCodec> codec = new MediaCodec(looper, pid);

    const status_t ret = codec->init(mime, true /* nameIsType */, encoder);
    if (err != NULL) {
        *err = ret;
    }
    return ret == OK ? codec : NULL; // NULL deallocates codec.
}

6、构造

MediaCodec::MediaCodec(const sp<ALooper> &looper, pid_t pid)
    : mState(UNINITIALIZED),
      mReleasedByResourceManager(false),
      mLooper(looper),
      mCodec(NULL),
      mReplyID(0),
      mFlags(0),
      mStickyError(OK),
      mSoftRenderer(NULL),
      mResourceManagerClient(new ResourceManagerClient(this)),
      mResourceManagerService(new ResourceManagerServiceProxy(pid)),
      mBatteryStatNotified(false),
      mIsVideo(false),
      mVideoWidth(0),
      mVideoHeight(0),
      mRotationDegrees(0),
      mDequeueInputTimeoutGeneration(0),
      mDequeueInputReplyID(0),
      mDequeueOutputTimeoutGeneration(0),
      mDequeueOutputReplyID(0),
      mHaveInputSurface(false),
      mHavePendingInputBuffers(false) {
}

这里有点意思,直接把MediaCodec.h中头文件中变量进行初始化,注意:(冒号),看MediaCodec.h中,第一句就是struct MediaCodec : public AHandler,也就是说MediaCodec是一个结构体,这个结构体继承AHandler(也是一个结构体),AHandler继承

最后疑惑点

在阅读时,还发现有NdkMediaCodec及NdkMediaCodec.cpp这些个class, 和上面几个class的区别是什么?有什么关系?为什么要这么设计?
frameworks\av\include\ndk\NdkMediaCodec.h

/*
 * This file defines an NDK API.
 * Do not remove methods.
 * Do not change method signatures.
 * Do not change the value of constants.
 * Do not change the size of any of the classes defined in here.
 * Do not reference types that are not part of the NDK.
 * Do not #include files that aren't part of the NDK.
 */

#ifndef _NDK_MEDIA_CODEC_H
#define _NDK_MEDIA_CODEC_H

#include <android/native_window.h>

#include "NdkMediaCrypto.h"
#include "NdkMediaError.h"
#include "NdkMediaFormat.h"

#ifdef __cplusplus
extern "C" {
#endif


struct AMediaCodec;
typedef struct AMediaCodec AMediaCodec;

struct AMediaCodecBufferInfo {
    int32_t offset;
    int32_t size;
    int64_t presentationTimeUs;
    uint32_t flags;
};
typedef struct AMediaCodecBufferInfo AMediaCodecBufferInfo;
typedef struct AMediaCodecCryptoInfo AMediaCodecCryptoInfo;

enum {
    AMEDIACODEC_BUFFER_FLAG_END_OF_STREAM = 4,
    AMEDIACODEC_CONFIGURE_FLAG_ENCODE = 1,
    AMEDIACODEC_INFO_OUTPUT_BUFFERS_CHANGED = -3,
    AMEDIACODEC_INFO_OUTPUT_FORMAT_CHANGED = -2,
    AMEDIACODEC_INFO_TRY_AGAIN_LATER = -1
};

/**
 * Create codec by name. Use this if you know the exact codec you want to use.
 * When configuring, you will need to specify whether to use the codec as an
 * encoder or decoder.
 */
AMediaCodec* AMediaCodec_createCodecByName(const char *name);

/**
 * Create codec by mime type. Most applications will use this, specifying a
 * mime type obtained from media extractor.
 */
AMediaCodec* AMediaCodec_createDecoderByType(const char *mime_type);

/**
 * Create encoder by name.
 */
AMediaCodec* AMediaCodec_createEncoderByType(const char *mime_type);

/**
 * delete the codec and free its resources
 */
media_status_t AMediaCodec_delete(AMediaCodec*);

/**
 * Configure the codec. For decoding you would typically get the format from an extractor.
 */
media_status_t AMediaCodec_configure(
        AMediaCodec*,
        const AMediaFormat* format,
        ANativeWindow* surface,
        AMediaCrypto *crypto,
        uint32_t flags);

/**
 * Start the codec. A codec must be configured before it can be started, and must be started
 * before buffers can be sent to it.
 */
media_status_t AMediaCodec_start(AMediaCodec*);

/**
 * Stop the codec.
 */
media_status_t AMediaCodec_stop(AMediaCodec*);

/*
 * Flush the codec's input and output. All indices previously returned from calls to
 * AMediaCodec_dequeueInputBuffer and AMediaCodec_dequeueOutputBuffer become invalid.
 */
media_status_t AMediaCodec_flush(AMediaCodec*);

/**
 * Get an input buffer. The specified buffer index must have been previously obtained from
 * dequeueInputBuffer, and not yet queued.
 */
uint8_t* AMediaCodec_getInputBuffer(AMediaCodec*, size_t idx, size_t *out_size);

/**
 * Get an output buffer. The specified buffer index must have been previously obtained from
 * dequeueOutputBuffer, and not yet queued.
 */
uint8_t* AMediaCodec_getOutputBuffer(AMediaCodec*, size_t idx, size_t *out_size);

/**
 * Get the index of the next available input buffer. An app will typically use this with
 * getInputBuffer() to get a pointer to the buffer, then copy the data to be encoded or decoded
 * into the buffer before passing it to the codec.
 */
ssize_t AMediaCodec_dequeueInputBuffer(AMediaCodec*, int64_t timeoutUs);

/**
 * Send the specified buffer to the codec for processing.
 */
media_status_t AMediaCodec_queueInputBuffer(AMediaCodec*,
        size_t idx, off_t offset, size_t size, uint64_t time, uint32_t flags);

/**
 * Send the specified buffer to the codec for processing.
 */
media_status_t AMediaCodec_queueSecureInputBuffer(AMediaCodec*,
        size_t idx, off_t offset, AMediaCodecCryptoInfo*, uint64_t time, uint32_t flags);

/**
 * Get the index of the next available buffer of processed data.
 */
ssize_t AMediaCodec_dequeueOutputBuffer(AMediaCodec*, AMediaCodecBufferInfo *info,
        int64_t timeoutUs);
AMediaFormat* AMediaCodec_getOutputFormat(AMediaCodec*);

/**
 * If you are done with a buffer, use this call to return the buffer to
 * the codec. If you previously specified a surface when configuring this
 * video decoder you can optionally render the buffer.
 */
media_status_t AMediaCodec_releaseOutputBuffer(AMediaCodec*, size_t idx, bool render);

/**
 * If you are done with a buffer, use this call to update its surface timestamp
 * and return it to the codec to render it on the output surface. If you
 * have not specified an output surface when configuring this video codec,
 * this call will simply return the buffer to the codec.
 *
 * For more details, see the Java documentation for MediaCodec.releaseOutputBuffer.
 */
media_status_t AMediaCodec_releaseOutputBufferAtTime(
        AMediaCodec *mData, size_t idx, int64_t timestampNs);


typedef enum {
    AMEDIACODECRYPTOINFO_MODE_CLEAR = 0,
    AMEDIACODECRYPTOINFO_MODE_AES_CTR = 1
} cryptoinfo_mode_t;

/**
 * Create an AMediaCodecCryptoInfo from scratch. Use this if you need to use custom
 * crypto info, rather than one obtained from AMediaExtractor.
 *
 * AMediaCodecCryptoInfo describes the structure of an (at least
 * partially) encrypted input sample.
 * A buffer's data is considered to be partitioned into "subsamples",
 * each subsample starts with a (potentially empty) run of plain,
 * unencrypted bytes followed by a (also potentially empty) run of
 * encrypted bytes.
 * numBytesOfClearData can be null to indicate that all data is encrypted.
 * This information encapsulates per-sample metadata as outlined in
 * ISO/IEC FDIS 23001-7:2011 "Common encryption in ISO base media file format files".
 */
AMediaCodecCryptoInfo *AMediaCodecCryptoInfo_new(
        int numsubsamples,
        uint8_t key[16],
        uint8_t iv[16],
        cryptoinfo_mode_t mode,
        size_t *clearbytes,
        size_t *encryptedbytes);

/**
 * delete an AMediaCodecCryptoInfo created previously with AMediaCodecCryptoInfo_new, or
 * obtained from AMediaExtractor
 */
media_status_t AMediaCodecCryptoInfo_delete(AMediaCodecCryptoInfo*);

/**
 * The number of subsamples that make up the buffer's contents.
 */
size_t AMediaCodecCryptoInfo_getNumSubSamples(AMediaCodecCryptoInfo*);

/**
 * A 16-byte opaque key
 */
media_status_t AMediaCodecCryptoInfo_getKey(AMediaCodecCryptoInfo*, uint8_t *dst);

/**
 * A 16-byte initialization vector
 */
media_status_t AMediaCodecCryptoInfo_getIV(AMediaCodecCryptoInfo*, uint8_t *dst);

/**
 * The type of encryption that has been applied,
 * one of AMEDIACODECRYPTOINFO_MODE_CLEAR or AMEDIACODECRYPTOINFO_MODE_AES_CTR.
 */
cryptoinfo_mode_t AMediaCodecCryptoInfo_getMode(AMediaCodecCryptoInfo*);

/**
 * The number of leading unencrypted bytes in each subsample.
 */
media_status_t AMediaCodecCryptoInfo_getClearBytes(AMediaCodecCryptoInfo*, size_t *dst);

/**
 * The number of trailing encrypted bytes in each subsample.
 */
media_status_t AMediaCodecCryptoInfo_getEncryptedBytes(AMediaCodecCryptoInfo*, size_t *dst);

#ifdef __cplusplus
} // extern "C"
#endif

#endif //_NDK_MEDIA_CODEC_H

在AMediaCodec中,有一个createAMediaCodec,如下:

static AMediaCodec * createAMediaCodec(const char *name, bool name_is_type, bool encoder) {
    AMediaCodec *mData = new AMediaCodec();
    mData->mLooper = new ALooper;
    mData->mLooper->setName("NDK MediaCodec_looper");
    status_t ret = mData->mLooper->start(
            false,      // runOnCallingThread
            true,       // canCallJava XXX
            PRIORITY_FOREGROUND);
    if (name_is_type) {
        mData->mCodec = android::MediaCodec::CreateByType(mData->mLooper, name, encoder);
    } else {
        mData->mCodec = android::MediaCodec::CreateByComponentName(mData->mLooper, name);
    }
    if (mData->mCodec == NULL) {  // failed to create codec
        AMediaCodec_delete(mData);
        return NULL;
    }
    mData->mHandler = new CodecHandler(mData);
    mData->mLooper->registerHandler(mData->mHandler);
    mData->mGeneration = 1;
    mData->mRequestedActivityNotification = false;
    mData->mCallback = NULL;

    return mData;
}

其中也有一个android::MediaCodec::CreateByType(mData->mLooper, name, encoder);调用的是MediaCodec的CreateByType函数。这个类作用暂时不明确,看Google标识是勿动,勿改,勿乱搞,这是个NDK API。

作者:hejjunlin 发表于2017/6/4 15:35:34 原文链接
阅读:15 评论:0 查看评论

React Native Application和Activity源码分析

$
0
0

基于V0.43.3版本 React Native Android端的ReactApplication和ReactActivity的实现原理.

ReactApplication

在Android端接入RN时, 需要ReactApplication作为接口被Application实现. 从源码中可以看出ReactApplication只是提供了一个获取实现ReactNativeHost的接口. 这个类既然叫Host那么肯定承载了RN的实例和一些配置, 可以通过下面的类图或源码看出它都有什么职责.

点击查看大图

从类图中可以看到RNHost中持有着Application的对象, 这个很好理解, 因为RN初始化一些部件或读取一些资源的时候肯定会需要用到Application. 而另外一个属性就是核心的ReactInstanceManager, RNHost类中的方法都是为InstanceManager服务的. R通过ReactInstanceManager管理配置负责Java和JS通讯的高层API类CatalystInstance, dev support的支持, 并且绑定并同步与ReactRootView所在容器的声明周期. 下面简单看一下Host类对ReactInstanceManager配置和初始化的10个方法.

  1. getReactInstanceManager方法

    public ReactInstanceManager getReactInstanceManager() {
        if (mReactInstanceManager == null) {
            mReactInstanceManager = createReactInstanceManager();
        }
        return mReactInstanceManager;
    }

    获取当前Host对象持有的ReactInstanceManager对象, 如果还没有创建的话就通过create方法初始化ReactInstanceManager对象. 因为都是在同一个线程中调用该方法, 所以这种懒加载的初始化方式不会有线程安全问题.

  2. hasInstance方法

    public boolean hasInstance() {
        return mReactInstanceManager != null;
    }

    判断当前ReactInstanceManager对象有没有实例化.

  3. getRedBoxHandler方法

    protected @Nullable RedBoxHandler getRedBoxHandler() {
        return null;
    }

    该方法用于初始化ReactInstanceManager对象实例, 根据实际情况选择复写该方法. RedBoxHandler提供运行时JS异常之后的交互, 默认情况下会有红色的页面出现并显示出JS异常的堆栈信息, 应该就是这个原因才叫RedBox吧. 复写该方法可以自定义一些JS异常之后的流程来丰富产品的兼容交互.

  4. getUIImplementationProvider方法

    protected UIImplementationProvider getUIImplementationProvider() {
        return new UIImplementationProvider();
    }

    该方法用法同上. 如果想要定制RN UI方面的话可以选择复写该方法, 源码中说这个部件是一个非常高级的定制功能, 使用默认的初始化就可以满足百分之九十九的场景, 如无必要不建议复写该方法.

  5. getJSMainModuleName方法

    protected String getJSMainModuleName() {
        return "index.android";
    }

    该方法用法同上. 设置js中的模块名称, 这样就可以在开启了DevSupport的情况下直接加载 packager server上的js bundle中的模块. 在DevSupport下加载的优先级是比较高的.

  6. getJSBundleFile方法

    protected @Nullable String getJSBundleFile() {
        return null;
    }

    该方法用法同上. 如果有在自定义路径下(sd卡或者app沙盒控件)加载js bundle文件的需求的话就要重写该方法并制定要加载的文件路径. 如果使用默认配置的话RN优先根据下面的方法从assets路径下拿js bundle文件.

  7. getBundleAssetName方法

    protected @Nullable String getBundleAssetName() {
        return "index.android.bundle";
    }

    该方法用法同上. 如果开启了DevSupport, RN会优先通过pack server远程加载js bundle文件. 然而在没有开启DevSupport并且没有复写6方法制定特定路径的情况下, RN会根据该方法配置的bundle文件名在assets下读取并加载该文件.

  8. getUseDeveloperSupport方法

    public abstract boolean getUseDeveloperSupport();

    该方法用法同上. getUseDeveloperSupport是一个虚方法. 通过它可以配置是否开启RN的DevSupport.

  9. getPackages方法

    protected abstract List<ReactPackage> getPackages();

    该方法用法同上. getPackages方法也是一个虚方法, 使用该方法来设置RN中JS和Native之间相互的通信模块, 自定义View的接口和Native本地资源的接口. 列表中最少要包含ReactPackage 的系统实现MainReactPackage对象, 如果项目使用到了自定的native module, js module和view managers 要将其添加到ReactPackage列表中.

  10. createReactInstanceManager方法

    protected ReactInstanceManager createReactInstanceManager() {
        ReactInstanceManagerBuilder builder = ReactInstanceManager.builder()
          .setApplication(mApplication)
          .setJSMainModuleName(getJSMainModuleName())
          .setUseDeveloperSupport(getUseDeveloperSupport())
          .setRedBoxHandler(getRedBoxHandler())
          .setUIImplementationProvider(getUIImplementationProvider())
          .setInitialLifecycleState(LifecycleState.BEFORE_CREATE);
    
        for (ReactPackage reactPackage : getPackages()) {
          builder.addPackage(reactPackage);
        }
    
        String jsBundleFile = getJSBundleFile();
        if (jsBundleFile != null) {
          builder.setJSBundleFile(jsBundleFile);
        } else {
          builder.setBundleAssetName(Assertions.assertNotNull(getBundleAssetName()));
        }
        return builder.build();
    }

    讲过上面七个参与ReactInstanceManager对象初始化的方法之后, 通过Builder的形式在createReactInstanceManager方法中将初始化所需的配置信息汇总起来并对ReactInstanceManager进行初始化操作.

ReactActivity

RN源码中有提供两个可以直接使用的Activity, 分别是ReactActivity和ReactFragmentActivity. 他们两个的差别是分别继承自Activity和FragmentActivity. 对RN来说没有太大的区别, 这里就只分析ReactActivity了. 并且可以通过对ReactActivity的分析, 再结合RN的设计就可以自己封装出来类似ReactFragment之类的组件出来.

点击查看大图

从ReactActivity相关的类图可以看到它实现了两个接口, 分别是用来将Android回退键点击事件代理给JS的DefaultHardwareBackBtnHandler 和 处理运行时权限授权申请和回调的PermissionAwareActivity. 除了这两个接口之外, ReactActivity还有一个ReactActivityDelegate的属性. 通过源码可以看到这个对象把ReactActivity的声明周期, 事件和权限管理相关全部都同步并代理到自己内部.

通过getMainComponentName方法指定在JS中注册的主模块名称, 并且在构造方法里对delegate对象进行初始化. 如果有特殊需求的话例如要向JS的主模块中传递一些数据等, 可以选择复写createReactActivityDelegate方法, 使用定制的ReactActivityDelegate子类进行初始化.

protected ReactActivity() {
    mDelegate = createReactActivityDelegate();
}

protected @Nullable String getMainComponentName() {
    return null;
}

protected ReactActivityDelegate createReactActivityDelegate() {
    return new ReactActivityDelegate(this, getMainComponentName());
}

经过上面的分析, ReactActivity其实就是一个代理行为的空壳, 真正的实现都在ReactActivityDelegate中. 接下来就进入类中看这个代理类究竟做了什么.首先看一下构造方法.由于ReactActivity和ReactFragmentActivity都使用了相同的Delegate类, 所以提供了两个不同的构造接受Activity的实例和js端注册的模块名称.

public ReactActivityDelegate(Activity activity, @Nullable String mainComponentName) {
    mActivity = activity;
    mMainComponentName = mainComponentName;
    mFragmentActivity = null;
}

public ReactActivityDelegate(FragmentActivity fragmentActivity,@Nullable String mainComponentName) {
    mFragmentActivity = fragmentActivity;
    mMainComponentName = mainComponentName;
    mActivity = null;
}

接下来看到有两个提供被代理方Context和Activity的方法, 方便内部使用.

private Context getContext() {
    if (mActivity != null) {
      return mActivity;
    }
    return Assertions.assertNotNull(mFragmentActivity);
}

private Activity getPlainActivity() {
    return ((Activity) getContext());
}

往下看就能发现在上面讲ReactApplication的时候主要涉及到的两个类. Application只是提供了一个getReactNativeHost的方法, 它本身并没有调用该方法对ReactInstanceManager进行初始化, 上面一节有说过ReactInstanceManager是懒加载初始化的, 从这看来ReactInstanceManager初始化的时机就是打开RN页面之后, 在RN页面装载的过程中对其初始化.

protected ReactNativeHost getReactNativeHost() {
    return ((ReactApplication) getPlainActivity().getApplication()).getReactNativeHost();
}

public ReactInstanceManager getReactInstanceManager() {
    return getReactNativeHost().getReactInstanceManager();
}

其他的方法例如 onResume, onPause, onDestroy, onActivityResult, onKeyUp, onBackPressed, onNewIntent, requestPermissions和onRequestPermissionsResult方法将声明周期同步到ReactInstanceManager中, 点击/回退与devSupport事件相关联, 动态权限申请和回调的管理起来, 这里就不一一细说了. 除此之外onDestroy方法触发的时候还会根据情况将rootView从ReactInstanceManager对象中卸载掉.

protected void onDestroy() {
    if (mReactRootView != null) {
        mReactRootView.unmountReactApplication();
        mReactRootView = null;
    }
    if (getReactNativeHost().hasInstance()) {
        getReactNativeHost().getReactInstanceManager().onHostDestroy(getPlainActivity());
    }
}

单独分析一下onCreate方法, 第一个大的if块是对DrawOverlays权限的判断. 因为从Android 6.0开始悬浮窗权限收紧, RN对该权限做了判断, 如果没有DrawOverlays权限的话就跳转到系统设置页面动态申请. 有权限了之后就会调用loadApp方法, 对RootView进行初始化. 最后初始化了一个双击R键的监听器, 服务于Dev Support.

protected void onCreate(Bundle savedInstanceState) {
    boolean needsOverlayPermission = false;
    if (getReactNativeHost().getUseDeveloperSupport() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
      // Get permission to show redbox in dev builds.
      if (!Settings.canDrawOverlays(getContext())) {
        needsOverlayPermission = true;
        Intent serviceIntent = new Intent(Settings.ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getContext().getPackageName()));
        FLog.w(ReactConstants.TAG, REDBOX_PERMISSION_MESSAGE);
        Toast.makeText(getContext(), REDBOX_PERMISSION_MESSAGE, Toast.LENGTH_LONG).show();
        ((Activity) getContext()).startActivityForResult(serviceIntent, REQUEST_OVERLAY_PERMISSION_CODE);
      }
    }

    if (mMainComponentName != null && !needsOverlayPermission) {
      loadApp(mMainComponentName);
    }
    mDoubleTapReloadRecognizer = new DoubleTapReloadRecognizer();
}

loadApp方法其实就是对RootView的初始化, 并通过rootView的startReactApplication方法将其装载进ReactInstanceManager进行管理, 与onDestroy方法中的unmountReactApplication相对应. 因为RootView就是一个view, 将其装载进acitivty中就可以绘制出来. 后面会单独分析ReactRootView.

protected ReactRootView createRootView() {
    return new ReactRootView(getContext());
}

protected void loadApp(String appKey) {
    if (mReactRootView != null) {
        throw new IllegalStateException("Cannot loadApp while app is already running.");
    }
    mReactRootView = createRootView();
    mReactRootView.startReactApplication(
      getReactNativeHost().getReactInstanceManager(),
      appKey,
      getLaunchOptions());

    getPlainActivity().setContentView(mReactRootView);
}

转载请注明出处:http://blog.csdn.net/l2show/article/details/72859653

作者:l2show 发表于2017/6/4 16:26:03 原文链接
阅读:391 评论:0 查看评论

swift 3.1 快速上手系列(二)

$
0
0

swift 3.1 快速上手系列(一) 中,主要介绍了 Xcode 8 以及 swift 3.1 在实际编程过程中的一些小技巧以及几种常用的解包方案。内容虽简单,但却很实用,这次,我们来谈谈 swift 3.1 中的异常处理机制以及 以及类型转换运算符 as .

异常处理机制是在 swift 2.0 引进的,下面直接以代码的形式(以反序列化 throw 抛出异常为例)进行比较学习:

let jsonString = "{\"name\":\"FeiGe\"}"
        let data = jsonString.data(using: .utf8)

        // 反序列化 throw 抛出异常
        // 方法一:推荐 try? ,如果解析成功,就有值,否则,为 nil
        let json1 = try? JSONSerialization.jsonObject(with: data!, options: [])
        print(json1)

        // 方法二:强烈不推荐 try!,如果解析成功,就有值,否则程序崩溃,有风险
        let json2 = try! JSONSerialization.jsonObject(with: data!, options: [])
        print(json2)

        // 方法三:处理异常,能够接收到错误,并且输出错误
        // 但是:语法结构复杂,而且 {} 中的智能提示不友好
        // 扩展:OC 中几乎没人用 try catch,因为 ARC 开发,编译器自动添加 retain / release / autoerlease,如果用 try catch ,一旦不平衡,就会出现内存泄露!
        do {
            let json3 = try JSONSerialization.jsonObject(with: data!, options: [])
            print(json3)
        } catch {
            print(error)
        }

结论:推荐使用 try?,采用 guard 守护解包。

再来简要谈谈类型转换运算符 as? 与 as!

as? 操作符会执行转换并返回期望类型的一个可选值,如果转换成功则返回的选项包含有效值,否则选项值为 nil。
as! 操作符会执行一个实例到目的类型的强制转换,因此使用该形式可能触发一个运行时错误。
所以比较推荐使用”as?”这种方式进行类型转换。这与上述的异常处理是类似的。

作者:huangfei711 发表于2017/6/4 16:32:38 原文链接
阅读:86 评论:0 查看评论

Kotlin 官方学习教程之可见性修饰符

$
0
0

可见性修饰符

类,对象,接口,构造函数,属性以及它们的 setter 方法都可以有可见性修饰词。( getter 总是具有与该属性相同的可见性。)。在 Kotlin 中有四种修饰词:private,protected,internal,以及 public 。默认的修饰符是 public。

下面请查看不同类型声明范围的说明。

函数,属性和类,对象和接口可以在 “top-level” 声明,例如直接定义在一个包内:

// file name: example.kt
package foo

fun baz() {}
class Bar {}
  • 如果您没有指定任何可见性修饰符,则默认使用 public,这意味着您的声明将在任何位置都可见;

  • 如果你声明为 private ,则只在包含声明的文件中可见;

  • 如果用 internal 声明,则在同一模块中的任何地方可见;

  • protected 在 “top-level” 中不可以使用

例子:

// file name: example.kt
package foo

private fun foo() {} // visible inside example.kt

public var bar: Int = 5 // 属性在任何地方可见
    private set         // setter 方法只在 example.kt 文件内可见

internal val baz = 6    // 在同一个 module 内可见

类和接口

对于在类中声明的成员:

  • private 只在该类(以及它所有的成员)中可见

  • protected 和 private 一样,但在子类中也可见

  • internal 在本模块的所有可以访问到声明区域的均可以访问该类的所有 internal 成员

  • public 任何地方可见

java 使用者注意:外部类不可以访问内部类的 private 成员。

如果重写 protected 成员但不指定可见性,重写成员的可见性也为 protected。

例子:

open class Outer {
    private val a = 1
    protected open val b = 2
    internal val c = 3
    val d = 4  // 默认为 public 型

    protected class Nested {
        public val e: Int = 5
    }
}

class Subclass : Outer() {
    // a 是不可见的
    // b, c 和d 是可见的
    // Nested 和 e 是可见的

    override val b = 5   // 'b' 是 protected 型
}

class Unrelated(o: Outer) {
    // o.a, o.b 是不可见的
    // o.c and o.d 是可见的 (在同一个 module 内)
    // Outer.Nested 是不可见的, and Nested::e 也是不可见的
}

构造函数

要指定类的主构造函数的可见性,请使用以下语法(请注意,您需要添加一个显式 constructor 关键字):

class C private constructor(a: Int) { ... }

这里构造函数是 private 。所有的构造函数默认是 public ,实际上只要类是可见的它们就是可见的 (注意 internal 类型的类中的 public 属性只能在同一个模块内才可以访问).

局部变量

局部变量,函数和类不能有可见性修饰符。

模块

internal 修饰符是指成员的可见性是只在同一个模块中才可见的。模块在 Kotlin 中就是一系列的 Kotlin 文件编译在一起:

  • an IntelliJ IDEA module;

  • a Maven or Gradle project;

  • a set of files compiled with one invocation of the Ant task.

作者:jim__charles 发表于2017/6/4 19:51:12 原文链接
阅读:87 评论:0 查看评论

【iOS沉思录】NSTimer你真的会用了吗

$
0
0

  看到这个标题,你可能会想NSTimer不就是计时器吗,谁不会用,不就是一个能够定时的完成任务的东西吗?

  我想说你知道NSTimer会retain你添加调用方法的对象吗?你知道NSTimer是要加到runloop中才会起作用吗?你知道NSTimer会并不是准确的按照你指定的时间触发的吗?你知道NSTimer就算添加到runloop了也不一定会按照你想象中的那样执行吗?

  如果上面提出的哪些问题,你并不全部了解,那么请细心的看完下面的文章,上面的那几个问题我会一一说明,并给出详细的例子。

一、什么是NSTimer

  官方给出解释是“A timer provides a way to perform a delayed action or a periodic action. The timer waits until a certain time interval has elapsed and then fires, sending a specified message to a specified object. ” 翻译过来就是timer就是一个能在从现在开始的后面的某一个时刻或者周期性的执行我们指定的方法的对象。

 

二、NSTimer和它调用的函数对象间到底发生了什么

   从前面官方给出的解释可以看出timer会在未来的某个时刻执行一次或者多次我们指定的方法,这也就牵扯出一个问题,如何保证timer在未来的某个时刻触发指定事件的时候,我们指定的方法是有效的呢?

  解决方法很简单,只要将指定给timer的方法的接收者retain一份就搞定了,实际上系统也是这样做的。不管是重复性的timer还是一次性的timer都会对它的方法的接收者进行retain,这两种timer的区别在于“一次性的timer在完成调用以后会自动将自己invalidate,而重复的timer则将永生,直到你显示的invalidate它为止”。

  下面我们看个小例子:

 

复制代码
//
//  SvTestObject.m
//  SvTimerSample
//
//  Created by  maple on 12/19/12.
//  Copyright (c) 2012 maple. All rights reserved.
//

#import "SvTestObject.h"

@implementation SvTestObject

- (id)init
{
    self = [super init];
    if (self) {
        NSLog(@"instance %@ has been created!", self);
    }
    
    return self;
}

- (void)dealloc
{
    NSLog(@"instance %@ has been dealloced!", self);
    
    [super dealloc];
}

- (void)timerAction:(NSTimer*)timer
{
    NSLog(@"Hi, Timer Action for instance %@", self);
}

@end
复制代码
SvTestObject.h
复制代码
//
//  SvTestObject.h
//  SvTimerSample
//
//  Created by  maple on 12/19/12.
//  Copyright (c) 2012 maple. All rights reserved.
//

#import <Foundation/Foundation.h>

@interface SvTestObject : NSObject

/*
 * @brief timer响应函数,只是用来做测试
 */
- (void)timerAction:(NSTimer*)timer;

@end
复制代码
SvTimerAppDelegate.m
复制代码
- (void)applicationDidBecomeActive:(UIApplication *)application
{
    // Restart any tasks that were paused (or not yet started) while the application was inactive. If the application was previously in the background, optionally refresh the user interface.
    
    // test Timer retain target
    [self testNonRepeatTimer];
//    [self testRepeatTimer];
}

- (void)testNonRepeatTimer
{
    NSLog(@"Test retatin target for non-repeat timer!");
    SvTestObject *testObject = [[SvTestObject alloc] init];
    [NSTimer scheduledTimerWithTimeInterval:5 target:testObject selector:@selector(timerAction:) userInfo:nil repeats:NO];
    [testObject release];
    NSLog(@"Invoke release to testObject!");
}

- (void)testRepeatTimer
{
    NSLog(@"Test retain target for repeat Timer");
    SvTestObject *testObject2 = [[SvTestObject alloc] init];
    [NSTimer scheduledTimerWithTimeInterval:5 target:testObject2 selector:@selector(timerAction:) userInfo:nil repeats:YES];
    [testObject2 release];
    NSLog(@"Invoke release to testObject2!");
}
复制代码

  上面的简单例子中,我们自定义了一个继承自NSObject的类SvTestObject,在这个类的init,dealloc和它的timerAction三个方法中分别打印信息。然后在appDelegate中分别测试一个单次执行的timer和一个重复执行的timer对方法接受者是否做了retain操作,因此我们在两种情况下都是shedule完timer之后立马对该测试对象执行release操作。

  测试单次执行的timer的结果如下:

  观察输出,我们会发现53分58秒的时候我们就对测试对象执行了release操作,但是知道54分03秒的时候timer触发完方法以后,该对象才实际的执行了dealloc方法。这就证明一次性的timer也会retain它的方法接收者,直到自己失效为之。

  测试重复性的timer的结果如下:

  观察输出我们发现,这个重复性的timer一直都在周期性的调用我们为它指定的方法,而且测试的对象也一直没有真正的被释放。

  通过以上小例子,我们可以发现在timer对它的接收者进行retain,从而保证了timer调用时的正确性,但是又引入了接收者的内存管理问题。特别是对于重复性的timer,它所引用的对象将一直存在,将会造成内存泄露。

  有问题就有应对方法,NSTimer提供了一个方法invalidate,让我们可以解决这种问题。不管是一次性的还是重复性的timer,在执行完invalidate以后都会变成无效,因此对于重复性的timer我们一定要有对应的invalidate。

  突然想起一种自欺欺人的写法,不知道你们有没有这么写过,我承认之前也有这样写过,哈哈,代码如下:

复制代码
SvCheatYourself.m

//
//  SvCheatYourself.m
//  SvTimerSample
//
//  Created by  maple on 12/19/12.
//  Copyright (c) 2012 maple. All rights reserved.
//
//  以下这种timer的用法,企图在dealloc中对timer进行invalidate是一种自欺欺人的做法
//  因为你的timer对self进行了retain,如果timer一直有效,则self的引用计数永远不会等于0

#import "SvCheatYourself.h"

@interface SvCheatYourself () {
    NSTimer *_timer;
}

@end

@implementation SvCheatYourself

- (id)init
{
    self = [super init];
    if (self) {
        _timer = [NSTimer scheduledTimerWithTimeInterval:1 target:self selector:@selector(testTimer:) userInfo:nil repeats:YES];
    }
    
    return self;
}

- (void)dealloc
{
    // 自欺欺人的写法,永远都不会执行到,除非你在外部手动invalidate这个timer
    [_timer invalidate];
    
    [super dealloc];
}

- (void)testTimer:(NSTimer*)timer
{
    NSLog(@"haha!");
}

@end
复制代码

 综上: timer都会对它的target进行retain,我们需要小心对待这个target的生命周期问题,尤其是重复性的timer。

 

三、NSTimer会是准时触发事件吗

  答案是否定的,而且有时候你会发现实际的触发时间跟你想象的差距还比较大。NSTimer不是一个实时系统,因此不管是一次性的还是周期性的timer的实际触发事件的时间可能都会跟我们预想的会有出入。差距的大小跟当前我们程序的执行情况有关系,比如可能程序是多线程的,而你的timer只是添加在某一个线程的runloop的某一种指定的runloopmode中,由于多线程通常都是分时执行的,而且每次执行的mode也可能随着实际情况发生变化。

  假设你添加了一个timer指定2秒后触发某一个事件,但是签好那个时候当前线程在执行一个连续运算(例如大数据块的处理等),这个时候timer就会延迟到该连续运算执行完以后才会执行。重复性的timer遇到这种情况,如果延迟超过了一个周期,则会和后面的触发进行合并,即在一个周期内只会触发一次。但是不管该timer的触发时间延迟的有多离谱,他后面的timer的触发时间总是倍数于第一次添加timer的间隙。

  原文如下“A repeating timer reschedules itself based on the scheduled firing time, not the actual firing time. For example, if a timer is scheduled to fire at a particular time and every 5 seconds after that, the scheduled firing time will always fall on the original 5 second time intervals, even if the actual firing time gets delayed. If the firing time is delayed so far that it passes one or more of the scheduled firing times, the timer is fired only once for that time period; the timer is then rescheduled, after firing, for the next scheduled firing time in the future.”

  下面请看一个简单的例子:

Simulate Thread Busy
复制代码
- (void)applicationDidBecomeActive:(UIApplication *)application
{
    SvTestObject *testObject2 = [[SvTestObject alloc] init];
    [NSTimer scheduledTimerWithTimeInterval:1 target:testObject2 selector:@selector(timerAction:) userInfo:nil repeats:YES];
    [testObject2 release];
    
    NSLog(@"Simulate busy");
    [self performSelector:@selector(simulateBusy) withObject:nil afterDelay:3];
}

// 模拟当前线程正好繁忙的情况
- (void)simulateBusy
{
    NSLog(@"start simulate busy!");
    NSUInteger caculateCount = 0x0FFFFFFF;
    CGFloat uselessValue = 0;
    for (NSUInteger i = 0; i < caculateCount; ++i) {
        uselessValue = i / 0.3333;
    }
    NSLog(@"finish simulate busy!");
}
复制代码

  例子中首先开启了一个timer,这个timer每隔1秒调用一次target的timerAction方法,紧接着我们在3秒后调用了一个模拟线程繁忙的方法(其实就是一个大的循环)。运行程序后输出结果如下:

  观察结果我们可以发现,当线程空闲的时候timer的消息触发还是比较准确的,但是在36分12秒开始线程一直忙着做大量运算,知道36分14秒该运算才结束,这个时候timer才触发消息,这个线程繁忙的过程超过了一个周期,但是timer并没有连着触发两次消息,而只是触发了一次。等线程忙完以后后面的消息触发的时间仍然都是整数倍与开始我们指定的时间,这也从侧面证明,timer并不会因为触发延迟而导致后面的触发时间发生延迟。

  综上: timer不是一种实时的机制,会存在延迟,而且延迟的程度跟当前线程的执行情况有关。

 

四、NSTimer为什么要添加到RunLoop中才会有作用

  前面的例子中我们使用的是一种便利方法,它其实是做了两件事:首先创建一个timer,然后将该timer添加到当前runloop的default mode中。也就是这个便利方法给我们造成了只要创建了timer就可以生效的错觉,我们当然可以自己创建timer,然后手动的把它添加到指定runloop的指定mode中去。

  NSTimer其实也是一种资源,如果看过多线程变成指引文档的话,我们会发现所有的source如果要起作用,就得加到runloop中去。同理timer这种资源要想起作用,那肯定也需要加到runloop中才会又效喽。如果一个runloop里面不包含任何资源的话,运行该runloop时会立马退出。你可能会说那我们APP的主线程的runloop我们没有往其中添加任何资源,为什么它还好好的运行。我们不添加,不代表框架没有添加,如果有兴趣的话你可以打印一下main thread的runloop,你会发现有很多资源。 

  下面我们看一个小例子:  

复制代码
- (void)applicationDidBecomeActive:(UIApplication *)application
{
    [self testTimerWithOutShedule];
}

- (void)testTimerWithOutShedule
{
    NSLog(@"Test timer without shedult to runloop");
    SvTestObject *testObject3 = [[SvTestObject alloc] init];
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:testObject3 selector:@selector(timerAction:) userInfo:nil repeats:NO];
    [testObject3 release];
    NSLog(@"invoke release to testObject3");
}

- (void)applicationWillResignActive:(UIApplication *)application
{
    NSLog(@"SvTimerSample Will resign Avtive!");
}
复制代码

  这个小例子中我们新建了一个timer,为它指定了有效的target和selector,并指出了1秒后触发该消息,运行结果如下:

  观察发现这个消息永远也不会触发,原因很简单,我们没有将timer添加到runloop中。

  综上: 必须得把timer添加到runloop中,它才会生效。


五、NSTimer加到了RunLoop中但迟迟的不触发事件

  为什么明明添加了,但是就是不按照预先的逻辑触发事件呢???原因主要有以下两个:

1、runloop是否运行

  每一个线程都有它自己的runloop,程序的主线程会自动的使runloop生效,但对于我们自己新建的线程,它的runloop是不会自己运行起来,当我们需要使用它的runloop时,就得自己启动。

  那么如果我们把一个timer添加到了非主线的runloop中,它还会按照预期按时触发吗?下面请看一段测试程序:

复制代码
- (void)applicationDidBecomeActive:(UIApplication *)application
{
    [NSThread detachNewThreadSelector:@selector(testTimerSheduleToRunloop1) toTarget:self withObject:nil];
}

// 测试把timer加到不运行的runloop上的情况
- (void)testTimerSheduleToRunloop1
{
    NSAutoreleasePool *pool = [[NSAutoreleasePool alloc] init];
    
    NSLog(@"Test timer shedult to a non-running runloop");
    SvTestObject *testObject4 = [[SvTestObject alloc] init];
    NSTimer *timer = [[NSTimer alloc] initWithFireDate:[NSDate dateWithTimeIntervalSinceNow:1] interval:1 target:testObject4 selector:@selector(timerAction:) userInfo:nil repeats:NO];
    [[NSRunLoop currentRunLoop] addTimer:timer forMode:NSDefaultRunLoopMode];
    // 打开下面一行输出runloop的内容就可以看出,timer却是已经被添加进去
    //NSLog(@"the thread's runloop: %@", [NSRunLoop currentRunLoop]);
    
    // 打开下面一行, 该线程的runloop就会运行起来,timer才会起作用
    //[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];
    
    [testObject4 release];
    NSLog(@"invoke release to testObject4");

    [pool release];
}

- (void)applicationWillResignActive:(UIApplication *)application
{
    NSLog(@"SvTimerSample Will resign Avtive!");
}
复制代码

  上面的程序中,我们新创建了一个线程,然后创建一个timer,并把它添加当该线程的runloop当中,但是运行结果如下:

  观察运行结果,我们发现这个timer知道执行退出也没有触发我们指定的方法,如果我们把上面测试程序中“//[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:3]];”这一行的注释去掉,则timer将会正确的掉用我们指定的方法。

2、mode是否正确

  我们前面自己动手添加runloop的时候,可以看到有一个参数runloopMode,这个参数是干嘛的呢?

  前面提到了要想timer生效,我们就得把它添加到指定runloop的指定mode中去,通常是主线程的defalut mode。但有时我们这样做了,却仍然发现timer还是没有触发事件。这是为什么呢?

  这是因为timer添加的时候,我们需要指定一个mode,因为同一线程的runloop在运行的时候,任意时刻只能处于一种mode。所以只能当程序处于这种mode的时候,timer才能得到触发事件的机会。

  举个不恰当的例子,我们说兄弟几个分别代表runloop的mode,timer代表他们自己的才水桶,然后一群人去排队打水,只有一个水龙头,那么同一时刻,肯定只能有一个人处于接水的状态。也就是说你虽然给了老二一个桶,但是还没轮到它,那么你就得等,只有轮到他的时候你的水桶才能碰上用场。

  最后一个例子我就不贴了,也很简单,需要的话,我qq发给你。

  综上: 要让timer生效,必须保证该线程的runloop已启动,而且其运行的runloopmode也要匹配。


作者:cordova 发表于2017/6/4 22:14:38 原文链接
阅读:105 评论:0 查看评论

Flutter进阶—平台插件

$
0
0

这篇文章我们会学习Flutter应用程序如何与iOS和Android设备上可用的平台特定代码集成,这包括设备API,(比如url_launcher和battery)和第三方平台SDK(比如Firebase)。

使用现有的平台插件

Flutter插件是一种特殊的包,一个插件包含一个用Dart编写的API定义,结合Android的平台特定实现,适用于iOS或两者兼容。

搜索插件

现有的Flutter插件可以在Flutter插件仓库查找,其显示在pub存储库中共享的插件。因为Flutter仍然是一个年轻的语言,目前在pub上只有一小部分插件,需要作为Flutter程序员的我们开发和发布新的插件!

在程序中添加插件

如果要添加一个插件“plugin1”到一个应用程序:

  1. 打开您的应用程序文件夹中的pubspec.yaml文件,并在dependencies下添加plugin1

  2. 获取插件
    在终端中:运行flutter packages get
    在IntelliJ中:在pubspec.yaml顶部的动作功能区中点击“Packages Get”

  3. 构建或运行您的应用程序,作为其中的一部分,Flutter将“插入”平台特定的代码,从插件到您的应用程序。

演示实例

URL Launcher插件允许您打开移动平台上的默认浏览器来显示给定的URL,它在Android和iOS上均受支持。我们就写一个使用Flutter URLLauncher插件启动浏览器的实例。

首先打开pubspec.yaml,并添加url_launcher插件:

dependencies:
  flutter:
    sdk: flutter
  url_launcher:

添加完成之后记得点击顶部的“Packages Get”,然后再打开lib/main.dart,并将其全部内容替换为以下代码:

import 'package:flutter/material.dart';
import 'package:url_launcher/url_launcher.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
        title: 'Flutter Demo',
        home: new DemoPage(),
    );
  }
}

class DemoPage extends StatelessWidget {
  launchURL() {
    launch('https://www.baidu.com/');
  }

  @override
  Widget build(BuildContext context) {
    return new Scaffold(
      body: new Center(
        child: new RaisedButton(
          onPressed: launchURL,
          child: new Text('百度首页'),
        )
      )
    );
  }
}

运行应用程序,当您点击“百度首页”时,您应该会看到手机的默认浏览器打开,并显示百度首页。

创建一个平台插件

如果您希望在多个Flutter应用程序中使用特定于平台的代码,将代码分离到位于主应用程序之外的目录中,作为平台插件是很好的方案,甚至还能分享给所有Flutter开发人员。

您可以使用–plugin标记与flutter create创建一个插件项目。使用–org选项指定您的组织,使用反向域名符号,该值用于生成的Android和iOS代码中的各种包和包标识符。

flutter create --org com.example --plugin hello

这将在hello/文件夹中创建一个包含以下内容的插件项目:

  • lib/hello.dart
    插件的Dart API

  • android/src/main/java/com/yourcompany/hello/HelloPlugin.java
    Android平台具体实现插件API

  • ios/Classes/HelloPlugin.m
    iOS平台具体实现插件API

  • example/
    一个如何使用插件的Flutter应用程序,插件的使用说明

默认情况下,该插件项目使用Objective-C的iOS代码和Java的Android代码。如果您喜欢Swift或Kotlin,则可以使用-i指定iOS语言或使用-a指定Android语言。

flutter create --plugin -i swift -a kotlin hello

编辑插件的源代码

插件API代码(.dart)

要编辑Dart插件API代码,需要在IntelliJ IDEA(或者您喜欢的Dart编辑器)中打开hello/,插件API位于项目视图中显示的lib/main.dart中。

要运行插件,您需要启动插件示例应用程序,这需要定义启动配置:

  1. 选择“Run > Edit Configurations…”
  2. 选择“+”,然后选择“Flutter”
  3. 在“Dart entrypoint”,输入<plugin folder>/example/lib/main.dart
  4. 选择“OK”
  5. 使用“Run”或“Debug”启动示例应用程序

Android平台的代码(.java/.kt)

在Android Studio中编辑Android平台代码之前,首先要确保代码已经构建至少一次,即从IntelliJ运行示例应用程序,或在终端中执行cd hello/example; flutter build apk

接下来,进行以下操作:

  1. 启动Android Studio
  2. 在“Welcome to Android Studio”对话框中选择“Import project”,或者在菜单中选择“File > New > Import Project…”,然后选择hello/example/android/build.gradle文件
  3. 在“Gradle Sync”对话框中,选择“OK”
  4. 在“Android Gradle Plugin Update”对话框中,选择“Don’t remind me again for this project”

您的插件的Android平台代码位于hello/java/com.yourcompany.hello/HelloPlugin中,您可以通过按▶按钮从Android Studio运行示例应用程序。

iOS平台的代码(.h+.m/.swift)

在编辑Xcode中的iOS平台代码之前,首先要确保代码已经构建至少一次,即从IntelliJ运行示例应用程序,或在终端中执行cd hello/example; flutter build ios

接下来,进行以下操作:

  1. 启动Xcode
  2. 选择“File > Open”,然后选择hello/example/ios/Runner.xcworkspace文件

插件的iOS平台代码位于项目导航器中的Pods/Development Pods/hello/Classes/中,您可以通过按▶按钮运行示例应用程序。

管理从Flutter程序到Flutter插件的依赖关系

一旦插件已经发布,您可以依赖它,只需在pubspec.yaml中列出它的名称,比如上面的例子。在开发尚未发布的插件或者不适用于公开发布的私有插件的开发中,依赖于插件还有其他的方法:

  • Path依赖:Flutter应用程序可以通过文件系统路径依赖path: dependency,该路径可以是相对的,也可以是绝对路径。例如,要使用位于应用程序旁边的目录中的插件“plugin1”,请使用以下语法:
dependencies:
  flutter:
    sdk: flutter
  plugin1:
    path: ../plugin1/
  • Git依赖:您还可以依赖于存储在Git存储库中的包,包必须位于报告(Repo)的根部,使用以下语法:
 dependencies:
   flutter:
     sdk: flutter
   plugin1:
     git:
       url: git://github.com/flutter/plugin1.git

发布平台插件

一旦你实现了插件,你可以在Pub发布。这使得其他开发人员可以轻松地使用它,如同上述UrlLauncher的演示实例。发布的方法会在以后的文章中详细讲解…

作者:hekaiyou 发表于2017/6/4 23:43:01 原文链接
阅读:157 评论:0 查看评论

深入理解Java并发之synchronized实现原理

$
0
0

【版权申明】未经博主同意,谢绝转载!(请尊重原创,博主保留追究权)
http://blog.csdn.net/javazejian/article/details/72828483
出自【zejian的博客】

关联文章:

深入理解Java类型信息(Class对象)与反射机制

深入理解Java枚举类型(enum)

深入理解Java注解类型(@Annotation)

深入理解Java并发之synchronized实现原理

本篇主要是对Java并发中synchronized关键字进行较为深入的探索,这些知识点结合博主对synchronized的个人理解以及相关的书籍的讲解(在结尾参考资料),如有误处,欢迎留言。

线程安全是并发编程中的重要关注点,应该注意到的是,造成线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。

synchronized的三种应用方式

synchronized关键字最主要有以下3种应用方式,下面分别介绍

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁

  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁

  • 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。

synchronized作用于实例方法

所谓的实例对象锁就是用synchronized修饰实例对象中的实例方法,注意是实例方法不包括静态方法,如下

public class AccountingSync implements Runnable{
    //共享资源(临界资源)
    static int i=0;

    /**
     * synchronized 修饰实例方法
     */
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        AccountingSync instance=new AccountingSync();
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();
        t2.start();
        System.out.println(i);
    }
    /**
     * 输出结果:
     * 2000000
     */
}

上述代码中,我们开启两个线程操作同一个共享资源即变量i,由于i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成,如果第二个线程在第一个线程读取旧值和写回新值期间读取i的域值,那么第二个线程就会与第一个线程一起看到同一个值,并执行相同值的加1操作,这也就造成了线程安全失败,因此对于increase方法必须使用synchronized修饰,以便保证线程安全。此时我们应该注意到synchronized修饰的是实例方法increase,在这样的情况下,当前线程的锁便是实例对象instance,注意Java中的线程同步锁可以是任意对象。从代码执行结果来看确实是正确的,倘若我们没有使用synchronized关键字,其最终输出结果就很可能小于2000000,这便是synchronized关键字的作用。这里我们还需要意识到,当一个线程正在访问一个对象的 synchronized 实例方法,那么其他线程不能访问该对象的其他 synchronized 方法,毕竟一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他synchronized实例方法,但是其他线程还是可以访问该实例对象的其他非synchronized方法,当然如果是一个线程 A 需要访问实例对象 obj1 的 synchronized 方法 f1(当前对象锁是obj1),另一个线程 B 需要访问实例对象 obj2 的 synchronized 方法 f2(当前对象锁是obj1),这样是允许的,因为两个实例对象锁并不同相同,此时如果两个线程操作数据并非共享的,线程安全是有保障的,遗憾的是如果两个线程操作的是共享数据,那么线程安全就有可能无法保证了,如下代码将演示出该现象

public class AccountingSyncBad implements Runnable{
    static int i=0;
    public synchronized void increase(){
        i++;
    }
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncBad());
        //new新实例
        Thread t2=new Thread(new AccountingSyncBad());
        t1.start();
        t2.start();
        //join含义:当前线程A等待thread线程终止之后才能从thread.join()返回
        t1.join();
        t2.join();
        System.out.println(i);
    }
}

上述代码与前面不同的是我们同时创建了两个新实例AccountingSyncBad,然后启动两个不同的线程对共享变量i进行操作,但很遗憾操作结果是1452317而不是期望结果2000000,因为上述代码犯了严重的错误,虽然我们使用synchronized修饰了increase方法,但却new了两个不同的实例对象,这也就意味着存在着两个不同的实例对象锁,因此t1和t2都会进入各自的对象锁,也就是说t1和t2线程使用的是不同的锁,因此线程安全是无法保证的。解决这种困境的的方式是将synchronized作用于静态的increase方法,这样的话,对象锁就当前类对象,由于无论创建多少个实例对象,但对于的类对象拥有只有一个,所有在这样的情况下对象锁就是唯一的。下面我们看看如何使用将synchronized作用于静态的increase方法。

synchronized作用于静态方法

当synchronized作用于静态方法时,其锁就是当前类的class对象锁。由于静态成员不专属于任何一个实例对象,是类成员,因此通过class对象锁可以控制静态 成员的并发操作。需要注意的是如果一个线程A调用一个实例对象的非static synchronized方法,而线程B需要调用这个实例对象所属类的静态 synchronized方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的class对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁,看如下代码

public class AccountingSyncClass implements Runnable{
    static int i=0;

    /**
     * 作用于静态方法,锁是当前class对象,也就是
     * AccountingSyncClass类对应的class对象
     */
    public static synchronized void increase(){
        i++;
    }

    /**
     * 非静态,访问时锁不一样不会发生互斥
     */
    public synchronized void increase4Obj(){
        i++;
    }

    @Override
    public void run() {
        for(int j=0;j<1000000;j++){
            increase();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //new新实例
        Thread t1=new Thread(new AccountingSyncClass());
        //new心事了
        Thread t2=new Thread(new AccountingSyncClass());
        //启动线程
        t1.start();t2.start();

        t1.join();t2.join();
        System.out.println(i);
    }
}

由于synchronized关键字修饰的是静态increase方法,与修饰实例方法不同的是,其锁对象是当前类的class对象。注意代码中的increase4Obj方法是实例方法,其对象锁是当前实例对象,如果别的线程调用该方法,将不会产生互斥现象,毕竟锁对象不同,但我们应该意识到这种情况下可能会发现线程安全问题(操作了共享静态变量i)。

synchronized同步代码块

除了使用关键字修饰实例方法和静态方法外,还可以使用同步代码块,在某些情况下,我们编写的方法体可能比较大,同时存在一些比较耗时的操作,而需要同步的代码又只有一小部分,如果直接对整个方法进行同步操作,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹,这样就无需对整个方法进行同步操作了,同步代码块的使用示例如下:

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    @Override
    public void run() {
        //省略其他耗时操作....
        //使用同步代码块对变量i进行同步操作,锁对象为instance
        synchronized(instance){
            for(int j=0;j<1000000;j++){
                    i++;
              }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

从代码看出,将synchronized作用于一个给定的实例对象instance,即当前实例对象就是锁对象,每次当线程进入synchronized包裹的代码块时就会要求当前线程持有instance实例对象锁,如果当前有其他线程正持有该对象锁,那么新到的线程就必须等待,这样也就保证了每次只有一个线程执行i++;操作。当然除了instance作为对象外,我们还可以使用this对象(代表当前实例)或者当前类的class对象作为锁,如下代码:

//this,当前实例对象锁
synchronized(this){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

//class对象锁
synchronized(AccountingSync.class){
    for(int j=0;j<1000000;j++){
        i++;
    }
}

了解完synchronized的基本含义及其使用方式后,下面我们将进一步深入理解synchronized的底层实现原理。

synchronized底层语义原理

Java 虚拟机中的同步(Synchronization)基于进入和退出管程(Monitor)对象实现, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块)还是隐式同步都是如此。在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。下面先来了解一个概念Java对象头,这对深入理解synchronized实现原理非常关键。

理解Java对象头与Monitor

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下:

  • 实例变量:存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按4字节对齐。

  • 填充数据:由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐,这点了解即可。

而对于顶部,则是Java头对象,它实现synchronized的锁对象的基础,这点我们重点分析它,一般而言,synchronized使用的锁对象是存储在Java对象头里的,jvm中采用2个字节来存储对象头(如果对象是数组则会分配3个字节,多出来的1个字节记录的是数组长度),其主要结构是由Mark Word 和 Class Metadata Address 组成,其结构说明如下表:

虚拟机位数 头对象结构 说明
32/64bit Mark Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
32/64bit Class Metadata Address 类型指针指向对象的类元数据,JVM通过这个指针确定该对象是哪个类的实例。

其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构

锁状态 25bit 4bit 1bit是否是偏向锁 2bit 锁标志位
无锁状态 对象HashCode 对象分代年龄 0 01

由于对象头的信息是与对象自身定义的数据没有关系的额外存储成本,因此考虑到JVM的空间效率,Mark Word 被设计成为一个非固定的数据结构,以便存储更多有效的数据,它会根据对象本身的状态复用自己的存储空间,如32位JVM下,除了上述列出的Mark Word默认存储结构外,还有如下可能变化的结构:

其中轻量级锁和偏向锁是Java 6 对 synchronized 锁进行优化后新增加的,稍后我们会简要分析。这里我们主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,C++实现的)

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //记录个数
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSe t集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示

由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析),ok~,有了上述知识基础后,下面我们将进一步分析synchronized在字节码层面的具体语义实现。

synchronized代码块底层原理

现在我们重新定义一个synchronized修饰的同步代码块,在代码块中操作共享变量i,如下

public class SyncCodeBlock {

   public int i;

   public void syncTask(){
       //同步代码库
       synchronized (this){
           i++;
       }
   }
}

编译上述代码并使用javap反编译后得到字节码如下(这里我们省略一部分没有必要的信息):

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncCodeBlock.class
  Last modified 2017-6-2; size 426 bytes
  MD5 checksum c80bc322c87b312de760942820b4fed5
  Compiled from "SyncCodeBlock.java"
public class com.zejian.concurrencys.SyncCodeBlock
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
  //........省略常量池中数据
  //构造函数
  public com.zejian.concurrencys.SyncCodeBlock();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 7: 0
  //===========主要看看syncTask方法实现================
  public void syncTask();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter  //注意此处,进入同步方法
         4: aload_0
         5: dup
         6: getfield      #2             // Field i:I
         9: iconst_1
        10: iadd
        11: putfield      #2            // Field i:I
        14: aload_1
        15: monitorexit   //注意此处,退出同步方法
        16: goto          24
        19: astore_2
        20: aload_1
        21: monitorexit //注意此处,退出同步方法
        22: aload_2
        23: athrow
        24: return
      Exception table:
      //省略其他字节码.......
}
SourceFile: "SyncCodeBlock.java"

我们主要关注字节码中的如下代码

3: monitorenter  //进入同步方法
//..........省略其他  
15: monitorexit   //退出同步方法
16: goto          24
//省略其他.......
21: monitorexit //退出同步方法

从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代码块的开始位置,monitorexit指令则指明同步代码块的结束位置,当执行monitorenter指令时,当前线程将试图获取 objectref(即对象锁) 所对应的 monitor 的持有权,当 objectref 的 monitor 的进入计数器为 0,那线程可以成功取得 monitor,并将计数器值设置为 1,取锁成功。如果当前线程已经拥有 objectref 的 monitor 的持有权,那它可以重入这个 monitor (关于重入性稍后会分析),重入时计数器的值也会加 1。倘若其他线程已经拥有 objectref 的 monitor 的所有权,那当前线程将被阻塞,直到正在执行线程执行完毕,即monitorexit指令被执行,执行线程将释放 monitor(锁)并设置计数器值为0 ,其他线程将有机会持有 monitor 。值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都有执行其对应 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。

synchronized方法底层原理

方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会 检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有monitor(虚拟机规范中用的是管程一词), 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放monitor。在方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor。如果一个同步方法执行期间抛 出了异常,并且在方法内部无法处理此异常,那这个同步方法所持有的monitor将在异常抛到同步方法之外时自动释放。下面我们看看字节码层面如何实现:

public class SyncMethod {

   public int i;

   public synchronized void syncTask(){
           i++;
   }
}

使用javap反编译后的字节码如下:

Classfile /Users/zejian/Downloads/Java8_Action/src/main/java/com/zejian/concurrencys/SyncMethod.class
  Last modified 2017-6-2; size 308 bytes
  MD5 checksum f34075a8c059ea65e4cc2fa610e0cd94
  Compiled from "SyncMethod.java"
public class com.zejian.concurrencys.SyncMethod
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool;

   //省略没必要的字节码
  //==================syncTask方法======================
  public synchronized void syncTask();
    descriptor: ()V
    //方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2                  // Field i:I
         5: iconst_1
         6: iadd
         7: putfield      #2                  // Field i:I
        10: return
      LineNumberTable:
        line 12: 0
        line 13: 10
}
SourceFile: "SyncMethod.java"

从字节码中可以看出,synchronized修饰的方法并没有monitorenter指令和monitorexit指令,取得代之的确实是ACC_SYNCHRONIZED标识,该标识指明了该方法是一个同步方法,JVM通过该ACC_SYNCHRONIZED访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。这便是synchronized锁在同步代码块和同步方法上实现的基本原理。同时我们还必须注意到的是在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,而操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,这也是为什么早期的synchronized效率低的原因。庆幸的是在Java 6之后Java官方对从JVM层面对synchronized较大优化,所以现在的synchronized锁效率也优化得很不错了,Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁,接下来我们将简单了解一下Java官方在JVM层面对synchronized锁的优化。

Java虚拟机对synchronized的优化

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级,关于重量级锁,前面我们已详细分析过,下面我们将介绍偏向锁和轻量级锁以及JVM的其他优化手段,这里并不打算深入到每个锁的实现和转换过程更多地是阐述Java虚拟机所提供的每个锁的核心优化思想,毕竟涉及到具体过程比较繁琐,如需了解详细过程可以查阅《深入理解Java虚拟机原理》。

偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时Mark Word 的结构也变为偏向锁结构,当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。所以,对于没有锁竞争的场合,偏向锁有很好的优化效果,毕竟极有可能连续多次是同一个线程申请相同的锁。但是对于锁竞争比较激烈的场合,偏向锁就失效了,因为这样场合极有可能每次申请锁的线程都是不相同的,因此这种场合下不应该使用偏向锁,否则会得不偿失,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。下面我们接着了解轻量级锁。

轻量级锁

倘若偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段(1.6之后加入的),此时Mark Word 的结构也变为轻量级锁的结构。轻量级锁能够提升程序性能的依据是“对绝大部分的锁,在整个同步周期内都不存在竞争”,注意这是经验数据。需要了解的是,轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间访问同一锁的场合,就会导致轻量级锁膨胀为重量级锁。

自旋锁

轻量级锁失败后,虚拟机为了避免线程真实地在操作系统层面挂起,还会进行一项称为自旋锁的优化手段。这是基于在大多数情况下,线程持有锁的时间都不会太长,如果直接挂起操作系统层面的线程可能会得不偿失,毕竟操作系统实现线程之间的切换时需要从用户态转换到核心态,这个状态之间的转换需要相对比较长的时间,时间成本相对较高,因此自旋锁会假设在不久将来,当前的线程可以获得锁,因此虚拟机会让当前想要获取锁的线程做几个空循环(这也是称为自旋的原因),一般不会太久,可能是50个循环或100循环,在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这就是自旋锁的优化方式,这种方式确实也是可以提升效率的。最后没办法也就只能升级为重量级锁了。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间,如下StringBuffer的append是一个同步方法,但是在add方法中的StringBuffer属于一个局部变量,并且不会被其他线程所使用,因此StringBuffer不可能存在共享资源竞争的情景,JVM会自动将其锁消除。

/**
 * Created by zejian on 2017/6/4.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 * 消除StringBuffer同步锁
 */
public class StringBufferRemoveSync {

    public void add(String str1, String str2) {
        //StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
        //因此sb属于不可能共享的资源,JVM会自动消除内部的锁
        StringBuffer sb = new StringBuffer();
        sb.append(str1).append(str2);
    }

    public static void main(String[] args) {
        StringBufferRemoveSync rmsync = new StringBufferRemoveSync();
        for (int i = 0; i < 10000000; i++) {
            rmsync.add("abc", "123");
        }
    }

}

关于synchronized 可能需要了解的关键点

synchronized的可重入性

从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功,在java中synchronized是基于原子性的内部锁机制,是可重入的,因此在一个线程调用synchronized方法的同时在其方法体内部调用该对象另一个synchronized方法,也就是说一个线程得到一个对象锁后再次请求该对象锁,是允许的,这就是synchronized的可重入性。如下:

public class AccountingSync implements Runnable{
    static AccountingSync instance=new AccountingSync();
    static int i=0;
    static int j=0;
    @Override
    public void run() {
        for(int j=0;j<1000000;j++){

            //this,当前实例对象锁
            synchronized(this){
                i++;
                increase();//synchronized的可重入性
            }
        }
    }

    public synchronized void increase(){
        j++;
    }


    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(instance);
        Thread t2=new Thread(instance);
        t1.start();t2.start();
        t1.join();t2.join();
        System.out.println(i);
    }
}

正如代码所演示的,在获取当前实例对象锁后进入synchronized代码块执行同步代码,并在代码块中调用了当前实例对象的另外一个synchronized方法,再次请求当前实例锁时,将被允许,进而执行方法体代码,这就是重入锁最直接的体现,需要特别注意另外一种情况,当子类继承父类时,子类也是可以通过可重入锁调用父类的同步方法。注意由于synchronized是基于monitor实现的,因此每次重入,monitor中的计数器仍会加1。

线程中断与synchronized

线程中断

正如中断二字所表达的意义,在线程运行(run方法)中间打断它,在Java中,提供了以下3个有关线程中断的方法

//中断线程(实例方法)
public void Thread.interrupt();

//判断线程是否被中断(实例方法)
public boolean Thread.isInterrupted();

//判断是否被中断并清除当前中断状态(静态方法)
public static boolean Thread.interrupted();

当一个线程处于被阻塞状态或者试图执行一个阻塞操作时,使用Thread.interrupt()方式中断该线程,注意此时将会抛出一个InterruptedException的异常,同时中断状态将会被复位(由中断状态改为非中断状态),如下代码将演示该过程:

public class InterruputSleepThread3 {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread() {
            @Override
            public void run() {
                //while在try中,通过异常中断就可以退出run循环
                try {
                    while (true) {
                        //当前线程处于阻塞状态,异常必须捕捉处理,无法往外抛出
                        TimeUnit.SECONDS.sleep(2);
                    }
                } catch (InterruptedException e) {
                    System.out.println("Interruted When Sleep");
                    boolean interrupt = this.isInterrupted();
                    //中断状态被复位
                    System.out.println("interrupt:"+interrupt);
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        //中断处于阻塞状态的线程
        t1.interrupt();

        /**
         * 输出结果:
           Interruted When Sleep
           interrupt:false
         */
    }
}

如上述代码所示,我们创建一个线程,并在线程中调用了sleep方法从而使用线程进入阻塞状态,启动线程后,调用线程实例对象的interrupt方法中断阻塞异常,并抛出InterruptedException异常,此时中断状态也将被复位。这里有些人可能会诧异,为什么不用Thread.sleep(2000);而是用TimeUnit.SECONDS.sleep(2);其实原因很简单,前者使用时并没有明确的单位说明,而后者非常明确表达秒的单位,事实上后者的内部实现最终还是调用了Thread.sleep(2000);,但为了编写的代码语义更清晰,建议使用TimeUnit.SECONDS.sleep(2);的方式,注意TimeUnit是个枚举类型。ok~,除了阻塞中断的情景,我们还可能会遇到处于运行期且非阻塞的状态的线程,这种情况下,直接调用Thread.interrupt()中断线程是不会得到任响应的,如下代码,将无法中断非阻塞状态下的线程:

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    System.out.println("未被中断");
                }
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /**
         * 输出结果(无限执行):
             未被中断
             未被中断
             未被中断
             ......
         */
    }
}

虽然我们调用了interrupt方法,但线程t1并未被中断,因为处于非阻塞状态的线程需要我们手动进行中断检测并结束程序,改进后代码如下:

public class InterruputThread {
    public static void main(String[] args) throws InterruptedException {
        Thread t1=new Thread(){
            @Override
            public void run(){
                while(true){
                    //判断当前线程是否被中断
                    if (this.isInterrupted()){
                        System.out.println("线程中断");
                        break;
                    }
                }

                System.out.println("已跳出循环,线程中断!");
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /**
         * 输出结果:
            线程中断
            已跳出循环,线程中断!
         */
    }
}

是的,我们在代码中使用了实例方法isInterrupted判断线程是否已被中断,如果被中断将跳出循环以此结束线程。综合所述,可以简单总结一下中断两种情况,一种是当线程处于阻塞状态或者试图执行一个阻塞操作时,我们可以使用实例方法interrupt()进行线程中断,执行中断操作后将会抛出interruptException异常(该异常必须捕捉无法向外抛出)并将中断状态复位,另外一种是当线程处于运行状态时,我们也可调用实例方法interrupt()进行线程中断,但同时必须手动判断中断状态,并编写中断线程的代码(其实就是结束run方法体的代码)。有时我们在编码时可能需要兼顾以上两种情况,那么就可以如下编写:

public void run(){
    try {
    //判断当前线程是否已中断,注意interrupted方法是静态的,执行后会对中断状态进行复位
    while (!Thread.interrupted()) {
        TimeUnit.SECONDS.sleep(2);
    }
    } catch (InterruptedException e) {

    }
}

中断与synchronized

事实上线程的中断操作对于正在等待获取的锁对象的synchronized方法或者代码块并不起作用,也就是对于synchronized来说,如果一个线程在等待锁,那么结果只有两种,要么它获得这把锁继续执行,要么它就保存等待,即使调用中断线程的方法,也不会生效。演示代码如下

/**
 * Created by zejian on 2017/6/2.
 * Blog : http://blog.csdn.net/javazejian [原文地址,请尊重原创]
 */
public class SynchronizedBlocked implements Runnable{

    public synchronized void f() {
        System.out.println("Trying to call f()");
        while(true) // Never releases lock
            Thread.yield();
    }

    /**
     * 在构造器中创建新线程并启动获取对象锁
     */
    public SynchronizedBlocked() {
        //该线程已持有当前实例锁
        new Thread() {
            public void run() {
                f(); // Lock acquired by this thread
            }
        }.start();
    }
    public void run() {
        //中断判断
        while (true) {
            if (Thread.interrupted()) {
                System.out.println("中断线程!!");
                break;
            } else {
                f();
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {
        SynchronizedBlocked sync = new SynchronizedBlocked();
        Thread t = new Thread(sync);
        //启动后调用f()方法,无法获取当前实例锁处于等待状态
        t.start();
        TimeUnit.SECONDS.sleep(1);
        //中断线程,无法生效
        t.interrupt();
    }
}

我们在SynchronizedBlocked构造函数中创建一个新线程并启动获取调用f()获取到当前实例锁,由于SynchronizedBlocked自身也是线程,启动后在其run方法中也调用了f(),但由于对象锁被其他线程占用,导致t线程只能等到锁,此时我们调用了t.interrupt();但并不能中断线程。

等待唤醒机制与synchronized

所谓等待唤醒机制本篇主要指的是notify/notifyAll和wait方法,在使用这3个方法时,必须处于synchronized代码块或者synchronized方法中,否则就会抛出IllegalMonitorStateException异常,这是因为调用这几个方法前必须拿到当前对象的监视器monitor对象,也就是说notify/notifyAll和wait方法依赖于monitor对象,在前面的分析中,我们知道monitor 存在于对象头的Mark Word 中(存储monitor引用指针),而synchronized关键字可以获取 monitor ,这也就是为什么notify/notifyAll和wait方法必须在synchronized代码块或者synchronized方法调用的原因。

synchronized (obj) {
       obj.wait();
       obj.notify();
       obj.notifyAll();         
 }

需要特别理解的一点是,与sleep方法不同的是wait方法调用完成后,线程将被暂停,但wait方法将会释放当前持有的监视器锁(monitor),直到有线程调用notify/notifyAll方法后方能继续执行,而sleep方法只让线程休眠并不释放锁。同时notify/notifyAll方法调用后,并不会马上释放监视器锁,而是在相应的synchronized(){}/synchronized方法执行结束后才自动释放锁。

ok~,篇幅已比较长了,关于synchronized,我们就暂且聊到这。如有错误,欢迎指正,谢谢~~。

本篇的主要参考资料:
《Java编程思想》
《深入理解Java虚拟机》
《实战Java高并发程序设计》



如果您喜欢我写的博文,读后觉得收获很大,不妨小额赞助我一下,让我有动力继续写出高质量的博文,感谢您的赞赏!支付宝、微信


        

作者:javazejian 发表于2017/6/4 17:44:44 原文链接
阅读:416 评论:1 查看评论

Android内存优化(一)DVM和ART原理初探

$
0
0

相关文章
Android性能优化系列
Java虚拟机系列

前言

要学习Android的内存优化,首先要了解Java虚拟机,此前我用了多篇文章来介绍Java虚拟机的知识,就是为了这个系列做铺垫。在Android开发中我们接触的是与Java虚拟机类似的Dalvik虚拟机和ART虚拟机,这一篇我们就来了解它们的基本原理。

1.Dalvik虚拟机

Dalvik虚拟机( Dalvik Virtual Machine ),简称Dalvik VM或者DVM。它是由Dan Bornstein编写的,名字源于他的祖先居住过的名为Dalvik的小渔村。DVM是Google专门为Android平台开发的虚拟机,它运行在Android运行时库中。需要注意的是DVM并不是一个Java虚拟机(以下简称JVM),至于为什么,下文会给你答案。

DVM与JVM的区别

DVM之所以不是一个JVM ,主要原因是DVM并没有遵循JVM规范来实现。DVM与JVM主要有以下区别。

基于的架构不同
JVM基于栈则意味着需要去栈中读写数据,所需的指令会更多,这样会导致速度慢,对于性能有限的移动设备,显然不是很适合。
DVM是基于寄存器的,它没有基于栈的虚拟机在拷贝数据而使用的大量的出入栈指令,同时指令更紧凑更简洁。但是由于显示指定了操作数,所以基于寄存器的指令会比基于栈的指令要大,但是由于指令数量的减少,总的代码数不会增加多少。

执行的字节码不同
在Java SE程序中,Java类会被编译成一个或多个.class文件,打包成jar文件,而后JVM会通过相应的.class文件和jar文件获取相应的字节码。执行顺序为: .java文件 -> .class文件 -> .jar文件
而DVM会用dx工具将所有的.class文件转换为一个.dex文件,然后DVM会从该.dex文件读取指令和数据。执行顺序为:
.java文件 –>.class文件-> .dex文件
QQ截图20170602163255.png

如上图所示,.jar文件里面包含多个.class文件,每个.class文件里面包含了该类的常量池、类信息、属性等等。当JVM加载该.jar文件的时候,会加载里面的所有的.class文件,JVM的这种加载方式很慢,对于内存有限的移动设备并不合适。
而在.apk文件中只包含了一个.dex文件,这个.dex文件里面将所有的.class里面所包含的信息全部整合在一起了,这样再加载就提高了速度。.class文件存在很多的冗余信息,dex工具会去除冗余信息,并把所有的.class文件整合到.dex文件中,减少了I/O操作,提高了类的查找速度。

DVM允许在有限的内存中同时运行多个进程
DVM经过优化,允许在有限的内存中同时运行多个进程。在Android中的每一个应用都运行在一个DVM实例中,每一个DVM实例都运行在一个独立的进程空间。独立的进程可以防止在虚拟机崩溃的时候所有程序都被关闭。

DVM由Zygote创建和初始化
Android系统启动流程(二)解析Zygote进程启动过程这篇文章中我介绍过 Zygote,可以称它为孵化器,它是一个DVM进程,同时它也用来创建和初始化DVM实例。每当系统需要创建一个应用程序时,Zygote就会fock自身,快速的创建和初始化一个DVM实例,用于应用程序的运行。

DVM架构

DVM的源码位于dalvik/目录下,其中dalvik/vm目录下的内容是DVM的具体实现部分,它会被编译成libdvm.so;dalvik/libdex会被编译成libdex.a静态库,作为dex工具使用;dalvik/dexdump是.dex文件的反编译工具;DVM的可执行程序位于dalvik/dalvikvm中,将会被编译成dalvikvm可执行程序。DVM架构如下图所示。

DVM架构_副本.png
从上图可以看出,首先Java编译器编译的.class文件经过DX工具转换为.dex文件,.dex文件由类加载器处理,接着解释器根据指令集对Dalvik字节码进行解释、执行,最后交与Linux处理。

DVM的运行时堆

DVM的运行时堆主要由两个Space以及多个辅助数据结构组成,两个Space分别是Zygote Space(Zygote Heap)和Allocation Space(Active Heap)。Zygote Space用来管理Zygote进程在启动过程中预加载和创建的各种对象,Zygote Space中不会触发GC,所有进程都共享该区域,比如系统资源。Allocation Space是在Zygote进程fork第一个子进程之前创建的,它是一种私有进程,Zygote进程和fock的子进程在Allocation Space上进行对象分配和释放。
除了这两个Space,还包含以下数据结构:

  • Card Table:用于DVM Concurrent GC,当第一次进行垃圾标记后,记录垃圾信息。
  • Heap Bitmap:有两个Heap Bitmap,一个用来记录上次GC存活的对象,另一个用来记录这次GC存活的对象。
  • Mark Stack:DVM的运行时堆使用标记-清除(Mark-Sweep)算法进行GC,不了解标记-清除算法的同学查看Java虚拟机(四)垃圾收集算法这篇文章。Mark Stack就是在GC的标记阶段使用的,它用来遍历存活的对象。

2.ART虚拟机

ART(Android Runtime)是Android 4.4发布的,用来替换Dalvik虚拟,Android 4.4默认采用的还是DVM,系统会提供一个选项来开启ART。在Android 5.0时,默认采用ART,DVM从此退出历史舞台。

ART与DVM的区别

DVM中的应用每次运行时,字节码都需要通过即时编译器(JIT,just in time)转换为机器码,这会使得应用的运行效率降低。而在ART中,系统在安装应用时会进行一次预编译(AOT,ahead of time),将字节码预先编译成机器码并存储在本地,这样应用每次运行时就不需要执行编译了,运行效率也大大提升。

ART的运行时堆

与DVM的GC不同的是,ART的GC类型有多种,主要分为Mark-Sweep GC和Compacting GC。ART的运行时堆的空间根据不同的GC类型也有着不同的划分,如果采用的是Mark-Sweep GC,运行时堆主要是由四个Space和多个辅助数据结构组成,四个Space分别是Zygote Space、Allocation Space、Image Space和Large Object Space。Zygote Space、Allocation Space和DVM中的作用是一样的。Image Space用来存放一些预加载类,Large Object Space用来分配一些大对象(默认大小为12k)。其中Zygote Space和Image Space是进程间共享的。
采用Mark-Sweep GC的运行时堆空间划分如下图所示。

DVM和ADT(1).png

除了这四个Space,ART的Java堆中还包括两个Mod Union Table,一个Card Table,两个Heap Bitmap,两个Object Map,以及三个Object Stack。如果想要跟多的了解它们,请参考ART运行时Java堆创建过程分析 – 罗升阳这篇文章。

参考资料
《深入解析Android虚拟机》
《Android技术内幕-系统卷》
《Android性能优化最佳实践》
stackoverflow:关于dvm存储的问题
Dalvik Virtual Machine – COSC 530
ART运行时Java堆创建过程分析 – 罗升阳
Dalvik虚拟机Java堆创建过程分析 – 罗升阳
ART运行时垃圾收集机制简要介绍和学习计划 – 罗升阳
Android 性能优化—Android memory 参数tuning(二)

作者:itachi85 发表于2017/6/5 0:24:14 原文链接
阅读:78 评论:1 查看评论

Android中解决破解签名验证之后导致的登录授权失效问题

$
0
0

一、前言

之前已经介绍了一款自动爆破应用签名工具kstools,不了解的同学还可以去看这篇文章:Android中自动爆破应用签名工具kstools;有了这个工具,就不用在担心签名校验了,不过在发布工具之后,很多热心的同学都很好奇就进行了尝试,有成功的,也有失败的,而在失败中最多的问题就在于应用本身签名爆破已经没问题了,但是在第三方登录的时候就失效了,对于这个问题,不光是这个工具会带来问题,主要是二次打包应用都会有这个问题,那么今天就来分析一下如何解决二次打包带来的登录失效问题。


二、社交登录SDK功能说明

首先我们要知道一件事,就是现在大部分的应用都采用了第三方登录SDK,也就是网上的ShareSDK功能包,这个功能包,其实很简单,就是把多家登录平台合成一起,就是聚合登录SDK功能:


因为我们要解决这个登录问题,所以得先去简单的看一下这个聚合登录SDK源码实现,这个不难,直接下载jar包工程,使用也非常简单:


下载下来之后,目录下有一个QuickIntegrater.jar工具,可以直接运行,然后填写具体的包名和项目名称,之后就会生成对应的集成素材,这里主要包括两部分,一部分是原始多家社交平台的jar包:


一部分就是配置信息文件ShareSDK.xml文件:


这个配置文件是关键核心,就是包含了多家平台的社交信息,而这些信息必须应用自己去各家平台上申请获取信息,这里举个QQ社交平台申请资料:


这里提交应用之后,会有一个appid和appkey这两个值,然后再把这两个值配置到自己的ShareSDK.xml中即可。所以这个聚合SDK其实没做啥事情。


三、问题跟踪

了解了上面聚合SDK功能之后,下面不多说了,直接看爆破app的源码,因为这个聚合SDK的包名是cn.sharesdk.x:


这里我们看一下WX登录失效问题,直接看社交登录源码,(有的同学好奇,我怎么一下就找到这个关键代码了,这个是有方法的,如果WX这种登录失效,在客户端只能依靠应用的签名信息来做判断依据,所以只要在这个包下面搜字符串"signature",就找到了):


这里看到了:会判断手机中安装的WX签名是否正确,如果失败了,就会登录失效,因为WX的签名信息不可能变动的,所以这里就直接写死判断了。那么这里就发现了第一个问题了:因为我们之前用kstools工具爆破的时候是hook应用的PMS服务,拦截获取签名信息方法,然后返回爆破app正确的签名,那么现在这里因为是获取WX的签名,但是也被我们拦截了,返回的是应用的签名,这明显就有问题了,所以我们解决这个问题需要修改一下爆破工具,修改很简单,在拦截签名信息加一个包名判断,只拦截本应用的签名信息


这个工具我修改了,已经更新到github上了,有问题的同学可以下载最新的在进行操作就可以了。


四、授权失败问题分析

那么到这里就介绍了,之前kstools工具的一个漏洞,无差别的拦截了,导致WX也被干了,需要做一层过滤,只需要拦截爆破app本身的签名即可。不过可惜的是,解决了这个问题,在登录还是有问题,比如下面是授权Q登录问题:


这个问题修复其实才是我们本文的重点,也是网上很多同学二次打包之后遇到的问题,那么下面就来详细分析这个问题导致的原因,如何进行修复。


在修复这个问题之前,我们先猜想一下,Q是如何判断应用被二次签名拒绝授权了,还是那句话,在客户端,只能依赖于签名校验信息做判断依据,那么就不科学了,因为app我们已经爆破成功了,理论上应该授权是成功的,可以骗过Q的,但是结果不是这样的。所以猜想:应用授权的时候给Q带过去哪些信息,从现象来看,应该不是应用签名了,不过应用的包名是肯定有的,还有appid,为什么了?因为之前说了如果想用Q社交登录功能,必须得去他后台提交应用获取对应的appid和appkey,而这时候上传app,TX后台已经记录了应用的签名信息,也就是在后台有包名+appid+签名信息对应关系了。所以app在授权登录带过去的肯定有appid信息和包名,然后Q在携带这些信息去服务端进行验证。所以获取应用签名信息肯定是Q端做的。


有了上面的猜想,下面可以进行验证了,首先找到入口,直接使用adb shell dumpsys activity top找到授权页面:


然后借助Jadx工具打开Q应用,这里再次强调一次:TX家的app都很庞大,比如WX,Q等都是多dex文件的,所以直接打开apk会卡死的,一般activity类都是在主dex中的,所以可以解压apk得到classes.dex直接打开就好了


看到代码之后,心终于宽敞了,就和我们的猜想一模一样,Q端得到传递过来的授权app包名,然后获取其签名信息,然后就开始进行网络请求授权:


这里用的是post方式,我们把data中的数据放到json格式工具中:


看到了,这里会上传包名,appid,已经签名信息,那么这里肯定会授权失败了,因为在Q代码中通过包名获取签名信息肯定是二次打包之后的,因为我们拦截签名信息只是在应用内,不是系统全局的哦。


五、问题修复

到这里我们了解了Q授权登录的大致流程了:需要授权app会携带自己在Q后端申请的appid,包名去Q端进行授权,然后Q端拿到包名之后,就获取手机中安装应用的签名信息,然后去服务端进行授权验证。因为我们二次打包签名应用了,所以授权肯定是失败的,所以Q提示非官方应用。了解流程之后,解决也是有很多种方法了,主要涉及两点:

第一点:如果本地不做任何操作,可以把二次打包之后的应用从新提交到Q后台获取新的appid和appkey,然后替换他的ShareSDK.xml中的配置信息即可,但是这种成功率几乎为0,以为Q后台有强大的查重机制,会被检查到的,提示app重复。

第二点:第一点其实说的是理论知识,实际中几乎行不通的,那么就需要在本地进行操作了,这里有两个方法处理:

1》借助Xposed进行系统拦截获取签名信息方法,通过判断获取签名信息的包名来做一次过滤。这样可以骗过Q,也可以骗过应用本身了。

2》因为Xposed需要的额外条件太多,不方便使用,所以这里还可以修改Q中的那段授权代码中获取签名信息,直接替换成需要授权app正确的签名信息。

这里为了给大家介绍更多的逆向知识,就采用第二种了,因为第一种现在谁都会了,第二种需要改Q代码,回编译Q有些问题正好给大家说明一下,下面就来操作一下。首先第一步,我们到构造出Q获取签名信息的算法,这个简单,直接把那部分代码拷贝出来就好了:


把这段代码已经HexUtil.a方法也考出来如下:


然后手机中安装正版的授权app,在运行这段代码,获取到正版授权app的签名信息:


这样,就拿到了,正确签名。然后在去修改Q代码,在修改Q代码有个问题,就是因为TX家的App都做了回编译混淆操作,所以这里不能借助apktools进行反编译修改器smali代码了,而需要用另外一种方式,直接解压出他的dex文件,然后操作即可。这里不多介绍流程,后面会单独出一个系列文章介绍如何进行回编译操作。把这个正确的签名信息在赋值给签名变量,修改之后如下:


然后在把这个dex替换回去,重新签名,安装Q之后,可惜的时候运行报错:


提示appid无效,不过这个问题已经对于我们来说不是问题了,因为我们有了kstools工具了,使用最新的kstools工具直接爆破即可。


这下就可以愉快的,进行Q登录授权了:


看到了,这里可以正常的Q授权登录了,到这里我们就介绍完了如何解决二次打包登录授权问题了,下面就来总结一下问题原因和具体操作。


本文主要讲解了Q端登录授权问题解决方案,但是其他平台原理都是类似,比如WX平台,可以找到授权页面,然后跟踪代码即可,感兴趣的同学可以自己研究解决了!


六、问题和解决方案总结

1、因为之前的kstools进行拦截app内部获取签名的方法,没有做应用包名区分,所以把登录的时候判断社交APP的签名也给拦截了,导致登录授权失败,这个问题直接修复了kstools工具,已经更新到github上,可下载最新的就好了。

2、正常二次打包签名登录授权统一报错信息,Q端提示非正版app,这个是因为在授权app去Q端进行授权的时候会携带自身的appid和包名,然后Q端会在本地通过包名获取其签名信息,然后去服务端进行验证,所以这里解决方法很多,可以借助Xposed直接hook系统的获取签名方法,还有就是修改Q端代码替换正确的app签名信息,当然第二种方式不需要借助Xposed,限制条件虽然少了,但是操作局限性很大,需要重新安装Q,而且只能对授权一个app有效,如果其他app需要授权,还得继续修改Q端代码。

3、关于TX家族的app对回编译都做了很多混淆策略,所以我们得用其他方式二次打包,具体方案会在后续给出一个系列介绍文章,详细介绍如何操作,敬请期待。


说明:关于kstools工具出来之后,很多公司做了新的防护策略,其实这里有一个新的防护策略很简单,就是读取META-INF目录下的RSA文件中的签名信息,这样可以直接校验,而不是通过系统的那个方法获取签名信息,这样二次打包之后,这个RSA文件签名肯定会发生变化的,所以这个就是一个可以抗衡kstools工具的好方案哦。所以说安全和逆向永不停息,安全不息,逆向不止!


kstools工具下载地址:https://github.com/fourbrother/kstools


严重声明:本文主要利用一个样本案例介绍了,如何修复二次打包之后授权登录失败问题修复方案,如果有人利用本文案例或者技术进行非法商业操作,带来的一切法律责任将由操作者本人负责,与文章作者无关,最后还是由衷的希望各位同学能够秉着学习逆向知识的态度阅读文本,非常感谢!


最后还是要感谢某某某同学给我制作的kstools工具效果图,太喜欢了



更多内容:点击这里

关注微信公众号,最新技术干货实时推送

编码美丽技术圈
微信扫一扫进入我的"技术圈"世界

扫一扫加小编微信
添加时请注明:“编码美丽”非常感谢!


作者:jiangwei0910410003 发表于2017/6/5 8:39:05 原文链接
阅读:284 评论:0 查看评论

深入分析setContentView

$
0
0

前言

对于Android的开发者来说,setContentView大家再熟悉不过了,在我们的Activity中首先就是要用它加载我们的布局,但是应该有一部分人是不知道加载布局的原理,今天就从源码的角度分析setContentView加载布局原理。

准备工作

由于我们使用的Android API部分源码是隐藏的,当我们在AndroidStudio中是不能找到源码的,我们可以去官网下载相应源码去查看,当然在GitHub下载相应版本的API替换我们sdk下platforms相应api的android.jar。这样我们就可以在AndroidStudio查看到隐藏的api了,可以断点调试帮助我们阅读源码。

本篇文章分析源码是Android7.1(API25)。

Activiy setContentView源码分析

/**
     * Set the activity content from a layout resource.  The resource will be
     * inflated, adding all top-level views to the activity.
 */
    public void setContentView(@LayoutRes int layoutResID) {
        getWindow().setContentView(layoutResID);
        initWindowDecorActionBar();
    }

在Activity中setContentView最终调用了getWindow()的setContentView·方法,getWindow()返回的是一个Window类,它表示一个窗口的概念,我们的Activity就是一个Window,Dialog和Toast也都是通过Window来展示的,这很好理解,它是一个抽象类,具体的实现是PhoneWindow,加载布局的相关逻辑都几乎都是它处理的。

 @Override
    public void setContentView(int layoutResID) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            final Scene newScene = Scene.getSceneForLayout(mContentParent, layoutResID,
                    getContext());
            transitionTo(newScene);
        } else {
            mLayoutInflater.inflate(layoutResID, mContentParent);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

先判断mContentParent 是否为空,当然第一次启动时mContentParent 时为空的,然后执行installDecor();方法。mContentParent不为空是通过hasFeature(FEATURE_CONTENT_TRANSITIONS)判断是否有转场动画,当没有的时候就把通过mContentParent.removeAllViews();移除mContentParent节点下的所有View.再通过inflate将我们的把布局填充到mContentParent,最后就是内容变化的回调。至于mContentParent 是什么东东,先留个悬念,稍后再说。

 private void installDecor() {
        mForceDecorInstall = false;
        if (mDecor == null) {
            mDecor = generateDecor(-1);
            mDecor.setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
            mDecor.setIsRootNamespace(true);
            if (!mInvalidatePanelMenuPosted && mInvalidatePanelMenuFeatures != 0) {
                mDecor.postOnAnimation(mInvalidatePanelMenuRunnable);
            }
        } else {
            mDecor.setWindow(this);
        }
        if (mContentParent == null) {
            mContentParent = generateLayout(mDecor);

            // Set up decor part of UI to ignore fitsSystemWindows if appropriate.
            mDecor.makeOptionalFitsSystemWindows();

            final DecorContentParent decorContentParent = (DecorContentParent) mDecor.findViewById(
                    R.id.decor_content_parent);

            if (decorContentParent != null) {
                mDecorContentParent = decorContentParent;
                mDecorContentParent.setWindowCallback(getCallback());
                if (mDecorContentParent.getTitle() == null) {
                    mDecorContentParent.setWindowTitle(mTitle);
                }

                final int localFeatures = getLocalFeatures();
                for (int i = 0; i < FEATURE_MAX; i++) {
                    if ((localFeatures & (1 << i)) != 0) {
                        mDecorContentParent.initFeature(i);
                    }
                }

                mDecorContentParent.setUiOptions(mUiOptions);

                if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) != 0 ||
                        (mIconRes != 0 && !mDecorContentParent.hasIcon())) {
                    mDecorContentParent.setIcon(mIconRes);
                } else if ((mResourcesSetFlags & FLAG_RESOURCE_SET_ICON) == 0 &&
                        mIconRes == 0 && !mDecorContentParent.hasIcon()) {
                    mDecorContentParent.setIcon(
                            getContext().getPackageManager().getDefaultActivityIcon());
                    mResourcesSetFlags |= FLAG_RESOURCE_SET_ICON_FALLBACK;
                }
                if ((mResourcesSetFlags & FLAG_RESOURCE_SET_LOGO) != 0 ||
                        (mLogoRes != 0 && !mDecorContentParent.hasLogo())) {
                    mDecorContentParent.setLogo(mLogoRes);
                }

                // Invalidate if the panel menu hasn't been created before this.
                // Panel menu invalidation is deferred avoiding application onCreateOptionsMenu
                // being called in the middle of onCreate or similar.
                // A pending invalidation will typically be resolved before the posted message
                // would run normally in order to satisfy instance state restoration.
                PanelFeatureState st = getPanelState(FEATURE_OPTIONS_PANEL, false);
                if (!isDestroyed() && (st == null || st.menu == null) && !mIsStartingWindow) {
                    invalidatePanelMenu(FEATURE_ACTION_BAR);
                }
            } else {
             //设置标题
                mTitleView = (TextView) findViewById(R.id.title);
                if (mTitleView != null) {
                    if ((getLocalFeatures() & (1 << FEATURE_NO_TITLE)) != 0) {
                        final View titleContainer = findViewById(R.id.title_container);
                        if (titleContainer != null) {
                            titleContainer.setVisibility(View.GONE);
                        } else {
                            mTitleView.setVisibility(View.GONE);
                        }
                        mContentParent.setForeground(null);
                    } else {
                        mTitleView.setText(mTitle);
                    }
                }
            }
            //......初始化属性变量
        }
    }

在上面的方法中主要工作就是初始化mDecor和mContentParent ,以及一些属性的初始化

    protected DecorView generateDecor(int featureId) {
        // System process doesn't have application context and in that case we need to directly use
        // the context we have. Otherwise we want the application context, so we don't cling to the
        // activity.
        Context context;
        if (mUseDecorContext) {
            Context applicationContext = getContext().getApplicationContext();
            if (applicationContext == null) {
                context = getContext();
            } else {
                context = new DecorContext(applicationContext, getContext().getResources());
                if (mTheme != -1) {
                    context.setTheme(mTheme);
                }
            }
        } else {
            context = getContext();
        }
        return new DecorView(context, featureId, this, getAttributes());
    }

generateDecor初始化一个DecorView对象,DecorView继承了FrameLayout,是我们要显示布局的顶级View,我们看到的布局,标题栏都是它里面。

然后将mDecor作为参数调用generateLayout初始化mContetParent

    protected ViewGroup generateLayout(DecorView decor) {
        // Apply data from current theme.
        //获取主题样式
        TypedArray a = getWindowStyle();
        //......省略样式的设置
        // Inflate the window decor.
        int layoutResource;
        //获取feature并根据其来加载对应的xml布局文件
        int features = getLocalFeatures();
        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            layoutResource = R.layout.screen_swipe_dismiss;
        } else if ((features & ((1 << FEATURE_LEFT_ICON) | (1 << FEATURE_RIGHT_ICON))) != 0) {
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleIconsDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                layoutResource = R.layout.screen_title_icons;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
            // System.out.println("Title Icons!");
        } else if ((features & ((1 << FEATURE_PROGRESS) | (1 << FEATURE_INDETERMINATE_PROGRESS))) != 0
                && (features & (1 << FEATURE_ACTION_BAR)) == 0) {
            // Special case for a window with only a progress bar (and title).
            // XXX Need to have a no-title version of embedded windows.
            layoutResource = R.layout.screen_progress;
            // System.out.println("Progress!");
        } else if ((features & (1 << FEATURE_CUSTOM_TITLE)) != 0) {
            // Special case for a window with a custom title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogCustomTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else {
                layoutResource = R.layout.screen_custom_title;
            }
            // XXX Remove this once action bar supports these features.
            removeFeature(FEATURE_ACTION_BAR);
        } else if ((features & (1 << FEATURE_NO_TITLE)) == 0) {
            // If no other features and not embedded, only need a title.
            // If the window is floating, we need a dialog layout
            if (mIsFloating) {
                TypedValue res = new TypedValue();
                getContext().getTheme().resolveAttribute(
                        R.attr.dialogTitleDecorLayout, res, true);
                layoutResource = res.resourceId;
            } else if ((features & (1 << FEATURE_ACTION_BAR)) != 0) {
                layoutResource = a.getResourceId(
                        R.styleable.Window_windowActionBarFullscreenDecorLayout,
                        R.layout.screen_action_bar);
            } else {
                layoutResource = R.layout.screen_title;
            }
            // System.out.println("Title!");
        } else if ((features & (1 << FEATURE_ACTION_MODE_OVERLAY)) != 0) {
            layoutResource = R.layout.screen_simple_overlay_action_mode;
        } else {
            // Embedded, so no decoration is needed.
            layoutResource = R.layout.screen_simple;
            // System.out.println("Simple!");
        }

        mDecor.startChanging();
        mDecor.onResourcesLoaded(mLayoutInflater, layoutResource);

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        if (contentParent == null) {
            throw new RuntimeException("Window couldn't find content container view");
        }

        if ((features & (1 << FEATURE_INDETERMINATE_PROGRESS)) != 0) {
            ProgressBar progress = getCircularProgressBar(false);
            if (progress != null) {
                progress.setIndeterminate(true);
            }
        }

        if ((features & (1 << FEATURE_SWIPE_TO_DISMISS)) != 0) {
            registerSwipeCallbacks();
        }

        // 给顶层窗口设置标题和背景
        if (getContainer() == null) {
            final Drawable background;
            if (mBackgroundResource != 0) {
                background = getContext().getDrawable(mBackgroundResource);
            } else {
                background = mBackgroundDrawable;
            }
            mDecor.setWindowBackground(background);

            final Drawable frame;
            if (mFrameResource != 0) {
                frame = getContext().getDrawable(mFrameResource);
            } else {
                frame = null;
            }
            mDecor.setWindowFrame(frame);

            mDecor.setElevation(mElevation);
            mDecor.setClipToOutline(mClipToOutline);

            if (mTitle != null) {
                setTitle(mTitle);
            }

            if (mTitleColor == 0) {
                mTitleColor = mTextColor;
            }
            setTitleColor(mTitleColor);
        }

        mDecor.finishChanging();

        return contentParent;
    }

代码较多,先通过getWindowStyle获取主题样式进行初始化,然后通过getLocalFeatures获取设置的不同features加载不同的布局,例如我们通常在Activity 加入requestWindowFeature(Window.FEATURE_NO_TITLE);来隐藏标题栏,不管根据Feature最终使用的是哪一种布局,里面都有一个android:id=”@android:id/content”的FrameLayout,我们的布局文件就添加到这个FrameLayout中了。我们看一下一个简单的布局

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:fitsSystemWindows="true">
    <!-- Popout bar for action modes -->
    <ViewStub android:id="@+id/action_mode_bar_stub"
              android:inflatedId="@+id/action_mode_bar"
              android:layout="@layout/action_mode_bar"
              android:layout_width="match_parent"
              android:layout_height="wrap_content"
              android:theme="?attr/actionBarTheme" />
    <FrameLayout
        android:layout_width="match_parent" 
        android:layout_height="?android:attr/windowTitleSize"
        style="?android:attr/windowTitleBackgroundStyle">
        <TextView android:id="@android:id/title" 
            style="?android:attr/windowTitleStyle"
            android:background="@null"
            android:fadingEdge="horizontal"
            android:gravity="center_vertical"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />
    </FrameLayout>
    <FrameLayout android:id="@android:id/content"
        android:layout_width="match_parent" 
        android:layout_height="0dip"
        android:layout_weight="1"
        android:foregroundGravity="fill_horizontal|top"
        android:foreground="?android:attr/windowContentOverlay" />
</LinearLayout>

通过上面的分析,你应该明白了requestWindowFeature为什么必须在setContentView之前设置了,如果在之后设置,那么通过上面的分析在setContentView执行时已经从本地读取features,而此时还没有设置,当然就无效了。

        ViewGroup contentParent = (ViewGroup)findViewById(ID_ANDROID_CONTENT);
        public static final int ID_ANDROID_CONTENT = com.android.internal.R.id.content;

通过上面findViewById获取该对象。不过在获取ViewGroup之前还有一个重要的方法

    void onResourcesLoaded(LayoutInflater inflater, int layoutResource) {
        mStackId = getStackId();

        if (mBackdropFrameRenderer != null) {
            loadBackgroundDrawablesIfNeeded();
            mBackdropFrameRenderer.onResourcesLoaded(
                    this, mResizingBackgroundDrawable, mCaptionBackgroundDrawable,
                    mUserCaptionBackgroundDrawable, getCurrentColor(mStatusColorViewState),
                    getCurrentColor(mNavigationColorViewState));
        }

        mDecorCaptionView = createDecorCaptionView(inflater);
        final View root = inflater.inflate(layoutResource, null);
        if (mDecorCaptionView != null) {
            if (mDecorCaptionView.getParent() == null) {
                addView(mDecorCaptionView,
                        new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
            }
            mDecorCaptionView.addView(root,
                    new ViewGroup.MarginLayoutParams(MATCH_PARENT, MATCH_PARENT));
        } else {

            // Put it below the color views.
            addView(root, 0, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
        }
        mContentRoot = (ViewGroup) root;
        initializeElevation();
    }

这个比较好理解,root就是在上面判断的根据不同的features,加载的布局,然后将该布局通过addView添加到DecorView.到这里初始都成功了.

 mLayoutInflater.inflate(layoutResID, mContentParent);

在回到最初setContentView中的一句代码,如上,我们也就好理解了,它就是将我们的布局文件inflate到mContentParent中。到这里Activity的加载布局文件就完毕了。

decor.png

AppCompatActivity的setContentView分析

由于AppCompatActivity的setContentView加载布局的与Activity有很多不同的地方,而且相对Activity稍微复杂点,在这里也简单分析一下。

    @Override
    public void setContentView(@LayoutRes int layoutResID) {
        getDelegate().setContentView(layoutResID);
    }

通过名字也就知道把加载布局交给了一个委托对象。

    @NonNull
    public AppCompatDelegate getDelegate() {
        if (mDelegate == null) {
            mDelegate = AppCompatDelegate.create(this, this);
        }
        return mDelegate;
    }

AppCompatDelegate时一个抽象类,如下图他有几个子类实现
1.png
为啥有那么多子类呢,其实通过名字我们也能猜到,是为了兼容。为了证明这点,我们看看create方法

    private static AppCompatDelegate create(Context context, Window window,
            AppCompatCallback callback) {
        final int sdk = Build.VERSION.SDK_INT;
        if (BuildCompat.isAtLeastN()) {
            return new AppCompatDelegateImplN(context, window, callback);
        } else if (sdk >= 23) {
            return new AppCompatDelegateImplV23(context, window, callback);
        } else if (sdk >= 14) {
            return new AppCompatDelegateImplV14(context, window, callback);
        } else if (sdk >= 11) {
            return new AppCompatDelegateImplV11(context, window, callback);
        } else {
            return new AppCompatDelegateImplV9(context, window, callback);
        }
    }

这里就很明显了,根据不同的API版本初始化不同的delegate。通过查看代码setContentView方法的实现是在AppCompatDelegateImplV9中

    @Override
    public void setContentView(int resId) {
        ensureSubDecor();
        ViewGroup contentParent = (ViewGroup) mSubDecor.findViewById(android.R.id.content);
        contentParent.removeAllViews();
        LayoutInflater.from(mContext).inflate(resId, contentParent);
        mOriginalWindowCallback.onContentChanged();
    }

有了分析Activity的加载经验,我们就很容易明白contentParent和Activity中的mContentParent是一个东东,ensureSubDecor就是初始mSubDecor,然后removeAllViews,再将我们的布局填充到contentParent中。最后执行回调。

    private void ensureSubDecor() {
        if (!mSubDecorInstalled) {
            mSubDecor = createSubDecor();
            //省略部分代码
            onSubDecorInstalled(mSubDecor);
        }
    }
 private ViewGroup createSubDecor() {
        TypedArray a = mContext.obtainStyledAttributes(R.styleable.AppCompatTheme);

        //如果哦们不设置置AppCompat主题会报错,就是在这个地方
        if (!a.hasValue(R.styleable.AppCompatTheme_windowActionBar)) {
            a.recycle();
            throw new IllegalStateException(
                    "You need to use a Theme.AppCompat theme (or descendant) with this activity.");
        }

       //省略..... 初始化一下属性
        ViewGroup subDecor = null;
  //PhtoWindowgetDecorView会调用installDecor,在Activity已经介绍过,主要工作就是初始化mDecor,mContentParent。
     mWindow.getDecorView();
     //省略
//根据设置加载不同的布局
        if (!mWindowNoTitle) {
            if (mIsFloating) {
                // If we're floating, inflate the dialog title decor
                subDecor = (ViewGroup) inflater.inflate(
                        R.layout.abc_dialog_title_material, null);

                // Floating windows can never have an action bar, reset the flags
                mHasActionBar = mOverlayActionBar = false;
            } else if (mHasActionBar) {
                /**
                 * This needs some explanation. As we can not use the android:theme attribute
                 * pre-L, we emulate it by manually creating a LayoutInflater using a
                 * ContextThemeWrapper pointing to actionBarTheme.
                 */
                TypedValue outValue = new TypedValue();
                mContext.getTheme().resolveAttribute(R.attr.actionBarTheme, outValue, true);

                Context themedContext;
                if (outValue.resourceId != 0) {
                    themedContext = new ContextThemeWrapper(mContext, outValue.resourceId);
                } else {
                    themedContext = mContext;
                }

                // Now inflate the view using the themed context and set it as the content view
                subDecor = (ViewGroup) LayoutInflater.from(themedContext)
                        .inflate(R.layout.abc_screen_toolbar, null);

                mDecorContentParent = (DecorContentParent) subDecor
                        .findViewById(R.id.decor_content_parent);
                mDecorContentParent.setWindowCallback(getWindowCallback());

                /**
                 * Propagate features to DecorContentParent
                 */
                if (mOverlayActionBar) {
                    mDecorContentParent.initFeature(FEATURE_SUPPORT_ACTION_BAR_OVERLAY);
                }
                if (mFeatureProgress) {
                    mDecorContentParent.initFeature(Window.FEATURE_PROGRESS);
                }
                if (mFeatureIndeterminateProgress) {
                    mDecorContentParent.initFeature(Window.FEATURE_INDETERMINATE_PROGRESS);
                }
            }
        } else {
            if (mOverlayActionMode) {
                subDecor = (ViewGroup) inflater.inflate(
                        R.layout.abc_screen_simple_overlay_action_mode, null);
            } else {
                subDecor = (ViewGroup) inflater.inflate(R.layout.abc_screen_simple, null);
            }

            if (Build.VERSION.SDK_INT >= 21) {
                // If we're running on L or above, we can rely on ViewCompat's
                // setOnApplyWindowInsetsListener
                ViewCompat.setOnApplyWindowInsetsListener(subDecor,
                        new OnApplyWindowInsetsListener() {
                            @Override
                            public WindowInsetsCompat onApplyWindowInsets(View v,
                                    WindowInsetsCompat insets) {
                                final int top = insets.getSystemWindowInsetTop();
                                final int newTop = updateStatusGuard(top);

                                if (top != newTop) {
                                    insets = insets.replaceSystemWindowInsets(
                                            insets.getSystemWindowInsetLeft(),
                                            newTop,
                                            insets.getSystemWindowInsetRight(),
                                            insets.getSystemWindowInsetBottom());
                                }

                                // Now apply the insets on our view
                                return ViewCompat.onApplyWindowInsets(v, insets);
                            }
                        });
            } else {
                // Else, we need to use our own FitWindowsViewGroup handling
                ((FitWindowsViewGroup) subDecor).setOnFitSystemWindowsListener(
                        new FitWindowsViewGroup.OnFitSystemWindowsListener() {
                            @Override
                            public void onFitSystemWindows(Rect insets) {
                                insets.top = updateStatusGuard(insets.top);
                            }
                        });
            }
        }

        if (subDecor == null) {
            throw new IllegalArgumentException(
                    "AppCompat does not support the current theme features: { "
                            + "windowActionBar: " + mHasActionBar
                            + ", windowActionBarOverlay: "+ mOverlayActionBar
                            + ", android:windowIsFloating: " + mIsFloating
                            + ", windowActionModeOverlay: " + mOverlayActionMode
                            + ", windowNoTitle: " + mWindowNoTitle
                            + " }");
        }

        if (mDecorContentParent == null) {
            mTitleView = (TextView) subDecor.findViewById(R.id.title);
        }

        // Make the decor optionally fit system windows, like the window's decor
        ViewUtils.makeOptionalFitsSystemWindows(subDecor);
        //contentView 是我们布局填充的地方
        final ContentFrameLayout contentView = (ContentFrameLayout) subDecor.findViewById(
                R.id.action_bar_activity_content);
      //这个就是和我们Activity中的介绍的mDecor层级中的mContentParent是一个东西,
        final ViewGroup windowContentView = (ViewGroup) mWindow.findViewById(android.R.id.content);
        if (windowContentView != null) {
            // There might be Views already added to the Window's content view so we need to
            // migrate them to our content view
            while (windowContentView.getChildCount() > 0) {
                final View child = windowContentView.getChildAt(0);
                windowContentView.removeViewAt(0);
                contentView.addView(child);
            }

            // Change our content FrameLayout to use the android.R.id.content id.
            // Useful for fragments.
            //清除windowContentView的id
            windowContentView.setId(View.NO_ID);
            //将contentView的id设置成android.R.id.content,在此我们应该明白了,contentView 就成为了Activity中的mContentParent,我们的布局加载到这个view中。
            contentView.setId(android.R.id.content);

            // The decorContent may have a foreground drawable set (windowContentOverlay).
            // Remove this as we handle it ourselves
            if (windowContentView instanceof FrameLayout) {
                ((FrameLayout) windowContentView).setForeground(null);
            }
        }

        // Now set the Window's content view with the decor
       //将subDecor 填充到DecorView中
        mWindow.setContentView(subDecor);

   //省略部分代码
        return subDecor;
    }

上面的处理逻辑就是先初始化一些主题样式,然后通过mWindow.getDecorView()初始化DecorView.和布局,然后createSubDecor根据主题加载不同的布局subDecor,通过findViewById获取contentView( AppCompat根据不同主题加载的布局中的View R.id.action_bar_activity_content)和windowContentView (
DecorView中的View android.R.id.content)控件。获取控件后将windowContentView 的id清空,并将 contentView的id由R.id.action_bar_activity_content更改为android.R.id.content。最后通过 mWindow.setContentView(subDecor);将subDecor添加到DecorView中。

//调用两个参数方法
 @Override
    public void setContentView(View view) {
        setContentView(view, new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
    }
//此处处理和在Activity中分析的setContentView传资源ID进行加载布局是一样的,不同的是此时mContentParent 不为空,先removeAllViews(无转场动画情况)后再直接执行mContentParent.addView(view, params);即将subDecor添加到mContentParent
    @Override
    public void setContentView(View view, ViewGroup.LayoutParams params) {
        // Note: FEATURE_CONTENT_TRANSITIONS may be set in the process of installing the window
        // decor, when theme attributes and the like are crystalized. Do not check the feature
        // before this happens.
        if (mContentParent == null) {
            installDecor();
        } else if (!hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            mContentParent.removeAllViews();
        }

        if (hasFeature(FEATURE_CONTENT_TRANSITIONS)) {
            view.setLayoutParams(params);
            final Scene newScene = new Scene(mContentParent, view);
            transitionTo(newScene);
        } else {
            mContentParent.addView(view, params);
        }
        mContentParent.requestApplyInsets();
        final Callback cb = getCallback();
        if (cb != null && !isDestroyed()) {
            cb.onContentChanged();
        }
        mContentParentExplicitlySet = true;
    }

关于subDecor到底是什么布局,我们随便看一个布局R.layout.abc_screen_toolbar,有标题(mWindowNoTitle为false)并且有ActionBar(mHasActionBar 为true)的情况加载的布局。

<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.ActionBarOverlayLayout
        xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:app="http://schemas.android.com/apk/res-auto"
        android:id="@+id/decor_content_parent"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:fitsSystemWindows="true">

    <include layout="@layout/abc_screen_content_include"/>

    <android.support.v7.widget.ActionBarContainer
            android:id="@+id/action_bar_container"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_alignParentTop="true"
            style="?attr/actionBarStyle"
            android:touchscreenBlocksFocus="true"
            android:gravity="top">

        <android.support.v7.widget.Toolbar
                android:id="@+id/action_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                app:navigationContentDescription="@string/abc_action_bar_up_description"
                style="?attr/toolbarStyle"/>

        <android.support.v7.widget.ActionBarContextView
                android:id="@+id/action_context_bar"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:visibility="gone"
                android:theme="?attr/actionBarTheme"
                style="?attr/actionModeStyle"/>

    </android.support.v7.widget.ActionBarContainer>

</android.support.v7.widget.ActionBarOverlayLayout>

不管哪个主题下的布局,都会有一个id 为 abc_screen_content_include最好将id更改为androd.R,content,然后添加到mDecor中的mContentParent中。我们可以同SDK中tools下hierarchyviewer工具查看我们的布局层级结构。例如我们AppCompatActivity中setContentView传入的布局文件,是一个线程布局,该布局下有一个Button,则查看到层级结构

hierarchy.png

到这里setContentView已经分析完毕,由于水平有限,难免有错误,若在阅读时发现不妥或者错误的地方留言指正,共同进步,谢谢,Have a wonderful day。

鸣谢
Wey Ye的Android走进Framework之AppCompatActivity.setContentView

作者:xiehuimx 发表于2017/6/5 8:44:38 原文链接
阅读:190 评论:0 查看评论

Android 动画:你真的会使用插值器与估值器吗?(含详细实例教学)

$
0
0

前言

  • 动画的使用 是 Android 开发中常用的知识
  • 可是动画的种类繁多、使用复杂,每当需要 采用自定义动画 实现 复杂的动画效果时,很多开发者就显得束手无策
  • Android中 补间动画 & 属性动画实现动画的原理是:

实现原理

  • 其中,步骤2中的 插值器(Interpolator)和估值器(TypeEvaluator)是实现 复杂动画效果的关键
  • 本文主要讲解 将详细讲解 插值器(Interpolator)和估值器(TypeEvaluator),通过阅读本文你将能轻松实现复杂的动画效果

学习Android 动画最好先了解:
1. 自定义View的原理,请参考我写的文章:
(1)自定义View基础 - 最易懂的自定义View原理系列
(2)自定义View Measure过程 - 最易懂的自定义View原理系列
(3)自定义View Layout过程 - 最易懂的自定义View原理系列
(4)自定义View Draw过程- 最易懂的自定义View原理系列
2. 自定义View的应用,请参考我写的文章:
手把手教你写一个完整的自定义View
Path类的最全面详解 - 自定义View应用系列
Canvas类的最全面详解 - 自定义View应用系列
为什么你的自定义View wrap_content不起作用?


目录

目录


1. 插值器(Interpolator)

1.1 简介

  • 定义:一个接口
  • 作用:设置 属性值 从初始值过渡到结束值 的变化规律
    1. 如匀速、加速 & 减速 等等
    2. 即确定了 动画效果变化的模式,如匀速变化、加速变化 等等

1.2 应用场景

实现非线性运动的动画效果

非线性运动:动画改变的速率不是一成不变的,如加速 & 减速运动都属于非线性运动

1.3 具体使用

a. 设置方式

插值器在动画的使用有两种方式:在XML / Java代码中设置:

设置方法1:在 动画效果的XML代码中设置插值器属性android:interpolator

<?xml version="1.0" encoding="utf-8"?>
<scale xmlns:android="http://schemas.android.com/apk/res/android"

    android:interpolator="@android:anim/overshoot_interpolator"
    // 通过资源ID设置插值器
    android:duration="3000"
    android:fromXScale="0.0"
    android:fromYScale="0.0"
    android:pivotX="50%"
    android:pivotY="50%"
    android:toXScale="2"
    android:toYScale="2" />

设置方法2:在 Java 代码中设置

Button mButton = (Button) findViewById(R.id.Button);
        // 步骤1:创建 需要设置动画的 视图View

Animation alphaAnimation = new AlphaAnimation(1,0);
        // 步骤2:创建透明度动画的对象 & 设置动画效果

        alphaAnimation.setDuration(3000);
        Interpolator overshootInterpolator = new OvershootInterpolator();
        // 步骤3:创建对应的插值器类对象

        alphaAnimation.setInterpolator(overshootInterpolator);
        // 步骤4:给动画设置插值器

        mButton.startAnimation(alphaAnimation);
        // 步骤5:播放动画
  • 那么使用插值器时的资源ID是什么呢?即有哪些类型的插值器可供我们使用呢?
  • 下面将介绍 Android内置默认的插值器

b. 系统内置插值器类型

  • Android内置了 9 种内置的插值器实现:
作用 资源ID 对应的Java类
动画加速进行 @android:anim/accelerate_interpolator AccelerateInterpolator
快速完成动画,超出再回到结束样式 @android:anim/overshoot_interpolator OvershootInterpolator
先加速再减速 @android:anim/accelerate_decelerate_interpolator AccelerateDecelerateInterpolator
先退后再加速前进 @android:anim/anticipate_interpolator AnticipateInterpolator
先退后再加速前进,超出终点后再回终点 @android:anim/anticipate_overshoot_interpolator AnticipateOvershootInterpolator
最后阶段弹球效果 @android:anim/bounce_interpolator BounceInterpolator
周期运动 @android:anim/cycle_interpolator CycleInterpolator
减速 @android:anim/decelerate_interpolator DecelerateInterpolator
匀速 @android:anim/linear_interpolator LinearInterpolator

使用时:

  • 当在XML文件设置插值器时,只需传入对应的插值器资源ID即可
  • 当在Java代码设置插值器时,只需创建对应的插值器对象即可

    系统默认的插值器是AccelerateDecelerateInterpolator,即先加速后减速

  • 系统内置插值器的效果图:
    效果图

  • 使用Android内置的插值器能满足大多数的动画需求

  • 如果上述9个插值器无法满足需求,还可以自定义插值器
  • 下面将介绍如何自定义插值器(Interpolator

c. 自定义插值器

  • 本质:根据动画的进度(0%-100%)计算出当前属性值改变的百分比
  • 具体使用:自定义插值器需要实现 Interpolator / TimeInterpolator接口 & 复写getInterpolation()
    1. 补间动画 实现 Interpolator接口;属性动画实现TimeInterpolator接口
    2. TimeInterpolator接口是属性动画中新增的,用于兼容Interpolator接口,这使得所有过去的Interpolator实现类都可以直接在属性动画使用

// Interpolator接口
public interface Interpolator {  

    // 内部只有一个方法
     float getInterpolation(float input) {  
         // 参数说明
         // input值值变化范围是0-1,且随着动画进度(0% - 100% )均匀变化
        // 即动画开始时,input值 = 0;动画结束时input = 1
        // 而中间的值则是随着动画的进度(0% - 100%)在0到1之间均匀增加

      ...// 插值器的计算逻辑

      return xxx;
      // 返回的值就是用于估值器继续计算的fraction值,下面会详细说明
    }  

// TimeInterpolator接口
// 同上
public interface TimeInterpolator {  

    float getInterpolation(float input);  

}  

在学习自定义插值器前,我们先来看两个已经实现好的系统内置差值器:

  • 匀速插值器:LinearInterpolator
  • 先加速再减速 插值器:AccelerateDecelerateInterpolator
// 匀速差值器:LinearInterpolator
@HasNativeInterpolator  
public class LinearInterpolator extends BaseInterpolator implements NativeInterpolatorFactory {  
   // 仅贴出关键代码
  ...
    public float getInterpolation(float input) {  
        return input;  
        // 没有对input值进行任何逻辑处理,直接返回
        // 即input值 = fraction值
        // 因为input值是匀速增加的,因此fraction值也是匀速增加的,所以动画的运动情况也是匀速的,所以是匀速插值器
    }  


// 先加速再减速 差值器:AccelerateDecelerateInterpolator
@HasNativeInterpolator  
public class AccelerateDecelerateInterpolator implements Interpolator, NativeInterpolatorFactory {  
      // 仅贴出关键代码
  ...
    public float getInterpolation(float input) {  
        return (float)(Math.cos((input + 1) * Math.PI) / 2.0f) + 0.5f;
        // input的运算逻辑如下:
        // 使用了余弦函数,因input的取值范围是0到1,那么cos函数中的取值范围就是π到2π。
        // 而cos(π)的结果是-1,cos(2π)的结果是1
        // 所以该值除以2加上0.5后,getInterpolation()方法最终返回的结果值还是在0到1之间。只不过经过了余弦运算之后,最终的结果不再是匀速增加的了,而是经历了一个先加速后减速的过程
        // 所以最终,fraction值 = 运算后的值 = 先加速后减速
        // 所以该差值器是先加速再减速的
    }  


    }

  • 从上面看出,自定义插值器的关键在于:对input值 根据动画的进度(0%-100%)通过逻辑计算 计算出当前属性值改变的百分比
  • 下面我将用一个实例来说明该如何自定义插值器

实例

  • 目的:写一个自定义Interpolator:先减速后加速

步骤1:根据需求实现Interpolator接口
DecelerateAccelerateInterpolator.java

/**
 * Created by Carson_Ho on 17/4/19.
 */

public class DecelerateAccelerateInterpolator implements TimeInterpolator {

    @Override
    public float getInterpolation(float input) {
        float result;
        if (input <= 0.5) {
            result = (float) (Math.sin(Math.PI * input)) / 2;
            // 使用正弦函数来实现先减速后加速的功能,逻辑如下:
            // 因为正弦函数初始弧度变化值非常大,刚好和余弦函数是相反的
            // 随着弧度的增加,正弦函数的变化值也会逐渐变小,这样也就实现了减速的效果。
            // 当弧度大于π/2之后,整个过程相反了过来,现在正弦函数的弧度变化值非常小,渐渐随着弧度继续增加,变化值越来越大,弧度到π时结束,这样从0过度到π,也就实现了先减速后加速的效果
        } else {
            result = (float) (2 - Math.sin(Math.PI * input)) / 2;
        }
        return result;
        // 返回的result值 = 随着动画进度呈先减速后加速的变化趋势
    }

}

MainActivity.java

 mButton = (Button) findViewById(R.id.Button);
        // 创建动画作用对象:此处以Button为例

        float curTranslationX = mButton.getTranslationX();
        // 获得当前按钮的位置

        ObjectAnimator animator = ObjectAnimator.ofFloat(mButton, "translationX", curTranslationX, 300,curTranslationX);
        // 创建动画对象 & 设置动画
        // 表示的是:
        // 动画作用对象是mButton
        // 动画作用的对象的属性是X轴平移
        // 动画效果是:从当前位置平移到 x=1500 再平移到初始位置
        animator.setDuration(5000);
        animator.setInterpolator(new DecelerateAccelerateInterpolator());
        // 设置插值器
        animator.start();
        // 启动动画

效果图

差值器.gif


2. 估值器(TypeEvaluator)

2.1 简介

  • 定义:一个接口
  • 作用:设置 属性值 从初始值过渡到结束值 的变化具体数值
    1. 插值器(Interpolator)决定 值 的变化规律(匀速、加速blabla),即决定的是变化趋势;而接下来的具体变化数值则交给
      而估值器
    2. 属性动画特有的属性

2.2 应用场景

协助插值器 实现非线性运动的动画效果

非线性运动:动画改变的速率不是一成不变的,如加速 & 减速运动都属于非线性运动

2.3 具体使用

a. 设置方式

ObjectAnimator anim = ObjectAnimator.ofObject(myView2, "height", new Evaluator(),13);
// 在第4个参数中传入对应估值器类的对象
// 系统内置的估值器有3个:
// IntEvaluator:以整型的形式从初始值 - 结束值 进行过渡
// FloatEvaluator:以浮点型的形式从初始值 - 结束值 进行过渡
// ArgbEvaluator:以Argb类型的形式从初始值 - 结束值 进行过渡

效果图

FloatEvaluator
IntEvaluator

  • 如果上述内置的估值器无法满足需求,还可以自定义估值器
    下面将介绍如何自定义插值器(Interpolator)

b. 自定义估值器

  • 本质:根据 插值器计算出当前属性值改变的百分比 & 初始值 & 结束值 来计算 当前属性具体的数值

    如:动画进行了50%(初始值=100,结束值=200 ),那么匀速插值器计算出了当前属性值改变的百分比是50%,那么估值器则负责计算当前属性值 = 100 + (200-100)x50% = 150.

  • 具体使用:自定义估值器需要实现 TypeEvaluator接口 & 复写evaluate()

public interface TypeEvaluator {  

    public Object evaluate(float fraction, Object startValue, Object endValue) {  
// 参数说明
// fraction:插值器getInterpolation()的返回值
// startValue:动画的初始值
// endValue:动画的结束值

        ....// 估值器的计算逻辑

        return xxx;
        // 赋给动画属性的具体数值
        // 使用反射机制改变属性变化

// 特别注意
// 那么插值器的input值 和 估值器fraction有什么关系呢?
// 答:input的值决定了fraction的值:input值经过计算后传入到插值器的getInterpolation(),然后通过实现getInterpolation()中的逻辑算法,根据input值来计算出一个返回值,而这个返回值就是fraction了
    }  
}  

在学习自定义插值器前,我们先来看一个已经实现好的系统内置差值器:浮点型插值器:FloatEvaluator

public class FloatEvaluator implements TypeEvaluator {  
// FloatEvaluator实现了TypeEvaluator接口

// 重写evaluate()
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
// 参数说明
// fraction:表示动画完成度(根据它来计算当前动画的值)
// startValue、endValue:动画的初始值和结束值
        float startFloat = ((Number) startValue).floatValue();  

        return startFloat + fraction * (((Number) endValue).floatValue() - startFloat);  
        // 初始值 过渡 到结束值 的算法是:
        // 1. 用结束值减去初始值,算出它们之间的差值
        // 2. 用上述差值乘以fraction系数
        // 3. 再加上初始值,就得到当前动画的值
    }  
}  
  • 属性动画中的ValueAnimator.ofInt() & ValueAnimator.ofFloat()都具备系统内置的估值器,即FloatEvaluator & IntEvaluator
    即系统已经默认实现了 如何从初始值 过渡到 结束值 的逻辑
  • 但对于ValueAnimator.ofObject(),从上面的工作原理可以看出并没有系统默认实现,因为对对象的动画操作复杂 & 多样,系统无法知道如何从初始对象过度到结束对象
  • 因此,对于ValueAnimator.ofObject(),我们需自定义估值器(TypeEvaluator)来告知系统如何进行从 初始对象 过渡到 结束对象的逻辑
  • 自定义实现的逻辑如下
// 实现TypeEvaluator接口
public class ObjectEvaluator implements TypeEvaluator{  

// 复写evaluate()
// 在evaluate()里写入对象动画过渡的逻辑
    @Override  
    public Object evaluate(float fraction, Object startValue, Object endValue) {  
        // 参数说明
        // fraction:表示动画完成度(根据它来计算当前动画的值)
        // startValue、endValue:动画的初始值和结束值

        ... // 写入对象动画过渡的逻辑

        return value;  
        // 返回对象动画过渡的逻辑计算后的值
    }  

实例说明

  • 下面我将用实例说明 该如何自定义TypeEvaluator接口并通过ValueAnimator.ofObject()实现动画效果
  • 实现的动画效果:一个圆从一个点 移动到 另外一个点
    效果图

  • 工程目录文件如下:
    工程目录

步骤1:定义对象类

  • 因为ValueAnimator.ofObject()是面向对象操作的,所以需要自定义对象类。
  • 本例需要操作的对象是 圆的点坐标
    Point.java
public class Point {

    // 设置两个变量用于记录坐标的位置
    private float x;
    private float y;

    // 构造方法用于设置坐标
    public Point(float x, float y) {
        this.x = x;
        this.y = y;
    }

    // get方法用于获取坐标
    public float getX() {
        return x;
    }

    public float getY() {
        return y;
    }
}

步骤2:根据需求实现TypeEvaluator接口

  • 实现TypeEvaluator接口的目的是自定义如何 从初始点坐标 过渡 到结束点坐标;
  • 本例实现的是一个从左上角到右下角的坐标过渡逻辑。
    效果图

PointEvaluator.java

// 实现TypeEvaluator接口
public class PointEvaluator implements TypeEvaluator {

    // 复写evaluate()
    // 在evaluate()里写入对象动画过渡的逻辑
    @Override
    public Object evaluate(float fraction, Object startValue, Object endValue) {

        // 将动画初始值startValue 和 动画结束值endValue 强制类型转换成Point对象
        Point startPoint = (Point) startValue;
        Point endPoint = (Point) endValue;

        // 根据fraction来计算当前动画的x和y的值
        float x = startPoint.getX() + fraction * (endPoint.getX() - startPoint.getX());
        float y = startPoint.getY() + fraction * (endPoint.getY() - startPoint.getY());

        // 将计算后的坐标封装到一个新的Point对象中并返回
        Point point = new Point(x, y);
        return point;
    }

}
  • 上面步骤是根据需求自定义TypeEvaluator的实现
  • 下面将讲解如何通过对 Point 对象进行动画操作,从而实现整个自定义View的动画效果。

步骤3:将属性动画作用到自定义View当中

MyView.java

/**
 * Created by Carson_Ho on 17/4/18.
 */
public class MyView extends View {
    // 设置需要用到的变量
    public static final float RADIUS = 70f;// 圆的半径 = 70
    private Point currentPoint;// 当前点坐标
    private Paint mPaint;// 绘图画笔


    // 构造方法(初始化画笔)
    public MyView(Context context, AttributeSet attrs) {
        super(context, attrs);
        // 初始化画笔
        mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
        mPaint.setColor(Color.BLUE);
    }

    // 复写onDraw()从而实现绘制逻辑
    // 绘制逻辑:先在初始点画圆,通过监听当前坐标值(currentPoint)的变化,每次变化都调用onDraw()重新绘制圆,从而实现圆的平移动画效果
    @Override
    protected void onDraw(Canvas canvas) {
        // 如果当前点坐标为空(即第一次)
        if (currentPoint == null) {
            currentPoint = new Point(RADIUS, RADIUS);
            // 创建一个点对象(坐标是(70,70))

            // 在该点画一个圆:圆心 = (70,70),半径 = 70
            float x = currentPoint.getX();
            float y = currentPoint.getY();
            canvas.drawCircle(x, y, RADIUS, mPaint);


 // (重点关注)将属性动画作用到View中
            // 步骤1:创建初始动画时的对象点  & 结束动画时的对象点
            Point startPoint = new Point(RADIUS, RADIUS);// 初始点为圆心(70,70)
            Point endPoint = new Point(700, 1000);// 结束点为(700,1000)

            // 步骤2:创建动画对象 & 设置初始值 和 结束值
            ValueAnimator anim = ValueAnimator.ofObject(new PointEvaluator(), startPoint, endPoint);
            // 参数说明
            // 参数1:TypeEvaluator 类型参数 - 使用自定义的PointEvaluator(实现了TypeEvaluator接口)
            // 参数2:初始动画的对象点
            // 参数3:结束动画的对象点

            // 步骤3:设置动画参数
            anim.setDuration(5000);
            // 设置动画时长

// 步骤3:通过 值 的更新监听器,将改变的对象手动赋值给当前对象
// 此处是将 改变后的坐标值对象 赋给 当前的坐标值对象
            // 设置 值的更新监听器
            // 即每当坐标值(Point对象)更新一次,该方法就会被调用一次
            anim.addUpdateListener(new ValueAnimator.AnimatorUpdateListener() {
                @Override
                public void onAnimationUpdate(ValueAnimator animation) {
                    currentPoint = (Point) animation.getAnimatedValue();
                    // 将每次变化后的坐标值(估值器PointEvaluator中evaluate()返回的Piont对象值)到当前坐标值对象(currentPoint)
                    // 从而更新当前坐标值(currentPoint)

// 步骤4:每次赋值后就重新绘制,从而实现动画效果
                    invalidate();
                    // 调用invalidate()后,就会刷新View,即才能看到重新绘制的界面,即onDraw()会被重新调用一次
                    // 所以坐标值每改变一次,就会调用onDraw()一次
                }
            });

            anim.start();
            // 启动动画


        } else {
            // 如果坐标值不为0,则画圆
            // 所以坐标值每改变一次,就会调用onDraw()一次,就会画一次圆,从而实现动画效果

            // 在该点画一个圆:圆心 = (30,30),半径 = 30
            float x = currentPoint.getX();
            float y = currentPoint.getY();
            canvas.drawCircle(x, y, RADIUS, mPaint);
        }
    }


}

步骤4:在布局文件加入自定义View空间

activity_main.xml

<?xml version="1.0" encoding="utf-8"?>
<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.valueanimator_ofobject.MainActivity">

    <scut.carson_ho.valueanimator_ofobject.MyView
        android:layout_width="match_parent"
        android:layout_height="match_parent"
         />
</RelativeLayout>

步骤5:在主代码文件设置显示视图

MainActivity.java

public class MainActivity extends AppCompatActivity {

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

效果图

效果图

源码地址

Carson_Ho的Github地址


3. 总结


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

作者:carson_ho 发表于2017/6/5 8:59:37 原文链接
阅读:153 评论:0 查看评论

Android开发笔记(一百四十六)仿支付宝的支付密码输入框

$
0
0
编辑框EditText算是Android的一个基础控件了,表面上看,EditText只负责接收用户手工输入的文本;可实际上,要把这看似简单的文本输入做得方便易用,并不是一个简单的事情。因为用户可能希望App会更加智能一些,比如用户希望编辑框提供关键词联想功能,又比如用户希望编辑框能够自我纠错等等;所以,Android从设计之初就努力尝试解决这些问题,先是自带了自动完成编辑框AutoCompleteTextView,后来又在Android5.0以后提供了文本输入布局TextInputLayout。

然而,计划赶不上变化,开发工作中总有一些现有控件无法直接实现的需求,就像支付宝的支付密码输入框,在一排方格区域内输入并显示密文密码,每个密文字符之间又有竖线分隔。为直观理解支付密码输入框的业务需求,下面还是先看看该输入框的最终效果图。


从图中可以看出,这个支付密码输入框由六个方格组成,每个方格输入并显示第几位的密文字符。可是单张静态截图无法准确体现支付密码输入框的具体功能,因此我们再来看看使用该输入框的完整操作流程,相关动图如下所示。


由这张动图可以发现,支付密码输入框至少需要完成以下功能:
1、一开始边框是灰色的,获得焦点后边框变蓝色;
2、输入框一共六个方格,每个方格之间以竖线隔开;
3、每个方格只显示一个密码字符,且字符位于方格中央;
4、密码不显示明文,而是显示密文,比如点号(·)或者星号(*);
5、输完六位密码,应自动触发密码输入完成的事件;

因为支付密码允许一位一位输入,也允许一位一位删除,所以它本质上还是一个编辑框,也就是说,支付密码的输入框必须实现EditText的功能。当然,在界面展现上,需要以横排方格的形式加以显示。于是可以考虑,把支付密码的输入与显示操作分离开来,即密码输入操作仍由EditText处理,而密码显示操作则由自定义的方格布局接管。

对于处理密码输入的EditText来说,需要实现以下几项操作:
1、把默认的下划线背景替换为圆角背景,且支持在获得焦点时高亮显示;
2、屏蔽输入光标,可调用setCursorVisible方法设置为不可见;
3、把输入文字变成不可见,这里建议把文字颜色设为透明,而不是把文字大小设为0,因为若将大小设为0就无法自适应高度;
4、设置输入字符串的长度为6,设置长度操作可调用setFilters方法;
5、添加文本变更监听器,每当密码输入或者删除之时,就通知方格布局更新密文显示;同时还得监控输入字符数是否达到6位,如果达到6位就触发密码完成事件;

对于接管密码显示的方格布局来说,需要实现以下几项操作:
1、建立一个密码文本队列,队列长度为6;
2、每项密码文本控件都是一个TextView,文字居中对齐;
3、往布局上添加TextView队列时,在相邻的TextView之间要添加一条竖线,也就是宽度为1的灰色View;
4、依据转换规则,决定当前显示明文还是密文;如果是密文,则显示哪个密文字符;
5、每当EditText里的文本发生变更之时,相应更新TextView队列的各项文本显示;

上述的改造内容,大部分都有可以直接调用的函数,但有两个功能的实现要特别注意:
首先,对于密文字符,Android默认显示点号(·),可显示星号(*)也很常见,那有没有办法把系统默认的点号替换为星号呢?
这个需求看起来很简单,只要强行给TextView队列调用setText方法即可,然而这不是安全的做法,因为它丢弃了CharSequence中的丰富信息。正确的做法是调用setTransformationMethod方法,给TextView设置转换方式。恰好系统提供了一个字符替换的转换方式类即HideReturnsTransformationMethod,该类的关键代码如下所示:
    private static char[] ORIGINAL = new char[] { '\r' };
    private static char[] REPLACEMENT = new char[] { '\uFEFF' };

    protected char[] getOriginal() {
        return ORIGINAL;
    }

    protected char[] getReplacement() {
        return REPLACEMENT;
    }
这几行代码的意思是,把回车符('\r')替换为Unicode编码的空格('\uFEFF'),其中getOriginal表示返回需要替换的字符列表,getReplacement表示返回替换后的字符列表。所以,若想把密码文本替换成点号或者星号,即可依样画葫芦,把数字字符('0'到'9')替换为'\u2022'(点号的Unicode编码)或者'\u002A'(星号的Unicode编码)。

其次,对于支付密码输入框的焦点获得问题,因为该输入框内部集成了EditText,所以不管是给输入框注册点击事件还是触摸事件,手势焦点都会被内部的EditText所抢占,使得密码输入框反而不会响应点击和触摸事件。详细的事件处理机制限于篇幅不再叙述,这里直接给出具体的解决步骤:
1、重写支付密码输入框布局的onInterceptTouchEvent方法,对所有触摸事件予以拦截,不让触摸事件传递给下级视图,代码如下所示:
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		return true;
	}
2、给支付密码输入框以及其它编辑框控件注册触摸监听器,并对触摸动作进行处理,在触摸密码输入框时强行使之获得焦点,处理触摸动作的代码如下所示:
	public boolean onTouch(View v, MotionEvent event) {
		if (v.getId() == R.id.et_account) {
			et_account.setCursorVisible(true);
		} else if (v.getId() == R.id.ppi_password) {
			et_account.setCursorVisible(false);
			et_account.clearFocus();
			ppi_password.requestFocus();
		}
		return false;
	}
如此改进之后,本文开头的支付密码输入框也就具备了应有的输入和显示功能。

下面是支付密码输入框控件的完整代码:
public class PayPasswodInput extends RelativeLayout implements TextWatcher {
	private final static String TAG = "PayPasswodInput";
	private Context mContext;
	private EditText mEditText; // 文本编辑框,实际看不见
	private LinearLayout mShowLayout; // 真正显示着的文本区域
	private TextView[] mTextViews; // 分隔开的密码框
	private int mBorderColor = Color.GRAY; // 边框与分隔线颜色
	private int mPasswordColor = Color.BLACK; // 密码文字颜色
	private int mPasswordSize = 30; // 密码文字大小
	private int mPasswordLength = 6; // 密码长度
	private TransformationMethod mPasswordMethod; // 密码的显示方式
	private int mSplitWidth; // 分隔线的宽度

	public PayPasswodInput(Context context) {
		this(context, null);
	}

	public PayPasswodInput(Context context, AttributeSet attrs) {
		this(context, attrs, 0);
	}

	public PayPasswodInput(Context context, AttributeSet attrs, int defStyleAttr) {
		super(context, attrs, defStyleAttr);
		mContext = context;
		mBorderColor = mContext.getResources().getColor(R.color.gray);
		mSplitWidth = Utils.dp2px(mContext, 1);
		mPasswordMethod = HideReturnsTransformationMethod.getInstance();
	}

	public void setPasswordStyle(int pwd_color, int pwd_size, int pwd_length, 
			boolean pwd_show, int pwd_type) {
		mPasswordColor = pwd_color;
		mPasswordSize = pwd_size;
		mPasswordLength = pwd_length;
		mPasswordMethod = pwd_show ? 
				HideReturnsTransformationMethod.getInstance() : //明文密码
					StarTransformationMethod.getInstance(pwd_type); //密文密码
		removeAllViews();
		showTextLayout();
	}

	private void showTextLayout() {
		LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT,
				ViewGroup.LayoutParams.WRAP_CONTENT);
		// 添加看不见的编辑框
		mEditText = new EditText(mContext);
		mEditText.setBackgroundResource(R.drawable.editext_selector);
		mEditText.setCursorVisible(false);
		mEditText.setTextSize(mPasswordSize);
		mEditText.setTextColor(Color.TRANSPARENT);
		// 设置最大长度
		mEditText.setFilters(new InputFilter[] { new InputFilter.LengthFilter(mPasswordLength) });
		mEditText.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD
				| InputType.TYPE_CLASS_NUMBER);
		mEditText.addTextChangedListener(this);
		addView(mEditText, layoutParams);

		// 添加可见的密码框布局
		mShowLayout = new LinearLayout(mContext);
		mShowLayout.setLayoutParams(layoutParams);
		mShowLayout.setGravity(Gravity.CENTER);
		mShowLayout.setOrientation(LinearLayout.HORIZONTAL);
		addView(mShowLayout);

		// 添加密码文本队列
		mTextViews = new TextView[mPasswordLength];
		LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
				0, LayoutParams.WRAP_CONTENT, 1);
		textParams.gravity = Gravity.CENTER;
		LinearLayout.LayoutParams splitParams = new LinearLayout.LayoutParams(
				mSplitWidth, LayoutParams.MATCH_PARENT);
		for (int i = 0; i < mTextViews.length; i++) {
			TextView textView = new TextView(mContext);
			textView.setLayoutParams(textParams);
			textView.setGravity(Gravity.CENTER);
			textView.setTextSize(mPasswordSize);
			textView.setTextColor(mPasswordColor);
			textView.setInputType(InputType.TYPE_NUMBER_VARIATION_PASSWORD
					| InputType.TYPE_CLASS_NUMBER);
			textView.setTransformationMethod(mPasswordMethod);
			textView.setPadding(0, Utils.dp2px(mContext, 5), 0, 0);
			mTextViews[i] = textView;
			mShowLayout.addView(mTextViews[i]);
			if (i < mTextViews.length - 1) {
				View view = new View(mContext);
				view.setBackgroundColor(mBorderColor);
				mShowLayout.addView(view, splitParams);
			}
		}
	}

	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		return true;
	}

	@Override
	public void beforeTextChanged(CharSequence s, int start, int count, int after) {
		Editable edit = mEditText.getText();
		Selection.setSelection(edit, edit.length());
	}

	@Override
	public void onTextChanged(CharSequence s, int start, int before, int count) {
	}

	@Override
	public void afterTextChanged(Editable s) {
		if (s.length() > 0) {
			int length = s.length();
			for (int i = 0; i < mPasswordLength; i++) {
				if (i < length) {
					for (int j = 0; j < length; j++) {
						char ch = s.charAt(j);
						mTextViews[j].setText(String.valueOf(ch));
					}
				} else {
					mTextViews[i].setText("");
				}
			}
		} else {
			for (int i = 0; i < mPasswordLength; i++) {
				mTextViews[i].setText("");
			}
		}
		if (s.length() == mPasswordLength) {
			if (onPasswordFinishListener != null) {
				onPasswordFinishListener.onFinishPassword(s.toString().trim());
			}
		}
	}

	private OnPasswordFinishListener onPasswordFinishListener;
	public void setOnPasswordFinishListener(OnPasswordFinishListener listener) {
		onPasswordFinishListener = listener;
		if (mEditText == null) {
			showTextLayout();
		}
	}

	public interface OnPasswordFinishListener {
		void onFinishPassword(String password);
	}

}


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

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

Flutter进阶—网络和HTTP

$
0
0

使用http包

Flutter支持http包,版本0.11.3+12或更高版本,首先在pubspec.yaml中声明对http的依赖,注意添加声明后按顶部的“Packages get”:

dependencies:
  flutter:
    sdk: flutter
  http: '>=0.11.3+12'

发出HTTP请求

接下来,创建一个HTTP客户端(Client),我们建议使用createHttpClient来启用测试以提供http.MockClient

import 'package:flutter/services.dart';

var httpClient = createHttpClient();

客户端支持常见的HTTP操作,比如:

  • HTTP GET:使用get获取一般的请求,read返回字符串的请求或返回字节的请求的readbytes

  • HTTP POST:使用post作为一般的的post。

演示代码:

postData() async {
  ...
  var response = await httpClient.post(url, body: {'name': 'doodle', 'color': 'blue'});
  print('Response status: ${response.statusCode}');
}

需要注意的是,HTTP API在返回值中使用Dart Futures,我们建议您使用具有async/await语法的API调用,比如上面的演示代码。

解码和编码JSON

支持解码和编码JSON的功能由dart:convert库提供,解码JSON字符串并将响应解析为Map:

Map data = JSON.decode(response.body);
/*
假设响应内容是这样的:['foo', { 'bar': 499 }]
barValue设置为499
 */
int barValue = data[1]['bar'];

要对JSON进行编码,要将一个简单的值(字符串,布尔值或数字文字)或Map、List或包含简单值的Map列表传递给encode方法:

String encodedString = JSON.encode([1, 2, { 'a': null }]);

演示实例

这个实例演示了如何在Flutter应用程序中从HTTPS GET调用中解码JSON,它调用httpbin.com的Web服务测试API,然后响应您的本地IP地址。请注意,使用安全网络(HTTPS)。

首先添加http依赖关系,然后再将lib/main.dart的内容替换为以下内容:

import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(new MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return new MaterialApp(
      title: 'Flutter Demo',
      home: new MyHomePage(),
    );
  }
}

class MyHomePage extends StatefulWidget {
  MyHomePage({Key key}) : super(key: key);

  @override
  _MyHomePageState createState() => new _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage> {
  String _ipAddress = "未知";

  _getIPAddress() async {
    String url = 'https://httpbin.org/ip';
    var httpClient = createHttpClient();
    var response = await httpClient.read(url);
    Map data = JSON.decode(response);
    String ip = data['origin'];

    /*
    bool mounted
    这个状态对象当前是否在树中。
    用于此处,如果控件在数据正在请求时从树中删除,则我们要丢弃该数据,而不是调用setState来更新实际不存在的内容。
     */
    if(!mounted) return;

    setState((){
      _ipAddress = ip;
    });
  }

  @override
  Widget build(BuildContext context) {
    var spacer = new SizedBox(height: 32.0);

    return new Scaffold(
      body: new Center(
        child: new Column(
          children: <Widget> [
            spacer,
            new Text('您当前的IP地址是:'),
            new Text('$_ipAddress'),
            spacer,
            new RaisedButton(
              onPressed: _getIPAddress,
              child: new Text('获取IP地址'),
            )
          ]
        )
      )
    );
  }
}
作者:hekaiyou 发表于2017/6/5 12:11:18 原文链接
阅读:27 评论:0 查看评论

【stm32f407】SysTick实现延时

$
0
0

一.  SysTick介绍:

CM4内核的处理和CM3一样,内部都包含了一个SysTick定时器,SysTick 是一个24 位的倒计数定时器,当计到0 时 ,将 从RELOAD 寄存器中自动重装载定时初值。只要不把它在SysTick 控制及状态寄存器中的使能位清除,就永不停息。我们就是利用STM32的内部SysTick来实现延时的,这样既不占用中断,也不占用系统定时器

       通常SysTick可以通过中断的方式来实现,后续会增加,但是目前只是通过轮询的方式去实现

二.  寄存器介绍

SysTick4个寄存器

对应的代码在core_cm4.h

typedefstruct
{
  __IO uint32_t CTRL;                    /*!< Offset: 0x000(R/W)  SysTick Control and StatusRegister */
  __IO uint32_t LOAD;                    /*!< Offset: 0x004(R/W)  SysTick Reload Value Register       */
  __IO uint32_t VAL;                     /*!< Offset: 0x008(R/W)  SysTick Current ValueRegister      */
  __I uint32_t CALIB;                  /*!< Offset: 0x00C (R/ ) SysTick Calibration Register       */
} SysTick_Type; 

1) CTR寄存器如图:

0位:ENABLESystick 使能位  0:关闭Systick功能;1:开启Systick功能)
1位:TICKINTSystick 中断使能位    0:关闭Systick中断;1:开启Systick中断)

2位:CLKSOURCESystick时钟源选择  0:使用HCLK/8 作为Systick时钟;1:使用HCLK作为Systick时钟)

16位:COUNTFLAGSystick计数比较标志,如果在上次读取本寄存器后,SysTick 已经数到了0,则该位为1。如果读取该位,该位将自动清零.

2) LOAD寄存器如图:

Systick是一个递减的定时器,当定时器递减至0时,重载寄存器中的值就会被重装载,继续开始递减。STK_LOAD 重载寄存器是个24位的寄存器最大计数0xFFFFFF

3) VAL寄存器如图:

也是个24位的寄存器,读取时返回当前倒计数的值,写它则使之清零,同时还会清除在SysTick 控制及状态寄存器中的COUNTFLAG 标志。

4) CALIB寄存器如图

一般不会用到

三.  源码

delay.h

#ifndef _DELAY_H_H_H
#define _DELAY_H_H_H
#include "stm32f4xx.h"

void delay_init(u8 SYSCLK);
void delay_ms(u16 nms);
void delay_us(u32 nus);
#endif

delay.c

#include "delay.h"

static u8  fac_us=0;		   
static u16 fac_ms=0;
void delay_init(u8 SYSCLK)
{
  SysTick->CTRL&=~(1<<2);
  fac_us=SYSCLK/8;
  fac_ms=((u32)SYSCLK*1000)/8;
}
void delay_xms(u16 nms)
{	 		  	  
  u32 temp;		   
  SysTick->LOAD=(u32)nms*fac_ms;
  SysTick->VAL =0x00;
  SysTick->CTRL=0x01 ;
  do
  {
    temp=SysTick->CTRL;
  }while((temp&0x01)&&!(temp&(1<<16)));
  SysTick->CTRL=0x00;
  SysTick->VAL =0X00;	  	    
} 

void delay_ms(u16 nms)
{
  u8 repeat=nms/540;
  u16 remain=nms%540;
  while(repeat)
  {
    delay_xms(540);
    repeat--;
  }
  if(remain)delay_xms(remain);
  
}
void delay_us(u32 nus)
{
  u32 temp;	    	 
  SysTick->LOAD=nus*fac_us;  		 
  SysTick->VAL=0x00;
  SysTick->CTRL=0x01 ;	 
  do
  {
    temp=SysTick->CTRL;
  }while((temp&0x01)&&!(temp&(1<<16)));
  SysTick->CTRL=0x00;
  SysTick->VAL =0X00;  
}



作者:XiaoXiaoPengBo 发表于2017/6/5 12:21:55 原文链接
阅读:9 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


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