핫리페어 클래스 장르와 덱스 장르의 구현 원리

수업 장르 원칙

기본 원칙: 클래스를 로드할 때 요소가 발견되고 각 요소는 dex에 해당합니다. 별도로 수정한 클래스를 dex 삽입 dexlist 앞에 넣고 싶은데, 클래스를 앞에서 뒤로 로드할 때 dex에서 가장 먼저 로드하는 것이 수리된 클래스입니다.

코드 구현

  1. 컨텍스트를 통해 pathClassLoader를 가져오고 발행한 dex를 기반으로 dexclassloader를 생성합니다.

  2. 두 개의 경로 목록을 가져오고 두 경로 목록의 요소를 가져온 다음 생성된 dexclassloader 요소를 pathclassloader 요소 앞에 배치합니다. 그런 다음 병합된 요소를 pathclassloader의 요소에 할당합니다.

Davlik 가상 머신에서 발생한 문제

예상치 못한Dex 충돌

unexpectDex crash()가 davlik 가상 머신에서 발생합니다.

비즈니스 상황: A는 수리할 클래스 B(발급 클래스)를 참조합니다.

세 가지 조건이 동시에 충족되면 예기치 않은Dex 충돌이 발생합니다.

이 충돌이 발생하려면 세 가지 조건이 동시에 충족되어야 합니다 .

  1. 패치 클래스는 정적 클래스나 인스턴스를 통해 참조되지 않습니다.
  2. 패치가 발급된 참조 클래스는 dexopt 단계에서 성공적으로 검증되며, 참조 클래스에는 CLASS_ISPERVRIFYIED 플래그가 표시됩니다.
  3. 이 두 클래스는 동일한 dex에 있지 않습니다.

앱이 참조된 클래스(A가 B를 참조, 즉 클래스 B를 로드할 때)를 로드할 때 이러한 검사를 수행하며, 이 세 가지 조건을 동시에 충족하면 충돌이 발생합니다.

패치 클래스는 덱스에 별도로 배치되기 때문에 세 번째 조건은 변경할 수 없다. 1과 2로만 시작할 수 있습니다.

애플리케이션 설치에는 dexopt 단계가 필요합니다. 이 단계에서는 dex를 odex로 최적화한 다음 로드된 odex를 실행하여 실행합니다.

dexopt 단계의 과정

정적 메소드, 프라이빗 메소드, 생성자, 가상 메소드에서 호출한 클래스가 현재 클래스와 동일한 dex에 있는지 확인(위 메소드 호출 시 A가 호출한 BCDE 클래스가 A 클래스와 동일한 dex에 있는지)

동일한 dex에서 가상 머신은 클래스 A에 대해 몇 가지 최적화를 수행하고 이를 CLASS_ISPREVERIFIED 플래그로 표시합니다.

예를 들어 A는 B를 나타냅니다. 그리고 A와 B가 동일한 dex에 있으면 클래스 A에는 CLASS_ISPERVRIFYIED 플래그가 표시됩니다.

예외가 발생했을 때

나중에 클래스 A(dexopt 단계에서 표시된 클래스)가 로드되면 가상 머신은 Verfiy 마크 결과를 확인하고 역방향 검증 검증을 수행합니다.

검증 과정에서 위의 3가지 조건이 동시에 충족되면 unExceptionDex 예외가 발생하지 않으며, 검증에 통과해야만 클래스가 로드됩니다.

QZone 계측 조직 사전 검증 솔루션

이 계획은 확실히 세 번째 조건을 충족하지 않으므로 첫 번째 또는 두 번째 조건부터만 시작할 수 있습니다.

QZone은 메이크업을 삽입하여 사전 검증을 방지하는 두 번째 조건부터 시작됩니다.

해결 방법: 위의 특수 메서드(생성자, 정적 함수...)가 동일한 dex에서 클래스를 호출하면 표시되므로 내 크로스 덱스 액세스는 표시되지 않습니다. 가장 간단한 방법은 생성자의 dex 전체에 액세스하여 동일한 dex에 없으면 표시되지 않도록 하는 것입니다.

성취하다:

빈 클래스를 생성하고 별도의 dex에 넣습니다.

모든 클래스의 생성자에서 독립 dex의 빈 클래스에 접근합니다. 모든 클래스는 cross-dex 접근권한을 가지므로 전체 앱의 모든 클래스는 표시되지 않습니다.

