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

网格变形动画

$
0
0

原文:Mesh Transforms
作者:Bartosz Ciechanowski
译者:kmyhy

我是 transform 属性的超级粉丝。让 UIView 或者 CALayer 的形体发生改变的最简单方法就是联合使用旋转、平移和缩放。在易于适用的同时,常规变换所能实现的效果也同时被限制住了——比如一个矩形只能变换成其它四边形。这是毫无疑问的,但除此之外我们还可以做的更多。

本文介绍网格变形。网格变形的核心概念非常简单:你可以将一个 layer 划分成若干个顶点,通过移动顶点的方式让几何形状发生改变:

本文的主要内容介绍了一个 Core Animateion 的私有 API,这个框架从 iOS 5.0 之后开始引入。不用担心你的思想会被私有 API 所“污染”,因为本文的第二部分会介绍一个可替换方案:一个与之类似的开源框架。

CAMeshTransform

当我第一次看见 iOS 运行时库的头文件时,我不禁为之痴迷。有这么多的私有类、隐藏属性,让人大开眼界。其中最有趣的一个发现就是 CAMeshTransform 以及 CALayer 的 meshTransform 属性。有一股强烈的求知欲望让我非要把搞清楚,直到最近我终于吃透了它。它看起来非常复杂,但网格转换的底层概念还是很容易搞懂的。CAMeshTransform 有一个构造方法是这个样子的:

+ (instancetype)meshTransformWithVertexCount:(NSUInteger)vertexCount
                                    vertices:(CAMeshVertex *)vertices
                                   faceCount:(NSUInteger)faceCount
                                       faces:(CAMeshFace *)faces
                          depthNormalization:(NSString *)depthNormalization;

这个方法清楚地描述了网格转换的基本构成——顶点、面和一个字符串用于描述 depth normalization。我们接下来会逐一讨论它们。

注:不幸的是,结构体内部的字段名被编译后无法看到,因此我不得不用自己的理解进行描述。原始的字段名可能不一样,但意思应该是一致的。

顶点 Vertex

一个顶点是一个拥有两个字段属性的结构:

typedef struct CAMeshVertex {
    CGPoint from;
    CAPoint3D to;
} CAMeshVertex;

CAPoint3D 和普通的 CGPoint 非常像——只是增加了一个 z 坐标:

typedef struct CAPoint3D {
    CGFloat x;
    CGFloat y;
    CGFloat z;
} CAPoint3D;

这样,CAMeshVertex 的用途就不难猜出了:它描述了一个 layer 平面上的二维点到 3D 空间中的点的映射。CAMeshVertex 定义了这样的行为:“获取 layer 上的点,并将这个点移动到指定位置。”因为 CAPoint3D 由 x、y、z 字段构成,因此网格转换注定不会是平面的:

平面 Face

CAMeshFace 也很简单:

typedef struct CAMeshFace {
    unsigned int indices[4];
    float w[4];
} CAMeshFace;

indecies 数组保存了一个平面上的 4 个顶点。因为 CAMeshTransform 中也包含了一个顶点数组,因此一个 CAMeshFace 可以通过顶点在这个数组中的索引来引用这些顶点。这中计算机图形学中的标准范式有一个好处——多个 Face 有可能引用同一个顶点。这不仅解决了数据复制的问题,而且要修改所有相邻平面的形状更加方便了:

平面由它们的顶点所定义

对于 CAMeshFace 的 w 字段,这将在后面进行讨论。

坐标

看过顶点和平面之后,我们仍然不是很清楚我们应该在一个 CAMeshVertex 中放入什么。在 CALayer 中许多属性是以点 Point 的形式定义的,有的使用了单元坐标系,比如 anchorPoint 就是最常见的一个。CAMeshVertex 也使用单元坐标系。点 {0.0, 0.0}对应于 layer 的左上角,而点 {1.0, 1.0} 对应于 layer 的右下角。下面的点 to 使用了同一坐标系:

顶点用单元坐标系进行定义

使用单元坐标系的原因是在 Core Animation Programming Guide 中有叙述:

使用单元坐标系是为了不和屏幕坐标系进行绑定,因为每个值都是相对于其他值的。

单元坐标系的最大好处在于它们的大小不会改变。你可以在小视图和大视图上都使用相同的网格,效果并无二致。我认为这才是在 CAMeshTransform 使用单元坐标系的最大原因。

修改网格变形

创建一个普通 CAMeshTransform 的坏处是它是不可变的,所有顶点和平面必须在变形创建之前就定义。幸运的是,它有一个可变的子类,叫做 CAMutableMeshTransform,允许我们随时添加、删除、替换顶点和平面。

两个网格变形的类都有一个相同的 subdivisionSteps 属性,指定当 layer 绘制在屏幕上时需要切分成多少部分。这个值是一个指数,设置为 3,表示每边被分成 8 片。默认值是 -1,这会让网格接近平滑。我觉得它会自动调整网格数以保证最终结果不会太糟糕。

不太明确的一点是,当 subdivisionSteps 不为零时,所产生的网格不会完全通过它的所有顶点。

切分后的网格形状和它的顶点

事实上,顶点是一个平面的控制点,通过观察它们是如何对几何形状产生影响,我发现 CAMeshTransform 实际上定义了一个三次 NURBS 曲面。这就不得不提到 CAMeshFace 的 w 字段了。将这个值设置为 w 数组中的 4 个索引中的一个时,似乎会影响到对应顶点的权重。这个系数并不像是 NURBS 公式中的 weight 系数。不幸的是,尽管我搜遍了几百行浮点汇编代码还是没有什么收获。

尽管 NURBS 曲面极其强大,它们也无法让遍历顶点的过程变快。在我定义自己的网格时,我需要完全的控制所产生的网格最终是什么样子,因此我将 subdivisionSteps 属性设置为 0。

应用网格变形

光光是创建一个 CAMeshTransform 是没有意义的,我们需要将它赋给一个 CALayer 的私有属性:

@property (copy) CAMeshTransform *meshTransform;

下列代码创建了一个波浪形的网格变形。代码非常冗长,因为我们想完整演示整个流程。只需要定义几个便利函数,就可以将代码缩减到几行代码.


- (CAMeshTransform *)wavyTransform
{
    const float Waves = 3.0;
    const float Amplitude = 0.15;
    const float DistanceShrink = 0.3;

    const int Columns = 40;

    CAMutableMeshTransform *transform = [CAMutableMeshTransform meshTransform];
    for (int i = 0; i <= Columns; i++) {

        float t = (float)i / Columns;
        float sine = sin(t * M_PI * Waves);

        CAMeshVertex topVertex = {
            .from = {t, 0.0},
            .to   = {t, Amplitude * sine * sine + DistanceShrink * t, 0.0}
        };
        CAMeshVertex bottomVertex = {
            .from = {t, 1.0},
            .to   = {t, 1.0 - Amplitude + Amplitude * sine * sine - DistanceShrink * t, 0.0}
        };

        [transform addVertex:topVertex];
        [transform addVertex:bottomVertex];
    }

    for (int i = 0; i < Columns; i++) {
        unsigned int topLeft     = 2 * i + 0;
        unsigned int topRight    = 2 * i + 2;
        unsigned int bottomRight = 2 * i + 3;
        unsigned int bottomLeft  = 2 * i + 1;

        [transform addFace:(CAMeshFace){.indices = {topLeft, topRight, bottomRight, bottomLeft}}];
    }

    transform.subdivisionSteps = 0;

    return transform;
}

这段代码对一个 UILabel 使用了网格变形:

值得一提的是,在模拟器和设备上运行会得到不同的结果。因为 iOS 模拟器的 Core Animation 版本在绘制 3D 图形时使用的是软件模拟,软件模拟的渲染器和 OpenGL ES 的渲染器是不同的。对于贴图纹理来说尤为明显。

抽象漏洞

当你在 retina 屏上仔细观察经过网格变形的 UILabel 时,你会发现它的文字质量有一点模糊。这可以用下面一句代码来解决:

label.layer.rasterizationScale = [UIScreen mainScreen].scale;

这可能是底层机制上的一个疏漏。CALayer 和它的 sublayer 上的内容被栅格化为单一纹理然后应用到顶点网格上。理论上,栅格化能够将所变形的 CALayer 的 sublayer 很好地放置在 superlayer 上,从而避免产生不正确的网格。而且在一般情况下,sublayer 的顶点会被放置在父 CALayer 的顶点之间,这回导致一个糟糕的 z-fighting 现象(Z 缓冲冲突)。栅格化是一种很好的解决方案。

我还发现另外一个问题来自于硬件。CAMeshTransform 提供了一个对平面的抽象,其实就是一个四边形。但是现代 GPU 只认三角形。四边形在发送给 GPU 之前必须被切分成两个三角形。这种切分会有两种不同的方式进行:


将一个四边形分成两个三角形的不同方法

表面上这不会产生什么大问题,但在执行同一个变形时会导致结果大不相同:


同一个网格,导致不同的结果

注意网格变形的形状是完全对称的,但是它们最终形成的结果却完全不是。左边的网格只有一个三角形被变形。右边的网格两个三角形都变形了。不难猜到为什么 Core Animation 要使用四方形进行切分了。注意当你改变组成平面的顶点的索引顺序时,也会导致同样的效果。

尽管栅格化和三角形的抽象漏洞会带来一些问题,而且这些问题确实不可忽略,但一些列解决这些问题的复杂性被掩盖了。

添加深度

单元坐标系对于宽高来说适合的。但是我们无法定义第三维——CALayer 的 bounds 的 size 属性中只有两维。一个宽度单位刚好等于 bounds.size.width 个像素,而高度也是类似的。但深度为 1 表示几个像素?Core Animation 的缔造者们用一种非常简单但极其有效的方式解决了这个问题。

CAMeshTransform 的 depthNoramlization 属性是一个字符串,它可能取值为下述 6 个常量之一:

extern NSString * const kCADepthNormalizationNone;
extern NSString * const kCADepthNormalizationWidth;
extern NSString * const kCADepthNormalizationHeight;
extern NSString * const kCADepthNormalizationMin;
extern NSString * const kCADepthNormalizationMax;
extern NSString * const kCADepthNormalizationAverage;

分别说明如下: CAMeshTransform 将 depthNormalization 当成是其他两维的一个函数。这些常量的含义和其字面意义相同,我们举例进行说明。如果将 depthNoramlization 设为 kCADepthNormalizationAverage,而 CALayer 的 bounds 为 GRectMake(0.0, 0.0, 100.0, 200.0)。由于我们使用平均 normalization,深度的 1 个单位等于 150.0 像素。如果 CAMeshVertext 的坐标为 {1.0, 0.5, 1.5} 转换成 3D 坐标系等于 {100.0, 100.0, 225.0}:

单位转换为点

为什么要进行单位坐标到像素点的转换?因为 CALayer 的 transform 属性的类型是 CATransform3D。CATranform3D 的属性是以点为单位定义的。实际上你可以在 CALayer 上使用任意变形,它都会影响到它的顶点。注意 z 坐标移动和透视变换会得益于这个特性。

这里我们看另一个例子,让 depthNormalization 不等于默认的 kCADepthNormalizationNone 就行。这会导致令人意外的结果——所有东西都是平面的。深度和顶点 z 坐标相加是非常令人难以置信的。我们先跳过这个步骤,讨论一个新的组件,这个组件会增强网格的斜度和曲率——阴影。

遭遇普罗米修斯

既然我们已经打开了私有 Core Animation 类的潘多拉之盒,那么我们还可以使用另一个私有API。不出意外,也有一个类叫做 CALight,它非常有用,因为 CALayer 还有一个私有的数组类型的 lights 属性。

CALight 用 + (id)lightWithType:(NSString *)lightType 创建,lightType 参数可能取值如下:

extern NSString * const kCALightTypeAmbient;
extern NSString * const kCALightTypeDirectional;
extern NSString * const kCALightTypePoint;
extern NSString * const kCALightTypeSpot;

我不会介绍 CALight 太多,我们直接就上例子。这次我们将使用两个自定义的 CAMutableMeshTransform 便利方法。第一个
是 identityMeshTransformWithNumberOfRows:numberOfColumns:,会用均匀分布的顶点创建一个网格,不带任何干扰纹。我们会用 mapVerticesUsingBlock: 方法修改顶点,这个方法将所有顶点转换为另一个顶点。

CALight *light = [CALight new]; // directional light by default
[label.superview.layer setLights:@[light]]; // has to be applied to superlayer

CAMutableMeshTransform *meshTransform = [CAMutableMeshTransform identityMeshTransformWithNumberOfRows:50 numberOfColumns:50];
[meshTransform mapVerticesUsingBlock:^CAMeshVertex(CAMeshVertex vertex, NSUInteger vertexIndex) {
    float x = vertex.from.x - 0.5f;
    float y = vertex.from.y - 0.5f;

    float r = sqrtf(x * x + y * y);

    vertex.to.z = 0.05 * sinf(r * 2.0 * M_PI * 4.0);

    return vertex;
}];
label.layer.meshTransform = meshTransform;

CATransform3D transform = CATransform3DMakeRotation(-M_PI_4, 1, 0, 0);
transform.m34 = -1.0/800.0; // some perspective
label.layer.transform = transform;

这是对一个正方形 UILabel 应用这段代码后的效果:

CALight, CAMeshTransform, 和 CATransform3D 一起使用

灯光看起来有点粗劣,但重要的是只需要很轻松的方式就能创造十分复杂的效果。

可以修改 CALight 的环境光、漫反射和镜面反射强度——标准的 Phong 反射模型中的参数。同时,CALayer 还有一堆表面反射属性。我对它们进行了短时间的尝试,结果一无所获。但我浏览了这些私有头文件,因此要测试 Core Animation 的灯光并不难。

为什么这些 API 是私有的

将一个 API 定义为私有的一个最主要的原因是它并不可靠,CAMeshTransform 当然也是这样。有几个证据足以说明。

首先,将 subdivisionSteps 设置为 20 就能轻易让设备重启。控制台中输出一堆内存警告清晰地表明这是为什么。这确实很糟心,但也很容易避免——不要碰这个属性,或者将它设置为 0。

如果你定义的某个平面发生退化,比如它的索引指向同一个顶点,你会让设备挂起。一切都会停止响应,包括硬件按钮(!),唯一能做的只有硬启动(长按 home 键+电源按钮)。这个框架似乎无法处理输入错误的情况。

为什么会这样?这是因为 backboard —— 另外一个进程,充当 Core Animation 的渲染服务器。严格来说,是系统而不是 app 让系统崩溃的,这是 iOS 核心组件因为错误使用而导致问题。

缺失的特性

一个能够进行网格变形的 CALayer 的通常的目的非常复杂,以至于 Core Animation 团队无法考虑到方方面面并忽略了一些潜在特性。

Core Animation 允许网格变形的 CALayer 拥有 alpha 通道。要正确绘制半透明对象根本不成问题。这是画笔程序常见的功能。z-排序步骤不难实现,实际上代码类似于进行基数排序,排序非常快,因为基数排序可以对浮点数进行排序。但是,却无法对三角形进行排序,因为三角形有可能相交或重叠。

对于这个问题通常的办法是切分三角形,以便所有边都会被删除。这部分算法似乎未实现。幸好,一个正确的、格式良好的网格基本上不会出现交叠,但偶尔会出现,这时网格变形的 CALayer 看起来会出现一些显示问题。

另一个被完全忽略掉的问题是 hit testing——CALayer 好像是从来没有被网格变形过一样。因为无论是 CALayer 还是 UIView 的 hitTest: 方法都不知道网格,所以整个控件的 hit test 区域都无法和它们的可视化外观进行匹配:

UISwitch 的 hit tes 区域不受网格变形的影响

这个问题的解决方式是发出一根射线,计算所击中的三角形是哪些,将点击点从 3D 空间投射到 2D 空间,然后进行普通的 hit testing。这是可行的,但不容易。

替换私有 API

考虑到 CAMeshTransform 的这些缺陷,大家可能会统一它是一个失败的产品。当然不是。它仍然有它的魅力。它打开了一个全新的 iOS 动画和交互的窗口。和过去痛苦的老旧的转换、渐入和模糊相比,这是一股清新空气。我想在每一样东西上使用网格转换,但我无法容忍要调用那么多的私有 API。因此我写了一个开源的、与之相近的替代物。

受 CAMeshTransform 的激发,我创建了一个 BCMeshTransform,它几乎重现了原类的所有功能。我的意图非常简单:如果 CAMeshTransform 中已经有的方法,你可以在任何 CALayer 上使用基本相同的网格变换函数并达到非常类似的效果,当然 CAMeshTransform 中没有方法另当别论。这样,你只需要将所有的 BC 类前缀替换为 CA 即可。

Transform 类已经有了,剩下的事情就是进行网格变形。为此我创建了 BCMeshTransformView,它是一个 UIView 的子类,拥有一个 meshTransform 属性。

没有直接公开地访问 Core Animation 渲染服务器,实现时我强制使用了 OpenGL。这不是最佳方案,因为它会带来一些原类所没有的缺陷,但这是目前唯一可行的解决方案。

几点提示

当我创建这个类时,我遇到了几个坑,这不妨碍我谈谈它们的解决办法。

UIView 的 Animation 块

编写一个类的可动画的自定义属性并不难。David Rönnqvistpresentation on UIView animations中提到,CALayer 在任何其可动画属性被赋值时会询问它的委托对象(一个 UIView 拥有这个 CALayer)。

如果我们在动画块中,当 actionForKey: 方法调用时,UIView 会返回一个 animation 对象。通过这个 CAAnimation,我们可以访问它的属性以获得动画参数并基于这些参数进行动画。

我最初的实现是这样的:

- (void)setMeshTransform:(BCMeshTransform *)meshTransform
{
    CABasicAnimation *animation = [self actionForLayer:self.layer forKey:@"opacity"];
    if ([animation isKindOfClass:[CABasicAnimation class]]) {
      // we're inside an animation block
      NSTimeInterval duration = animation.duration;
      ...
    }
    ...
}

很快我就知道这样是不行的——这个完成块永远不会被触发。当一个基于动画的块被创建时,UIKit 会创建一个 UIViewAnimationState 实例并赋给块中创建的 CAAnimation 的委托。我的猜测也被验证了,UIViewAnimationState 等它所有的 animation 完成或取消后才会调用这个完成块。为了读取 animation 的属性,我引用了这个 animation,但是它没有被添加到任何 CALayer 中,因此它永远不会完成。

解决办法比我想象的更简单。我为 BCMeshTransformView 创建了一个 subview 作为它的“替代品”。这是我目前的实现:

- (void)setMeshTransform:(BCMeshTransform *)meshTransform
{
    [self.dummyAnimationView.layer removeAllAnimations];
    self.dummyAnimationView.layer.opacity = 1.0;
    self.dummyAnimationView.layer.opacity = 0.0;
    CAAnimation *animation = [self.dummyAnimationView.layer animationForKey:@"opacity"];

    if ([animation isKindOfClass:[CABasicAnimation class]]) {
      // we're inside UIView animation block
    }
    ...
}

对 opacity 属性进行两次赋值是必要的,这样才能保证这个属性的值发生了变化。animation 不会被加到 CALayer,如果它已经是目标状态的话。同时,CALayer 必须存在于 UIView 或 UIWindow 的视图树中才行,否则它的属性不会动画。

因为要对网格进行动画,需要让 Core Animation 插入浮点数,这就需要将这些数转换成 NSNumber,放进数组,实现 needsDisplayForKey: 类方法并在 setValue:forKey: 方法中负责显示 CALayer 的改变。在获得便利的同时,这些方法有严重的性能问题。一个 25x25 个平面的网格就无法在 60 FPS 下进行动画,哪怕是用 iPad Air。封包和解包的代价太大了。

替代 Core Animation 的方法,我使用一个非常简单的基于 CADisplayLink 的动画引擎。这获得了更好的性能,100x100 的平面仍然能够在 60 FPS 下流畅地动画。这不是最佳解决方案,我们因此失去了许多 CAAnimation 带来的便利,但 16 倍速度的好处完全可以忽略这些。

渲染内容

BCMeshTransformView 的目的是呈现它的网格变形的 subview。在提交给 OpenGL 之前 UIView 层次必须被渲染成纹理。然后这个纹理化的顶点网格用 GLKView 显示出来,而后者是 BCMeshTransformView 的核心。从高度抽象的层次看非常简单,但不等不提到一个将 subview 层次进行截图问题。

我们不想对 GLKView 进行截取,因为这会导致一个镜面隧道效应。另外,我们不想直接显示其他 subview ——它们的作用主要是在 OpenGL 中可见但在 UIKit 视图层次中不可见。它们不能放在 GLKView 下,因为它们必须是不透明的。要解决这个问题我使用了 contentView 的概念,就像 UITableViewCell 管理它的 subview 一样。这个视图层次类似于下图所示:

BCMesthTransformView 的视图树

contentView 嵌到一个 containerView 中。containerView 的 frame 为 CGRectZero,clipsToBounds 为 YES,这样它就隐藏了,但它仍然存在于 BCMeshTransformView 中。每个需要进行网格变形的 subview 都必须添加到 contentView 中。

contentView 的内容通过 drawViewHierarchyInRect:afterScreenUpdates: 方法渲染到纹理中。截取并上传纹理的整个过程是非常快的,但不幸的是对于较大的视图会花去 16 毫秒以上的时间。对于每帧需要绘制一次视图树来说这就太长了。尽管 BCMeshTransformView 会自动观察 contentView 的 subview 的改变并重新绘制其纹理,它也不支持对网格化的 subview 的动画。

结束

毫无疑问,网格变形是一个神奇的概念,然而仍然有太多秘密未被人们所知。它也给一成不变的屏幕带来了更多的乐趣。事实上,你可以立即体验一把网格变形的威力。在你的 iOS 设备上点开 Game Center,观察气泡的细微变化。这正是 CAMeshTransform 的力量。

我建议你下载 BCMeshTranformView 的示例 app。它包含了几个将网格变换用于丰富界面交互的例子,比如简单但强大的 Dribble。要想体验更多关于网格的精彩创意,Experiments by Marcus Eckert 是一个不错的地方。

我真心希望 BCMeshTransformView 在 WWDC 2014 的第一天被废弃。Core Animation 能够实现网格变形的更多功能并紧密集成到系统中。尽管目前它还不能正确地处理某些边缘情况,还有一些需要完善的地方。希望 6 月 2 号能有好消息传来。

作者:kmyhy 发表于2017/3/21 17:22:47 原文链接
阅读:73 评论:0 查看评论

linux驱动开发module(2) 原理

$
0
0

前言

前文中介绍了 module 的基本组成部分,现在在上一节的基础上,对 module 做一点点深入的理解。

正文

我们的 module 模块,如果动态的加载到内核中,通常在 Makefile 中,以这样的形式进行标识

obj-m +=first_drv.o 

如果静态的加载到内核中,使用的是下列的方式

obj-y +=first_drv.o 

module_init分析

module_init 这个宏在 include/linux/init.h 这个目录下,一下仅仅摘录和module相关的代码

typedef void (*exitcall_t)(void);
#ifndef MODULE

#define __define_initcall(level,fn,id) \
    static initcall_t __initcall_##fn##id __used \
    __attribute__((__section__(".initcall" level ".init"))) = fn

#define device_initcall(fn)     __define_initcall("6",fn,6)

#define __initcall(fn) device_initcall(fn)

#define __exitcall(fn) \
    static exitcall_t __exitcall_##fn __exit_call = fn

#define module_init(x)  __initcall(x);
#define module_exit(x)  __exitcall(x);

