移动端高级架构 – 饿了么物流移动端性监控体系

本文已获饿了么物流技术团队授权转载发布
原文地址: /juejin.im/post/5c3577e0f265da616c65ca80?utm_source=gold_browser_extension

作者简介

锦洋,负责饿了么蜂鸟APP的架构、研发等工作。目前关注的技术方向为移动端监控、移动端架构、移动端性能优化等方向。

在这个重视稳定性的年代,很多公司在移动端性能监控上花了很大的力气,对业务可用性监控的投入不足,但是移动端可用是由性能可用和业务可用共同组成,缺一不可,因为业 界性能监控已经比较成熟,有很多第三方的平台,所以避开性能监控不谈,下面介绍一下饿了么物流移动端在业务可用性监控体系建设上的一些探索。

饿了么物流移动端作为骑手直接使用的配送工具,需要每天承载千万量级的配送单量,骑手app具备以下三个特点:时效要求高,网络环境复杂,重度使用。骑手需要在30分钟内将订单配送到用户手中,中间实施多次订单操作,可谓争分夺秒,如果遇到网络或者定位异常都可能导致有损操作,为了保证骑手的操作顺畅,我们需要将骑手整个配送过程纳入可用性监控体系建设中,经过长期的探索,我们建立一套自己的移动端业务监控体系。

整个监控体系就像一个数据漏斗:

第一层 E-Monitor:全局业务监控,纵观全局,掌握业务大盘趋势;

第二层 TimeBomb:异常事件监控,定点插针,实时报警;

第三层 Dogger:单点日志监控,全量日志,还原现场;

第四层 EDW:离线大数据,T+1报表,大数据分析业务健康度;

下面给大家详细介绍一下这四层监控

E-Monitor

大家都知道作为移动端本身不需要对接口监控敏感,因为后端有各种维度的API服务监控,但是App作为上层应用,接口的成功失败,并不能完全替代用户的感知,这是有调用方的特征决定的。一次业务请求包含:准备请求数据->发送请求->网络链路->请求回调->解析->渲染,任何一个环节的失败都可能代表着用户的一次交互失败,所以要想完全掌控线上大盘的核心功能使用曲线,完全依赖后端接口监控是不行的,必须要梳理调用链路,搭建客户端业务监控。

作为全局业务监控只看单个用户的数据是没有什么意义的,需要将所有用户的数据采集,存储,可视化。这里数据的采集我们使用饿了么MT部门提供的Skynet作为采集和上传通道,它具备编译期AOP插入,序列化存储,针对移动端优化的对齐上传等特性,保证了数据采集上传的可靠,稳定。

而在服务端的存储和可视化我们采用LinDB+E-Monitor的监控架构,LinDB是一款优秀的时间序列数据库,适合存储设备性能、日志等带时间戳的数据。能轻松处理高写入和高查询负载。配合E-Monitor强大的可视化能力,可以完美展现骑手订单操作主流程的稳定情况,异常报警。

报警的策略有以下几种:移动端常用的是阈值模型和趋势模型配合同环比

最终生成一个大盘的监控面板,这里因为铭感数据只放出了部分脱敏面板

TimeBomb

TimeBomb-定时炸弹,从名字就可以猜到它和异常有关,它专门负责监控在规定时间和次数限定下没有达成用户交互结果的逻辑,TimeBomb作为全局业务监控的补充,在排查异常中立下汗马功劳。它的设计初衷是通过简单的代码插入,由计数,时间间隔等条件触发异常事件上传, 适用于:

  1. 登录多次登录不上
  2. 多次点击确认送达,都失败
  3. 定位一直报错
  4. 定位上传多次失败
  5. 任何接⼝口多次报错

等等…

总结来说,TimeBomb可以随意定义异常的监控力度,并且可以灵活的远端配置次数,时间,采样率。

TimeBomb的数据采集和展示是通过我们自研的服务,主要包括两个功能:

  1. 根据选择的Tag和时间显示异常曲线
  2. 选择节点之后就可以查看异常明细列表 在Parameter中查看异常上报的次数和时间配置还有上报的原因,点击日志拉取就可以拉取到用户的详细日志数据。

骑手App通过TimeBomb完成了很多异常问题的上报,修复,观测,优化,再观测,这是一个异常问题解决的正向循环,而且特别适合一些需要多轮验证的极端case的排查观测。

Dogger

Dogger包含两个部分:

  1. Trojan日志写入上传SDK
  2. Dogger-Service 日志解析服务

Trojan是一个面对高性能,极致体验要求下,产出的轻量级,高效率的移动端日志监控方案,它就像一只听话的狗狗,他在客户端默默的记录着用户的各项操作日志和技术性能埋点,最终在需要的时候,把日志抛上来,交给Dogger-Service解析,通过完善的埋点,我们可以很快的还原骑手的操作现场,借助对特定日志的横向分析,可以帮我们快速定位问题。

Trojan具备以下四个特点:

  • 第一点:开发透明,使用者不需要关心日志文件的读写。
  • 第二点:高效收集,一.采用AOP技术植入到埋点自动收集日志,二.高效的文件读写方式,毫秒级别的耗时。
  • 第三点:敏感加密,对于用户相关的敏感信息,为保证日志的安全性,我们提供加密方式,比如说DES、AES。
  • 第四点:流量开销低,日志写在本地,指定用户压缩上传。

