老司机种菜


  • 首页

  • 分类

  • 关于

  • 归档

  • 标签

  • 公益404

  • 搜索

应用targetSdkVersion升级指导

发表于 2019-03-05 | 分类于 Android

应电信终端产业协会(TAF)发布的《移动应用软件高 API 等级预置与分发自律公约》(以下简称《公约》)要求:截止到2019年5月1日所有新发布的应用 API 必须为26或更高,2019年8月1日现有应用 API 必须升级为26或更高。《公约》发布至今得到了国内主流互联网及终端制造企业的积极响应。

  • 对应用开发者来说,不按时更新Target SDK版本的应用,应用市场将采取强制下架的策略,已上架应用将无法更新。
  • 对用户来说,未适配Android M或更高版本的应用安装在Android M或更高版本时,会默认授予申请的所有权限,且应用运行时无弹框授权提示。其中包括通讯录、电话、短信、通讯记录、位置、麦克风、相机等危险权限,导致用户在不知情的情况下泄露隐私信息,对用户个人信息安全造成危害。

应用targetSdkVersion升级流程 tips-android-targetsdkversion-201935161819

targetSdkVersion 相关变更介绍

Android5.x(22-19)及以下版本变更

tips-android-targetsdkversion-201935184537

Android6.0变更

(一)相关变更

运行时权限

此版本引入了一种新的权限模式,用户可直接在运行时管理应用权限。这种模式让用户能够更好地了解和控制权限,为安装的应用分别授予或撤销权限,同时为开发者精简了安装和自动更新过程。

对于以 Android 6.0(API 级别 23)或更高版本为目标平台的应用,请务必在运行时检查和请求权限。确定应用是否已被授予权限,可调用新增的 checkSelfPermission() 方法。请求权限,可调用新增的 requestPermissions() 方法。具体参考:

https://developer.android.google.cn/training/permissions/requesting

####(二)适配指导

• 解释需要权限的原因:系统在开发者调用 requestPermissions() 时显示的权限对话框将说明应用需要的权限,但不会解释为何需要这些权限。某些情况下,用户可能会感到困惑,因此,建议在调用 requestPermissions() 之前向用户解释应用需要相应权限的原因。

  • 仅申请应用真正需要的权限
  • 如果应用在启动之后一次要求用户提供多项权限,用户可能会感到无所适从并因此退出应用 。建议开发者应根据需要请求权限,对于某一些权限应用可以在真正需要使用的时候再尝试申请用户动态授权。

Android7.0

(一)相关变更

1.系统禁止链接到非 NDK 库