하지만 독립 dex를 먼저 로드해야 합니다. 왜냐하면 APP의 PathClassLoader가 이 클래스를 찾을 수 없기 때문입니다. 부모 위임 모델 메커니즘(클래스를 로드할 때 먼저 버퍼에서 검색)을 사용하여 이 빈 클래스를 먼저 로드한 다음 나중에 이 클래스에 액세스할 수 있습니다.


결점:

odex의 체크섬 최적화 프로세스에 영향을 미치는 성능 문제가 있습니다.

APP 시작 성능을 줄이고 실행 메모리를 늘립니다.

Qfix 초기 constclass 참조 체계

던져진 첫 번째 조건부터 시작

정적 클래스 호출 및 인스턴스 오브 이외의 메서드에 대해서는 예외가 발생합니다.

패치 클래스를 호출하기 위해 정적 클래스를 사용하는 경우 크로스 덱스 호출 손상된 플래그가 있어도 예외가 발생하지 않습니다. 동시에 클래스 로더가 클래스를 로드할 때 로드된 한, 이 메커니즘을 활용하기 위해 우선적으로 캐시에서 읽습니다.

Davlik 가상 머신으로 클래스를 로드하는 프로세스:

먼저 dex 캐시에서 검색하여 존재하는 경우 바로 반환하며 이후의 검증 및 로딩 과정은 없으며 이후의 로딩 및 검증이 완료된 후 역시 dex 캐시에 배치됩니다. .

구현 아이디어

APP가 시작될 때 패치 클래스를 넣은 후 패치 클래스를 미리 정적으로 참조합니다. 이 참조는 예외를 발생시키지 않고(정적 클래스 참조 방법) 패치 클래스를 가상 머신의 캐시에 미리 로드합니다. , 나중에 액세스하더라도 비정적이며 플래그 충돌이 있어도 확인할 필요가 없습니다. 버퍼에서 이 클래스를 직접 반환하고 이후에 읽을 수 있습니다.

구현 코드:
  1. 우리는 애플리케이션 시작 시 패치 클래스를 로드하기 위해 정적 클래스를 사용하는데 어떤 클래스를 수정해야 할지 모르기 때문에 애플리케이션에서 모든 클래스를 로드하는 것은 불가능합니다.(하하하, 비과학적인)
  2. QFix는 Nativehook을 통해 가상 머신 로딩 클래스의 네이티브 메소드를 직접 호출하며 , 각 클래스의 dexId와 classId는 APP 패키징 시 저장됩니다. 실행 시 패치 클래스가 위치한 dexid와 classid를 찾아 jni 측에서 클래스를 파싱하는 가상머신의 메서드를 적극적으로 호출합니다. (formUnverifedConstant 매개변수를 true로 설정한다는 것은 이 호출이 Constantof나 인스턴스of의 형태로 호출된다는 뜻입니다. 이것이 사실이라면 (사전 검증은 하지 않을 것입니다.) 이번에는 패치가 호출되어 캐시에 저장될 것입니다. 이후 사용을 위해서는 캐시에서 직접 찾아 사용하면 되며 검증할 필요가 없습니다. .

클래스를 로드하기 위해 가상 머신의 기본 메소드가 호출되기 때문에 다양한 가상 머신에서 많은 조정이 이루어지며 안정성 문제도 발생합니다. 공유 문서에 X86에 문제가 있다고 나와 있습니다.

Art 가상 머신에서 발생한 문제

아래 캐스케이드 최적화 문제 뿐만 아니라 dex 장르에서 지적되고 있는 다른 문제들도 있습니다.

Art 가상머신에서는 메소드 인라이닝이 더 큰 문제를 일으키게 되는데, 어떤 가상머신이든 설치 단계에서 dex 최적화 과정이 있습니다.

Android 버전마다 odex 컴파일러가 다릅니다. 초기 컴파일러에서는 QuickCompile을 사용했고 나중에는 OptimizingCompare를 더 자주 사용했습니다.

컴파일러마다 메소드 인라인화에 대한 메소드 조건이 다르며, Optiminzing은 호출된 메소드가 가상 머신의 인라인화 조건을 충족하는 경우 연쇄 최적화 작업 (method1이 method2를 호출하고, 이 메소드가 method3을 호출하고, 이 메소드가 method4를 호출함)을 수행합니다.

최종 컴파일된 method1에는 method2method3method4(메소드)의 코드가 직접 포함되어 있습니다.

2는 3과 4의 코드를 포함하고, 3은 4)의 코드를 포함하며, 인라인은 메소드 id를 통해 호출하는 대신 코드를 직접 작성하는 것을 의미합니다.

