각 핫 패치 방안의 분석과 비교

30175 단어 androidHotpatch
최근 개원계에 핫패치 프로젝트가 쏟아져 나오고 있지만 방안상으로는 덱스포이드, 앤드픽스, 클라스로드(원래는 QZone, 현재 타오바오의 엔지니어 진종, 15년 초부터 실현된 것) 세 가지가 포함된다.앞의 두 가지는 모두 알리바바 내부의 서로 다른 팀(타오바오와 알리페이)이고 후자는 텐센트의 QQ공간팀에서 왔다.
개원계는 종종 하나의 방안이 여러 가지 실현될 수 있다(예를 들어 ClassLoader 방안은 이미 세 가지가 실현되지 않았다). 그러나 이 세 가지 방안의 원리는 다르다. 그러면 이 세 가지 방안의 원리와 각자의 장단점을 살펴보자.
Dexposed
Xposed 기반의 AOP 프레임워크로 방법급 입도로 AOP 프로그래밍, 플러그, 핫패치, SDK hook 등의 기능을 할 수 있다.
Xposed에는 루트 권한이 필요합니다. 다른 응용 프로그램, 시스템의 행동을 수정해야 하기 때문에 하나의 응용 프로그램에 루트가 필요하지 않습니다.Xposed는 안드로이드 Dalvik이 실행될 때의 Zygote 프로세스를 수정하고 Xposed Bridge를 사용하여 hook 방법을 사용하고 자신의 코드를 주입하여 비침입적인runtime 수정을 실현합니다.예를 들어 잠자리 fm와 히말라야가 하는 일은 사실 이런 장면에 적합하다. 다른 사람들이 시장에서 다운로드한 코드를 반컴파일하는 것은 패치를 볼 수 없는 행위이다.샤오미(onVmCreated에 아직 샤오미가 자원 처리를 하지 않았다)도 dexposed를 중용하여 사용자 정의 주제를 만드는 기능, 그리고 몰입식 상태바 등을 많이 만들었다.
우리는 응용 프로그램이 시작될 때 forkzygote 프로세스를 불러오고class와 invoke의 각종 초기화 방법을 불러오는 것을 알고 있다. Xposed는 바로 이 과정에서 app 를 교체한 것이다.프로세스,hook은 각종 입구급 방법(예를 들어handle Bind Application,ServerThread,Activity Thread,Application Package Manager의get Resources For Application 등)을 사용하여 Xposed Bridge를 불러옵니다.jar는 동적 훅 기반을 제공합니다.
구체적인 방법은 XposedBridge:
1
2
3
4
5
/**
 * Intercept every call to the specified method and call a handler function instead.
 * @param method The method to intercept
 */
private native synchronized static void hookMethodNative(Member method, Class<?> declaringClass, int slot, Object additionalInfo);

그 구체적인native 구현은 Xposed의libxposedcommon.cpp에 등록되어 있으며 시스템 버전에 따라libxposedDalvik 및 libxposedart에서 Dalvik을 예로 들면 대체적으로 원래의 방법 정보를 기록하고 방법 지침을 우리의 hooked Method Callback을 가리키며 차단하는 목적을 실현한다.
방법급의 교체는 방법 전, 방법 후 코드를 삽입하거나 직접 교체할 수 있는 방법을 말한다.자바 방법만 차단할 수 있고 C 방법은 지원되지 않습니다.
딱딱한 상처를 말해 보세요. 하트가 지원되지 않습니다. 하트가 지원되지 않습니다. 하트가 지원되지 않습니다.중요한 일은 세 번 말해야 한다.6월 프로젝트 사이트의 로드맵은 7, 8월에 하트를 지원한다고 썼지만 아직은 하트의 호환성을 해결할 수 없는 것이 사실이다.
또한 온라인release 버전이 혼동되면 패치를 쓰는 것도 고통스러운 일이다. 반사+내부류, 가방 이름과 내부류의 이름이 충돌할 수 있다. 한마디로 고통스럽게 쓴 것이다.
AndFix
같은 방법의 훅에서 AndFix는 Dexposed가 Method에서 시작하지 않고 Field를 삽입점으로 한다.
Java 포털 보기AndFixManager.fix:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
/**
 * fix
 *
 * @param file patch file
 * @param classLoader classloader of class that will be fixed
 * @param classes classes will be fixed
 */