Trojan的架构图:

Trojan 用C的方式通过mmap(内存映射)的方案写入日志,对比java api的写入方式性能提高了一倍,低CPU,低内存消耗。

在性能监控这种大数据量写入的场景上满足了我们的需求,再配合文件的gzip压缩可以将日志这种多重复字母的文件达到50倍的压缩效果,实测一个43M的文件,压缩上传只要860kb。1M以内的文件上传对移动端来说也是一个可以接受的大小,这也对未来trojan除了完成逻辑回放,提供了可能。

经过三个版本的迭代,Trojan已经涵盖了用户的点击事件,页面生命周期,请求监控,流量,电量,内存,线程等方方面面, 文件的写入和上传都完成了,那一个几十M的文件该如何分析尼?下面就介绍一下我们的Trojan配套解析服务Dogger-Service。

Dogger-Service 主要的功能分为三个部分:

  1. ActionChart-全局展示骑手一天的页面跳转,电量,内存,网络切换,定位频率,特定请求频率等

通过ActionChart我们可以直观的看出骑手在时空坐标系下的操作和资源使用情况,可以方便协助我们观察某个时间点出现某个问题的环境,这种全局掌控用户操作可能是行业内第一次达到。

  1. Origin -可以轻松的完成百兆以内的文件解析和展示,按时间查找,全局高亮搜索等功能

通过对原始数据解析,我们可以拖拽时间滑片,直接定位到某个时间段查看骑手的日志明细,也可以选择某个关注的Tag,或者直接通过关键字搜索高亮查找,Origin模块让我们可以灵活的查找问题的蛛丝马迹,给定位问题的root cause提供了保障。

这样就够了吗?

  1. Statistics – 统计模块是基于特定的Tag数据,数据挖掘分析和展示。

当前实现了对电量,网络,流量,卡顿,请求,生命周期,内存,定位的数据分析。比如下面的内存分析,我们可以通过最长间隔,知道骑手有哪些时间段app是关闭着的,内存的峰值和低谷,平均内存各是多少,内存波动比较大的时间段是哪几个,波动大代表着资源开销可能异常,是需要仔细排查的点。

可以Loc Tag查看骑手的定位轨迹,分析是否有定位漂移或者定位失败情况

还可以通过PunchLoc Tag查看定位上传的失败占比,分析失败的原因是否和当时的网络状态有关

通过THttpReq可以查看网络请求的Host和Path占比情况,方便优化请求流量

Trojan和Dogger-Service组成了Dogger这个有机的整体,日志和解析配合,可以让我们在排查单个case的时候,对用户的行为了如指掌,丰富的埋点数据可以为我们的排查提供数据支撑。

目前Dogger服务中的日志写入sdk Trojan已经开源,欢迎交流学习

当我们有了实时的全局大盘和异常监控,还有单个用户的全周期日志数据,就够了吗?

大盘的曲线正常,异常的毛刺消除只能代表业务大盘稳定,但是业务功能真实的质量还不能一概而论,这时我们需要对数据漏斗的终点—离线数据池进行大数据挖掘分析来做最后的监控兜底。

下面介绍一下最后一层监控EDW

EDW:

离线报表监控作为全局大盘的另一种视角,E-Monitor属于实时大盘监控,只能观察实时曲线趋势和昨天做对比,判断粗粒度的业务是否异常,但是离线数据可以挖掘分析完整一天的数据,细粒度的判断每一个订单的健康程度,聚合定位失败的原因占比,获取复杂条件筛选出的各种比例,让我们从上帝视角观察整个业务线,评估线上业务健康度,分析趋势,表征产出,是移动端监控体系中不可或缺的利器。

公司大数据平台部自研的edw为我们提供了优质的离线大数据服务,它融合了即时查询、数据抽取、数据计算、数据推送、元数据管理、数据监控等多种数据服务的平台型产品。

当前我们在流量,定位质量,骑手多设备使用,离线送达,推送质量,订单异常等关键业务场景都有完备的离线报表。比如上图的流量报表,可以知晓线上流量消耗Top 100的骑手device_id 和流量数据,而排行第一的骑手response数据远大于request数据,通过Dogger拉日志后发现,骑手有多次下载app的行为。第二幅图则是线上主流程的偏向业务的流转时长监控,因为数据敏感所以打码了。这些报表可以说明线上业务的真实健康度,这一点能够让我们对全局的把控更有自信。基于离线数据的聚合分析,可以发现优化点,为改善方案提供依据。

实战

这里记录一下最近发生的一次网络层问题的排查过程,让大家直观感受这几层监控的作用。

第一步:我们的gafana的监控发现Android骑手的订单相关请求平均成功率降低到了98.69%,而正常请求成功率应该在99%以上

上面说到grafana属于可用性全局监控,如果这边的数据异常,将会影响全盘,所以我们不敢怠慢,立马着手排查。

首先我们怀疑是DNS解析问题,我们通过EDW拉取了出现问题骑手的id,然后配置了Dogger的骑手日志拉取,经过分析发现,DNS失败的场景多发生在断网等弱网环境,属于正常情况,而且我们发现日志上出问题的请求的requestID在后端的trace系统上都查不到,查看了skynet网络监控拦截器的代码

