章节内容

一. Android Hook 系列教程(一) Xposed Hook 原理分析

二. Android Hook 系列教程(二) 自己写APK实现Hook Java层函数

三. Android Hook 系列教程(三) Cydia Hook Native 原理分析

四. Android Hook 系列教程(四) 自己写APK实现Hook Native层函数

五. Android Hook 系列教程(五) 更多Hook方法

六. Andoird Hook 系列教程(六) Hook的总结

源代码下载

Java层:https://github.com/rovo89/XposedBridge

Native层:https://github.com/rovo89/Xposed

开始

一个好的开始等于成功了一半.

为了分析Xposed Hook是怎么实现的,选取了findAndHookMethod函数作为研究对象.

在分析过程中,我们应该做到有详有略,我将以关键代码(略过对分析目的无关紧要的部分,留下必须部分).

当然在关键代码后面会贴出全部代码以供参照.

Jave层分析

findAndHookMethod

从源代码:XposedHelpers.java处找到findAndHookMethod

findAndHookMethod关键代码

public static XC_MethodHook.Unhook findAndHookMethod(Class> clazz, String methodName, Object... parameterTypesAndCallback) {

...

XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length-1];//获取回调函数

Method m = findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback));//获取Method

return XposedBridge.hookMethod(m, callback);

}

public static XC_MethodHook.Unhook findAndHookMethod(Class> clazz, String methodName, Object... parameterTypesAndCallback) {

if (parameterTypesAndCallback.length == 0 || !(parameterTypesAndCallback[parameterTypesAndCallback.length-1] instanceof XC_MethodHook))

throw new IllegalArgumentException("no callback defined");

XC_MethodHook callback = (XC_MethodHook) parameterTypesAndCallback[parameterTypesAndCallback.length-1];//获取回调函数

Method m = findMethodExact(clazz, methodName, getParameterClasses(clazz.getClassLoader(), parameterTypesAndCallback));//获取Method

return XposedBridge.hookMethod(m, callback);

}

上面的代码都很容易懂,简单说一下几个函数

getParameterClasses

功能:把函数所有参数转换为Class>数组返回.

比如参数列表为

String.class,int.class,"java.util.Map",回调函数

返回结果相当于

Class> []={

String.class,

int.class,

Map.class

};

findMethodExact

功能:根据方法名和方法参数类获取Method

关键代码:

public static Method findMethodExact(Class> clazz, String methodName, Class>... parameterTypes) {

...

Method method = clazz.getDeclaredMethod(methodName, parameterTypes);//获取方法声明

method.setAccessible(true);

methodCache.put(fullMethodName, method);

return method;

}

完整代码:

public static Method findMethodExact(Class> clazz, String methodName, Class>... parameterTypes) {

String fullMethodName = clazz.getName() + '#' + methodName + getParametersString(parameterTypes) + "#exact";

if (methodCache.containsKey(fullMethodName)) {

Method method = methodCache.get(fullMethodName);

if (method == null)

throw new NoSuchMethodError(fullMethodName);

return method;

}

try {

Method method = clazz.getDeclaredMethod(methodName, parameterTypes);//获取方法声明

method.setAccessible(true);

methodCache.put(fullMethodName, method);

return method;

} catch (NoSuchMethodException e) {

methodCache.put(fullMethodName, null);

throw new NoSuchMethodError(fullMethodName);

}

}

让我们继续跟进hookMethod

hookMethod

功能:把方法的参数类型,返回类型,回调函数记录到AdditionalHookInfo类里

并通过hookMethodNative方法进入Native层进行Hook.

在这个方法里面差不多都是关键代码,也就不省略,全部贴出.

​ 完整代码:

