安卓修改大师可以在没有源代码的基础上,通过代码注入插桩的方式,添加任何界面和任何逻辑功能。本教程主要通过在一款名为“多媒体评价器”的app上,将原来的显示静态图片的图片框变为多图片轮播的功能。通过讲解,给大家一个明确的插桩方式添加业务逻辑代码的思路,抛砖引玉而已。
为了方便大家按照本教程操作,附带了所需要的文件,请点击这里下载
1、 需求描述:根据用户的需要,需要在下述截屏应用的右侧添加图片轮播功能(目前是单独的图片,不能多张滚动),要求图片内置在apk中,放到Assets目录下面的指定文件夹中,图片数量不限,自动从该文件夹读取图片并随机自动轮播显示。

2、 在没有源代码的情况下,如果要在apk中添加额外的逻辑,实现自定义功能,需要通过代码注入的方式来实现。一般的做法是,先用Android Studio开发一个完整实现所需功能的Demo项目,然后编译为apk,并通过安卓修改大师将apk进行反编译,获得smali代码和资源文件,最终将获得的代码和资源文件整合到目标项目,重新打包即可。
3、 按照上述思路分步骤进行讲解说明,向大家完整展示如何通过插桩注入的方式,在任意的apk添加额外逻辑。
第一步:创建Android Studio项目,并实现一个从Asset目录读取图片,并在ViewPaper实现轮播功能的工具类。代码如下:
public class MarqueeImageControl {
static ViewPager viewPager;
static ArrayList<ImageView> imageviews;
static Activity context;
// 图片资源
static Hashtable<Integer, AdData> hsAd = new Hashtable<Integer, AdData>();
static int preposition = 0;// 设置高亮的位置
static Handler handler = new Handler() {
public void handleMessage(android.os.Message msg) {
int item = viewPager.getCurrentItem() + 1;
viewPager.setCurrentItem(item);
// 延迟发消息
handler.sendEmptyMessageDelayed(0, 3000);
}
;
};
static boolean isdragging = false;
public static class AdData {
public String Title;
public String Url;
public Bitmap image;
public AdData(String Title, String Url, Bitmap image) {
this.Title = Title;
this.Url = Url;
this.image = image;
}
}
public static void show(final Activity context, int resid) {
try {
AssetManager assets = context.getAssets();
//获取/assets/目录下所有文件
String[] images = assets.list("pics");
if (images == null) return;
for (int i = 0; i < images.length; i++) {
hsAd.put(i, new AdData("", "pics/" + images[i], null));
}
if (hsAd.size() <= 0)
return;
viewPager = new ViewPager(context);
viewPager.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
ViewGroup view = context.findViewById(resid);
view.removeAllViews();
view.addView(viewPager);
context.runOnUiThread(new Runnable() {
public void run() {
imageviews = new ArrayList<ImageView>();
for (int i = 0; i < hsAd.size(); i++) {
AdData adData = (AdData) hsAd.get(i);
ImageView imageview = new ImageView(context);
AssetManager assets = context.getAssets();
InputStream in = null;
try {
in = assets.open(adData.Url);
imageview.setImageBitmap(BitmapFactory.decodeStream(in));
} catch (IOException e) {
e.printStackTrace();
}
imageview.setScaleType(ImageView.ScaleType.FIT_START);
imageview.setLayoutParams(new ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
imageviews.add(imageview);
}
viewPager.setAdapter(new Mypager());
viewPager.setOnPageChangeListener(new myon());
int item = Integer.MAX_VALUE / 2 - Integer.MAX_VALUE / 2 % imageviews.size();
viewPager.setCurrentItem(item);
handler.sendEmptyMessageDelayed(0, 3000);
}
});
} catch (Exception e) {
e.printStackTrace();
}
}
public static class myon implements ViewPager.OnPageChangeListener {
@Override
public void onPageScrollStateChanged(int arg0) {
if (arg0 == ViewPager.SCROLL_STATE_DRAGGING) {// 拖拽
isdragging = true;
} else if (arg0 == ViewPager.SCROLL_STATE_SETTLING) {// 滚动
} else if (arg0 == ViewPager.SCROLL_STATE_IDLE && isdragging) {// 静止
isdragging = false;
handler.removeCallbacksAndMessages(null);
handler.sendEmptyMessageDelayed(0, 3000);
}
}
@Override
public void onPageScrolled(int arg0, float arg1, int arg2) {
}
@Override
public void onPageSelected(int arg0) {
int realpostion = arg0 % imageviews.size();
preposition = realpostion;
}
}
public static class Mypager extends PagerAdapter {
@Override
public int getCount() {
return Integer.MAX_VALUE;// int类型的最大值
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
int realPostion = position % imageviews.size();
final ImageView imageview = imageviews.get(realPostion);
container.addView(imageview);// 添加到Viewpager中
imageview.setOnTouchListener(new OnTouchListener() {
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:// 手指按下时的操作
handler.removeCallbacksAndMessages(null);
break;
case MotionEvent.ACTION_MOVE:// 手指移动时的操作
break;
case MotionEvent.ACTION_CANCEL:// 事件取消
handler.removeCallbacksAndMessages(null);
handler.sendEmptyMessageDelayed(0, 3000);
break;
case MotionEvent.ACTION_UP:// 手指抬起时的操作
handler.removeCallbacksAndMessages(null);
handler.sendEmptyMessageDelayed(0, 3000);
break;
}
return false;
}
});
imageview.setTag(realPostion);
return imageview;
}
@Override
public boolean isViewFromObject(View arg0, Object arg1) {
return arg0 == arg1;
}
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
container.removeView((View) object);
}
}
}
需要重点说明的是,为了减少整合的复杂度,插桩的代码尽量放到单独的类里面,入口的调用方法尽量是静态方法,例如本例的入口调用函数是:
public static void show(final Activity context, int resid)
该方法有两个参数,一个是当前Activity类入口,另外一个是插入轮播图片的宿主布局的资源id。插桩代码放到单独类的好处是,反编译后将该类所有的生成的smali文件全部拷贝到目标项目中即可,不用考虑彼此之前的关联关系,也不用考虑类和变量的耦合问题,降低整合的复杂度,使整合更简单。
在Demo的Activity测试页面中调用上述的方法为:
MarqueeImageControl.show(this, R.id.pic);
上述的R.id.pic是xml布局中定义的一个类似于LinerLayout这样的布局,作为放置轮播功能的控件宿主。
代码为:
<LinearLayout
android:orientation="vertical"
android:id="@+id/pic"
android:layout_gravity="center"
android:background="@color/cardview_dark_background"
android:layout_width="300dp"
android:layout_height="600dp"/>
在将来插桩整合的时候,目标应用中也应该有这样的控件,用来接纳需要添加进来的轮播功能。
确保经过测试,该demo实现了相关的功能,然后通过Android Studio的打包功能,将Demo项目打包为apk备用。
第二步:将上述Demo的Apk文件通过安卓修改大师反编译,反编译后获得smali代码,将获得的代码和资源复制到目标项目中进行整合。
反编译demo项目并打开目录,同样也打开目标项目的项目目录,如下图:

将上述demo反编译生成的类拖拽到目标项目的smali目录下,demo目录下面的类文件请通过类的包名路径在上述目录中依次展开找到。需要植入的插桩的smali类文件可以放到目标项目的smali目录下面的任何目录,建议直接放到smali根目录或者自定义创建的目录中,方便查看和修改。

通过上述方法,将核心的类文件已经集成到了目标项目的smali源代码目录中。如果你实现的类文件有第三方引用的类,需要将相关的类也要一并通过上述方法拷贝到目标项目的smali目录中(例如demo类用到了androidx类,需要将androidx类一并拷贝到目标项目中)。
第三步:通过安卓修改大师的代码布局定位功能,定位要添加和修改的布局控件。

确保手机和电脑连接成功,安卓修改大师底部处于连接状态,点击修改大师左侧的代码布局定位功能,手机上面浏览到需要添加和修改布局的页面,然后点击上述页面上的抓取界面布局按钮,即可获取当前页面的界面布局和布局层次情况。点击左侧预览图的需要添加插件的区域,右下角会显示该控件的id名称(iv_show),点击右侧的定位布局和代码,将自动进行代码和布局查找工作。
通过上述的界面抓取功能,也同时获得该界面的类名和包名。见上述截图的上部。类名为com.yntd.jhpj/com.yntd.jhpj.ui.MainActivity,请牢记,后面有用。
系统自动查找到该图片控件的布局和控件:

双击查询结果,将进入布局xml界面,下图列出来的是该控件的布局xml(下图下面的红框),一般如果要做界面插入,建议不要动原来的界面元素,因此我们把原来的图片框元素添加 n1:visibility="gone" 进行隐藏,在该元素的上部添加了单独的布局(用来作为轮播控件的宿主控件)元素用来放置新添加的轮播功能(下图的上面红框),请注意为了保持界面布局一致性,确保新插入的布局控件和原来的控件的布局和大小尺寸的属性一样。
插入的布局xml:
<LinearLayout n1:id="@id/iv_pic" n1:orientation="vertical" n1:layout_width="fill_parent" n1:layout_height="450.0dp" n1:layout_marginLeft="20.0dip" n1:layout_weight="5.0" n1:scaleType="fitXY" CurrentID="34" />

到此为止已经添加了宿主控件,为将来显示轮播图片打好了基础工作。新增加的这个布局为了方便程序中调用,给定了新的id,目前该id还没有对应的资源id(前面写的注入的类需要宿主的资源id参数),布局中临时定义的id,需要重新编译后才能自动生成资源id。
点击左侧的打包/签名工作,然后打开的页面中点击项目打包按钮,将自动进行项目打包。

确保能顺利打包完成,打包成功后,新添加的界面布局控件id才会生成资源id,切记。我们前面为那个布局新定义的id为“iv_pic”,因此点击安卓修改大师面板左侧的“搜索替换”功能,并搜索“iv_pic”,在结果中有一条public.xml文件的搜索结果,该文件里面就是全部的资源对应的资源id,记录下iv_pic对应的资源id(见下面的红框),后续有用。

第四步:通过插桩方式插入注入的代码。前面已经通过界面抓取获得类名com.yntd.jhpj.ui.MainActivity,在安卓修改大师左侧的代码布局修改功能,点击人代码树状导航,按照上述类路径依次点击找到该类,一般是在oncreate方法里面添加注入方
法。

插入的代码行为:
#集成的代码
const v0, 0x7f0800e7
invoke-static {p0, v0}, Lcom/kongyu/project/MarqueeImageControl;->show(Landroid/app/Activity;I)V
两个参数分别为当前的类的引用p0和上述新创建的宿主控件的资源id,改调用方法为smali语句,如果不熟悉java对应的方法如何用smali调用,可以在前面第一步的demo里面写好调用示例,第二步反编译的时候即可获得对应的smali写法。
至此,已经完整实现了通过插桩的模式插入自定义的逻辑代码,这种方式适合在任何apk中插入任何逻辑和任何布局,只不过是复杂度的区别罢了。
一切修改完毕后,注意在编辑器右上角点击保存,然后回到打包签名进行项目打包,手机点击电脑的话,会自动在手机上面安装打包后的成果apk。

本次教程到此结束,文中提及的资源和代码,以及项目apk在文中已经附带,大家可以跟随学习。