질문

ClassA가 패치 클래스를 참조하고 패치 클래스가 이전에 가상 머신 최적화 중에 인라인 조건을 충족한 경우 이전 메서드가 참조 클래스에 기록된 것입니다. 이때, 복구를 위해 새 클래스가 발행되면 클래스가 정상적으로 로드될 수 있지만 구현이 참조 클래스에 작성되었기 때문에 새 클래스에 대한 메소드 호출이 호출되지 않습니다. 문제가있을 것입니다

인라인으로 인해 실행 흐름이 새 메서드로 점프하지 않고 참조 클래스의 메서드는 이전 메서드를 사용합니다 . 참조 클래스의 경우 기존 메소드의 지역 변수 테이블에 저장된 내용을 그대로 사용하므로 멤버 문자열을 찾는 데는 기존 메소드의 인덱스를 사용한다. 그러나 새 패치 클래스 인덱스는 변경될 수 있는 참조 클래스에 액세스할 때 충돌 오류를 일으킬 수 있습니다.

해결책

캐스케이드 최적화가 존재하기 때문에 복구하려는 클래스, 하위 클래스, 호출하는 클래스를 모두 패치에 배치해야 하며 전체 패치가 배포되므로 전체 패치의 크기가 매우 커집니다.

Dex 장르 핫 리페어 원리

클래스가 간섭하는 시스템 API는 상대적으로 낮은 수준이므로 적응 및 호환성 문제가 있습니다.

나중에 Tinker는 덱스 스톡의 핫 수리 경로에 착수했습니다.

원리 : 덱스 전체를 교체하되, 덱스 전체 배송이 불가능하여 덱스의 차등 배송을 한다.

새 dex와 이전 dex의 diff는 diff 알고리즘을 통해 서버 측에서 생성됩니다.

Sigma는 보다 일반적인 BsDiff를 사용합니다.

Tinker는 보다 심층적인 작업을 수행하고 dex 구조를 기반으로 하는 dexdiff 알고리즘을 발명했습니다. 이를 통해 diff 차이 패키지는 더 작아지고 합성 효율성은 높아집니다.

단계

  1. 서버가 이전 dex와 새 dex의 차이점을 생성한 후 차이점 패키지를 생성합니다. 패치 프로세스에서 차이 패키지가 요청되고 설치된 dex를 기반으로 로컬로 새 dex로 병합됩니다. 즉, 패치를 통해 새 dex로 복원됩니다.
  2. 새 dex를 통해 새 dexclassloader를 생성 하고 이 새 dexclassloader를 앱 pathclassloader의 상위로 설정합니다. 상위 위임 모델에 따르면 로드하는 것은 복구된 클래스인 새로운 dexclassloader입니다.
왜 수리를 별도의 프로세스로 수행해야 합니까?
  1. 비즈니스 프로세스가 무선으로 충돌하더라도 패치 프로세스로 문제를 해결할 수 있습니다.
  2. 비즈니스 프로세스가 반복될 수 있으며 병합으로 인해 충돌이 발생할 수 있습니다.
  3. 독립된 프로세스로 수행할 경우 메인 프로세스의 기동에 의존하지 않으며, 통합 복구를 위한 패치 프로세스를 끌어 올려 다른 업무 프로세스의 기동도 가능합니다.
주의점

Parch 프로세스 의 PathCore 병합 코어 코드의 일부 작업은 Application과 함께 PathClassLoader에 의해 로드됩니다.pathcore 가 분리 없이 비즈니스 로직을 호출하는 경우 path는 이때(pathclassloader 로딩을 통해) 이전 비즈니스 클래스를 로드합니다. 위임 모델의 후속 이전 비즈니스 클래스는 패치 프로세스 병합 후 dexclassloader 대신 pathClassLoader 캐시에서 가져옵니다. 문제가 발생하여 호출 클래스와 로딩 클래스가 일치하지 않게 되므로 비즈니스 디커플링을 조정해야 합니다.

