您的位置:首页 > 移动开发 > Android开发

Android插件化的思考——仿QQ一键换肤,思考比实现更重要!

2016-12-09 09:07 357 查看
标签: android插件博客qq
2016-09-17 15:58 2025人阅读 评论(7) 收藏 举报


 分类:

Android(110) 


版权声明:本文为博主原创文章,博客地址:http://blog.csdn.net/qq_26787115,未经博主允许不得转载。

目录(?)[+]


Android插件化的思考——仿QQ一键换肤,思考比实现更重要!

今天群友希望写一个关于插件的Blog,思来想去,插件也不是很懂,只是用大致的思路看看能不能模拟一个,思路还是比较重要的,如果你有兴趣的话,也可以加群:555974449,你也可以说出你想看的Blog哦,嘿嘿!好的,不多说,我们进入正题:

关于QQ的换肤,他们的实现思路我不是很清楚,但是你可以看一下这张换肤的截图



我们想使用哪个主题就直接下载就好了,这一实现的过程我们大致的可以猜想:

首选是下载到本地指定文件夹,然后通过插件加载到我们的apk,最后应用为皮肤,逻辑大致是这样的逻辑了,那我们是不是应该动动手啊动动脑?

首选我们新建一个工程好了——PlugInSample



一.实现思路

其实说起来,这个插件的实现思路,确实是比较的麻烦,思来想去,还是一种办法比较靠谱,首先,我们刻意去获取手机上所有的安装的/未安装的程序,过滤掉没用的,留下我们的插件apk,我们的插件apk怎么去辨别呢?我们可用通过设置sharedUserId,然后用实体类把插件名称和包名保存下来,有了包名,就比较好说了,我们可用获取插件的上下文,也就是createPackageContext,然后就可以做点坏事了,我们可以去剖析我们的R文件



因为R文件里面都是静态的原因,我们很容易联想到反射机制,是的,我们可以再一次过滤掉无用的信息,通过我们的PathClassLoader去加载,访问我们的内加载器反射到我们的图片ID,也就是后面的那段数字,然后,嘿嘿,就可以使用了,是不是思路比较清晰了?这里要注意的就是图片命名统一,这样就比较号过来,那具体我们应该怎么做?

二.PlugIn主程序

我们写一个Spinner,每次切换就直接换肤怎么样?OK,每次换的时候就从插件APK里加载我们的图片资源,看起来是比较顺畅的逻辑,那我们具体该怎么做呢?

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
android:id="@+id/mLinearLayout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center"
android:orientation="vertical">

<Spinner
android:id="@+id/mSpinner"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>

</LinearLayout>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

1.初始化

/**
* 初始化View
*/
private void initView() {
//初始化控件
mSpinner = (Spinner) findViewById(R.id.mSpinner);
}
1
2
3
4
5
6
7
1
2
3
4
5
6
7

当然,我这刚应用就一个View,但是实际开发当中可不止,所以步骤一定要明了

2.获取所有的插件

/**
* 获取手机里的插件
*
* @return
*/
private List<PlugInBean> findPlugIn() {
mList = new ArrayList<>();
//获取相关信息
PackageManager mPackageManager = getPackageManager();
//获取卸载/未安装的安装包信息
List<PackageInfo> mUninstallPackage = mPackageManager.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES);
//遍历拿到我们的信息
for (PackageInfo info : mUninstallPackage) {
String pkgNmae = info.packageName;
//获取shareId,根据id判断是否是我们的ID
String shareUserId = info.sharedUserId;
if (!TextUtils.isEmpty(shareUserId)) {
//如果id相同
if (shareUserId.equals("com.liuguilin.share")) {
//且排除自己的包名
if (!pkgNmae.equals(getPackageName())) {
//这个就是我们的插件了
String lable = mPackageManager.getApplicationLabel(info.applicationInfo).toString();
PlugInBean bean = new PlugInBean();
bean.setLabelNmae(lable);
bean.setPackagNmae(pkgNmae);
mList.add(bean);
}
}
}
}
return mList;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

这里就是过滤了一下,通过sharedUserId去拿到我们的插件APK了,然后就可以拿到我们的包名和应用名,他返回给我们一个数据集

//所有的插件
List<PlugInBean> allPlugIn = findPlugIn();
1
2
1
2

3.加载皮肤数据