从 Android 7.0 开始,系统将阻止应用动态链接非公开 NDK 库,原因为NDK 库可能会导致应用崩溃,此行为变更主要目的在为跨平台更新和不同设备提供统一的应用体验。即使应用中的代码不会链接私有库,但第三方静态库可能会进行链接,因此建议所有开发者都需进行相应检查,确保应用不会在运行 Android 7.0 的设备上崩溃。如果应用使用的是原生代码,则只能使用公开 NDK API。(https://developer.android.com/ndk/guides/stable_apis)

2.低电耗模式DOZE(系统状态)

进入条件:灭屏、未充电、静止持续1小时 退出条件:亮屏或移动或充电

限制资源

  • 限制应用访问网络
  • 暂停应用的Sync任务
  • 暂停应用的JobScheduler任务
  • 忽略应用的wakelocks
  • 标准Alarm推迟到维护窗口
  • 不执行wifi扫描
3.低电耗模式Lite Idle(系统状态)

进入条件:灭屏、未充电持续5分钟(Android P调整为3分钟) 退出条件:亮屏或充电 限制资源

  • 限制应用访问网络。
  • 暂停应用运行Sync任务 。
  • 暂停应用运行JobScheduler任务。
4.AppStandby模式(应用状态)

进入AppIdle条件:应用后台空闲总时间>48小时且亮屏后台空闲时间 > 12小时 排除&退出条件:前台应用、有前台服务的应用、通知栏或锁屏通知消息的应用或进行充电 不限制的应用:系统应用UID<10000的,Persist常驻应用,电池优化白名单,系统关联的其他应用 限制的资源

  • 限制应用访问网络
  • 暂停执行应用的Sync任务
  • 暂停执行应用的JobScheduler任务
5.App Standby Bucket 应用待机分组模式( Android P特性)

应用待机分组模式是在Doze模式未生效时(亮屏或刚刚灭屏时),对运行在后台的用户不可感知应用的耗电管控扩展。

(二)适配指导

低电耗模式和应用待机模式适配指导如下:

• 前台服务

应用需要在后台访问网络或者使用CPU时可以通过此方式。

• Doze白名单

查询应用是不是再doze白名单:

1
2
PowerManager powerManager = (PowerManager)getSystemService(POWER_SERVICE);
boolean hasIgnored = powerManager.isIgnoringBatteryOptimizations(activity.getPackageName());

向用户申请授权添加doze白名单:

1
2
3
Intent intent = new Intent(Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS);
intent.setData(Uri.parse("package:"+activity.getPackageName()));
startActivity(intent);

Android8.0 变更

1.后台服务限制

①哪些应用会受到后台服务限制?
  • TargetSDK>=26的应用
  • TargetSDK<26且被用户主动设置为限制后台活动的应用(华为EMUI8.x未提供配置)

注意:Doze 白名单中的应用不受限制(用户可以设置,手机管家配置有默认值白名单)

②前台如何定义?
  • 前台有可见 Activity
  • 前台服务可以后台播放
  • 绑定服务的情况
    ③后台服务限制,限制了哪些行为?
  • 应用进入 uidldle 后,会被调用 Service.stopself()
  • 应用进入 uidldle 后,不允许通过 startService启动服务
  • 应用进程仍然存在,JobScheduler、Alarm、广播等均能触发
  • 广播接收线程处理短暂业务,无法拉起后台服务
  • 非安卓组件线程 CPU 超标时,谷歌原生机制会强制kill进程
    ④临时白名单机制
    系统调用应用时,少量场景会将应用添加到临时白名单(有效时间30秒~300秒):
  • 处理高优先级 FCM 消息
  • 接收短信彩信
  • 用户点击通知栏,执行 PendingIntent
    ⑤用户设置限制后台活动之后的影响(Android P优化)
  • 应用退后台,1分钟就会被停止 Service(包括正在执行的前台任务)
  • 限制访问网络
  • 限制 Alarm 触发
  • 限制 JobScheduler 执行

2.广播限制

Android 8.0 的应用无法继续在其清单中为隐式广播注册广播接收器,但也存在例外情况:

  • 应用可以继续在清单中注册显式广播
  • 应用可以在运行时使用 Context.registerReceiver() 为任意广播(不管是隐式还是显式)注 册接收器
  • 需要签名权限的广播不受此限制所限,因为这些广播只会发送到使用相同证书签名的应用 ,而不会发送至设备上的所有应用
  • 白名单豁免隐式广播的列表:https://developer.android.com/guide/components/broadcast-exceptions

后台执行限制的适配建议

  • 使用 JobScheduler 代替
  • 增加前台服务
  • 加 Doze 白名单(不推荐)

3.最大屏幕纵横比

以 Android 7.1(API 级别 25)或更低版本为目标平台中应用默认的最大屏幕纵横比为1.86。针对 Android 8.0 或更高版本的应用没有默认的最大纵横比,如需设置请在应用 androidmanifest 文件定义 maxAspectRatio 属性(注意:如果应用没有显示申明不支持 resizeableActivity,系统将会忽略应用设置的 maxAspectRatio属性)

4.其他变更

变更 详细说明
提醒窗口 使用SYSTEM_ALERT_WINDOW权限的应用无法再使用以下窗口类型来在其他应用和系统窗口上方显示提醒窗口:TYPE_PHONE,TYPE_PRIORITY_PHONE,TYPE_SYSTEM_ALERT,TYPE_SYSTEM_OVERLAY,TYPE_SYSTEM_ERROR,应用必须使用名为TYPE_APPLICATION_OVERLAY的新窗口类型
权限 在Android8.0之前,如果应用在运行时请求权限并且被授予该权限,系统会错误地将属于同一权限组并且在清单中注册的其他权限也一起授予应用.对于针对8.0的应用,此行为已被纠正.系统只会授予应用明确请求的权限.然而,一旦用户为应用授予某个权限,则所有后续对该权限组中权限的请求都被自动批准.建议应用在使用所有的敏感权限之前,都先判断一下权限是否已经被授予,如果没有授予需要申请动态权限.
Linker O版本在linker中新增加检查就是在load之前检测一下需要加载的section的权限,被加载的段不允许同事具有E(可执行)和W(可写)权限.如果有这样的段,则linker报错
Build.SERIAL弃用 需要知道硬件序列号的应用应改为使用新的Build.getSerial()函数,该函数要求具有READ_PHONE_STATE权限

Android9.0 变更

1.非SDK管控

名单类型 影响 名单说明
浅灰名单 targetSDK>=P时,警告 已有应用在使用非SDK接口,仍然可以继续使用
深灰名单 targetSDK<P时,警告;>=时,不允许调用 应用可能还在使用
黑名单 所有三方应用不允许调用 灰名单(深灰+浅灰)之外的其他所有非SDK接口都会被添加到黑名单,应用发现有使用黑名单的接口需要马上整改,或者反馈给谷歌申请加灰名单

使用谷歌提供的非 SDK 扫描工具查看应用使用的深灰名单和黑名单非 SDK 接口: https://android.googlesource.com/platform/prebuilts/runtime/+/ master/appcompat/

2. Apache HTTP 客户端弃用

默认情况下该内容库已从 bootclasspath 中移除且不可用于应用,应用不能使用系统的 classloader 加载 org.apache.http.* 库,否则会抛 NoClassDefFoundError。

适配建议

  • 方法一:如果要继续使用 Apache HTTP 客户端,以 Android 9.0及更高版本为目标的应用可以向其 AndroidManifest.xml 添加以下内容:<uses-library android:name="org.apache.http.legacy" android:required="false"/>
  • 方法二:如果必须要继续使用 Apache HTTP 客户端,开发者可以将 org.apache.http.legacy库打包进自己的apk。
  • 推荐方法:使用 HttpURLConnection 类替代 apache-http

3.内联方法不允许跨dex

Google 在 Android P 新增检测:如果调用某个 inline 方法的类与 inline 方法所在的类由不同的 classloader 加载,就会主动发起 abort(inline不允许跨dex文件)导致应用 crash。 tips-android-targetsdkversion-201935181033

兼容性影响

对使用插件和热修复的应用有很大影响,需要重点测试。

测试方法
  • 启动应用,构造热修复场景,在 app 侧触发热修复
  • adb shell cmd package compile -m speed -f my-package 应用包名 (inline编译)
  • 重启应用,检查是否会出现闪退问题
适配建议
  • 尽量避免使用不同的 classloader 加载相关的类。
  • 如果一定要这样做的话,需要避免内联,比如在函数里面加 try catch, 这样 compiler就不会将这个函数 inline。

4.其他变更

变更 详细说明
前台服务 使用前台服务的应用必须请求FOREGROUND_SERVICE权限.这是普通权限,因此,系统会自动为请求权限的应用授予此权限.如果针对Android9或更高版本的应用尝试创建一个前台服务且未请求FOREGROUND_SERVICE,则系统会引发SecurityException.
DNS隐私 应用应采用私有DNS API.具体而言,当系统解析程序正在执行DNS-over-TLS时,应用应确保任何内置DNS客户端均使用加密的DNS查找和系统相同的主机名,或停用它而改用系统解析程序
默认情况下启用网络传输层安全协议(TLS) 默认情况下isCleartextTrafficPermitted()函数返回false.如果您的应用需要为特定域名启动明文,您必须在应用的网络安全性配置中针对这些域名将cleartextTrafficPermitted显式设置为true
webview数据目录不允许共享 应用无法再让多个进程公用同一个WebView数据目录.如果应用中的多个进程需要访问同一网络数据,您需要自行在这些进程之间复制数据.例如,您可以调用getCookie()和setCooki(),在不同进程之间手动传输Cookie数据
以应用为单位的SELinux域名 应用的私有数据只能由该应用访问.要与其他应用共享文件,请使用contentprovider

其他的变更和非 TargetSdkVersion 相关的变更以及新特性,可以在谷歌开发者网站查阅:https://developer.android.google.cn/about/versions/oreo/

Android启动过程深入解析

发表于 2019-03-05 | 分类于 Android
  • A当按下Android设备电源键时究竟发生了什么?
  • Android的启动过程是怎么样的?
  • A什么是Linux内核?
  • A桌面系统linux内核与Android系统linux内核有什么区别?
  • A什么是引导装载程序?
  • A什么是Zygote?
  • A什么是X86以及ARM linux?
  • A什么是init.rc?
  • A什么是系统服务? 当我们想到Android启动过程时,脑海中总是冒出很多疑问。本文将介绍Android的启动过程,希望能帮助你找到上面这些问题的答案。

Android是一个基于Linux的开源操作系统。x86(x86是一系列的基于intel 8086 CPU的计算机微处理器指令集架构)是linux内核部署最常见的系统。然而,所有的Android设备都是运行在ARM处理器(ARM 源自进阶精简指令集机器,源自ARM架构)上,除了英特尔的Xolo设备(http://xolo.in/xolo-x900-features)。Xolo来源自凌动1.6GHz x86处理器。Android设备或者嵌入设备或者基于linux的ARM设备的启动过程与桌面版本相比稍微有些差别。这篇文章中,我将解释Android设备的启动过程。深入linux启动过程是一篇讲桌面linux启动过程的好文。

当你按下电源开关后Android设备执行了以下步骤。

tips-android-system-load-201935141326

第一步:启动电源以及系统启动

当电源按下,引导芯片代码开始从预定义的地方(固化在ROM)开始执行。加载引导程序到RAM,然后执行。

第二步:引导程序

引导程序是在Android操作系统开始运行前的一个小程序。引导程序是运行的第一个程序,因此它是针对特定的主板与芯片的。设备制造商要么使用很受欢迎的引导程序比如redboot、uboot、qi bootloader或者开发自己的引导程序,它不是Android操作系统的一部分。引导程序是OEM厂商或者运营商加锁和限制的地方。

引导程序分两个阶段执行。第一个阶段,检测外部的RAM以及加载对第二阶段有用的程序;第二阶段,引导程序设置网络、内存等等。这些对于运行内核是必要的,为了达到特殊的目标,引导程序可以根据配置参数或者输入数据设置内核。

Android引导程序可以在\bootable\bootloader\legacy\usbloader找到。 传统的加载器包含的个文件,需要在这里说明:

  • init.s初始化堆栈,清零BBS段,调用main.c的_main()函数;
  • main.c初始化硬件(闹钟、主板、键盘、控制台),创建linux标签。 更多关于Android引导程序的可以在这里了解。

第三步:内核

Android内核与桌面linux内核启动的方式差不多。内核启动时,设置缓存、被保护存储器、计划列表,加载驱动。当内核完成系统设置,它首先在系统文件中寻找”init”文件,然后启动root进程或者系统的第一个进程。

第四步:init进程

init是第一个进程,我们可以说它是root进程或者说有进程的父进程。init进程有两个责任,一是挂载目录,比如/sys、/dev、/proc,二是运行init.rc脚本。

init进程可以在/system/core/init找到。 init.rc文件可以在/system/core/rootdir/init.rc找到。 readme.txt可以在/system/core/init/readme.txt找到。 对于init.rc文件,Android中有特定的格式以及规则。在Android中,我们叫做Android初始化语言。 Android初始化语言由四大类型的声明组成,即Actions(动作)、Commands(命令)、Services(服务)、以及Options(选项)。

Action(动作):动作是以命令流程命名的,有一个触发器决定动作是否发生。

语法

1
2
3
4
5
; html-script: false ]
on &lt;trigger&gt;
&lt;command&gt;
&lt;command&gt;
&lt;command&gt;

Service(服务):服务是init进程启动的程序、当服务退出时init进程会视情况重启服务。

语法

1
2
3
4
5
; html-script: false ]
service &lt;name&gt; &lt;pathname&gt; [&lt;argument&gt;]*
&lt;option&gt;
&lt;option&gt;
...

