First, preface
Today is the weekend again. It feels like time is going very fast. I have to write a blog again.
So today, let's look at the application of the principle of skin-changing analysis.
In a previous blog post I spoke about the basics of plugin development in Android: Knowledge of class loader. Students who have not seen can go to:
http://blog.csdn.net/jiangwei0910410003/article/details/41384667
second, the principle of introduction
There are many applications on the market today that have a skin-changing function. is to be able to provide users with some skin packs. then download, replace. And some of the skin is charged.
In fact, there is no technical difficulty for this function, but he includes a technique that is very fire at this stage: dynamic loading
All right. Since we are talking about dynamic loading . So if there are unfamiliar classmates, can go to see another blog:
http://blog.csdn.net/jiangwei0910410003/article/details/17679823
Let's take a look at one app on the market. Example of a skin-changing function: QQ space
Click on my Space + personalization + original theme + Select Download Theme
Download the theme. And then be able to replace it. Now, let's see where this theme bag is. Since the download must be stored up.
Two places to put: One is the SD card. One is the app's Data folder
Let's take a look at the application's folder ( the ADB command is configured ):
First step: Get the package name of QQ space:
Open the QQ space app and don't quit.
Then execute the command:adb shell dumpsys activity top
This command is still very useful, you can get the package name of an application at high speed
We see the package name of the QQ space: Com.qzone
The second step: Enter the QQ application folder, view the corresponding resources
We found the Theme.xml file in his shared_prefs and looked at the file to find the location of the corresponding skin:
/data/user/0/com.qzone/files/cache/qz_external_resource/theme_res/38
We go to this folder:
It is not very familiar to see the folder structure and file name in the red circle above. Yes, this is what we get after extracting a normal apk. Then we can conclude. The skin pack in QQ space is actually an apk, and then dynamically loads the APK, takes the corresponding resource and then replaces it.
third, how to design a skin-changing plug-in
Well, since the above we read the skin-changing function of QQ space, but also know its general principle, the following we have to make our own DIY theme package.
About the dynamic loading of the relevant technology is not specifically described here, see my previous mention of the two related articles of introduction.
We need to build three project here:
Host Program (main program): Resourceloader
PROJECT:RESOURCELOADERAPK1 of Theme Pack 1
Project:resourceloaderapk2 of Theme Pack 2
In the host program we need to write dynamically loaded code:
Take a look at the specific code:
Mainactivity.java
Package Com.example.resourceloader;import Java.io.file;import Java.lang.reflect.field;import Java.lang.reflect.method;import Android.annotation.suppresslint;import Android.content.context;import Android.graphics.drawable.drawable;import Android.os.bundle;import Android.util.log;import Android.view.View; Import Android.view.view.onclicklistener;import Android.widget.imageview;import android.widget.LinearLayout; Import Android.widget.textview;public class Mainactivity extends Baseactivity {/** * need to replace the theme of the control * Here are the list of three: TextView, Imageview,linearlayout */private TextView textv;private ImageView imgv;private linearlayout layout; @Overrideprotected void OnCreate (Bundle savedinstancestate) {super.oncreate (savedinstancestate); Setcontentview (r.layout.activity_ Main); Textv = (TextView) Findviewbyid (r.id.text); IMGV = (ImageView) Findviewbyid (r.id.imageview); layout = ( LinearLayout) Findviewbyid (r.id.layout); Findviewbyid (R.ID.BTN1). Setonclicklistener (New Onclicklistener () {@ overridepublic void OnClick (View arg0) {String Filesdir = Getcachedir (). GetAbsolutePath (); String FilePath = filesdir + file.separator + "apk1.apk"; LOG.I ("Loader", "FilePath:" +filepath); LOG.I ("Loader", "Isexist:" +new File (FilePath). exists ());//loadresources (FilePath);//setcontent ();// Printresourceid (); SetContent1 ();//printrfield ();}}); Findviewbyid (R.ID.BTN2). Setonclicklistener (New Onclicklistener () {@Overridepublic void OnClick (View v) {String Filesdir = Getcachedir (). GetAbsolutePath (); String FilePath = filesdir + file.separator + "apk2.apk";//loadresources (FilePath); SetContent ();});} /** * Dynamically load resources in the theme pack and replace each control */@SuppressLint ("Newapi") private void SetContent () {Try{class clazz = Classloader.loadclass ("Com.example.resourceloaderapk.UIUtil"); method = Clazz.getmethod ("gettextstring", Context.class); String str = (string) Method.invoke (null, this); Textv.settext (str); method = Clazz.getmethod ("Getimagedrawable", Context.class);D rawable drawable = (drawable) method.invoke (null, this); Imgv.setbackground (drawable); method = Clazz.getmethod ("GetLayout", Context.class); View view = (view) Method.invoke (null, this); Layout.addview (view);} catch (Exception e) {log.i ("Loader", "Error:" +log.getstacktracestring (e));}} /** * Another way to get */private void SetContent1 () {int stringid = Gettextstringid (); int drawableid = Getimgdrawableid (); int Layo Utid = Getlayoutid (); LOG.I ("Loader", "Stringid:" +stringid+ ", Drawableid:" +drawableid+ ", LayoutID:" +layoutid ");} @SuppressLint ("Newapi") private int Gettextstringid () {Try{class clazz = Classloader.loadclass (" Com.example.resourceloaderapk1.r$string "); Field field = Clazz.getfield ("app_name"), int resId = (int) field.get (null); return resId;} catch (Exception e) {log.i ("Loader", "Error:" +log.getstacktracestring (e));} return 0;} @SuppressLint ("Newapi") private int Getimgdrawableid () {Try{class clazz = Classloader.loadclass (" Com.example.resourceloaderapk1.r$drawable "); Field field = Clazz.getfield ("Ic_launcher"), int resId = (int) field.get (null); return resId;} catch (Exception e) {log.i ("Loader", "ERRor: "+log.getstacktracestring (e));} return 0;} @SuppressLint ("Newapi") private int Getlayoutid () {Try{class clazz = Classloader.loadclass (" Com.example.resourceloaderapk1.r$layout "); Field field = Clazz.getfield ("Activity_main"), int resId = (int) field.get (null); return resId;} catch (Exception e) {log.i ("Loader", "Error:" +log.getstacktracestring (e));} return 0;} @SuppressLint ("Newapi") private void Printresourceid () {Try{class clazz = Classloader.loadclass (" Com.example.resourceloaderapk.UIUtil "); method = Clazz.getmethod ("Gettextstringid", null); Object obj = Method.invoke (null, NULL); LOG.I ("Loader", "Stringid:" +obj); LOG.I ("Loader", "NewId:" +r.string.app_name); method = Clazz.getmethod ("Getimagedrawableid", null); obj = Method.invoke (null, NULL); LOG.I ("Loader", "Drawableid:" +obj); LOG.I ("Loader", "NewId:" +r.drawable.ic_launcher); method = Clazz.getmethod ("Getlayoutid", null); obj = Method.invoke ( NULL, NULL); LOG.I ("Loader", "LayoutID:" +obj); LOG.I ("Loader", "NewId:" +r.layout.activity_main); catch (excePtion e) {log.i ("Loader", "Error:" +log.getstacktracestring (e));}} private void Printrfield () {Class clazz = R.id.class; field[] fields = Clazz.getfields (); for (Field field:fields) {log.i ("Loader", "Fields:" +field);} Class Clazzs = R.layout.class; field[] Fieldss = Clazzs.getfields (); for (Field Field:fieldss) {log.i ("Loader", "FIELDSS:" +field);}}}
There is no big difficulty with the code here, which is that
we use the Dexclassloader class to load the APK package for each theme, and then invoke the methods in the APK package using the reflection method to get the resources.
Take a look at the topic Package Project code:
Uiutil.java
Package Com.example.resourceloaderapk;import Android.content.context;import android.graphics.drawable.Drawable; Import Android.view.layoutinflater;import Android.view.view;import Com.example.resourceloaderapk1.r;public class Uiutil {public static String gettextstring (Context ctx) {return ctx.getresources (). getString (R.string.app_name);} public static drawable getimagedrawable (Context ctx) {return ctx.getresources (). getdrawable (R.drawable.ic_launcher);} public static View GetLayout (Context ctx) {return Layoutinflater.from (CTX). Inflate (r.layout.activity_main, null);} public static int Gettextstringid () {return r.string.app_name;} public static int Getimagedrawableid () {return r.drawable.ic_launcher;} public static int Getlayoutid () {return r.layout.activity_main;}}
This class is to provide external access to resources, we are in the host program that is reflected in this method to obtain resources, in this way we provide two ways to obtain resources: one is to directly return the contents of the resource. The other is to return the ID of a resource.
Project on topic Package 2 is not covered here. The code is the same, just the resources are different.
We execute two theme packs. Get two apk
resourceloaderapk1.apk
resourceloaderapk2.apk
At this point we use the adb push command. Place the two apk in the host program's cache folder.
Warm tips:
You cannot place the apk that needs to be loaded outside the sandbox folder of the non-host program. Otherwise it will load the failure and throw an exception. The Sandbox folder concept of the program is actually very good to understand: is the/data/data/xxx.xxx/folder, that is, this folder is unique to this program, other apps that do not share permissions are not accessible (of course, in addition to root privileges), This is actually a very good understanding of why this is done, Google is also for security. The apk/dex/jar that he needs to be loaded should be protected.
Of course, it does not have to be placed in the cache folder, only if the sandbox folder can be. Creating a new folder is also possible. Only the cache folder is generally used.
Project Address: http://download.csdn.net/detail/jiangwei0910410003/9008423
At this point we execute the host program:
Two btn, can load different subject content, but the question comes. After clicking Find no effect, catch exception, we print log to see:
ADB logcat-s Loader
He said he could not find the resource exception, let's analyze it.
In theme 1project, we are invoking the resources in theme 1apk r.string.xxx
But we know that the time to get the resources is to use the resource class to get, for a program a context will only hold a resource object, but when we load the APK, the theme apk does not get the corresponding context. Because dynamic loading does not want to perform a program properly, each program has a global context variable. But it is not in the words that are loaded in.
Then someone said: In the code, we use reflection to get it when the host's context variable is not passed past it?
Right. No matter what it looks like. But here is actually the question: how the host context can load the resources in the plugin apk, we know that an app's project resource file will be insinuate to the R file, and the package name of the R file is the package name of the application, a package name generally corresponding to a context. So now that we pass the context of the host and the package name of the host, that is, the R file that hosts project, we cannot find the corresponding resource.
In fact, the problem we have to solve is to add the plugin apk to the host apk.
This is a way to use the mechanism of reflection:
By calling the Addassetpath method in Assetmanager. We are able to load the resources of an apk into the resource. Since Addassetpath is a hidden API, we cannot invoke it directly, so we can only pass reflection, and the following is its declaration. By gazing we can see that the path passed can be a zip file or a resource folder, and the APK is a zip. So just pass the APK path to it. The resource is loaded into Assetmanager, and then a new resources object is created through Assetmanager, which is the resource in the APK that we can use.
Let's look at the code:
protected void Loadresources (String dexpath) { try { Assetmanager Assetmanager = AssetManager.class.newInstance (); Method Addassetpath = Assetmanager.getclass (). GetMethod ("Addassetpath", string.class); Addassetpath.invoke (Assetmanager, dexpath); Massetmanager = Assetmanager; } catch (Exception e) { e.printstacktrace (); } Resources superres = Super.getresources (); Superres.getdisplaymetrics (); Superres.getconfiguration (); Mresources = new Resources (Massetmanager, Superres.getdisplaymetrics (), superres.getconfiguration ()); Mtheme = Mresources.newtheme (); Mtheme.setto (Super.gettheme ()); }
A parameter is the path to a package that needs to be loaded into the resource.
Of course, there are three ways we need to rewrite the context:
@Override public Assetmanager getassets () { return massetmanager = = null?Super.getassets (): Massetmanager; } @Override public Resources getresources () { return mresources = = null? Super.getresources (): mresources; } @Override public Theme gettheme () { return mtheme = = null? Super.gettheme (): Mtheme; }
The three overriding methods are to let the system get the variables we loaded into the APK package to
Here, let's change the code: Add a Baseactivity class to the host project:
Baseactivity.java
Package Com.example.resourceloader;import Java.io.file;import Java.lang.reflect.method;import android.app.Activity ; Import Android.content.res.assetmanager;import Android.content.res.resources;import Android.content.res.resources.theme;import Android.os.bundle;import Dalvik.system.dexclassloader;public Class Baseactivity extends activity{protected assetmanager massetmanager;//resource Manager protected Resources mresources;//Resource Protected Theme mtheme;//theme @Overrideprotected void OnCreate (Bundle savedinstancestate) {super.oncreate ( Savedinstancestate);} protected void Loadresources (String dexpath) {try {Assetmanager Assetmanager = Assetmanager.class. Newinstance (); Method Addassetpath = Assetmanager.getclass (). GetMethod ("Addassetpath", String.class); Addassetpath.invoke (Assetmanager, Dexpath); Massetmanager = Assetmanager; } catch (Exception e) {e.printstacktrace (); } Resources superres = Super.gEtresources (); Superres.getdisplaymetrics (); Superres.getconfiguration (); Mresources = new Resources (Massetmanager, Superres.getdisplaymetrics (), superres.getconfiguration ()); Mtheme = Mresources.newtheme (); Mtheme.setto (Super.gettheme ()); } @Override Public Assetmanager getassets () {return Massetmanager = = null? Super.getassets (): Massetmanager; } @Override Public Resources getresources () {return mresources = = null?Super.getresources (): mresources; } @Override Public Theme Gettheme () {return mtheme = = null? Super.gettheme (): Mtheme; } }
Call the LoadResource method in Mainactivity:
At this point we are executing the host program:
Click on Topic 1: We found that the text became: resourceloaderapk; because the pictures here are all used robots so it doesn't seem to change, look at the bottom of the LinearLayout load the layout XML content in the theme package.
Click on theme 2: The effect is the same as the content is only the theme package 2apk.
All right.
Here we have developed our own skin-changing function perfectly. But some students may feel that this is the function of skin changing. Didn't see the effect? My example is not completely full of developing a skin-changing project.
Just to introduce the principle. Just really change the skin is not difficult, we need to solve some problems:
1, for the need to replace the theme of the control needs to be unified definition.
2, for each theme package of the external interface in project to unify (or to conform to certain specifications), for example, in this sample must have a theme package:
Com.example.resourceloaderapk.UIUtil class. And there must be three methods in this class: Gettextstring. Getimagedrawable,getlayout
So this is a specification, of course, I am not very good at the standard design here. The correct approach is to provide an interface. Then each theme package project must implement this interface, and then the topic Package Project and host project all include this interface. This will be very flexible.
3, the general theme package apk is downloaded from the Internet, so we need to set a few default theme packages in the local. Assuming that the theme pack downloaded from the Web fails, let's load the default theme.
This will not occur regardless of the abnormal situation.
Iv. Summary of issues
In fact this article we see above we actually solve a problem, is how to load the theme package APK resources. In fact, there is another idea that we are going to have to pack all the resources we need (in a format that doesn't matter what the package is). After downloading it from the Internet, unzip the file. Each resource file is read in project through a stream. In fact, such a way is feasible. But there's a very big problem with efficiency (I haven't tried it anyway). So this is a very convenient and efficient way to do this.
v. Practical uses
The technology that this article is talking about today is basically the most important thing on the market:
1, online replacement theme (skin), language packs, etc.
2. Reduce the package size of the main apk. Package A resource that is not very important as an apk and put it on the server.
Vi. Summary
This article mainly introduces the principle of applying skin-changing. The core technology is: How to load the resources in the plugin apk. may also be technical commentary on the use of plug-ins in Android: free to install the implementation of the program, in production ...
To interrupt a message
$*********************************************************************************************$
Bo Main recommendation:
The wind is easy to water cold, "naïve" a go no more. How to find the innocence of the happiness.
Small make-up recommended app: innocence without harmonic
Download method: Pea pod. App Bao. 360 Mobile phone assistant. Baidu mobile phone assistant. Android. 91 Market Search: innocence without harmonic
Follow us on: View Details
$*********************************************************************************************$
Plug-in development in Android----application of skin-changing principle analysis