public synchronized void fix(File file, ClassLoader classLoader, List<String> classes) {
		//   ...      ,    ,     dex  

		ClassLoader patchClassLoader = new ClassLoader(classLoader) {
			@Override
			protected Class<?> findClass(String className) throws ClassNotFoundException {
				Class<?> clazz = dexFile.loadClass(className, this);
				if (clazz == null && className.startsWith("com.alipay.euler.andfix")) {
					return Class.forName(className);// annotation’s class not found
				}
				if (clazz == null) {
					throw new ClassNotFoundException(className);
				}
				return clazz;
			}
		};
		Enumeration<String> entrys = dexFile.entries();
		Class<?> clazz = null;
		while (entrys.hasMoreElements()) {
			String entry = entrys.nextElement();
			if (classes != null && !classes.contains(entry)) {
				continue;// skip, not need fix
			}
      //    ,    class
			clazz = dexFile.loadClass(entry, patchClassLoader);
			if (clazz != null) {
				fixClass(clazz, classLoader);
			}
		}
	} catch (IOException e) {
		Log.e(TAG, "pacth", e);
	}
}

보아하니 최종적으로fix는fixClass :
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
private void fixClass(Class<?> clazz, ClassLoader classLoader) {
  Method[] methods = clazz.getDeclaredMethods();
  MethodReplace methodReplace;
  String clz;
  String meth;
  //     class    ,      ,annotation            
  for (Method method : methods) {
    methodReplace = method.getAnnotation(MethodReplace.class);
    if (methodReplace == null)
      continue;
    clz = methodReplace.clazz();
    meth = methodReplace.method();
    if (!isEmpty(clz) && !isEmpty(meth)) {
      replaceMethod(classLoader, clz, meth, method);
    }
  }
}

private void replaceMethod(ClassLoader classLoader, String clz, String meth, Method method) {
  try {
    String key = clz + "@" + classLoader.toString();
    Class<?> clazz = mFixedClass.get(key);
    if (clazz == null) {// class not load
      //      class
      Class<?> clzz = classLoader.loadClass(clz);
      //        ,  C ,  accessFlags,            (Field)   public,     Method   
      clazz = AndFix.initTargetClass(clzz);
    }
    if (clazz != null) {// initialize class OK
      mFixedClass.put(key, clazz);
      //         
      Method src = clazz.getDeclaredMethod(meth, method.getParameterTypes());
      //       jni,art dalvik           , cpp    
      AndFix.addReplaceMethod(src, method);
    }
  } catch (Exception e) {
    Log.e(TAG, "replaceMethod", e);
  }
}

Dalvik와art에서 시스템의 호출은 다르지만 원리는 유사하다. 여기서 우리는 신선한 것을 맛본다. 6.0을 예로 들면art_method_replace_6_0:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
//        
void replace_6_0(JNIEnv* env, jobject src, jobject dest) {
	art::mirror::ArtMethod* smeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(src);
	art::mirror::ArtMethod* dmeth = (art::mirror::ArtMethod*) env->FromReflectedMethod(dest);

	dmeth->declaring_class_->class_loader_ =
			smeth->declaring_class_->class_loader_; //for plugin classloader
	dmeth->declaring_class_->clinit_thread_id_ =
			smeth->declaring_class_->clinit_thread_id_;
	dmeth->declaring_class_->status_ = smeth->declaring_class_->status_-1;

  //                  
	smeth->declaring_class_ = dmeth->declaring_class_;
	smeth->dex_cache_resolved_types_ = dmeth->dex_cache_resolved_types_;
	smeth->access_flags_ = dmeth->access_flags_;
	smeth->dex_cache_resolved_methods_ = dmeth->dex_cache_resolved_methods_;
	smeth->dex_code_item_offset_ = dmeth->dex_code_item_offset_;
	smeth->method_index_ = dmeth->method_index_;
	smeth->dex_method_index_ = dmeth->dex_method_index_;

  //            
	smeth->ptr_sized_fields_.entry_point_from_interpreter_ =
			dmeth->ptr_sized_fields_.entry_point_from_interpreter_;
	smeth->ptr_sized_fields_.entry_point_from_jni_ =
			dmeth->ptr_sized_fields_.entry_point_from_jni_;
	smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_ =
			dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_;

	LOGD("replace_6_0: %d , %d",
			smeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_,
			dmeth->ptr_sized_fields_.entry_point_from_quick_compiled_code_);
}

//         ,      public ,       jni       ,java   c         
void setFieldFlag_6_0(JNIEnv* env, jobject field) {
	art::mirror::ArtField* artField =
			(art::mirror::ArtField*) env->FromReflectedField(field);
	artField->access_flags_ = artField->access_flags_ & (~0x0002) | 0x0001;
	LOGD("setFieldFlag_6_0: %d ", artField->access_flags_);
}

