Android Hook Activity 的几种姿势

将看到以下 log


Hook AMS


上面 hook activity 的两种方法其实都有一定缺陷,比如,第一种方法,只能 hook 住通过 Activity startActivity 的 activity。第二种方法,只能 hook 住通过 getApplicationContext().startActivity 启动的 activity。那有没有一种方法能 hook 上述两种的,其实是有的,那就是 hook AMS。下面让我们一起来看一下。

上面 hook startActivity 其实都是 hook 相应的 mInstrumentation.execStartActivity 方法,因此,我们可以从这里下手,看 mInstrumentation.execStartActivity 里面有没有一些共性的东西,可以 hook。

我们先来 mInstrumentation.execStartActivity 方法

public ActivityResult execStartActivity(

Context who, IBinder contextThread, IBinder token, Activity target,

Intent intent, int requestCode, Bundle options) {

IApplicationThread whoThread = (IApplicationThread) contextThread;

Uri referrer = target != null ? target.onProvideReferrer() : null;

if (referrer != null) {

intent.putExtra(Intent.EXTRA_REFERRER, referrer);

}

if (mActivityMonitors != null) {

synchronized (mSync) {

final int N = mActivityMonitors.size();

for (int i=0; i<N; i++) {

final ActivityMonitor am = mActivityMonitors.get(i);

ActivityResult result = null;

if (am.ignoreMatchingSpecificIntents()) {

result = am.onStartActivity(intent);

}

if (result != null) {

am.mHits++;

return result;

} else if (am.match(who, null, intent)) {

am.mHits++;

if (am.isBlocking()) {

return requestCode >= 0 ? am.getResult() : null;

}

break;

}

}

}

}

try {

intent.migrateExtraStreamToClipData();

intent.prepareToLeaveProcess(who);

int result = ActivityManager.getService()

.startActivity(whoThread, who.getBasePackageName(), intent,

intent.resolveTypeIfNeeded(who.getContentResolver()),

token, target != null ? target.mEmbeddedID : null,

requestCode, 0, null, options);

checkStartActivityResult(result, intent);

} catch (RemoteException e) {

throw new RuntimeException(“Failure from system”, e);

}

return null;

}

这里我们留意 ActivityManager.getService().startActivity 这个方法

public static IActivityManager getService() {

return IActivityManagerSingleton.get();

}

private static final Singleton IActivityManagerSingleton =

new Singleton() {

@Override

protected IActivityManager create() {

final IBinder b = ServiceManager.getService(Context.ACTIVITY_SERVICE);

final IActivityManager am = IActivityManager.Stub.asInterface(b);

return am;

}

};

可以看到 IActivityManagerSingleton 是一个单例对象,因此,我们可以 hook 它。

public static void hookAMSAfter26() throws Exception {

// 第一步:获取 IActivityManagerSingleton

Class<?> aClass = Class.forName(“android.app.ActivityManager”);

Field declaredField = aClass.getDeclaredField(“IActivityManagerSingleton”);

declaredField.setAccessible(true);

Object value = declaredField.get(null);

Class<?> singletonClz = Class.forName(“android.util.Singleton”);

Field instanceField = singletonClz.getDeclaredField(“mInstance”);

instanceField.setAccessible(true);

Object iActivityManagerObject = instanceField.get(value);

// 第二步:获取我们的代理对象,这里因为 IActivityManager 是接口,我们使用动态代理的方式

Class<?> iActivity = Class.forName(“android.app.IActivityManager”);

InvocationHandler handler = new AMSInvocationHandler(iActivityManagerObject);

Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new

Class<?>[]{iActivity}, handler);

// 第三步:偷梁换柱,将我们的 proxy 替换原来的对象

instanceField.set(value, proxy);

}

public class AMSInvocationHandler implements InvocationHandler {

private static final String TAG = “AMSInvocationHandler”;

Object iamObject;

public AMSInvocationHandler(Object iamObject) {

this.iamObject = iamObject;

}

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

// Log.e(TAG, method.getName());

if (“startActivity”.equals(method.getName())) {

Log.i(TAG, “ready to startActivity”);

for (Object object : args) {

Log.d(TAG, “invoke: object=” + object);

}

}

return method.invoke(iamObject, args);

}

}

执行以下测试代码

try {

HookHelper.hookAMS();

} catch (Exception e) {

e.printStackTrace();

}

