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

IOS端K线系列之K线-绘制蜡烛图

$
0
0

k线系列目录

查看目录请点击这儿


补充知识

K线虽然看上去复杂,但仔细观察就会知道几乎所有的k线无非由以下几种组成:

(1)蜡烛

蜡烛

(2)OHLC

OHLC

(3)线段

线段

(4)带状

带状

(5)圆形

圆形

除此之外还有其他的,都是比较少见的。

蜡烛绘制

补充了一点知识后,我们来进入主题。绘制上述所说的第一种,蜡烛。如果不懂什么是蜡烛,点这里补一下基础知识。

首先来分解蜡烛,其实它是由一个长方形加上两根线段(如果长方形是非空心的话,就是一根)。长方形是由开盘价坐标点、收盘价坐标点计算生成,线段是由最高价坐标点、最低价坐标点连接而成。

所以代码逻辑也就很简单了:

  1. 绘制一个长方形(实心)
  2. 然后再绘制一条线段

OK!上代码:

/**
 生成蜡烛Layer

 @param model 蜡烛坐标模型
 @return 返回layer
 */
+ (CAShapeLayer *)getCandleLayerWithPointModel:(YKCandlePointModel *)model
{
    //判断是否为涨跌
    BOOL isRed = model.oPoint.y >= model.cPoint.y ? YES : NO;

    //生成柱子的rect
    CGRect candleFrame = CGRectMake(isRed ? model.cPoint.x - 6 : model.oPoint.x-6,isRed ? model.cPoint.y : model.oPoint.y,12,ABS(model.oPoint.y - model.cPoint.y));

    UIBezierPath *path = [UIBezierPath bezierPathWithRect:candleFrame];

    //绘制上下影线
    [path moveToPoint:model.lPoint];
    [path addLineToPoint:model.hPoint];

    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.path = path.CGPath;

    //判断涨跌来设置颜色
    if (isRed)
    {
        //涨,设置红色
        layer.strokeColor = [UIColor redColor].CGColor;
        layer.fillColor = [UIColor redColor].CGColor;

    } else
    {
        //跌,设置绿色
        layer.strokeColor = [UIColor greenColor].CGColor;
        layer.fillColor = [UIColor greenColor].CGColor;
    }

    return layer;
}

代码效果如下:

蜡烛

最后把Demo献上,点这里下载。

作者:yunkai666 发表于2017/5/24 23:21:41 原文链接
阅读:153 评论:0 查看评论

最近小程序频繁搞事情,看他们都更新了哪些牛逼的功能

$
0
0

最近微信团队频繁在深夜放大招,戏称他们没有性生活。来看看他们都放了哪些大招吧:

1、微信内长按识别二维码可以打开小程序

更新日期:0414
用法:可以分享带有小程序二维码的图片到朋友圈,即一定程度上相当于可以分享小程序到朋友圈了。

2、小程序专用小程序码,功能同二维码

更新日期:0418
玩法:生成带有小程序标识的小程序码,更具辨识度。

3、公众号可以关联不同主体的小程序

更新日期:0420
玩法:可以用不同的公众号为你的小程序导流。

4、公众号文章内可以插入小程序

更新日期:0422
玩法:通过公众号文章为你的小程序导流。

5、不用开发,公众号就可以快速创建门店小程序

更新日期:0426
玩法:不用开发,商家就可以创建小程序了。

6、小程序分享在群聊中,点击打开后可以获取群ID和群名称

更新日期:0508
玩法:基于微信群的各种协作、游戏等。

7、附近的小程序

更新日期:0510
玩法:通过附近小程序这个入口导流,增加小程序的暴光度。

8、小程序数据助手

更新日期:0512
玩法:实时查看自己的小程序活跃数据。

9、小程序开放自定义转发功能

更新日期:0519
玩法:在你需要的位置自定义一个按钮,就可以实现一步转发了。

so,赶快去升级你的小程序吧!

作者:anda0109 发表于2017/5/25 8:45:12 原文链接
阅读:65 评论:0 查看评论

Android 热修复 Tinker Gradle Plugin解析

$
0
0

本文已在我的公众号hongyangAndroid原创首发。
转载请标明出处:
http://blog.csdn.net/lmj623565791/article/details/72667669
本文出自张鸿洋的博客

一、概述

前面写了两篇分析了tinker的loader部分源码以及dex diff/patch算法相关解析,那么为了保证完整性,最后一篇主要写tinker-patch-gradle-plugin相关了。

(距离看的时候已经快两个月了,再不写就忘了,赶紧记录下来)

注意:

本文基于1.7.7

前两篇文章分别为:

有兴趣的可以查看~

在介绍细节之前,我们可以先考虑下:通过一个命令生成一个patch文件,这个文件可以用于下发做热修复(可修复常规代码、资源等),那么第一反应是什么呢?

正常思维,需要设置oldApk,然后我这边build生成newApk,两者需要做diff,找出不同的代码、资源,通过特定的算法将diff出来的数据打成patch文件。

ok,的确是这样的,但是上述这个过程有什么需要注意的么?

  1. 我们在新增资源的时候,可能会因为我们新增的一个资源,导致非常多的资源id发生变化,如果这样直接进行diff,可能会导致资源错乱等(id指向了错误的图片)问题。所以应当保证,当资源改变或者新增、删除资源时,早已存在的资源的id不会发生变化。
  2. 我们在上线app的时候,会做代码混淆,如果没有做特殊的设置,每次混淆后的代码难以保证规则一致;所以,build过程中理论上需要设置混淆的mapping文件。
  3. 当项目比较大的时候,我们可能会遇到方法数超过65535的问题,我们很多时候会通过分包解决,这样就有主dex和其他dex的概念。集成了tinker之后,在应用的Application启动时会非常早的就去做tinker的load操作,所以就决定了load相关的类必须在主dex中。
  4. 在接入一些库的时候,往往还需要配置混淆,比如第三方库中哪些东西不能被混淆等(当然强制某些类在主dex中,也可能需要配置相对应的混淆规则)。

如果大家尝试过接入tinker并使用gradle的方式生成patch相关,会发现在需要在项目的build.gradle中,添加一些配置,这些配置中,会要求我们配置oldApk路径,资源的R.txt路径,混淆mapping文件路径、还有一些比较tinker相关的比较细致的配置信息等。

不过并没有要求我们显示去处理上述几个问题(并没有让你去keep混淆规则,主dex分包规则,以及apply mapping文件),所以上述的几个实际上都是tinker的gradle plugin 帮我们做了。

所以,本文将会以这些问题为线索来带大家走一圈plugin的代码(当然实际上tinker gradle plugin所做的事情远不止上述)。

其次,tinker gradle plugin也是非常好的gradle的学习资料~

二、寻找查看代码入口

下载tinker的代码,导入后,plugin的代码都在tinker-patch-gradle-plugin中,不过当然不能抱着代码一行一行去啃了,应该有个明确的入口,有条理的去阅读这些代码。

那么这个入口是什么呢?

其实很简单,我们在打patch的时候,需要执行tinkerPatchDebug(注:本篇博客基于debug模式讲解)。

当执行完后,将会看到执行过程包含以下流程:

:app:processDebugManifest
:app:tinkerProcessDebugManifest(tinker)
:app:tinkerProcessDebugResourceId (tinker)
:app:processDebugResources
:app:tinkerProguardConfigTask(tinker)
:app:transformClassesAndResourcesWithProguard
:app:tinkerProcessDebugMultidexKeep (tinker)
:app:transformClassesWidthMultidexlistForDebug
:app:assembleDebug
:app:tinkerPatchDebug(tinker)

注:包含(tinker)的都是tinker plugin 所添加的task

可以看到部分task加入到了build的流程中,那么这些task是如何加入到build过程中的呢?

在我们接入tinker之后,build.gradle中有如下代码:

if (buildWithTinker()) {
    apply plugin: 'com.tencent.tinker.patch'
    tinkerPatch {} // 各种参数
}

如果开启了tinker,会apply一个plugincom.tencent.tinker.patch

名称实际上就是properties文件的名字,该文件会对应具体的插件类。

对于gradle plugin不了解的,可以参考http://www.cnblogs.com/davenkin/p/gradle-learning-10.html,后面写会抽空单独写一篇详细讲gradle的文章。

下面看TinkerPatchPlugin,在apply方法中,里面大致有类似的代码:

// ... 省略了一堆代码
TinkerPatchSchemaTask tinkerPatchBuildTask 
        = project.tasks.create("tinkerPatch${variantName}", TinkerPatchSchemaTask)
tinkerPatchBuildTask.dependsOn variant.assemble

TinkerManifestTask manifestTask 
        = project.tasks.create("tinkerProcess${variantName}Manifest", TinkerManifestTask)
manifestTask.mustRunAfter variantOutput.processManifest
variantOutput.processResources.dependsOn manifestTask

TinkerResourceIdTask applyResourceTask 
        = project.tasks.create("tinkerProcess${variantName}ResourceId", TinkerResourceIdTask)
applyResourceTask.mustRunAfter manifestTask
variantOutput.processResources.dependsOn applyResourceTask

if (proguardEnable) {
    TinkerProguardConfigTask proguardConfigTask 
            = project.tasks.create("tinkerProcess${variantName}Proguard", TinkerProguardConfigTask)
    proguardConfigTask.mustRunAfter manifestTask

    def proguardTask = getProguardTask(project, variantName)
    if (proguardTask != null) {
        proguardTask.dependsOn proguardConfigTask
    }

}
if (multiDexEnabled) {
    TinkerMultidexConfigTask multidexConfigTask 
            = project.tasks.create("tinkerProcess${variantName}MultidexKeep", TinkerMultidexConfigTask)
    multidexConfigTask.mustRunAfter manifestTask

    def multidexTask = getMultiDexTask(project, variantName)
    if (multidexTask != null) {
        multidexTask.dependsOn multidexConfigTask
    }
}

可以看到它通过gradle Project API创建了5个task,通过dependsOn,mustRunAfter插入到了原本的流程中。

例如:

TinkerManifestTask manifestTask = ...
manifestTask.mustRunAfter variantOutput.processManifest
variantOutput.processResources.dependsOn manifestTask

TinkerManifestTask必须在processManifest之后执行,processResources在manifestTask后执行。

所以流程变为:

processManifest-> manifestTask-> processResources

其他同理。

ok,大致了解了这些task是如何注入的之后,接下来就看看每个task的具体作用吧。

注:如果我们有需求在build过程中搞事,可以参考上述task编写以及依赖方式的设置。

三、每个Task的具体行为

我们按照上述的流程来看,依次为:

TinkerManifestTask
TinkerResourceIdTask
TinkerProguardConfigTask
TinkerMultidexConfigTask
TinkerPatchSchemaTask

丢个图,对应下:

四、TinkerManifestTask

#TinkerManifestTask
@TaskAction
def updateManifest() {
    // Parse the AndroidManifest.xml
    String tinkerValue = project.extensions.tinkerPatch.buildConfig.tinkerId

    tinkerValue = TINKER_ID_PREFIX + tinkerValue;//"tinker_id_"

    // /build/intermediates/manifests/full/debug/AndroidManifest.xml
    writeManifestMeta(manifestPath, TINKER_ID, tinkerValue)

    addApplicationToLoaderPattern()
    File manifestFile = new File(manifestPath)
    if (manifestFile.exists()) {
        FileOperation.copyFileUsingStream(manifestFile, project.file(MANIFEST_XML))
    }
}

这里主要做了两件事:

  • writeManifestMeta主要就是解析AndroidManifest.xml,在<application>内部添加一个meta标签,value为tinkerValue。

    例如:

     <meta-data
            android:name="TINKER_ID"
            android:value="tinker_id_com.zhy.abc" />

这里不详细展开了,话说groovy解析XML真方便。

  • addApplicationToLoaderPattern主要是记录自己的application类名和tinker相关的一些load class com.tencent.tinker.loader.*,记录在project.extensions.tinkerPatch.dex.loader中。

最后copy修改后的AndroidManifest.xmlbuild/intermediates/tinker_intermediates/AndroidManifest.xml

这里我们需要想一下,在文初的分析中,并没有想到需要tinkerId这个东西,那么它到底是干嘛的呢?

看一下微信提供的参数说明,就明白了:

在运行过程中,我们需要验证基准apk包的tinkerId是否等于补丁包的tinkerId。这个是决定补丁包能运行在哪些基准包上面,一般来说我们可以使用git版本号、versionName等等。

想一下,在非强制升级的情况下,线上一般分布着各个版本的app。但是。你打patch肯定是对应某个版本,所以你要保证这个patch下发下去只影响对应的版本,不会对其他版本造成影响,所以你需要tinkerId与具体的版本相对应。

ok,下一个TinkerResourceIdTask。

五、TinkerResourceIdTask

文初提到,打patch的过程实际上要控制已有的资源id不能发生变化,这个task所做的事就是为此。

如果保证已有资源的id保持不变呢?

实际上需要public.xmlids.xml的参与,即预先在public.xml中的如下定义,在第二次打包之后可保持该资源对应的id值不变。

注:对xml文件的名称应该没有强要求。

<public type="id" name="search_button" id="0x7f0c0046" />

很多时候我们在搜索固化资源,一般都能看到通过public.xml去固化资源id,但是这里有个ids.xml是干嘛的呢?

下面这篇文章有个很好的解释~

http://blog.csdn.net/sbsujjbcy/article/details/52541803

首先需要生成public.xml,public.xml的生成通过aapt编译时添加-P参数生成。相关代码通过gradle插件去hook Task无缝加入该参数,有一点需要注意,通过appt生成的public.xml并不是可以直接用的,该文件中存在id类型的资源,生成patch时应用进去编译的时候会报resource is not defined,解决方法是将id类型型的资源单独记录到ids.xml文件中,相当于一个声明过程,编译的时候和public.xml一样,将ids.xml也参与编译即可。

ok,知道了public.xml和ids.xml的作用之后,需要再思考一下如何保证id不变?

首先我们在配置old apk的时候,会配置tinkerApplyResourcePath参数,该参数对应一个R.txt,里面的内容涵盖了所有old apk中资源对应的int值。

那么我们可以这么做,根据这个R.txt,把里面的数据写成public.xml不就能保证原本的资源对应的int值不变了么。

的确是这样的,不过tinker做了更多,不仅将old apk的中的资源信息写到public.xml,而且还干涉了新的资源,对新的资源按照资源id的生成规则,也分配的对应的int值,写到了public.xml,可以说该task包办了资源id的生成。

分析前的总结

好了,由于代码非常长,我决定在这个地方先用总结性的语言总结下,如果没有耐心看代码的可以直接跳过源码分析阶段:

首先将设置的old R.txt读取到内存中,转为:

  • 一个Map,key-value都代表一个具体资源信息;直接复用,不会生成新的资源信息。
  • 一个Map,key为资源类型,value为该类资源当前的最大int值;参与新的资源id的生成。

接下来遍历当前app中的资源,资源分为:

  • values文件夹下文件

对所有values相关文件夹下的文件已经处理完毕,大致的处理为:遍历文件中的节点,大致有item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction这些节点,将所有的节点按类型分类存储到rTypeResourceMap(key为资源类型,value为对应类型资源集合Set)中。

其中declare-styleable这个标签,主要读取其内部的attr标签,对attr标签对应的资源按上述处理。

  • res下非values文件夹

打开自己的项目有看一眼,除了values相关还有layout,anim,color等文件夹,主要分为两类:

一类是对 文件 即为资源,例如R.layout.xxx,R.drawable.xxx等;另一类为xml文档中以@+(去除@+android:id),其实就是找到我们自定义id节点,然后截取该节点的id值部分作为属性的名称(例如:@+id/tv,tv即为属性的名称)。

如果和设置的old apk中文件中相同name和type的节点不需要特殊处理,直接复用即可;如果不存在则需要生成新的typeId、resourceId等信息。

会将所有生成的资源都存到rTypeResourceMap中,最后写文件。

这样就基本收集到了所有的需要生成资源信息的所有的资源,最后写到public.xml即可。

总结性的语言难免有一些疏漏,实际以源码分析为标准。

开始源码分析

@TaskAction
def applyResourceId() {
     // 资源mapping文件
    String resourceMappingFile = project.extensions.tinkerPatch.buildConfig.applyResourceMapping

    // resDir /build/intermediates/res/merged/debug
    String idsXml = resDir + "/values/ids.xml";
    String publicXml = resDir + "/values/public.xml";
    FileOperation.deleteFile(idsXml);
    FileOperation.deleteFile(publicXml);

    List<String> resourceDirectoryList = new ArrayList<String>();
    // /build/intermediates/res/merged/debug
    resourceDirectoryList.add(resDir);

    project.logger.error("we build ${project.getName()} apk with apply resource mapping file ${resourceMappingFile}");

    project.extensions.tinkerPatch.buildConfig.usingResourceMapping = true;

    // 收集所有的资源,以type->type,name,id,int/int[]存储
    Map<RDotTxtEntry.RType, Set<RDotTxtEntry>> rTypeResourceMap = PatchUtil.readRTxt(resourceMappingFile);

    AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap);

    PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml);
    File publicFile = new File(publicXml);
    if (publicFile.exists()) {
        FileOperation.copyFileUsingStream(publicFile, project.file(RESOURCE_PUBLIC_XML));
        project.logger.error("tinker gen resource public.xml in ${RESOURCE_PUBLIC_XML}");
    }
    File idxFile = new File(idsXml);
    if (idxFile.exists()) {
        FileOperation.copyFileUsingStream(idxFile, project.file(RESOURCE_IDX_XML));
        project.logger.error("tinker gen resource idx.xml in ${RESOURCE_IDX_XML}");
    }
}

大体浏览下代码,可以看到首先检测是否设置了resource mapping文件,如果没有设置会直接跳过。并且最后的产物是public.xmlids.xml

因为生成patch时,需要保证两次打包已经存在的资源的id一致,需要public.xmlids.xml的参与。

首先清理已经存在的public.xmlids.xml,然后通过PatchUtil.readRTxt读取resourceMappingFile(参数中设置的),该文件记录的格式如下:

int anim abc_slide_in_bottom 0x7f050006
int id useLogo 0x7f0b0012
int[] styleable AppCompatImageView { 0x01010119, 0x7f010027 }
int styleable AppCompatImageView_android_src 0
int styleable AppCompatImageView_srcCompat 1

大概有两类,一类是int型各种资源;一类是int[]数组,代表styleable,其后面紧跟着它的item(熟悉自定义View的一定不陌生)。

PatchUtil.readRTxt的代码就不贴了,简单描述下:

首先正则按行匹配,每行分为四部分,即idType,rType,name,idValue(四个属性为RDotTxtEntry的成员变量)。

  • idType有两种INTINT_ARRAY
  • rType包含各种资源:

ANIM, ANIMATOR, ARRAY, ATTR, BOOL, COLOR, DIMEN, DRAWABLE, FRACTION,
ID, INTEGER, INTERPOLATOR, LAYOUT, MENU, MIPMAP, PLURALS, RAW,
STRING, STYLE, STYLEABLE, TRANSITION, XML

http://developer.android.com/reference/android/R.html

name和value就是普通的键值对了。

这里并没有对styleable做特殊处理。

最后按rType分类,存在一个Map中,即key为rType,value为一个RDotTxtEntry类型的Set集合。

回顾下剩下的代码:

//...省略前半部分
     AaptResourceCollector aaptResourceCollector = AaptUtil.collectResource(resourceDirectoryList, rTypeResourceMap);
    PatchUtil.generatePublicResourceXml(aaptResourceCollector, idsXml, publicXml);
    File publicFile = new File(publicXml);
    if (publicFile.exists()) {
        FileOperation.copyFileUsingStream(publicFile, project.file(RESOURCE_PUBLIC_XML));
        project.logger.error("tinker gen resource public.xml in ${RESOURCE_PUBLIC_XML}");
    }
    File idxFile = new File(idsXml);
    if (idxFile.exists()) {
        FileOperation.copyFileUsingStream(idxFile, project.file(RESOURCE_IDX_XML));
        project.logger.error("tinker gen resource idx.xml in ${RESOURCE_IDX_XML}");
    }

那么到了AaptUtil.collectResource方法,传入了resDir目录和我们刚才收集了资源信息的Map,返回了一个AaptResourceCollector对象,看名称是对aapt相关的资源的收集:

看代码:

public static AaptResourceCollector collectResource(List<String> resourceDirectoryList,
                                                    Map<RType, Set<RDotTxtEntry>> rTypeResourceMap) {
    AaptResourceCollector resourceCollector = new AaptResourceCollector(rTypeResourceMap);
    List<com.tencent.tinker.build.aapt.RDotTxtEntry> references = new ArrayList<com.tencent.tinker.build.aapt.RDotTxtEntry>();
    for (String resourceDirectory : resourceDirectoryList) {
        try {
            collectResources(resourceDirectory, resourceCollector);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    for (String resourceDirectory : resourceDirectoryList) {
        try {
            processXmlFilesForIds(resourceDirectory, references, resourceCollector);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
    return resourceCollector;
}

首先初始化了一个AaptResourceCollector对象,看其构造方法:

public AaptResourceCollector(Map<RType, Set<RDotTxtEntry>> rTypeResourceMap) {
    this();
    if (rTypeResourceMap != null) {
        Iterator<Entry<RType, Set<RDotTxtEntry>>> iterator = rTypeResourceMap.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<RType, Set<RDotTxtEntry>> entry = iterator.next();
            RType rType = entry.getKey();
            Set<RDotTxtEntry> set = entry.getValue();

            for (RDotTxtEntry rDotTxtEntry : set) {
                originalResourceMap.put(rDotTxtEntry, rDotTxtEntry);

                ResourceIdEnumerator resourceIdEnumerator = null;
                    // ARRAY主要是styleable
                if (!rDotTxtEntry.idType.equals(IdType.INT_ARRAY)) {
                        // 获得resourceId
                    int resourceId = Integer.decode(rDotTxtEntry.idValue.trim()).intValue();
                    // 获得typeId
                    int typeId = ((resourceId & 0x00FF0000) / 0x00010000);


                    if (typeId >= currentTypeId) {
                        currentTypeId = typeId + 1;
                    }

                        // type -> id的映射
                    if (this.rTypeEnumeratorMap.containsKey(rType)) {
                        resourceIdEnumerator = this.rTypeEnumeratorMap.get(rType);
                        if (resourceIdEnumerator.currentId < resourceId) {
                            resourceIdEnumerator.currentId = resourceId;
                        }
                    } else {
                        resourceIdEnumerator = new ResourceIdEnumerator();
                        resourceIdEnumerator.currentId = resourceId;
                        this.rTypeEnumeratorMap.put(rType, resourceIdEnumerator);
                    }
                }
            }
        }
    }
}

对rTypeResourceMap根据rType进行遍历,读取每个rType对应的Set集合;然后遍历每个rDotTxtEntry:

  1. 加入到originalResourceMap,key和value都是rDotTxtEntry对象
  2. 如果是int型资源,首先读取其typeId,并持续更新currentTypeId(保证其为遍历完成后的最大值+1)
  3. 初始化rTypeEnumeratorMap,key为rType,value为ResourceIdEnumerator,且ResourceIdEnumerator中的currentId保存着目前同类资源的最大的resouceId,也就是说rTypeEnumeratorMap中存储了各个rType对应的最大的资源Id。

结束完成构造方法,执行了

  1. 遍历了resourceDirectoryList,目前其中只有一个resDir,然后执行了collectResources方法;
  2. 遍历了resourceDirectoryList,执行了processXmlFilesForIds

分别读代码了:

collectResources

private static void collectResources(String resourceDirectory, AaptResourceCollector resourceCollector) throws Exception {
    File resourceDirectoryFile = new File(resourceDirectory);
    File[] fileArray = resourceDirectoryFile.listFiles();
    if (fileArray != null) {
        for (File file : fileArray) {
            if (file.isDirectory()) {
                String directoryName = file.getName();
                if (directoryName.startsWith("values")) {
                    if (!isAValuesDirectory(directoryName)) {
                        throw new AaptUtilException("'" + directoryName + "' is not a valid values directory.");
                    }
                    processValues(file.getAbsolutePath(), resourceCollector);
                } else {
                    processFileNamesInDirectory(file.getAbsolutePath(), resourceCollector);
                }
            }
        }
    }
}

遍历我们的resDir中的所有文件夹

  • 如果是values相关文件夹,执行processValues
  • 非values相关文件夹则执行processFileNamesInDirectory

processValues处理values相关文件,会遍历每一个合法的values相关文件夹下的文件,执行processValuesFile(file.getAbsolutePath(), resourceCollector);

public static void processValuesFile(String valuesFullFilename,
                                     AaptResourceCollector resourceCollector) throws Exception {
    Document document = JavaXmlUtil.parse(valuesFullFilename);
    String directoryName = new File(valuesFullFilename).getParentFile().getName();
    Element root = document.getDocumentElement();

    for (Node node = root.getFirstChild(); node != null; node = node.getNextSibling()) {
        if (node.getNodeType() != Node.ELEMENT_NODE) {
            continue;
        }

        String resourceType = node.getNodeName();
        if (resourceType.equals(ITEM_TAG)) {
            resourceType = node.getAttributes().getNamedItem("type").getNodeValue();
            if (resourceType.equals("id")) {
                resourceCollector.addIgnoreId(node.getAttributes().getNamedItem("name").getNodeValue());
            }
        }

        if (IGNORED_TAGS.contains(resourceType)) {
            continue;
        }

        if (!RESOURCE_TYPES.containsKey(resourceType)) {
            throw new AaptUtilException("Invalid resource type '<" + resourceType + ">' in '" + valuesFullFilename + "'.");
        }

        RType rType = RESOURCE_TYPES.get(resourceType);
        String resourceValue = null;
        switch (rType) {
            case STRING:
            case COLOR:
            case DIMEN:
            case DRAWABLE:
            case BOOL:
            case INTEGER:
                resourceValue = node.getTextContent().trim();
                break;
            case ARRAY://has sub item
            case PLURALS://has sub item
            case STYLE://has sub item
            case STYLEABLE://has sub item
                resourceValue = subNodeToString(node);
                break;
            case FRACTION://no sub item
                resourceValue = nodeToString(node, true);
                break;
            case ATTR://no sub item
                resourceValue = nodeToString(node, true);
                break;
        }
        try {
            addToResourceCollector(resourceCollector,
                    new ResourceDirectory(directoryName, valuesFullFilename),
                    node, rType, resourceValue);
        } catch (Exception e) {
            throw new AaptUtilException(e.getMessage() + ",Process file error:" + valuesFullFilename, e);
        }
    }
}

values下相关的文件基本都是xml咯,所以遍历xml文件,遍历其内部的节点,(values的xml文件其内部一般为item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction等),每种类型的节点对应一个rType,根据不同类型的节点也会去获取节点的值,确定一个都会执行:

addToResourceCollector(resourceCollector,
    new ResourceDirectory(directoryName, valuesFullFilename),
    node, rType, resourceValue);

注:除此以外,这里在ignoreIdSet记录了声明的id资源,这些id是已经声明过的,所以最终在编写ids.xml时,可以过滤掉这些id。

下面继续看:addToResourceCollector

源码如下:

private static void addToResourceCollector(AaptResourceCollector resourceCollector,
                                           ResourceDirectory resourceDirectory,
                                           Node node, RType rType, String resourceValue) {
    String resourceName = sanitizeName(rType, resourceCollector, extractNameAttribute(node));

    if (rType.equals(RType.STYLEABLE)) {

        int count = 0;
        for (Node attrNode = node.getFirstChild(); attrNode != null; attrNode = attrNode.getNextSibling()) {
            if (attrNode.getNodeType() != Node.ELEMENT_NODE || !attrNode.getNodeName().equals("attr")) {
                continue;
            }
            String rawAttrName = extractNameAttribute(attrNode);
            String attrName = sanitizeName(rType, resourceCollector, rawAttrName);

            if (!rawAttrName.startsWith("android:")) {
                resourceCollector.addIntResourceIfNotPresent(RType.ATTR, attrName);
            }
        }
    } else {
        resourceCollector.addIntResourceIfNotPresent(rType, resourceName);
    }
}

如果不是styleable的资源,则直接获取resourceName,然后调用resourceCollector.addIntResourceIfNotPresent(rType, resourceName)。

如果是styleable类型的资源,则会遍历找到其内部的attr节点,找出非android:开头的(因为android:开头的attr的id不需要我们去确定),设置rType为ATTR,value为attr属性的name,调用addIntResourceIfNotPresent。

public void addIntResourceIfNotPresent(RType rType, String name) { //, ResourceDirectory resourceDirectory) {
    if (!rTypeEnumeratorMap.containsKey(rType)) {
        if (rType.equals(RType.ATTR)) {
            rTypeEnumeratorMap.put(rType, new ResourceIdEnumerator(1));
        } else {
            rTypeEnumeratorMap.put(rType, new ResourceIdEnumerator(currentTypeId++));
        }
    }

    RDotTxtEntry entry = new FakeRDotTxtEntry(IdType.INT, rType, name);
    Set<RDotTxtEntry> resourceSet = null;
    if (this.rTypeResourceMap.containsKey(rType)) {
        resourceSet = this.rTypeResourceMap.get(rType);
    } else {
        resourceSet = new HashSet<RDotTxtEntry>();
        this.rTypeResourceMap.put(rType, resourceSet);
    }
    if (!resourceSet.contains(entry)) {
        String idValue = String.format("0x%08x", rTypeEnumeratorMap.get(rType).next());
        addResource(rType, IdType.INT, name, idValue); //, resourceDirectory);
    }
}

首先构建一个entry,然后判断当前的rTypeResourceMap中是否存在该资源实体,如果存在,则什么都不用做。

如果不存在,则需要构建一个entry,那么主要是id的构建。

关于id的构建:

还记得rTypeEnumeratorMap么,其内部包含了我们设置的”res mapping”文件,存储了每一类资源(rType)的资源的最大resourceId值。

那么首先判断就是是否已经有这种类型了,如果有的话,获取出该类型当前最大的resourceId,然后+1,最为传入资源的resourceId.

如果不存在当前这种类型,那么如果类型为ATTR则固定type为1;否则的话,新增一个typeId,为当前最大的type+1(currentTypeId中也是记录了目前最大的type值),有了类型就可以通过ResourceIdEnumerator.next()来获取id。

经过上述就可以构造出一个idValue了。

最后调用:

addResource(rType, IdType.INT, name, idValue);

查看代码:

public void addResource(RType rType, IdType idType, String name, String idValue) {
    Set<RDotTxtEntry> resourceSet = null;
    if (this.rTypeResourceMap.containsKey(rType)) {
        resourceSet = this.rTypeResourceMap.get(rType);
    } else {
        resourceSet = new HashSet<RDotTxtEntry>();
        this.rTypeResourceMap.put(rType, resourceSet);
    }
    RDotTxtEntry rDotTxtEntry = new RDotTxtEntry(idType, rType, name, idValue);

    if (!resourceSet.contains(rDotTxtEntry)) {
        if (this.originalResourceMap.containsKey(rDotTxtEntry)) {
            this.rTypeEnumeratorMap.get(rType).previous();
            rDotTxtEntry = this.originalResourceMap.get(rDotTxtEntry);
        } 
        resourceSet.add(rDotTxtEntry);
    }

}

大体意思就是如果该资源不存在就添加到rTypeResourceMap。

首先构建出该资源实体,判断该类型对应的资源集合是否包含该资源实体(这里contains只比对name和type),如果不包含,判断是否在originalResourceMap中,如果存在(这里做了一个previous操作,其实与上面的代码的next操作对应,主要是针对资源存在我们的res map中这种情况)则取出该资源实体,最终将该资源实体加入到rTypeResourceMap中。

ok,到这里需要小节一下,我们刚才对所有values相关文件夹下的文件已经处理完毕,大致的处理为:遍历文件中的节点,大致有item,dimen,color,drawable,bool,integer,array,style,declare-styleable,attr,fraction这些节点,将所有的节点按类型分类存储到rTypeResourceMap中(如果和设置的”res map”文件中相同name和type的节点不需要特殊处理,直接复用即可;如果不存在则需要生成新的typeId、resourceId等信息)。

其中declare-styleable这个标签,主要读取其内部的attr标签,对attr标签对应的资源按上述处理。

处理完成values相关文件夹之后,还需要处理一些res下的其他文件,比如layout、layout、anim等文件夹,该类资源也需要在R中生成对应的id值,这类值也需要固化。

processFileNamesInDirectory

public static void processFileNamesInDirectory(String resourceDirectory,
                                               AaptResourceCollector resourceCollector) throws IOException {
    File resourceDirectoryFile = new File(resourceDirectory);
    String directoryName = resourceDirectoryFile.getName();
    int dashIndex = directoryName.indexOf('-');
    if (dashIndex != -1) {
        directoryName = directoryName.substring(0, dashIndex);
    }

    if (!RESOURCE_TYPES.containsKey(directoryName)) {
        throw new AaptUtilException(resourceDirectoryFile.getAbsolutePath() + " is not a valid resource sub-directory.");
    }
    File[] fileArray = resourceDirectoryFile.listFiles();
    if (fileArray != null) {
        for (File file : fileArray) {
            if (file.isHidden()) {
                continue;
            }
            String filename = file.getName();
            int dotIndex = filename.indexOf('.');
            String resourceName = dotIndex != -1 ? filename.substring(0, dotIndex) : filename;

            RType rType = RESOURCE_TYPES.get(directoryName);
            resourceCollector.addIntResourceIfNotPresent(rType, resourceName);

            System.out.println("rType = " + rType + " , resName = " + resourceName);

            ResourceDirectory resourceDirectoryBean = new ResourceDirectory(file.getParentFile().getName(), file.getAbsolutePath());
            resourceCollector.addRTypeResourceName(rType, resourceName, null, resourceDirectoryBean);
        }
    }
}

遍历res下所有文件夹,根据文件夹名称确定其对应的资源类型(例如:drawable-xhpi,则认为其内部的文件类型为drawable类型),然后遍历该文件夹下所有的文件,最终以文件名为资源的name,文件夹确定资源的type,最终调用:

resourceCollector
.addIntResourceIfNotPresent(rType, resourceName);

processXmlFilesForIds

public static void processXmlFilesForIds(String resourceDirectory,
                                         List<RDotTxtEntry> references, AaptResourceCollector resourceCollector) throws Exception {
    List<String> xmlFullFilenameList = FileUtil
            .findMatchFile(resourceDirectory, Constant.Symbol.DOT + Constant.File.XML);
    if (xmlFullFilenameList != null) {
        for (String xmlFullFilename : xmlFullFilenameList) {
            File xmlFile = new File(xmlFullFilename);

            String parentFullFilename = xmlFile.getParent();
            File parentFile = new File(parentFullFilename);
            if (isAValuesDirectory(parentFile.getName()) || parentFile.getName().startsWith("raw")) {
                // Ignore files under values* directories and raw*.
                continue;
            }
            processXmlFile(xmlFullFilename, references, resourceCollector);
        }
    }
}

遍历除了raw*以及values*相关文件夹下的xml文件,执行processXmlFile。

public static void processXmlFile(String xmlFullFilename, List<RDotTxtEntry> references, AaptResourceCollector resourceCollector)
        throws IOException, XPathExpressionException {
    Document document = JavaXmlUtil.parse(xmlFullFilename);
    NodeList nodesWithIds = (NodeList) ANDROID_ID_DEFINITION.evaluate(document, XPathConstants.NODESET);
    for (int i = 0; i < nodesWithIds.getLength(); i++) {
        String resourceName = nodesWithIds.item(i).getNodeValue();


        if (!resourceName.startsWith(ID_DEFINITION_PREFIX)) {
            throw new AaptUtilException("Invalid definition of a resource: '" + resourceName + "'");
        }

        resourceCollector.addIntResourceIfNotPresent(RType.ID, resourceName.substring(ID_DEFINITION_PREFIX.length()));
    }

    // 省略了无关代码
}

主要找xml文档中以@+(去除@+android:id),其实就是找到我们自定义id节点,然后截取该节点的id值部分作为属性的名称(例如:@+id/tv,tv即为属性的名称),最终调用:

resourceCollector
    .addIntResourceIfNotPresent(RType.ID, 
        resourceName.substring(ID_DEFINITION_PREFIX.length()));

上述就完成了所有的资源的收集,那么剩下的就是写文件了:


public static void generatePublicResourceXml(AaptResourceCollector aaptResourceCollector,
                                             String outputIdsXmlFullFilename,
                                             String outputPublicXmlFullFilename) {
    if (aaptResourceCollector == null) {
        return;
    }
    FileUtil.createFile(outputIdsXmlFullFilename);
    FileUtil.createFile(outputPublicXmlFullFilename);

    PrintWriter idsWriter = null;
    PrintWriter publicWriter = null;
    try {
        FileUtil.createFile(outputIdsXmlFullFilename);
        FileUtil.createFile(outputPublicXmlFullFilename);
        idsWriter = new PrintWriter(new File(outputIdsXmlFullFilename), "UTF-8");

        publicWriter = new PrintWriter(new File(outputPublicXmlFullFilename), "UTF-8");
        idsWriter.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        publicWriter.println("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
        idsWriter.println("<resources>");
        publicWriter.println("<resources>");
        Map<RType, Set<RDotTxtEntry>> map = aaptResourceCollector.getRTypeResourceMap();
        Iterator<Entry<RType, Set<RDotTxtEntry>>> iterator = map.entrySet().iterator();
        while (iterator.hasNext()) {
            Entry<RType, Set<RDotTxtEntry>> entry = iterator.next();
            RType rType = entry.getKey();
            if (!rType.equals(RType.STYLEABLE)) {
                Set<RDotTxtEntry> set = entry.getValue();
                for (RDotTxtEntry rDotTxtEntry : set) {
                    String rawName = aaptResourceCollector.getRawName(rType, rDotTxtEntry.name);
                    if (StringUtil.isBlank(rawName)) {
                        rawName = rDotTxtEntry.name;
                    }
                    publicWriter.println("<public type=\"" + rType + "\" name=\"" + rawName + "\" id=\"" + rDotTxtEntry.idValue.trim() + "\" />");          
                }
                Set<String> ignoreIdSet = aaptResourceCollector.getIgnoreIdSet();
                for (RDotTxtEntry rDotTxtEntry : set) {
                    if (rType.equals(RType.ID) && !ignoreIdSet.contains(rDotTxtEntry.name)) {
                        idsWriter.println("<item type=\"" + rType + "\" name=\"" + rDotTxtEntry.name + "\"/>");
                    } 
                }
            }
            idsWriter.flush();
            publicWriter.flush();
        }
        idsWriter.println("</resources>");
        publicWriter.println("</resources>");
    } catch (Exception e) {
        throw new PatchUtilException(e);
    } finally {
        if (idsWriter != null) {
            idsWriter.flush();
            idsWriter.close();
        }
        if (publicWriter != null) {
            publicWriter.flush();
            publicWriter.close();
        }
    }
}

主要就是遍历rTypeResourceMap,然后每个资源实体对应一条public标签记录写到public.xml中。

此外,如果发现该元素节点的type为Id,并且不在ignoreSet中,会写到ids.xml这个文件中。(这里有个ignoreSet,这里ignoreSet中记录了values下所有的<item type=id的资源,是直接在项目中已经声明过的,所以去除)。

六、TinkerProguardConfigTask

还记得文初说:

  1. 我们在上线app的时候,会做代码混淆,如果没有做特殊的设置,每次混淆后的代码差别应该非常巨大;所以,build过程中理论上需要设置混淆的mapping文件。
  2. 在接入一些库的时候,往往还需要配置混淆,比如第三方库中哪些东西不能被混淆等(当然强制某些类在主dex中,也可能需要配置相对应的混淆规则)。

这个task的作用很明显了。有时候为了确保一些类在main dex中,简单的做法也会对其在混淆配置中进行keep(避免由于混淆造成类名更改,而使main dex的keep失效)。

如果开启了proguard会执行该task。

这个就是主要去设置混淆的mapping文件,和keep一些必要的类了。

@TaskAction
def updateTinkerProguardConfig() {
    def file = project.file(PROGUARD_CONFIG_PATH)
    project.logger.error("try update tinker proguard file with ${file}")

    // Create the directory if it doesnt exist already
    file.getParentFile().mkdirs()

    // Write our recommended proguard settings to this file
    FileWriter fr = new FileWriter(file.path)

    String applyMappingFile = project.extensions.tinkerPatch.buildConfig.applyMapping

    //write applymapping
    if (shouldApplyMapping && FileOperation.isLegalFile(applyMappingFile)) {
        project.logger.error("try add applymapping ${applyMappingFile} to build the package")
        fr.write("-applymapping " + applyMappingFile)
        fr.write("\n")
    } else {
        project.logger.error("applymapping file ${applyMappingFile} is illegal, just ignore")
    }

    fr.write(PROGUARD_CONFIG_SETTINGS)

    fr.write("#your dex.loader patterns here\n")
    //they will removed when apply
    Iterable<String> loader = project.extensions.tinkerPatch.dex.loader
    for (String pattern : loader) {
        if (pattern.endsWith("*") && !pattern.endsWith("**")) {
            pattern += "*"
        }
        fr.write("-keep class " + pattern)
        fr.write("\n")
    }
    fr.close()
    // Add this proguard settings file to the list
    applicationVariant.getBuildType().buildType.proguardFiles(file)
    def files = applicationVariant.getBuildType().buildType.getProguardFiles()

    project.logger.error("now proguard files is ${files}")
}

读取我们设置的mappingFile,设置

-applymapping applyMappingFile

然后设置一些默认需要keep的规则:

PROGUARD_CONFIG_SETTINGS =
"-keepattributes *Annotation* \n" +
"-dontwarn com.tencent.tinker.anno.AnnotationProcessor \n" +
"-keep @com.tencent.tinker.anno.DefaultLifeCycle public class *\n" +
"-keep public class * extends android.app.Application {\n" +
"    *;\n" +
"}\n" +
"\n" +
"-keep public class com.tencent.tinker.loader.app.ApplicationLifeCycle {\n" +
"    *;\n" +
"}\n" +
"-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {\n" +
"    *;\n" +
"}\n" +
"\n" +
"-keep public class com.tencent.tinker.loader.TinkerLoader {\n" +
"    *;\n" +
"}\n" +
"-keep public class * extends com.tencent.tinker.loader.TinkerLoader {\n" +
"    *;\n" +
"}\n" +
"-keep public class com.tencent.tinker.loader.TinkerTestDexLoad {\n" +
"    *;\n" +
"}\n" +
"\n"

最后是keep住我们的application、com.tencent.tinker.loader.**以及我们设置的相关类。

TinkerManifestTask中:addApplicationToLoaderPattern主要是记录自己的application类名和tinker相关的一些load class com.tencent.tinker.loader.*,记录在project.extensions.tinkerPatch.dex.loader

七、TinkerMultidexConfigTask

对应文初:

当项目比较大的时候,我们可能会遇到方法数超过65535的问题,我们很多时候会通过分包解决,这样就有主dex和其他dex的概念。集成了tinker之后,在应用的Application启动时会非常早的就去做tinker的load操作,所以就决定了load相关的类必须在主dex中。

如果multiDexEnabled开启。

主要是让相关类必须在main dex。

"-keep public class * implements com.tencent.tinker.loader.app.ApplicationLifeCycle {\n" +
    "    *;\n" +
    "}\n" +
    "\n" +
    "-keep public class * extends com.tencent.tinker.loader.TinkerLoader {\n" +
    "    *;\n" +
    "}\n" +
    "\n" +
    "-keep public class * extends android.app.Application {\n" +
    "    *;\n" +
    "}\n"
Iterable<String> loader = project.extensions.tinkerPatch.dex.loader
    for (String pattern : loader) {
        if (pattern.endsWith("*")) {
            if (!pattern.endsWith("**")) {
                pattern += "*"
            }
        }
        lines.append("-keep class " + pattern + " {\n" +
                "    *;\n" +
                "}\n")
                .append("\n")
    }

相关类都在loader这个集合中,在TinkerManifestTask中设置的。

八、TinkerPatchSchemaTask

主要执行Runner.tinkerPatch

protected void tinkerPatch() {
    try {
        //gen patch
        ApkDecoder decoder = new ApkDecoder(config);
        decoder.onAllPatchesStart();
        decoder.patch(config.mOldApkFile, config.mNewApkFile);
        decoder.onAllPatchesEnd();

        //gen meta file and version file
        PatchInfo info = new PatchInfo(config);
        info.gen();

        //build patch
        PatchBuilder builder = new PatchBuilder(config);
        builder.buildPatch();

    } catch (Throwable e) {
        e.printStackTrace();
        goToError();
    }
}

主要分为以下环节:

  • 生成patch
  • 生成meta-file和version-file,这里主要就是在assets目录下写一些键值对。(包含tinkerId以及配置中configField相关信息)
  • build patch

(1)生成pacth

顾名思义就是两个apk比较去生成各类patch文件,那么从一个apk的组成来看,大致可以分为:

  • dex文件比对的patch文件
  • res文件比对的patch res文件
  • so文件比对生成的so patch文件

看下代码:

public boolean patch(File oldFile, File newFile) throws Exception {
    //check manifest change first
    manifestDecoder.patch(oldFile, newFile);

    unzipApkFiles(oldFile, newFile);

    Files.walkFileTree(mNewApkDir.toPath(), new ApkFilesVisitor(config, mNewApkDir.toPath(),
            mOldApkDir.toPath(), dexPatchDecoder, soPatchDecoder, resPatchDecoder));

    soPatchDecoder.onAllPatchesEnd();
    dexPatchDecoder.onAllPatchesEnd();
    manifestDecoder.onAllPatchesEnd();
    resPatchDecoder.onAllPatchesEnd();

    //clean resources
    dexPatchDecoder.clean();
    soPatchDecoder.clean();
    resPatchDecoder.clean();
    return true;
}

代码内部包含四个Decoder:

  • manifestDecoder
  • dexPatchDecoder
  • soPatchDecoder
  • resPatchDecoder

刚才提到需要对dex、so、res文件做diff,但是为啥会有个manifestDecoder。目前tinker并不支持四大组件,也就是说manifest文件中是不允许出现新增组件的。

所以,manifestDecoder的作用实际上是用于检查的:

  1. minSdkVersion<14时仅允许dexMode使用jar模式(TODO:raw模式的区别是什么?)
  2. 会解析manifest文件,读取出组大组件进行对比,不允许出现新增的任何组件。

代码就不贴了非常好理解,关于manifest的解析是基于该库封装的:

https://github.com/clearthesky/apk-parser

然后就是解压两个apk文件了,old apk(我们设置的),old apk 生成的。

解压的目录为:

  • old apk: build/intermediates/outputs/old apk名称/
  • new apk: build/intermediates/outputs/app-debug/

解压完成后,就是单个文件对比了:

对比的思路是,以newApk解压目录下所有的文件为基准,去oldApk中找同名的文件,那么会有以下几个情况:

  1. 在oldApkDir中没有找到,那么说明该文件是新增的
  2. 在oldApkDir中找到了,那么比对md5,如果不同,则认为改变了(则需要根据情况做diff)

有了大致的了解后,可以看代码:

Files.walkFileTree(
    mNewApkDir.toPath(), 
    new ApkFilesVisitor(
        config, 
        mNewApkDir.toPath(),
        mOldApkDir.toPath(), 
        dexPatchDecoder, 
        soPatchDecoder, 
        resPatchDecoder));

Files.walkFileTree会以mNewApkDir.toPath()为基准,遍历其内部所有的文件,ApkFilesVisitor中可以对每个遍历的文件进行操作。

重点看ApkFilesVisitor是如何操作每个文件的:

@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {

    Path relativePath = newApkPath.relativize(file);
    // 在oldApkDir中找到该文件
    Path oldPath = oldApkPath.resolve(relativePath);

    File oldFile = null;
    //is a new file?!
    if (oldPath.toFile().exists()) {
        oldFile = oldPath.toFile();
    }

    String patternKey = relativePath.toString().replace("\\", "/");

    if (Utils.checkFileInPattern(config.mDexFilePattern, patternKey)) {
        dexDecoder.patch(oldFile, file.toFile());
    }
    if (Utils.checkFileInPattern(config.mSoFilePattern, patternKey)) {
        soDecoder.patch(oldFile, file.toFile());
    }
    if (Utils.checkFileInPattern(config.mResFilePattern, patternKey)) {
         resDecoder.patch(oldFile, file.toFile());
    }
    return FileVisitResult.CONTINUE;
}

首先去除newApkDir中的一个文件,在oldApkDir中寻找同名的apk;然后根据名称判断该文件属于:

  1. dexFile -> dexDecoder.patch 完成dex文件间的比对
  2. soFile -> soDecoder.patch 完成so文件的比对
  3. resFile -> resDecoder.patch 完成res文件的比对

各种文件的规则是可配置的。

(1)dexDecoder.patch

public boolean patch(final File oldFile, final File newFile)  {
    final String dexName = getRelativeDexName(oldFile, newFile);

    // 检查loader class,省略了抛异常的一些代码
    excludedClassModifiedChecker.checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);


    File dexDiffOut = getOutputPath(newFile).toFile();

    final String newMd5 = getRawOrWrappedDexMD5(newFile);

    //new add file
    if (oldFile == null || !oldFile.exists() || oldFile.length() == 0) {
        hasDexChanged = true;
        copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);
        return true;
    }

    final String oldMd5 = getRawOrWrappedDexMD5(oldFile);

    if ((oldMd5 != null && !oldMd5.equals(newMd5)) || (oldMd5 == null && newMd5 != null)) {
        hasDexChanged = true;
        if (oldMd5 != null) {
            collectAddedOrDeletedClasses(oldFile, newFile);
        }
    }

    RelatedInfo relatedInfo = new RelatedInfo();
    relatedInfo.oldMd5 = oldMd5;
    relatedInfo.newMd5 = newMd5;

    // collect current old dex file and corresponding new dex file for further processing.
    oldAndNewDexFilePairList.add(new AbstractMap.SimpleEntry<>(oldFile, newFile));

    dexNameToRelatedInfoMap.put(dexName, relatedInfo);

    return true;
}

首先执行:

checkIfExcludedClassWasModifiedInNewDex(oldFile, newFile);

该方法主要用处是检查 tinker loader相关classes**必须存在primary dex中**,且不允许新增、修改和删除。

所有首先将两个dex读取到内存中,按照config.mDexLoaderPattern进行过滤,找出deletedClassInfosaddedClassInfoschangedClassInfosMap,必须保证deletedClassInfos.isEmpty() && addedClassInfos.isEmpty() && changedClassInfosMap.isEmpty()即不允许新增、删除、修改loader 相关类。

继续,拿到输出目录:

  • build/intermediates/outputs/tinker_result/

然后如果oldFile不存在,则newFile认为是新增文件,直接copy到输出目录,并记录log

copyNewDexAndLogToDexMeta(newFile, newMd5, dexDiffOut);

如果存在,则计算两个文件的md5,如果md5不同,则认为dexChanged(hasDexChanged = true),执行:

collectAddedOrDeletedClasses(oldFile, newFile);

该方法收集了addClasses和deleteClasses的相关信息,记录在:

  • addedClassDescToDexNameMap key为addClassDesc 和 该dex file的path
  • deletedClassDescToDexNameMap key为deletedClassDesc 和 该dex file的path

后续会使用这两个数据结构,mark一下。

继续往下走,初始化了一个relatedInfo记录了两个文件的md5,以及在oldAndNewDexFilePairList中记录了两个dex file,在dexNameToRelatedInfoMap中记录了dexName和relatedInfo的映射。

后续会使用该变量,mark一下。

到此,dexDecoder的patch方法就结束了,仅将新增的文件copy到了目标目录。

那么发生改变的文件,理论上应该要做md5看来在后面才会执行。

如果文件是so文件,则会走soDecoder.patch。

(2)soDecoder.patch

soDecoder实际上是BsDiffDecoder

@Override
public boolean patch(File oldFile, File newFile)  {
    //new add file
    String newMd5 = MD5.getMD5(newFile);
    File bsDiffFile = getOutputPath(newFile).toFile();

    if (oldFile == null || !oldFile.exists()) {
        FileOperation.copyFileUsingStream(newFile, bsDiffFile);
        writeLogFiles(newFile, null, null, newMd5);
        return true;
    }

    //new add file
    String oldMd5 = MD5.getMD5(oldFile);

    if (oldMd5.equals(newMd5)) {
        return false;
    }

    if (!bsDiffFile.getParentFile().exists()) {
        bsDiffFile.getParentFile().mkdirs();
    }
    BSDiff.bsdiff(oldFile, newFile, bsDiffFile);

    //超过80%,返回false
    if (Utils.checkBsDiffFileSize(bsDiffFile, newFile)) {
        writeLogFiles(newFile, oldFile, bsDiffFile, newMd5);
    } else {
        FileOperation.copyFileUsingStream(newFile, bsDiffFile);
        writeLogFiles(newFile, null, null, newMd5);
    }
    return true;
}

如果oldFile不存在,则认为newFile为新增文件,直接copy到目标文件(连着so相关目录)。

若oldFile存在,则比对二者md5,如果md5不一致,则直接进行bsdiff算法,直接在目标位置写入bsdiff产生的bsDiffFile。

本来到此应该已经结束了,但是接下来做了一件挺有意思的事:

继续判断了生成的patch文件是否已经超过newFile的80%,如果超过80%,则直接copy newFile到目标目录,直接覆盖了刚生成的patch文件。

那么soPatch整个过程:

  1. 如果是新增文件,直接copy至目标文件夹,记录log
  2. 如果是改变的文件,patch文件超过新文件的80%,则直接copy新文件至目标文件夹,记录log
  3. 如果是改变的文件,patch文件不超过新文件的80%,则copy patch文件至目标文件夹,记录log

如果newFile是res 资源,则会走resDecoder

(3)resDecoder.patch

@Override
public boolean patch(File oldFile, File newFile) throws IOException, TinkerPatchException {
    String name = getRelativePathStringToNewFile(newFile);

    File outputFile = getOutputPath(newFile).toFile();

    if (oldFile == null || !oldFile.exists()) {
        FileOperation.copyFileUsingStream(newFile, outputFile);
        addedSet.add(name);
        writeResLog(newFile, oldFile, TypedValue.ADD);
        return true;
    }

    //new add file
    String newMd5 = MD5.getMD5(newFile);
    String oldMd5 = MD5.getMD5(oldFile);

    //oldFile or newFile may be 0b length
    if (oldMd5 != null && oldMd5.equals(newMd5)) {
        return false;
    }
    if (Utils.checkFileInPattern(config.mResIgnoreChangePattern, name)) {
        Logger.d("found modify resource: " + name + ", but it match ignore change pattern, just ignore!");
        return false;
    }
    if (name.equals(TypedValue.RES_MANIFEST)) {
        Logger.d("found modify resource: " + name + ", but it is AndroidManifest.xml, just ignore!");
        return false;
    }
    if (name.equals(TypedValue.RES_ARSC)) {
        if (AndroidParser.resourceTableLogicalChange(config)) {
            Logger.d("found modify resource: " + name + ", but it is logically the same as original new resources.arsc, just ignore!");
            return false;
        }
    }
    dealWithModeFile(name, newMd5, oldFile, newFile, outputFile);
    return true;
}

如果oldFile不存在,则认为新增文件,直接copy且加入到addedSet集合,并记录log

如果存在,且md5不同调研dealWithModeFile(设置的sIgnoreChangePattern、MANIFEST和逻辑上相同的ARSC不做处理)。


private boolean dealWithModeFile(String name, String newMd5, File oldFile, File newFile, File outputFile) {
    if (checkLargeModFile(newFile)) {
        if (!outputFile.getParentFile().exists()) {
            outputFile.getParentFile().mkdirs();
        }
        BSDiff.bsdiff(oldFile, newFile, outputFile);
        //未超过80%返回true
        if (Utils.checkBsDiffFileSize(outputFile, newFile)) {
            LargeModeInfo largeModeInfo = new LargeModeInfo();
            largeModeInfo.path = newFile;
            largeModeInfo.crc = FileOperation.getFileCrc32(newFile);
            largeModeInfo.md5 = newMd5;
            largeModifiedSet.add(name);
            largeModifiedMap.put(name, largeModeInfo);
            writeResLog(newFile, oldFile, TypedValue.LARGE_MOD);
            return true;
        }
    }
    modifiedSet.add(name);
    FileOperation.copyFileUsingStream(newFile, outputFile);
    writeResLog(newFile, oldFile, TypedValue.MOD);
    return false;
}

这里,首先check了largeFile,即改变的文件是否大于100K(该值可以配置)。

如果非大文件,则直接copy至目标文件,且记录到modifiedSet,并记录了log。

如果是大文件,则直接bsdiff,生成patch File;接下来也检查了一下patch file是否超过newFile的80%,如果超过,则直接copy newFile覆盖刚生成的patch File;

总体和so patch基本一致。

到这里,除了dex patch中对改变的dex文件没有做处理以外,so 和 res都做了。

接下来执行了:


public boolean patch(File oldFile, File newFile) throws Exception {
    //...

    soPatchDecoder.onAllPatchesEnd();
    dexPatchDecoder.onAllPatchesEnd();
    manifestDecoder.onAllPatchesEnd();
    resPatchDecoder.onAllPatchesEnd();

    //clean resources
    dexPatchDecoder.clean();
    soPatchDecoder.clean();
    resPatchDecoder.clean();
    return true;
}

其中dexPatchDecoder和resPatchDecoder有后续实现。

(4) dexPatchDecoder.onAllPatchesEnd

# DexDiffDecoder
@Override
public void onAllPatchesEnd() throws Exception {
    if (!hasDexChanged) {
        Logger.d("No dexes were changed, nothing needs to be done next.");
        return;
    }

    generatePatchInfoFile();

    addTestDex();
}

如果dex文件没有改变,直接返回。

private void generatePatchInfoFile() throws IOException {
    generatePatchedDexInfoFile();

    logDexesToDexMeta();

    checkCrossDexMovingClasses();
}

主要看generatePatchedDexInfoFile


private void generatePatchedDexInfoFile() {
    // Generate dex diff out and full patched dex if a pair of dex is different.
    for (AbstractMap.SimpleEntry<File, File> oldAndNewDexFilePair : oldAndNewDexFilePairList) {
        File oldFile = oldAndNewDexFilePair.getKey();
        File newFile = oldAndNewDexFilePair.getValue();
        final String dexName = getRelativeDexName(oldFile, newFile);
        RelatedInfo relatedInfo = dexNameToRelatedInfoMap.get(dexName);
        if (!relatedInfo.oldMd5.equals(relatedInfo.newMd5)) {
            diffDexPairAndFillRelatedInfo(oldFile, newFile, relatedInfo);
        } else {
            // In this case newDexFile is the same as oldDexFile, but we still
            // need to treat it as patched dex file so that the SmallPatchGenerator
            // can analyze which class of this dex should be kept in small patch.
            relatedInfo.newOrFullPatchedFile = newFile;
            relatedInfo.newOrFullPatchedMd5 = relatedInfo.newMd5;
        }
    }
}

oldAndNewDexFilePairList中记录了两个dex文件,然后根据dex file获取到dexName,再由dexNameToRelatedInfoMap根据name获得到RelatedInfo。

RelatedInfo中包含了两个dex file的md5,如果不同,则执行diffDexPairAndFillRelatedInfo

private void diffDexPairAndFillRelatedInfo(File oldDexFile, 
                        File newDexFile, RelatedInfo relatedInfo) {
    //outputs/tempPatchedDexes
    File tempFullPatchDexPath = new File(config.mOutFolder 
                + File.separator + TypedValue.DEX_TEMP_PATCH_DIR);
    final String dexName = getRelativeDexName(oldDexFile, newDexFile);

    File dexDiffOut = getOutputPath(newDexFile).toFile();
    ensureDirectoryExist(dexDiffOut.getParentFile());


    // dex diff , 去除loader classes
    DexPatchGenerator dexPatchGen = new DexPatchGenerator(oldDexFile, newDexFile);
    dexPatchGen.setAdditionalRemovingClassPatterns(config.mDexLoaderPattern);

    dexPatchGen.executeAndSaveTo(dexDiffOut);


    relatedInfo.dexDiffFile = dexDiffOut;
    relatedInfo.dexDiffMd5 = MD5.getMD5(dexDiffOut);

    File tempFullPatchedDexFile = new File(tempFullPatchDexPath, dexName);

    try {
        new DexPatchApplier(oldDexFile, dexDiffOut).executeAndSaveTo(tempFullPatchedDexFile);

        Logger.d(
                String.format("Verifying if patched new dex is logically the same as original new dex: %s ...", getRelativeStringBy(newDexFile, config.mTempUnzipNewDir))
        );

        Dex origNewDex = new Dex(newDexFile);
        Dex patchedNewDex = new Dex(tempFullPatchedDexFile);
        checkDexChange(origNewDex, patchedNewDex);

        relatedInfo.newOrFullPatchedFile = tempFullPatchedDexFile;
        relatedInfo.newOrFullPatchedMd5 = MD5.getMD5(tempFullPatchedDexFile);
    } catch (Exception e) {
        e.printStackTrace();
        throw new TinkerPatchException(
                "Failed to generate temporary patched dex, which makes MD5 generating procedure of new dex failed, either.", e
        );
    }

    if (!tempFullPatchedDexFile.exists()) {
        throw new TinkerPatchException("can not find the temporary full patched dex file:" + tempFullPatchedDexFile.getAbsolutePath());
    }
    Logger.d("\nGen %s for dalvik full dex file:%s, size:%d, md5:%s", dexName, tempFullPatchedDexFile.getAbsolutePath(), tempFullPatchedDexFile.length(), relatedInfo.newOrFullPatchedMd5);
}

开始针对两个dex文件做dex diff,最终将生成的patch 文件放置在目标文件夹中。

接下来,生成一个临时文件夹,通过DexPatchApplier针对生成的patch文件和old dex file,直接做了合并操作,相当于在本地模拟执行了在客户端上的patch操作。

然后再对新合并生成的patchedNewDex与之前的origNewDex,进行了checkDexChange,即这两者类级别对比,应该所有的类都相同。

最后在dexDecoder的onAllPatchesEnd中还执行了一个addTestDex

private void addTestDex() throws IOException {
    //write test dex
    String dexMode = "jar";
    if (config.mDexRaw) {
        dexMode = "raw";
    }

    final InputStream is = DexDiffDecoder.class.getResourceAsStream("/" + TEST_DEX_NAME);
    String md5 = MD5.getMD5(is, 1024);
    is.close();

    String meta = TEST_DEX_NAME + "," + "" + "," + md5 + "," + md5 + "," + 0 + "," + 0 + "," + dexMode;

    File dest = new File(config.mTempResultDir + "/" + TEST_DEX_NAME);
    FileOperation.copyResourceUsingStream(TEST_DEX_NAME, dest);
    Logger.d("\nAdd test install result dex: %s, size:%d", dest.getAbsolutePath(), dest.length());
    Logger.d("DexDecoder:write test dex meta file data: %s", meta);

    metaWriter.writeLineToInfoFile(meta);
}

copy了一个test.dex文件至目标文件夹,该文件存储在tinker-patch-lib的resources文件夹下,主要用于在app上进行测试。

完成了所有的diff工作后,后面就是生成patch文件了。

(2)打包所有生成的patch文件

//build patch
PatchBuilder builder = new PatchBuilder(config);
builder.buildPatch();

详细代码:

public PatchBuilder(Configuration config) {
    this.config = config;
    this.unSignedApk = new File(config.mOutFolder, PATCH_NAME + "_unsigned.apk");
    this.signedApk = new File(config.mOutFolder, PATCH_NAME + "_signed.apk");
    this.signedWith7ZipApk = new File(config.mOutFolder, PATCH_NAME + "_signed_7zip.apk");
    this.sevenZipOutPutDir = new File(config.mOutFolder, TypedValue.OUT_7ZIP_FILE_PATH);
}

public void buildPatch() throws Exception {
    final File resultDir = config.mTempResultDir;
    //no file change
    if (resultDir.listFiles().length == 0) {
        return;
    }
generateUnsignedApk(unSignedApk);
    signApk(unSignedApk, signedApk);

    use7zApk(signedApk, signedWith7ZipApk, sevenZipOutPutDir);

    if (!signedApk.exists()) {
        Logger.e("Result: final unsigned patch result: %s, size=%d", unSignedApk.getAbsolutePath(), unSignedApk.length());
    } else {
        long length = signedApk.length();
        Logger.e("Result: final signed patch result: %s, size=%d", signedApk.getAbsolutePath(), length);
        if (signedWith7ZipApk.exists()) {
            long length7zip = signedWith7ZipApk.length();
            Logger.e("Result: final signed with 7zip patch result: %s, size=%d", signedWith7ZipApk.getAbsolutePath(), length7zip);
            if (length7zip > length) {
                Logger.e("Warning: %s is bigger than %s %d byte, you should choose %s at these time!",
                    signedWith7ZipApk.getName(),
                    signedApk.getName(),
                    (length7zip - length),
                    signedApk.getName());
            }
        }
    }

}

主要会生成3个文件:unSignedApksignedApk以及signedWith7ZipApk

unSignedApk只要将tinker_result中的文件压缩到一个压缩包即可。
signedApk将unSignedApk使用jarsigner进行签名。

signedWith7ZipApk主要是对signedApk进行解压再做sevenZip压缩。

好了,到此茫茫长的文章就结束啦~~~

受限于本人知识,文中难免出现错误,可以直接留言指出。

九、总结

一直关注tinker的更新,也在项目中对tinker进行了使用与定制,tinker中包含了大量的可学习的知识,项目本身在也具有非常强的价值。

对于tinker的“技术的初心与坚持”一文感触颇深,希望tinker越来越好~

可以阅读以下文章,继续了解tinker~~


支持我的话可以关注下我的公众号,每天都会推送新知识~

欢迎关注我的微信公众号:hongyangAndroid
(可以给我留言你想学习的文章,支持投稿)

作者:lmj623565791 发表于2017/5/23 23:13:53 原文链接
阅读:1001 评论:5 查看评论

ConstraintLayout 属性详解 和Chain的使用

$
0
0

想看我更多文章:【张旭童的博客】http://blog.csdn.net/zxt0601
想来gayhub和我gaygayup:【mcxtzhang的Github主页】https://github.com/mcxtzhang

概述

小伙伴们好久不见,我又回来啦。
说实话这篇文章写的算是比较晚了,距离ConstraintLayout出现至今已经有一年了。
且自AS2.3起创建新的Activity,默认的layout根布局就是ConstraintLayout
所以再不学习就真的晚了。
我也是正式开始学习的道路,先说一下我的学习过程:
* 先阅读了ConstraintLayout官方文档Guideline官方文档
* 实践每个属性并记下笔记(翻译)
* 学习了郭神关于ConstraintLayout可视化操作(拖拖拽拽)的博客,发现博客中对Chain的概念没有提及
* 查询关于Chain以及一些疑点的资料
* 整理成文
* 当然中间也遇到了许许多多的问题

本文的顺序,大体按照ConstraintLayout官方文档的顺序依次讲解(翻译)属性和用法,并对疑难点进行额外说明。
关于可视化操作,本文不再赘述,可参考郭神博客学习。

使用前的准备

引入也有坑,无力吐槽。
先放上 截止至20170524,最新版本1.0.1

compile 'com.android.support.constraint:constraint-layout:1.0.1'

坑是啥?因为我使用的是最新的release版AndroidStudio2.3.2,新建Activity后,自动帮我引入的是1.0.8-alpha版本,
开始我就这么愉快的学习了,可是当我学习到Chain相关姿势时,特码的,他居然报错。说找不到属性:

Error:(10) No resource identifier found for attribute ‘layout_constraintHorizontal_chainStyle’ in package ‘com.mcxtzhang.constraintlayoutdemo’

ok,那我百度,显然搜不到的,ok,那我再google,特么的居然也搜不到。
震惊,于是机智的我去看源码,发现我使用的1.0.8-alpha版本的源码里根本没有Chain相关属性的支持,所以我就觉得一定是引入的版本有问题,于是我用google搜索”ConstraintLayout last version”,发现诶~官方有说最新版链接如下:
http://tools.android.com/recent/constraintlayout102isnowavailable
按照这个链接提示,最新版是1.0.2,嗯哼,当我换成1.0.2后,发现无法download….
不知道是网络问题还是什么问题,提示我无法下载,具体的错误记不清了。反正就是无法获取到这个版本。
特么的机智的我又直接去AndroidStudio的Library Dependency里去搜索,发现居然搜不到“ConstraintLayout “的库。再次懵逼。
后来我进行最后的一次尝试,因为我看google官方上1.0.2版本的上一个版本是1.0.1.于是我修改版本号,sync gradle,居然成功了。
总结踩坑历程:
* 1 最新Release版AndroidStudio模板自带的是1.0.8alpha版ConstraintLayout
* 2 使用Chain相关属性报错
* 3 发现该版本源码没有Chain相关属性
* 4 官网说的最新版1.0.2 我无法下载
* 5 AndroidStudio自带的Library Dependency搜不到ConstraintLayout
* 6 修改版本号为1.0.1 下载

对此,我只能说“惊不惊喜! 意不意外!”

ConstraintLayout是什么

先概况一下,它是一个为了解决布局嵌套和模仿前端flexible布局的一个新布局。

从字面上理解,ConstraintLayout约束布局
在我理解,这是一个RelativeLayout的升级版。
而当初推出RelativeLayout的目的是为了在减少多层嵌套布局
推出ConstraintLayout也是同样的目的,尽可能的使布局 宽而短,而不是 窄而长。
ConstraintLayout更加强大,很多需要多层嵌套的布局,使用ConstraintLayout只需要一层即可解决。
它的Chain几种style方式,和前端的flexbox布局风格一致,官方文档中也说了它是flexible方式布局控件的东西。

A ConstraintLayout is a ViewGroup which allows you to position and size widgets in a flexible way.

而且搭配可视化的操作,使得布局也变得更轻松。
Google官方推荐所有操作都在”Design”区域搞定,即通过可视化拖拖拽拽生成布局大致的样子,然后针对具体属性、约束 精细修改。
甚至可以这么说,你完全不需要知道ConstraintLayout的具体属性值分别是什么,只通过拖拽和鼠标点击就可以实现一些布局。

那么本文的意义何在呢?

我觉得首先是要知其然知其所以然,那些拖拽点击生成的代码属性到底是什么意思?通过本文可以了解。
另外 虽然大部分操作可以在“Design”区域完成,但是保不齐的需要你切换至“Text”区域,写上一两行属性代码,了解 这些属性 总是有益无害的。
而且,有一些属性是无法简单通过拖拽点击完成的,例如Margins when connected to a GONE widget

刚才提到RelativeLayout,其实RelativeLayout也是通过约束来布局子View的呀,
以前RelativeLayout的约束有两种:
* 1 子控件和子控件之间的约束(如android:layout_below="@id/title"
* 2 子控件和父控件的约束(如 android:layout_alignParentTop="true"

现在ConstraintLayout也是类似的,只不过除了以上两种约束,还多了一种
* 3 子控件和Guideline的约束

其实关于和Guideline的约束,也可以理解成约束1,因为Guideline其实就是一个在屏幕上不显示的View罢了。稍后讲到Guideline会带大家看看它巨简单的源码。

下面开始正文,开始属性的讲解

相对定位 (Relative positioning)

这一节的属性和相对布局的很像,
值得注意的是参数取值是 ID(@id/button1)代表约束1、3, 或者 字符串"parent" 代表约束2:
* layout_constraintLeft_toLeftOf
* layout_constraintLeft_toRightOf
* layout_constraintRight_toLeftOf
* layout_constraintRight_toRightOf
* layout_constraintTop_toTopOf
* layout_constraintTop_toBottomOf
* layout_constraintBottom_toTopOf
* layout_constraintBottom_toBottomOf
* layout_constraintBaseline_toBaselineOf
* layout_constraintStart_toEndOf
* layout_constraintStart_toStartOf
* layout_constraintEnd_toStartOf
* layout_constraintEnd_toEndOf


属性都形如layout_constraintXXX_toYYYOf,
这里我的理解,constraintXXX里的XXX代表是这个子控件自身的哪条边(Left、Right、Top、Bottom、Baseline),
toYYYOf里的YYY代表的是和约束控件哪条边 发生约束 (取值同样是 Left、Right、Top、Bottom、Baseline)。
XXXYYY相反时,表示控件自身的XXX在约束控件的YYY的一侧,
例如app:layout_constraintLeft_toRightOf="@id/button1" ,表示的是控件自身的左侧在button1的右侧。

XXXYYY相同时,表示控件自身的XXX和约束控件的YYY的一侧 对齐
例如:app:layout_constraintBottom_toBottomOf="parent",表示控件自身底端和父控件底端对齐。

代码为:

    <Button
        android:id="@+id/button1"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Demo"/>
    <Button
        android:id="@+id/button2"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button2"
        app:layout_constraintLeft_toRightOf="@id/button1"/>
    <Button
        android:id="@+id/button3"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginBottom="20dp"
        android:text="button3 跳转match页"
        app:layout_constraintBottom_toBottomOf="parent"/>

图示:

Margins

margin和以往的使用一致,注意margin**不能为负值**即可。
在上图中也顺便展示了margin的使用。

当约束的widget为GONE时的Margins

Margins when connected to a GONE widget

举例,当A控件 约束 在B控件的左边,B控件GONE了,此时A会额外拥有一个margin的能力,来“补充”B消失的导致的“位移”。
这就是本节的属性。
这一节的属性开始我并没有理解,后来是通过写了一些Demo实验才明白。奈何官方文档惜字如金,只有一句话,并没有Demo展示:

When a position constraint target’s visibility is View.GONE, you can also indicates a different margin value to be used using the following attributes:

先看属性:
* layout_goneMarginStart
* layout_goneMarginEnd
* layout_goneMarginLeft
* layout_goneMarginTop
* layout_goneMarginRight
* layout_goneMarginBottom

在看Demo:

    <Button
        android:id="@+id/button4"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:text="button4"
        app:layout_constraintRight_toRightOf="parent"
        />

    <!-- android:layout_marginRight="10dp" 
    配合 app:layout_goneMarginRight="110dp"一起使用,
    在约束的布局gone时,起用goneMargin,
    但是一定要预先设置对应方向上的margin -->
    <Button
        android:id="@+id/button5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="10dp"
        android:text="button5"
        app:layout_constraintRight_toLeftOf="@id/button4"
        app:layout_goneMarginRight="110dp"/>

此时图示:


当给button4 隐藏GONE掉以后:
图示:

会发现Button5纹丝不动,并没有收到Button4消失的影响。
这里我们再仔细看看button4的android:layout_width="100dp"
而button5的android:layout_marginRight="10dp",app:layout_goneMarginRight="110dp"
110 = 100 +10 , 这是一道小学计算题。

什么意思?
几个注意事项:
* app:layout_goneMarginRight要配合android:layout_marginRight一起使用。
* 如果只设置了app:layout_goneMarginRight没有设置android:layout_marginRight,则无效。(alpha版本的bug,1.0.1版本已经修复)
* 在约束的布局gone时,控件自身的marginXXX会被goneMarginXXX替换掉,以本文Demo为例,原本button4宽度是100,button5的marginRight是10, 加起来是110,如果想让button4隐藏之后,button5仍然纹丝不动,则需要设置goneMarginRight为10+100 = 110.

居中定位和倾向(Centering positioning and bias)

居中定位

约束布局一个有用的地方是它如何处理“不可能”的约束。
比如你定义如下:

<android.support.constraint.ConstraintLayout
    ...
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="button"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>
</android.support.constraint.ConstraintLayout>

按照我们第一小节讲的属性值,这个定义的意思是,Button的左边和父控件的左边对齐,Button的右边和父控件的右边对齐。
可是控件是wrap_content的,它如果不铺满父控件要如何能满足这两个约束呢?
实际效果如下:

控件会居中显示,因为这两个约束作用 类似于 水平方向上,有相反的力 去拉控件,最终控件会居中显示。

倾向(Bias)

搭配bias,能使约束偏向某一边,默认是0.5,有以下属性:
* layout_constraintHorizontal_bias (0最左边 1最右边)
* layout_constraintVertical_bias (0最上边 1 最底边)

比如上个Demo,我加入app:layout_constraintHorizontal_bias="0.9" ,则会在水平方向上向右偏移至90%。

<android.support.constraint.ConstraintLayout
    ...
    android:layout_width="match_parent"
    android:layout_height="match_parent">
    <Button
        ...
        app:layout_constraintHorizontal_bias="0.9"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"/>
</android.support.constraint.ConstraintLayout>

对可见性的处理(Visibility behavior)

这一节是对前一节goneMargin的补充。
重点是Gone隐藏掉的控件,会被解析成一个点,并忽略margin。

ConstraintLayout能为View.GoneView特殊处理。
通常,GONE的控件不会被显示,并且不是布局本身的一部分(即如果标记为GONE,则其实际尺寸并不会更改)。
但是在布局计算方面,GONE的View仍然是其中的一个重要区别:
对于布局传递,它们的维度将被视为零(基本上它们将被解析为一个点
如果他们对其他小部件有约束力,那么他们仍然会受到尊重,但任何margin都将等于零


注意A的margin也被忽略了。

拿上个Demo改一下,为A 加上一个android:layout_marginRight="10dp"
为了使A 隐藏后,B仍能纹丝不动,则B的app:layout_goneMarginRight="120dp"
B goneMarginRight120 = A宽度100 + A marginRight10 +B marginRight10

    <Button
        android:id="@+id/button4"
        android:layout_width="100dp"
        android:layout_height="wrap_content"
        android:layout_marginRight="10dp"
        android:text="button4"
        app:layout_constraintRight_toRightOf="parent"
        />
    <Button
        android:id="@+id/button5"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginRight="10dp"
        android:text="button5"
        app:layout_constraintRight_toLeftOf="@id/button4"
        app:layout_goneMarginRight="120dp"/>

尺寸约束(Dimensions constraints)

ConstraintLayout的最小尺寸 (Minimum dimensions on ConstraintLayout)

可以为ConstraintLayout 自身定义最小的尺寸,他会在 ConstraintLayoutWRAP_CONTENT时起作用。
● android:minWidth
● android:minHeight

控件尺寸约束(Widgets dimension constraints)

控件的宽高有三种方式为其设置:
* 确定尺寸
* WRAP_CONTENT
* 0dp,就等于MATCH_CONSTRAINT

有些人可能有疑问,为什么不用MATCH_PARENT了。
官方文档如是说:

MATCH_PARENT is not supported for widgets contained in a ConstraintLayout, though similar behavior can be defined by using MATCH_CONSTRAINT with the corresponding left/right or top/bottom constraints being set to “parent”.

意思是MATCH_PARENT不再被支持了,通过MATCH_CONSTRAINT替代。
我们写个Demo看一下三种方式设置的效果吧:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
    ...
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:text="Button"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <Button
        android:id="@+id/button10"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintLeft_toLeftOf="@+id/button"
        app:layout_constraintRight_toRightOf="@+id/button"
        app:layout_constraintTop_toBottomOf="@+id/button"/>

    <Button
        android:id="@+id/button11"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintLeft_toLeftOf="@+id/button10"
        app:layout_constraintRight_toRightOf="@+id/button10"
        app:layout_constraintTop_toBottomOf="@+id/button10"/>

</android.support.constraint.ConstraintLayout>

效果如图:


有些人是不是要说,你特么逗我,不是说好的0dp等于MATCH_CONSTRAINT,应该是撑满屏幕的呀,
OK ,把刀放下。让我们仔细看这个MATCH_CONSTRAINT属性。它match的是约束。
而这里第三个按钮的约束是第二个按钮,所以它的宽度设置为MATCH_CONSTRAINT 时,是和它的约束按钮,即第二个按钮一样宽。
注意,此时,竖直方向上没有约束,所以不能使用MATCH_CONSTRAINT属性.

我们仅仅将第三个按钮的属性修改为

        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"

则它宽度会撑满屏幕:

我们再修改Demo,分别为后两个按钮加上margin:

<android.support.constraint.ConstraintLayout
    ...
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <Button
        android:id="@+id/button"
        android:layout_width="200dp"
        android:layout_height="100dp"
        android:text="Button"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintTop_toTopOf="parent"/>

    <Button
        android:id="@+id/button10"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintLeft_toLeftOf="@+id/button"
        app:layout_constraintRight_toRightOf="@+id/button"
        app:layout_constraintTop_toBottomOf="@+id/button"/>


    <Button
        android:id="@+id/button12"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="10dp"
        app:layout_constraintLeft_toLeftOf="@id/button10"
        app:layout_constraintRight_toRightOf="@id/button10"
        app:layout_constraintTop_toBottomOf="@id/button10"/>

    <Button
        android:id="@+id/button11"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginRight="10dp"
        android:text="Button"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/button12"/>

</android.support.constraint.ConstraintLayout>

效果如图:

最后,记住一句话约束要和 0dp 的 方向一致。否则无效

比例(Ratio)

只有一个方向约束:

可以以比例去定义View的宽高
为了做到这一点,需要将至少一个约束维度设置为0dp(即MATCH_CONSTRAINT
并将属性layout_constraintDimentionRatio设置为给定的比例。

例如:

    <Button
        android:layout_width="200dp"
        android:layout_height="0dp"
        android:text="Ratio"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="2:1"
        app:layout_constraintTop_toTopOf="parent"/>

如图:

比例值有两种取值:
* 浮点值,表示宽度和高度之间的比率 (2,0.5)
* “width:height”形式的比例 (5:1,1:5)

当约束多于一个(宽高都被约束了)

如果两个维度均设置为MATCH_CONSTRAINT(0dp),也可以使用比例。 在这种情况下,系统会使用满足所有约束条件和比率的最大尺寸
如果需要根据一个维度的尺寸去约束另一个维度的尺寸。
则可以在比率值的前面添加 W 或者 H 来分别约束宽度或者高度

例如,如果一个尺寸被两个目标约束(比如宽度为0,在父容器中居中),可以使用 W 或H 来指定哪个维度被约束。

    <Button
        android:layout_width="0dp"
        android:layout_height="0dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintDimensionRatio="H,2:1"
        app:layout_constraintTop_toTopOf="parent"/>

这里用“H”表示以高度为约束,高度的最大尺寸就是父控件的高度,“2:1”表示高:宽 = 2 : 1.
则宽度为高度的一半:

链条(Chains)

链条在同一个轴上(水平或者垂直)提供一个类似群组的统一表现。另一个轴可以单独控制。

创建链条(Creating a chain)

如果一组小部件通过双向连接(见图,显示最小的链,带有两个小部件),则将其视为链条。

链条头(Chain heads)

链条由在链的第一个元素(链的“头”)上设置的属性控制:

头是水平链最左边的View,或垂直链最顶端的View。

链的margin(Margins in chains)

如果在连接上指定了边距,则将被考虑在内。
例如

    <Button
        android:id="@+id/buttonA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="50dp"
        android:text="Button"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/buttonB"/>

    <Button
        android:id="@+id/buttonB"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintLeft_toRightOf="@+id/buttonA"
        app:layout_constraintRight_toRightOf="parent"/>

通过app:layout_constraintRight_toLeftOf="@+id/buttonB"app:layout_constraintLeft_toRightOf="@+id/buttonA"就建立了链条,(我中有你,你中有我)。
然后它们两个成了一个整体,所以链条左边设置app:layout_constraintLeft_toLeftOf="parent" 使得和父控件左对齐,
右边设置app:layout_constraintRight_toRightOf="parent"使得和父控件右对齐,
这样整个链条就居中了,最后对左控件设置了margin,相当于整个链条左边有了margin
效果:

链条样式(Chain Style)

当在链的第一个元素上设置属性 layout_constraintHorizontal_chainStylelayout_constraintVertical_chainStyle 时,链的行为将根据指定的样式(默认为CHAIN_SPREAD)而更改。
看图这里就很像JS里的flexible有木有。因为ConstraintLayout就是模仿flexible做的。


取值如下:
* spread - 元素将被展开(默认样式)
* 加权链 - 在spread模式下,如果某些小部件设置为MATCH_CONSTRAINT,则它们将拆分可用空间
* spread_inside - 类似,但链的端点将不会扩展
* packed - 链的元素将被打包在一起。 孩子的水平或垂直偏差属性将影响包装元素的定位

拿加权链举个例子:

    <Button
        android:id="@+id/buttonA"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginLeft="50dp"
        android:text="Button"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/buttonB"/>

    <Button
        android:id="@+id/buttonB"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintLeft_toRightOf="@+id/buttonA"
        app:layout_constraintRight_toRightOf="parent"/>

加权链(Weighted chains)

LinearLayout的weight类似。

链的默认行为是在可用空间中平均分配元素。 如果一个或多个元素使用MATCH_CONSTRAINT,它们将使用剩余的空白空间(在它们之间相等)。 属性layout_constraintHorizontal_weightlayout_constraintVertical_weight将决定这些都设置了MATCH_CONSTRAINT的View如何分配空间。 例如,在包含使用MATCH_CONSTRAINT的两个元素的链上,第一个元素使用权重为2,第二个元素的权重为1,第一个元素占用的空间将是第二个元素的两倍

最后关于链条,再给大家看一个关于margin的demo:

    <Button
        android:id="@+id/buttonA"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginLeft="50dp"
        android:text="Button"
        app:layout_constraintHorizontal_chainStyle="spread"
        app:layout_constraintLeft_toLeftOf="parent"
        app:layout_constraintRight_toLeftOf="@+id/buttonB"/>

    <Button
        android:id="@+id/buttonB"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:text="Button"
        app:layout_constraintLeft_toRightOf="@+id/buttonA"
        app:layout_constraintRight_toRightOf="parent"/>


一图胜千言,可以看到虽然他们的weight相等,但是margin是被计算在约束里的,所以左边的按钮宽度比右边的小。


Guideline

Guideline只能用于ConstraintLayout中,是一个工具类,不会被显示,仅仅用于辅助布局
它可以是horizontal或者 vertical的。(例如:android:orientation="vertical"
* verticalGuideline宽度为零,高度为ConstraintLayout的高度
* horizontalGuideline高度为零,宽度为ConstraintLayout的高度

定位Guideline有三种方式:
* 指定距离左侧或顶部的固定距离(layout_constraintGuide_begin
* 指定距离右侧或底部的固定距离(layout_constraintGuide_end
* 指定在父控件中的宽度或高度的百分比layout_constraintGuide_percent

一个栗子一看便知:

<android.support.constraint.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <android.support.constraint.Guideline
        android:id="@+id/guideline"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        app:layout_constraintGuide_begin="100dp"/>

    <Button
        android:id="@+id/button"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="16dp"
        android:text="Button"
        app:layout_constraintLeft_toLeftOf="@+id/guideline"
        app:layout_constraintTop_toTopOf="parent"/>

</android.support.constraint.ConstraintLayout>

预览:

Guideline源码:

public class Guideline extends View {
    public Guideline(Context context) {
        super(context);
        super.setVisibility(8);
    }
    public Guideline(Context context, AttributeSet attrs) {
        super(context, attrs);
        super.setVisibility(8);
    }
    public Guideline(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        super.setVisibility(8);
    }
    public Guideline(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr);
        super.setVisibility(8);
    }
    public void setVisibility(int visibility) {
    }
    public void draw(Canvas canvas) {
    }
    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
        this.setMeasuredDimension(0, 0);
    }
}
public static final int GONE = 0x00000008;

源码就这么点,这货的源码和ViewStub有点像啊,可以看出
* 它默认是GONE的。8 就是View.GONE的值。
* 它的public void setVisibility(int visibility)方法被空实现了,所以用户也没办法改变它的可见度。
* 推导出它一定是GONE的。在屏幕上不可见
* this.setMeasuredDimension(0, 0);public void draw(Canvas canvas)的空实现,表明这是一个超轻量的View,不可见,没有宽高,也不绘制任何东西。仅仅作为我们的锚点使用。

总结

很久不写博客了,一是工作太忙了,二也是随意的写怕误人子弟。
这篇文章我写了整整一天,每个例子我都边写边跑了一遍,
也看了几篇别人的文章,有些人简单的翻译了官方文档,但是对文档中一些没有举例, 不那么好理解的地方也没有说明,于是便有了此文。
关于可视化操作,建议直接看郭神博文可视化操作
我觉得ConstraintLayout ,有这两篇就够了。

文中代码地址在我的Demo合集中:
https://github.com/mcxtzhang/Demos/tree/master/constraintlayoutdemo/src/main

想看我更多文章:【张旭童的博客】http://blog.csdn.net/zxt0601
想来gayhub和我gaygayup:【mcxtzhang的Github主页】https://github.com/mcxtzhang

作者:zxt0601 发表于2017/5/24 18:04:09 原文链接
阅读:113 评论:0 查看评论

当Kotlin遇见RxJava多数据源

$
0
0

温馨提醒

阅读本文最好有Kotlin基础,若没有基础,可参考之前文章Kotlin初探使用Kotlin优雅的开发Android应用,以及RxJava基础(本文基于RxJava2),当然我也会尽可能详细解释让你顺利阅读本文。

源码传送门

写在前面

最近几天回过头,看了之前的总结RxJava操作符系列,感觉对Rxjava多数据源的处理不是很理解,所以在总结学习一波。大家都知道,最近Kotlin语言一直占据热搜榜,褒贬不一,但我想说,不管有什么想法都要抛在脑后,毕竟Google爸爸出手,你不情愿也要跟随它的步伐。鉴于此,本篇对RxJava多数据源的总结是基于Kotlin语言,也让大家明白,使用Kotlin开发应用并不是不能使用Java库,现在有一部分人担心,Kotlin第三方库那么少,如果使用Kotlin开发那不是给自己找罪受,其实你完全错了,当你说这话的时候,我敢断定你都还没有接触Kotlin,因为Koltin有一个最重要的优势就是和Java绝对兼容。

多数据源处理操作符

在RxJava中多数据源处理的操作符很多,但是最经典的就要数merge,contact,zip了。如果对这三个操作符不是很熟悉的话,可以去查看它的使用,当然如果你懒得去看,我也会简单提一下。merge操作符可以处理多个Observable发送的数据,它是一个异步操作,不保证数据发送的顺序,即有可能出现数据交叉,当一个Observable发送了onError后,未执行的Observable不在继续执行,直接执行merge的onError方法。

contact操作符执行时一个同步操作,严格按照contact中传入Observable先后执行,即前面的先执行后面的后执行,并且最终发送的数据也是有序的,即第一个Observable的数据发送完毕再发送第二个,依次类推。

zip操作符和contact和merge有了本质的区别,它会将每个Observable个数据项分布对应返回一个Observable再发送,最终发送的数据量与最小数据长度相同。

使用场景分析

假如现在我们有三种商品,有一个查询商品信息的接口,根据接口可以查询该商品的价格以及出售地点。商品实体类

data class Goods(var id:Int,var price: Int, var address: String)

在Kotlin语言中,实体类创建用data class 关键词,我们不需要和Java一样创建get/set方法,只需一行代码搞定。

创建模拟网络请求

object NetRequest {
    //模拟网络请求
    fun getGoodsObservable(id: Int): Observable<Goods> {
fun getGoodsObservable(id: Int): Observable<Goods> {
        return Observable.create {
            source ->
            Thread.sleep(Random().nextInt(1000).toLong())
            var data = Goods(id, Random().nextInt(20), "地址${id}")
            source.onNext(data)
            source.onComplete()
            Log.e("getGoodsObservable:", "${id}")
        }
    }
}

在上面我们创建了一个单例类,在Kotlin中使用object修饰类时即给我们自动创建了一个单例对象。在每一句代码结尾我们不需要再和Java一样写一个分号“;”来结束,什么也不用写。

Observable.create使用的是lambda表达式,在Kotlin语言中是支持lambda表达式的。source 就是ObservableEmitter,所以我们可以调用onNext发送数据。为了更准确的模拟网络请求,使用Thread.sleep随机的延迟,模拟网络请求的时间。

  fun getGoodsObservable(id: Int): Observable<Goods> {
        return Observable.create {
            source ->
            Thread.sleep(Random().nextInt(1000).toLong())
            var data = Goods(id, Random().nextInt(20), "地址${id}")
            source.onNext(data)
            source.onComplete()
            Log.e("getGoodsObservable:", "${id}")
        }

当然由于subscribe只有一个参数,所以我们也可以这样写。也就是省略了source ->,此时it就表示该参数数据。

return Observable.create {
            Thread.sleep(Random().nextInt(1000).toLong())
            var data = Goods(id, Random().nextInt(20), "地址${id}")
            it.onNext(data)
            it.onComplete()
            Log.e("getGoodsObservable:", "${id}")
        }

在java中实现如下

  return Observable.create(new ObservableOnSubscribe<Goods>() {

            @Override
            public void subscribe(@NonNull ObservableEmitter<Goods> e) throws Exception {
             //处理逻辑
            }
        });

merge

准备好了请求操作,开始使用merge看看执行的效果。

  fun executeMerge() {
        Observable.merge(getGoodsObservable(1).subscribeOn(Schedulers.newThread()),
                getGoodsObservable(2).subscribeOn(Schedulers.newThread()),
                getGoodsObservable(3).subscribeOn(Schedulers.newThread()))
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .toList()
                .subscribe({
                    Log.e(TAG, it.toString())
                }, {
                    Log.e(TAG, it.toString())
                })
    }

merge中有三个网络请求操作,并通过subscribeOn(Schedulers.newThread())将网络请求切换到线程中执行,数据都请求成功后,再通过observeOn(AndroidSchedulers.mainThread())切换到主线程请求数据。为了三请求都成功后,我们在更新UI,所以通过toList()将请求的数据转换成List一块发送。在上面的subscribe依然使用的lambda表达式,subscribe({},{})中第一个括号是onSuccess回调,里面的it是接收到的List< Goods >数据,第二个括号是onError回调,it表示异常Throwable对象。
subscribe部分Java代码

.subscribe(new Consumer<List<Goods>>() {
                    @Override
                    public void accept(@NonNull List<Goods> goodses) throws Exception {

                    }
                }, new Consumer<Throwable>() {
                    @Override
                    public void accept(@NonNull Throwable throwable) throws Exception {

                    }
                });

当然如果你想使用RxJava2中onSubscribe(@NonNull Disposable d) ,你可以这样使用subscribe

.subscribe(object : SingleObserver<List<Goods>> {
                    override fun onSubscribe(d: Disposable?) {
                    }
                    override fun onError(e: Throwable?) {
                    }
                    override fun onSuccess(t: List<Goods>?) {
                    }
                })

为了观察,我们将请求成功的数据显示在界面上,我们创建一个Button,TextView。

class MainActivity : AppCompatActivity(), View.OnClickListener {

    val TAG = "MainActivity"
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)
        setSupportActionBar(toolbar)
        //加入这句import kotlinx.android.synthetic.main.activity_main.*
        //不用再findViewById,可直接使用
        merge.setOnClickListener(this)

    }
    override fun onClick(v: View) {
        when (v.id) {
            R.id.merge -> {
                executeMerge()
            }
        }
        //when 关键字和Java中的Switch关键词是类似的,
        //只不过它比Java中的Switch强大的多,可以接收任何参数,
        //然后判断使用,也可以如下使用
        when (v) {
            merge -> {
            }
        }
    }
}

contact

我们点击执行几次发现,返回的List的数据并不是按照merge参数的先后顺序执行的,它是并发的,最终的顺序,是由网络请求的快慢决定的,请求返回数据越快也就表示该数据最早发送,即在List中最靠前。那么此时出现一个问题,如果我想返回数据的List顺序严格按照位置的先后顺序呢?那此时使用merge的话,是不太现实了。当然前面我们提到contact可以使用。那么直接将merge更改为contact执行以下试试,

    fun executeContact() {
        Observable.concat(getGoodsObservable(1).subscribeOn(Schedulers.newThread()),
                getGoodsObservable(2).subscribeOn(Schedulers.newThread()),
                getGoodsObservable(3).subscribeOn(Schedulers.newThread()))
                .subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .toList()
                .subscribe({
                    Log.e(TAG, it.toString())
                }, {
                    Log.e(TAG, it.toString())
                })
    }

的确,发现无论执行多少次List的数据都能按照contact中Observable顺序发送,我们想要的效果可以实现了,不过你会发现,效率太差了,这是同步执行啊,只有第一个请求成功,才会去请求第二个,然后第三个,假如一次请求需要一秒,那三次请求至少三秒啊,不能忍。

zip

鉴于上面两种方式的利弊,如果我们既想如merge一样并发执行,又想和contact一样保证顺序,是不是有点强迫症的意思,当然强大的zip就能实现我们想要的效果。如下实现。.

    fun executeZip() {
        Observable.zip(getGoodsObservable(1),
                getGoodsObservable(2),
                getGoodsObservable(3),
                Function3<Goods, Goods, Goods, List<Goods>>
                { goods0, goods1, goods2 ->
                    val list = ArrayList<Goods>()
                    list.add(goods0)
                    list.add(goods1)
                    list.add(goods2)
                    list
                }).subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    Log.e(TAG, it.toString())
                }, {
                    Log.e(TAG, it.toString())
                })
    }

既然实现了,那我们运行几次,发现完美的实现了我们想要的效果,即并发的执行了,也保证了我们请求数据的顺序性。

在回调中运用RxJava

在上面我们的单个网络请求是一个同步的请求,如果我们的网络请求封装了,在线程中请求,请求成功后在主线程中回调,那我们又该如何创建呢使用呢?
先来模拟一个子线程请求网络,请求成功回调数据给主线程。

    fun getGoods(ctx:Context,id: Int,callbacks:(goods:Goods)->Unit): Unit {
        ctx.doAsync {
            Thread.sleep(Random().nextInt(1000).toLong())
            var data = Goods(id, Random().nextInt(20), "地址${id}")
            ctx.runOnUiThread {
                callbacks(data)
            }
        }
    }

getGoods传了三个参数,第一个Context对象,第二个是商品ID,第三个参数是一个函数,(goods:Goods)->Unit表示第三个参数的类型是一个参数为Goods类型并且返回Unit的函数。使用doAsync 模拟异步请求,请求成功后runOnUiThread 切换到UI线程。然后callbacks(data)将数据回调。这种使用方式比Java中回调优美好用太多了。
接下来就开始在回调成功后创建Observable

  fun getGoodsCallBack(id: Int): Observable<Goods> {
        var subscrbe: ObservableEmitter<Goods>? = null
        var o = Observable.create<Goods> {
            subscrbe = it
        }
        //Kotlin特性
        getGoods(this@MainActivity, id) {
            subscrbe?.onNext(it)
        }
        return o
    }
    fun executeZipCallBack() {
        Observable.zip(getGoodsCallBack(1).subscribeOn(Schedulers.newThread()),
                getGoodsCallBack(2).subscribeOn(Schedulers.newThread()),
                getGoodsCallBack(3).subscribeOn(Schedulers.newThread()),
                Function3<Goods, Goods, Goods, List<Goods>>
                { goods0, goods1, goods2 ->
                    val list = ArrayList<Goods>()
                    list.add(goods0)
                    list.add(goods1)
                    list.add(goods2)
                    list
                }).subscribeOn(Schedulers.newThread())
                .observeOn(AndroidSchedulers.mainThread())
                .subscribe({
                    Log.e(TAG, it.toString())
                }, {
                    Log.e(TAG, it.toString())
                })
    }

ok,到这里回调情况下创建使用RxJava也介绍完毕,到此本篇文章就结束了,有问题欢迎指出,内容杂乱,多多担待,Hava a wonderful day.

作者:xiehuimx 发表于2017/5/25 0:43:25 原文链接
阅读:304 评论:0 查看评论

IOS端K线系列之K线-绘制OHLC图、线段、圆形

$
0
0

k线系列目录

查看目录请点击这儿


接着上篇文章继续说。在上文中我们把蜡烛绘制完,也讲到在K线中常见的几种图形:

  1. 蜡烛
  2. OHLC
  3. 线段
  4. 带状
  5. 圆形

那在这篇文章里,我们把其余的也绘制一下,争取做一个基础的类库,这样在开发框架时就可以直接使用。


OHLC图

先上两张效果图:

OHLC图

OHLC图

如图所示,它是由4个数据生成:开盘、收盘、最高、最低。中间的线是最高点至最低点连接而成,两边的分别是开盘点和收盘点与中间线连接而成。

OK,知道思路以后,直接上代码:

/**
 生成OHLC

 @param model 蜡烛坐标模型
 @return 返回图层
 */
+ (CAShapeLayer *)getOHLCLayerWithPointModel:(YKCandlePointModel *)model
{
    //判断是否为涨跌
    BOOL isRed = model.oPoint.y >= model.cPoint.y ? YES : NO;

    UIBezierPath *path = [UIBezierPath bezierPath];
    //绘制上下影线
    [path moveToPoint:model.lPoint];
    [path addLineToPoint:model.hPoint];
    //开盘线
    [path moveToPoint:model.oPoint];
    [path addLineToPoint:CGPointMake(model.oPoint.x -6, model.oPoint.y)];
    //开盘线
    [path moveToPoint:model.cPoint];
    [path addLineToPoint:CGPointMake(model.cPoint.x +6, model.cPoint.y)];

    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.path = path.CGPath;

    //判断涨跌来设置颜色
    if (isRed)
    {
        //涨,设置红色
        layer.strokeColor = [UIColor redColor].CGColor;
    } else
    {
        //跌,设置绿色
        layer.strokeColor = [UIColor greenColor].CGColor;
    }
    layer.fillColor = [UIColor clearColor].CGColor;

    return layer;
}

单、多条线

效果图:

线段

上图中的淡绿色、粉色、黄色线就是我们所要绘制的,这里再解释一下为什么标题是单、多条线,因为在有的K线指标中规定某一个值是用一条连续的线段表示;但有的指标数值中间会有无效值,那无效值就不会连接,这样的话这条线段就是不连续的,形成多条线。

其实在分时线绘制的时候,也说过如何绘制一条线,这里就不再赘述了,直接上代码:

单条线工具类:

/**
 生成单条线

 @param pointArr 坐标点数组
 @param lineColor 线颜色
 @return 返回线段图层
 */
+ (CAShapeLayer *)getSingleLineLayerWithPointArray:(NSArray *)pointArr lineColor:(UIColor *)lineColor
{
    UIBezierPath *path = [UIBezierPath getBezierPathWithPointArr:pointArr];

    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.path = path.CGPath;
    layer.lineWidth = 1.f;
    layer.strokeColor = lineColor.CGColor;
    layer.fillColor = [UIColor clearColor].CGColor;

    return layer;
}

多条线工具类:

/**
 生成包含多条线的线段

 @param pointArr 坐标点数组
 @param lineColor 线颜色
 @return 返回线段图层
 */
+ (CAShapeLayer *)getMultipleLineLayerWithPointArray:(NSArray *)pointArr lineColor:(UIColor *)lineColor
{
    UIBezierPath *path = [UIBezierPath bezierPath];

    for (int idxX=0; idxX<pointArr.count; idxX++)
    {
        NSArray *idxXArr = pointArr[idxX];

        [path moveToPoint:[[idxXArr firstObject] CGPointValue]];
        for (int idxY=1; idxY<idxXArr.count; idxY++)
        {
            [path addLineToPoint:[idxXArr[idxY] CGPointValue]];
        }
    }

    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.path = path.CGPath;
    layer.lineWidth = 1.f;
    layer.strokeColor = lineColor.CGColor;
    layer.fillColor = [UIColor clearColor].CGColor;

    return layer;
}

圆形

接下来是圆形,圆形就比较简单了,就是那么一个单纯的圆。只是在开发中,一般都是由多个圆形成一定的趋势。

这里直接上代码:

/**
 生成圆

 @param point 圆心坐标
 @param color 圆颜色
 @return 返回图层
 */
+ (CAShapeLayer *)getCircleLayerWithPoint:(CGPoint )point color:(UIColor *)color
{
    //直接用圆描述路径
    UIBezierPath *path = [UIBezierPath bezierPathWithArcCenter:point radius:3.f startAngle:0.f
  endAngle:(CGFloat)M_PI * 2 clockwise:true];
    //生成图层 并设置路径和属性
    CAShapeLayer *layer = [CAShapeLayer layer];
    layer.path = path.CGPath;
    layer.fillColor = color.CGColor;
    layer.strokeColor = color.CGColor;

    return layer;
}

带状

这里要说明一下,带状这种图形用文字叙述不清楚,需要结合实际的数据才能理解。所以这里就先不写Demo,等到后面我们在框架中接入实际数据了,再详述。

绘制效果

上面光发代码了,这里把最后的运行结果发上来:

运行结果

需要源码的点击这里

作者:yunkai666 发表于2017/5/25 15:30:11 原文链接
阅读:41 评论:0 查看评论

React Native之react-native-scrollable-tab-view详解

$
0
0

在React Native开发中,官方为我们提供的Tab控制器有两种:TabBarIOS和ViewPagerAndroid。TabBarIOS,仅适用于IOS平台
ViewPagerAndroid,仅适用于Android平台(严格来讲并不算,因为我们还需要自己实现Tab)。在项目开发中,我们优先选择一些开源兼容性比较好的第三方库,例如,react-navigation,以及本文即将说到的react-native-scrollable-tab-view(官方地址)。react-native-scrollable-tab-view不仅可以实现顶部的Tab切换,还能实现底部的切换。
这里写图片描述这里写图片描述

我们再来看一下官方的Demo。
这里写图片描述

属性及方法介绍

1, renderTabBar(Function:ReactComponent)

TabBar的样式,系统提供了两种默认的,分别是DefaultTabBar和ScrollableTabBar。当然,我们也可以自定义一个,我们会在下篇文章重点讲解如何去自定义TabBar样式。
注意:每个被包含的子视图需要使用tabLabel属性,表示对应Tab显示的文字。
DefaultTabBar:Tab会平分在水平方向的空间。
ScrollableTabBar:Tab可以超过屏幕范围,滚动可以显示。

render() {
  return (
    <ScrollableTabView
      renderTabBar={() => <DefaultTabBar/>}>
      <Text tabLabel='Tab1'/>
      <Text tabLabel='Tab2'/>
      <Text tabLabel='Tab3'/>
      <Text tabLabel='Tab4'/>
      <Text tabLabel='Tab5'/>
      <Text tabLabel='Tab6'/>
    </ScrollableTabView>
  );
}

2,tabBarPosition(String,默认值’top’)
top:位于屏幕顶部
bottom:位于屏幕底部
overlayTop:位于屏幕顶部,悬浮在内容视图之上(看颜色区分:视图有颜色,Tab栏没有颜色)
overlayBottom:位于屏幕底部,悬浮在内容视图之上(看颜色区分:视图有颜色,Tab栏没有颜色)

render() {
  return (
    <ScrollableTabView
      tabBarPosition='top'
      renderTabBar={() => <DefaultTabBar/>}>
      ...
    </ScrollableTabView>
  );
}

3, onChangeTab(Function)
Tab切换之后会触发此方法,包含一个参数(Object类型),这个对象有两个参数:
i:被选中的Tab的下标(从0开始)
ref:被选中的Tab对象(基本用不到)

render() {
  return (
    <ScrollableTabView
      renderTabBar={() => <DefaultTabBar/>}
      onChangeTab={(obj) => {
          console.log('index:' + obj.i);
        }
      }>
      ...
    </ScrollableTabView>
  );
}

4,onScroll(Function)
视图正在滑动的时候触发此方法,包含一个Float类型的数字,范围是[0, tab的数量-1]

render() {
  return (
    <ScrollableTabView
      renderTabBar={() => <DefaultTabBar/>}
      onScroll={(postion) => {  
          // float类型 [0, tab数量-1]  
          console.log('scroll position:' + postion);
        }
      }>
      ...
    </ScrollableTabView>
  );
}

5, locked(Bool,默认为false)
表示手指是否能拖动视图,默认为false(表示可以拖动)。设为true的话,我们只能“点击”Tab来切换视图。

render() {
  return (
    <ScrollableTabView
      locked={false}
      renderTabBar={() => <DefaultTabBar/>}>
      ...
    </ScrollableTabView>
  );
}

6, initialPage(Integer)
初始化时被选中的Tab下标,默认是0(即第一页)。

render() {
  return (
    <ScrollableTabView
      initialPage={1}
      renderTabBar={() => <DefaultTabBar/>}>
      ...
    </ScrollableTabView>
  );
}

7,page(Integer)
设置选中指定的Tab。

8,children(ReactComponents)
表示所有子视图的数组,比如下面的代码,children则是一个长度为6的数组,元素类型为Text。

render() {
  return (
    <ScrollableTabView
      renderTabBar={() => <DefaultTabBar/>}>
      <Text tabLabel='Tab1'/>
      <Text tabLabel='Tab2'/>
      <Text tabLabel='Tab3'/>
      <Text tabLabel='Tab4'/>
      <Text tabLabel='Tab5'/>
      <Text tabLabel='Tab6'/>
    </ScrollableTabView>
  );
}

9,tabBarUnderlineStyle(style)
设置DefaultTabBar和ScrollableTabBarTab选中时下方横线的颜 色。
10.,tabBarBackgroundColor(String)
设置整个Tab这一栏的背景颜色
11,tabBarActiveTextColor(String)
设置选中Tab的文字颜色。
12,tabBarInactiveTextColor(String)
设置未选中Tab的文字颜色。
13,contentProps(Object)
这里要稍微说下react-native-scrollable-tab-view的实现,其实在Android平台底层用的是ViewPagerAndroid,iOS平台用的是ScrollView。这个属性的意义是:比如我们设置了某个属性,最后这个属性会被应用在ScrollView/ViewPagerAndroid,这样会覆盖库里面默认的,通常官方不建议我们去使用。
14,scrollWithoutAnimation(Bool,默认为false)
设置“点击”Tab时,视图切换是否有动画,默认为false(即:有动画效果)。

render() {
  return (
    <ScrollableTabView
      scrollWithoutAnimation={true}
      renderTabBar={() => <DefaultTabBar/>}>
      ...
    </ScrollableTabView>
  );
}

顶部导航示例

顶部导航的代码是比较简单的。例如,我们实现上图的新闻Tab导航的效果。
这里写图片描述

相关代码:

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

 import React, { Component } from 'react';
 import ScrollableTabView, {DefaultTabBar,ScrollableTabBar} from 'react-native-scrollable-tab-view';
 import {
   AppRegistry,
   StyleSheet,
   Text,
   Image,
   View
 } from 'react-native';

var Dimensions = require('Dimensions');
var ScreenWidth = Dimensions.get('window').width;

class TabTopView extends Component {
    render() {
        return (
            <ScrollableTabView
                style={styles.container}
                renderTabBar={() => <DefaultTabBar />}
                tabBarUnderlineStyle={styles.lineStyle}
                tabBarActiveTextColor='#FF0000'>

                <Text style={styles.textStyle} tabLabel='娱乐'>娱乐</Text>
                <Text style={styles.textStyle} tabLabel='科技'>科技</Text>
                <Text style={styles.textStyle} tabLabel='军事'>军事</Text>
                <Text style={styles.textStyle} tabLabel='体育'>体育</Text>
            </ScrollableTabView>
        );
    }
    }


 const styles = StyleSheet.create({
     container: {
         flex: 1,
         marginTop: 20
     },
     lineStyle: {
         width:ScreenWidth/4,
         height: 2,
         backgroundColor: '#FF0000',
     },
     textStyle: {
         flex: 1,
         fontSize:20,
         marginTop:20,
         textAlign:'center',
     },

 });

export default TabTopView;

然后在index.ios.js或index.android.js中导入组件。

export default class RNDemo extends Component {
    render() {
        return (
            <TabBottomView/>
        );
    }
}

底部Tab切换示例

这里写图片描述

需要注意的是项目中用到了Navigator这个组件,在最新的版本中,系统标识Navigator已经过时被抛弃,所以我们需要使用命令先按照相关的库:

npm install --save react-native-deprecated-custom-components

这里写图片描述

然后在使用的界面中导入Navigator。

import {
    Navigator,
} from 'react-native-deprecated-custom-components';

好了其他的不再说明,直接上代码:
TabBottomView.js

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow
 */

import React, {Component} from 'react';
import {
    Navigator,
} from 'react-native-deprecated-custom-components';

import TabBarView from './TabBarView'
import TabDefaultView from './TabDefaultView'

import {
    AppRegistry,
    StyleSheet,
    Text,
    Image,
    View,
    AlertIOS,
    StatusBar,
} from 'react-native';



var Dimensions = require('Dimensions');
var ScreenWidth = Dimensions.get('window').width;

class TabBottomView extends Component {

    counter = 0;
    configureScene = route => {
        if (route.sceneConfig) return route.sceneConfig

        return {
            ...Navigator.SceneConfigs.PushFromRight,
            gestures: {}    // 禁用左滑返回手势
        }
    }

    renderScene = (route, navigator) => {
        let Component = route.component
        return <Component navigator={navigator}{...route.passProps}/>
    }

    inc = () => {
        ++this.counter;
    };

    dec = () => {
        --this.counter;
    };

    OnChangeText = v => {
        try {
            this.counter = parseInt(v);
        } catch (err) {
        }

    };

    OnClickText = (title) => {
        alert('title=' + title);
    }

    render() {
        const initialPage = TabDefaultView;
        const initialPageName = 'TabBarView';

        return (
            <View style={styles.container}>
                <StatusBar barStyle={'light-content'}/>
                <Navigator
                    initialRoute={{name: initialPageName, component: initialPage}}
                    configureScene={this.configureScene}
                    renderScene={this.renderScene}/>
            </View>

        );
    }
}


const styles = StyleSheet.create({
    container: {
        flex: 1,
        marginTop: 20
    },


});

export default TabBottomView;

TabBottomView设计到的两个自定义View:
TabBarView.js

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow TextInput自动提示输入
 */

import React, {Component} from 'react';
import {
    AppRegistry,
    StyleSheet,
    Text,
    TouchableOpacity,
    Image,
    TextInput,
    View
}
from
'react-native';


class TabBarView extends Component {

    static propType = {
        goToPage    : React.PropTypes.func,
        activeTab   : React.PropTypes.number,
        tabs        : React.PropTypes.array,

        tabNames    : React.PropTypes.array,
        tabIconNames: React.PropTypes.array,
        selectedTabIconNames: React.PropTypes.array
    };

    componentDidMount() {
        this.props.scrollValue.addListener(this.setAnimationValue);
    }

    setAnimationValue({value}) {
        console.log(value);
    }

    render() {
        return (
            <View style={styles.tabs}>
                {this.props.tabs.map((tab, i) => {
                    let color = this.props.activeTab === i ? 'green' : 'gray';
                    let icon = this.props.activeTab == i ? this.props.selectedTabIconNames[i] : this.props.tabIconNames[i];
                    return (
                        <TouchableOpacity
                            key={i}
                            activeOpacity={0.8}
                            style={styles.tab}
                            onPress={()=>this.props.goToPage(i)}>
                            <View style={styles.tabItem}>
                                <Image
                                    style={styles.icon}
                                    source={icon}/>
                                <Text style={{color: color, fontSize: 12}}>
                                    {this.props.tabNames[i]}
                                </Text>
                            </View>
                        </TouchableOpacity>
                    )
                })}
            </View>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#ffffff',
        marginTop: 20
    },
    tabs: {
        flexDirection: 'row',
        height: 49,
        borderTopColor: '#d9d9d9',
        borderTopWidth:2
    },
    tab: {
        flex: 1,
        justifyContent: 'center',
        alignItems: 'center',
    },
    tabItem: {
        flexDirection: 'column',
        alignItems: 'center',
        justifyContent: 'space-around'
    },
    icon: {
        width: 26,
        height: 26,
        marginBottom: 2
    }
});

export default TabBarView;

TabDefaultView.js(默认界面)

/**
 * Sample React Native App
 * https://github.com/facebook/react-native
 * @flow TextInput自动提示输入
 */

import React, {Component} from 'react';
import TabBarView from './TabBarView'
import ScrollableTabView, {DefaultTabBar, ScrollableTabBar} from 'react-native-scrollable-tab-view';
import HomeScreen from '../widght/HomeScreen';
import MineScreen from '../widght/MineScreen';

import {
    AppRegistry,
    StyleSheet,
    Text,
    TouchableOpacity,
    Image,
    TextInput,
    StatusBar,
    View
}from 'react-native';

const tabTitles = ['首页', '我的']
//Tab图标
const tabIcon = [
    require('../image/tabbar_homepage.png'),
    require('../image/tabbar_mine.png'),
]
const tabSelectedIcon = [
    require('../image/tabbar_homepage_selected.png'),
    require('../image/tabbar_mine_selected.png'),
]

class TabDefaultView extends Component {

    onChangeTabs = ({i}) => 'light-content';

    render() {
        return (
            <ScrollableTabView
                renderTabBar={() =>
                    <TabBarView
                        tabNames={tabTitles}
                        tabIconNames={tabIcon}
                        selectedTabIconNames={tabSelectedIcon}/>
                }
                tabBarPosition='bottom'
                locked
                scrollWithoutAnimationz
                onChangeTab={this.onChangeTabs}>

                <HomeScreen tabLabel="Home" navigator={this.props.navigator}/>
                <MineScreen tabLabel="Mine" navigator={this.props.navigator}/>

            </ScrollableTabView>
        );
    }
}

const styles = StyleSheet.create({
    container: {
        flex: 1,
        backgroundColor: '#ffffff',
        marginTop: 20
    },
    value:{
        paddingHorizontal:10,
        paddingVertical:8,
        width:100,
        marginLeft:120,
    }
});

export default TabDefaultView;

最后在index.ios.js或index.android.js中导入组件。


export default class RNDemo extends Component {
    render() {
        return (
            <TabBottomView/>
        );
    }
}

附件:源码下载

作者:xiangzhihong8 发表于2017/5/25 16:40:29 原文链接
阅读:208 评论:0 查看评论

7. Cordova文件操作和IO

$
0
0

前言

这里学完了基本就告一段落了,文件操作按照原生来说,是需要一定权限的,这里就直接在root目录进行操作,存储位置位于内置app包名目录下。

这里代码已经写好了,今天要转java去写后台了,估计没时间写文档了,直接贴代码吧

构建

  • 安装插件
    cordova plugin add cordova-plugin-file

HTML代码

<!doctype html>
<html>
<head>
    <meta http-equiv="Content-Security-Policy" content="default-src 'self' data: gap: https://ssl.gstatic.com 'unsafe-eval'; style-src 'self' 'unsafe-inline'; media-src *; img-src 'self' data: content:;">
    <meta name="format-detection" content="telephone=no">
    <meta charset="utf-8" />
    <meta name="msapplication-tap-highlight" content="no">
    <meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width">
    <link rel="stylesheet" type="text/css" href="css/index.css">
</head>
<body>

<div class="app">
<h1>文件操作</h1>
    <div id="deviceready" class="blink">
        <p class="event listening">Connecting to Device</p>
        <p class="event received">Device is Ready</p>
    </div>
<button id = "createFile"></button>
<button id = "writeFile"></button>
<button id = "readFile"></button>
<button id = "removeFile"></button>
<button id="nextPage">NEXT PAGE</button>
<textarea id = "textarea"></textarea>
</div>


<script type="text/javascript" src="cordova.js"></script>
<script type="text/javascript" src="js/file_manager.js"></script>
<script type="text/javascript" src="js/jquery-3.1.1.min.js"></script>

</body>
</html>

JS代码

var app = {

    initialize: function () {
        document.addEventListener('deviceready', this.onDeviceReady.bind(this), false);
    },

    // deviceready Event Handler
    //
    // Bind any cordova events here. Common events are:
    // 'pause', 'resume', etc.
    onDeviceReady: function () {
        var datas=null;
        var filePath = "";
        var fileName = "";

        this.receivedEvent('deviceready');

        document.getElementById("createFile").addEventListener("click", createFile);
        document.getElementById("writeFile").addEventListener("click", writeFile);
        document.getElementById("readFile").addEventListener("click", readFile);
        document.getElementById("removeFile").addEventListener("click", removeFile);
        document.getElementById("nextPage").addEventListener("click", nextPage);

        function createFile() {
            //设置类型 [TEMPORARY , PERSISTENT]
            var type = window.TEMPORARY;
            //设置大小
            var size = 5 * 1024 * 1024;
            //请求文件系统 ,参数直接看下面
            window.requestFileSystem(type, size, successCallback, errorCallback)
            //创建一个hello.txt文件,存到到根目录
            function successCallback(fs) {
                fs.root.getFile('hello.txt', {create: true, exclusive: true}, function (fileEntry) {
                    console.log('successfull!')
                }, errorCallback);
            }

            function errorCallback(error) {
                alert("未创建成功:" + error)
            }

        }


        function writeFile() {
            var type = window.TEMPORARY;
            var size = 5 * 1024 * 1024;

            window.requestFileSystem(type, size, successCallback, errorCallback)

            function successCallback(fs) {

                fs.root.getFile('hello.txt', {create: true}, function (fileEntry) {
                    //在本FileEntry中创建一个和文件关联的FileWriter方法
                    fileEntry.createWriter(function (fileWriter) {
                        fileWriter.onwriteend = function (e) {
                            alert("写入成功!");
                        };

                        fileWriter.onerror = function (e) {
                            alert("写入失败:" + e.toString());
                        };
                        //获取文本框中的值
                        var blob = new Blob([$(document).ready(function () {
                            $("#textarea").val();
                        })], {type: 'text/plain'});
                        //想文件中写数据
                        fileWriter.write(blob);
                    }, errorCallback);

                }, errorCallback);

            }

            function errorCallback(error) {
                alert("ERROR: " + error.code)
            }

        }

        function readFile() {
            var type = window.TEMPORARY;
            var size = 5 * 1024 * 1024;

            window.requestFileSystem(type, size, successCallback, errorCallback)

            function successCallback(fs) {

                fs.root.getFile('hello.txt', {}, function (fileEntry) {

                    fileEntry.file(function (file) {
                        //创建文件输入流
                        var reader = new FileReader();
                        //读完数据赋值给文本域
                        reader.onloadend = function (e) {
                            var txtArea = document.getElementById('textarea');
                            txtArea.value = this.result;
                        };
                        //从关联文件中读数据
                        reader.readAsText(file);

                    }, errorCallback);

                }, errorCallback);
            }

            function errorCallback(error) {
                alert("ERROR: " + error.code)
            }

        }

        function removeFile() {
            var type = window.TEMPORARY;
            var size = 5 * 1024 * 1024;

            window.requestFileSystem(type, size, successCallback, errorCallback)

            function successCallback(fs) {
                fs.root.getFile('hello.txt', {create: false}, function (fileEntry) {

                    fileEntry.remove(function () {
                        alert('File removed.');
                    }, errorCallback);

                }, errorCallback);
            }

            function errorCallback(error) {
                alert("ERROR: " + error.code)
            }

        }

        function nextPage() {
            location.href = "callActivity.html";
        }

    },


    // Update DOM on a Received Event
    receivedEvent: function (id) {
        var parentElement = document.getElementById(id);
        var listeningElement = parentElement.querySelector('.listening');
        var receivedElement = parentElement.querySelector('.received');

        listeningElement.setAttribute('style', 'display:none;');
        receivedElement.setAttribute('style', 'display:block;');

        console.log('Received Event: ' + id);
    }

};

app.initialize();

JS代码的另一种简单模式,点击下一页之后的js代码



    // Wait for device API libraries to load
    //
    function onLoad() {
        document.addEventListener("deviceready", onDeviceReady, false);
    }

    // device APIs are available
    //
    function onDeviceReady() {
        // Now safe to use device APIs
        alert("设备准备就绪,给你看个空页面!");
    }
作者:lftaoyuan 发表于2017/5/25 18:03:36 原文链接
阅读:82 评论:0 查看评论

《Android群英传》笔记5——自定义View

$
0
0

自定义View

本文是读了《Android 群英传》第三章--Android体控件架构与自定义空间详解--之后的读书笔记,感谢作者,在此特别推荐此书。

      Android给我们提供了常用组件,然而随着开发的深入,这些组件渐渐无法满足我们各式各样的需求,此时就需要我们在已有的组件上创建新的功能,甚至是直接自己写一个新的View控件,来满足自己的需要。这就是我们常说的自定义View

  在自定义View时候,我们常常会重写onDraw()方法来重新绘制我们的控件;当该控件需要用wrap_content属性时候,还需要用到onMearsure()方法来重新测量;以及需要一些特殊样式时候,还可以通过修改attrs.xml(或者写其他xml)来设置控件属性。

  在View中一些重要的回调方法有:


      onFinishInflate():加载完XML组件后回调。

      onSizeChanged():当组件大小变化时回调。

      onMearsure():通过回调该方法进行控件测量。

      onLayout():通过回调该方法确定控件显示的位置。

      onTouchEvent():监听到触摸事件后回调。

 

      通常情况下,我们用三种方法来实现自定义的控件:

      l 对现有控件的扩展;

      l 通过组合方式实现新的控件;

      l 通过重写View实现新的控件。

 

1.对现有控件的扩展

  对现有控件的扩展指的是在Android原生控件的基础上进行一些功能扩展。一般情况下都是在进行重写onDraw方法来实现。

  以TextView为例,我们想在写的字下面加上两层背景,再让字实现闪烁滚动的效果。此时就可以先自定义View继承TextView类,然后可以重写onDraw()的方法。

  注意下重写onDraw()方法时候,super.onDraw()方法的位置。这个方法是继承于父类,对于TextView来说就是显示文字。所以我们需要将绘制背景写在super.onDraw()方法前,而将字体闪烁设置写在super.onDraw()方法后。如下所示:

@Override
protected void onDraw(Canvas canvas) {
    canvas.drawRect(0,0,getMeasuredWidth()+10,getMeasuredHeight()+10,mPaint1);
    canvas.drawRect(10,10,getMeasuredWidth()-10,getMeasuredHeight()-10,mPaint2);
    canvas.save();
    canvas.translate(10,0);
    super.onDraw(canvas);
    canvas.restore();
    if(matrix!=null){
        mTranslate+=mViewwidth/5;
        if(mTranslate>2*mViewwidth){
            mTranslate=-mViewwidth;
        }
        matrix.setTranslate(mTranslate,0);
        linearGradient.setLocalMatrix(matrix);
        postInvalidateDelayed(100);
    }
}
  完整的代码为:

public class TestTextView extends TextView {
    private Paint mPaint;
    private Paint mPaint1;
    private Paint mPaint2;
    private int mTranslate;
    private int mViewwidth=0;
    private Matrix matrix;
    private LinearGradient linearGradient;

    public TestTextView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    public TestTextView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }
    public TestTextView(Context context) {
        super(context);
        init(context);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        canvas.drawRect(0,0,getMeasuredWidth()+10,getMeasuredHeight()+10,mPaint1);
        canvas.drawRect(10,10,getMeasuredWidth()-10,getMeasuredHeight()-10,mPaint2);
        canvas.save();
        canvas.translate(10,0);
        super.onDraw(canvas);
        canvas.restore();
        if(matrix!=null){
            mTranslate+=mViewwidth/5;
            if(mTranslate>2*mViewwidth){
                mTranslate=-mViewwidth;
            }
            matrix.setTranslate(mTranslate,0);
            linearGradient.setLocalMatrix(matrix);
            postInvalidateDelayed(100);
        }
    }

    @Override
    protected void onSizeChanged(int w,int h,int oldw,int oldh){
        super.onSizeChanged(w, h, oldw, oldh);
        if(mViewwidth==0){
            mViewwidth=getMeasuredWidth();
            if(mViewwidth>0){
                mPaint=getPaint();
                linearGradient=new LinearGradient(0,0,mViewwidth,0,new int[]{Color.BLUE,0xffffffff,Color.BLUE},null, Shader.TileMode.CLAMP);
                mPaint.setShader(linearGradient);
                matrix=new Matrix();
            }
        }
    }

    private void init(Context context) {
        mPaint1=new Paint();
        mPaint1.setColor(Color.BLUE);
        mPaint2=new Paint();
        mPaint2.setColor(Color.RED);
    }
}
  运行一下看看效果为:


  

2.通过组合方式实现新的控件

      这种方式的自定义View是非常常见的,通常集成与一个合适的ViewGroup,比如LinearLayout,RelativeLayout等各种布局。然后将各种控件组合在里面,形成一个新的控件,可以供多个布局复用,也可以通过这样的方式将一个大的布局拆分为多块,便于管理。
      比如我想做个标题+HelloChart柱状图的整体控件,我就自定义一个类继承LinearLayout,布局文件里布置好各种控件,如下所示:
      
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <RelativeLayout
        android:id="@+id/rl_title"
        android:layout_width="match_parent"
        android:layout_height="60dp"
        android:background="@color/blue">

        <TextView
            android:id="@+id/tv_title_areacompareform"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_marginLeft="@dimen/sm_10"
            android:gravity="center_vertical"
            android:textColor="@color/white"
            android:text="标题"/>

        <TextView
            android:id="@+id/tv_unit"
            android:layout_width="wrap_content"
            android:layout_height="match_parent"
            android:layout_alignParentRight="true"
            android:layout_marginRight="@dimen/sm_20"
            android:gravity="center"
            android:text="单位:%"
            android:textColor="@color/white" />
    </RelativeLayout>

    <RelativeLayout
        android:id="@+id/rl_linechartview"
        android:layout_below="@+id/rl_title"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <lecho.lib.hellocharts.view.LineChartView
            android:id="@+id/lcv_gdl"
            android:layout_width="match_parent"
            android:layout_marginTop="40dp"
            android:layout_marginBottom="30dp"
            android:layout_marginLeft="10dp"
            android:layout_marginRight="20dp"
            android:paddingLeft="5dp"
            android:paddingRight="25dp"
            android:paddingBottom="20dp"
            android:layout_height="match_parent"></lecho.lib.hellocharts.view.LineChartView>

    </RelativeLayout>

</RelativeLayout>

  然后在自定义View里填充并给这个控件里的各个子控件赋值,代码如下:

public GDLLineChartView(Context context, String title, List<GDLCompareBean> beanList) {
        super(context);
        this.context = context;
        view = LayoutInflater.from(context).inflate(R.layout.view_gdl_lcv, this);
        ButterKnife.bind(this, view);
        tvTitle.setText(title);
        //       tvFormTitle.setText(title);
        initForm(beanList);
    }

    private void initForm( List<GDLCompareBean> beanList){
        List<Line> lines = new ArrayList<Line>();
        List<PointValue> values = new ArrayList<PointValue>();
        List<AxisValue> axisValues = new ArrayList<AxisValue>();
        for (int j = 0; j < beanList.size(); ++j) {
            values.add(new PointValue(j, Float.valueOf(beanList.get(j).getGdl())));
            axisValues.add(new AxisValue(j).setLabel(beanList.get(j).getYear()));//添加X轴显示的刻度值
        }

        Line line = new Line(values);
        LineChartValueFormatter formatter=new SimpleLineChartValueFormatter(2);
        line.setFormatter(formatter);
        line.setColor(0xFF0088a8);
        line.setShape(shape);
        line.setStrokeWidth(5);//设置折线宽度
        line.setFilled(false);//设置折线覆盖区域颜色
        line.setPointColor(Color.RED);//设置节点颜色
        line.setPointRadius(5);//设置节点半径
        line.setHasLabels(true);//是否显示节点数据
        line.setHasLines(true);//是否显示折线
        lines.add(line);
        data = new LineChartData(lines);
        data.setAxisXBottom(new Axis(axisValues).setHasLines(true));
        data.setAxisYLeft(new Axis().setHasLines(true).setMaxLabelChars(3));
        data.setBaseValue(20);//设置反向覆盖区域颜色
        data.setValueLabelBackgroundAuto(false);//设置数据背景是否跟随节点颜色
        data.setValueLabelBackgroundEnabled(false);//设置是否有数据背景
        data.setValueLabelsTextColor(Color.RED);//设置数据文字颜色
        data.setValueLabelTextSize(12);//设置数据文字大小
        data.setValueLabelTypeface(Typeface.MONOSPACE);//设置数据文字样式
        lcvGDL.setLineChartData(data);
        lcvGDL.setOnValueTouchListener(new LineChartOnValueSelectListener() {
            @Override
            public void onValueSelected(int lineIndex, int pointIndex, PointValue value) {
                Log.i("sss","sss");
                if(gdlLineChartViewSelectedListener!=null){
                    gdlLineChartViewSelectedListener.onSelected((int)value.getX());
                }
            }

            @Override
            public void onValueDeselected() {
                Log.i("sss","sss");
            }
        });
    }
  接下来暴露接口,以便调用者可以调用接口中相应的点击方法:

public void setOnSelectedListener(GDLLineChartViewSelectedListener gdlLineChartViewSelectedListener){
    this.gdlLineChartViewSelectedListener=gdlLineChartViewSelectedListener;
}

public interface GDLLineChartViewSelectedListener{
    void onSelected(int position);
}
  调用时候只需如此调用即可:

GDLLineChartView lview = new GDLLineChartView(getContext(), MapTitle, gdlCompareBeanList);
llLineChartView.addView(lview);

lview.setOnSelectedListener(new GDLLineChartView.GDLLineChartViewSelectedListener() {
    @Override
    public void onSelected(int i) {
        SingleModuleBean bean = singleModuleBeanList.get(i);
        String[] titleList = bean.getTitles().split(",");
        String FormTitle = titleList[1];
        String Year = bean.getYear() + "年";
        FormTitle = Year + FormTitle;
        List<GDLBean> gdlBeanList = getList(bean.getTablejson());
        GDLAreaFormView aview = new GDLAreaFormView(getContext(), FormTitle, gdlBeanList);
        llAreaCompareForm.removeAllViews();
        llAreaCompareForm.addView(aview);
    }
});
  效果图大概为:


3.通过重写View实现新的控件

  当有时候需求原生控件里完全没有的控件时候,可以自己创建一个全新的自定义View来。这样的控件继承于View类,通过onDraw()来实现该View的绘制。

  以音频图为例子,音频图可以通过绘制一系列的矩形来实现,具体代码如下:

public class VoiceView extends View {
    private Paint mPaint;
    private int count=10;
    private int mRectWidth=60;
    private int mRectHeight=800;
    private int off=10;
    private int mWidth;

    public VoiceView(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        init(context);
    }

    public VoiceView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }
    public VoiceView(Context context) {
        super(context);
        init(context);
    }

    private void init(Context context){
        mWidth=getMeasuredWidth();
        mPaint=new Paint();
        mPaint.setColor(Color.BLUE);
    }

    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);
        for(int i=0;i<count;i++){
            Double random=Math.random();
            canvas.drawRect(
                    (float)(mWidth*0.4/2+mRectWidth*i+off),
                    (float)(random*mRectHeight),
                    (float)(mWidth*0.4/2+mRectWidth*i+mRectWidth),
                    mRectHeight,
                    mPaint);

        }
    }
  这里我在onDraw()方法里绘制了10个宽度为50,高度为随机的蓝色矩形。然后创建一个activity,添加这个自定义View:

public class MainActivity extends AppCompatActivity {

    private VoiceView myView;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        myView =  new VoiceView(this);//初始化自定义View
        this.setContentView(myView);//设置当前的用户界面
    }
}
  可以看看效果图为:


 


作者:bit_kaki 发表于2017/5/25 18:39:47 原文链接
阅读:149 评论:0 查看评论

linux驱动开发之字符设备--内核和用户空间数据的交换(sysfs)

$
0
0

前言

设备驱动程序中与用户层的接口,除了 read/write/ioctl 方式外,还有sysfs属性。 对于sysfs属性提供的接口,在应用层,可以直接使用 shell 进行交互,而不必进行 编写 C 代码的方式进行对底层的读写。

正文

API接口

struct kobject *kobject_create_and_add(const char *name, struct kobject *parent)

功能:动态的创建 kobject,并注册到 sysfs 文件系统中。
参数:

  • name 在 /sys/下显示的目录
  • parenet :上述目录的父目录
int sysfs_create_group(struct kobject *kobj,const struct attribute_group *grp)

功能: 在kobject 下创建一组目录
参数:

  • kobject :指定的目录
  • grp: 在指定目录下创建group

分析 一下这个 struct attribute_group 数据类型

struct attribute_group {
    const char      *name;
    struct attribute    **attrs;
};

有两个成员,分别是name 和attrs。

struct attribute {
    const char      *name;
    umode_t         mode;
};

以上两个成员,通常先宏下列宏先定义,在取成员 attr 的方式。

#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)

看一下 __ATTR 的定义

// (kernel-3.10\include\linux\Device.h)
#define DEVICE_ATTR(_name, _mode, _show, _store) \
    struct device_attribute dev_attr_##_name = __ATTR(_name, _mode, _show, _store)

展开后得到

#define DEVICE_ATTR(_name, _mode, _show, _store) \
struct device_attribute dev_attr_##_name =          \
     .attr = {.name = __stringify(_name), .mode = _mode },  \
    .show   = _show,                    \
    .store  = _store,                   \
}

示例

#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

#include <linux/cdev.h>
#include <linux/fs.h>
#include <linux/device.h>
#include <linux/sysfs.h>
#include <linux/kobject.h>

#include <linux/uaccess.h>
//幻数
#define IOCTL_TYPE 'b'

#define COUNT   32

struct  ioctl_arg {
    int val;
    char buf[COUNT];
};
//定义的命令码
#define CMDCTL  _IO(IOCTL_TYPE,0)
#define CMDR    _IOR(IOCTL_TYPE,1,struct ioctl_arg)
#define CMDW    _IOW(IOCTL_TYPE,2,struct ioctl_arg)

#define DEVICE_NAME "cdev_demo"

static struct cdev *pdev = NULL;
static int major = 0;
static int minor = 0;
static int count = 2;


#define BUF_SIZE        (1024)
static char kbuf[BUF_SIZE];
static int  buf_count = 0;

static int cdev_demo_open(struct inode * inode, struct file * file)
{
    printk("%s,%d\n",__func__,__LINE__);
    return 0;
}
static int cdev_demo_release(struct inode *inode, struct file * file)
{
    printk("%s,%d\n",__func__,__LINE__);
    return 0;
}
static ssize_t cdev_demo_read(struct file * file, char __user * buffer, size_t size, loff_t * loff)
{
    printk("%s,%d\n",__func__,__LINE__);

    if(0 == buf_count){
        return -EAGAIN;
    }

    if(buf_count < size){
        size = buf_count;
    }

    if(size == copy_to_user(buffer,kbuf,size)){
        return -EAGAIN;
    }

    buf_count  = 0;

    return size;

}

static ssize_t cdev_demo_write(struct file * file, const char __user * buffer, size_t size, loff_t * loff)
{
    printk("%s,%d\n",__func__,__LINE__);
    printk("buffer=%s   size=%d\n",buffer,size);
    if(size >BUF_SIZE){
        return -ENOMEM;
    }
    if(size == copy_from_user(kbuf,buffer,size)){
        return -EAGAIN;
    }

    buf_count = size;
    return size;
}
//ioctl
static long cdev_demo_ioctl (struct file *filep, unsigned int cmd, unsigned long arg)
{
    static struct ioctl_arg buf;
    printk("%s,%d\n",__func__,__LINE__);
    //分辨不同命令码
    switch(cmd){
        case CMDCTL:
                printk("do CMDCTL\n");
                break;
        case CMDR:
            //使用 _IOC_SIZE()获得命令码中的数据长度
            if(sizeof(buf) != _IOC_SIZE(cmd)){
                return -EINVAL;
            }

            if(sizeof(buf) == copy_to_user((struct ioctl_arg*)arg,&buf,sizeof(struct ioctl_arg))){
                return -EAGAIN;
            }
            printk("do CMDR\n");
            break;
        case CMDW:
            if(sizeof(buf)!= _IOC_SIZE(cmd)){
                 return -EINVAL;
            }

            if(sizeof(buf) == copy_from_user(&buf,(struct ioctl_arg*)arg,sizeof(buf))){
                return -EAGAIN;
            }
            printk("do CMDW\n");
            printk("%d,%s \n",buf.val,buf.buf);
            break;

        default:
            break;

    }

    return 0;
}
static struct file_operations fops ={
    .owner   = THIS_MODULE,
    .open    = cdev_demo_open,
    .release = cdev_demo_release,
    .read    = cdev_demo_read,
    .write   = cdev_demo_write,
    .unlocked_ioctl = cdev_demo_ioctl,
};
//
char cdev_buf[2] = "a";
//定义在 应用层执行 cat 时的操作函数
static ssize_t cdev_demo_show(struct device *dev,struct device_attribute *attr, char *buf)  
{
    char *s = buf;
    s = sprintf(s,"%s",cdev_buf);
    return sizeof(s);
}
//定义在 应用层执行 echo 时的操作函数
static ssize_t cdev_demo_store(struct device *dev, struct device_attribute *attr, const char *buf, size_t count)
{

    memcpy(cdev_buf,buf,count);
    return count;
}
//利用DEVICE_ATTR定义一个变量
static DEVICE_ATTR(cdev_demo, 0666, cdev_demo_show, cdev_demo_store);

//去除上边定义变量的成员的attr 作为数组成员
static struct attribute  *g[] = {
    &dev_attr_cdev_demo.attr,
    NULL,
};
//定义了attr_group
static struct attribute_group attr_group = {
    .attrs = g,
};
static int __init cdev_demo_init(void)
{
    dev_t dev;
    int ret;
    struct kobject *cdev_obj;
    printk("%s,%d\n",__func__,__LINE__);

    pdev = cdev_alloc();
    if(NULL == pdev){
        printk("cdev_alloc failed.\n");
        return -ENOMEM;
    }

    cdev_init(pdev,&fops);

    ret = alloc_chrdev_region(&dev,minor,count,DEVICE_NAME);
    if(ret){
        printk("alloc_chrdev_region failed.\n");
        goto ERROR_CDEV;
    }
    major = MAJOR(dev);
    ret = cdev_add(pdev, dev,count);
    if(ret) {
        printk("cdev_add failed.\n");
        goto  ERROR_ADD;
    }
    //在 sys下创建  /cdev_demo目录
    cdev_obj = kobject_create_and_add("cdev_demo",NULL);
    if(!cdev_obj){
        return -ENOMEM;
    }
    //在/cdev_demo目录下创建 attr_grop 
    ret = sysfs_create_group(cdev_obj,&attr_group);
    if(ret)
        goto SYSFS_ERR;


    return 0;
SYSFS_ERR:
    kobject_del(cdev_obj);
    kobject_put(cdev_obj);

ERROR_ADD:
    unregister_chrdev_region(dev,count);
ERROR_CDEV:
    cdev_del(pdev);
    return ret;
}
static void __exit cdev_demo_exit(void)
{
    printk("%s,%d\n",__func__,__LINE__);
    unregister_chrdev_region(MKDEV(major,minor),count);

    cdev_del(pdev);
}

module_init(cdev_demo_init);
module_exit(cdev_demo_exit);
MODULE_LICENSE("GPL");

执行 下列命令 查看变化

echo b > /sys/cdev_demo/cdev_demo 
 cat / sys/cdev_demo/cdev_demo 

总结

通过 sys的方式,可以更加方便的查看底层的一些数据。 sys也可以用来进行调试信息。

参考文献

Linux设备模型(2)_Kobject
使用 /sys 文件系统访问 Linux 内核

“`

作者:u013377887 发表于2017/5/25 20:38:52 原文链接
阅读:215 评论:0 查看评论

Android实习生面试--怼丫的

$
0
0

我没找到工作,我没找到工作,我还没找到工作。我,非专业Android人士,人模狗样的敲着键盘,写着博客,以为自己要升天了呢。读者看到这儿,或许会觉得作者满满的负能量都快溢出屏幕了。您来仔细听听这段时间我都经历了什么。

  • 本人就读在一所非985&211的一所天津学校,但在天津至少也是能排进前五的本科院校,我们学院还是国家级重点示范学院,然而,这并没有什么卵用。本人大三,本来我们学院在大四的时候是会安排企业培训,并且推荐就业的。但本人实在是想赶紧提高自己,找到自己的短板,尽早熟悉社会,熟悉对口行业,我出发点多么美好,多么理所当然,其实就是他么缺钱了,想挣点儿,但这也是没有错的吧。
  • 其实我忍着不写博客好久了,心中憋着一股火,就因为在拉勾网上看到有则招聘信息(招Android实习生),是个啥破公司我就不说了,人家不要会写博客的(你要是非985&211的不要,我都能忍你)。。。我也不知道这个求职平台是怎么审的核,我当时都惊呆了,下次再直白一点儿,直接不要我们东北和黄泛区的就好了呀。我主要是不解啊,我妈从小就让我每天写日记,我小学不写日记不知道被老师打了多少耳光,好不容易养成了这个习惯,现在因为写个博客,还不能找工作了呢,这叫什么,这就叫毁三观。
  • 我现在只是想告诉自己和大家,宁缺毋烂,没错就是这个烂,咱们本来都是可以靠脸吃饭的,现在咱们凭能力,融资都他妈费劲,你丫还挑。对于这些奇葩货色,我祝你们招到命硬克公司的人。
  • -

现在我理解为什么有的90后会炒公司鱿鱼了。

作者:baidu_34750904 发表于2017/5/25 22:23:36 原文链接
阅读:183 评论:0 查看评论

3 创建一个kotlin新工程

$
0
0

创建一个kotlin工程

在Android Studio中创建一个新工程

在Android Studio 3.0 中,相比创建一个java工程来说, 创建一个kotlin工程只是多点一下,选中Include Kotlin Support 即可, 其他步骤和创建java 工程是一样的。
1. 在Android Studio 中 点击File -> New -> New Project 或者在欢迎页向导窗口中点击 Start a new Android Studio Project
2. 在创建页面中,选中Include Kotlin support
3. 点击Next, 直到完成创建向导。
这里写图片描述

在现有工程中添加Kotlin

如果想在现有工程中添加Kotlin代码,点击File -> New ,然后选择一个模板。
这里写图片描述
在向导页中, 把Source language 选成Kotlin即可。
这里写图片描述

把Java代码转成Kotlin代码

在Android Studio 3.0 中,把Java代码复制到Kotlin文件中,编辑器自动提示是否要转换成Kotlin代码
这里写图片描述
也可以在菜单中选择Code -> Covert Java File to Kotlin File

用Kotlin 访问Android接口

Kotlin 支持和Java语言的无缝衔接访问, 所以用Kotlin调用Android接口和用Java调用Android接口看起来差不多。 用Kotlin调用是会使用Kotlin语法。

下面是一些两种语言接口访问的例子。

Declare Activity in Kotlin

class MyActivity : AppCompatActivity() {
  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity)
  }
}

On-click listener in Kotlin

val fab = findViewById(R.id.fab) as FloatingActionButton
fab.setOnClickListener {
  ...
}

Item click listener in Kotlin

private val mOnNavigationItemSelectedListener
    = BottomNavigationView.OnNavigationItemSelectedListener { item ->
  when (item.itemId) {
    R.id.navigation_home -> {
      mTextMessage.setText(R.string.title_home)
      return@OnNavigationItemSelectedListener true
    }
    R.id.navigation_dashboard -> {
      mTextMessage.setText(R.string.title_dashboard)
      return@OnNavigationItemSelectedListener true
    }
 }
 false
}

Declare Activity in Java

public class MyActivity extends AppCompatActivity {
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity);
  }
}

On-click listener in Java

FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
  @Override
  public void onClick(View view) {
    ...
  }
});

Item click listener in Java

private BottomNavigationView.OnNavigationItemSelectedListener mOnNavigationItemSelectedListener
    = new BottomNavigationView.OnNavigationItemSelectedListener() {
  @Override
  public boolean onNavigationItemSelected(@NonNull MenuItem item) {
    switch (item.getItemId()) {
      case R.id.navigation_home:
        mTextMessage.setText(R.string.title_home);
        return true;
      case R.id.navigation_dashboard:
        mTextMessage.setText(R.string.title_dashboard);
        return true;
    }
    return false;
  }
};

测试运行正常

在创建完项目,添加完Activity 后,编译、运行、安装。运行正常,现在开始真正的kotlin Android 开发之旅。
这里写图片描述

总结

本文介绍了如何使用kotlin 新建一个工程,新建一个activity, 如何把转换现有Java代码转成Kotlin代码,简单介绍了几个kotlin调用Anroid接口,并把新建的kotlin工程运行了起来。为后面的Kotlin开发做好了环境准备。

作者:farmer_cc 发表于2017/5/25 9:41:58 原文链接
阅读:26 评论:0 查看评论

动态图解&实例 ConstraintLayout Chain

$
0
0

想看我更多文章:【张旭童的博客】http://blog.csdn.net/zxt0601
想来gayhub和我gaygayup:【mcxtzhang的Github主页】https://github.com/mcxtzhang

概述

昨儿写了一篇全文字和JPG、无Gif的博文ConstraintLayout 属性详解 和Chain的使用
主要详解了ConstraintLayout的所有属性,
但是有朋友问Chain是如何通过可视化的方式去添加,
于是便有了此文。

但是只讲解Chain又显得太单薄,
于是我又加入了一个用ConstraintLayout实现商品详情、Item布局的实例。
废话少说,开整:

构建链条 Chain 图文步骤

假设我们要需要构建 竖直的链条。

第一步,先拖三个Button竖直排列。
可以看到在“Design”区域,除了位置,我们还可以调整控件的大小。

第二步,选中三个Button。
可以用鼠标划一个范围选中这N个控件。
也可以用键盘Ctrl + A 全选控件。

第三步,右键->Center Vertically.
链条已经构成了。

第四步,在Design区域上方的工具栏ALign选项中,可以切换水皮方向控件的排列规则
依次左对齐-居中-右对齐

当链条已经构成时,可以从图中看到,每个控件的下方多了一个链条样式的小图标。
第五步,单击链条图标,可以切换ChainStyle。


由上图可以看出,会在三种模式依次循环切换:spread-spread_inside-packed
图示在上文ConstraintLayout 属性详解 和Chain的使用也已经给出:

实战

talking is cheap。让我们一起实战一波,先看一个设计稿:


这是一个常见的详情或Item的布局。
以往情况,如果不使用ConstraintLayout,我们一般会通过嵌套LinearLayout或者使用RelativeLayout去实现它。
嵌套LinearLayout肯定是低效的low方式,我们不提。
而如果有经验的老司机应该会知道,RelativeLayout实现起这种布局是很烦心的,
因为它大体是按照竖直排列,在局部又水平分布了几个控件,我们要死去活来的写N多的below rightOf
(而且如果RelativeLayout如果要实现几个TextSize不一样 TextView 居中对齐是挺麻烦的。 虽然这个设计稿上没有出现这种case。而现在可以通过ChainAlign轻松实现它。)

DuangDuangDuangDuang,现在ConstraintLayout横空出世,让我们用拖拖拽拽点点的方式来轻松实现它吧。

第一步,去xml修改父布局宽高为WRAP_CONTENT
第二步,拖拽一个ImageView,并按设计稿设置尺寸,布局在父控件顶端


我们只是为了测试,可以设置图片为fitxy,便于观察效果。

第三步,拖拽四个TextView,竖直排列。顺便按照设计稿填充颜色、尺寸等元素

(这两个步骤我撸了四五次,都撸不出完整的gif,大家凑合看,这两张图已经展示了,可以通过Design区域去设置text、textColor、textSize等等等属性)
第四步,设置四个TextView之间的约束、间距等
(我尽力了,电脑上撸这么大的GIF图貌似时长有问题,所以我重复了N次这个过程,分别撸了几张图如下:)
操作步骤就是:利用控件四个边中点的圆圈,和其他控件发生约束。可以利用右侧面板修改控件的属性值。


第五步,拖拽两个TextView放在底部,按照设计稿填充颜色、尺寸等
图略,和第三步相似。(其实是图又录坏了)
第六步,按照设计稿约束底部三个TextView,设置边距等


对于精细尺寸,例如这里三个TextView间距大概5dp,可以通过Inspector区域设置:

第七步, 布局 购物车 加减按钮
购物车按钮使用的是一个自定义View
所以这一小节也顺便讲解如何在ConstraintLayout中拖拽使用自定义View。
关于这个购物车控件,我之前写过相关博文,github地址:https://github.com/mcxtzhang/AnimShopButton


在左上角Pallete区域点击Advanced->view,然后拖动至布局区域后,会弹出弹框让我们选择,我们搜索AnimShopButton(自定义View的名称)即可。
第八步,为 购物车控件 添加约束
查看设计稿,该控件是位于父控件右下角,距离右边和底边有14dp的间距:

第九步,使底部三个TextView竖直居中
这一步是我强加的戏,为了展示Chain的作用。
我把底部的几个控件链接成Chain,并且改变了使它们竖直方向也相对居中。
这时界面突然收缩了,显然是约束哪里出了问题。
于是我们切换到Text页面,查看代码具体哪里出了问题,

定位到价格TextView后,
检查约束,
如果是我们自己手写约束,
app:layout_constraintVertical_bias="1.0"应该不会被写下来,
这一条约束应该是和父控件的WRAP_CONTENT一起,导致布局收缩了。

删掉该条约束,界面预览正常。
第十步,在根布局外面套一层CardView,运行
运行后效果:

总结

可以看出,ConstraintLayoutChain的概念,和前端JS中flex布局是一样的。
如果有ReactNative或者前端经验的童鞋应该一眼可以看出。或者更适应这种布局方式。

使用ConstraintLayout去完成布局,经历短暂的不习惯后,上手后速度还是挺快的,
相比较RelativeLayout它有 可拖拽、可视化的优势,以及轻松完成一些RelativeLayout不太方便完成的布局。
还是推荐大家去使用它的。

文中代码地址在我的Demo合集中:
https://github.com/mcxtzhang/Demos/tree/master/constraintlayoutdemo/src/main

想看我更多文章:【张旭童的博客】http://blog.csdn.net/zxt0601
想来gayhub和我gaygayup:【mcxtzhang的Github主页】https://github.com/mcxtzhang

作者:zxt0601 发表于2017/5/25 18:43:33 原文链接
阅读:180 评论:1 查看评论

4.1 Java语言和Kotlin语言对比(1)

$
0
0

4.1 Java语言和Kotlin语言对比(1)

void类型的映射

Java中返回void类型,对应的Kotlin返回Unit

Java中特殊标识的转义

有些Kotlin定义的关键字,是Java中的合法标识名,例如in, object, is etc。如果Java库使用一个Kotlin的关键字作为函数名,可以转义之后再调用改函数,转义时添加 bracktick(`)字符

foo.`is`(bar)

空指针安全和平台类型

基础类型的映射

Kotlin特殊处理了Java 的基础数据类型,把他们映射成了Kotlin中的类型。这种映射的改变是在编译时期的改变,运行时机器内存表示是保持一致的。

Java type Kotlin type
byte kotlin.Byte
short kotlin.Short
int kotlin.Int
long kotlin.Long
char kotlin.Char
float kotlin.Float
double kotlin.Double
boolean kotlin.Boolean

Java类型到Kotlin的映射

Java type Kotlin type
java.lang.Object kotlin.Any!
java.lang.Cloneable kotlin.Cloneable!
java.lang.Comparable kotlin.Comparable!
java.lang.Enum kotlin.Enum!
java.lang.Annotation kotlin.Annotation!
java.lang.Deprecated kotlin.Deprecated!
java.lang.CharSequence kotlin.CharSequence!
java.lang.String kotlin.String!
java.lang.Number kotlin.Number!
java.lang.Throwable kotlin.Throwable!

Java封装类型到Kotlin的映射

Java type Kotlin type
java.lang.Byte kotlin.Byte?
java.lang.Short kotlin.Short?
java.lang.Integer kotlin.Int?
java.lang.Long kotlin.Long?
java.lang.Char kotlin.Char?
java.lang.Float kotlin.Float?
java.lang.Double kotlin.Double?
java.lang.Boolean kotlin.Boolean?
作者:farmer_cc 发表于2017/5/26 10:23:53 原文链接
阅读:30 评论:0 查看评论

[算法]iOS 视频添加水印,合成视频两种方案(整体渲染和分割渲染)

$
0
0

        现手机里有一段视频,通过APP给他添加一个水印。iOS提供了在视频上添加layer的接口,添加一个水印还是很方便的(添加水印)。添加完水印有一个渲染过程,在手机这种设备上还是比较慢的,比如:对1分钟的高清视频(960x540)进行渲染需要20秒左右。如何在现有API基础上提高渲染速度,提升用户体验,成了问题。笔者发现两种渲染方法:
        先看图,这有一个6秒的视频,我抓了四张关键帧。只在第2,3两张关键帧上添加字幕(一个关键帧代表1.5秒。所以,两个关键帧就代表是3秒时长)


        第一种方案:视频分割 + 逐段渲染 + 合并
            将视频分割为3端,即第1、第2,3、第4。有水印的是一组,没水印也是一组。对第2,3渲染。最后将三段视频合并到一起。
        第二种方案:整体渲染
            水印虽然不是从一开始出现,但是,我们可以对layer添加动画(参考下面OC代码)。
举个例子,假如水印少的情况。比如只对第一帧添加水印。分割渲染方案是3.0秒,整体渲染方案是3.5秒。假如水印多的情况,第1,2,3帧都有水印。分割渲染方案是4.1秒,整体渲染方案还是3.5秒。通过结果得出一点结论:整体渲染,无论水印多还是少,耗时是一样的。分割渲染怎么会有这么个差别呢?
        分割渲染:由于涉及三步:1.分割,2.渲染视频,3.合并视频。每一步都需要时间。根据测试经验,第一步分割视频的时间很少,可以忽略。那么就剩下渲染和合并时间了。就拿刚才6秒的视频来说:一张水印和三张水印。合并视频时间是一样的。都是两段(一段1.5秒,另外一段4.5秒)。但是水印多了渲染时间就长了。
        最后得出结论:水印少的时候,使用分割渲染方法。水印多的情况使用整体渲染。

整体渲染代码:

#define kEffectVideoFileName_Animation @"tmpMov-effect.mov"
    - (void)renderWholeVideo:(AVAsset *)asset{
    // 1 - Early exit if there's no video file selected
    if (!asset) {
        UIAlertView *alert = [[UIAlertView alloc] initWithTitle:@"Error" message:@"Please Load a Video Asset First"
                                                       delegate:nil cancelButtonTitle:@"OK" otherButtonTitles:nil];
        [alert show];
        return;
    }
    
    // 2 - Create AVMutableComposition object. This object will hold your AVMutableCompositionTrack instances.
    AVMutableComposition *mixComposition = [[AVMutableComposition alloc] init];
    
    // 3 - Video track
    AVMutableCompositionTrack *videoTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeVideo
                                                                        preferredTrackID:kCMPersistentTrackID_Invalid];
    [videoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration)
                        ofTrack:[[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0]
                         atTime:kCMTimeZero error:nil];
    
    AVMutableCompositionTrack *audioTrack = [mixComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
    [audioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, asset.duration) ofTrack:[asset tracksWithMediaType:AVMediaTypeAudio][0] atTime:kCMTimeZero error:nil];
    
    // 3.1 - Create AVMutableVideoCompositionInstruction
    AVMutableVideoCompositionInstruction *mainInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
    mainInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, asset.duration);
    
    // 3.2 - Create an AVMutableVideoCompositionLayerInstruction for the video track and fix the orientation.
    __block     CGSize naturalSize;
    AVMutableVideoCompositionLayerInstruction *videolayerInstruction = [self transformVideo:asset track:videoTrack isVideoAssetPortrait:^(CGSize finalSize) {
        naturalSize = finalSize;
    }];
    [videolayerInstruction setOpacity:0.0 atTime:asset.duration];
    
    // 3.3 - Add instructions
    mainInstruction.layerInstructions = [NSArray arrayWithObjects:videolayerInstruction,nil];
    
    AVMutableVideoComposition *mainCompositionInst = [AVMutableVideoComposition videoComposition];
    
    
    float renderWidth, renderHeight;
    renderWidth = naturalSize.width;
    renderHeight = naturalSize.height;
    mainCompositionInst.renderSize = CGSizeMake(renderWidth, renderHeight);
    mainCompositionInst.instructions = [NSArray arrayWithObject:mainInstruction];
    mainCompositionInst.frameDuration = CMTimeMake(1, 30);
    
    
    
    [self applyVideoEffectsWithAnimation:mainCompositionInst size:naturalSize];
    
    
    NSString *myPathDocs = [NSTemporaryDirectory() stringByAppendingPathComponent:kEffectVideoFileName_Animation];
    NSURL *url = [NSURL fileURLWithPath:myPathDocs];
    /*先移除旧文件*/
    [PublicUIMethod removeFile:url];
    
    // 5 - Create exporter
    AVAssetExportSession *exporter = [[AVAssetExportSession alloc] initWithAsset:mixComposition
                                                                      presetName:AVAssetExportPresetHighestQuality];
    [self.exportSessions addObject:exporter];
    exporter.outputURL=url;
    exporter.outputFileType = AVFileTypeQuickTimeMovie;
    exporter.shouldOptimizeForNetworkUse = YES;
    exporter.videoComposition = mainCompositionInst;
    weakifyself;
    [exporter exportAsynchronouslyWithCompletionHandler:^{
        strongifyself;
        dispatch_async(dispatch_get_main_queue(), ^{
            switch ([exporter status]) {
                case AVAssetExportSessionStatusFailed:
                    
                    DDLogWarn(@"render Export failed: %@ and order : %d", [exporter error], 0);
                    break;
                case AVAssetExportSessionStatusCancelled:
                    
                    NSLog(@"render Export canceled order : %d", 0);
                    break;
                default:
                {
                    NSLog(@"'%@' render finish",[myPathDocs lastPathComponent]);
                    [self pushToPreviePage:myPathDocs];
                }
                    break;
            }
        });
    }];
    [self monitorSingleExporter:exporter];
}

- (void)applyVideoEffectsWithAnimation:(AVMutableVideoComposition *)composition size:(CGSize)size
{
    // Set up layer
    CALayer *parentLayer = [CALayer layer];
    CALayer *videoLayer = [CALayer layer];
    parentLayer.frame = CGRectMake(0, 0, size.width, size.height);
    videoLayer.frame = CGRectMake(0, 0, size.width, size.height);
    [parentLayer addSublayer:videoLayer];
    
    
    /**/
    CMTime timeFrame = [self frameTime];
    CGFloat granularity = CMTimeGetSeconds(timeFrame);
    /*caption layer*/
    for (int j=0; j<self.effectsArray.count; j++) {
        NSArray* effectSeries = (NSArray *)self.effectsArray[j];
        FSVideoCaptionDescriptionModel *description = [[effectSeries firstObject] as:FSVideoCaptionDescriptionModel.class];
        NSArray *captions = [description reOrder];
        if (!captions || captions.count == 0) {
            //没有字幕就别瞎搞了
            continue;
        }
        
        FSCaptionModel *captionModel = captions.firstObject;
        UIImage *image = captionModel.image;/*将水印生成图片,采用图片方法添加水印*/
        CGFloat scaleY = captionModel.scaleY;
        CGFloat scaleHeight = captionModel.scaleHeight;
        CALayer *layer = [CALayer layer];
        layer.frame = CGRectMake(0, size.height * scaleY, size.width, size.height * scaleHeight);
        layer.contents = (__bridge id)image.CGImage;
        
        /*
         字幕动画由两个组成:
         1. 显示所有字幕<动画开始前保持,初始状态。动画时间是0.结束后不移除动画>
         2. 隐藏字幕,到最后。
         */
        CGFloat showStartTime = description.startIndex * granularity;
        CGFloat hiddenAginStartTime = showStartTime + effectSeries.count*granularity;
        
        CABasicAnimation *animation = nil;
        if (showStartTime > 0) {
            animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
            animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
            [animation setFromValue:[NSNumber numberWithFloat:0.0]];
            [animation setToValue:[NSNumber numberWithFloat:1.0]];
            [animation setBeginTime:showStartTime];
            [animation setFillMode:kCAFillModeBackwards];/*must be backwards*/
            [animation setRemovedOnCompletion:NO];/*must be no*/
            [layer addAnimation:animation forKey:@"animateOpacityShow"];
        }
        /*最后一个字幕片段不是整的1.5s或者5秒。就不隐藏动画了*/
        if (j != self.effectsArray.count-1) {
            animation = [CABasicAnimation animationWithKeyPath:@"opacity"];
            animation.timingFunction = [CAMediaTimingFunction functionWithName:kCAMediaTimingFunctionDefault];
            [animation setFromValue:[NSNumber numberWithFloat:1.0]];
            [animation setToValue:[NSNumber numberWithFloat:0.0]];
            [animation setBeginTime:hiddenAginStartTime];
            [animation setRemovedOnCompletion:NO];/*must be no*/
            [animation setFillMode:kCAFillModeForwards];
            [layer addAnimation:animation forKey:@"animateOpacityHiddenAgin"];
        }
        
        [parentLayer addSublayer:layer];
    }
    
    parentLayer.geometryFlipped = YES;
    composition.animationTool = [AVVideoCompositionCoreAnimationTool
                                 videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:parentLayer];
}

- (void)monitorSingleExporter:(AVAssetExportSession *)exporter{
    double delay = 1.0;
    int64_t delta = (int64_t)delay * NSEC_PER_SEC;
    dispatch_time_t poptime = dispatch_time(DISPATCH_TIME_NOW, delta);
    dispatch_after(poptime, dispatch_get_main_queue(), ^{
        if (exporter.status == AVAssetExportSessionStatusExporting) {
            NSLog(@"whole progress is %f",  exporter.progress);
            [self monitorSingleExporter:exporter];
        }
    });
}
-(AVMutableVideoCompositionLayerInstruction *) transformVideo:(AVAsset *)asset track:(AVMutableCompositionTrack *)firstTrack isVideoAssetPortrait:(void(^)(CGSize size))block{
    AVMutableVideoCompositionLayerInstruction *videolayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:firstTrack];
    
    
    AVAssetTrack *videoAssetTrack = [[asset tracksWithMediaType:AVMediaTypeVideo] objectAtIndex:0];
    UIImageOrientation videoAssetOrientation_  = UIImageOrientationUp;
    BOOL isVideoAssetPortrait_  = NO;
    CGAffineTransform videoTransform = videoAssetTrack.preferredTransform;
    if (videoTransform.a == 0 && videoTransform.b == 1.0 && videoTransform.c == -1.0 && videoTransform.d == 0) {
        videoAssetOrientation_ = UIImageOrientationRight;
        videoTransform = CGAffineTransformMakeRotation(M_PI_2);

        videoTransform = CGAffineTransformTranslate(videoTransform, 0, -videoAssetTrack.naturalSize.height);
        isVideoAssetPortrait_ = YES;
    }
    if (videoTransform.a == 0 && videoTransform.b == -1.0 && videoTransform.c == 1.0 && videoTransform.d == 0) {
        videoAssetOrientation_ =  UIImageOrientationLeft;
        //这个地方很恶心,涉及到reveal看不到的坐标系
        videoTransform = CGAffineTransformMakeRotation(-M_PI_2);
        videoTransform = CGAffineTransformTranslate(videoTransform, - videoAssetTrack.naturalSize.width, 0);
        isVideoAssetPortrait_ = YES;
    }
    if (videoTransform.a == 1.0 && videoTransform.b == 0 && videoTransform.c == 0 && videoTransform.d == 1.0) {
        videoAssetOrientation_ =  UIImageOrientationUp;
    }
    if (videoTransform.a == -1.0 && videoTransform.b == 0 && videoTransform.c == 0 && videoTransform.d == -1.0) {
        videoTransform = CGAffineTransformMakeRotation(-M_PI);
        videoTransform = CGAffineTransformTranslate(videoTransform, -videoAssetTrack.naturalSize.width, -videoAssetTrack.naturalSize.height);
//        videoTransform = CGAffineTransformRotate(videoTransform, M_PI/180*45);
        videoAssetOrientation_ = UIImageOrientationDown;
    }
    [videolayerInstruction setTransform:videoTransform atTime:kCMTimeZero];
    
    CGSize naturalSize;
    if(isVideoAssetPortrait_){
        naturalSize = CGSizeMake(videoAssetTrack.naturalSize.height, videoAssetTrack.naturalSize.width);
    } else {
        naturalSize = videoAssetTrack.naturalSize;
    }
    
    
    if(block){
        block(naturalSize);
    }
    return videolayerInstruction;
}


作者:hherima 发表于2017/5/25 14:49:40 原文链接
阅读:158 评论:0 查看评论

Xcode 8 中阶调试技巧

$
0
0

原文:Intermediate Debugging with Xcode 8
作者:George Andrews
译者:kmyhy

更新说明: 本教程由 George Andrews 升级为 Xcode 8 和 Swift 3。原文作者为 Brain Moakley。

软件开发中唯一不变的主题就是 bug。让我们面对现实吧,没有人能够一次就能做对。从输出错误到不正确的假设,软件开发就好比是蟑螂屋里烤蛋糕——只不过制造蟑螂的人就是开发者自己。

幸运的是,Xcode 提供了大量防止这种事情发生的工具。虽然你热衷于 debugger,但通过这些工具你能够做的更好,而不仅仅是查看变量和单步执行!

本文针对中级 iOS 开发者,你将学到一些少为人知但又非常重要的调试技巧,比如:

  • 去除 NSLog,而用断点日志代替
  • 通过编译脚本生成 TODO 和 FIXME 编译器警告
  • 通过表达式设置条件断点
  • 通过 LLDB 动态修改数据
  • 等等

我个人的目标是成为一个真正懒惰的开发者。我宁可麻烦在前,而享受在后。幸运的是,Xcode 为我节省下了“马丁尼时间”。它提供了许多工具,使我不再没日没夜地粘在我的电脑面前。

我们先来看看有哪些工具。拖过一把豆袋椅。打开饮料。让我们放松一下:]

本教程假设你熟悉 Xcode 调试器。如果你不知道如何在 Xcode 中进行调试,请先阅读新手调试教程

开始

我为本教程准备了一个示例 app。你可以在这里下载

这个 app 叫 Gift Lister,它会记录你想为谁购买的礼物。就像 Gifts 2 HD,它获得了 2012 年印象最深刻的读者 App。Gift Lister 和 Gift 2 HD 很像……当然要差许多。

首先,它有许多 bug。开发者(也就是我,只不过穿着不同的衣服)雄心勃勃,尝试用传统的方式修复这些 bug……但是无功而返:]

本教程会教你如何在尽可能偷懒的同时修复这个 app。

好了,让我们开始吧——但也不必太过紧张。:]

打开项目,看一眼它的文件。你会注意到这个 app 有一个简单的前端和一个简单的 Core Data 数据库。

注意:如果你不熟悉 Core Data,也没关系!Core Data 是一个面向对象的持久化框架,它有现成的教程。在本教程中,我们不会过多深入这个框架,也不需要和 Core Data 对象进行任何有意义的交互,因此你也不需要了解得太多。只需要知道 Core Data 会为你加载对象、保存对象就可以了。

大概浏览完之后,我们开始打开调试器。

打开 Debugger 控制台

进行任何调试过程之前,首先需要打开 debugger 控制台。点击主工具栏上的这颗按钮:

这颗按钮很好找,每次调试会话开始时点击这颗按钮,将避免你的指尖的不必要的磨损 ;] 为什么不让 Xcode 为你多做一些工作呢?

然后,打开 Xcode 偏好设置,通过 [command + ,] 或者 Xcode\Preferences 菜单。点击 Behaviors 按钮(齿轮图标)。

点击左边的 Running\Starts。你会看到一堆选项。点击右边的第七个选项框,然后在最后边下拉列表中选择 Variables & Console View 。

在 Pauses 和 Generates Output 上重复同样动作,它们紧紧挨在 Starts 下边。

Variables & Console View 选项告诉调试器要显示本地变量清单,以及每当调试会话开始后显示控制台输出。如果你只想看到控制台输出,你可以选择 Console View。相反,如果只想看变量,则选择 Variable View。

Current Views 选项默认会显示最后一次调试的调试器视图。例如,如果你关闭了 Variables 仅仅显示控制台视图,那么下一次调试时就只会显示控制台视图。

关闭对话框,运行 app。

现在,每次编译运行 app 都会打开调试器——而不需要再点击那颗按钮了。尽管这个动作只需要 1 秒钟,但一周下来也会浪费你几分钟。毕竟,你的目标是做一个懒惰的程序员:]

应接不暇的 NSLog

在继续下一步之前,有一个重要的事情,就是回顾一下断点定义。

断点是程序中的一个时间点,允许你在运行程序的过程中执行某些动作。有时,程序可以在指定的某个点暂停,允许你查看程序状态或者单步执行代码。

你还可以运行代码,改变变量,让计算机引述莎士比亚语录。我们将在后面的内容中进行所有的这些动作。

注意:本教程将设计部分断点的高级用法。如果你还对诸如步进、步出、跳过之类的概念不太清楚,请阅读My App Crashed, Now What? 教程

运行 app。然后,加一个新朋友,以便记录他的礼物。不出意外,当你添加新朋友时,app 崩溃了。我们来搞定它。

这是你第一次运行 app 的样子:

这个项目需要头脑清醒。现在,你无法看到编译错误的原因。要找到它,需要添加异常断点,以记录错误的原因。

切换到断点导航器:

然后,点击面板底部的 + 号按钮。在弹出菜单中,选择 Exception Breakpoint… 。

你会看到这个对话框:

Exception 字段允许你通过 O-C、C++ 或所有语言来触发断点。保持默认的 All 不改变。

Break 字段允许你在错误被抛出还是被捕捉时暂停。保持默认的 on throw(抛出时)不变。如果你是在自己的代码中进行了错误处理,则可以选择 On Catch(捕捉时)。对于本教程,请使用 on throw。

后面两个字段稍后介绍。点击对话框外任意地方,关闭对话框,然后运行 app。

这次调试器给出了更清晰的结果:

看一眼控制台——它打印了一些消息,其中大部分都是不需要的。

调试代码时日志是至关重要的。日志信息需要被过滤,否则控制台将被垃圾信息所占据。干扰信息会浪费你的时间,因此必须过滤掉它们,否则你会在一个问题上花去更多的时间。

打开 AppDelegate.swift,你会看到在 didFinishLaunchingWithOptions 方法中看到一大堆过时的消息。选中它们,然后删除。

然后搜索其他的日志输出语句。打开搜索栏,查找 in viewDidLoad。

点击搜索结果,这将打开 FriendSelectionViewController.swift 并跳到产生了日志的那句代码。

注意,这次使用的是 print 语句,而不是 NSLog 语句。通常在 Swift 中,标准输出使用 print,当然你也可以用 NSLog。

以 log 方式输出日志信息有一个重要的地方,当你在多线程中输出日志时,你不必自己保持同步。这两种方法都可以用在调试会话中,将信息输出到控制台。

这里,管理你的日志语句的工作开始逐步累积。它看起来不太多,但每分钟都会增加。到了项目后期,这种零零散散的时间加起来很容易就突破了几个小时。

硬编码日志语句带来的另一个“好处”是,每当你添加了一个语句到代码库中,就相当于添加了新的 bug 到代码中。只需要敲了几个键,再加上自动完成,以及稍微不注意——你以前正常的 app 就多了一个 bug。

是时候将这些日志语句移除代码中了,它们只属于断点。

首先,注释两条 print 语句。然后,在每条语句左边的边栏中左键,添加一个断点。

你的代码窗口看起来应该是这个样子:

*** Terminating app due to uncaught exception 'NSInvalidArgumentException', reason: '+entityForName: nil is not a legal NSManagedObjectContext parameter searching for entity name 'Friend''

Core Data 代码有问题。
浏览代码,你会看到实体对象是通过 persistentContainer 的 viewContext 来创建的。你的直觉会告诉你,可能是 persistenContainer 导致了问题。

仔细看控制台,你会找到这个调用栈:

Failed to load model named GiftList
CoreData: error:  Failed to load model named GiftList
Could not fetch. Error Domain=Foundation._GenericObjCError Code=0 "(null)", [:]

这个信息告诉你 CoreData 无法加载一个数据模型,叫做: GiftList。如果你查看这个项目的数据模型,你会发现它实际上应该叫做 “GiftLister”。

看一眼 AppDelegate.swift 中的其它代码。

因为我的疏忽大意,我将 persistentContainer 的 name 参数写错了。我将 “GiftLister” 给写成了 “GiftList”。

将 “GiftList” 改成 “GiftLister”。

let container = NSPersistentContainer(name: "GiftLister")

运行 app。现在添加一个朋友。哈—— app 现在好像正常了。

断点和表达式

看起来不错,但你可能注意到一点,断点输出的消息中不包含时间,对于调试来说这可是很有用的哦!幸好,通过断点表达式这个问题很好搞定!

注意:日期输出十分有用,但同时会让日志输出变慢,因为系统需要查询日期信息。记住哪怕是调用日志输出自身都会导致 app 性能降低。

让我们恢复原来的日志语句。右键(或 ctrl+左键)点击 FriendSelectionViewController.swift 中的第二个断点。点击 Edit Breakpoint。在 action 列表中将 Log Message 修改为 Debugger Command ,然后在文本框中输入:

expression NSLog("Loading friends...")

类似这个样子:

Debugger 命令将在运行时计算这个表达式。

运行 app。你会看到:

2012-12-20 08:57:39.942 GiftLister[1984:11603] Loading friends...

在断点中添加 NSLog 语句意味着你不需要为输出某些重要的数据而停止程序的执行,这会减少你产生新 bug 的机会,因为你根本没有碰代码——最好的一点是,你不需要在发布前争分夺秒地删除代码中的 debug 语句。

现在关闭 app 中的日志输出。非常简单,点击 debugger 视图中的断点按钮即可。

点击这个按钮,运行 app。现在控制台干净多了。你也可以在断点导航器中关闭某个断点。

现在,不得不一一注释代码中日志语句的日子一去不复返了!:]

MARKSs、TODOs、FIXMEs、我的个天!

接下来要做的事情是创建更多的朋友,这样你可以记录一张建议给他们的礼物清单。

运行 app。点击 Add a friend 行。app 会显示另一个 view controller,包含一个姓名输入框和一个日期选择器。输入名字,选择生日,然后点击 OK 按钮。

这将会回到根控制器,而你添加的朋友将显示在列表中。再次点击 Add a friend。

输入新朋友的名字,这次将生日设置为 2010 年 2 月 31 日。

在正常的日期选择器中,这样一个日期根本是不可能出现的。然而对于我们的这个神奇则不然。因为大脑抽风,我决定使用普通的 picker 来代替 date picker。这样我就不得不重写日期校验逻辑,同时也带来了几个 bug。

点击 OK 按钮。很不幸,这个无效的日期被保存了。让我们来看看这会导致什么样的错误吧。

打开 AddFriendViewController.swift ,在 saveFriend 方法开始处添加一个断点。

注意:在一个大文件中定位方法比较费事。比较麻烦的做法是逐行扫描代码,直到找到这个方法。另外一种方法是使用跳转栏,通过方法列表来查找。我最喜欢的方法是使用查找,当然不是在搜索栏中,而是在跳转栏中搜索。点击跳转栏,然后开始键入文本。你的方法名就会出现,就像你在搜索栏中所做的一样。

在模拟器中,点击 Add a friend 按钮,很之前一样,添加一个无效的生日。单步执行,知道你到达这一行:

if name.hasText, isValidDateComposedOf(month: selectedMonth, day: selectedDay, year: selectedYear) {

步进到 isValidDateComposedOf 方法。很显然,校验代码出错了——这里什么都没有!只有一个注释,表明以后会实现它。

注释是一种很好的描述代码块意图的方法,但你无法用它们来进行任务的管理。再小的项目,也有太多的任务,注释的任务经常会被遗忘。

真正防止它们被遗忘的方法是让它们非常显眼。其中一种方式就是让信息在跳转栏中显眼。

打开跳转栏,你会看到:

你还可以用 FIXME: 或者 MARK:。

代码中的 MARK:、TODO: 和 FIXME: 注释语句会显示在跳转栏中。此外,如果你在 MARK: 后面加一个连字符,比如 MARK: - UIPickerViewDataSource,跳转栏会在注释前面添加一个水平分割线,就更容易阅读了!

这些语句并不会让编译器当成警告或错误,但会比方法底部的普通注释更容易看到。这是让注释以及注释所标注的任务形成一个待办任务清单,从而突出于代码库。

但是,为什么不能让 Xcode 编译器为代码中出现的 TODO: 、 FIXME: 注释发出警告呢?我真心觉得这很不错!

要做到这一点,你需要在项目中添加一个 build 脚本,搜索代码中所有的 TODO: 和 FIXME: 注释,并将之作为编译器警告。

要创建 build 脚本,请在项目导航器中选中项目,然后点 Build Phases。点击 + 按钮添加一个新的 Run Script Phase。

然后,编写 build 脚本如下:

TAGS="TODO:|FIXME:"
echo "searching ${SRCROOT} for ${TAGS}"
find "${SRCROOT}" \( -name "*.swift" \) -print0 | xargs -0 egrep --with-filename --line-number --only-matching "($TAGS).*\$" | perl -p -e "s/($TAGS)/ warning: \$1/"

你的 Run Script 代码看起来是这样的:

编译项目,打开 issue 导航器:

现在 TODO: 注释被显示成一个 shell 脚本调用警告,这样你总没办法遗忘了吧?:]

变量视图和返回值

现在,我们来看一个从 Xcode4.4 就有的小功能。

重新运行 app,保持在空的校验方法中的断点不变。现在,步出这段代码。打开 debugger 的 Variables 视图,你会看到:

显示返回值并不是多稀罕的功能,但它足以节省你很多时间。试想从这里调用这段代码:

if name.hasText, isValidDateComposedOf(month: selectedMonth, day: selectedDay, year: selectedYear) {

这句代码会调用 isValidDateComposedOf 方法并立即在表达式中使用它的返回值。

在这个功能出现之前,你需要离开这行代码,如果你想查看返回值的话,还必须打印它。现在,你可以简单地步出一个方法,然后在调试器中查看返回值。

成功的 debugging 的条件

有时,我们不得不以固定的间隔修改应用程序的状态。有时这种改变发生在大量时间序列之中,这使得正常的 debugging 变得十分困难。这就要使用到条件(conditions)了。

现在,app 中列出了几个好友,点击他们的名字将打开礼物界面。这是一个简单的分组表格,可以对表格进行排序,以决定某件礼物是否应该购买。

点击导航条上的 add 按钮添加一件礼物。名称输入 shoes,价格输入 88。然后点 OK 按钮。这样 shoes 就显示在礼物清单中了。

接着添加这些礼物:

  • Candles / 1.99
  • Sleigh / 540.00
  • XBox / 299.99
  • iPad / 499.99

哎呀,你突然想到,其实你想添加的应该是 PS4 而不是 XBox。你可以点击这一行进行修改,但为了方便我们演示的缘故,你可以通过 debugger 来进行修改。

打开 GiftListsViewController.swift 找到 cellForRowAtIndexPath。添加一个断点在这一行下面:.

if (gift) {

看起来是这个样子:

在这个断点上右键(或者 ctrl+左键),选择 Edit Breakpoint。

然后来添加条件。你可以把它当成一个简单的 if 语句。添加这个代码:

gift.name == "Xbox"

然后,点击 segmented 控件上的 Bought 按钮。表格会刷新,但断点不会被触发。

点击 segmented 控件的 Saved 按钮。这次会暂停了,同时所选中的礼物会高亮显示在控制台中。

在控制台中,输入下列代码:

expression gift.name = "PS4"

现在,点击 Run 按钮,表格会继续加载。PS4 会替换掉礼物中的 XBox。

你可以通过修改循环变量来达到同样效果。ctrl+左键(或右键)点击这个断点,选择 Edit Breakpoint。这次,将 Condition 栏清空,将 Ignore 设置为数字 2,然后点 Done。

现在,点击 segmeted 控件的 Bought 按钮,然后点击 segmented 控件的 Saved 按钮。这将触发同一个断点。

要确认这是我们需要的对象,可以输入:

(lldb) po gift

现在,和之前一样修改对象的状态:

(lldb) expression gift.name = "Xbox"

表格会显示出修改的结果。实时修改是不是更爽?

执行清理动作

在开发数据驱动的 app 时,经常需要清除数据库。可以用很多办法去做这个事情,比如重置 iPhone 模拟器,或者在 Mac 上查找真实的数据库并进行删除。重复干这样的事让人厌倦,我们可以偷点懒,让 Xcode 为我们做这个。

一开始需要创建一个 shell 脚本。一个 shell 脚本是一个命令集合,让操作系统自动执行某些动作。要创建 shell 脚本,需要用 application 菜单创建一个新文件。点击 File\New\File 或 Command-N。在分类中,先选择 Other 然后选择 Shell Script 类型。

文件名设为 wipe-db.sh。

为了真正清除数据库,我们需要用 remove 命令以及数据库的完整路径(包括当前用户名)。你可以用 Finder 或者终端程序找到数据库所在位置,然后复制/粘贴它的路径到 shell 脚本中。但在 Xcode 8 中,保存数据库的文件夹在每次编译、运行 app 时总是不固定的。

要解决这个问题,我们可以使用 whoami 命令输出当前用户,用通配符 * 来代替会变的文件夹。

因此可以这样编写脚本:

rm /Users/$(whoami)/Library/Developer/CoreSimulator/Devices/*/data/Containers/Data/Application/*/Library/Application\ Support/GiftLister.sqlite

保存并关闭脚本。

默认情况下,shell 脚本是只读的。你可以在终端中将脚本设置为可执行。

如果你找不到终端程序,你可以在应用程序文件夹的工具中找到它。

打开终端,进入你的 home 目录,输入:

YourComputer$ cd ~

然后列出目录中的文件:

YourComputer$ ls

我们需要切到项目目录。如果你的项目目录位于桌面文件夹,你可以这样进入到项目目录:

YourComputer$ cd Desktop
YourComputer$ cd GiftLister

如果要向上返回一级目录,你可以用:

YourComputer$ cd ..

经过在终端中进行一番艰苦的摸索,我们终于可以看到我们的项目文件了。要将 shell 脚本修改为可执行,需要:

YourComputer$ chmod a+x wipe-db.sh

chmod 用于修改文件的权限。a+x 表示文件可以被所有用户、组和其它人执行。

哇……好麻烦。深呼吸。这是必需的。做这么多的工作目的就是为了偷懒。:]

关闭终端,回到 Xcode。打开 AppDelegate.swift。

在 didFinishLaunchingWithOptions 第一行打断点。右键(或 ctrl+左键)点击断点,选择 Edit Breakpoint,添加一个 action 并选择 Shell Command。在后面的对话框中,点击 Choose 然后选择我们所创建的脚本。勾选 Automatically continue after evaluating 选项,然后关闭对话框。

如果模拟器正在运行,请关闭它。

编译运行,数据库被删除了。

模拟器默认会缓存许多数据,因此最好是用 Xcode 的 Product/Clean 菜单执行一次“干净”的 build,然后再 build & run。否则,当你启动 app,停止、再次运行后。缓存的数据会和全新的数据库混在一起。

当 App 还在开发时,清空数据库只需要按一下按钮。如果不需要这个功能,只需要关闭这个断点。

注意:我们创建的脚本仅仅包含了一条简单的 Unix 命令,用于删除文件。你也可以在脚本中通过一个 PHP 文件来干同样的事情。你还可以打开一个 Java 程序、Python 脚本或者任意电脑上的其他程序。这样,你不需要学习 shell 脚本就能通过断点来操作底层操作系统。

加分章节:深入保存方法

此时,我们的 app 已经拥有不少数据了。是时候保存它们了。

对于这类 app,保存工作应当经常性的进行,以免丢失数据。

我们的 app 的情况有所不同,它只会在用户退出 app 时保存数据。

如果你现在没有看见根 View Controller,请点击导航条上的 Back 按钮返回根控制器。然后按下 Home 键。你可以点击模拟器菜单的 Hardware\Home 或者 Shift-Command-H 返回模拟器桌面。

从 Xcode 中终止程序,然后编译运行。table view 中是空的。这个 app 什么也没有保存。

打开 AppDelegate.swift。在 applicationDidEnterBackground 方法,你会看到问题在于 doLotsOfWork 方法。这个方法无法按时完成,iOS 就终止了 app,无论它有没有完成收尾的工作。这导致 saveData 方法还没有被调用。

首先需要确保数据被保存。将 saveContext 方法调用放到 doLotsOfWork 方法调用之前:

saveContext()
doLotsOfWork()

现在在 doLotsOfWork 这行打断点。右键(或 ctrl+左键)点击断点,然后选择 Edit Breakpoint。在 Action 中选择 sound action 以及 Submarine 声效。在使用 sound action 时,尽量避免使用系统声音,以免断点被忽略。

然后,勾选 Automatically continue after evaluating。

最后,点击 build & run。

当 app 再次启动,添加一个新的好友,然后在模拟器中按下 Home 键。当 app 关闭后,你会听到有海底泉的声音,表明我们的数据已经保存。

用 Xcode 停止程序,按下 Run。你会看到所有的数据都显示了。通过声音,我们能够知道某些代码已经被执行,不需要我们查看日志。你还可以提供自定义生效,比如在某种严重崩溃时播放爆炸声音。

这需要将你的声音文件放到这个文件夹:

YOUR_HOME_DIRECTORY/Library/Sounds

在使用这些声音之前你必须重启 Xcode,但注意这可能成为某些人的恶作剧:]

还有一个有趣的地方。找到 FriendSelectionViewController 中的第一个断点,右键(或 ctrl+左键)点击这个断点,选择 Edit Breakpoint,在对话框中,点击 + 按钮,这里可以添加更多的动作,而不仅仅是断点本身。

选择 Log Message 动作,这次,我们输入 To be, or not to be。勾选 Speak Message 单选框,然后点击 Done。对话框最终显示成这个样子:

build & run,好好乐一下子吧!

注意:除了新奇,这个功能也是很有用的!语音消息在调试复杂的网络代码等情形时尤其有用。

结束

你可以在这里下载已完成的项目。

如你所见,Xcode 调试工具在面对日常开发中的挑战时,非常灵活。例如,LLDB 提供了动态查看和修改代码的能力,而不会增加更多的 bug。

无论如何,这只是一个开始。LLDB 还提供了许多其它特性,比如自定义变量概览、动态断点、用 Python 定义调试脚本等。

当然,将 NSLog() 和调试代码删除会是一个麻烦,但最终你会发现你的项目变得更健壮。你不需要在发布的前一晚为移除所有的调试代码而忧心忡忡,也不需要为创建清晰的调试环境而编写复杂的宏代码。Xcode 为你提供了能够让你轻松度过最后一天的一整套工具。

如果你想了解更多,可以从 LLDB 开始。一个学习 LLDB 的好去处就是 Brian Moakley 录制的视频教程:Using LLDB in iOS

在 WWDC 2016 大会中也介绍了LLDB 的新特性:Debugging Tips and Tricks

有任何问题和建议,请在下面留言!

作者:kmyhy 发表于2017/5/25 18:31:29 原文链接
阅读:153 评论:0 查看评论

动态加载so库的实现方法与问题处理

$
0
0
前一阵项目上要求实现App的so库动态加载功能,因为这块本来就有成熟的方案,所以一般的实现没什么难度。可是到项目测试中,才发现有不少意料之外的情况,需要一一针对处理,故此记录一下具体的解决办法,以供后来者参考。

按App加载so库的正常流程,在编译前就要把so文件放到工程的jniLibs目录,这样会把so直接打包进apk安装包,然后App在启动时就会预先加载so库。具体的加载代码一般是在Activity页面中增加下面几行,表示在实例化该页面的时候,一开始就从系统目录加载名为libjni_mix.so的库:
	static {
		System.loadLibrary("jni_mix");
	}

若要运用动态加载技术,编译前不把so文件放入jniLibs目录(原因很多,比如想减小安装包的大小),自然打包生成的安装包也不包含该so。接着在手机上安装这个apk并启动App,如果App的运行不涉及到jni方法的调用,那相安无事就当so不存在;如果App打开了某个页面,而该页面又需要调用jni方法,则App自动到指定地址下载需要的so文件,然后保存到用户目录,并从用户目录加载该so,最后再调用jni方法。
把下载完成的so文件复制到用户目录,可参考以下代码(注意判断文件大小,如果用户目录已经存在相同大小的文件,就无需重复拷贝了):
	public static boolean copyLibraryFile(Context context, String origPath, String destPath) {
		boolean copyIsFinish = false;
		try {
			File dirFile = new File(destPath.substring(0, destPath.lastIndexOf("/")));
			if (dirFile.exists() != true) {
				dirFile.mkdirs();
			}
			FileInputStream is = new FileInputStream(new File(origPath));
			File file = new File(destPath);
			if (file.exists()) {
				Log.d(TAG, "src file size="+is.available());
				Log.d(TAG, "dest file size="+file.length());
				if (file.length() == is.available()) {
					return true;
				}
			}
			file.createNewFile();
			FileOutputStream fos = new FileOutputStream(file);
			byte[] temp = new byte[1024];
			int i = 0;
			while ((i = is.read(temp)) > 0) {
				fos.write(temp, 0, i);
			}
			fos.close();
			is.close();
			copyIsFinish = true;
		} catch (Exception e) {
			e.printStackTrace();
		}
		return copyIsFinish;
	}

so文件复制完成,接下来就可以加载用户目录下的so了,完整的加载代码如下所示:
		File dir = this.getDir("libs", Activity.MODE_PRIVATE);
		File destFile = new File(dir.getAbsolutePath() + File.separator + fileName);
		if (copyLibraryFile(this, path, destFile.getAbsolutePath())){
			//使用load方法加载内部储存的SO库
			System.load(destFile.getAbsolutePath());
			//下面调用jni方法,举例如下:
			//String desc = JniCpuActivity.cpuFromJNI(1, 0.5f, 99.9, true);
		}

不出意外的话,以上代码已经实现so库的动态加载功能。可是这并不意味着大功告成,因为项目里面用到了第三方的sdk,即一个增强现实厂商推出的EasyAR,他们的sdk除了libEasyAR.so,还有另外一个jar包即EasyAR.jar。虽然App工程里面对so文件做了动态加载处理,但运行时加载so仍然报错“java.lang.UnsatisfiedLinkError: dalvik.system.PathClassLoader *** couldn't find "libEasyAR.so"”。排查结果发现,EasyAR.jar里面的EasyARNative类会从系统目录加载so库,也就是仍然调用了“System.loadLibrary("EasyAR");”。因为App无法把so文件复制到系统目录,所以导致System.loadLibrary方法找不到libEasyAR.so。

关于系统目录找不到so库的问题,解决办法找到了以下两个:
1、把App动态加载so的目录加入到系统目录列表nativeLibraryDirectories,
	private static void createNewNativeDir(Context context) throws Exception {
		PathClassLoader pathClassLoader = (PathClassLoader) context.getClassLoader();
		Field declaredField = Class.forName("dalvik.system.BaseDexClassLoader").getDeclaredField("pathList");
		declaredField.setAccessible(true);
		Object pathList = declaredField.get(pathClassLoader);
		// 获取当前类的属性
		Object nativeLibraryDirectories = pathList.getClass().getDeclaredField(
				"nativeLibraryDirectories");
		((Field) nativeLibraryDirectories).setAccessible(true);
		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) {
			// 获取 DEXPATHList中的属性值
			File[] files = (File[]) ((Field) nativeLibraryDirectories).get(pathList);
			Object filesss = Array.newInstance(File.class, files.length + 1);
			// 添加自定义.so路径
			Array.set(filesss, 0, getLibraryDir(context));
			// 将系统自己的追加上
			for (int i = 1; i < files.length + 1; i++) {
				Array.set(filesss, i, files[i - 1]);
			}
			((Field) nativeLibraryDirectories).set(pathList, filesss);
		} else {
			ArrayList<File> files = (ArrayList<File>) ((Field) nativeLibraryDirectories).get(pathList);
			ArrayList<File> filesss = (ArrayList<File>) files.clone();
			filesss.add(0, getLibraryDir(context));
			((Field) nativeLibraryDirectories).set(pathList, filesss);
		}
	}
不料好事多磨,该办法在4.4真机上测试通过,但在6.0真机上依然出现闪退。

2、删除EasyAR.jar里面的EasyARNative.class文件,另外在项目工程新建同样类名且同样文件内容的EasyARNative.java,只是把里面的下述代码删除:
 static {
  System.loadLibrary("EasyAR");
 }
这样做的目的是不从系统目录加载so,只从用户目录加载so文件。接下来重新编译程序,4.4真机和6.0真机都能正常调用jni方法了。


正所谓一波三折,麻烦事还没结束,换台运行Android7.0的真机,动态加载so时再次出现闪退,真叫人欲哭无泪(出错日志为Java.lang.UnsatisfiedLinkError: dlopen failed: "***.so" is 32-bit instead of 64-bit)。只能硬着头皮再三想办法,查阅了大量资料,最终定位原因如下:
一、所有的App在运行时,都是由Zygote进程创建VM再运行的。
二、一般设备只支持32位系统,但有些新设备已经支持64位(同时兼容32位)。对于这些新设备来说,有两个Zytgote(一个32位,一个64位)进程同时运行。
三、当App运行在64位系统上,又区分以下三种情况: 
1、如果App只包含64位的so库,则它将运行在一个64位的进程中,即VM是由Zytgote 64创建的。
2、如果App包含32位的so库,则它将运行在一个32位的进程中,即VM是由Zytgote创建的。
3、如果App不包含任何so库,则它将默认运行在64位的进程中。
显然上面采用动态加载的App属于第三种情况,此时启动了64位进程,但动态加载的so库却是32位的,所以会闪退。如果不采用动态加载,一开始就把so库打进安装包,则属于第二种情况,App运行时启动的是32位进程,此时不会闪退。

因此,对于7.0真机这种64位的系统,处理动态加载so的可能办法有两个:
1、所有so文件都编译为64位版本,但这样就无法在32位系统上调用so,故而不可行;
2、先把一个32位的so文件打进安装包,其它so库在运行时动态加载,这样App启动的是32位进程,动态加载的so库也是32位版本,运行时就不再闪退;


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

__________________________________________________________________________
博主现已开通微信公众号“老欧说安卓”,打开微信扫一扫下面的二维码,或者直接搜索公众号“老欧说安卓”添加关注,更快更方便地阅读技术干货。

作者:aqi00 发表于2017/5/26 10:09:25 原文链接
阅读:211 评论:0 查看评论

android sqlite 判断表和表中字段是否存在方法

$
0
0
/**
    *检查某表是否存在
    * @param tableName 表名
    * @return  true:存在  false:不存在
    */


public boolean tabIsExist(String tabName){
        boolean result = false;
        if(tabName == null){
                return false;
        }
        Cursor cursor = null;
        try {
               
                String sql = "select count(*) as c from sqlite_master where type ='table' and name ='"+tabName.trim()+"' ";
                cursor = mUDB.rawQuery(sql, null);
                if(cursor.moveToNext()){
                        int count = cursor.getInt(0);
                        if(count>0){
                                result = true;
                        }
                }
                
        } catch (Exception e) {
        }                
        return result;
}


/**
    *检查表中某列是否存在
    * @param db
    * @param tableName 表名
    * @param columnName 列名
    * @return  true:存在  false:不存在
    */
    private boolean checkColumnExists2(SQLiteDatabase db, String tableName , String columnName) {
        boolean result = false ;
        Cursor cursor = null ;


        try{
            cursor = db.rawQuery( "select * from sqlite_master where name = ? and sql like ?"
               , new String[]{tableName , "%" + columnName + "%"} );
            result = null != cursor && cursor.moveToFirst() ;
        }catch (Exception e){
            Log.e("","checkColumnExists2..." + e.getMessage()) ;
        }finally{
            if(null != cursor && !cursor.isClosed()){
                cursor.close() ;
            }
        }


        return result ;
    }

作者:shaoyezhangliwei 发表于2017/5/26 11:27:18 原文链接
阅读:109 评论:0 查看评论

Android 进阶7:进程通信之 AIDL

$
0
0

读完本文你将了解:

记得 2015 年实习面试,笔试题里就有这道题:请介绍下 AIDL。

当时的我是懵逼的,只好老老实实空着。没想到后来面试时面试官大哥嘿嘿一笑说他也没用过这玩意,真是够实诚的。

笔试完查了这个知识点,似懂非懂也没深究。去年看《安卓开发艺术探索》时也学了这部分内容,但是可能当时水平不够,或者只是看起来努力,没有真正理解精髓,没多久就又忘了个七八成。

这次复习,还是老老实实敲出来,总结成文字吧,方便以后回顾。

AIDL 是什么

AIDL(Android 接口定义语言) 是 Android 提供的一种进程间通信 (IPC) 机制。

我们可以利用它定义客户端与服务使用进程间通信 (IPC) 进行相互通信时都认可的编程接口。

在 Android 上,一个进程通常无法访问另一个进程的内存。 尽管如此,进程需要将其对象分解成操作系统能够识别的原语,并将对象编组成跨越边界的对象。

编写执行这一编组操作的代码是一项繁琐的工作,因此 Android 会使用 AIDL 来处理。

通过这种机制,我们只需要写好 aidl 接口文件,编译时系统会帮我们生成 Binder 接口。

AIDL 支持的数据类型

共 4 种:

  1. Java 的基本数据类型
  2. List 和 Map
    • 元素必须是 AIDL 支持的数据类型
    • Server 端具体的类里则必须是 ArrayList 或者 HashMap
  3. 其他 AIDL 生成的接口
  4. 实现 Parcelable 的实体

AIDL 如何编写

AIDL 的编写主要为以下三部分:

  1. 创建 AIDL
    • 创建要操作的实体类,实现 Parcelable 接口,以便序列化/反序列化
    • 新建 aidl 文件夹,在其中创建接口 aidl 文件以及实体类的映射 aidl 文件
    • Make project ,生成 Binder 的 Java 文件
  2. 服务端
    • 创建 Service,在其中创建上面生成的 Binder 对象实例,实现接口定义的方法
    • onBind() 中返回
  3. 客户端
    • 实现 ServiceConnection 接口,在其中拿到 AIDL 类
    • bindService()
    • 调用 AIDL 类中定义好的操作请求

AIDL 实例

下面以实例代码演示一个 AIDL 的编写。

1.创建 AIDL

①创建要操作的实体类,实现 Parcelable 接口,以便序列化/反序列化


package net.sxkeji.shixinandroiddemo2.bean;

import android.os.Parcel;
import android.os.Parcelable;

public class Person implements Parcelable {
    private String mName;

    public Person(String name) {
        mName = name;
    }

    protected Person(Parcel in) {
        mName = in.readString();
    }

    public static final Creator<Person> CREATOR = new Creator<Person>() {
        @Override
        public Person createFromParcel(Parcel in) {
            return new Person(in);
        }

        @Override
        public Person[] newArray(int size) {
            return new Person[size];
        }
    };

    @Override
    public int describeContents() {
        return 0;
    }

    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeString(mName);
    }

    @Override
    public String toString() {
        return "Person{" +
                "mName='" + mName + '\'' +
                '}';
    }
}

实现 Parcelable 接口是为了后序跨进程通信时使用。

关于 Parcelable 可以看我的这篇文章 Android 进阶6:两种序列化方式 Serializable 和 Parcelable

注意 实体类所在的包名。

②新建 aidl 文件夹,在其中创建接口 aidl 文件以及实体类的映射 aidl 文件

在 main 文件夹下新建 aidl 文件夹,使用的包名要和 java 文件夹的包名一致:

shixiznhang

先创建实体类的映射 aidl 文件,Person.aidl:

// Person.aidl
package net.sxkeji.shixinandroiddemo2.bean;

//还要和声明的实体类在一个包里
parcelable Person;

在其中声明映射的实体类名称与类型

注意,这个 Person.aidl 的包名要和实体类包名一致。

然后创建接口 aidl 文件,IMyAidl.aidl:

// IMyAidl.aidl
package net.sxkeji.shixinandroiddemo2;

// Declare any non-default types here with import statements
import net.sxkeji.shixinandroiddemo2.bean.Person;

interface IMyAidl {
    /**
     * 除了基本数据类型,其他类型的参数都需要标上方向类型:in(输入), out(输出), inout(输入输出)
     */
    void addPerson(in Person person);

    List<Person> getPersonList();
}

在接口 aidl 文件中定义将来要在跨进程进行的操作,上面的接口中定义了两个操作:

  • addPerson: 添加 Person
  • getPersonList:获取 Person 列表

需要注意的是:

  • 非基本类型的数据需要导入,比如上面的 Person,需要导入它的全路径。
    • 这里的 Person 我理解的是 Person.aidl,然后通过 Person.aidl 又找到真正的实体 Person 类。
  • 方法参数中,除了基本数据类型,其他类型的参数都需要标上方向类型
    • in(输入), out(输出), inout(输入输出)

③Make Project ,生成 Binder 的 Java 文件

AIDL 真正的强大之处就在这里,通过简单的定义 aidl 接口,然后编译,就会为我们生成复杂的 Java 文件。

点击 Build -> Make Project,然后等待构建完成。

然后就会在 build/generated/source/aidl/你的 flavor/ 下生成一个 Java 文件:

shixinzhang

现在我们有了跨进程 Client 和 Server 的通信媒介,接着就可以编写客户端和服务端代码了。

我们先跑通整个过程,这个文件的内容下篇文章介绍。

2.编写服务端代码

创建 Service,在其中创建上面生成的 Binder 对象实例,实现接口定义的方法;然后在 onBind() 中返回

创建将来要运行在另一个进程的 Service,在其中实现了 AIDL 接口中定义的方法:

public class MyAidlService extends Service {
    private final String TAG = this.getClass().getSimpleName();

    private ArrayList<Person> mPersons;

    /**
     * 创建生成的本地 Binder 对象,实现 AIDL 制定的方法
     */
    private IBinder mIBinder = new IMyAidl.Stub() {

        @Override
        public void addPerson(Person person) throws RemoteException {
            mPersons.add(person);
        }

        @Override
        public List<Person> getPersonList() throws RemoteException {
            return mPersons;
        }
    };

    /**
     * 客户端与服务端绑定时的回调,返回 mIBinder 后客户端就可以通过它远程调用服务端的方法,即实现了通讯
     * @param intent
     * @return
     */
    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        mPersons = new ArrayList<>();
        LogUtils.d(TAG, "MyAidlService onBind");
        return mIBinder;
    }
}

上面的代码中,创建的对象是一个 IMyAidl.Stub() ,它是一个 Binder,具体为什么是它我们下篇文章介绍。

别忘记在 Manifest 文件中声明:

<service
    android:name="net.sxkeji.shixinandroiddemo2.service.MyAidlService"
    android:enabled="true"
    android:exported="true"
    android:process=":aidl"/>

服务端实现了接口,在 onBind() 中返回这个 Binder,客户端拿到就可以操作数据了。

3.编写客户端代码

这里我们以一个 Activity 为客户端。

①实现 ServiceConnection 接口,在其中拿到 AIDL 类

private IMyAidl mAidl;

private ServiceConnection mConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName name, IBinder service) {
        //连接后拿到 Binder,转换成 AIDL,在不同进程会返回个代理
        mAidl = IMyAidl.Stub.asInterface(service);
    }

    @Override
    public void onServiceDisconnected(ComponentName name) {
        mAidl = null;
    }
};

在 Activity 中创建一个服务连接对象,在其中调用 IMyAidl.Stub.asInterface() 方法将 Binder 转为 AIDL 类。

②接着绑定服务

Intent intent1 = new Intent(getApplicationContext(), MyAidlService.class);
bindService(intent1, mConnection, BIND_AUTO_CREATE);

要执行 IPC,必须使用 bindService() 将应用绑定到服务上。

注意:

5.0 以后要求显式调用 Service,所以我们无法通过 action 或者 filter 的形式调用 Service,具体内容可以看这篇文章 Android 进阶:Service 的一些细节

③拿到 AIDL 类后,就可以调用 AIDL 类中定义好的操作,进行跨进程请求

@OnClick(R.id.btn_add_person)
public void addPerson() {
    Random random = new Random();
    Person person = new Person("shixin" + random.nextInt(10));

    try {
        mAidl.addPerson(person);
        List<Person> personList = mAidl.getPersonList();
        mTvResult.setText(personList.toString());
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

运行结果

shixinzhang

可以看到,Activity 与 另外一个进程的 Service 通信成功了。

总结

这篇文章介绍了 AIDL 的简单编写流程,其中也踩过一些坑,比如文件所在包的路径不统一,绑定服务收不到回调等问题。

到最后虽然跨进程通信成功,但是我们还是有很多疑问的,比如:

  • AIDL 生成的文件内容?
  • 什么是 Binder?
  • 为什么要这么写?

知其然还要知其所以然,这一切都要从 Binder 讲起,且听下一回合介绍。

代码地址

Thanks

《Android 开发艺术探索》
https://developer.android.com/guide/components/aidl.html
http://www.jianshu.com/p/b9b15252b3d6
http://rainbow702.iteye.com/blog/1149790

作者:u011240877 发表于2017/5/26 11:43:31 原文链接
阅读:240 评论:4 查看评论

Flutter进阶—实现动画效果(六)

$
0
0

上一篇文章中,我们之前对BarChart.lerp的定义并不是高效的,我们正在创建的Bar实例,仅作为Bar.lerp的参数给出,并且针对动画参数t的每个值重复出现。每秒60帧,这意味着可能很多Bar实例被送到垃圾收集器,即使是相对较短的动画。

我们可以采用以下三种解决方案:

  • Bar实例可以通过在Bar类中仅创建一次而不是每次调用collapsed来重复使用,但这种方法不适合我们的应用程序。

  • 重用可以通过BarChartTween来处理,通过使其构造函数创建一个列表_tween的BarTween实例,在创建补间条形图时使用(i) => _tweens[i].lerp(t)。这种方法破坏了使用静态方法lerp的惯例,静态BarChart.lerp中没有涉及到任何对象,用于在动画持续时间内存储补间列表。相反,BarChartTween对象完全适合这一点。

  • 假设Bar.lerp中有合适的条件逻辑,可以使用null来表示折叠条,这种方法是非常高效的,但是需要注意避免引用或误解null。null常用在Flutter SDK中,其中静态方法lerp会将null视为动画终点,通常将其解释为某种不可见元素,比如完全透明的颜色或零尺寸的图形元素。在我们的代码中,lerpDouble将null视为零,除非两个动画结束点都为null。

综合考虑之下,我们使用最后一种解决方案,首先我们需要更新BarChart的部分代码。

class BarChart {
  // ...
  static BarChart lerp(BarChart begin, BarChart end, double t) {
    final barCount = max(begin.bars.length, end.bars.length);
    final bars = new List.generate(
      barCount,
      (i) => Bar.lerp(begin._barOrNull(i), end._barOrNull(i), t)
    );
    return new BarChart(bars);
  }
  // ...
}

然后我们还需要更新一下Bar的条件逻辑。

class Bar {
  Bar(this.x, this.width, this.height, this.color);
  final double x;
  final double width;
  final double height;
  final Color color;

  static Bar lerp(Bar begin, Bar end, double t) {
    if(begin == null && end == null)
      return null;
    return new Bar(
        lerpDouble((begin??end).x, (end??begin).x, t),
        // ?:变量可以为null
        lerpDouble(begin?.width, end?.width, t),
        lerpDouble(begin?.height, end?.height, t),
        Color.lerp((begin??end).color, (end??begin).color, t)
    );
  }
}

现在我们的应用程序里,如何将使用折叠的条形作为不可见元素的判断,写在Bar.lerp的条件逻辑中,实现我们想要的高效率。换一个角度来看,不知道大家有没有发现,现在代码的可维护性已经不如上一个版本了。这就是为什么之前选择看起来效率较低的解决方案。在性能与可维护性之间选择,需要通过衡量之后再作出决定。

未完待续~~~

作者:hekaiyou 发表于2017/5/26 12:17:54 原文链接
阅读:98 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


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