apmNetInterceptor插在最后一个,数据没有传上去,说明请求在发送前就已经抛了错,所以我们开始排查请求发送前的逻辑。 第二步:通过EDW抽取出现问题骑手,对他们的请求失败原因聚合,得到了ioException异常占比最大

第三步:由于请求前的日志数据过少,所以我们升级了okhttp到3.11,使用EventListener来获取请求生命周期埋点,针对上报问题的骑手发了内测版本,希望获得出问题请求的链路明细。

完整的链路大致如下:

再次捞出有问题骑手的日志,发现有些时候网络状态是良好的,但是在 responseHeaderStart之后会直接抛错或者是 timeout:

于是我们撸了多遍okhttp的源码,觉得应该是连接池复用的问题,复用了已经失效的连接. 我们又加入 IOException 的 stacktrace日志.发现一个奇怪的问题:

线上的请求走的竟然是http/2的协议,仔细阅读Okhttp 握手相关的代码发现,Okhttp 在 https的情况下会判断服务端是否支持 http/2,如果支持则会走 http/2的协议,相关代码参见RealConnection.java的establishProtocol方法。

 private void establishProtocol(ConnectionSpecSelector connectionSpecSelector,      int pingIntervalMillis, Call call, EventListener eventListener) throws IOException {    if (route.address().sslSocketFactory() == null) {      if (route.address().protocols().contains(Protocol.H2_PRIOR_KNOWLEDGE)) {        socket = rawSocket;        protocol = Protocol.H2_PRIOR_KNOWLEDGE;        startHttp2(pingIntervalMillis);        return;      }      socket = rawSocket;      protocol = Protocol.HTTP_1_1;      return;    }    eventListener.secureConnectStart(call);    connectTls(connectionSpecSelector);    eventListener.secureConnectEnd(call, handshake);    if (protocol == Protocol.HTTP_2) {      startHttp2(pingIntervalMillis);    }  }

最终发现的确是 Okhttp在 http/2上对连接池的复用问题存在 bug ,在StreamAllocation.java上

 public void streamFailed(IOException e) {    boolean noNewStreams = false;    synchronized (connectionPool) {      if (e instanceof StreamResetException) {      } else if (connection != null          && (!connection.isMultiplexed() || e instanceof ConnectionShutdownException)) {        noNewStreams = true;      }      socket = deallocate(noNewStreams, false, true);      }  }

当协议是http/2的时候,noNewStreams为false 而在ConnectionPool.java的connectionBecameIdle就不会将这个connection从ConnectionPool中移除

  boolean connectionBecameIdle(RealConnection connection) {    if (connection.noNewStreams || maxIdleConnections == 0) {      connections.remove(connection);      return true;    } else {    }  }

结合前段时间,后端将路由层切换了公司的SoPush服务上,而SoPush是支持Http/2的,切换的时间和曲线异常时间吻合,可以确定问题就在这里。

线上使用的Okhttp版本还是3.8.4, 在Okhttp 3.10.0版本之后,加入了对http2的连接池中的连接做了严格的ping验证, 下面是 changelog

可以看到 http/2 才刚加Ping机制,所以OKhttp对Http/2支持有问题的版本是<3.10,但是即使使用了3.11的最新版本依旧有一定概率发生这个问题.于是我们觉得先强制指定Android版本的协议为http/1.1,后面接入集团的网络库再支持Http/2。修改完再次发布内测版本,曲线恢复正常,问题解决。

这次网络层的排查,我们使用E-Monitor监控和分析问题严重程度,EDW离线数据过滤出问题骑手ID,Dogger单点日志灵活埋点验证修复方案,就这样一次由第三方变动引起的客户端可用性异常就这样解决了,业务可用性监控功不可没。

总结:

上面就是饿了么物流移动当前在业务可用性监控领域做出的一些探索,我们按照数据漏斗,从全量埋点,异常监控,单用户日志,离线数据,由面到点再到面,每一个切面都插入了不同纬度的监控,希望能做到全面覆盖业务,稳定性和质量兼顾,因为我们深信,只有做好监控,才能做到可报警,可排查,可优化。但是移动端可用性监控是一个长期建设的工程,需要不断的优化迭代,当前我们也面临数据冗余和性能监控冲突,监控本身带来的性能损耗等问题,未来我们也将在这些问题上做进一步探索、实践和分享。

扫描识别二维码,关注~

陈不二
博客地址:
 /www.chenbu2.com
简书地址: /www.jianshu.com/u/f4874eb81c5b
GitHub地址: supervolition
爱学啊合作讲师: musculomembranous
公众号: 陈不2
技术交流QQ群: 858499698

****欢迎一块交流学习~****

喜欢的朋友记得点赞转发分享哦

客户端log日志系统

背景

我们在开发过程中出现了个别bug,而我们往往很难定位到问题所在,这个不仅仅局限于移动端,只是移动端不容易定位问题。我们常见的实现方式可能是,

1. 创建一个带队列的线程。

2. 把要上报的数据抛入线程队列中。

3. 数据过多本地写入文件。