Options(选项)

选项是对服务的描述。它们影响init进程如何以及何时启动服务。 咱们来看看默认的init.rc文件。这里我只列出了主要的事件以及服务。 Table |—|—| |Action/Service| 描述| |on early-init |设置init进程以及它创建的子进程的优先级,设置init进程的安全环境| |on init |设置全局环境,为cpu accounting创建cgroup(资源控制)挂载点| |on fs |挂载mtd分区| |on post-fs |改变系统目录的访问权限| |on post-fs-data |改变/data目录以及它的子目录的访问权限| |on boot |基本网络的初始化,内存管理等等| |service servicemanager |启动系统管理器管理所有的本地服务,比如位置、音频、Shared preference等等…| |service zygote |启动zygote作为应用进程| 在这个阶段你可以在设备的屏幕上看到“Android”logo了。

第五步

在Java中,我们知道不同的虚拟机实例会为不同的应用分配不同的内存。假如Android应用应该尽可能快地启动,但如果Android系统为每一个应用启动不同的Dalvik虚拟机实例,就会消耗大量的内存以及时间。因此,为了克服这个问题,Android系统创造了”Zygote”。Zygote让Dalvik虚拟机共享代码、低内存占用以及最小的启动时间成为可能。Zygote是一个虚拟器进程,正如我们在前一个步骤所说的在系统引导的时候启动。Zygote预加载以及初始化核心库类。通常,这些核心类一般是只读的,也是Android SDK或者核心框架的一部分。在Java虚拟机中,每一个实例都有它自己的核心库类文件和堆对象的拷贝。

Zygote加载进程

  1. 加载ZygoteInit类,源代码:/frameworks/base/core/java/com/android/internal/os/ZygoteInit.java
  2. registerZygoteSocket()为zygote命令连接注册一个服务器套接字。
  3. preloadClassed “preloaded-classes”是一个简单的包含一系列需要预加载类的文本文件,你可以在/frameworks/base找到“preloaded-classes”文件。
  4. preloadResources() preloadResources也意味着本地主题、布局以及android.R文件中包含的所有东西都会用这个方法加载。 在这个阶段,你可以看到启动动画。

第六步:系统服务或服务

完成了上面几步之后,运行环境请求Zygote运行系统服务。系统服务同时使用native以及java编写,系统服务可以认为是一个进程。同一个系统服务在Android SDK可以以System Services形式获得。系统服务包含了所有的System Services。

Zygote创建新的进程去启动系统服务。你可以在ZygoteInit类的”startSystemServer”方法中找到源代码。

核心服务:

  1. 启动电源管理器;
  2. 创建Activity管理器;
  3. 启动电话注册;
  4. 启动包管理器;
  5. 设置Activity管理服务为系统进程;
  6. 启动上下文管理器;
  7. 启动系统Context Providers;
  8. 启动电池服务;
  9. 启动定时管理器;
  10. 启动传感服务;
  11. 启动窗口管理器;
  12. 启动蓝牙服务;
  13. 启动挂载服务

其他服务:

  1. 启动状态栏服务;
  2. 启动硬件服务;
  3. 启动网络状态服务;
  4. 启动网络连接服务;
  5. 启动通知管理器;
  6. 启动设备存储监视服务;
  7. 启动定位管理器;
  8. 启动搜索服务;
  9. 启动剪切板服务;
  10. 启动登记服务;
  11. 启动壁纸服务;
  12. 启动音频服务;
  13. 启动耳机监听;
  14. 启动AdbSettingsObserver(处理adb命令)。

第七步:引导完成

一旦系统服务在内存中跑起来了,Android就完成了引导过程。在这个时候“ACTION_BOOT_COMPLETED”开机启动广播就会发出去。

解决方案之美团APP对Crash的治理之路

发表于 2019-03-05

Crash率是衡量一款APP质量好坏的重要指标之一,不仅会影响用户体验,也可能影响用户存量。一旦出现问题,可能会给企业带来严重损失。

本文由美团技术专家谌天洲分享美团APP Crash率从千分之一到万分之一治理过程中所做的大量实践工作。

美团Crash治理背景

美团作为一个平台化的APP,背后有20+团队设计和30+业务。

在Crash治理过程中面对的挑战有三项:体量大、迭代快和日活高。这三项挑战带来的直接影响是沟通成本上升和防范难度加大。因此在实际治理过程,主要围绕基础能力、治理效率两个层面进行探索和优化建设。 tips-solution-2-201935223013

基础能力

Crash治理的基础能力主要体现在三个层面:能发现、能定位和能修复。

在发现能力层面,美团有一套异常监控退出系统,可发现除Java&JNI Crash&ANR以外其他类型的异常退出。在定位能力层面,有可提供内存泄漏路径及OOM时的内存快照的内存监控体系,有可提供线程现场及任务现场的线程管控体系。除此以外,还有动态日志系统提供额外的方法调用链及参数信息。

内存监控体系

内存问题最典型的呈现形式是OOM,其中80%通过Leak监控系统发现预防,另外20%的内存问题,对于大体量APP需要从全局对内存资源问题进行监控和调查分析。

美团的内存监控体系分为线下和线上两个场景。线下通过Leak监控系统能预防发现80%的OOM问题,线上建立随时获取OOM内存现场的监控能力 tips-solution-2-201935223125

动态日志

美团APP经常会遇到用户个性化的使用场景无法复现和定位的问题。对此,美团提出了一套动态日志系统——

在编译期对应用代码通过插桩实现代理,运行期同步记录,出现异常时可主动触发上报,也可以由服务端主动回捞。基于插桩实现的代理逻辑,可实施获取原方法执行时的方法名、入参和返回值信息,再将这些信息序列化后存储到数据库,由此可在必要的时候获取到较完善的方法调用栈历史,进而定位问题。 tips-solution-2-201935223215 tips-solution-2-201935223229

修复能力

在修复能力层面,美团APP一度深受机型多、系统杂带来的framework层的问题困扰。此外,美团APP也经常会遇到常规日志体系无法覆盖的接口问题。

针对这两类问题,参考热修复的方法替换原理,开发并完善了一套小工具——“Graft”。它的基本原理是在native层通过方法替换实现对Java层方法的hook和代理,进而在Java层实现方法代理和方法替换。

这套工具可以动态代理或替换几乎所有Java层的方法(包括framework层),使得美团APP的修复能力从自有代码和第三方代码有效覆盖到framework层。

tips-solution-2-201935223340 tips-solution-2-201935223353

效率提升

为了提高治理效率,实际治理过程逐渐形成PR检查流程、自动检查平台和Crash平台三大流程和平台。

PR检查流程主要针对PR阶段进行代码规范性检查、代码准入检查和稳定性案例检查;自动检查平台针对以往案例进行定制化防范检查。Crash平台是整个稳定性治理的核心,在建设的考量中主要遵循规范化、流程化、自动化,它主要涵盖接入管控、聚合策略、频道工单、报警系统、基础工具、模块覆盖,可以通过强大的复用能力快速接入并管理几乎所有稳定性相关的问题。 tips-solution-2-201935223433

Crash平台

Crash平台是整个稳定性治理的核心,在建设的考量中主要遵循规范化、流程化、自动化,它主要涵盖接入管控、聚合策略、频道工单、报警系统、基础工具、模块覆盖,可以通过强大的复用能力快速接入并管理几乎所有稳定性相关的问题。

在PR阶段,PR检查流程可自动识别出增量代码是否被现有体系覆盖,并通过Crash平台的接入管控系统督促增量代码的责任人完善基本信息、频道信息、聚合配合及自动工单配置等等。

在开发或全量过程中一旦发现异常,Crash平台会自动完成堆栈聚合、频道识别、报警评估及工单跟踪等工作。

tips-solution-2-201935223539 tips-solution-2-20193522365

Crash率是APP最重要的指标之一,谌天洲建议开发者建立解决Crash的长效机制,找到最合理的解决方案。随着版本的不断迭代,Crash治理之路才能离目标越来越近。

性能度量指标及数据平台

发表于 2019-03-04 | 分类于 Android

性能和稳定性系统化提升方案

高可用平台的定义及指标,自动化测试框架和性能稳定性数据平台

高可用的定义及度量指标