public static XC_MethodHook.Unhook hookMethod(Member hookMethod, XC_MethodHook callback) {

/*检测是否是支持的Method类型*/

if (!(hookMethod instanceof Method) && !(hookMethod instanceof Constructor>)) {

throw new IllegalArgumentException("Only methods and constructors can be hooked: " + hookMethod.toString());

} else if (hookMethod.getDeclaringClass().isInterface()) {

throw new IllegalArgumentException("Cannot hook interfaces: " + hookMethod.toString());

} else if (Modifier.isAbstract(hookMethod.getModifiers())) {

throw new IllegalArgumentException("Cannot hook abstract methods: " + hookMethod.toString());

}

boolean newMethod = false;

CopyOnWriteSortedSet callbacks;

synchronized (sHookedMethodCallbacks) {

callbacks = sHookedMethodCallbacks.get(hookMethod);

if (callbacks == null) {//如果为null则函数没有被Hook,否则已经Hook

callbacks = new CopyOnWriteSortedSet<>();

sHookedMethodCallbacks.put(hookMethod, callbacks);

newMethod = true;

}

}

callbacks.add(callback);//记录回调函数

if (newMethod) {//建立新Hook

Class> declaringClass = hookMethod.getDeclaringClass();//获取类声明

int slot;

Class>[] parameterTypes;

Class> returnType;

if (runtime == RUNTIME_ART) {//判断是否是ART模式

slot = 0;

parameterTypes = null;

returnType = null;

} else if (hookMethod instanceof Method) {//实例方法

slot = getIntField(hookMethod, "slot");//获取slot

parameterTypes = ((Method) hookMethod).getParameterTypes();//获取参数类型Class>数组 和前面相同

returnType = ((Method) hookMethod).getReturnType();//获取返回类型

} else {//构造函数

slot = getIntField(hookMethod, "slot");//获取slot

parameterTypes = ((Constructor>) hookMethod).getParameterTypes();//获取参数类型Class>数组 和前面相同

returnType = null;//构造参数返回类型为null

}

//把回调函数,参数类型,返回类型 记录在一个AdditionalHookInfo类里

AdditionalHookInfo additionalInfo = new AdditionalHookInfo(callbacks, parameterTypes, returnType);

//Native方法

hookMethodNative(hookMethod, declaringClass, slot, additionalInfo);

}

return callback.new Unhook(hookMethod);

}

看一下AdditionalHookInfo构造函数

private AdditionalHookInfo(CopyOnWriteSortedSet callbacks, Class>[] parameterTypes, Class> returnType) {

this.callbacks = callbacks;

this.parameterTypes = parameterTypes;

this.returnType = returnType;

}

Native层分析

hookMethodNativevoid XposedBridge_hookMethodNative(JNIEnv* env, jclass clazz, jobject reflectedMethodIndirect,

jobject declaredClassIndirect, jint slot, jobject additionalInfoIndirect) {

// Usage errors?

if (declaredClassIndirect == NULL || reflectedMethodIndirect == NULL) {

dvmThrowIllegalArgumentException("method and declaredClass must not be null");

return;

}

// Find the internal representation of the method

ClassObject* declaredClass = (ClassObject*) dvmDecodeIndirectRef(dvmThreadSelf(), declaredClassIndirect);

Method* method = dvmSlotToMethod(declaredClass, slot);

if (method == NULL) {

dvmThrowNoSuchMethodError("Could not get internal representation for method");

return;

}

if (isMethodHooked(method)) {

// already hooked

return;

}

// Save a copy of the original method and other hook info

XposedHookInfo* hookInfo = (XposedHookInfo*) calloc(1, sizeof(XposedHookInfo));

memcpy(hookInfo, method, sizeof(hookInfo->originalMethodStruct));

hookInfo->reflectedMethod = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(reflectedMethodIndirect));

hookInfo->additionalInfo = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(additionalInfoIndirect));

// Replace method with our own code

SET_METHOD_FLAG(method, ACC_NATIVE);

method->nativeFunc = &hookedMethodCallback;

method->insns = (const u2*) hookInfo;

method->registersSize = method->insSize;

method->outsSize = 0;