즉, pathclassloader의 부모를 대체하기 위해 새 dex를 생성하기 전에 이전 클래스에 액세스하면 pathClassLoader에 의해 로드되어 로드된 클래스가 이전 dex가 됩니다. 그리고 캐시로 인해 병합 후 복구된 dexClassLoader 클래스 대신 항상 pathClassLoader에 의해 로드된 클래스가 사용됩니다.

기본적인 공통 문제

dex의 핫 복구에서 해결해야 할 몇 가지 기본적이고 일반적인 문제가 있습니다.

  1. 패치의 진입과 패치의 핵심사업은 사업과 분리되어야 한다.
  2. 패치 병합은 별도의 프로세스로 수행해야 함
  3. 각 패키지의 매핑이 변경됩니다 . 난독화에 개입하지 않으면 각 패키지의 난독화 규칙이 변경되므로 작은 변경이라도 두 패키지의 dex에 매우 큰 차이가 발생하므로 다음을 수행해야 합니다. 난독화합니다. 매핑을 저장합니다. 새 패키지를 빌드할 때 이 매핑을 적용하면 혼란과 일관성이 유지되며 차이가 발생하지 않습니다.
  4. 각 패키지의 하도급 결과가 변경됩니다 . APP가 큰 경우 크로스 덱스 액세스가 가능합니다(이 멀티 덱스 상황에서는 수정하지 않더라도 하도급 결과가 달라집니다). 따라서 기준 패키지 구축 시 해당 하도급 결과도 저장해야 합니다(새 패키지 구축 시 이 결과에 따라 하도급 계약).
  5. 패치 프로세스가 패치 병합을 완료한 후, 기본 프로세스는 패치를 사용할 때 즉시 검은색 화면이 표시되거나 오류가 발생합니다 . 가상 머신은 dex에 직접 액세스하지 않으며 dexopt 단계가 있습니다(애플리케이션 설치 중에 수행되며 이 단계는 dex를 동적으로 로드할 때도 수행됩니다). dexopt는 시스템에 의해 트리거됩니다. 따라서 검은색 화면은 기본 프로세스가 동적으로 로드된 dex를 직접 사용하여 dexopt를 트리거하여 검은색 화면이 나타나기 때문입니다. 따라서 패치 프로세스가 새 dex를 병합한 직후 dexopt가 트리거되어야 합니다.
dexopt를 실행하는 방법

새 dexclassloader를 수동으로 직접 생성하면 가상 머신이 독립 프로세스에서 전체 dexopt를 수행합니다(dexopt 프로세스가 독립 패치 프로세스에 배치되더라도 여전히 약간의 문제가 발생하며 문제는 나중에 나열됩니다). )

Art dex2oat가 핫 수리에 미치는 영향

dex2oat는 dex를 컴파일하는 프로세스입니다. 아트 가상 머신에서 dex를 가상 머신에 로드하고 실행하려면 먼저 머신 코드로 컴파일해야 합니다.

dex2oat 컴파일 모드

컴파일 프로세스에는 12개 이상의 모드가 있으며 그 중 더 중요한 것은 세 가지뿐입니다.

  1. 해석 전용 : 이 모드는 처음 부팅 또는 설치(첫 번째 부팅 또는 설치) 중에 수행됩니다. 검증만 수행되고 코드는 계속 해석 및 실행되며 기계어 컴파일은 수행되지 않습니다. 성능은 davlik 가상 머신과 일치합니다.
  2. speed : 이 모드는 새로운 DexClassLoader가 있을 때 트리거됩니다. 전체 기계어 코드 컴파일을 수행합니다.
  3. Speed ​​profile : 이 모드는 시스템이 OAT 업그레이드 또는 하이브리드 컴파일을 할 때 앱에 해당하는 프로필에 저장된 핫 코드만 컴파일합니다(시스템이 유휴 상태일 때 dexopt를 수행하기 위해 깨어나는 백그라운드 dexopt가 있습니다). 이 부분이 핫코드입니다.

전체 기계 코드 컴파일: 성능을 향상시키기 위해 ART 가상 머신은 전체 기계 코드로 코드를 컴파일합니다. 이 프로세스는 ClassLoader가 클래스를 로드할 때 수신 opt 경로에 odex 파일이 존재하지 않는다는 것을 발견하면 자동으로 트리거됩니다. newclassloader가 이전에 컴파일되지 않은 것이 이번이 처음이기 때문에 odex 파일이 없으므로 완전히 컴파일됩니다.