移动端高可用定义

移动端高可用旨在通过设计关键的度量指标,以期望能够客观反映和量化用户再使用过程中的真是感受,同时通过指标,建⽴一系列的工具和平台,从线下到线上快速发现、分析、定位和解决包括稳定性、性能、功能等各类问题,以进一步提升用户体验的系统化解决方案。

可高勇度量指标

高可用度量指标由 性能 和 稳定性 两大度量指标组成。性能度量指标有七个维度,分别是卡顿率、启动时长、页面秒开率、帧率、ANR率、流量、耗电;稳定性度量指标主要是Crash率,分为Java Crash率和Native Crash率。

tips-solution-1-201935105618

自动化测试框架及性能稳定性数据平台

自动化测试框架
性能稳定性数据平台

性能稳定性数据平台,由四个模块组成,用来展示各个维度的监控数据。 tips-solution-1-201935111941

1.崩溃分析

主要是Java Crash和Native Crash分析,Java Crash包含了Crash当时调用栈,当前页面、用户历史访问页面、当前内存水位以及logcat信息,帮助开发同学快速分析Java崩溃的原因,从而快速解决问题。Native崩溃主要包含了崩溃的信号量、崩溃使用的调用栈、其他线程的调度栈、logcat信息以及已加载的so信息,通过这些信息,可以让开发同学快速地发现Native崩溃原因。

2.异常分析

各个性能维度的指标会在这里展示,主线程卡顿主要是哪条消息超过了阈值,它的调用栈是什么样子的。ANR展示的主要是/data/anr下的文件信息,发生ANR的现场是什么样子的。主线程IO这块展示的是主线程操作IO的一些调用栈以及它的耗时。内存泄露这块分两部分,Java泄露组件的名称,Native泄露的so名称,通过这两部分可以快速地定位内存泄露的原因。资源泄露这块主要展示开发同学调用资源open时的调用栈信息。

3.性能APM

启动性能监控了用户点击图标到真正进入页面可视可交互的时间。页面性能是从点击页面图表到下一个页面真正可视可交互的时间。系统监控启动所有阶段子任务的耗时,通过数据变化决策版本的发布是否符合质量标准。如果符合质量标准,可以发布;如果不符合,再进一步分析子任务耗时,具体是哪些任务导致不能正常发布。快速地定位分析,最终解决问题。手淘把数据能力开放给各个业务,通过他们个性化的需求自定义配置性能报表。

4.远程工具

远程工具主要是针对特殊用户的特殊案例。当线上用户向舆情平台反馈设备存在某一个性能问题时,通过这个工具,快速地从用户那里获取远程日志、Dump内存和每个方法的耗时,快速分析原因,从而给出解决方案。

Android系统图形栈(一):OpenGL 介绍

发表于 2019-03-01 | 分类于 OpenGL

OpenGL基本概念

OpenGL的结构可以从逻辑上划分为下面3个部分:

  • 图元(Primitives)
  • 缓冲区(Buffers)
  • 光栅化(Rasterize)

图元(Primitives)

在 OpenGL 的世界里,我们只能画点、线、三角形这三种基本图形,而其它复杂的图形都可以通过三角形来组成。所以这里的图元指的就是这三种基础图形:

  • 点:点存在于三维空间,坐标用(x,y,z)表示。
  • 线:由两个三维空间中的点组成。
  • 三角形:由三个三维空间的点组成。

缓冲区(Buffers)

OpenGL 中主要有 3 种 Buffer:

  • 帧缓冲区(Frame Buffers) 帧缓冲区:这个是存储OpenGL 最终渲染输出结果的地方,它是一个包含多个图像的集合,例如颜色图像、深度图像、模板图像等。
  • 渲染缓冲区(Render Buffers) 渲染缓冲区:渲染缓冲区就是一个图像,它是 Frame Buffer 的一个子集。
  • 缓冲区对象(Buffer Objects) 缓冲区对象就是程序员输入到 OpenGL 的数据,分为结构类和索引类的。前者被称为“数组缓冲区对象”或“顶点缓冲区对象”(“Array Buffer Object”或“Vertex Buff er Object”),即用来描述模型的数组,如顶点数组、纹理数组等; 后者被称为“索引缓冲区对象”(“Index Buffer Object”),是对上述数组的索引。

光栅化(Rasterize)

在介绍光栅化之前,首先来补充 OpenGL 中的两个非常重要的概念:

  • Vertex Vertex 就是图形中顶点,一系列的顶点就围成了一个图形。
  • Fragment Fragment 是三维空间的点、线、三角形这些基本图元映射到二维平面上的映射区域,通常一个 Fragment 对应于屏幕上的一个像素,但高分辨率的屏幕可能会用多个像素点映射到一个 Fragment,以减少 GPU 的工作。

而光栅化是把点、线、三角形映射到屏幕上的像素点的过程。

着色器程序(Shader)

Shader 用来描述如何绘制(渲染),GLSL 是 OpenGL 的编程语言,全称 OpenGL Shader Language,它的语法类似于 C 语言。OpenGL 渲染需要两种 Shader:Vertex Shader 和 Fragment Shader。

  • Vertex Shader Vertex Shader 对于3D模型网格的每个顶点执行一次,主要是确定该顶点的最终位置。
  • Fragment Shader Fragment Shader对光栅化之后2D图像中的每个像素处理一次。3D物体的表面最终显示成什么样将由它决定,例如为模型的可见表面添加纹理,处理光照、阴影的影响等等。

OpenGL 流水线

OpenGL 中有两种流水线,一种是固定流水线,另外一种则是可编程流水线。

软件架构思考

发表于 2019-03-01 | 分类于 tips

架构是对工程整体结构与组件的抽象描述,是软件工程的基础骨架。架构在工程层面不分领域,且思想是通用的。引用维基百科对于软件架构的定义:

软件体系结构是构建计算机软件实践的基础。与建筑师设定建筑项目的设计原则和目标,作为绘图员画图的基础一样,软件架构师或者系统架构师陈述软件架构以作为满足不同客户需求的实际系统设计方案的基础。从和目的、主题、材料和结构的联系上来说,软件架构可以和建筑物的架构相比拟。一个软件架构师需要有广泛的软件理论知识和相应的经验来实施和管理软件产品的高级设计。软件架构师定义和设计软件的模块化,模块之间的交互,用户界面风格,对外接口方法,创新的设计特性,以及高层事物的对象操作、逻辑和流程。

架构的合理设计可以解决面对复杂系统时可能面临的很多问题,例如:

  1. 业务边界与模块职责划分问题
  2. 代码权限控制问题(数据库不应直接被业务方调用)
  3. 代码重复,逻辑分支多,坏味道多的问题
  4. 由于考虑不周,可能存在隐藏bug
  5. 修改一个逻辑需要修改N个地方代码逻辑

移动端通用架构:

  1. Toolkit/ToolkitSDK module :工具类及与工具类相关的SDK的集合。工具类属于工程架构里最基础的模块,提供了通用的方法与工具类服务(工具类服务是指可以被抽象成一个独立的与业务无关的基础服务,如缓存、数据库操作等)。工具类通常作为最底层的module,被其他所有模块引用。
  2. 基础组件库/基础组件库module:基础组件库提供与业务相关的基础组件,是构建一个移动端应用所需要的通用组件的集合。它与工具类的区别在于基础组件库可能会包含少量业务逻辑代码,是无法拆分给其他应用使用的;另一方面,基础组件库是基础服务接口的实现,是不对业务层暴露的,避免了业务层与基础SDK打交道,有利于整体替换底层基础框架的实现(例如Volley替换为OkHttp、Fresco替换为Glide)
  3. 基础服务接口/业务服务接口module:基础服务接口声明了一组通用的基础服务,业务层通过基础服务接口获取基础服务,如网络请求、图片加载等。业务服务接口声明了一组该模块提供给其他模块的服务,业务之间的通信也是通过服务接口来完成的。例如首页模块需要获取购物车的商品数量,首先通过服务调度中心获取购物车的服务接口,再通过服务接口调用购物车获取商品数量的接口方法即可。
  4. 服务调度中心module:服务调度中心,是一个接口收集与管理的容器。服务调度中心将所有基础服务接口与业务接口收集起来,通过一定的方式与它们的实现类进行绑定。所有的业务都需要通过服务调度中心才能够获取到服务。服务的注册与发现和Spring容器的IoC思想是类似的
  5. 业务module:务层是每个业务的具体实现的集合。业务层的业务之间是没有直接引用关系的,业务层提供了业务服务接口中暴露的服务的具体实现。业务之间的通信需要通过服务调度中心获取其他业务的服务接口。

