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

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再添加回来。


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

Nexus 6P 外媒评测:华为造出了最好的 Android 手机?

$
0
0

Nexus 系列一直都是 Android 阵营标杆的存在,其“纯粹 Android”的体验也有相当数量的拥趸。而今年的 Google 也算是顺应潮流在一场发布会上同时发布了两款 Nexus 手机。其中的 Nexus 6P 除了是今年的 Nexus 旗舰之外,还有一个相比往年的 Nexus 手机更吸引中国用户的地方——它是首款国产品牌代工的 Nexus。

其配置也在今年的 Android 机群中属于第一梯队:

  • 5.7 吋 2K AMOLED 屏幕
  • 前置 800 万像素摄像头/后置 1230万像素摄像头
  • 骁龙 810 + 3GB RAM
  • USB Type-c 接口
  • 电池 3450mAh 支持快充
  • 背部指纹识别
  • Android 6.0 Marshmallow

那么,纯净 Android 加上不俗的硬件,会擦出怎样的火花呢?

最为纯粹的 Android 系统体验

Nexus 这个品牌的产品,对于多数消费者以及开放者来说,最大的意义在于,能第一时间升级到 Android 系统的最新版本。作为今年最新的 Nexus 手机,Nexus 6P 搭载的自然是最新的 Android 6.0 Marshmallow。Nexus 6P 的系统体验高度也在很大程度上代表了 Android 6.0 Marshmallow 系统体验高度。

Android-m-1

Android 6.0 Marshmallow 作为一个较大的更新,我站之前已经有过详细的体验,各位可以移步《 解读 Android 6.0 如何让你甜到心窝》,对这个系统深入了解一下。

Now on Tap 即时语义搜索、应用权限控制与应用链接管理等新功能的加入让 Android 的体验进一步补完。很值得说到的一点是指纹识别,由于 Android 6.0 从系统层面支持了指纹识别,所以 Nexus 6P 的指纹识别过程流畅而准确。

nexus-6p-9705.0

如果说 Android 5.0 是 Google 对用户界面和人机交互思考的结晶,而 Android 6.0 便是 Google 对用户体验的整体思考的答卷。这个版本的系统非常值得升级。

紧随潮流的大面积金属设计

Nexus 系列手机虽然一直被戏称为“买系统,送手机”,但 Nexus 手机在材质方面还是有一定尝试的。比如第一款 Nexus 手机——Nexus One 就尝试了金属材质,尽管当时的手机多以工程塑料作为机身材质。

Nexus-8-980x653

而今年的两款 Nexus 手机中,相对高端的 Nexus 6P 则直接用上了大面积的阳极氧化铝,这让手机的质感和触感都不错。用 The Verge 的话来说是,后盖的大面积金属让 Nexus 6P 看起来很“高级(Premium)”,手感也不会粗糙;也不会有此前担心的太过滑腻的问题。

Nexus-26-980x653

至于那块效果图上让不少人吐槽的黑条,实际上并没有那么突兀,尤其是在黑色的机身上。不过 The Verge 还是认为这块儿设计不够讨喜。

整体来说,Nexus 6P 的设计还是不错的。 Engadget则还指出了一些小缺点,Nexus 6P 的设计不过不失,只是略显单调;另外,电源键有些太过敏感。

不再鸡肋的相机?

此前多代 Nexus 手机的系统体验部分都非常出色、工业设计也各有特色,但多媒体(尤其是摄像头部分)的表现都不尽如人意。

nexus-6p-9748.0

Nexus 6P 的摄像头采用了索尼的 1230 万 CMOS ,型号为 IMX377,并且支持激光对焦,而发布会上,Google 一直强调的是 Nexus 6P 的主相机单像素尺寸为 1.55μm,而像素值相近的 iPhone 6s Plus 为 1.22μm。一般来说,更大的单位像素面积能带来更好的弱光拍照效果。

硬件之外,在 DxO 的拍照排名中,Nexus 6P 的表现颇佳,获得了照片 86 分、视频 79 分,综合 84 分的好成绩,在整个排行榜中排名第三,超过了 iPhone 6s,仅次于索尼 Z5 和三星 S6 edge。

DxO 对其拍照的评价是:低光下的细节表现力极其出色;户外情况下,细节表现、白平衡以及色彩很好;对焦也快速准确;曝光准确。不足之处在于,低光下,HDR+ 模式会出现可见的曝光、色彩、质感和噪点问题;高动态下,高光部分偶尔表现不佳。

理论是理论,样张才是王道。The Verge 用 Nexus 6P 拍摄了一组样张:

IMG_20151017_135703.0IMG_20151017_184357.0IMG_20151017_133722.0IMG_20151017_132850.0IMG_20151017_132645.0IMG_20151016_193608.0

而 Engadget 对 Nexus 6P 的拍照表现也是持肯定态度的,认为 6P 比前代 Nexus 6 的相机表现超出不少,不过相比三星等竞争对手还有差距。

用了骁龙 810 的亲儿子

Nexus 6P 使用了骁龙 810 这款被吐槽不少的处理器,旗舰性能自是不用担心。Engadget 使用 Nexus 6P 与其他旗舰一起进行了跑分测试。由于 Nexus 一向没有针对跑分软件的优化技巧,所以 Nexus 6P 的分数很有 Nexus “传统”——跑分低于常见的 Android 旗舰机。

Nexus 6P bench

但有骁龙 810 处理器的加持,性能这些就不会是瓶颈。The Verge 和 Engadget 都表示,搭配着 Android 6.0,Nexus 6P 无论是日常操作还是大型游戏,都非常快,丝毫不会有卡顿出现。

而真正的隐忧是与骁龙 810 伴生的功耗问题。Nexus 6P 有历代 Nexus 手机中最大的 3450mAh 电池,但 Nexus 6P 的续航表现并没有达到人们对这块大电池的高预期。整体续航表现比 Moto X Pure 版要强一些,但仍不比 3000mAh 的三星 Note5。

最新款的 Nexus 手机与最好的 Android 手机

这台由华为打造的 Nexus,配置、硬件、拍照等都达到了一定的水准,加上目前独一份的 Android 6.0 系统。这些都让 Nexus 6P 的竞争力和吸引力大了不少。The Verge 对于 Nexus 6P 的综合评价颇高:

这就是最好的 Android 手机。

只是,由于中国市场的特殊性,这个“最好”显得有些不够接地气。

图片来自 The VergeEngadgetArstechnica

相逢意气为君饮,系马高楼垂柳边。

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



爱范儿 |原文链接· 查看评论· 新浪微博


Android APP性能调优 一本正经的胡说八道的前言

$
0
0

“一本正经地胡说八道”用日语怎么说?大概是「真面目にふざけている」吧。这篇日志大概就是这么一个意思?

一直以来都想对Android APP开发的性能调优做一下总结,其实性能调优涉及到多方面的工作,每次有一些心得我都会记录下来,零零散散记录了很多,最近发现许多地方重复了,感觉还是得做一下整理的,知识就是这么牢固起来的。

“APP卡顿”是一个问题,我们既需要知道怎么查找出哪里造成卡顿,也需要掌握规避这些卡顿的技巧,所以这个话题可以分为“如何定位APP中的性能问题”和“提高性能需要注意哪些点”这两部分,后续在陆续对这两点展开讨论吧,今天先从整体分析下问题存在的原因。

开始正题之前先让我吐一吐苦水吧。

我个人喜欢日语,所以学了很久的日语了,同样我也是因为喜欢Android,才开始跳进了Android开发这个坑。不过非常遗憾的是,就和“当初我在日语班里,周围大部分人只是因为日语专业比较轻松才选了它”一样,我周围的Android研发同事大部分用的是IOS手机,甚至有人问过我“我说你工资也不至于那么低吧,怎么每天都拿着安卓手机”。产品则是每次都说“你这个交互和IOS的不一样,我需求文档里写得清清楚楚,要保持一致的用户体验”。设计的话,我从来就没有遇见过拿安卓手机的。

我尝试说过Android不比IOS差,但是没人站在我这边。

自从Android系统诞生以来一直都有一个疑问,“为什么Android手机这么卡?”,无论Android设备的硬件再怎么升级,版本再怎么迭代,“Android比IOS卡顿”似乎成为一个板上钉钉的事实。Android手机真的卡么?

至少在Android Kitkat之前,许多Android开发者都会选择回避这个问题;Kitkat之后,有一部分开发者已经有底气回答这个问题了;随着Android Lollipop以及Marshmallow先后的出现,我觉得Android开发者都可以自信地回答说“Android不卡”了。

在一个公司内部分享会中,国内Android领域的大神罗升阳在分享他对ART模式的研究中说到,继Kitkat采用了ART模式后,Lollipop中,除了UI线程之外又提供了一条专门用于执行动画的线程,这样一来UI线程就可以专注于展示数据和响应用户的输入,这让Android的流畅度又上了一个档次。IOS是闭源的所以我们无从得知,但是有人分析说IOS之所以这么流畅很大一个原因就是它大概也采用了类似动画线程的机制。老罗也相信“Android系统不会比IOS卡,甚至已经比它还流畅”。

这可不是随便说说,我的Nexus5升级到Lollipop之后我觉得它已经可以匹敌同事的IPhone6了,前不久升级到Marshmallow内测版,我更是觉得它已经拉开IPhone6一个档次了。

那为什么许多人觉得Android用起来还是比IOS卡?注意老罗说“Android系统”比IOS流畅,而不是“Android应用”,言下之意就是Android系统本身不卡,卡的是设备开发商开发的ROM以及开发者开发的第三方APP。我个人觉得Android卡顿问题大致有以下的原因。

客观原因

Android设备系统升级速度慢

现在Lollipop甚至Kitkat的普及率还不算很高,更别谈Marshmallow了,这是Android碎片化的问题决定的,具体原因有兴趣的可以自己Google,网上一堆比我在这里吹的靠谱多的分析。

当然这里也有很大一部分是设备开发商的锅,Google开源的AOSP项目只能兼容Nexus系列手机的硬件,如果第三方设备开发商需要使用AOSP的话,最起码也要把AOSP里面的驱动部分改成能兼容自己的设备的,此外,他们还喜欢把系统UI风格做成自己家的,这起码也要自己写一个Launcher应用和自定义主题(这也是许多Android ROM被吐槽成换皮肤的原因);此外有一些特色功能,比如指纹设备,Android Marshmallow之前AOSP并没有这个功能,所以开发商就得自己出解决方案了。这一系列的工作,造成了开发商无法在Google发布Android的新版本之后迅速升级自家Android设备的系统。

Android APP需要做过多旧版本的兼容

Android4.0版本相比之前的版本性能上优化了许多,无奈我接触的Android产品都要求最低支持Android2.3,有一款SDK甚至要求支持到Android1.6而且不能使用Support库(今年可是2015年!你能想象一个手动写Thread去控制一个复杂的属性动画的效果有多糟糕吗?),所以Android开发者需要做一大堆向下兼容的工作,有时候为了保证在一些奇葩机型的兼容性,选择了保守的实现方式而不采用Android的新特性。向下兼容的逻辑使得APP即使在高级版本的Android系统上也要跑一堆没用的判断逻辑,如果这些逻辑出现在循环体内则更糟糕;采用保守的实现方式,使得APP无法发挥新版本Android的性能,即使用户手机升级了Android系统版本也享♂受新版本带来的体验。此外,即使许多新的APP产品都选择最低支持Android4.0,这使得许多新特性都不用Support库支持就能直接使用,但是许多手机设备还是无法升级到Android4.4版本的系统,即使有,很多ROM还是把ART模式给严格了,无法体验其脱胎换骨版的顺畅。

大量采用“黑科技”

最近支付宝被Google Play下架了,原因就是其“从Play市场以外的服务器下载可执行代码”,意思就是它使用了动态加载技术。Android的动态加载也不是新鲜的事物了,我的项目中也采用过,简单来说原理就是Android APP采用“APK空壳+可执行代码”的开发方式,APK只是一个空壳,用户安装过一次后就不用重新安装了,如果需要升级APP,APK空壳会从服务器下载新的可执行代码,更换本地的即可完成升级(具体实现方案现在网上一堆教程,也可以参考我Github上的相关项目)。采用这种开发方式,开发者可以迅速完成用户安装好的APP的升级,在某种意义上提高了用户的体验,但是开发者的开发方式也会变得比较“绕”,开发成本增加了不少,为了保证兼容性也会放弃使用Android的一些无法在动态加载框架上使用的功能,所以体验往往比不上“正统”的Android开发方式。

常驻内存、全家桶以及互相唤醒

缺少了Google Play的约束,一些Android APP就变得肆意妄为了,这一点在BAT系中显得格外明显。常驻内存就接受到服务器的推送信息的成功率就比较高了,用户明明关了一些APP,但是它们就不想退出,就算我们手动关闭了它们,一旦重新启动、网络变化等,它们就又重新启动了,占着本来就珍贵的手机内存,很快就不够用了,这也是Android卡顿的一大原因。“百度全家桶”你怕不怕?,一旦安装了百度家族启动的一个APP,就会偷偷帮你安装上全家族的APP,就算是我这种做Android研发的都经常中招,不用说普通的用户了。更可恨的是这些APP还会互相唤醒,Andriod Lollipop之后你可以彻底关闭一个APP,除非你手动启动它否则它无法自启动,但是家族APP之间互相唤醒使得这成为了可能,“绿色守护”等后台清理神器在“互相唤醒”大法之下也没辙了。

H5内容、硬生生把Android应用做成IOS应用等等

Android原生与H5界面交互的框架已经很成熟了,许多中大型的APP的有H5的界面,特别是淘宝、天猫、京东这种购物类的,这些APP有大量和时间相关的活动,在节日之前开发一个新版本的APP再发布到应用市场显得太蠢了,H5网页这种随时可以更新在线内容的技术最适合这种活动了。然而H5界面的性能在还是肛不过原生界面,特别是各种黑科技附身的Lollipop之后的时代,特别是当一个界面有H5的东西也有原生的东西那就更加糟糕了。此外,国内设计师都喜欢把Android的美术图设计得和IOS一样,这里不讨论这样做的原因,然而结果就是Android的开发需要使用大量的自定义View,这样一来,许多系统自带控件的加速效果(特别是当用到系统公共资源的时候)就没有什么卵用了,而且往往这些自定义View都有或多或少的Bug,可能设计得不合理,也可能有内存泄露,这些都会影响APP的性能。

主观原因

上面说的客观原因除了由于Android的碎片化问题之外,其实有很大部分是开发者有意为之,但是我相信这并不是Android开发的责任,我们是都是清白的,都是无奈的,错的不是我们,是世界啊!╮( ̄▽ ̄")╭。也不是产品经理的锅,而是大环境造就的。国内互联网的竞争堪比电商,不这么做,不抢占用户的手机的话根本就无法生存,就算百度不去做,阿里也会去……所以勉强可以把这些归类为客观因素。但是瞎BB了这么多,都不是我想说的主要内容,我想许多人都不感兴趣,我只是一时来兴致了敲了这么多字,感觉如果不贴出来的话不就亏了嘛。
…………
……

既然你都看到这了,顺便把主观原因部分也看完吧_(-ω-`_)⌒)_。

主观原因主要是由于开发过程中的过错导致的,总体上来说大致有以下几方面的原因

没做好异步

APP要流畅的话就好让它保持高度相应,CPU要能及时响应UI线程的操作。不过不可能把所有非UI的工作统统扔到异步任务里面去,有时候一些工作直接在UI线程搞会非常方便,而且太多后台线程也会造成大量的性能开销。这是一个矛盾,这时候就需要成熟的异步任务框架咯,如果这个框架没写好的话,就可能导致后台任务混乱,产生多余的线程开销,UI线程得不到及时的响应,更甚,如果有异步任务造成内存泄露,内存不够用很快就卡顿了,甚至直接OOM嗝屁了。

内存泄露以及GC

内存是非常宝贵的,再多也不嫌多。所以当一个对象离开他的作用域后,我们一定立刻回收它占有的内存,最理想的状态是对每一个对象都能做到这样,如果有一个对象做不到,就说明他泄露了。Android中,Activity对象以及Bitmap的像素数据往往占用非常大的内存,如果这两者发生泄漏会导致可用内存急剧减少,那卡顿则就无法避免。此外,即使没有严重的内存泄露,但是频繁创建对象和回收对象的话,会引发虚拟机频繁的GC,GC占用比较大的资源开销,同样也可能会导致卡顿。总的来说,开发过程中要对内存的使用保持敏感,知根知底。

没做好界面的布局优化

设计师给出的同一张美术图可以有多张的布局实现,不同方案之间的性能可能相差很远。ListView等列表控件的Adapter也有许多优化点,许多人容易疏忽。

没做好代码优化

这就不只是Android领域的问题啦,某些特定的业务应该采用最优算法,把时间复杂度降到最低,尽量避免指数级别的时间复杂度,尽量以空间换时间,必要的时候使用Native库来提高算法。

其他

一开始我也提到了,其实性能调优涉及到多方面的工作,比如一些静态的网络资源要做好缓存不要重复请求;频繁数据库操作的话最好使用异步任务,SQLite默认实在UI线程直接操作数据库的;使用反射也会比较性能,不过反射有时候确实挺方便的,特别是项目庞大的时候,这个看取舍;过度使用“设计模式”也会有额外的开销,设计意味着更多接口和多态,业务跑起来需要额外的空间和时间;尽量不要开多进程,进程之间的通讯比同一进程之间的互调消耗的性能非常多,一般项目只要一个进程就够了,有推送的可以多一个推送进程;此外,不限制与Android客户端开发,H5、服务器的优化也能提高APP的性能。

性能问题的排查方法

这个在后面的具体分析中,再结合实际遇到的问题一起介绍吧。

关于性能优化的技术点,欢迎大家到这个日志里补充: [收集向] Android 性能调优的技术点

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 数据。

DexClassLoader 实现 Android 插件加载

$
0
0

Java 中的 ClassLoader:

Java 中 ClassLoader用于动态加载 Class到 JVM,包含 BootstrapClassLoader(C++ 编写,用于加载系统核心类)、 ExtClassLoader(用于加载 lib/ext/ 目录的扩展 API)、 AppClassLoader(加载 CLASSPATH目录下的类)。

双亲委托机制:

  • 任何自定义 ClassLoader 都必须继承 ClassLoader抽象类,并指定其 parent 加载器,默认为 BootstrapClassLoader;

  • 任何自定义 ClassLoader 在加载一个类之前都会先委托其 parent 去加载,只有 parent 加载失败才会自己加载;

    • 这样既可以防止重复加载,又可以排除安全隐患(防止用户替换系统核心类);

    • 所以一般只需要重写 findClass()方法即可(在 parent 加载失败时调用);

  • 双亲委托机制是在 loadClass()方法实现的,要想避开(自己验证安全性,比如 Tomcat 的 WebAppClassLoader),必须重写 loadClass()方法;

自定义 ClassLoader 用途:

  • 在执行非置信代码前先做签名认证等;

  • 从网络、数据库等动态加载类;

类的卸载:

  • 只有当类的实例被回收,才会被 unload,但被 BootstrapClassLoader加载的系统类除外;

  • 重复加载类会报异常,只能重新定义新的 ClassLoader 再次加载;

Dalvik 的 ClassLoader:

  • Android 里 ClassLoaderdefineClass()方法直接抛出 UnsupportedOperationException异常,必须借助 DexClassLoaderPathClassLoader

  • DexClassLoaderPathClassLoader都遵循双亲委托机制,因为只重写了 findClass()方法,没有重写 loadClass()方法;

  • Dalvik 虚拟机识别的是 DexFile而不是 JarFile;且 DexFile.loadClass()方法必须通过类加载器调用,否则无效;

利用 DexClassLoader 实现 Android 插件加载:

比如我们在主应用 HostApp 中需要调用 视频插件 VideoPlayerPlugin 中的 playVideo() 方法。

给插件加入 Intent 标识:

HostApp 要查询插件信息,只能通过 PackageManager。这里我首先想到的是直接通过其 getPackageInfo()方法。但是试想,可能插件有很多个,而且包名不同。所以最好还是通过在插件中定义空的 Activity并加入 Intent标识,然后调用 queryIntentActivities()方法去查询插件信息:

1     
2
3
4
5
<activity android:name=".plugin">     
<intent-filter>
<action android:name="com.rincliu.videoplayerplugin"/>
</intent-filter>
</activity>

查询插件信息:

首先使用 PackageMananer查询到插件的 packageNameApplicationInfo:

1     
2
3
4
5
Intent intent = new Intent("com.rincliu.videoplayerplugin");     
List<ResolveInfo> plugins = getPackageManager().queryIntentActivities(intent, 0);
ActivityInfo act = plugins.get(0).activityInfo;
String packageName = act.packageName;
ApplicationInfo app = act.applicationInfo;

上面是直接读取的第一条信息( plugins要先判空),如果有很多种插件,或者有好几个版本,这样就需要继续读取插件的版本号等配置信息作进一步区分:

1     
2
3
Resources res = pm.getResourcesForApplication(packageName);     
int resId = res.getIdentifier("version", "string", packageName);
String version = res.getString(resId);

使用 DexClassLoader 调用插件

创建 DexClassLoader对象:

1     
2
3
4
5
String dexSourceDir = app.sourceDir;     
String dexOutputDir = getApplicationInfo().dataDir;
String dexLibDir = app.nativeLibraryDir;
ClassLoader parentLoader = this.getClass().getClassLoader();
DexClassLoader loader = new DexClassLoader(dexSourceDir, dexOutputDir, dexLibDir, parentLoader);

使用反射调用插件中的方法:

1     
2
3
4
5
6
7
8
9
10
11
12
try {     
Class<?> clazz = loader.loadClass(packageName + ".VideoPlayerPlugin");

//Object obj = clazz.newInstance();
Constructor<?> localConstructor = clazz.getConstructor();
Object obj = localConstructor.newInstance();

//Method method = ((Class<?>) obj).getMethod("play", String.class);
Method method = clazz.getDeclaredMethod("play", String.class);

method.invoke(obj, "/sdcard/demo.mp4");
} catch (Exception e) {}

完整代码

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

$
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表中的所有数据

最近碰到的一些 SSL 问题记录

$
0
0

最近碰到一些 SSL 的小问题,特记录下。

我们有个 Java 实现的 SSL TCP 服务端,为客户端(PC、Android 和 iOS)提供 SSL 接入连接服务。最近有用户反馈其手机上 App 不能正常连接登录,别人手机上都可以。经过单独回访调查该用户使用的手机操作系统是 Android 6.0,经搜索了解了 Android 6.0 之后 Google 使用了自家的 BoringSSL 替换了原来的 OpenSSL,怀疑是这里在捣鬼。

继续搜索类似问题解决方案,在参考[1] 中找到答案:

SSL/TLS握手过程中,假如选中了诸如 TLS_DHE_RSA_WITH_AES_128_CBC_SHA 这样使用 deffie-hellman 密钥的 cipher,那么在 deffie-hellman 密钥交换过程中会使用的一个P参数(prime number),服务器侧提供的 P 参数在 JDK8 之前都只用了 768bit 的长度,小于 1024bit 存在安全漏洞可导致 logjam attack,会被最新本版的浏览器和 BoringSSL 拒绝。

明了了原因后我们只好把 JDK 从 6 升级到了 8,顺利解决 Android6.0 SSL 握手失败问题。但解决完这个后,没多久又发现 APNS iOS 推送又不可用了,和苹果推送服务器建立 SSL 连接失败,无法推送消息。唯一的变化就是升级到了 JDK8 自然就将怀疑目标对准了 JDK8。

继续 Google 一把找了同类受害者,他已经搞明白了原因,见参考[2]

The problem was the exported keystore (in PKCS12 format) contained the private key as well as the production certificate and the development certificate for push notifications. Java can use keystores in the PKCS12 format. But Java 6 and Java 8 did not read-in the keystore the same way. It looks like Java 6 read in the production certificate for the private key and Java 8 read in the development certificate.

解决办法也很简单,先用 JDK6 提供的 keytool 将 .p12 格式的证书转换为 .jks 格式。再用 JDK8 提供的 keytool 将刚生成的 .jks 证书转换为 .p12 格式。转换命令如下:

.p12 -> .jks
/JDK6/keytool -importkeystore -destkeystore apns.jks -srckeystore apns_jdk6.p12 -srcstoretype PKCS12

.jks -> .p12
/JDK8/keytool -importkeystore -srckeystore apns.jks -srcstoretype JKS -deststoretype PKCS12 -destkeystore apns_jdk8.p12

参考

[1] liuxian233. Android 6.0 HTTPS连接ssl3_get_server_key_exchange:BAD_DH_P_LENGTH错误问题
[2] szediwy. Apple Push Notification with Java
[3] ASHISH PARAB. APPLE PUSH NOTIFICATION SERVICE CERTIFICATE ISSUE WITH JDK 7


写点文字,画点画儿,「瞬息之间」一切都变了。觉得不错,可长按或扫描二维码关注。

浅谈移动应用的跨平台开发工具(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 开源项目和一个 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

入职安居客三年从工程师到Team Leader,见证了Android团队一路走来的发展历程。因此有心将这些记录下来与大家分享,也算是对自己三年来一部分工作的总结。希望对大家有所帮助,更希望能得到大家宝贵的建议。

三网合并

三年前入职时安居客在业务上刚完成了三网合并(新房、二手房、好租和商业地产多个平台多个网站合成现在的anjuke.com,这在公司的历史上称之为三网合并),因此app端也将原先的新房、二手房、好租和商业地产多个app合并成为了现在的安居客app。所谓的合并也差不多就是将多个项目的代码拷贝到了一起组成了新的Anjuke Project。下面这张图能更加直观的呈现当时的状况。
clipboard.png

这一时期代码结构混乱、层次不清,各业务技术方案不统一,冗余代码充斥项目的各个角落;甚至连基本的包结构也是胡乱不堪,项目架构更是无从谈起。大家只不过是不停地往上堆砌代码添加新功能罢了。于是我进入公司的第一件事就是向Leader申请梳理了整个项目的结构。

而后随着项目的迭代,我们不断引入了Retrofit、UniversalImageLoader、OKHttp、ButterKnife等一系列成熟的开源库,同时我们也开发了自己的UI组件库UIComponent、基础工具库CommonUtils、基于第三方地图封装的MapSDK、即时聊天模块ChatLibrary等等。这之后安居客项目架构大致演变成了由基础组件层、业务组件层和业务层组成的三层架构。如下图:
clipboard.png

其中业务层是一种非标准的MVC架构,Activity和Fragment承担了View和Controller的职责:
clipboard.png

前面这种分层的架构本身是没太大问题的,即使到了现在我们的业务项目也已然是基于这种分层的架构来构建的,只不过在不断的迭代中我们做了些许调整(分层架构后面在介绍组件化和模块化的时候会详细介绍)。但是随着业务的不断迭代,我们慢慢发现业务层这种非标准的MVC架构带来了种种影响团队开发效率的问题:

  • Activity和Fragment越来越多的同时承担了Controller和View的职责,导致他们变得及其臃肿且难以维护;

  • 由于Controller和View的揉合,导致单元测试起来很困难;

  • 回调嵌套太多,面对负责业务时的代码逻辑不清晰,难以理解且不利于后期维护;

  • 各层次模块之间职责不清晰等等

鉴于三网合并时期我还未加入安居客,所以对这一块的理解难免有偏差,如果有安居客的老同事发现文章中的描述有不对的地方还望批评指正。

由RxJava驱动的MVP架构

一种技术架构无法满足所有的业务项目,更不可能有一种架构方案能够一劳永逸。正如上一节中提到的随着业务的不断迭代,现有架构的缺陷逐渐浮出水面,项目架构必需不断升级迭代才能更好地服务于业务。

MVP的设计与实现

在研究了Google推出的基于MVP架构的demo后,我们发现MVP架构能解决现在所面临过的很多问题,于是我们学习并引入到了我们的项目中来,并针对性的做了部分调整。下图呈现的是安居客MVP方案:
clipboard.png

以前面提到的三层架构的方案来看是这样的:
clipboard.png

基于此架构我在GitHub上开源了一个项目 MinimalistWeather,有兴趣的小伙伴可以去clone下来看看,如果觉得对你有帮助就给个star吧。 :)

  • View Layer: 只负责UI的绘制呈现,包含Fragment和一些自定义的UI组件,View层需要实现ViewInterface接口。Activity在项目中不再负责View的职责,仅仅是一个全局的控制者,负责创建View和Presenter的实例;

  • Model Layer: 负责检索、存储、操作数据,包括来自网络、数据库、磁盘文件和SharedPreferences的数据;

  • Presenter Layer: 作为View Layer和Module Layer的之间的纽带,它从model层中获取数据,然后调用View的接口去控制View;

  • Contact: 我们参照Google的demo加入契约类Contact来统一管理View和Presenter的接口,使得某一功能模块的接口能更加直观的呈现出来,这样做是有利于后期维护的。

另外这套MVP架构还为我们带来了一个额外的好处: 我们有了足够明确的开发规范和标准。细致到了每一个类应该放到哪个包下,哪个类具体应该负责什么职责等等。这对于我们的Code Review、接手他人的功能模块等都提供了极大的便利。前面提到的 MinimalistWeather就是为了定规范定标准而开发的。

这一时期我们还在项目中引入了RxJava,很好的解决了前面提到的嵌套回调的问题,同时能够帮助我们简化复杂业务场景下的代码逻辑(当然RxJava的好处远远不止这么一点,对RxJava不了解的同学可以去翻翻我之前 一系列关于RxJava的文章)。我们也将网络库升级到了Retrofit2+OKHttp3,它们和RxJava之间能更好的配合。

MVP带来的新问题及解决方案

是不是升级到了MVP架构就高枕无忧了呢?很明显不是这样!MVP架构也会带来以下新的问题:

  • 由于大量的业务逻辑处理转移到了Presenter层,在一些复杂的业务场景中Presenter同样会变得臃肿难懂。细心的同学可能注意到了前面的架构图中的Model层有个Data Repository模块,Data Repository在这里有两个作用:一是可以将原本由Presenter处理的部分逻辑转移到这里来处理,包括数据的校验、部分单纯只与数据相关的逻辑等等,向Presenter屏蔽数据处理细节,比如作为Presenter就不必关心Model层传递过来的数据到底是来至网络还是来至数据库还是来至本地文件等等;二是我们引入了RxJava,但是只有网络层中的Retrofit能返回Observable对象,其他模块都是返回的还是一些非Observable的Java对象,为了能在整个Presenter层中都体验RxJava带来的美妙之处,因此可以通过Data Repository做一层转换;

  • 现在的MVP架构中最重的部分就是Model Layer了,这一点从前面的架构图中就能体现。因此这就要求我们在Model层的设计过程中职责划分要足够清晰,分包更明确,耦合度更低。至于分包大家可以参考 MinimalistWeather的方案:db包为数据库模块、http包为网络模块、preference包是对SharedPreferences的一些封装、repository包就是前面提到的Data Repository模块;

  • 同时还有一点需要注意,很多人在使用RxJava的过程中往往忘记了对生命周期的管理,这很容易造成内存泄露。 MinimalistWeather中采用了CompositeSubscription来管理,你也可以使用RxLifecycle这类开源库来管理生命周期。

组件化与模块化

去年下半年我们Android团队内部成立了技术小组,基础组件的开发是技术小组很重要的一部分工作,所以组件化是我们正在做的事;模块化更多的是现有的方案受到来自业务上的挑战以及受到了Oasis Feng在MDCC上的分享和整个大环境的启发,现在正处于设计规划和demo开发的阶段。

组件化

组件化不是个新概念,通俗的讲组件化就是基于可重用的目的,将一个大的软件系统拆分成一个个独立组件。

组件化的带来的好处不言而喻:

  • 避免重复造轮子,节省开发维护成本;

  • 降低项目复杂性,提升开发效率;

  • 多个团队公用同一个组件,在一定层度上确保了技术方案的统一性。

现在的安居客有是三个业务团队:安居客用户app、经纪人app、集客家app。为了避免各个业务团队重复造轮子,团队中也需要有一定的技术沉淀,因此组件化是必须的。从本篇的第一节大家就能看到组件化的影子,只不过在这之前我们做的并不好。现在我们需要提供更多的、职能单一、性能更优的组件供业务团队使用。根据业务相关性,我们将这些组件分为:基础组件和业务组件。后面在介绍模块化的时候会有进一步的描述。

模块化

自从Oasis Feng在去年的MDCC2016上分享了模块化的经验后,模块化在Android社区越来越多的被提起。我们自然也不落俗的去做了一些研究和探索。安居客现在面临很多问题:例如全量编译时间太长(我这台13款的MacBook Pro打一次包得花十多分钟);例如新房、二手房、租房等等模块间耦合严重,不利于多团队并行开发测试;另外在17年初公司重新将租房app捡起推广,单独让人来开发维护一个三年前的项目并不划算,所以我们希望能直接从现在的安居客用户端中拆分出租房模块作为一个单独的app发布上线。这样看来模块化似乎是一个不错的选择。

所以我们做模块化的目的大致是这样的:

  • 业务模块间解耦

  • 单个业务模块单独编译打包,加快编译速度

  • 多团队间并行开发、测试

  • 解决好租App需要单独维护的问题,降低研发成本

15年Trinea还在安居客的时候开发了一套插件化框架,但受限于当时的团队规模并且插件化对整个项目的改造太大,因此在安居客团队中插件化并未实施下来。而模块化其实是个很好的过渡方案,将项目按照模块拆分后各业务模块间解耦的问题不存在了,后续如有必要,再进行插件化改造只不过是水到渠成的事。

来看看安居客用户app的模块化设计图:
clipboard.png

整个项目分为三层,从下往上分别是:

  • Basic Component Layer: 基础组件层,顾名思义就是一些基础组件,包含了各种开源库以及和业务无关的各种自研工具库;

  • Business Component Layer: 业务组件层,这一层的所有组件都是业务相关的,例如上图中的支付组件AnjukePay、数据模拟组件DataSimulator等等;

  • Business Module Layer: 业务module层,在Android Studio中每块业务对应一个单独的module。例如安居客用户app我们就可以拆分成新房module、二手房module、IM module等等,每个单独的Business Module都必须准遵守前面提到的MVP架构。

同时针对模块化我们也需要定义一些自己的游戏规则:

  • 对于Business Module Layer,各业务模块之间的通讯跳转采用路由框架Router来实现(可能会采用成熟的开源库,也可能会选择重复造轮子);

  • 对于Business Component Layer,单一业务组件只能对应某一项具体的业务,对于有个性化需求的对外部提供接口让调用方定制;

  • 合理控制各组件和各业务模块的拆分粒度,太小的公有模块不足以构成单独组件或者模块的,我们先放到类似于CommonBusuness的组件中,在后期不断的重构迭代中视情况进行进一步的拆分(这一点的灵感来源于 Trinea的文章);

  • 上层的公有的业务或者功能模块可以逐步下放到下层,合理把握好度就好;

  • 各Layer间严禁反向依赖,横向依赖关系由各业务Leader和技术小组商讨决定。

对于模块化项目,每个单独的business module都可以单独编译成APK。在开发阶段需要单独打包编译,项目发布的时候又需要它作为项目的一个module来整体编译打包。简单的说就是开发时是application,发布时是library。因此需要你在business module的gradle配置文件中加入如下代码:

if(isBuildModule.toBoolean()){
    apply plugin: 'com.android.application'
}else{
    apply plugin: 'com.android.library'
}

如果我们需要把租房模块打包成一个单独的租房app,像下面这样就好:
clipboard.png

我们可以把Basic Component Layer和Business Component Layer放在一起看做是Anjuke SDK,新的业务或者项目只需要依赖Anjuke SDK就好(这一点同样是受到了 Trinea文章的启发)。甚至我们可以做得更极致一些,开发一套自己的组件管理平台,业务方可以根据自己的需求选择自己需要的组件,定制业务专属的Anjuke SDK。业务端和Anjuke SDK的关系如下图所示:
clipboard.png

最后看看安居客模块化的整体设计图:
clipboard.png

模块化拆分对于安居客这种比较大型的商业项目而言,由于历史比较久远很多代码都运行五六年了;各个业务相互交叉耦合严重,所以实施起来还是有很大难度的。过程中难免会有预料不到的坑,这就需要我们对各个业务有较深的理解同时也要足够的耐心和细致。虽然辛苦,但是一旦完成模块化拆分对整个团队及公司业务上的帮助是很大的。

以上是我的简单总结以及对模块化的一些思考,不足之处还望大家批评指正。后面模块化的demo完善后我会把它放到GitHub,并再出一篇文章详细介绍模块化的设计实现细节。

参考资料:

如果你喜欢我的文章,就关注下我的知乎专栏或者在GitHub上添个star吧!

如何正确地在Android手机上安装国产软件?

$
0
0

tumblr_inline_o6xf9b3IAi1r19dv9_1280.jpg

国产软件往往会申请与之功能不符的权限,读取着用户手机信息,开机后就驻足系统,这些对于注重隐私的用户来说都是问题。如何“干净”地安装国产流氓软件呢?笔者整理了一些思路供大家探讨。

国产app的全家桶问题一直被大家诟病,一个应用启动后就会“唤醒”其“家族”内的其他应用,有时还会通过其他方式确保应用始终在后台运行;与此同时,很多应用还会申请与之功能完全不符的权限,这些严重破坏了Android系统的体验。

要获得一个纯净的体验,我们需要解决的是两种问题,首先是软件自启动、驻足后台、频繁唤醒;其次是应用对手机信息的读取。对应这两个问题,我们要寻找各种解决方案,而手机本身的环境也是我们需要考虑的因素。笔者将“环境“分为三类:已安装Xposed框架、已root、未root,三种环境下解决问题的难度也各不相同。

Xposed

Xposed简单来说是一个框架,它可以不修改APK的情况下改变系统和应用程序的行为,所有的更改都在内存中。

如果你安装了Xposed,问题就变得简单了,得益于其强大的可定制性,Xposed中有大量应用软件能够满足你的需求。绿色守护(Greenify)的Xposed模式可以阻止应用在后台运行并且禁止链式唤醒,最近新加入的“处方”模式还可以屏蔽软件广播。

Android平台的应用程序分为七个优先级:前台进程、可见进程、主要服务、次要服务、后台进程、内容供应节点、空进程。

第6级别就是一些动作标记,程序设定了遇到什么节点时将采取什么动作,比如:插入耳机线,就是一个“内容供应节点”,收音机程序遇到这个“节点”就会激活并自启,变成后台进程了。要想收音机不会因此启动,有两个办法,要么屏蔽这个节点,使你无论是否插入耳机线,都不产生这个节点的信号,要么我们冻结收音机程序,让他感受不到这个节点。一些无良的应用,动不动就自己在后台启动了,就是这个道理。绿色守护能将一个程序绿化,就是使其感受不到“指定的”“内容供应节点”,从而当发生这些“节点”时,程序不会自动启动,除非用户自己让它启动(用户的点击程序图标也是一个“内容供应节点”)。

想要更直接地阻止应用后台运行,也可以使用“阻止运行”和“黑域”。

除了绿色守护,为了保障你的隐私,你还应该安装Xprivacy,这款软件可以设置软件读取隐私信息的权限。对于某些“流氓”应用强制用户开启权限的情况,Xprivacy还可以伪造随机信息从而保障隐私。原理也是利用Xposed框架能够修改内存信息的方法修改软件获取到的信息。

565009367188452541.png  186969661959223167.png 

Root

Android平台本来有类似iOS推送服务APNS的GCM(Google Cloud Messaging),但由于众所周知的原因,这项服务在国内非常不稳定,于是各大厂商就纷纷推出自己的推送服务,而利用这些推送服务,往往一个app开启后就会其他应用都可能关联启动,尤其以百度、阿里、腾讯为甚。这也就是前面所说的“全家桶”。

如果你Root了手机,写轮眼(My Android Tools)可以说是一款神器,它能够禁用activity、service、receiver、provider,其中的Service就包括软件用来接收推送的服务,因此,写轮眼(My Android Tools)可以阻止软件唤醒。除此之外由于能够禁用activity、service,它能做的还不止这些,经过合理配置,写轮眼能够帮你屏蔽广告,优化软件。

348759852042097543.png

对于那些不常用的软件,可以使用“冻结”类应用将他们禁用。“冰箱”就是其中的代表。它的原理非常简单,adb中提供了一个adb shell pm disable命令,在root权限下可以直接用来冻结app,而冰箱基本可以理解为使用了adb shell pm disable命令。不过它提供了非常方便的launcher,让你能够快速地启动冻结app。

436404800914951612.png

保护隐私方面,对于已经Root的手机,可以使用AppOps应用控制应用权限。很多国产ROM甚至系统原生ROM都有权限控制,但首先,这些权限往往不完整;其次,AppOps能将权限设置为ignore,很多国产软件会强制用户开启部分权限,设置ignore后,应用会得到数据,但数据为空,从而绕过这些权限申请。比如如果我们直接拒绝微信使用电话权限,就无法正常进入微信,而用AppOps将权限设为忽略就可以顺利进入。

548807995870182378.png237945419321612689.png

左图为 通过系统权限管理拒绝微信读取设备ID,右图为 通过AppOps忽略微信读取设备ID请求

未Root

未Root手机其实本文的重点。笔者自从去年入手了S7 edge后就一直纠结于是否Root。Root后永久无法使用Samsung Pay,更可怕的是还会导致前置摄像头黑屏、系统卡顿发热等各种问题。可是又无法忍受各种流氓软件在手机里捣乱,因此一直在寻找方案。

对于TouchWiz系统,我们可以利用KNOX Standard SDK的机制冻结应用(包括系统应用),常见的利用这一机制的软件包括“空调狗”、“冻住”,它们都可以做到应用使用后立即冻结,从根源拒绝“关联启动”等流氓行为,只可惜这一机制只针对三星系列手机。

59004780468787091.png

隐私方面,可以通过AppOps将部分权限设置为ignore。实际上AppOps在Android 4.4.2之前是集成在系统中的,4.4.2之后,Google不知处于何种想法将其从系统中移除了,系统自身没有权限进行管理。上文中所使用的AppOps是以root为前提的。如果手机没有root,只有两种相对比较麻烦的方法,两种方法都需要电脑端操作。

一是通过电脑端adb进行设置:

adb shell appops set 包名 权限名称 ignore   

其中权限名称包括:

READ_CONTACTS 读取联系人

POST_NOTIFICATION 通知

CALL_PHONE 拨打电话

READ_SMS 读取短信

WRITE_SMS 写入短信

RECEIVE_SMS 接收短信

OP_READ_PHONE_STATE 读取设备ID(包括IMEI)

FINE_LOCATION 定位权限

COARSE_LOCATION 定位权限

VIBRATE 震动

CAMERA 摄像头

WRITE_CLIPBOARD 写入剪切板

READ_EXTERNAL_STORAGE 读取外部存储空间

WRITE_EXTERNAL_STORAGE写入外部存储空间

BOOT_COMPLETED 开机时启动

GET_ACCOUNTS 获取设备账号信息

第二种方法是使用App Ops手机端软件,在免root模式下,App Ops使用远程adb进行权限控制,而远程adb仍然需要在电脑端开启:adb tcpip 5555,将端口号5555输入App Ops即可使用。但需要注意的是,重启后需要重新开启远程adb。

然而,想要完全将流氓软件隔离开来,我们要用到类似“沙盒”的机制。好在Android自从5.0以后就加入了Android for Work功能,功能的初衷是为了让大家在工作时能够使用自己的私人设备,通过Android for Work,我们可以建立“个人”和“工作”两套档案,工作档案由企业的IT管理员管理,为了安全起见,在两套方案中的应用无法互相访问,应用数据存储的区域其实也是不一样的,Island中的/data目录、内置存储空间、通话记录、联系人、日历等数据是与原用户独立的。

利用这种思路,我们也可以将这套机制用来对付流氓应用,将应用安装到工作档案后,它就无法获取到个人档案的应用和数据。Island就是利用Android for Work将流氓应用隔离在“岛”上的应用,这款软件与绿色守护出自同一作者,通过上述方式来实现类似沙盒的效果。

除了沙盒,Island还自带了冻结应用的功能。然而,使用了Island中自带的冻结功能后再次解冻时,该应用的AppOps状态就会被恢复到初始状态(也就是允许所有权限)需要重新设置权限。如上文所说,进行AppOps设置的两种方法都需要用到电脑,因此冻结后再解冻软件需要重新设置权限的成本较高。

124869754338902445.png

接下来我们就在沙盒环境内解决唤醒和隐私的问题。

唤醒问题我们可以通过在Island中安装绿色守护来解决。尽管是在非root环境下,它依然能够强制关闭程序(原理是通过Android辅助功能模拟点击“强制停止”程序)。建议开启“嗜睡模式”,它能够部分禁止应用的后台行为,该模式利用的是Android 6.0引入的Doze Mode。在睡眠状态下,系统将停止一些软件运行,例如一些非即时通讯软件的后台就会在锁屏的状态下被Android清理掉。从而达到节省电量、延长续航时间的作用。绿色守护中的“嗜睡模式”会将进入Doze Mode的时间缩短,从而进一步节省电量。

main-qimg-74497cf9d299509541c6a2c2b15a0158.png 

而隐私问题则可以通过在沙盒中安装App Ops来控制,或者同样地,可以通过电脑端adb操作,但需要注意的是由于沙盒实际上是在Android中使用了多用户,因此需要在参数中指定用户:

adb shell pm list users
adb shell appops get com.eg.android.AlipayGphone --user XX   

adb2.png

除了上述方案,笔者最近还发现了一款名叫“容器”的应用。

27464467823348405.png

与Island的思路完全不同,该应用利用的是VirtualApp和文件夹重定向。VirtualApp会在你的App内创建一个虚拟空间,你可以在虚拟空间内任意的安装、启动和卸载APK,这一切都与外部隔离,如同一个沙盒。目前“容器”尚在开发中,但按照作者的说法,今后会加入隐私、权限的控制和应对流氓行为的功能。

总结

要想解决流氓app的唤醒和隐私读取问题,Xposed平台和Root后的手机上有各种简单的方案,包括Greenify与XPrivacy,而非root环境下则相对困难,我们可以使用Island创造Android for Work环境,我们可以把这环境看作沙盒,将应用安装在沙盒内,应用的启动问题则通过Android 6.0后引入的Doze Mode来解决。

想这么多方法来安装国产App,一方面是为了设备保持流畅的状态,不至于被各种唤醒的应用长期占据内存和耗电,另外也考虑到隐私问题。折腾安装国产应用,实际上是无奈之举,也希望国内的Android生态能够良性发展。

*本文作者:JohnChu,转载请注明来自FreeBuf(FreeBuf.COM)

Android优化

$
0
0

I. 网络相关

更多网络优化,可参考:  Android网络

  • http头信息带Cache-Control域 确定缓存过期时间 防止重复请求
  • 直接用IP直连,不用域名,策略性跟新本地IP列表。 – DNS解析过程耗时在百毫秒左右,并且还有可能存在DNS劫持。
  • 图片、JS、CSS等静态资源,采用CDN(当然如果是使用7牛之类的服务就已经给你搭建布置好了)
  • 全局图片处理采用漏斗模型全局管控,所请求的图片大小最好依照业务大小提供/最大不超过屏幕分辨率需要,如果请求原图,也不要超过 GL10.GL_MAX_TEXTURE_SIZE
  • 全局缩略图直接采用webp,在尽可能不损失图片质量的前提下,图片大小与png比缩小30% ~ 70%
  • 如果列表里的缩略图服务器处理好的小图,可以考虑直接在列表数据请求中,直接以base64在列表数据中直接带上图片(国内还比较少,海外有些这种做法,好像web端比较常见)
  • 轮询或者socket心跳采用系统 AlarmManager提供的闹钟服务来做,保证在系统休眠的时候cpu可以得到休眠,在需要唤醒时可以唤醒(持有cpu唤醒锁)
  • 可以通过将零散的网路的请求打包进行一次操作,避免过多的无线信号引起电量消耗。

1. 传输数据格式选择

  • 如果是需要全量数据的,考虑使用 Protobuffers (序列化反序列化性能高于json),并且考虑使用 nano protocol buffer
  • 如果传输回来的数据不需要全量读取,考虑使用 Flatbuffers (序列化反序列化几乎不耗时,耗时是在读取对象时(就这一部分如果需要优化,可以参看 Flatbuffer Use Optimize

2. 输入流

使用具有缓存策略的输入流

建议替换为
InputStreamBufferedInputStream
ReaderBufferedReader

II. 基础相关

1. 数据结构

如果已知大概需要多大,就直接给初始大小,减少扩容时额外开销。

  • ArrayList: 里面就一数组,内存小,有序取值快,扩容效率低
  • LinkedList: 里面就一双向链表,内存大,随机插入删除快,扩容效率高。
  • HashSet: 里面就一个 HashMap,用key对外存储,目的就是不允许重复元素。
  • ConcurrentHashMap: 线程安全,采用细分锁,锁颗粒更小,并发性能更优
  • Collections.synchronizedMap: 线程安全,采用当前对象作为锁,颗粒较大,并发性能较差。
  • SparseArraySparseBooleanArraySparseIntArray: 针对Key为 IntBoolean进行了优化,采用二分法查找,简单数组存储。相比 HashMap而言, HashMap每添加一个数据,大约会需要申请额外的32字节的数据,因此 Sparsexxx在内存方面的开销会小很多。

2. 编码习惯

  • 尽量简化,不要做不需要的操作。
  • 尽量避免分配内存(创建对象): 1) 如果一个方法返回一个 String,并且这个方法的返回值始终都是被用来 append到一个 StringBuffer上,就改为传入 StringBuffer直接 append上去,避免创建一个短生命周期的临时对象;2) 如果使用的字符串是截取自某一个字符串,就直接从那个字符串上面 substring,不要拷贝一份,因为通过 substring虽然创建了新的 String对象,但是共享了里面的 char数组中的 char对象,减少了这块对象的创建;量使用多个一维数组,其性能高于多维数组; int数组性能远大于 Integer数组性能;
  • 如果你确定不需要访问类成员,让方法 static,这样调用时可以提升15%~20%的速度,因为不需要切换对象状态。
  • 如果某个参数是常量,别忘了使用 static final,这样可以让 Class首次初始化时,不需要调用 <clinit>来创建 static方法,而是在编译时就直接将常量替换代码中使用的位置。
  • Android开发中,类内尽量避免通过 get/set访问成员变量,虽然这在语言的开发中是一个好的习惯,但是Android虚拟机中,对方法的调用开销远大于对变量的直接访问。在没有JIT的情况下,直接的变量访问比调用方法快3倍,在JIT下,直接的变量访问更是比调用方法快7倍!
  • 当内部类需要访问外部类的私有 方法/变量时,考虑将这些外部类的私有 方法/变量改用包可见的方式。首先在编写代码的时候,通过内部类访问外部类的私有 方法/变量是合法的,但是在编译的时候为了满足这个会将需要被内部类访问的私有 方法/变量封装一层包可见的方法,实现让内部类访问这些私有的 方法/变量,根据前面我们有提到说方法的调用开销大于变量的调用,因此这样使得性能变差,所以我们在编码的时候可以考虑直接将需要被内部类调用的外部类私有 方法/变量,改为包可见。
  • 尽量少使用 float。在很多现代设备中, double的性能与 float的性能几乎没有差别,但是从大小上面 doublefloat的两倍的大小。
  • 尽量考虑使用整型而非浮点数,在较好的Android设备中,浮点数比整型慢一倍。
  • 尽量不要使用除法操作,有很多处理器有乘法器,但是没有除法器,也就是说在这些设备中需要将除法分解为其他的计算方式速度会比较慢。
  • 尽量使用系统sdk中提供的方法,而非自己去实现。如 String.indexOf()相关的API,Dalvik将会替换为内部方法; System.arraycopy()方法在Nexus One手机上,会比我们上层写的类似方法的执行速度快9倍。
  • 谨慎编写native,性能不一定更好,Native并不是用于使得性能更好,而是用于有些已经存在的库是使用native语言实现的,我们需要引入Android,这时才使用。1) 需要多出开销在维持Java-native的通信;2) 在native中创建的资源由于在native heap上面,因此需要主动的释放;3) 需要对不同的处理器架构进行支持,存在明显的兼容性问题需要解决。
  • 在没有JIT的设备中,面向接口编程的模式(如 Map map),相比直接访问对象类(如 HashMap map),会慢6%,但是在存在JIT的设备中,两者的速度差不多。但是内存占用方面面向接口变成会消耗更多内存,因此如果你的面向接口编程不是十分的必要的情况下可以考虑不用。
  • 在没有JIT的设备中,访问本地化变量相对与成员变量会快20%,但是在存在JIT的设备中,两者速度差不多。
遍历优化

尽量使用 Iterable而不是通过长度判断来进行遍历。

// 这种性能是最差的,JIT也无法对其优化。
public void zero() {
    int sum = 0;
    for (int i = 0; i < mArray.length; ++i) {
        sum += mArray[i].mSplat;
    }
}
// 相对zero()来说,这种写法会更快些,在存在JIT的情况下速度几乎和two()速度一样快。
public void one() {
    int sum = 0;
    // 1) 通过本地化变量,减少查询,在不存在JIT的手机下,优化较明显。
    Foo[] localArray = mArray;
    // 2) 获取队列长度,减少每次遍历访问变量的长度,有效优化。
    int len = localArray.length;
    for (int i = 0; i < len; ++i) {
        sum += localArray[i].mSplat;
    }
}
// 在无JIT的设备中,是最快的遍历方式,在存在JIT的设备中,与one()差不多快。
public void two() {
    int sum = 0;
    for (Foo a : mArray) {
        sum += a.mSplat;
    }
}

III. 数据库相关

建多索引的原则: 哪个字段可以最快的 减少查询结果,就把该字段放在最前面

无法使用索引的情况

  • 操作符 BETWEENLIKEOR
  • 表达式
  • CASE WHEN

不推荐

  • 不要设计出索引是其他索引的前缀(没有意义)
  • 更新时拒绝直接全量更新,要更新哪列就put哪列的数据
  • 如果最频繁的是更新与插入,别建很多索引 (原本表就很小就也没必要建)
  • 拒绝用大字符串创建索引
  • 避免建太多索引,查询时可能就不会选择最好的来执行

推荐

  • 多使用整型索引,效率远高于字符串索引
  • 搜索时使用SQL参数( "?", parameter)代替字符串拼接(底层有特殊优化与缓存)
  • 查询需要多少就limit多少(如判断是否含有啥,就limit 1就行了嘛)
  • 如果出现很宽的列(如blob类型),考虑放在单独表中(在查询或者更新其他列数据时防止不必要的大数据i/o影响性能)

IV. JNI抉择

Android JVM相关知识,可参看:  ART、Dalvik

Android JNI、NDK相关知识,可参看:  NDK

JNI不一定显得更快,有些会更慢。

特点: 不用在虚拟机的框子下写代码

  • 可以调用更底层的高性能的代码库 – Good
  • 如果是Dalvik,将省去了由JIT编译期转为本地代码的这个步骤。 – Good
  • Java调用JNI的耗时较Java调用Java肯定更慢,虽然随着JDK版本的升级,差距已经越来越小(JDK1.6版本是5倍Java调用Java方法的耗时) – Bad
  • 内存不在Java Heap,没有OOM风险,有效减少gc。 – Good

一些重要的参数之类,也可以考虑放在Native层,保证安全性。参考:  Android应用程序通用自动脱壳方法研究

V. 多进程抉择

360 17个进程:  360手机卫士 Android开发 InfoQ视频 总结 ,但是考虑到多进程的消耗,我们更需要关注多个组件复用同一进程。
在没有做任何操作的空进程而言,其大约需要额外暂用1.4MB的内存。

  • 充分独立,解耦部分
  • 大内存(如临时展示大量图片的Activity)、无法解决的crash、内存泄漏等问题,考虑通过独立进程解决
  • 独立于UI进程,需要在后台长期存活的服务(参看 Android中线程、进程与组件的关系)
  • 非己方第三方库(无法保证稳定、性能等问题,并且独立组件),可考虑独立进程

最后,多进程存在的两个问题: 1. 由于进程间通讯或者首次调起进程的消耗等,带来的cpu、i/o等的资源竞争。2. 也许对于部分同事来说,会还有可读性问题吧,毕竟多了层IPC绕了点。

VI. UI层面

相关深入优化,可参看 Android绘制布局相关

对于卡顿相关排查推荐参看:  Android性能优化案例研究(上)Android性能优化案例研究(下)

  • 减少不必要的不透明背景相互覆盖,减少重绘,因为GPU不得不一遍又一遍的画这些图层
  • 保证UI线程一次完整的绘制(measure、layout、draw)不超过16ms(60Hz),否则就会出现掉帧,卡顿的现象
  • 在UI线程中频繁的调度中,尽量少的对象创建,减少gc等。
  • 分步加载(减少任务颗粒)、预加载、异步加载(区别出耗时任务,采用异步加载)

VII. 库推荐

可以参考Falcon Pro作者的推荐:  Falcon Pro 3如何完成独立开发演讲分析

1. 响应式编程

RxJava (响应式编程,代码更加简洁,异步处理更快快捷、异常处理更加彻底、数据管道理念)

相关了解可以参看:  RxJava

2. 图片加载:

3. 网络底层库:

Okhttp: 默认gzip、缓存、安全等

4. 网络基层:

Retrofit: 非常好用的REST Client,结合RxJava简单API实现、类型安全,简单快捷

5. 数据库层:

Realm: 效率极高(Falcon Pro 3的作者Joaquim用了该库以后,所有数据库操作都放到了UI线程)(基于TightDB,底层C++闭源,Java层开源,简单使用,性能远高于SQLite等)

6. Crash上报:

Fabric: 全面的信息(新版本还支持JNI Crash获取和上报)、稳定的数据、及时的通知、强大的反混淆(其实在混淆后有上传mapping)

7. 内存泄漏自动化检测

LeakCanary: 自动化泄漏检测与分析 ( 可以看看这个 LeakCanary使用总结Leakcanary Square的一款Android/Java内存泄漏检测工具)

8. 其他

VIII. 内存

根据设备可用内存的不同,每个设备给应用限定的Heap大小是有限的,当达到对应限定值还申请空间时,就会收到 OutOfMemoryError的异常。

1. 内存管理

Android根据不同的进程优先级,对不同进程进行回收来满足内存的供求,可以参照这篇文章:  Android中线程、进程与组件的关系
在后台进程的LRU队列中,除了LRU为主要的规则以外,系统也会根据杀死一个后台进程所获得的内存是否更多作为一定的参考依据,因此后台进程为了保活,尽量少的内存,尽可能的释放内存也是十分必要的。

  • 尽可能的缩短 Service的存活周期(可以考虑直接使用执行完任务直接关闭自己的 IntentService),也就是说在Service没有任何任务的时候,尽可能的将其关闭,以减少系统资源的浪费。
  • 可以通过系统服务 ActivityManager中的 getMemoryClass()获知当前设备允许每个应用大概可以有多少兆的内存使用(如果在 AndroidManifest设置了 largeHeap=true,使用 getLargeMemoryClass()获知),并且让应用中的内存始终低于这个值,避免OOM。
  • 相对于静态常量而言,通常 Enum枚举需要大于两倍的内存空间来存储相同的数据。
  • Java中的每个 class(或者匿名类)大约占用500字节。
  • 每个对象实例大约开销12~16字节的内存。

onTrimMemory()回调处理

监听 onTrimMemory()的回调,根据不同的内存等级,做相应的释放以此让系统资源更好的利用,以及自己的进程可以更好的保活。

当应用还在前台
  • TRIM_MEMORY_RUNNING_MODERATE: 当前应用还在运行不会被杀,但是设备可运行的内存较低,系统正在从后台进程的LRU列表中杀死进程其他进程。
  • TRIM_MEMORY_RUNNING_LOW: 当前应用还在运行不会被杀,但是设备可运行内存很低了,会直接影响当前应用的性能,当前应用也需要考虑释放一些无用资源。
  • TRIM_MEMORY_RUNNING_CRITICAL: 当前应用还在运行中,但是系统已经杀死了后台进程LRU队列中绝大多数的进程了,当前应用需要考虑释放所有不重要的资源,否则很可能系统就会开始清理服务进程,可见进程等。也就说,如果内存依然不足以支撑,当前应用的服务也很有可能会被清理掉。
TRIM_MEMORY_UI_HIDDEN

当回调回来的时候,说明应用的UI对用户不可见的,此时释放UI使用的一些资源。这个不同于 onStop()onStop()的回调,有可能仅仅是当前应用中进入了另外一个 Activity

当应用处于后台
  • TRIM_MEMORY_BACKGROUND: 系统已经处于低可用内存的情况,并且当前进程处于后台进程LRU队列队头附近,因此还是比较安全的,但是系统可能已经开始从LRU队列中清理进程了,此时当前应用需要释放部分资源,以保证尽量的保活。
  • TRIM_MEMORY_MODERATE: 系统处于低可用内存的情况,并且当前进程处于后台进程LRU队列中间的位置,如果内存进一步紧缺,当前进程就有可能被清理掉,需要进一步释放资源。
  • TRIM_MEMORY_COMPLETE: 系统处于低可用内存的情况,并且当前进程处于后天进程LRU队列队首的位置,如果内存进一步紧缺,下一个清理的就是当前进程,需要释放尽可能的资源来保活当前进程。在API14之前, onLowMemory()就相当于这个级别的回调。

2. 避免内存泄漏相关

  • 无法解决的泄漏(如系统底层引起的)移至独立进程(如2.x机器存在webview的内存泄漏)
  • 大图片资源/全屏图片资源,要不放在 assets下,要不放在 nodpi下,要不都带,否则缩放会带来额外耗时与内存问题
  • 4.x在 AndroidManifest中配置 largeHeap=true,一般dvm heep最大值可增大50%以上。但是没有特殊明确的需要,尽可能的避免这样设置,因为这样一来很可能隐藏了消耗了完全没有必要的内存的问题。
  • Activity#onDestory以后,遍历所有View,干掉所有View可能的引用(通常泄漏一个Activity,连带泄漏其上的View,然后就泄漏了大于全屏图片的内存)。
  • 万金油: 静态化内部类,使用 WeakReference引用外部类,防止内部类长期存在,泄漏了外部类的问题。

3. 图片

Android 2.3.x或更低版本的设备,是将所有的Bitmap对象存储在native heap,因此我们很难通过工具去检测其内存大小,在Android 3.0或更高版本的设备,已经调整为存储到了每个应用自身的Dalvik heap中了。

  • 全局统一 BitmapFactory#decode出口,捕获此处decode oom,控制长宽(小于屏幕分辨率大小 )
  • 如果采用RGB_8888 oom了,尝试RGB_565(相比内存小一半以上(w h2(bytes)))
  • 如果还考虑2.x机器的话,设置 BitmapFactory#optionsInNativeAlloc参数为true,此时decode的内存不会上报到dvm中,便不会oom。
  • 建议采用 lingochamp/QiniuImageLoader的方式,所有图片的操作都放到云端处理,本地默认使用Webp,并且获取的每个位置的图片,尽量通过精确的大小按需获取,避免内存没必要的消耗。

IX. 线程

X. 编译与发布

  • 考虑采用DexGuard,或ProGuard结合相关资源混淆来提高安全与包大小,参考:  DexGuard、Proguard、Multi-dex
  • 结合Gradle、Gitlab-CI 与Slack(Incoming WebHooks),快速实现,打相关git上打相关Tag,自动编相关包通知Slack。
  • 结合Gitlab-CI与Slack(Incoming WebHooks),快速实现,所有的push,Slack快速获知。
  • 结合Gradle中Android提供的 productFlavors参数,定义不同的variations,快速批量打渠道包
  • 迭代过程中,包定期做多纬度扫描,如包大小、字节码大小变化、红线扫描、资源变化扫描、相同测试用例耗电量内存等等,更多的可以参考  360手机卫士 Android开发 InfoQ视频 总结
  • 迭代过程中,对关键 Activity以及 Application对打开的耗时进行统计,观察其变化,避免因为迭代导致某些页面非预期的打开变慢。

XI. 工具

  • TraceView可以有效的更重一段时间内哪个方法最耗时,但是需要注意的是目前TraceView在录制过中,会关闭JIT,因此也许有些JIT的优化在TraceView过程被忽略了。
  • Systrace可以有效的分析掉帧的原因。
  • HierarchyViewer可以有效的分析View层级以及布局每个节点 measurelayoutdraw的耗时。

XII. 其他

  • final能用就用(高效: 编译器在调用 final方法时,会转入内嵌机制)
  • 懒预加载,如简单的 ListViewRecyclerView等滑动列表控件,停留在当前页面的时候,可以考虑直接预加载下个页面所需图片
  • 智能预加载,通过权重等方式结合业务层面,分析出哪些更有可能被用户浏览使用,然后再在某个可能的时刻进行预加载。如,进入朋友圈之前通过用户行为,智能预加载部分原图。
  • 做好有损体验的准备,在一些无法避免的问题面前做好有损体验(如,非UI进程crash,可以自己解决就不要让用户感知,或者UI进程crash了,做好场景恢复)
  • 做好各项有效监控:crash(注意还有JNI的)、anr(定期扫描文件)、掉帧(绘制监控、activity生命周期监控等)、异常状态监控(本地Log根据需要不同级别打Log并选择性上报监控)等
  • 文件存储推荐放在 /sdcard/Android/data/[package name]/里(在应用卸载时,会随即删除)( Context#getExternalFilesDir()),而非 /sdcard/根目录建文件夹(节操问题)
  • 通过gradle的 shrinkResourcesminifyEnabled参数可以简单快速的在编包的时候自动删除无用资源
  • 由于resources.arsc在api8以后,aapt中默认采用UTF-8编码,导致资源中大都是中文的resources.arsc相比采用UTF-16编码更大,此时,可以考虑aapt中指定使用UTF-16
  • 谷歌建议,大于10M的大型应用考虑安装到SD卡上:  App Install Location
  • 当然运维也是一方面:  Optimize Your App
  • 在已知并且不需要栈数据的情况下,就没有必要需要使用异常,或创建 Throwable生成栈快照是一项耗时的工作。
  • 需要十分明确发布环境以及测试环境,明确仅仅为了方便测试的代码以及工具在发布环境不会被带上。

相关文章


收紧 Android 控制权,Google 或强制 OEM 预装 20 款应用

$
0
0

Google apps

尽管 Android 已经占据全世界 85% 的智能手机,但几乎每家厂商都在试图在 Android 之上再造一个自己的生态系统,从 HTC Sense、三星 TouchWiz 到国内的 MIUI、Flyme,从系统定制到 App 预装,去 Google 化几乎成为各家厂商秘而不宣的必经之路。

不过,Google 似乎企图在以另一种途径收紧对 Android 的控制权, DailyTech援引 The Information 的一份绝密文件称,Google 可能将强制要求 OEM 合作伙伴在 Android 手机中预装更多 GMS(Google 移动服务)。

在这项协议下,三星、LG、HTC、华为等开放手机联盟的成员将必须在它们新机上安装多达 20 款 Google 应用,包括 Google Play、Google Chrome、Google Maps、Google Drive、YouTube、Gmail 等 Google 服务。而在 2011 年,这一数字仅为 9 个。这些应用可以统一放在首屏的 Google 文件夹中,但是无法被删除。

与此同时,Google Search 还需要在首屏上变得更易访问,支持以“OK Google”唤醒设备激活搜索,另外,设备还不允许集成 Bing、Yahoo 等 Google 之外的搜索服务。

在加大 Android 的控制权、减少系统分裂这件老生常谈的事情上,Google 此前的做法是推动厂商尽快升级到新系统:早在 2011 年,Google 曾与主流厂商达成升级联盟,要求厂商在 18 个月内升级到新系统,但是,这个计划毫无悬念地破产了。后来,Google 采用新的策略,淡化系统升级,把重点转换为服务升级,具体来说,就是把 Google 应用逐渐独立出来,不依赖系统本身,同时,把 Google Play Services 的功能大大扩展了,使其包含了大量系统级的 API。

由于 Google Play Service 是完全由 Google 控制,并且是自动升级的,Google 可以让尽量多的用户第一时间享受到 Google 服务,无需等待系统升级。同时,这也成为 Google 控制 Android 生态圈的一种手段。当大量的系统级 API 转移到 Play Services,厂商和开发者对于 Google 的依赖就更深了。

在今年的 Google I/O 大会上,Android 部门的主管 Sundar Pichai 也强调了 Google Play Service 的重要性,并且说,它会每六周更新一次。因此,当 Android L 正式发布后,即使用户无法及时获得系统更新,也能够享受到许多服务上的更新。

倘若泄漏的这份文件为真,或许意味着我们将在 OEM 合作伙伴的手机上看到更少的定制功能。可无法避免的是,随着 Android L 的发布,系统版本的分裂问题,几乎每年都会重演一次。

 

题图来自: motorola

新媒介的拥趸,相信“ 媒介即讯息 ”,关注媒介技术的力量。

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



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


10个 iOS 用户暂可以嘲笑 Android 的特点

$
0
0

Android 与 iOS 设备之间的争斗从未停止,毕竟一切高科技产品的理念和实际表现方式都不相同。就拿 Android 来说,很多功能令用户并
不太开心,甚至是令人愤怒,下面让我们来简单的盘点 10 个 iOS 比 Android 优秀的特征。当然,这并不意味
着 Android 比 iOS 差,因为每天让库克最为头痛的事情,就是每天都会有用户转投 Android,反之亦然,因为我们还会盘
点 Android 比 iOS 更好的 10 个特征。

1、设计不一致


我们面对现实,即使谷歌公布了 Material Design(材料设计)作为设计语言,并希望开发者能够遵循,但是目前真正采
用 Material Design 风格界面的应用程序非常少,而大多数仍然使用老旧的 Holo Design 设计语言。不清楚是否是因
为  Material Design 不佳还是开发者认为没必要,Google Play 上还是有很多不同风格应用程序,而且看起来统一设计的道路还
非常长。就设计语言凝聚力和统一性而言,苹果的应用程序做得好很多,大多数应用程序都已经专门针对新的系统风格调整用户界面设计。

2、本身就不像精简的 iOS

Android 操
作系统远不及 iOS 直观,随便一台 Android 设备总能找到不同的选项或功能,而苹果的 iOS  系统上做每一件事情都使用了同样的方式。当
然,原生的 Android 操作系统可能最为直观,但设备制造商就是不喜欢原生 Android,几乎每一个品牌的智能手机都有自家定制的用户界面。这
意味着,一个用户如果要换不同品牌的手机,必须要通过一定的学习才能适应,这个学习过程有可能是轻量级,也可能难以使用。更重要的是,同一品牌的智能手
机,每一款的界面还不一样,这一点与 iOS 用户更换新  iPhone 直接上手相比体验大为不同。

3、系统更新升级


一点也是事实。每当 Android 的新版本出来时,大多数 Android 用户必须等待很长一段时间,才能够获得来自官方的正式版系统升级,而这已
经是幸运儿了,如不幸可能永久等待也无任何升级希望。反观 iOS 设备,在规定新系统版本发布之日,总是会有大量用户直接升级,随后新系统的人数占据绝
大多数。当然,如果用户手持 Nexus 设备的话,将最快获得升级,不过 Nexus 在全球 Android 手机之中市场份额相当之低。

4、内置大量臃肿的应用程序


置应用程序的数量和臃肿程度,完全取决于用户购买的是哪一款 Android 智能手机。品牌制造商和运营商,两者都非常喜欢在智能手机里预装各种应用程
序,而且大部分没办法直接禁用或者完全手动删除,不过其中也有一些设备不会太过于臃肿。反观 iOS 设备,虽然也有不少预装应用程序,一些对个人可能没
有多大用处,比如 Apple Watch,但所预装的应用程序并没有让用户感受到十分臃肿。总之,虽然 iOS 系统正在变大,但不可否认很
多 Android 设备的体验的确毁在大量预装应用上。

5、控制中心更直观易用


一点不同的用户感受不同,反应有好有坏。iOS 设备支持直接从任何界面向上滑动访问控制中心,并提供一些重要的开关,包括音乐控制和音量调节。
Android 是最早提供快捷设置和通知栏智能手机,但是一些用户认为其排列混乱不够直观,比如 Android Lollipop 系统里,用户必须
滑动那个两次顶部或者使用双指手势,才能找到需要的快捷开关,而 iOS 只要简单的从底部滑动,显得更简单也更人性化,只是缺陷在于快捷开关无法自定
义。

6、内置的相机应用功能有限

就默认相机功能而言,iOS 相比 Android 看起来更直观功能也更多,比如手动控制曝光,自动曝光/自动对焦等,很多功能在 Android 设备上的默认相机里缺失。不过,新版 Android 以及大多数设备制造商提供的相机应用,可以作为弥补。

7、无 iCloud 备份功能

iOS 自
带的强大的数据备份功能莫过于 iCloud,而且直观性和易用性良好,用户只要打开开关,选择需要备份的应用和数据即可,在 Wi-Fi 环境下还能自
动备份,随时还原。Android 上也有相类似的解决方案,只是大多数难以完整的备份,真要完美则需要 Root 获取权限,然后再通过第三
方 Recovery 备份和恢复,比如 Nandroid 和钛备份的方案。

8、应用程序更新


一点也是事实,Google Play 现在的确比苹果 App Store 拥有更多的游戏和应用,但后者总是更讨开发者欢迎,尤其当涉及到发布新款或
新版应用程序,iOS 总是开发者优先首选,Android 才紧随其后,很多用户对开发者或开发商的“应用无更新”不满也源于此,不过苹果的确为开发者
带来了更多的收入。

9、缺乏连续互通功能


果家里有苹果家族全套产品,比如 Mac、iPad 和 iPhone,只要移动设备升级到了新版 iOS 8,很多工作和生活上的任务处理将变得更方便
一些,比如 Mac 未完成的工作可以在 iPad 上继续,iPhone 来电时 Mac 可以代替接电话,就算没有 Wi-Fi 也能共享热点等等,
很多功能在苹果设备之间都能实现无缝衔接。而这一点在 Android 上还无法真正实现,谷歌也正在完善 Chrome OS,让其代替接受通知,甚至
就直接运行 Android 应用。

10、苹果的健康应用目前比 Google Fit 完善

提到苹果的健康应用就难免不让人想起 Google Fit。相比苹果而言,谷歌的方案目前在功能上还是令很多很多用户失望,尤其是生态系统不够完善。苹果
的健康功能更为丰富一些,同时还拥有很多配套的第三方应用程序和配件产品,得益于完善的 HealthKit 和 ResearchKit,iOS 设备
就像是一个强大的医学平台。

Android 和 Mac 连爆毁灭级安全漏洞,数字居民何以安身立命?

$
0
0

glebstock140501410

数字安全事件我们常有耳闻,比如  12306 账户信息泄露和携程用户信用卡泄露,它们都属于网络安全范畴。而最近两件重大安全事件则实属罕见,一个是系统级别的漏洞,一个是固件层面的隐形蠕虫。

几天前,网络安全机构 Zimperium 在 Android 系统上发现了一个堪称 Android 有史以来最严重漏洞——Stagefright。

这个漏洞波及了 9.5 亿左右的 Android 手机,从多年前的 Android 2.2 到最近的 Android 5.1 之间的各个系统版本都有可能中招。它的厉害之处在于,黑客只需要知道你的手机号码,发一条彩信就可以入侵用户的手机,在远端执行代码读取、控制你手机中的内容,甚至不需要你去打开这条彩信。

stagefright

无独有偶,近日不少媒体接连报道了一个在 Mac 固件层面上的隐形蠕虫—— Thunderstrike 2。与计算机病毒不同的是,计算机蠕虫不需要附在别的程序内。计算机蠕虫未必会直接破坏被感染的系统,却几乎都对网络有害。

这个蠕虫其实是 Thunderstrike 的升级版。今年年初德国汉堡举行的 CCC 大会(Chaos Communication Congress )上,安全专家 Trammell Hudson 展示了一种针对 Mac 固件层面的恶意软件——Thunderstrike(雷击)。它实际上是利用了一个在 Thunderbolt Option ROM 的漏洞,这个漏洞在 2012 年首次被发现但至今仍未修补。

Thunderstrike 主要通过 Mac 上的 Thunderbolt 接口传播恶意软件。恶意软件通过 Thunderbolt 接口进入到 Mac 中,然后自动在固件层面安装,获得系统控制权限,接着肆无忌惮地偷取用户一切信息。

这种 Thunderbolt 恶意软件厉害的地方在于,它独立于操作系统和硬盘驱动,难以被系统侦测到,即使格式化硬盘和重装操作系统也没有办法彻底根除。

近日,这位 Trammell Hudson 联手另一位安全专家 Xeno Kovah 研究出了 Thunderstrike 的升级版——Thunderstrike 2。前面说到,Thunderstrike 依靠 Thunderbolt 接口的物理接触,而 Thunderstrike 2 则可以通过恶意网站或者电子邮件来远程传播。

对于这两个重大安全漏洞,Google 和苹果的反应还算迅速。Google 今日 宣布,除了常规的平台升级之外,从本周开始,所有 Nexus 设备每个月都将收到 OTA 升级推送。首个安全更新在 8 月 5 日放出。

据《卫报》 报道,苹果已经承诺尽快修复 Mac 这个固件层面的漏洞。与此同时,苹果将采取临时的措施来阻止漏洞被利用,其中包括废除利用这个漏洞的开发者证书,以及任何利用恶意软件进行升级的应用。

(Thunderstrike 2 攻击演示)

对于没有技术背景的大众普通消费者来说,不管是 Android 在系统层面上的漏洞,还是 Mac 在固件上的蠕虫,能做的似乎也只有保持关注,厂商或者软件开发商一发布漏洞补丁立刻更新安装。

事实上,相较于上面提到的系统安全问题和计算机固件层面的漏洞,我们平时使用电子设备上网遇到的更多是网络安全问题。

中国互联网协会近日发布的《中国网民权益保护调查报告(2015)》 显示,78% 的网民个人身份信息曾遭泄露,包括姓名、身份证号码、工作单位及家庭住址等。63% 的网民个人网上活动信息被泄露过,包括通话记录、网络浏览纪录、IP 地址及地理位置等。82% 的网民亲身感受到了个人信息泄露对日常生活造成的影响。

image

日常生活中我们知道保护手机号码和身份证号码,正确的网络安全防范措施同样应该成为每个“数字居民”的常识。

Google 近期发表了一篇名为“没有人能攻击我的大脑:专业人士与非专业人士安全行为对比”的 报告。这份报告针对 231 名安全专家和 294 名非安全专家的网络用户做了调查,询问他们在网络安全方面通常采取哪些措施。

调查结果显示,专业人士的 5 个首选安全行为是:

  1. 安装软件更新
  2. 使用独有密码
  3. 使用双重认证
  4. 使用安全性强的密码
  5. 使用密码管理器

而非安全专家的网络用户的 5 个首选安全行为则是:

  1. 使用防病毒软件
  2. 使用安全性强的密码
  3. 经常更改密码
  4. 仅访问自己了解的网站
  5. 不共享个人信息

w1408

这里有一个有趣的点,专业人士的 5 个首选安全行为里面全部没有“使用防病毒软件”,而非安全专家的的网络用户则把“使用防病毒软件”放在了第 1 位。

调查指出,非专业人士往往没有意识到及时更新软件的重要性,并且有所质疑:

我不了解更新软件是否能够确保安全。如果下载了恶意软件岂不是很糟糕?我认为软件自动更新并不安全,因为它可能会被用于更新恶意内容。

对此,专业人士解释道,防病毒软件有它自身的优势,但它会误导用户对安全性的理解,以为安装了防病毒软件就能一劳永逸。及时更新软件才是网络安全的重要保障。

在专业人士的选择中,我们还可以看到 2-4 位的都是和密码相关的安全措施。“使用独有密码”可以最大限度地防止“ 撞库攻击”所造成的密码泄露。之前 12306 用户信息泄露事件就是黑客通过“撞库攻击”所引起的。

“双重认证”相当于给你的家门上了两把锁,而且是两把完全不一样的锁。像 Gmail 的双重认证就包括第一重的密码认证,以及第二重的短信认证或者身份验证器( Google Authenticator)生成的随机数字组合认证。

Google 的调查还显示,仅有 24% 的非专业人士为部分账户使用密码管理器,而 73% 的专业人士会把全部密码都交由密码管理器管理。调研结果指出,这种差异存在的原因在于,非专业人士对密码管理器的优势缺乏了解,以及对此类程序缺乏信任。

尽管 LastPass 曾爆出安全漏洞,但专业付费的密码管理器仍是现在安全系数比较高的密码管理解决方案。密码管理器还可以生成高强度的不规律密码组合,提高密码的破解难度。

对于希望进一步了解网络安全的读者,我们鼓励阅读这份调研报告的 英文原文。报告不仅阐述了大众对于基本网络安全实践的主要误解,还提供一系列技巧帮助大家过上更安全的数字生活。

 

题图来自 123RF

报道倾向:消费电子、虚拟现实、工具类 app。工作邮箱:oudi@ifanr.com。

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



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


数据来看,Android 还是挺安全的

$
0
0

android-960x854-wallpaper-832

Google 近日公布了一份白皮书,详细记载了 Android 安全小组去年一整年的工作内容。

从白皮书中可以看出,Google 需处理的每一个安全威胁的重要程度。据记载,2014 年 Google 共标记 79 个漏洞,其中 41 个为低危漏洞。这些安全威胁根据严重等级区分,相比 2013 年缓和不少。

The Verge报道,Google 从未停止打击恶意程序(PHA),文件称 2014 年全球恶意程序安装比例下降了 50%。Google 的数据则显示,去年受 PHA 干扰的手机只有不到 1%。而从 Google Play 检测到的 PHA 比例则下降到 15%。

去年 10 月,安全的 Android 设备百分比为 99.5%,这竟是去年最低值。由此可见,Android 设备总体安全性来讲,还是非常高的。不过,这项数据没统计 root 过的手机,Android 手机 root 后等于将安全设置从系统中完全删除。国内来说,root 现象尤为严重。

3044608-inline-i-3-androidgoogle

值得注意的是,安全最低值出现在 Android 4.4 更新和 Android 5.0 推出之前。这表明 Android 系统更新后增加的磁盘加密、访客模式,并改进身份验证方法,这些都真正加强了系统的安全性。开发者们也增加了访问工具来检测和修复漏洞,这更有利于加快找到漏洞修补方案。

Google 公布的 79 个漏洞中,73 个已经转交给 Android 开源项目组,剩下 6 个有待观察。据 engadget 报道,手机制造商和运营商都会及时收到漏洞信息,以便在普遍发现前找到修复补丁。这份文件还详细描述了 Google 如何在 Android 系统强化安全措施。

不过,该文件并未提到此前发现的 WebView 漏洞,它影响到运行 Android 4.1 及更早版本的手机。今年早些时候,Google 曾表示 不再为 Android 4.4 以前版本出官方补丁。Android 4.4 仍被广泛使用,约占 41% 的份额,4.4 之前版本也占据不小份额。

Google 的安全防范已升级到 Google Play,他们希望通过整体安全网络来保卫用户,以免除网络攻击的困扰。目前,安全小组在每一百万个应用中能发现一个恶意程序。此外,Google 也在密切留意 Chrome 扩展,上周他们宣布禁用了近 200 个不良扩展,这些扩展总体影响到 1400 万用户。

3044608-inline-i-1-googleandroid

据 Google 统计,Verify Apps 服务会对 Android 设备中的应用持续监控,约 10 亿 Android 设备接受其安全保护,每天就有超过 2 亿设备进行安全扫描。但是,Google 这样做可能侵犯到用户隐私。对此,Google 补充表示,可以保证用户的照片、位置数据以及个人信息等都不会出现任何形式的窃取和泄露。

 

题图源自 dailymobile,插图源自 FastComcany

这时代,很带感。

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



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


关于移动端的钓鱼式攻击

$
0
0

phishing-1今天,在微博上看了一篇《 微信和淘宝到底是谁封谁》的文章,我觉得文章中逻辑错乱,所以,我发了一篇 关于这篇文章逻辑问题的长微博。后面,我被原博主冷嘲热讽了一番,说是什么鸡汤啊,什么我与某某之流的人在一起混淆视听啊,等等。并且也有一些网友找我讨论一下相关的钓鱼式攻击的技术问题。所以,我想写下这篇纯技术文章,因为我对那些商业利益上的东西不关心,所以,只谈技术,这样最简单。

首先说明一下, 我个人不是一个安全专家,也不是一个移动开发专家,按道理来说,这篇文章不应该我来写,但是我就试一试,请原谅我的无知,也期待抛砖引玉了,希望安全的同学斧正

关于钓鱼式攻击,其实是通过一种社会工程学的方式来愚弄用户的攻击式,攻击者通常会模仿一个用户信任的网站来偷取用户的机密信息,比如用户密码或是信用卡。一般来说,攻击者会通过邮件和实时通信工具完成,给被攻击者发送一个高仿的网站,然后让用户看不出来与正统网站的差别,然后收集用户的机密数据。

移动端钓鱼攻击点分析

因为钓鱼式攻击并不新鲜,所以我这里只讲移动方面的。

在移动端,这个事情会更容易干,因为移动端有如下特点:

  • 移动端的UI只能有一个应用占据整个屏幕,你只能看到一个应用,而且用户屏幕小,能显示的信息有限,比如浏览器里的网址是显示不全的。这会给钓鱼攻击有很多可乘之机。
  • 移动端的平台有其安全的设计。每个应用都是隔离开的,一个应用无法获取另一个应用的数据。而且应用的下载基本上来说都是来自合法的地方。比如iOS的设备通过App Store下载,每个程序都有自己的签名保证不会被篡改。而且移动端的的应用有各种权限配置,这样也能很大程度提高安全性。
  • 移动端的APP有些有些是收费的,所以自然会有盗版需求,虽然在平台上做了一些安全设计,但是并不完美。用户可以越狱,可以root。这给恶意软件有了可乘之机。

下面我们来分析下移动端的用户操作,我们重点关注用户控制权的切换过程(因为这是攻击点)

在移动设备上,基本上来说,用户的控制切换有四种:

  • 从一个APP切到另一个APP,也就是我们所谓的唤出APP。
  • 从一个APP唤出一个Web,常见为一个嵌入式的WebView或是一个浏览器
  • 从一个Web唤出一个APP,这需要浏览器支持一些非标准的HTTP协议,比如skype://之类的。
  • 从一个Web到另一个Web,这和Web上的方式差不多。

基本上来说, 黑客的攻击从来都是找这样的转换环节来做文章的,并且需要一个用户非常熟悉的场景(这样用户才会放松警惕)

通过观察移动APP的特性,我们可以知道,当用户控制切换时,有下面的这些特性:

  • 到另一个APP时,需要用户登录(如果登录的session过期了)
  • 当支付的时候,需要用户输入支付信息(信用卡信息、支持密码)

那么用户在移动APP上经常做的事是什么?

  • 社交分享:分享到微博,分享到微信等等,分享的时候,可能需要你输入用户名和口令。
  • 应用内购:一般来说APP会有两种,一种免费的,一种是收费的,大量的用户都是下载免费的,然后通过什么“开通更多关卡”、“去广告”、“买道具”之类的东西,让用户输入支付信息。Apple的支付的时候也会要用户输入Apple ID的密码。
  • 点击链接:有时候,我们收到短信,或是二维码,或是一个微信微博,会让我们去点击一个网站链接,这个网站链接要么就是打开一个网页,要么就是启动应用,要么就是跳转到应用市场去下载应用(如果你没安装)。

所以,一个好的钓鱼式攻击一定会从这些地方入手,然后高仿UI以及交互流程,这个交互流程和用户日常操作的完全一样,让用户无法察觉。任何方式的钓鱼攻击简单来说,会有两种:

  • 一种是直接攻击:你下载了一个恶意的APP,或是打开了一个恶意的冒牌APP。
  • 一种是中间人攻击:用户控制权转换时的两端都是正规应用,但是中间的过程不是正常的。

攻击方式

下面是一些常见的攻击方式:

从一个应用唤起另一个应用的方式

直接攻击

当你点击一个社交分享按钮,或是一个支付按钮的时候。就会转到一个页面,这个页面需要你输入用户机密信息(密码或是支付信息),然后再唤起真正的APP。

一个有恶意的APP可能会让你放松警惕,因为,这个你在安装这个APP的时候,你会发现这个APP根本不需要任何的权限(Android上的),甚至连网络访问的权限都不要,因为在Android下,App可以通过别的组件访问互联网,比如:恶意应用可能创建一个MediaPlayer Object,然后就可以通过这个对象访问一个URL然后把偷到的信息发送出去。

你的手机要被安装一个恶意的应用并不难,同样通过社工的方式,比如:盗版,色情,伪装成客服等等通过人性的弱点让你去一些非受信的市场上安装。iOS设备上的应用也可以不用通过App Store安装(通过itms-services协议,可以通过safari浏览器直接在IOS设备上安装应用程序)。

还有,人们都是贪小便宜的人,所以,会到某些地方买一些便宜的手机(比如淘宝),现在的高仿手机,翻新的二手手机对于一般人甚至安全专家来说完全没有识别能力。这些手机中有很大可能藏有恶意程序。你千万不要以为你格式化手机就OK了。今天(2015年4月14日)早上CCTV2台的“第一时间”就说了一个案例,你可以看看。另外,你可以看看相关的新闻。(另外,你把你的旧手机卖了也要小心,因为你的数据就在里面,旧手机已经成了一个灰色产业链)

另外,Apple的App需要有一个review过程,这个过程对大众是神秘的,但我觉得应该会包括安全方面的review。不过,这个审核过程可能也有空子可以钻。比如:在review的时候,这个应用完全正常,但在用户使用的时候,会自己从网站下载一些自己的配置文件而改变行为(更为直接的就是访问外部网页时在审核时和在用户应用时可能完全不一样,Apple应该完全没有能力审核应用要访问的外部站点)

中间人攻击

我们知道,一个APP唤起另一个APP好多都是用url-scheme的,也就是某种协议,审核这样的协议非常简单,所以如果有恶意的东西在里面基本上很容易看到。但是,如果某些APP并没有注册自己的url-scheme,或是没有被安装,反而,另一个有恶意的APP注册了这个scheme,那么,就会导致恶意的APP被唤起来了( 这就是我为什么在我的微博中说,如果用户没有安装淘宝的客户端,那么,让微信唤起淘宝的客户端时,有可能是另一个有恶意的APP。但是很多人不懂这个事。在iOS下,两个APP通讯正确的做法是“钥匙串机制”)。

当然如果有两个应用被注册了同一个scheme,那么,iOS和Android会给出一个选择,让用户来选(注:iOS的系统有可能会直接跳某个 App 上去,不同版本的跳规则不明确,可以认为是随机跳转)。于是乎,恶意的APP就要努力的让自己比正规的APP看起来更像个正规的APP就可以了。

在Android平台上,这个事可能更变态,只要恶意的应用有两个权限,一个是随手机操作系统在后台启动,一个是task list(然而这两个权限都是一般权限)。这样一来,当你进行两个APP切换时,恶意程序可以通过task list权限监控到,然后自己马上先于正规的app出现,等到收集完用户数据后简单的退出就好了。这个方式只需要你的程序能在10ms以内反应过来(最佳是5ms左右),人的肉眼根本看不出来。(在iOS设备下,除了jail break后的iPhone可以这么干,正常的都iPhone还没有找到这样的攻击方式)

在一个应用内内嵌Web的方式

这种方式更容易攻击了,现在很多很多应用都是内嵌的Web的形式,你完全不知道打开的网页的网站是什么,因为这些内嵌的WebView你连地址都看不见。而且无论是iOS或Android,其WebView都可以执行任何的Javascript代码,就算显示URL,URL也可能是被混乱过的,你也看不全,你也很容易上当。当然,那些使用带SSL证书的支持HTTPS的网站(尤其是EV证书)可以在地址栏上显示一个绿色的标记表示你访问的就是正确网址,但是并不是所有的浏览器都会这样,比如iPhone的Safari并没有这个提示,所以,你一定要用Chrome。

更狠的是就算你打开的是一个正确的URL,你依然可能被中间人攻击。尤其是这个网站使用了明文的HTTP协议,而你又喜欢蹭那些免费的WiFi,于是很容易给把服务器返回给你的网页中做修改,比如,修改网页中login表单或是支付表单提交的网站(想想天朝的网络运营商给你访问的正常的网页弹广告这事吧)

关于DNS劫持,有些人觉得这事可能遇不上,因为这是一个全网的问题,如果你有这样的想法你就错了。还是那样,你爱占便宜,蹭上那些没有密码的WiFi,你完不知道,你连上去的那个WiFi会设置什么样的DNS服务器,你输入了www.taobao.com,但你打开的网站根本就是不是淘定,而是一个钓鱼网站。你会知道你打开的是错的了么?基本不可能。所以,安全点的网站都是要用HTTPS,但是还是那句话,iPhone的Safari并不会提示你打开网站的SSL证书合不合法(事实上,在手机上的很多浏览器都不会这提示,只有Chrome会)。

关于攻击的方式我不想讲太多,还有很多高级+猥琐的方式我也不是完全知道,知道了我也不说,不然,教人犯罪了。

关于从Web端唤起APP是和,APP唤醒APP的攻击方式基本一样。我就不说了。

怎么防范钓鱼式攻击

首先,我们要知道,钓鱼式攻击是一件非常难搞的事。要搞定这个事,一般来说需要四个方面: 立法层面用户培训层面宣传层面、与 技术保全措施层面

教育方面

打击网钓的策略之一,是试着培养人们识别网钓,并教导怎样处理这些问题。只需要稍稍修改人们浏览习惯的方式,很多问题都可以避免。随着人们越来越认识到网钓者所使用的社会工程学技俩,传统的网钓欺诈技术可能在未来过时。

  • 对别人发来的链接要小心,尤其是让你输入机密信息的链接要小心检查。
  • 到正规的地方买手机,不要贪图小便宜。旧手机在卖前要“物理删除”数据。
  • 不要对手机越狱,不要root。
  • 不要从非信任的地方下载软件。
  • 要小心免费的WiFi。
  • 输入机密数据的时候一定要小心检查。
  • 多依赖一些不同的安全体系,比如:网上支付不要只依赖支付宝,尽量使用信用卡(信用卡千万不要设密码),这样就算是被钓鱼了,你还有一个银行安全的缓冲地带——可以不承认交易。
  • 现在使用手机的频率越来越高,所以,我非常建议你使用更为安全的iPhone手机,一定要打开“查找我的iPhone”功能,然后设上开机密码。iPhone手机可以做到手机丢失了别人都无法使用,包括刷机都刷不了(iOS7以上版本)
  • 对于一些关键网站,开启两步验证,这样就算你的用户名和密码被钓走了,还有一个动态手机口令做为登录的关卡。

技术方面

  • 利用SSL证书来保证从浏览器到网站的访问是现在采用比较多的方式,也是在理论上可行的方式。现代的浏览器都会在URL上放上一个锁的标志,对于EV证书,你会看到浏览器的URL是绿色的(很容易分辨)
  • 另外,像firefox浏览器有一个petname的插件,你可以为你常上的网站设置一些标签。这样,当你打开钓鱼网站的时候,你会发现这些标签没有显示出来,那就有问题了。
  • 关于SSL的CA认证机构,你需要管理好你浏览的那些根证书,有些根证书你需要删掉。
  • 增加式登录方式。这种方式被美国银行采用,就是说,你可以上传一个你自己知道的图片,而当你打开登录页面里时,输入了自己的用户名后,你会看到你设置的这个图片被显示出来。如果没有或是显示错了,表示你打开的是钓鱼网站。
  • 两步验证,通过用户自设密码+手机动态口令登录(好些网站都在使用Google Authenticator的方式,这有点像公司VPN的动态口令)。

上述都是PC Web上的防范,然而我们的手机移动端做的并不够好,移动端的安全还是要加油。

安全风控方面

什么叫安全风控,说白了就是拿钱出来赔偿给被骗的用户,大家相信我,这个事情在基本上所有的公司都会做的,也就是说,无论你怎么做安全也无法保证绝对的安全,你只能缓解或是降低用户被骗的数量或概率。所以,几乎所有的公司都会有一笔钱专门用来赔偿。

在西方国家,用户体验很好,我说一个故事,我有一个妹妹在英国,有一天她到ATM上取钱,取完钱后忘了把卡取出,结果后面的人把她的卡里的钱取走了,于是她报了警,等警察做完笔录后,她给银行的客服打了个电话说明了情况,本想冻结银行卡的,但是银行方面二话不说就赔偿了她所有的损失。为什么英国的巴克莱银行这么痛快,是因为他们有风控基金,专门用来处理这样的事的。

在中国,其实银行和一些大的公司都有这笔安全风控基金,但是,要你非常坚持不懈地申诉,他们才会赔给你,而且还不是全部。要全部的话,我估计你要做一个“刁民”,否则欺负你,没道理。

关于微信和淘宝

微信和淘宝到底是谁先屏蔽谁我并不关心,这里面的商业利益我也不关心,微信是不是支持卖东西我也不关心。我关心的是寒冬文章中所说的微信上有淘宝钓鱼的安全问题。

从技术上来说,我觉得要微信和淘宝一起干这事,单方都不行,需要两边的安全专家一起讨论(如果需要,我可以帮你们约)。我这里给一个可能很不成熟的方案,算是抛砖引玉(我不考虑你们之间的商业竞争,我只从用户的角度出发,客户第一):

我觉得,从业务上来说,淘宝可以在微信上有一个官方的商城。而淘宝的商家,需要取得微信的认证后入住,才能分享相关的商品或店家链接,对此,商家入住,我觉得可通过微信的服务账号与淘宝的商家后台集成可以做到。

然后,商家也好,买家也好,他们分享商品只能通过微信官方的商城或是商家的服务账号分享出去,而分享出去的商品信息可以是一个比较unique的形式(比如有一个不能伪造的官方认证的标签),而用户的支付可以通过内置的微信支付也可以通过内置的支付宝(通过唤起App并不是一个好的方式,还是应该你们在服务端进行相互的通信)。

然后微信和淘宝双方通过宣传手段告诉全社会,微信里的商品什么才是正规的,才不是钓鱼的,并给教育用户更为安全地使用手机。

P.S. 我虽然这么说,但从我个人来说,我非常理解微信为了让用户有很好的体验而不让微信成为一个四处都是营销商品的地方。所以,我从个人来说,希望微信不要成为一个商家的营销地。另外,我也知道阿里对移动端的看重,所以,上述的方案虽然对用户体验和安全都比较好,但是从目前商业利益的情况看来基本无法实现。不过我这里也只是抛砖引玉了。

面对安全和用户这两个事, 你们两个中国最大的互联网公司,应该带头做好榜样,你们都是不缺钱的公司,应该更多的承担起社会的责任,真正为用户做点什么,而不是整天想着流量入口,互相屏蔽,互相指责,想着自己能有多少用户,这TMD太LOW了,和你们的地位完全不符。所以,从站在用户的角度上来说,我希望微信和淘宝都能站在用户的角度上思考问题,一起合作来真正的为用户更好的服务。

(全文完)

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

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

相关文章

Viewing all 101 articles
Browse latest View live


<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>