솔루션 진화
  • 따라서 기본 프로세스가 시작되면 전체 컴파일을 직접 수행하고 직접 중단됩니다.
  • 패치에서 전체 컴파일을 수행하면 dex2oat 프로세스가 매우 길기 때문에 일부 모델에서는 몇 분 정도 20~30초 정도 기다려야 할 수 있으며 많은 리소스를 차지할 수 있습니다. 프로세스가 제 시간에 완료되지 않습니다. 예를 들어, 사용자는 항상 뉴스를 클릭하고 몇 초 동안 시청한 후 종료하므로 최적화 및 복구를 완료하지 못하여 기본 프로세스가 느려지고 ARN이 발생할 수 있습니다.
  • Tinker의 솔루션: 따라서 패치 프로세스는 먼저 경량 컴파일을 수행합니다. 완료되면 사용됩니다. 완료할 수 없는 경우 이전 애플리케이션이 먼저 사용자에게 제공되고 전체 컴파일은 방지됩니다(이미 이전 버전을 사용하므로 할 필요가 없습니다.) 과도한 전체 규모 컴파일은 과도한 리소스 사용 및 비즈니스 프로세스 정체로 이어질 수 있습니다.) . 패치를 경량 컴파일에 사용할 수 있으면 사용하고, 사용할 수 없으면 전체 컴파일을 피하고 사용자가 먼저 실행하도록 하세요. (전체 컴파일을 피하는 방법은 나중에 소개하겠습니다.)

경량 컴파일도 시간이 많이 걸리므로 첫 번째 시작 속도가 느려집니다. 또한 경량 컴파일을 수행한 후에는 독립 프로세스도 무제한입니다. 전체 컴파일을 수행할 때 리소스를 확보하여 기본 프로세스가 가득 차고 ANR이 발생할 수 있습니다(확률은 낮으며 Tinker는 이를 무시할 준비가 되어 있습니다. 왜냐하면 APP 성능은 충분합니다)

  • 포그라운드에서 앱을 실행하면 패치 프로세스에서 리소스를 점유하고 오류가 발생할 수도 있습니다. 따라서 이를 바탕으로 추가 최적화가 수행됩니다. 패치 프로세스는 먼저 패치를 가져온 후 경량 컴파일을 수행하고 , 기본 프로세스는 경량 컴파일 패치 사용에 우선순위를 부여합니다. 전체 컴파일을 수행할 적절한 시간(적절한 시간: 내 앱이 백그라운드로 이동하고 다른 앱이 포그라운드에 있거나 화면을 잠글 때)을 찾고 시스템이 사용되지 않을 때 시스템이 백그라운드 dexopt를 수행합니다.
전체 컴파일 방지

세 가지 옵션이 있습니다:

  1. Atlas 솔루션: Native 측에서 Art 가상 머신의 실행 모드를 수정하고 DexFile 기본 인터페이스를 직접 사용하여 Dex 파일을 로드합니다(동일한 프로세스의 dex 로딩에 영향을 미치며 버전 O 이상에서는 DexFile이 폐기됩니다). 유용성 및 호환성 문제.
  2. Tbs 해결 방법: 새 DexClassLoader에서 optDir이 null로 전달되면 oat_location이 공백으로 남고 전체 컴파일이 수행되지 않는 것으로 나타났습니다(8.0의 시스템은 전달한 경로를 무시합니다).
  3. Tinker 솔루션: dexopt는 가상 머신을 실행하기 위한 명령줄이므로 시스템이 전체 컴파일을 트리거하기 전에 수동으로 dex2oat 명령을 호출하여 컴파일 모드인 Intercept-Only를 실행하고 쿨 컴파일만 수행하십시오. 먼저 경량 컴파일로 얻은 결과를 사용하십시오. 처음 시작 또는 설치 후 실행 효과는 가상 머신의 효과와 동일합니다. 먼저 실행하도록 하는 것도 문제가 발생한 후 최적화 솔루션입니다.

Android N 하이브리드 컴파일이 핫픽스에 미치는 영향

하이브리드 컴파일: AOT, 해석 및 JIT 모드가 공존합니다.

