如何通过代码注入的方式在任意apk中添加图片轮播功能

安卓修改大师可以在没有源代码的基础上,通过代码注入插桩的方式,添加任何界面和任何逻辑功能。本教程主要通过在一款名为“多媒体评价器”的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(03000);
        }

        ;
    };
    
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 == nullreturn;

            
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 - Integer.MAX_VALUE imageviews.size();
                    
viewPager.setCurrentItem(item);
                    
handler.sendEmptyMessageDelayed(03000);
                }
            });
        } 
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(03000);

            }
        }
        
@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(03000);
                            
break;
                        
case MotionEvent.ACTION_UP:// 手指抬起时的操作
                            handler.removeCallbacksAndMessages(null);
                            
handler.sendEmptyMessageDelayed(03000);
                            
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 {p0v0}, Lcom/kongyu/project/MarqueeImageControl;->show(Landroid/app/Activity;I)V

 

两个参数分别为当前的类的引用p0和上述新创建的宿主控件的资源id,改调用方法为smali语句,如果不熟悉java对应的方法如何用smali调用,可以在前面第一步的demo里面写好调用示例,第二步反编译的时候即可获得对应的smali写法。

 

至此,已经完整实现了通过插桩的模式插入自定义的逻辑代码,这种方式适合在任何apk中插入任何逻辑和任何布局,只不过是复杂度的区别罢了。

 

一切修改完毕后,注意在编辑器右上角点击保存,然后回到打包签名进行项目打包,手机点击电脑的话,会自动在手机上面安装打包后的成果apk。

 

 

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

 

原创文章 24 获赞 42 访问量 6万+

猜你喜欢

转载自blog.csdn.net/pinksofts/article/details/106138751