Dalvik에서의 실현은 약간 다르다. jnibridge를 통해 패치를 가리키는 방법이다.
사용에 있어서 새로운 클래스를 직접 씁니다. 패치 도구가 주석을 생성하여 패치할 클래스와 방법의 대응 관계를 설명합니다.
ClassLoader
원 텐센트 공간 안드로이드 엔지니어이자 나의 계몽 선생님인 진종이 발명한 핫 패치 방안은 그가 원본 코드를 볼 때 우연히 발견한 착안점이다.
멀티덱스 방안의 실현은 사실 여러 개의 dex를 app의classloader에 넣고 모든 dex의 클래스를 찾을 수 있도록 하는 것이다.실제findClass 과정에서 중복된 클래스가 나타나면 아래의 클래스 불러오는 실현을 참조하여 첫 번째로 찾은 클래스를 사용합니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Class findClass(String name, List<Throwable> suppressed) {  

    for (Element element : dexElements) {  //  Element    dex  
        DexFile dex = element.dexFile;
        if (dex != null) {
            Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
            if (clazz != null) {
              return clazz;
            }
        }
    }
    if (dexElementsSuppressedExceptions != null) {  
        suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
    }
    return null;
}

이 핫 패치 방안은 바로 이런 점에서 출발하여 문제가 있는 클래스를 복원한 후 하나의 단독 dex에 놓고 반사를 통해 dex Elements 그룹의 맨 앞에 삽입하면 가상 기기를 패치가 끝난 클라스로 불러올 수 있지 않습니까?
여기까지 말하면 이미 완전한 방안인 것 같지만 실천 과정에서 로드 클래스를 실행할 때preverified 오류를 보고할 수 있다. 원래DexPrepare.cpp에 dex를 odex로 전환하는 과정에서DexVerify.cpp 검사를 하고 직접 인용된 클래스와clazz가 같은 dex에 있는지 검증한다. 만약에 그렇다면 CLASSISPREVERIFIED 플래그입니다.모든 클래스 (Application을 제외하고 사용자 정의 클래스의 코드를 불러오지 않았을 때) 의 구조 함수를 단독dex에 있는 클래스에 대한 인용을 삽입하면 이 문제를 해결할 수 있습니다.공간은javaassist를 사용하여 컴파일할 때 바이트 코드를 삽입합니다.
오픈 소스 구현에는 Nuwa, HotFix, DroidFix가 있습니다.
비교
Dexposed는 Art 모드(5.0+)를 지원하지 않으며 패치를 쓰기가 어렵습니다. 혼동된 코드를 반사해서 써야 합니다. 입도가 너무 가늘어서 교체할 방법이 많으면 작업량이 비교적 많습니다.
AndFix는 2.3-6.0을 지원하지만 일부 기종의 구덩이가 안에 있는지 잘 모르겠다. 왜냐하면 jni층은 자바와 같은 표준이 아니기 때문이다. 실현에 있어 방법은 Dexposed와 유사하고 모두 jni를 통해 방법을 바꾸지만 실현에 있어 더욱 간결하고 직접적이며 패치를 사용하면 다시 시작할 필요가 없다.그러나 실현에서 클래스 초기화를 직접 건너뛰고 초기화 완료로 설정했기 때문에 정적 함수, 정적 구성원, 구조 함수에 문제가 생길 수 있고 복잡한 클래스클래스클래스클래스클래스가 있다.forname은 바로 끊을 수 있습니다.
ClassLoader 방안은 2.3-6.0을 지원하는데 시작 속도에 약간의 영향을 줄 수 있습니다. 다음 앱이 시작될 때만 적용됩니다. 공간에 비교적 긴 온라인 앱이 있기 때문에 다음 부팅에 패치를 적용할 수 있다면 좋은 선택입니다.
전체적으로 말하자면 호환성 안정성에 있어ClassLoader 방안은 매우 믿을 만하다. 만약에 다시 시작하지 않으면 복구할 수 있고 방법이 충분하여 AndFix를 사용할 수 있다. 그러나 Dexposed는art를 지원할 수 없기 때문에 잠시 포기할 수밖에 없다. 개발자들이art모드를 지원할 수 있도록 개선할 수 있기를 바란다.아무래도 xposed의 여러 가지 능력은 사람을 끌어당긴다(예를 들어 훅 다른 사람의 앱 방법으로 해독된 데이터를 얻는 것, 헤헤), 그리고 흔적이 없는 매립점, 온라인 추적 문제 등은 언제든지 내려갈 수 있다.
원문http://blog.zhaiyifan.cn/2015/11/20/HotPatchCompare/

좋은 웹페이지 즐겨찾기