/**
* 加载皮肤
*
* @param allPlugIn
*/
private void LoadSkin(List<PlugInBean> allPlugIn) {
//遍历
for (PlugInBean bean : allPlugIn) {
HashMap<String, Object> mMap = new HashMap<>();
mMap.put("lable", bean.getLabelNmae());
mMap.put("package", bean.getPackagNmae());
mData.add(mMap);
}
//建立Adapter并且绑定数据源
mAdapter = new SimpleAdapter(this, mData, android.R.layout.simple_list_item_1, new String[]{"lable"}, new int[]{android.R.id.text1});
//设置数据
mSpinner.setAdapter(mAdapter);
//设置监听事件
mSpinner.setOnItemSelectedListener(this);

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

我们通过刚才的数据集便可以把我们拿到的数据给直接显示出来了,这里其实可以判断一下size是否为0,如果为0的话也就没有插件,OK,我们设置adapter和监听,做到这里,其实你可以运行一下,虽然我们现在什么都没有,我们要做的还有很多

4.获取插件Context

/**
* 选中监听事件
*
* @param adapterView
* @param view
* @param i
* @param l
*/
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
PlugInBean bean = mList.get(i);
//插件的包名
String packageNmae = bean.getPackagNmae();
Context mContext = null;
try {
//无视警告 访问代码
mContext = createPackageContext(packageNmae, CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_CODE);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
//获取图片
getImg(packageNmae, mContext);
//通过ID加载插件的图片
getWindow().setBackgroundDrawable(mContext.getResources().getDrawable(mListId.get(i)));
}

@Override
public void onNothingSelected(AdapterView<?> adapterView) {

}
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
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

这里的代码就比较有意思,一定要仔细看,我们首先拿到选中的item的包名,通过我们的createPackageContext拿到我们的上下文,通过这两个我们可用拿到我们的资源ID,也就是R清单里面的ID,然后直接设置window的背景,这里为了好看才设置window的背景,实际上你要设置的是你根布局的背景,那好,我们来看一下如何通过插件的上下文和包名拿到R清单的资源ID


5.获取插件图片 / 返回图片R文件ID / 反射R文件

/**
* 获取插件图片 / 返回图片R文件ID / 反射R文件
*
* @param packageNmae
* @param mContext
*/
private void getImg(String packageNmae, Context mContext) {
//类加载器反射插件
PathClassLoader pathClass = new PathClassLoader(mContext.getPackageResourcePath(), ClassLoader.getSystemClassLoader());
//反射 $ 访问类加载器
try {
Class<?> forNmae = Class.forName(packageNmae + ".R$drawable", true, pathClass);
//拿到所有图片的id
Field[] files = forNmae.getDeclaredFields();
for (Field id : files) {
//过滤 / 这里的命名可以注意一下
if (id.getName().startsWith("img")) {
int drawId = 0;
////这就是我们图片R下的ID
drawId = id.getInt(R.drawable.class);
mListId.add(drawId);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
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
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

这里我们做了很多事情,首选是拿到我们的类加载器去反射我们的插件,然后通过Class去拿我们的资源,这里注意packageNmae是我们的文件目录,他下面的R文件,$代表类部类的意思,他下面的drawable子节点,然后再一次过滤,过滤之后我们可用遍历一遍拿到我们的ID用List保存起来,也就有了我们选中的时候的设置,好的,到这里主程序算是编写完成了,不过要注意的是,记住要添加sharedUserId啊,至关重要!!!

android:sharedUserId="com.liuguilin.share"
1
1

我们现在运行也是空的,无意义,我们直接来写我们的插件吧!

三PlugInApk插件

插件的编写很简单,我们新建一个PlugInApk的工程



工程里要做的事情就三件

1.添加sharedUserId
android:sharedUserId="com.liuguilin.share"
1
1

2.更改name

这就取决于你了,比如我这里是Angelababy的主题,我就把名字改成Angelababy

3.把图片放在drawable文件夹下

好的,做完这三部,我们本能的把插件运行一下,运行之后,我们再次启动主程序,你会看到….



其实我们主程序里啥也没有,对吧,但是的却加载进来了,这就说明我们的插件化算是圆满实现了,那我们多来点主题看看最终的效果是什么样子的?



通过这个思路确实可以加载到图片,但是这个逻辑依旧有些不完美,不过最重要的,思考比实现更重要,对吧,后续的也就是一步步的优化了,希望大家和我一起探讨一下!

当上完整的代码

MainActivity

package com.liuguilin.pluginsample;

import android.content.Context;
import android.content.pm.PackageInfo;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.text.TextUtils;
import android.view.View;
import android.widget.AdapterView;
import android.widget.SimpleAdapter;
import android.widget.Spinner;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import dalvik.system.PathClassLoader;

public class MainActivity extends AppCompatActivity implements AdapterView.OnItemSelectedListener {

//下拉
private Spinner mSpinner;
//数据源
private SimpleAdapter mAdapter;
//插件数据
private List<PlugInBean> mList;
//加载的皮肤数据
private List<Map<String, Object>> mData = new ArrayList<>();
//资源id
private int drawId = 0;

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

initView();
//所有的插件 List<PlugInBean> allPlugIn = findPlugIn();
//加载皮肤数据
LoadSkin(allPlugIn);
}

/**
* 加载皮肤
*
* @param allPlugIn
*/
private void LoadSkin(List<PlugInBean> allPlugIn) {
//遍历
for (PlugInBean bean : allPlugIn) {
HashMap<String, Object> mMap = new HashMap<>();
mMap.put("lable", bean.getLabelNmae());
mMap.put("package", bean.getPackagNmae());
mData.add(mMap);
}
//建立Adapter并且绑定数据源
mAdapter = new SimpleAdapter(this, mData, android.R.layout.simple_list_item_1, new String[]{"lable"}, new int[]{android.R.id.text1});
//设置数据
mSpinner.setAdapter(mAdapter);
//设置监听事件
mSpinner.setOnItemSelectedListener(this);

}
/** * 获取手机里的插件 * * @return */ private List<PlugInBean> findPlugIn() { mList = new ArrayList<>(); //获取相关信息 PackageManager mPackageManager = getPackageManager(); //获取卸载/未安装的安装包信息 List<PackageInfo> mUninstallPackage = mPackageManager.getInstalledPackages(PackageManager.GET_UNINSTALLED_PACKAGES); //遍历拿到我们的信息 for (PackageInfo info : mUninstallPackage) { String pkgNmae = info.packageName; //获取shareId,根据id判断是否是我们的ID String shareUserId = info.sharedUserId; if (!TextUtils.isEmpty(shareUserId)) { //如果id相同 if (shareUserId.equals("com.liuguilin.share")) { //且排除自己的包名 if (!pkgNmae.equals(getPackageName())) { //这个就是我们的插件了 String lable = mPackageManager.getApplicationLabel(info.applicationInfo).toString(); PlugInBean bean = new PlugInBean(); bean.setLabelNmae(lable); bean.setPackagNmae(pkgNmae); mList.add(bean); } } } } return mList; }

/**
* 初始化View
*/
private void initView() {
//初始化控件
mSpinner = (Spinner) findViewById(R.id.mSpinner);

}

/**
* 选中监听事件
*
* @param adapterView
* @param view
* @param i
* @param l
*/
@Override
public void onItemSelected(AdapterView<?> adapterView, View view, int i, long l) {
PlugInBean bean = mList.get(i);
//插件的包名
String packageNmae = bean.getPackagNmae();
Context mContext = null;
try {
//无视警告 访问代码
mContext = createPackageContext(packageNmae, CONTEXT_IGNORE_SECURITY | CONTEXT_INCLUDE_CODE);
} catch (PackageManager.NameNotFoundException e) {
e.printStackTrace();
}
//获取图片
getImg(packageNmae, mContext);
//通过ID加载插件的图片
getWindow().setBackgroundDrawable(mContext.getResources().getDrawable(drawId));
//findViewById(R.id.mLinearLayout).setBackgroundDrawable(mContext.getResources().getDrawable(drawId));
}

@Override
public void onNothingSelected(AdapterView<?> adapterView) {

}

/**
* 获取插件图片 / 返回图片R文件ID / 反射R文件
*
* @param packageNmae
* @param mContext
*/
private void getImg(String packageNmae, Context mContext) {
//类加载器反射插件
PathClassLoader pathClass = new PathClassLoader(mContext.getPackageResourcePath(), ClassLoader.getSystemClassLoader());
//反射 $ 访问类加载器
try {
Class<?> forNmae = Class.forName(packageNmae + ".R$drawable", true, pathClass);
//拿到所有图片的id
Field[] files = forNmae.getDeclaredFields();
for (Field id : files) {
//过滤 / 这里的命名可以注意一下
if (id.getName().startsWith("img")) {
////这就是我们图片R下的ID
drawId = id.getInt(R.drawable.class);
//mListId.add(drawId);
}
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174

这里还有一个实体类哦,具体看Demo:

package com.liuguilin.pluginsample;

/*
*  项目名:  PlugInSample
*  包名:    com.liuguilin.pluginsample
*  文件名:   PlugInBean
*  创建者:   LGL
*  创建时间:  2016/9/17 4:18
*  描述:    插件实体类
*/

public class PlugInBean {

//包名
private String packagNmae;
//应用名
private String labelNmae;

public String getPackagNmae() {
return packagNmae;
}

public void setPackagNmae(String packagNmae) {
this.packagNmae = packagNmae;
}

public String getLabelNmae() {
return labelNmae;
}

public void setLabelNmae(String labelNmae) {
this.labelNmae = labelNmae;
}

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


主程序及插件程序:http://download.csdn.net/detail/qq_26787115/9632026

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: