• 中文
    • English
  • 注册
  • PHP语言 PHP语言 关注:6 内容:8

    通过代码注入在apk中添加图片轮播功能

  • 查看作者
  • 打赏作者
  • PHP语言
  • Lv.15
    靓号:888

    通过代码注入在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(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在文中已经附带,大家可以跟随学习。

    请登录之后再进行评论

    登录
  • 发布
  • 做任务
  • 更换主题
  • 帖子间隔 侧栏位置: