Android進階(十)資源和Service的外掛化

猥瑣發育_別浪發表於2019-02-26

一、系統資源載入

1、資源類別

  • res目錄下存放的資原始檔。編譯時會在R檔案中生成資原始檔的十六進位制值。res目錄下資源通過Context.getResource方法獲取到Resource物件,然後通過getXXX獲取資源。
  • assets目錄下存放的原始檔案,編譯時不會被編譯。通過AssetManager的open方法獲取目錄下檔案資源,AssetManager來源於Resources類的getAssets方法

2、Resources

(1)AssetManager

Android進階(十)資源和Service的外掛化

  • AssetManage有一個addAssetPath方法,將apk路徑傳入,Resources就能訪問當前apk的所有資源。可以通過反射的方式將外掛apk路徑傳入addAssetPath方法。
  • AssetManager內部有一個NDK方法,用來訪問檔案。apk打包時會生成一個resources.arsc檔案,是一個Hash表,存放著十六進位制和資源的對應關係

二、VirtualApk外掛資源載入

資源外掛化實現方式:

  • 合併資源:將外掛的資源合併到宿主的Resources中,可以訪問宿主的資源。可能存在外掛和宿主的資源id重複的情況。
    解決方式:
    (1)修改Android打包流程中使用到的aapt命令,為外掛的資源id指定字首,避免與宿主資源id衝突。
    (2)在Android打包生成resources.arsc檔案之後,對這個resources.arsc檔案進行修改。
  • 單獨載入外掛資源:每個外掛都會構造單獨的Resources去載入外掛資源,不能訪問宿主資源

1、Resources建立

#LoadedPlugin
public LoadedPlugin(PluginManager pluginManager, Context context, File apk) throws Exception {
    ......
    this.mResources = createResources(context, getPackageName(), apk);
    ......
}
protected Resources createResources(Context context, String packageName, File apk) throws Exception {
    if (Constants.COMBINE_RESOURCES) {
        //外掛資源合併到宿主中,外掛可訪問宿主資源
        return ResourcesManager.createResources(context, packageName, apk);
    } else {
        //外掛建立獨立的Resources,不與宿主關聯
        Resources hostResources = context.getResources();
        AssetManager assetManager = createAssetManager(context, apk);
        return new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
    }
}
複製程式碼

2、外掛資源獨立

主要通過反射建立新的AssetManager物件,通過addAssetPath載入外掛資源。適用於資源獨立的情況,無法呼叫宿主資源

protected AssetManager createAssetManager(Context context, File apk) throws Exception {
    //通過反射建立新的AssetManager物件,通過addAssetPath載入外掛資源
    AssetManager am = AssetManager.class.newInstance();
    Reflector.with(am).method("addAssetPath", String.class).call(apk.getAbsolutePath());
    return am;
}
複製程式碼

3、外掛資源合併

先獲取到宿主資源的AssetManager,再通過反射呼叫AssetManager的addAssetPath新增外掛資源,返回新的Resources

#ResourcesManager
public static synchronized Resources createResources(Context hostContext, String packageName, File apk) throws Exception {
    //根據版本建立Resources物件
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        //(1)
        return createResourcesForN(hostContext, packageName, apk);
    }
    //(2)
    Resources resources = ResourcesManager.createResourcesSimple(hostContext, apk.getAbsolutePath());
    ResourcesManager.hookResources(hostContext, resources);
    return resources;
}
//建立Resource物件
private static Resources createResourcesSimple(Context hostContext, String apk) throws Exception {
    //宿主Resources物件
    Resources hostResources = hostContext.getResources();
    Resources newResources = null;
    AssetManager assetManager;
    Reflector reflector = Reflector.on(AssetManager.class).method("addAssetPath", String.class);
    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
        //通過反射建立AssetManager
        assetManager = AssetManager.class.newInstance();
        reflector.bind(assetManager);
        final int cookie1 = reflector.call(hostContext.getApplicationInfo().sourceDir);;
        if (cookie1 == 0) {
            throw new RuntimeException("createResources failed, can't addAssetPath for " + hostContext.getApplicationInfo().sourceDir);
        }
    } else {
        //獲取到宿主的AssetManager
        assetManager = hostResources.getAssets();
        reflector.bind(assetManager);
    }
    final int cookie2 = reflector.call(apk);
    if (cookie2 == 0) {
        throw new RuntimeException("createResources failed, can't addAssetPath for " + apk);
    }
    List<LoadedPlugin> pluginList = PluginManager.getInstance(hostContext).getAllLoadedPlugins();
    for (LoadedPlugin plugin : pluginList) {
        final int cookie3 = reflector.call(plugin.getLocation());
        if (cookie3 == 0) {
            throw new RuntimeException("createResources failed, can't addAssetPath for " + plugin.getLocation());
        }
    }
    //通過不同的手機品牌建立Resources物件
    if (isMiUi(hostResources)) {
        newResources = MiUiResourcesCompat.createResources(hostResources, assetManager);
    } else if (isVivo(hostResources)) {
        newResources = VivoResourcesCompat.createResources(hostContext, hostResources, assetManager);
    } else if (isNubia(hostResources)) {
        newResources = NubiaResourcesCompat.createResources(hostResources, assetManager);
    } else if (isNotRawResources(hostResources)) {
        newResources = AdaptationResourcesCompat.createResources(hostResources, assetManager);
    } else {
        // is raw android resources
        newResources = new Resources(assetManager, hostResources.getDisplayMetrics(), hostResources.getConfiguration());
    }
    // lastly, sync all LoadedPlugin to newResources
    for (LoadedPlugin plugin : pluginList) {
        plugin.updateResources(newResources);
    }
    
    return newResources;
}
複製程式碼

Hook住了ContextImpl的mResources和LoadedApk的mResources

public static void hookResources(Context base, Resources resources) {
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
        return;
    }
    try {
        Reflector reflector = Reflector.with(base);
        //hook mResources
        reflector.field("mResources").set(resources);
        Object loadedApk = reflector.field("mPackageInfo").get();
        //hook mResources
        Reflector.with(loadedApk).field("mResources").set(resources);

        Object activityThread = ActivityThread.currentActivityThread();
        Object resManager;
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            resManager = android.app.ResourcesManager.getInstance();
        } else {
            resManager = Reflector.with(activityThread).field("mResourcesManager").get();
        }
        Map<Object, WeakReference<Resources>> map = Reflector.with(resManager).field("mActiveResources").get();
        Object key = map.keySet().iterator().next();
        map.put(key, new WeakReference<>(resources));
    } catch (Exception e) {
        Log.w(TAG, e);
    }
}
複製程式碼

4、Activity啟動資源處理

#VAInstrumentation
@Override
public Activity newActivity(ClassLoader cl, String className, Intent intent) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
    try {
        cl.loadClass(className);
        Log.i(TAG, String.format("newActivity[%s]", className));
        
    } catch (ClassNotFoundException e) {
        ......

        // 通過反射將Resources賦值給Activity的mResources
        Reflector.QuietReflector.with(activity).field("mResources").set(plugin.getResources());
        return newActivity(activity);
    }

    return newActivity(mBase.newActivity(cl, className, intent));
}
複製程式碼

三、so的外掛化

so的外掛化,有兩種方案:基於System.Load和基於System.LoadLibrary。

1、VirtualApk的實現

#LoadedPlugin
protected ClassLoader createClassLoader(Context context, File apk, File libsDir, ClassLoader parent) throws Exception {
    File dexOutputDir = getDir(context, Constants.OPTIMIZE_DIR);
    String dexOutputPath = dexOutputDir.getAbsolutePath();
    DexClassLoader loader = new DexClassLoader(apk.getAbsolutePath(), dexOutputPath, libsDir.getAbsolutePath(), parent);

    if (Constants.COMBINE_CLASSLOADER) {
        DexUtil.insertDex(loader, parent, libsDir);
    }
    return loader;
}

複製程式碼

建立了一個DexClassLoader,解析出每個外掛apk中的so檔案,解壓到某個位置,把這些路徑用逗號連線起來成為一個字串,放到DexClassLoader的建構函式的第3個引數中。這樣外掛中的so,就和宿主App中jniLib目錄下的so一樣,通過System.loadLibrary方法來載入。

#DexUtil
public static void insertDex(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception {
    Object baseDexElements = getDexElements(getPathList(baseClassLoader));
    Object newDexElements = getDexElements(getPathList(dexClassLoader));
    //將宿主和外掛的DexElements合併得到allDexElements
    Object allDexElements = combineArray(baseDexElements, newDexElements);
    Object pathList = getPathList(baseClassLoader);
    //通過反射將dexElements替換為allDexElements
    Reflector.with(pathList).field("dexElements").set(allDexElements);
    
    insertNativeLibrary(dexClassLoader, baseClassLoader, nativeLibsDir);
}
複製程式碼

so外掛化核心程式碼

private static synchronized void insertNativeLibrary(DexClassLoader dexClassLoader, ClassLoader baseClassLoader, File nativeLibsDir) throws Exception {
    if (sHasInsertedNativeLibrary) {
        return;
    }
    sHasInsertedNativeLibrary = true;

    Context context = ActivityThread.currentApplication();
    //獲取宿主的PathList
    Object basePathList = getPathList(baseClassLoader);
    if (Build.VERSION.SDK_INT > Build.VERSION_CODES.LOLLIPOP_MR1) {
        Reflector reflector = Reflector.with(basePathList);
   
        List<File> nativeLibraryDirectories = reflector.field("nativeLibraryDirectories").get();
        nativeLibraryDirectories.add(nativeLibsDir);
        //獲取到宿主的so集合
        Object baseNativeLibraryPathElements = reflector.field("nativeLibraryPathElements").get();
        final int baseArrayLength = Array.getLength(baseNativeLibraryPathElements);

        Object newPathList = getPathList(dexClassLoader);
        //獲取到外掛的so集合
        Object newNativeLibraryPathElements = reflector.get(newPathList);
        Class<?> elementClass = newNativeLibraryPathElements.getClass().getComponentType();
        Object allNativeLibraryPathElements = Array.newInstance(elementClass, baseArrayLength + 1);
        //將原來宿主的so集合拷貝到新集合中
        System.arraycopy(baseNativeLibraryPathElements, 0, allNativeLibraryPathElements, 0, baseArrayLength);

        Field soPathField;
        if (Build.VERSION.SDK_INT >= 26) {
            soPathField = elementClass.getDeclaredField("path");
        } else {
            soPathField = elementClass.getDeclaredField("dir");
        }
        soPathField.setAccessible(true);
        //將外掛的so集合拷貝到新集合中
        final int newArrayLength = Array.getLength(newNativeLibraryPathElements);
        for (int i = 0; i < newArrayLength; i++) {
            Object element = Array.get(newNativeLibraryPathElements, i);
            String dir = ((File)soPathField.get(element)).getAbsolutePath();
            if (dir.contains(Constants.NATIVE_DIR)) {
                Array.set(allNativeLibraryPathElements, baseArrayLength, element);
                break;
            }
        }
        //將宿主和外掛so的合集替換上去
        reflector.set(allNativeLibraryPathElements);
    } else {
        Reflector reflector = Reflector.with(basePathList).field("nativeLibraryDirectories");
        File[] nativeLibraryDirectories = reflector.get();
        final int N = nativeLibraryDirectories.length;
        File[] newNativeLibraryDirectories = new File[N + 1];
        System.arraycopy(nativeLibraryDirectories, 0, newNativeLibraryDirectories, 0, N);
        newNativeLibraryDirectories[N] = nativeLibsDir;
        reflector.set(newNativeLibraryDirectories);
    }
}
複製程式碼

獲取宿主so集合,獲取外掛so集合,二者合併後通過反射替換原so集合,外掛so檔案就能正常被載入了

四、VirtualApk的Service外掛化

1、Service啟動分析

Android進階(十)資源和Service的外掛化

Android進階(十)資源和Service的外掛化
外掛化分析:

  • Service啟動跟Instrumentation沒關係,不能通過Hook Instrumentation來處理
  • 在Standard模式下多次啟動佔位Activity可建立多個Activity,但是多次啟動佔位Service並不會建立多個Service例項
  • 通過代理分發實現:啟動一個代理Service統一管理,攔截所有Service方法,修改為startService到代理Service,在代理Service的onStartCommond統一管理,建立/停止目標service。

2、Hook IActivityManager

VirtualApk初始化時通過ActivityManagerProxy Hook了IActivityManager。啟動服務時,通過ActivityManagerProxy攔截到了startService的操作

public class ActivityManagerProxy implements InvocationHandler {

    protected Object startService(Object proxy, Method method, Object[] args) throws Throwable {
        IApplicationThread appThread = (IApplicationThread) args[0];
        //跳轉的intent
        Intent target = (Intent) args[1];
        //檢查Service資訊
        ResolveInfo resolveInfo = this.mPluginManager.resolveService(target, 0);
        if (null == resolveInfo || null == resolveInfo.serviceInfo) {
            // is host service
            return method.invoke(this.mActivityManager, args);
        }

        return startDelegateServiceForTarget(target, resolveInfo.serviceInfo, null, RemoteService.EXTRA_COMMAND_START_SERVICE);
    }

	protected ComponentName startDelegateServiceForTarget(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
        Intent wrapperIntent = wrapperTargetIntent(target, serviceInfo, extras, command);
        return mPluginManager.getHostContext().startService(wrapperIntent);
    }

    protected Intent wrapperTargetIntent(Intent target, ServiceInfo serviceInfo, Bundle extras, int command) {
        // 將目標Service的相關資訊儲存起來
        target.setComponent(new ComponentName(serviceInfo.packageName, serviceInfo.name));
        String pluginLocation = mPluginManager.getLoadedPlugin(target.getComponent()).getLocation();

        // 根據processName判斷是否為遠端服務
        boolean local = PluginUtil.isLocalService(serviceInfo);
        // 判斷交給LocalService還是RemoteService進行處理
        Class<? extends Service> delegate = local ? LocalService.class : RemoteService.class;
        // 引數傳遞
        Intent intent = new Intent();
        intent.setClass(mPluginManager.getHostContext(), delegate);
        intent.putExtra(RemoteService.EXTRA_TARGET, target);
        intent.putExtra(RemoteService.EXTRA_COMMAND, command);
        intent.putExtra(RemoteService.EXTRA_PLUGIN_LOCATION, pluginLocation);
        if (extras != null) {
            intent.putExtras(extras);
        }
        return intent;
    }
}
複製程式碼

3、LocalService

public class LocalService extends Service {

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
       	......
        switch (command) {
            case EXTRA_COMMAND_START_SERVICE: {
            	//獲取ActivityThread
                ActivityThread mainThread = ActivityThread.currentActivityThread();
                IApplicationThread appThread = mainThread.getApplicationThread();
                Service service;

                if (this.mPluginManager.getComponentsHandler().isServiceAvailable(component)) {
                	//獲取Service
                    service = this.mPluginManager.getComponentsHandler().getService(component);
                } else {
                    try {
                    	//通過DexClassLoader載入Service
                        service = (Service) plugin.getClassLoader().loadClass(component.getClassName()).newInstance();

                        Application app = plugin.getApplication();
                        IBinder token = appThread.asBinder();
                        Method attach = service.getClass().getMethod("attach", Context.class, ActivityThread.class, String.class, IBinder.class, Application.class, Object.class);
                        IActivityManager am = mPluginManager.getActivityManager();
                       	//通過attach方法繫結Context 		
                        attach.invoke(service, plugin.getPluginContext(), mainThread, component.getClassName(), token, app, am);
                        //呼叫Service的onCreate方法
                        service.onCreate();
                        this.mPluginManager.getComponentsHandler().rememberService(component, service);
                    } catch (Throwable t) {
                        return START_STICKY;
                    }
                }
                //呼叫service的onStartCommand方法
                service.onStartCommand(target, 0, this.mPluginManager.getComponentsHandler().getServiceCounter(service).getAndIncrement());
                break;
            }
            ......
        }
    }
}
複製程式碼

4、RemoteService

public class RemoteService extends LocalService {
    
    private static final String TAG = Constants.TAG_PREFIX + "RemoteService";

    @Override
    public IBinder onBind(Intent intent) {
        return null;
    }

    @Override
    public int onStartCommand(Intent intent, int flags, int startId) {
        if (intent == null) {
            return super.onStartCommand(intent, flags, startId);
        }
        //獲取目標service的intent
        Intent target = intent.getParcelableExtra(EXTRA_TARGET);
        if (target != null) {
            //獲取外掛路徑
            String pluginLocation = intent.getStringExtra(EXTRA_PLUGIN_LOCATION);
            ComponentName component = target.getComponent();
            LoadedPlugin plugin = PluginManager.getInstance(this).getLoadedPlugin(component);
            if (plugin == null && pluginLocation != null) {
                try {
                    //載入apk外掛檔案
                    PluginManager.getInstance(this).loadPlugin(new File(pluginLocation));
                } catch (Exception e) {
                    Log.w(TAG, e);
                }
            }
        }

        return super.onStartCommand(intent, flags, startId);
    }
}
複製程式碼

啟動遠端服務多了一步載入其他外掛的Service的操作

5、Service外掛化總結

  • 初始化時通過ActivityManagerProxy Hook住了IActivityManager。
  • 服務啟動時通過ActivityManagerProxy攔截,判斷是否為遠端服務,如果為遠端服務,啟動RemoteService,如果為同程式服務則啟動LocalService。
  • 如果為LocalService,則通過DexClassLoader載入目標Service,然後反射呼叫attach方法繫結Context,然後執行Service的onCreate、onStartCommand方法
  • 如果為RemoteService,則先載入外掛的遠端Service,後續跟LocalService一致。

參考資料:

相關文章