실제로 사용자가 사용하는 클래스는 극히 일부에 불과할 수 있는데, 왜 코드의 20~30%에 해당하는 코드를 모두 컴파일해야 합니까? 필요 없음

N 이전의 Art 가상 머신에 설치하려면 전체 컴파일이 필요했기 때문에 설치 시간이 오래 걸리고 Jit 실시간 컴파일도 매우 느렸습니다.

혼합 컴파일을 통해 설치 시간을 단축하여 N에서 이 문제를 해결했으며 시스템 OAT 업그레이드가 더 빠릅니다. 설치 및 첫 시작은 컴파일 없이 인터럽트 전용 방식으로 수행됩니다(davlik 가상 머신과 동일한 효과) . 컴파일은 어떻습니까? N의 증분 컴파일 프로세스를 살펴보겠습니다.

Android N 가상 머신 증분 컴파일 프로세스

가상 머신은 APP 코드 실행 중에 실행 코드를 수집하여 프로필 파일에 저장하고, 시스템은 jobSchedule을 통해 BackgroundDexOptService를 시작합니다. 이 서비스는 화면이 꺼져 있거나 충전 중일 때 시작됩니다. 밤에 잠자리에 들 때나 휴대폰이 유휴 상태일 때 수집된 코드를 컴파일하는 작업이 시작됩니다 (이러한 핫 코드는 자주 실행되므로 속도가 더 빨라집니다). 나중에 시작하면 매우 블록화되므로 이 방법으로 APP를 점진적으로 컴파일할 수 있습니다. 컴파일 후 base.odex 및 base가 생성됩니다. 예술(앱의 이미지라고 함)

가상 머신은 이것이 핫 코드라고 생각하므로 앱이 시작될 때 미리 코드의 이 부분을 로드합니다. ClassLoader가 ClassLinker를 생성할 때 dexcache에 한 번 로드합니다.

따라서 방금 애플리케이션을 시작했고 다른 작업을 수행하기 전에 이미 일부 클래스(이전에 컴파일된 핫 코드)를 로드했습니다.

세 가지 상황에서 Art 하이브리드 편집이 핫 리페어에 미치는 영향 분석
  • 복구하려는 클래스가 앱 이미지에 없음: Dex 장르는 부모 위임을 사용하며 상위를 통해 로드될 것으로 예상됩니다. 복구하려는 클래스가 앱 이미지에 없는 경우, 즉 로드되지 않은 것입니다. 사전에 이 메커니즘이 올바르고 패치가 적용될 수 있습니다.
  • 수정할 클래스가 부분적으로 appimage에 있습니다. 해당 클래스의 일부가 appimage에 있는 경우입니다. 결과적으로 일부는 새 것을 사용하고 일부는 오래된 것을 사용합니다. 이러한 방식으로 액세스하면 주소 혼란과 충돌이 발생합니다.
  • 복구할 클래스가 이미 appimage에 있습니다. 모두 appimage에 있고 복구한 클래스가 이전에 수집된 경우 패치가 적용되지 않습니다.
해결책

N 이상의 장치의 경우 상위 설정 모드를 포기하고 상위를 설정하는 대신 pathclassloader를 직접 교체합니다.

구현 단계
  1. 패치 dex를 위한 DexClassLoader 생성
  2. contextimpl을 통해 loadkedApk를 가져온 다음 보유된 PathClassLoader 객체를 가져옵니다. 이는 시스템이 우리를 위해 생성한 pathClassloader입니다.
  3. 리플렉션을 통해 이 속성을 패치된 클래스로더로 대체하세요.

원리: 시스템의 appimage가 시스템의 pathClassloader 캐시에 미리 로드되기 때문입니다 . 이후에 실행하는 것은 교체한 클래스로더이므로 이 새 클래스로더에는 앱 이미지가 더 이상 존재하지 않습니다.

영향: 앱이미지가 더 이상 존재하지 않기 때문에 성능은 희생되지만 복구 목적은 달성할 수 있으며, 통계적으로 보면 영향은 매우 미미합니다.

이 글은 재출간된 글입니다

원본 링크: 핫 리페어 클래스 장르와 덱스 장르의 구현 원리 - 너겟(juejin.cn)

추천

출처blog.csdn.net/m0_65909361/article/details/132939045