4. 线程异步开始上报。

5. 上报完成后删除本地文件。

但这个实现方案经常会有一些问题:

1. 上层写入数据过快,写文件线程来不及写入文件,此时应用发生crash或app被杀,导致上报数据丢失。

2. 多业务场景很难复用。

3. 文件序列化经常出现问题,导致数据丢失。

4. 不跨平台,安卓和iOS通常出现不同的实现,总是各自出现不同的问题,不容易定位,统一解决。

因为我们对数据要求很高,完全不允许数据出现丢失。所以针对上面这种方案的实现是不允许的。所以我们想针对这种不允许数据丢失的需求,开发一套准实时数据上报组件,感兴趣的同学可以一块研究研究。

我们希望有以下功能:

1. 数据不丢失,app发生crash或者异常被杀,数据不丢失,下次启动会再次被上报

2. 数据上报有序性,上报数据会保证与push进来的数据顺序是一致的,不会造成数据乱序

3. 上报效率高,可配置单次上报数量,当缓存数据过多时,可进行一次上报多条数据,减少链接次数

4. 多业务服用,多业务可创建不同实例,进行上报,互不影响

5. 跨平台,c++实现,Android/iOS共用一套代码,逻辑完全统一, 不会存在出现不同异常现象

5. 效率高,底层采用c++实现,同时采用单线程模型进程上报,多业务共用一个线程,有效节省开销。当上报数据为空时线程进入休眠。

6. 异常兼容好,在最恶劣的情况,手机系统突然挂掉或异常关机,(正常关机不受影响)。若此时正在写入数据,可能导致数据的不完整性,对常规的序列化方式,整个数据会导致不可用。

这里推荐2款个人比较看好的log日志系统:

DataReporter

git地址:

/github.com/luojilab/DataReporter

原理:

为了保证数据在app未被卸载和手机异常关机的情况下,能够100%不丢失。这套方案我们参考了目前比较流行的。mmap。因为mmap可以创建一块内存,把内存映射到文件。这块内存的管理由操作系统来维护。只要操作系统不出现异常挂掉。该内存的数据是不会丢失的。即使当前的App已经被杀死后者发生crash。下次app启动,用之前的文件映射去调用系统接口,还是能换出之前crash之前的内存数据。这样就保证了数据在app crash或者异常被杀掉的情况下,数据不丢失。因为最早mmap是用来进程间传递数据使用,所以具有这种进程挂掉数据不丢失的特点。

目前采用mmap作为数据缓冲的项目已经呈雨后春笋般。像腾讯的xlog和MMKV都采用了mmap技术。

原理图

如图:DataReporter大致原理如图所示, 业务调用主要接口只有一个push。上层调用push接口,把数据push到PushBuffer中,然后唤起上报方法Report。Report方法,copy PushBuffer中数据到UploadBuffer,这样做是为了保证上报数据和push数据独立,同时不block数据的push。上层可以畅通的调用push。UploadBuffer中的数据通过,上层实现的Upload接口进行上报。上报成功后通过UploadSuccess接口通知底层组件。底层根据上传成功后条数,开始上报下一批数据。当某批次数据上报失败。通过UploadFiald接口通知,底层组件,底层开始进行delay之后再次上报。因为如果一直进行错误尝试,会导致上报尝试过频繁。导致服务器压力过大。所以上报失败一次,进行了5秒的delay。再次失败,时间累计。如果上层想马上再次尝试,可以使用reaWaken接口,立马开始错误重报。

当PushBuffer数据过多。开始触发写文件操作。写文件方法WriToFile方法,开始把PushBuffer数据拷贝到WriteBuffer,并开始写入磁盘。当上报方法Reporter发现磁盘有文件时,先进行文件的上报,再进行PushBuffer内存的上报。保证上报的顺序性。

这里还是要提一下腾讯的xlog,这个代码在进行数据写文件时,没有使用mmap作为buffer,而是直接使用的普通内存。所以在拷贝文件过程中,如果出现crash。拷贝buffer中的数据就会丢失,所以还是存在很大风险的。希望后面他们能改进。

数据组织方式:

本地数据的序列化DataReporter采用了强纠错的结构。

数据结构

如图:每条数据都保存数据crc。当某条数据写入一半时,发生crash,单条数据损坏。这时不应该整个数据丢弃,而只应该丢弃单条,这种如果用平台实现,就很难实现这么细的粒度。但是我们用c++独立设计数据存储结构。实现单条数据损坏,只丢弃单条数据。整体数据稳定性要稳健很多。

注意:

在调用Release之后,其他方法都不能再被调用。一个好的处理方式,是方法的调用都放在ui线程。不用担心会导致ui线程耗时。底层没有任何耗时或者io的操作,io都是异步的io。不会有任何block操作。

多业务多接口:

对于多业务 多接口可以创建不同的实例。只要不同实例配置的缓存路径不同即可。不同业务的上报互不影响。但是共用一个线程。所以不用担心性能损耗,已经控制的非常细致了。

Logan

git地址:

/github.com/Meituan-Dianping/Logan

原理:

Logan是美团点评集团移动端基础日志组件,这个名称是Log和An的组合,代表个体日志服务。同时Logan也是“金刚狼”大叔的名号,当然我们更希望这个产品能像金刚狼大叔一样犀利。

Logan已经稳定迭代了一年多的时间。目前美团点评绝大多数App已经接入并使用Logan进行日志收集、上传、分析。近日,我们决定开源Logan生态体系中的存储SDK部分(Android/iOS),个人感觉比者成熟很多,各位可以参考参考。

这里就不做详细介绍了。

文档地址:

/tech.meituan.com/logan_open_source.html

长按(或扫码)识别二维码,关注~

陈不二
博客地址:
5672642236
简书地址:
card clothier
GitHub地址:
(747) 221-4163
爱学啊合作讲师:
/www.ixuea.com/
公众号:
陈不2
技术交流QQ群:
858499698

欢迎一块交流学习~

喜欢的朋友记得点赞转发分享哦!

Facebook开源的一款Android调试工具

概述

Stetho是Facebook开源的一个Android调试工具,git地址:facebook/stetho
通过Stetho,开发者可以使用chrome的inspect功能,对Android应用进行调试和查看。

功能概述

Stetho提供的功能主要有:

Network Inspection:网络抓包,如果你使用的是当前流行的OkHttp或者Android自带的 HttpURLConnection等,你可以轻松地在chrome inspect窗口的network一栏抓到所有的网络请求和回包。
Database Inspection:数据库查看,可以直接看到当前应用的sqlite数据库。
View Hierarchy:布局层级查看,免去使用查看布局边界的花花绿绿带来的痛苦和卡顿,并且能看到每个view和layout的各类属性。
Dump App:命令行拓展,构造了一个命令行与Android App的交互通道,在命令行输入一行命令,App可以收到并且在命令行上进行反馈输出。
Javascript Console:Javascript控制台,在inspect的console窗口,输入Javascript可以直接进行Java调用。使用这个功能,得先引入facebook/stethostetho-js-rhino和mozilla/rhino。
到这里大概了解了它的用处,感兴趣的同学可以去看看源码,本文这里只介绍其用法。

1、项目集成

1、集成项目依赖

    implementation 'com.facebook.stetho:stetho:1.5.0'
    implementation 'com.facebook.stetho:stetho-okhttp3:1.5.0'

gradle3.0之前版本添加依赖方式,这里就不做多介绍了,可自行搜。。。

    compile 'com.facebook.stetho:stetho:1.5.0'
    compile 'com.facebook.stetho:stetho-okhttp3:1.5.0'

2、初始化

private fun initStetho() {
        Stetho.initializeWithDefaults(this);
    }

3、配置拦截器
Java:

 builder.addNetworkInterceptor(new StethoInterceptor())

Kotlin:

 builder.addNetworkInterceptor(StethoInterceptor())

2、查看相关数据

打开Chrome浏览器,输入

chrome://inspect/#devices

看到如下界面(需要翻墙):

inspect

然后可以看到如下界面:

View

请求网络我们可以看到如下:

network

是不是很方便,我们可以看到Status等栏目都会变化,展示httpcode,请求耗时、回包数据类型等信息。

network

databases

官方对dump app的api说明实在太少,有需要大家可以去研究研究,即首先需要在App内,通过enableDumpapp方法注册自己的插件:

.enableDumpapp(new DumperPluginsProvider() {  @Override
  public Iterable<DumperPlugin> get() {    return new Stetho.DefaultDumperPluginsBuilder(context)
        .provide(new MyDumperPlugin())
        .finish();
  }
})
.enableWebKitInspector(Stetho.defaultInspectorModulesProvider(context))
.build())

对于Console,大家也可以试试,复制下面代码:

importPackage(android.widget);
importPackage(android.os);var handler = new Handler(Looper.getMainLooper());
handler.post(function() { Toast.makeText(context, "Hello from JavaScript", Toast.LENGTH_LONG).show() });

总结

总的来说这块开发工具还是不错的,虽然可以日志输出,但是查看比较麻烦。有时间可以去看看源码,学习学习,这里就不叙述了。

大家在使用的时候切记仅debug模式开放哈~
大家在使用的时候切记仅debug模式开放哈~
大家在使用的时候切记仅debug模式开放哈~

最后,大家动动小手关注一波吧~

长按(或扫码)识别二维码,关注~

陈不二
博客地址:
/www.chenbu2.com
简书地址:
/www.jianshu.com/u/f4874eb81c5b
GitHub地址:
(229) 930-4752
爱学啊合作讲师:
/www.ixuea.com/
公众号:
陈不2
技术交流QQ群:
858499698

欢迎一块交流学习~

喜欢的朋友记得点赞转发分享哦!

让 Android 开发像 web 开发一样爽

原文链接: /juejin.im/post/5bf12c8751882511a8527ed4?utm_source=gold_browser_extension

做移动端开发,做蛋疼的就是不能动态发版,不能像 web 那样发版立即全部用户生效,然而 3407791222语言 为其提供了可能性。使用 lua 来构建跨平台原生应用有许多好处,比如 lua 语言简洁高效,可移植性好, Lua虚拟机极为轻量,仅占用200到300k的内存空间,且速度极快。

演示

写一个简单的代码演示一下。新建一个 lua 文件,叫做 view.lua, 放在手机的 sdcard 上,文件目录为 /sdcard/view.lua

require "import"
import "android.widget.*"
import "android.content.*"

function getView()
    local layout = {
        LinearLayout,
        orientation = "vertical",
        layout_width = "fill",
        layout_height = "fill",
        {
            Button,
            id = "btn",
            layout_marginTop="8dp",
            layout_width = "fill",
            layout_height = "50dp",
            text = "click"
        },
    }
    local view = loadlayout(layout)
    return view
end

运行一下,

屏幕中上半部分是 Android 的 xml 布局中写好的代码,当点击运行按钮时,加载 lua 脚本,返回一个 View 对象,然后添加到布局中。一个简单的 lua 脚本编写的视图就写好了。 接下来修改一下,设置个点击事件。

require "import"
import "android.widget.*"
import "android.content.*"

function getView()
    local layout = {
        LinearLayout,
        orientation = "vertical",
        layout_width = "fill",
        layout_height = "fill",
        {
            Button,
            id = "btn",
            layout_marginTop="8dp",
            layout_width = "fill",
            layout_height = "50dp",
            text = "click"
        },
    }
    local ids = {} -- store ids to find view
    local view = loadlayout(layout, ids)
    ids.btn.onClick = function()
        Toast.makeText(activity,"2333",0).show()
    end
    return view
end

运行效果

再来个稍微复杂点的例子,写个列表,新建 list.lua 文件,放在手机的 sdcard/list.lua

require "import"
import "android.widget.*"
import "android.content.*"
import "android.view.View"
import "androlua.LuaHttp"
import "androlua.LuaAdapter"
import "androlua.LuaImageLoader"

local JSON = require("cjson")
local uihelper = require('uihelper')

-- create view table
local layout = {
    LinearLayout,
    orientation = "vertical",
    layout_width = "fill",
    layout_height = "fill",
    {
        ListView,
        id = "listview",
        dividerHeight = 0,
        layout_width = "fill",
        layout_height = "fill",
    },
}

local item_view = {
    FrameLayout,
    layout_width = "fill",
    layout_height = "240dp",
    {
        ImageView,
        id = "iv_image",
        layout_width = "fill",
        layout_height = "fill",
        scaleType = "centerCrop",
    },
    {
        TextView,
        id = "tv_title",
        background = "#66000000",
        layout_width = "fill",
        layout_height = "fill",
        padding = "32dp",
        gravity = "center",
        maxLines = "5",
        lineSpacingMultiplier = '1.2',
        textSize = "14sp",
        textColor = "#CCFFFFFF",
    },
}


local data = {
    dailyList = {}
}
local adapter