后端通用架构:

  1. 展示层(View):展示层是系统与用户打交道的地方,提供与用户交互的界面。对于用户而言,只有展示层是可见的、可操作的。展示层对于某些工程来说不是必须的,例如提供纯后台服务的工程。
  2. 控制层(Controller):主要负责与Model和View打交道,但同时又保持其相对独立。Controller决定使用哪些Model,对Model执行什么操作,为视图准备哪些数据,是MVC中沟通的桥梁。在Controller层提供了http服务供展示层调用。在依赖管理中,控制层需要依赖服务层提供服务。
  3. 服务层(Service/Facade):服务层是业务逻辑实现的地方,上层需要使用的功能都在服务层来实现具体的业务逻辑。服务层就是将底层的数据通过一定的条件和方式进行数据组装并提供给上层调用。服务层可以拆分为业务接口和业务实现,业务实现可以对外部隐藏。在投放工程中,控制层既依赖了业务接口,又依赖了业务实现。后面的改造我们可以看到,编译期红色线依赖是完全没有必要的。服务层需要依赖数据关系映射层与持久层的数据打交道。
  4. 对象关系映射层(ORM):对象关系映射层的作用是在持久层和业务实体对象之间作一层数据实体的映射,这样在具体操作业务对象时,只需简单的操作对象的属性和方法,不需要去和复杂的SQL语句打交道。ORM使得业务不需要关心底层数据库的任何细节,包括使用的数据库类型、数据库连接与释放细节等。对象关系映射层只依赖数据服务层提供服务。
  5. 数据服务层(Data Server):数据服务就是提供数据源的地方。数据服务可以提供持久化数据及缓存数据。持久,即把数据(如内存中的对象)保存到可永久保存的存储设备中(如磁盘)。持久化的主要应用是将内存中的数据存储在关系型的数据库中,当然也可以存储在磁盘文件中、XML数据文件中等等。而缓存是将信息(数据或页面)放在内存中以避免频繁的数据库存储或执行整个页面的生命周期,直到缓存的信息过期或依赖变更才再次从数据库中读取数据或重新执行页面的生命周期。数据服务层是数据源头,处于架构的最底层。

android自动化测试(三):常见问题

发表于 2019-02-28 | 分类于 autotest

####### 1.appium执行登录按钮的click在华为(FRD-AL10)手机上执行完不起作用 最开始怀疑是因为输入法遮挡,后来调用hide_keyboard后无法隐藏键盘(在三星手机Samsung SM-N9200上调用hide_keyboard会抛异常,华为手机不显示输入法调用也没有问题),最后在设置-系统设置-语言和输入法中关闭”安全输入”,软件盘就不再弹出了,但是还是执行完不生效,最后appium中指定automationName=UiAutomator2,使用UiAutomator2后该问题得到解决.

有人说需要打开安全设置中的”允许模拟点击”,但是并没有找到这个开关

注意:元素必须先出现在页面上,才可以使用click/tap方法。所以需要先使用isDisplay()方法判读该元素出现, 然后点击该元素。但是有可能即使isDisplay()返回的结果为true, 该元素还会因为不可见而点击报错。因为虽然该元素的isDisplay()返回了true,但是该元素的中心点可能仍然不在屏幕上。 tap方法其实点击的就是元素的中心点。可以使用swipe方法再滑动一下屏幕让这个元素完全显示在屏幕上。

at-android-appium-introduce

发表于 2019-02-28 | 分类于 autotest

Appium UiAutomator2 Server 运行原理分析 https://testerhome.com/topics/9240

android自动化测试(N):UiAutomator用法

发表于 2019-02-28 | 分类于 autotest

它是一个Android自动化测试框架,是谷歌在Android4.1版本发布时推出的一款用Java编写的UI测试框架。它只能用于UI也就是黑盒方面的测试。所以UiAutomator只能运行在4.1以后的版本中。其最大的特点就是可以跨进程操作,我们可以使用uiautomator框架提供的一些方便的API来对安卓应用进行一系列的自动化测试操作,如点击、滑动、键盘输入、长按以及常用的断言方法等。可以替代以前繁琐的手工测试。

下面总结一下该框架的几个优点:

  1. Google自家推出的,其稳定性和后续的维护更新可以得到保障,运行时也有更多的权限。
  2. 可以跨进程操作,这点比起其它基于instrumentation框架的自动化工具如Robotium是无法直接做到的。
  3. 运行速度快。 缺点:
  4. 不支持Android4.1以下的版本。
  5. 不支持Webview,所以一般无法对浏览器应用进行测试。
UiAutomator 框架原理分析:

首先,UiAutomator是Google参考微软的UiAutomation提供的一套用在Android上的自动化测试框架。基于Android AccessilibilityService提供。那么至于什么是AccessilibilityService,在这里简单介绍下:Android AccessilibilityService,是一个可访问服务,它是一个为增强用户界面并帮助残疾用户的应用程序,或者用户可能无法完全与设备的交互。举个简单的例子,假如一个用户在开车。那么用户就有可能需要添加额外的或者替代的用户反馈方式。其应用方式一般有两种:

第一种方法是:UiAutomatorView + monkey。它与hierachyview + monkey差不多。其区别是:UiAutomatorView通过ADB向设备侧发送一个dump命令,而不是建立一个socket,下载一个包含当前界面控件布局信息的xml文件。相比较hierachyview下载的内容而言,该文件小很多。因此,从效率上讲,这种方法比第一种应用模式快很多。

第二种方法是: 直接调用UiAutomator框架对外提供的API,主要有UiDevice、UiSelector、UiObject和 UiScrollable等。其原理与第一种方式即HierachyView + Monkey差不多。其过程大致是:首先,UiAutomator测试框架通过Accessibilityservice,获取当前窗口的控件层次关系及属性信息,并查找到目标控件。若是点击事件,则计算出该控件的中心点坐标。其次,UiAutomator通过 InputManager.getInstance().injectInputEvent隐藏接口来注入用户事件(点击、输入类操作),从而实现跨进程自动化的目的。

UiAutomatorTestCase :这个类是继承自Junit TestCase (Junit),对外提供setup、teardown等,以便初始化用例、清除环境等。所以我们在编写的UiAutomator 的脚本时一般都要继承这个类,这样就可以直接使用它的一些方法和Junit单元测试框架中的Assert断言机制。

UIAutomator2.0

We’re pleased to announce the release of UIAutomator 2.0! This version is a significant update from the previous release. Most importantly, UI Automator is now based on Android Instrumentation and you can build and run tests with the ‘./gradlew connectedCheck’ command.

UiAutomator2.0的jar包并不是在以前SDK/platforms/android-19/下。现在我们要这么做

  1. 通过Android SDK Manager中的 Android Support Repository 项进行安装

  2. 下载下来的jar包的路径为/extras/android/m2repository

  3. 新建一个android项目,编写一个简单的应用

  4. 在build.gradle中配置依赖项:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    dependencies {
    androidTestCompile 'com.android.support.test:runner:0.3'
    // Set this dependency to use JUnit 4 rules
    androidTestCompile 'com.android.support.test:rules:0.3'
    // Set this dependency to build and run Espresso tests
    androidTestCompile 'com.android.support.test.espresso:espresso-core:2.2'
    // Set this dependency to build and run UI Automator tests
    androidTestCompile 'com.android.support.test.uiautomator:uiautomator-v18:2.1.1'
    }
  5. 设置AndroidJunitRunner为默认的testInstrumentationRunner

    1
    2
    3
    4
    5
    android {
    defaultConfig {
    testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
    }
    }
  6. 编写测试代码,在androidTest目录下面新建测试类

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    public class LoginTest extends InstrumentationTestCase {

    protected UiDevice device = null;
    protected String appName = "magicCard";

    public void runApp(String appName) throws UiObjectNotFoundException, RemoteException {
    device = UiDevice.getInstance(getInstrumentation());
    device.pressHome();
    device.waitForWindowUpdate("", 2000);

    UiObject2 allAppsButton = device.findObject(By.desc("Apps"));
    allAppsButton.click();
    device.waitForWindowUpdate("", 2000);

    UiScrollable appViews = new UiScrollable(new UiSelector().scrollable(true));
    appViews.setAsHorizontalList();

    UiObject settingsApp = appViews.getChildByText(new UiSelector().className(TextView.class.getName()), appName);
    settingsApp.clickAndWaitForNewWindow();

    assertTrue("Unable to detect app", settingsApp != null);
    }

    @Override
    public void setUp() throws RemoteException, UiObjectNotFoundException {
    this.runApp(appName);
    }

    @Override
    public void tearDown() throws RemoteException, UiObjectNotFoundException {
    //Empty for the moment
    }

    public void testUS1() {
    UiObject2 usernameLabel = device.findObject(By.clazz(TextView.class.getName()).text("Username"));
    assertTrue("Username label not found", usernameLabel != null);
    }
    }

