反编译学习:dex2jar+jd-gui--看大佬是如何代码的

前言

阅读【Xposed】抖音短视频检测 Xposed 分析这篇文章时,突然有感,要不,也去看一下抖音的代码是如何写的,学习一下大厂是如何规范写代码的。再加上自己懂一些反编译的知识,于是就有了这篇文章。

当然这篇文章不会深入研究抖音Android版本的技术,只是从表面稍微学习一下。如果文章有错误或者描述不当,请JYM见谅。

准备工作

  1. adb环境变量。这个和下面的java环境变量在安装Android Studio的时候就搞定了。
  2. java环境变量。
  3. apktool下载。查看apk中的资源,这个可下载可不下载。
  4. dex2jar安装。用来解压dex文件。
  5. jd-gui安装。查看加压后的jar文件中的具体内容。
  6. 抖音apk文件。这篇文章所引用的VersionCode为270801,VersionName='27.8.0',包名'com.ss.android.ugc.aweme'

反编译抖音apk

使用apktool解压apk文件

打开终端,执行命令行

java -jar apktool_2.9.0.jar d demo.apk -o demo

其中 demo.apk是抖音apk文件,-o demo是输出目标文件。

注:这一步的前提是,java环境变量的配置。

等命令行执行完毕后,打开demo文件,可以看到50个文件(可能版本不同,或者不同的apk,解压出来的文件数量是不同的)。除了assets、lib、res和AndroidManifest文件外,还有smali文件。

这里分享一篇文章Android逆向系列。同时分享另一篇帖子android app相关破解技术大揭秘咯,从这篇帖子可以知道smali文件是干什么的。

这里贴ManiFragment$6.smali的一部分代码:

ruby 复制代码
.method private synthetic LIZ()V
    .locals 1

    .prologue
    .line 65536
    iget-object v0, p0, Lcom/ss/android/ugc/aweme/main/MainFragment$6;->LIZ:Lcom/ss/android/ugc/aweme/main/MainFragment;

    .line 65537
    .line 65538
    invoke-virtual {v0}, Lcom/ss/android/ugc/aweme/main/MainFragment;->LJIILIIL()V

    .line 65539
    .line 65540
    .line 65541
    return-void
.end method

对应的代码如下:

arduino 复制代码
  public final void LIZ(Ve7 paramVe7) {
    this.LIZ.superFinish();
  }

dex2jar和jd-gui查看源码

提取apk文件中的class.dex,apk文件可以用压缩软件打开。打开apk文件后就犯难了,要解压的dex文件居然有42个。比如,我自己写的apk,里面的class.dex一般都在3个范围内,但douyin.apk里面居然有42个class.dex,太多了。

dex2jar可以让dex转jar,也可以让apk转jar。这个apk的dex太多了,那试试直接转apk吧。

执行命令行

d2j-dex2jar douyin.apk -o douyin.jar

报错了:

css 复制代码
Exception in thread "main" java.lang.OutOfMemoryError: GC overhead limit exceeded
        at java.util.LinkedList.linkLast(Unknown Source)
        at java.util.LinkedList.add(Unknown Source)
        at com.googlecode.d2j.reader.DexFileReader.travelInsn(DexFileReader.java:1174)
        at com.googlecode.d2j.reader.DexFileReader.findLabels(DexFileReader.java:1135)
        at com.googlecode.d2j.reader.DexFileReader.acceptCode(DexFileReader.java:1412)
        at com.googlecode.d2j.reader.DexFileReader.acceptMethod(DexFileReader.java:1064)
        at com.googlecode.d2j.reader.DexFileReader.acceptClass(DexFileReader.java:862)
        at com.googlecode.d2j.reader.DexFileReader.accept(DexFileReader.java:662)
        at com.googlecode.d2j.reader.MultiDexFileReader.accept(MultiDexFileReader.java:117)
        at com.googlecode.d2j.reader.MultiDexFileReader.accept(MultiDexFileReader.java:110)
        at com.googlecode.d2j.dex.Dex2jar.doTranslate(Dex2jar.java:86)
        at com.googlecode.d2j.dex.Dex2jar.to(Dex2jar.java:285)
        at com.googlecode.dex2jar.tools.Dex2jarCmd.doCommandLine(Dex2jarCmd.java:112)
        at com.googlecode.dex2jar.tools.BaseCmd.doMain(BaseCmd.java:288)
        at com.googlecode.dex2jar.tools.Dex2jarCmd.main(Dex2jarCmd.java:33)

