…
…
]]>《算法图解》 (完成)
《图解HTTP》 (完成)
《设计模式之禅(第二版)》 (完成)
…
《非暴力沟通》 (完成)
《三体》全集 (完成)
《月亮与6便士》 (完成)
《万历十五年》 (完成)
《云边有个小卖部》 (完成)
《谁杀了她》 (完成)
《百年孤独》 (完成)
《黄金时代》 (完成)
《流浪地球》 (完成)
…
]]>Gradle插件将可重复使用的构建逻辑封装起来,可用于许多不同的项目和构建。 Gradle插件就相当于jar包,里面封装了一些共有的方法以及一些自定义的task
等。
可以使用任何语言开发Gradle插件,只要最终生成字节码就行。一般常用的语言是Groovy
、Java
、Kotlin
。
下面以Groovy
语言为例(IDE使用Intellij Idea
),介绍Gradle插件开发的流程:
1, 首先需要创建一个工程,然后新建一个名为my-plugin
的模块,模块类型选Gradle
,语言选择Groovy
(如果使用的是Android studio
可以新建一个Java
模块,然后将main/java/
文件夹改名为main/groovy
)
2, 修改build.gradle
,增加对gradle sdk
和groovy sdk
的依赖:1
2
3
4
5
6
7
8
9
10// 导入groovy插件,用于编译groovy等
apply plugin: 'groovy'
dependencies {
testCompile group: 'junit', name: 'junit', version: '4.12'
// 依赖Gradle sdk,插件开发中需要用到gradle的api
compile gradleApi()
// 依赖groovy sdk
compile localGroovy()
}
3, 准备工作做好后就可以开始编写插件了。在groovy
目录下新建名为MyPlugin
的类(注意:groovy
类文件的后缀是groovy
)1
2
3
4
5
6
7
8package com.pptv.plugin
class MyPlugin implements Plugin<Project> {
void apply(Project project) {
project.tasks.create("my-task")
}
}
自定义插件必须实现Plugin
接口,覆盖其apply
方法,并在apply
方法中完成该自定义插件的功能。上述代码中创建了一个名为my-task
的task
,但该task
什么也不做。
4, 插件写好后,要想在其他项目或模块中使用该插件,需要先发布插件。
1, 首先需要在插件模块的resources
目录下新建META-INF/gradle-plugins
目录,然后再在gradle-plugins
目录下新建一个properties
属性文件,文件名称可以任意,但最好与项目功能相关,因为之后会用到这个文件名。然后在该文件中输入:1
implementation-class=com.pptv.plugin.MyPlugin
其中implementation-class
属性的值就是刚才新建的MyPlugin
类的类名(带包名)。
2, 在插件模块的build.gradle
中新增uploadArchives
节点,用于配置发布插件所需的信息:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28uploadArchives {
repositories {
// 以下用于将插件发布到本地
flatDir {
name "localRepository"
dir "${getRootDir()}${File.separator}build"
}
// 以下可用于将插件发布到私有maven仓库
maven {
url "http://maven.pptv.com/repositories/"
credentials {
username "${props['MAVEN_REPO_USERNAME']}"
password "${props['MAVEN_REPO_PASSWORD']}"
}
}
// 以下也可用于将插件发布到私有maven仓库, 且可以配置的内容更多些
// apply plugin: 'maven'
// mavenDeployer {
// repository (url:"http://maven.pptv.com/repositories/"){
// authentication (userName:"${props['MAVEN_REPO_USERNAME']}", password:"${props['MAVEN_REPO_PASSWORD']}")
// pom.groupId = "${GROUP_ID}"
// pom.artifactId = project.getName()
// pom.version = "${VERSION_NAME}"
// }
// }
}
}
(完整的build.gradle
见文章最后)
其中:
flatDir
节点用于配置将插件发布到本地时,插件存放的目录(dir
) 信息;maven
节点用于配置将插件发布到远端maven仓库,需配置远端仓库的url
和账号信息(credentials
);注释掉的mavenDeployer
节点也可以用于将插件发布到maven仓库,但这个配置需要依赖maven插件(apply plugin: 'maven'
)。 其中注释掉的代码中${GROUP_ID}
和${VERSION_NAME}
的定义如下:
1 | // group id |
具体要将插件发布到本地还是远端,可以自己选择。
插件开发完成后,要在其他项目或模块中使用,需要在其他项目的根build.gradle
中加入如下配置:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22buildscript {
repositories {
// 本地库配置
flatDir {
name "localRepository"
// 本地插件存放的路径
dir "${getRootDir()}${File.separator}build"
}
// 远端库配置
maven {
url "http://maven.pptv.com/repositories/"
credentials {
username "${props['MAVEN_REPO_USERNAME']}"
password "${props['MAVEN_REPO_PASSWORD']}"
}
}
}
dependencies {
// 导入插件库, 格式为 -> group:module:version
classpath "com.pptv.plugin:my-plugin:1.0.0"
}
}
上述配置中:
flatDir
节点的配置同发布插件的配置,指明了插件在本地存放的位置信息;maven
节点的配置同发布插件的配置,指明了远端仓库的url
等信息;dependencies
节点的classpath
属性的取值格式是group:插件module名称:插件版本号version
。然后需要在使用插件的模块的build.gradle
中添加:1
apply plugin: 'my-plugin'
其中my-plugin
为插件模块resources/META-INF/gradle-plugins
下properties
文件的文件名(该名称可以和插件模块的名称不相同)。
刷新下项目,在Gradle视图窗口中就可以看到自定义插件创建的 my-task
了:
至此,一个简单的Gradle插件就开发完成了。
插件模块完整 build.gradle
]]>参考 : https://docs.gradle.org/current/userguide/userguide_single.html#custom_plugins
最近做的需求需要频繁使用3G/4G网络,可惜公司给的测试卡流量只有几百M,播几个视频流量就耗光了,测试起来非常不方便。于是就想没有什么工具或者软件可以在wifi环境下模拟3G/4G网络,在网上找半天,结果无功而返。
既然没有软件能做到,那只能从代码层面下手了。代码中判断当前手机网络类型的代码比较简单:1
2
3
4
5
6
7
8
9
10
11public static boolean isMobileNetwork(Context context) {
ConnectivityManager cm = (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
if (cm == null) {
return false;
}
NetworkInfo info = cm.getActiveNetworkInfo();
if (info == null || !info.isAvailable()) {
return false;
}
return ConnectivityManager.TYPE_MOBILE == info.getType();
}
如果能让wifi环境下也返回是3G/4G网络就可以达到目的了,但是直接修改上述代码是不行的,因为判断当前是否是3G/4G网络的地方很多,一一修改比较耗时间,关键是项目中导入的很多SDK中也有网络类型的判断,SDK中的代码我们没法修改。
既然不能直接修改网络判断的代码,那能不能通过修改ConnectivityManager
类(以下简称CM
)的getActiveNetworkInfo
方法的返回值,让wifi环境下getActiveNetworkInfo
方法也返回3G/4G网络的NetworkInfo
呢?
听起来有些似乎有点困难。让我们先看看源代码,一步一步分析。
从源码看起来CM
类使用了单例模式,其getActiveNetworkInfo
方法实现如下:1
2
3
4
5
6
7public NetworkInfo getActiveNetworkInfo() {
try {
return mService.getActiveNetworkInfo();
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
其中mService
对象是IConnectivityManager
接口(以下简称ICM
)的一个实例,这个接口是一个aidl接口,不属于Android SDK的一部分,源码在framework中。如果能自己创建一个ICM
对象替换调CM
类中原本的mService
属性,就可以实现修改getActiveNetworkInfo
方法返回值的效果了。
mService
对象ICM
对AndroidSDK不可见,我们不能直接创建一个类并实现了ICM
接口。另外这个接口声明了50个左右的方法,即使能创建实现了ICM
接口的实例,要重写这50个左右的方法也不那么容易。那有没有其他简便的方法创建ICM
实例呢?有,使用动态代理。
关于什么是动态代理,动态代理可以做什么,可以参考这篇文章,这里不再介绍(其实是技术太菜,说不清楚)。下面是使用动态代理替换CM
中原有的mService
属性的过程:
使用反射获取原有的mService
对象:
1 | // 先获取ConnectivityManager对象 |
使用动态代理创建ICM
对象,并修改 getActiveNetworkInfo
的返回值为自己准备好的networkInfo
,其他方法仍然调用mService
的相应方法。
1 | final Object mFinalService = mService; |
将原来的mService
对象替换为上一步中创建的mProxyService
对象:
1 | // 将自己创建的代理对象`mProxyService`替换调原来的`mService`对象。 |
这样就完成了修改getActiveNetworkInfo
方法的返回值的目标,同时还能控制什么情况下返回自己定义的NetworkInfo
(上述代码中只有当SettingsPreferences.getHookNetwork(context)
为true的情况下才返回自己创建的networkInfo,否则仍然返回系统原本的networkInfo)。
通过上述方式是不是能做到让代码中所有调用
getActiveNetworkInfo
方法的地方都返回自己创建的NetworkInfo
呢?
实践中发现:CM
类看起来使用了单例模式,但事实上,在Api level 19 及以上的系统使用不同的context
调用getSystemService
方法返回的是不同的CM
对象。
也就是说在代码中调用 getSystemService
方法时用的context
和上述代码中context
不一样,那么获取到的NetworkInfo
对象仍然可能是系统本身的NetworkInfo
而不是我们自己创建的NetworkInfo
。
IConnectivityManager
对象的创建过程既然直接替换mService
属性不完全可行,那能不能尝试找到mService
被创建的地方,然后替换掉mService
的创建过程,让所有给mService
赋值的地方都返回我们自己创建的ICM
对象呢?
这个过程就好比现在小区内有好几家超市都在卖矿泉水,你想让这些超市都卖你自己生产的矿泉水,现在通过动态代理的方式你可以做到让其中一家超市卖你生产的矿泉水了。接下来你想让所有超市都卖你生产的矿泉水,如果能找到这些超市在哪家供应商进的货,然后通过某种方式,把供应商的货都替换为你生产的矿泉水那就ok了。
来看看mService
是何时以及怎么被创建的。
首先,CM
是通过Context
对象的getSystemService
方法获取的。我们看看getSystemService
方法是怎么实现的(Context
的实现在ContextImpl
里面):1
2
3
4public Object getSystemService(String name) {
ServiceFetcher<?> fetcher = SYSTEM_SERVICE_FETCHERS.get(name);
return fetcher != null ? fetcher.getService(ctx) : null;
}
从上面的代码可以看出来,所有的service对象都保存在一张map(SYSTEM_SERVICE_FETCHERS
)中,该map的初始化过程如下:1
2
3
4
5
6
7
8registerService(Context.CONNECTIVITY_SERVICE, ConnectivityManager.class,
new StaticApplicationContextServiceFetcher<ConnectivityManager>() {
public ConnectivityManager createService(Context context) {
IBinder b = ServiceManager.getService(Context.CONNECTIVITY_SERVICE);
IConnectivityManager service = IConnectivityManager.Stub.asInterface(b);
return new ConnectivityManager(context, service);
}});
从上述代码看来,ICM
的创建依赖于ServiceManager.getService
方法返回的IBinder
对象,使用这个IBinder
对象再调用IConnectivityManager.Stub
类的静态方法asInterface
就可以将其转为本地接口ICM
对象(这也使用Binder
进行跨进程方法调用的基本流程):1
2
3
4
5
6
7
8
9
10public static android.net.IConnectivityManager asInterface(android.os.IBinder obj) {
if ((obj == null)) {
return null;
}
android.os.IInterface iin = obj.queryLocalInterface(DESCRIPTOR);
if (((iin != null) && (iin instanceof android.net.IConnectivityManager))) {
return ((android.net.IConnectivityManager) iin);
}
return new android.net.IConnectivityManager.Stub.Proxy(obj);
}
总结一下:
CM
的getActiveNetworkInfo
方法直接调用了其成员变量mService
的getActiveNetworkInfo
方法,而mService
是一个ICM
的实例,它是在CM
对象创建时被赋值的(构造函数中)。CM
对象的创建位于ContextImpl
类中,其创建时会首先调用ServiceManager.getService
方法获取到一个IBinder
对象,并通过IConnectivityManager.Stub
的静态方法asInterface
将这个IBinder
对象转为ICM
对象,最终以这个ICM
对象作为参数创建了CM
对象。
找到CM
以及ICM
创建的地方后,目标就很明确了:只要自己创建一个IBinder
对象替换掉ServiceManager.getService
方法返回的IBinder
对象,然后修改这个IBinder
的queryLocalInterface
方法让它始终返回同一个ICM
对象就可以了。
要想替换掉ServiceManager.getService
的返回值,我们先看看这个方法的代码:1
2
3
4
5
6
7
8
9
10
11
12
13public static IBinder getService(String name) {
try {
IBinder service = sCache.get(name);
if (service != null) {
return service;
} else {
return getIServiceManager().getService(name);
}
} catch (RemoteException e) {
Log.e(TAG, "error in getService", e);
}
return null;
}
上述代码中, sCache
是ServiceManager
的一个静态成员变量(Map
类型),我们可以自己创建一个IBinder
对象(跟之前一样,使用动态代理创建),然后通过Map
的put
方法将sCache
里面的内容替换,从而达到瞒天过海的目的:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21// 首先获取ServiceManager class
Class<?> serviceManager = Class.forName("android.os.ServiceManager");
Method getService = serviceManager.getDeclaredMethod("getService", String.class);
// 调用ServiceManag的"getService"方法获取原始的 IBinder 对象
final IBinder rawBinder = (IBinder) getService.invoke(null, Context.CONNECTIVITY_SERVICE);
// 使用动态代理伪造一个新的 IBinder对象
IBinder hookedBinder = (IBinder) newProxyInstance(serviceManager.getClassLoader(),
new Class<?>[] {IBinder.class}, new InvocationHandler()
{
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable
{
// ....
}
});
// 把伪造的 IBinder 对象放进ServiceManager的cache里面
Field cacheField = serviceManager.getDeclaredField("sCache");
cacheField.setAccessible(true);
"unchecked") (
Map<String, IBinder> cache = (Map<String, IBinder>) cacheField.get(null);
cache.put(Context.CONNECTIVITY_SERVICE, hookedBinder);
替换掉ServiceManager.getService
的返回值以后,接着我们需要修改我们刚刚创建的IBinder
对象的queryLocalInterface
方法,让它始终返回同一个ICM
对象:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33new InvocationHandler() {
private Object mIConnectivityManager = null;
/**
* 获取原本的IConnectivityManager对象
*/
private Object getBaseManager(@NonNull IBinder binder, Class<?> stubCls) {
try {
Method asInterfaceMethod = stubCls.getDeclaredMethod("asInterface", IBinder.class);
return asInterfaceMethod.invoke(null, binder);
} catch (Exception e) {
LogUtils.error("wentaoli hook => createIConnectivityManager error: " + e, e);
}
return null;
}
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
if (!"queryLocalInterface".equals(method.getName())) {
return method.invoke(baseBinder, args);
}
if (mIConnectivityManager != null) {
return mIConnectivityManager;
}
// 替换掉 queryLocalInterface 方法的返回值
Object base = getBaseManager(baseBinder, Class.forName("android.net.IConnectivityManager$Stub"));
mIConnectivityManager = Proxy.newProxyInstance(proxy.getClass().getClassLoader(),
// asInterface 的时候会检测是否是特定类型的接口然后进行强制转换, 因此这里的动态代理生成的类型信息的类型必须是正确的
new Class[]{IInterface.class, Class.forName("android.net.IConnectivityManager")},
new BinderHookHandler(context, base));
return mIConnectivityManager;
}
}
1 | private static class BinderHookHandler implements InvocationHandler { |
大功告成。
经过上面操作,已经可以做到在wifi环境下模拟移动网络了,但还有一些可以完善的地方,比如说网络的切换。
考虑如下情况:当在应用中通过手动点击某个按钮将上述SettingsPreferences.getHookNetwork(context)
的值从false
变为true
,这时候getActiveNetworkInfo
方法返回的NetworkInfo
就从原来的wifi变成了自定义的3g/4g网络,这事实上就相当于发生了一次网络状态的变化。按照正常的流程,如果手机发生网络状态变化系统会发送相应的广播,同时app中动态或者静态注册的广播接收器会收到相应的广播。但是现在我们只是模拟网络变化,系统自然不会帮忙发送网络状态变化广播,那怎么去模拟这个过程呢?
网络状态变化广播是一个敏感的广播,需要系统级权限才能发送,普通应用是不允许发送的。既然不能直接发送这个广播,那能不能通过获取到当前应用中所注册的广播接收器,然后直接调用这些广播接收器的onReceive
方法来模拟广播接收过程呢?让我们来分析下。
首先来看静态注册的广播接收器。
要获取当前app中的注册了哪些静态广播接收器是比较简单的,就跟获取app中声明了哪些Activity
是一样的逻辑:1
2
3Intent i = new Intent(ConnectivityManager.CONNECTIVITY_ACTION);
i.setPackage(context.getPackageName());
List<ResolveInfo> list = context.getPackageManager().queryBroadcastReceivers(i, PackageManager.MATCH_ALL);
通过上述代码拿到能接收网络状态变化的静态广播接收器后(当然并不是BroadcastReceiver
实例对象,仅仅是一些BroadcastReceiver
描述信息),只需遍历这些接收器信息,然后通过Class.newInstance
方法创建相应的BroadcastReceiver
实例,再调用其onReceive
方法就行了,代码如下:1
2
3
4
5
6
7
8
9for (ResolveInfo resolveInfo : list) {
try {
Class<?> clazz = Class.forName(resolveInfo.activityInfo.name);
BroadcastReceiver receiver = (BroadcastReceiver) clazz.newInstance();
receiver.onReceive(context, intent);
} catch (Exception e) {
LogUtils.error("wentaoli hook static receiver error " + e, e);
}
}
这样就完成了模拟静态注册的广播接收器收到广播的场景。
下面再看动态注册的广播接收器
要获得应用中所有动态注册的广播接收器并不容易。通过对广播注册与接收的源代码的分析得知(关于广播的注册接收过程可以参考这篇博客),广播的接收过程最终会走到LoadedApk
的一个内部类 ReceiverDispatcher
的performReceive
方法中,而LoadedApk
的一个成员变量mReceivers
则保存了当前app中所有动态注册的广播接收器:1
private final HashMap<Context, HashMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>> mReceivers = new HashMap<Context, HashMap<BroadcastReceiver, LoadedApk.ReceiverDispatcher>>();
只要能拿到LoadedApk
实例的mReceviers
属性值,则可以获取到所有动态注册的广播。要获取LoadedApk
的mReceivers
属性值,首先得拿到LoadedApk
的实例。事实上,ActivityThread
的mPackages
属性就持有LoadedApk
的实例(看过四大组件启动流程源代码的同学应该对ActivityThread
类很熟悉),接着获取LoadedApk
以及LoadedApk
的mReceivers
属性就都不是问题了:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20// 先获取到当前应用的ActivityThread对象
Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
currentActivityThreadMethod.setAccessible(true);
Object currentActivityThread = currentActivityThreadMethod.invoke(null);
// 获取mPackages属性
Field f = activityThreadClass.getDeclaredField("mPackages");
f.setAccessible(true);
Map map = (Map) f.get(currentActivityThread);
// 获取LoadedApk实例
Object loadedApkRef = map.get(context.getPackageName());
if (loadedApkRef instanceof WeakReference) {
Object loadedApk = ((WeakReference) loadedApkRef).get();
// 获取LoadedApk实例的mReceivers属性值
field = loadedApk.getClass().getDeclaredField("mReceivers");
field.setAccessible(true);
Map mReceiversMap = (Map) field.get(loadedApk);
// 通过mReceivers属性获取所有动态注册的广播
....
}
在获取到所有动态注册的广播接收器后,只要知道哪些广播接收器可以接收网络状态变化广播,然后再直接调用这些广播接收器的onReceive
方法就好了。但事实上这并不容易,具体怎么判断哪些广播接收器可以接收网络状态变化广播,就不再说明了(其实是懒),有兴趣的可以到这里查看代码。
到此为止就完成了对发送网络状态变化广播的模拟,也就完成了Wifi下模拟3g/4g网络的整个流程的模拟。
关于动态代理:
动态代理是AOP
的基础。像 DroidPlugin 之类插件框架以动态代理作为基础的。
关于兼容性:
文中的代码中使用了较多的反射调用以及系统隐藏的api,由于公司没有那么多手机,没有测试代码的兼容性。
]]>http://weishu.me/2016/04/12/understand-plugin-framework-receiver/
http://weishu.me/2016/01/28/understand-plugin-framework-proxy-hook/
http://blog.csdn.net/u013263323/article/details/76014494
http://blog.csdn.net/Luoshengyang/article/details/6744448
很多应用都有更换主题的需求:新闻、阅读类应用通常需要支持白天、夜间模式切换,以便在不同时间都能提供良好的阅读体验;电商类应用需要在节假日、促销活动期间提供相应的主题以烘托活动气氛;社交娱乐类应用需要支持用户自行设置主题以满足用户个性化需要。本文将探索Android APP主题切换功能的各种实现方案,并结合聚力视频APP主题切换功能的实现过程,总结各个方案的优缺点以及适应场景。
Android 原生支持通过setTheme
方法为任一界面单独设置主题样式。要通过这种方式实现多主题,首先需要为那些支持主题切换的元素(控件)定义相应的属性。然后在不同的主题style
中为这些属性赋不同的值。接下来在布局文件中通过?attr/**
的形式设置控件的属性值。最终在Activity
中通过setTheme
方法(该方法需要在setContentView
方法之前调用)为当前界面设置主题。
当更换主题时可以finish
当前界面,然后改变主题使用的style
的值,并重新启动当前界面,这时候就可以完成主题替换。
改变主题时重启当前界面对用户来说不是一个好的体验,可以通过记录当前界面需要支持主题切换的控件,在切换主题时直接为这些控件设置新的属性值来规避这个问题。
图1-1是一个示例程序的运行效果:
示例程序源码: https://github.com/likebamboo/ThemeSwitch/tree/style
通过App内置的多个style实现主题切换小结:
优点 | 简单,容易实现;Android 原生支持 |
---|---|
缺点 | 主题资源只能内置,不能动态增删主题,增加了安装包的大小;必须由用户主动切换主题 |
适用场景 | 白天模式、夜间模式切换 |
典型APP | 网易新闻、QQ浏览器、哔哩哔哩动画 |
通过加载不同的zip主题包实现主题切换,首先需要将配置文件、资源文件打包压缩为zip主题包。然后在APP中内置或在适当的时候下载该主题包。在切换主题的时候通过解析配置文件加载zip包中的资源,实时为相应控件设置新的属性值。这样便可以实现主题切换。
需要注意的是,如果主题包比较大,解析、加载其中的资源文件将会是一个耗时的过程,有必要放在一个独立的线程中。另外,可能需要为不同分辨率的设备提供不同尺寸的主题包,防止低分辨率的手机在加载主题资源时出现内存溢出(OOM
)的问题。
图2-1是一个示例程序的运行效果:
通过普通zip主题包实现主题切换小结:
优点 | 可扩展行高,支持主题包动态下载并由后端控制主题切换时机 |
---|---|
缺点 | 需要自己解析zip包中的资源文件;针对不同手机分辨率可能需要多个zip主题包 |
适用场景 | 少量控件的颜色或图标切换 |
典型APP | 暂时未发现 |
方案2虽然可以实现动态的主题切换,也方便制作多套主题。但是,需要在程序中自己实现查找、解析资源的过程,切换主题耗时比较长。
如果可以像使用APP内部资源一样直接通过资源id或描述符使用主题包资源,就省去了查找、解析资源的过程。调研发现,Android的资源查找、管理离不开AssetManager
和Resources
这两个类,APP资源的管理实际是由这两个类实现的。其中Resources
类可以根据id来查找资源,AssetManager
类根据文件名来查找资源。如图3-1
而AssetManager
恰巧可以通过addAssetPath
方法实现对非APP内部的其他资源的管理。
因此,为了实现使用id或资源描述符来查找主题包中的资源,我们需要将主题资源编译打包成一个apk文件。然后,创建一个独立的AssetManager
和Resources
对象用于管理主题包中的资源。代码如图3-2
需要注意的是,addAssetPath
方法是Android隐藏的API,所以需要通过反射的方式调用。另外主题包中的资源id和APP内部的资源id不可能完全一样,所以使用APP内部的资源id去主题apk包中查找资源会出现找不到或找不准的情况,这时候可以通过APP内部的资源名去主题包中查找对应的资源。
有了管理主题资源的Resources
对象,在切换主题时,只要通过Resources
对象从apk主题包中获取主题资源,并为对应的支持主题切换的控件设置相应的属性值即可。
图3-3是一个示例程序的运行效果:
通过apk主题包实现主题切换小结:
优点 | 针对不同手机分辨率可以加载最合适的皮肤资源,无需多个主题包;支持主题包动态下载并由后端控制主题切换时机 |
---|---|
缺点 | 实现起来相对困难,容易出错;主题包太大时会对程序性能有一定影响 |
适用场景 | 少量控件的颜色或图标切换 |
典型APP | 聚力视频、爱奇艺 |
以上就是主题切换功能常见的几种实现方案,聚力视频APP目前采用最后一种。也许还有其他的实现方案。但这些实现方案中很难说哪种最好,毕竟就技术而言,常常只有最适合的方案,而没有最好的方案。
]]>参考文章 :
https://blog.stylingandroid.com/prism-fundamentals-part-1/
原文:http://www.developerphil.com/android-studio-tips-of-the-day-roundup-6/
1 | Mac : ctrl + t |
这是一个针对当前选择的代码显示上下文所有可用的重构的快捷键。这个列表可以通过键盘进行检索并且你也可以使用左侧的数字进行快速访问。
1 | Mac : cmd + shift + e |
这个和Recents
弹出框有所不同,这个列出是在本地最近被修改的文件。它是按修改的顺序进行存储(最上面是最近被编辑的)。更方便的是你可以输入字符进行过滤列表。
1 | Mac : cmd + ctrl + up |
它可以帮助你很轻松地在布局和Activity/fragment
之间进行切换。还有一个快捷方式是在类名的旁边和布局文件的顶部。。
1 | Mac : cmd + alt + v |
它是一个可以不通过重构菜单就自动提取变量的快捷键。
当你动态生成代码时你可以不用输入变量的声明就可以直接生成变量名称。IDE将会生成声明并且还会给出一些建议的变量名称。
相关技巧:
如果你想修改声明类型为一些更通用的(如:
List
而不是ArrayList
),你可以使用Shift+Tab
它会给出一个可用类型的列表。
1 | Mac : cmd + alt + p |
它是一个可以不通过重构菜单就自动提取参数的快捷键。
当你意识到一个方法可能是通用的时候,可以通过提取一部分做为一个参数。它会使用当前值作为一个参数然后复制原先的值作为调用者的参数。
1 | Mac : cmd + alt + m |
跟着我提取的思路进行重构,这个操作可以提取一个代码块做为一个新的方法。
这个功能是相当有用的。无论什么时候,当你遇到一个变得有点复杂的方法的时候,你可以使用这个操作安全地抽取一部分代码生成一个单独的方法。我所说的安全是因为IDE不会像我们可能会犯一个愚蠢的复制粘贴错误。
译者注: Eclipse中该操作的快捷键是
alt + shift + m
1 | Mac : cmd + alt + n |
你使用提取有一点疯狂并且现在有太多的东西?你可以使用反向操作,它叫做inline
。
它可以作用于方法,Fields
,参数和变量。
1 | Mac : shift + f6 |
使用这个,你可以将一个变量,field,方法,类和甚至是包重命名。
当然了,它会确保重命名在你整个应用的上下文中是有意义的,它不会简单地做一个查找然后替换所有文件!
译者注: Eclipse中该操作的快捷键是
alt + shift + r
1 | Mac : ctrl + t 然后选择成员 |
这里指的上拉成员的意思是我们将当前类的一些成员(通常是方法或属性)发送它到父类或接口。
如果继承于一个类,内容会被移动。如果是实现的一个接口,它将会声明方法作为接口的一部分,在你的类中保持原有的方法并且添加@Override
注解。
这里指的下推成员,这正好是反向操作,我们会从父类或接口发送一些成员到子类。
原文:http://www.developerphil.com/android-studio-tips-of-the-day-roundup-5/
1 | enter or tab |
你可以用Enter
或Tab
来实现代码自动补全并且它们之间有一个有趣的差异。
使用Enter
将会自动完成你想要的语句。使用Tab
将会自动完成语句并且向前删除所有代码直到下一个点号,括号,分号或空格。(译者注:看下图就明白了)
1 | Mac : cmd + u |
如果你的光标在重写父类的一个方法内(如:Activity#onCreate()
),这个将会跳到父类的实现上。
如果你的光标在类名上,它将会跳到父类。
有很多快捷键使光标离开编辑器(type hierarchy
,find usages
等)
如果你想退回到编辑器,你可以选择如下操作:
Escape
: 简单地返回到编辑器。Shift+Escape
: 关闭当前面板然后使光标返回到编辑器。
1 | Mac : f12 |
有时候,你从面板返回到编辑器,但是你发现不得不再返回到这个面板。例如:浏览find usages
。使用这个快捷键,你可以不用鼠标返回到这个面板。
1 | Mac : cmd + shift + f12 |
让编辑器进入某种形式上的全屏模式。再次调用这个快捷键可以返回所有面板到它们之前的状态。
1 | Mac : cmd + number |
你可能注意到一些面板名字左边有一个数字。这是打开它们的快捷键。
如果你看不到面板的名字,可以点击IDE的左下角的盒子似的东西。
1 | Mac : cmd + p |
当你正调用一个方法时会显示一个方法参数的列表。这在你想看已存在的方法参数时是挺有用的。
你光标所在位置的参数会用黄色显示。如果没有参数用黄色,这意味着方法调用是无效的,可能有参数不能被正确地强转(如:将一个float
值传给一个int
类型的参数)
当你正在写一个方法调用参数信息意外地消失了,就像我经常做的,你也可以输入一个逗号(,
)用来触发参数信息的显示。
1 | Mac : ctrl + tab |
这个功能和IDE
的alt + tab
/ cmd + tab
差不多。它允许你导航到一个tab
或一个panel
。
一旦它被打开,只要你按住ctrl
键,你可以使用数字或字母快捷键快速导航。你也可以通过按下backspace
键关闭一个已选择的tab
或panel
。
1 | Mac : ctrl + shift + q |
这个将会显示你当前位置,当你的定义范围超出滚动的区域时。通常,这将会是类或内部类的名称,但它也可能是当前方法名。
在我看来,它最好的作用是快速查看当前类继承或实现关系。
它也可以在xml文件中使用。
原文:http://www.developerphil.com/android-studio-tips-of-the-day-roundup-4/
1 | 菜单 : `Analyze -> Analyze Data Flow to Here` |
这个可以查看当前变量、参数或field
调用的路径!当你进入到一个你不熟悉的代码而又想理解这个参数是怎样传到那里的时候,这个操作就非常又有用了。
该操作也有一个反向操作Analyze Data Flow
from
Here
,它将会显示变量、field
或返回类型的被调用的路径。
1 | Mac : ctrl + g |
这是一个格外棒的功能!
当选中当前选择部分后,它会同时选中下一个出现代码的地方并且添加一个光标。这就意味着你可以在同一个文件中拥有有多个光标!你编辑的任何内容会在每个光标处都相同地执行一遍。
1 | Mouse: alt + 拖动鼠标 |
列选择,也被称为块选择。基本上,如果你向下移动光标,它将直接向下选择而不会很烦人地选择到行尾。
该操作同样会在块选择的每行后面放置一个光标。
译者注: 在Eclipse 中一直没找到该功能,不知道是不是我孤陋寡闻,平时只要涉及到按列选择,都是使用 notepad++ 完成的。
这个不是特别的直观但功能依然很强大。该操作简化了用其它的东西包裹当前代码的操作,而不必大量的敲击键盘。
例如:为了实现对一个列表的迭代,你可以使用myList.for
, 然后按下Tab
键,它将会给你生成一个for
循环。
一些我个人非常喜欢的:
.for
(用于生成for
循环).format
(用String.format()
包裹一个 字符串).cast
(用一个强制类型转换包裹一个语句)
1 | Mouse: 右击选中的部分 -> 选择 `Compare With Clipboard` |
它可以让当前选择的部分和剪贴板的内容做一个diff
。
1 | Mac : cmd + f2 |
该操作会终止当前正在运行的任务或者显示一个可以终止的任务列表(当不止一个任务正在运行时)。
对于终止调试或终止构建非常有用.
1 | Mac : alt + f10 |
当正在调试时,该操作会让光标返回到当前正在调试的地方。
通常用于如下情况:
1 | Mac : ctrl + v |
该操作可以显示版本控制最频繁的操作选项。如果你的工程没在git
或其他版本控制系统管理下,它至少给你一个Android Studio
维护的本地历史。
1 | 菜单 (for git): `VCS -> Git -> Compare With Branch` |
假设你的工程在Git下,你可以将当前的文件或文件夹和别的分支比较。对于查看和你的主分支有多少不同时相当有用。
原文:http://www.developerphil.com/android-studio-tips-of-the-day-roundup-3/
下面几个技巧是关于调试的。
1 | Mac : cmd + f8 |
我们从最简单的一个开始学习:添加一个断点。相信你已经调试过应用并且知道在左边框上通过鼠标左键单击设置或取消断点。如果不使用鼠标,你可用通过该快捷键设置断点。
译者注: Eclipse 中该操作快捷键是
ctrl + shift + b
.
1 | Mouse : 在断点上右击,然后输入一个条件。 |
简而言之,只有在某些条件下才打开断点。你可以输入任何基于当前范围返回一个boolean
类型的java
表达式。而且可喜的是是条件文本框支持代码自动补全。
1 | Mouse : 在断点上右击,取消选中`Suspend`(暂停),在`Log evaluated Expression`输入你的消息。 |
这是一个输出日志信息但不会中断运行的断点。当你想打印一些东西但又不能或不想添加打印日志的代码时该断点就非常有用了。
1 | Mouse : 在左侧框上`alt + 单击` |
添加一个断点, 第一次运行触发到它后自动移除该断点。
1 | Mouse : 在左侧框上的已存在的断点上 `alt + 单击` |
这将禁用该断点。当你有一些复杂的条件或日志断点,你现在不需要但你不想下次重新创建的时候可以禁用断点。
译者注: Eclipse 中该操作快捷键是 在左侧框上
shift + 双击
.
1 | Mouse: 点击这个图标或从菜单选择`Build->Attach to Android Process`(译者注:不知道此菜单是否为MAC上的功能,Windows下的为`Run->Attach debugger to Android Process`) |
当你没有以调试模式启动应用时可以通过该方法调试应用。这个是非常有用的, 因为你不用重新以调试模式部署应用。当有人在测试应用时,测出一个bug, 给你他的设备时,这个操作就相当有用。
1 | Mouse: 点击这个图标或从菜单选择`Build->Attach to Android Process`(译者注:不知道此菜单是否为MAC上的功能,Windows下的为`Run->Attach debugger to Android Process`) |
它被用来检查一个变量的内容以及对任何有效的java表达式求值。需要知道的是如果你的状态改变了,当你恢复程序的执行时候它还会保持那种结果。
译者注: Eclipse 中在断点处选中变量、表达式然后
右键 -> watch
查看表达式的值
1 | Mouse: 在表达式上` alt + 单击` |
查看一个表达式的值不会打开Evaluate
表达式对话框。
译者注: Eclipse 中该操作的快捷键是
ctrl + shift + i
, 或者使用鼠标右键 -> Inspect
在variables
或watch
面板上1
2
3Mouse: `右击`选择 `Mark Object`
Mac : 选中对象后 + `f3`
Windows / Linux : 选中对象后 + `f11`
在调试会话中,这个可以在一个指定的对象上添加一个标签,因此稍后你可以识别它。在你有一些相似的对象并且你想知道它和之前的是同一个对象时这样的调试会话中是非常有用的。
1 | 菜单: `Analyze -> Analyze Stacktrace` |
该操作可以抓取已经显示在logcat
中的异常栈并使之可以点击跳转到相关代码。当从bug
日志或终端复制异常堆栈时非常有用。
原文:http://www.developerphil.com/android-studio-tips-of-the-day-roundup-2/
接上篇:
1 | Mac : cmd + d |
正如字面意思:它将会复制当前行并且粘贴在下一行,它并不会复制到剪贴板。当它被用于复制当前行时它将会是非常有用的。( 译者注: 当然,该命令也可以复制选中的行)。
译者注:Eclipse 中该操作的快捷键是
ctrl + alt + up/down
1 | Mac : alt + up/down |
这个能扩大当前选中的区域。如它能选择当前的变量,然后是声明,然后是方法等等。
1 | Mac : cmd + alt + t |
该快捷键被用来用一些结构包裹代码块。通常使用if
、while
、try-catch
或runnable
。
如果什么也没有选中,它将会包裹当前行。
1 | Mac : cmd + e |
在第一篇文章中已经提到,使用这个可以得到一个最近打开的文件的可搜索的列表!
1 | Mac : cmd + j |
在线模板是一个快速插入代码片段的方式。使用在线模板有趣的是它能参数化,当你插入代码时它可以使用参数智能的引导你。
相关技巧
- 如果你知道它的缩写你就不需要调用快捷键。你可以直接输入它并用
Tab
键完成输入。
1 | Mac : cmd + alt + up/down |
这个和移动当前行类似,但它可以用于整个方法。它可以上下移动一个方法不用复制粘贴。
这个action
真正的名称是Move Statement
。这意味着它可以移动statement
中的任何一种。如:你可以重新排列字段和内部类的顺序。
1 | Mac : cmd + shift + enter |
它可以在编写语句时生成未完成的代码,通常用于下列情况:
if
、while
或for
添加括号和花括号相关技巧
- 如果
statement
已经完成,它会直接跳到下一行,即使光标没有在当前行的最后一个字符。
1 | Mac : cmd + shift + backspace |
在第一篇文章中已经提到,它将会跳到你最后修改代码的位置。这个和工具栏上的后退是不同的,它会在你的编辑历史中跳转,而不是导航历史.
1 | Mac : ctrl + shift + j |
这个比在行尾模拟删除键能做的更多!它会保存格式化规则,还有下面的特性:
//
相关技巧
- 如果你选择一个多行的字符串,它将会合并成一行。
1 | Mac : alt + f1 |
询问你从哪选择当前的文件。恕我直言,这是最有用的快捷键对于在工程结构中或你的文件资源管理器中打开。每个action
都有一个字母或数字的前缀,这是快速调用它的快捷键。
通常,我会使用Alt+F1
然后回车为了在工程视图中打开和Alt+F1+8
在Mac
的文件资源管理器中打开。
你可以从工程视图中调用这个对于一个文件或文件夹。
1 | Mac : cmd + shift + delete |
移除周围代码。它可以移除if
、while
、try/catch
甚至一个runnable
。这个正好和Surround With
(包裹代码块)的快捷键功能相反。
原文:http://www.developerphil.com/android-studio-tips-of-the-day-roundup-1/
1 | Mac : cmd + shift + f7 |
这个快捷键将会高亮当前选中字符所有的出现之处。当然这个快捷键不仅仅只是一些简单地模式匹配,它还会理解当前的变量所处范围,只高亮相关的字符。
高亮之后你就可以使用Edit → Find → Find Next/Previous
处定义的快捷方式来选择你要操作的高亮字符。
译者注: Eclipse 中高亮显示成员变量的快捷键是
alt + shift + o
.
相关技巧:
- 高亮代码方法中的
return
或者throw
, 同时也会高亮这个方法的所有出口。- 高亮
Java
类的extends
或者implements
的定义部分也会高亮对应的重写或者实现的方法。- 高亮
import
语句也会高亮它被使用的地方。Esc
键可以取消高亮。
1 | Mac : ctrl + up/down |
这个快捷键可以让你很方便的在当前文件的方法或者类上面跳转。
如果你当前处于一个方法中,此快捷键(向上)可以让你的光标跳至方法名处。这对你重构代码或者找到这个代码的使用之处很有帮助。
译者注: Eclipse 中该快捷键是
ctrl + shift + up/down
.
1 | Mac : cmd + f12 |
这个快捷键可以帮助你展示当前类文件的方法结构。你可以使用这个快捷键弹出弹窗,查找你想要的方法名。
译者注: Eclipse 中该快捷键是
ctrl + o
.
相关技巧:
- 你可以使用驼峰字符来过滤候选方法列表。例如:输入
oCr
就可以找到onCreate
方法。- 你可以选择是否展示匿名类。如果你勾选了
是
就可以很方便的查找OnClickListener
里面的onClick
方法了。
1 | Mac : ctrl + alt + h |
这个快捷键会显示一个方法的声明和它的调用之间的可能的路径。
1 | Mac : alt + space |
你是否曾经想在当前页面查看一个方法或类的定义?使用这个快捷键在当前页面查找它。
1 | Mac : alt + plus/minus |
这个特性的目的是隐藏在某一时刻你不关心的代码。在这个简单的形式中,它将会隐藏整个代码块(如:当你打开一个新文件时忽略导入列表)。更有趣的是它可以隐藏匿名内部类周围的模板代码让它看起来像是一个lambda
表达式。
相关技巧:
- 你可以在
Setting->Editor->Code Folding
中设置默认的折叠范围
1 | Mac : f3 |
1 | Mac : alt + f3 |
如你给书签分配了一个数字,你可以使用下面的快捷键返回到对应书签:1
ctrl+number
1 | Mac : cmd + f3 |
译者注: Eclipse 中貌似并没有书签的快捷键,但是同样也可以设置书签,只要在编辑窗口左边的边框上右键就可以看到添加书签的菜单。 同时可以通过
window -> show view -> Bookmarks
来查看所有书签。
1 | Mac : cmd + shift + a |
你可以通过名称在Android Studio中调用任何你知道的菜单或action
!对于你之前使用过但没有快捷键的命令来说这是非常有用的。
相关技巧:
- 如果这个Action有快捷键,它将会显示在旁边。
1 | Mac : alt + shift + up/down |
对,这个是用来上下移动当前或选择行代码。没有什么更多要说的,享受它吧。
译者注: Eclipse 中的快捷键是:
alt + up/down
1 | Mac : cmd + backspace |
这个是用来删除当前或选中的行代码
]]>译者注: Eclipse 中的快捷键是:
ctrl + d
原文: http://www.developerphil.com/android-studio-tips-tricks-moving-around/
你应该知道的关于我的两件事:
两年前,当我转向Intellij IDEA
,Android Studio
基于它,为了更高效的开发,我花费了大量的时间去寻找快捷键以及使用技巧。当你看到这篇文章,我相信你也做了很多同样的事情,所以我努力让它更容易而且更方便一些。
在这个系列教程中,我们将学习到每个开发者都应该知道的最基本的开发技巧以及Android Studio
中更多高级的技能。
Android Studio
提供了不同的键位映射(即快捷键和它对应的操作之间的映射),你可以在 Settings->Keymap
菜单里面查看当前所使用的键位映射。
单纯列出每个键位映射是不切实际的,因此将会使用下面的:
Windows: 默认
Linux: 默认
OSX: Mac OSX 10.5+(不是默认的一个,强烈建议使用Jetbrains)
我们花费了大量的时间在代码跳转上,让我们尝试提高它的效率。
1 | Mac : cmd + o |
假设你要切换到名为MainActivity.java
的类,就可以使用该快捷键然后输入Main
就可以了。
译者注: Eclipse 中打开类的快捷键是
ctrl + shift + t
1 | Mac : cmd + shift + o |
和打开类相似,但是该快捷键可以打开工程目录下的任意文件。这可以快速帮你打开如AndroidManifest.xml
或res
和assets
目录下的文件
译者注: Eclipse 中打开任意文件的快捷键是
ctrl + shift + r
1 | Mac : cmd + alt + o |
功能强大但没有前面的两个快捷键出名:你可以通过搜索方法或变量名称直接跳转。
例如,你知道工程中的某个地方有个名为getFormattedDate()
的方法,你可以使用这个快捷键直接找到它。
技巧:
部分匹配: 如果你有一个类叫
ItemDetailFragment
,你可以在搜索的时候直接输入IDF
就可以查找到的
行号: 假设你有一个同事刚刚告诉你XXX
在ExcitingClass
的第23
行,可以在打开类快捷键上中加上ExcitingClass:23
或者EC:23
可以快速跳转到指定行号
1 | Mac : cmd + e |
该快捷键将弹出一个最近打开文件的对话框
1 | Mac : cmd + shift + e |
和上面功能类似,但列出的仅仅是被修改过的文件。
译者注: Eclipse 中貌似没有列出最近使用的文件的功能,
ctrl + e
快捷键用于列表当前编辑窗口中打开的文件。
技巧:
输入字符可以进行列表过滤
1 | Mac : cmd + alt + left/right |
想要更好地理解这个快捷键,你应该想想web
浏览器上前进和后退是怎样工作的。现在不是在web
页面上,而是源代码中!因此当你跳转到一行代码或打开一个新的文件时,IDE将会记住你之前的位置,并且可以快速返回。
译者注: Eclipse 中前进或后退的快捷键是
alt + left/right
.
1 | Mac : cmd + shift + backspace |
这个是上面的快捷键一个衍生,它可以在上次修改代码位置之间进行跳转。
想像你正在修改一个让人讨厌的bug。你觉得你可以解决它并且开始修复它,但当你意识到在你的工程中你不得不去看android
源代码和其它类的时候,你进入其它类的一个功能,然后又跳到其它文件中20步以后,你终于完成了你的修复,但你刚才正在编辑的是哪一行?只要使用这个快捷键你就可以正确地返回。
1 | Mac : alt + f7 |
该快捷键可以显示被引用地方。对于一个类变量来说,会显示变量使用和赋值的地方。对于一个类方法来说,会显示方法被调用的地方。对于一个类来说,会显示创建实例的地方。
你可以使用箭头键和返回键在显示结果中查看。然后可以使用Esc
返回到编辑窗口。
译者注: Eclipse 中显示引用的快捷键是
ctrl + shift + g
.
1 | Mac : cmd + alt + f7 |
和上面作用一样,显示在弹出框中。
这里有三个关于符号的快捷键
1 | Mac : cmd + b 或者 cmd + click |
跳到类、方法或变量声明的地方。跳到类和方法的实现上是很有用的.
译者注: Eclipse 中跳转到声明的快捷键是
f3
或者ctrl + click
.
1 | Mac : cmd + alt + b |
显示所有类/接口的实现类/接口。对于方法也适用,会显示重写的方法。对于变量,会跳转到声明
1 | Mac : cmd + shift + b |
当光标在一个变量上,它会跳到变量类型的声明处。例如,下面一行代码:1
Developer phil = new Developer("Phil");
如果光标在phil
变量上,按下快捷键会跳到Developer
类的声明处。
1 | Mac : cmd + u |
这个快捷键会打开当前选中的父类,和跳转到实现的功能想相反。如果光标在一个重写的方法是,将会直接跳转的父类的方法。如果光标在一个类中但在方法之外或光标在类名上,那么它会打开父类。
快速跳转到类、变量或者方法的声明。主要用在类和方法
最初想到的就是使用开源图片加载框架 UIL , 作为目前使用最广泛的图片加载框架,不用真是可惜了,于是优先考虑它了。
Demo很快就写好了,大部分手机上也测试OK,但是在一台小米3手机上出现了内存溢出的问题,如下图(OOM导致了部分图片出现加载失败):
究其原因,米三手机的分辨率(1920*1080)太大,拍照拍出来的图片也大(一般都在2M左右),当一次加载的图片太多的时候就容易出现内存溢出的情况。
为了不占用太多的内存,我按照官方的说法(见UIL项目介绍):
将内存缓存的图片尺寸改小(之前没有指定,UIL框架默认会按照手机的分辨率来确定缓存图片的尺寸):
1 | ImageLoaderConfiguration.Builder builder = new ImageLoaderConfiguration.Builder(context) |
将显示时的图片的质量降低、同时图片缩放:
1 | DisplayImageOptions options = new DisplayImageOptions.Builder() |
OK,内存溢出的现象是不见了,但是新的问题出现了:
由于UIL的缓存配置是全局的,如果设置缓存的图片尺寸较小,那么当用户需要查看大图片的时候,可能加载出来的图片是小尺寸的缓存图片。
况且像这样指定缓存图片的大小可能影响到其他使用的UIL显示大图界面,所以为了不影响UIL在其他界面的使用,还是不要限制图片在内存中缓存的尺寸。
另外UIL在缓存图片的时候虽然也有控制图片缩放的参数.imageScaleType
但是却不能根据需要来缩放,所以种种原因迫使我放弃使用UIL来加载本地图片。
在决定放弃使用UIL加载本地图片之后,我开始自己写缓存图片的方案,我的想法是只在内存中缓存就好,没必要再在本地磁盘中缓存一份,同时为了保存不OOM,需要限定缓存图片的规格。
于是我专门写了一个加载本地图片的类,使用android支持包中的LruCache作为内存缓存,根据要显示图片的ImageView控件的宽和高来缩放图片以降低内存使用量。
使用时也比较简单:
由于使用LruCache,出现内存不足的时候,系统会自动gc,所以一般情况不会出现OOM的情况。同时由于没有缓存的时候都会新建一个线程用于加载图片所以加载图片的速度也还可以接受。
但是问题是,当图片多的时候,频繁新建线程的内存开销会比较大,这样会导致UI线程卡慢的情况(ListView或GridView滚动不流畅)。
出现这种情况后我立马就想可以,用线程池来代替每次都新建线程的情况。
用线程池代替Thread比较简单,Java中的Executors类以工厂模式的方式提供了一些快速创建常用的线程池的方法。例如:
Executors.newFixedThreadPool(int nThreads);
Executors.newSingleThreadExecutor();
Executors.newCachedThreadPool();
Executors.newScheduledThreadPool(int nThreads);
使用线程池后UI主线程不卡了,但又有问题,当图片多的时候,Gridview滑动到底部,这时候图片可能要等很久才能加载出来。这种情况很好理解,当图片很多的时候,线程池中的那几个线程根本不够用,所以这时候图片的加载还是会表现出一定的顺序性。如果直接滑动到GridView或ListView的底部,图片自然要等一段时间才能加载出来。
于是乎我想,可以通过监听ListView/GridView的滚动来控制图片是否加载,当控件滚动的时候不加载图片,只有当其静止的时候才加载。
最初想到这种方式后并没有立即就去做,原因就是这种方式的可复用性比较低,要所有显示本地图片的界面都去监听滚动事件有点不切实际,况且从没见过哪个图片加载框架要监听控件的滚动,但他们都运行的好好的,所以这种方法肯定不是最好的。
更现实的问题是当某次要加载的图片特别多的时候(比如说超过1000张),如果用户恰好要选择靠后的图片,这时候控件大部分时间处于滚动的状态,如果此时不加载图片,就会给用户一个图片没有加载的信号,用户感官上会觉得图片加载很慢。同时在滑动的过程中,从用户手指离开屏幕到控件完全停止滚动是一个减速的过程,这个过程有一段时间,如果在这段时间就开始加载图片而不是等到控件完全停止才加载,那么等控件停止的时候就可以省下不少加载图片的时间了。
综合考虑以上因素,我放弃了通过监听ListView/GridView 的滚动事假来判断是否应该加载图片的这种想法。
重新审视这个问题,我觉得至少有两个条件必须满足:
第二个问题是目前最棘手的问题。如果优先加载ListView/GridView可见区域的图片而暂时忽略不可见部分的图片,由于可见部分的图片数量比较少,即使单个图片比较大也能在短时间内将可见区域的图片加载完。这样不管用户想要看前面的图片还是后面的图片都能在短时间内在界面上显示,这样用户的体验会比较好。
前面讲到的监听控件的滚动事件其实际也是优先加载可见区域的图片。那么可不可以用其他方法优先加载可视化区域的图片呢?
经过一段时间的探索,我的最终方案确定了:
由于在ListView/GridView滚动的时候会调用其adapter的getView方法(该方法中发送加载图的请求),而且是只有当ListView或GridView中的Item可见的时候才会调用getView方法,
这样我们就可以人为定义图片加载的优先级了,总结起来一句话:后来居上。 具体实施方案如下:
这样最终效果还不错,也达到了我与其的效果。
]]>