首页 > 其他分享 >小白也能看懂的插件化DroidPlugin原理(三)-- 如何拦截startActivity方法

小白也能看懂的插件化DroidPlugin原理(三)-- 如何拦截startActivity方法

时间:2023-09-21 16:03:20浏览次数:51  
标签:Instrumentation 插件 startActivity -- Intent Context Activity intent

小白也能看懂的插件化DroidPlugin原理(三)-- 如何拦截startActivity方法_ide

**前言:**在前两篇文章中分别介绍了动态代理、反射机制和Hook机制,如果对这些还不太了解的童鞋建议先去参考一下前两篇文章。经过了前面两篇文章的铺垫,终于可以玩点真刀实弹的了,本篇将会通过 Hook 掉 startActivity 方法的一个小例子来介绍如何找出合适的 Hook 切入点。 开始之前我们需要知道的一点就是,其实在 Android 里面启动一个 Activity 可以通过两种方式实现,一种是我们常用的调用 Activity.startActivity 方法,一种是调用 Context.startActivity 方法,两种方法相比之下, 第一种启动Activity的方式更为简单,所以先以第一种为例。


一、Hook 掉 Activity 的 startActivity 的方法

在 Hook Activity 的 startActivity 方法之前,我们首先明确一下我们的目标,我们先通过追踪源码找出 startActivity 调用的真正起作用的方法,然后想办法把目标方法拦截掉,并输出我们的一条 Log 信息。

我们先来一步步分析 startActivity 的源码,随手写一个 startActivity 的示例,按住 command 键( windows 下按住 control )用鼠标点击 startActivity的方法即可跳转到方法里面。

startActivity(Intent intent) 源码如下:

1 public void startActivity(Intent intent) {
2         this.startActivity(intent, null);
3 }

接着看 this.startActivity(intent, null) 方法源码:

1 public void startActivity(Intent intent, @Nullable Bundle options) {
2         if (options != null) {
3             startActivityForResult(intent, -1, options);
4         } else {
5             // Note we want to go through this call for compatibility with
6             // applications that may have overridden the method.
7             startActivityForResult(intent, -1);
8         }
9 }

从上一步传入的参数 options 为 null 我们就可以知道这一步调用了 startActivityForResult(intent, -1) 的代码。

startActivityForResult(Intent intent, int requestCode) 源码如下:

1 public void startActivityForResult(@RequiresPermission Intent intent, int requestCode) {
2         startActivityForResult(intent, requestCode, null);
3 }

startActivityForResult(Intent intent, int requestCode, Bundle options) 源码如下:

1 public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,
 2             @Nullable Bundle options) {
 3         if (mParent == null) {
 4             options = transferSpringboardActivityOptions(options);
 5             Instrumentation.ActivityResult ar =
 6                 mInstrumentation.execStartActivity(
 7                     this, mMainThread.getApplicationThread(), mToken, this,
 8                     intent, requestCode, options);
 9             if (ar != null) {
10                 mMainThread.sendActivityResult(
11                     mToken, mEmbeddedID, requestCode, ar.getResultCode(),
12                     ar.getResultData());
13             }
14             if (requestCode >= 0) {
15                 // If this start is requesting a result, we can avoid making
16                 // the activity visible until the result is received.  Setting
17                 // this code during onCreate(Bundle savedInstanceState) or onResume() will keep the
18                 // activity hidden during this time, to avoid flickering.
19                 // This can only be done when a result is requested because
20                 // that guarantees we will get information back when the
21                 // activity is finished, no matter what happens to it.
22                 mStartedActivity = true;
23             }
24 
25             cancelInputsAndStartExitTransition(options);
26             // TODO Consider clearing/flushing other event sources and events for child windows.
27         } else {
28             if (options != null) {
29                 mParent.startActivityFromChild(this, intent, requestCode, options);
30             } else {
31                 // Note we want to go through this method for compatibility with
32                 // existing applications that may have overridden it.
33                 mParent.startActivityFromChild(this, intent, requestCode);
34             }
35         }
36 }

到这一步我们已经看到了关键点,注意上面代码块中红色的代码,其实 startActivity 真正调用的是 mInstrumentation.execStartActivity(...) 方法,mInstrumentation 是 Activity 的一个私有变量。接下来的任务将变得非常简单,回忆一下上一篇博文《小白也能看懂插件化DroidPlugin原理(二)-- 反射机制和Hook入门》中的方案一,在替换汽车引擎时我们继承原来的汽车引擎类创建了一个新类,然后在新引擎类中拦截了最大速度的方法,这里的思路是一样的,我们直接新建一个继承 Instrumentation 的新类,然后重写 execStartActivity() 。对此有不明白的童鞋建议再看一遍上一篇博文《小白也能看懂插件化DroidPlugin原理(二)-- 反射机制和Hook入门》。代码如下:

1 public class EvilInstrumentation extends Instrumentation {
 2     private Instrumentation instrumentation;
 3     public EvilInstrumentation(Instrumentation instrumentation) {
 4         this.instrumentation = instrumentation;
 5     }
 6     public ActivityResult execStartActivity(
 7             Context who, IBinder contextThread, IBinder token, Activity target,
 8             Intent intent, int requestCode, Bundle options) {
 9         Logger.i(EvilInstrumentation.class, "请注意! startActivity已经被hook了!");
10         try {
11             Method execStartActivity = Instrumentation.class.getDeclaredMethod("execStartActivity", Context.class,
12                     IBinder.class, IBinder.class, Activity.class,
13                     Intent.class, int.class, Bundle.class);
14             return (ActivityResult)execStartActivity.invoke(instrumentation, who, contextThread, token, target,
15                     intent, requestCode, options);
16         } catch (Exception e) {
17             e.printStackTrace();
18         }
19 
20         return null;
21     }
22 }

重写工作已经做完了,接着我们通过反射机制用新建的 EvilInstrumentation 替换掉 Activity 的 mInstrumentation 变量,具体代码如下:

1 public static void doActivityStartHook(Activity activity){
 2         try {
 3             Field mInstrumentationField = Activity.class.getDeclaredField("mInstrumentation");
 4             mInstrumentationField.setAccessible(true);
 5             Instrumentation originalInstrumentation = (Instrumentation)mInstrumentationField.get(activity);
 6             mInstrumentationField.set(activity, new EvilInstrumentation(originalInstrumentation));
 7         } catch (Exception e) {
 8             e.printStackTrace();
 9         }
10 }

这对于我们来说已经很是轻车熟路了,很快就写完了,然后我们在 Activity 的 onCreate() 方法中需要调用一下 doActivityStartHook 即可完成对 Activity.startActivity 的 hook。MainActivity 的代码如下:

1 public class MainActivity extends Activity {
 2     private Button btn_start_by_activity;
 3     @Override
 4     protected void onCreate(Bundle savedInstanceState) {
 5         super.onCreate(savedInstanceState);
 6         setContentView(R.layout.activity_main);
 7         // hook Activity.startActivity()的方法时不知道这行代码为什么放在attachBaseContext里面不行?
 8         // 调试发现,被hook的Instrumentation后来又会被替换掉原来的。
10         ActivityThreadHookHelper.doActivityStartHook(this);
11         btn_start_by_activity = (Button) findViewById(R.id.btn_start_by_activity);
12         btn_start_by_activity.setOnClickListener(new View.OnClickListener() {
13             @Override
14             public void onClick(View v) {
15                 Intent intent = new Intent(MainActivity.this, OtherActivity.class);
16                 startActivity(intent);
17             }
18         });
19     }
20 }

程序运行之后,点击启动 Activity 的按钮将输出以下 Log:

[EvilInstrumentation] : 请注意! startActivity已经被hook了!

到此为止我们已经 hook 了 Activity 的 startActivity 方法,非常简单,代码量也很少,但我们也很轻易的发现这种方法需要在每一个 Activity 的 onCreate 方法里面调用一次 doActivityStartHook 方法,显然这不是一个好的方案,所以我们在寻找 hook 点时一定要注意尽量找一些在进程中保持不变或不容易被改变的变量,就像单例和静态变量。

问题1:在这里有一点值得一提,我们将 doActivityStartHook(...) 方法的调用如果放到 MainActivity 的 attachBaseContext(...) 方法中替换工作将不会生效,为什么?

调试发现,我们在 attachBaseContext(..) 里面执行完毕 doActivityStartHook(...) 方法后确实将 Activity 的 mInstrumentation 变量换成了我们自己的 EvilInstrumentation,但程序执行到 onCreate() 方法后就会发现这时候 mInstrumentation 变成了系统自己的 Instrumentation 对象了。这时候我们可以确信的是 mInstrumentation 变量一定是在 attachBaseContext() 之后被初始化或者赋值的。带着这个目标我们很轻松就在 Activity 源码的 attach() 方法中找到如下代码:

Activity.attach() 的源码如下(注意第8行和第26行):

1   final void attach(Context context, ActivityThread aThread,
 2             Instrumentation instr, IBinder token, int ident,
 3             Application application, Intent intent, ActivityInfo info,
 4             CharSequence title, Activity parent, String id,
 5             NonConfigurationInstances lastNonConfigurationInstances,
 6             Configuration config, String referrer, IVoiceInteractor voiceInteractor,
 7             Window window) {
 8         attachBaseContext(context);
 9 
10         mFragments.attachHost(null /*parent*/);
11 
12         mWindow = new PhoneWindow(this, window);
13         mWindow.setWindowControllerCallback(this);
14         mWindow.setCallback(this);
15         mWindow.setOnWindowDismissedCallback(this);
16         mWindow.getLayoutInflater().setPrivateFactory(this);
17         if (info.softInputMode != WindowManager.LayoutParams.SOFT_INPUT_STATE_UNSPECIFIED) {
18             mWindow.setSoftInputMode(info.softInputMode);
19         }
20         if (info.uiOptions != 0) {
21             mWindow.setUiOptions(info.uiOptions);
22         }
23         mUiThread = Thread.currentThread();
24 
25         mMainThread = aThread;
26         mInstrumentation = instr;
27         mToken = token;
28         mIdent = ident;
29         mApplication = application;
30         mIntent = intent;
31         mReferrer = referrer;
32         mComponent = intent.getComponent();
33         mActivityInfo = info;
34         mTitle = title;
35         mParent = parent;
36         mEmbeddedID = id;
37         mLastNonConfigurationInstances = lastNonConfigurationInstances;
38         if (voiceInteractor != null) {
39             if (lastNonConfigurationInstances != null) {
40                 mVoiceInteractor = lastNonConfigurationInstances.voiceInteractor;
41             } else {
42                 mVoiceInteractor = new VoiceInteractor(voiceInteractor, this, this,
43                         Looper.myLooper());
44             }
45         }
46 
47         mWindow.setWindowManager(
48                 (WindowManager)context.getSystemService(Context.WINDOW_SERVICE),
49                 mToken, mComponent.flattenToString(),
50                 (info.flags & ActivityInfo.FLAG_HARDWARE_ACCELERATED) != 0);
51         if (mParent != null) {
52             mWindow.setContainer(mParent.getWindow());
53         }
54         mWindowManager = mWindow.getWindowManager();
55         mCurrentConfig = config;
56     }

至此,问题1算是找到了答案。

二、Hook 掉 Context 的 startActivity 的方法

文章开头我们就说 Android 中有个两种启动 Activity 的方式,一种是 Activity.startActivity 另一种是 Context.startActivity,但需要注意的时,我们在使用 Context.startActivity 启动一个 Activity 的时候将 flags 指定为 FLAG_ACTIVITY_NEW_TASK。

在接下来的分析中需要查看 Android 源码,先推荐两个查看 Android 源码的网站:

http://androidxref.com

http://grepcode.com/project/repository.grepcode.com/java/ext/com.google.android/android/

我们试着 hook 掉 Context.startActivity 方法,我们依然随手写一个 Context 方式启动 Activity 的示例,如下:

1 Intent intent = new Intent(MainActivity.this, OtherActivity.class);
2 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
3 getApplicationContext().startActivity(intent);

照着(一)中的姿势点入 startActivity() 方法里面,由于 Context 是一个抽象类,所以我们需要找到它的实现类才能看到具体的代码,通过查看 Android 源码我们可以在 ActivityTread 中可知 Context 的实现类是 ContextImpl。(在这里大家先知道这一点就行,具体的调用细节将会在下一篇博文中详细介绍)


1 private Activity performLaunchActivity(ActivityClientRecord r, Intent customIntent) {
 2         ...
 3             if (activity != null) {
 4                 Context appContext = createBaseContextForActivity(r, activity);
 5                 CharSequence title = r.activityInfo.loadLabel(appContext.getPackageManager());
 6                 Configuration config = new Configuration(mCompatConfiguration);
 7         ...
 8 }
 9         ...
10 private Context createBaseContextForActivity(ActivityClientRecord r, final Activity activity) {
11          ContextImpl appContext = ContextImpl.createActivityContext(this, r.packageInfo, r.token);
12          appContext.setOuterContext(activity);
13          Context baseContext = appContext;
14          ...
15 }

现在我们来查看 ContextImpl.startActivity() 的源码。


1 @Override
2 public void startActivity(Intent intent) {
3         warnIfCallingFromSystemProcess();
4         startActivity(intent, null);
5 }

再进入 startActivity(intent, null) 查看源码如下:

1 @Override
 2 public void startActivity(Intent intent, Bundle options) {
 3         warnIfCallingFromSystemProcess();
 4         if ((intent.getFlags()&Intent.FLAG_ACTIVITY_NEW_TASK) == 0) {
 5             throw new AndroidRuntimeException(
 6                     "Calling startActivity() from outside of an Activity "
 7                     + " context requires the FLAG_ACTIVITY_NEW_TASK flag."
 8                     + " Is this really what you want?");
 9         }
10         mMainThread.getInstrumentation().execStartActivity(
11             getOuterContext(), mMainThread.getApplicationThread(), null,
12             (Activity)null, intent, -1, options);
13 }

由上面第四行代码可以看出在代码中判断了 intent 的 flag 类型,如果非 FLAG_ACTIVITY_NEW_TASK 类型就会抛出异常。接着看红色部分的关键代码,可以看出先从 ActivityTread 中获取到了 Instrumentation 最后还是调用了 Instrumentation 的 execStartActivity(...) 方法,我们现在需要做的就是分析 ActivityTread 类,并想办法用我们自己写的 EvilInstrumentation 类将 ActivityTread 的 mInstrumentation 替换掉。


ActivityTread 部分代码如下:

206     private static ActivityThread sCurrentActivityThread;
207     Instrumentation mInstrumentation;
...
1597    public static ActivityThread currentActivityThread() {
1598        return sCurrentActivityThread;
1599    }
...
1797    public Instrumentation getInstrumentation()
1798    {
1799        return mInstrumentation;
1800    }

这里需要告诉大家是,ActivityTread 即代表应用的主线程,而一个应用中只有一个主线程,并且由源码可知,ActivityTreadd 的对象又是以静态变量的形式存在的,太好了,这正是我们要找的 Hook 点。废话不多说了,现在我们只需利用反射通过 currentActivityThread() 方法拿到 ActivityThread 的对象,然后在将 mInstrumentation 替换成 EvilInstrumentation 即可,代码如下:

1   public static void doContextStartHook(){
 2         try {
 3             Class<?> activityThreadClass = Class.forName("android.app.ActivityThread");
 4             Method currentActivityThreadMethod = activityThreadClass.getDeclaredMethod("currentActivityThread");
 5             Object activityThread = currentActivityThreadMethod.invoke(null);
 6 
 7             Field mInstrumentationField = activityThreadClass.getDeclaredField("mInstrumentation");
 8             mInstrumentationField.setAccessible(true);
 9             Instrumentation originalInstrumentation = (Instrumentation)mInstrumentationField.get(activityThread);
10             mInstrumentationField.set(activityThread, new EvilInstrumentation(originalInstrumentation));
11         } catch (Exception e) {
12             e.printStackTrace();
13         }
14     }

其实代码也不难理解,跟 Hook Activity 的 startActivity() 方法是一个思路,只是 Hook 的点不同而已。下面我们在 MainActivity 的 attachBaseContext() 方法中调用 doContextStartHook() 方法,并添加相关测试代码,具体代码如下:

1 public class MainActivity extends Activity {
 2     private Button btn_start_by_context;
 3     @Override
 4     protected void onCreate(Bundle savedInstanceState) {
 5         super.onCreate(savedInstanceState);
 6         setContentView(R.layout.activity_main);
 7         btn_start_by_context = (Button) findViewById(R.id.btn_start_by_context);
 8         btn_start_by_context.setOnClickListener(new View.OnClickListener() {
 9             @Override
10             public void onClick(View v) {
11                 Intent intent = new Intent(MainActivity.this, OtherActivity.class);
12                 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
13                 getApplicationContext().startActivity(intent);
14             }
15         });
16     }
17     @Override
18     protected void attachBaseContext(Context newBase) {
19         super.attachBaseContext(newBase);
20         ActivityThreadHookHelper.doContextStartHook();
21     }
22 }

点击按钮后查看 Log 输出如下:

[EvilInstrumentation] : 请注意! startActivity已经被hook了!

看到这样的 Log,说明我们已经成功的 Hook 了 Context.startActivity()。而且 doContextStartHook() 方法只在程序开始的时候调用一次即可,后面在程序其他的 Activity 中调用 Context.startActivity() 时此拦截工作均可生效,这是因为 Context.startActivity() 在执行启动 Activity 的操作时调是通过 ActivityTread 获取到 Instrumentation,然后再调用 Instrumentation.execStartActivity() 方法,而 ActivityTread 在程序中是以单例的形式存在的,这就是原因。所以说调用 doContextStartHook() 方法最好的时机应该是放在 Application 中。

注意!前方惊现彩蛋一枚!!

将 doContextStartHook() 方法放入到了 MyApplication 的 attachBaseContext() 里面后,代码如下:

1 public class MyApplication extends Application {
2     @Override
3     protected void attachBaseContext(Context base) {
4         super.attachBaseContext(base);
5         ActivityThreadHookHelper.doContextStartHook();
6     }
7 }

MainActivity 的代码如下:

1 public class MainActivity extends Activity {
 2     private final static String TAG = MainActivity.class.getSimpleName();
 3     private Button btn_start_by_activity;
 4     private Button btn_start_by_context;
 5     @Override
 6     protected void onCreate(Bundle savedInstanceState) {
 7         super.onCreate(savedInstanceState);
 8         setContentView(R.layout.activity_main);
 9         btn_start_by_activity = (Button) findViewById(R.id.btn_start_by_activity);
10         btn_start_by_context = (Button) findViewById(R.id.btn_start_by_context);
11         ActivityThreadHookHelper.doActivityStartHook(this);
12         btn_start_by_activity.setOnClickListener(new View.OnClickListener() {
13             @Override
14             public void onClick(View v) {
15                 Log.i(TAG, "onClick: Activity.startActivity()");
16                 Intent intent = new Intent(MainActivity.this, OtherActivity.class);
17                 startActivity(intent);
18             }
19         });
20 
21         btn_start_by_context.setOnClickListener(new View.OnClickListener() {
22             @Override
23             public void onClick(View v) {
24                 Log.i(TAG, "onClick: Context.startActivity()");
25                 Intent intent = new Intent(MainActivity.this, OtherActivity.class);
26                 intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
27                 getApplicationContext().startActivity(intent);
28             }
29         });
30     }
31 }

代码如上,布局文件很简单就不贴出来了,就是两个按钮,一个测试 Activity.startActivity() 方法,一个测试 Context.startActivity() 方法,然后在 MainActivity 的 onCreate() 中调用了 doActivityStartHook() 在 MyApplication 里面调用了 doContextStartHook(), 目前看来代码很正常,符合我们上面的思路,但楼主在点击按钮发现 Log 输出如下:

小白也能看懂的插件化DroidPlugin原理(三)-- 如何拦截startActivity方法_Android_02

是的,Activity.startActivity 被 hook 的信息输出了两次!为什么?

我们不妨先猜想一下,一定是 Activity 的 mInstrumentation 对象在我们替换之前就已经变成了 EvilInstrumentation, 然后我们又在 Activity.onCreate 方法调用了一次 doActivityStartHook(), 相当于我们又用 EvilInstrumentation 又重写了 EvilInstrumentation 的 startActivity() 方法,所以导致 log 信息输出了两次。

那问题又来了,为什么 Activity 的 mInstrumentation 对象在我们替换之前就已经变成了 EvilInstrumentation?

纵观代码,只有一个地方有疑点,那就是我们放到 MyApplication.attachBaseContext() 方法里面的 doContextStartHook() 起的作用!

还是先直接简单说一下事实的真相吧,结合上文所说,一个应用内只存在一个 ActivityTread 对象,也只存在一个 Instrumentation 对象,这个 Instrumentation 是 ActivityTread 的成员变量,并在 ActivityTread 内完成初始化,在启动一个 Activity 的流程中大概在最后的位置 ActivityTread 会回调 Activity 的 attach() 方法,并将自己的 Instrumentation 对象传给 Activity。启动 Activity 的详细流程及调用细节将会在下一篇博文介绍,敬请期待!

三、小结

本篇文章通过拦截 Context.startActivity() 和 Activity.startActivity() 两个方法,将上一篇文章中介绍的 Hook 技术实践 Activity 的启动流程之中,同时通过这两个小例子初步了解了 Android 源码以及怎么样去选定一个合适的 Hook 点。想要了解插件化的基本原理,熟悉 Activity 的启动流程是必不可少的,下一篇文章将会详细介绍 Activity 的启动流程,感兴趣的同学可以关注一下!


标签:Instrumentation,插件,startActivity,--,Intent,Context,Activity,intent
From: https://blog.51cto.com/u_16175637/7554969

相关文章

  • Android Framework原理解决大龄程序员的催命符
    有人说对于咱们程序员而言:每过一年,都像是在催命。35岁的坎是每个程序员都逃不过的宿命,每过一年离这个坎就又近一步。所以大家都很焦虑,而这份焦虑恰恰又被各种自媒体,公众号,博客等等平台所利用,每年都有人在说互联网又寒冬了,某某公司又大规模裁员了,Android开发不行了这类的负面的消息......
  • 联发科MTK6877/MT6877(天玑900)安卓核心板_5G安卓AI智能模块
    MTK/联发科5G安卓AI智能模块(MT6877天玑900平台)开发板方案定制支持NR-SA/NR-NSA/LTE-FDD(CAT-18)/LTE-TDD(CAT-18)/WCDMA/TD-SCDMA/EVDO/CDMA/GSM等多种制式;支持WiFi6,802.11a/b/g/n/ac/ax,BTv2.1+EDR,3.0+HS,v4.1+HS,V5.2,支持Beidou(北斗),Galileo,Glonass,GPS,QZSS,GNSS(L1+L2......
  • 良好的测试环境应该怎么搭建?对软件产品起到什么作用?
    为了确保软件产品的高质量,搭建一个良好的测试环境是至关重要的。在本文中,我们将从多个角度出发,详细描述良好的测试环境的搭建方法、注意事项以及对软件产品的作用。一、软件测试环境的搭建1、从硬件设备的选择与配置开始。对于大型软件产品的测试,建议使用高性能的服务......
  • 安卓音视频入门难,分享一份杭州某大厂音视频内部文档
    前言最近在写作过程中,我注意到很多读者私下向我提问,他们对安卓音视频方面非常感兴趣,但苦于没有系统的学习方法。今天,我想和大家分享一些我在音视频开发方面的经验。首先,要学习音视频开发,你需要掌握一些基础知识点,这些知识点包括:FFmpeg:这是一款强大的音视频处理库,可以帮助你进行音视......
  • 宠物社区-如何搭建一个宠物社交平台
    随着我国宠物经济的蓬勃发展和市场规模的稳步扩大,尽管与一些已经成熟的发达国家相比,国内宠物行业仍有广阔的发展前景,因此宠物行业蕴含着巨大的发展机遇;数据来源:艾瑞咨询(截至2021年3月)数据来源:中国宠物行业白皮书——2022年中国宠物消费报告随着时间的推移,宠物主人对于宠物的情感需......
  • 【虹科分享】杭州亚运会精彩纷呈,在下来摆龙门阵!
    咳咳,江湖传闻,有武林盟会,相约每隔四年,擂鼓鸣金,以武会友。华夏之国,尝历事东道主,庚午年八月初四于燕京(北京)、庚寅年十月初七于楚庭(广州),数年已过,今岁,钱塘将承此盛会!此可谓是空前盛事,来赴会者约有数万之众。所谓,有朋自远方来,不亦乐乎?于是,就有了这钱塘之壮观,号称:给我一个亚运会,我要惊起整......
  • 产品经理想升职加薪?这个证书你考了吗?
    考证是一种强制的系统化学习,通过反复的理论学习,可以帮助大家迅速成长起来,有领悟能力较强者,甚至可结合工作生活中的经验和经历,达到个人业务能力的提升。 两个同样能力的人,如果你有更多的证书,或者学历更高,会更加受青睐。既然这样,让我们一起来了解下有关产品经理必备的证书吧。 NPDP......
  • Java(day20):泛型和枚举
    前言Java是一种面向对象的、跨平台的编程语言,在软件开发中应用广泛。在Java中,泛型和枚举是两种重要的特性,它们能够提高代码的可读性和重用性。本文将介绍Java泛型和枚举的概念、语法、使用方法、测试用例等方面。摘要泛型是Java的一种抽象类型,它允许使用者在编写代码时不指定数......
  • 资深java面试题及答案整理
    编写Java程序时,如何在Java中创建死锁并修复它?经典但核心Java面试问题之一。如果你没有参与过多线程并发Java应用程序的编码,你可能会失败。如何避免Java线程死锁?如何避免Java中的死锁?是Java面试的热门问题之一,也是多线程的编程中的重口味之一,主要在招高级程序员时......
  • unity3d 清空控制台
    unity3d清空控制台usingSystem.Collections;usingSystem.Collections.Generic;usingUnityEngine;usingUnityEngine.UI;usingAssemblyCSharp;usingSystem;#ifUNITY_EDITORusingUnityEditor;#endifpublicclassVCClearConsole:MonoBehaviour{//......