有点烦,还是用死办法吧,一个一个把dex文件转成jar文件。

d2j-dex2jar classes1.dex -o class1.jar

如此,一个dex文件一个dex文件依次执行过来。最后成功把42个dex文件都转换成jar文件。

执行命令行打开gui查看代码

java -jar jd-gui-1.6.6.jar

这个是classes1.jar文件的目录

把42个classes1.dex~classes42.dex全部拖入可视化gui界面。

那接下来从何入手呢?

我决定从AndroidManifest入手。这个文件容易拿到,前面提到的apktool可以获取到,甚至直接用解压工具都可以获取到这个文件,也可以直接把apk拖入Android Studio,也能看得到AndroidManifest.xml文件。

打开AndroidMainfest.xml文件,找到启动页,发现居然还真是SplashActivity

ini 复制代码
<activity-alias android:name="com.ss.android.ugc.aweme.splash.SplashActivity"
	android:screenOrientation="portrait"
	android:targetActivity="com.ss.android.ugc.aweme.main.MainActivity" 
	android:theme="@style/APKTOOL_DUPLICATE_style_0x7f120274">
    <intent-filter>
        <action android:name="android.intent.action.MAIN"/>
        <category android:name="android.intent.category.LAUNCHER"/>
    </intent-filter>
</activity-alias>

打开抖音,这是用adb查看当前Activity

adb shell dumpsys activity activities

这句命令行可以查看当前所有活动中的Activity。