基于Instrument的方便一点就是不需要remote debug的方式进行调试。并且做参数化之类的也方便了很多。 2.0不用再继承UiAutomatorTestCase,但却需要继承InstrumentationTestCase。

获取设备的方式也变化了,UiDevice.getInstance(getInstrumentation()) 这才是正确的使用方法。之前常用的两种方式都不再可行。

可以通过如下的adb命令调用

1
adb shell am instrument -w -r   -e debug false -e class com.cxq.uiautomatordemo.UiTest com.cxq.uiautomatordemo.test/android.test.InstrumentationTestRunner

在dependencies中用到了compile、testCompile、androidTestCompile三种依赖方式,让我们来看看他们有什么区别:

compile:参与编译,并且会打包到debug/release apk中。 testCompile:只参与单元测试编译,不会打包到debug/release apk包中,不需要设备支持。 androidTestCompile:只参与UI测试编译,不会打包到debug/release apk包中,需要设备支持。

除此之外还有Provided、APK、Debug compile和Release compile:

Provided:只参与编译,不会打包到debug/release apk中。 APK:不参与编译,只会打包到debug/release apk中。 Debug compile:只参与debug编译,只会打包到debug apk中。 Release compile:只参与release编译,只会打包到release apk中。

UIAutomator1.0
  1. 新建Java工程

  2. 导入lib包 android.jar 和 uiautomator.jar ,选中点击右键Add to buildPath

  3. 新建测试类demo

    1
    public class Demo extends UiAutomatorTestCase{}
  4. 写测试方法A,B,C(testcase)

  5. 编译运行:

    1. <android-sdk>/tools/android create uitest-project -n <name> -t 1 -p <path> 说明一下各个参数的作用,如果已经将android sdk的路径配置到了系统的path中,输入命令“android create uitest-project”就可以查看到相应的帮助
  • -n --name : Project name. 就是在eclipse中创建的项目的名字。
  • -t --target : Target ID of the new project. [required] 这个id是本机上android targets的id,可以通过命令 “android list”来查询,得到结果,选择android-17以上版本前面所对应的id,运行完成后,工作空间下生成文件build.xml

5.2. 修改build.xml 将help改为build

1
2
<?xml version="1.0" encoding="UTF-8"?>            
<project name="demo1" default="build">

5.3.在build.xml上点击右键,选择“Run As” -> “Ant Build”,编译成功,在工作空间bin下生成一个jar包demo.jar

5.4. adb push demo.jar /data/local/tmp/

5.5. adb shell uiautomator runtest demo.jar -c A -c B -c C(可指定多个testcase,不指定则运行所有)

uiautomator的help帮助: 支持三个子命令:rutest/dump/events

  • runtest命令-c指定要测试的class文件,用逗号分开,没有指定的话默认执行测试脚本jar包的所有测试类.注意用户可以以格式$class/$method来指定只是测试该class的某一个指定的方法
  • runtest命令-e参数可以指定是否开启debug模式
  • runtest命令-e参数可以指定test runner,不指定就使用系统默认。我自己从来没有指定过
  • runtest命令-e参数还可以通过键值对来指定传递给测试类的参数

UiAutomator2改进

  1. 基于 Instrumentation,可以获取应用Context,使用 Android服务及接口

  2. 基于 Junit4,测试用例无需继承于任何父类,方法名不限,使用注解 Annotation进行

  3. UI执行效率比 1.0 快,测试执行可使用AndroidJunit 方式及gradle 方式

  4. API 更新,新增UiObject2、Until、By、BySelector等:APIFor UI Automator

  5. Log 输出变更,以往使用System.out.print输出流回显至执行端,2.0 输出至Logcat

AndroidJUnitRunner AndroidJUnitRunner InstrumentationTestRunner Fundamentals of Testing UI Automator androidx.test Test UI for multiple apps Instrumentation

Android TextView对URL识别

发表于 2019-02-26 | 分类于 Android

IM开发过程中,对文本消息中的超练级进行点击处理,使用系统的tv.setAutoLinkMask(Linkify.PHONE_NUMBERS | Linkify.WEB_URLS);方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
/**
* 拦截超链接
*/
public static void interceptHyperLink(TextView tv, ChatContext chatContext, int msg_type,
long msg_id, String send_ucid) {
tv.setAutoLinkMask(Linkify.PHONE_NUMBERS | Linkify.WEB_URLS);
tv.setMovementMethod(LinkMovementMethod.getInstance());
CharSequence text = tv.getText();
if (text instanceof Spannable) {
int end = text.length();
Spannable spannable = (Spannable) tv.getText();
URLSpan[] urlSpans = spannable.getSpans(0, end, URLSpan.class);
if (urlSpans.length == 0) {
return;
}

SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text);
for (URLSpan uri : urlSpans) {
String url = uri.getURL();
CustomURLSpan custom = new CustomURLSpan(url, chatContext, msg_type, msg_id, send_ucid);
spannableStringBuilder.setSpan(custom, spannableStringBuilder.getSpanStart(uri),
spannableStringBuilder.getSpanEnd(uri), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
tv.setText(spannableStringBuilder);
}
}

Android自带的表达式(android.util.Patterns),在不同的ROM上表现形式是不一样的,在一些比较诡异的case上基本识别不出来,比如对于http://lianjia.com/xxx 啊啊啊这种连接,华为手机正常识别了,三星手机把后面的汉字也一起识别了,手机兼容性问题,最后只能自己写正则去匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
public class LinkifySpannableUtils {

public static LinkifySpannableUtils mInstance;

private Context mContext;
private TextView mTextView;
private SpannableStringBuilder mSpannableStringBuilder;

private LinkifySpannableUtils() {
}

public static LinkifySpannableUtils getInstance() {
if (mInstance == null) {
mInstance = new LinkifySpannableUtils();
}
return mInstance;
}

public void setSpan(Context context, TextView textView) {
this.mContext = context;
this.mTextView = textView;
addLinks();
}

private void addLinks() {
Linkify.addLinks(mTextView, WEB_URL, null);
Linkify.addLinks(mTextView, EMAIL_ADDRESS, null);
Linkify.addLinks(mTextView, PHONE, null);

CharSequence cSequence = mTextView.getText();
if (cSequence instanceof Spannable) {
int end = mTextView.getText().length();
Spannable sp = (Spannable) mTextView.getText();
URLSpan[] urls = sp.getSpans(0, end, URLSpan.class);
mSpannableStringBuilder = new SpannableStringBuilder(sp);
mSpannableStringBuilder.clearSpans();

for (URLSpan url : urls) {
String urlString = url.getURL();
PatternURLSpan patternURLSpan = new PatternURLSpan(urlString);
if (urlString != null && urlString.length() > 0) {
int _start = sp.getSpanStart(url);
int _end = sp.getSpanEnd(url);
try {
mSpannableStringBuilder.setSpan(patternURLSpan, _start, _end, Spannable.SPAN_EXCLUSIVE_INCLUSIVE);
} catch (Exception e) {
e.printStackTrace();
}
}
}
mTextView.setLinkTextColor(ColorStateList.valueOf(Color.BLUE));
mTextView.setHighlightColor(Color.parseColor("#AAAAAA"));
mTextView.setText(mSpannableStringBuilder);
}
}

private class PatternURLSpan extends ClickableSpan {

private String mString;

PatternURLSpan(String str) {
this.mString = str;
}

@Override
public void onClick(View widget) {
if (EMAIL_ADDRESS.matcher(mString).find()) {
sendEmail(mString);
} else if (WEB_URL.matcher(mString).find()) {
openUrl(mString);
} else if (PHONE.matcher(mString).find()) {
dialNum(mString);
} else {
if (mString.contains(".")) {
if (mString.startsWith("http")) {
openUrl(mString);
} else {
openUrl("http://" + mString);
}
}
}
}
}


/**
* 打开系统浏览器
* @param url
*/
private void openUrl(String url) {
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setData(Uri.parse(url));
intent.setClassName("com.android.browser",
"com.android.browser.BrowserActivity");
mContext.startActivity(intent);
}


/**
* 拨打电话
* @param num
*/
private void dialNum(final String num) {
if (num != null && num.length() > 0) {
call(num, mContext);
}
}

/**
* 调用邮箱
* @param address
*/
private void sendEmail(String address) {
String[] receive = new String[]{address};
Intent intent = new Intent(Intent.ACTION_SEND);
intent.setType("plain/text");
intent.putExtra(Intent.EXTRA_EMAIL, receive);
mContext.startActivity(Intent.createChooser(intent, ""));
}


private void call(final String mobile, final Context activity) {
if (mobile == null || mobile.length() == 0) {
Toast.makeText(activity, "电话号码为空", Toast.LENGTH_SHORT).show();
return;
}
String phone = mobile.toLowerCase();
if (!phone.startsWith("tel:")) {
phone = "tel:" + mobile;
}
final String callMobile = phone;

//适配6.0系统,申请权限
if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {

ActivityCompat.requestPermissions((Activity) activity,
new String[]{Manifest.permission.CALL_PHONE},
MainActivity.REQUESTCODE);
}else {
callPhone(activity,callMobile);
}


}

public static void callPhone(Context activity, String callMobile) {
Intent intent = new Intent(Intent.ACTION_CALL, Uri.parse(callMobile));
if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
return;
}
activity.startActivity(intent);
}


public final String TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL =
"(?:"
+ "(?:aero|arpa|asia|a[cdefgilmnoqrstuwxz])"
+ "|(?:biz|b[abdefghijmnorstvwyz])"
+ "|(?:cat|com|coop|c[acdfghiklmnoruvxyz])"
+ "|d[ejkmoz]"
+ "|(?:edu|e[cegrstu])"
+ "|f[ijkmor]"
+ "|(?:gov|g[abdefghilmnpqrstuwy])"
+ "|h[kmnrtu]"
+ "|(?:info|int|i[delmnoqrst])"
+ "|(?:jobs|j[emop])"
+ "|k[eghimnprwyz]"
+ "|l[abcikrstuvy]"
+ "|(?:mil|mobi|museum|m[acdeghklmnopqrstuvwxyz])"
+ "|(?:name|net|n[acefgilopruz])"
+ "|(?:org|om)"
+ "|(?:pro|p[aefghklmnrstwy])"
+ "|qa"
+ "|r[eosuw]"
+ "|s[abcdeghijklmnortuvyz]"
+ "|(?:tel|travel|t[cdfghjklmnoprtvwz])"
+ "|u[agksyz]"
+ "|v[aceginu]"
+ "|w[fs]"
+ "|(?:\u03b4\u03bf\u03ba\u03b9\u03bc\u03ae|\u0438\u0441\u043f\u044b\u0442\u0430\u043d\u0438\u0435|\u0440\u0444|\u0441\u0440\u0431|\u05d8\u05e2\u05e1\u05d8|\u0622\u0632\u0645\u0627\u06cc\u0634\u06cc|\u0625\u062e\u062a\u0628\u0627\u0631|\u0627\u0644\u0627\u0631\u062f\u0646|\u0627\u0644\u062c\u0632\u0627\u0626\u0631|\u0627\u0644\u0633\u0639\u0648\u062f\u064a\u0629|\u0627\u0644\u0645\u063a\u0631\u0628|\u0627\u0645\u0627\u0631\u0627\u062a|\u0628\u06be\u0627\u0631\u062a|\u062a\u0648\u0646\u0633|\u0633\u0648\u0631\u064a\u0629|\u0641\u0644\u0633\u0637\u064a\u0646|\u0642\u0637\u0631|\u0645\u0635\u0631|\u092a\u0930\u0940\u0915\u094d\u0937\u093e|\u092d\u093e\u0930\u0924|\u09ad\u09be\u09b0\u09a4|\u0a2d\u0a3e\u0a30\u0a24|\u0aad\u0abe\u0ab0\u0aa4|\u0b87\u0ba8\u0bcd\u0ba4\u0bbf\u0baf\u0bbe|\u0b87\u0bb2\u0b99\u0bcd\u0b95\u0bc8|\u0b9a\u0bbf\u0b99\u0bcd\u0b95\u0baa\u0bcd\u0baa\u0bc2\u0bb0\u0bcd|\u0baa\u0bb0\u0bbf\u0b9f\u0bcd\u0b9a\u0bc8|\u0c2d\u0c3e\u0c30\u0c24\u0c4d|\u0dbd\u0d82\u0d9a\u0dcf|\u0e44\u0e17\u0e22|\u30c6\u30b9\u30c8|\u4e2d\u56fd|\u4e2d\u570b|\u53f0\u6e7e|\u53f0\u7063|\u65b0\u52a0\u5761|\u6d4b\u8bd5|\u6e2c\u8a66|\u9999\u6e2f|\ud14c\uc2a4\ud2b8|\ud55c\uad6d|xn\\-\\-0zwm56d|xn\\-\\-11b5bs3a9aj6g|xn\\-\\-3e0b707e|xn\\-\\-45brj9c|xn\\-\\-80akhbyknj4f|xn\\-\\-90a3ac|xn\\-\\-9t4b11yi5a|xn\\-\\-clchc0ea0b2g2a9gcd|xn\\-\\-deba0ad|xn\\-\\-fiqs8s|xn\\-\\-fiqz9s|xn\\-\\-fpcrj9c3d|xn\\-\\-fzc2c9e2c|xn\\-\\-g6w251d|xn\\-\\-gecrj9c|xn\\-\\-h2brj9c|xn\\-\\-hgbk6aj7f53bba|xn\\-\\-hlcj6aya9esc7a|xn\\-\\-j6w193g|xn\\-\\-jxalpdlp|xn\\-\\-kgbechtv|xn\\-\\-kprw13d|xn\\-\\-kpry57d|xn\\-\\-lgbbat1ad8j|xn\\-\\-mgbaam7a8h|xn\\-\\-mgbayh7gpa|xn\\-\\-mgbbh1a71e|xn\\-\\-mgbc0a9azcg|xn\\-\\-mgberp4a5d4ar|xn\\-\\-o3cw4h|xn\\-\\-ogbpf8fl|xn\\-\\-p1ai|xn\\-\\-pgbs0dh|xn\\-\\-s9brj9c|xn\\-\\-wgbh1c|xn\\-\\-wgbl6a|xn\\-\\-xkc2al3hye2a|xn\\-\\-xkc2dl3a5ee0h|xn\\-\\-yfro4i67o|xn\\-\\-ygbi2ammx|xn\\-\\-zckzah|xxx)"
+ "|y[et]" + "|z[amw]))";

public final String GOOD_IRI_CHAR = "a-zA-Z0-9\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF";


public final Pattern WEB_URL = Pattern
.compile("((?:(http|https|Http|Https|rtsp|Rtsp):\\/\\/(?:(?:[a-zA-Z0-9\\$\\-\\_\\.\\+\\!\\*\\'\\(\\)"
+ "\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,64}(?:\\:(?:[a-zA-Z0-9\\$\\-\\_"
+ "\\.\\+\\!\\*\\'\\(\\)\\,\\;\\?\\&\\=]|(?:\\%[a-fA-F0-9]{2})){1,25})?\\@)?)?"
+ "((?:(?:["
+ GOOD_IRI_CHAR
+ "]["
+ GOOD_IRI_CHAR
+ "\\-]{0,64}\\.)+" // named host
+ TOP_LEVEL_DOMAIN_STR_FOR_WEB_URL
+ "|(?:(?:25[0-5]|2[0-4]" // or ip address
+ "[0-9]|[0-1][0-9]{2}|[1-9][0-9]|[1-9])\\.(?:25[0-5]|2[0-4][0-9]"
+ "|[0-1][0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1]"
+ "[0-9]{2}|[1-9][0-9]|[1-9]|0)\\.(?:25[0-5]|2[0-4][0-9]|[0-1][0-9]{2}" + "|[1-9][0-9]|[0-9])))"
+ "(?:\\:\\d{1,5})?)" // plus option port number
+ "(\\/(?:(?:[" + GOOD_IRI_CHAR + "\\;\\/\\?\\:\\@\\&\\=\\#\\~" // plus option query
// params
+ "\\-\\.\\+\\!\\*\\'\\(\\)\\,\\_])|(?:\\%[a-fA-F0-9]{2}))*)?" + "(?:\\b|$)");

public static final Pattern EMAIL_ADDRESS = Pattern.compile("[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" + "\\@"
+ "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" + "(" + "\\." + "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" + ")+");
public static final Pattern EMAIL_PATTERN = Pattern.compile("[A-Z0-9a-z\\._%+-]+@([A-Za-z0-9-]+\\.)+[A-Za-z]{2,4}");
public static final Pattern WEB_PATTERN =
Pattern
.compile("((http[s]{0,1}|ftp)://[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)|(www.[a-zA-Z0-9\\.\\-]+\\.([a-zA-Z]{2,4})(:\\d+)?(/[a-zA-Z0-9\\.\\-~!@#$%^&*+?:_/=<>]*)?)");

public static final Pattern PHONE = Pattern.compile( // sdd = space, dot, or dash
"(\\+[0-9]+[\\- \\.]*)?" // +<digits><sdd>*
+ "(\\([0-9]+\\)[\\- \\.]*)?" // (<digits>)<sdd>*
+ "([0-9][0-9\\- \\.][0-9\\- \\.]+[0-9])");


}