startActivity(new Intent(this,TestActivityStart.class));

将会看到以下 log

I/AMSInvocationHandler: ready to startActivity

接下来我们一起来看一下 API 25 Instrumentation 的代码(自 API 26 开始 ,Instrumentation execStartActivity 方法有所改变)

public ActivityResult execStartActivity(

Context who, IBinder contextThread, IBinder token, Activity target,

Intent intent, int requestCode, Bundle options) {

IApplicationThread whoThread = (IApplicationThread) contextThread;

Uri referrer = target != null ? target.onProvideReferrer() : null;

if (referrer != null) {

intent.putExtra(Intent.EXTRA_REFERRER, referrer);

}

if (mActivityMonitors != null) {

synchronized (mSync) {

final int N = mActivityMonitors.size();

for (int i=0; i<N; i++) {

final ActivityMonitor am = mActivityMonitors.get(i);

if (am.match(who, null, intent)) {

am.mHits++;

if (am.isBlocking()) {

return requestCode >= 0 ? am.getResult() : null;

}

break;

}

}

}

}

try {

intent.migrateExtraStreamToClipData();

intent.prepareToLeaveProcess(who);

int result = ActivityManagerNative.getDefault()

.startActivity(whoThread, who.getBasePackageName(), intent,

intent.resolveTypeIfNeeded(who.getContentResolver()),

token, target != null ? target.mEmbeddedID : null,

requestCode, 0, null, options);

checkStartActivityResult(result, intent);

} catch (RemoteException e) {

throw new RuntimeException(“Failure from system”, e);

}

return null;

}

可以看到这里启动 activity 是调用 ActivityManagerNative.getDefault().startActivity 启动的。

public abstract class ActivityManagerNative extends Binder implements IActivityManager

{

/**

  • Retrieve the system’s default/global activity manager.

*/

static public IActivityManager getDefault() {

return gDefault.get();

}

private static final Singleton gDefault = new Singleton() {

protected IActivityManager create() {

IBinder b = ServiceManager.getService(“activity”);

if (false) {

Log.v(“ActivityManager”, "default service binder = " + b);

}

IActivityManager am = asInterface(b);

if (false) {

Log.v(“ActivityManager”, "default service = " + am);

}

return am;

}

};

}

同理我们看到 ActivityManagerNative 的 gDefault 是一个静态变量,因此,我们可以尝试 hook gDefault.

public static void hookAmsBefore26() throws Exception {

// 第一步:获取 IActivityManagerSingleton

Class<?> forName = Class.forName(“android.app.ActivityManagerNative”);

Field defaultField = forName.getDeclaredField(“gDefault”);

defaultField.setAccessible(true);

Object defaultValue = defaultField.get(null);

Class<?> forName2 = Class.forName(“android.util.Singleton”);

Field instanceField = forName2.getDeclaredField(“mInstance”);

instanceField.setAccessible(true);

Object iActivityManagerObject = instanceField.get(defaultValue);

// 第二步:获取我们的代理对象,这里因为 IActivityManager 是接口,我们使用动态代理的方式

Class<?> iActivity = Class.forName(“android.app.IActivityManager”);

InvocationHandler handler = new AMSInvocationHandler(iActivityManagerObject);

Object proxy = Proxy.newProxyInstance(Thread.currentThread().getContextClassLoader(), new Class<?>[]{iActivity}, handler);

// 第三步:偷梁换柱,将我们的 proxy 替换原来的对象

instanceField.set(defaultValue, proxy);

}

到此,hook Activity 的三种方式已讲解完毕


启动一个没有在 AndroidManifest 声明的 Activity


我们知道,当我们启动一个没有在 AndroidManifest 中声明的 activity,会抛出 ActivityNotFoundException 异常。

Caused by: android.content.ActivityNotFoundException: Unable to find explicit activity class {com.xj.hookdemo/com.xj.hookdemo.activityhook.TargetAppCompatActivity}; have you declared this activity in your AndroidManifest.xml?

at android.app.Instrumentation.checkStartActivityResult(Instrumentation.java:2124)

at android.app.Instrumentation.execStartActivity(Instrumentation.java:1802)

at android.app.Activity.startActivityForResult(Activity.java:4514)

at android.support.v4.app.BaseFragmentActivityApi16.startActivityForResult(BaseFragmentActivityApi16.java:54)

at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:67)

at android.app.Activity.startActivityForResult(Activity.java:4472)