ini 复制代码
TaskRecord{8b667e0 #3 A=com.ss.android.ugc.aweme U=0 StackId=2 sz=1}
      userId=0 effectiveUid=u0a116 mCallingUid=u0a24 mUserSetupComplete=true mCallingPackage=com.miui.home
      affinity=com.ss.android.ugc.aweme
      intent={act=android.intent.action.MAIN cat=[android.intent.category.LAUNCHER] flg=0x10200000 cmp=com.ss.android.ugc.aweme/.main.MainActivity}

发现当前播放视频的Activity是MainActivity(命名还是很标准的)。

根据路径com.ss.android.ugc.aweme.main

还是笨办法,在42个classes.jar文件中找这个目录下的文件。

在classes7.jar中找到main文件夹,不过只有MainFragment$6类,不是目标。

在classes9.jar中找到main文件夹,不过只有TabAlphaController类,不是目标。

在classes11.jar中找到main文件夹,不过只有MainPageFragment类,不是目标。

在classes14.jar中找到main文件夹,不过只有IMainPageMobHolper类,不是目标。

终于,在classes21.jar中找到MainActivity文件

MainActivity.class

随便找其中一个方法

enterAuthPage
csharp 复制代码
private void enterAuthPage(Intent paramIntent) {
    if (paramIntent != null && paramIntent.getExtras() != null && paramIntent.getExtras().containsKey("openplatform_after_switch_account"))
      try {
        SmartRouter.buildRoute((Context)this, "aweme://authorizedy/").withParam(paramIntent.getExtras()).open();
        if (!"pc_auth".equals(paramIntent.getExtras().getString("source")))
          finish(); 
        return;
      } catch (Exception exception) {
        GlobalProxyLancet.com_ss_android_ugc_aweme_lancet_ThrowableLancet_thrPrintStackTrace(exception);
      }  
  }

发现抖音Android端是用自己的路由跳转代码SmartRouter,既不是Arouter,也不是TheRouter。aweme://authorizedy/这个看起来像是路径,不过没找到对应的Fragment或者Activity。

java 复制代码
  public boolean onFeedPage() {
    if (getTabChangeManager().LIZJ() instanceof MainFragment) {
      ewF ewF = f9k.LIZ.LJIIIZ();
      if (ewF != null && ewF.LJIIJJI())
        return true; 
    } 
    return false;
  }

这串代码中,有MainFragment,与其同一个文件夹下还有个类MainPageFragment,想来,这两个MainFragmentMainPageFragment,一个是首页,一个是推荐。

再仔细看两个类的代码, MainFragment中有一段代码

arduino 复制代码
  private void LIZ(String paramString1, String paramString2) {
    boolean bool;
    if (getActivity() != null) {
      bool = (wob.LIZ.LIZLLL(getActivity())).LIZ;
    } else {
      bool = false;
    } 
    EventMapBuilder eventMapBuilder = EventMapBuilder.newBuilder();
    eventMapBuilder.appendParam("click_method", paramString1);
    eventMapBuilder.appendParam("city_info", mfK.LIZ());
    eventMapBuilder.appendParam("display", paramString2);
    eventMapBuilder.appendParam("is_reshape", Boolean.valueOf(bool));
    QiB.LIZ("homepage_fresh_click", eventMapBuilder.builder(), "com.ss.android.ugc.aweme.main.MainFragment");
  }

MainPageFragment中有一段代码

less 复制代码
  private void changeNearByTabName(String paramString, boolean paramBoolean) {
    if (isViewValid() && !TextUtils.isEmpty(paramString) && eyN.LJIIJJI() == 1) {
      if (TextUtils.equals(paramString, f7v.LIZ.LJFF("NEARBY")))
        return; 
      if (wob.LIZ.LJJIIZ()) {
        f7v.LIZ.LIZIZ("NEARBY", getString(2131831656));
        return;
      } 
      if (!SimpleLocationHelper.LIZJ() && fwj.LJ() == null) {
        f7v.LIZ.LIZIZ("NEARBY", getString(2131831656));
        return;
      } 
      f7v.LIZ.LIZ("NEARBY", paramString, paramBoolean);
    } 
  }

猜测MainPageFragment应该是首页,MainFragment应该是推荐。

enterLiveRoom

下面还有private void enterLiveRoom(String paramString)方法,这个是用来进入直播间的;

kotlin 复制代码
  private void enterLiveRoom(String paramString) {
    try {
      StringBuilder stringBuilder = StringBuilderCache.get();
      stringBuilder.append("sslocal://webcast_room/?");
      stringBuilder.append(paramString);
      SmartRouter.buildRoute((Context)this, StringBuilderCache.release(stringBuilder)).open();
      return;
    } catch (Exception exception) {
      GlobalProxyLancet.com_ss_android_ugc_aweme_lancet_ThrowableLancet_thrPrintStackTrace(exception);
      return;
    } 
  }

"sslocal://webcast_room/?"这个路径找不到,不过找到一个类StartLiveActivity,猜测这个可能就是enterLiveRoom这个方法跳转的类。

onCreate
scss 复制代码
public static void com_ss_android_ugc_aweme_main_MainActivity_androidx_fragment_app_FixSpecialEffectControllerLancet_onCreate(MainActivity paramMainActivity, Bundle paramBundle) {
    AppTrace.b(1898);
    String str = paramMainActivity.getClass().getName();
    List list = L48.LIZ.LIZ();
    if (list != null && !list.isEmpty() && list.contains(str))
      uPG.LIZ(paramMainActivity.getSupportFragmentManager()); 
    paramMainActivity.com_ss_android_ugc_aweme_main_MainActivity__onCreate$___twin___(paramBundle);
    AppTrace.e(1898);
  }

这是onCreate方法,可以看到第七行,实现了onCreate_twin,在onCreate_twin,实现了enterAuthPage和其他的初始化操作。

其他

private final boolean filterMoveEvent(MotionEvent paramMotionEvent),这个方法从名字来看,应该是重写了触摸反馈;

addTestInfo()细看这个方法,除了ViewGroup是用xml文件写的,其他的TextInfo都是通过new方法生成的。怪不得,当初跑无障碍服务的时候,快手可以用id来确定按钮,而抖音的控件id是动态的。当时还很疑惑,现在一看,原来这么回事啊。

多class.dex解压

回到dex2jar解压dex文件,因为报错OOM,所以只能使用最笨的办法,一个一个解压文件,代码块散乱在不同的class.dex文件中,虽然这样也能读,但好麻烦啊。

LoginActivity

举一个栗子,假如要找登录LoginActivity, 在AndroidMainfest文件中查找LoginActivity,发现有四个LoginActivity文件,

复制代码
com.ss.android.ugc.aweme.account.business.login.DYLoginActivity
com.ss.android.ugc.aweme.commerce.sdk.login.ShadowLoginActivity
com.ss.android.ugc.aweme.im.sdk.chat.ChatCheckLoginActivity
com.ss.android.ugc.aweme.login.PushLoginActivity

而四个不同的类还都在不同的文件夹下面,加上一共42个class.dex,这得找到什么时候啊。不行,这个问题得解决,不就是oom吗,这个我熟。

必应一下,找到一个解决方案OutOfMemoryError #518

OutOfMemoryError

找到dex-tool文件夹,打开d2j_invoke.bat文件,将java -Xms512m -Xmx2048m -cp "%CP%" %*修改为java -Xms512m -Xmx10240m -cp "%CP%" %*,将内存改到10G,新启一个终端,重新输入执行命令行

d2j-dex2jar douyin.apk douyin.jar

发现还是报错OOM,10G的内存大小对dex-tool加压apk文件还是不够,再改,一口气加到15G(内存也才16G),终于成功加压出jar文件。

用jd-gui文件打开jar文件,终于,可以一口气看所有的代码文件了。

查看当前活动

回到LoginActivity文件,此时找4个LoginActivity容易多了,但到底是哪个?

此时想到了两个方法,因为App的入口Activity是只有一个的,根据Intent-filter标签确定入口Splash文件,然后一步一步进入LoginActivity。

第二个方法就是adb命令行。

adb shell dumpsys activity activities这个命令行是查看所有活动中的Activity,文章上面提到过。不过一下子把所有的Activity都列出来,每个TaskRecord的内容都太多了,看得眼花缭乱的,换一个命令行。

adb shell dumpsys activity top | findstr ACTIVITYadb shell dumpsys window | findstr mCurrentFocus 这两个命令行都是当前activity的,不过一个是查看最顶部的Activity,一个是查看当前正在获取焦点的Activity的。

执行上面的命令行,可以发现,登录Activity是DYLoginActivity。

ini 复制代码
mCurrentFocus=Window{c1303d u0 com.ss.android.ugc.aweme/com.ss.android.ugc.aweme.account.business.login.DYLoginActivity}

DYLoginActivity.class

java 复制代码
package com.ss.android.ugc.aweme.account.business.login;

import X.1W7;
import X.4YA;
import X.C11;
import X.L48;
import X.W0Z;
import X.dI3;
import X.sdz;
import X.uPG;
import android.app.Activity;
import android.os.Build;
import android.os.Bundle;
import android.view.View;
import com.bytedance.android.btm.api.BtmSDK;
import com.bytedance.apm.agent.v2.instrumentation.ActivityAgent;
import com.bytedance.covode.number.Covode;
import com.bytedance.ies.abmock.ABManager;
import com.bytedance.jarvis.trace.apptrace.AppTrace;
import com.bytedance.sysoptimizer.EnterTransitionCrashOptimizer;
import com.ss.android.ugc.aweme.ml.api.MLCommonService;
import com.ss.android.ugc.aweme.pad_impl.common.PadCommonServiceImpl;
import com.ss.android.ugc.playerkit.exp.PlayerSettingCenter;
import java.lang.ref.WeakReference;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

public final class DYLoginActivity extends W0Z {
  public Map<Integer, View> LJ;
  
  static {
    Covode.recordClassIndex(697363);
  }
  
  public final View LIZ(int paramInt) {
    Map<Integer, View> map = this.LJ;
    View view2 = map.get(Integer.valueOf(paramInt));
    View view1 = view2;
    if (view2 == null) {
      view1 = findViewById(paramInt);
      if (view1 != null) {
        map.put(Integer.valueOf(paramInt), view1);
        return view1;
      } 
      view1 = null;
    } 
    return view1;
  }
  
  public final void onCreate(Bundle paramBundle) {
    AppTrace.bc(15600);
    String str = getClass().getName();
    List list = L48.LIZ.LIZ();
    if (list != null && !list.isEmpty() && list.contains(str))
      uPG.LIZ(getSupportFragmentManager()); 
    ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onCreate", true);
    if (1W7.LIZIZ()) {
      WeakReference<DYLoginActivity> weakReference = new WeakReference<DYLoginActivity>(this);
      dI3.LIZ.LIZ(weakReference);
    } 
    super.onCreate(paramBundle);
    if (!getIntent().getBooleanExtra("use_one_key_login_half_screen_force", false) && !getIntent().getBooleanExtra("login_activity_without_anim", false)) {
      overridePendingTransition(2130772644, 2130772061);
    } else {
      overridePendingTransition(0, 0);
    } 
    ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onCreate", false);
    AppTrace.ec(15600);
  }
  
  public final void onPause() {
    AppTrace.bc(15601);
    super.onPause();
    PadCommonServiceImpl.LIZ(false).LIZIZ();
    AppTrace.ec(15601);
  }
  
  public final void onResume() {
    AppTrace.bc(15603);
    BtmSDK.INSTANCE.getService().getActivityLifeCycleAopListener().onActivityPreResumeAop((Activity)this);
    ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onResume", true);
    super.onResume();
    ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onResume", false);
    AppTrace.ec(15603);
  }
  
  public final void onSaveInstanceState(Bundle paramBundle) {
    4YA 4YA = (4YA)ABManager.getInstance().getValueSafely(true, "transaction_too_large_opt", 31744, 4YA.class, C11.LIZ);
    if (4YA != null && 4YA.LIZ && Build.VERSION.SDK_INT >= 29 && 4YA.LJIIIZ && 4YA.LIZJ != null) {
      String str = getClass().getName();
      Iterator iterator = 4YA.LIZJ.iterator();
      while (iterator.hasNext()) {
        if (str.equals(iterator.next())) {
          System.out.println("skipOnSaveInstanceState");
          return;
        } 
      } 
    } 
    super.onSaveInstanceState(paramBundle);
  }
  
  public final void onStart() {
    AppTrace.bc(15602);
    BtmSDK.INSTANCE.getService().getActivityLifeCycleAopListener().onActivityPreStartAop((Activity)this);
    ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onStart", true);
    super.onStart();
    ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onStart", false);
    AppTrace.ec(15602);
  }
  
  public final void onStop() {
    AppTrace.bc(15607);
    super.onStop();
    if (EnterTransitionCrashOptimizer.getContext() != null)
      int j = Build.VERSION.SDK_INT; 
    int i = Build.VERSION.SDK_INT;
    try {
      return;
    } finally {
      Exception exception = null;
      AppTrace.cc(15607);
      AppTrace.ec(15607);
    } 
  }
  
  public final void onWindowFocusChanged(boolean paramBoolean) {
    AppTrace.bc(15605);
    if (PlayerSettingCenter.INSTANCE.getENABLE_ADJUST_BRIGHT_STRATEGY())
      sdz.LIZ().LIZ((Activity)this, paramBoolean); 
    MLCommonService.instance().LIZ((Activity)this, paramBoolean);
    ActivityAgent.onTrace("com.ss.android.ugc.aweme.account.business.login.DYLoginActivity", "onWindowFocusChanged", true);
    super.onWindowFocusChanged(paramBoolean);
    AppTrace.ec(15605);
  }
}

虽然混淆了一部分代码,但大部分的代码都是能读的,而且也能根据命名知道方法是干什么的。

解读onStop
ini 复制代码
  public final void onStop() {
    AppTrace.bc(15607);
    super.onStop();
    if (EnterTransitionCrashOptimizer.getContext() != null)
      int j = Build.VERSION.SDK_INT; 
    int i = Build.VERSION.SDK_INT;
    try {
      return;
    } finally {
      Exception exception = null;
      AppTrace.cc(15607);
      AppTrace.ec(15607);
    } 
  }

AppTrace应该是字节的数据埋点,猜测的,包括ActivityAgent也看起来像数据埋点相关的内容。

ifij看起来毫无意义,推测可能是在smali里面增加代码破译的难度。

onCreate

最初看onCreate方法的时候,会疑惑为什么没有setContentView,没有布局?登录页有两个页面,第一个是专门输入手机号码的,第二个是输入验证码的。由这个点切入,感觉可能是用Fragment来实现的,发现一行代码:

ini 复制代码
uPG.LIZ(getSupportFragmentManager()); 

由这个找到uPG.classFragmentManager.classSpecialEffectsController这三个类,再往下就找不下去了,因为代码已经有点看不懂了。从登录页的逻辑来看,是输入手机号码,勾选同意按钮,发送验证码,切换Fragment。从我的理解来看,发现是用uPG.classFragmentManager.classSpecialEffectsController这三个类来操控Fragment的切换与否,但代码看了一圈,甚至连View的点击事件都没有找到。

哎,还是水平不够啊,居然在开始的时候就妄想破解抖音的客户端技术,连视频的门槛都还没见呢。

结论

目前也只是略微看了一下MainActivity.classDYLoginActivity.class,发现单单这两个文件,就有好多自己可以学习的。

比如,原本自己是知道什么是单一职责原则的,但因为向来都是在小厂里打混,代码规范什么的,也只是了解设计模式几大原则,看了阿里的Java代码规范文档,但真正执行起来,还是怎么代码舒服就怎么代码。而在MainActivity.class中,有好多部分都是严格遵守单一职责原则的。

虽然目前也只是到登录就卡住了,但我相信,随着不断尝试和努力,我最后会放弃的--从入门到放弃。


最后

感谢你看到最后,最后说一两点~

1.如果你有不同的看法,欢迎你在文章下面进行评论留言,本人一直是采取开放的态度,同不同意是你的事,采纳不采纳是我的事。

2.如果文章对你有帮助,或者你认可的话,请随手点个赞,支持一下。

3.最后不知道这篇文章能不能发出来,最后有多少人能看到,看到了又有多少能到最后的,既然看到最后了,就请顺手点个赞,给我更新文章的动力吧。

(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除,同样,转载请提前告知)

相关推荐
冬奇Lab1 小时前
OpenClaw 源码精读(2):Channel & Routing——一条消息如何找到它的 Agent?
人工智能·开源·源码阅读
桦说编程1 小时前
从 ForkJoinPool 的 Compensate 看并发框架的线程补偿思想
java·后端·源码阅读
阿巴斯甜10 小时前
Android 报错:Zip file '/Users/lyy/develop/repoAndroidLapp/l-app-android-ble/app/bu
android
Kapaseker10 小时前
实战 Compose 中的 IntrinsicSize
android·kotlin
xq952711 小时前
Andorid Google 登录接入文档
android
黄林晴12 小时前
告别 Modifier 地狱,Compose 样式系统要变天了
android·android jetpack
冬奇Lab1 天前
Android触摸事件分发、手势识别与输入优化实战
android·源码阅读
城东米粉儿1 天前
Android MediaPlayer 笔记
android
Jony_1 天前
Android 启动优化方案
android
阿巴斯甜1 天前
Android studio 报错:Cause: error=86, Bad CPU type in executable
android