上述WEB_URL正则仍不能正常识别,最后采用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// all domain names
private static final String[] ext = {
"top", "com.cn", "com", "net", "org", "edu", "gov", "int", "mil", "cn", "tel", "biz", "cc", "tv", "info",
"name", "hk", "mobi", "asia", "cd", "travel", "pro", "museum", "coop", "aero", "ad", "ae", "af",
"ag", "ai", "al", "am", "an", "ao", "aq", "ar", "as", "at", "au", "aw", "az", "ba", "bb", "bd",
"be", "bf", "bg", "bh", "bi", "bj", "bm", "bn", "bo", "br", "bs", "bt", "bv", "bw", "by", "bz",
"ca", "cc", "cf", "cg", "ch", "ci", "ck", "cl", "cm", "cn", "co", "cq", "cr", "cu", "cv", "cx",
"cy", "cz", "de", "dj", "dk", "dm", "do", "dz", "ec", "ee", "eg", "eh", "es", "et", "ev", "fi",
"fj", "fk", "fm", "fo", "fr", "ga", "gb", "gd", "ge", "gf", "gh", "gi", "gl", "gm", "gn", "gp",
"gr", "gt", "gu", "gw", "gy", "hk", "hm", "hn", "hr", "ht", "hu", "id", "ie", "il", "in", "io",
"iq", "ir", "is", "it", "jm", "jo", "jp", "ke", "kg", "kh", "ki", "km", "kn", "kp", "kr", "kw",
"ky", "kz", "la", "lb", "lc", "li", "lk", "lr", "ls", "lt", "lu", "lv", "ly", "ma", "mc", "md",
"mg", "mh", "ml", "mm", "mn", "mo", "mp", "mq", "mr", "ms", "mt", "mv", "mw", "mx", "my", "mz",
"na", "nc", "ne", "nf", "ng", "ni", "nl", "no", "np", "nr", "nt", "nu", "nz", "om", "qa", "pa",
"pe", "pf", "pg", "ph", "pk", "pl", "pm", "pn", "pr", "pt", "pw", "py", "re", "ro", "ru", "rw",
"sa", "sb", "sc", "sd", "se", "sg", "sh", "si", "sj", "sk", "sl", "sm", "sn", "so", "sr", "st",
"su", "sy", "sz", "tc", "td", "tf", "tg", "th", "tj", "tk", "tm", "tn", "to", "tp", "tr", "tt",
"tv", "tw", "tz", "ua", "ug", "uk", "us", "uy", "va", "vc", "ve", "vg", "vn", "vu", "wf", "ws",
"ye", "yu", "za", "zm", "zr", "zw"
};

static {
StringBuilder sb = new StringBuilder();
sb.append("(");
for (int i = 0; i < ext.length; i++) {
sb.append(ext[i]);
sb.append("|");
}
sb.deleteCharAt(sb.length() - 1);
sb.append(")");
// final pattern str
String pattern = "((https?|s?ftp|irc[6s]?|git|afp|telnet|smb)://)?((\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})|((www\\.|[a-zA-Z\\.\\-]+\\.)?[a-zA-Z0-9\\-]+\\." + sb.toString() + "(:[0-9]{1,5})?))((/[a-zA-Z0-9\\./,;\\?'\\+&%\\$#=~_\\-]*)|([^\\u4e00-\\u9fa5\\s0-9a-zA-Z\\./,;\\?'\\+&%\\$#=~_\\-]*))";
// Log.v(TAG, "pattern = " + pattern);
WEB_URL = Pattern.compile(pattern);
}

设置了Linkify.addLinks后导致ClickableSpan的点击无法拦截

设置了Linkify.addLinks后导致ClickableSpan的点击无法拦截,会调用隐式意图打开配置了filter的Activity 使用下面步骤:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public static void interceptHyperLink(TextView tv, ChatContext chatContext, int msg_type,
long msg_id, String send_ucid) {
tv.setMovementMethod(LinkMovementMethod.getInstance());
CharSequence text = tv.getText();
if (text instanceof Spannable) {
SpannableStringBuilder spannableStringBuilder = new SpannableStringBuilder(text.toString());
Linkify.addLinks(spannableStringBuilder, CommonPatterns.CHINESE_PHONE_NUMBER, PHONE_SCHEME);
Linkify.addLinks(spannableStringBuilder, CommonPatterns.WEB_URL, HTTP_SCHEME);
Linkify.addLinks(spannableStringBuilder, CommonPatterns.AUTOLINK_WEB_URL, HTTP_SCHEME);

URLSpan[] urlSpans = spannableStringBuilder.getSpans(0, spannableStringBuilder.length(), URLSpan.class);
if (urlSpans.length == 0) {
return;
}
for (URLSpan uri : urlSpans) {
String url = uri.getURL();
CustomURLSpan custom = new CustomURLSpan(url, chatContext, msg_type, msg_id, send_ucid);
int spanStart = spannableStringBuilder.getSpanStart(uri);
int spanEnd = spannableStringBuilder.getSpanEnd(uri);
spannableStringBuilder.removeSpan(uri);
spannableStringBuilder.setSpan(custom, spanStart,
spanEnd, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
tv.setText(spannableStringBuilder);
}
}

当需要使自定义模式和内置模式web,phone等一起被识别时,一定要先声明内置模式,然后再声明自定义模式,而且不能在xml中通过autoLink属性声明,否则自定义模式不起作用。因为在设置内置模式时,会先删除已有模式。

使用该方式拦截点击事件的话,Linkify.addLinks(spannableStringBuilder, CommonPatterns.WEB_URL, HTTP_SCHEME); http和https需要分开,如果不分开,https的链接也会被加上http变成http://http://xxx,同时HTTP_SCHEME不能设置为空,如果设置为空的话,不再判断系统的scheme头,如baidu.com不会自动增加http变成https://baidu.com.

1…456…19
轻口味

轻口味

190 日志
27 分类
63 标签
RSS
GitHub 微博 豆瓣 知乎
友情链接
  • SRS
© 2015 - 2019 轻口味
京ICP备17018543号
本站访客数 人次 本站总访问量 次