前言

VirtualAPK是didi出品的插件化框架,最近读了读源码,简单做些总结,先针对资源的处理这一块,dodola的这篇文章对原理阐述的很详尽,建议大家先看一下,我这边对文章中几个点再阐述下以及对gradle插件所做的事做一些总结。所在分支是dev。

资源的处理

一、先看下ResourcesManager.javacreateResources方法片段:

	if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
	    assetManager = AssetManager.class.newInstance();
	    ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", hostContext.getApplicationInfo().sourceDir);
	} else {
	    assetManager = hostResources.getAssets();
	}
	ReflectUtil.invoke(AssetManager.class, assetManager, "addAssetPath", apk);

这边针对资源的加载做了兼容处理,差异主要是在AssetManager.cppaddAssetPath方法,可以比较下4.4Android O的代码,会发现Android O中会将插件apk的resources.arsc所代表的Asset对象add到现有的索引表ResTable中,而4.4中只是将该apk的path加入到path列表里,所以它这边选择了new一个AssetManager。

文章中还提到了淘宝发布的《深入探索 Android 热修复技术原理》中的一种少兼容少hook的方案,简单理解就是重构native端的AssetManager,添加所有插件的path,接着AssetManager::getResTable时去生成新的索引表ResTable,而Java层的Resource对象地址是不变的,也就省去了兼容和hook。

二、Activity 启动过程中对资源的处理

VAInstrumentation.javanewActivity方法片段:

	Activity activity = mBase.newActivity(plugin.getClassLoader(), targetClassName, intent);
	activity.setIntent(intent);
	try {
		// for 4.1+
		ReflectUtil.setField(ContextThemeWrapper.class, activity, "mResources", plugin.getResources());
	} catch (Exception ignored) {
		// ignored.
	}

文章中提到此处需要重设mResources的原因在ActivityThrea的performLaunchActivity方法(以Android O版本为例)

	private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
		...
		ContextImpl appContext = createBaseContextForActivity(r);
		activity = mInstrumentation.newActivity(cl, component.getClassName(), r.intent);
		...
		int theme = r.activityInfo.getThemeResource();
		if (theme != 0) {
			activity.setTheme(theme);
		}
		...
		if (r.isPersistable()) {
			mInstrumentation.callActivityOnCreate(activity, r.state, r.persistentState);
		} else {
			mInstrumentation.callActivityOnCreate(activity, r.state);
		}
	}

在 createBaseContextForActivity 方法中创建出来的 ContextImpl appContext 使用的是宿主的Resources,如果不进行处理紧接着Activity会走入onCreate的生命周期中,此时插件加载资源的时候还是使用的宿主的资源,而不是我们特意为插件所创建出来的Resources对象,则会发生找不到资源的问题

这里说的有点问题,在走到onCreate之前,框架是hook了Instrumentation.callActivityOnCreate并且重设了mResources等一系列值,其实是可以找到插件的资源的,真正的问题出在上面的几句设置theme的代码,我们知道框架hook了mInstrumentation.newActivity此时返回的已经是插件的Activity实例,而此时给这个插件ActivitysetTheme这个theme是哪个Activity的theme,如果不做任何操作,肯定是占位Activity也就是宿主Application中的默认theme,而框架把这个theme已经换掉了,代码在VAInstrumentationhandleMessage方法中:

	public boolean handleMessage(Message msg) {
		if (msg.what == LAUNCH_ACTIVITY) {
			// ActivityClientRecord r
			Object r = msg.obj;
			try {
				Intent intent = (Intent) ReflectUtil.getField(r.getClass(), r, "intent");
				intent.setExtrasClassLoader(VAInstrumentation.class.getClassLoader());
				ActivityInfo activityInfo = (ActivityInfo) ReflectUtil.getField(r.getClass(), r, "activityInfo");
				if (PluginUtil.isIntentFromPlugin(intent)) {
					int theme = PluginUtil.getTheme(mPluginManager.getHostContext(), intent);
					if (theme != 0) {
						Log.i(TAG, "resolve theme, current theme:" + activityInfo.theme + "  after :0x" + Integer.toHexString(theme));
						activityInfo.theme = theme;
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return false;
	}

所以如果此时上面的newActivityhook中不去重设mResourcessetTheme方法就会报错。

文章中还提到:

另:如果采用上述所说的AssetManager销毁的方法,则无需在创建Activity后设置Resources对象,因为此处全局都是宿主+插件的资源。

这里简单解释下,虽然每次new一个新的Activity的时候都会产生一个新的ContextImpl,但是这个ContextImpl去生成Resource实例的时候首先会有个取缓存的过程,这个缓存的key一般情况下都是相同的,所以底层也就共用一个AssetManager。可以跟踪下ResourcesManager.javacreateBaseActivityResources方法看下。

gradle插件中对资源的处理

一、VAHostPlugin

宿主的这个plugin是为插件plugin服务的,作用是生成一些文件。与资源处理相关的是备份了宿主的R.txt和生成宿主的依赖列表versions.txt,放在宿主module/build/VAHost

二、VAPlugin

插件的这个plugin针对资源处理相关的操作主要是hook了app编译过程中几个Task,如Merge[Variant]AssetsProcess[Variant]Resources等以达到剔除与宿主共享资源,重设插件中资源id,裁剪resources.arsc等目的,下面我们具体看下这些hook。

  • PrepareDependenciesHooker

hook了pre[Variant]Buildtask,主要目的是过滤插件依赖(aar or jar),将其与宿主共享的依赖和其独有的依赖分开放到若干集合中,用到了上面说到的宿主依赖列表versions.txt。

  • MergeAssetsHooker

hook了Merge[Variant]Assetstask,这个task是用于merge插件依赖的各个aar中的assets,然后会交由AAPT处理,上一步过滤出了插件与宿主共享的aar,那么这些aar中的assets是不需要插件AAPT再处理了,所以需要此hook剔除这些assets。

  • ProcessResourcesHooker

hook了Process[Variant]Resourcestask,这个task最终是调用AAPT去编译资源生成R文件和resources-[Variant].ap_,这个.ap_文件位置在build/intermediates/res下,可以解压出来包含了resources.arsc,编译过的AndroidManifest.xml和资源。这个hook的主要作用就是去修改.ap_文件和R文件。

我们看下ProcessResourcesHooker.groovy中的repackage方法里的一些关键点:

	void repackage(ProcessAndroidResources par, File apFile) {
		...
		resourceCollector = new ResourceCollector(project, par)
		resourceCollector.collect() // ①
		...
		def aapt = new Aapt(resourcesDir, rSymbolFile, androidConfig.buildToolsRevision)
		//Delete host resources, must do it before filterPackage
		aapt.filterResources(retainedTypes, filteredResources)  // ②
		//Modify the arsc file, and replace ids of related xml files
		aapt.filterPackage(retainedTypes, retainedStylealbes, virtualApk.packageId, resIdMap, libRefTable, updatedResources) // ③
		...
		/*
		 * Delete filtered entries and then add updated resources into resources-${variant.name}.ap_
		 */
		com.didi.virtualapk.utils.ZipUtil.with(apFile).deleteAll(filteredResources + updatedResources) // ④
		project.exec {
		    executable par.buildTools.getPath(BuildToolInfo.PathId.AAPT)
		    workingDir resourcesDir
		    args 'add', apFile.path
		    args updatedResources
		    standardOutput = System.out
		    errorOutput = System.err
		} // ⑤
		updateRJava(aapt, par.sourceOutputDir) // ⑥
		...
	}

① 解析宿主和插件的R.txt按照ResType收集资源,并从收集的插件资源集合中过滤出与宿主同名的资源(资源类型和名称相同,参见ResourceEntry.groovyequals())。补充下,R.txt也是AAPT的产物,除了自身的资源也包括了其依赖的aar中的资源,输出位置一般在build/intermediates/symbols下。接着重设资源ID,与宿主共享部分的资源,其ID需要设置成宿主中的值;插件独有的资源需要重新设置,0XPPTTEEEE,PP段设置成我们在插件gradle文件中设置的值,TT和EEEE段按顺序重新赋值。这一步主要生成几个集合供下面的几步使用。

② 遍历从.ap_文件中解压出来的所有资源,将与宿主共享的资源全部删除。

③ 开始修改与裁剪resources.arsc各个chunk段的值,只留下插件独有的资源索引。关于arsc的格式,可以去参照老罗的博客,下面图片来自ArscEditor.groovy中:

arsc

接着将从.ap_文件中解压的已经编译过的xml文件中的引用的资源ID替换成上面新生成的ID值。编译的xml文件格式也可以参考老罗的博客,所有修改的文件(包括arsc,各种xml)都会放到一个集合中。下面便是打包新的.ap_文件。

④ 删除.ap_文件中所有与宿主共享的和所有修改过的资源的原始版本

⑤ 通过aapt add命令将所有修改的资源重新打包到.ap_文件,.ap_文件的修改全部结束。

⑥ 下面便是更新插件的R.java包括其依赖的aar的R.java,将其中的所有ID值更新,这样便能正确索引arsc文件了。这一步还做了一个操作,将插件独有的资源写到一个单独的R.java中,作用会在下面说。到此为止,AAPT的产物都已经修改裁剪完毕,通过重设资源ID的PP段以达到避免ID冲突的问题,这种方式相比修改AAPT的源码兼容性相对好些,VirtualAPK的这种方式参照了Small的实现

  • DxTaskHooker

hook了dex这个Transform,作用是在dex操作之前去覆盖插件app的R.class(不包括其依赖的aar的R.class),只留下插件独有的资源ID,能这么操作是因为插件app编译后的class文件中对R中资源ID的引用都已经编译成对一个常量的引用,如下图(而aar中则是编译成对一个变量的引用,因为aar中ID值在编译时会和主module融合导致其值变化):

app_R

而为了兼容宿主反射调用插件中的独有资源,则要留下插件独有的资源ID。这个hooker我觉得是可以去掉的,不去覆盖R.class也不会有什么影响。

TL;DR

大体VirtualAPK针对资源的处理都梳理了,简单总结下,编译阶段,针对插件apk重设资源ID,运行阶段,宿主加载插件,将插件和宿主的apk添加到同一个AssetManager中,接着构建一个新的Resources去替换原来的Resources,这样通过Context去取资源的时候就能在一个完整的resources.arsc里索引。

前言

最近在新项目中采用了MVVM + DataBinding + RxJava的架构,后面会把一些知识点都记录下来,这篇先来说说DataBinding

之前使用MVP模式时,ViewPresenter之间总要定义一个介面IViewPresenter必须通过这个IView来操作View。但是现在使用了MVVM + DataBindingViewModel通过DataBinding机制将Model塞给View,无需这个IView了,而且再也不需要findViewById()或者ButterKnife这样的注入框架了,使用下来确实少写很多代码。

DataBinding允许你直接在XML布局中绑定对象,并且可以监听对象的变化以便及时更新UI,但是现在只能单向绑定,比如你给EditText绑定了一个String,该String的变化会及时显示在EditText,但是你在EditText中输入的内容不会直接赋值到String中。现在IDE对DataBinding支持一般,没有代码提示,毕竟推出不久,估计Android Studio 2.0之后会好些。

示例

我用V2EX的API写了个简单的项目用来示例DataBinding的一些用法,采用了MVVM + DataBinding + RxJava

集成

在项目中集成DataBinding很容易,只要你的Android Studio版本>=1.3以及Android的Gradle插件版本>=1.5,然后在build.gradle中加入如下配置即可:

android {
    ....
    dataBinding {
        enabled = true
        // version = 1.1 // 可选,建议最好不加,使用Gradle插件默认的版本
    }
}

gradle插件会帮你下载DataBinding相关的依赖包,1.5版本的gradle插件默认集成的是1.0-rc5版本的DataBinding,version选项经实验最好不加,使用当前Gradle插件默认的DataBinding版本即可,否则会有些莫名奇妙的编译问题,比如我遇到这个问题。可以从jcenter上查看到DataBinding的最新版本。

一些用法

Layout相关

  • 除了java.lang.*包下的类,其他的类要么使用全名,要么需要import
<import type="android.view.View"/>
  • 可以在xml中使用静态方法:
android:text="@{Html.fromHtml(mainItem.content_rendered), default=无内容}"
  • 自定义Binding类名字,默认根据xml的名字生成
<data class="MainListBinding">
    ...
</data>
  • Include

如果xml中包含include标签,可以如下方式传递:

// activity_main.xml
<include
	id="@+id/empty_layout"
 	layout="@layout/empty_content"
 	app:isShowContent="@{viewModel.isShowContent}" />
 	
// empty_content.xml
<data>
   <variable
       name="isShowContent"
       type="boolean"/>
</data>

如果你需要引用被include布局中view元素,include标签的id属性不可少。

  • xml中带id属性的views都会在binding类中生成一个public final类型的引用,引用名即id属性值:
<TextView
	android:id="@+id/tv_username"
	android:layout_width="wrap_content"
	android:layout_height="wrap_content"/>
	
// 生成 public final android.widget.TextView tvUsername;

这样我们代码中就不需要findViewById()了,直接binding.tvUsername即可引用。

  • 空指针问题
android:text="@{mainItem.title, default=无标题}"/>

如果mainItem为空,不会报错,text的值是mainItem.title的默认值null

Observable

要想让对象的变化及时通知到view,则该对象必须实现Observable接口或者是一个ObservableFields

Adapter中的绑定

因为RecyclerView或者ListView等每个item的布局不一定相同,可能会产生多个Binding类,这种情况可以使用Binding的基类ViewDataBinding,然后在bind data到每个view时,如果data相同,则可使用setVariable(),如果data不同,可以根据每个view type的不同,强制转换ViewDataBinding至具体的Binding类,再调用其setXxx方法。可参见示例。

绑定变量的变化会自动调用属性的setter方法

比如TextViewandroid:text其实是去调用了其setText(String)方法,注意参数类型的匹配,因为可能有多个重载方法。而且哪怕一个控件并没有相应的属性,但是有setter方法,我们仍然可以这么写,比如示例中:

<android.support.v4.widget.SwipeRefreshLayout
  android:id="@+id/refresh_layout"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  app:refreshing="@{viewModel.isRefresh}"/>

SwipeRefreshLayout控件并没有refreshing这个属性,但是却有setRefresh(boolean)方法,用来控制刷新状态。可以点击菜单上的刷新来测验这个写法。

@BindingAdapter

因为并不是所有属性都有setter方法,这时候就需要用这个注解来自定义属性的逻辑。比如示例中:

<android.support.v7.widget.RecyclerView
	android:id="@+id/list_hot"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	app:contentList="@{viewModel.contentList}"/>

RecyclerView并没有contentList这个属性而且没有setContentList()方法,所以需要我们自定义:

@BindingAdapter("bind:contentList")
public static void setContentList(RecyclerView view, List<Topic> list) {
   MainAdapter adapter = (MainAdapter) view.getAdapter();
   adapter.addRows(list);
}

这样每次刷新时只要将返回数据set给viewModel.contentList就会调用上面的方法刷新adapter了。官网文档还有更详尽的@BindingAdapter例子。

总结

详细的文档还是直接参考官网吧,有的写法比如@BindingConversion,我的示例中并未用到,后面想到使用场景时再用。我发这篇博客时,最新版本已经到2.0.0-alpha9了,想必会和Android studio2.0一起出正式版,DataBinding能方便的实现MVVM模型,又有官方支持,应该会慢慢流行起来,所以建议可以在项目中尝试了。

前言

最近在项目中使用了Square家的Retrofit网络库,主要是为了配合RxJava,使用下来感觉还不错,这里稍微记录下。

我使用时最新版本是compile 'com.squareup.retrofit2:retrofit:2.0.0-beta2',按照Jake Wharton的说法虽然还是beta版但是接口已经相对稳定了,所以我们可以在项目中依赖它。由于我并没有在项目中使用过1.x版本,所以不会过多的与1.x版本比较。至于与其他网络库的对比,可自行Google,还是有很多文章的,比如stackoverflow上的这篇

初始化Retrofit实例

public static String BASE_URL = "http://example.com/";

OkHttpClient okHttpClient = new OkHttpClient();
HttpLoggingInterceptor loggingInterceptor = new HttpLoggingInterceptor();
loggingInterceptor.setLevel(HttpLoggingInterceptor.Level.BODY);
okHttpClient.interceptors().add(loggingInterceptor);

Retrofit retrofit = new Retrofit.Builder().baseUrl(BASE_URL).client(okHttpClient)
                .addConverterFactory(FastJsonConvertFactory.create())
                .addCallAdapterFactory(RxJavaCallAdapterFactory.create())
                .build();

Retrofit 2.0强制依赖okhttp,且不可替换。我们可以通过okhttp Interceptors对request和response进行一些监视或者处理,比如打印request和response,加入公用的http headers等。Interceptors是链式的,我们可以串接多个。

Converter会对request body和response body进行序列化和反序列化,它也可以添加多个,按照添加顺序,如果前面的Converter不能处理该种类型,则传递到下一个。一般我们服务端要求的请求和返回数据都是json格式,所以我们要添加一个JsonConverter进行对象的序列化和反序列化,Retrofit默认有几个写好的Converter,请参照官网。由于我在项目中采用fastjson解析json,而且需要对数据进行base64编码,所以需要自己写一个Converter。注意:json相关的Converter要放在最后,因为无法通过什么确切条件来判断一个对象是否是json对象,所以它会让所有类型的对象都通过。

CallAdapter原理跟Converter较像,默认一个请求方法会返回Call对象(可以参照源码中的DefaultCallAdapter)。我们也可以返回其他对象,比如返回RxJavaObservable对象,这时就需要添加一个自定义的CallAdapter,而Retrofit提供了对RxJava的支持。

定义请求接口

Retrofit使用特殊的注解来映射请求参数及请求方法等。

public interface Api {
	@GET
	Observable<QueryInfoResponse> queryInfo(@Url String queryInfoRequest);
	
	@GET("group/{id}/users")
  	Call<List<User>> groupList(@Path("id") int groupId, @Query("sort") String sort);
	
	@POST("login")
  	Observable<LoginResponse> login(@Body LoginRequest loginRequest);
}

关于Url的拼接,盗张图说明下吧:

url

所以建议是:

  • Base URL:总是以/结尾
  • @GET/POST等:不要以/开头

返回类型决定了使用哪一个CallAdapter

请求

Api api = retrofit.create(Api.class);
api.queryInfo("...");

下载

下载时ResponseBody返回的是一个流,相应接口如下:

public interface DownloadApi {
	@GET
	Observable<ResponseBody> downloadFile(@Url String downloadRequest);
}

downloadApi.downloadFile(url).map(new Func1<ResponseBody, File>() {
  @Override
  public File call(ResponseBody responseBody) {
      FileOutputStream outputStream = null;
      try {
          byte[] bytes = responseBody.bytes();
          File file = new File(context.getExternalFilesDir(null), "fileName");
          outputStream = new FileOutputStream(file);
          outputStream.write(bytes);
          return file;
      } catch (IOException e) {
          e.printStackTrace();
      } finally {
          if (outputStream != null) {
              try {
                  outputStream.close();
              } catch (IOException e) {
                  e.printStackTrace();
              }
          }
      }
      return null;
  }
}).subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread());

上传文件

上传文件时是multipart请求,相应接口如下:

public interface UploadApi {
	@Multipart
  	@POST(Constants.ADD)
  	Observable<BlankDataResponse> add(@PartMap Map<String, RequestBody> 	params);
}

@PartMap代表由boundary分隔的每一部分,对于text/plain类型的part,可用下面的方法生成Part

Map<String, RequestBody> params = new HashMap<>();
RequestBody requestBody = RequestBody.create(MediaType.parse("text/plain"), "token");
params.put("token", requestBody);

对于图片类型则可以如下:

File file = new File(path);
RequestBody requestBody = RequestBody.create(MediaType.parse("image/jpeg"), file);
params.put("AttachmentKey\"; filename=\"" + image.getFileName(), requestBody);

上面的代码对应于Http RequesBody内容如下:

–88fc3b38-77d8-4ec3-b057-35600d14f0b3
Content-Disposition: form-data; name=”AttachmentKey”; filename=”example.jpg”
Content-Transfer-Encoding: binary
Content-Type: image/jpeg
Content-Length: 169913

其他

  • 如果你想在Http Headers里加入公用的Header,可以如下做:
// 自定义okhttp的Interceptor
public class RequestInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request().newBuilder().addHeader("version", 				"1.0.0").build();
        return chain.proceed(request);
    }
}

// 加入链中
RequestInterceptor requestInterceptor = new RequestInterceptor();
okHttpClient.interceptors().add(requestInterceptor);
  • 取消正在进行的网络请求

对于返回类型是Call的请求,调用Call.cancel()即可。

对于返回类型是Observable的请求,调用你Subscriberunsubscribe()即可,它最终还是会去调用Call.cancel()方法。