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

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.初始化

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

当然,我这刚应用就一个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;
    }

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

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

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);

    }

我们通过刚才的数据集便可以把我们拿到的数据给直接显示出来了,这里其实可以判断一下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) {

    }

这里的代码就比较有意思,一定要仔细看,我们首先拿到选中的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();
        }
    }

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

android:sharedUserId="com.liuguilin.share"

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

三PlugInApk插件

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

工程里要做的事情就三件

  • 1.添加sharedUserId
android:sharedUserId="com.liuguilin.share"
  • 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();
        }
    }
}

这里还有一个实体类哦,具体看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;
    }

}

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

有兴趣的可以加群:555974449

时间: 2024-10-29 00:01:39

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

js css 仿QQ网站换肤效果代码

<!doctype html public "-//w3c//dtd xhtml 1.0 transitional//en" "http://www.w3.org/tr/xhtml1/dtd/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="content-

【我的Android进阶之旅】Android插件化开发学习资料

1.目前开源的插件开发框架大致有哪些? 1. 任玉刚 的 dynamic-load-apk Github 地址:https://github.com/singwhatiwanna/dynamic-load-apk 2.mmyydd 的 Direct-Load-apk Github 地址:https://github.com/mmyydd/Direct-Load-apk 3.limpoxe 的 Android-Plugin-Framework Github 地址:https://github.co

Android 插件化 动态升级

不少朋友私信问到这个问题,这里简单介绍下我的了解 1.作用 大多数朋友开始接触这个问题是因为 App 爆棚了,方法数超过了一个 Dex 最大方法数 65535 的上限,因而便有了插件化的概念,将一个 App 划分为多个插件(Apk 或相关格式) 常用的其他解决方法还包括:Google Multidex,用 H5 代替部分逻辑,删无用代码,买付费版的 Proguard 当插件化作用不止于此,还包括:(1) 模块解耦,(2) 动态升级,(3) 高效并行开发(编译速度更快) (4) 按需加载,内存占用

Android插件化之资源动态加载_Android

Android插件化之资源动态加载 一.概述 Android插件化的一个重要问题就是插件资源访问问题,先列出会面对的问题 1.如何加载插件资源 2.如何处理插件资源与宿主资源的处突:插件化资源问题要做到的效果是,如果我们要获取的资源在插件中找得到,则加载优先加载插件的,如果找不到,则到宿主资源中找.这样能做到动态更新的效果. 3.如何确保插件和宿主使用到的是被修改过的资源. 二.原理分析 在做一件事之前必须先弄清楚原理,所以,这里先要弄清楚Android的资源体系原理. 1.资源链   Cont

android插件化之路

概论  插件式开发通俗的讲就是把一个很大的app分成n多个比较小的app,其中有一个app是主app.基本上可以理解为让一个apk不安装也可以被运行.只不过这个运行是有很多限制的运行,所以才叫插件. 其实在目前淘宝.百度.腾讯等都有成熟的动态加载框架,包括apkplug,但是它们都是不开源的,你只需要按照他们的文档操作即可. 做插件化有什么好处么? 做插件化真正的目的:是为了去适应并行开发,是为了解耦各个模块,是为了避免模块之间的交叉依赖,是为了加快编译速度,从而提高并行开发效率. 插件化,组件

Android使用Item Swipemenulistview实现仿QQ侧滑删除功能

大家都用过QQ,肯定有人好奇QQ滑动删除Item的效果是怎样实现的,其实我们使用Swipemenulistview就可以简单的实现.先看看我们项目中的效果: 使用的时候可以把Swipemenulistview作为一个library,也可以把Swipemenulistview的源码拷贝到我们的项目中来,使用步骤大致可以分为三步:1.在布局中配置:2.在Java代码中初始化配置:3.按钮点击事件的处理 1.在布局中配置 xml布局文件中只需要简单使用这个自定义的ListView就行了,需要注意的是必

Android插件化之资源动态加载

Android插件化之资源动态加载 一.概述 Android插件化的一个重要问题就是插件资源访问问题,先列出会面对的问题 1.如何加载插件资源 2.如何处理插件资源与宿主资源的处突:插件化资源问题要做到的效果是,如果我们要获取的资源在插件中找得到,则加载优先加载插件的,如果找不到,则到宿主资源中找.这样能做到动态更新的效果. 3.如何确保插件和宿主使用到的是被修改过的资源. 二.原理分析 在做一件事之前必须先弄清楚原理,所以,这里先要弄清楚Android的资源体系原理. 1.资源链 Contex

js实现仿QQ秀换装效果的方法_javascript技巧

本文实例讲述了js实现仿QQ秀换装效果的方法.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="www.w3.org/1999/xhtml"> <he

基于jQuery实现仿百度首页换肤背景图片切换代码_jquery

不多说了,先给大家展示效果图,本文支持源码下载哦,需要的朋友可以直接下载使用,代码很简单哦- 在线预览    源码下载 html代码: <a href="#">换肤</a> <div class="heitiao"></div> <div class="con"><img src="images/content.png" /></div> &l