local function getData()
    -- /baobab.kaiyanapp.com/api/v1/feed
    local url = data.nextPageUrl
    if url == nil then url = '/baobab.kaiyanapp.com/api/v1/feed?udid=3e7ee30c6fc0004a773dc33b0597b5732b145c04' end
    if url:find('udid=') == nil then url = url .. '&udid=3e7ee30c6fc0004a773dc33b0597b5732b145c04' end
    print(url)
    LuaHttp.request({ url = url }, function(error, code, body)
        if error or code ~= 200 then
            print('fetch data error')
            return
        end
        local str = JSON.decode(body)
        uihelper.runOnUiThread(activity, function()
            data.nextPageUrl = str.nextPageUrl
            local list = str.dailyList[1].videoList
            for i = 1, #list do
                data.dailyList[#data.dailyList + 1] = list[i]
            end
            adapter.notifyDataSetChanged()
        end)
    end)
end

local function launchDetail(item)
    Toast.makeText(activity, item.title, 0).show()
end

function getView()
    local view = loadlayout(layout)
    adapter = LuaAdapter(luajava.createProxy("androlua.LuaAdapter$AdapterCreator", {
        getCount = function() return #data.dailyList end,
        getItem = function(position) return nil end,
        getItemId = function(position) return position end,
        getView = function(position, convertView, parent)
            position = position + 1 -- lua 索引从 1开始
            if position == #data.dailyList then
                getData()
            end
            if convertView == nil then
                local views = {} -- store views
                convertView = loadlayout(item_view, views, ListView)
                if parent then
                    local params = convertView.getLayoutParams()
                    params.width = parent.getWidth()
                end
                convertView.setTag(views)
            end
            local views = convertView.getTag()
            local item = data.dailyList[position]
            if item then
                LuaImageLoader.load(views.iv_image, item.coverForFeed)
                views.tv_title.setText(item.title)
            end
            return convertView
        end
    }))
    listview.setAdapter(adapter)
    listview.setOnItemClickListener(luajava.createProxy("android.widget.AdapterView$OnItemClickListener", {
        onItemClick = function(adapter, view, position, id)
            launchDetail(data.dailyList[position + 1])
        end,
    }))
    getData()
    return view
end

创建 listView , 设置 adapter ,网络请求,刷新列表。看下效果吧。

代码放到了 github 👉 源码

原理图

写了几篇文章比较详细的介绍了原理,想了解的可以看一下

支持 iOS 吗?

Lua 是用 c 语言开发的,可移植性比较好,想支持 iOS 的话,原理时一样的,不过参考目前已有的跨平台技术。关于跨平台方面的一些个人见解,目前已有的跨平台技术每当涉及到不同平台的特性时,事情就比较蛋疼了,需要单独去适配,还有建立一堆连接库,比如选取本地图片,不同平台的数据库,平台特有 api,真是一份代码到处运行终是梦,一份儿代码到处采坑才是真。

Android 开发能支持到什么程度?

看到了上面的原理图就可以知道,支持 Android SDK 几乎所有的 API。

   长按(或扫码)识别二维码,关注~

陈不二
博客地址:
/www.chenbu2.com
简书地址:
(201) 427-8608
GitHub地址:
/github.com/ChenBu2
爱学啊合作讲师:
grippleness
公众号:
陈不2
技术交流QQ群:
858499698

欢迎一块交流学习~

喜欢的朋友记得点赞转发分享哦!

React Native动态表单 新框架 新设计

作者:i校长 | 链接
2245171950

简述

在脚本语言里,设计一个动态表单,确实比java要容易的多,下面介绍一个动态表单框架,在阅读了网络很多开源的动态表单设计源码之后,我们决定自己动手做一个,使用了ES6的特性,也解决了我们一些问题,在动态扩展这个设计上要比其他的框架要好很多,欢迎参考或者吐槽,如有不对的设计欢迎指正。

项目 概述

React Native动态生成表单项目,根据特定的领域模型,自动匹配UI组件,自动映射数据。支持动态扩展组件。

架构图

framwork.png

整体结构分为四大模块,如下

Form

用于接收json数据,基本数据模型已经定义好如下:

{
            type: "input",  /定义类型,输入框
            key: "username", /定义key,通过该key可以获取到该组件对象,方便自定义操作
            title: "用户名", /组件标题
            icon: 'userName', /图片名称
            placeholder: '请输入用户名' /输入框提示
        }

当然你可以自定义模型,目前唯一的约束就是type与key,必须在数据模型中存在。

ComponentFactory

用于注册自定义的组件,给Form层提供组件,

ComponentBuilder

用于组件的构建,构建完后会自动注册组件到ComponentFactory中,往Form中添加新的组件时,就要完成该层的创建。

Components

React Native中自定义组件,都是继承自React.Component,都是一些组合组件。

计划

  • 添加到npm仓库中
  • 数据通过async-validator校验
  • 添加选择框,日期框,开关按钮等常用组件。

用例

export default class App extends Component<Props> {
  render() {

    let items =  [
        {
            type: "input",
            key: "username",
            title: "用户名",
            icon: 'userName',
            placeholder: '请输入用户名'
        },
        {
            type: "input",
            key: "password",
            title: "密码",
            icon: 'password',
            placeholder: '请输入密码'
        },
    ];

    return (
      <View style={styles.container}>
        <Form ref={(c) => {this.form = c;}} items={items}/>
      </View>
    );
  }
}

上面是基本使用方法,如何获取里面输入框里面的值呢,如下:

onPress() {
        const value = this.form.getValue();
        alert(JSON.stringify(value));
    }

只需要调用 form表单的getValue方法,便可以返回如下:

{
    "username": "zhang" ,
    "password": "234"
}

获取姓名对应的组件如下:

const input = this.form.getComponentByKey("username");

等等…

License

DynamicForm is released under the Apache License.

项目地址

DynamicForm

 

   长按(或扫码)识别二维码,关注~

陈不二,喜欢技术,欢迎一块交流学习~
大家可以关注我的个人博客:
/www.chenbu2.com
简书地址:
/www.jianshu.com/u/f4874eb81c5b
GitHub地址:
/github.com/ChenBu2
爱学啊合作讲师:
/www.ixuea.com/
公众号:
陈不2
技术交流QQ群:
858499698

grub ax

背景:

用过IntellijIDEA的人应该都知道,有个插件可以查看网络请求相关的各种信息,而相对于App端来说,往往我们在开发过程中集成各种第三方日志输出来查看各种请求信息,本文将介绍一个OkHttpProfiler插件,兼容Java及Kotlin,在此感谢我之前领导的提供。使用方式很简单,如下:

OkHttpProfiler

OkHttp Profiler插件可以直接在Android Studio工具窗口中显示来自OkHttp库的请求。它支持okhttp v3 (/square.github.io/okhttp/)或Retrofit v2 (/square.github.io/retrofit/)

我们可以调试OkHttp请求或响应头,将JSON检查为树,作为纯文本等,可以轻松地从数据创建Java/Kotlin模型。只需在树的根元素(或任何其他元素)上单击鼠标右键,选择Java或Kotlin,然后为项目中的新文件选择一个文件夹。

效果如下:

Installation

首先配置你的 build.gradle

implementation 'com.itkacher.okhttpprofiler:okhttpprofiler:1.0.2' 

然后添加拦截器

For OkHttp
Java
OkHttpClient.Builder builder = new OkHttpClient.Builder();
 if (BuildConfig.DEBUG) {
     builder.addInterceptor(new OkHttpProfilerInterceptor());
 }   
OkHttpClient client = builder.build(); 
Kotlin
val builder = OkHttpClient.Builder()
if (BuildConfig.DEBUG) {
    builder.addInterceptor(OkHttpProfilerInterceptor() )
}    
val client = builder.build()
For Retrofit
Java
OkHttpClient.Builder builder = new OkHttpClient.Builder();
 if (BuildConfig.DEBUG) {
     builder.addInterceptor(new OkHttpProfilerInterceptor());
 }   
OkHttpClient client = builder.build(); 
Retrofit retrofit = new Retrofit.Builder()
            ......
            .client(client)
            .build();
Kotlin
val builder = OkHttpClient.Builder()
if (BuildConfig.DEBUG) {
    builder.addInterceptor( OkHttpProfilerInterceptor() )
}    
val client = builder.build()
val retrofit = Retrofit.Builder()
        ......
        .client(client)
        .build()

出于安全考虑,建议debug模式开启OkHttpProfilerInterceptor !

线上版本记得删除哈。

安装Android Studio插件方法如下:

是不是很简单~

参考地址:/plugins.jetbrains.com/plugin/11249-okhttp-profiler

陈不二,喜欢技术,欢迎一块交流学习~
大家可以关注我的个人博客:
/www.chenbu2.com
简书地址:
/www.jianshu.com/u/f4874eb81c5b
GitHub地址:
/github.com/ChenBu2
爱学啊合作讲师:
7047756005
公众号:


陈不2
技术交流QQ群:
858499698

NDK 你不知道的事

背景:

先说下背景,不知道大家在开发在中有没有遇到这个问题:

   No toolchains found in the NDK toolchains folder for ABI with prefix: mips64el-linux-android

我先说下我在项目中是怎么出现的吧。
我在公司的项目集成NDK开发,起初都没啥问题,sync别的项目(不含NDK)也没问题;
最近更新了AS到3.2.0版本(应该不会出现此bug)及更新了最新的NDK版本,如图:

 

 

然后编译不过去,一直sync失败,然后按照提示修改了gradle文件;
修改前:

   externalNativeBuild {
            cmake {
                cppFlags ""
                arguments "-DANDROID_ARM_MODE=arm", "-DANDROID_TOOLCHAIN=gcc"
            }
        }

-DANDROID_TOOLCHAIN=gcc,猜测应该此工具链已经弃用了,从而导致编译失败的吧,然后改成如下:

   externalNativeBuild {
            cmake {
                cppFlags ""
                arguments "-DANDROID_ARM_MODE=arm", "-DANDROID_TOOLCHAIN=clang"
            }
        }

当然,默认的工具链就是clang,你也可以不写;
然后编译成功。

后来在看其他源码demo时,所有的都编译失败,当时也没太在意,我就用了最简单的处理方式;

处理方式一

卸载NDK,成功过后再安装NDK,刚开始还能忍受,后来实在不能忍了,正所谓是可忍孰不可忍(哈哈哈,瞎扯淡的。。。),然后有了方式二

处理方式二

从提示当中就可以看出我们就可以看出,缺少相应的架构工具链,然后从查看NDK文档也明确的说明了每个架构有自己的工具链,而此处编译不了无非就是缺少这个mips64el-linux-android;
知道问题所在过后我们去官网下载最新的NDK:
下载地址一:/developer.android.com/ndk/downloads/?hl=zh-cn
需要翻墙
下载地址二:/developer.android.google.cn/ndk/downloads/?hl=zh-cn
安卓开发的同学我相信肯定能下载到的,这里就不细说了;
下载完过后解压,然后找到相应的文件放入已有的NDK中就可以了,建议把64位和32位的都copy进入,避免再次出现这种问题,我这里只替换了需要的,如下图:

下面已有的,大家可以明显看到缺少了相应的架构吧:

好了,copy进去,解决此问题。
由此值得我们好好研究下,能不能让AS在编译的时候不去执行查找NDK呢!或者有什么更好的方式需要NDK的项目可以去查找NDK,不需要的就没必要去查找了。
要是有哪位同学知道答案可以告诉我哈~~

大家可以关注我的个人博客:
6469020964
简书地址:
uncounterfeit
GitHub地址:
/github.com/ChenBu2
爱学啊合作讲师:
/www.ixuea.com/
公众号:
陈不2
技术交流QQ群:
858499698

(925) 355-7781

双11红包来啦,拼购1折起

image

原文链接:
/mp.weixin.qq.com/s/5N21xLqA1Bt9Q6RUKmUBTw

陈不二,喜欢技术,欢迎一块交流学习~
大家可以关注我的个人博客:
/www.chenbu2.com
简书地址:
/www.jianshu.com/u/f4874eb81c5b
GitHub地址:
/github.com/ChenBu2
爱学啊合作讲师:
/www.ixuea.com/
公众号:
陈不2
技术交流QQ群:
858499698

(908) 889-2074

大家好,由于数据库异常,原博客地址:www.chensan6.com

迁移至:www.chenbu2.com;

同时不影响访问,谢谢~

 

陈不二,喜欢技术,欢迎一块交流学习~
大家可以关注我的个人博客:
/www.chenbu2.com
简书地址:
(727) 754-8746
GitHub地址:
308-277-7195
爱学啊合作讲师:
/www.ixuea.com/
公众号:
陈不二
技术交流QQ群:
858499698