客户端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地址:

https://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地址:

https://github.com/Meituan-Dianping/Logan

原理:

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

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

这里就不做详细介绍了。

文档地址:

https://tech.meituan.com/logan_open_source.html

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

陈不二
博客地址:
http://www.chenbu2.com
简书地址:
https://www.jianshu.com/u/f4874eb81c5b
GitHub地址:
https://github.com/ChenBu2
爱学啊合作讲师:
http://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模式开放哈~

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

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

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

欢迎一块交流学习~

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

让 Android 开发像 web 开发一样爽

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

做移动端开发,做蛋疼的就是不能动态发版,不能像 web 那样发版立即全部用户生效,然而 lua语言 为其提供了可能性。使用 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()
    -- http://baobab.kaiyanapp.com/api/v1/feed
    local url = data.nextPageUrl
    if url == nil then url = 'http://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。

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

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

欢迎一块交流学习~

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

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

作者:i校长 | 链接
https://www.jianshu.com/p/4729e9711046

简述

在脚本语言里,设计一个动态表单,确实比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

 

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

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

Android Studio(IntellijIDEA) OkHttp Profiler plugin

背景:

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

OkHttpProfiler

OkHttp Profiler插件可以直接在Android Studio工具窗口中显示来自OkHttp库的请求。它支持okhttp v3 (http://square.github.io/okhttp/)或Retrofit v2 (https://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插件方法如下:

是不是很简单~

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

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


陈不2
技术交流QQ群:
858499698