at android.support.v4.app.FragmentActivity.startActivityForResult(FragmentActivity.java:720)

at android.app.Activity.startActivity(Activity.java:4833)

at android.app.Activity.startActivity(Activity.java:4801)

at com.xj.hookdemo.activityhook.TestStartActivityNoRegister.onB

从报错的堆栈中,我们非常定位到 Instrumentation.execStartActivity 方法

public ActivityResult execStartActivity(

Context who, IBinder contextThread, IBinder token, String resultWho,

Intent intent, int requestCode, Bundle options, UserHandle user) {

----- // 省略若干代码

try {

intent.migrateExtraStreamToClipData();

intent.prepareToLeaveProcess(who);

int result = ActivityManager.getService()

.startActivityAsUser(whoThread, who.getBasePackageName(), intent,

intent.resolveTypeIfNeeded(who.getContentResolver()),

token, resultWho,

requestCode, 0, null, options, user.getIdentifier());

checkStartActivityResult(result, intent);

} catch (RemoteException e) {

throw new RuntimeException(“Failure from system”, e);

}

return null;

}

在该方法中,调用 startActivityAsUser 方法通过传入的 intent 获取 result,再通过 checkStartActivityResult 方法,判断 result 是否合法。

而我们知道我们启动的 activity 信息都储存在 intent 中,那么我们若想要 启动一个没有在 AndroidManifest 声明的 Activity,那我们只需要在 某个时机,即调用 startActivity 方法之前欺骗 AMS 我们的 activity 已经注册(即替换 intent),这样就不会抛出 ActivityNotFoundException 异常。

在前面的时候,我们已经讲解到如何 hook ams,这里我们不再具体讲述,主要步骤如下

  • 第一步, API 26 以后,hook android.app.ActivityManager.IActivityManagerSingleton, API 25 以前,hook android.app.ActivityManagerNative.gDefault

  • 第二步,获取我们的代理对象,这里因为是接口,所以我们使用动态代理的方式

  • 第三步:设置为我们的代理对象

private static void hookAMS(Context context) throws ClassNotFoundException,

NoSuchFieldException, IllegalAccessException {

// 第一步, API 26 以后,hook android.app.ActivityManager.IActivityManagerSingleton,

// API 25 以前,hook android.app.ActivityManagerNative.gDefault

Field gDefaultField = null;

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {

Class<?> activityManager = Class.forName(“android.app.ActivityManager”);

gDefaultField = activityManager.getDeclaredField(“IActivityManagerSingleton”);

} else {

Class<?> activityManagerNativeClass = Class.forName(“android.app.ActivityManagerNative”);

gDefaultField = activityManagerNativeClass.getDeclaredField(“gDefault”);

}

gDefaultField.setAccessible(true);

Object gDefaultObj = gDefaultField.get(null); //所有静态对象的反射可以通过传null获取。如果是实列必须传实例

Class<?> singletonClazz = Class.forName(“android.util.Singleton”);

Field amsField = singletonClazz.getDeclaredField(“mInstance”);

amsField.setAccessible(true);

Object amsObj = amsField.get(gDefaultObj);

//

String pmName = getPMName(context);

String hostClzName = getHostClzName(context, pmName);

// 第二步,获取我们的代理对象,这里因为是接口,所以我们使用动态代理的方式

amsObj = Proxy.newProxyInstance(context.getClass().getClassLoader(), amsObj.getClass()

.getInterfaces(), new AMSHookInvocationHandler(amsObj, pmName, hostClzName));

// 第三步:设置为我们的代理对象

amsField.set(gDefaultObj, amsObj);

}

接着,我们在动态代理对象中,当调用 startActivity 方法的时候,我们把 intent 信息替换,校验的时候就可以绕过系统对 activity 的校验,这样就不会跑出 ActivityNotFoundException 异常。

public class AMSHookInvocationHandler implements InvocationHandler {

public static final String ORIGINALLY_INTENT = “originallyIntent”;

private Object mAmsObj;

private String mPackageName;

private String cls;

public AMSHookInvocationHandler(Object amsObj, String packageName, String cls) {

this.mAmsObj = amsObj;

this.mPackageName = packageName;

this.cls = cls;

}

@Override

public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {

// 对 startActivity进行Hook

if (method.getName().equals(“startActivity”)) {

int index = 0;

// 找到我们启动时的intent

for (int i = 0; i < args.length; i++) {

if (args[i] instanceof Intent) {

index = i;

break;

}

}

// 取出在真实的Intent

Intent originallyIntent = (Intent) args[index];

Log.i(“AMSHookUtil”, “AMSHookInvocationHandler:” + originallyIntent.getComponent()

.getClassName());

// 自己伪造一个配置文件已注册过的Activity Intent

Intent proxyIntent = new Intent();

// 因为我们调用的Activity没有注册,所以这里我们先偷偷换成已注册。使用一个假的Intent

ComponentName componentName = new ComponentName(mPackageName, cls);

proxyIntent.setComponent(componentName);

// 在这里把未注册的Intent先存起来 一会儿我们需要在Handle里取出来用

proxyIntent.putExtra(ORIGINALLY_INTENT, originallyIntent);

args[index] = proxyIntent;

}

return method.invoke(mAmsObj, args);

}

}

但是,如果仅仅这样做,会存在一个问题,因为 intent 信息在校验的时候被我们替换了,但是我们并没有将其还原,这样,启动的 activity 就不是我们想要的 activity。

那么,我们要在哪个实际将 intent 信息还原呢?

我们回过头再来看一下 Activity 的 startActivityForResult 方法

public void startActivityForResult(@RequiresPermission Intent intent, int requestCode,

@Nullable Bundle options) {

if (mParent == null) {

options = transferSpringboardActivityOptions(options);

Instrumentation.ActivityResult ar =

mInstrumentation.execStartActivity(

this, mMainThread.getApplicationThread(), mToken, this,

intent, requestCode, options);

if (ar != null) {

mMainThread.sendActivityResult(

mToken, mEmbeddedID, requestCode, ar.getResultCode(),

ar.getResultData());

}

if (requestCode >= 0) {

// If this start is requesting a result, we can avoid making

// the activity visible until the result is received. Setting

// this code during onCreate(Bundle savedInstanceState) or onResume() will keep the

// activity hidden during this time, to avoid flickering.

// This can only be done when a result is requested because

// that guarantees we will get information back when the

// activity is finished, no matter what happens to it.

mStartedActivity = true;

}

cancelInputsAndStartExitTransition(options);

// TODO Consider clearing/flushing other event sources and events for child windows.

} else {

if (options != null) {

mParent.startActivityFromChild(this, intent, requestCode, options);

} else {

// Note we want to go through this method for compatibility with

// existing applications that may have overridden it.

mParent.startActivityFromChild(this, intent, requestCode);

}

}

}

该方法主要分为两个逻辑,当 mParent 为空的时候即不为空的时候

  • 第一种情况,mParent 不为空的时候,调用到 mInstrumentation.execStartActivity 方法之后,会调用 mMainThread.sendActivityResult 方法

  • 第二种情况,当 mParent 为空的时候,会调用 mParent.startActivityFromChild

public void startActivityFromChild(@NonNull Activity child, @RequiresPermission Intent intent,

int requestCode, @Nullable Bundle options) {

options = transferSpringboardActivityOptions(options);

Instrumentation.ActivityResult ar =

mInstrumentation.execStartActivity(

this, mMainThread.getApplicationThread(), mToken, child,

intent, requestCode, options);

if (ar != null) {

mMainThread.sendActivityResult(

mToken, child.mEmbeddedID, requestCode,

ar.getResultCode(), ar.getResultData());

}

cancelInputsAndStartExitTransition(options);

}

在 startActivityFromChild 方法里面,又会调用到 mMainThread.sendActivityResult 方法。因此,我们只需看一下该方法是怎样 send ActivityResult 的。

public final class ActivityThread {


public final void sendActivityResult(

IBinder token, String id, int requestCode,

int resultCode, Intent data) {

if (DEBUG_RESULTS) Slog.v(TAG, “sendActivityResult: id=” + id

  • " req=" + requestCode + " res=" + resultCode + " data=" + data);

ArrayList list = new ArrayList();

list.add(new ResultInfo(id, requestCode, resultCode, data));

mAppThread.scheduleSendResult(token, list);

}

public final void scheduleSendResult(IBinder token, List results) {

ResultData res = new ResultData();

res.token = token;

res.results = results;

sendMessage(H.SEND_RESULT, res);

}

private void sendMessage(int what, Object obj, int arg1, int arg2, boolean async) {

if (DEBUG_MESSAGES) Slog.v(

TAG, "SCHEDULE " + what + " " + mH.codeToString(what)

  • ": " + arg1 + " / " + obj);

Message msg = Message.obtain();

msg.what = what;

msg.obj = obj;

msg.arg1 = arg1;

msg.arg2 = arg2;

if (async) {

msg.setAsynchronous(true);

}

mH.sendMessage(msg);

}

final H mH = new H();

}

跟踪 ActivityThread 的代码发现 sendActivityResult 方法会调用 scheduleSendResult 方法发送,而 scheduleSendResult 方法又会调用 sendMessage 方法,在 sendMessage 方法里面,会调用 mH 发送消息(即 Handler)

因此,我们只需要在回调 H 的 handleMessage 消息之前还原我们的 intent 信息即可。

private class H extends Handler {

public void handleMessage(Message msg) {

if (DEBUG_MESSAGES) Slog.v(TAG, ">>> handling: " + codeToString(msg.what));

switch (msg.what) {

case LAUNCH_ACTIVITY: {

Trace.traceBegin(Trace.TRACE_TAG_ACTIVITY_MANAGER, “activityStart”);

final ActivityClientRecord r = (ActivityClientRecord) msg.obj;

r.packageInfo = getPackageInfoNoCheck(

r.activityInfo.applicationInfo, r.compatInfo);

handleLaunchActivity(r, null, “LAUNCH_ACTIVITY”);

Trace.traceEnd(Trace.TRACE_TAG_ACTIVITY_MANAGER);

} break;

}

ok,这里我们重新理一下 Activity 大概的启动流程:

app 调用 startActivity 方法 -> Instrumentation 类通过 ActivityManagerNative 或者 ActivityManager( API 26以后)将启动请求发送给 AMS -> AMS 进行一系列检查并将此请求通过 Binder 派发给所属 app -> app 通过 Binder 收到这个启动请求 -> ActivityThread 中的实现将收到的请求进行封装后送入 Handler -> 从 Handler 中取出这个消息,开始 app 本地的 Activity 初始化和启动逻辑。

hook ActivityThread 的 mH

/**

  • @param context

  • @param isAppCompatActivity 表示是否是 AppCompatActivity

  • @throws Exception

*/

private static void hookLaunchActivity(Context context, boolean isAppCompatActivity) throws

Exception {

Class<?> activityThreadClazz = Class.forName(“android.app.ActivityThread”);

Field sCurrentActivityThreadField = activityThreadClazz.getDeclaredField(“sCurrentActivityThread”);

sCurrentActivityThreadField.setAccessible(true);

Object sCurrentActivityThreadObj = sCurrentActivityThreadField.get(null);

Field mHField = activityThreadClazz.getDeclaredField(“mH”);

mHField.setAccessible(true);

Handler mH = (Handler) mHField.get(sCurrentActivityThreadObj);

Field callBackField = Handler.class.getDeclaredField(“mCallback”);

callBackField.setAccessible(true);

callBackField.set(mH, new ActivityThreadHandlerCallBack(context, isAppCompatActivity));

}

public static class ActivityThreadHandlerCallBack implements Handler.Callback {

private final boolean mIsAppCompatActivity;

private final Context mContext;

public ActivityThreadHandlerCallBack(Context context, boolean isAppCompatActivity) {

mIsAppCompatActivity = isAppCompatActivity;

mContext = context;

}

@Override

public boolean handleMessage(Message msg) {

int LAUNCH_ACTIVITY = 0;

try {

Class<?> clazz = Class.forName(“android.app.ActivityThread$H”);

Field field = clazz.getField(“LAUNCH_ACTIVITY”);

LAUNCH_ACTIVITY = field.getInt(null);

} catch (Exception e) {

}

if (msg.what == LAUNCH_ACTIVITY) {

handleLaunchActivity(mContext, msg, mIsAppCompatActivity);

}

return false;

}

}

private static void handleLaunchActivity(Context context, Message msg, boolean

isAppCompatActivity) {

try {

Object obj = msg.obj;

Field intentField = obj.getClass().getDeclaredField(“intent”);

intentField.setAccessible(true);

Intent proxyIntent = (Intent) intentField.get(obj);

//拿到之前真实要被启动的Intent 然后把Intent换掉

Intent originallyIntent = proxyIntent.getParcelableExtra(ORIGINALLY_INTENT);

if (originallyIntent == null) {

return;

}

proxyIntent.setComponent(originallyIntent.getComponent());

Log.e(TAG, “handleLaunchActivity:” + originallyIntent.getComponent().getClassName());

// 如果不需要兼容 AppCompatActivity

if (!isAppCompatActivity) {

return;

}

//兼容AppCompatActivity,假如不加上该方法,当 activity instanceOf AppCompatActivity 时,会抛出 PackageManager$NameNotFoundException 异常。

hookPM(context);

} catch (Exception e) {

e.printStackTrace();

}

}

运行以上代码,当你启动一个没有在 AndroidManifest 注册的 Activity,你会发现是可以正常启动的。但是,当未注册的 Activity 是 AppCompatActivity 的子类的时候,会抛出以下异常

Caused by: java.lang.IllegalArgumentException: android.content.pm.PackageManager$NameNotFoundException: ComponentInfo{com.xj.hookdemo/com.xj.hookdemo.activityhook.TargetAppCompatActivity}

at android.support.v4.app.NavUtils.getParentActivityName(NavUtils.java:215)

at android.support.v7.app.AppCompatDelegateImplV9.onCreate(AppCompatDelegateImplV9.java:155)

at android.support.v7.app.AppCompatDelegateImplV14.onCreate(AppCompatDelegateImplV14.java:59)

at android.support.v7.app.AppCompatActivity.onCreate(AppCompatActivity.java:72)

at com.xj.hookdemo.activityhook.TargetAppCompatActivity.onCreate(TargetAppCompatActivity.java:12)

at android.app.Activity.performCreate(Activity.java:7026)

at android.app.Activity.performCreate(Activity.java:7017)

at android.app.Instrumentation.callActivityOnCreate(Instrumentation.java:1231)

从上面的异常信息来看,主要是在 NavUtils.getParentActivityName 方法中抛出异常。

@Nullable

public static String getParentActivityName(Context context, ComponentName componentName)

throws NameNotFoundException {

PackageManager pm = context.getPackageManager();

ActivityInfo info = pm.getActivityInfo(componentName, PackageManager.GET_META_DATA);

if (Build.VERSION.SDK_INT >= 16) {

String result = info.parentActivityName;

if (result != null) {

return result;

}

}

if (info.metaData == null) {

return null;

}
自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

img

img

img

img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

【Android高级架构视频学习资源】

**Android部分精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!

OPPO等大厂,18年进入阿里一直到现在。**

深知大多数初中级Android工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则近万的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Android移动开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。

[外链图片转存中…(img-YmAUbl7j-1712140495653)]

[外链图片转存中…(img-va03RQvx-1712140495654)]

[外链图片转存中…(img-GtVXZvm3-1712140495655)]

[外链图片转存中…(img-WjVTZUIs-1712140495655)]

[外链图片转存中…(img-xL36K7qK-1712140495655)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Android开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注:Android)

最后

其实Android开发的知识点就那么多,面试问来问去还是那么点东西。所以面试没有其他的诀窍,只看你对这些知识点准备的充分程度。so,出去面试时先看看自己复习到了哪个阶段就好。

上面分享的腾讯、头条、阿里、美团、字节跳动等公司2019-2021年的高频面试题,博主还把这些技术点整理成了视频和PDF(实际上比预期多花了不少精力),包含知识脉络 + 诸多细节,由于篇幅有限,上面只是以图片的形式给大家展示一部分。

【Android思维脑图(技能树)】

知识不体系?这里还有整理出来的Android进阶学习的思维脑图,给大家参考一个方向。

[外链图片转存中…(img-210o4yrl-1712140495656)]

【Android高级架构视频学习资源】

**Android部分精讲视频领取学习后更加是如虎添翼!**进军BATJ大厂等(备战)!现在都说互联网寒冬,其实无非就是你上错了车,且穿的少(技能),要是你上对车,自身技术能力够强,公司换掉的代价大,怎么可能会被裁掉,都是淘汰末端的业务Curd而已!现如今市场上初级程序员泛滥,这套教程针对Android开发工程师1-6年的人员、正处于瓶颈期,想要年后突破自己涨薪的,进阶Android中高级、架构师对你更是如鱼得水,赶快领取吧!

《Android学习笔记总结+移动架构视频+大厂面试真题+项目实战源码》,点击传送门即可获取!
  • 10
    点赞
  • 29
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值