if (PTR_gDvmJit != NULL) {

// reset JIT cache

char currentValue = *((char*)PTR_gDvmJit + MEMBER_OFFSET_VAR(DvmJitGlobals,codeCacheFull));

if (currentValue == 0 || currentValue == 1) {

MEMBER_VAL(PTR_gDvmJit, DvmJitGlobals, codeCacheFull) = true;

} else {

ALOGE("Unexpected current value for codeCacheFull: %d", currentValue);

}

}

}

详细分析:

得到Native层的Method

ClassObject* declaredClass = (ClassObject*) dvmDecodeIndirectRef(dvmThreadSelf(), declaredClassIndirect);

Method* method = dvmSlotToMethod(declaredClass, slot);

从Android源代码的Dalvik虚拟机代码里找到dvmSlotToMethod实现如下

实现非常简单,所以slot相等于记录Method的下标

Method* dvmSlotToMethod(ClassObject* clazz, int slot)

{

if (slot < 0) {

slot = -(slot+1);

assert(slot < clazz->directMethodCount);

return &clazz->directMethods[slot];

} else {

assert(slot < clazz->virtualMethodCount);

return &clazz->virtualMethods[slot];

}

}

Method结构

struct Method {

/* the class we are a part of */

ClassObject* clazz;

/* access flags; low 16 bits are defined by spec (could be u2?) */

u4 accessFlags;

/*

* For concrete virtual methods, this is the offset of the method

* in "vtable".

*

* For abstract methods in an interface class, this is the offset

* of the method in "iftable[n]->methodIndexArray".

*/

u2 methodIndex;

/*

* Method bounds; not needed for an abstract method.

*

* For a native method, we compute the size of the argument list, and

* set "insSize" and "registerSize" equal to it.

*/

u2 registersSize; /* ins + locals */

u2 outsSize;

u2 insSize;

/* method name, e.g. "" or "eatLunch" */

const char* name;

/*

* Method prototype descriptor string (return and argument types).

*

* TODO: This currently must specify the DexFile as well as the proto_ids

* index, because generated Proxy classes don't have a DexFile. We can

* remove the DexFile* and reduce the size of this struct if we generate

* a DEX for proxies.

*/

DexProto prototype;

/* short-form method descriptor string */

const char* shorty;

/*

* The remaining items are not used for abstract or native methods.

* (JNI is currently hijacking "insns" as a function pointer, set

* after the first call. For internal-native this stays null.)

*/

/* the actual code */

const u2* insns; /* instructions, in memory-mapped .dex */

/* JNI: cached argument and return-type hints */

int jniArgInfo;

/*

* JNI: native method ptr; could be actual function or a JNI bridge. We

* don't currently discriminate between DalvikBridgeFunc and

* DalvikNativeFunc; the former takes an argument superset (i.e. two

* extra args) which will be ignored. If necessary we can use

* insns==NULL to detect JNI bridge vs. internal native.

*/

DalvikBridgeFunc nativeFunc;

/*

* JNI: true if this static non-synchronized native method (that has no

* reference arguments) needs a JNIEnv* and jclass/jobject. Libcore

* uses this.

*/

bool fastJni;

/*

* JNI: true if this method has no reference arguments. This lets the JNI

* bridge avoid scanning the shorty for direct pointers that need to be

* converted to local references.

*

* TODO: replace this with a list of indexes of the reference arguments.

*/

bool noRef;

/*

* JNI: true if we should log entry and exit. This is the only way

* developers can log the local references that are passed into their code.

* Used for debugging JNI problems in third-party code.

*/

bool shouldTrace;

/*

* Register map data, if available. This will point into the DEX file

* if the data was computed during pre-verification, or into the

* linear alloc area if not.

*/

const RegisterMap* registerMap;

/* set if method was called during method profiling */

bool inProfile;

};

保存一些Hook信息

XposedHookInfo* hookInfo = (XposedHookInfo*) calloc(1, sizeof(XposedHookInfo));

memcpy(hookInfo, method, sizeof(hookInfo->originalMethodStruct));

hookInfo->reflectedMethod = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(reflectedMethodIndirect));

hookInfo->additionalInfo = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(additionalInfoIndirect));