#else 
#define module_init(initfn)                 \
    static inline initcall_t __inittest(void)       \
    { return initfn; }                  \
    int init_module(void) __attribute__((alias(#initfn)));
#define module_exit(exitfn)                 \
    static inline exitcall_t __exittest(void)       \
    { return exitfn; }                  \
    void cleanup_module(void) __attribute__((alias(#exitfn)));
  • 首先解决第一个疑惑,这里列举了两个 module_init ,到底选择哪一个 module_init

  • 这两个 moudle_init 使用一个 MODULE 的宏进行分隔,这里的 MOUDLE 是在 Makefile 中进行确定是否进行定义的,前边知道如果编译为模块在 makefile 中会使用 obj-m,此时使用的是第二个 module_init 的定义。

动态加载

动态加载使用的命令为 insmod 或者 rmmod ,在加载模块的时候,只会加载默认的init_module cleanup_module 这两个函数;所以使用 module_initmodule_exit 这两个宏的目的就是给我们写的入口函数和出口函数定义一个别名为 module_initmodule_exit,这样就能实现正常的加载。

在这里使用了 _attribute_((alias(#initfn))); 进行定义别名。

## 静态加载

静态加载的时候,会将入口函数,和出口函数,分别放入对应的 section 段中。
具体可参考如下文章
module_init宏分析

总结

静态加载的模块,存放在特定的 section 段中,静态加载模块,使用module_init这个宏给起一个别名 init_module,供加载模块的是进行调用。

参考文献

#ifdef MODULE 关于MODULE的问题
module_init宏分析

作者:u013377887 发表于2017/3/21 20:32:36 原文链接
阅读:134 评论:0 查看评论

Android Things:用户驱动-输入驱动

$
0
0
一、驱动介绍
输入用户驱动程序为应用程序提供接口,向Android的输入管道注入事件。有了这个API,应用程序可以使用Peripheral I/O模拟一个人机界面的设备(HID)或者连接外部硬件到输入系统。比如说,我们可以使用开关按钮GPIO的信号输入,通过输入驱动API模拟呈键盘上按键的输入事件,下面我们会具体演示如何实现。
二、使用步骤
实现输入用户驱动,有如下步骤:
1.创建驱动:使用InputDriver.Builder和源类型SOURCE_CLASS_BUTTON创建一个新的输入驱动实例。
2.注册驱动:使用UserDriverManager的registerInputDriver()方法注册这个驱动。
public class TouchpadDriverService extends Service {
    // Driver parameters
    private static final String DRIVER_NAME = "Touchpad";
    private static final int DRIVER_VERSION = 1;
    private InputDriver mDriver;
    @Override
    public void onCreate() {
        super.onCreate();
        mDriver = InputDriver.builder(InputDevice.SOURCE_TOUCHPAD)
                .setName(DRIVER_NAME)
                .setVersion(DRIVER_VERSION)
                .setAbsMax(MotionEvent.AXIS_X, 255)
                .setAbsMax(MotionEvent.AXIS_Y, 255)
                .build();
        UserDriverManager manager = UserDriverManager.getManager();
        manager.registerInputDriver(mDriver);
    }
}
3.转换事件:当一个硬件事件发生,使用当前的事件代码和输入动作为每个状态改变构造一个新的KeyEvent。
4.注入事件:使用输入驱动的emit()方法向这个驱动中注入事件。
private void triggerEvent(boolean pressed) {
     int action = pressed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP;
     KeyEvent[] events = new KeyEvent[] {new KeyEvent(action, KEY_CODE)};
     if (!mDriver.emit(events)) {
            Log.w(TAG, "Unable to emit key event");
     }
}
5.处理事件:在前台Activity通过实现onKeyDown()、onKeyUp()和onGenericMotionEvent()方法来获取相关事件,并处理。
public class HomeActivity extends Activity {
    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        // Handle key pressed and repeated events
        return true;
    }
    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        // Handle key released events
        return true;
    }
    @Override
    public boolean onGenericMotionEvent(MotionEvent event) {
        // Handle motion input events
        return true;
    }
}
6.注销驱动:当你不在需要关键事件的时候注销驱动。
public class TouchpadDriverService extends Service {
    @Override
    public void onDestroy() {
         super.onDestroy();
         UserDriverManager manager = UserDriverManager.getManager();
         manager.unregisterInputDriver(mDriver);
    }
}
三、案例展示
这里我们实现一个最简单的模拟键盘,通过两个开关按钮的GPIO信号输入,通过输入驱动API分别模拟键盘上a和b字母的输入。
1.硬件准备:
 
树莓派3开发板 1块
  面包板 1块
  按钮开关 2个
  电阻 2个
  杜邦线(母对公,公对公) 若干
广告时间咯:如果你还没有自己的开发板和元器件,到我们的“1024工场微店”来逛逛一逛吧(文章底部二维码),这里能一次性有买到你想要的!
2.电路搭建:

3.代码实现:
InputDemo\app\src\main\java\com\chengxiang\inputdemo\KeyDriverService.java
public class KeyDriverService extends Service {
    private static final String TAG = KeyDriverService.class.getSimpleName();

    private static final String A_DRIVER_NAME = "Akey";
    private static final int A_DRIVER_VERSION = 1;
    private static final int A_KEY_CODE = KeyEvent.KEYCODE_A;

    private static final String B_DRIVER_NAME = "Bkey";
    private static final int B_DRIVER_VERSION = 1;
    private static final int B_KEY_CODE = KeyEvent.KEYCODE_B;

    private static final String A_GPIO_NAME = "BCM5";
    private static final String B_GPIO_NAME = "BCM6";

    private UserDriverManager mUserDriverManager;
    private InputDriver mADriver;
    private InputDriver mBDriver;

    private Gpio mAGpio;
    private Gpio mBGpio;

    private GpioCallback mGpioCallback = new GpioCallback() {
        @Override
        public boolean onGpioEdge(Gpio gpio) {
            Log.d(TAG, "onGpioEdge");
            try {
                //获取开关按键的信号输入后,转换成A和B按键的输入事件
                if (gpio == mAGpio) {
                    triggerEvent(mADriver, gpio.getValue(), A_KEY_CODE);
                } else if (gpio == mBGpio) {
                    triggerEvent(mBDriver, gpio.getValue(), B_KEY_CODE);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return true;
        }

        @Override
        public void onGpioError(Gpio gpio, int error) {
            Log.w(TAG, gpio + ": Error event " + error);
        }
    };

    public KeyDriverService() {
    }

    @Override
    public void onCreate() {
        super.onCreate();
        PeripheralManagerService manager = new PeripheralManagerService();
        try {
            mAGpio = manager.openGpio(A_GPIO_NAME);
            mAGpio.setDirection(Gpio.DIRECTION_IN);
            mAGpio.setActiveType(Gpio.ACTIVE_LOW);
            mAGpio.setEdgeTriggerType(Gpio.EDGE_BOTH);
            mAGpio.registerGpioCallback(mGpioCallback);


            mBGpio = manager.openGpio(B_GPIO_NAME);
            mBGpio.setDirection(Gpio.DIRECTION_IN);
            mBGpio.setActiveType(Gpio.ACTIVE_LOW);
            mBGpio.setEdgeTriggerType(Gpio.EDGE_BOTH);
            mBGpio.registerGpioCallback(mGpioCallback);
        } catch (IOException e) {
            e.printStackTrace();
        }

        //分别创建A字母按键和B字母按键输入驱动实例
        mADriver = InputDriver.builder(InputDevice.SOURCE_CLASS_BUTTON).setName(A_DRIVER_NAME)
                .setVersion(A_DRIVER_VERSION).setKeys(new int[]{A_KEY_CODE}).build();
        mBDriver = InputDriver.builder(InputDevice.SOURCE_CLASS_BUTTON).setName(B_DRIVER_NAME)
                .setVersion(B_DRIVER_VERSION).setKeys(new int[]{B_KEY_CODE}).build();
       
        //注册A字母按键和B字母按键输入驱动
        mUserDriverManager = UserDriverManager.getManager();
        mUserDriverManager.registerInputDriver(mADriver);
        mUserDriverManager.registerInputDriver(mBDriver);
    }

    @Override
    public IBinder onBind(Intent intent) {
        // TODO: Return the communication channel to the service.
        throw new UnsupportedOperationException("Not yet implemented");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        if (mAGpio != null) {
            try {
                mAGpio.unregisterGpioCallback(mGpioCallback);
                mAGpio.close();
                mAGpio = null;
            } catch (IOException e) {
                Log.w(TAG, "Unable to close GPIO", e);
            }
        }

        if (mBGpio != null) {
            try {
                mBGpio.unregisterGpioCallback(mGpioCallback);
                mBGpio.close();
                mBGpio = null;
            } catch (IOException e) {
                Log.w(TAG, "Unable to close GPIO", e);
            }
        }
        
        //当你不在需要关键事件的时候注销驱动
        mUserDriverManager.unregisterInputDriver(mADriver);
        mUserDriverManager.unregisterInputDriver(mBDriver);
    }

    private void triggerEvent(InputDriver inputDriver, boolean pressed, int keyCode) {
       int action = pressed ? KeyEvent.ACTION_DOWN : KeyEvent.ACTION_UP;
        KeyEvent[] events = new KeyEvent[]{new KeyEvent(action, keyCode)};
        //使用emit()方法转换成Android事件
        if (!inputDriver.emit(events)) {
            Log.w(TAG, "Unable to emit key event");
        }
    }
}
InputDemo\app\src\main\java\com\chengxiang\inputdemo\MainActivity.java
public class MainActivity extends AppCompatActivity {
    private static final String TAG = MainActivity.class.getSimpleName();
    private EditText mEditText;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        mEditText = (EditText) findViewById(R.id.edittext);
        //这里我们将输入焦点放在输入框中,故按下开关按钮会输入A和B字母
        mEditText.requestFocus();
    }

    //在前台Activity通过实现onKeyDown()、onKeyUp()和onGenericMotionEvent()方法来获取相关事件
    @Override
    public boolean onKeyUp(int keyCode, KeyEvent event) {
        Log.d(TAG,"onKeyUp:"  + keyCode);
        return true;
    }

    @Override
    public boolean onKeyDown(int keyCode, KeyEvent event) {
        Log.d(TAG,"onKeyDown:"  + keyCode);
        return true;
    }

    public void toNext(View view) {
        Intent intent = new Intent(this,NextActivity.class);
        startActivity(intent);
    }
}
4.运行结果:
点击开关按钮,在输入框中会输入对应的A和B字母。




作者:p106786860 发表于2017/3/22 1:14:00 原文链接
阅读:63 评论:0 查看评论

你不知道的 Android WebView 使用漏洞

$
0
0

前言

  • 现在很多App里都内置了Web网页(Hyprid App),比如说很多电商平台,淘宝、京东、聚划算等等,如下图

京东首页

  • 上述功能是由 Android的WebView 实现的,但是 WebView 使用过程中存在许多漏洞,容易造成用户数据泄露等等危险,而很多人往往会忽视这个问题
  • 今天我将全面介绍 Android WebView的使用漏洞 及其修复方式

阅读本文前请先阅读:
Android开发:最全面、最易懂的Webview详解
最全面 & 最详细的 Android WebView与JS的交互方式 汇总


目录

目录


1. 类型

WebView中,主要漏洞有三类:
  • 任意代码执行漏洞
  • 密码明文存储漏洞
  • 域控制不严格漏洞

2. 具体分析

2.1 WebView 任意代码执行漏洞

出现该漏洞的原因有三个:

  • WebView 中 addJavascriptInterface() 接口
  • WebView 内置导出的 searchBoxJavaBridge_对象
  • WebView 内置导出的 accessibilityaccessibilityTraversalObject 对象

2.1.1 addJavascriptInterface 接口引起远程代码执行漏洞

A. 漏洞产生原因

JS调用Android的其中一个方式是通过addJavascriptInterface接口进行对象映射:

 webView.addJavascriptInterface(new JSObject(), "myObj");
// 参数1:Android的本地对象
// 参数2:JS的对象
// 通过对象映射将Android中的本地对象和JS中的对象进行关联,从而实现JS调用Android的对象和方法

所以,漏洞产生原因是:当JS拿到Android这个对象后,就可以调用这个Android对象中所有的方法,包括系统类(java.lang.Runtime 类),从而进行任意代码执行。

如可以执行命令获取本地设备的SD卡中的文件等信息从而造成信息泄露

具体获取系统类的描述:(结合 Java 反射机制)

  • Android中的对象有一公共的方法:getClass() ;
  • 该方法可以获取到当前类 类型Class
  • 该类有一关键的方法: Class.forName;
  • 该方法可以加载一个类(可加载 java.lang.Runtime 类)
  • 而该类是可以执行本地命令的

以下是攻击的Js核心代码:

function execute(cmdArgs)  
{  
    // 步骤1:遍历 window 对象
    // 目的是为了找到包含 getClass ()的对象
    // 因为Android映射的JS对象也在window中,所以肯定会遍历到
    for (var obj in window) {  
        if ("getClass" in window[obj]) {  

      // 步骤2:利用反射调用forName()得到Runtime类对象
            alert(obj);          
            return  window[obj].getClass().forName("java.lang.Runtime")  

      // 步骤3:以后,就可以调用静态方法来执行一些命令,比如访问文件的命令
getMethod("getRuntime",null).invoke(null,null).exec(cmdArgs);  

// 从执行命令后返回的输入流中得到字符串,有很严重暴露隐私的危险。
// 如执行完访问文件的命令之后,就可以得到文件名的信息了。
        }  
    }  
}   
  • 当一些 APP 通过扫描二维码打开一个外部网页时,攻击者就可以执行这段 js 代码进行漏洞攻击。
  • 在微信盛行、扫一扫行为普及的情况下,该漏洞的危险性非常大

B. 解决方案

B1. Android 4.2版本之后

Google 在Android 4.2 版本中规定对被调用的函数以 @JavascriptInterface进行注解从而避免漏洞攻击

B2. Android 4.2版本之前

在Android 4.2版本之前采用拦截prompt()进行漏洞修复。
具体步骤如下:

  • 继承 WebView ,重写 addJavascriptInterface 方法,然后在内部自己维护一个对象映射关系的 Map;

    将需要添加的 JS 接口放入该Map中

  • 每次当 WebView 加载页面前加载一段本地的 JS 代码,原理是:

    • 让JS调用一Javascript方法:该方法是通过调用prompt()把JS中的信息(含特定标识,方法名称等)传递到Android端;
    • 在Android的onJsPrompt()中 ,解析传递过来的信息,再通过反射机制调用Java对象的方法,这样实现安全的JS调用Android代码。

关于Android返回给JS的值:可通过prompt()把Java中方法的处理结果返回到Js中

具体需要加载的JS代码如下:

javascript:(function JsAddJavascriptInterface_(){  
// window.jsInterface 表示在window上声明了一个Js对象

//   jsInterface = 注册的对象名
// 它注册了两个方法,onButtonClick(arg0)和onImageClick(arg0, arg1, arg2)
// 如果有返回值,就添加上return
    if (typeof(window.jsInterface)!='undefined') {      
        console.log('window.jsInterface_js_interface_name is exist!!');}   
    else {  
        window.jsInterface = {     

     // 声明方法形式:方法名: function(参数)
            onButtonClick:function(arg0) {   
// prompt()返回约定的字符串
// 该字符串可自己定义
// 包含特定的标识符MyApp和 JSON 字符串(方法名,参数,对象名等)    
                return prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onButtonClick',args:[arg0]}));  
            },  


            onImageClick:function(arg0,arg1,arg2) {   
         return
prompt('MyApp:'+JSON.stringify({obj:'jsInterface',func:'onImageClick',args:[arg0,arg1,arg2]}));  
            },  
        };  
    }  
}  
)()

// 当JS调用 onButtonClick() 或 onImageClick() 时,就会回调到Android中的 onJsPrompt ()
// 我们解析出方法名,参数,对象名
// 再通过反射机制调用Java对象的方法

关于该方法的其他细节

细节1:加载上述JS代码的时机

  • 由于当 WebView 跳转到下一个页面时,之前加载的 JS 可能已经失效
  • 所以,通常需要在以下方法中加载 JS:
onLoadResource();
doUpdateVisitedHistory();
onPageStarted();
onPageFinished();
onReceivedTitle();
onProgressChanged();

细节2:需要过滤掉 Object 类的方法

  • 由于最终是通过反射得到Android指定对象的方法,所以同时也会得到基类的其他方法(最顶层的基类是 Object类)
  • 为了不把 getClass()等方法注入到 JS 中,我们需要把 Object 的共有方法过滤掉,需要过滤的方法列表如下:
getClass()
hashCode()
notify()
notifyAl()
equals()
toString()
wait()

总结

  • 对于Android 4.2以前,需要采用拦截prompt()的方式进行漏洞修复
  • 对于Android 4.2以后,则只需要对被调用的函数以 @JavascriptInterface进行注解
  • 关于 Android 系统占比,Google公布的数据:截止 2017 .1 .8 ,Android4.4 之下占有约15%,所以需要重视。

    具体数据如下:


Paste_Image.png

2.1.2 searchBoxJavaBridge_接口引起远程代码执行漏洞

A. 漏洞产生原因

  • 在Android 3.0以下,Android系统会默认通过searchBoxJavaBridge_的Js接口给 WebView 添加一个JS映射对象:searchBoxJavaBridge_对象
  • 该接口可能被利用,实现远程任意代码。

B. 解决方案

删除searchBoxJavaBridge_接口

// 通过调用该方法删除接口
removeJavascriptInterface();

2.1.3 accessibilityaccessibilityTraversal接口引起远程代码执行漏洞

问题分析与解决方案同上,这里不作过多阐述。

2.2 密码明文存储漏洞

2.2.1 问题分析

WebView默认开启密码保存功能 :

mWebView.setSavePassword(true)`
  • 开启后,在用户输入密码时,会弹出提示框:询问用户是否保存密码;
  • 如果选择”是”,密码会被明文保到 /data/data/com.package.name/databases/webview.db 中,这样就有被盗取密码的危险

2.2.2 解决方案

关闭密码保存提醒

WebSettings.setSavePassword(false) 

2.3 域控制不严格漏洞

2.3.1 问题分析

先看Android里的WebViewActivity.java

public class WebViewActivity extends Activity {
    private WebView webView;
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_webview);
        webView = (WebView) findViewById(R.id.webView);

        //webView.getSettings().setAllowFileAccess(false);                    (1)
        //webView.getSettings().setAllowFileAccessFromFileURLs(true);         (2)
        //webView.getSettings().setAllowUniversalAccessFromFileURLs(true);    (3)
        Intent i = getIntent();
        String url = i.getData().toString(); //url = file:///data/local/tmp/attack.html 
        webView.loadUrl(url);
    }
 }

/**Mainifest.xml**/
// 将该 WebViewActivity 在Mainifest.xml设置exported属性
// 表示:当前Activity是否可以被另一个Application的组件启动
android:exported="true"

即 A 应用可以通过 B 应用导出的 Activity 让 B 应用加载一个恶意的 file 协议的 url,从而可以获取 B 应用的内部私有文件,从而带来数据泄露威胁

具体:当其他应用启动此 Activity 时, intent 中的 data 直接被当作 url 来加载(假定传进来的 url 为 file:///data/local/tmp/attack.html ),其他 APP 通过使用显式 ComponentName 或者其他类似方式就可以很轻松的启动该 WebViewActivity 并加载恶意url。

下面我们着重分析WebView中getSettings类的方法对 WebView 安全性的影响:

  • setAllowFileAccess()
  • setAllowFileAccessFromFileURLs()
  • setAllowUniversalAccessFromFileURLs()

1. setAllowFileAccess()

// 设置是否允许 WebView 使用 File 协议
webView.getSettings().setAllowFileAccess(true);     
// 默认设置为true,即允许在 File 域下执行任意 JavaScript 代码

使用 file 域加载的 js代码能够使用进行同源策略跨域访问,从而导致隐私信息泄露

  1. 同源策略跨域访问:对私有目录文件进行访问
  2. 针对 IM 类产品,泄露的是聊天信息、联系人等等
  3. 针对浏览器类软件,泄露的是cookie 信息泄露。

如果不允许使用 file 协议,则不会存在上述的威胁;

webView.getSettings().setAllowFileAccess(true);     

但同时也限制了 WebView 的功能,使其不能加载本地的 html 文件,如下图:

移动版的 Chrome 默认禁止加载 file 协议的文件

Paste_Image.png

解决方案:

  • 对于不需要使用 file 协议的应用,禁用 file 协议;
setAllowFileAccess(false); 
  • 对于需要使用 file 协议的应用,禁止 file 协议加载 JavaScript。
setAllowFileAccess(true); 

// 禁止 file 协议加载 JavaScript
if (url.startsWith("file://") {
    setJavaScriptEnabled(false);
} else {
    setJavaScriptEnabled(true);
}

2. setAllowFileAccessFromFileURLs()

// 设置是否允许通过 file url 加载的 Js代码读取其他的本地文件
webView.getSettings().setAllowFileAccessFromFileURLs(true);
// 在Android 4.1前默认允许
// 在Android 4.1后默认禁止

AllowFileAccessFromFileURLs()设置为 true 时,攻击者的JS代码为:


<script>
function loadXMLDoc()
{
    var arm = "file:///etc/hosts";
    var xmlhttp;
    if (window.XMLHttpRequest)
    {
        xmlhttp=new XMLHttpRequest();
    }
    xmlhttp.onreadystatechange=function()
    {
        //alert("status is"+xmlhttp.status);
        if (xmlhttp.readyState==4)
        {
              console.log(xmlhttp.responseText);
        }
    }
    xmlhttp.open("GET",arm);
    xmlhttp.send(null);
}
loadXMLDoc();
</script>

// 通过该代码可成功读取 /etc/hosts 的内容数据

解决方案:设置setAllowFileAccessFromFileURLs(false);

当设置成为 false 时,上述JS的攻击代码执行会导致错误,表示浏览器禁止从 file url 中的 javascript 读取其它本地文件。

3. setAllowUniversalAccessFromFileURLs()

// 设置是否允许通过 file url 加载的 Javascript 可以访问其他的源(包括http、https等源)
webView.getSettings().setAllowUniversalAccessFromFileURLs(true);

// 在Android 4.1前默认允许(setAllowFileAccessFromFileURLs()不起作用)
// 在Android 4.1后默认禁止

AllowFileAccessFromFileURLs()被设置成true时,攻击者的JS代码是:

// 通过该代码可成功读取 http://www.so.com 的内容
<script>
function loadXMLDoc()
{
    var arm = "http://www.so.com";
    var xmlhttp;
    if (window.XMLHttpRequest)
    {
        xmlhttp=new XMLHttpRequest();
    }
    xmlhttp.onreadystatechange=function()
    {
        //alert("status is"+xmlhttp.status);
        if (xmlhttp.readyState==4)
        {
             console.log(xmlhttp.responseText);
        }
    }
    xmlhttp.open("GET",arm);
    xmlhttp.send(null);
}
loadXMLDoc();
</script>

解决方案:设置setAllowUniversalAccessFromFileURLs(false);

4. setJavaScriptEnabled()

// 设置是否允许 WebView 使用 JavaScript(默认是不允许)
webView.getSettings().setJavaScriptEnabled(true);  

// 但很多应用(包括移动浏览器)为了让 WebView 执行 http 协议中的 JavaScript,都会主动设置为true,不区别对待是非常危险的。

即使把setAllowFileAccessFromFileURLs()setAllowUniversalAccessFromFileURLs()都设置为 false,通过 file URL 加载的 javascript 仍然有方法访问其他的本地文件:符号链接跨源攻击

前提是允许 file URL 执行 javascript,即webView.getSettings().setJavaScriptEnabled(true);

这一攻击能奏效的原因是:通过 javascript 的延时执行和将当前文件替换成指向其它文件的软链接就可以读取到被符号链接所指的文件。具体攻击步骤:
1. 把恶意的 js 代码输出到攻击应用的目录下,随机命名为 xx.html,修改该目录的权限;
2. 修改后休眠 1s,让文件操作完成;
3. 完成后通过系统的 Chrome 应用去打开该 xx.html 文件
4. 等待 4s 让 Chrome 加载完成该 html,最后将该 html 删除,并且使用 ln -s 命令为 Chrome 的 Cookie 文件创建软连接

注:在该命令执行前 xx.html 是不存在的;执行完这条命令之后,就生成了这个文件,并且将 Cookie 文件链接到了 xx.html 上。

于是就可通过链接来访问 Chrome 的 Cookie

  1. Google 没有进行修复,只是让Chrome 最新版本默认禁用 file 协议,所以这一漏洞在最新版的 Chrome 中并不存在
  2. 但是,在日常大量使用 WebView 的App和浏览器,都有可能受到此漏洞的影响。通过利用此漏洞,容易出现数据泄露的危险

如果是 file 协议,禁用 javascript 可以很大程度上减小跨源漏洞对 WebView 的威胁。

  1. 但并不能完全杜绝跨源文件泄露。
  2. 例:应用实现了下载功能,对于无法加载的页面,会自动下载到 sd 卡中;由于 sd 卡中的文件所有应用都可以访问,于是可以通过构造一个 file URL 指向被攻击应用的私有文件,然后用此 URL 启动被攻击应用的 WebActivity,这样由于该 WebActivity 无法加载该文件,就会将该文件下载到 sd 卡下面,然后就可以从 sd 卡上读取这个文件了

最终解决方案

  • 对于不需要使用 file 协议的应用,禁用 file 协议;
// 禁用 file 协议;
setAllowFileAccess(false); 
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);
  • 对于需要使用 file 协议的应用,禁止 file 协议加载 JavaScript。
// 需要使用 file 协议
setAllowFileAccess(true); 
setAllowFileAccessFromFileURLs(false);
setAllowUniversalAccessFromFileURLs(false);

// 禁止 file 协议加载 JavaScript
if (url.startsWith("file://") {
    setJavaScriptEnabled(false);
} else {
    setJavaScriptEnabled(true);
}


3. 总结

  • 本文主要对Android WebView的使用漏洞及其修复方式进行了全面介绍
  • 接下来我会继续讲解其他安卓开发的知识,有兴趣可以继续关注Carson_Ho的安卓开发笔记!!!!

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

作者:carson_ho 发表于2017/3/22 9:13:52 原文链接
阅读:925 评论:1 查看评论

Android Sip使用及坑

$
0
0

https://github.com/android/platform_development/tree/master/samples/SipDemo
上面是Android自带的SipDemo,下
https://developer.android.com/reference/android/net/sip/package-summary.html
Android 官网对sip的相关使用的介绍。

使用

1、注册广播

 private void registerInComingCallReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction("android.SipDemo.INCOMING_CALL");
        callReceiver = new IncomingCallReceiver();
        this.registerReceiver(callReceiver, filter);
    }

2、实例SipManager

 if (manager == null) {
            manager = SipManager.newInstance(this);
        }

3、SipProfile

   SipProfile.Builder builder = new SipProfile.Builder("Android004", "115.236.167.22");
            builder.setPassword("78cdb4164g");
//            builder.setOutboundProxy("45.56.91.117");
            me = builder.build();

4、设置接听电话广播,如果不需要接电话可以直接manager.open(me);就可以了

  Intent i = new Intent();
            i.setAction("android.SipDemo.INCOMING_CALL");
            PendingIntent pi = PendingIntent.getBroadcast(this, 0, i, Intent.FILL_IN_DATA);

            manager.open(me, pi, null);

5、设置是否登录成功的监听,必须在上面的open方法之后调用

manager.setRegistrationListener(me.getUriString(), new SipRegistrationListener() {
                public void onRegistering(String localProfileUri) {
                //修改textview显示的文字
                    updateStatus("Registering with SIP Server...");
                }

                public void onRegistrationDone(String localProfileUri, long expiryTime) {
                    updateStatus("Ready");

                }

                public void onRegistrationFailed(String localProfileUri, int errorCode,
                                                 String errorMessage) {
                    updateStatus("Registration failed.  Please check settings.");
                }
            });

6、初始化电话

//sipaddress电话号码
  updateStatus(sipAddress);
  SipAudioCall.Listener listener = new SipAudioCall.Listener() {
                // Much of the client's interaction with the SIP Stack will
                // happen via listeners.  Even making an outgoing call, don't
                // forget to set up a listener to set things up once the call is established.
                @Override
                public void onCallEstablished(SipAudioCall call) {
                    call.startAudio();
                    call.setSpeakerMode(true);
                    call.toggleMute();
                    updateStatus(call);
                }

                @Override
                public void onCallEnded(SipAudioCall call) {
                    updateStatus("Ready.");
                }
            };

            call = manager.makeAudioCall(me.getUriString(), sipAddress, listener, 30);

7、关闭SipProfile

public void closeLocalProfile() {
        if (manager == null) {
            return;
        }
        try {
            if (me != null) {
                manager.close(me.getUriString());
            }
        } catch (Exception ee) {
              Log.d( "Failed to close profile", ee.toString());
        }
    }

权限

  <uses-sdk android:minSdkVersion="10" />
    <uses-permission android:name="android.permission.USE_SIP" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />

    <uses-feature android:name="android.hardware.sip.voip" android:required="true" />
    <uses-feature android:name="android.hardware.wifi" android:required="true" />
    <uses-feature android:name="android.hardware.microphone" android:required="true" />

receiver

   <receiver android:name=".ui.demo.sip.IncomingCallReceiver" android:label="Call Receiver"/>

坑1:WalkieTalkieActivity这个类的类名不能修改,之前试过好多次,自己创建了一个类,代码也是一样的,怎么弄都登录失败,很郁闷!不知道是不是我自己的问题。
坑2:不能设置stun,要想设置自己的stun可以使用sipdroid或者想要实现网络电话的话也可以使用开源的linphone,不过lingphone需要在linux环境下进行编译,并且下载源码编译需要翻墙,所以你必须有一个好的网络环境!
坑3:closeLocalProfile()方法最好放在opause()方法中进行调用。
坑4:安装到手机上,经常出注册失败的问题,但是重启一下手机就变成ready状态了,很无语,不知道是不是所有手机都这样,我只试了华为手机。

相对完整一点的代码

相对完整一点的代码,由于后面发现没法设置stun,所以通话的建立方法一直没能实现。InComingCall类在最开头的连接中下载即可。

public class WalkieTalkieActivity extends AppCompatActivity implements View.OnClickListener {
    private Button mButtonSipCall;
    public String sipAddress = "111";
    public SipManager manager = null;
    public SipProfile me = null;
    public SipAudioCall call = null;
    public IncomingCallReceiver callReceiver;
    private boolean isCalling = false;
    @Override
    public void onCreate(Bundle savedInstanceState) {

        super.onCreate(savedInstanceState);
        setContentView(R.layout.walkietalkie);
        mButtonSipCall = (Button) findViewById(R.id.btn_sipcall);
        mButtonSipCall.setOnClickListener(this);
        registerInComingCallReceiver();
    }

    private void registerInComingCallReceiver() {
        IntentFilter filter = new IntentFilter();
        filter.addAction("android.SipDemo.INCOMING_CALL");
        callReceiver = new IncomingCallReceiver();
        this.registerReceiver(callReceiver, filter);
    }

    @Override
    public void onStart() {
        super.onStart();
        initSipManager();
    }

    @Override
    protected void onPause() {
        super.onPause();
        if (call != null) {
            call.close();
        }

        closeLocalProfile();

        if (callReceiver != null) {
            this.unregisterReceiver(callReceiver);
        }
    }


    public void initSipManager() {
        if (manager == null) {
            manager = SipManager.newInstance(this);
        }
        if (manager == null) {
            return;
        }

        if (me != null) {
            closeLocalProfile();
        }
        buildSipProfile();
        setReceiveCallListener();
        setRegistrationListener();
    }
    public void buildSipProfile() {
        try {
            SipProfile.Builder builder = new SipProfile.Builder("Android", "000.000.000.000");
            builder.setPassword("abc");

            me = builder.build();
        } catch (ParseException e) {
            e.printStackTrace();
        }
    }

    private void setRegistrationListener() {
          try {
            manager.setRegistrationListener(me.getUriString(), new SipRegistrationListener() {
                public void onRegistering(String localProfileUri) {
                    updateStatus("Registering with SIP Server...");
                }

                public void onRegistrationDone(String localProfileUri, long expiryTime) {
                    updateStatus("Ready");
                    initiateCall();
                }

                public void onRegistrationFailed(String localProfileUri, int errorCode,
                                                 String errorMessage) {
                    updateStatus("Registration failed.  Please check settings.");
                }
            });
        } catch (SipException e) {
            e.printStackTrace();
        }
    }

    private void setReceiveCallListener() {
        try {
            Intent i = new Intent();
            i.setAction("android.SipDemo.INCOMING_CALL");
            PendingIntent pi = PendingIntent.getBroadcast(this, 0, i, Intent.FILL_IN_DATA);

            manager.open(me, pi, null);
        } catch (SipException e) {
            e.printStackTrace();
        }
    }
    public void closeLocalProfile() {
        if (manager == null) {
            return;
        }
        try {
            if (me != null) {
                manager.close(me.getUriString());
            }
        } catch (Exception ee) {
              Log.d( "Failed to close profile", ee.toString());
        }
    }

    /**
     * Make an outgoing call.
     */
    public void initiateCall() {
        updateStatus(sipAddress);
        try {
            SipAudioCall.Listener listener = new SipAudioCall.Listener() {
                // Much of the client's interaction with the SIP Stack will
                // happen via listeners.  Even making an outgoing call, don't
                // forget to set up a listener to set things up once the call is established.
                @Override
                public void onCallEstablished(SipAudioCall call) {
                    call.startAudio();
                    call.setSpeakerMode(true);
                    call.toggleMute();
                    mButtonSipCall.setText("正在拨打电话");
                    updateStatus(call);
                }

                @Override
                public void onCallEnded(SipAudioCall call) {
                    updateStatus("Ready.");
                }
            };

            call = manager.makeAudioCall(me.getUriString(), sipAddress, listener, 30);

        } catch (Exception e) {
            //   Log.i("WalkieTalkieActivity/InitiateCall", "Error when trying to close manager.", e);
            if (me != null) {
                try {
                    manager.close(me.getUriString());
                } catch (Exception ee) {
//                    Log.i("WalkieTalkieActivity/InitiateCall",
//                            "Error when trying to close manager.", ee);
                    ee.printStackTrace();
                }
            }
            if (call != null) {
                call.close();
            }
        }
    }


    public void updateStatus(final String status) {
        // Be a good citizen.  Make sure UI changes fire on the UI thread.
        this.runOnUiThread(new Runnable() {
            public void run() {
                TextView labelView = (TextView) findViewById(R.id.sipLabel);
                labelView.setText(status);
            }
        });
    }

    /**
     * Updates the status box with the SIP address of the current call.
     *
     * @param call The current, active call.
     */
    public void updateStatus(SipAudioCall call) {
        String useName = call.getPeerProfile().getDisplayName();
        if (useName == null) {
            useName = call.getPeerProfile().getUserName();
        }
        updateStatus(useName + "@" + call.getPeerProfile().getSipDomain());
    }




    @Override
    public void onClick(View v) {
        if (isCalling){
            mButtonSipCall.setText("拨打电话");
            if (call != null) {
                try {
                    call.endCall();
                } catch (SipException se) {

                }
                call.close();
            }
        }else {
//            putInCallNumber();
            initiateCall();
            if (call == null) {
                return;
            }else if ( call != null && call.isMuted()) {
                call.toggleMute();
            } else if ( call.isMuted()) {
                call.toggleMute();
            }


        }
        isCalling=!isCalling;
    }

    private void putInCallNumber() {
        initiateCall();
//        LayoutInflater factory = LayoutInflater.from(this);
//        final View textBoxView = factory.inflate(R.layout.call_address_dialog, null);
//        new AlertDialog.Builder(this)
//                .setTitle("Call Someone.")
//                .setView(textBoxView)
//                .setPositiveButton(
//                        android.R.string.ok, new DialogInterface.OnClickListener() {
//                            public void onClick(DialogInterface dialog, int whichButton) {
//                                EditText textField = (EditText)
//                                        (textBoxView.findViewById(R.id.calladdress_edit));
//                                sipAddress = textField.getText().toString();
//                                initiateCall();
//
//                            }
//                        })
//                .setNegativeButton(
//                        android.R.string.cancel, new DialogInterface.OnClickListener() {
//                            public void onClick(DialogInterface dialog, int whichButton) {
//                                // Noop.
//                            }
//                        })
//                .create();
    }

}
作者:danfengw 发表于2017/3/22 9:43:32 原文链接
阅读:70 评论:0 查看评论

深入理解Android Instant Run运行机制

$
0
0

Instant Run

Instant Run,是android studio2.0新增的一个运行机制,在你编码开发、测试或debug的时候,它都能显著减少你对当前应用的构建和部署的时间。通俗的解释就是,当你在Android Studio中改了你的代码,Instant Run可以很快的让你看到你修改的效果。而在没有Instant Run之前,你的一个小小的修改,都肯能需要几十秒甚至更长的等待才能看到修改后的效果。

传统的代码修改及编译部署流程

传统的代码修改及编译流程如下:构建整个apk → 部署app → app重启 → 重启Activity
这里写图片描述

Instant Run编译和部署流程

Instant Run构建项目的流程:构建修改的部分 → 部署修改的dex或资源 → 热部署,温部署,冷部署

热拔插,温拔插,冷拔插

热拔插:代码改变被应用、投射到APP上,不需要重启应用,不需要重建当前activity。
场景:适用于多数的简单改变(包括一些方法实现的修改,或者变量值修改)
**温拔插:**activity需要被重启才能看到所需更改。
场景:典型的情况是代码修改涉及到了资源文件,即resources。
**冷拔插:**app需要被重启(但是仍然不需要重新安装)
场景:任何涉及结构性变化的,比如:修改了继承规则、修改了方法签名等。

首次运行Instant Run,Gradle执行过程

一个新的App Server类会被注入到App中,与Bytecode instrumentation协同监控代码的变化。
同时会有一个新的Application类,它注入了一个自定义类加载器(Class Loader),同时该Application类会启动我们所需的新注入的App Server。于是,Manifest会被修改来确保我们的应用能使用这个新的Application类。(这里不必担心自己继承定义了Application类,Instant Run添加的这个新Application类会代理我们自定义的Application类)
至此,Instant Run已经可以跑起来了,在我们使用的时候,它会通过决策,合理运用冷温热拔插来协助我们大量地缩短构建程序的时间。

在Instant Run运行之前,Android Studio会检查是否能连接到App Server中。并且确保这个App Server是Android Studio所需要的。这同样能确保该应用正处在前台。

热拔插

这里写图片描述
Android Studio monitors: 运行着Gradle任务来生成增量.dex文件(这个dex文件是对应着开发中的修改类) Android Studio会提取这些.dex文件发送到App Server,然后部署到App(Gradle修改class的原理,请戳链接)。

App Server会不断监听是否需要重写类文件,如果需要,任务会被立马执行。新的更改便能立即被响应。我们可以通过打断点的方式来查看。

温拔插

温拔插需要重启Activity,因为资源文件是在Activity创建时加载,所以必须重启Activity来重载资源文件。
目前来说,任何资源文件的修改都会导致重新打包再发送到APP。但是,google的开发团队正在致力于开发一个增量包,这个增量包只会包装修改过的资源文件并能部署到当前APP上。
所以温拔插实际上只能应对少数的情况,它并不能应付应用在架构、结构上的变化。

注:温拔插涉及到的资源文件修改,在manifest上是无效的(这里的无效是指不会启动Instant Run),因为,manifest的值是在APK安装的时候被读取,所以想要manifest下资源的修改生效,还需要触发一个完整的应用构建和部署。

冷拔插

应用部署的时候,会把工程拆分成十个部分,每部分都拥有自己的.dex文件,然后所有的类会根据包名被分配给相应的.dex文件。当冷拔插开启时,修改过的类所对应的.dex文件,会重组生成新的.dex文件,然后再部署到设备上。
之所以能这么做,是依赖于Android的ART模式,它能允许加载多个.dex文件。ART模式在android4.4(API-19)中加入,但是Dalvik依然是首选,到了android5.0(API-21),ART模式才成为系统默认首选,所以Instant Run只能运行在API-21及其以上版本。

使用Instant Run一些注意点

Instant Run是被Android Studio控制的。所以我们只能通过IDE来启动它,如果通过设备来启动应用,Instant Run会出现异常情况。在使用Instant Run来启动Android app的时候,应注意以下几点:

  1. 如果应用的minSdkVersion小于21,可能多数的Instant Run功能会挂掉,这里提供一个解决方法,通过product flavor建立一个minSdkVersion大于21的新分支,用来debug。
  2. Instant Run目前只能在主进程里运行,如果应用是多进程的,类似微信,把webView抽出来单独一个进程,那热、温拔插会被降级为冷拔插。
  3. 在Windows下,Windows Defender Real-Time Protection可能会导致Instant Run挂掉,可用通过添加白名单列表解决。
  4. 暂时不支持Jack compiler,Instrumentation Tests,或者同时部署到多台设备。

结合Demo深度理解

为了方便大家的理解,我们新建一个项目,里面不写任何的逻辑功能,只对application做一个修改:
这里写图片描述

首先,我们先反编译一下APK的构成,使用的工具:d2j-dex2jar 和jd-gui。
这里写图片描述
我们要看的启动的信息就在这个instant-run.zip文件里面,解压instant-run.zip,我们会发现,我们真正的业务代码都在这里。
这里写图片描述

从instant-run文件中我们猜想是BootstrapApplication替换了我们的application,Instant-Run代码作为一个宿主程序,将app作为资源dex加载起来。

那么InstantRun是怎么把业务代码运行起来的呢?

Instant Run如何启动app

按照我们上面对instant-run运行机制的猜想,我们首先看一下appliaction的分析attachBaseContext和onCreate方法。

attachBaseContext()

protected void attachBaseContext(Context context) {
if (!AppInfo.usingApkSplits) {
String apkFile = context.getApplicationInfo().sourceDir;
long apkModified = apkFile != null ? new File(apkFile)
.lastModified() : 0L;
createResources(apkModified);
setupClassLoaders(context, context.getCacheDir().getPath(),
apkModified);
}
createRealApplication();
super.attachBaseContext(context);
if (this.realApplication != null) {
try {
Method attachBaseContext = ContextWrapper.class
.getDeclaredMethod("attachBaseContext",
new Class[] { Context.class });
attachBaseContext.setAccessible(true);
attachBaseContext.invoke(this.realApplication,
new Object[] { context });
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
}

我们依次需要关注的方法有:
createResources → setupClassLoaders → createRealApplication → 调用realApplication的attachBaseContext方法

createResources()

private void createResources(long apkModified) {
FileManager.checkInbox();
File file = FileManager.getExternalResourceFile();
this.externalResourcePath = (file != null ? file.getPath() : null);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Resource override is "
+ this.externalResourcePath);
}
if (file != null) {
try {
long resourceModified = file.lastModified();
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Resource patch last modified: "
+ resourceModified);
Log.v("InstantRun", "APK last modified: " + apkModified
+ " "
+ (apkModified > resourceModified ? ">" : "<")
+ " resource patch");
}
if ((apkModified == 0L) || (resourceModified <= apkModified)) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Ignoring resource file, older than APK");
}
this.externalResourcePath = null;
}
} catch (Throwable t) {
Log.e("InstantRun", "Failed to check patch timestamps", t);
}
}
}

说明:该方法主要是判断资源resource.ap_是否改变,然后保存resource.ap_的路径到externalResourcePath中。

setupClassLoaders()

private static void setupClassLoaders(Context context, String codeCacheDir,
long apkModified) {
List dexList = FileManager.getDexList(context, apkModified);
Class server = Server.class;
Class patcher = MonkeyPatcher.class;
if (!dexList.isEmpty()) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Bootstrapping class loader with dex list "
+ join('\n', dexList));
}
ClassLoader classLoader = BootstrapApplication.class
.getClassLoader();
String nativeLibraryPath;
try {
nativeLibraryPath = (String) classLoader.getClass()
.getMethod("getLdLibraryPath", new Class[0])
.invoke(classLoader, new Object[0]);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Native library path: "
+ nativeLibraryPath);
}
} catch (Throwable t) {
Log.e("InstantRun", "Failed to determine native library path "
+ t.getMessage());
nativeLibraryPath = FileManager.getNativeLibraryFolder()
.getPath();
}
IncrementalClassLoader.inject(classLoader, nativeLibraryPath,
codeCacheDir, dexList);
}
}

说明,该方法是初始化一个ClassLoaders并调用IncrementalClassLoader。
IncrementalClassLoader的源码如下:

public class IncrementalClassLoader extends ClassLoader {
public static final boolean DEBUG_CLASS_LOADING = false;
private final DelegateClassLoader delegateClassLoader;
public IncrementalClassLoader(ClassLoader original,
String nativeLibraryPath, String codeCacheDir, List dexes) {
super(original.getParent());
this.delegateClassLoader = createDelegateClassLoader(nativeLibraryPath,
codeCacheDir, dexes, original);
}
public Class findClass(String className) throws ClassNotFoundException {
try {
return this.delegateClassLoader.findClass(className);
} catch (ClassNotFoundException e) {
throw e;
}
}
private static class DelegateClassLoader extends BaseDexClassLoader {
private DelegateClassLoader(String dexPath, File optimizedDirectory,
String libraryPath, ClassLoader parent) {
super(dexPath, optimizedDirectory, libraryPath, parent);
}
public Class findClass(String name) throws ClassNotFoundException {
try {
return super.findClass(name);
} catch (ClassNotFoundException e) {
throw e;
}
}
}
private static DelegateClassLoader createDelegateClassLoader(
String nativeLibraryPath, String codeCacheDir, List dexes,
ClassLoader original) {
String pathBuilder = createDexPath(dexes);
return new DelegateClassLoader(pathBuilder, new File(codeCacheDir),
nativeLibraryPath, original);
}
private static String createDexPath(List dexes) {
StringBuilder pathBuilder = new StringBuilder();
boolean first = true;
for (String dex : dexes) {
if (first) {
first = false;
} else {
pathBuilder.append(File.pathSeparator);
}
pathBuilder.append(dex);
}
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Incremental dex path is "
+ BootstrapApplication.join('\n', dexes));
}
return pathBuilder.toString();
}
private static void setParent(ClassLoader classLoader, ClassLoader newParent) {
try {
Field parent = ClassLoader.class.getDeclaredField("parent");
parent.setAccessible(true);
parent.set(classLoader, newParent);
} catch (IllegalArgumentException e) {
throw new RuntimeException(e);
} catch (IllegalAccessException e) {
throw new RuntimeException(e);
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
public static ClassLoader inject(ClassLoader classLoader,
String nativeLibraryPath, String codeCacheDir, List dexes) {
IncrementalClassLoader incrementalClassLoader = new IncrementalClassLoader(
classLoader, nativeLibraryPath, codeCacheDir, dexes);
setParent(classLoader, incrementalClassLoader);
return incrementalClassLoader;
}
}

inject方法是用来设置classloader的父子顺序的,使用IncrementalClassLoader来加载dex。由于ClassLoader的双亲委托模式,也就是委托父类加载类,父类中找不到再在本ClassLoader中查找。
调用的效果图如下:

这里写图片描述

为了方便我们对委托父类加载机制的理解,我们可以做一个实验,在我们的application做一些Log。

@Override
public void onCreate() {
super.onCreate();
try{
Log.d(TAG,"###onCreate in myApplication");
String classLoaderName = getClassLoader().getClass().getName();
Log.d(TAG,"###onCreate in myApplication classLoaderName = "+classLoaderName);
String parentClassLoaderName = getClassLoader().getParent().getClass().getName();
Log.d(TAG,"###onCreate in myApplication parentClassLoaderName = "+parentClassLoaderName);
String pParentClassLoaderName = getClassLoader().getParent().getParent().getClass().getName();
Log.d(TAG,"###onCreate in myApplication pParentClassLoaderName = "+pParentClassLoaderName);
}catch (Exception e){
e.printStackTrace();
}
}

输出结果:

03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication classLoaderName = dalvik.system.PathClassLoader
03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication parentClassLoaderName = com.android.tools.fd.runtime.IncrementalClassLoader
03-20 10:43:42.475 27307-27307/mobctrl.net.testinstantrun D/MyApplication: ###onCreate in myApplication pParentClassLoaderName = java.lang.BootClassLoader

由此,我们知道,当前PathClassLoader委托IncrementalClassLoader加载dex。

我们继续对attachBaseContext()继续分析:

attachBaseContext.invoke(this.realApplication,
new Object[] { context });

createRealApplication

private void createRealApplication() {
if (AppInfo.applicationClass != null) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"About to create real application of class name = "
+ AppInfo.applicationClass);
}
try {
Class realClass = (Class) Class
.forName(AppInfo.applicationClass);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Created delegate app class successfully : "
+ realClass + " with class loader "
+ realClass.getClassLoader());
}
Constructor constructor = realClass
.getConstructor(new Class[0]);
this.realApplication = ((Application) constructor
.newInstance(new Object[0]));
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Created real app instance successfully :"
+ this.realApplication);
}
} catch (Exception e) {
throw new IllegalStateException(e);
}
} else {
this.realApplication = new Application();
}
}

该方法就是用classes.dex中的AppInfo类的applicationClass常量中保存的app真实的application。由例子的分析我们可以知道applicationClass就是com.xzh.demo.MyApplication。通过反射的方式,创建真是的realApplication。
看完attachBaseContext我们继续看BootstrapApplication();

BootstrapApplication()

我们首先看一下onCreate方法:

onCreate()

public void onCreate() {
if (!AppInfo.usingApkSplits) {
MonkeyPatcher.monkeyPatchApplication(this, this,
this.realApplication, this.externalResourcePath);
MonkeyPatcher.monkeyPatchExistingResources(this,
this.externalResourcePath, null);
} else {
MonkeyPatcher.monkeyPatchApplication(this, this,
this.realApplication, null);
}
super.onCreate();
if (AppInfo.applicationId != null) {
try {
boolean foundPackage = false;
int pid = Process.myPid();
ActivityManager manager = (ActivityManager) getSystemService("activity");
List processes = manager
.getRunningAppProcesses();
boolean startServer = false;
if ((processes != null) && (processes.size() > 1)) {
for (ActivityManager.RunningAppProcessInfo processInfo : processes) {
if (AppInfo.applicationId
.equals(processInfo.processName)) {
foundPackage = true;
if (processInfo.pid == pid) {
startServer = true;
break;
}
}
}
if ((!startServer) && (!foundPackage)) {
startServer = true;
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Multiprocess but didn't find process with package: starting server anyway");
}
}
} else {
startServer = true;
}
if (startServer) {
Server.create(AppInfo.applicationId, this);
}
} catch (Throwable t) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Failed during multi process check", t);
}
Server.create(AppInfo.applicationId, this);
}
}
if (this.realApplication != null) {
this.realApplication.onCreate();
}
}

在onCreate()中我们需要注意以下方法:
monkeyPatchApplication → monkeyPatchExistingResources → Server启动 → 调用realApplication的onCreate方法

monkeyPatchApplication

public static void monkeyPatchApplication(Context context,
Application bootstrap, Application realApplication,
String externalResourceFile) {
try {
Class activityThread = Class
.forName("android.app.ActivityThread");
Object currentActivityThread = getActivityThread(context,
activityThread);
Field mInitialApplication = activityThread
.getDeclaredField("mInitialApplication");
mInitialApplication.setAccessible(true);
Application initialApplication = (Application) mInitialApplication
.get(currentActivityThread);
if ((realApplication != null) && (initialApplication == bootstrap)) {
mInitialApplication.set(currentActivityThread, realApplication);
}
if (realApplication != null) {
Field mAllApplications = activityThread
.getDeclaredField("mAllApplications");
mAllApplications.setAccessible(true);
List allApplications = (List) mAllApplications
.get(currentActivityThread);
for (int i = 0; i < allApplications.size(); i++) {
if (allApplications.get(i) == bootstrap) {
allApplications.set(i, realApplication);
}
}
}
Class loadedApkClass;
try {
loadedApkClass = Class.forName("android.app.LoadedApk");
} catch (ClassNotFoundException e) {
loadedApkClass = Class
.forName("android.app.ActivityThread$PackageInfo");
}
Field mApplication = loadedApkClass
.getDeclaredField("mApplication");
mApplication.setAccessible(true);
Field mResDir = loadedApkClass.getDeclaredField("mResDir");
mResDir.setAccessible(true);
Field mLoadedApk = null;
try {
mLoadedApk = Application.class.getDeclaredField("mLoadedApk");
} catch (NoSuchFieldException e) {
}
for (String fieldName : new String[] { "mPackages",
"mResourcePackages" }) {
Field field = activityThread.getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(currentActivityThread);
for (Map.Entry> entry : ((Map>) value)
.entrySet()) {
Object loadedApk = ((WeakReference) entry.getValue()).get();
if (loadedApk != null) {
if (mApplication.get(loadedApk) == bootstrap) {
if (realApplication != null) {
mApplication.set(loadedApk, realApplication);
}
if (externalResourceFile != null) {
mResDir.set(loadedApk, externalResourceFile);
}
if ((realApplication != null)
&& (mLoadedApk != null)) {
mLoadedApk.set(realApplication, loadedApk);
}
}
}
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}

说明:该方法的作用是替换所有当前app的application为realApplication。
替换的过程如下:
1.替换ActivityThread的mInitialApplication为realApplication
2.替换mAllApplications 中所有的Application为realApplication
3.替换ActivityThread的mPackages,mResourcePackages中的mLoaderApk中的application为realApplication。

monkeyPatchExistingResources

public static void monkeyPatchExistingResources(Context context,
String externalResourceFile, Collection activities) {
if (externalResourceFile == null) {
return;
}
try {
AssetManager newAssetManager = (AssetManager) AssetManager.class
.getConstructor(new Class[0]).newInstance(new Object[0]);
Method mAddAssetPath = AssetManager.class.getDeclaredMethod(
"addAssetPath", new Class[] { String.class });
mAddAssetPath.setAccessible(true);
if (((Integer) mAddAssetPath.invoke(newAssetManager,
new Object[] { externalResourceFile })).intValue() == 0) {
throw new IllegalStateException(
"Could not create new AssetManager");
}
Method mEnsureStringBlocks = AssetManager.class.getDeclaredMethod(
"ensureStringBlocks", new Class[0]);
mEnsureStringBlocks.setAccessible(true);
mEnsureStringBlocks.invoke(newAssetManager, new Object[0]);
if (activities != null) {
for (Activity activity : activities) {
Resources resources = activity.getResources();
try {
Field mAssets = Resources.class
.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class
.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass()
.getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
Resources.Theme theme = activity.getTheme();
try {
try {
Field ma = Resources.Theme.class
.getDeclaredField("mAssets");
ma.setAccessible(true);
ma.set(theme, newAssetManager);
} catch (NoSuchFieldException ignore) {
Field themeField = Resources.Theme.class
.getDeclaredField("mThemeImpl");
themeField.setAccessible(true);
Object impl = themeField.get(theme);
Field ma = impl.getClass().getDeclaredField(
"mAssets");
ma.setAccessible(true);
ma.set(impl, newAssetManager);
}
Field mt = ContextThemeWrapper.class
.getDeclaredField("mTheme");
mt.setAccessible(true);
mt.set(activity, null);
Method mtm = ContextThemeWrapper.class
.getDeclaredMethod("initializeTheme",
new Class[0]);
mtm.setAccessible(true);
mtm.invoke(activity, new Object[0]);
Method mCreateTheme = AssetManager.class
.getDeclaredMethod("createTheme", new Class[0]);
mCreateTheme.setAccessible(true);
Object internalTheme = mCreateTheme.invoke(
newAssetManager, new Object[0]);
Field mTheme = Resources.Theme.class
.getDeclaredField("mTheme");
mTheme.setAccessible(true);
mTheme.set(theme, internalTheme);
} catch (Throwable e) {
Log.e("InstantRun",
"Failed to update existing theme for activity "
+ activity, e);
}
pruneResourceCaches(resources);
}
}
Collection> references;
if (Build.VERSION.SDK_INT >= 19) {
Class resourcesManagerClass = Class
.forName("android.app.ResourcesManager");
Method mGetInstance = resourcesManagerClass.getDeclaredMethod(
"getInstance", new Class[0]);
mGetInstance.setAccessible(true);
Object resourcesManager = mGetInstance.invoke(null,
new Object[0]);
try {
Field fMActiveResources = resourcesManagerClass
.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
ArrayMap> arrayMap = (ArrayMap) fMActiveResources
.get(resourcesManager);
references = arrayMap.values();
} catch (NoSuchFieldException ignore) {
Field mResourceReferences = resourcesManagerClass
.getDeclaredField("mResourceReferences");
mResourceReferences.setAccessible(true);
references = (Collection) mResourceReferences
.get(resourcesManager);
}
} else {
Class activityThread = Class
.forName("android.app.ActivityThread");
Field fMActiveResources = activityThread
.getDeclaredField("mActiveResources");
fMActiveResources.setAccessible(true);
Object thread = getActivityThread(context, activityThread);
HashMap> map = (HashMap) fMActiveResources
.get(thread);
references = map.values();
}
for (WeakReference wr : references) {
Resources resources = (Resources) wr.get();
if (resources != null) {
try {
Field mAssets = Resources.class
.getDeclaredField("mAssets");
mAssets.setAccessible(true);
mAssets.set(resources, newAssetManager);
} catch (Throwable ignore) {
Field mResourcesImpl = Resources.class
.getDeclaredField("mResourcesImpl");
mResourcesImpl.setAccessible(true);
Object resourceImpl = mResourcesImpl.get(resources);
Field implAssets = resourceImpl.getClass()
.getDeclaredField("mAssets");
implAssets.setAccessible(true);
implAssets.set(resourceImpl, newAssetManager);
}
resources.updateConfiguration(resources.getConfiguration(),
resources.getDisplayMetrics());
}
}
} catch (Throwable e) {
throw new IllegalStateException(e);
}
}

说明:该方法的作用是替换所有当前app的mAssets为newAssetManager。
monkeyPatchExistingResources的流程如下:
1.如果resource.ap_文件有改变,那么新建一个AssetManager对象newAssetManager,然后用newAssetManager对象替换所有当前Resource、Resource.Theme的mAssets成员变量。
2.如果当前的已经有Activity启动了,还需要替换所有Activity中mAssets成员变量

判断Server是否已经启动,如果没有启动,则启动Server。然后调用realApplication的onCreate方法代理realApplication的生命周期。

接下来我们分析下Server负责的热部署温部署冷部署等问题。

Server热部署、温部署和冷部署

首先重点关注一下Server的内部类SocketServerReplyThread。

SocketServerReplyThread

private class SocketServerReplyThread extends Thread {
private final LocalSocket mSocket;
SocketServerReplyThread(LocalSocket socket) {
this.mSocket = socket;
}
public void run() {
try {
DataInputStream input = new DataInputStream(
this.mSocket.getInputStream());
DataOutputStream output = new DataOutputStream(
this.mSocket.getOutputStream());
try {
handle(input, output);
} finally {
try {
input.close();
} catch (IOException ignore) {
}
try {
output.close();
} catch (IOException ignore) {
}
}
return;
} catch (IOException e) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Fatal error receiving messages", e);
}
}
}
private void handle(DataInputStream input, DataOutputStream output)
throws IOException {
long magic = input.readLong();
if (magic != 890269988L) {
Log.w("InstantRun",
"Unrecognized header format " + Long.toHexString(magic));
return;
}
int version = input.readInt();
output.writeInt(4);
if (version != 4) {
Log.w("InstantRun",
"Mismatched protocol versions; app is using version 4 and tool is using version "
+ version);
} else {
int message;
for (;;) {
message = input.readInt();
switch (message) {
case 7:
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received EOF from the IDE");
}
return;
case 2:
boolean active = Restarter
.getForegroundActivity(Server.this.mApplication) != null;
output.writeBoolean(active);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Received Ping message from the IDE; returned active = "
+ active);
}
break;
case 3:
String path = input.readUTF();
long size = FileManager.getFileSize(path);
output.writeLong(size);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received path-exists(" + path
+ ") from the " + "IDE; returned size="
+ size);
}
break;
case 4:
long begin = System.currentTimeMillis();
path = input.readUTF();
byte[] checksum = FileManager.getCheckSum(path);
if (checksum != null) {
output.writeInt(checksum.length);
output.write(checksum);
if (Log.isLoggable("InstantRun", 2)) {
long end = System.currentTimeMillis();
String hash = new BigInteger(1, checksum)
.toString(16);
Log.v("InstantRun", "Received checksum(" + path
+ ") from the " + "IDE: took "
+ (end - begin) + "ms to compute "
+ hash);
}
} else {
output.writeInt(0);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received checksum(" + path
+ ") from the "
+ "IDE: returning ");
}
}
break;
case 5:
if (!authenticate(input)) {
return;
}
Activity activity = Restarter
.getForegroundActivity(Server.this.mApplication);
if (activity != null) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Restarting activity per user request");
}
Restarter.restartActivityOnUiThread(activity);
}
break;
case 1:
if (!authenticate(input)) {
return;
}
List changes = ApplicationPatch
.read(input);
if (changes != null) {
boolean hasResources = Server.hasResources(changes);
int updateMode = input.readInt();
updateMode = Server.this.handlePatches(changes,
hasResources, updateMode);
boolean showToast = input.readBoolean();
output.writeBoolean(true);
Server.this.restart(updateMode, hasResources,
showToast);
}
break;
case 6:
String text = input.readUTF();
Activity foreground = Restarter
.getForegroundActivity(Server.this.mApplication);
if (foreground != null) {
Restarter.showToast(foreground, text);
} else if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun",
"Couldn't show toast (no activity) : "
+ text);
}
break;
}
}
}
}
}

说明:socket开启后,开始读取数据,当读到1时,获取代码变化的ApplicationPatch列表,然后调用handlePatches来处理代码的变化。

handlePatches

private int handlePatches(List changes,
boolean hasResources, int updateMode) {
if (hasResources) {
FileManager.startUpdate();
}
for (ApplicationPatch change : changes) {
String path = change.getPath();
if (path.endsWith(".dex")) {
handleColdSwapPatch(change);
boolean canHotSwap = false;
for (ApplicationPatch c : changes) {
if (c.getPath().equals("classes.dex.3")) {
canHotSwap = true;
break;
}
}
if (!canHotSwap) {
updateMode = 3;
}
} else if (path.equals("classes.dex.3")) {
updateMode = handleHotSwapPatch(updateMode, change);
} else if (isResourcePath(path)) {
updateMode = handleResourcePatch(updateMode, change, path);
}
}
if (hasResources) {
FileManager.finishUpdate(true);
}
return updateMode;
}

说明:本方法主要通过判断Change的内容,来判断采用什么模式(热部署、温部署或冷部署)

  • 如果后缀为“.dex”,冷部署处理handleColdSwapPatch
  • 如果后缀为“classes.dex.3”,热部署处理handleHotSwapPatch
  • 其他情况,温部署,处理资源handleResourcePatch

handleColdSwapPatch冷部署

private static void handleColdSwapPatch(ApplicationPatch patch) {
if (patch.path.startsWith("slice-")) {
File file = FileManager.writeDexShard(patch.getBytes(), patch.path);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received dex shard " + file);
}
}
}

说明:该方法把dex文件写到私有目录,等待整个app重启,重启之后,使用前面提到的IncrementalClassLoader加载dex即可。

handleHotSwapPatch热部署

private int handleHotSwapPatch(int updateMode, ApplicationPatch patch) {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Received incremental code patch");
}
try {
String dexFile = FileManager.writeTempDexFile(patch.getBytes());
if (dexFile == null) {
Log.e("InstantRun", "No file to write the code to");
return updateMode;
}
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Reading live code from " + dexFile);
}
String nativeLibraryPath = FileManager.getNativeLibraryFolder()
.getPath();
DexClassLoader dexClassLoader = new DexClassLoader(dexFile,
this.mApplication.getCacheDir().getPath(),
nativeLibraryPath, getClass().getClassLoader());
Class aClass = Class.forName(
"com.android.tools.fd.runtime.AppPatchesLoaderImpl", true,
dexClassLoader);
try {
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Got the patcher class " + aClass);
}
PatchesLoader loader = (PatchesLoader) aClass.newInstance();
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Got the patcher instance " + loader);
}
String[] getPatchedClasses = (String[]) aClass
.getDeclaredMethod("getPatchedClasses", new Class[0])
.invoke(loader, new Object[0]);
if (Log.isLoggable("InstantRun", 2)) {
Log.v("InstantRun", "Got the list of classes ");
for (String getPatchedClass : getPatchedClasses) {
Log.v("InstantRun", "class " + getPatchedClass);
}
}
if (!loader.load()) {
updateMode = 3;
}
} catch (Exception e) {
Log.e("InstantRun", "Couldn't apply code changes", e);
e.printStackTrace();
updateMode = 3;
}
} catch (Throwable e) {
Log.e("InstantRun", "Couldn't apply code changes", e);
updateMode = 3;
}
return updateMode;
}

说明:该方法将patch的dex文件写入到临时目录,然后使用DexClassLoader去加载dex。然后反射调用AppPatchesLoaderImpl类的load方法。
需要强调的是:AppPatchesLoaderImpl继承自抽象类AbstractPatchesLoaderImpl,并实现了抽象方法:getPatchedClasses。而AbstractPatchesLoaderImpl抽象类代码如下:

public abstract class AbstractPatchesLoaderImpl implements PatchesLoader {
public abstract String[] getPatchedClasses();
public boolean load() {
try {
for (String className : getPatchedClasses()) {
ClassLoader cl = getClass().getClassLoader();
Class aClass = cl.loadClass(className + "$override");
Object o = aClass.newInstance();
Class originalClass = cl.loadClass(className);
Field changeField = originalClass.getDeclaredField("$change");
changeField.setAccessible(true);
Object previous = changeField.get(null);
if (previous != null) {
Field isObsolete = previous.getClass().getDeclaredField(
"$obsolete");
if (isObsolete != null) {
isObsolete.set(null, Boolean.valueOf(true));
}
}
changeField.set(null, o);
if ((Log.logging != null)
&& (Log.logging.isLoggable(Level.FINE))) {
Log.logging.log(Level.FINE, String.format("patched %s",
new Object[] { className }));
}
}
} catch (Exception e) {
if (Log.logging != null) {
Log.logging.log(Level.SEVERE, String.format(
"Exception while patching %s",
new Object[] { "foo.bar" }), e);
}
return false;
}
return true;
}
}

Instant Run热部署原理

由上面的代码分析,我们对Instant Run的流程可以分析如下:

1,在第一次构建apk时,在每一个类中注入了一个$change的成员变量,它实现了IncrementalChange接口,并在每一个方法中,插入了一段类似的逻辑。

IncrementalChange localIncrementalChange = $change;
if (localIncrementalChange != null) {
localIncrementalChange.access$dispatch(
"onCreate.(Landroid/os/Bundle;)V", new Object[] { this,
... });
return;
}

当$change不为空的时候,执行IncrementalChange方法。

2,当我们修改代码中方法的实现之后,点击InstantRun,它会生成对应的patch文件来记录你修改的内容。patch文件中的替换类是在所修改类名的后面追加$override,并实现IncrementalChange接口。

3,生成AppPatchesLoaderImpl类,继承自AbstractPatchesLoaderImpl,并实现getPatchedClasses方法,来记录哪些类被修改了。

4,调用load方法之后,根据getPatchedClasses返回的修改过的类的列表,去加载对应的overridechange设置为对应的实现了IncrementalChange接口的$override类。

Instant Run运行机制总结

Instant Run运行机制主要涉及到热部署、温部署和冷部署,主要是在第一次运行,app运行时期,有代码修改时。

第一次编译

1.把Instant-Run.jar和instant-Run-bootstrap.jar打包到主dex中
2.替换AndroidManifest.xml中的application配置
3.使用asm工具,在每个类中添加$change,在每个方法前加逻辑
4.把源代码编译成dex,然后存放到压缩包instant-run.zip中

app运行时

1.获取更改后资源resource.ap_的路径
2.设置ClassLoader。setupClassLoader:
使用IncrementalClassLoader加载apk的代码,将原有的BootClassLoader → PathClassLoader改为BootClassLoader → IncrementalClassLoader → PathClassLoader继承关系。
3.createRealApplication:
创建apk真实的application
4.monkeyPatchApplication
反射替换ActivityThread中的各种Application成员变量
5.monkeyPatchExistingResource
反射替换所有存在的AssetManager对象
6.调用realApplication的onCreate方法
7.启动Server,Socket接收patch列表

有代码修改时

1.生成对应的$override类
2.生成AppPatchesLoaderImpl类,记录修改的类列表
3.打包成patch,通过socket传递给app
4.app的server接收到patch之后,分别按照handleColdSwapPatch、handleHotSwapPatch、handleResourcePatch等待对patch进行处理
5.restart使patch生效

在Android插件化、Android热修复、apk加壳/脱壳中借鉴了Instant Run运行机制,所以理解Instant Run运行机制对于向更深层次的研究是很有帮助的,对于我们自己书写框架也是有借鉴意义的。

作者:xiangzhihong8 发表于2017/3/22 10:59:52 原文链接
阅读:184 评论:0 查看评论

Android开发:使用EasyPay打造全能移动支付框架

$
0
0

前言

在这之前,笔者发布了两篇移动app支付相关博文,得到一些关注,但是由于博文中代码零碎,有些读者私信博主,以及加笔者qq咨询相关问题。考虑到这些,笔者把之前项目中的支付相关代码从业务中剥离出来,重构,形成了现在的EasyPay

EasyPay项目地址:https://github.com/xiong-it/EasyPay

本文原创作者:MichaelX,博客链接:http://blog.csdn.net/xiong_it 转载请注明来源,谢谢合作!


EasyPay是什么

EasyPay旨在帮助Android开发者快速集成接入移动支付SDK,其中包括主流的微信APP支付支付宝APP支付,银联支付(开发中)。


为什么要用EasyPay

EasyPay和微信支付等移动支付SDK的区别

EasyPay是一个开源的聚合支付可定制化框架,目前已集成微信APP支付,支付宝APP支付SDK。银联支付(开发中)。

Android开发者只需要简单调用EasyPay的几行代码,即可调起支付客户端,完成支付流程,得到支付结果。

EasyPay宗旨:简单,易用,可扩展。

EasyPay和其他第三方聚合支付的区别

第三方聚合支付,如知名的Ping++,需要同时接入其Server端SDK和Client端SDK,使接入企业面临风险:

1、信息泄露风险
2、支付集成服务商提供服务跟不上商户业务发展需要的风险
3、支付集成服务商系统稳定性、安全性的风险
4、资金安全风险
—知乎:《使用第三方支付集成有何风险,例如 Beecloud 或者 Ping++?》

此外,天下没有免费的午餐,第三方聚合支付平台一般需要收取5%~15%左右的手续费等各种服务费用,使得接入企业收益受损。

而使用开源的EasyPay,代码透明,与Server端无关,Android开发者只需要根据自己需求对EasyPay进行个性化定制,即可打造一个支付平台齐全的无风险支付框架。但是客观的讲,这同时也是EasyPay的短板,它只简化了APP端开发者的调用工作,Server端工作人员仍需要按照移动支付第三方平台的SDK文档进行开发。

如果觉得EasyPay对你有帮助,你付出的仅仅是一个点赞,或者一个star或者fork,如果不满意,请帮忙提issue指出,而不是5%-15%左右的手续费等各种服务费用。

通过阅读EasyPay源码,你可以知道移动支付的流程是怎样的:

APP->APP服务器->支付平台后台服务器->APP服务器->APP->支付客户端->APP

通过扩展EasyPay,你可以较快的搭建一个私有的功能完善的支付框架。


EasyPay怎么用

用户场景:

APP用户选择一个价格为666元的商品:”皮皮虾”,商品描述:”此商品属性过于强大,难以调教,一般人切勿轻易购买,吼吼!”,然后用户进入收款台,选择了微信支付。

好勒,皮皮虾,咱们走!此处省略:皮皮虾,咱们走.jpg

 PayParams params = new PayParams.Builder(this)
                .wechatAppID("your_wechat_appid")// 仅当支付方式选择微信支付时需要此参数
                .payWay(PayWay.WechatPay)
                .goodsPrice(66600)// 单位为:分
                .goodsName("皮皮虾")
                .goodsIntroduction("此商品属性过于强大,难以调教,一般人切勿轻易购买,吼吼!")
                .httpType(HttpType.Get)
                .httpClientType(NetworkClientType.Retrofit)
                .requestBaseUrl("http://blog.csdn.net/")// 此处替换为为你的app服务器host主机地址
                .build();

  EasyPay.newInstance(params).requestPayInfo(new OnPayInfoRequestListener() {
            @Override
            public void onPayInfoRequetStart() {
                // TODO 在此处做一些loading操作,progressbar.show();
            }

            @Override
            public void onPayInfoRequstSuccess() {
                // TODO 可以将loading状态去掉了。请求预支付信息成功,开始跳转到客户端支付。
            }

            @Override
            public void onPayInfoRequestFailure() {
                // / TODO 可以将loading状态去掉了。获取预支付信息失败,会同时得到一个支付失败的回调。
            }
        }).toPay(new OnPayResultListener() {

            @Override
            public void onPaySuccess(PayWay payWay) {
                // 支付成功
            }

            @Override
            public void onPayCancel(PayWay payWay) {
                // 支付流程被用户中途取消
            }

            @Override
            public void onPayFailure(PayWay payWay, int errCode) {
                // 支付失败,errCode码详见来源博客或者github项目主页的README文档
            }
        });

开发者调用步骤:
1. 通过建造者模式创建支付参数PayParams实例并传入EasyPay的创建方法中
2. 传入支付结果回调接口实例得到支付结果回调

假如你的app中每个商品有id,请求服务器时可以用商品id代替价格,让服务器自己去查询价格,防止客户端中的商品价格被恶意修改。


开发者需要做什么

上一节是开发者在Activity/Fragment之类的View层调用代码,除此之外,开发者还需要做一些少量的额外的工作。

需要理解移动支付的流程

  1. APP将商品信息post给APP服务器
  2. APP服务器携带商品信息和一些其他信息请求支付平台服务器,获取预支付订单信息
  3. APP服务器得到预支付订单信息并返给APP
  4. APP解析预支付订单信息
  5. APP利用解析后的预支付信息调起支付客户端(微信,支付宝,等)
  6. 支付客户端将支付结果返给APP
  7. APP向用户展示支付结果

需要导入EasyPay/library源码依赖并修改app客户端相关文件

  1. 下载EasyPay源码到本地
  2. 在Android Studio中打开你的app项目
  3. Android Studio左上角File->New->Import Module->… 选择library目录导入,app会自动依赖library这个module
  4. 复制EasyPay/samplewxapi包到你的包路径下,假如你的包名:com.app.payclient,那么wxapi包应该放在payclient包下面
  5. 按照EasyPay/sampleAndroidMenifest.xml文件修改你的清单文件
  6. 按照EasyPay/sampleproguard-rules.pro修改你的混淆文件

需要修改服务器请求路径和请求字段和返回的json解析

由于笔者并不知道你的服务器地址和请求路径及字段和返回json格式,所以你只需要动动小手改下library中的相应代码即可。

假如你的支付api接口文档如下:
host:http://api.yourhost.com/
路径:pay/
请求方式为:Http,get
请求需要的参数字段为:

字段 类型 意义
pay_way int 支付方式:0-微信,1-支付宝,2-银联 等
price int 商品价格,单位:人民币-分
goods_name String(128) 商品名称,将在支付客户端展示
goods_introduction String(512) 商品描述,微信支付可填,支付宝必填



假设你想使用的网络框架是Retrofit2。那么network/NetwrokClientInterf的实现类RetrofitClient需要做如下修改:
由于Retrofit请求一般需要借助一个xxService类,那么实际修改的是xxService类,以EasyPay源码中的network/PrePayInfoService为例,它需要修改成如下:

public interface PrePayInfoService {
    @GET("pay/")
    Call<ResponseBody> getPrePayInfo(@Query("pay_way")String payWay, @Query("price") String GoddsPrice, @Query("goods_name") String goodsName, @Query(("goods_introduction")) String goodsIntroduce);

   /*@POST("pay/")
    Call<ResponseBody> postPrePayInfo(@Query("pay_way")String payWay, @Query("price") String GoddsPrice, @Query("goods_name") String goodsName, @Query(("goods_introduction")) String goodsIntroduce);*/
}
// (如需更多字段请自行添加参数)

当网络连接使用其他框架时,需要在NetworkClientInterf对应的实现类中修改路径及请求参数字段。

假如当前用户使用了微信支付,当服务器返回的数据格式如下时:

字段 类型 意义
errCode int 错误码,0表示数据正确返回
errString String 错误提示
data String 返给客户端的json数据

假如json的格式如下:

字段 类型 意义
appid String 微信appid
partnerid String 商户号
prepayid String 预支付交易id
package String 固定值:Sign=WXPay
noncestr String 随机字符串
timestamp String 时间戳
sign String 签名



以你们server端人员给出的实际json字段来修改pay/PrePayInfo

当为其他支付方式时,也需要在对应的PayStragetyInterf支付实现策略类中修改解析。

library源码中需要修改的地方都打上了TODO标签,导入Android Studio后,如下图方式查看:
TODO


开发者能做什么

EasyPay目前实现了微信,支付宝app支付,如果你觉得支付逻辑代码不OK?完全可以通过实现PayStragetyInterf来完全重写一个自己的微信,支付宝支付策略。

EasyPay支持的平台(微信,支付宝,银联)不在你的需求范围内?可以通过实现PayStragetyInterf来扩展一种支付方式。

EasyPay支持的网络框架(HttpUrlConnection,OkHttp3(前两者严格意义上不属于框架),Volley,Retrofit2)用的不顺手?那就自己撸一个NetworkClientInterf接口的实现类来实现自己的网络请求客户端。

其他,还是不够满足你的需求,欢迎提出issue,或者加入一起开发,完善该repo,打造一个更加优秀的EasyPay。


本文原创作者:MichaelX,博客地址:http://blog.csdn.net/xiong_it.转载请注明来源

欢迎光临:MichaelX’s Blog

后记

EasyPay算笔者的第一个正式的开源项目吧,受益于开源社区,也希望为开源奉献一点力量,帮助一些开发者快速打造一个无风险的功能完备的支付框架。

EasyPay项目地址

https://github.com/xiong-it/EasyPay

移动支付开发博文

微信支付:Android App支付系列(一):微信支付接入详细指南
支付宝:Android App支付系列(二):支付宝SDK接入详细指南

EasyPay的回调errCode错误码列表

通用errCode 意义
1 当前网络无连接(尚未进入支付阶段)
2 请求APP服务器超时(尚未进入支付阶段)
-1 支付失败-原因未知,需要开发者手动排查
微信errCode 一般不会碰到
-3 微信接收支付请求失败
-4 微信支付认证失败,拒绝支付交易
-5 微信版本低,不支持交易
-6 微信拒绝了支付交易
-7 未安装微信客户端,交易失败
支付宝errCode 一般不会碰到
8000 支付结果待确认,生成了交易订单,但是未支付。
6002 网络差导致支付失败
6004 支付结果未知
6005 支付失败,原因未知

作者:Xiong_IT 发表于2017/3/22 15:05:18 原文链接
阅读:65 评论:0 查看评论

Android:在Fragment中,onClick失效的解决方法

$
0
0

平时习惯写onclic方法,

但是有时候不是在Activity中,比如:Fragment中用onclick无效,
只能用 id 跳转或者弹框

比如按钮 Button 或者 Lineayout 布局

这里有3个id ,三个弹框显示


<Button  
        android:onClick="onclick1"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:text="按钮一"  />  
   <Button  
        android:onClick="onclick2"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:text="按钮二"  />  
   <Button  
        android:onClick="onclick3"  
        android:layout_width="wrap_content"  
        android:layout_height="wrap_content"  
        android:text="按钮三"  />  



方法:

在Fragment 中不能直接:

public void 单击方法(View view){

......

}

只能 这样:

在 onActivityCreated方法中,获取id,再setOnClickListener

this 也改为:getActivity()


方法:

public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);

        Button b1=(Button) getActivity().findViewById(R.id.b1);
        b1.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                AlertDialog.Builder builder=new Builder(getActivity());
                builder.setIcon(R.drawable.meiqin); //设置图标
                builder.setTitle("需要加入阿库西斯教吗?");//设置对话框的标题
                builder.setMessage("你确定要入教吗?");//设置对话框的内容
                builder.setPositiveButton("确定", new OnClickListener() {  //这个是设置确定按钮

                    @Override
                    public void onClick(DialogInterface arg0, int arg1) {
                        Toast.makeText(getActivity(), "入教成功!", Toast.LENGTH_SHORT).show();

                    }
                });
                builder.setNegativeButton("取消", new OnClickListener() {  //取消按钮

                    @Override
                    public void onClick(DialogInterface arg0, int arg1) {
                        Toast.makeText(getActivity(), "入教失败!",Toast.LENGTH_SHORT).show();

                    }
                });
                AlertDialog b=builder.create();
                b.show();  //必须show一下才能看到对话框,跟Toast一样的道理
            }
        });

        Button b2=(Button) getActivity().findViewById(R.id.b1);
        b2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                AlertDialog.Builder builder=new Builder(getActivity());
                builder.setIcon(android.R.drawable.ic_dialog_info);
                builder.setTitle("请选择性别");
                final String []items=new String[]{"男","女"};
                builder.setSingleChoiceItems(items, -1, new OnClickListener() {
                    //which指的是用户选择的条目的下标
                    //dialog:触发这个方法的对话框
                    @Override
                    public void onClick(DialogInterface dialog, int which) {
                        Toast.makeText(getActivity(), "您选择的是:"+items[which], Toast.LENGTH_SHORT).show();
                        dialog.dismiss();//当用户选择了一个值后,对话框消失

                    }
                });
                builder.show();//这样也是一个show对话框的方式,上面那个也可以
            }
        });

        Button b3=(Button) getActivity().findViewById(R.id.b1);
        b3.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                AlertDialog.Builder builder=new Builder(getActivity());
                builder.setIcon(android.R.drawable.ic_dialog_info);
                builder.setTitle("请选择你觉得萌的人");
                final String []itemsId=new String[]{"和真","阿库娅","惠惠","达克尼斯"};
                final boolean []checkedItems=new boolean[]{true,true,false,false};//这里的true是默认第几个人已经被选中
                builder.setMultiChoiceItems(itemsId, checkedItems,new OnMultiChoiceClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int which, boolean ischeck) {
                        checkedItems[which]=ischeck;
                    }
                });
                //设置一个确定按钮
                builder.setPositiveButton("确定", new OnClickListener() {

                    @Override
                    public void onClick(DialogInterface dialog, int whick) {
                        String text="";
                        for(int i=0;i<itemsId.length;i++)
                        {
                            text+=checkedItems[i]?itemsId[i]+",":"";
                        }

                        Toast.makeText(getActivity(), text, Toast.LENGTH_SHORT).show();
                        dialog.dismiss();
                    }
                });
                builder.show();
            }
        });


同样,页面跳转在 方法中写:

//根据ID跳转 微信页面
        LinearLayout l2=(LinearLayout) getActivity().findViewById(R.id.weixin);
        l2.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v1) {
                Intent intent = new Intent(getActivity(), WeiXinActivity.class);
                startActivity(intent);
            }
        });

最后,感谢小马 的文章!



 

作者:ssh159 发表于2017/3/22 15:47:17 原文链接
阅读:59 评论:0 查看评论

【一图流】_03_一张图看懂 Android系统_Binder原理 及其 调用流程:

$
0
0

【一图流】_03_一张图看懂 Android系统_Binder原理 及其 调用流程:

    

     

作者:MLQ8087 发表于2017/3/22 18:05:46 原文链接
阅读:42 评论:0 查看评论

将C++对象嵌入到带有上下文属性的QML中

$
0
0

简述

当加载一个 QML 对象到一个 C++ 应用程序中时,直接嵌入可以从 QML 代码中使用的一些 C++ 数据是非常有用的。例如,对嵌入的对象调用一个 C++ 函数,或者使用一个 C++ 对象实例作为 QML 视图的数据模型。

通过 QQmlContext 类可以将 C++ 数据注入到 QML 对象中。该类将数据暴露给一个 QML 对象的上下文,以便可以直接从 QML 代码中引用数据。

版权所有:一去丶二三里,转载请注明出处:http://blog.csdn.net/liang19890820

QQmlContext

QQmlContext 类定义了 QML 引擎内的上下文,上下文允许将数据暴露给由 QML 引擎实例化的 QML 组件。

每个 QQmlContext 包含一组属性,允许以名称将数据显式地绑定到上下文。通过调用 QQmlContext::setContextProperty() 来定义和更新上下文属性。

相应的接口:

void QQmlContext::setContextProperty(const QString &name, const QVariant &value)
简单的上下文属性,对应的值为 QVariant 类型。

void QQmlContext::setContextProperty(const QString &name, QObject *value)
相对来说稍微复杂一些,QObject * 对象类型。

可以看出,上下文属性可以保存 QVariant 或 QObject * 类型的值。也就是说,既可以使用这种方式注入简单的 QVariant 类型;也可以注入自定义 C++ 对象,并且这些对象可以在 QML 中被直接修改和读取。

设置简单的上下文属性

这里写图片描述

例如,下面的 QML 文本项,引用了一个当前范围中不存在的 currentDateTime 值:

// main.qml
import QtQuick 2.3

Rectangle {
    width: 200; height: 100

    Text {
        anchors.centerIn: parent
        text: currentDateTime
    }
}

这个 currentDateTime 值可以由加载 QML 组件的 C++ 应用程序直接设置,使用 QQmlContext::setContextProperty():

// main.cpp
#include <QGuiApplication>
#include <QQuickView>
#include <QQmlContext>
#include <QDateTime>

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQuickView view;
    // 设置上下文属性
    view.rootContext()->setContextProperty("currentDateTime", QDateTime::currentDateTime());
    view.setSource(QUrl("qrc:/main.qml"));
    view.setIcon(QIcon(":/logo.png"));
    view.setTitle(QStringLiteral("将C++对象嵌入到带有上下文属性的QML中"));
    view.show();

    return app.exec();
}

注意: 由于在特定上下文中评估 QML 中的所有求值表达式,因此如果上下文被修改,则该上下文中的所有绑定都将被重新求值。因此,应该在应用程序初始化之外谨慎使用上下文属性,因为这可能会降低应用程序性能。

将对象设置为上下文属性

修改上述示例,嵌入一个 QObject 实例而非一个 QDateTime 值,并且从 QML 调用对象实例上的一个函数:

// application_data.h
#ifndef APPLICATION_DATA_H
#define APPLICATION_DATA_H

#include <QObject>
#include <QDateTime>

class ApplicationData : public QObject
{
    Q_OBJECT

public:
    Q_INVOKABLE QDateTime getCurrentDateTime() const {
        return QDateTime::currentDateTime();
    }
};

#endif // APPLICATION_DATA_H

注意: 并非所有的 C++ 函数都可以从 QML 调用,除了 Q_INVOKABLE() 标记的 public 函数之外,还可以是 public 槽函数。

和前面的类似,只不过这里传递的是 QObject * 对象类型:

// main.cpp
#include <QGuiApplication>
#include <QQuickView>
#include <QQmlContext>
#include "application_data.h"

int main(int argc, char *argv[])
{
    QGuiApplication app(argc, argv);

    QQuickView view;
    // 设置上下文属性
    ApplicationData data;
    view.rootContext()->setContextProperty("applicationData", &data);
    view.setSource(QUrl("qrc:/main.qml"));
    view.setIcon(QIcon(":/logo.png"));
    view.setTitle(QStringLiteral("将C++对象嵌入到带有上下文属性的QML中"));
    view.show();

    return app.exec();
}

然后,就可以在 QML 中使用 applicationData:

// main.qml
import QtQuick 2.3

Rectangle {
    width: 200; height: 100

    Text {
        anchors.centerIn: parent
        text: applicationData.getCurrentDateTime()
    }
}

注意: 从 C++ 到 QML 返回的 date/time 可以通过 Qt.formatDateTime() 和相关函数进行格式化。

从上下文属性中接收信号

如果 QML 需要从上下文属性中接收信号,可以使用 Connections 类型进行连接。

上面的日期时间是固定不变的,我们来为其添加一个定时器(每秒更新),让时间跑起来:

这里写图片描述

例如,ApplicationData 具有一个名为 dataChanged() 的信号,在定时器超时后发射:

// application_data.h
#ifndef APPLICATION_DATA_H
#define APPLICATION_DATA_H

#include <QObject>
#include <QDateTime>
#include <QTimer>

class ApplicationData : public QObject
{
    Q_OBJECT

public:
    ApplicationData() {
        // 定时器
        QTimer *pTimer = new QTimer(this);
        pTimer->setInterval(1000);
        connect(pTimer, &QTimer::timeout, this, &ApplicationData::onTimeout);
        pTimer->start();
    }

    Q_INVOKABLE QDateTime getCurrentDateTime() const {
        return m_dateTime;
    }

signals:
    void dataChanged();

private slots:
    void onTimeout() {
        m_dateTime = QDateTime::currentDateTime();
        emit dataChanged();  // 超时后发射信号
    }

private:
    QDateTime m_dateTime;
};

#endif // APPLICATION_DATA_H

在 QML 中,定义一个 Connections 对象,将该信号连接至一个名为 onDataChanged 的处理程序:

// main.qml
import QtQuick 2.3

Rectangle {
    width: 200; height: 100

    Text {
        id: timer
        anchors.centerIn: parent
        text: "Timer"
    }

    Connections {  // 连接信号
        target: applicationData
        onDataChanged: {
            timer.text = applicationData.getCurrentDateTime()
            console.log("The application data changed!")
        }
    }
}

其中 target 为目标对象,也就是我们的上下文属性 applicationData。

作者:u011012932 发表于2017/3/22 19:09:28 原文链接
阅读:70 评论:0 查看评论

Visual Studio-新建一个cs文件时自动添加region块和自动添加头注释

$
0
0

在visual stuido中新建一个cs文件时,如果您想自动实现添加文件头部,自动添加#region #endregion这种模块机制,实现如下图所示的效果,您知道如何修改吗?


这里写图片描述


操作步骤

1 找到VS2010路径(高版本的在相似路径下, 自行解决)
X:\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\IDE\ItemTemplatesCache\CSharp\Code\2052\Class.zip

2 打开后可以看到两个文件:Class.cs和Class.vstemplate,我们只处理Class.cs
用记事本或其他文本编辑程序将其内容修改如下:

/* ==============================================================================
 * 功能描述:$safeitemrootname$  
 * 创 建 者:$username$
 * 创建日期:$time$
 * ==============================================================================*/
using System;
using System.Collections.Generic;
$if$ ($targetframeworkversion$ >= 3.5)using System.Linq;
$endif$using System.Text;

namespace $rootnamespace$
{
    /// <summary>
    /// $safeitemrootname$
    /// </summary>
    public class $safeitemrootname$
    {
        #region 构造函数

        public $safeitemrootname$()
        {
        }

        #endregion

        #region 属性字段

        #endregion

        #region 私有方法

        #endregion

        #region 公有方法

        #endregion

    }
}

扩展

按个人习惯修改,这里var 都是系统的变量,模板参数是要区分大小写的,大家需要注意。系统提供的可用的参数如下:
clrversion 公共语言运行库(CLR) 的当前版本。
projectname 用户在新建项目对话框中提供的名称。
time 以 DD/MM/YYYY 00:00:00 格式表示的当前时间。
userdomain 当前的用户域。
username 当前的用户名。
year 以 YYYY 格式表示的当前年份。

safeitemname 用户在“添加新项”对话框中提供的名称,名称中移除了所有不安全的字符和空格。
safeprojectname 用户在“新建项目”对话框中提供的名称,名称中移除了所有不安全的字符和空格。
registeredorganization HKLM/Software/Microsoft/Windows NT/CurrentVersion/RegisteredOrganization 中的注册表项值。
rootnamespace 当前项目的根命名空间。此参数用于替换正向项目中添加的项中的命名空间。
GUID[1-10] 用于替换项目文件中的项目 GUID 的 GUID。最多可以指定 10 个唯一的 GUID(例如,guid1))。
itemname 用户在添加新项对话框中提供的名称
machinename 当前的计算机名称(例如,Computer01)。
projectname 用户在新建项目对话框中提供的名称。

作者:daigualu 发表于2017/3/22 14:58:45 原文链接
阅读:15 评论:0 查看评论

Visual Studio-添加定制的方法注释

$
0
0

在visual studio 中为方法添加特定格式的方法注释,而不是默认的格式,那么应该怎么实现呢?需要借助于宏和键盘组合键实现。

操作步骤:
1 选择Macro资源管理器


这里写图片描述


2 新建一个宏,MemberAddAuthorDate,宏的创建用VB语言,和你用的C#语言没有关系,一般注释头的格式用VB实现的源码网上很多。


这里写图片描述

新建的宏如下图所示,编辑完成后保存。

这里写图片描述


3 选择工具-选项,


这里写图片描述


4 选中键盘,界面如下图所示,按照界面中的提示选择,这时候刚才编辑好的MemberAddAuthorDate中的方法MemberFileHeader出现在我们的视野中,然后选择未被占用的组合快捷键。
这里写图片描述

5测试,在一个cs文件中,按下”Alt和[“后,自动弹出刚才的模板行,


这里写图片描述

6完成!

作者:daigualu 发表于2017/3/22 16:12:09 原文链接
阅读:20 评论:0 查看评论

Android Things 专题6 完整的栗子:运用TensorFlow解析图像

$
0
0

文| 谷歌开发技术专家 (GDE) 王玉成 (York Wang)

前面絮叨了这么多,好像还没有一个总体的概念,我们如何写一个完整的代码呢?

现在深度学习很火,那我们就在Android Things中,利用摄像头抓拍图片,让 TensorFlow 去识别图像,最后用扬声器告诉我们结果。

是不是很酷?说基本的功能就说了这么长一串,那垒代码得垒多久啊?

项目结构

我们就从 Android Studio 的环始境开始说起吧。

启动 Android Studio 之后,务必把 SDK Tools 的版本升级到 24 及以上。然后再把 SDK 升级到 Android 7.0 及以上。让 Android Studio 自己完成相关组件的更新,导入项目,项目的结构如下:
这里写图片描述
代码中的 imageclassifier 是用于跟 TensorFlow 做交互的,还有摄头,图片处理的相关 handler。

我们再来看看外部的引用库:
这里写图片描述

包括了 Android Things 和 TensorFlow 的相关库,当然,Android 的 API 的版本是24。gradle 的依赖和 Manifest 中的 filer 是和之前搭建开发环境的讲解一致的。
引用的 TensorFlow 的库是 aar 打包的 Tensorflow-Android-Inference-alpha-debug.aar,这就意味着,我们不需要 NDK 环境就能够编译整个项目了。

主要是留意 dependencies 这一项,包括了 TensorFlow 的库和 Android thing 的库:
这里写图片描述

再申请了摄头相关的权限。补充一下,Android Things 是不支持动态权限的申请的。
这里写图片描述

硬件连接

接下来便是硬件如何连接了。

这里写图片描述

硬件清单如下:
Android Things 兼容的开发板,比如 Raspberry Pi 3
Android Things 兼容的摄像头,比如 Raspberry Pi 3 摄头模块
元器件:
1 个按钮,见面包板
2 个电阻,这块儿必须要说明一下:由于图片是接的 5V 的电压,一般来说 GPIO 和 led 的承压能力是 3V,有些 GPIO 是兼容 5V 的,所以中间需要串联 100~200 欧的电阻。当然,为了保险,建议用 3.3V 的电压。
1 个LED灯
1 个面包板
杜邦线若干
可选:扬声器或者耳机
可选:HDMI输出

连完了硬件,我们这时候就要理解操作流程了。

操作流程

按照前面讲解的内容,用 Andorid Studio,连接 ADB,配置好开发板的 Wi-Fi,然后把应用加载到开发板上。

操作流程如下:
重启设备,运行程序,直到 LED 灯开始闪烁;
把镜头对准猫啊,狗啊,或者一些家具;
按下开关,开始拍摄图片;
在 Raspberry Pi 3 中,一般在 1s 之内,可以完成图片抓拍,经 Tensorflow 处理,然后再通过 TTS 放出声音。在运行的过程中 LED 灯是熄灭的;
Logcat 中会打印出最终的结果,如果是有显示设备连接的话,图片和结果都会显示出来;
如果有扬声器或者是耳机的话,会把结果语音播报出来。

由于代码的结构特别简单,注意一下几段关健的操作即可。想必图形、摄头的操作在Android 的编程中大家都会了,所以不做讲解了。

代码流程

主要是看 LED 的初始化操作:
这里写图片描述

有必要说一下,ImageClassifierActivity.java 是应用唯一的 Activity 的入口。在 Manifest 中已经有定义,它初始化了 LED, Camera, TensorfFlow 等组件。其中,我们用到的 Button 是 BCM32 这个管脚,用到的 LED 是 BCM6 管脚,相关的初始化在这个 Activity 中已经完成。
这里写图片描述

这部分代码是捕捉按键按下的代码。当按下按键时,摄头开始捕捉数据。

这里写图片描述
把摄像头拍摄的数据转成 Bitmap 文件之后,我们会调用 TensorFlow 来处理图像。

这里写图片描述
这个函数调用了 TensorFlow 进行处理,最后把结果输出到 logcat 中。如果代码中调用了 tts 引擎,那么则把结果转成语音读出来。看来,最重要的就是 TensorFlowClassifie 类的 recognizeImage() 这个接口了。我们继续往下看。
这里写图片描述

这是最后的一步,调用 TensorFlow 进行图像识别:
把 RGB 图像转成 TensorFlow 能够识别的数据;
把数据拷到 TensorFlow 中;
识别出图像,给出结果。

调用 TensorFlow 的过程还是挺好玩的,也挺方便。那么,为啥 TensorFlow 一下子就能够识别出是什么图片呢?Tensorflow 的官网给出的解答:
www.tensorflow.org/tutorials/image_recognition

有一点需要提示,TensorFlow 的图像识别分类可以用网络提交到服务器识别,也可以离线的数据识别。可以先把 200M 左右的识别数据放在本地,然后提交后识别。现在大概能分出 1000 个类别的图像,哪 1000 个类别呢?项目代码中已经包含了哦。

是不是运用 TensorFlow 来处理物联网的数据会特别简单,不光是 TensorFlow, Firebase 也可以用到 Android Things 中来。这功能,强大的没话说了!

今天提到的这个项目,来源于 Google 在 GitHub 上维护的项目,项目的地址是
github.com/androidthings/sample-tensorflow-imageclassifier

当然,GitHub 上还有很多 Android Things 的代码可以参考。

是不是迫不急待的自己写一个应用呢?实际上,这个项目稍加改动便能有新的玩法。例如加上一个红外感应器,一旦有生物在附近就马上拍图片,并且识别。

大开你的脑洞吧

后记

这一篇文章是这个专题的最后一篇了。写完整个专题,发现 Android Things 带给开发者太多太多的便利,如何烧写文件?如何运用 SDK?甚至如何用 Google 的其它服务做物联网相关的数据处理?都有太多太多的现成的方案供我们选择,感叹使用 Android Things 进行物联网应用开发实在太方便了!

您如果有任何涉及到 Android Things 方面的想法,都欢迎大家在下方留言,我们会把好的建议转交给 Android Things 的产品部门。也许在某一天,你的建议就是 Andorid Things 的一部分。

作者:wfing 发表于2017/3/22 20:53:32 原文链接
阅读:403 评论:0 查看评论

iOS透明导航栏的平滑过渡(进阶版)

$
0
0

如我在传送门:iOS导航栏切换界面时隐藏和显示中所说,现在很多App的个人中心模块都是不保留导航栏的,会直接使导航栏透明,比如做的很好的QQ个人信息界面:

为什么说QQ做的很好呢?既然有透明的导航栏也有不透明的导航栏,那一定会在界面切换之间存在一个过渡的过程,而这个过程,QQ做的特别好,在从透明导航栏界面返回到不透明导航栏界面时,导航栏的透明度是一个渐进的过渡效果,甚至会有一种毛玻璃的效果,感兴趣的可以打开手机QQ到个人界面看一看,效果很赞。

而很多App的做法其实比较粗糙,类似于我在传送门:iOS导航栏切换界面时隐藏和显示中的做法,需要导航栏透明时,直接将导航栏隐藏起来。直接隐藏起来的意思是,整个导航栏就用不了了,也就是说,标题、返回按钮等都需要自己去做,这是一个比较麻烦的地方,此外,在有无导航栏的界面间切换时,过程是比较生硬的,导航栏不是渐变出现的。如果说这些都可以接受,那最大的一个问题,也是我在那篇文章里提到的,如果正好处于用UITabbarConatroller切换界面,那么导航栏会有一个往上缩回的快速动画,这其实就很不美观了,当然我们可以通过将隐藏导航栏的动画去掉来达到对Tabbar切换友好的效果:

[self.navigationController setNavigationBarHidden:NO animated:NO];

但是这样一来你在UINavigationController体系下切换界面时由于没有了动画,这边的效果又会变得很差。这两个矛盾没有想到可以调和的手段,除非在业务上就不显示Tabbar了,但始终不是长久之计。

同时,我们虽然说QQ做的很好,但也依然有一些不足,多把玩一下导航栏过渡的过程就会发现,如果准备从透明导航栏返回时又决定不反回了,还是停留在导航栏透明的界面,这时候导航栏虽然会回到透明,但会有一个导航栏闪现一下的小瑕疵。

现在问题已经讲完了,基于这些问题,我们自己来尝试实现一种更好的平滑过渡效果,不自定义导航栏,直接利用系统原生的导航栏,使用Category和Runtime的技术,达到这个效果:

代码可以在示例工程下载(觉得有帮助的小伙伴请不吝加Star~):https://github.com/Cloudox/SmoothNavDemo

实现过程

其实我们的目的总结起来有三个:

1、不去自定义导航栏,就用系统原生的,标题、返回按钮啥的都方便加,这也就是说不隐藏导航栏,而是要单独让导航栏背景透明;
2、在导航栏透明与否的界面间切换时透明度有渐变效果;
3、在UINavigationController体系和UITabarController体系下切换界面都很完美。

对于第三个目的,我们之前在UITabarController下切换时会有导航栏隐藏的小动画,但如果我们满足了第一个目的,那就不存在隐藏导航栏了,所以第三个问题也就不会存在了。

我们先来看第一个目的。

设置导航栏背景透明度

导航栏上应该是有很多view的,我们要做的是只让背景透明,而保留标题、返回按钮。iOS没有直接给我们提供对于导航栏背景view的访问途径,那么我们只能自己来找了。

首先我们遍历打印出UINavigationBar的所有子视图,是所有,包括子视图的一层层子视图,来看看到底导航栏都包含了哪些东西:

上面这张图就是导航栏UINavigationBar所包含的所有子view了,序号和缩进表示了其层级归属关系,打印的方法可以看这篇文章:传送门:iOS遍历打印所有子视图

从这些子view的类名能够大概猜出他们都是导航栏上的什么,让我们大胆猜测一下,_UIBarBackground 是背景视图,下属的 UIImageView 是背景图片,_UINavigationBarBackIndicatorView 是返回箭头,UINavigationItemView 是添加的一些导航栏按钮,包括返回按钮,因为我没有给导航栏添加任何其他按钮,所以这里一定是返回按钮,下属的 UILabel 就是 “返回” 两个字了。

根据上面得到的信息,我们就尝试将_UIBarBackground、UIImageView、UIVisualEffectView的 alpha 值设为 1 或者 0 来改变导航栏背景的透明度。

我们可以给 UINavigationController 创建一个类别,来给这个类添加一个方法,用于设置导航栏的透明度:

// UIViewController+Cloudox.m

- (void)setNeedsNavigationBackground:(CGFloat)alpha {
    // 导航栏背景透明度设置
    UIView *barBackgroundView = [[self.navigationBar subviews] objectAtIndex:0];// _UIBarBackground
    UIImageView *backgroundImageView = [[barBackgroundView subviews] objectAtIndex:0];// UIImageView
    if (self.navigationBar.isTranslucent) {
        if (backgroundImageView != nil && backgroundImageView.image != nil) {
            barBackgroundView.alpha = alpha;
        } else {
            UIView *backgroundEffectView = [[barBackgroundView subviews] objectAtIndex:1];// UIVisualEffectView
            if (backgroundEffectView != nil) {
                backgroundEffectView.alpha = alpha;
            }
        }
    } else {
        barBackgroundView.alpha = alpha;
    }
}

到目前为止,我们会得到什么效果呢?看一下:

我们成功的将导航栏背景设为透明了!但是那条细线是什么情况?!有它在岂不是前功尽弃了,再用上面的方法已经不管用了,这条线不在我们找出来的子view之中,通过查资料,要隐藏这跟细线的方法很多,但是要跟我们对导航栏背景的设置不冲突,又要能到只在将导航栏背景设为透明时才隐藏,下面这种方法是比较好的方法:

// 对导航栏下面那条线做处理
self.navigationBar.clipsToBounds = alpha == 0.0;

当我们对导航栏的透明度设为 0 时,就会隐藏细线,否则不隐藏,这样当切换到其他界面时,细线就又会出来了。

现在导航栏的透明就比较完美了:

对于这种将导航栏背景直接设为透明的情况,在 Tabbar 切换界面时,也不会出现导航栏收起的小动画:

为UIViewController添加导航栏透明度属性

为了方便,我们创建一个 UIViewController 的Category,为其增加一个属性——导航栏透明度(navBarBgAlpha),Category一般是不可以添加属性的,但我们可以通过Runtime的关联对象来做到,具体做法参看我的这篇文章:传送门:iOS中OC给Category添加属性,由于只能关联对象,所以我们无法直接添加 CGFloat 类型的属性,我们就直接添加 NSString 类型的属性就好了,用的时候再用 [NSString floatValue] 方法。这样每个 ViewController 都可以管理自己的导航栏透明度,在这个新增属性的setter方法中,我们调用前面在在 UINavigationController 的Category 中添加的设置导航栏透明度的方法,这样就打通了。

UIViewController的设置方法如下:

// UIViewController+Cloudox.h

@interface UIViewController (Cloudox)
@property (copy, nonatomic) NSString *navBarBgAlpha;
@end

// UIViewController+Cloudox.m
#import "UIViewController+Cloudox.h"
// 导入runtime才可以使用关联对象
#import <objc/runtime.h>
// 导入我们的Category才可以调用我们添加的方法
#import "UINavigationController+Cloudox.h"

@implementation UIViewController (Cloudox)

//定义常量 必须是C语言字符串
static char *CloudoxKey = "CloudoxKey";

-(void)setNavBarBgAlpha:(NSString *)navBarBgAlpha{
    /*
     OBJC_ASSOCIATION_ASSIGN;            //assign策略
     OBJC_ASSOCIATION_COPY_NONATOMIC;    //copy策略
     OBJC_ASSOCIATION_RETAIN_NONATOMIC;  // retain策略

     OBJC_ASSOCIATION_RETAIN;
     OBJC_ASSOCIATION_COPY;
     */
    /*
     * id object 给哪个对象的属性赋值
     const void *key 属性对应的key
     id value  设置属性值为value
     objc_AssociationPolicy policy  使用的策略,是一个枚举值,和copy,retain,assign是一样的,手机开发一般都选择NONATOMIC
     objc_setAssociatedObject(id object, const void *key, id value, objc_AssociationPolicy policy);
     */

    objc_setAssociatedObject(self, CloudoxKey, navBarBgAlpha, OBJC_ASSOCIATION_COPY_NONATOMIC);

    // 设置导航栏透明度(利用Category自己添加的方法)
    [self.navigationController setNeedsNavigationBackground:[navBarBgAlpha floatValue]];
}

-(NSString *)navBarBgAlpha{
    return objc_getAssociatedObject(self, CloudoxKey);
}

@end

使用时我们只需要:

// 让导航栏透明
self.navBarBgAlpha = @"0.0";

// 让导航栏不透明
self.navBarBgAlpha = @"1.0";

实现切换界面时渐变过渡

现在实现了比较好的透明导航栏效果,但在透明的导航栏与不透明的导航栏界面直接切换时,导航栏的透明度是直接跳变的:

而我们想要的是像QQ一样从完全透明到不透明之间有一个随着滑动手势变化的透明度渐变效果,这样是最好的转场效果了。

我们需要的随着手势滑动返回界面的进度,来实时变化导航栏的透明度,比如滑动到了界面一半的时候,导航栏透明度应该是 0.5。对于这个需求,首先想到的是,我们要监控这个滑动事件的滑动进度。

正好,UINavigationController 有一个方法 _updateInteractiveTransition: 就是监控这个手势及其进度的,那么我们就可以使用 Runtime 黑魔法——方法交换来实现我们的需求。

怎么交换呢?通过要交换的方法和我们定义的方法的名称,获取到对应的方法实现,然后用 method_exchangeImplementations 方法交换两个方法的实现:

+ (void)initialize {
    if (self == [UINavigationController self]) {
        // 交换方法
        SEL originalSelector = NSSelectorFromString(@"_updateInteractiveTransition:");
        SEL swizzledSelector = NSSelectorFromString(@"et__updateInteractiveTransition:");
        Method originalMethod = class_getInstanceMethod([self class], originalSelector);
        Method swizzledMethod = class_getInstanceMethod([self class], swizzledSelector);
        method_exchangeImplementations(originalMethod, swizzledMethod);
    }
}

这一步我们在 initialize 方法中去做,这样一调用时就会生效了,关于 initialize 可以查看这篇文章:传送门:OC中load方法和initialize方法的异同

我们自己创建一个用于交换的方法,这个方法中,除了调用原方法外(注意由于方法名称对应的实现已经交换了,这里我们目的是调用原实现,但是使用的名称确实本方法自己的名称),还添加一个处理,_updateInteractiveTransition: 有一个参数就是界面滑动过程的百分比,那么我们获取上一个界面的导航栏透明度、下一个界面的导航栏透明度、以及滑动的进度,通过很简单的数学计算就可以得出当前进度应该对应的透明度是多少了,这里也可以看出我们给 ViewController 添加一个导航栏透明度属性是多么有意义,这里就可以直接调用了,当然,要记得导入我们的Category:

// 交换的方法,监控滑动手势
- (void)et__updateInteractiveTransition:(CGFloat)percentComplete {
    [self et__updateInteractiveTransition:(percentComplete)];
    UIViewController *topVC = self.topViewController;
    if (topVC != nil) {
        id<UIViewControllerTransitionCoordinator> coor = topVC.transitionCoordinator;
        if (coor != nil) {
            // 随着滑动的过程设置导航栏透明度渐变
            CGFloat fromAlpha = [[coor viewControllerForKey:UITransitionContextFromViewControllerKey].navBarBgAlpha floatValue];
            CGFloat toAlpha = [[coor viewControllerForKey:UITransitionContextToViewControllerKey].navBarBgAlpha floatValue];
            CGFloat nowAlpha = fromAlpha + (toAlpha - fromAlpha) * percentComplete;
            NSLog(@"from:%f, to:%f, now:%f",fromAlpha, toAlpha, nowAlpha);
            [self setNeedsNavigationBackground:nowAlpha];
        }
    }
}

我们打印了透明度渐变的过程,可以看一下:

是按照预想地在随着滑动界面的进度渐变透明度的,实际的效果也是这样的:

一些小瑕疵的修补

就目前的效果,其实还是不错的,不过也有一些小瑕疵,比如滑动到一半松手时会有一个小跳变,对于这一点,我们可以在 UINavigationController 的 Delegate 中添加一个处理,监控松手后时自动完成返回还是取消返回操作,同时使用 UIView 动画(关于 UIView 动画可以看我的这篇文章:传送门:iOS基础动画教程),在自动操作的那个时间内将透明度变为对应界面的导航栏透明度,让其变化的不那么跳跃:

#pragma mark - UINavigationController Delegate
- (void)navigationController:(UINavigationController *)navigationController willShowViewController:(UIViewController *)viewController animated:(BOOL)animated {
    UIViewController *topVC = self.topViewController;
    if (topVC != nil) {
        id<UIViewControllerTransitionCoordinator> coor = topVC.transitionCoordinator;
        if (coor != nil) {
            [coor notifyWhenInteractionChangesUsingBlock:^(id<UIViewControllerTransitionCoordinatorContext> context){
                [self dealInteractionChanges:context];
            }];
        }
    }
}

- (void)dealInteractionChanges:(id<UIViewControllerTransitionCoordinatorContext>)context {
    if ([context isCancelled]) {// 自动取消了返回手势
        NSTimeInterval cancelDuration = [context transitionDuration] * (double)[context percentComplete];
        [UIView animateWithDuration:cancelDuration animations:^{
            CGFloat nowAlpha = [[context viewControllerForKey:UITransitionContextFromViewControllerKey].navBarBgAlpha floatValue];
            NSLog(@"自动取消返回到alpha:%f", nowAlpha);
            [self setNeedsNavigationBackground:nowAlpha];
        }];
    } else {// 自动完成了返回手势
        NSTimeInterval finishDuration = [context transitionDuration] * (double)(1 - [context percentComplete]);
        [UIView animateWithDuration:finishDuration animations:^{
            CGFloat nowAlpha = [[context viewControllerForKey:
                                 UITransitionContextToViewControllerKey].navBarBgAlpha floatValue];
            NSLog(@"自动完成返回到alpha:%f", nowAlpha);
            [self setNeedsNavigationBackground:nowAlpha];
        }];
    }
}

对于直接点击返回按钮以及 push 到下一个界面的操作,也可以增加一次处理:

#pragma mark - UINavigationBar Delegate
- (void)navigationBar:(UINavigationBar *)navigationBar didPopItem:(UINavigationItem *)item {
    if (self.viewControllers.count >= navigationBar.items.count) {// 点击返回按钮
        UIViewController *popToVC = self.viewControllers[self.viewControllers.count - 1];
        [self setNeedsNavigationBackground:[popToVC.navBarBgAlpha floatValue]];
    }
}

- (void)navigationBar:(UINavigationBar *)navigationBar didPushItem:(UINavigationItem *)item {
    // push到一个新界面
    [self setNeedsNavigationBackground:[self.topViewController.navBarBgAlpha floatValue]];
}

不过意义不是特别大。

以上这些处理基本都在 Category 里写代码,一次搞定,真正在自己的 ViewController 需要做的只是一句:

- (void)viewWillAppear:(BOOL)animated {
    [super viewWillAppear:animated];

    self.navBarBgAlpha = @"0.0";
}

很简单吧~更多效果有兴趣的可以自己继续修修补补,这个过程也是很有意思的。

再次宣传,代码可以在示例工程下载(觉得有帮助的小伙伴请不吝加Star~):https://github.com/Cloudox/SmoothNavDemo


版权所有:http://blog.csdn.net/cloudox_
参考(swift):http://www.jianshu.com/p/454b06590cf1

作者:Cloudox_ 发表于2017/3/22 22:18:41 原文链接
阅读:239 评论:0 查看评论

【Unity优化】如何实现Unity编辑器中的协程

$
0
0

Unity编辑器中何时需要协程

当我们定制Unity编辑器的时候,往往需要启动额外的协程或者线程进行处理。比如当执行一些界面更新的时候,需要大量计算,如果用户在不断修正一个参数,比如从1变化到2,这种变化过程要经历无数中间步骤,调用N多次Update,如果直接在Update中不断刷新,界面很容易直接卡死。所以在一个协程中进行一些优化,只保留用户最后一次参数修正,省去中间步骤,就会好很多。这属于Unity编辑器的内容,也属于优化的内容,还是放在优化中吧。

解决问题思路

Unity官网的questions里面也有很多人在搜索这个问题,不过后来是看到有个人提到了这个方法。问题的关键点就是“EditorApplication.update ”,有个这样的方法,你把要执行的协程传递给它就可以在编辑器下自动执行循环调用。

老外的写法

当然,后来我也找到一个老外的写法,代码贴出来如下:

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

public static class EditorCoroutineRunner
{
    private class EditorCoroutine : IEnumerator
    {
        private Stack<IEnumerator> executionStack;

        public EditorCoroutine(IEnumerator iterator)
        {
            this.executionStack = new Stack<IEnumerator>();
            this.executionStack.Push(iterator);
        }

        public bool MoveNext()
        {
            IEnumerator i = this.executionStack.Peek();

            if (i.MoveNext())
            {
                object result = i.Current;
                if (result != null && result is IEnumerator)
                {
                    this.executionStack.Push((IEnumerator)result);
                }

                return true;
            }
            else
            {
                if (this.executionStack.Count > 1)
                {
                    this.executionStack.Pop();
                    return true;
                }
            }

            return false;
        }

        public void Reset()
        {
            throw new System.NotSupportedException("This Operation Is Not Supported.");
        }

        public object Current
        {
            get { return this.executionStack.Peek().Current; }
        }

        public bool Find(IEnumerator iterator)
        {
            return this.executionStack.Contains(iterator);
        }
    }

    private static List<EditorCoroutine> editorCoroutineList;
    private static List<IEnumerator> buffer;

    public static IEnumerator StartEditorCoroutine(IEnumerator iterator)
    {
        if (editorCoroutineList == null)
        {
            editorCoroutineList = new List<EditorCoroutine>();
        }
        if (buffer == null)
        {
            buffer = new List<IEnumerator>();
        }
        if (editorCoroutineList.Count == 0)
        {
            EditorApplication.update += Update;
        }

        // add iterator to buffer first
        buffer.Add(iterator);

        return iterator;
    }

    private static bool Find(IEnumerator iterator)
    {
        // If this iterator is already added
        // Then ignore it this time
        foreach (EditorCoroutine editorCoroutine in editorCoroutineList)
        {
            if (editorCoroutine.Find(iterator))
            {
                return true;
            }
        }

        return false;
    }

    private static void Update()
    {
        // EditorCoroutine execution may append new iterators to buffer
        // Therefore we should run EditorCoroutine first
        editorCoroutineList.RemoveAll
        (
            coroutine => { return coroutine.MoveNext() == false; }
        );

        // If we have iterators in buffer
        if (buffer.Count > 0)
        {
            foreach (IEnumerator iterator in buffer)
            {
                // If this iterators not exists
                if (!Find(iterator))
                {
                    // Added this as new EditorCoroutine
                    editorCoroutineList.Add(new EditorCoroutine(iterator));
                }
            }

            // Clear buffer
            buffer.Clear();
        }

        // If we have no running EditorCoroutine
        // Stop calling update anymore
        if (editorCoroutineList.Count == 0)
        {
            EditorApplication.update -= Update;
        }
    }
}

用法就是大概在你自己的类的Start方法中稍作修改,再增加一个协程函数,如下:

        void Start()
        {
            rope = gameObject.GetComponent<QuickRope>();
            #if UNITY_EDITOR
            //调用方法
            EditorCoroutineRunner.StartEditorCoroutine(OnThreadLoop());
            #endif
        }
        public IEnumerator OnThreadLoop()
        {
            while(true)
            {
                Debug.Log("Looper");
                yield return null;
            }
        }

当然最好是加上#if UNITY_EDITOR预处理了。这个类基本是满足要求了。如果你把你自己的脚本做了这样的修改之后,它是可以在编辑状态不断执行到Loop的,要注意它需要先执行到Start,也就是说,你可能需要把GameObject做成Prefab,然后把它从场景中删除,再把Prefab拖回场景,才会在编辑状态下触发脚本上的Star方法,从而激发Loop。

我的写法

然而,用久了你就会发现几个问题,一旦Loop开始了,你是无法停止的,哪怕你把GameObject从场景中删掉都无济于事,当然隐藏也没有效果。为了解决这个问题,也把脚本弄得简单点儿,我重写了这个脚本,希望需要的同学可以愉快地使用。

using UnityEngine;
using UnityEditor;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.CompilerServices;

public static class EditorCoroutineLooper
{

    private static Dictionary<IEnumerator,MonoBehaviour> m_loopers = new Dictionary<IEnumerator,MonoBehaviour> ();
    private static bool M_Started = false;
    /// <summary>
    /// 开启Loop
    /// </summary>
    /// <param name="mb">脚本</param>
    /// <param name="iterator">方法</param>
    public static void StartLoop(MonoBehaviour mb, IEnumerator iterator)
    {
        if(mb!=null && iterator != null)
        {
            if(!m_loopers.ContainsKey(iterator))
            {
                m_loopers.Add(iterator,mb);
            }
            else
            {
                m_loopers[iterator]=mb;
            }
        }
        if (!M_Started)
        {
            M_Started = true;
            EditorApplication.update += Update;
        }
    }
    private static List<IEnumerator> M_DropItems=new List<IEnumerator>();
    private static void Update()
    {
        if (m_loopers.Count > 0)
        {

            var allItems = m_loopers.GetEnumerator();
            while(allItems.MoveNext())
            {
                var item = allItems.Current;
                var mb = item.Value;
                //卸载时丢弃Looper
                if(mb == null)
                {
                    M_DropItems.Add(item.Key);
                    continue;
                }
                //隐藏时别执行Loop
                if(!mb.gameObject.activeInHierarchy)
                {
                    continue;
                }
                //执行Loop,执行完毕也丢弃Looper
                IEnumerator ie = item.Key;
                if(!ie.MoveNext())
                {
                    M_DropItems.Add(item.Key);
                }
            }
            //集中处理丢弃的Looper
            for(int i = 0;i < M_DropItems.Count;i++)
            {
                if(M_DropItems[i] != null)
                {
                    m_loopers.Remove(M_DropItems[i]);
                }
            }
            M_DropItems.Clear();
        }


        if (m_loopers.Count == 0)
        {
            EditorApplication.update -= Update;
            M_Started = false;
        }
    }
}
//调用方法原来这个样
            EditorCoroutineRunner.StartEditorCoroutine(OnThreadLoop());
//现在改成这个样
            EditorCoroutineLooper.StartLoop(this,OnThreadLoop());

使用这个脚本的时候,需要传两个参数,一个就是你自己的脚本,另外一个就是协程函数。原理就是代码里面会检测你的脚本状态,当脚本关闭或者卸载的时候,都会停掉Loop调用。老外有时候写代码,也不那么讲究,有没有?

作者:AndrewFan 发表于2017/3/22 22:46:45 原文链接
阅读:428 评论:0 查看评论

Android Sensor详解(6)sensor framework层详解第一篇

$
0
0

sensor app获取

如下表,如需要获取sensor,就需要获取相关的type,这里先给大家罗列一下

那么在ap中我们是如何使用数据的呢?

  1. 导入 android.hardware 包
  2. 实现 SensorEventListener 接口
  3. 使用的步骤:
    获取SensorManager:
    getSystemService(SENSOR_SERVICE)
    获取传感器:
    SensorManager.getDefaultSensor()
    注册传感器:
    SensorManager.registerListener()
    使用完后取消注册:
    SensorManager.unregisterListener();

拿一个简单获取accelerometer sensor举例如下:

public class SensorActivity extends Activity, implements SensorEventListener {
     private final SensorManager mSensorManager;
     private final Sensor mAccelerometer;

     public SensorActivity() {
         mSensorManager = (SensorManager)getSystemService(SENSOR_SERVICE);
         mAccelerometer = mSensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER);
     }

     protected void onResume() {
         super.onResume();
         mSensorManager.registerListener(this, mAccelerometer, SensorManager.SENSOR_DELAY_NORMAL);
     }

     protected void onPause() {
         super.onPause();
         mSensorManager.unregisterListener(this);
     }

     public void onAccuracyChanged(Sensor sensor, int accuracy) {}

     public void onSensorChanged(SensorEvent event) {}
 }

这里我们能很清晰地看到整个sensor监听的过程,那么sensor在这里仅仅是去拿到的sensorManager,然后对这个sensormanger进行操作,那么对于AP来讲的话,只有Manager是可见的。

sensor的manger机制

根据sensor的AP与framerwork,我大致又可以为整个sensor自ap向下分为:

在使用sensor服务前,需要先获取sensorManager,

为何要有这样的架构

由于服务进程和应用运行在不同的进程,应用必须和服务建立进程间通信,这就为应用开发增加了许多繁琐的细节,为此, 引进Manager机制达到了暴露服务接口同时又隐藏共享服务的目的。

这样的做法有助于applications层的开发人员更加方便地进行开发

sensor Framework架构简介

在这里首先我先画出整个sensor的线路,有助于大家理解。

Android对Sensor HAL及其上层作出了定义,但是对于sensorHAL以下未作出定义,因而在kernel层却可以更加的随意,当然我在这里仍然是使用event dev的方式,通过input子系统向上发送数据(后面我又想开个专栏专门讲整个linux的子系统机制)。大家可以参考Android Sensor详解(4)driver的攻防战一文,看看我是如何写驱动的。

sensor的数据流向

在了解了sensor的大体流程后,那么我们可以联系ap层看看sensor又是如何去获取数据的。
于是画图如下:

当一个ap注册了传感器监听后,获取相关type的systemSensorManger,通过注册监听sensoreventqueue的数据变化,而这些变化又是由sensorService从sensor的hal层拿到的,而hal层通过input子系统,从kernel拿到了数据。
可以看到以下为sensor在hal层的event格式

typedef struct sensors_event_t {
    int32_t version;
    int32_t sensor;            //标识符
    int32_t type;                //传感器类型
    int32_t reserved0;
    int64_t timestamp;        //时间戳
    union {
        float           data[16];
        sensors_vec_t   acceleration;  //加速度
        sensors_vec_t   magnetic;       //磁矢量
        sensors_vec_t   orientation;     //方向
        sensors_vec_t   gyro;               //陀螺仪
        float           temperature;           //温度
        float           distance;                 //距离
        float           light;                        //光照
        float           pressure;                 //压力
        float           relative_humidity;    //相对湿度
    };
    uint32_t        reserved1[4];
} sensors_event_t;

当然随着科技发展,现在的ic越来越强大,出现了light sensor/ p sensor等合集的IC,那么此时,lightsensor可能需要的不光光是lux了,在这里先卖个关子,后面在详解HAL层的时候再详细解释一下如何使用这个结构体

以下是各种类

  • SystemSensorManager:传感器框架中的java层Manager的实现者, 继承了java类 SensorManager
  • SensorManager: 特指native代码中的SensorManager类, 其提供了从SensorService获取传感器列表,建立连接通道的功能
  • SensorService:传感器框架中的 Service的实现者
  • SensorDevice:封装了Sensor HAL,并提供相应接口
  • SensorFusion:虚拟传感器的实现,根据所依赖的硬件传感器的值, 计算出虚拟传感器的值
  • SensorEventQueue,SensorEventConnnection:Service和Manager之间的连接而者之间具有层级关系

sensor框架初始化

大致上sensor的框架可以分为3个阶段
* sensorService实例化
* SensorManager初始化
* 建立Service与Manager之间的连接

sensorService实例化

  1. andorid启动到Init调用init.rc文件以service方式启动Zygote
  2. Zygoto会将启动android中所有应用程序及运行系统关键服务的system进程,它会创建一下虚拟机实力孵化新的进程,这里主要是启动了systemService
  3. SystemService用于管理系统的关键服务,它的main 方法中调用了init1(), 这是一个native 方法, 在其实现中, 间接调用了SensorService::instantiate()方法,导致了SensorService的实例化
  4. SensorService实现具体的方法如下图

SensorService启动后,通过defaultServiceManager::addService() 将自己添加到了系统服务管理中,然后等来自SensorManager的连接。

后续的SensorManager就可以使用defaultServiceManager:getService() 来获取 SensorService的实例来建立连接。
这就与前面讲到的ap串起来了。

SensorManager初始化

当在Ap中获取SensorManager的时候, 就会导致初始化流程进入第二个阶段。

setp1 获取SensorManager实例

初始化SensorManager时, java层的Manager(SystemSensorManager)在每个Context的实例注册时实例化一次,native层的Manager(SensorManager),在整个系统中只有唯一个一个实例

你通过同一个Context的实例只会获取到同一个SystemSensorService的实例

调用native_class_init

native_class_init 在native层实现,它仅仅只是将java层中Sensor 类的成员变量 的ID保存起来, 这样就可以在native代码中修改java中对象的值了。

保存的ID 后面将用于初始化java层的Sensor列表
在JNI机制中,native层无论是访问java层的变量还是方法,都需要先获取他们的ID值,然后再使用JNI机制提供的方法来调用方法或访问类对象的成员变量。

调用Sensor_module_init

Sensor_module_init()内部仅仅只是调用了
SensorManager::getInstance(),在系统中如果不存在Sensoranager的实例时,这将导致构造一个SensorManager对象

setp2 SensorManager初始化

mSensorList(0)调用

SensorManager的构造函数中将mSensorList初始化为空。

在HAL层中保存所有硬件上实现的传感器。

native层在HAL的基础上还会添加一些虚拟传感器。

java层保存的传感器列表在native层基础上有所删减

getService()调用

从defaultServiceManager处获取SensorService的指针,最多尝试4次

回想一下,SensorService在启动后将自己注册到defaultServiceManager现在,SensorManager直接找到组织,和SensorService接上头了。

这一过程将导致 SensorService的 OnFirstRef方法被调用,执行SensorService的初始化

获取的SensorService指针被保存在一个强引用对象中,自动强引用SensorService,调用SensorService的 OnFirstRef方法

setp3 SensorService初始化

调用SensorDevice:getInstance()

step4 SensorDevice初始化

调用hw_get_module/sensor_open/activate

调用hw_get_module加载Sensor模块的共享库文件(在system/lib64/)
调用sensors_open打开设备
调用activate 使能sensor模块

step5 SensorService初始化继续

调用getSensorList

通过SensorDevice.getSensorList向HAL层请求传感器列表并在Sensorervice中注册:
* 添加到mSensorList中去;
* 添加到sensorMap中去,传感器的Handle相关连;
* 新建一个传感器事件数据对象event 到mLastEventSeen中,和传感器的handle相关连

调用SensorFusion::getInstance实例化

SensorDevice封装了所有的 hardware sensor;SensorFusion则是封装了所有虚拟传感器,实现了它们的操作接口

调用registerVirtualSensor

虚拟传感器的注册过程和hardware sensor 的注册过程相同,在此基础上,还将其添加到了mVirtualSensorList中

  • 旋转矢量传感器
  • 重力传感器
  • 线性加速度传感器
  • 方向传感器
  • 真实的陀螺仪

step6 SensorService启动工作线程

调用run
SensorService的初始化已经完成, 现在,启动它的工作线程, 然后返回到调用它的SensorManager的构造函数中,继续执行。
SensorService进入工作线程循环后的细节, 等到整个Sensor framwork都初始化完毕

step7 SensorManger初始化

调用linkTodeath

注册一个到 SensorService的死亡监听器

SensorService异常退出时,sensorManager会自行死亡

通过systemService申请了某个service的Binder后,可以调用这个IBinder的linkToDeath函数注册一个Ibinder.DeathRecipient类型的对象。Ibinder.DeathRecipient是IBinder类中定义的一个嵌入类。当这个IBinder所对应的Service进程被异常的退出时,比如被kill掉,这时系统会调用这个Ibinder之前通过linkToDeath注册的DeathRecipient类对象的binderDied函数。

调用SensorService.getSensorList

又是获取传感器列表, 每一层都会从下层获取传感器列表并保存

step8 SystemSensorManager初始化

进一步调用Sensors_Module_get_next_sensor

回忆一下 native_class_init 中,将java层的Sensor类的成员变量保存在 native层SensorManager中。
这一步调用native层的 Sensor_module_get_next_sensor, 利用保存在SensorManager中的传感器列表来初始化SystemSensorManager 中的Sensor列表

new SensorThread

创建SystemSensorManager 的工作线程,负责从 native 层 的 SensorEventQueue 中读取传感器事件数据。

此时整个基础架构已经搭建完毕

好了,后面应该讲解如何建立连接了,但是时间关系,我在此便打住,会尽快将第二篇整理发出。

后言

我的想法是整个framework sensor部分分3大块解说,第一块是主体框架,第二块是源码分析,第三块是与HAL层的联系

敬请期待吧

作者:u013983194 发表于2017/3/22 23:41:27 原文链接
阅读:124 评论:0 查看评论

基于MVVM,用于快速搭建设置页,个人信息页的框架

$
0
0

写一个小小轮子~

写UITableView的时候,我们经常遇到的是完全依赖于网络请求,需要自定义的动态cell的需求(比如微博帖子列表)。但是同时,大多数app里面几乎也都有设置页,个人页等其他以静态表格为主的页面。

而且这些页面的共性比较多:
1. 大多数情况下在进入页面之前就已经拿到所有数据。
2. cell样式单一,自定义cell出现的几率比较小(几乎都是高度为44的cell)。
3. 多数都分组。

因为自己非常想写一个开源的东西出来(也可以暴露自己的不足),同时又受限于水平,所以就打算写这么一个比较简单,又具有通用性的框架:一个定制性比较高的适合在个人页和设置页使用的UITableView

在真正写之前,看了几篇类似的文章,挑出三篇自己觉得比较好的:
1. Clean Table View Code
2. 如何写好一个UITableView
3. 利用MVVM设计快速开发个人中心、设置等模块

看完总结之后,利用上周3天的业余时间写好了这个框架,为了它实用性,我仿照了微信客户端的发现页,个人页和设置页写了一个Demo,来看一下效果图:

发现页 | 个人页 | 个人信息页 | 设置页

项目所用资源来自:GitHub:zhengwenming/WeChat
Demo地址:GitHub: knightsj/SJStaticTableView

为了体现出这个框架的定制性,我自己也在里面添加了两个页面,入口在设置页里面:

分组定制 | 同组定制

先不要纠结分组定制和同组定制的具体意思,在后面讲到定制性的时候我会详细说明。现在只是让大家看一下效果。

在大概了解了功能之后,开始详细介绍这个框架。写这篇介绍的原因倒不是希望有多少人来用,而是表达一下我自己的思路而已。各位觉得不好的地方请多批评。

在正式讲解之前,先介绍一下本篇的基本目录:
1. 用到的技术点。
2. 功能说明。
3. 使用方法。
4. 定制性介绍。

1. 用到的技术点


框架整体来说还是比较简单的,主要还是基于苹果的UITableView组件,为了解耦和责任分离,主要运用了以下技术点:
- MVVM:采用MVVM架构,将每一行“纯粹”的数据交给一个单独的ViewModel,让其持有每个cell的数据(行高,cell类型,文本宽度,图片高度等等)。而且每一个section也对应一个ViewModel,它持有当前section的配置数据(title,header和footer的高度等等)。
- 轻UIViewController:分离UITableViewDataSourceUIViewController,让单独一个类来实现UITableViewDataSource的职能。
- block:使用block来调用cell的绘制方法。
- 分类:使用分类来定义每一种不同的cell的绘制方法。

知道了主要运用的技术点以后,给大家详细介绍一下该框架的功能。

2. 功能介绍


这个框架可以用来快速搭建设置页,个人信息页能静态表格页面,使用者只需要给tableView的DataSource传入元素是viewModel的数组就可以了。

虽说这类页面的布局还是比较单一的,但是还是会有几种不同的情况(cell的布局类型),我对比较常见的cell布局做了封装,使用者可以直接使用。

我在定义这些cell的类型的时候,大致划分了两类:
1. 第一类是系统风格的cell,大多数情况下,cell高度为44;在cell左侧会有一张图,一个label,也可以只存在一种(但是只存在图片的情况很少);在cell右侧一般都有一个向右的箭头,而且有时这个箭头的左侧还可能有label,image,也可以两个都有。
2. 第二类就是自定义的cell了,它的高度不一定是44,而且布局和系统风格的cell很不一样,需要用户自己添加。

基于这两大类,再细分了几种情况,可以由下面这张图来直观看一下:

既然是cell的类型,那么就类型的枚举就需要定义在cell的viewModel里面:

typedef NS_ENUM(NSInteger, SJStaticCellType) {

    //系统风格的各种cell类型,已封装好,可以直接用
    SJStaticCellTypeSystemLogout,                          //退出登录cell
    SJStaticCellTypeSystemAccessoryNone,                   //右侧没有任何控件
    SJStaticCellTypeSystemAccessorySwitch,                 //右侧是开关
    SJStaticCellTypeSystemAccessoryDisclosureIndicator,    //右侧是三角箭头(箭头左侧可以有一个image或者一个label,或者二者都有,根据传入的参数决定)

    //需要用户自己添加的自定义cell类型
    SJStaticCellTypeMeAvatar,                              //个人页“我”cell    
};

来一张图直观得体会一下:

支持cell类型

在这里有三点需要说一下:

  1. 这里面除了自定义的cell以外,其他类型的cell都不需要开发者自己布局,都已经被我封装好,只需要在cell的ViewModel里面传入相应的类型和数据(文字,图片)即可。
  2. 因为左侧的两个控件(图片和文字)是至少存在一个而且左右顺序固定(图片永远在最左侧),所以该框架通过开发者传入的左侧需要显示的图片和文字,可以自己进行cell的布局。所以类型的判断主要作用于cell的右侧。
  3. 值得一提的是,在”最右侧是一个箭头”子分支的五个类型其实都属于一个类型,只需要传入文字和图片,以及文字图片的显示顺序参数(这个参数只在同时存在图片和文字的时候有效)就可以自行判断布局。

在了解了该框架的功能之后,我们先看一下如何使用这个框架:

3. 使用方法


最开始先用文字说明一下:

  1. 将SJStaticTableViewComponent文件夹复制到工程里。
  2. 将要开发的页面的ViewController继承SJStaticTableViewController
  3. 在新ViewController里实现createDataSource方法,将viewModel数组传给控制器的dataSource属性。
  4. 根据不同的cell类型,调用不同的cell绘制方法。
  5. 如果需要接受cell的点击,需要实现didSelectViewModel方法。

可能感觉比较抽象,我拿设置页来具体说明一下:

先看一下设置页的布局:

设置页

然后我们看一下设置的ViewController的代码:

- (void)viewDidLoad {
    [super viewDidLoad];
    self.navigationItem.title = @"设置";
}


- (void)createDataSource
{
    self.dataSource = [[SJStaticTableViewDataSource alloc] initWithViewModelsArray:[Factory settingPageData] configureBlock:^(SJStaticTableViewCell *cell, SJStaticTableviewCellViewModel *viewModel) {

        switch (viewModel.staticCellType)
        {
            case SJStaticCellTypeSystemAccessoryDisclosureIndicator:
            {
                [cell configureAccessoryDisclosureIndicatorCellWithViewModel:viewModel];
            }
                break;

            case SJStaticCellTypeSystemAccessorySwitch:
            {
                [cell configureAccessorySwitchCellWithViewModel:viewModel];
            }
                break;

            case SJStaticCellTypeSystemLogout:
            {
                [cell configureLogoutTableViewCellWithViewModel:viewModel];
            }
                break;

            case SJStaticCellTypeSystemAccessoryNone:
            {
                [cell configureAccessoryNoneCellWithViewModel:viewModel];
            }
                break;

            default:
                break;
        }
    }];
}


- (void)didSelectViewModel:(SJStaticTableviewCellViewModel *)viewModel atIndexPath:(NSIndexPath *)indexPath
{

    switch (viewModel.identifier)
    {

        case 6:
        {
            NSLog(@"退出登录");
            [self showAlertWithMessage:@"真的要退出登录嘛?"];
        }
            break;

        case 8:
        {
            NSLog(@"清理缓存");
        }
            break;

        case 9:
        {
            NSLog(@"跳转到定制性cell展示页面 - 分组");
            SJCustomCellsViewController *vc = [[SJCustomCellsViewController alloc] init];
            [self.navigationController pushViewController:vc animated:YES];
        }
            break;

        case 10:
        {
            NSLog(@"跳转到定制性cell展示页面 - 同组");
            SJCustomCellsOneSectionViewController *vc = [[SJCustomCellsOneSectionViewController alloc] init];
            [self.navigationController pushViewController:vc animated:YES];
        }
            break;

        default:
            break;
    }
}

看到这里,你可能会有这些疑问:
1. UITableViewDataSource方法哪儿去了?
2. viewModel数组是如何设置的?
3. cell的绘制方法是如何区分的?
4. UITableViewDelegate的方法哪里去了?

下面我会一一解答,看完了下面的解答,就能几乎完全掌握这个框架的思路了:

问题1:UITableViewDataSource方法哪儿去了?

我自己封装了一个类SJStaticTableViewDataSource专门作为数据源,需要控制器给它一个viewModel数组。

来看一下它的实现文件:

//SJStaticTableViewDataSource.m
- (NSInteger)numberOfSectionsInTableView:(UITableView *)tableView
{
    return self.viewModelsArray.count;
}

- (NSInteger)tableView:(UITableView *)tableView numberOfRowsInSection:(NSInteger)section
{
    SJStaticTableviewSectionViewModel *vm = self.viewModelsArray[section];
    return vm.cellViewModelsArray.count;
}

- (UITableViewCell *)tableView:(UITableView *)tableView cellForRowAtIndexPath:(NSIndexPath *)indexPath
{
    //获取section的ViewModel
    SJStaticTableviewSectionViewModel *sectionViewModel = self.viewModelsArray[indexPath.section];
    //获取cell的viewModel
    SJStaticTableviewCellViewModel *cellViewModel = sectionViewModel.cellViewModelsArray[indexPath.row];

    SJStaticTableViewCell *cell = [tableView dequeueReusableCellWithIdentifier:cellViewModel.cellID];
    if (!cell) {
        cell = [[SJStaticTableViewCell alloc] initWithStyle:UITableViewCellStyleDefault reuseIdentifier:cellViewModel.cellID];
    }
    self.cellConfigureBlock(cell,cellViewModel);

    return cell;

}

- (NSString *)tableView:(UITableView *)tableView titleForHeaderInSection:(NSInteger)section{
    SJStaticTableviewSectionViewModel *vm = self.viewModelsArray[section];
    return vm.sectionHeaderTitle;  
}

- (NSString *)tableView:(UITableView *)tableView titleForFooterInSection:(NSInteger)section
{
    SJStaticTableviewSectionViewModel *vm = self.viewModelsArray[section];
    return vm.sectionFooterTitle;
}

表格的cell和section都设置了与其对应的viewModel,用于封装其对应的数据:

cell的viewModel(大致看一下即可,后面有详细说明):

typedef NS_ENUM(NSInteger, SJStaticCellType) {

    //系统风格的各种cell类型,已封装好,可以直接用
    SJStaticCellTypeSystemLogout,                          //退出登录cell(已封装好)
    SJStaticCellTypeSystemAccessoryNone,                   //右侧没有任何控件
    SJStaticCellTypeSystemAccessorySwitch,                 //右侧是开关
    SJStaticCellTypeSystemAccessoryDisclosureIndicator,    //右侧是三角箭头(箭头左侧可以有一个image或者一个label,或者二者都有,根据传入的参数决定)

    //需要用户自己添加的自定义cell类型
    SJStaticCellTypeMeAvatar,                              //个人页“我”cell

};


typedef void(^SwitchValueChagedBlock)(BOOL isOn);           //switch开关切换时调用的block


@interface SJStaticTableviewCellViewModel : NSObject

@property (nonatomic, assign) SJStaticCellType staticCellType;                  //类型


@property (nonatomic, copy)   NSString *cellID;                                  //cell reuser identifier
@property (nonatomic, assign) NSInteger identifier;                              //区别每个cell,用于点击

// =============== 系统默认cell左侧 =============== //
@property (nonatomic, strong) UIImage  *leftImage;                               //左侧的image,按需传入
@property (nonatomic, assign) CGSize leftImageSize;                              //左侧image的大小,存在默认设置

@property (nonatomic, copy)   NSString *leftTitle;                               //cell主标题,按需传入
@property (nonatomic, strong) UIColor *leftLabelTextColor;                       //当前组cell左侧label里文字的颜色
@property (nonatomic, strong) UIFont *leftLabelTextFont;                         //当前组cell左侧label里文字的字体

@property (nonatomic, assign) CGFloat leftImageAndLabelGap;                      //左侧image和label的距离,存在默认值


// =============== 系统默认cell右侧 =============== //
@property (nonatomic, copy)   NSString *indicatorLeftTitle;                      //右侧箭头左侧的文本,按需传入
@property (nonatomic, strong) UIColor *indicatorLeftLabelTextColor;              //右侧文字的颜色,存在默认设置,也可以自定义
@property (nonatomic, strong) UIFont *indicatorLeftLabelTextFont;                //右侧文字的字体,存在默认设置,也可以自定义
@property (nonatomic, strong) UIImage *indicatorLeftImage;                       //右侧箭头左侧的image,按需传入
@property (nonatomic, assign) CGSize indicatorLeftImageSize;                     //右侧尖头左侧image大小,存在默认设置,也可以自定义

@property (nonatomic, assign, readonly)  BOOL hasIndicatorImageAndLabel;         //右侧尖头左侧的文本和image是否同时存在,只能通过内部计算

@property (nonatomic, assign) CGFloat indicatorLeftImageAndLabelGap;             //右侧尖头左侧image和label的距离,存在默认值
@property (nonatomic, assign) BOOL isImageFirst;                                 //右侧尖头左侧的文本和image同时存在时,是否是image挨着箭头,默认为YES
@property (nonatomic, copy) SwitchValueChagedBlock switchValueDidChangeBlock;    //切换switch开关的时候调用的block


// =============== 长宽数据 =============== //
@property (nonatomic, assign) CGFloat cellHeight;                                //cell高度,默认是44,可以设置
@property (nonatomic, assign) CGSize  leftTitleLabelSize;                        //左侧默认Label的size,传入text以后内部计算
@property (nonatomic, assign) CGSize  indicatorLeftLabelSize;                    //右侧label的size


// =============== 自定义cell的数据放在这里 =============== //
@property (nonatomic, strong) UIImage *avatarImage;
@property (nonatomic, strong) UIImage *codeImage;
@property (nonatomic, copy)   NSString *userName;
@property (nonatomic, copy)   NSString *userID;

section的viewModel(大致看一下即可,后面有详细说明):

@interface SJStaticTableviewSectionViewModel : NSObject

@property (nonatomic, copy)   NSString *sectionHeaderTitle;         //该section的标题
@property (nonatomic, copy)   NSString *sectionFooterTitle;         //该section的标题
@property (nonatomic, strong) NSArray  *cellViewModelsArray;        //该section的数据源

@property (nonatomic, assign) CGFloat  sectionHeaderHeight;         //header的高度
@property (nonatomic, assign) CGFloat  sectionFooterHeight;         //footer的高度

@property (nonatomic, assign) CGSize leftImageSize;                 //当前组cell左侧image的大小
@property (nonatomic, strong) UIColor *leftLabelTextColor;          //当前组cell左侧label里文字的颜色
@property (nonatomic, strong) UIFont *leftLabelTextFont;            //当前组cell左侧label里文字的字体
@property (nonatomic, assign) CGFloat leftImageAndLabelGap;         //当前组左侧image和label的距离,存在默认值

@property (nonatomic, strong) UIColor *indicatorLeftLabelTextColor; //当前组cell右侧label里文字的颜色
@property (nonatomic, strong) UIFont *indicatorLeftLabelTextFont;   //当前组cell右侧label里文字的字体
@property (nonatomic, assign) CGSize indicatorLeftImageSize;        //当前组cell右侧image的大小
@property (nonatomic, assign) CGFloat indicatorLeftImageAndLabelGap;//当前组cell右侧image和label的距离,存在默认值


- (instancetype)initWithCellViewModelsArray:(NSArray *)cellViewModelsArray;

你可能会觉得属性太多了,但这些属性的存在意义是为cell的定制性服务的,在后文会有解释。

现在了解了我封装好的数据源,cell的viewModel,section的viewModel以后,我们看一下第二个问题:

问题2: viewModel数组是如何设置的?

我们来看一下设置页的viewModel数组的设置:

+ (NSArray *)settingPageData
{
    // ========== section 0
    SJStaticTableviewCellViewModel *vm0 = [[SJStaticTableviewCellViewModel alloc] init];
    vm0.leftTitle = @"账号与安全";
    vm0.identifier = 0;
    vm0.indicatorLeftTitle = @"已保护";
    vm0.indicatorLeftImage = [UIImage imageNamed:@"ProfileLockOn"];
    vm0.isImageFirst = NO;

    SJStaticTableviewSectionViewModel *section0 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm0]];



    // ========== section 1
    SJStaticTableviewCellViewModel *vm1 = [[SJStaticTableviewCellViewModel alloc] init];
    vm1.leftTitle = @"新消息通知";
    vm1.identifier = 1;

    //额外添加switch
    SJStaticTableviewCellViewModel *vm7 = [[SJStaticTableviewCellViewModel alloc] init];
    vm7.leftTitle = @"夜间模式";
    vm7.switchValueDidChangeBlock = ^(BOOL isON){
        NSString *message = isON?@"打开夜间模式":@"关闭夜间模式";
        NSLog(@"%@",message);
    };
    vm7.staticCellType = SJStaticCellTypeSystemAccessorySwitch;
    vm7.identifier = 7;

    SJStaticTableviewCellViewModel *vm8 = [[SJStaticTableviewCellViewModel alloc] init];
    vm8.leftTitle = @"清理缓存";
    vm8.indicatorLeftTitle = @"12.3M";
    vm8.identifier = 8;

    SJStaticTableviewCellViewModel *vm2 = [[SJStaticTableviewCellViewModel alloc] init];
    vm2.leftTitle = @"隐私";
    vm2.identifier = 2;


    SJStaticTableviewCellViewModel *vm3 = [[SJStaticTableviewCellViewModel alloc] init];
    vm3.leftTitle = @"通用";
    vm3.identifier = 3;

    SJStaticTableviewSectionViewModel *section1 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm1,vm7,vm8,vm2,vm3]];




    // ========== section 2
    SJStaticTableviewCellViewModel *vm4 = [[SJStaticTableviewCellViewModel alloc] init];
    vm4.leftTitle = @"帮助与反馈";
    vm4.identifier = 4;

    SJStaticTableviewCellViewModel *vm5 = [[SJStaticTableviewCellViewModel alloc] init];
    vm5.leftTitle = @"关于微信";
    vm5.identifier = 5;

    SJStaticTableviewSectionViewModel *section2 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm4,vm5]];



      // ========== section 4
    SJStaticTableviewCellViewModel *vm9 = [[SJStaticTableviewCellViewModel alloc] init];
    vm9.leftTitle = @"定制性cell展示页面 - 分组";
    vm9.identifier = 9;

    SJStaticTableviewCellViewModel *vm10 = [[SJStaticTableviewCellViewModel alloc] init];
    vm10.leftTitle = @"定制性cell展示页面 - 同组";
    vm10.identifier = 10;

    SJStaticTableviewSectionViewModel *section4 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm9,vm10]];



    // ========== section 3
    SJStaticTableviewCellViewModel *vm6 = [[SJStaticTableviewCellViewModel alloc] init];
    vm6.staticCellType = SJStaticCellTypeSystemLogout;
    vm6.cellID = @"logout";
    vm6.identifier = 6;

    SJStaticTableviewSectionViewModel *section3 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm6]];

    return @[section0,section1,section2,section4,section3];
}

我们可以看到,交给dataSource的数组是一个二维数组:
- 第一维是section数组,元素是每一个section对应的viewModel:SJStaticTableviewSectionViewModel
- 第二维是cell数组,元素是每一个cell对应的viewModel:SJStaticTableviewCellViewModel

有几个SJStaticTableviewCellViewModel的属性需要强调一下:
1. isImageFirst:因为该页面第一组的cell右侧的箭头左边同时存在一个image和一个label,所以需要额外设置二者的顺序。因为默认紧挨着箭头的是图片,所以我们需要重新设置它为NO,作用是让label紧挨着箭头。
2. identifier:这个属性是一个整数,它用来标记每个cell,用于在用户点击cell的时候进行判断。我没有将用户的点击与cell的index相关联,是因为有的时候因为需求我们可能会更改cell的顺序或者删除某个cell,所以依赖cell的index是不妥的,容易出错。
3. cellID:这个属性用来cell的复用。因为总是有个别cell的布局是不同的:在这里出现了一个退出登录的cell,所以需要和其他的cell区别开来(cellID可以不用设置,有默认值,用来标记最常用的cell类型)。

显然,Factory类属于Model,它将“纯数据”交给了dataSource使用的两个viewModel。这个类是我自己定义的,读者在使用这个框架的时候可以根据需求自己定义。

现在知道了数据源的设置方法,我们看一下第三个问题:

问题3:cell的绘制方法是如何区分的?

心细的同学会发现,在dataSource的cellForRow:方法里,我用了block方法来绘制了cell。

先看一下这个block的定义:

typedef void(^SJStaticCellConfigureBlock)(SJStaticTableViewCell *cell, SJStaticTableviewCellViewModel * viewModel);

这个block在控制器里面回调,通过判断cell的类型来绘制不同的cell。

那么不同类型的cell是如何区分的呢?
— 我用的是分类。

有分类,就一定有一个被分类的类: SJStaticTableViewCell

看一下它的头文件:

//所有cell都是这个类的分类

@interface SJStaticTableViewCell : UITableViewCell

@property (nonatomic, strong) SJStaticTableviewCellViewModel *viewModel;

// =============== 系统风格cell的所有控件 =============== //

//左半部分
@property (nonatomic, strong) UIImageView *leftImageView;               //左侧的ImageView
@property (nonatomic, strong) UILabel *leftTitleLabel;                  //左侧的Label

//右半部分
@property (nonatomic, strong) UIImageView *indicatorArrow;              //右侧的箭头
@property (nonatomic, strong) UIImageView *indicatorLeftImageView;      //右侧的箭头的左边的imageview
@property (nonatomic, strong) UILabel *indicatorLeftLabel;              //右侧的箭头的左边的Label
@property (nonatomic, strong) UISwitch *indicatorSwitch;                //右侧的箭头的左边的开关
@property (nonatomic, strong) UILabel *logoutLabel;                     //退出登录的label

// =============== 用户自定义的cell里面的控件 =============== //

//MeViewController里面的头像cell
@property (nonatomic, strong) UIImageView *avatarImageView;
@property (nonatomic, strong) UIImageView *codeImageView;
@property (nonatomic, strong) UIImageView *avatarIndicatorImageView;
@property (nonatomic, strong) UILabel *userNameLabel;
@property (nonatomic, strong) UILabel *userIdLabel;


//统一的,布局cell左侧部分的内容(标题 / 图片 + 标题),所有系统风格的cell都要调用这个方法
- (void)layoutLeftPartSubViewsWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;

@end

在这里我定义了所有的控件和一个布局cell左侧的控件的方法。因为几乎所有的分类的左侧几乎都是类似的,所以将它抽取出来。

那么究竟有几个分类呢?(可以参考上面cellViewModel头文件里的枚举类型)

//右侧有剪头的cell(最常见)
@interface SJStaticTableViewCell (AccessoryDisclosureIndicator)
- (void)configureAccessoryDisclosureIndicatorCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
//右侧没有控件的cell
@interface SJStaticTableViewCell (AccessoryNone)
- (void)configureAccessoryNoneCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
//右侧是开关的 cell
@interface SJStaticTableViewCell (AccessorySwitch)
- (void)configureAccessorySwitchCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
//退出登录cell
@interface SJStaticTableViewCell (Logout)
- (void)configureLogoutTableViewCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end
//一个自定义的cell(在个人页的第一排)
@interface SJStaticTableViewCell (MeAvatar)
- (void)configureMeAvatarTableViewCellWithViewModel:(SJStaticTableviewCellViewModel *)viewModel;
@end

在使用这个框架的时候,如果遇到不满足当前需求的情况,可以自己添加分类。

问题4:UITableViewDelegate的方法哪里去了?

说到UITableViewDelegate的代理方法,我们最熟悉的莫过于didSelectRowAtIndexPath:了。

但是我在写这个框架的时候,自己定义了一个继承于UITableViewDelegate的代理:SJStaticTableViewDelegate,并给它添加了一个代理方法:

@protocol SJStaticTableViewDelegate <UITableViewDelegate>

@optional

- (void)didSelectViewModel: (SJStaticTableviewCellViewModel *)viewModel atIndexPath:(NSIndexPath *)indexPath;

@end

这个方法返回的是当前点击的cell对应的viewModel,弱化了indexPath的作用。

为什么要这么做?

想一想原来点击cell的代理方法:didSelectRowAtIndexPath:。我们通过这个点击方法,拿到的是cell对应的indexPath,然后再通过这个indexPath,就可以在数据源里面查找对应的模型(viewModel或者model)。

因此,我定义的这个方法直接返回了被点击cell对应的viewModel,等于说帮使用者节省了一个步骤。当然如果要使用的话也可以使用系统原来的didSelectRowAtIndexPath:方法。

来看一下这个新的代理方法是如何实现的:

//SJStaticTableView.m
- (void)tableView:(UITableView *)tableView didSelectRowAtIndexPath:(NSIndexPath *)indexPath
{
    [tableView deselectRowAtIndexPath:indexPath animated:YES];
    if ((self.sjDelegate) && [self.sjDelegate respondsToSelector:@selector(didSelectViewModel:atIndexPath:)]) {

        SJStaticTableviewCellViewModel *cellViewModel = [self.sjDataSource tableView:tableView cellViewModelAtIndexPath:indexPath];
        [self.sjDelegate didSelectViewModel:cellViewModel atIndexPath:indexPath];

    }else if((self.sjDelegate)&& [self.sjDelegate respondsToSelector:@selector(tableView:didSelectRowAtIndexPath:)]){

        [self.sjDelegate tableView:tableView didSelectRowAtIndexPath:indexPath];

    }
}

现在读者应该大致了解了这个框架的实现思路,现在我讲一下这个框架的定制性。

4. 定制性


这个框架有一个配置文件:SJConst.h,它定义了这个框架的所有默认数据和默认配置,比如cell左侧lable的字体,颜色;左侧label和image的距离;右侧label的字体和颜色,右侧图片的默认大小等等。来看一下代码:

#ifndef SJConst_h
#define SJConst_h

//distance
#define SJScreenWidth      [UIScreen mainScreen].bounds.size.width
#define SJScreenHeight     [UIScreen mainScreen].bounds.size.height

#define SJTopGap 8               //same as bottom gap
#define SJLeftGap 12             //same as right gap
#define SJLeftMiddleGap 10       //in left  part: the gap between image and label
#define SJRightMiddleGap 6       //in right part: the gap between image and label
#define SJImgWidth 30            //default width and height
#define SJTitleWidthLimit 180    //limt width of left and right labels

//image
#define SJIndicatorArrow @"arrow"

//font
#define SJLeftTitleTextFont               [UIFont systemFontOfSize:15]
#define SJLogoutButtonFont                [UIFont systemFontOfSize:16]
#define SJIndicatorLeftTitleTextFont      [UIFont systemFontOfSize:13]

//color
#define SJColorWithRGB(R,G,B,A)           [UIColor colorWithRed:R/255.0 green:G/255.0 blue:B/255.0 alpha:A]
#define SJLeftTitleTextColor              [UIColor blackColor]
#define SJIndicatorLeftTitleTextColor     SJColorWithRGB(136,136,136,1)

#endif /* SJConst_h */

这里定义的默认配置在cellViewModel和sectionViewModel初始化的时候使用:

cell的viewModel:

//SJStaticTableviewCellViewModel.m
- (instancetype)init
{
    self = [super init];
    if (self) {        
        _cellHeight = 44;
        _cellID = @"defaultCell";
        _staticCellType = SJStaticCellTypeSystemAccessoryDisclosureIndicator;//默认是存在三角箭头的cell
        _isImageFirst = YES;

        //都是默认配置
        _leftLabelTextFont = SJLeftTitleTextFont;
        _leftLabelTextColor = SJLeftTitleTextColor;
        _leftImageSize = CGSizeMake(SJImgWidth, SJImgWidth);
        _leftImageAndLabelGap = SJLeftMiddleGap;
        _indicatorLeftLabelTextFont = SJIndicatorLeftTitleTextFont;
        _indicatorLeftLabelTextColor = SJIndicatorLeftTitleTextColor;
        _indicatorLeftImageSize = CGSizeMake(SJImgWidth, SJImgWidth);
        _indicatorLeftImageAndLabelGap = SJRightMiddleGap;
    }
    return self;
}

section的viewModel:

- (instancetype)initWithCellViewModelsArray:(NSArray *)cellViewModelsArray
{
    self = [super init];
    if (self) {
        _sectionHeaderHeight = 10;
        _sectionFooterHeight = 10;
        _leftLabelTextFont = SJLeftTitleTextFont;
        _leftLabelTextColor = SJLeftTitleTextColor;
        _leftImageSize = CGSizeMake(SJImgWidth, SJImgWidth);
        _leftImageAndLabelGap = SJLeftMiddleGap;
        _indicatorLeftLabelTextFont = SJIndicatorLeftTitleTextFont;
        _indicatorLeftLabelTextColor = SJIndicatorLeftTitleTextColor;
        _indicatorLeftImageSize = CGSizeMake(SJImgWidth, SJImgWidth);
        _indicatorLeftImageAndLabelGap = SJRightMiddleGap;
        _cellViewModelsArray = cellViewModelsArray;        
    }
    return self;
}

显然,这个默认配置只有一组,但是可能一个app里面同时存在一个设置页和一个个人页。而这两个页面的风格也可能是不一样的,所以这个默认配置只能给其中一个页面,另一个页面需要另外配置,于是就有了定制性的功能。

再来看一下展示定制性效果的图:

分组定制 | 同组定制

参照这个效果图,我们看一下这两个页面的数据源是如何设置的:

分组页面:

+ (NSArray *)customCellsPageData
{
    //默认配置
    SJStaticTableviewCellViewModel *vm1 = [[SJStaticTableviewCellViewModel alloc] init];
    vm1.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm1.leftTitle = @"全部默认配置,用于对照";
    vm1.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm1.indicatorLeftTitle = @"王者荣耀!";

    SJStaticTableviewSectionViewModel *section1 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm1]];

    SJStaticTableviewCellViewModel *vm2 = [[SJStaticTableviewCellViewModel alloc] init];
    vm2.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm2.leftTitle = @"左侧图片变小";
    vm2.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm2.indicatorLeftTitle = @"王者荣耀!";

    SJStaticTableviewSectionViewModel *section2 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm2]];
    section2.leftImageSize = CGSizeMake(20, 20);

    SJStaticTableviewCellViewModel *vm3 = [[SJStaticTableviewCellViewModel alloc] init];
    vm3.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm3.leftTitle = @"字体变小变红";
    vm3.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm3.indicatorLeftTitle = @"王者荣耀!";

    SJStaticTableviewSectionViewModel *section3 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm3]];
    section3.leftLabelTextFont = [UIFont systemFontOfSize:8];
    section3.leftLabelTextColor = [UIColor redColor];


    SJStaticTableviewCellViewModel *vm4 = [[SJStaticTableviewCellViewModel alloc] init];
    vm4.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm4.leftTitle = @"左侧两个控件距离变大";
    vm4.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm4.indicatorLeftTitle = @"王者荣耀!";

    SJStaticTableviewSectionViewModel *section4 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm4]];
    section4.leftImageAndLabelGap = 20;


    SJStaticTableviewCellViewModel *vm5 = [[SJStaticTableviewCellViewModel alloc] init];
    vm5.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm5.leftTitle = @"右侧图片变小";
    vm5.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm5.indicatorLeftTitle = @"王者荣耀!";

    SJStaticTableviewSectionViewModel *section5 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm5]];
    section5.indicatorLeftImageSize = CGSizeMake(15, 15);


    SJStaticTableviewCellViewModel *vm6= [[SJStaticTableviewCellViewModel alloc] init];
    vm6.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm6.leftTitle = @"右侧字体变大变蓝";
    vm6.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm6.indicatorLeftTitle = @"王者荣耀!";

    SJStaticTableviewSectionViewModel *section6 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm6]];
    section6.indicatorLeftLabelTextFont = [UIFont systemFontOfSize:18];
    section6.indicatorLeftLabelTextColor = [UIColor blueColor];


    SJStaticTableviewCellViewModel *vm7= [[SJStaticTableviewCellViewModel alloc] init];
    vm7.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm7.leftTitle = @"右侧两个控件距离变大";
    vm7.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm7.indicatorLeftTitle = @"王者荣耀!";

    SJStaticTableviewSectionViewModel *section7 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm7]];
    section7.indicatorLeftImageAndLabelGap = 18;


    return @[section1,section2,section3,section4,section5,section6,section7];

}

我们可以看到,定制的代码都作用于section的viewModel。

同组页面:

+ (NSArray *)customCellsOneSectionPageData
{
    //默认配置
    SJStaticTableviewCellViewModel *vm1 = [[SJStaticTableviewCellViewModel alloc] init];
    vm1.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm1.leftTitle = @"全部默认配置,用于对照";
    vm1.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm1.indicatorLeftTitle = @"王者荣耀!";


    SJStaticTableviewCellViewModel *vm2 = [[SJStaticTableviewCellViewModel alloc] init];
    vm2.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm2.leftTitle = @"左侧图片变小";
    vm2.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm2.indicatorLeftTitle = @"王者荣耀!";
    vm2.leftImageSize = CGSizeMake(20, 20);


    SJStaticTableviewCellViewModel *vm3 = [[SJStaticTableviewCellViewModel alloc] init];
    vm3.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm3.leftTitle = @"字体变小变红";
    vm3.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm3.indicatorLeftTitle = @"王者荣耀!";
    vm3.leftLabelTextFont = [UIFont systemFontOfSize:8];
    vm3.leftLabelTextColor = [UIColor redColor];


    SJStaticTableviewCellViewModel *vm4 = [[SJStaticTableviewCellViewModel alloc] init];
    vm4.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm4.leftTitle = @"左侧两个控件距离变大";
    vm4.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm4.indicatorLeftTitle = @"王者荣耀!";
    vm4.leftImageAndLabelGap = 20;


    SJStaticTableviewCellViewModel *vm5 = [[SJStaticTableviewCellViewModel alloc] init];
    vm5.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm5.leftTitle = @"右侧图片变小";
    vm5.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm5.indicatorLeftTitle = @"王者荣耀!";
    vm5.indicatorLeftImageSize = CGSizeMake(15, 15);


    SJStaticTableviewCellViewModel *vm6= [[SJStaticTableviewCellViewModel alloc] init];
    vm6.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm6.leftTitle = @"右侧字体变大变蓝";
    vm6.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm6.indicatorLeftTitle = @"王者荣耀!";
    vm6.indicatorLeftLabelTextFont = [UIFont systemFontOfSize:18];
    vm6.indicatorLeftLabelTextColor = [UIColor blueColor];


    SJStaticTableviewCellViewModel *vm7= [[SJStaticTableviewCellViewModel alloc] init];
    vm7.leftImage = [UIImage imageNamed:@"MoreGame"];
    vm7.leftTitle = @"右侧两个控件距离变大";
    vm7.indicatorLeftImage = [UIImage imageNamed:@"wzry"];
    vm7.indicatorLeftTitle = @"王者荣耀!";
    vm7.indicatorLeftImageAndLabelGap = 18;

    SJStaticTableviewSectionViewModel *section1 = [[SJStaticTableviewSectionViewModel alloc] initWithCellViewModelsArray:@[vm1,vm2,vm3,vm4,vm5,vm6,vm7]];

    return @[section1];
}

为了方便比较,同组页面的定制和分组是一致的。我们可以看到,定制代码都作用于cell的viewModel上了。

为什么要有同组和分组展示?

同组和分组展示的目的,是为了展示这个框架的两种定制性。

  • 分组页面所展示的是section级的定制性:cell的配置任务交给section层的viewModel。一旦设置,该section里面的所有cell都能保持这一配置。

  • 同组页面所展示的是cell级的定制性:cell的配置任务交给cell层的viewModel。一旦设置,只有当前cell具有这个配置,不影响其他cell。

其实为了省事,只在section层的viewModel上配置即可(如果给每个cell都给设置相同的配置太不优雅了),因为从设计角度来看,一个section里面的cell的风格不一致的情况比较少见(我觉得不符合设计):比如在一个section里面,不太可能两个cell里面的图片大小是不一样的,或者字体大小也不一样。

还是看一下section级的定制代码吧:

//重新设置了该组全部cell里面左侧label的字体
- (void)setLeftLabelTextFont:(UIFont *)leftLabelTextFont
{
    if (_leftLabelTextFont != leftLabelTextFont) {

        if (![self font1:_leftLabelTextFont hasSameFontSizeOfFont2:leftLabelTextFont]) {

            _leftLabelTextFont = leftLabelTextFont;

            //如果新的宽度大于原来的宽度,需要重新设置,否则不需要
            [_cellViewModelsArray enumerateObjectsUsingBlock:^(SJStaticTableviewCellViewModel * viewModel, NSUInteger idx, BOOL * _Nonnull stop) {
                viewModel.leftLabelTextFont = _leftLabelTextFont;
                CGSize size = [self sizeForTitle:viewModel.leftTitle withFont:_leftLabelTextFont];
                if (size.width > viewModel.leftTitleLabelSize.width) {
                    viewModel.leftTitleLabelSize = size;
                }
            }];

        }
    }
}

//重新设置了该组全部cell里面左侧label的字的颜色
- (void)setLeftLabelTextColor:(UIColor *)leftLabelTextColor
{
    if (![self color1:_leftLabelTextColor hasTheSameRGBAOfColor2:leftLabelTextColor]) {
         _leftLabelTextColor = leftLabelTextColor;
        [_cellViewModelsArray makeObjectsPerformSelector:@selector(setLeftLabelTextColor:) withObject:_leftLabelTextColor];
    }
}

//重新设置了该组全部cell里面左侧图片等大小
- (void)setLeftImageSize:(CGSize)leftImageSize
{
    SJStaticTableviewCellViewModel *viewMoel = _cellViewModelsArray.firstObject;

    CGFloat cellHeight = viewMoel.cellHeight;
    if ( (!CGSizeEqualToSize(_leftImageSize, leftImageSize)) && (leftImageSize.height < cellHeight)) {
        _leftImageSize = leftImageSize;
        [_cellViewModelsArray enumerateObjectsUsingBlock:^(SJStaticTableviewCellViewModel *viewModel, NSUInteger idx, BOOL * _Nonnull stop)
        {
            viewMoel.leftImageSize = _leftImageSize;
        }];
    }
}

因为每个section都持有它内部的所有cell的viewModel,所以在set方法里面,如果发现传进来的配置与当前配置不一致,就需要更新所有cell的viewModel对应的属性。

既然section的ViewModel能做这些,为什么还要有一个cell层的配置呢?

– 只是为了提高配置的自由度罢了,万一突然来个需求需要某个cell很独特呢?(大家应该知道我说的神么意思 ^^)

cell的viewModel属性的set方法的实现和section的一致,这里就不上代码了。

好了,到这里就讲差不多了,代码量虽然不多,但是都说清楚还是感觉挺需要时间想的。
希望如果各位觉得哪里不好,可以给出您的宝贵意见~

本篇已同步到个人博客:传送门

本篇已同步到简书:传送门


本文已在版权印备案,如需转载请访问版权印。48422928

获取授权

作者:apple890111 发表于2017/3/23 8:33:42 原文链接
阅读:464 评论:0 查看评论

算法-发明KMP算法的唐纳德·克努特是怎么想到失配函数next[j]的?

$
0
0

背景

字符串模式匹配,普通模式非常好理解,拿着模式串依次与主串做比较,知道完全匹配,但是这种算法,主串得不断地回溯,时间复杂度O(n*m)。


这里写图片描述

唐纳德·克努特

有没有降低时间复杂度的可能,唐纳德·克努特等人想到了一种办法不用使主串不停地回溯,而每次使模式串的某个字符与主串的待比较字符对齐,这个算法简称KMP。求解模式串的哪个字符该与这次比较的主串字符对齐,是KMP算法的核心,简称next函数或失配函数。这种算法求解复杂度降低到O(n+m)。

next函数语义

next[j]=k表达的意思是从模式串的 1~j-1 组成的子模式串,最长相同的前、后缀的长度为 k-1。举例说明,如下的字符串,next[6]=3,因为编号为6的字符c的最长前缀为编号为1的a ,编号为2的b 字符,最长后缀为编号为4的字符,编号为5的字符b,所以 k=3。

1 2 3 4 5 6 7 8
a b a a b c a c

再看一下失配函数next[j]的严格定义,模式串字符的编码从1开始。


这里写图片描述

next函数分析

next 函数值仅取决于模式串本身而与相匹配的主串无关。从next函数的定义出发用递推的方法求next函数值。

由定义得知 next[1]=0,设next[j]=k,这表明在模式串中存在下列关系

"P1...Pk-1" = "Pj-k+1...Pj-1"

图形化显示(一条竖线表示一个字符):


这里写图片描述

其中k为满足1 < k < j的某个值,并且不能存在k’ > k满足上个等式。此时 next[j+1]=? 分两种情况讨论,

1)若 Pk = Pj ,则 next[j+1] = next[j] + 1 ,即 k + 1 ,如下图显示:


这里写图片描述

2)若Pk不等于Pj,如下图所示,我们把如下字符,看成一个字符串,寻找它的最长相同的前、后缀:

"P1...Pj+1"


这里写图片描述

此时我们已知一个条件:

"P1...Pk-1" = "Pj-k+1...Pj-1"

也就是在上图中2个黄色区域表示的前、后缀字符串相等,这样我们依然在上图中的左侧黄色部分中寻找。最终找到了2块咖啡色区域 1~k’-1, k-k’+1~k-1 相等,根据next函数的定义,便是:

next[k]=k'


这里写图片描述

并且我们根据已知条件 ‘P1…Pk-1’ = ‘Pj-k+1…Pj-1’,可以推导出在右侧黄色区域也存在这样的咖啡色区域,根据等式传递,我们可以得出:

"P1...Pk'-1" = "Pj-k'+1...Pj-1"

因为Pj不等于Pk,所以我们新找出了一个k’(很显然1 < k’ < k),如果它真的满足了 Pj=Pk’,则 next[j+1] = k’ + 1 ,即 :

next[j+1] = next[k] + 1  

如果它很遗憾地又不等于Pj,也没关系,我们继续在[1,k’]这个区间内找这样的K点,如果真的不存在这样的k’,那么 根据定义可以得出:

next[j+1]=1

失配函数代码实现

        /// <summary>
        /// 失配函数
        /// </summary>
        /// <param name="p">模式字符串(编码从索引位置1开始)</param>
        /// <returns>模式字符串中每个字符的失配值数组</returns>
        private static int[] getNext(char[] p)
        {
            int[] next = new int[p.Length];
            next[1] = 0;
            int j = 1;
            int k = 0;

            while (j < p.Length - 1)
            {
                if (k == 0 || p[j] == p[k])
                {
                    next[++j] = ++k; //上述分析中的k'+1赋值给next[j+1]
                }
                else
                {
                    k = next[k]; //next[k]赋值给k,相当于上述分析中的k'
                }
            }
            return next;
        }

模拟分析

模拟失配函数求解的整个过程代码。

        static void Main(string[] args)
        {
            string pattern = "abaabcac";
            char[] pcharsfrom1 = preOperate(pattern);
            Console.WriteLine();
            int[] next = getNextWithTest(pcharsfrom1);

            printf(next);
            Console.ReadLine();
        }

预处理字符串,将字符串整体后移1位

        /// <summary>
        /// 预处理字符串,将字符串整体后移1位
        /// </summary>
        /// <returns></returns>
        private static char[] preOperate(string pattern)
        {
            char[] pchars = pattern.ToCharArray(0, pattern.Length);
            char[] pcharsfrom1 = new char[pchars.Length + 1];
            for (int i = pchars.Length; i > 0; i--)
                pcharsfrom1[i] = pchars[i - 1];
            return pcharsfrom1;
        }
        private static int[] getNextWithTest(char[] p)
        {
            int[] next = new int[p.Length];
            next[1] = 0;
            int j = 1;
            int k = 0;

            printf(p);


            while (j < p.Length - 1)
            {
                if (k != 0)
                    Console.WriteLine("p[{0}]({1}) == p[{2}]({3})??", j, p[j], k, p[k]);
                if (k == 0 || p[j] == p[k])
                {
                    if (k == 0)
                    {
                        ++j;
                        ++k;
                        next[j] = k;
                        Console.WriteLine("根据k=0得出:p[{0}]={1}", j, k);
                        Console.ForegroundColor = ConsoleColor.DarkGreen;
                        Console.WriteLine("--------------------------------");
                        Console.ForegroundColor = ConsoleColor.White;
                    }
                    else
                    {

                        ++j;
                        ++k;
                        next[j] = k;
                        Console.WriteLine("根据p[j] == p[k]得出:p[{0}]={1}", j, k);
                        Console.ForegroundColor = ConsoleColor.DarkGreen;
                        Console.WriteLine("--------------------------------");
                        Console.ForegroundColor = ConsoleColor.White;
                    }
                }
                else
                {
                    k = next[k];
                }
            }
            return next;
        }

        private static void printf<T>(T[] p)
        {
            int eachlineCount = 10;
            for (int line = 0; line < p.Length / eachlineCount + 1; line++)
            {
                for (int i = 0; i < eachlineCount && line * eachlineCount + i < p.Length; i++)
                {
                    Console.Write("  {0}  ", line * eachlineCount + i);
                }
                Console.Write("\n");
                for (int i = 0; i < eachlineCount && line * eachlineCount + i < p.Length; i++)
                {
                    if (line == 0)
                        Console.Write("  {0}  ", p[line * eachlineCount + i]);
                    else
                    {
                        Console.Write("   {0}  ", p[line * eachlineCount + i]);
                    }
                }

                Console.Write("\n\n");
            }
        }

模拟结果展示:


这里写图片描述

源码下载:

http://download.csdn.net/detail/daigualu/9791023

作者:daigualu 发表于2017/3/23 13:06:19 原文链接
阅读:204 评论:0 查看评论

unity-AssetBundle资源冗余检测

$
0
0
  • 最近搞了下 unity 的 AssetBundle(以下简称 AB) 资源冗余检测,并导出一个md文件列出冗余的资源及其被打入进哪几个AB中,方便排除冗余

思路

  • 科普下AB资源冗余,当然这里并不会详细说明,只会放个链接 点我 (需要科学上网)
  • 可以搜到大部分的文章都是在说一个 侑虎科技 的第三方检测平台,我也去上面检测了下正确检测出来,据说是免费几次后就需要付费。但是资源不加密就上传上去貌似不是很靠谱,资源大了上传也麻烦。所以就自己实现了一个检测。
    • 检测的思路也很简单
      1. 先用主AB生产一个所有 资源及所在AB 的一个 映射表
      2. 递归遍历打出来的包下的所有的AB,通过 AssetDatabase.GetDependencies 获取到 AB中资源名字在 AssetDatabase 中所有的依赖。(此时要求工程下Asset下有正常的资源)
      3. 遍历所有依赖,是否在 映射表 中,如果 不存在超过两次,相同 资源A 被打进了两个AB中,而不是 资源A 打成一个AB,被其他AB依赖进去。
      4. 最后会收集到这些 不存在超过两次 的资源名及被打进去的AB文件名,导出到一个md文件中,使用md编辑器查看(这里推荐个md客户端叫 Haroopad,平常都用这个写md)

源码 及 使用

  • 鉴于源码就一个cs文件,就不上传到git,直接这里贴了,同时也会上传几个测试的 源资源(test_res.rar)、打包出的 有冗余(ABoutput.rar)、无冗余(ABoutput_red.rar) 的资源
  • 使用:

    1. 分别解压出来,test_res目录放到工程Asset目录下(AssetDatabase才能找到资源,获取依赖),打包出的资源随意放(最好英文路径)
    2. 选择打包出的资源的主AB
      这里写图片描述

      这里写图片描述

    3. 开始检测,有冗余会导出md文件
      这里写图片描述

      这里写图片描述

  • 资源传送门:unity3d冗余测试资源.rar

  • 源码:

    using System;
    using System.Collections.Generic;
    using System.Text;
    using UnityEngine;
    using UnityEditor;
    using System.IO;
    using System.Linq;
    
    class ABRedundancyChecker : EditorWindow
    {
        class CRedAsset
        {
            public CRedAsset()
            { }
            public string mName = "";
            public string mType = "";
            public List<string> mUsers = new List<string>();
        }
    
        List<Type> mAssetTypeList = new List<Type> {
            typeof(Material), typeof(Texture2D), typeof(AnimationClip), typeof(AudioClip), typeof(Sprite), typeof(Shader), typeof(Font), typeof(Mesh)
        };
    
        const string kABRedundencyDir = "/a_ABRedundency"; //输出文件的目录
        const string kSearchPattern = "*.assetbundle";
        string kResultPath = Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory) + kABRedundencyDir;
        //const string kABPath = "Assets/test_res/ABoutput_red";
        //const string kManiFest = "ABoutput";
        bool mIsForQ6 = false;
        string mABPath = "D:\\svn_q6_app\\win64\\q6-v0.0.63.0\\q6_Data\\StreamingAssets\\resource";
        string mMainAb = "resource";
    
        List<string> mAllABFiles = null;
        Dictionary<string, string> mAssetGenMap = null;
        Dictionary<string, CRedAsset> mRedAssetMap = null;
        float mCheckTime = 0f;
    
        static ABRedundancyChecker mInstance = null;
        public static ABRedundancyChecker Ins
        {
            get
            {
                if (mInstance == null)
                    mInstance = new ABRedundancyChecker();
                return mInstance;
            }
        }
    
        //[MenuItem("AB冗余检测/AB检测")]
        ////[MenuItem("Q5/Bundle相关/Bundle冗余检测")]
        //public static void Launch()
        //{
        //    ABRedundancyChecker.Ins.StartCheck();
        //}
    
        // 提供给其他脚本调用的接口
        public void StartCheck(string path, string abName)
        {
            mABPath = path;
            mMainAb = abName;
            mIsForQ6 = true;
            StartCheck();
        }
    
        void StartCheck()
        {
            EditorUtility.DisplayCancelableProgressBar("AB资源冗余检测中", "资源读取中......", 0f);
            mCheckTime = UnityEngine.Time.realtimeSinceStartup;
            if (mAllABFiles == null)
                mAllABFiles = new List<string>();
            if (mAssetGenMap == null)
                mAssetGenMap = new Dictionary<string, string>();
            if (mRedAssetMap == null)
                mRedAssetMap = new Dictionary<string, CRedAsset>();
    
            if (!GenAssetMap(mABPath, mMainAb))
            {
                EditorUtility.ClearProgressBar();
                EditorUtility.DisplayDialog("错误", "请检查是否选择正确的AB资源", "Ok");
                return;
            }
    
            GetAllFiles(mAllABFiles, mABPath, kSearchPattern);
            int startIndex = 0;
    
            EditorApplication.CallbackFunction myUpdate = null;
            myUpdate = () =>
            {
                string file = mAllABFiles[startIndex];
                AssetBundle ab = null;
                try
                {
                    ab = CreateABAdapter(file);
                    string[] arr = file.Split('/');
                    CheckABInfo(ab, arr[arr.Length - 1]);
                }
                catch (Exception e)
                {
                    Debug.LogError("MyError:" + e.StackTrace);
                }
                finally
                {
                    if (ab != null)
                        ab.Unload(true);
                }
    
                bool isCancel = EditorUtility.DisplayCancelableProgressBar("AB资源冗余检测中", file, (float)startIndex / (float)mAllABFiles.Count);
                startIndex++;
                if (isCancel || startIndex >= mAllABFiles.Count)
                {
                    EditorUtility.ClearProgressBar();
                    if (!isCancel)
                    {
                        CullNotRed();
                        mCheckTime = UnityEngine.Time.realtimeSinceStartup - mCheckTime;
                        EditorUtility.DisplayDialog("AssetBundle资源冗余检测结果", Export(), "Ok");
                    }
    
                    mAllABFiles.Clear();
                    mAllABFiles = null;
                    mAssetGenMap.Clear();
                    mAssetGenMap = null;
                    mRedAssetMap.Clear();
                    mRedAssetMap = null;
                    Resources.UnloadUnusedAssets();
                    EditorUtility.UnloadUnusedAssetsImmediate();
                    GC.Collect();
                    EditorApplication.update -= myUpdate;
                    startIndex = 0;
                }
            };
    
            EditorApplication.update += myUpdate;
        }
    
        //适配项目打包(有加密) 或 原生打包
        AssetBundle CreateABAdapter(string path)
        {
            //if (mIsForQ6)
            //    return UtilCommon.CreateBundleFromFile(path);
            //else
                return AssetBundle.LoadFromFile(path);
        }
    
        bool GenAssetMap(string path, string maniFest)
        {
            path = path.Replace("\\", "/");
            AssetBundle maniFestAb = CreateABAdapter(System.IO.Path.Combine(path, maniFest));
            if (maniFestAb == null)
                return false;
    
            AssetBundleManifest manifest = maniFestAb.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
            if (manifest == null)
                return false;
    
            string[] allBundles = manifest.GetAllAssetBundles();
            maniFestAb.Unload(true);
            foreach (string abName in allBundles)
            {
                string filePath = System.IO.Path.Combine(path, abName);
                AssetBundle ab = CreateABAdapter(filePath);
                foreach (string asset in ab.GetAllAssetNames())
                {
                    mAssetGenMap.Add(asset.ToLower(), abName);
                }
                foreach (string asset in ab.GetAllScenePaths())
                {
                    mAssetGenMap.Add(asset.ToLower(), abName);
                }
                ab.Unload(true);
            }
    
            if (mAssetGenMap.Count == 0)
                return false;
    
            return true;
        }
    
        void CheckABInfo(AssetBundle ab, string abName)
        {
            EditorSettings.serializationMode = SerializationMode.ForceText;
            string[] names = ab.GetAllAssetNames();
            string[] dependencies = AssetDatabase.GetDependencies(names);
            string[] allDepen = dependencies.Length > 0 ? dependencies : names;
    
            string currDep = "";
            for (int i = 0; i < allDepen.Length; i++)
            {
                currDep = allDepen[i].ToLower();
                CalcuDenpend(currDep, abName);
                //UnityEngine.Object obj = ab.LoadAsset(currDep, typeof(UnityEngine.Object));
                //if (obj != null)
                //{
                //    Debugger.Log("--- obj type:{0}", GetObjectType(obj));
                //}
            }
        }
    
        //todo: 待加入 类型
        void CalcuDenpend(string depName, string abName)
        {
            if (depName.EndsWith(".cs"))
                return;
    
            if (!mAssetGenMap.ContainsKey(depName)) //不存在这个ab,记录一下
            {
                if (!mRedAssetMap.ContainsKey(depName))
                {
                    CRedAsset ra = new CRedAsset();
                    ra.mName = depName;
                    ra.mType = "我了个去";
                    mRedAssetMap.Add(depName, ra);
                    ra.mUsers.Add(abName);
                }
                else
                {
                    CRedAsset ra = mRedAssetMap[depName];
                    ra.mUsers.Add(abName);
                }
            }
        }
    
        // mRedAssetMap 中 CRedAsset 的 mUsers 只有一个的,视为不冗余的资源,直接打到了该 ab 中
        void CullNotRed()
        {
            List<string> keys = new List<string>();
            foreach (var item in mRedAssetMap)
            {
                if (item.Value.mUsers.Count == 1)
                    keys.Add(item.Key);
            }
    
            foreach (var value in keys)
                mRedAssetMap.Remove(value);
        }
    
        List<string> GetAllFiles(List<string> files, string folder, string pattern)
        {
            folder = folder.Replace("\\", "/");
            System.IO.DirectoryInfo dir = new System.IO.DirectoryInfo(folder);
            foreach (var file in dir.GetFiles(pattern))
            {
                files.Add((System.IO.Path.Combine(folder, file.Name).Replace("\\", "/")).ToLower());
            }
            foreach (var sub in dir.GetDirectories())
            {
                files = GetAllFiles(files, System.IO.Path.Combine(folder, sub.Name), pattern);
            }
            return files;
        }
    
        string GetObjectType(UnityEngine.Object obj)
        {
            string longType = obj.GetType().ToString();
            string[] longTypeArr = longType.Split('.');
            return longTypeArr[longTypeArr.Length - 1];
        }
    
        private string AppendSlash(string path)
        {
            if (path == null || path == "")
                return "";
            int idx = path.LastIndexOf('/');
            if (idx == -1)
                return path + "/";
            if (idx == path.Length - 1)
                return path;
            return path + "/";
        }
    
        string Export()
        {
            if (mRedAssetMap.Count == 0)
                return "未检查到有资源冗余";
    
            List<CRedAsset> raList = mRedAssetMap.Values.ToList<CRedAsset>();
            string currTime = System.DateTime.Now.ToString("yyyyMMdd_HHmmss");
            string path = string.Format("{0}/{1}_{2}.md", kResultPath, "ABRedundency", currTime);
            if (!System.IO.Directory.Exists(kResultPath))
                System.IO.Directory.CreateDirectory(kResultPath);
    
            using (FileStream fs = File.Create(path))
            {
                StringBuilder sb = new StringBuilder();
                sb.Append(string.Format("## 资源总量:{0},冗余总量:{1},检测时间:{2},耗时:{3:F2}s\r\n---\r\n", mAllABFiles.Count, raList.Count, currTime, mCheckTime));
                sb.Append("| 排序 | 资源名称 | 资源类型 | AB文件数量 | AB文件名 |\r\n");
                sb.Append("|---|---|:---:|:---:|---|\r\n");
    
                CRedAsset ra = null;
    
                StringBuilder abNames = new StringBuilder();
    
                raList.Sort((CRedAsset ra1, CRedAsset ra2) =>
                {//排序优先级: ab文件个数 -> 名字
                    int ret = ra2.mUsers.Count.CompareTo(ra1.mUsers.Count);
                    if (ret == 0)
                        ret = ra1.mName.CompareTo(ra2.mName);
                    return ret;
                });
    
                for (int i = 0; i < raList.Count; i++)
                {
                    ra = raList[i];
                    foreach (var abName in ra.mUsers)
                        abNames.Append(string.Format("**{0}**, ", abName));
                    //abNames.Append(string.Format("{0}<br>", abName)); //另一种使用换行
    
                    sb.Append(string.Format("| {0} | **{1}** | {2} | {3} | {4} |\r\n"
                        , i + 1, ra.mName, ra.mType, ra.mUsers.Count, abNames.ToString()));
                    abNames.Length = 0;
                }
                byte[] info = new UTF8Encoding(true).GetBytes(sb.ToString());
                fs.Write(info, 0, info.Length);
            }
            return "有冗余,导出结果:" + path.Replace("\\", "/");
        }
    
        //---------------- gui begin ------------
    
        [MenuItem("AB冗余检测/AB检测")]
        static void Init()
        {
            EditorWindow.GetWindow(typeof(ABRedundancyChecker), false, "AB资源冗余检测");
        }
    
        void Awake()
        {
            mInstance = this;
        }
        string mSelPath = "";
        public void OnGUI()
        {
            EditorGUILayout.Space();
            EditorGUILayout.LabelField("路径:", EditorStyles.boldLabel);
            EditorGUILayout.Space();
            GUILayout.Label(mSelPath);
            EditorGUILayout.Space();
            if (GUILayout.Button("选择主AB文件"))
                mSelPath = EditorUtility.OpenFilePanelWithFilters("选择主AB文件", mSelPath, null);
            EditorGUILayout.Space();
            //mIsForQ6 = EditorGUILayout.Toggle("是否Q6(Q6有解密机制)", mIsForQ6);
            EditorGUILayout.Space();
            if (GUILayout.Button("开始检测"))
            {
                if (mSelPath == "")
                    EditorUtility.DisplayDialog("错误", "请先 选择主AB文件", "Ok");
                else
                {
                    mSelPath = mSelPath.Replace("\\", "/");
                    string[] arr = mSelPath.Split('/');
                    mMainAb = arr[arr.Length - 1];
                    mABPath = mSelPath.Substring(0, mSelPath.LastIndexOf('/'));
                    StartCheck();
                }
            }
        }
    }
作者:yangxuan0261 发表于2017/3/23 14:49:42 原文链接
阅读:4 评论:0 查看评论

3D轮廓检测技术讲解

$
0
0

笔者介绍:姜雪伟,IT公司技术合伙人,IT高级讲师,CSDN社区专家,特邀编辑,畅销书作者,国家专利发明人;已出版书籍:《手把手教你架构3D游戏引擎》电子工业出版社和《Unity3D实战核心技术详解》电子工业出版社等。

CSDN视频网址:http://edu.csdn.net/lecturer/144

今天我们将讨论一种可以检测到3D物体的轮廓方式。 为了使事情变得更清楚,我指的是一个3D对象的轮廓,当光从任意方向落在它上面时。 移动光源可能会相应地改变轮廓。 这完全不同于在图像空间中的轮廓检测,其处理在2D图像中找到对象的边界(其通常不依赖于光源的位置)。 虽然轮廓检测的主题本身可能是有趣的,但对我们来说,其主要目标是实施Stencil Shadow Volume(在上篇博客中介绍)的第一步。 这是一种渲染阴影的技术,在处理点光时尤其有用。 我们将在上一篇博文中研究此技术。

以下图像演示了我们想要检测的轮廓:


在上面的图像中,轮廓是由光线照射。。。。。。
让我们现在转向更传统的3D语言,模型基本上由三角形组成,因此轮廓必须由三角形边缘创建。我们如何决定边缘是否是剪影的一部分?诀窍是基于漫射光模型,根据该模型,光强度是基于三角形法线和光矢量之间的点积,如果三角形远离光源,该点积运算的结果将小于或等于零。在这种情况下,光线根本不影响三角形。为了确定三角形边缘是否是剪影的一部分,我们需要找到共享相同边缘的相邻三角形,并计算原始三角形及其相邻的光线方向和法线之间的点积,如果一个三角形面向光,但其相邻的边则不会将边缘视为剪影边缘。

下图显示了2D对象简单轮廓产生:


红色箭头表示射中法线为1,2和3的三个边缘的光线(这些法线之间的点积与反向光矢量显然大于零), 法线为4,5和6的边缘面向远离光(这里相同的点积小于或等于零), 两个蓝色圆圈标记对象的轮廓,原因是边缘1面向光,但其相邻边缘6不是, 因此他们之间的点是一个轮廓, 另一个剪影点也是如此。

你可以看到,找到轮廓的算法非常简单。 然而,它要求我们知道每个三角形的三个相邻, 这被称为三角形的邻接。 不幸的是,Assimp不支持自动相邻计算,所以我们需要自己实现这样一个算法 在编码部分,我们将回顾一个能够满足我们需求的简单算法。

轮廓算法本身的最佳选择是什么? 请记住,我们需要在光矢量和三角正态之间做一个点积,以及三个相邻三角形的法线。 这需要我们访问整个模型信息, 所以VS还不够。 看起来GS更合适,因为它允许访问所有顶点。  幸运的是,OpenGL的设计人员已经给予了很多想法,并创建了一种称为“邻接三角形”的拓扑类型。 如果您提供具有邻接信息的顶点缓冲区,它将正确加载它,并为每个三角形提供GS顶点,而不是三个顶点。 附加的三个顶点属于相邻的三角形,不与当前三角形共享。 以下图像应该使这更清楚:

上图中的红色顶点属于原始三角形,蓝色的顶点是相邻的顶点(忽略边缘e1-e6,它们在代码部分稍后引用) 当我们以上述格式提供顶点缓冲器时,对每个顶点(相邻和不相邻)执行VS,并且在包含三角形及其相邻顶点的六个顶点的组中执行GS(如果存在)。 当GS存在时,由开发人员提供输出拓扑,但是如果没有GS,则光栅化知道如何处理这种方案,并且仅光栅化实际的三角形(忽略相邻的三角形)。 如果在使用英特尔HD 3000的Macbook上产生了错误,或者如果遇到类似的问题,只需使用通过GS的通行证,或者更改拓扑类型。

请注意,顶点缓冲区中的相邻顶点与常规顶点具有相同的格式和属性使它们相邻的只是它们在每组六个顶点内的相对位置。 在三角形连续的情况下,根据当前的三角形,相同的顶点有时会是规则的,有时是相邻的 由于节省顶点缓冲区中的空间,这使索引绘制更具吸引力。

源代码如下所示:

void Mesh::FindAdjacencies(const aiMesh* paiMesh, vector& Indices)
{ 
    for (uint i = 0 ; i < paiMesh->mNumFaces ; i++) {
        const aiFace& face = paiMesh->mFaces[i];

        Face Unique;

        // If a position vector is duplicated in the VB we fetch the 
        // index of the first occurrence.
        for (uint j = 0 ; j < 3 ; j++) { 
            uint Index = face.mIndices[j];
            aiVector3D& v = paiMesh->mVertices[Index];

            if (m_posMap.find(v) == m_posMap.end()) {
                m_posMap[v] = Index;
            }
            else {
                Index = m_posMap[v];
            } 

            Unique.Indices[j] = Index;
        }

        m_uniqueFaces.push_back(Unique);

        Edge e1(Unique.Indices[0], Unique.Indices[1]);
        Edge e2(Unique.Indices[1], Unique.Indices[2]);
        Edge e3(Unique.Indices[2], Unique.Indices[0]);

        m_indexMap[e1].AddNeigbor(i);
        m_indexMap[e2].AddNeigbor(i);
        m_indexMap[e3].AddNeigbor(i);
    }
大多数邻接逻辑都包含在上述函数和几个辅助结构中, 该算法由两个阶段组成。 在第一阶段,我们在每个边缘和共享它的两个三角形之间创建一个地图。 这在上面的for循环中发生在这个循环的前半部分,我们生成每个顶点位置和引用它的第一个索引之间的映射。 不同索引可能指向具有相同位置的顶点的原因是有时其他属性会强制Assimp将相同的顶点分割成两个顶点。 例如 相同的顶点对于共享它的两个相邻三角形可能具有不同的纹理属性 这对我们的邻接算法造成了一个问题,我们更喜欢每个顶点只显示一次。 因此,我们创建一个位置和第一个索引之间的映射,并且从现在开始只使用这个索引。

代码如下所示:

for (uint i = 0 ; i < paiMesh->mNumFaces ; i++) { 
        const Face& face = m_uniqueFaces[i];

        for (uint j = 0 ; j < 3 ; j++) { 
            Edge e(face.Indices[j], face.Indices[(j + 1) % 3]);
            assert(m_indexMap.find(e) != m_indexMap.end());
            Neighbors n = m_indexMap[e];
            uint OtherTri = n.GetOther(i);

            assert(OtherTri != -1)

            const Face& OtherFace = m_uniqueFaces[OtherTri];
            uint OppositeIndex = OtherFace.GetOppositeIndex(e);

            Indices.push_back(face.Indices[j]);
            Indices.push_back(OppositeIndex); 
        }
    } 
}
在第二阶段,我们用索引矢量填充每个匹配三角形列表的拓扑结构的六个顶点以及我们之前看到的邻接关系。 我们在第一阶段创建的地图在这里帮助我们,因为对于三角形中的每个边缘,很容易找到共享它的相邻三角形,然后找到与该边缘相反的三角形中的顶点。 循环中的最后两行将折叠缓冲区的内容从当前三角形的顶点和与当前三角形边缘相反的相邻三角形的顶点进行交替。

实现轮廓监测的Shader代码如下所示:

(silhouette.vs)
#version 330

layout (location = 0) in vec3 Position; 
layout (location = 1) in vec2 TexCoord; 
layout (location = 2) in vec3 Normal; 

out vec3 WorldPos0; 

uniform mat4 gWVP; 
uniform mat4 gWorld; 

void main() 
{ 
    vec4 PosL = vec4(Position, 1.0);
    gl_Position = gWVP * PosL;
    WorldPos0 = (gWorld * PosL).xyz; 
}
我们只需要使用WVP矩阵将位置转换为裁剪空间,并向GS提供世界空间中的顶点(因为轮廓算法在世界空间中发生)。

(silhouette.gs)
#version 330

layout (triangles_adjacency) in;
layout (line_strip, max_vertices = 6) out;

in vec3 WorldPos0[];

void EmitLine(int StartIndex, int EndIndex)
{
    gl_Position = gl_in[StartIndex].gl_Position;
    EmitVertex();

    gl_Position = gl_in[EndIndex].gl_Position;
    EmitVertex();

    EndPrimitive();
}

uniform vec3 gLightPos;

void main()
{
    vec3 e1 = WorldPos0[2] - WorldPos0[0];
    vec3 e2 = WorldPos0[4] - WorldPos0[0];
    vec3 e3 = WorldPos0[1] - WorldPos0[0];
    vec3 e4 = WorldPos0[3] - WorldPos0[2];
    vec3 e5 = WorldPos0[4] - WorldPos0[2];
    vec3 e6 = WorldPos0[5] - WorldPos0[0];

    vec3 Normal = cross(e1,e2);
    vec3 LightDir = gLightPos - WorldPos0[0];

    if (dot(Normal, LightDir) > 0.00001) {

        Normal = cross(e3,e1);

        if (dot(Normal, LightDir) <= 0) {
            EmitLine(0, 2);
        }

        Normal = cross(e4,e5);
        LightDir = gLightPos - WorldPos0[2];

        if (dot(Normal, LightDir) <=0) {
            EmitLine(2, 4);
        }

        Normal = cross(e2,e6);
        LightDir = gLightPos - WorldPos0[4];

        if (dot(Normal, LightDir) <= 0) {
            EmitLine(4, 0);
        }
    }
}

所有轮廓逻辑都包含在GS中,当使用具有相邻拓扑的三角形列表时,GS接收六个顶点的数组。我们首先计算一些选定的边,这将有助于我们计算当前三角形法线以及三个相邻的三角形。使用上图来了解如何将e1-e6映射到实际边。然后我们通过计算其法线和光方向(光向量朝向光)之间的点积来检查三角形是否面向光。如果点积的结果为正,则答案为是(由于浮点不准确,我们使用小的epsilon),如果三角形不面对光,那么这是光就对它不起作用,但是如果它是光面对的,我们在光矢量和三个相邻三角形中的每一个之间进行相同的点积运算。如果我们碰到一个不面向光的相邻三角形,我们称之为EmitLine()函数(不出意料的)发出三角形(面向光)和它的相邻(没有)之间的共享边, FS只是把这边缘画成红色。

实现的主要函数如下所示:

void RenderScene()
{
    // Render the object as-is
    m_LightingTech.Enable();

    Pipeline p;
    p.SetPerspectiveProj(m_persProjInfo);
    p.SetCamera(m_pGameCamera->GetPos(), m_pGameCamera->GetTarget(), m_pGameCamera->GetUp()); 
    p.WorldPos(m_boxPos);
    m_LightingTech.SetWorldMatrix(p.GetWorldTrans()); 
    m_LightingTech.SetWVP(p.GetWVPTrans()); 
    m_mesh.Render();

    // Render the object's silhouette
    m_silhouetteTech.Enable();

    m_silhouetteTech.SetWorldMatrix(p.GetWorldTrans()); 
    m_silhouetteTech.SetWVP(p.GetWVPTrans()); 
    m_silhouetteTech.SetLightPos(Vector3f(0.0f, 10.0f, 0.0f));

    glLineWidth(5.0f);

    m_mesh.Render(); 
}
这就是我们如何使用轮廓技术相同的对象被渲染两次。 首先用标准的照明着色器 然后与轮廓着色器。 请注意,如何使用glLightWidth()函数来使轮廓更粗大,从而更加明显。

如果您使用上述代码创建演示,您可能会注意到轮廓线周围的轻微变化。 原因是第二个渲染生成与原始网格边缘大致相同深度的线。 这导致一种被称为Z作为轮廓的像素现象,并且原始网格以不一致的方式彼此覆盖(再次,由于浮点精度)。 为了解决这个问题,我们调用glDepthFunc(GL_LEQUAL)来放宽深度测试。 这意味着如果第二个像素呈现在具有相同深度的先前像素的顶部,则最后一个像素总是优先。



作者:jxw167 发表于2017/3/23 16:16:30 原文链接
阅读:133 评论:0 查看评论
Viewing all 5930 articles
Browse latest View live


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