构建一个插件化了解几个关键点,就可以实现一个可以架构一个支持比较简单粗糙的插件化apk了,能拿到插件apk classloader的 就可以实现加载任意插件apk的类
知道Resources
如何根据apk生成自然就可以操作任意布局,其他的就是使用类似包装模式一样的架构完成生命周期的代理.
动态加载插件apk dex
File dexOutFileDir = context.getDir("dex", Context.MODE_PRIVATE);
String pluginApkPath=new File(context.getDir("plugin"),"my.apk").getAbsolutePath();
dexClassLoader = new DexClassLoader(pluginApkPath, dexOutFileDir .getAbsolutePath() , null, context.getClassLoader());
//拿到了classloader就可以加载任意类了
动态加载资源
try {
AssetManager assetManager = AssetManager.class.newInstance();
Method addAssetPath = AssetManager.class.getMethod("addAssetPath", String.class);
addAssetPath.invoke(assetManager, path);
resources = new Resources(assetManager, context.getResources().getDisplayMetrics(), context.getResources().getConfiguration());
} catch (Exception e) {
e.printStackTrace();
Toast.makeText(context, "加载失败", Toast.LENGTH_SHORT).show();
}
演示架构代理插件activity
//主activiy声明,但是不能是单例,这个不需要多说,其次在onCreate里面加载插件apk。
public class MyProxyActivity extends Activity {
private String className;
IPluginProxyActivity ipluginProxyActivity;
// com.dongnao.alvin.taopiaopiao.MainActivity
@Override
public void onCreate(@Nullable Bundle savedInstanceState ) {
super.onCreate(savedInstanceState );
className = getIntent().getStringExtra("className");
// class
try {
Class activityClass = getClassLoader().loadClass(className);
Constructor constructor = activityClass.getConstructor(new Class[]{});
Object instance= constructor.newInstance(new Object[]{});
// 可以
ipluginProxyActivity = (IPluginProxyActivity) instance;
ipluginProxyActivity.attach(this);
Bundle bundle = new Bundle();
ipluginProxyActivity.onCreate(bundle);
} catch (Exception e) {
Toast.makeText(this, "找不到class:"+className, Toast.LENGTH_SHORT).show();
e.printStackTrace();
}
}
@Override
public void startActivity(Intent intent) {
String className1=intent.getStringExtra("className");
Intent intent1 = new Intent(this, MyProxyActivity.class);
intent1.putExtra("className", className1);
super.startActivity(intent1);
}
@Override
public ComponentName startService(Intent service) {
String serviceName = service.getStringExtra("serviceName");
Intent intent1 = new Intent(this, ProxyService.class);
intent1.putExtra("serviceName", serviceName);
return super.startService(intent1);
}
@Override
public ClassLoader getClassLoader() {
return PluginManager.getInstance().getDexClassLoader();//可以定义pluginId,方便扩展
}
@Override
public Resources getResources() {
return PluginManager.getInstance().getResources();//可以定义pluginId,方便扩展
}
@Override
protected void onStart() {
super.onStart();
if(ipluginProxyActivity ==null){
Toast.makeText(this, "获取payInterActivity fail", Toast.LENGTH_SHORT).show();
return;
}
ipluginProxyActivity.onStart();
}
@Override
protected void onDestroy() {
super.onDestroy();
ipluginProxyActivity.onDestroy();
}
@Override
protected void onPause() {
super.onPause();
ipluginProxyActivity.onPause();
}
}
根据资源id查资源名或反查
public int getIdentifier(int resId) {
if (isDefaultSkin) {
return resId;
}
//在皮肤包中不一定就是 当前程序的 id
//获取对应id 在当前的名称 colorPrimary
//R.drawable.ic_launcher
String resName = mAppResources.getResourceEntryName(resId);//ic_launcher
String resType = mAppResources.getResourceTypeName(resId);//drawable
int skinId = mSkinResources.getIdentifier(resName, resType, mSkinPkgName);
return skinId;
}
public int getColor(int resId) {
if (isDefaultSkin) {
return mAppResources.getColor(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColor(resId);
}
return mSkinResources.getColor(skinId);
}
public ColorStateList getColorStateList(int resId) {
if (isDefaultSkin) {
return mAppResources.getColorStateList(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getColorStateList(resId);
}
return mSkinResources.getColorStateList(skinId);
}
public Drawable getDrawable(int resId) {
//如果有皮肤 isDefaultSkin false 没有就是true
if (isDefaultSkin) {
return mAppResources.getDrawable(resId);
}
int skinId = getIdentifier(resId);
if (skinId == 0) {
return mAppResources.getDrawable(resId);
}
return mSkinResources.getDrawable(skinId);
}
hook拦截activity启动
//可以追踪startAcitivty最终调用哪里
Class> activityManagerNativeClass = Class.forName("android.app.ActivityManagerNative");
Field gDefaultField = activityManagerNativeClass.getDeclaredField("gDefault");
gDefaultField.setAccessible(true);
Object gDefault = gDefaultField.get(null);
// gDefault是一个 android.util.Singleton对象; 我们取出这个单例里面的字段
Class> singleton = Class.forName("android.util.Singleton");
Field mInstanceField = singleton.getDeclaredField("mInstance");
mInstanceField.setAccessible(true);
// ActivityManagerNative 的gDefault对象里面原始的 IActivityManager对象
Object rawIActivityManager = mInstanceField.get(gDefault);
// 创建一个这个对象的代理对象, 然后替换这个字段, 让我们的代理对象帮忙干活
Class> iActivityManagerInterface = Class.forName("android.app.IActivityManager")
Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader
new Class>[] { iActivityManagerInterface }, new IActivityManagerHandler(
mInstanceField.set(gDefault, proxy);
反射实现静态recervicer变成动态加载
//反射的代码 根据 如下2个文件源码分析总结
//\frameworks\base\services\core\java\com\android\server\pm\PackageManagerService.java
//\frameworks\base\core\java\android\content\pm\PackageParser.java
try {
Class packageParserClass = Class.forName("android.content.pm.PackageParser");
Method parsePackageMethod = packageParserClass.getDeclaredMethod("parsePackage", File.class, int.class);
Object packageParser = packageParserClass.newInstance();
Object packageObj= parsePackageMethod.invoke(packageParser, new File(path), PackageManager.GET_ACTIVITIES);
Field receiverField=packageObj.getClass().getDeclaredField("receivers");
List receivers = (List) receiverField.get(packageObj);
Class> componentClass = Class.forName("android.content.pm.PackageParser$Component");
Field intentsField = componentClass.getDeclaredField("intents");
// 调用generateActivityInfo 方法, 把PackageParser.Activity 转换成
Class> packageParser$ActivityClass = Class.forName("android.content.pm.PackageParser$Activity");
// generateActivityInfo方法
Class> packageUserStateClass = Class.forName("android.content.pm.PackageUserState");
Object defaltUserState= packageUserStateClass.newInstance();
Method generateReceiverInfo = packageParserClass.getDeclaredMethod("generateActivityInfo",
packageParser$ActivityClass, int.class, packageUserStateClass, int.class);
Class> userHandler = Class.forName("android.os.UserHandle");
Method getCallingUserIdMethod = userHandler.getDeclaredMethod("getCallingUserId");
int userId = (int) getCallingUserIdMethod.invoke(null);
for (Object activity : receivers) {
ActivityInfo info= (ActivityInfo) generateReceiverInfo.invoke(packageParser, activity,0, defaltUserState, userId);
BroadcastReceiver broadcastReceiver= (BroadcastReceiver) dexClassLoader.loadClass(info.name).newInstance();
List extends IntentFilter> intents= (List extends IntentFilter>) intentsField.get(activity);
for (IntentFilter intentFilter : intents) {
context.registerReceiver(broadcastReceiver, intentFilter);
}
}
//generateActivityInfo
} catch (Exception e) {
e.printStackTrace();
}
定义接口实现类加载后直接强转接口
支持拦截的代理接口
//Proxy 只支持接口,因此没有定义接口的没戏
public interface IActivityManager extends IInterface {
public int startActivity(IApplicationThread caller,
Intent intent, String resolvedType, Uri[] grantedUriPermissions,
int grantedMode, IBinder resultTo, String resultWho, int requestCode,
boolean onlyIfNeeded, boolean debug) throws RemoteException;
public WaitResult startActivityAndWait(IApplicationThread caller,
Intent intent, String resolvedType, Uri[] grantedUriPermissions,
int grantedMode, IBinder resultTo, String resultWho, int requestCode,
boolean onlyIfNeeded, boolean debug) throws RemoteException;
public int startActivityWithConfig(IApplicationThread caller,
Intent intent, String resolvedType, Uri[] grantedUriPermissions,
int grantedMode, IBinder resultTo, String resultWho, int requestCode,
boolean onlyIfNeeded, boolean debug, Configuration newConfig) throws RemoteException;
public int startActivityIntentSender(IApplicationThread caller,
IntentSender intent, Intent fillInIntent, String resolvedType,
IBinder resultTo, String resultWho, int requestCode,
int flagsMask, int flagsValues) throws RemoteException;
public boolean startNextMatchingActivity(IBinder callingActivity,
Intent intent) throws RemoteException;
public boolean finishActivity(IBinder token, int code, Intent data)
throws RemoteException;
public void finishSubActivity(IBinder token, String resultWho, int requestCode) throws RemoteException;
public boolean willActivityBeVisible(IBinder token) throws RemoteException;
public Intent registerReceiver(IApplicationThread caller,
IIntentReceiver receiver, IntentFilter filter,
String requiredPermission) throws RemoteException;
public void unregisterReceiver(IIntentReceiver receiver) throws RemoteException;
public static final int BROADCAST_SUCCESS = 0;
public static final int BROADCAST_STICKY_CANT_HAVE_PERMISSION = -1;
public int broadcastIntent(IApplicationThread caller, Intent intent,
String resolvedType, IIntentReceiver resultTo, int resultCode,
String resultData, Bundle map, String requiredPermission,
boolean serialized, boolean sticky) throws RemoteException;
public void unbroadcastIntent(IApplicationThread caller, Intent intent) throws RemoteException;
/* oneway */
public void finishReceiver(IBinder who, int resultCode, String resultData, Bundle map, boolean abortBroadcast) throws RemoteException;
public void attachApplication(IApplicationThread app) throws RemoteException;
/* oneway */
public void activityIdle(IBinder token, Configuration config) throws RemoteException;
public void activityPaused(IBinder token, Bundle state) throws RemoteException;
/* oneway */
public void activityStopped(IBinder token,
Bitmap thumbnail, CharSequence description) throws RemoteException;
/* oneway */
public void activityDestroyed(IBinder token) throws RemoteException;
public String getCallingPackage(IBinder token) throws RemoteException;
public ComponentName getCallingActivity(IBinder token) throws RemoteException;
public List getTasks(int maxNum, int flags,
IThumbnailReceiver receiver) throws RemoteException;
public List getRecentTasks(int maxNum,
int flags) throws RemoteException;
public List getServices(int maxNum, int flags) throws RemoteException;
public List getProcessesInErrorState()
throws RemoteException;
public void moveTaskToFront(int task) throws RemoteException;
public void moveTaskToBack(int task) throws RemoteException;
public boolean moveActivityTaskToBack(IBinder token, boolean nonRoot) throws RemoteException;
public void moveTaskBackwards(int task) throws RemoteException;
public int getTaskForActivity(IBinder token, boolean onlyRoot) throws RemoteException;
public void finishOtherInstances(IBinder token, ComponentName className) throws RemoteException;
/* oneway */
public void reportThumbnail(IBinder token,
Bitmap thumbnail, CharSequence description) throws RemoteException;
public ContentProviderHolder getContentProvider(IApplicationThread caller,
String name) throws RemoteException;
public void removeContentProvider(IApplicationThread caller,
String name) throws RemoteException;
public void publishContentProviders(IApplicationThread caller,
List providers) throws RemoteException;
public PendingIntent getRunningServiceControlPanel(ComponentName service)
throws RemoteException;
public ComponentName startService(IApplicationThread caller, Intent service,
String resolvedType) throws RemoteException;
public int stopService(IApplicationThread caller, Intent service,
String resolvedType) throws RemoteException;
public boolean stopServiceToken(ComponentName className, IBinder token,
int startId) throws RemoteException;
public void setServiceForeground(ComponentName className, IBinder token,
int id, Notification notification, boolean keepNotification) throws RemoteException;
public int bindService(IApplicationThread caller, IBinder token,
Intent service, String resolvedType,
IServiceConnection connection, int flags) throws RemoteException;
public boolean unbindService(IServiceConnection connection) throws RemoteException;
public void publishService(IBinder token,
Intent intent, IBinder service) throws RemoteException;
public void unbindFinished(IBinder token, Intent service,
boolean doRebind) throws RemoteException;
/* oneway */
public void serviceDoneExecuting(IBinder token, int type, int startId,
int res) throws RemoteException;
public IBinder peekService(Intent service, String resolvedType) throws RemoteException;
public boolean bindBackupAgent(ApplicationInfo appInfo, int backupRestoreMode)
throws RemoteException;
public void backupAgentCreated(String packageName, IBinder agent) throws RemoteException;
public void unbindBackupAgent(ApplicationInfo appInfo) throws RemoteException;
public void killApplicationProcess(String processName, int uid) throws RemoteException;
public boolean startInstrumentation(ComponentName className, String profileFile,
int flags, Bundle arguments, IInstrumentationWatcher watcher)
throws RemoteException;
public void finishInstrumentation(IApplicationThread target,
int resultCode, Bundle results) throws RemoteException;
public Configuration getConfiguration() throws RemoteException;
public void updateConfiguration(Configuration values) throws RemoteException;
public void setRequestedOrientation(IBinder token,
int requestedOrientation) throws RemoteException;
public int getRequestedOrientation(IBinder token) throws RemoteException;
public ComponentName getActivityClassForToken(IBinder token) throws RemoteException;
public String getPackageForToken(IBinder token) throws RemoteException;
public static final int INTENT_SENDER_BROADCAST = 1;
public static final int INTENT_SENDER_ACTIVITY = 2;
public static final int INTENT_SENDER_ACTIVITY_RESULT = 3;
public static final int INTENT_SENDER_SERVICE = 4;
public IIntentSender getIntentSender(int type,
String packageName, IBinder token, String resultWho,
int requestCode, Intent intent, String resolvedType, int flags) throws RemoteException;
public void cancelIntentSender(IIntentSender sender) throws RemoteException;
public boolean clearApplicationUserData(final String packageName,
final IPackageDataObserver observer) throws RemoteException;
public String getPackageForIntentSender(IIntentSender sender) throws RemoteException;
public void setProcessLimit(int max) throws RemoteException;
public int getProcessLimit() throws RemoteException;
public void setProcessForeground(IBinder token, int pid, boolean isForeground) throws RemoteException;
public int checkPermission(String permission, int pid, int uid)
throws RemoteException;
public int checkUriPermission(Uri uri, int pid, int uid, int mode)
throws RemoteException;
public void grantUriPermission(IApplicationThread caller, String targetPkg,
Uri uri, int mode) throws RemoteException;
public void revokeUriPermission(IApplicationThread caller, Uri uri,
int mode) throws RemoteException;
public void showWaitingForDebugger(IApplicationThread who, boolean waiting)
throws RemoteException;
public void getMemoryInfo(ActivityManager.MemoryInfo outInfo) throws RemoteException;
public void killBackgroundProcesses(final String packageName) throws RemoteException;
public void forceStopPackage(final String packageName) throws RemoteException;
// Note: probably don't want to allow applications access to these.
public void goingToSleep() throws RemoteException;
public void wakingUp() throws RemoteException;
public void unhandledBack() throws RemoteException;
public ParcelFileDescriptor openContentUri(Uri uri) throws RemoteException;
public void setDebugApp(
String packageName, boolean waitForDebugger, boolean persistent)
throws RemoteException;
public void setAlwaysFinish(boolean enabled) throws RemoteException;
public void setActivityController(IActivityController watcher)
throws RemoteException;
public void enterSafeMode() throws RemoteException;
public void noteWakeupAlarm(IIntentSender sender) throws RemoteException;
public boolean killPids(int[] pids, String reason) throws RemoteException;
//省略了很多............................
我利用这个技术做了什么?
我的机器人插件功能就是这样实现的,各位想开发QQ机器人插件的朋友欢迎加入哦!
插件sdk支持禁言,踢人
插件demo地址
https://gitee.com/51bwn/RobotPluginSDK
插件机器人没有ui界面哈
DexClassLoader dexClassLoader = new DexClassLoader(currentApk.getAbsolutePath(), getDefaultPluginDexPath(context).getAbsolutePath(), null, context.getClassLoader());
try {
Class> aClass = dexClassLoader.loadClass(Cns.PLUGIN_MAIN_ENTRY_FILE);
PluginInterface pluginInterface = (PluginInterface) aClass.newInstance();
pluginInterface.onCreate(context);
QueryPluginModel model = new QueryPluginModel();
model.setPluginInterface(pluginInterface);
model.setOfficial(pluginInterface.getPackageName().startsWith(BuildConfig.APPLICATION_ID));
model.setPath(currentApk.getAbsolutePath());
if (onCreateNotify != null) {
onCreateNotify.onEach(pluginInterface);
}
list.add(model);
if (BuildConfig.DEBUG) {
Log.w(TAG, "加载插件" + currentApk.getAbsolutePath() + "成功");
}
} catch (ClassNotFoundException e) {
e.printStackTrace();
Log.e(TAG, "加载" + currentApk.getName() + "文件插件失败,非机器人插件或插件apk被混淆,导致找不到" + Cns.PLUGIN_MAIN_ENTRY_FILE + "类");
} catch (IllegalAccessException e) {
e.printStackTrace();
Log.e(TAG, "加载插件失败,无法创建对象 IllegalAccessException 可能没有访问权限");
} catch (InstantiationException e) {
e.printStackTrace();
Log.e(TAG, "加载插件失败,无法创建对象InstantiationException ");
} catch (Exception e) {
e.printStackTrace();
Log.e(TAG, "加载插件失败,未知异常 " + e.getMessage());
} catch (Error e) {
e.printStackTrace();
Log.e(TAG, "加载插件失败,未知错误 " + e.getMessage());
}
上面的方式是固定的class规定,实际上也可以读取资产目录,然后只读取资产目录文件里面规定的类型,
//PluginInterface 文件
/**
* Created by qssq on 2018/1/21 [email protected]
*/
public interface PluginInterface {
/**
* 插件作者
* @return
*/
public String getAuthorName();
/**
* 当机器人插件被创建加载后,会回调配置api控制类给插件,插件可以自己存储为成员变量,在适当的逻辑中进行操作,此api接口回调支持踢人禁言,发消息
* @param instance
*/
public void onReceiveControlApi(PluginCtronolInterface instance);
/**
* 插件的版本号
*
* @return
*/
public int getVersionCode();
public String getBuildTime();
/**
*
* @return 插件的版本名
*/
public String getVersionName();
/**
*
* @return 插件的包名(不要和作者的包名一样!否则)为了用户的安全,恶意流氓插件我会强制处理,对于冒充官方包名的插件校验签名。所以不要用cn.
* qssq666开头的包名。
*/
public String getPackageName();
/**
* 此机器人插件的描述信息
*
* @return 返回此插件的描述信息
*/
public String getDescript();
/**
* 插件的名字,这里当然是中文名了,给用户看的名字嘛
* @return
*/
public String getPluginName();
// boolean isOfficial();
/**
* 本插件是否被禁用了,这和卸载不同,卸载的话文件都删除了,但是是否禁用是由用户自己实现的
*
* @return
*/
boolean isDisable();
/**
* ,在修改机器人插件配置右边的选项卡会调用setDisable
* 不写无法禁用,影响体验,导致的后果就是被用户卸载咯!具体怎么实现禁用请参考demo源码
*
* @param disable
*/
void setDisable(boolean disable);
/**
* 插件被加载进来后会把宿主的上下文传递过来,不过我感觉有风险啊,有了这东西什么都可以反射了。
*
* @param context
*/
void onCreate(Context context);
/**
* 这里是处理逻辑的关键,首先自身命令和管理员命令逻辑会先走,之后走这里提供消息判断是否处理了此消息,如果处理了就返回true,
*
* @param item
* @return 插件处理了本消息就返回true,那么其他插件,或者机器人将不会被处理,防止冲突。
*/
boolean onReceiveMsgIsNeedIntercept(MsgItem item);
/**
* 这个方法暂时没用,
*/
void onDestory();
/**
* 插件刚加载后会提供一个机器人配置查询接口,比如已经禁用了群聊功能了,你这个机器人插件不听话还进行处理,那么下场也是被卸载了.
*
* @param robotConfigInterface
*/
void onReceiveRobotConfig(RobotConfigInterface robotConfigInterface);
}
分析手机QQ插件化原理
//com.tencent.mobileqq.pluginsdk.PluginProxyActivity
private void b() throws Exception {
PackageInfo packageInfoWithException;
PackageInfo packageInfo = (PackageInfo) PluginStatic.d.get(this.g);
if (packageInfo == null) {
if (DebugHelper.sDebug) {
DebugHelper.log("PluginProxyActivity.initPlugin start getPackageInfo");
}
try {
packageInfoWithException = ApkFileParser.getPackageInfoWithException(this, this.g, 129);
if (DebugHelper.sDebug) {
DebugHelper.log("PluginProxyActivity.initPlugin end getPackageInfo, " + packageInfoWithException);
}
if (packageInfoWithException == null) {
throw new a("Get Package Info Failed!");
}
PluginStatic.d.put(this.g, packageInfoWithException);
} catch (Throwable th) {
a aVar = new a("getPackageInfoWithException", th);
}
} else {
packageInfoWithException = packageInfo;
}
if (this.j == null || this.j.length() == 0) {
if (packageInfoWithException.activities == null || packageInfoWithException.activities.length == 0) {
throw new b();
}
this.j = packageInfoWithException.activities[0].name;
}
ClassLoader a = PluginStatic.a((Context) this, this.i, this.g);
getIntent().setExtrasClassLoader(a);
if (DebugHelper.sDebug) {
DebugHelper.log("PluginProxyActivity.initPlugin start loadClass");
}
this.d = a.loadClass(this.j);
if (DebugHelper.sDebug) {
DebugHelper.log("PluginProxyActivity.initPlugin start loadClass");
}
this.mPluginActivity = (IPluginActivity) this.d.newInstance();
this.mPluginActivity.IInit(this.i, this.g, this, a, packageInfoWithException, this.e, this.f);
Intent intent = new Intent(getIntent());
Bundle bundleExtra = intent.getBundleExtra(INNER_INTENT_EXTRAS);
if (bundleExtra != null) {
intent.putExtras(bundleExtra);
intent.removeExtra(INNER_INTENT_EXTRAS);
}
this.mPluginActivity.ISetIntent(intent);
}
分析com.tencent.mobileqq.pluginsdk.PluginStatic
public abstract class PluginStatic {
public static final String PARAM_CLASS_STATISTICS_UPLOADER = "clsUploader";
public static final String PARAM_CLEAR_TOP = "cleartop";
public static final String PARAM_EXTRA_INFO = "pluginsdk_extraInfo";
public static final String PARAM_LAUNCH_ACTIVITY = "pluginsdk_launchActivity";
public static final String PARAM_LAUNCH_SERVICE = "pluginsdk_launchService";
public static final String PARAM_PATH = "pluginsdk_pluginpath";
public static final String PARAM_PLUGIN_GESTURELOCK = "param_plugin_gesturelock";
public static final String PARAM_PLUGIN_INTERNAL_ACTIVITIES_ONLY = "PARAM_PLUGIN_INTERNAL_ACTIVITIES_ONLY";
public static final String PARAM_PLUGIN_LOCATION = "pluginsdk_pluginLocation";
public static final String PARAM_PLUGIN_NAME = "pluginsdk_pluginName";
public static final String PARAM_PLUGIN_RECEIVER_CLASS_NAME = "pluginsdk_launchReceiver";
public static final String PARAM_UIN = "pluginsdk_selfuin";
public static final String PARAM_USE_QQ_RESOURCES = "userQqResources";
public static final String PARAM_USE_SKIN_ENGINE = "useSkinEngine";
public static final int USER_QQ_RESOURCES_BOTH = 2;
public static final int USER_QQ_RESOURCES_NO = -1;
public static final int USER_QQ_RESOURCES_YES = 1;
static final String a = "com.tencent.mobileqq";
static final String b = "pluginsdk_IsPluginActivity";
static final ConcurrentHashMap c = new ConcurrentHashMap();
static final ConcurrentHashMap d = new ConcurrentHashMap();
private static final HashMap e = new HashMap();
private static ArrayList f = new ArrayList();
public interface IPluginLife {
void onLoad();
void onUnload();
}
//上下文 读取 插件的id,插件apk的路径
static synchronized ClassLoader a(Context context, String str, String str2) throws Exception {
ClassLoader classLoader;
synchronized (PluginStatic.class) {
classLoader = (DexClassLoader) c.get(str);
if (classLoader == null) {
ClassLoader soDexClassLoader;
QLog.d("plugin_tag", 1, "getOrCreateClassLoader:" + str);
long currentTimeMillis = System.currentTimeMillis();
String canonicalPath = PluginUtils.getOptimizedDexPath(context).getCanonicalPath();
String canonicalPath2 = PluginUtils.getPluginLibPath(context, str).getCanonicalPath();
if (str2.endsWith(".so")) {
soDexClassLoader = new SoDexClassLoader(str2, canonicalPath, canonicalPath2, context.getClassLoader());
} else {
if (str.startsWith("qzone_live_video_plugin")) {
long currentTimeMillis2 = System.currentTimeMillis();
ClassLoader orCreateClassLoader = getOrCreateClassLoader(context, "qzone_plugin.apk");
QLog.d("plugin_tag", 1, "get qzone classloader cost=" + (System.currentTimeMillis() - currentTimeMillis2));
soDexClassLoader = new QZoneLiveClassLoader(str2, canonicalPath, canonicalPath2, orCreateClassLoader);
} else {
String str3;
if (str2 != null) {
if (!new File(str2).exists()) {
QLog.d("plugin_tag", 1, "getOrCreateClassLoader notExist " + str2);
classLoader = new DexClassLoader("", canonicalPath, canonicalPath2, context.getClassLoader());
}
}
if (PluginUtils.isOsNeedReleaseDex() && IPluginAdapterProxy.getProxy().isSupportMultiDex(str)) {
File multiDexSecondDex = PluginUtils.getMultiDexSecondDex(context, str);
if (multiDexSecondDex.exists()) {
str3 = str2 + File.pathSeparator + multiDexSecondDex.getAbsolutePath();
QLog.d("plugin_tag", 1, "multiDex dexsPath" + str3);
QLog.d("plugin_tag", 1, "dexsPath" + str3);
soDexClassLoader = new DexClassLoader(str3, canonicalPath, canonicalPath2, context.getClassLoader());
}
}
str3 = str2;
QLog.d("plugin_tag", 1, "dexsPath" + str3);
soDexClassLoader = new DexClassLoader(str3, canonicalPath, canonicalPath2, context.getClassLoader());
}
}
PackageInfo packageInfo = (PackageInfo) d.get(str2);
if (packageInfo == null) {
try {
packageInfo = ApkFileParser.getPackageInfoWithException(context, str2, 129);
} catch (Throwable th) {
DebugHelper.log("plugin_tag", "PluginStatic.getOrCreateClassLoaderByPath Get Package Info Failed!", th);
}
if (packageInfo == null) {
DebugHelper.log("PluginStatic.getOrCreateClassLoaderByPath Get Package Info Failed! " + new File(str2).exists());
}
d.put(str2, packageInfo);
}
if (packageInfo != null) {
a(packageInfo, str, soDexClassLoader);
}
QLog.w("plugin_tag", 1, "getOrCreateClassLoaderCost:" + str + " c:" + (System.currentTimeMillis() - currentTimeMillis));
c.put(str, soDexClassLoader);
classLoader = soDexClassLoader;
}
}
return classLoader;
}
static List a() {
return f;
}
static void a(PackageInfo packageInfo, String str, ClassLoader classLoader) {
try {
if (((IPluginLife) e.get(str)) == null && packageInfo != null && packageInfo.applicationInfo != null && packageInfo.applicationInfo.metaData != null) {
String string = packageInfo.applicationInfo.metaData.getString("PLUGIN_LIFE_CLASS");
if (string != null) {
IPluginLife iPluginLife = (IPluginLife) classLoader.loadClass(string).newInstance();
e.put(str, iPluginLife);
iPluginLife.onLoad();
}
}
} catch (Throwable th) {
}
}
static void a(IPluginActivity iPluginActivity) {
b();
synchronized (f) {
f.add(new WeakReference(iPluginActivity));
}
}
static boolean a(Bundle bundle) {
if (bundle == null) {
return false;
}
try {
String string = bundle.getString(PARAM_PLUGIN_LOCATION);
if (TextUtils.isEmpty(string) || string.substring(0, string.lastIndexOf(".")).contains(".") || TextUtils.isEmpty(bundle.getString(PARAM_PLUGIN_NAME))) {
return false;
}
string = bundle.getString(PARAM_PATH);
return !TextUtils.isEmpty(string) ? TextUtils.isEmpty(string) ? true : a(string) : false;
} catch (Throwable th) {
return false;
}
}
static boolean a(String str) {
try {
if (str.contains("..")) {
return false;
}
if (!str.endsWith(".so")) {
return str.endsWith(".apk") ? a(str, PluginUtils.getPluginInstallDir(BaseApplication.getContext())) : false;
} else {
String parent = BaseApplication.getContext().getFilesDir().getParent();
File file = new File(parent + SoLoadCore.PATH_TX_LIB);
if (a(str, new File(parent + SoLoadCore.PATH_LIB)) || a(str, file)) {
}
return true;
}
} catch (Exception e) {
return false;
}
}
private static boolean a(String str, File file) throws IOException {
String canonicalPath = file.getCanonicalPath();
String canonicalPath2 = new File(str).getParentFile().getCanonicalPath();
if (QLog.isColorLevel()) {
QLog.d("plugin_tag", 2, "path:" + str + "-> [" + canonicalPath2 + ", " + canonicalPath + "]");
}
return canonicalPath2.equals(canonicalPath);
}
static void b() {
synchronized (f) {
int i = 0;
while (i < f.size()) {
int i2;
if (((WeakReference) f.get(i)).get() == null) {
f.remove(i);
i2 = i - 1;
} else {
i2 = i;
}
i = i2 + 1;
}
}
}
static void b(IPluginActivity iPluginActivity) {
b();
c(iPluginActivity);
}
public static String byteArrayToHex(byte[] byteArray) {
int i = 0;
char[] cArr = new char[]{'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
char[] cArr2 = new char[(byteArray.length * 2)];
int length = byteArray.length;
int i2 = 0;
while (i < length) {
byte b = byteArray[i];
int i3 = i2 + 1;
cArr2[i2] = cArr[(b >>> 4) & 15];
i2 = i3 + 1;
cArr2[i3] = cArr[b & 15];
i++;
}
return new String(cArr2);
}
private static boolean c(IPluginActivity iPluginActivity) {
synchronized (f) {
for (int i = 0; i < f.size(); i++) {
if (((WeakReference) f.get(i)).get() == iPluginActivity) {
f.remove(i);
return true;
}
}
return false;
}
}
public static String encodeFile(String filePath) {
Throwable e;
String str = "";
File file = new File(filePath);
if (file.exists() && file.isFile()) {
FileInputStream fileInputStream;
try {
byte[] bArr;
MessageDigest instance = MessageDigest.getInstance("MD5");
fileInputStream = new FileInputStream(file);
try {
bArr = new byte[16384];
} catch (OutOfMemoryError e2) {
bArr = new byte[4096];
}
while (true) {
try {
int read = fileInputStream.read(bArr);
if (read == -1) {
break;
}
instance.update(bArr, 0, read);
} catch (Exception e3) {
e = e3;
}
}
String byteArrayToHex = byteArrayToHex(instance.digest());
if (fileInputStream == null) {
return byteArrayToHex;
}
try {
fileInputStream.close();
return byteArrayToHex;
} catch (IOException e4) {
return byteArrayToHex;
}
} catch (Exception e5) {
e = e5;
fileInputStream = null;
try {
if (QLog.isColorLevel()) {
QLog.e("plugin_tag", 2, "encode-Exception:" + QLog.getStackTraceString(e));
}
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e6) {
}
}
return str;
} catch (Throwable th) {
e = th;
if (fileInputStream != null) {
try {
fileInputStream.close();
} catch (IOException e7) {
}
}
throw e;
}
} catch (Throwable th2) {
e = th2;
fileInputStream = null;
if (fileInputStream != null) {
fileInputStream.close();
}
throw e;
}
}
if (QLog.isColorLevel()) {
QLog.e("plugin_tag", 2, "encode-File does not exist or is not file");
}
return str;
}
public static synchronized ClassLoader getClassLoader(String pluginID) {
DexClassLoader dexClassLoader;
synchronized (PluginStatic.class) {
dexClassLoader = (DexClassLoader) c.get(pluginID);
}
return dexClassLoader;
}
//加载红包插件也是这样玩的,实际上就是加载一个apk的dexclassloader,如果已缓存就从内从中取出,否则就加载一个, 传递plugindId,和 安装路径就饿可到了一个新的classloader
public static synchronized ClassLoader getOrCreateClassLoader(Context c, String pluginID) throws Exception {
ClassLoader classLoader;
synchronized (PluginStatic.class) {
classLoader = (ClassLoader) c.get(pluginID);
if (classLoader == null) {
classLoader = a(c, pluginID, PluginUtils.getInstalledPluginPath(c, pluginID).getCanonicalPath());
}
}
return classLoader;
}
public static List isProcessesExist(Context c, List processNames) {
if (processNames == null) {
return null;
}
List arrayList = new ArrayList();
List runningAppProcesses = ((ActivityManager) c.getSystemService("activity")).getRunningAppProcesses();
if (runningAppProcesses == null) {
for (int i = 0; i < processNames.size(); i++) {
arrayList.add(Boolean.FALSE);
}
return arrayList;
}
for (String str : processNames) {
boolean z;
for (RunningAppProcessInfo runningAppProcessInfo : runningAppProcesses) {
if (str.equalsIgnoreCase(runningAppProcessInfo.processName)) {
z = true;
break;
}
}
z = false;
arrayList.add(Boolean.valueOf(z));
}
return arrayList;
}
public static synchronized ClassLoader removeClassLoader(String pluginId) {
ClassLoader classLoader;
synchronized (PluginStatic.class) {
QLog.d("plugin_tag", 1, "removeClassLoader:" + pluginId);
classLoader = (ClassLoader) c.remove(pluginId);
}
return classLoader;
}
}
分析QQ如何加载插件apk的so文件
public class PluginUtils {
public static final String CONFIG_FILE_EXTEND_NAME = ".cfg";
private static final int a = 8192;
private static Map b = new ConcurrentHashMap();
private static Map c = new ConcurrentHashMap();
private static final String d = ".tmp";
static class a extends Exception {
private static final long a = 1;
public a(String str) {
super(str);
}
}
static class b extends Exception {
private static final long a = 1;
b() {
}
}
public static String getExceptionInfo(Throwable t) {
while (t.getCause() != null) {
t = t.getCause();
}
Writer stringWriter = new StringWriter();
t.printStackTrace(new PrintWriter(stringWriter, true));
return stringWriter.getBuffer().toString();
}
}
从上面的源码可以看出来extractLibs
方法做了这个事情,首先把apk文件读入到QZipFile,然后 得到实体遍历文件夹得到真正的apk路径。
上面的代码目前还无法得知干嘛的,暂时放一放谈谈pluginId,qq定义为一个没有路径的apk文件名
public String getPluginID() {
return "qwallet_plugin.apk";
}
QWalletPluginProxyActivity
红包界面源码可以得知是这样写的。
腾讯QQ的插件activity接口
public interface IPluginActivity {
boolean IDispatchTouchEvent(MotionEvent motionEvent);
void IFinish();
View IGetContentView();
Handler IGetInHandler();
Resources IGetResource();
void IInit(String str, String str2, Activity activity, ClassLoader classLoader, PackageInfo packageInfo, boolean z, int i);
boolean IIsWrapContent();
void IOnActivityResult(int i, int i2, Intent intent);
void IOnAttachFragment(Fragment fragment);
boolean IOnBackPressed();
void IOnConfigurationChanged(Configuration configuration);
void IOnCreate(Bundle bundle);
boolean IOnCreateOptionsMenu(Menu menu);
void IOnDestroy();
boolean IOnKeyDown(int i, KeyEvent keyEvent);
boolean IOnKeyMultiple(int i, int i2, KeyEvent keyEvent);
boolean IOnKeyUp(int i, KeyEvent keyEvent);
boolean IOnMenuItemSelected(int i, MenuItem menuItem);
void IOnNewIntent(Intent intent);
boolean IOnOptionsItemSelected(MenuItem menuItem);
void IOnPause();
boolean IOnPrepareOptionsMenu(Menu menu);
void IOnRestart();
void IOnRestoreInstanceState(Bundle bundle);
void IOnResume();
void IOnSaveInstanceState(Bundle bundle);
void IOnSetTheme();
void IOnStart();
void IOnStop();
boolean IOnTouchEvent(MotionEvent motionEvent);
void IOnUserInteraction();
void IOnWindowFocusChanged(boolean z);
void ISetIntent(Intent intent);
void ISetIsTab();
void ISetOutHandler(Handler handler);
void ISetParent(BasePluginActivity basePluginActivity);
ImmersiveConfig IgetImmersiveConfig();
}
实现类BasePluginActivity
this.f就是插件apk,说明如果是插件模式的话就调用的是this.f ,也就是说这个插件apk是可以单独运行的。
还是不看了,感觉由于不能跟踪代码,看起来头晕
项目庞大,一般的分析工具都卡死了。
再谈谈android 的其他hook 如破解签名hook
interface IPackageManager {
PackageInfo getPackageInfo(String packageName, int flags);
int getPackageUid(String packageName);
int[] getPackageGids(String packageName);
String[] currentToCanonicalPackageNames(in String[] names);
String[] canonicalToCurrentPackageNames(in String[] names);
PermissionInfo getPermissionInfo(String name, int flags);
List queryPermissionsByGroup(String group, int flags);
PermissionGroupInfo getPermissionGroupInfo(String name, int flags);
List getAllPermissionGroups(int flags);
ApplicationInfo getApplicationInfo(String packageName, int flags);
ActivityInfo getActivityInfo(in ComponentName className, int flags);
ActivityInfo getReceiverInfo(in ComponentName className, int flags);
ServiceInfo getServiceInfo(in ComponentName className, int flags);
ProviderInfo getProviderInfo(in ComponentName className, int flags);
int checkPermission(String permName, String pkgName);
int checkUidPermission(String permName, int uid);
boolean addPermission(in PermissionInfo info);
void removePermission(String name);
boolean isProtectedBroadcast(String actionName);
int checkSignatures(String pkg1, String pkg2);
int checkUidSignatures(int uid1, int uid2);
String[] getPackagesForUid(int uid);
String getNameForUid(int uid);
int getUidForSharedUser(String sharedUserName);
ResolveInfo resolveIntent(in Intent intent, String resolvedType, int flags);
List queryIntentActivities(in Intent intent,
String resolvedType, int flags);
List queryIntentActivityOptions(
in ComponentName caller, in Intent[] specifics,
in String[] specificTypes, in Intent intent,
String resolvedType, int flags);
List queryIntentReceivers(in Intent intent,
String resolvedType, int flags);
ResolveInfo resolveService(in Intent intent,
String resolvedType, int flags);
List queryIntentServices(in Intent intent,
String resolvedType, int flags);
ParceledListSlice getInstalledPackages(int flags, in String lastRead);
ParceledListSlice getInstalledApplications(int flags, in String lastRead);
List getPersistentApplications(int flags);
ProviderInfo resolveContentProvider(String name, int flags);
void querySyncProviders(inout List outNames,
inout List outInfo);
List queryContentProviders(
String processName, int uid, int flags);
InstrumentationInfo getInstrumentationInfo(
in ComponentName className, int flags);
List queryInstrumentation(
String targetPackage, int flags);
void installPackage(in Uri packageURI, IPackageInstallObserver observer, int flags,
in String installerPackageName);
void finishPackageInstall(int token);
n performDexOpt(String packageName);
void updateExternalMediaStatus(boolean mounted, boolean reportStatus);
String nextPackageToClean(String lastPackage);
void movePackage(String packageName, IPackageMoveObserver observer, int flags);
boolean addPermissionAsync(in PermissionInfo info);
boolean setInstallLocation(int loc);
int getInstallLocation();
}
hook
public class IPackageManagerHook extends ProxyHook {
private static final String TAG = IPackageManagerHook.class.getSimpleName();
public IPackageManagerHook(Context hostContext) {
super(hostContext);
}
@Override
protected BaseHookHandle createHookHandle() {
return new IPackageManagerHookHandle(mHostContext);
}
@Override
protected void onInstall(ClassLoader classLoader) throws Throwable {
Object currentActivityThread = ActivityThreadCompat.currentActivityThread();
setOldObj(FieldUtils.readField(currentActivityThread, "sPackageManager"));
Class> iPmClass = mOldObj.getClass();
List> interfaces = Utils.getAllInterfaces(iPmClass);
Class[] ifs = interfaces != null && interfaces.size() > 0 ? interfaces.toArray(new Class[interfaces.size()]) : new Class[0];
Object newPm = MyProxy.newProxyInstance(iPmClass.getClassLoader(), ifs, this);
FieldUtils.writeField(currentActivityThread, "sPackageManager", newPm);
PackageManager pm = mHostContext.getPackageManager();
Object mPM = FieldUtils.readField(pm, "mPM");
if (mPM != newPm) {
FieldUtils.writeField(pm, "mPM", newPm);
}
}
public static void fixContextPackageManager(Context context) {
try {
Object currentActivityThread = ActivityThreadCompat.currentActivityThread();
Object newPm = FieldUtils.readField(currentActivityThread, "sPackageManager");
PackageManager pm = context.getPackageManager();
Object mPM = FieldUtils.readField(pm, "mPM");
if (mPM != newPm) {
FieldUtils.writeField(pm, "mPM", newPm);
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
hook启动的另外一一种方式
public class InstrumentationHook extends Hook {
private static final String TAG = InstrumentationHook.class.getSimpleName();
private List mPluginInstrumentations = new ArrayList();
public InstrumentationHook(Context hostContext) {
super(hostContext);
}
@Override
protected BaseHookHandle createHookHandle() {
return null;
}
@Override
public void setEnable(boolean enable, boolean reinstallHook) {
if (reinstallHook) {
try {
onInstall(null);
} catch (Throwable throwable) {
Log.i(TAG, "setEnable onInstall fail", throwable);
}
}
for (PluginInstrumentation pit : mPluginInstrumentations) {
pit.setEnable(enable);
}
super.setEnable(enable,reinstallHook);
}
@Override
protected void onInstall(ClassLoader classLoader) throws Throwable {
Object target = ActivityThreadCompat.currentActivityThread();
Class ActivityThreadClass = ActivityThreadCompat.activityThreadClass();
/*替换ActivityThread.mInstrumentation,拦截组件调度消息*/
Field mInstrumentationField = FieldUtils.getField(ActivityThreadClass, "mInstrumentation");
Instrumentation mInstrumentation = (Instrumentation) FieldUtils.readField(mInstrumentationField, target);
if (!PluginInstrumentation.class.isInstance(mInstrumentation)) {
PluginInstrumentation pit = new PluginInstrumentation(mHostContext, mInstrumentation);
pit.setEnable(isEnable());
mPluginInstrumentations.add(pit);
FieldUtils.writeField(mInstrumentationField, target, pit);
Log.i(TAG, "Install Instrumentation Hook old=%s,new=%s", mInstrumentationField, pit);
} else {
Log.i(TAG, "Instrumentation has installed,skip");
}
}
}