替换方法(最关键的一步)

SET_METHOD_FLAG(method, ACC_NATIVE);

method->nativeFunc = &hookedMethodCallback;//替换方法为这个

method->insns = (const u2*) hookInfo;

method->registersSize = method->insSize;

method->outsSize = 0;

到这里我们已经分析完了,就是保存一些信息,并把方法替换为hookedMethodCallback.

hookedMethodCallbackvoid hookedMethodCallback(const u4* args, JValue* pResult, const Method* method, ::Thread* self) {

if (!isMethodHooked(method)) {

dvmThrowNoSuchMethodError("Could not find Xposed original method - how did you even get here?");

return;

}

XposedHookInfo* hookInfo = (XposedHookInfo*) method->insns;

Method* original = (Method*) hookInfo;

Object* originalReflected = hookInfo->reflectedMethod;

Object* additionalInfo = hookInfo->additionalInfo;

// convert/box arguments

const char* desc = &method->shorty[1]; // [0] is the return type.

Object* thisObject = NULL;

size_t srcIndex = 0;

size_t dstIndex = 0;

// for non-static methods determine the "this" pointer

if (!dvmIsStaticMethod(original)) {

thisObject = (Object*) args[0];

srcIndex++;

}

ArrayObject* argsArray = dvmAllocArrayByClass(objectArrayClass, strlen(method->shorty) - 1, ALLOC_DEFAULT);

if (argsArray == NULL) {

return;

}

//循环获取参数

while (*desc != '\0') {

char descChar = *(desc++);

JValue value;

Object* obj;

switch (descChar) {

case 'Z':

case 'C':

case 'F':

case 'B':

case 'S':

case 'I':

value.i = args[srcIndex++];

obj = (Object*) dvmBoxPrimitive(value, dvmFindPrimitiveClass(descChar));

dvmReleaseTrackedAlloc(obj, self);

break;

case 'D':

case 'J':

value.j = dvmGetArgLong(args, srcIndex);

srcIndex += 2;

obj = (Object*) dvmBoxPrimitive(value, dvmFindPrimitiveClass(descChar));

dvmReleaseTrackedAlloc(obj, self);

break;

case '[':

case 'L':

obj = (Object*) args[srcIndex++];

break;

default:

ALOGE("Unknown method signature description character: %c", descChar);

obj = NULL;

srcIndex++;

}

setObjectArrayElement(argsArray, dstIndex++, obj);//把获取的参数加入数组

}

// call the Java handler function

JValue result;

dvmCallMethod(self, (Method*) methodXposedBridgeHandleHookedMethod, NULL, &result,

originalReflected, (int) original, additionalInfo, thisObject, argsArray);

dvmReleaseTrackedAlloc(argsArray, self);

// exceptions are thrown to the caller

if (dvmCheckException(self)) {

return;

}

// return result with proper type

ClassObject* returnType = dvmGetBoxedReturnType(method);

if (returnType->primitiveType == PRIM_VOID) {

// ignored

} else if (result.l == NULL) {

if (dvmIsPrimitiveClass(returnType)) {

dvmThrowNullPointerException("null result when primitive expected");

}

pResult->l = NULL;

} else {

if (!dvmUnboxPrimitive(result.l, returnType, pResult)) {

dvmThrowClassCastException(result.l->clazz, returnType);

}

}

}

void XposedBridge_hookMethodNative(JNIEnv* env, jclass clazz, jobject reflectedMethodIndirect,

jobject declaredClassIndirect, jint slot, jobject additionalInfoIndirect) {

// Usage errors?

if (declaredClassIndirect == NULL || reflectedMethodIndirect == NULL) {

dvmThrowIllegalArgumentException("method and declaredClass must not be null");

return;

}

// Find the internal representation of the method

ClassObject* declaredClass = (ClassObject*) dvmDecodeIndirectRef(dvmThreadSelf(), declaredClassIndirect);

Method* method = dvmSlotToMethod(declaredClass, slot);

if (method == NULL) {

dvmThrowNoSuchMethodError("Could not get internal representation for method");

return;

}

if (isMethodHooked(method)) {

// already hooked

return;

}

// Save a copy of the original method and other hook info

XposedHookInfo* hookInfo = (XposedHookInfo*) calloc(1, sizeof(XposedHookInfo));

memcpy(hookInfo, method, sizeof(hookInfo->originalMethodStruct));

hookInfo->reflectedMethod = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(reflectedMethodIndirect));

hookInfo->additionalInfo = dvmDecodeIndirectRef(dvmThreadSelf(), env->NewGlobalRef(additionalInfoIndirect));

// Replace method with our own code

SET_METHOD_FLAG(method, ACC_NATIVE);

method->nativeFunc = &hookedMethodCallback;

method->insns = (const u2*) hookInfo;

method->registersSize = method->insSize;

method->outsSize = 0;

if (PTR_gDvmJit != NULL) {

// reset JIT cache

char currentValue = *((char*)PTR_gDvmJit + MEMBER_OFFSET_VAR(DvmJitGlobals,codeCacheFull));

if (currentValue == 0 || currentValue == 1) {

MEMBER_VAL(PTR_gDvmJit, DvmJitGlobals, codeCacheFull) = true;

} else {

ALOGE("Unexpected current value for codeCacheFull: %d", currentValue);

}

}

}

相信到了这里读者已经可以根据注释自己看懂了,我在把流程梳理一下

获取原先保存的Hook信息

获取参数

调用Java层函数

dvmCallMethod(self, (Method*) methodXposedBridgeHandleHookedMethod, NULL, &result,

originalReflected, (int) original, additionalInfo, thisObject, argsArray);

dvmReleaseTrackedAlloc(argsArray, self);

看一下参数methodXposedBridgeHandleHookedMethod是什么,可以看到是上层的Java

#define CLASS_XPOSED_BRIDGE "de/robv/android/xposed/XposedBridge"

classXposedBridge = env->FindClass(CLASS_XPOSED_BRIDGE);

jmethodID methodXposedBridgeHandleHookedMethod = NULL;

methodXposedBridgeHandleHookedMethod = env->GetStaticMethodID(classXposedBridge, "handleHookedMethod",

"(Ljava/lang/reflect/Member;ILjava/lang/Object;Ljava/lang/Object;[Ljava/lang/Object;)Ljava/lang/Object;");

可以看到调用Java层的函数为XposedBridge.handleHookedMethod

Java层函数handleHookedMethod

功能:

调用Hook该方法的所有beforeHookedMethod

调用原始方法

调用Hook该方法的所有afterHookedMethod

关键代码

private static Object handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj,

Object thisObject, Object[] args) throws Throwable {

AdditionalHookInfo additionalInfo = (AdditionalHookInfo) additionalInfoObj;

MethodHookParam param = new MethodHookParam();

param.method = method;

param.thisObject = thisObject;

param.args = args;

// call "before method" callbacks

int beforeIdx = 0;

do {

try {

((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param);

} catch (Throwable t) {

XposedBridge.log(t);

// reset result (ignoring what the unexpectedly exiting callback did)

param.setResult(null);

param.returnEarly = false;

continue;

}

if (param.returnEarly) {

// skip remaining "before" callbacks and corresponding "after" callbacks

beforeIdx++;

break;

}

} while (++beforeIdx < callbacksLength);

// call original method if not requested otherwise

if (!param.returnEarly) {

try {

param.setResult(invokeOriginalMethodNative(method, originalMethodId,

additionalInfo.parameterTypes, additionalInfo.returnType, param.thisObject, param.args));

} catch (InvocationTargetException e) {

param.setThrowable(e.getCause());

}

}

// call "after method" callbacks

int afterIdx = beforeIdx - 1;

do {

Object lastResult = param.getResult();

Throwable lastThrowable = param.getThrowable();

try {

((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param);

} catch (Throwable t) {

XposedBridge.log(t);

// reset to last result (ignoring what the unexpectedly exiting callback did)

if (lastThrowable == null)

param.setResult(lastResult);

else

param.setThrowable(lastThrowable);

}

} while (--afterIdx >= 0);

// return

if (param.hasThrowable())

throw param.getThrowable();

else

return param.getResult();

}

完整代码

private static Object handleHookedMethod(Member method, int originalMethodId, Object additionalInfoObj,

Object thisObject, Object[] args) throws Throwable {

AdditionalHookInfo additionalInfo = (AdditionalHookInfo) additionalInfoObj;

if (disableHooks) {

try {

return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes,

additionalInfo.returnType, thisObject, args);

} catch (InvocationTargetException e) {

throw e.getCause();

}

}

Object[] callbacksSnapshot = additionalInfo.callbacks.getSnapshot();

final int callbacksLength = callbacksSnapshot.length;

if (callbacksLength == 0) {

try {

return invokeOriginalMethodNative(method, originalMethodId, additionalInfo.parameterTypes,

additionalInfo.returnType, thisObject, args);

} catch (InvocationTargetException e) {

throw e.getCause();

}

}

MethodHookParam param = new MethodHookParam();

param.method = method;

param.thisObject = thisObject;

param.args = args;

// call "before method" callbacks

int beforeIdx = 0;

do {

try {

((XC_MethodHook) callbacksSnapshot[beforeIdx]).beforeHookedMethod(param);

} catch (Throwable t) {

XposedBridge.log(t);

// reset result (ignoring what the unexpectedly exiting callback did)

param.setResult(null);

param.returnEarly = false;

continue;

}

if (param.returnEarly) {

// skip remaining "before" callbacks and corresponding "after" callbacks

beforeIdx++;

break;

}

} while (++beforeIdx < callbacksLength);

// call original method if not requested otherwise

if (!param.returnEarly) {

try {

param.setResult(invokeOriginalMethodNative(method, originalMethodId,

additionalInfo.parameterTypes, additionalInfo.returnType, param.thisObject, param.args));

} catch (InvocationTargetException e) {

param.setThrowable(e.getCause());

}

}

// call "after method" callbacks

int afterIdx = beforeIdx - 1;

do {

Object lastResult = param.getResult();

Throwable lastThrowable = param.getThrowable();

try {

((XC_MethodHook) callbacksSnapshot[afterIdx]).afterHookedMethod(param);

} catch (Throwable t) {

XposedBridge.log(t);

// reset to last result (ignoring what the unexpectedly exiting callback did)

if (lastThrowable == null)

param.setResult(lastResult);

else

param.setThrowable(lastThrowable);

}

} while (--afterIdx >= 0);

// return

if (param.hasThrowable())

throw param.getThrowable();

else

return param.getResult();

}

好了,到这里我们已经分析完成.

简单总结一下:

Java层:

记录需要Hook方法的信息包括:参数数组,返回值类型,回调函数.并且记录在AdditionalHookInfo类里面

获取slot,声明方法的类

把这些信息通过Native函数:hookMethodNative进行Hook

Native层

替换Native层Method的字段nativeFunc为函数 hookedMethodCallback完成Hook

hookedMethodCallback里回调Java层函数XposedBridge.handleHookedMethod.

handleHookedMethod里分别调用beforeHookedMethod,原始方法,afterHookedMethod

xposed hook java_[原创]Android Hook 系列教程(一) Xposed Hook 原理分析相关推荐

  1. [置顶] 【稀饭】react native 实战系列教程之热更新原理分析与实现

    很多人在技术选型的时候,会选择RN是因为它具有热更新,而且这是它的一个特性,所以实现起来会相对比较简单,不像原生那样,原生的热更新是一个大工程.那就目前来看,RN的热更新方案已有的,有微软的CodeP ...

  2. react native 实战系列教程之热更新原理分析与实现

    很多人在技术选型的时候,会选择RN是因为它具有热更新,而且这是它的一个特性,所以实现起来会相对比较简单,不像原生那样,原生的热更新是一个大工程.那就目前来看,RN的热更新方案已有的,有微软的CodeP ...

  3. [转]Android Studio系列教程六--Gradle多渠道打包

    转自:http://www.stormzhang.com/devtools/2015/01/15/android-studio-tutorial6/ Android Studio系列教程六--Grad ...

  4. Android新手系列教程(申明:来源于网络)

    Android新手系列教程(申明:来源于网络) 地址:http://blog.csdn.net/column/details/androidcoder666.html 转载于:https://www. ...

  5. Android Studio系列教程三:快捷键

    原文出处:http://stormzhang.com/devtools/2014/12/09/android-studio-tutorial3/ Android Studio 1.0正式版发布啦 今天 ...

  6. 史上最详细的Android Studio系列教程四--Gradle基础

    史上最详细的Android Studio系列教程四--Gradle基础 转载于:https://www.cnblogs.com/zhujiabin/p/5125917.html

  7. android studio代码教程,史上最详细的Android Studio系列教程三

    Android Studio 1.0正式版发布啦 今天是个大日子,Android Studio 1.0 终于发布了正式版, 这对于Android开发者来说简直是喜大普奔的大消息啊,那么就果断来下载使用 ...

  8. ClickHouse系列教程三:MergeTree引擎分析

    ClickHouse系列教程: ClickHouse系列教程 Clickhouse之MergeTree引擎分析 CRUD Clickhouse支持查询(select)和增加(insert),但是不直接 ...

  9. Android Jetpack组件ViewModel基本使用和原理分析

    本文整体流程:首先要知道什么是 ViewModel,然后演示一个例子,来看看 ViewModel 是怎么使用的,接着提出问题为什么是这样的,最后读源码来解释原因! 1.什么是ViewModel 1.1 ...

最新文章

  1. python tcp server分包_如何创建线程池来监听tcpserver包python
  2. 传蔚来计划回国内科创板上市,关闭硅谷办公室
  3. minicom使用总结
  4. 关掉ajax 的异步,asp.net ajax 取消异步回送
  5. ImageWatch的使用
  6. 第四单元和课程总结:简单的架构设计意识
  7. Hexo中Next主题个性化美化的解决方案
  8. Unity3D之NGUI基础6.1:按钮交互
  9. oracle中sysdate函数 ro,ORACLE常用函數
  10. Redis数据类型(上)
  11. js获取当前页面url信息
  12. 富士通Fujitsu DPK9500GA Pro 打印机驱动
  13. 首份2020信创报告出炉,四大巨头市场格局立现(附全文下载)
  14. 计算机不能删除用户,删除用户时提示无法在内置账户上运行此操作 -电脑资料...
  15. 设计一个简单的基于三层交换技术的校园网络——计算机网络课程设计
  16. Zedboard(一)开发环境Vivado
  17. 如何练就超强的学习能力?这才是最好的答案
  18. 广东迅视资管 别让“顺风车”再度行驶至安全边缘
  19. C/C++实现双目矫正(不使用OpenCV内部函数)及矫正源码解析
  20. 【文献阅读1】Comparative cytological and transcriptomic analysis of pollen development in autotetraploid a

热门文章

  1. hhvm php5.6,PHP_5.5_/_PHP5.6_/_PHP-NG_和_HHVM_哪个性能更好?
  2. 案例详解:Linux文件系统异常导致数据库文件无法访问
  3. 实战课堂:一则CPU 100%的故障分析处理知识和警示
  4. 图解带你掌握`JVM`运行时核心内存区
  5. 从结构体、内存池初始化到申请释放,详细解读鸿蒙轻内核的动态内存管理
  6. 能够让机器狗学会灭火, ModelArts3.0让AI离我们又近一步
  7. 授人以渔:stm32资料查询技巧
  8. 【华为云技术分享】云容器引擎 CCE权限管理实践
  9. href up test.php,test.php
  10. dell r230u盘启动安装2008_dell r230服务器 怎么u盘开启